From 7591b10219222f818d523f457fa6d01e1891260b Mon Sep 17 00:00:00 2001
From: sysadminstory <sysadminstory@users.noreply.github.com>
Date: Tue, 22 Aug 2023 20:44:36 +0200
Subject: [PATCH] [Core] New feature : User Interface to "Detect" Feed from an
 URL (#3436)

* [Core] New feature : User Interface to "Detect" Feed from an URL

Detect Action has been expanded to support returning a Feed in a JSON
format instead of a Redirect. Existing usage of the Detect action will
keep working as usual.

Frontpage template has now a section to display the Feed detection
result, and a button to start the Feed Detection.

A new JS file contains the necessary JS (Ajax and Event management) to
fill the Feed Detection section.

* Coding policy fixes

* [Core] New feature : User Interface to "Detect" Feed from an URL

- Switch from old school XMLHttpRequest to fetch
- Enhance UX of search results
- Revert to it's original content
- Switch to a new Action : FindfeedAction.php
- Switch to template literals instead of string concatenation
- FindFeed action could retrun multiple feeds
- Results are sent with an absolute URL
- Switch to Json::encode() helper function

* [Core] New feature : User Interface to "Detect" Feed from an URL

- Move specific JS code to rss-bridge.js
- Change HTML tag for the button to have a consistant style with th rest
  of the page

* [Core] New feature : User Interface to "Detect" Feed from an URL

- If no context is sent, assume there is only one unnamed context
- Find parameter name in global and currect context

* fix

* remove typo

---------

Co-authored-by: Dag <me@dvikan.no>
---
 actions/FindfeedAction.php   | 89 ++++++++++++++++++++++++++++++++++++
 static/rss-bridge.js         | 79 ++++++++++++++++++++++++++++++++
 static/style.css             | 35 ++++++++++++++
 templates/frontpage.html.php |  9 ++++
 4 files changed, 212 insertions(+)
 create mode 100644 actions/FindfeedAction.php

diff --git a/actions/FindfeedAction.php b/actions/FindfeedAction.php
new file mode 100644
index 00000000..25fe4714
--- /dev/null
+++ b/actions/FindfeedAction.php
@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * This action is used by the frontpage form search.
+ * It finds a bridge based off of a user input url.
+ * It uses bridges' detectParameters implementation.
+ */
+class FindfeedAction implements ActionInterface
+{
+    public function execute(array $request)
+    {
+        $targetURL = $request['url'] ?? null;
+        $format = $request['format'] ?? null;
+
+        if (!$targetURL) {
+            return new Response('You must specify a url', 400);
+        }
+        if (!$format) {
+            return new Response('You must specify a format', 400);
+        }
+
+        $bridgeFactory = new BridgeFactory();
+
+        $results = [];
+        foreach ($bridgeFactory->getBridgeClassNames() as $bridgeClassName) {
+            if (!$bridgeFactory->isEnabled($bridgeClassName)) {
+                continue;
+            }
+
+            $bridge = $bridgeFactory->create($bridgeClassName);
+
+            $bridgeParams = $bridge->detectParameters($targetURL);
+
+            if ($bridgeParams === null) {
+                continue;
+            }
+
+            // It's allowed to have no 'context' in a bridge (only a default context without any name)
+            // In this case, the reference to the parameters are found in the first element of the PARAMETERS array
+
+            $context = $bridgeParams['context'] ?? 0;
+
+            $bridgeData = [];
+            // Construct the array of parameters
+            foreach ($bridgeParams as $key => $value) {
+                // 'context' is a special case : it's a bridge parameters, there is no "name" for this parameter
+                if ($key == 'context') {
+                    $bridgeData[$key]['name'] = 'Context';
+                    $bridgeData[$key]['value'] = $value;
+                } else {
+                    $bridgeData[$key]['name'] = $this->getParameterName($bridge, $context, $key);
+                    $bridgeData[$key]['value'] = $value;
+                }
+            }
+
+            $bridgeParams['bridge'] = $bridgeClassName;
+            $bridgeParams['format'] = $format;
+            $content = [
+                'url' => get_home_page_url() . '?action=display&' . http_build_query($bridgeParams),
+                'bridgeParams' => $bridgeParams,
+                'bridgeData' => $bridgeData,
+                'bridgeMeta' => [
+                        'name' => $bridge::NAME,
+                        'description' => $bridge::DESCRIPTION,
+                        'parameters' => $bridge::PARAMETERS,
+                        'icon' => $bridge->getIcon(),
+                    ],
+            ];
+            $results[] = $content;
+        }
+        if ($results === []) {
+            return new Response(Json::encode(['message' => 'No bridge found for given url']), 404, ['content-type' => 'application/json']);
+        }
+        return new Response(Json::encode($results), 200, ['content-type' => 'application/json']);
+    }
+
+    // Get parameter name in the actual context, or in the global parameter
+    private function getParameterName($bridge, $context, $key)
+    {
+        if (isset($bridge::PARAMETERS[$context][$key]['name'])) {
+            $name = $bridge::PARAMETERS[$context][$key]['name'];
+        } else if (isset($bridge::PARAMETERS['global'][$key]['name'])) {
+            $name = $bridge::PARAMETERS['global'][$key]['name'];
+        } else {
+            $name = 'Variable "' . $key . '" (No name provided)';
+        }
+        return $name;
+    }
+}
diff --git a/static/rss-bridge.js b/static/rss-bridge.js
index 498acd37..82069d8c 100644
--- a/static/rss-bridge.js
+++ b/static/rss-bridge.js
@@ -47,3 +47,82 @@ function rssbridge_toggle_bridge(){
         bridge.getElementsByClassName('showmore-box')[0].checked = true;
     }
 }
