aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorAlexandre Alapetite <alexandre@alapetite.fr>2024-02-26 09:01:03 +0100
committerGitHub <noreply@github.com>2024-02-26 09:01:03 +0100
commit39cc1c11ec596176e842cc98e6a54337e3c04d7e (patch)
treedab89beb80268acb5e4bd58dfc55297bd30a8486
parent25166c218be4e1ce1cb098de274a231b623d527e (diff)
downloadfreshrss-39cc1c11ec596176e842cc98e6a54337e3c04d7e.tar.gz
freshrss-39cc1c11ec596176e842cc98e6a54337e3c04d7e.zip
New feature: shareable user query (#6052)
* New feature: shareable user query Share the output of a user query by RSS / HTML / OPML with other people through unique URLs. Replaces the global admin token, which was the only option (but unsafe) to share RSS outputs with other people. Also add a new HTML output for people without an RSS reader. fix https://github.com/FreshRSS/FreshRSS/issues/3066#issuecomment-648977890 fix https://github.com/FreshRSS/FreshRSS/issues/3178#issuecomment-769435504 * Remove unused method * Fix token saving * Implement HTML view * Update i18n for master token * Revert i18n get_favorite * Fix missing i18n for user queries from before this PR * Remove irrelevant tests * Add link to RSS version * Fix getGet * Fix getState * Fix getSearch * Alternative getSearch * Default getOrder * Explicit default state * Fix test * Add OPML sharing * Remove many redundant SQL queries from original implementation of user queries * Fix article tags * Use default user settings * Prepare public search * Fixes * Allow user search on article tags * Implement user search * Revert filter bug * Revert wrong SQL left outer join change * Implement checkboxes * Safe check of OPML * Fix label * Remove RSS button to favour new sharing method That sharing button was using a global admin token * First version of HTTP 304 * Disallow some recusrivity fix https://github.com/FreshRSS/FreshRSS/issues/6086 * Draft of nav * Minor httpConditional * Add support for offset for pagination * Fix offset pagination * Fix explicit order ASC * Add documentation * Help links i18n * Note about deprecated master token * Typo * Doc about format
-rw-r--r--README.fr.md6
-rw-r--r--README.md6
-rw-r--r--app/Controllers/configureController.php56
-rw-r--r--app/Controllers/feedController.php2
-rw-r--r--app/Controllers/importExportController.php2
-rw-r--r--app/Controllers/indexController.php59
-rw-r--r--app/Controllers/statsController.php4
-rw-r--r--app/Controllers/subscriptionController.php2
-rw-r--r--app/Controllers/tagController.php2
-rw-r--r--app/FreshRSS.php2
-rw-r--r--app/Models/BooleanSearch.php34
-rw-r--r--app/Models/Category.php54
-rw-r--r--app/Models/CategoryDAO.php72
-rw-r--r--app/Models/Context.php54
-rw-r--r--app/Models/Entry.php22
-rw-r--r--app/Models/EntryDAO.php20
-rw-r--r--app/Models/Feed.php5
-rw-r--r--app/Models/FeedDAO.php20
-rw-r--r--app/Models/TagDAO.php14
-rw-r--r--app/Models/UserConfiguration.php16
-rw-r--r--app/Models/UserQuery.php210
-rw-r--r--app/Models/View.php10
-rw-r--r--app/Models/ViewJavascript.php6
-rw-r--r--app/Models/ViewStats.php6
-rw-r--r--app/Services/ExportService.php2
-rw-r--r--app/Utils/dotpathUtil.php3
-rw-r--r--app/i18n/cz/admin.php4
-rw-r--r--app/i18n/cz/conf.php13
-rw-r--r--app/i18n/de/admin.php4
-rw-r--r--app/i18n/de/conf.php13
-rw-r--r--app/i18n/el/admin.php4
-rw-r--r--app/i18n/el/conf.php13
-rw-r--r--app/i18n/en-us/admin.php4
-rw-r--r--app/i18n/en-us/conf.php13
-rw-r--r--app/i18n/en/admin.php4
-rw-r--r--app/i18n/en/conf.php13
-rw-r--r--app/i18n/es/admin.php4
-rw-r--r--app/i18n/es/conf.php13
-rw-r--r--app/i18n/fa/admin.php4
-rw-r--r--app/i18n/fa/conf.php13
-rw-r--r--app/i18n/fr/admin.php4
-rw-r--r--app/i18n/fr/conf.php13
-rw-r--r--app/i18n/he/admin.php4
-rw-r--r--app/i18n/he/conf.php13
-rw-r--r--app/i18n/hu/admin.php4
-rw-r--r--app/i18n/hu/conf.php13
-rw-r--r--app/i18n/id/admin.php4
-rw-r--r--app/i18n/id/conf.php15
-rw-r--r--app/i18n/it/admin.php4
-rw-r--r--app/i18n/it/conf.php13
-rw-r--r--app/i18n/ja/admin.php4
-rw-r--r--app/i18n/ja/conf.php13
-rw-r--r--app/i18n/ko/admin.php4
-rw-r--r--app/i18n/ko/conf.php13
-rw-r--r--app/i18n/lv/admin.php4
-rw-r--r--app/i18n/lv/conf.php13
-rw-r--r--app/i18n/nl/admin.php4
-rw-r--r--app/i18n/nl/conf.php13
-rw-r--r--app/i18n/oc/admin.php4
-rw-r--r--app/i18n/oc/conf.php13
-rw-r--r--app/i18n/pl/admin.php4
-rw-r--r--app/i18n/pl/conf.php13
-rw-r--r--app/i18n/pt-br/admin.php4
-rw-r--r--app/i18n/pt-br/conf.php13
-rw-r--r--app/i18n/ru/admin.php4
-rw-r--r--app/i18n/ru/conf.php13
-rw-r--r--app/i18n/sk/admin.php4
-rw-r--r--app/i18n/sk/conf.php13
-rw-r--r--app/i18n/tr/admin.php4
-rw-r--r--app/i18n/tr/conf.php13
-rw-r--r--app/i18n/zh-cn/admin.php4
-rw-r--r--app/i18n/zh-cn/conf.php13
-rw-r--r--app/i18n/zh-tw/admin.php4
-rw-r--r--app/i18n/zh-tw/conf.php13
-rw-r--r--app/layout/header.phtml48
-rw-r--r--app/layout/layout.phtml16
-rw-r--r--app/layout/nav_menu.phtml43
-rw-r--r--app/layout/simple.phtml42
-rw-r--r--app/views/configure/queries.phtml3
-rw-r--r--app/views/helpers/configure/query.phtml67
-rw-r--r--app/views/helpers/export/articles.phtml2
-rw-r--r--app/views/helpers/feed/update.phtml3
-rw-r--r--app/views/helpers/htmlPagination.phtml21
-rw-r--r--app/views/helpers/index/article.phtml117
-rw-r--r--app/views/helpers/index/normal/entry_header.phtml3
-rw-r--r--app/views/helpers/index/tags.phtml42
-rw-r--r--app/views/index/html.phtml32
-rw-r--r--app/views/index/normal.phtml102
-rw-r--r--app/views/index/reader.phtml194
-rw-r--r--app/views/index/rss.phtml6
-rw-r--r--app/views/user/profile.phtml1
-rw-r--r--config-user.default.php6
-rw-r--r--docs/en/img/users/user-query-share.pngbin0 -> 4976 bytes
-rw-r--r--docs/en/users/03_Main_view.md1
-rw-r--r--docs/en/users/05_Configuration.md5
-rw-r--r--docs/en/users/10_filter.md28
-rw-r--r--docs/en/users/user_queries.md63
-rw-r--r--lib/Minz/Request.php5
-rw-r--r--p/api/greader.php6
-rw-r--r--p/api/query.php175
-rw-r--r--p/scripts/main.js15
-rw-r--r--p/themes/base-theme/frss.css4
-rw-r--r--p/themes/base-theme/frss.rtl.css4
-rw-r--r--tests/app/Models/CategoryTest.php21
-rw-r--r--tests/app/Models/UserQueryTest.php112
105 files changed, 1477 insertions, 809 deletions
diff --git a/README.fr.md b/README.fr.md
index 2687978f1..1903dab06 100644
--- a/README.fr.md
+++ b/README.fr.md
@@ -15,7 +15,11 @@ Il y a une API pour les clients (mobiles), ainsi qu’une [interface en ligne de
Grâce au standard [WebSub](https://freshrss.github.io/FreshRSS/fr/users/08_PubSubHubbub.html),
FreshRSS est capable de recevoir des notifications push instantanées depuis les sources compatibles, [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, Medium, etc.
-FreshRSS supporte nativement le moissonnage du Web (Web Scraping) basique, basé sur [XPath](https://www.w3.org/TR/xpath-10/), pour les sites Web sans flux RSS / Atom.
+FreshRSS supporte nativement le [moissonnage du Web (Web Scraping)](https://freshrss.github.io/FreshRSS/en/users/11_website_scraping.html) basique,
+basé sur [XPath](https://www.w3.org/TR/xpath-10/), pour les sites Web sans flux RSS / Atom.
+Supporte aussi les documents JSON.
+
+FreshRSS permet de [repartager des sélections d’articles par HTML, RSS, et OPML](https://freshrss.github.io/FreshRSS/en/users/user_queries.html).
Plusieurs [méthodes de connexion](https://freshrss.github.io/FreshRSS/en/admins/09_AccessControl.html) sont supportées : formulaire Web (avec un mode anonyme), Authentification HTTP (compatible avec proxy), OpenID Connect.
diff --git a/README.md b/README.md
index 4f2629975..953e858b0 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,11 @@ There is an API for (mobile) clients, and a [Command-Line Interface](cli/README.
Thanks to the [WebSub](https://freshrss.github.io/FreshRSS/en/users/WebSub.html) standard,
FreshRSS is able to receive instant push notifications from compatible sources, such as [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, Medium, etc.
-FreshRSS natively supports basic Web scraping, based on [XPath](https://www.w3.org/TR/xpath-10/), for Web sites not providing any RSS / Atom feed.
+FreshRSS natively supports basic [Web scraping](https://freshrss.github.io/FreshRSS/en/users/11_website_scraping.html),
+based on [XPath](https://www.w3.org/TR/xpath-10/), for Web sites not providing any RSS / Atom feed.
+Also supports JSON documents.
+
+FreshRSS offers the ability to [reshare selections of articles by HTML, RSS, and OPML](https://freshrss.github.io/FreshRSS/en/users/user_queries.html).
Different [login methods](https://freshrss.github.io/FreshRSS/en/admins/09_AccessControl.html) are supported: Web form (including an anonymous option), HTTP Authentication (compatible with proxy delegation), OpenID Connect.
diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php
index 8db36a899..e7f877428 100644
--- a/app/Controllers/configureController.php
+++ b/app/Controllers/configureController.php
@@ -301,12 +301,8 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
public function queriesAction(): void {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/draggable.js?' . @filemtime(PUBLIC_PATH . '/scripts/draggable.js')));
- $category_dao = FreshRSS_Factory::createCategoryDao();
- $feed_dao = FreshRSS_Factory::createFeedDao();
- $tag_dao = FreshRSS_Factory::createTagDao();
-
if (Minz_Request::isPost()) {
- /** @var array<int,array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $params */
+ /** @var array<int,array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $params */
$params = Minz_Request::paramArray('queries');
$queries = [];
@@ -318,7 +314,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
if (!empty($query['search'])) {
$query['search'] = urldecode($query['search']);
}
- $queries[$key] = (new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao))->toArray();
+ $queries[$key] = (new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
}
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
@@ -327,13 +323,13 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
} else {
$this->view->queries = [];
foreach (FreshRSS_Context::userConf()->queries as $key => $query) {
- $this->view->queries[intval($key)] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
+ $this->view->queries[intval($key)] = new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
}
}
- $this->view->categories = $category_dao->listCategories(false) ?: [];
- $this->view->feeds = $feed_dao->listFeeds();
- $this->view->tags = $tag_dao->listTags() ?: [];
+ $this->view->categories = FreshRSS_Context::categories();
+ $this->view->feeds = FreshRSS_Context::feeds();
+ $this->view->tags = FreshRSS_Context::labels();
if (Minz_Request::paramTernary('id') !== null) {
$id = Minz_Request::paramInt('id');
@@ -363,20 +359,21 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
return;
}
- $category_dao = FreshRSS_Factory::createCategoryDao();
- $feed_dao = FreshRSS_Factory::createFeedDao();
- $tag_dao = FreshRSS_Factory::createTagDao();
-
- $query = new FreshRSS_UserQuery(FreshRSS_Context::userConf()->queries[$id], $feed_dao, $category_dao, $tag_dao);
+ $query = new FreshRSS_UserQuery(FreshRSS_Context::userConf()->queries[$id], FreshRSS_Context::categories(), FreshRSS_Context::labels());
$this->view->query = $query;
$this->view->queryId = $id;
- $this->view->categories = $category_dao->listCategories(false) ?: [];
- $this->view->feeds = $feed_dao->listFeeds();
- $this->view->tags = $tag_dao->listTags() ?: [];
+ $this->view->categories = FreshRSS_Context::categories();
+ $this->view->feeds = FreshRSS_Context::feeds();
+ $this->view->tags = FreshRSS_Context::labels();
if (Minz_Request::isPost()) {
$params = array_filter(Minz_Request::paramArray('query'));
$queryParams = [];
+ $name = Minz_Request::paramString('name') ?: _t('conf.query.number', $id + 1);
+ if ('' === $name) {
+ $name = _t('conf.query.number', $id + 1);
+ }
+ $queryParams['name'] = $name;
if (!empty($params['get']) && is_string($params['get'])) {
$queryParams['get'] = htmlspecialchars_decode($params['get'], ENT_QUOTES);
}
@@ -389,15 +386,21 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
if (!empty($params['state']) && is_array($params['state'])) {
$queryParams['state'] = (int)(array_sum($params['state']));
}
- $name = Minz_Request::paramString('name') ?: _t('conf.query.number', $id + 1);
- if ('' === $name) {
- $name = _t('conf.query.number', $id + 1);
+ if (empty($params['token']) || !is_string($params['token'])) {
+ $queryParams['token'] = FreshRSS_UserQuery::generateToken($name);
+ } else {
+ $queryParams['token'] = $params['token'];
+ }
+ if (!empty($params['shareRss']) && ctype_digit($params['shareRss'])) {
+ $queryParams['shareRss'] = (bool)$params['shareRss'];
+ }
+ if (!empty($params['shareOpml']) && ctype_digit($params['shareOpml'])) {
+ $queryParams['shareOpml'] = (bool)$params['shareOpml'];
}
- $queryParams['name'] = $name;
$queryParams['url'] = Minz_Url::display(['params' => $queryParams]);
$queries = FreshRSS_Context::userConf()->queries;
- $queries[$id] = (new FreshRSS_UserQuery($queryParams, $feed_dao, $category_dao, $tag_dao))->toArray();
+ $queries[$id] = (new FreshRSS_UserQuery($queryParams, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
@@ -433,18 +436,15 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
* lean data.
*/
public function bookmarkQueryAction(): void {
- $category_dao = FreshRSS_Factory::createCategoryDao();
- $feed_dao = FreshRSS_Factory::createFeedDao();
- $tag_dao = FreshRSS_Factory::createTagDao();
$queries = [];
foreach (FreshRSS_Context::userConf()->queries as $key => $query) {
- $queries[$key] = (new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao))->toArray();
+ $queries[$key] = (new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
}
$params = $_GET;
unset($params['rid']);
$params['url'] = Minz_Url::display(['params' => $params]);
$params['name'] = _t('conf.query.number', count($queries) + 1);
- $queries[] = (new FreshRSS_UserQuery($params, $feed_dao, $category_dao, $tag_dao))->toArray();
+ $queries[] = (new FreshRSS_UserQuery($params, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php
index d393a142e..63c358da7 100644
--- a/app/Controllers/feedController.php
+++ b/app/Controllers/feedController.php
@@ -776,7 +776,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
*/
private static function applyLabelActions(int $nbNewEntries) {
$tagDAO = FreshRSS_Factory::createTagDao();
- $labels = $tagDAO->listTags() ?: [];
+ $labels = FreshRSS_Context::labels();
$labels = array_filter($labels, static function (FreshRSS_Tag $label) {
return !empty($label->filtersAction('label'));
});
diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php
index 0de75d0ff..2a437e34c 100644
--- a/app/Controllers/importExportController.php
+++ b/app/Controllers/importExportController.php
@@ -364,7 +364,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
}
$tagDAO = FreshRSS_Factory::createTagDao();
- $labels = $tagDAO->listTags() ?: [];
+ $labels = FreshRSS_Context::labels();
$knownLabels = [];
foreach ($labels as $label) {
$knownLabels[$label->name()]['id'] = $label->id();
diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php
index 20223d340..79e1a6210 100644
--- a/app/Controllers/indexController.php
+++ b/app/Controllers/indexController.php
@@ -6,6 +6,10 @@ declare(strict_types=1);
*/
class FreshRSS_index_Controller extends FreshRSS_ActionController {
+ public function firstAction(): void {
+ $this->view->html_url = Minz_Url::display(['c' => 'index', 'a' => 'index'], 'html', 'root');
+ }
+
/**
* This action only redirect on the default view mode (normal or global)
*/
@@ -36,7 +40,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
}
try {
- FreshRSS_Context::updateUsingRequest();
+ FreshRSS_Context::updateUsingRequest(true);
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
@@ -48,7 +52,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
'media-src' => '*',
]);
- $this->view->categories = FreshRSS_Context::$categories;
+ $this->view->categories = FreshRSS_Context::categories();
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
$title = FreshRSS_Context::$name;
@@ -60,15 +64,10 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
FreshRSS_Context::$id_max = time() . '000000';
$this->view->callbackBeforeFeeds = static function (FreshRSS_View $view) {
- try {
- $tagDAO = FreshRSS_Factory::createTagDao();
- $view->tags = $tagDAO->listTags(true) ?: [];
- $view->nbUnreadTags = 0;
- foreach ($view->tags as $tag) {
- $view->nbUnreadTags += $tag->nbUnread();
- }
- } catch (Exception $e) {
- Minz_Log::notice($e->getMessage());
+ $view->tags = FreshRSS_Context::labels(true);
+ $view->nbUnreadTags = 0;
+ foreach ($view->tags as $tag) {
+ $view->nbUnreadTags += $tag->nbUnread();
}
};
@@ -117,12 +116,12 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
try {
- FreshRSS_Context::updateUsingRequest();
+ FreshRSS_Context::updateUsingRequest(true);
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
- $this->view->categories = FreshRSS_Context::$categories;
+ $this->view->categories = FreshRSS_Context::categories();
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
$title = _t('index.feed.title_global');
@@ -141,6 +140,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
/**
* This action displays the RSS feed of FreshRSS.
+ * @deprecated See user query RSS sharing instead
*/
public function rssAction(): void {
$allow_anonymous = FreshRSS_Context::systemConf()->allow_anonymous;
@@ -156,7 +156,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
}
try {
- FreshRSS_Context::updateUsingRequest();
+ FreshRSS_Context::updateUsingRequest(false);
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
@@ -168,13 +168,19 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
Minz_Error::error(404);
}
- // No layout for RSS output.
- $this->view->rss_url = PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
+ $this->view->html_url = Minz_Url::display('', 'html', true);
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
+ $this->view->rss_url = htmlspecialchars(
+ PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']), ENT_COMPAT, 'UTF-8');
+
+ // No layout for RSS output.
$this->view->_layout(null);
header('Content-Type: application/rss+xml; charset=utf-8');
}
+ /**
+ * @deprecated See user query OPML sharing instead
+ */
public function opmlAction(): void {
$allow_anonymous = FreshRSS_Context::systemConf()->allow_anonymous;
$token = FreshRSS_Context::userConf()->token;
@@ -187,7 +193,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
}
try {
- FreshRSS_Context::updateUsingRequest();
+ FreshRSS_Context::updateUsingRequest(false);
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
@@ -196,25 +202,23 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
$type = (string)$get[0];
$id = (int)$get[1];
- $catDAO = FreshRSS_Factory::createCategoryDao();
- $categories = $catDAO->listCategories(true, true);
$this->view->excludeMutedFeeds = true;
switch ($type) {
case 'a':
- $this->view->categories = $categories;
+ $this->view->categories = FreshRSS_Context::categories();
break;
case 'c':
- $cat = $categories[$id] ?? null;
+ $cat = FreshRSS_Context::categories()[$id] ?? null;
if ($cat == null) {
Minz_Error::error(404);
return;
}
- $this->view->categories = [ $cat ];
+ $this->view->categories = [ $cat->id() => $cat ];
break;
case 'f':
// We most likely already have the feed object in cache
- $feed = FreshRSS_CategoryDAO::findFeed($categories, $id);
+ $feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
@@ -223,7 +227,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
return;
}
}
- $this->view->feeds = [ $feed ];
+ $this->view->feeds = [ $feed->id() => $feed ];
break;
case 's':
case 't':
@@ -255,17 +259,14 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
$id = 0;
}
- $limit = FreshRSS_Context::$number;
-
$date_min = 0;
- if (FreshRSS_Context::$sinceHours) {
+ if (FreshRSS_Context::$sinceHours > 0) {
$date_min = time() - (FreshRSS_Context::$sinceHours * 3600);
- $limit = FreshRSS_Context::userConf()->max_posts_per_rss;
}
foreach ($entryDAO->listWhere(
$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
- $limit, FreshRSS_Context::$first_id,
+ FreshRSS_Context::$number, FreshRSS_Context::$offset, FreshRSS_Context::$first_id,
FreshRSS_Context::$search, $date_min)
as $entry) {
yield $entry;
diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php
index f40f0dd29..8ff2744ae 100644
--- a/app/Controllers/statsController.php
+++ b/app/Controllers/statsController.php
@@ -193,7 +193,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
if ($id !== 0) {
$this->view->displaySlider = true;
$feedDAO = FreshRSS_Factory::createFeedDao();
- $this->view->feed = $feedDAO->searchById($id);
+ $this->view->feed = $feedDAO->searchById($id) ?? FreshRSS_Feed::default();
}
}
@@ -222,7 +222,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
}
$this->view->categories = $categoryDAO->listCategories(true) ?: [];
- $this->view->feed = $id === null ? null : $feedDAO->searchById($id);
+ $this->view->feed = $id === null ? FreshRSS_Feed::default() : ($feedDAO->searchById($id) ?? FreshRSS_Feed::default());
$this->view->days = $statsDAO->getDays();
$this->view->months = $statsDAO->getMonths();
diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php
index cf23c018e..554243725 100644
--- a/app/Controllers/subscriptionController.php
+++ b/app/Controllers/subscriptionController.php
@@ -59,7 +59,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
break;
default:
$feedDAO = FreshRSS_Factory::createFeedDao();
- $this->view->feed = $feedDAO->searchById($id);
+ $this->view->feed = $feedDAO->searchById($id) ?? FreshRSS_Feed::default();
break;
}
}
diff --git a/app/Controllers/tagController.php b/app/Controllers/tagController.php
index 6233207ed..091da6a6f 100644
--- a/app/Controllers/tagController.php
+++ b/app/Controllers/tagController.php
@@ -199,6 +199,6 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
Minz_Error::error(403);
}
$tagDAO = FreshRSS_Factory::createTagDao();
- $this->view->tags = $tagDAO->listTags() ?: [];
+ $this->view->tags = $tagDAO->listTags(true) ?: [];
}
}
diff --git a/app/FreshRSS.php b/app/FreshRSS.php
index c31655aa0..1e172c165 100644
--- a/app/FreshRSS.php
+++ b/app/FreshRSS.php
@@ -143,7 +143,7 @@ class FreshRSS extends Minz_FrontController {
}
}
//Use prepend to insert before extensions. Added in reverse order.
- if (Minz_Request::controllerName() !== 'index') {
+ if (!in_array(Minz_Request::controllerName(), ['index', ''], true)) {
FreshRSS_View::prependScript(Minz_Url::display('/scripts/extra.js?' . @filemtime(PUBLIC_PATH . '/scripts/extra.js')));
}
FreshRSS_View::prependScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php
index 78b7593b2..dd8b95efb 100644
--- a/app/Models/BooleanSearch.php
+++ b/app/Models/BooleanSearch.php
@@ -16,14 +16,12 @@ class FreshRSS_BooleanSearch {
private string $operator;
/** @param 'AND'|'OR'|'AND NOT' $operator */
- public function __construct(string $input, int $level = 0, string $operator = 'AND') {
+ public function __construct(string $input, int $level = 0, string $operator = 'AND', bool $allowUserQueries = true) {
$this->operator = $operator;
$input = trim($input);
if ($input === '') {
return;
}
- $this->raw_input = $input;
-
if ($level === 0) {
$input = preg_replace('/:&quot;(.*?)&quot;/', ':"\1"', $input);
if (!is_string($input)) {
@@ -34,9 +32,11 @@ class FreshRSS_BooleanSearch {
return;
}
- $input = $this->parseUserQueryNames($input);
- $input = $this->parseUserQueryIds($input);
+ $input = $this->parseUserQueryNames($input, $allowUserQueries);
+ $input = $this->parseUserQueryIds($input, $allowUserQueries);
+ $input = trim($input);
}
+ $this->raw_input = $input;
// Either parse everything as a series of BooleanSearch’s combined by implicit AND
// or parse everything as a series of Search’s combined by explicit OR
@@ -46,7 +46,7 @@ class FreshRSS_BooleanSearch {
/**
* Parse the user queries (saved searches) by name and expand them in the input string.
*/
- private function parseUserQueryNames(string $input): string {
+ private function parseUserQueryNames(string $input, bool $allowUserQueries = true): string {
$all_matches = [];
if (preg_match_all('/\bsearch:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matchesFound)) {
$all_matches[] = $matchesFound;
@@ -60,7 +60,7 @@ class FreshRSS_BooleanSearch {
/** @var array<string,FreshRSS_UserQuery> */
$queries = [];
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
- $query = new FreshRSS_UserQuery($raw_query);
+ $query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
$queries[$query->getName()] = $query;
}
@@ -74,7 +74,11 @@ class FreshRSS_BooleanSearch {
$name = trim($matches['search'][$i]);
if (!empty($queries[$name])) {
$fromS[] = $matches[0][$i];
- $toS[] = '(' . trim($queries[$name]->getSearch()->getRawInput()) . ')';
+ if ($allowUserQueries) {
+ $toS[] = '(' . trim($queries[$name]->getSearch()->getRawInput()) . ')';
+ } else {
+ $toS[] = '';
+ }
}
}
}
@@ -87,7 +91,7 @@ class FreshRSS_BooleanSearch {
/**
* Parse the user queries (saved searches) by ID and expand them in the input string.
*/
- private function parseUserQueryIds(string $input): string {
+ private function parseUserQueryIds(string $input, bool $allowUserQueries = true): string {
$all_matches = [];
if (preg_match_all('/\bS:(?P<search>\d+)/', $input, $matchesFound)) {
@@ -95,14 +99,10 @@ class FreshRSS_BooleanSearch {
}
if (!empty($all_matches)) {
- $category_dao = FreshRSS_Factory::createCategoryDao();
- $feed_dao = FreshRSS_Factory::createFeedDao();
- $tag_dao = FreshRSS_Factory::createTagDao();
-
/** @var array<string,FreshRSS_UserQuery> */
$queries = [];
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
- $query = new FreshRSS_UserQuery($raw_query, $feed_dao, $category_dao, $tag_dao);
+ $query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
$queries[] = $query;
}
@@ -117,7 +117,11 @@ class FreshRSS_BooleanSearch {
$id = (int)(trim($matches['search'][$i])) - 1;
if (!empty($queries[$id])) {
$fromS[] = $matches[0][$i];
- $toS[] = '(' . trim($queries[$id]->getSearch()->getRawInput()) . ')';
+ if ($allowUserQueries) {
+ $toS[] = '(' . trim($queries[$id]->getSearch()->getRawInput()) . ')';
+ } else {
+ $toS[] = '';
+ }
}
}
}
diff --git a/app/Models/Category.php b/app/Models/Category.php
index 1f5b4dc61..6674b4e72 100644
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -95,7 +95,7 @@ class FreshRSS_Category extends Minz_Model {
}
/**
- * @return array<FreshRSS_Feed>
+ * @return array<int,FreshRSS_Feed>
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
@@ -110,10 +110,8 @@ class FreshRSS_Category extends Minz_Model {
$this->nbNotRead += $feed->nbNotRead();
$this->hasFeedsWithError |= ($feed->inError() && !$feed->mute());
}
-
$this->sortFeeds();
}
-
return $this->feeds ?? [];
}
@@ -143,7 +141,6 @@ class FreshRSS_Category extends Minz_Model {
if (!is_array($values)) {
$values = [$values];
}
-
$this->feeds = $values;
$this->sortFeeds();
}
@@ -157,7 +154,6 @@ class FreshRSS_Category extends Minz_Model {
}
$feed->_category($this);
$this->feeds[] = $feed;
-
$this->sortFeeds();
}
@@ -243,8 +239,54 @@ class FreshRSS_Category extends Minz_Model {
if ($this->feeds === null) {
return;
}
- usort($this->feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
+ uasort($this->feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
return strnatcasecmp($a->name(), $b->name());
});
}
+
+ /**
+ * Access cached feed
+ * @param array<FreshRSS_Category> $categories
+ */
+ public static function findFeed(array $categories, int $feed_id): ?FreshRSS_Feed {
+ foreach ($categories as $category) {
+ foreach ($category->feeds() as $feed) {
+ if ($feed->id() === $feed_id) {
+ $feed->_category($category); // Should already be done; just to be safe
+ return $feed;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Access cached feeds
+ * @param array<FreshRSS_Category> $categories
+ * @return array<int,FreshRSS_Feed>
+ */
+ public static function findFeeds(array $categories): array {
+ $result = [];
+ foreach ($categories as $category) {
+ foreach ($category->feeds() as $feed) {
+ $result[$feed->id()] = $feed;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * @param array<FreshRSS_Category> $categories
+ */
+ public static function countUnread(array $categories, int $minPriority = 0): int {
+ $n = 0;
+ foreach ($categories as $category) {
+ foreach ($category->feeds() as $feed) {
+ if ($feed->priority() >= $minPriority) {
+ $n += $feed->nbNotRead();
+ }
+ }
+ }
+ return $n;
+ }
}
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php
index 8ea8090b8..90c3db30d 100644
--- a/app/Models/CategoryDAO.php
+++ b/app/Models/CategoryDAO.php
@@ -245,19 +245,19 @@ SQL;
$sql = 'SELECT * FROM `_category` WHERE id=:id';
$res = $this->fetchAssoc($sql, ['id' => $id]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
- $cat = self::daoToCategory($res);
- return $cat[0] ?? null;
+ $categories = self::daoToCategories($res);
+ return reset($categories) ?: null;
}
public function searchByName(string $name): ?FreshRSS_Category {
$sql = 'SELECT * FROM `_category` WHERE name=:name';
$res = $this->fetchAssoc($sql, ['name' => $name]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
- $cat = self::daoToCategory($res);
- return $cat[0] ?? null;
+ $categories = self::daoToCategories($res);
+ return reset($categories) ?: null;
}
- /** @return array<FreshRSS_Category> */
+ /** @return array<int,FreshRSS_Category> */
public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false): array {
$categories = $this->listCategories($prePopulateFeeds, $details);
@@ -277,7 +277,7 @@ SQL;
return $categories;
}
- /** @return array<FreshRSS_Category> */
+ /** @return array<int,FreshRSS_Category> */
public function listCategories(bool $prePopulateFeeds = true, bool $details = false): array {
if ($prePopulateFeeds) {
$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.kind AS c_kind, c.`lastUpdate` AS c_last_update, c.error AS c_error, c.attributes AS c_attributes, '
@@ -293,7 +293,7 @@ SQL;
$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
/** @var array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'category'?:int,'website'?:string,'priority'?:int,'error'?:int|bool,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $res */
- return self::daoToCategoryPrepopulated($res);
+ return self::daoToCategoriesPrepopulated($res);
} else {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
@@ -305,11 +305,11 @@ SQL;
} else {
$res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name');
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
- return $res == null ? [] : self::daoToCategory($res);
+ return empty($res) ? [] : self::daoToCategories($res);
}
}
- /** @return array<FreshRSS_Category> */
+ /** @return array<int,FreshRSS_Category> */
public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0): array {
$sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`'
. ($limit < 1 ? '' : ' LIMIT ' . $limit);
@@ -318,7 +318,7 @@ SQL;
$stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) &&
$stm->bindValue(':lu', time() - $defaultCacheDuration, PDO::PARAM_INT) &&
$stm->execute()) {
- return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC));
+ return self::daoToCategories($stm->fetchAll(PDO::FETCH_ASSOC));
} else {
$info = $stm ? $stm->errorInfo() : $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
@@ -333,9 +333,9 @@ SQL;
$sql = 'SELECT * FROM `_category` WHERE id=:id';
$res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
- $cat = self::daoToCategory($res);
- if (isset($cat[0])) {
- return $cat[0];
+ $categories = self::daoToCategories($res);
+ if (isset($categories[self::DEFAULTCATEGORYID])) {
+ return $categories[self::DEFAULTCATEGORYID];
} else {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n");
@@ -394,41 +394,13 @@ SQL;
return isset($res[0]) ? (int)$res[0] : -1;
}
- /** @param array<FreshRSS_Category> $categories */
- public static function findFeed(array $categories, int $feed_id): ?FreshRSS_Feed {
- foreach ($categories as $category) {
- foreach ($category->feeds() as $feed) {
- if ($feed->id() === $feed_id) {
- $feed->_category($category); // Should already be done; just to be safe
- return $feed;
- }
- }
- }
- return null;
- }
-
- /**
- * @param array<FreshRSS_Category> $categories
- */
- public static function countUnread(array $categories, int $minPriority = 0): int {
- $n = 0;
- foreach ($categories as $category) {
- foreach ($category->feeds() as $feed) {
- if ($feed->priority() >= $minPriority) {
- $n += $feed->nbNotRead();
- }
- }
- }
- return $n;
- }
-
/**
* @param array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'website'?:string,'priority'?:int,
* 'error'?:int|bool,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $listDAO
* @return array<int,FreshRSS_Category>
*/
- private static function daoToCategoryPrepopulated(array $listDAO): array {
+ private static function daoToCategoriesPrepopulated(array $listDAO): array {
$list = [];
$previousLine = [];
$feedsDao = [];
@@ -441,11 +413,11 @@ SQL;
$cat = new FreshRSS_Category(
$previousLine['c_name'],
$previousLine['c_id'],
- $feedDao::daoToFeed($feedsDao, $previousLine['c_id'])
+ $feedDao::daoToFeeds($feedsDao, $previousLine['c_id'])
);
$cat->_kind($previousLine['c_kind']);
$cat->_attributes($previousLine['c_attributes'] ?? '[]');
- $list[(int)$previousLine['c_id']] = $cat;
+ $list[$cat->id()] = $cat;
$feedsDao = []; //Prepare for next category
}
@@ -459,13 +431,13 @@ SQL;
$cat = new FreshRSS_Category(
$previousLine['c_name'],
$previousLine['c_id'],
- $feedDao::daoToFeed($feedsDao, $previousLine['c_id'])
+ $feedDao::daoToFeeds($feedsDao, $previousLine['c_id'])
);
$cat->_kind($previousLine['c_kind']);
$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
$cat->_error($previousLine['c_error'] ?? 0);
$cat->_attributes($previousLine['c_attributes'] ?? []);
- $list[(int)$previousLine['c_id']] = $cat;
+ $list[$cat->id()] = $cat;
}
return $list;
@@ -473,11 +445,10 @@ SQL;
/**
* @param array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $listDAO
- * @return array<FreshRSS_Category>
+ * @return array<int,FreshRSS_Category>
*/
- private static function daoToCategory(array $listDAO): array {
+ private static function daoToCategories(array $listDAO): array {
$list = [];
-
foreach ($listDAO as $dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'lastUpdate', 'error']);
$cat = new FreshRSS_Category(
@@ -488,9 +459,8 @@ SQL;
$cat->_lastUpdate($dao['lastUpdate'] ?? 0);
$cat->_error($dao['error'] ?? 0);
$cat->_attributes($dao['attributes'] ?? '');
- $list[] = $cat;
+ $list[$cat->id()] = $cat;
}
-
return $list;
}
}
diff --git a/app/Models/Context.php b/app/Models/Context.php
index 2d22290bc..37a2064c6 100644
--- a/app/Models/Context.php
+++ b/app/Models/Context.php
@@ -10,11 +10,11 @@ final class FreshRSS_Context {
/**
* @var array<int,FreshRSS_Category>
*/
- public static array $categories = [];
+ private static array $categories = [];
/**
* @var array<int,FreshRSS_Tag>
*/
- public static array $tags = [];
+ private static array $tags = [];
public static string $name = '';
public static string $description = '';
public static int $total_unread = 0;
@@ -47,6 +47,7 @@ final class FreshRSS_Context {
*/
public static string $order = 'DESC';
public static int $number = 0;
+ public static int $offset = 0;
public static FreshRSS_BooleanSearch $search;
public static string $first_id = '';
public static string $next_id = '';
@@ -173,10 +174,33 @@ final class FreshRSS_Context {
FreshRSS_Context::$user_conf = null;
}
+ /** @return array<int,FreshRSS_Category> */
+ public static function categories(): array {
+ if (empty(self::$categories)) {
+ $catDAO = FreshRSS_Factory::createCategoryDao();
+ self::$categories = $catDAO->listSortedCategories(true, false);
+ }
+ return self::$categories;
+ }
+
+ /** @return array<int,FreshRSS_Feed> */
+ public static function feeds(): array {
+ return FreshRSS_Category::findFeeds(self::categories());
+ }
+
+ /** @return array<int,FreshRSS_Tag> */
+ public static function labels(bool $precounts = false): array {
+ if (empty(self::$tags) || $precounts) {
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ self::$tags = $tagDAO->listTags($precounts) ?: [];
+ }
+ return self::$tags;
+ }
+
/**
* This action updates the Context object by using request parameters.
*
- * Parameters are:
+ * HTTP GET request parameters are:
* - state (default: conf->default_view)
* - search (default: empty string)
* - order (default: conf->sort_order)
@@ -187,18 +211,15 @@ final class FreshRSS_Context {
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
- public static function updateUsingRequest(): void {
- if (empty(self::$categories)) {
- $catDAO = FreshRSS_Factory::createCategoryDao();
- self::$categories = $catDAO->listSortedCategories();
+ public static function updateUsingRequest(bool $computeStatistics): void {
+ if ($computeStatistics && self::$total_unread === 0) {
+ // Update number of read / unread variables.
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ self::$total_starred = $entryDAO->countUnreadReadFavorites();
+ self::$total_unread = FreshRSS_Category::countUnread(self::categories(), FreshRSS_Feed::PRIORITY_MAIN_STREAM);
+ self::$total_important_unread = FreshRSS_Category::countUnread(self::categories(), FreshRSS_Feed::PRIORITY_IMPORTANT);
}
- // Update number of read / unread variables.
- $entryDAO = FreshRSS_Factory::createEntryDao();
- self::$total_starred = $entryDAO->countUnreadReadFavorites();
- self::$total_unread = FreshRSS_CategoryDAO::countUnread(self::$categories, FreshRSS_Feed::PRIORITY_MAIN_STREAM);
- self::$total_important_unread = FreshRSS_CategoryDAO::countUnread(self::$categories, FreshRSS_Feed::PRIORITY_IMPORTANT);
-
self::_get(Minz_Request::paramString('get') ?: 'a');
self::$state = Minz_Request::paramInt('state') ?: FreshRSS_Context::userConf()->default_state;
@@ -224,6 +245,7 @@ final class FreshRSS_Context {
FreshRSS_Context::userConf()->max_posts_per_rss,
FreshRSS_Context::userConf()->posts_per_page);
}
+ self::$offset = Minz_Request::paramInt('offset');
self::$first_id = Minz_Request::paramString('next');
self::$sinceHours = Minz_Request::paramInt('hours');
}
@@ -394,7 +416,7 @@ final class FreshRSS_Context {
break;
case 'f':
// We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description
- $feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
+ $feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_Category::findFeed(self::$categories, $id);
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
@@ -417,7 +439,7 @@ final class FreshRSS_Context {
if ($cat === null) {
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
}
- //self::$categories[$id] = $cat;
+ self::$categories[$id] = $cat;
} else {
$cat = self::$categories[$id];
}
@@ -433,7 +455,7 @@ final class FreshRSS_Context {
if ($tag === null) {
throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
}
- //self::$tags[$id] = $tag;
+ self::$tags[$id] = $tag;
} else {
$tag = self::$tags[$id];
}
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 9caca1fb7..c782f4c94 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -816,6 +816,28 @@ HTML;
}
/**
+ * @return array{array<string>,array<string>} Array of first tags to show, then array of remaining tags
+ */
+ public function tagsFormattingHelper(): array {
+ $firstTags = [];
+ $remainingTags = [];
+
+ if (FreshRSS_Context::hasUserConf() && in_array(FreshRSS_Context::userConf()->show_tags, ['b', 'f', 'h'], true)) {
+ $maxTagsDisplayed = (int)FreshRSS_Context::userConf()->show_tags_max;
+ $tags = $this->tags();
+ if (!empty($tags)) {
+ if ($maxTagsDisplayed > 0) {
+ $firstTags = array_slice($tags, 0, $maxTagsDisplayed);
+ $remainingTags = array_slice($tags, $maxTagsDisplayed);
+ } else {
+ $firstTags = $tags;
+ }
+ }
+ }
+ return [$firstTags,$remainingTags];
+ }
+
+ /**
* Integer format conversion for Google Reader API format
* @param string|int $dec Decimal number
* @return string 64-bit hexa http://code.google.com/p/google-reader-api/wiki/ItemId
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index 2f0e2b919..f770ce400 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -1063,7 +1063,7 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
- string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
+ string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
int $date_min = 0): array {
if (!$state) {
$state = FreshRSS_Entry::STATE_ALL;
@@ -1120,7 +1120,9 @@ SQL;
. 'WHERE ' . $where
. $search
. 'ORDER BY e.id ' . $order
- . ($limit > 0 ? ' LIMIT ' . intval($limit) : '')]; //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
+ . ($limit > 0 ? ' LIMIT ' . $limit : '') // http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
+ . ($offset > 0 ? ' OFFSET ' . $offset : '')
+ ];
}
/**
@@ -1131,9 +1133,9 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
- string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
+ string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
int $date_min = 0) {
- [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
+ [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
if ($order !== 'DESC' && $order !== 'ASC') {
$order = 'DESC';
@@ -1152,7 +1154,7 @@ SQL;
} else {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
- return $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
+ return $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
@@ -1167,9 +1169,9 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
- string $order = 'DESC', int $limit = 1, string $firstId = '',
+ string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '',
?FreshRSS_BooleanSearch $filters = null, int $date_min = 0): Traversable {
- $stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
+ $stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
if ($stm) {
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
if (is_array($row)) {
@@ -1233,9 +1235,9 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
- string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null): ?array {
+ string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null): ?array {
- [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
+ [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters);
$stm = $this->pdo->prepare($sql);
if ($stm !== false && $stm->execute($values) && ($res = $stm->fetchAll(PDO::FETCH_COLUMN, 0)) !== false) {
/** @var array<numeric-string> $res */
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index 2eab0a3cf..b8425e86b 100644
--- a/app/Models/Feed.php
+++ b/app/Models/Feed.php
@@ -76,7 +76,7 @@ class FreshRSS_Feed extends Minz_Model {
}
}
- public static function example(): FreshRSS_Feed {
+ public static function default(): FreshRSS_Feed {
$f = new FreshRSS_Feed('http://example.net/', false);
$f->faviconPrepare();
return $f;
@@ -708,7 +708,8 @@ class FreshRSS_Feed extends Minz_Model {
$view = new FreshRSS_View();
$view->_path('index/rss.phtml');
$view->internal_rendering = true;
- $view->rss_url = $feedSourceUrl;
+ $view->rss_url = htmlspecialchars($feedSourceUrl, ENT_COMPAT, 'UTF-8');
+ $view->html_url = $view->rss_url;
$view->entries = [];
try {
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index 0744970de..417c59da1 100644
--- a/app/Models/FeedDAO.php
+++ b/app/Models/FeedDAO.php
@@ -322,7 +322,7 @@ SQL;
}
/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
- $feeds = self::daoToFeed($res);
+ $feeds = self::daoToFeeds($res);
return $feeds[$id] ?? null;
}
@@ -331,7 +331,7 @@ SQL;
$res = $this->fetchAssoc($sql, [':url' => $url]);
/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
- return empty($res[0]) ? null : (current(self::daoToFeed($res)) ?: null);
+ return empty($res[0]) ? null : (current(self::daoToFeeds($res)) ?: null);
}
/** @return array<int> */
@@ -343,14 +343,14 @@ SQL;
}
/**
- * @return array<FreshRSS_Feed>
+ * @return array<int,FreshRSS_Feed>
*/
public function listFeeds(): array {
$sql = 'SELECT * FROM `_feed` ORDER BY name';
$res = $this->fetchAssoc($sql);
/** @var array<array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'ttl':int,'attributes':string}>|null $res */
- return $res == null ? [] : self::daoToFeed($res);
+ return $res == null ? [] : self::daoToFeeds($res);
}
/** @return array<string,string> */
@@ -375,7 +375,7 @@ SQL;
/**
* @param int $defaultCacheDuration Use -1 to return all feeds, without filtering them by TTL.
- * @return array<FreshRSS_Feed>
+ * @return array<int,FreshRSS_Feed>
*/
public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0): array {
$sql = 'SELECT id, url, kind, category, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes, `cache_nbEntries`, `cache_nbUnreads` '
@@ -387,7 +387,7 @@ SQL;
. ($limit < 1 ? '' : 'LIMIT ' . intval($limit));
$stm = $this->pdo->query($sql);
if ($stm !== false) {
- return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+ return self::daoToFeeds($stm->fetchAll(PDO::FETCH_ASSOC));
} else {
$info = $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
@@ -409,7 +409,7 @@ SQL;
/**
* @param bool|null $muted to include only muted feeds
- * @return array<FreshRSS_Feed>
+ * @return array<int,FreshRSS_Feed>
*/
public function listByCategory(int $cat, ?bool $muted = null): array {
$sql = 'SELECT * FROM `_feed` WHERE category=:category';
@@ -425,9 +425,9 @@ SQL;
* @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res
*/
- $feeds = self::daoToFeed($res);
+ $feeds = self::daoToFeeds($res);
- usort($feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
+ uasort($feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
return strnatcasecmp($a->name(), $b->name());
});
@@ -585,7 +585,7 @@ SQL;
* 'pathEntries'?:string,'httpAuth'?:string,'error'?:int|bool,'ttl'?:int,'attributes'?:string,'cache_nbUnreads'?:int,'cache_nbEntries'?:int}> $listDAO
* @return array<int,FreshRSS_Feed>
*/
- public static function daoToFeed(array $listDAO, ?int $catID = null): array {
+ public static function daoToFeeds(array $listDAO, ?int $catID = null): array {
$list = [];
foreach ($listDAO as $key => $dao) {
diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php
index 391bde36d..b5611a7d6 100644
--- a/app/Models/TagDAO.php
+++ b/app/Models/TagDAO.php
@@ -184,16 +184,16 @@ SQL;
public function searchById(int $id): ?FreshRSS_Tag {
$res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE id=:id', [':id' => $id]);
/** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */
- return $res === null ? null : self::daoToTag($res)[0] ?? null;
+ return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
public function searchByName(string $name): ?FreshRSS_Tag {
$res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE name=:name', [':name' => $name]);
/** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */
- return $res === null ? null : self::daoToTag($res)[0] ?? null;
+ return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
- /** @return array<FreshRSS_Tag>|false */
+ /** @return array<int,FreshRSS_Tag>|false */
public function listTags(bool $precounts = false) {
if ($precounts) {
$sql = <<<'SQL'
@@ -211,7 +211,7 @@ SQL;
$stm = $this->pdo->query($sql);
if ($stm !== false) {
$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
- return self::daoToTag($res);
+ return self::daoToTags($res);
} else {
$info = $this->pdo->errorInfo();
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
@@ -430,9 +430,9 @@ SQL;
/**
* @param iterable<array{'id':int,'name':string,'attributes'?:string}> $listDAO
- * @return array<FreshRSS_Tag>
+ * @return array<int,FreshRSS_Tag>
*/
- private static function daoToTag(iterable $listDAO): array {
+ private static function daoToTags(iterable $listDAO): array {
$list = [];
foreach ($listDAO as $dao) {
if (empty($dao['id']) || empty($dao['name'])) {
@@ -446,7 +446,7 @@ SQL;
if (isset($dao['unreads'])) {
$tag->_nbUnread($dao['unreads']);
}
- $list[] = $tag;
+ $list[$tag->id()] = $tag;
}
return $list;
}
diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php
index a1e0dbbaa..7ccaa2671 100644
--- a/app/Models/UserConfiguration.php
+++ b/app/Models/UserConfiguration.php
@@ -41,7 +41,7 @@ declare(strict_types=1);
* @property bool $onread_jump_next
* @property string $passwordHash
* @property int $posts_per_page
- * @property array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries
+ * @property array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $queries
* @property bool $reading_confirm
* @property int $since_hours_posts_per_rss
* @property bool $show_fav_unread
@@ -82,6 +82,20 @@ final class FreshRSS_UserConfiguration extends Minz_Configuration {
}
/**
+ * Access the default configuration for users.
+ * @throws Minz_FileNotExistException
+ */
+ public static function default(): FreshRSS_UserConfiguration {
+ static $default_user_conf = null;
+ if ($default_user_conf == null) {
+ $namespace = 'user_default';
+ FreshRSS_UserConfiguration::register($namespace, '_', FRESHRSS_PATH . '/config-user.default.php');
+ $default_user_conf = FreshRSS_UserConfiguration::get($namespace);
+ }
+ return $default_user_conf;
+ }
+
+ /**
* @param non-empty-string $key
* @return array<int|string,mixed>|null
*/
diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php
index 000cfbbdd..156b2df4a 100644
--- a/app/Models/UserQuery.php
+++ b/app/Models/UserQuery.php
@@ -18,17 +18,34 @@ class FreshRSS_UserQuery {
private FreshRSS_BooleanSearch $search;
private int $state = 0;
private string $url = '';
- private ?FreshRSS_FeedDAO $feed_dao;
- private ?FreshRSS_CategoryDAO $category_dao;
- private ?FreshRSS_TagDAO $tag_dao;
+ private string $token = '';
+ private bool $shareRss = false;
+ private bool $shareOpml = false;
+ /** @var array<int,FreshRSS_Category> $categories */
+ private array $categories;
+ /** @var array<int,FreshRSS_Tag> $labels */
+ private array $labels;
+
+ public static function generateToken(string $salt): string {
+ if (!FreshRSS_Context::hasSystemConf()) {
+ return '';
+ }
+ $hash = md5(FreshRSS_Context::systemConf()->salt . $salt . random_bytes(16));
+ if (function_exists('gmp_init')) {
+ // Shorten the hash if possible by converting from base 16 to base 62
+ $hash = gmp_strval(gmp_init($hash, 16), 62);
+ }
+ return $hash;
+ }
/**
- * @param array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string} $query
+ * @param array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,shareRss?:bool,shareOpml?:bool} $query
+ * @param array<int,FreshRSS_Category> $categories
+ * @param array<int,FreshRSS_Tag> $labels
*/
- public function __construct(array $query, FreshRSS_FeedDAO $feed_dao = null, FreshRSS_CategoryDAO $category_dao = null, FreshRSS_TagDAO $tag_dao = null) {
- $this->category_dao = $category_dao;
- $this->feed_dao = $feed_dao;
- $this->tag_dao = $tag_dao;
+ public function __construct(array $query, array $categories, array $labels) {
+ $this->categories = $categories;
+ $this->labels = $labels;
if (isset($query['get'])) {
$this->parseGet($query['get']);
}
@@ -49,8 +66,18 @@ class FreshRSS_UserQuery {
if (!isset($query['search'])) {
$query['search'] = '';
}
+ if (!empty($query['token'])) {
+ $this->token = $query['token'];
+ }
+ if (isset($query['shareRss'])) {
+ $this->shareRss = $query['shareRss'];
+ }
+ if (isset($query['shareOpml'])) {
+ $this->shareOpml = $query['shareOpml'];
+ }
+
// linked too deeply with the search object, need to use dependency injection
- $this->search = new FreshRSS_BooleanSearch($query['search']);
+ $this->search = new FreshRSS_BooleanSearch($query['search'], 0, 'AND', false);
if (!empty($query['state'])) {
$this->state = intval($query['state']);
}
@@ -59,16 +86,19 @@ class FreshRSS_UserQuery {
/**
* Convert the current object to an array.
*
- * @return array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}
+ * @return array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}
*/
public function toArray(): array {
return array_filter([
'get' => $this->get,
'name' => $this->name,
'order' => $this->order,
- 'search' => $this->search->__toString(),
+ 'search' => $this->search->getRawInput(),
'state' => $this->state,
'url' => $this->url,
+ 'token' => $this->token,
+ 'shareRss' => $this->shareRss,
+ 'shareOpml' => $this->shareOpml,
]);
}
@@ -77,93 +107,44 @@ class FreshRSS_UserQuery {
*/
private function parseGet(string $get): void {
$this->get = $get;
- if (preg_match('/(?P<type>[acfst])(_(?P<id>\d+))?/', $get, $matches)) {
+ if (preg_match('/(?P<type>[acfistT])(_(?P<id>\d+))?/', $get, $matches)) {
$id = intval($matches['id'] ?? '0');
switch ($matches['type']) {
case 'a':
- $this->parseAll();
+ $this->get_type = 'all';
break;
case 'c':
- $this->parseCategory($id);
+ $this->get_type = 'category';
+ $c = $this->categories[$id] ?? null;
+ $this->get_name = $c === null ? '' : $c->name();
break;
case 'f':
- $this->parseFeed($id);
+ $this->get_type = 'feed';
+ $f = FreshRSS_Category::findFeed($this->categories, $id);
+ $this->get_name = $f === null ? '' : $f->name();
+ break;
+ case 'i':
+ $this->get_type = 'important';
break;
case 's':
- $this->parseFavorite();
+ $this->get_type = 'favorite';
break;
case 't':
- $this->parseTag($id);
+ $this->get_type = 'label';
+ $l = $this->labels[$id] ?? null;
+ $this->get_name = $l === null ? '' : $l->name();
+ break;
+ case 'T':
+ $this->get_type = 'all_labels';
break;
}
+ if ($this->get_name === '' && in_array($matches['type'], ['c', 'f', 't'], true)) {
+ $this->deprecated = true;
+ }
}
}
/**
- * Parse the query string when it is an "all" query
- */
- private function parseAll(): void {
- $this->get_name = 'all';
- $this->get_type = 'all';
- }
-
- /**
- * Parse the query string when it is a "category" query
- */
- private function parseCategory(int $id): void {
- if ($this->category_dao === null) {
- $this->category_dao = FreshRSS_Factory::createCategoryDao();
- }
- $category = $this->category_dao->searchById($id);
- if ($category !== null) {
- $this->get_name = $category->name();
- } else {
- $this->deprecated = true;
- }
- $this->get_type = 'category';
- }
-
- /**
- * Parse the query string when it is a "feed" query
- */
- private function parseFeed(int $id): void {
- if ($this->feed_dao === null) {
- $this->feed_dao = FreshRSS_Factory::createFeedDao();
- }
- $feed = $this->feed_dao->searchById($id);
- if ($feed !== null) {
- $this->get_name = $feed->name();
- } else {
- $this->deprecated = true;
- }
- $this->get_type = 'feed';
- }
-
- /**
- * Parse the query string when it is a "tag" query
- */
- private function parseTag(int $id): void {
- if ($this->tag_dao === null) {
- $this->tag_dao = FreshRSS_Factory::createTagDao();
- }
- $tag = $this->tag_dao->searchById($id);
- if ($tag !== null) {
- $this->get_name = $tag->name();
- } else {
- $this->deprecated = true;
- }
- $this->get_type = 'tag';
- }
-
- /**
- * Parse the query string when it is a "favorite" query
- */
- private function parseFavorite(): void {
- $this->get_name = 'favorite';
- $this->get_type = 'favorite';
- }
-
- /**
* Check if the current user query is deprecated.
* It is deprecated if the category or the feed used in the query are
* not existing.
@@ -219,7 +200,7 @@ class FreshRSS_UserQuery {
}
public function getOrder(): string {
- return $this->order;
+ return $this->order ?: FreshRSS_Context::userConf()->sort_order;
}
public function getSearch(): FreshRSS_BooleanSearch {
@@ -227,11 +208,74 @@ class FreshRSS_UserQuery {
}
public function getState(): int {
- return $this->state;
+ $state = $this->state;
+ if (!($state & FreshRSS_Entry::STATE_READ) && !($state & FreshRSS_Entry::STATE_NOT_READ)) {
+ $state |= FreshRSS_Entry::STATE_READ | FreshRSS_Entry::STATE_NOT_READ;
+ }
+ if (!($state & FreshRSS_Entry::STATE_FAVORITE) && !($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) {
+ $state |= FreshRSS_Entry::STATE_FAVORITE | FreshRSS_Entry::STATE_NOT_FAVORITE;
+ }
+ return $state;
}
public function getUrl(): string {
return $this->url;
}
+ public function getToken(): string {
+ return $this->token;
+ }
+
+ public function setToken(string $token): void {
+ $this->token = $token;
+ }
+
+ public function setShareRss(bool $shareRss): void {
+ $this->shareRss = $shareRss;
+ }
+
+ public function shareRss(): bool {
+ return $this->shareRss;
+ }
+
+ public function setShareOpml(bool $shareOpml): void {
+ $this->shareOpml = $shareOpml;
+ }
+
+ public function shareOpml(): bool {
+ return $this->shareOpml;
+ }
+
+ protected function sharedUrl(bool $xmlEscaped = true): string {
+ $currentUser = Minz_User::name() ?? '';
+ return Minz_Url::display("/api/query.php?user={$currentUser}&t={$this->token}", $xmlEscaped ? 'html' : '', true);
+ }
+
+ public function sharedUrlRss(bool $xmlEscaped = true): string {
+ if ($this->shareRss && $this->token !== '') {
+ return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=rss';
+ }
+ return '';
+ }
+
+ public function sharedUrlHtml(bool $xmlEscaped = true): string {
+ if ($this->shareRss && $this->token !== '') {
+ return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=html';
+ }
+ return '';
+ }
+
+ /**
+ * OPML is only safe for some query types, otherwise it risks leaking unwanted feed information.
+ */
+ public function safeForOpml(): bool {
+ return in_array($this->get_type, ['all', 'category', 'feed'], true);
+ }
+
+ public function sharedUrlOpml(bool $xmlEscaped = true): string {
+ if ($this->shareOpml && $this->token !== '' && $this->safeForOpml()) {
+ return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=opml';
+ }
+ return '';
+ }
}
diff --git a/app/Models/View.php b/app/Models/View.php
index 4dd0be36a..2595cd1fa 100644
--- a/app/Models/View.php
+++ b/app/Models/View.php
@@ -10,7 +10,7 @@ class FreshRSS_View extends Minz_View {
public $callbackBeforeFeeds;
/** @var callable */
public $callbackBeforePagination;
- /** @var array<FreshRSS_Category> */
+ /** @var array<int,FreshRSS_Category> */
public array $categories;
public ?FreshRSS_Category $category;
public ?FreshRSS_Tag $tag;
@@ -18,11 +18,11 @@ class FreshRSS_View extends Minz_View {
/** @var iterable<FreshRSS_Entry> */
public $entries;
public FreshRSS_Entry $entry;
- public ?FreshRSS_Feed $feed;
- /** @var array<FreshRSS_Feed> */
+ public FreshRSS_Feed $feed;
+ /** @var array<int,FreshRSS_Feed> */
public array $feeds;
public int $nbUnreadTags;
- /** @var array<FreshRSS_Tag> */
+ /** @var array<int,FreshRSS_Tag> */
public array $tags;
/** @var array<int,array{'id':int,'name':string,'id_entry':string,'checked':bool}> */
public array $tagsForEntry;
@@ -100,6 +100,8 @@ class FreshRSS_View extends Minz_View {
public int $nbPage;
// RSS view
+ public FreshRSS_UserQuery $userQuery;
+ public string $html_url = '';
public string $rss_title = '';
public string $rss_url = '';
public string $rss_base = '';
diff --git a/app/Models/ViewJavascript.php b/app/Models/ViewJavascript.php
index 38a0a74f0..2b3c87537 100644
--- a/app/Models/ViewJavascript.php
+++ b/app/Models/ViewJavascript.php
@@ -3,11 +3,11 @@ declare(strict_types=1);
final class FreshRSS_ViewJavascript extends FreshRSS_View {
- /** @var array<FreshRSS_Category> */
+ /** @var array<int,FreshRSS_Category> */
public array $categories;
- /** @var array<FreshRSS_Feed> */
+ /** @var array<int,FreshRSS_Feed> */
public array $feeds;
- /** @var array<FreshRSS_Tag> */
+ /** @var array<int,FreshRSS_Tag> */
public array $tags;
public string $nonce;
diff --git a/app/Models/ViewStats.php b/app/Models/ViewStats.php
index d7bb08c5f..ca98c554a 100644
--- a/app/Models/ViewStats.php
+++ b/app/Models/ViewStats.php
@@ -3,10 +3,10 @@ declare(strict_types=1);
final class FreshRSS_ViewStats extends FreshRSS_View {
- /** @var array<FreshRSS_Category> */
+ /** @var array<int,FreshRSS_Category> */
public array $categories;
- public ?FreshRSS_Feed $feed;
- /** @var array<FreshRSS_Feed> */
+ public FreshRSS_Feed $feed;
+ /** @var array<int,FreshRSS_Feed> */
public array $feeds;
public bool $displaySlider = false;
diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php
index 7f027441b..ea268fc0f 100644
--- a/app/Services/ExportService.php
+++ b/app/Services/ExportService.php
@@ -95,7 +95,7 @@ class FreshRSS_Export_Service {
$view = new FreshRSS_View();
$view->categories = $this->category_dao->listCategories(true) ?: [];
- $feed = FreshRSS_CategoryDAO::findFeed($view->categories, $feed_id);
+ $feed = FreshRSS_Category::findFeed($view->categories, $feed_id);
if ($feed === null) {
return null;
}
diff --git a/app/Utils/dotpathUtil.php b/app/Utils/dotpathUtil.php
index b4da1506e..939434c5a 100644
--- a/app/Utils/dotpathUtil.php
+++ b/app/Utils/dotpathUtil.php
@@ -107,7 +107,8 @@ final class FreshRSS_dotpath_Util
$view = new FreshRSS_View();
$view->_path('index/rss.phtml');
$view->internal_rendering = true;
- $view->rss_url = $feedSourceUrl;
+ $view->rss_url = htmlspecialchars($feedSourceUrl, ENT_COMPAT, 'UTF-8');
+ $view->html_url = $view->rss_url;
$view->entries = [];
$view->rss_title = isset($dotPaths['feedTitle'])
diff --git a/app/i18n/cz/admin.php b/app/i18n/cz/admin.php
index ad67d0b23..846523ead 100644
--- a/app/i18n/cz/admin.php
+++ b/app/i18n/cz/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (pro pokročilé uživatele s HTTPS)',
'none' => 'Žádný (nebezpečné)',
'title' => 'Ověřování',
- 'token' => 'Ověřovací token',
- 'token_help' => 'Umožňuje přístup k výstupu RSS výchozího uživatele bez ověřování:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Metoda ověřování',
'unsafe_autologin' => 'Povolit nebezpečné automatické přihlášení pomocí formátu: ',
),
diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php
index e928f287c..8c9adb120 100644
--- a/app/i18n/cz/conf.php
+++ b/app/i18n/cz/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Zobrazit podle kanálu',
'order' => 'Seřadit podle data',
'search' => 'Výraz',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Stav',
'tags' => 'Zobrazit podle štítku',
'type' => 'Typ',
),
'get_all' => 'Zobrazit všechny články',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Zobrazit kategorii „%s“',
'get_favorite' => 'Zobrazit oblíbené články',
'get_feed' => 'Zobrazit kanál „%s“',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Název',
'no_filter' => 'Žádný filtr',
'number' => 'Dotaz č. %d',
'order_asc' => 'Zobrazit nejdříve nejstarší články',
'order_desc' => 'Zobrazit nejdříve nejnovější články',
'search' => 'Hledat „%s“',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Zobrazit všechny články',
'state_1' => 'Zobrazit přečtené články',
'state_2' => 'Zobrazit nepřečtené články',
diff --git a/app/i18n/de/admin.php b/app/i18n/de/admin.php
index 336e7ff02..5d35dc7c3 100644
--- a/app/i18n/de/admin.php
+++ b/app/i18n/de/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (HTTPS für erfahrene Benutzer)',
'none' => 'Keine (gefährlich)',
'title' => 'Authentifizierung',
- 'token' => 'Authentifizierungs-Token',
- 'token_help' => 'Erlaubt den Zugriff auf die RSS-Ausgabe des Standardbenutzers ohne Authentifizierung.',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Authentifizierungsmethode',
'unsafe_autologin' => 'Erlaube unsicheres automatisches Anmelden mit folgendem Format: ',
),
diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php
index db4e8b7ad..120125c13 100644
--- a/app/i18n/de/conf.php
+++ b/app/i18n/de/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Nach Feed filtern',
'order' => 'Nach Datum sortieren',
'search' => 'Suchbegriff',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Eigenschaft',
'tags' => 'Nach Labels filtern',
'type' => 'Filter-Typ',
),
'get_all' => 'Alle Artikel anzeigen',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Kategorie „%s“ anzeigen',
'get_favorite' => 'Lieblingsartikel anzeigen',
'get_feed' => 'Feed „%s“ anzeigen',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Name', // IGNORE
'no_filter' => 'Kein Filter',
'number' => 'Abfrage Nr. %d',
'order_asc' => 'Älteste Artikel zuerst anzeigen',
'order_desc' => 'Neueste Artikel zuerst anzeigen',
'search' => 'Suche nach „%s“',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Alle Artikel anzeigen',
'state_1' => 'Gelesene Artikel anzeigen',
'state_2' => 'Ungelesene Artikel anzeigen',
diff --git a/app/i18n/el/admin.php b/app/i18n/el/admin.php
index edceeba9f..7c561b94b 100644
--- a/app/i18n/el/admin.php
+++ b/app/i18n/el/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (για έμπειρους χρήστες με )',
'none' => 'Καμία (ριψοκίνδυνο)',
'title' => 'Πιστοποίηση',
- 'token' => 'Διακριτικό Πιστοποίησης (token)',
- 'token_help' => 'Επιτρέπει την πρόσβαση στα RSS αποτελέσματα του προεπιλεγμένου χρήστη χωρίς έλεγχο ταυτότητας:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Μέθοδος Πιστοποίησης',
'unsafe_autologin' => 'Επιτρέψτε την μη ασφαλή αυτόματη σύνδεση με την χρήση της μορφής: ',
),
diff --git a/app/i18n/el/conf.php b/app/i18n/el/conf.php
index bbd90e3f4..c92fdc13a 100644
--- a/app/i18n/el/conf.php
+++ b/app/i18n/el/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Display by feed', // TODO
'order' => 'Sort by date', // TODO
'search' => 'Expression', // TODO
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'State', // TODO
'tags' => 'Display by label', // TODO
'type' => 'Type', // TODO
),
'get_all' => 'Display all articles', // TODO
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Display “%s” category', // TODO
'get_favorite' => 'Display favourite articles', // TODO
'get_feed' => 'Display “%s” feed', // TODO
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Name', // TODO
'no_filter' => 'No filter', // TODO
'number' => 'Query n°%d', // TODO
'order_asc' => 'Display oldest articles first', // TODO
'order_desc' => 'Display newest articles first', // TODO
'search' => 'Search for “%s”', // TODO
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Display all articles', // TODO
'state_1' => 'Display read articles', // TODO
'state_2' => 'Display unread articles', // TODO
diff --git a/app/i18n/en-us/admin.php b/app/i18n/en-us/admin.php
index 54cd42cbd..5d57df310 100644
--- a/app/i18n/en-us/admin.php
+++ b/app/i18n/en-us/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (for advanced users with HTTPS)', // IGNORE
'none' => 'None (dangerous)', // IGNORE
'title' => 'Authentication', // IGNORE
- 'token' => 'Authentication token', // IGNORE
- 'token_help' => 'Allows access to RSS output of the default user without authentication:', // IGNORE
+ 'token' => 'Master authentication token', // IGNORE
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // IGNORE
'type' => 'Authentication method', // IGNORE
'unsafe_autologin' => 'Allow unsafe automatic login using the format: ', // IGNORE
),
diff --git a/app/i18n/en-us/conf.php b/app/i18n/en-us/conf.php
index 7b87d5b28..bee649741 100644
--- a/app/i18n/en-us/conf.php
+++ b/app/i18n/en-us/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Display by feed', // IGNORE
'order' => 'Sort by date', // IGNORE
'search' => 'Expression', // IGNORE
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // IGNORE
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // IGNORE
'state' => 'State', // IGNORE
'tags' => 'Display by label', // IGNORE
'type' => 'Type', // IGNORE
),
'get_all' => 'Display all articles', // IGNORE
+ 'get_all_labels' => 'Display articles with any label', // IGNORE
'get_category' => 'Display “%s” category', // IGNORE
'get_favorite' => 'Display favorite articles',
'get_feed' => 'Display “%s” feed', // IGNORE
+ 'get_important' => 'Display articles from important feeds', // IGNORE
+ 'get_label' => 'Display articles with “%s” label', // IGNORE
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // IGNORE
'name' => 'Name', // IGNORE
'no_filter' => 'No filter', // IGNORE
'number' => 'Query n°%d', // IGNORE
'order_asc' => 'Display oldest articles first', // IGNORE
'order_desc' => 'Display newest articles first', // IGNORE
'search' => 'Search for “%s”', // IGNORE
+ 'share' => array(
+ '_' => 'Share this query by link', // IGNORE
+ 'help' => 'Give this link if you want to share this query with anyone', // IGNORE
+ 'html' => 'Shareable link to the HTML page', // IGNORE
+ 'opml' => 'Shareable link to the OPML list of feeds', // IGNORE
+ 'rss' => 'Shareable link to the RSS feed', // IGNORE
+ ),
'state_0' => 'Display all articles', // IGNORE
'state_1' => 'Display read articles', // IGNORE
'state_2' => 'Display unread articles', // IGNORE
diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php
index 20fa0e6c6..4ce82a3ff 100644
--- a/app/i18n/en/admin.php
+++ b/app/i18n/en/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (for advanced users with HTTPS)',
'none' => 'None (dangerous)',
'title' => 'Authentication',
- 'token' => 'Authentication token',
- 'token_help' => 'Allows access to RSS output of the default user without authentication:',
+ 'token' => 'Master authentication token',
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:',
'type' => 'Authentication method',
'unsafe_autologin' => 'Allow unsafe automatic login using the format: ',
),
diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php
index 1b3c14391..dfe8fca51 100644
--- a/app/i18n/en/conf.php
+++ b/app/i18n/en/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Display by feed',
'order' => 'Sort by date',
'search' => 'Expression',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds',
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS',
'state' => 'State',
'tags' => 'Display by label',
'type' => 'Type',
),
'get_all' => 'Display all articles',
+ 'get_all_labels' => 'Display articles with any label',
'get_category' => 'Display “%s” category',
'get_favorite' => 'Display favourite articles',
'get_feed' => 'Display “%s” feed',
+ 'get_important' => 'Display articles from important feeds',
+ 'get_label' => 'Display articles with “%s” label',
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.',
'name' => 'Name',
'no_filter' => 'No filter',
'number' => 'Query n°%d',
'order_asc' => 'Display oldest articles first',
'order_desc' => 'Display newest articles first',
'search' => 'Search for “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link',
+ 'help' => 'Give this link if you want to share this query with anyone',
+ 'html' => 'Shareable link to the HTML page',
+ 'opml' => 'Shareable link to the OPML list of feeds',
+ 'rss' => 'Shareable link to the RSS feed',
+ ),
'state_0' => 'Display all articles',
'state_1' => 'Display read articles',
'state_2' => 'Display unread articles',
diff --git a/app/i18n/es/admin.php b/app/i18n/es/admin.php
index 1643ed814..2c83bf76d 100644
--- a/app/i18n/es/admin.php
+++ b/app/i18n/es/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (para usuarios avanzados con HTTPS)',
'none' => 'Ninguno (peligroso)',
'title' => 'Identificación',
- 'token' => 'Clave de identificación',
- 'token_help' => 'Permite el acceso a la salida RSS del usuario por defecto sin necesidad de identificación:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Método de identificación',
'unsafe_autologin' => 'Permite la identificación automática insegura usando el formato: ',
),
diff --git a/app/i18n/es/conf.php b/app/i18n/es/conf.php
index 282f8e6c1..d03b1e45a 100644
--- a/app/i18n/es/conf.php
+++ b/app/i18n/es/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Mostrar por feed',
'order' => 'Ordenar por fecha',
'search' => 'Expresión',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Estado',
'tags' => 'Mostrar por etiqueta',
'type' => 'Tipo',
),
'get_all' => 'Mostrar todos los artículos',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Mostrar la categoría “%s”',
'get_favorite' => 'Mostrar artículos favoritos',
'get_feed' => 'Mostrar fuente “%s”',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Nombre',
'no_filter' => 'Sin filtro',
'number' => 'Consulta n° %d',
'order_asc' => 'Mostrar primero los artículos más antiguos',
'order_desc' => 'Mostrar primero los artículos más recientes',
'search' => 'Buscar “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Mostrar todos los artículos',
'state_1' => 'Mostrar artículos leídos',
'state_2' => 'Mostrar artículos pendientes',
diff --git a/app/i18n/fa/admin.php b/app/i18n/fa/admin.php
index 514074a6a..7bc62eee8 100644
--- a/app/i18n/fa/admin.php
+++ b/app/i18n/fa/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => ' HTTP (برای کاربران پیشرفته با HTTPS)',
'none' => ' هیچ (خطرناک)',
'title' => ' احراز هویت',
- 'token' => ' نشانه احراز هویت',
- 'token_help' => ' امکان دسترسی به خروجی RSS کاربر پیش فرض بدون احراز هویت را می دهد:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => ' روش احراز هویت',
'unsafe_autologin' => ' اجازه ورود خودکار ناامن را با استفاده از قالب:',
),
diff --git a/app/i18n/fa/conf.php b/app/i18n/fa/conf.php
index 0daa2d34f..9a025db65 100644
--- a/app/i18n/fa/conf.php
+++ b/app/i18n/fa/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => ' نمایش با فید',
'order' => ' مرتب سازی بر اساس تاریخ',
'search' => ' بیان',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => ' ایالت',
'tags' => ' نمایش بر اساس برچسب',
'type' => ' نوع',
),
'get_all' => ' نمایش همه مقالات',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => ' دسته «%s» را نمایش دهید',
'get_favorite' => ' نمایش مقالات مورد علاقه',
'get_feed' => ' فید "%s" را نمایش دهید',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => ' نام',
'no_filter' => ' بدون فیلتر',
'number' => ' پرس و جو n°%d',
'order_asc' => ' ابتدا قدیمی ترین مقالات را نمایش دهید',
'order_desc' => ' ابتدا جدیدترین مقالات را نمایش دهید',
'search' => ' «%s» را جستجو کنید',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'نمایش همه مقالات',
'state_1' => 'نمایش مقالات خوانده شده',
'state_2' => 'نمایش مقالات خوانده نشده',
diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php
index b7178b6d1..4e8ddca53 100644
--- a/app/i18n/fr/admin.php
+++ b/app/i18n/fr/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (pour utilisateurs avancés avec HTTPS)',
'none' => 'Aucune (dangereux)',
'title' => 'Authentification',
- 'token' => 'Jeton d’identification',
- 'token_help' => 'Permet d’accéder à la sortie RSS de l’utilisateur par défaut sans besoin de s’authentifier :',
+ 'token' => 'Jeton d’identification maître',
+ 'token_help' => 'Permet d’accéder à toutes les sorties RSS de l’utilisateur et au rafraîchissement des flux sans besoin de s’authentifier :',
'type' => 'Méthode d’authentification',
'unsafe_autologin' => 'Autoriser les connexions automatiques non-sûres au format : ',
),
diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php
index 5556e468a..3aaf6e37a 100644
--- a/app/i18n/fr/conf.php
+++ b/app/i18n/fr/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Afficher par flux',
'order' => 'Tri par date',
'search' => 'Expression', // IGNORE
+ 'shareOpml' => 'Active le partage par OPML des catégories et flux correspondants',
+ 'shareRss' => 'Active le partage par HTML &amp; RSS',
'state' => 'État',
'tags' => 'Afficher par étiquette',
'type' => 'Type', // IGNORE
),
'get_all' => 'Afficher tous les articles',
+ 'get_all_labels' => 'Afficher les articles avec une étiquette',
'get_category' => 'Afficher la catégorie <em>%s<em>',
'get_favorite' => 'Afficher les articles favoris',
'get_feed' => 'Afficher le flux <em>%s</em>',
+ 'get_important' => 'Afficher les articles des flux importants',
+ 'get_label' => 'Afficher les articles avec l’étiquette “%s”',
+ 'help' => 'Voir la <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation pour les filtres utilisateurs et repartage par HTML / RSS / OPML</a>.',
'name' => 'Nom',
'no_filter' => 'Aucun filtre appliqué',
'number' => 'Filtre n°%d',
'order_asc' => 'Afficher les articles les plus anciens en premier',
'order_desc' => 'Afficher les articles les plus récents en premier',
'search' => 'Recherche de « %s »',
+ 'share' => array(
+ '_' => 'Partager ce filtre par lien',
+ 'help' => 'Donner ce lien pour partager le contenu du filtre avec d’autres personnes',
+ 'html' => 'Lien partageable de la page HTML',
+ 'opml' => 'Lien partageable de la liste des flux au format OPML',
+ 'rss' => 'Lien partageable du flux RSS',
+ ),
'state_0' => 'Afficher tous les articles',
'state_1' => 'Afficher les articles lus',
'state_2' => 'Afficher les articles non lus',
diff --git a/app/i18n/he/admin.php b/app/i18n/he/admin.php
index 22a49e126..068b22fec 100644
--- a/app/i18n/he/admin.php
+++ b/app/i18n/he/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (למשתמשים מתקדמים עם HTTPS)',
'none' => 'ללא (מסוכן)',
'title' => 'Authentication', // TODO
- 'token' => 'מחרוזת אימות',
- 'token_help' => 'Allows to access RSS output of the default user without authentication:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'שיטת אימות',
'unsafe_autologin' => 'הרשאה להתחברות אוטומטית בפורמט: ',
),
diff --git a/app/i18n/he/conf.php b/app/i18n/he/conf.php
index d2142b1fd..6816a6497 100644
--- a/app/i18n/he/conf.php
+++ b/app/i18n/he/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Display by feed', // TODO
'order' => 'Sort by date', // TODO
'search' => 'Expression', // TODO
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'State', // TODO
'tags' => 'Display by label', // TODO
'type' => 'Type', // TODO
),
'get_all' => 'הצגת כל המאמרים',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'הצגת קטגוריה “%s”',
'get_favorite' => 'הצגת מאמרים מועדפים',
'get_feed' => 'הצגת הזנה %s',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Name', // TODO
'no_filter' => 'ללא סינון',
'number' => 'שאילתה מספר °%d',
'order_asc' => 'הצגת מאמרים ישנים בראש',
'order_desc' => 'הצגת מאמרים חדשים בראש',
'search' => 'חיפוש “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'הצגת כל המאמרים',
'state_1' => 'הצגת מאמרים שנקראו',
'state_2' => 'הצגת מאמרים שלא נקראו',
diff --git a/app/i18n/hu/admin.php b/app/i18n/hu/admin.php
index d90e33613..e7a7f487e 100644
--- a/app/i18n/hu/admin.php
+++ b/app/i18n/hu/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (haladó felhasználóknak HTTPS-el)',
'none' => 'nincs (veszélyes)',
'title' => 'Hitelesítés',
- 'token' => 'Hitelesítő token',
- 'token_help' => 'Engedélyezi az alapértelmezett felhasználó RSS-ének olvasását hitelesítés nélkül:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Hitelesítési módszer',
'unsafe_autologin' => 'Engedélyezze a nem biztonságos automata bejelentkezést a következő formátummal: ',
),
diff --git a/app/i18n/hu/conf.php b/app/i18n/hu/conf.php
index cfe2b478a..1d2140610 100644
--- a/app/i18n/hu/conf.php
+++ b/app/i18n/hu/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Rendezés hírforrás szerint',
'order' => 'Rendezés dátum szerint',
'search' => 'Kifejezés',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Státusz',
'tags' => 'Rendezés címke szerint',
'type' => 'Típus',
),
'get_all' => 'Minden cikk megjelenítése',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Listáz “%s” kategóriát',
'get_favorite' => 'Kedvenc cikkek megjelenítése',
'get_feed' => 'Listáz “%s” hírforrást',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Név',
'no_filter' => 'Nincs szűrés',
'number' => 'Lekérdezés %d',
'order_asc' => 'Régebbi cikkek előre',
'order_desc' => 'Újabb cikkek előre',
'search' => 'Keresse a “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Minden cikk',
'state_1' => 'Olvasott cikkek',
'state_2' => 'Olvasatlan cikkek',
diff --git a/app/i18n/id/admin.php b/app/i18n/id/admin.php
index cb7e5c17e..5c6c20fd5 100644
--- a/app/i18n/id/admin.php
+++ b/app/i18n/id/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (untuk pengguna tingkat lanjut HTTPS)',
'none' => 'None (dangerous)', // TODO
'title' => 'Authentication', // TODO
- 'token' => 'Authentication token', // TODO
- 'token_help' => 'Memungkinkan akses ke output RSS dari pengguna default tanpa otentikasi:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Authentication method', // TODO
'unsafe_autologin' => 'Izinkan login otomatis yang tidak aman menggunakan format: ',
),
diff --git a/app/i18n/id/conf.php b/app/i18n/id/conf.php
index f780140ba..1fca94c7e 100644
--- a/app/i18n/id/conf.php
+++ b/app/i18n/id/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Display by feed', // TODO
'order' => 'Sort by date', // TODO
'search' => 'Expression', // TODO
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'State', // TODO
'tags' => 'Display by label', // TODO
'type' => 'Type', // TODO
),
'get_all' => 'Display all articles', // TODO
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Display “%s” category', // TODO
- 'get_favorite' => 'Display favorite articles',
+ 'get_favorite' => 'Display favourite articles', // TODO
'get_feed' => 'Display “%s” feed', // TODO
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Name', // TODO
'no_filter' => 'No filter', // TODO
'number' => 'Query n°%d', // TODO
'order_asc' => 'Display oldest articles first', // TODO
'order_desc' => 'Display newest articles first', // TODO
'search' => 'Search for “%s”', // TODO
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Display all articles', // TODO
'state_1' => 'Display read articles', // TODO
'state_2' => 'Display unread articles', // TODO
diff --git a/app/i18n/it/admin.php b/app/i18n/it/admin.php
index c22713fca..06deb0ec6 100644
--- a/app/i18n/it/admin.php
+++ b/app/i18n/it/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (per gli utenti avanzati con HTTPS)',
'none' => 'Nessuno (pericoloso)',
'title' => 'Autenticazione',
- 'token' => 'Token di autenticazione',
- 'token_help' => 'Consenti accesso agli RSS dell utente predefinito senza autenticazione:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Metodo di autenticazione',
'unsafe_autologin' => 'Consenti accesso automatico non sicuro usando il formato: ',
),
diff --git a/app/i18n/it/conf.php b/app/i18n/it/conf.php
index c369690be..9f91285ae 100644
--- a/app/i18n/it/conf.php
+++ b/app/i18n/it/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Mostra per feed',
'order' => 'Ordina per data',
'search' => 'Espressione',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Stato',
'tags' => 'Mostra per tag', // DIRTY
'type' => 'Tipo',
),
'get_all' => 'Mostra tutti gli articoli',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Mostra la categoria “%s” ',
'get_favorite' => 'Mostra articoli preferiti',
'get_feed' => 'Mostra feed “%s” ',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Nome',
'no_filter' => 'Nessun filtro',
'number' => 'Ricerca n°%d',
'order_asc' => 'Mostra prima gli articoli più vecchi',
'order_desc' => 'Mostra prima gli articoli più nuovi',
'search' => 'Cerca per “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Mostra tutti gli articoli',
'state_1' => 'Mostra gli articoli letti',
'state_2' => 'Mostra gli articoli non letti',
diff --git a/app/i18n/ja/admin.php b/app/i18n/ja/admin.php
index 98742e744..205aab98c 100644
--- a/app/i18n/ja/admin.php
+++ b/app/i18n/ja/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (上級者はHTTPSでも)',
'none' => 'なし (危険)',
'title' => '認証',
- 'token' => '認証トークン',
- 'token_help' => 'ユーザーが承認無しで、RSSを出力できるようにします。:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => '認証メソッド',
'unsafe_autologin' => '危険な自動ログインを有効にします',
),
diff --git a/app/i18n/ja/conf.php b/app/i18n/ja/conf.php
index 0a77b106c..2ef0739bf 100644
--- a/app/i18n/ja/conf.php
+++ b/app/i18n/ja/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'フィードごとに表示する',
'order' => '日付ごとにソートする',
'search' => '式',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => '状態',
'tags' => 'タグごとに表示する',
'type' => 'タイプ',
),
'get_all' => 'すべての著者を表示する',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => '“%s”カテゴリを表示する',
'get_favorite' => 'お気に入りの著者を表示する',
'get_feed' => '“%s”フィードを表示する',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => '名前',
'no_filter' => 'フィルターはありません',
'number' => 'クエリ n°%d',
'order_asc' => '古い著者を最初に表示する',
'order_desc' => '新しい著者を最初に表示する',
'search' => '“%s”で検索する',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'すべての記事を表示する',
'state_1' => '既読の記事を表示する',
'state_2' => '未読の記事を表示する',
diff --git a/app/i18n/ko/admin.php b/app/i18n/ko/admin.php
index 4d6dbb1db..7b9d885d5 100644
--- a/app/i18n/ko/admin.php
+++ b/app/i18n/ko/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (HTTPS를 사용하는 고급 사용자용)',
'none' => '사용하지 않음 (위험)',
'title' => '인증',
- 'token' => '인증 토큰',
- 'token_help' => '기본 사용자의 RSS에 인증 없이 접근할 수 있도록 합니다:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => '인증',
'unsafe_autologin' => '다음과 같은 안전하지 않은 방식의 로그인을 허가합니다: ',
),
diff --git a/app/i18n/ko/conf.php b/app/i18n/ko/conf.php
index e6952feeb..4753a9a45 100644
--- a/app/i18n/ko/conf.php
+++ b/app/i18n/ko/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => '피드별로 표시',
'order' => '날짜순으로 정렬',
'search' => '정규 표현식',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => '상태',
'tags' => '태그별로 표시',
'type' => '유형',
),
'get_all' => '모든 글 표시',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => '“%s” 카테고리 표시',
'get_favorite' => '즐겨찾기에 등록된 글 표시',
'get_feed' => '“%s” 피드 표시',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => '이름',
'no_filter' => '필터가 없습니다',
'number' => '쿼리 #%d',
'order_asc' => '오래된 글 먼저 표시',
'order_desc' => '최근 글 먼저 표시',
'search' => '“%s”의 검색 결과',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => '모든 글 표시',
'state_1' => '읽은 글 표시',
'state_2' => '읽지 않은 글 표시',
diff --git a/app/i18n/lv/admin.php b/app/i18n/lv/admin.php
index 91ce5a935..e13846254 100644
--- a/app/i18n/lv/admin.php
+++ b/app/i18n/lv/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (pieredzējušiem lietotājiem ar HTTPS)',
'none' => 'Nav (bīstami)',
'title' => 'Autentifikācija',
- 'token' => 'Autentifikācijas žetons',
- 'token_help' => 'Ļauj piekļūt noklusējuma lietotāja RSS izvadei bez autentifikācijas:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Autentifikācijas metode',
'unsafe_autologin' => 'Atļaut nedrošu automātisku pieteikšanos, izmantojot formātu: ',
),
diff --git a/app/i18n/lv/conf.php b/app/i18n/lv/conf.php
index 1237f1d15..46b535037 100644
--- a/app/i18n/lv/conf.php
+++ b/app/i18n/lv/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Rādīt pēc barotnes',
'order' => 'Kārtot pēc datuma',
'search' => 'Izteiksme',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Stāvoklis',
'tags' => 'Rādīt pēc birkas',
'type' => 'Veids',
),
'get_all' => 'Rādīt visus rakstus',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Rādīt kategoriju “%s”',
'get_favorite' => 'Rādīt mīļākos rakstus',
'get_feed' => 'Rādīt barotni “%s”',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Vārds',
'no_filter' => 'Bez filtra',
'number' => 'Pieprasījums nr. %d',
'order_asc' => 'Vispirms rādīt vecākos rakstus',
'order_desc' => 'Vispirms rādīt jaunākos rakstus',
'search' => 'Meklēt “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Rādīt visus rakstus',
'state_1' => 'Rādīt lasītos rakstus',
'state_2' => 'Rādīt nelasītos rakstus',
diff --git a/app/i18n/nl/admin.php b/app/i18n/nl/admin.php
index 2ce4f19ad..250c7ca4c 100644
--- a/app/i18n/nl/admin.php
+++ b/app/i18n/nl/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (voor gevorderde gebruikers met HTTPS)',
'none' => 'Geen (gevaarlijk)',
'title' => 'Authenticatie',
- 'token' => 'Authenticatie teken',
- 'token_help' => 'Sta toegang toe tot de RSS uitvoer van de standaard gebruiker zonder authenticatie:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Authenticatie methode',
'unsafe_autologin' => 'Sta onveilige automatische log in toe met het volgende formaat: ',
),
diff --git a/app/i18n/nl/conf.php b/app/i18n/nl/conf.php
index b164a8361..7fe1a7940 100644
--- a/app/i18n/nl/conf.php
+++ b/app/i18n/nl/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Weergeven op feed',
'order' => 'Sorteren op datum',
'search' => 'Expressie',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Status',
'tags' => 'Weergeven op label',
'type' => 'Type', // IGNORE
),
'get_all' => 'Toon alle artikelen',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Toon „%s” categorie',
'get_favorite' => 'Toon favoriete artikelen',
'get_feed' => 'Toon „%s” feed',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Naam',
'no_filter' => 'Geen filter',
'number' => 'Query n°%d', // IGNORE
'order_asc' => 'Toon oudste artikelen eerst',
'order_desc' => 'Toon nieuwste artikelen eerst',
'search' => 'Zoek naar „%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Toon alle artikelen',
'state_1' => 'Toon gelezen artikelen',
'state_2' => 'Toon ongelezen artikelen',
diff --git a/app/i18n/oc/admin.php b/app/i18n/oc/admin.php
index 358e8a054..a98acbd58 100644
--- a/app/i18n/oc/admin.php
+++ b/app/i18n/oc/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (per utilizaires avançats amb HTTPS)',
'none' => 'Cap (perilhós)',
'title' => 'Autentificacion',
- 'token' => 'Geton d’autentificacion',
- 'token_help' => 'Permetre l’accès a la sortida RSS de l’utilizaire per defaut sens cap d’autentificacion :',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Mòde d’autentification',
'unsafe_autologin' => 'Autorizar las connexions automaticas pas seguras al format : ',
),
diff --git a/app/i18n/oc/conf.php b/app/i18n/oc/conf.php
index 159b46e7d..a54bb6f46 100644
--- a/app/i18n/oc/conf.php
+++ b/app/i18n/oc/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Afichatge per flux',
'order' => 'Triar per data',
'search' => 'Expression', // IGNORE
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Estat',
'tags' => 'Afichatge per etiqueta',
'type' => 'Tipe',
),
'get_all' => 'Mostrar totes los articles',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Mostrar la categoria « %s »',
'get_favorite' => 'Mostrar los articles favorits',
'get_feed' => 'Mostrar lo flux « %s »',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Nom',
'no_filter' => 'Cap de filtre aplicat',
'number' => 'Filtre n°%d',
'order_asc' => 'Mostrar los articles mai ancians en primièr',
'order_desc' => 'Mostrar los articles mai recents en primièr',
'search' => 'Recèrca de « %s »',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Mostrar totes los articles',
'state_1' => 'Mostrar los articles pas legits',
'state_2' => 'Mostrar los articles pas legits',
diff --git a/app/i18n/pl/admin.php b/app/i18n/pl/admin.php
index 695e04010..352728e42 100644
--- a/app/i18n/pl/admin.php
+++ b/app/i18n/pl/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (dla zaawansowanych użytkowników, z wykorzystaniem HTTPS)',
'none' => 'Brak (niebezpieczna)',
'title' => 'Uwierzytelnianie',
- 'token' => 'Token uwierzytelniania',
- 'token_help' => 'Pozwala na dostęp do treści RSS domyślnego użytkownika bez uwierzytelnienia:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Metoda uwierzytelniania',
'unsafe_autologin' => 'Pozwól na niebezpieczne automatyczne logowanie następującym schematem: -> todo',
),
diff --git a/app/i18n/pl/conf.php b/app/i18n/pl/conf.php
index ee1c2b21e..a11f719ad 100644
--- a/app/i18n/pl/conf.php
+++ b/app/i18n/pl/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Według kanału',
'order' => 'Sortowanie wg daty',
'search' => 'Wyrażenie',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Stan',
'tags' => 'Według tagu',
'type' => 'Rodzaj',
),
'get_all' => 'Wyświetlenie wszystkich wiadomości',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Wyświetlenie kategorii “%s”',
'get_favorite' => 'Wyświetlenie ulubionych wiadomości',
'get_feed' => 'Wyświetlenie kanału “%s”',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Nazwa',
'no_filter' => 'Brak filtrów',
'number' => 'Zapytanie nr %d',
'order_asc' => 'Wyświetl najpierw najstarsze wiadomości',
'order_desc' => 'Wyświetl najpierw najnowsze wiadomości',
'search' => 'Szukaj “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Wyświetl wszystkie wiadomości',
'state_1' => 'Wyświetl przeczytane wiadomości',
'state_2' => 'Wyświetl nieprzeczytane wiadomości',
diff --git a/app/i18n/pt-br/admin.php b/app/i18n/pt-br/admin.php
index b4642a886..8aacd6f17 100644
--- a/app/i18n/pt-br/admin.php
+++ b/app/i18n/pt-br/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (Para usuários avançados com HTTPS)',
'none' => 'Nenhum (Perigoso)',
'title' => 'Autenticação',
- 'token' => 'Token de autenticação ',
- 'token_help' => 'Permitir acesso a saída RSS para o usuário padrão sem autenticação',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Método de autenticação',
'unsafe_autologin' => 'Permitir login automática insegura usando o seguinte formato: ',
),
diff --git a/app/i18n/pt-br/conf.php b/app/i18n/pt-br/conf.php
index 2af785f0d..8fa69ddf3 100644
--- a/app/i18n/pt-br/conf.php
+++ b/app/i18n/pt-br/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Exibir por feed',
'order' => 'Ordenar por data',
'search' => 'Expressão',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Estado',
'tags' => 'Exibir por tag', // DIRTY
'type' => 'Tipo',
),
'get_all' => 'Mostrar todos os artigos',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Visualizar “%s” categoria',
'get_favorite' => 'Visualizar artigos favoritos',
'get_feed' => 'Visualizar “%s” feed',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Nome',
'no_filter' => 'Sem filtro',
'number' => 'Query n°%d', // IGNORE
'order_asc' => 'Exibir artigos mais antigos primeiro',
'order_desc' => 'Exibir artigos mais novos primeiro',
'search' => 'Busca por “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Exibir todos os artigos',
'state_1' => 'Exibir artigos lidos',
'state_2' => 'Exibir artigos não lidos',
diff --git a/app/i18n/ru/admin.php b/app/i18n/ru/admin.php
index 399ecf250..10ff58ed4 100644
--- a/app/i18n/ru/admin.php
+++ b/app/i18n/ru/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (для опытных пользователей с HTTPS)',
'none' => 'Без аутентификации (небезопасно)',
'title' => 'Аутентификации',
- 'token' => 'Токен аутентификации',
- 'token_help' => 'Разрешает доступ к RSS-лентам пользователя по умолчанию без аутентификации:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Способ аутентификации',
'unsafe_autologin' => 'Разрешить небезопасный автоматический вход с использованием следующего формата: ',
),
diff --git a/app/i18n/ru/conf.php b/app/i18n/ru/conf.php
index 5e3fdbd23..057e38e4c 100644
--- a/app/i18n/ru/conf.php
+++ b/app/i18n/ru/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Отображение по ленте',
'order' => 'Сортировать по дате',
'search' => 'Выражение',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Состояние',
'tags' => 'Отображение по метке',
'type' => 'Тип',
),
'get_all' => 'Показать все статьи',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Показать категорию “%s”',
'get_favorite' => 'Показать избранные статьи',
'get_feed' => 'Показать ленту “%s”',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Название',
'no_filter' => 'Нет фильтров',
'number' => 'Запрос №%d',
'order_asc' => 'Показывать сначала старые статьи',
'order_desc' => 'Показывать сначала новые статьи',
'search' => 'Искать “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Показать все статьи',
'state_1' => 'Показать прочитанные статьи',
'state_2' => 'Показать непрочитанные статьи',
diff --git a/app/i18n/sk/admin.php b/app/i18n/sk/admin.php
index ca42ae032..1490694b1 100644
--- a/app/i18n/sk/admin.php
+++ b/app/i18n/sk/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (pre pokročilých používateľov s HTTPS)',
'none' => 'Žiadny (nebezpečné)',
'title' => 'Prihlásenie',
- 'token' => 'Token prihlásenia',
- 'token_help' => 'Povoliť prístup k výstupu RSS prednastaveného používateľa bez prihlásenia:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Spôsob prihlásenia',
'unsafe_autologin' => 'Povoliť nebezpečné automatické prihlásenie pomocou webového formulára: ',
),
diff --git a/app/i18n/sk/conf.php b/app/i18n/sk/conf.php
index 0f01d09b8..83a168186 100644
--- a/app/i18n/sk/conf.php
+++ b/app/i18n/sk/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Zobraziť podľa kanála',
'order' => 'Zobraziť podľa dátumu',
'search' => 'Výraz',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Štát',
'tags' => 'Zobraziť podľa štítku',
'type' => 'Typ',
),
'get_all' => 'Zobraziť všetky články',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Zobraziť kategóriu “%s”',
'get_favorite' => 'Zobraziť obľúbené články',
'get_feed' => 'Zobraziť kanál “%s”',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Meno',
'no_filter' => 'Žiadny filter',
'number' => 'Dopyt číslo %d',
'order_asc' => 'Zobraziť staršie články hore',
'order_desc' => 'Zobraziť novšie články hore',
'search' => 'Vyhľadáva sa: “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Zobraziť všetky články',
'state_1' => 'Zobraziť prečítané články',
'state_2' => 'Zobraziť neprečítané články',
diff --git a/app/i18n/tr/admin.php b/app/i18n/tr/admin.php
index 8b34c551e..fb6903f7b 100644
--- a/app/i18n/tr/admin.php
+++ b/app/i18n/tr/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (ileri kullanıcılar için, HTTPS)',
'none' => 'Hiçbiri (tehlikeli)',
'title' => 'Kimlik doğrulama',
- 'token' => 'Kimlik doğrulama işareti',
- 'token_help' => 'Kimlik doğrulama olmaksızın öntanımlı kullanıcının RSS çıktısına erişime izin ver:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => 'Kimlik doğrulama yöntemi',
'unsafe_autologin' => 'Güvensiz otomatik girişe izin ver: ',
),
diff --git a/app/i18n/tr/conf.php b/app/i18n/tr/conf.php
index 4cebac293..c67d7750e 100644
--- a/app/i18n/tr/conf.php
+++ b/app/i18n/tr/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => 'Akışa göre göster',
'order' => 'Tarihe göre göster',
'search' => 'İfade',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Durum',
'tags' => 'Etikete göre göster',
'type' => 'Tür',
),
'get_all' => 'Tüm makaleleri göster',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => '“%s” kategorisini göster',
'get_favorite' => 'Favori makaleleri göster',
'get_feed' => '“%s” akışını göster',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'İsim',
'no_filter' => 'Filtre yok',
'number' => 'Sorgu n°%d',
'order_asc' => 'Önce eski makaleleri göster',
'order_desc' => 'Önce yeni makaleleri göster',
'search' => '“%s” için arama',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => 'Tüm makaleleri göster',
'state_1' => 'Okunmuş makaleleri göster',
'state_2' => 'Okunmamış makaleleri göster',
diff --git a/app/i18n/zh-cn/admin.php b/app/i18n/zh-cn/admin.php
index 4af1bde9e..709abfeca 100644
--- a/app/i18n/zh-cn/admin.php
+++ b/app/i18n/zh-cn/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP(面向启用 HTTPS 的高级用户)',
'none' => '无(危险)',
'title' => '认证',
- 'token' => '认证口令',
- 'token_help' => '用于不经认证访问默认用户的 RSS 输出:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => '认证方式',
'unsafe_autologin' => '允许不安全的自动登陆方式:',
),
diff --git a/app/i18n/zh-cn/conf.php b/app/i18n/zh-cn/conf.php
index a216b10a3..30d790170 100644
--- a/app/i18n/zh-cn/conf.php
+++ b/app/i18n/zh-cn/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => '按订阅源显示',
'order' => '按日期排序',
'search' => '表达式',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => '状态',
'tags' => '按标签显示',
'type' => '类型',
),
'get_all' => '显示所有文章',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => '显示分类 “%s”',
'get_favorite' => '显示收藏文章',
'get_feed' => '显示订阅源 “%s”',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => '名称',
'no_filter' => '无过滤器',
'number' => '查询 n°%d',
'order_asc' => '由旧至新显示文章',
'order_desc' => '由新至旧显示文章',
'search' => '搜索 “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => '显示所有文章',
'state_1' => '显示已读文章',
'state_2' => '显示未读文章',
diff --git a/app/i18n/zh-tw/admin.php b/app/i18n/zh-tw/admin.php
index f41634927..b6529d53c 100644
--- a/app/i18n/zh-tw/admin.php
+++ b/app/i18n/zh-tw/admin.php
@@ -19,8 +19,8 @@ return array(
'http' => 'HTTP(面向啟用 HTTPS 的高級用戶)',
'none' => '無認證(危險)',
'title' => '認證',
- 'token' => '認證口令',
- 'token_help' => '用於不經認證訪問預設使用者的 RSS 輸出:',
+ 'token' => 'Master authentication token', // TODO
+ 'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'type' => '認證方式',
'unsafe_autologin' => '允許不安全的自動登入方式:',
),
diff --git a/app/i18n/zh-tw/conf.php b/app/i18n/zh-tw/conf.php
index 16e0cc5ed..b6b008dac 100644
--- a/app/i18n/zh-tw/conf.php
+++ b/app/i18n/zh-tw/conf.php
@@ -120,20 +120,33 @@ return array(
'feeds' => '按訂閱源顯示',
'order' => '按日期排序',
'search' => '表達式',
+ 'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
+ 'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => '狀態',
'tags' => '按標簽顯示',
'type' => '類型',
),
'get_all' => '顯示所有文章',
+ 'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => '顯示分類 “%s”',
'get_favorite' => '顯示收藏文章',
'get_feed' => '顯示訂閱源 “%s”',
+ 'get_important' => 'Display articles from important feeds', // TODO
+ 'get_label' => 'Display articles with “%s” label', // TODO
+ 'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => '名稱',
'no_filter' => '無過濾器',
'number' => '查詢 n°%d',
'order_asc' => '由舊至新顯示文章',
'order_desc' => '由新至舊顯示文章',
'search' => '搜尋 “%s”',
+ 'share' => array(
+ '_' => 'Share this query by link', // TODO
+ 'help' => 'Give this link if you want to share this query with anyone', // TODO
+ 'html' => 'Shareable link to the HTML page', // TODO
+ 'opml' => 'Shareable link to the OPML list of feeds', // TODO
+ 'rss' => 'Shareable link to the RSS feed', // TODO
+ ),
'state_0' => '顯示所有文章',
'state_1' => '顯示已讀文章',
'state_2' => '顯示未讀文章',
diff --git a/app/layout/header.phtml b/app/layout/header.phtml
index 18e67fd2d..9ab0da4c4 100644
--- a/app/layout/header.phtml
+++ b/app/layout/header.phtml
@@ -1,9 +1,10 @@
<?php
declare(strict_types=1);
+ /** @var FreshRSS_View $this */
?>
<header class="header">
<div class="item title">
- <a href="<?= _url('index', 'index') ?>">
+ <a href="<?= Minz_Url::display(['c' => 'index', 'a' => 'index'], 'html', 'root') ?>">
<?php if (FreshRSS_Context::systemConf()->logo_html == '') { ?>
<img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" loading="lazy" />
<?php
@@ -16,32 +17,29 @@
<div class="item search">
<?php if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::systemConf()->allow_anonymous) { ?>
- <form action="<?= _url('index', 'index') ?>" method="get">
+ <form action="<?= $this->html_url ?>" method="get">
<div class="stick">
+ <?php if (Minz_Request::controllerName() === 'index'): ?>
+ <?php if (in_array(Minz_Request::actionName(), ['normal', 'global', 'reader'], true)) { ?>
+ <input type="hidden" name="a" value="<?= Minz_Request::actionName() ?>" />
+ <?php } if (Minz_Request::paramString('get') !== '') { ?>
+ <input type="hidden" name="get" value="<?= FreshRSS_Context::currentGet() ?>" />
+ <?php } if (Minz_Request::paramInt('state') !== 0) { ?>
+ <input type="hidden" name="state" value="<?= Minz_Request::paramInt('state') ?>" />
+ <?php } ?>
+ <?php endif; ?>
+ <?php if (Minz_Request::paramString('user') !== '') { ?>
+ <input type="hidden" name="user" value="<?= Minz_User::name() ?>" />
+ <?php } if (ctype_alnum(Minz_Request::paramString('t'))) { ?>
+ <input type="hidden" name="t" value="<?= Minz_Request::paramString('t') ?>" />
+ <?php } if (ctype_upper(Minz_Request::paramString('order'))) { ?>
+ <input type="hidden" name="order" value="<?= FreshRSS_Context::$order ?>" />
+ <?php } if (ctype_lower(Minz_Request::paramString('f'))) { ?>
+ <input type="hidden" name="f" value="<?= Minz_Request::paramString('f') ?>" />
+ <?php } ?>
<input type="search" name="search" id="search"
- value="<?= htmlspecialchars(htmlspecialchars_decode(FreshRSS_Context::$search->getRawInput(), ENT_QUOTES), ENT_COMPAT, 'UTF-8') ?>"
+ value="<?= htmlspecialchars(htmlspecialchars_decode(Minz_Request::paramString('search'), ENT_QUOTES), ENT_COMPAT, 'UTF-8') ?>"
placeholder="<?= _t('gen.menu.search') ?>" />
-
- <?php $param_a = Minz_Request::actionName(); ?>
- <?php if (in_array($param_a, ['normal', 'global', 'reader'], true)) { ?>
- <input type="hidden" name="a" value="<?= $param_a ?>" />
- <?php } ?>
-
- <?php $get = Minz_Request::paramString('get'); ?>
- <?php if ($get !== '') { ?>
- <input type="hidden" name="get" value="<?= $get ?>" />
- <?php } ?>
-
- <?php $order = Minz_Request::paramString('order'); ?>
- <?php if ($order !== '') { ?>
- <input type="hidden" name="order" value="<?= $order ?>" />
- <?php } ?>
-
- <?php $state = Minz_Request::paramString('state'); ?>
- <?php if ($state !== '') { ?>
- <input type="hidden" name="state" value="<?= $state ?>" />
- <?php } ?>
-
<button class="btn" type="submit"><?= _i('search') ?></button>
</div>
</form>
@@ -120,7 +118,7 @@
</nav>
<?php } elseif (FreshRSS_Auth::accessNeedsAction()) { ?>
<div class="item configure">
- <a class="signin" href="<?= _url('auth', 'login') ?>"><?= _i('login') ?><?= _t('gen.auth.login') ?></a>
+ <a class="signin" href="<?= Minz_Url::display(['c' => 'auth', 'a' => 'login'], 'html', 'root') ?>"><?= _i('login') ?><?= _t('gen.auth.login') ?></a>
</div>
<?php } ?>
</header>
diff --git a/app/layout/layout.phtml b/app/layout/layout.phtml
index ba6dc4a96..2183f9804 100644
--- a/app/layout/layout.phtml
+++ b/app/layout/layout.phtml
@@ -2,15 +2,17 @@
declare(strict_types=1);
/** @var FreshRSS_View $this */
FreshRSS::preLayout();
+ $class = '';
+ if (_t('gen.dir') === 'rtl') {
+ echo ' dir="rtl"';
+ $class = 'rtl ';
+ }
+ if (FreshRSS_Context::userConf()->darkMode !== 'no') {
+ $class .= 'darkMode_' . FreshRSS_Context::userConf()->darkMode;
+ }
?>
<!DOCTYPE html>
-<html lang="<?= FreshRSS_Context::userConf()->language ?>" xml:lang="<?= FreshRSS_Context::userConf()->language ?>"<?php
-$class = '';
-if (_t('gen.dir') === 'rtl') {
- echo ' dir="rtl"';
- $class = 'rtl ';
-}
-?> class="<?= $class ?><?= (FreshRSS_Context::userConf()->darkMode === 'no') ? '' : 'darkMode_' . FreshRSS_Context::userConf()->darkMode ?>">
+<html lang="<?= FreshRSS_Context::userConf()->language ?>" xml:lang="<?= FreshRSS_Context::userConf()->language ?>" class="<?= $class ?>">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
diff --git a/app/layout/nav_menu.phtml b/app/layout/nav_menu.phtml
index 3d0027f17..f8b687f74 100644
--- a/app/layout/nav_menu.phtml
+++ b/app/layout/nav_menu.phtml
@@ -41,26 +41,15 @@
<li class="item">
<span>
<form action="<?= _url('index', 'index') ?>" method="get">
- <?php $param_a = Minz_Request::actionName(); ?>
- <?php if (in_array($param_a, ['normal', 'global', 'reader'], true)) { ?>
- <input type="hidden" name="a" value="<?= $param_a ?>" />
+ <?php if (in_array(Minz_Request::actionName(), ['normal', 'global', 'reader'], true)) { ?>
+ <input type="hidden" name="a" value="<?= Minz_Request::actionName() ?>" />
+ <?php } if (Minz_Request::paramString('get') !== '') { ?>
+ <input type="hidden" name="get" value="<?= FreshRSS_Context::currentGet() ?>" />
+ <?php } if (ctype_upper(Minz_Request::paramString('order'))) { ?>
+ <input type="hidden" name="order" value="<?= FreshRSS_Context::$order ?>" />
+ <?php } if (Minz_Request::paramInt('state') !== 0) { ?>
+ <input type="hidden" name="state" value="<?= FreshRSS_Context::$state ?>" />
<?php } ?>
-
- <?php $get = Minz_Request::paramString('get'); ?>
- <?php if ($get !== '') { ?>
- <input type="hidden" name="get" value="<?= $get ?>" />
- <?php } ?>
-
- <?php $order = Minz_Request::paramString('order'); ?>
- <?php if ($order !== '') { ?>
- <input type="hidden" name="order" value="<?= $order ?>" />
- <?php } ?>
-
- <?php $state = Minz_Request::paramString('state'); ?>
- <?php if ($state !== '') { ?>
- <input type="hidden" name="state" value="<?= $state ?>" />
- <?php } ?>
-
<div class="stick search">
<input type="search" name="search"
value="<?= htmlspecialchars(htmlspecialchars_decode(FreshRSS_Context::$search->getRawInput(), ENT_QUOTES), ENT_COMPAT, 'UTF-8'); ?>"
@@ -89,7 +78,7 @@
<?php if (!empty($raw_query['url'])): ?>
<a href="<?= $raw_query['url'] ?>"><?= $raw_query['name'] ?? $raw_query['url'] ?></a>
<?php else: ?>
- <?php $query = new FreshRSS_UserQuery($raw_query); ?>
+ <?php $query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels()); ?>
<a href="<?= $query->getUrl() ?>"><?= $query->getName() ?></a>
<?php endif; ?>
</li>
@@ -210,20 +199,6 @@
<?php
}
?>
-
- <?php
- $url_output['a'] = 'rss';
- if (FreshRSS_Context::userConf()->token) {
- $url_output['params']['user'] = Minz_User::name();
- $url_output['params']['token'] = FreshRSS_Context::userConf()->token;
- }
- if (FreshRSS_Context::userConf()->since_hours_posts_per_rss) {
- $url_output['params']['hours'] = FreshRSS_Context::userConf()->since_hours_posts_per_rss;
- }
- ?>
- <a class="view-rss btn" target="_blank" rel="noreferrer" title="<?= _t('index.menu.rss_view') ?>" href="<?= Minz_Url::display($url_output) ?>">
- <?= _i('rss') ?>
- </a>
</div>
<?php $nav_menu_hooks = Minz_ExtensionManager::callHookString('nav_menu'); ?>
diff --git a/app/layout/simple.phtml b/app/layout/simple.phtml
index 065b69fb9..e460e283b 100644
--- a/app/layout/simple.phtml
+++ b/app/layout/simple.phtml
@@ -2,17 +2,27 @@
declare(strict_types=1);
/** @var FreshRSS_View $this */
FreshRSS::preLayout();
+ $class = '';
+ if (_t('gen.dir') === 'rtl') {
+ echo ' dir="rtl"';
+ $class = 'rtl ';
+ }
+ if (FreshRSS_Context::userConf()->darkMode !== 'no') {
+ $class .= 'darkMode_' . FreshRSS_Context::userConf()->darkMode;
+ }
?>
<!DOCTYPE html>
-<html lang="<?= FreshRSS_Context::userConf()->language ?>" xml:lang="<?= FreshRSS_Context::userConf()->language ?>">
+<html lang="<?= FreshRSS_Context::userConf()->language ?>" xml:lang="<?= FreshRSS_Context::userConf()->language ?>" class="<?= $class ?>">
<head>
<meta charset="UTF-8" />
- <meta name="viewport" content="initial-scale=1.0" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
+ <?= FreshRSS_View::metaThemeColor() ?>
<?= FreshRSS_View::headStyle() ?>
<script id="jsonVars" type="application/json">
<?php $this->renderHelper('javascript_vars'); ?>
</script>
<?= FreshRSS_View::headScript() ?>
+ <link rel="manifest" href="<?= Minz_Url::display('/themes/manifest.json') ?>" />
<link rel="shortcut icon" id="favicon" type="image/x-icon" sizes="16x16 64x64" href="<?= Minz_Url::display('/favicon.ico') ?>" />
<link rel="icon msapplication-TileImage apple-touch-icon" type="image/png" sizes="256x256" href="<?= Minz_Url::display('/themes/icons/favicon-256.png') ?>" />
<link rel="apple-touch-icon" href="<?= Minz_Url::display('/themes/icons/apple-touch-icon.png') ?>" />
@@ -20,9 +30,15 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="<?= FreshRSS_Context::systemConf()->title ?>">
<meta name="msapplication-TileColor" content="#FFF" />
+ <meta name="theme-color" content="#FFF" />
+<?php if (!FreshRSS_Context::systemConf()->allow_referrer) { ?>
<meta name="referrer" content="never" />
- <meta name="robots" content="noindex,nofollow" />
+<?php } ?>
<?= FreshRSS_View::headTitle() ?>
+ <?php if ($this->rss_url != ''): ?>
+ <link rel="alternate" type="application/rss+xml" title="<?= $this->rss_title ?>" href="<?= $this->rss_url ?>" />
+ <?php endif; ?>
+ <meta name="robots" content="noindex,nofollow" />
</head>
<body>
@@ -30,7 +46,7 @@
<div class="app-layout app-layout-simple">
<div class="header">
<div class="item title">
- <a href="<?= _url('index', 'index') ?>">
+ <a href="<?= Minz_Url::display(['c' => 'index', 'a' => 'index'], 'html', 'root') ?>">
<?php if (FreshRSS_Context::systemConf()->logo_html == '') { ?>
<img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" loading="lazy" />
<?php
@@ -43,14 +59,20 @@
<div class="item"></div>
- <div class="item">
- <?php if (FreshRSS_Auth::accessNeedsAction()) { ?>
- <a class="signout" href="<?= _url('auth', 'logout') ?>">
- <?= _i('logout') . _t('gen.auth.logout') ?>
+ <?php if (FreshRSS_Auth::accessNeedsAction()): ?>
+ <div class="item configure">
+ <?php if (FreshRSS_Auth::hasAccess()): ?>
+ <a class="signout" href="<?= Minz_Url::display(['c' => 'auth', 'a' => 'logout'], 'html', 'root') ?>">
+ <?= _i('logout') ?><?= _t('gen.auth.logout') ?>
(<?= htmlspecialchars(Minz_User::name() ?? '', ENT_NOQUOTES, 'UTF-8') ?>)
</a>
- <?php } ?>
- </div>
+ <?php else: ?>
+ <a class="signin" href="<?= Minz_Url::display(['c' => 'auth', 'a' => 'login'], 'html', 'root') ?>">
+ <?= _i('login') ?><?= _t('gen.auth.login') ?>
+ </a>
+ <?php endif; ?>
+ </div>
+ <?php endif; ?>
</div>
<?php $this->render(); ?>
diff --git a/app/views/configure/queries.phtml b/app/views/configure/queries.phtml
index 2a55eb1b2..26534307e 100644
--- a/app/views/configure/queries.phtml
+++ b/app/views/configure/queries.phtml
@@ -18,6 +18,9 @@
<div class="box-title">
<a class="configure open-slider" href="<?= _url('configure', 'query', 'id', '' . $key) ?>"><?= _i('configure') ?></a><h2><?= $query->getName() ?></h2>
<input type="hidden" id="queries_<?= $key ?>_name" name="queries[<?= $key ?>][name]" value="<?= $query->getName() ?>"/>
+ <input type="hidden" id="queries_<?= $key ?>_token" name="queries[<?= $key ?>][token]" value="<?= $query->getToken() ?>"/>
+ <input type="hidden" id="queries_<?= $key ?>_shareRss" name="queries[<?= $key ?>][token]" value="<?= $query->shareRss() ?>"/>
+ <input type="hidden" id="queries_<?= $key ?>_shareOpml" name="queries[<?= $key ?>][token]" value="<?= $query->shareOpml() ?>"/>
<input type="hidden" id="queries_<?= $key ?>_url" name="queries[<?= $key ?>][url]" value="<?= $query->getUrl() ?>"/>
<input type="hidden" id="queries_<?= $key ?>_search" name="queries[<?= $key ?>][search]" value="<?= urlencode($query->getSearch()->getRawInput()) ?>"/>
<input type="hidden" id="queries_<?= $key ?>_state" name="queries[<?= $key ?>][state]" value="<?= $query->getState() ?>"/>
diff --git a/app/views/helpers/configure/query.phtml b/app/views/helpers/configure/query.phtml
index 145425271..49ffbad87 100644
--- a/app/views/helpers/configure/query.phtml
+++ b/app/views/helpers/configure/query.phtml
@@ -7,7 +7,6 @@
?>
<div class="post">
<h2><?= $this->query->getName() ?></h2>
-
<div>
<a href="<?= $this->query->getUrl() ?>"><?= _i('link') ?> <?= _t('gen.action.filter') ?></a>
</div>
@@ -18,15 +17,53 @@
<div class="form-group">
<label class="group-name" for="name"><?= _t('conf.query.name') ?></label>
<div class="group-controls">
- <input type="text" name="name" id="name" value="<?= $this->query->getName() ?>" />
+ <input type="text" name="name" id="name" value="<?= $this->query->getName() ?>" />
+ <input type="hidden" name="query[token]" id="query_token" value="<?= $this->query->getToken() ?>" />
</div>
</div>
- <legend><?= _t('conf.query.filter') ?></legend>
+ <legend><?= _t('conf.query.share') ?></legend>
+ <div class="form-group">
+ <div class="group-controls">
+ <label class="checkbox" for="shareRss">
+ <input type="checkbox" name="query[shareRss]" id="shareRss" value="1" <?= $this->query->shareRss() ? 'checked="checked"' : ''?> />
+ <?= _t('conf.query.filter.shareRss') ?>
+ </label>
+ <?php if ($this->query->sharedUrlRss() !== ''): ?>
+ <ul>
+ <li><a href="<?= $this->query->sharedUrlHtml() ?>"><?= _i('link') ?> <?= _t('conf.query.share.html') ?></a></li>
+ <li><a href="<?= $this->query->sharedUrlRss() ?>"><?= _i('link') ?> <?= _t('conf.query.share.rss') ?></a></li>
+ </ul>
+ <?php endif; ?>
+ </div>
+ <div class="group-controls">
+ <label class="checkbox" for="shareOpml">
+ <input type="checkbox" name="query[shareOpml]" id="shareOpml" value="1" <?= $this->query->shareOpml() && $this->query->safeForOpml() ? 'checked="checked"' : '' ?>
+ <?= $this->query->safeForOpml() ? '' : 'disabled="disabled"' ?> />
+ <?= _t('conf.query.filter.shareOpml') ?>
+ </label>
+ <?php if ($this->query->sharedUrlOpml() !== ''): ?>
+ <ul>
+ <li><a href="<?= $this->query->sharedUrlOpml() ?>"><?= _i('link') ?> <?= _t('conf.query.share.opml') ?></a></li>
+ </ul>
+ <?php endif; ?>
+ </div>
+ <p class="help"><?= _i('help') ?> <?= _t('conf.query.share.help') ?></a></p>
+ <p class="help"><?= _i('help') ?> <?= _t('conf.query.help') ?></a></p>
+ </div>
+
+ <div class="form-group form-actions">
+ <div class="group-controls">
+ <button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
+ </div>
+ </div>
+
+ <legend><?= _t('conf.query.filter') ?></legend>
<div class="form-group">
<label class="group-name" for=""><?= _t('conf.query.filter.search') ?></label>
<div class="group-controls">
<input type="text" id="query_search" name="query[search]" value="<?= htmlspecialchars($this->query->getSearch()->getRawInput(), ENT_COMPAT, 'UTF-8') ?>"/>
+ <p class="help"><?= _i('help') ?> <?= _t('gen.menu.search_help') ?></a></p>
</div>
</div>
<div class="form-group">
@@ -58,22 +95,24 @@
<label class="group-name" for="query_get"><?= _t('conf.query.filter.type') ?></label>
<div class="group-controls">
<select name="query[get]" id="query_get" size="10">
- <option value=""></option>
- <option value="s" <?= 's' === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= _t('conf.query.get_favorite') ?></option>
+ <option value="a" <?= in_array($this->query->getGet(), ['', 'a'], true) ? 'selected="selected"' : '' ?>><?= _t('index.feed.title') ?></option>
+ <option value="i" <?= 'i' === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= _t('index.menu.important') ?></option>
+ <option value="s" <?= 's' === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= _t('index.feed.title_fav') ?></option>
+ <option value="T" <?= 'T' === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= _t('index.menu.tags') ?></option>
+ <optgroup label="<?= _t('conf.query.filter.tags') ?>">
+ <?php foreach ($this->tags as $tag): ?>
+ <option value="t_<?= $tag->id() ?>" <?= "t_{$tag->id()}" === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= $tag->name() ?></option>
+ <?php endforeach?>
+ </optgroup>
<optgroup label="<?= _t('conf.query.filter.categories') ?>">
<?php foreach ($this->categories as $category): ?>
<option value="c_<?= $category->id() ?>" <?= "c_{$category->id()}" === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= $category->name() ?></option>
<?php endforeach?>
</optgroup>
<optgroup label="<?= _t('conf.query.filter.feeds') ?>">
- <?php foreach ($this->feeds as $feed): ?>
- <option value="f_<?= $feed->id() ?>" <?= "f_{$feed->id()}" === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= $feed->name() ?></option>
- <?php endforeach?>
- </optgroup>
- <optgroup label="<?= _t('conf.query.filter.tags') ?>">
- <?php foreach ($this->tags as $tag): ?>
- <option value="t_<?= $tag->id() ?>" <?= "t_{$tag->id()}" === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= $tag->name() ?></option>
- <?php endforeach?>
+ <?php foreach ($this->feeds as $feed): ?>
+ <option value="f_<?= $feed->id() ?>" <?= "f_{$feed->id()}" === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= $feed->name() ?></option>
+ <?php endforeach?>
</optgroup>
</select>
</div>
@@ -83,8 +122,8 @@
<div class="group-controls">
<select name="query[order]" id="query_order">
<option value=""></option>
- <option value="ASC" <?= 'ASC' === $this->query->getOrder() ? 'selected="selected"' : '' ?>><?= _t('conf.query.order_asc') ?></option>
<option value="DESC" <?= 'DESC' === $this->query->getOrder() ? 'selected="selected"' : '' ?>><?= _t('conf.query.order_desc') ?></option>
+ <option value="ASC" <?= 'ASC' === $this->query->getOrder() ? 'selected="selected"' : '' ?>><?= _t('conf.query.order_asc') ?></option>
</select>
</div>
</div>
diff --git a/app/views/helpers/export/articles.phtml b/app/views/helpers/export/articles.phtml
index 6903c3c69..40390d832 100644
--- a/app/views/helpers/export/articles.phtml
+++ b/app/views/helpers/export/articles.phtml
@@ -24,7 +24,7 @@ foreach ($this->entries as $entry) {
continue;
}
- $feed = $this->feed ?? FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feedId());
+ $feed = $this->feed ?? FreshRSS_Category::findFeed($this->categories, $entry->feedId());
$entry->_feed($feed);
$article = $entry->toGReader('freshrss', $this->entryIdsTagNames['e_' . $entry->id()] ?? []);
diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml
index 13a751c09..9d3aa59fa 100644
--- a/app/views/helpers/feed/update.phtml
+++ b/app/views/helpers/feed/update.phtml
@@ -1,9 +1,6 @@
<?php
declare(strict_types=1);
/** @var FreshRSS_View $this */
- if ($this->feed === null) {
- throw new FreshRSS_Context_Exception('Feed not initialised!');
- }
?>
<div class="post" id="feed_update">
<h1><?= $this->feed->name() ?></h1>
diff --git a/app/views/helpers/htmlPagination.phtml b/app/views/helpers/htmlPagination.phtml
new file mode 100644
index 000000000..d1f895425
--- /dev/null
+++ b/app/views/helpers/htmlPagination.phtml
@@ -0,0 +1,21 @@
+<?php
+ declare(strict_types=1);
+ /** @var FreshRSS_View $this */
+?>
+<nav class="nav-pagination nav-list">
+ <ul class="pagination">
+ <?php if (FreshRSS_Context::$offset > 0): ?>
+ <li class="item pager-first">
+ <a href="<?= $this->userQuery->sharedUrlHtml() . '&nb=' . FreshRSS_Context::$number ?>">« <?= _t('conf.logs.pagination.first') ?></a>
+ </li>
+ <li class="item pager-previous">
+ <a href="<?= $this->userQuery->sharedUrlHtml() . '&nb=' . FreshRSS_Context::$number .
+ '&offset=' . max(0, FreshRSS_Context::$offset - FreshRSS_Context::$number) ?>">‹ <?= _t('conf.logs.pagination.previous') ?></a>
+ </li>
+ <?php endif; ?>
+ <li class="item pager-next">
+ <a href="<?= $this->userQuery->sharedUrlHtml() . '&nb=' . FreshRSS_Context::$number .
+ '&offset=' . (FreshRSS_Context::$offset + FreshRSS_Context::$number) ?>"><?= _t('conf.logs.pagination.next') ?> ›</a>
+ </li>
+ </ul>
+</nav>
diff --git a/app/views/helpers/index/article.phtml b/app/views/helpers/index/article.phtml
new file mode 100644
index 000000000..caf06359d
--- /dev/null
+++ b/app/views/helpers/index/article.phtml
@@ -0,0 +1,117 @@
+<?php
+ declare(strict_types=1);
+ /** @var FreshRSS_View $this */
+ $entry = $this->entry;
+ $feed = $this->feed;
+?>
+<article class="flux_content" dir="auto">
+<div class="content <?= FreshRSS_Context::userConf()->content_width ?>">
+ <header>
+ <?php
+ $favoriteUrl = ['c' => 'entry', 'a' => 'bookmark', 'params' => ['id' => $entry->id()]];
+ if ($entry->isFavorite()) {
+ $favoriteUrl['params']['is_favorite'] = 0;
+ }
+ $readUrl = ['c' => 'entry', 'a' => 'read', 'params' => ['id' => $entry->id()]];
+ if ($entry->isRead()) {
+ $readUrl['params']['is_read'] = 0;
+ }
+ ?>
+ <div class="article-header-topline">
+ <?php if (FreshRSS_Auth::hasAccess()) { ?>
+ <a class="read" href="<?= Minz_Url::display($readUrl) ?>" title="<?= _t('conf.shortcut.mark_read') ?>"><?= _i($entry->isRead() ? 'read' : 'unread') ?></a>
+ <a class="bookmark" href="<?= Minz_Url::display($favoriteUrl) ?>" title="<?= _t('conf.shortcut.mark_favorite') ?>"><?= _i($entry->isFavorite() ? 'starred' : 'non-starred') ?></a>
+ <?php } ?>
+ <?php if (FreshRSS_Context::userConf()->show_feed_name === 't') { ?>
+ <a class="website" href="<?= _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
+ <?php if (FreshRSS_Context::userConf()->show_favicons): ?>
+ <img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
+ endif; ?><span><?= $feed->name() ?></span></a>
+ <?php } ?>
+ </div>
+
+ <?php
+ if (in_array(FreshRSS_Context::userConf()->show_tags, ['b', 'h'], true)) {
+ $this->renderHelper('index/tags');
+ }
+ ?>
+
+ <h1 class="title"><a target="_blank" rel="noreferrer" class="go_website" href="<?= $entry->link() ?>"><?= $entry->title() ?></a></h1>
+ <?php if (FreshRSS_Context::userConf()->show_author_date === 'h' || FreshRSS_Context::userConf()->show_author_date === 'b') { ?>
+ <div class="subtitle">
+ <?php if (FreshRSS_Context::userConf()->show_feed_name === 'a') { ?>
+ <div class="website"><a href="<?= $this->internal_rendering ? $feed->website() : _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
+ <?php if (FreshRSS_Context::userConf()->show_favicons): ?>
+ <img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
+ endif; ?><span><?= $feed->name() ?></span></a></div>
+ <?php } ?>
+ <div class="author"><?php
+ $authors = $entry->authors();
+ if (is_array($authors)) {
+ if ($this->internal_rendering):
+ foreach ($authors as $author): ?>
+ <?= $author ?>
+ <?php endforeach;
+ else:
+ foreach ($authors as $author): ?>
+ <a href="<?= Minz_Url::display(Minz_Request::modifiedCurrentRequest(['search' => 'author:' . str_replace(' ', '+', htmlspecialchars_decode($author, ENT_QUOTES))])) ?>">
+ <?= $author ?>
+ </a>
+ <?php endforeach;
+ endif;
+ } ?>
+ </div>
+ <div class="date">
+ <time datetime="<?= $entry->machineReadableDate() ?>"><?= $entry->date() ?></time>
+ </div>
+ </div>
+ <?php } ?>
+ </header>
+
+ <div class="text">
+ <?= $entry->content(true) ?>
+ </div>
+ <?php
+ $display_authors_date = in_array(FreshRSS_Context::userConf()->show_author_date, ['b', 'f'], true);
+ $display_tags = in_array(FreshRSS_Context::userConf()->show_tags, ['b', 'f'], true);
+
+ if ($display_authors_date || $display_tags) {
+ ?>
+ <footer>
+ <?php if ($display_authors_date) { ?>
+ <div class="subtitle">
+ <?php if (FreshRSS_Context::userConf()->show_feed_name === 'a') { ?>
+ <div class="website"><a href="<?= _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
+ <?php if (FreshRSS_Context::userConf()->show_favicons): ?>
+ <img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
+ endif; ?><span><?= $feed->name() ?></span></a></div>
+ <?php } ?>
+ <div class="author"><?php
+ $authors = $entry->authors();
+ if (is_array($authors)) {
+ foreach ($authors as $author) {
+ ?>
+ <a href="<?= Minz_Url::display(Minz_Request::modifiedCurrentRequest(['search' => 'author:' . str_replace(' ', '+', htmlspecialchars_decode($author, ENT_QUOTES))])) ?>">
+ <?= $author ?>
+ </a>
+ <?php
+ }
+ }
+ ?>
+ </div>
+ <div class="date">
+ <time datetime="<?= $entry->machineReadableDate() ?>"><?= $entry->date() ?></time>
+ </div>
+ </div>
+ <?php
+ }
+
+ if ($display_tags) {
+ $this->renderHelper('index/tags');
+ }
+ ?>
+ </footer>
+ <?php
+ } ?>
+</div>
+</article>
diff --git a/app/views/helpers/index/normal/entry_header.phtml b/app/views/helpers/index/normal/entry_header.phtml
index b324a5949..f550dfa21 100644
--- a/app/views/helpers/index/normal/entry_header.phtml
+++ b/app/views/helpers/index/normal/entry_header.phtml
@@ -1,9 +1,6 @@
<?php
declare(strict_types=1);
/** @var FreshRSS_View $this */
- if ($this->feed === null) {
- throw new FreshRSS_Context_Exception('Feed not initialised!');
- }
$topline_read = FreshRSS_Context::userConf()->topline_read;
$topline_favorite = FreshRSS_Context::userConf()->topline_favorite;
$topline_website = FreshRSS_Context::userConf()->topline_website;
diff --git a/app/views/helpers/index/tags.phtml b/app/views/helpers/index/tags.phtml
new file mode 100644
index 000000000..8f67784dd
--- /dev/null
+++ b/app/views/helpers/index/tags.phtml
@@ -0,0 +1,42 @@
+<?php
+ declare(strict_types=1);
+ /** @var FreshRSS_View $this */
+ [$firstTags,$remainingTags] = $this->entry->tagsFormattingHelper();
+?>
+<div class="tags">
+<?php if (!empty($firstTags)): ?>
+ <?= _i('tag') ?><ul class="list-tags">
+ <?php if (Minz_Request::controllerName() === 'index'): ?>
+ <?php foreach ($firstTags as $tag): ?>
+ <li class="item tag"><a class="link-tag" href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li>
+ <?php endforeach; ?>
+ <?php else: // API public access ?>
+ <?php foreach ($firstTags as $tag): ?>
+ <li class="item tag"><a class="link-tag" href="<?= $this->html_url . '&search=%23' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES)) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li>
+ <?php endforeach; ?>
+ <?php endif; ?>
+
+ <?php if (!empty($remainingTags)): // more than 7 tags: show dropdown menu ?>
+ <li class="item tag">
+ <div class="dropdown">
+ <div id="dropdown-tags2-<?= $this->entry->id() ?>" class="dropdown-target"></div>
+ <a class="dropdown-toggle" href="#dropdown-tags2-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
+ <ul class="dropdown-menu">
+ <li class="dropdown-header"><?= _t('index.tag.related') ?></li>
+ <?php if (Minz_Request::controllerName() === 'index'): ?>
+ <?php foreach ($remainingTags as $tag): ?>
+ <li class="item"><a href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li>
+ <?php endforeach; ?>
+ <?php else: ?>
+ <?php foreach ($remainingTags as $tag): ?>
+ <li class="item tag"><a class="link-tag" href="<?= $this->html_url . '&search=%23' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES)) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li>
+ <?php endforeach; ?>
+ <?php endif; ?>
+ </ul>
+ <a class="dropdown-close" href="#close">❌</a>
+ </div>
+ </li>
+ <?php endif; ?>
+ </ul>
+<?php endif; ?>
+</div>
diff --git a/app/views/index/html.phtml b/app/views/index/html.phtml
new file mode 100644
index 000000000..149bebee4
--- /dev/null
+++ b/app/views/index/html.phtml
@@ -0,0 +1,32 @@
+<?php
+ declare(strict_types=1);
+ /** @var FreshRSS_View $this */
+ // Override some layout preferences for the public API output
+ FreshRSS_Context::userConf()->content_width = 'large';
+ FreshRSS_Context::userConf()->show_author_date = FreshRSS_UserConfiguration::default()->show_author_date;
+ FreshRSS_Context::userConf()->show_favicons = FreshRSS_UserConfiguration::default()->show_favicons;
+ FreshRSS_Context::userConf()->show_feed_name = FreshRSS_UserConfiguration::default()->show_feed_name;
+ FreshRSS_Context::userConf()->show_tags = FreshRSS_UserConfiguration::default()->show_tags;
+ FreshRSS_Context::userConf()->show_tags_max = FreshRSS_UserConfiguration::default()->show_tags_max;
+?>
+<?php $this->renderHelper('htmlPagination'); ?>
+<main id="stream" class="reader api">
+ <h2>
+ <a href="<?= $this->html_url ?>"><?= FreshRSS_View::title() ?></a> ·
+ <a class="view-rss" href="<?= $this->rss_url ?>" title="<?= _t('index.menu.rss_view') ?>">
+ <?= _i('rss') ?>
+ </a>
+ </h2>
+ <?php
+ foreach ($this->entries as $entry):
+ $this->entry = $entry;
+ $this->feed = $this->feeds[$entry->feedId()] ??
+ FreshRSS_Category::findFeed($this->categories, $entry->feedId()) ??
+ FreshRSS_Feed::default();
+ ?>
+ <div class="flux">
+ <?php $this->renderHelper('index/article'); ?>
+ </div>
+ <?php endforeach; ?>
+</main>
+<?php $this->renderHelper('htmlPagination'); ?>
diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml
index 26e38dc91..9596ebc89 100644
--- a/app/views/index/normal.phtml
+++ b/app/views/index/normal.phtml
@@ -11,23 +11,18 @@ call_user_func($this->callbackBeforeEntries, $this);
$display_today = true;
$display_yesterday = true;
$display_others = true;
-$hidePosts = !FreshRSS_Context::userConf()->display_posts;
-$lazyload = FreshRSS_Context::userConf()->lazyload;
-$content_width = FreshRSS_Context::userConf()->content_width;
-$MAX_TAGS_DISPLAYED = (int)FreshRSS_Context::userConf()->show_tags_max;
$useKeepUnreadImportant = !FreshRSS_Context::isImportant() && !FreshRSS_Context::isFeed();
$today = @strtotime('today');
?>
-<main id="stream" class="normal<?= $hidePosts ? ' hide_posts' : '' ?>">
+<main id="stream" class="normal<?= FreshRSS_Context::userConf()->display_posts ? '' : ' hide_posts' ?>">
<h1 class="title_hidden"><?= _t('conf.reading.view.normal') ?></h1>
<div id="new-article">
<a href="<?= Minz_Url::display(Minz_Request::currentRequest()) ?>"><?= _t('gen.js.new_article'); /* TODO: move string in JS*/ ?></a>
</div><?php
$lastEntry = null;
$nbEntries = 0;
- /** @var FreshRSS_Entry */
foreach ($this->entries as $item):
$lastEntry = $item;
$nbEntries++;
@@ -40,8 +35,8 @@ $today = @strtotime('today');
$this->entry = $item;
// We most likely already have the feed object in cache, otherwise make a request
- $this->feed = FreshRSS_CategoryDAO::findFeed($this->categories, $this->entry->feedId()) ??
- $this->entry->feed() ?? FreshRSS_Feed::example();
+ $this->feed = FreshRSS_Category::findFeed($this->categories, $this->entry->feedId()) ??
+ $this->entry->feed() ?? FreshRSS_Feed::default();
if ($display_today && $this->entry->isDay(FreshRSS_Days::TODAY, $today)) {
?><div class="day" id="day_today"><?php
@@ -74,27 +69,8 @@ $today = @strtotime('today');
?>" data-priority="<?= $this->feed->priority()
?>"><?php
$this->renderHelper('index/normal/entry_header');
- if ($this->feed === null) {
- throw new FreshRSS_Context_Exception('Feed not initialised!');
- }
-
- $tags = null;
- $firstTags = array();
- $remainingTags = array();
-
- if (FreshRSS_Context::userConf()->show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'f' || FreshRSS_Context::userConf()->show_tags === 'b') {
- $tags = $this->entry->tags();
- if (!empty($tags)) {
- if ($MAX_TAGS_DISPLAYED > 0) {
- $firstTags = array_slice($tags, 0, $MAX_TAGS_DISPLAYED);
- $remainingTags = array_slice($tags, $MAX_TAGS_DISPLAYED);
- } else {
- $firstTags = $tags;
- }
- }
- }
?><article class="flux_content" dir="auto">
- <div class="content <?= $content_width ?>">
+ <div class="content <?= FreshRSS_Context::userConf()->content_width ?>">
<header>
<?php if (FreshRSS_Context::userConf()->show_feed_name === 't') { ?>
<div class="website"><a href="<?= _url('index', 'index', 'get', 'f_' . $this->feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
@@ -103,36 +79,8 @@ $today = @strtotime('today');
endif; ?><span><?= $this->feed->name() ?></span></a>
</div>
<?php } ?>
- <?php if (FreshRSS_Context::userConf()->show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'b') { ?>
- <div class="tags">
- <?php
- if (!empty($tags)) {
- ?><?= _i('tag') ?><ul class="list-tags"><?php
- foreach ($firstTags as $tag) {
- ?><li class="item tag"><a class="link-tag" href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li><?php
- }
-
- if (!empty($remainingTags)) { // more than 7 tags: show dropdown menu ?>
- <li class="item tag">
- <div class="dropdown">
- <div id="dropdown-tags2-<?= $this->entry->id() ?>" class="dropdown-target"></div>
- <a class="dropdown-toggle" href="#dropdown-tags2-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
- <ul class="dropdown-menu">
- <li class="dropdown-header"><?= _t('index.tag.related') ?></li>
- <?php
- foreach ($remainingTags as $tag) {
- ?><li class="item"><a href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>"><?= $tag ?></a></li><?php
- } ?>
- </ul>
- <a class="dropdown-close" href="#close">❌</a>
- </div>
- </li>
- <?php
- } ?>
- </ul><?php
- } ?>
- </div>
- <?php
+ <?php if (FreshRSS_Context::userConf()->show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'b') {
+ $this->renderHelper('index/tags');
} ?>
<h1 class="title"><a target="_blank" rel="noreferrer" class="go_website" href="<?= $this->entry->link() ?>" title="<?= _t('conf.shortcut.see_on_website')?>"><?= $this->entry->title() ?></a></h1>
<?php if (FreshRSS_Context::userConf()->show_author_date === 'h' || FreshRSS_Context::userConf()->show_author_date === 'b') { ?>
@@ -163,8 +111,8 @@ $today = @strtotime('today');
</div>
<?php } ?>
</header>
- <div class="text"><?php
- echo $lazyload && $hidePosts ? lazyimg($this->entry->content(true)) : $this->entry->content(true);
+ <div class="text"><?=
+ FreshRSS_Context::userConf()->lazyload && !FreshRSS_Context::userConf()->display_posts ? lazyimg($this->entry->content(true)) : $this->entry->content(true)
?></div>
<?php
$display_authors_date = FreshRSS_Context::userConf()->show_author_date === 'f' || FreshRSS_Context::userConf()->show_author_date === 'b';
@@ -201,36 +149,10 @@ $today = @strtotime('today');
</div>
<?php
}
- if ($display_tags) { ?>
- <div class="tags">
- <?php
- if (!empty($tags)) {
- ?><?= _i('tag') ?><ul class="list-tags"><?php
- foreach ($firstTags as $tag) {
- ?><li class="item tag"><a class="link-tag" href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li><?php
- }
- if (!empty($remainingTags)) { ?>
- <li class="item tag">
- <div class="dropdown">
- <div id="dropdown-tags3-<?= $this->entry->id() ?>" class="dropdown-target"></div>
- <a class="dropdown-toggle" href="#dropdown-tags3-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
- <ul class="dropdown-menu">
- <li class="dropdown-header"><?= _t('index.tag.related') ?></li>
- <?php
- foreach ($remainingTags as $tag) {
- ?><li class="item"><a href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>"><?= $tag ?></a></li><?php
- } ?>
- </ul>
- <a class="dropdown-close" href="#close">❌</a>
- </div>
- </li>
- <?php
- } ?>
- </ul><?php
- } ?>
- </div>
- <?php
- } ?>
+ if ($display_tags) {
+ $this->renderHelper('index/tags');
+ }
+ ?>
</footer>
<?php
} ?>
diff --git a/app/views/index/reader.phtml b/app/views/index/reader.phtml
index af51933cf..ccca9e50c 100644
--- a/app/views/index/reader.phtml
+++ b/app/views/index/reader.phtml
@@ -9,8 +9,6 @@ if (!Minz_Request::paramBoolean('ajax')) {
call_user_func($this->callbackBeforeEntries, $this);
$lazyload = FreshRSS_Context::userConf()->lazyload;
-$content_width = FreshRSS_Context::userConf()->content_width;
-$MAX_TAGS_DISPLAYED = (int)FreshRSS_Context::userConf()->show_tags_max;
?>
<main id="stream" class="reader">
<h1 class="title_hidden"><?= _t('conf.reading.view.reader') ?></h1>
@@ -19,197 +17,21 @@ $MAX_TAGS_DISPLAYED = (int)FreshRSS_Context::userConf()->show_tags_max;
</div><?php
$lastEntry = null;
$nbEntries = 0;
- /** @var FreshRSS_Entry */
- foreach ($this->entries as $item):
- $lastEntry = $item;
+ foreach ($this->entries as $entry):
+ $lastEntry = $entry;
$nbEntries++;
ob_flush();
/** @var FreshRSS_Entry */
- $item = Minz_ExtensionManager::callHook('entry_before_display', $item);
- if ($item == null) {
+ $entry = Minz_ExtensionManager::callHook('entry_before_display', $entry);
+ if ($entry == null) {
continue;
}
- $this->entry = $item;
-
- $tags = null;
- $firstTags = array();
- $remainingTags = array();
-
- if (FreshRSS_Context::userConf()->show_tags == 'h' || FreshRSS_Context::userConf()->show_tags == 'f' || FreshRSS_Context::userConf()->show_tags == 'b') {
- $tags = $this->entry->tags();
- if (!empty($tags)) {
- if ($MAX_TAGS_DISPLAYED > 0) {
- $firstTags = array_slice($tags, 0, $MAX_TAGS_DISPLAYED);
- $remainingTags = array_slice($tags, $MAX_TAGS_DISPLAYED);
- } else {
- $firstTags = $tags;
- }
- }
- }
+ $this->entry = $entry;
//We most likely already have the feed object in cache, otherwise make a request
- $feed = FreshRSS_CategoryDAO::findFeed($this->categories, $item->feedId()) ?? $item->feed() ?? FreshRSS_Feed::example();
- ?><div class="flux<?= !$item->isRead() ? ' not_read' : '' ?><?= $item->isFavorite() ? ' favorite' : '' ?>" id="flux_<?= $item->id() ?>" data-priority="<?= $feed->priority() ?>">
- <article class="flux_content" dir="auto">
-
- <div class="content <?= $content_width ?>">
- <header>
- <?php
- $favoriteUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id()));
- if ($item->isFavorite()) {
- $favoriteUrl['params']['is_favorite'] = 0;
- }
- $readUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id()));
- if ($item->isRead()) {
- $readUrl['params']['is_read'] = 0;
- }
- ?>
- <div class="article-header-topline">
- <?php if (FreshRSS_Auth::hasAccess()) { ?>
- <a class="read" href="<?= Minz_Url::display($readUrl) ?>" title="<?= _t('conf.shortcut.mark_read') ?>"><?= _i($item->isRead() ? 'read' : 'unread') ?></a>
- <a class="bookmark" href="<?= Minz_Url::display($favoriteUrl) ?>" title="<?= _t('conf.shortcut.mark_favorite') ?>"><?= _i($item->isFavorite() ? 'starred' : 'non-starred') ?></a>
- <?php } ?>
- <?php if (FreshRSS_Context::userConf()->show_feed_name === 't') { ?>
- <a class="website" href="<?= _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
- <?php if (FreshRSS_Context::userConf()->show_favicons): ?>
- <img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
- endif; ?><span><?= $feed->name() ?></span></a>
- <?php } ?>
- </div>
-
- <?php if (FreshRSS_Context::userConf()->show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'b') { ?>
- <div class="tags">
- <?php
- if (!empty($tags)) {
- ?><?= _i('tag') ?><ul class="list-tags"><?php
- foreach ($firstTags as $tag) {
- ?><li class="item tag"><a class="link-tag" href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li><?php
- }
-
- if (!empty($remainingTags)) { // more than 7 tags: show dropdown menu ?>
- <li class="item tag">
- <div class="dropdown">
- <div id="dropdown-tags-<?= $this->entry->id() ?>" class="dropdown-target"></div>
- <a class="dropdown-toggle" href="#dropdown-tags-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
- <ul class="dropdown-menu">
- <li class="dropdown-header"><?= _t('index.tag.related') ?></li>
- <?php
- foreach ($remainingTags as $tag) {
- ?><li class="item"><a href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>"><?= $tag ?></a></li><?php
- } ?>
- </ul>
- <a class="dropdown-close" href="#close">❌</a>
- </div>
- </li>
- <?php
- } ?>
- </ul><?php
- } ?>
- </div>
- <?php } ?>
-
- <h1 class="title"><a target="_blank" rel="noreferrer" class="go_website" href="<?= $item->link() ?>"><?= $item->title() ?></a></h1>
- <?php if (FreshRSS_Context::userConf()->show_author_date === 'h' || FreshRSS_Context::userConf()->show_author_date === 'b') { ?>
- <div class="subtitle">
- <?php if (FreshRSS_Context::userConf()->show_feed_name === 'a') { ?>
- <div class="website"><a href="<?= _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
- <?php if (FreshRSS_Context::userConf()->show_favicons): ?>
- <img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
- endif; ?><span><?= $feed->name() ?></span></a></div>
- <?php } ?>
- <div class="author"><?php
- $authors = $item->authors();
- if (is_array($authors)) {
- foreach ($authors as $author) {
- ?>
- <a href="<?= Minz_Url::display(Minz_Request::modifiedCurrentRequest(['search' => 'author:' . str_replace(' ', '+', htmlspecialchars_decode($author, ENT_QUOTES))])) ?>">
- <?= $author ?>
- </a>
- <?php
- }
- }
- ?>
- </div>
- <div class="date">
- <time datetime="<?= $item->machineReadableDate() ?>"><?= $item->date() ?></time>
- </div>
- </div>
- <?php } ?>
- </header>
-
- <div class="text">
- <?= $item->content(true) ?>
- </div>
- <?php
- $display_authors_date = FreshRSS_Context::userConf()->show_author_date === 'f' || FreshRSS_Context::userConf()->show_author_date === 'b';
- $display_tags = FreshRSS_Context::userConf()->show_tags === 'f' || FreshRSS_Context::userConf()->show_tags === 'b';
-
- if ($display_authors_date || $display_tags) {
- ?>
- <footer>
- <?php if ($display_authors_date) { ?>
- <div class="subtitle">
- <?php if (FreshRSS_Context::userConf()->show_feed_name === 'a') { ?>
- <div class="website"><a href="<?= _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
- <?php if (FreshRSS_Context::userConf()->show_favicons): ?>
- <img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
- endif; ?><span><?= $feed->name() ?></span></a></div>
- <?php } ?>
- <div class="author"><?php
- $authors = $item->authors();
- if (is_array($authors)) {
- foreach ($authors as $author) {
- ?>
- <a href="<?= Minz_Url::display(Minz_Request::modifiedCurrentRequest(['search' => 'author:' . str_replace(' ', '+', htmlspecialchars_decode($author, ENT_QUOTES))])) ?>">
- <?= $author ?>
- </a>
- <?php
- }
- }
- ?>
- </div>
- <div class="date">
- <time datetime="<?= $item->machineReadableDate() ?>"><?= $item->date() ?></time>
- </div>
- </div>
- <?php
- }
-
- if ($display_tags) { ?>
- <div class="tags">
- <?php
- if (!empty($tags)) {
- ?><?= _i('tag') ?><ul class="list-tags"><?php
- foreach ($firstTags as $tag) {
- ?><li class="item tag"><a class="link-tag" href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li><?php
- }
-
- if (!empty($remainingTags)) { // more than 7 tags: show dropdown menu ?>
- <li class="item tag">
- <div class="dropdown">
- <div id="dropdown-tags2-<?= $this->entry->id() ?>" class="dropdown-target"></div>
- <a class="dropdown-toggle" href="#dropdown-tags2-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
- <ul class="dropdown-menu">
- <li class="dropdown-header"><?= _t('index.tag.related') ?></li>
- <?php
- foreach ($remainingTags as $tag) {
- ?><li class="item"><a href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>"><?= $tag ?></a></li><?php
- } ?>
- </ul>
- <a class="dropdown-close" href="#close">❌</a>
- </div>
- </li>
- <?php
- } ?>
- </ul><?php
- } ?>
- </div>
- <?php } ?>
- </footer>
- <?php
- } ?>
- </div>
- </article>
+ $this->feed = FreshRSS_Category::findFeed($this->categories, $entry->feedId()) ?? $entry->feed() ?? FreshRSS_Feed::default();
+ ?><div class="flux<?= !$entry->isRead() ? ' not_read' : '' ?><?= $entry->isFavorite() ? ' favorite' : '' ?>" id="flux_<?= $entry->id() ?>" data-priority="<?= $this->feed->priority() ?>">
+ <?php $this->renderHelper('index/article'); ?>
</div><?php
endforeach;
diff --git a/app/views/index/rss.phtml b/app/views/index/rss.phtml
index 1c036abd2..80768c5b7 100644
--- a/app/views/index/rss.phtml
+++ b/app/views/index/rss.phtml
@@ -8,14 +8,12 @@
>
<channel>
<title><?= $this->rss_title ?></title>
- <link><?= $this->internal_rendering ? htmlspecialchars($this->rss_url, ENT_NOQUOTES, 'UTF-8') : Minz_Url::display('', 'html', true) ?></link>
+ <link><?= $this->html_url ?></link>
<description><?= _t('index.feed.rss_of', $this->rss_title) ?></description>
<pubDate><?= date('D, d M Y H:i:s O') ?></pubDate>
<lastBuildDate><?= gmdate('D, d M Y H:i:s') ?> GMT</lastBuildDate>
- <atom:link href="<?= $this->internal_rendering ? htmlspecialchars($this->rss_url, ENT_COMPAT, 'UTF-8') :
- Minz_Url::display($this->rss_url, 'html', true) ?>" rel="self" type="application/rss+xml" />
+ <atom:link href="<?= $this->rss_url ?>" rel="self" type="application/rss+xml" />
<?php
-/** @var FreshRSS_Entry */
foreach ($this->entries as $item) {
if (!$this->internal_rendering) {
/** @var FreshRSS_Entry */
diff --git a/app/views/user/profile.phtml b/app/views/user/profile.phtml
index 576e821b2..1ce964365 100644
--- a/app/views/user/profile.phtml
+++ b/app/views/user/profile.phtml
@@ -62,6 +62,7 @@
<p class="help"><?= _i('help') ?> <?= _t('admin.auth.token_help') ?></p>
<kbd><?= Minz_Url::display(array('a' => 'rss', 'params' => array('user' => Minz_User::name(),
'token' => $token, 'hours' => FreshRSS_Context::userConf()->since_hours_posts_per_rss)), 'html', true) ?></kbd>
+ <p class="help"><?= _i('help') ?> <?= _t('conf.query.help') ?></a></p>
</div>
</div>
<?php } ?>
diff --git a/config-user.default.php b/config-user.default.php
index 6282fc61b..224567907 100644
--- a/config-user.default.php
+++ b/config-user.default.php
@@ -36,10 +36,10 @@ return array (
'auto_load_more' => true,
'display_posts' => false,
'display_categories' => 'active', //{ active, remember, all, none }
- 'show_tags' => '0',
+ 'show_tags' => 'f', // {0 => none, b => both, f => footer, h => header}
'show_tags_max' => 7,
- 'show_author_date' => 'h',
- 'show_feed_name' => 'a',
+ 'show_author_date' => 'h', // {0 => none, b => both, f => footer, h => header}
+ 'show_feed_name' => 'a', // {0 => none, a => with authors, t => above title}
'hide_read_feeds' => true,
'onread_jump_next' => true,
'lazyload' => true,
diff --git a/docs/en/img/users/user-query-share.png b/docs/en/img/users/user-query-share.png
new file mode 100644
index 000000000..f4cf47991
--- /dev/null
+++ b/docs/en/img/users/user-query-share.png
Binary files differ
diff --git a/docs/en/users/03_Main_view.md b/docs/en/users/03_Main_view.md
index 7a0320cb6..6b2cf4313 100644
--- a/docs/en/users/03_Main_view.md
+++ b/docs/en/users/03_Main_view.md
@@ -38,3 +38,4 @@ Reader view will display a feed will all articles already open for reading. Feed
Read more:
* [Refreshing the feeds](./09_refreshing_feeds.md)
* [Filter the feeds and search](./10_filter.md)
+* [User queries](./user_queries.md)
diff --git a/docs/en/users/05_Configuration.md b/docs/en/users/05_Configuration.md
index e2e0f1205..529e3b63f 100644
--- a/docs/en/users/05_Configuration.md
+++ b/docs/en/users/05_Configuration.md
@@ -167,10 +167,7 @@ This means that if you assign a shortcut to more than one action, you’ll end u
# User queries
-You can configure your [user queries](./03_Main_view.md) in that section. There is not much to say here as it is pretty straightforward.
-You can only change user query titles or drop them.
-
-At the moment, there is no helper to build a user query from here.
+You can configure your [user queries](./user_queries.md) in that section.
# Profile
diff --git a/docs/en/users/10_filter.md b/docs/en/users/10_filter.md
index 60310c161..519130c14 100644
--- a/docs/en/users/10_filter.md
+++ b/docs/en/users/10_filter.md
@@ -119,34 +119,18 @@ Finally, parentheses may be used to express more complex queries, with basic neg
You can change the sort order by clicking the toggle button available in the header.
-## Store your filters
+## Bookmark the current query
-Once you came up with your perfect filter, it would be a shame if you need to recreate it every time you need to use it.
+Once you came up with your perfect filter, it would be a shame if you had to recreate it every time you need to use it.
-Hopefully, there is a way to bookmark them for later use.
-We call them *user queries*.
+Luckily, there is a way to bookmark them for later use.
+We call them [*user queries*](./user_queries.md).
You can create as many as you want, the only limit is how they will be displayed on your screen.
-### Bookmark the current query
-
-Display the user queries drop-down by clicking the button next to the state buttons.
-![User queries drop-down](../img/users/user.queries.drop-down.empty.png)
-
-Then click on the bookmark action.
-
-Congratulations, you’re done!
-
-### Using a bookmarked query
-
-Display the user queries drop-down by clicking the button next to the state buttons.
-![User queries drop-down](../img/users/user.queries.drop-down.not.empty.png)
-
-Then click on the bookmarked query, the previously stored query will be applied.
-
-> Note that only the query is stored, not the articles.
-> The results you are seeing now could be different from the results on the day you've created the query.
+Read more about [*user queries*](./user_queries.md) to learn how to create them, use them, and even reshare them via HTML / RSS / OPML.
---
Read more:
* [Normal, Global and Reader view](./03_Main_view.md)
* [Refreshing the feeds](./09_refreshing_feeds.md)
+* [User queries](./user_queries.md)
diff --git a/docs/en/users/user_queries.md b/docs/en/users/user_queries.md
new file mode 100644
index 000000000..68f0898df
--- /dev/null
+++ b/docs/en/users/user_queries.md
@@ -0,0 +1,63 @@
+# User queries
+
+*User queries* are a way to store any FreshRSS search query.
+
+Read about [the filters](./10_filter.md) to learn the different ways to search and filter
+articles in FreshRSS.
+
+## Bookmark the current query
+
+Once you have a search query with a filter, it can be saved.
+
+To do so, display the user queries drop-down menu by clicking the button next to the state buttons:
+
+![User queries drop-down](../img/users/user.queries.drop-down.empty.png)
+
+Then click on the bookmark action.
+
+## Using a bookmarked query
+
+Display the user queries drop-down menu by clicking the button next to the state buttons:
+
+![User queries drop-down](../img/users/user.queries.drop-down.not.empty.png)
+
+Then click on the bookmarked query, the previously stored query will be applied.
+
+> ℹ️ Note that only the search query is stored, not the articles.
+> So the results you are seeing one day might be different another day.
+
+## Share your user queries
+
+A prerequisite is that the FreshRSS API(s) must be enabled in FreshRSS authentication settings.
+
+From the configuration page of the user queries,
+it is possible to share the output of the user queries with external users,
+in the formats HTML, RSS, and OPML:
+
+![Share user query](../img/users/user-query-share.png)
+
+> ℹ️ Note that the sharing as OPML is only available for user queries based on all feeds, a category, or a feed.
+> Sharing by OPML is **not** available for queries based on user labels or favourites or important feeds,
+> to avoid leaking some feed details in an unintended manner.
+
+### Additional parameters for shared user queries
+
+Some parameters can be manually added to the URL:
+
+* `f`: Format of output. Can be `html`, `rss` (`atom` is an alias), or `opml`.
+* `hours`: Show only the articles newer than this number of hours.
+* `nb`: Number of articles to return. Limited by `max_posts_per_rss` in the user configuration. Can be used in combination with `offset` for pagination.
+* `offset`: Skip a number of articles. Used in particular by the HTML view for pagination.
+* `order`: Show the newest articles at the top with `DESC`, or the oldest articles at the top with `ASC`. By default, will use the sort order defined by the user query.
+
+## Sharing with a master token (deprecated)
+
+Before FreshRSS 1.24, the only option to reshare an RSS output was by using a master token,
+like `https://freshrss.example.net/?a=rss&user=alice&token1234`
+
+This was mostly intended for sharing between systems controlled by the same user, and not for sharing publicly.
+
+This method **is not advised anymore** as it is not safe to use the same master token for multiple outputs,
+especially not when shared with other persons.
+
+Now, sharing RSS outputs via user queries is the recommended approach for all scenarios.
diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php
index 9bf1ff4fb..662dc6db9 100644
--- a/lib/Minz/Request.php
+++ b/lib/Minz/Request.php
@@ -162,11 +162,11 @@ class Minz_Request {
* Setteurs
*/
public static function _controllerName(string $controller_name): void {
- self::$controller_name = $controller_name;
+ self::$controller_name = ctype_alnum($controller_name) ? $controller_name : '';
}
public static function _actionName(string $action_name): void {
- self::$action_name = $action_name;
+ self::$action_name = ctype_alnum($action_name) ? $action_name : '';
}
/** @param array<string,string> $params */
@@ -187,6 +187,7 @@ class Minz_Request {
* Initialise la Request
*/
public static function init(): void {
+ self::_params($_GET);
self::initJSON();
}
diff --git a/p/api/greader.php b/p/api/greader.php
index f8390e3ef..615f83567 100644
--- a/p/api/greader.php
+++ b/p/api/greader.php
@@ -572,7 +572,7 @@ final class GReaderAPI {
continue;
}
- $feed = FreshRSS_CategoryDAO::findFeed($categories, $entry->feedId());
+ $feed = FreshRSS_Category::findFeed($categories, $entry->feedId());
if ($feed === null) {
continue;
}
@@ -694,7 +694,7 @@ final class GReaderAPI {
}
$entryDAO = FreshRSS_Factory::createEntryDao();
- $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, $searches);
+ $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, 0, $continuation, $searches);
$entries = iterator_to_array($entries); //TODO: Improve
$items = self::entriesToArray($entries);
@@ -746,7 +746,7 @@ final class GReaderAPI {
}
$entryDAO = FreshRSS_Factory::createEntryDao();
- $ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, $continuation, $searches);
+ $ids = $entryDAO->listIdsWhere($type, $id, $state, $order === 'o' ? 'ASC' : 'DESC', $count, 0, $continuation, $searches);
if ($ids === null) {
self::internalServerError();
}
diff --git a/p/api/query.php b/p/api/query.php
new file mode 100644
index 000000000..0ba14453f
--- /dev/null
+++ b/p/api/query.php
@@ -0,0 +1,175 @@
+<?php
+declare(strict_types=1);
+require(__DIR__ . '/../../constants.php');
+require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
+
+Minz_Request::init();
+
+$token = Minz_Request::paramString('t');
+if (!ctype_alnum($token)) {
+ header('HTTP/1.1 422 Unprocessable Entity');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Invalid token `t`!' . $token);
+}
+
+$format = Minz_Request::paramString('f');
+if (!in_array($format, ['atom', 'html', 'opml', 'rss'], true)) {
+ header('HTTP/1.1 422 Unprocessable Entity');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Invalid format `f`!');
+}
+
+$user = Minz_Request::paramString('user');
+if (!FreshRSS_user_Controller::checkUsername($user)) {
+ header('HTTP/1.1 422 Unprocessable Entity');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Invalid user!');
+}
+
+Minz_Session::init('FreshRSS', true);
+
+FreshRSS_Context::initSystem();
+if (!FreshRSS_Context::hasSystemConf() || !FreshRSS_Context::systemConf()->api_enabled) {
+ header('HTTP/1.1 503 Service Unavailable');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('Service Unavailable!');
+}
+
+FreshRSS_Context::initUser($user);
+if (!FreshRSS_Context::hasUserConf()) {
+ usleep(rand(100, 10000)); //Primitive mitigation of scanning for users
+ header('HTTP/1.1 404 Not Found');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('User not found!');
+} else {
+ usleep(rand(20, 200));
+}
+
+if (!file_exists(DATA_PATH . '/no-cache.txt')) {
+ require(LIB_PATH . '/http-conditional.php');
+ // TODO: Consider taking advantage of $feedMode, only for monotonous queries {all, categories, feeds} and not dynamic ones {read/unread, favourites, user labels}
+ if (httpConditional(FreshRSS_UserDAO::mtime($user) ?: time(), 0, 0, false, PHP_COMPRESSION, false)) {
+ exit(); //No need to send anything
+ }
+}
+
+Minz_Translate::init(FreshRSS_Context::userConf()->language);
+Minz_ExtensionManager::init();
+Minz_ExtensionManager::enableByList(FreshRSS_Context::userConf()->extensions_enabled, 'user');
+
+$query = null;
+$userSearch = null;
+foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
+ if (!empty($raw_query['token']) && $raw_query['token'] === $token) {
+ switch ($format) {
+ case 'atom':
+ case 'html':
+ case 'rss':
+ if (empty($raw_query['shareRss'])) {
+ continue 2;
+ }
+ break;
+ case 'opml':
+ if (empty($raw_query['shareOpml'])) {
+ continue 2;
+ }
+ break;
+ default:
+ continue 2;
+ }
+ $query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
+ Minz_Request::_param('get', $query->getGet());
+ if (Minz_Request::paramString('order') === '') {
+ Minz_Request::_param('order', $query->getOrder());
+ }
+ Minz_Request::_param('state', $query->getState());
+
+ $search = $query->getSearch()->getRawInput();
+ // Note: we disallow references to user queries in public user search to avoid sniffing internal user queries
+ $userSearch = new FreshRSS_BooleanSearch(Minz_Request::paramString('search'), 0, 'AND', false);
+ if ($userSearch->getRawInput() !== '') {
+ if ($search === '') {
+ $search = $userSearch->getRawInput();
+ } else {
+ $search .= ' (' . $userSearch->getRawInput() . ')';
+ }
+ }
+ Minz_Request::_param('search', $search);
+ break;
+ }
+}
+if ($query === null || $userSearch === null) {
+ usleep(rand(100, 10000));
+ header('HTTP/1.1 404 Not Found');
+ header('Content-Type: text/plain; charset=UTF-8');
+ die('User query not found!');
+}
+
+$view = new FreshRSS_View();
+
+try {
+ FreshRSS_Context::updateUsingRequest(false);
+ Minz_Request::_param('search', $userSearch->getRawInput()); // Restore user search
+ $view->entries = FreshRSS_index_Controller::listEntriesByContext();
+} catch (Minz_Exception $e) {
+ Minz_Error::error(400, 'Bad user query!');
+ die();
+}
+
+$get = FreshRSS_Context::currentGet(true);
+$type = (string)$get[0];
+$id = (int)$get[1];
+
+switch ($type) {
+ case 'c': // Category
+ $cat = FreshRSS_Context::categories()[$id] ?? null;
+ if ($cat === null) {
+ Minz_Error::error(404, "Category {$id} not found!");
+ die();
+ }
+ $view->categories = [ $cat->id() => $cat ];
+ break;
+ case 'f': // Feed
+ $feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
+ if ($feed === null) {
+ Minz_Error::error(404, "Feed {$id} not found!");
+ die();
+ }
+ $view->feeds = [ $feed->id() => $feed ];
+ $view->categories = [];
+ break;
+ default:
+ $view->categories = FreshRSS_Context::categories();
+ break;
+}
+
+$view->disable_aside = true;
+$view->excludeMutedFeeds = true;
+$view->internal_rendering = true;
+$view->userQuery = $query;
+$view->html_url = $query->sharedUrlHtml();
+$view->rss_url = $query->sharedUrlRss();
+$view->rss_title = $query->getName();
+if ($query->getName() != '') {
+ FreshRSS_View::_title($query->getName());
+}
+FreshRSS_Context::systemConf()->allow_anonymous = true;
+
+if (in_array($format, ['rss', 'atom'], true)) {
+ header('Content-Type: application/rss+xml; charset=utf-8');
+ $view->_layout(null);
+ $view->_path('index/rss.phtml');
+} elseif ($format === 'opml') {
+ if (!$query->safeForOpml()) {
+ Minz_Error::error(404, 'OPML not allowed for this user query!');
+ die();
+ }
+ header('Content-Type: application/xml; charset=utf-8');
+ $view->_layout(null);
+ $view->_path('index/opml.phtml');
+} else {
+ $view->_layout('layout');
+ $view->_path('index/html.phtml');
+}
+
+$view->build();
diff --git a/p/scripts/main.js b/p/scripts/main.js
index ca52bac56..d07568617 100644
--- a/p/scripts/main.js
+++ b/p/scripts/main.js
@@ -439,7 +439,7 @@ function toggleContent(new_active, old_active, skipping) {
const nav_menu = document.querySelector('.nav_menu');
let nav_menu_height = 0;
- if (getComputedStyle(nav_menu).position === 'fixed' || getComputedStyle(nav_menu).position === 'sticky') {
+ if (nav_menu && (getComputedStyle(nav_menu).position === 'fixed' || getComputedStyle(nav_menu).position === 'sticky')) {
nav_menu_height = nav_menu.offsetHeight;
}
@@ -1941,11 +1941,14 @@ function init_main_afterDOM() {
if (stream) {
init_load_more(stream);
init_posts();
- init_nav_entries();
- init_notifs_html5();
- toggle_bigMarkAsRead_button();
- setTimeout(faviconNbUnread, 1000);
- setInterval(refreshUnreads, 120000);
+ if (document.getElementById('new-article')) {
+ // Only relevant for interactive views
+ init_nav_entries();
+ init_notifs_html5();
+ toggle_bigMarkAsRead_button();
+ setTimeout(faviconNbUnread, 1000);
+ setInterval(refreshUnreads, 120000);
+ }
}
if (window.console) {
diff --git a/p/themes/base-theme/frss.css b/p/themes/base-theme/frss.css
index ebbd7a627..ab2ac57ab 100644
--- a/p/themes/base-theme/frss.css
+++ b/p/themes/base-theme/frss.css
@@ -110,6 +110,10 @@ h2 {
line-height: 1.5;
}
+.api > h2 {
+ text-align: center;
+}
+
h2 .icon,
legend .icon {
height: 0.8em;
diff --git a/p/themes/base-theme/frss.rtl.css b/p/themes/base-theme/frss.rtl.css
index 45b967b63..239c8ca8b 100644
--- a/p/themes/base-theme/frss.rtl.css
+++ b/p/themes/base-theme/frss.rtl.css
@@ -110,6 +110,10 @@ h2 {
line-height: 1.5;
}
+.api > h2 {
+ text-align: center;
+}
+
h2 .icon,
legend .icon {
height: 0.8em;
diff --git a/tests/app/Models/CategoryTest.php b/tests/app/Models/CategoryTest.php
index ac450de1d..842ebe200 100644
--- a/tests/app/Models/CategoryTest.php
+++ b/tests/app/Models/CategoryTest.php
@@ -59,9 +59,12 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
$feeds = $category->feeds();
self::assertCount(3, $feeds);
- self::assertEquals('AAA', $feeds[0]->name());
- self::assertEquals('lll', $feeds[1]->name());
- self::assertEquals('ZZZ', $feeds[2]->name());
+ $feed = reset($feeds) ?: FreshRSS_Feed::default();
+ self::assertEquals('AAA', $feed->name());
+ $feed = next($feeds) ?: FreshRSS_Feed::default();
+ self::assertEquals('lll', $feed->name());
+ $feed = next($feeds) ?: FreshRSS_Feed::default();
+ self::assertEquals('ZZZ', $feed->name());
/** @var FreshRSS_Feed&PHPUnit\Framework\MockObject\MockObject */
$feed_4 = $this->getMockBuilder(FreshRSS_Feed::class)
@@ -75,9 +78,13 @@ class CategoryTest extends PHPUnit\Framework\TestCase {
$feeds = $category->feeds();
self::assertCount(4, $feeds);
- self::assertEquals('AAA', $feeds[0]->name());
- self::assertEquals('BBB', $feeds[1]->name());
- self::assertEquals('lll', $feeds[2]->name());
- self::assertEquals('ZZZ', $feeds[3]->name());
+ $feed = reset($feeds) ?: FreshRSS_Feed::default();
+ self::assertEquals('AAA', $feed->name());
+ $feed = next($feeds) ?: FreshRSS_Feed::default();
+ self::assertEquals('BBB', $feed->name());
+ $feed = next($feeds) ?: FreshRSS_Feed::default();
+ self::assertEquals('lll', $feed->name());
+ $feed = next($feeds) ?: FreshRSS_Feed::default();
+ self::assertEquals('ZZZ', $feed->name());
}
}
diff --git a/tests/app/Models/UserQueryTest.php b/tests/app/Models/UserQueryTest.php
index 9f067e848..828bd4276 100644
--- a/tests/app/Models/UserQueryTest.php
+++ b/tests/app/Models/UserQueryTest.php
@@ -8,15 +8,13 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
public function test__construct_whenAllQuery_storesAllParameters(): void {
$query = array('get' => 'a');
- $user_query = new FreshRSS_UserQuery($query);
- self::assertEquals('all', $user_query->getGetName());
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals('all', $user_query->getGetType());
}
public function test__construct_whenFavoriteQuery_storesFavoriteParameters(): void {
$query = array('get' => 's');
- $user_query = new FreshRSS_UserQuery($query);
- self::assertEquals('favorite', $user_query->getGetName());
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals('favorite', $user_query->getGetType());
}
@@ -28,14 +26,8 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
->method('name')
->withAnyParameters()
->willReturn($category_name);
- /** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
- $cat_dao = $this->createMock(FreshRSS_CategoryDAO::class);
- $cat_dao->expects(self::atLeastOnce())
- ->method('searchById')
- ->withAnyParameters()
- ->willReturn($cat);
$query = array('get' => 'c_1');
- $user_query = new FreshRSS_UserQuery($query, null, $cat_dao);
+ $user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
self::assertEquals($category_name, $user_query->getGetName());
self::assertEquals('category', $user_query->getGetType());
}
@@ -45,24 +37,28 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
/** @var FreshRSS_Feed&PHPUnit\Framework\MockObject\MockObject */
$feed = $this->createMock(FreshRSS_Feed::class);
$feed->expects(self::atLeastOnce())
+ ->method('id')
+ ->withAnyParameters()
+ ->willReturn(1);
+ $feed->expects(self::atLeastOnce())
->method('name')
->withAnyParameters()
->willReturn($feed_name);
- /** @var FreshRSS_FeedDAO&PHPUnit\Framework\MockObject\MockObject */
- $feed_dao = $this->createMock(FreshRSS_FeedDAO::class);
- $feed_dao->expects(self::atLeastOnce())
- ->method('searchById')
+ /** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
+ $cat = $this->createMock(FreshRSS_Category::class);
+ $cat->expects(self::atLeastOnce())
+ ->method('feeds')
->withAnyParameters()
- ->willReturn($feed);
+ ->willReturn([1 => $feed]);
$query = array('get' => 'f_1');
- $user_query = new FreshRSS_UserQuery($query, $feed_dao, null);
+ $user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
self::assertEquals($feed_name, $user_query->getGetName());
self::assertEquals('feed', $user_query->getGetType());
}
public function test__construct_whenUnknownQuery_doesStoreParameters(): void {
$query = array('get' => 'q');
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEmpty($user_query->getGetName());
self::assertEmpty($user_query->getGetType());
}
@@ -70,33 +66,33 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
public function test__construct_whenName_storesName(): void {
$name = 'some name';
$query = array('name' => $name);
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals($name, $user_query->getName());
}
public function test__construct_whenOrder_storesOrder(): void {
$order = 'some order';
$query = array('order' => $order);
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals($order, $user_query->getOrder());
}
public function test__construct_whenState_storesState(): void {
- $state = FreshRSS_Entry::STATE_ALL;
+ $state = FreshRSS_Entry::STATE_NOT_READ | FreshRSS_Entry::STATE_FAVORITE;
$query = array('state' => $state);
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals($state, $user_query->getState());
}
public function test__construct_whenUrl_storesUrl(): void {
$url = 'some url';
$query = array('url' => $url);
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertEquals($url, $user_query->getUrl());
}
public function testToArray_whenNoData_returnsEmptyArray(): void {
- $user_query = new FreshRSS_UserQuery(array());
+ $user_query = new FreshRSS_UserQuery([], [], []);
self::assertCount(0, $user_query->toArray());
}
@@ -109,7 +105,7 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
'state' => FreshRSS_Entry::STATE_ALL,
'url' => 'some url',
);
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertCount(6, $user_query->toArray());
self::assertEquals($query, $user_query->toArray());
}
@@ -118,100 +114,100 @@ class UserQueryTest extends PHPUnit\Framework\TestCase {
$query = array(
'search' => 'some search',
);
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertTrue($user_query->hasSearch());
}
public function testHasSearch_whenNoSearch_returnsFalse(): void {
- $user_query = new FreshRSS_UserQuery(array());
+ $user_query = new FreshRSS_UserQuery([], [], []);
self::assertFalse($user_query->hasSearch());
}
public function testHasParameters_whenAllQuery_returnsFalse(): void {
$query = array('get' => 'a');
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertFalse($user_query->hasParameters());
}
public function testHasParameters_whenNoParameter_returnsFalse(): void {
$query = array();
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertFalse($user_query->hasParameters());
}
public function testHasParameters_whenParameter_returnTrue(): void {
$query = array('get' => 's');
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertTrue($user_query->hasParameters());
}
public function testIsDeprecated_whenCategoryExists_returnFalse(): void {
/** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
$cat = $this->createMock(FreshRSS_Category::class);
- /** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
- $cat_dao = $this->createMock(FreshRSS_CategoryDAO::class);
- $cat_dao->expects(self::atLeastOnce())
- ->method('searchById')
+ $cat->expects(self::atLeastOnce())
+ ->method('name')
->withAnyParameters()
- ->willReturn($cat);
+ ->willReturn('cat 1');
$query = array('get' => 'c_1');
- $user_query = new FreshRSS_UserQuery($query, null, $cat_dao);
+ $user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
self::assertFalse($user_query->isDeprecated());
}
public function testIsDeprecated_whenCategoryDoesNotExist_returnTrue(): void {
- /** @var FreshRSS_CategoryDAO&PHPUnit\Framework\MockObject\MockObject */
- $cat_dao = $this->createMock(FreshRSS_CategoryDAO::class);
- $cat_dao->expects(self::atLeastOnce())
- ->method('searchById')
- ->withAnyParameters()
- ->willReturn(null);
$query = array('get' => 'c_1');
- $user_query = new FreshRSS_UserQuery($query, null, $cat_dao);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertTrue($user_query->isDeprecated());
}
public function testIsDeprecated_whenFeedExists_returnFalse(): void {
/** @var FreshRSS_Feed&PHPUnit\Framework\MockObject\MockObject */
$feed = $this->createMock(FreshRSS_Feed::class);
- /** @var FreshRSS_FeedDAO&PHPUnit\Framework\MockObject\MockObject */
- $feed_dao = $this->createMock(FreshRSS_FeedDAO::class);
- $feed_dao->expects(self::atLeastOnce())
- ->method('searchById')
+ $feed->expects(self::atLeastOnce())
+ ->method('id')
->withAnyParameters()
- ->willReturn($feed);
+ ->willReturn(1);
+ $feed->expects(self::atLeastOnce())
+ ->method('name')
+ ->withAnyParameters()
+ ->willReturn('feed 1');
+ /** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
+ $cat = $this->createMock(FreshRSS_Category::class);
+ $cat->expects(self::atLeastOnce())
+ ->method('feeds')
+ ->withAnyParameters()
+ ->willReturn([1 => $feed]);
$query = array('get' => 'f_1');
- $user_query = new FreshRSS_UserQuery($query, $feed_dao, null);
+ $user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
self::assertFalse($user_query->isDeprecated());
}
public function testIsDeprecated_whenFeedDoesNotExist_returnTrue(): void {
- /** @var FreshRSS_FeedDAO&PHPUnit\Framework\MockObject\MockObject */
- $feed_dao = $this->createMock(FreshRSS_FeedDAO::class);
- $feed_dao->expects(self::atLeastOnce())
- ->method('searchById')
+ /** @var FreshRSS_Category&PHPUnit\Framework\MockObject\MockObject */
+ $cat = $this->createMock(FreshRSS_Category::class);
+ $cat->expects(self::atLeastOnce())
+ ->method('feeds')
->withAnyParameters()
- ->willReturn(null);
+ ->willReturn([]);
$query = array('get' => 'f_1');
- $user_query = new FreshRSS_UserQuery($query, $feed_dao, null);
+ $user_query = new FreshRSS_UserQuery($query, [1 => $cat], []);
self::assertTrue($user_query->isDeprecated());
}
public function testIsDeprecated_whenAllQuery_returnFalse(): void {
$query = array('get' => 'a');
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertFalse($user_query->isDeprecated());
}
public function testIsDeprecated_whenFavoriteQuery_returnFalse(): void {
$query = array('get' => 's');
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertFalse($user_query->isDeprecated());
}
public function testIsDeprecated_whenUnknownQuery_returnFalse(): void {
$query = array('get' => 'q');
- $user_query = new FreshRSS_UserQuery($query);
+ $user_query = new FreshRSS_UserQuery($query, [], []);
self::assertFalse($user_query->isDeprecated());
}