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 $params */ + /** @var array $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('/:"(.*?)"/', ':"\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[\'"])(?P.*)(?P=delim)/U', $input, $matchesFound)) { $all_matches[] = $matchesFound; @@ -60,7 +60,7 @@ class FreshRSS_BooleanSearch { /** @var array */ $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\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 */ $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 + * @return array * @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 $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 $categories + * @return array + */ + 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 $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 $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 $res */ - $cat = self::daoToCategory($res); - return $cat[0] ?? null; + $categories = self::daoToCategories($res); + return reset($categories) ?: null; } - /** @return array */ + /** @return array */ public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false): array { $categories = $this->listCategories($prePopulateFeeds, $details); @@ -277,7 +277,7 @@ SQL; return $categories; } - /** @return array */ + /** @return array */ 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 $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 $res */ - return $res == null ? [] : self::daoToCategory($res); + return empty($res) ? [] : self::daoToCategories($res); } } - /** @return array */ + /** @return array */ 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 $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 $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 $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 $listDAO * @return array */ - 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 $listDAO - * @return array + * @return array */ - 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 */ - public static array $categories = []; + private static array $categories = []; /** * @var array */ - 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 */ + public static function categories(): array { + if (empty(self::$categories)) { + $catDAO = FreshRSS_Factory::createCategoryDao(); + self::$categories = $catDAO->listSortedCategories(true, false); + } + return self::$categories; + } + + /** @return array */ + public static function feeds(): array { + return FreshRSS_Category::findFeeds(self::categories()); + } + + /** @return array */ + 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 @@ -815,6 +815,28 @@ HTML; ]; } + /** + * @return array{array,array} 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 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 $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 $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 $res */ - return empty($res[0]) ? null : (current(self::daoToFeed($res)) ?: null); + return empty($res[0]) ? null : (current(self::daoToFeeds($res)) ?: null); } /** @return array */ @@ -343,14 +343,14 @@ SQL; } /** - * @return array + * @return array */ public function listFeeds(): array { $sql = 'SELECT * FROM `_feed` ORDER BY name'; $res = $this->fetchAssoc($sql); /** @var array|null $res */ - return $res == null ? [] : self::daoToFeed($res); + return $res == null ? [] : self::daoToFeeds($res); } /** @return array */ @@ -375,7 +375,7 @@ SQL; /** * @param int $defaultCacheDuration Use -1 to return all feeds, without filtering them by TTL. - * @return array + * @return array */ 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 + * @return array */ public function listByCategory(int $cat, ?bool $muted = null): array { $sql = 'SELECT * FROM `_feed` WHERE category=:category'; @@ -425,9 +425,9 @@ SQL; * @var array $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 */ - 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|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|null $res */ - return $res === null ? null : self::daoToTag($res)[0] ?? null; + return $res === null ? null : (current(self::daoToTags($res)) ?: null); } - /** @return array|false */ + /** @return array|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 $listDAO - * @return array + * @return array */ - 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 $queries + * @property array $queries * @property bool $reading_confirm * @property int $since_hours_posts_per_rss * @property bool $show_fav_unread @@ -81,6 +81,20 @@ final class FreshRSS_UserConfiguration extends Minz_Configuration { return parent::get('user'); } + /** + * 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|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 $categories */ + private array $categories; + /** @var array $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 $categories + * @param array $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,92 +107,43 @@ class FreshRSS_UserQuery { */ private function parseGet(string $get): void { $this->get = $get; - if (preg_match('/(?P[acfst])(_(?P\d+))?/', $get, $matches)) { + if (preg_match('/(?P[acfistT])(_(?P\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 @@ -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 ? '&' : '&') . 'f=rss'; + } + return ''; + } + + public function sharedUrlHtml(bool $xmlEscaped = true): string { + if ($this->shareRss && $this->token !== '') { + return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&' : '&') . '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 ? '&' : '&') . '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 */ + /** @var array */ public array $categories; public ?FreshRSS_Category $category; public ?FreshRSS_Tag $tag; @@ -18,11 +18,11 @@ class FreshRSS_View extends Minz_View { /** @var iterable */ public $entries; public FreshRSS_Entry $entry; - public ?FreshRSS_Feed $feed; - /** @var array */ + public FreshRSS_Feed $feed; + /** @var array */ public array $feeds; public int $nbUnreadTags; - /** @var array */ + /** @var array */ public array $tags; /** @var array */ 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 */ + /** @var array */ public array $categories; - /** @var array */ + /** @var array */ public array $feeds; - /** @var array */ + /** @var array */ 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 */ + /** @var array */ public array $categories; - public ?FreshRSS_Feed $feed; - /** @var array */ + public FreshRSS_Feed $feed; + /** @var array */ 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', '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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 %s', 'get_favorite' => 'Afficher les articles favoris', 'get_feed' => 'Afficher le flux %s', + 'get_important' => 'Afficher les articles des flux importants', + 'get_label' => 'Afficher les articles avec l’étiquette “%s”', + 'help' => 'Voir la documentation pour les filtres utilisateurs et repartage par HTML / RSS / OPML.', '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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 & 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 documentation for user queries and resharing by HTML / RSS / OPML.', // 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 @@
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; + } ?> - class="darkMode === 'no') ? '' : 'darkMode_' . FreshRSS_Context::userConf()->darkMode ?>"> + 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 @@
  • - - - + + + + + + + + - - - - - - - - - - - - - - - -
  • @@ -210,20 +199,6 @@ - - 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; - } - ?> - - - 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; + } ?> - + - + + + @@ -20,9 +30,15 @@ + +allow_referrer) { ?> - + + rss_url != ''): ?> + + + @@ -30,7 +46,7 @@
    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 @@

    getName() ?>

    + + + 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 @@ ?>

    query->getName() ?>

    - @@ -18,15 +17,53 @@
    - + +
    - + +
    +
    + + query->sharedUrlRss() !== ''): ?> + + +
    +
    + + query->sharedUrlOpml() !== ''): ?> +
      +
    • +
    + +
    +

    +

    +
    + +
    +
    + +
    +
    + +
    +

    @@ -58,22 +95,24 @@
    @@ -83,8 +122,8 @@
    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 @@ feed === null) { - throw new FreshRSS_Context_Exception('Feed not initialised!'); - } ?>

    feed->name() ?>

    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 @@ + + 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 @@ +entry; + $feed = $this->feed; +?> +
    +
    +
    + '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; + } + ?> +
    + + isRead() ? 'read' : 'unread') ?> + isFavorite() ? 'starred' : 'non-starred') ?> + + show_feed_name === 't') { ?> + + show_favicons): ?> + ✇name() ?> + +
    + + show_tags, ['b', 'h'], true)) { + $this->renderHelper('index/tags'); + } + ?> + +

    title() ?>

    + show_author_date === 'h' || FreshRSS_Context::userConf()->show_author_date === 'b') { ?> +
    + show_feed_name === 'a') { ?> + + +
    authors(); + if (is_array($authors)) { + if ($this->internal_rendering): + foreach ($authors as $author): ?> + + + + + + +
    +
    + +
    +
    + +
    + +
    + content(true) ?> +
    + show_author_date, ['b', 'f'], true); + $display_tags = in_array(FreshRSS_Context::userConf()->show_tags, ['b', 'f'], true); + + if ($display_authors_date || $display_tags) { + ?> +
    + +
    + show_feed_name === 'a') { ?> + + +
    authors(); + if (is_array($authors)) { + foreach ($authors as $author) { + ?> + + + + +
    +
    + +
    +
    + renderHelper('index/tags'); + } + ?> +
    + +
    +
    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 @@ 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 @@ +entry->tagsFormattingHelper(); +?> +
    + +
      + + +
    • #
    • + + + +
    • #
    • + + + + +
    • + +
    • + +
    + +
    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 @@ +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; +?> +renderHelper('htmlPagination'); ?> +
    +

    + · + + + +

    + entries as $entry): + $this->entry = $entry; + $this->feed = $this->feeds[$entry->feedId()] ?? + FreshRSS_Category::findFeed($this->categories, $entry->feedId()) ?? + FreshRSS_Feed::default(); + ?> +
    + renderHelper('index/article'); ?> +
    + +
    +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'); ?> -
    +

    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)) { ?>
    " data-priority="feed->priority() ?>">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; - } - } - } ?>
    -
    +
    show_feed_name === 't') { ?> - show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'b') { ?> -
    -
    • #
    • -
    • - -
    • - -
    -
    - show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'b') { + $this->renderHelper('index/tags'); } ?>

    entry->title() ?>

    show_author_date === 'h' || FreshRSS_Context::userConf()->show_author_date === 'b') { ?> @@ -163,8 +111,8 @@ $today = @strtotime('today');
    -
    entry->content(true)) : $this->entry->content(true); +
    lazyload && !FreshRSS_Context::userConf()->display_posts ? lazyimg($this->entry->content(true)) : $this->entry->content(true) ?>
    show_author_date === 'f' || FreshRSS_Context::userConf()->show_author_date === 'b'; @@ -201,36 +149,10 @@ $today = @strtotime('today');
    -
    -
    • #
    • -
    • - -
    • - -
    -
    - + if ($display_tags) { + $this->renderHelper('index/tags'); + } + ?> 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; ?>

    @@ -19,197 +17,21 @@ $MAX_TAGS_DISPLAYED = (int)FreshRSS_Context::userConf()->show_tags_max;
    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(); - ?>
    -
    - -
    -
    - '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; - } - ?> -
    - - isRead() ? 'read' : 'unread') ?> - isFavorite() ? 'starred' : 'non-starred') ?> - - show_feed_name === 't') { ?> - - show_favicons): ?> - ✇name() ?> - -
    - - show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'b') { ?> -
    -
    • #
    • -
    • - -
    • - -
    -
    - - -

    title() ?>

    - show_author_date === 'h' || FreshRSS_Context::userConf()->show_author_date === 'b') { ?> -
    - show_feed_name === 'a') { ?> - - -
    authors(); - if (is_array($authors)) { - foreach ($authors as $author) { - ?> - - - - -
    -
    - -
    -
    - -
    - -
    - content(true) ?> -
    - 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) { - ?> -
    - -
    - show_feed_name === 'a') { ?> - - -
    authors(); - if (is_array($authors)) { - foreach ($authors as $author) { - ?> - - - - -
    -
    - -
    -
    - -
    -
    • #
    • -
    • - -
    • - -
    -
    - -
    - -
    -
    + $this->feed = FreshRSS_Category::findFeed($this->categories, $entry->feedId()) ?? $entry->feed() ?? FreshRSS_Feed::default(); + ?>
    + renderHelper('index/article'); ?>
    <?= $this->rss_title ?> - internal_rendering ? htmlspecialchars($this->rss_url, ENT_NOQUOTES, 'UTF-8') : Minz_Url::display('', 'html', true) ?> + html_url ?> rss_title) ?> GMT - + 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 @@

    'rss', 'params' => array('user' => Minz_User::name(), 'token' => $token, 'hours' => FreshRSS_Context::userConf()->since_hours_posts_per_rss)), 'html', true) ?> +

    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 Binary files /dev/null and b/docs/en/img/users/user-query-share.png 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 $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 @@ +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()); } @@ -44,25 +36,29 @@ class UserQueryTest extends PHPUnit\Framework\TestCase { $feed_name = 'some feed name'; /** @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()); }