+
+var rssbridge_feed_finder = (function() {
+    /*
+     * Code for "Find feed by URL" feature
+     */
+
+    // Start the Feed search
+    async function rssbridge_feed_search(event) {
+        const input = document.getElementById('searchfield');
+        let content = input.value;
+        if (content) {
+            const findfeedresults = document.getElementById('findfeedresults');
+            findfeedresults.innerHTML = 'Searching for matching feeds ...';
+            let baseurl = window.location.protocol + window.location.pathname;
+            let url = baseurl + '?action=findfeed&format=Html&url=' + content;
+            const response = await fetch(url);
+            if (response.ok) {
+                const data = await response.json();
+                rss_bridge_feed_display_found_feed(data);
+            } else {
+                rss_bridge_feed_display_feed_search_fail();
+            }
+        } else {
+            rss_bridge_feed_display_find_feed_empty();
+        }
+    }
+
+    // Display the found feeds
+    function rss_bridge_feed_display_found_feed(obj) {
+        const findfeedresults = document.getElementById('findfeedresults');
+
+        let content = 'Found Feed(s) :';
+
+        // Let's go throug every Feed found
+        for (const element of obj) {
+            content += `<div class="search-result">
+                        <div class="icon">
+                            <img src="${element.bridgeMeta.icon}" width="60" />
+                        </div>
+                        <div class="content">
+                        <h2><a href="${element.url}">${element.bridgeMeta.name}</a></h2>
+                        <p>
+                        <span class="description"><a href="${element.url}">${element.bridgeMeta.description}</a></span>
+                        </p>
+                        <div>
+                            <ul>`;
+
+            // Now display every Feed parameter
+            for (const param in element.bridgeData) {
+                content += `<li>${element.bridgeData[param].name} : ${element.bridgeData[param].value}</li>`;
+            }
+            content += `</div>
+              </div>
+            </div>`;
+        }
+        content += '<p><div class="alert alert-info" role="alert">This feed may be only one of the possible feeds. You may find more feeds using one of the bridges with different parameters, for example.</div></p>';
+        findfeedresults.innerHTML = content;
+    }
+
+    // Display an error if no feed were found
+    function rss_bridge_feed_display_feed_search_fail() {
+        const findfeedresults = document.getElementById('findfeedresults');
+        findfeedresults.innerHTML = 'No Feed found !<div class="alert alert-info" role="alert">Not every bridge supports feed detection. You can check below within the bridge parameters to create a feed.</div>';
+    }
+
+    // Empty the Found Feed section
+    function rss_bridge_feed_display_find_feed_empty() {
+        const findfeedresults = document.getElementById('findfeedresults');
+        findfeedresults.innerHTML = '';
+    }
+
+    // Add Event to 'Detect Feed" button
+    var rssbridge_feed_finder = function() {
+        const button = document.getElementById('findfeed');
+        button.addEventListener("click", rssbridge_feed_search);
+        button.addEventListener("keyup", rssbridge_feed_search);
+    };
+    return rssbridge_feed_finder;
+}());
diff --git a/static/style.css b/static/style.css
index a83e25e4..a9d5933a 100644
--- a/static/style.css
+++ b/static/style.css
@@ -453,3 +453,38 @@ button {
         color: #d8d3cb;
     }
 }
+
+/* find-feed */
+.search-result {
+  background-color: #f0f0f0;
+  border-radius: 5px;
+  padding: 15px;
+  display: flex;
+  position: relative;
+  text-align: left;
+}
+@media (prefers-color-scheme: dark) {
+    .search-result {
+        background-color: #202325;
+    }
+}
+.search-result h2 {
+  color: #288cfc;
+}
+
+.search-result a {
+  text-decoration: none;
+  color: #248afa;
+}
+.search-result .icon {
+  margin: 0 15px 0 0;
+}
+.search-result span {
+  margin-right: 10px;
+}
+.search-result .description {
+  font-size: 110%;
+  margin-right: 0 !important;
+  margin-top: 5px !important;
+}
+/* end find-feed */
diff --git a/templates/frontpage.html.php b/templates/frontpage.html.php
index 63f4a2ab..99e2ffd9 100644
--- a/templates/frontpage.html.php
+++ b/templates/frontpage.html.php
@@ -2,6 +2,7 @@
 <script>
     document.addEventListener('DOMContentLoaded', rssbridge_toggle_bridge);
     document.addEventListener('DOMContentLoaded', rssbridge_list_search);
+    document.addEventListener('DOMContentLoaded', rssbridge_feed_finder);
 </script>
 
 <section class="searchbar">
@@ -15,6 +16,14 @@
         onkeyup="rssbridge_list_search()"
         value=""
     >
+    <button
+        type="button"
+	    id="findfeed"
+        name="findfeed"
+    />Find Feed from URL</button>
+    <section id="findfeedresults">
+    </section>
+
 </section>
 
 <?= raw($bridges) ?>