New feature: shareable user query (#6052)

* New feature: shareable user query
Share the output of a user query by RSS / HTML / OPML with other people through unique URLs.
Replaces the global admin token, which was the only option (but unsafe) to share RSS outputs with other people.
Also add a new HTML output for people without an RSS reader.

fix https://github.com/FreshRSS/FreshRSS/issues/3066#issuecomment-648977890
fix https://github.com/FreshRSS/FreshRSS/issues/3178#issuecomment-769435504

* Remove unused method

* Fix token saving

* Implement HTML view

* Update i18n for master token

* Revert i18n get_favorite

* Fix missing i18n for user queries from before this PR

* Remove irrelevant tests

* Add link to RSS version

* Fix getGet

* Fix getState

* Fix getSearch

* Alternative getSearch

* Default getOrder

* Explicit default state

* Fix test

* Add OPML sharing

* Remove many redundant SQL queries from original implementation of user queries

* Fix article tags

* Use default user settings

* Prepare public search

* Fixes

* Allow user search on article tags

* Implement user search

* Revert filter bug

* Revert wrong SQL left outer join change

* Implement checkboxes

* Safe check of OPML

* Fix label

* Remove RSS button to favour new sharing method
That sharing button was using a global admin token

* First version of HTTP 304

* Disallow some recusrivity
fix https://github.com/FreshRSS/FreshRSS/issues/6086

* Draft of nav

* Minor httpConditional

* Add support for offset for pagination

* Fix offset pagination

* Fix explicit order ASC

* Add documentation

* Help links i18n

* Note about deprecated master token

* Typo

* Doc about format
This commit is contained in:
Alexandre Alapetite 2024-02-26 09:01:03 +01:00 committed by GitHub
parent 25166c218b
commit 39cc1c11ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
105 changed files with 1477 additions and 809 deletions

View File

@ -15,7 +15,11 @@ Il y a une API pour les clients (mobiles), ainsi quune [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 darticles 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.

View File

@ -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.

View File

@ -301,12 +301,8 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
public function queriesAction(): void {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/draggable.js?' . @filemtime(PUBLIC_PATH . '/scripts/draggable.js')));
$category_dao = FreshRSS_Factory::createCategoryDao();
$feed_dao = FreshRSS_Factory::createFeedDao();
$tag_dao = FreshRSS_Factory::createTagDao();
if (Minz_Request::isPost()) {
/** @var array<int,array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $params */
/** @var array<int,array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $params */
$params = Minz_Request::paramArray('queries');
$queries = [];
@ -318,7 +314,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
if (!empty($query['search'])) {
$query['search'] = urldecode($query['search']);
}
$queries[$key] = (new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao))->toArray();
$queries[$key] = (new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
}
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
@ -327,13 +323,13 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
} else {
$this->view->queries = [];
foreach (FreshRSS_Context::userConf()->queries as $key => $query) {
$this->view->queries[intval($key)] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
$this->view->queries[intval($key)] = new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
}
}
$this->view->categories = $category_dao->listCategories(false) ?: [];
$this->view->feeds = $feed_dao->listFeeds();
$this->view->tags = $tag_dao->listTags() ?: [];
$this->view->categories = FreshRSS_Context::categories();
$this->view->feeds = FreshRSS_Context::feeds();
$this->view->tags = FreshRSS_Context::labels();
if (Minz_Request::paramTernary('id') !== null) {
$id = Minz_Request::paramInt('id');
@ -363,20 +359,21 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
return;
}
$category_dao = FreshRSS_Factory::createCategoryDao();
$feed_dao = FreshRSS_Factory::createFeedDao();
$tag_dao = FreshRSS_Factory::createTagDao();
$query = new FreshRSS_UserQuery(FreshRSS_Context::userConf()->queries[$id], $feed_dao, $category_dao, $tag_dao);
$query = new FreshRSS_UserQuery(FreshRSS_Context::userConf()->queries[$id], FreshRSS_Context::categories(), FreshRSS_Context::labels());
$this->view->query = $query;
$this->view->queryId = $id;
$this->view->categories = $category_dao->listCategories(false) ?: [];
$this->view->feeds = $feed_dao->listFeeds();
$this->view->tags = $tag_dao->listTags() ?: [];
$this->view->categories = FreshRSS_Context::categories();
$this->view->feeds = FreshRSS_Context::feeds();
$this->view->tags = FreshRSS_Context::labels();
if (Minz_Request::isPost()) {
$params = array_filter(Minz_Request::paramArray('query'));
$queryParams = [];
$name = Minz_Request::paramString('name') ?: _t('conf.query.number', $id + 1);
if ('' === $name) {
$name = _t('conf.query.number', $id + 1);
}
$queryParams['name'] = $name;
if (!empty($params['get']) && is_string($params['get'])) {
$queryParams['get'] = htmlspecialchars_decode($params['get'], ENT_QUOTES);
}
@ -389,15 +386,21 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
if (!empty($params['state']) && is_array($params['state'])) {
$queryParams['state'] = (int)(array_sum($params['state']));
}
$name = Minz_Request::paramString('name') ?: _t('conf.query.number', $id + 1);
if ('' === $name) {
$name = _t('conf.query.number', $id + 1);
if (empty($params['token']) || !is_string($params['token'])) {
$queryParams['token'] = FreshRSS_UserQuery::generateToken($name);
} else {
$queryParams['token'] = $params['token'];
}
if (!empty($params['shareRss']) && ctype_digit($params['shareRss'])) {
$queryParams['shareRss'] = (bool)$params['shareRss'];
}
if (!empty($params['shareOpml']) && ctype_digit($params['shareOpml'])) {
$queryParams['shareOpml'] = (bool)$params['shareOpml'];
}
$queryParams['name'] = $name;
$queryParams['url'] = Minz_Url::display(['params' => $queryParams]);
$queries = FreshRSS_Context::userConf()->queries;
$queries[$id] = (new FreshRSS_UserQuery($queryParams, $feed_dao, $category_dao, $tag_dao))->toArray();
$queries[$id] = (new FreshRSS_UserQuery($queryParams, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
@ -433,18 +436,15 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
* lean data.
*/
public function bookmarkQueryAction(): void {
$category_dao = FreshRSS_Factory::createCategoryDao();
$feed_dao = FreshRSS_Factory::createFeedDao();
$tag_dao = FreshRSS_Factory::createTagDao();
$queries = [];
foreach (FreshRSS_Context::userConf()->queries as $key => $query) {
$queries[$key] = (new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao))->toArray();
$queries[$key] = (new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
}
$params = $_GET;
unset($params['rid']);
$params['url'] = Minz_Url::display(['params' => $params]);
$params['name'] = _t('conf.query.number', count($queries) + 1);
$queries[] = (new FreshRSS_UserQuery($params, $feed_dao, $category_dao, $tag_dao))->toArray();
$queries[] = (new FreshRSS_UserQuery($params, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();

View File

@ -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'));
});

View File

@ -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();

View File

@ -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;

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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) ?: [];
}
}

View File

@ -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')));

View File

@ -16,14 +16,12 @@ class FreshRSS_BooleanSearch {
private string $operator;
/** @param 'AND'|'OR'|'AND NOT' $operator */
public function __construct(string $input, int $level = 0, string $operator = 'AND') {
public function __construct(string $input, int $level = 0, string $operator = 'AND', bool $allowUserQueries = true) {
$this->operator = $operator;
$input = trim($input);
if ($input === '') {
return;
}
$this->raw_input = $input;
if ($level === 0) {
$input = preg_replace('/:&quot;(.*?)&quot;/', ':"\1"', $input);
if (!is_string($input)) {
@ -34,9 +32,11 @@ class FreshRSS_BooleanSearch {
return;
}
$input = $this->parseUserQueryNames($input);
$input = $this->parseUserQueryIds($input);
$input = $this->parseUserQueryNames($input, $allowUserQueries);
$input = $this->parseUserQueryIds($input, $allowUserQueries);
$input = trim($input);
}
$this->raw_input = $input;
// Either parse everything as a series of BooleanSearchs combined by implicit AND
// or parse everything as a series of Searchs combined by explicit OR
@ -46,7 +46,7 @@ class FreshRSS_BooleanSearch {
/**
* Parse the user queries (saved searches) by name and expand them in the input string.
*/
private function parseUserQueryNames(string $input): string {
private function parseUserQueryNames(string $input, bool $allowUserQueries = true): string {
$all_matches = [];
if (preg_match_all('/\bsearch:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matchesFound)) {
$all_matches[] = $matchesFound;
@ -60,7 +60,7 @@ class FreshRSS_BooleanSearch {
/** @var array<string,FreshRSS_UserQuery> */
$queries = [];
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
$query = new FreshRSS_UserQuery($raw_query);
$query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
$queries[$query->getName()] = $query;
}
@ -74,7 +74,11 @@ class FreshRSS_BooleanSearch {
$name = trim($matches['search'][$i]);
if (!empty($queries[$name])) {
$fromS[] = $matches[0][$i];
$toS[] = '(' . trim($queries[$name]->getSearch()->getRawInput()) . ')';
if ($allowUserQueries) {
$toS[] = '(' . trim($queries[$name]->getSearch()->getRawInput()) . ')';
} else {
$toS[] = '';
}
}
}
}
@ -87,7 +91,7 @@ class FreshRSS_BooleanSearch {
/**
* Parse the user queries (saved searches) by ID and expand them in the input string.
*/
private function parseUserQueryIds(string $input): string {
private function parseUserQueryIds(string $input, bool $allowUserQueries = true): string {
$all_matches = [];
if (preg_match_all('/\bS:(?P<search>\d+)/', $input, $matchesFound)) {
@ -95,14 +99,10 @@ class FreshRSS_BooleanSearch {
}
if (!empty($all_matches)) {
$category_dao = FreshRSS_Factory::createCategoryDao();
$feed_dao = FreshRSS_Factory::createFeedDao();
$tag_dao = FreshRSS_Factory::createTagDao();
/** @var array<string,FreshRSS_UserQuery> */
$queries = [];
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
$query = new FreshRSS_UserQuery($raw_query, $feed_dao, $category_dao, $tag_dao);
$query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
$queries[] = $query;
}
@ -117,7 +117,11 @@ class FreshRSS_BooleanSearch {
$id = (int)(trim($matches['search'][$i])) - 1;
if (!empty($queries[$id])) {
$fromS[] = $matches[0][$i];
$toS[] = '(' . trim($queries[$id]->getSearch()->getRawInput()) . ')';
if ($allowUserQueries) {
$toS[] = '(' . trim($queries[$id]->getSearch()->getRawInput()) . ')';
} else {
$toS[] = '';
}
}
}
}

View File

@ -95,7 +95,7 @@ class FreshRSS_Category extends Minz_Model {
}
/**
* @return array<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed>
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
@ -110,10 +110,8 @@ class FreshRSS_Category extends Minz_Model {
$this->nbNotRead += $feed->nbNotRead();
$this->hasFeedsWithError |= ($feed->inError() && !$feed->mute());
}
$this->sortFeeds();
}
return $this->feeds ?? [];
}
@ -143,7 +141,6 @@ class FreshRSS_Category extends Minz_Model {
if (!is_array($values)) {
$values = [$values];
}
$this->feeds = $values;
$this->sortFeeds();
}
@ -157,7 +154,6 @@ class FreshRSS_Category extends Minz_Model {
}
$feed->_category($this);
$this->feeds[] = $feed;
$this->sortFeeds();
}
@ -243,8 +239,54 @@ class FreshRSS_Category extends Minz_Model {
if ($this->feeds === null) {
return;
}
usort($this->feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
uasort($this->feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
return strnatcasecmp($a->name(), $b->name());
});
}
/**
* Access cached feed
* @param array<FreshRSS_Category> $categories
*/
public static function findFeed(array $categories, int $feed_id): ?FreshRSS_Feed {
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
if ($feed->id() === $feed_id) {
$feed->_category($category); // Should already be done; just to be safe
return $feed;
}
}
}
return null;
}
/**
* Access cached feeds
* @param array<FreshRSS_Category> $categories
* @return array<int,FreshRSS_Feed>
*/
public static function findFeeds(array $categories): array {
$result = [];
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
$result[$feed->id()] = $feed;
}
}
return $result;
}
/**
* @param array<FreshRSS_Category> $categories
*/
public static function countUnread(array $categories, int $minPriority = 0): int {
$n = 0;
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
if ($feed->priority() >= $minPriority) {
$n += $feed->nbNotRead();
}
}
}
return $n;
}
}

View File

@ -245,19 +245,19 @@ SQL;
$sql = 'SELECT * FROM `_category` WHERE id=:id';
$res = $this->fetchAssoc($sql, ['id' => $id]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
$cat = self::daoToCategory($res);
return $cat[0] ?? null;
$categories = self::daoToCategories($res);
return reset($categories) ?: null;
}
public function searchByName(string $name): ?FreshRSS_Category {
$sql = 'SELECT * FROM `_category` WHERE name=:name';
$res = $this->fetchAssoc($sql, ['name' => $name]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
$cat = self::daoToCategory($res);
return $cat[0] ?? null;
$categories = self::daoToCategories($res);
return reset($categories) ?: null;
}
/** @return array<FreshRSS_Category> */
/** @return array<int,FreshRSS_Category> */
public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false): array {
$categories = $this->listCategories($prePopulateFeeds, $details);
@ -277,7 +277,7 @@ SQL;
return $categories;
}
/** @return array<FreshRSS_Category> */
/** @return array<int,FreshRSS_Category> */
public function listCategories(bool $prePopulateFeeds = true, bool $details = false): array {
if ($prePopulateFeeds) {
$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.kind AS c_kind, c.`lastUpdate` AS c_last_update, c.error AS c_error, c.attributes AS c_attributes, '
@ -293,7 +293,7 @@ SQL;
$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
/** @var array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'category'?:int,'website'?:string,'priority'?:int,'error'?:int|bool,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $res */
return self::daoToCategoryPrepopulated($res);
return self::daoToCategoriesPrepopulated($res);
} else {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
@ -305,11 +305,11 @@ SQL;
} else {
$res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name');
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
return $res == null ? [] : self::daoToCategory($res);
return empty($res) ? [] : self::daoToCategories($res);
}
}
/** @return array<FreshRSS_Category> */
/** @return array<int,FreshRSS_Category> */
public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0): array {
$sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`'
. ($limit < 1 ? '' : ' LIMIT ' . $limit);
@ -318,7 +318,7 @@ SQL;
$stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) &&
$stm->bindValue(':lu', time() - $defaultCacheDuration, PDO::PARAM_INT) &&
$stm->execute()) {
return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC));
return self::daoToCategories($stm->fetchAll(PDO::FETCH_ASSOC));
} else {
$info = $stm ? $stm->errorInfo() : $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
@ -333,9 +333,9 @@ SQL;
$sql = 'SELECT * FROM `_category` WHERE id=:id';
$res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
$cat = self::daoToCategory($res);
if (isset($cat[0])) {
return $cat[0];
$categories = self::daoToCategories($res);
if (isset($categories[self::DEFAULTCATEGORYID])) {
return $categories[self::DEFAULTCATEGORYID];
} else {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n");
@ -394,41 +394,13 @@ SQL;
return isset($res[0]) ? (int)$res[0] : -1;
}
/** @param array<FreshRSS_Category> $categories */
public static function findFeed(array $categories, int $feed_id): ?FreshRSS_Feed {
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
if ($feed->id() === $feed_id) {
$feed->_category($category); // Should already be done; just to be safe
return $feed;
}
}
}
return null;
}
/**
* @param array<FreshRSS_Category> $categories
*/
public static function countUnread(array $categories, int $minPriority = 0): int {
$n = 0;
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
if ($feed->priority() >= $minPriority) {
$n += $feed->nbNotRead();
}
}
}
return $n;
}
/**
* @param array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'website'?:string,'priority'?:int,
* 'error'?:int|bool,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $listDAO
* @return array<int,FreshRSS_Category>
*/
private static function daoToCategoryPrepopulated(array $listDAO): array {
private static function daoToCategoriesPrepopulated(array $listDAO): array {
$list = [];
$previousLine = [];
$feedsDao = [];
@ -441,11 +413,11 @@ SQL;
$cat = new FreshRSS_Category(
$previousLine['c_name'],
$previousLine['c_id'],
$feedDao::daoToFeed($feedsDao, $previousLine['c_id'])
$feedDao::daoToFeeds($feedsDao, $previousLine['c_id'])
);
$cat->_kind($previousLine['c_kind']);
$cat->_attributes($previousLine['c_attributes'] ?? '[]');
$list[(int)$previousLine['c_id']] = $cat;
$list[$cat->id()] = $cat;
$feedsDao = []; //Prepare for next category
}
@ -459,13 +431,13 @@ SQL;
$cat = new FreshRSS_Category(
$previousLine['c_name'],
$previousLine['c_id'],
$feedDao::daoToFeed($feedsDao, $previousLine['c_id'])
$feedDao::daoToFeeds($feedsDao, $previousLine['c_id'])
);
$cat->_kind($previousLine['c_kind']);
$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
$cat->_error($previousLine['c_error'] ?? 0);
$cat->_attributes($previousLine['c_attributes'] ?? []);
$list[(int)$previousLine['c_id']] = $cat;
$list[$cat->id()] = $cat;
}
return $list;
@ -473,11 +445,10 @@ SQL;
/**
* @param array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $listDAO
* @return array<FreshRSS_Category>
* @return array<int,FreshRSS_Category>
*/
private static function daoToCategory(array $listDAO): array {
private static function daoToCategories(array $listDAO): array {
$list = [];
foreach ($listDAO as $dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'lastUpdate', 'error']);
$cat = new FreshRSS_Category(
@ -488,9 +459,8 @@ SQL;
$cat->_lastUpdate($dao['lastUpdate'] ?? 0);
$cat->_error($dao['error'] ?? 0);
$cat->_attributes($dao['attributes'] ?? '');
$list[] = $cat;
$list[$cat->id()] = $cat;
}
return $list;
}
}

View File

@ -10,11 +10,11 @@ final class FreshRSS_Context {
/**
* @var array<int,FreshRSS_Category>
*/
public static array $categories = [];
private static array $categories = [];
/**
* @var array<int,FreshRSS_Tag>
*/
public static array $tags = [];
private static array $tags = [];
public static string $name = '';
public static string $description = '';
public static int $total_unread = 0;
@ -47,6 +47,7 @@ final class FreshRSS_Context {
*/
public static string $order = 'DESC';
public static int $number = 0;
public static int $offset = 0;
public static FreshRSS_BooleanSearch $search;
public static string $first_id = '';
public static string $next_id = '';
@ -173,10 +174,33 @@ final class FreshRSS_Context {
FreshRSS_Context::$user_conf = null;
}
/** @return array<int,FreshRSS_Category> */
public static function categories(): array {
if (empty(self::$categories)) {
$catDAO = FreshRSS_Factory::createCategoryDao();
self::$categories = $catDAO->listSortedCategories(true, false);
}
return self::$categories;
}
/** @return array<int,FreshRSS_Feed> */
public static function feeds(): array {
return FreshRSS_Category::findFeeds(self::categories());
}
/** @return array<int,FreshRSS_Tag> */
public static function labels(bool $precounts = false): array {
if (empty(self::$tags) || $precounts) {
$tagDAO = FreshRSS_Factory::createTagDao();
self::$tags = $tagDAO->listTags($precounts) ?: [];
}
return self::$tags;
}
/**
* This action updates the Context object by using request parameters.
*
* Parameters are:
* HTTP GET request parameters are:
* - state (default: conf->default_view)
* - search (default: empty string)
* - order (default: conf->sort_order)
@ -187,18 +211,15 @@ final class FreshRSS_Context {
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
public static function updateUsingRequest(): void {
if (empty(self::$categories)) {
$catDAO = FreshRSS_Factory::createCategoryDao();
self::$categories = $catDAO->listSortedCategories();
public static function updateUsingRequest(bool $computeStatistics): void {
if ($computeStatistics && self::$total_unread === 0) {
// Update number of read / unread variables.
$entryDAO = FreshRSS_Factory::createEntryDao();
self::$total_starred = $entryDAO->countUnreadReadFavorites();
self::$total_unread = FreshRSS_Category::countUnread(self::categories(), FreshRSS_Feed::PRIORITY_MAIN_STREAM);
self::$total_important_unread = FreshRSS_Category::countUnread(self::categories(), FreshRSS_Feed::PRIORITY_IMPORTANT);
}
// Update number of read / unread variables.
$entryDAO = FreshRSS_Factory::createEntryDao();
self::$total_starred = $entryDAO->countUnreadReadFavorites();
self::$total_unread = FreshRSS_CategoryDAO::countUnread(self::$categories, FreshRSS_Feed::PRIORITY_MAIN_STREAM);
self::$total_important_unread = FreshRSS_CategoryDAO::countUnread(self::$categories, FreshRSS_Feed::PRIORITY_IMPORTANT);
self::_get(Minz_Request::paramString('get') ?: 'a');
self::$state = Minz_Request::paramInt('state') ?: FreshRSS_Context::userConf()->default_state;
@ -224,6 +245,7 @@ final class FreshRSS_Context {
FreshRSS_Context::userConf()->max_posts_per_rss,
FreshRSS_Context::userConf()->posts_per_page);
}
self::$offset = Minz_Request::paramInt('offset');
self::$first_id = Minz_Request::paramString('next');
self::$sinceHours = Minz_Request::paramInt('hours');
}
@ -394,7 +416,7 @@ final class FreshRSS_Context {
break;
case 'f':
// We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description
$feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
$feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_Category::findFeed(self::$categories, $id);
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
@ -417,7 +439,7 @@ final class FreshRSS_Context {
if ($cat === null) {
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
}
//self::$categories[$id] = $cat;
self::$categories[$id] = $cat;
} else {
$cat = self::$categories[$id];
}
@ -433,7 +455,7 @@ final class FreshRSS_Context {
if ($tag === null) {
throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
}
//self::$tags[$id] = $tag;
self::$tags[$id] = $tag;
} else {
$tag = self::$tags[$id];
}

View File

@ -815,6 +815,28 @@ HTML;
];
}
/**
* @return array{array<string>,array<string>} Array of first tags to show, then array of remaining tags
*/
public function tagsFormattingHelper(): array {
$firstTags = [];
$remainingTags = [];
if (FreshRSS_Context::hasUserConf() && in_array(FreshRSS_Context::userConf()->show_tags, ['b', 'f', 'h'], true)) {
$maxTagsDisplayed = (int)FreshRSS_Context::userConf()->show_tags_max;
$tags = $this->tags();
if (!empty($tags)) {
if ($maxTagsDisplayed > 0) {
$firstTags = array_slice($tags, 0, $maxTagsDisplayed);
$remainingTags = array_slice($tags, $maxTagsDisplayed);
} else {
$firstTags = $tags;
}
}
}
return [$firstTags,$remainingTags];
}
/**
* Integer format conversion for Google Reader API format
* @param string|int $dec Decimal number

View File

@ -1063,7 +1063,7 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
int $date_min = 0): array {
if (!$state) {
$state = FreshRSS_Entry::STATE_ALL;
@ -1120,7 +1120,9 @@ SQL;
. 'WHERE ' . $where
. $search
. 'ORDER BY e.id ' . $order
. ($limit > 0 ? ' LIMIT ' . intval($limit) : '')]; //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
. ($limit > 0 ? ' LIMIT ' . $limit : '') // http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
. ($offset > 0 ? ' OFFSET ' . $offset : '')
];
}
/**
@ -1131,9 +1133,9 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
int $date_min = 0) {
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
if ($order !== 'DESC' && $order !== 'ASC') {
$order = 'DESC';
@ -1152,7 +1154,7 @@ SQL;
} else {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
return $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
@ -1167,9 +1169,9 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
string $order = 'DESC', int $limit = 1, string $firstId = '',
string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '',
?FreshRSS_BooleanSearch $filters = null, int $date_min = 0): Traversable {
$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
if ($stm) {
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
if (is_array($row)) {
@ -1233,9 +1235,9 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null): ?array {
string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null): ?array {
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters);
$stm = $this->pdo->prepare($sql);
if ($stm !== false && $stm->execute($values) && ($res = $stm->fetchAll(PDO::FETCH_COLUMN, 0)) !== false) {
/** @var array<numeric-string> $res */

View File

@ -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 {

View File

@ -322,7 +322,7 @@ SQL;
}
/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
$feeds = self::daoToFeed($res);
$feeds = self::daoToFeeds($res);
return $feeds[$id] ?? null;
}
@ -331,7 +331,7 @@ SQL;
$res = $this->fetchAssoc($sql, [':url' => $url]);
/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
return empty($res[0]) ? null : (current(self::daoToFeed($res)) ?: null);
return empty($res[0]) ? null : (current(self::daoToFeeds($res)) ?: null);
}
/** @return array<int> */
@ -343,14 +343,14 @@ SQL;
}
/**
* @return array<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed>
*/
public function listFeeds(): array {
$sql = 'SELECT * FROM `_feed` ORDER BY name';
$res = $this->fetchAssoc($sql);
/** @var array<array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'ttl':int,'attributes':string}>|null $res */
return $res == null ? [] : self::daoToFeed($res);
return $res == null ? [] : self::daoToFeeds($res);
}
/** @return array<string,string> */
@ -375,7 +375,7 @@ SQL;
/**
* @param int $defaultCacheDuration Use -1 to return all feeds, without filtering them by TTL.
* @return array<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed>
*/
public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0): array {
$sql = 'SELECT id, url, kind, category, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes, `cache_nbEntries`, `cache_nbUnreads` '
@ -387,7 +387,7 @@ SQL;
. ($limit < 1 ? '' : 'LIMIT ' . intval($limit));
$stm = $this->pdo->query($sql);
if ($stm !== false) {
return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
return self::daoToFeeds($stm->fetchAll(PDO::FETCH_ASSOC));
} else {
$info = $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
@ -409,7 +409,7 @@ SQL;
/**
* @param bool|null $muted to include only muted feeds
* @return array<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed>
*/
public function listByCategory(int $cat, ?bool $muted = null): array {
$sql = 'SELECT * FROM `_feed` WHERE category=:category';
@ -425,9 +425,9 @@ SQL;
* @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res
*/
$feeds = self::daoToFeed($res);
$feeds = self::daoToFeeds($res);
usort($feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
uasort($feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
return strnatcasecmp($a->name(), $b->name());
});
@ -585,7 +585,7 @@ SQL;
* 'pathEntries'?:string,'httpAuth'?:string,'error'?:int|bool,'ttl'?:int,'attributes'?:string,'cache_nbUnreads'?:int,'cache_nbEntries'?:int}> $listDAO
* @return array<int,FreshRSS_Feed>
*/
public static function daoToFeed(array $listDAO, ?int $catID = null): array {
public static function daoToFeeds(array $listDAO, ?int $catID = null): array {
$list = [];
foreach ($listDAO as $key => $dao) {

View File

@ -184,16 +184,16 @@ SQL;
public function searchById(int $id): ?FreshRSS_Tag {
$res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE id=:id', [':id' => $id]);
/** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */
return $res === null ? null : self::daoToTag($res)[0] ?? null;
return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
public function searchByName(string $name): ?FreshRSS_Tag {
$res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE name=:name', [':name' => $name]);
/** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */
return $res === null ? null : self::daoToTag($res)[0] ?? null;
return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
/** @return array<FreshRSS_Tag>|false */
/** @return array<int,FreshRSS_Tag>|false */
public function listTags(bool $precounts = false) {
if ($precounts) {
$sql = <<<'SQL'
@ -211,7 +211,7 @@ SQL;
$stm = $this->pdo->query($sql);
if ($stm !== false) {
$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
return self::daoToTag($res);
return self::daoToTags($res);
} else {
$info = $this->pdo->errorInfo();
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
@ -430,9 +430,9 @@ SQL;
/**
* @param iterable<array{'id':int,'name':string,'attributes'?:string}> $listDAO
* @return array<FreshRSS_Tag>
* @return array<int,FreshRSS_Tag>
*/
private static function daoToTag(iterable $listDAO): array {
private static function daoToTags(iterable $listDAO): array {
$list = [];
foreach ($listDAO as $dao) {
if (empty($dao['id']) || empty($dao['name'])) {
@ -446,7 +446,7 @@ SQL;
if (isset($dao['unreads'])) {
$tag->_nbUnread($dao['unreads']);
}
$list[] = $tag;
$list[$tag->id()] = $tag;
}
return $list;
}

View File

@ -41,7 +41,7 @@ declare(strict_types=1);
* @property bool $onread_jump_next
* @property string $passwordHash
* @property int $posts_per_page
* @property array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries
* @property array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $queries
* @property bool $reading_confirm
* @property int $since_hours_posts_per_rss
* @property bool $show_fav_unread
@ -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<int|string,mixed>|null

View File

@ -18,17 +18,34 @@ class FreshRSS_UserQuery {
private FreshRSS_BooleanSearch $search;
private int $state = 0;
private string $url = '';
private ?FreshRSS_FeedDAO $feed_dao;
private ?FreshRSS_CategoryDAO $category_dao;
private ?FreshRSS_TagDAO $tag_dao;
private string $token = '';
private bool $shareRss = false;
private bool $shareOpml = false;
/** @var array<int,FreshRSS_Category> $categories */
private array $categories;
/** @var array<int,FreshRSS_Tag> $labels */
private array $labels;
public static function generateToken(string $salt): string {
if (!FreshRSS_Context::hasSystemConf()) {
return '';
}
$hash = md5(FreshRSS_Context::systemConf()->salt . $salt . random_bytes(16));
if (function_exists('gmp_init')) {
// Shorten the hash if possible by converting from base 16 to base 62
$hash = gmp_strval(gmp_init($hash, 16), 62);
}
return $hash;
}
/**
* @param array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string} $query
* @param array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,shareRss?:bool,shareOpml?:bool} $query
* @param array<int,FreshRSS_Category> $categories
* @param array<int,FreshRSS_Tag> $labels
*/
public function __construct(array $query, FreshRSS_FeedDAO $feed_dao = null, FreshRSS_CategoryDAO $category_dao = null, FreshRSS_TagDAO $tag_dao = null) {
$this->category_dao = $category_dao;
$this->feed_dao = $feed_dao;
$this->tag_dao = $tag_dao;
public function __construct(array $query, array $categories, array $labels) {
$this->categories = $categories;
$this->labels = $labels;
if (isset($query['get'])) {
$this->parseGet($query['get']);
}
@ -49,8 +66,18 @@ class FreshRSS_UserQuery {
if (!isset($query['search'])) {
$query['search'] = '';
}
if (!empty($query['token'])) {
$this->token = $query['token'];
}
if (isset($query['shareRss'])) {
$this->shareRss = $query['shareRss'];
}
if (isset($query['shareOpml'])) {
$this->shareOpml = $query['shareOpml'];
}
// linked too deeply with the search object, need to use dependency injection
$this->search = new FreshRSS_BooleanSearch($query['search']);
$this->search = new FreshRSS_BooleanSearch($query['search'], 0, 'AND', false);
if (!empty($query['state'])) {
$this->state = intval($query['state']);
}
@ -59,16 +86,19 @@ class FreshRSS_UserQuery {
/**
* Convert the current object to an array.
*
* @return array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}
* @return array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}
*/
public function toArray(): array {
return array_filter([
'get' => $this->get,
'name' => $this->name,
'order' => $this->order,
'search' => $this->search->__toString(),
'search' => $this->search->getRawInput(),
'state' => $this->state,
'url' => $this->url,
'token' => $this->token,
'shareRss' => $this->shareRss,
'shareOpml' => $this->shareOpml,
]);
}
@ -77,92 +107,43 @@ class FreshRSS_UserQuery {
*/
private function parseGet(string $get): void {
$this->get = $get;
if (preg_match('/(?P<type>[acfst])(_(?P<id>\d+))?/', $get, $matches)) {
if (preg_match('/(?P<type>[acfistT])(_(?P<id>\d+))?/', $get, $matches)) {
$id = intval($matches['id'] ?? '0');
switch ($matches['type']) {
case 'a':
$this->parseAll();
$this->get_type = 'all';
break;
case 'c':
$this->parseCategory($id);
$this->get_type = 'category';
$c = $this->categories[$id] ?? null;
$this->get_name = $c === null ? '' : $c->name();
break;
case 'f':
$this->parseFeed($id);
$this->get_type = 'feed';
$f = FreshRSS_Category::findFeed($this->categories, $id);
$this->get_name = $f === null ? '' : $f->name();
break;
case 'i':
$this->get_type = 'important';
break;
case 's':
$this->parseFavorite();
$this->get_type = 'favorite';
break;
case 't':
$this->parseTag($id);
$this->get_type = 'label';
$l = $this->labels[$id] ?? null;
$this->get_name = $l === null ? '' : $l->name();
break;
case 'T':
$this->get_type = 'all_labels';
break;
}
if ($this->get_name === '' && in_array($matches['type'], ['c', 'f', 't'], true)) {
$this->deprecated = true;
}
}
}
/**
* Parse the query string when it is an "all" query
*/
private function parseAll(): void {
$this->get_name = 'all';
$this->get_type = 'all';
}
/**
* Parse the query string when it is a "category" query
*/
private function parseCategory(int $id): void {
if ($this->category_dao === null) {
$this->category_dao = FreshRSS_Factory::createCategoryDao();
}
$category = $this->category_dao->searchById($id);
if ($category !== null) {
$this->get_name = $category->name();
} else {
$this->deprecated = true;
}
$this->get_type = 'category';
}
/**
* Parse the query string when it is a "feed" query
*/
private function parseFeed(int $id): void {
if ($this->feed_dao === null) {
$this->feed_dao = FreshRSS_Factory::createFeedDao();
}
$feed = $this->feed_dao->searchById($id);
if ($feed !== null) {
$this->get_name = $feed->name();
} else {
$this->deprecated = true;
}
$this->get_type = 'feed';
}
/**
* Parse the query string when it is a "tag" query
*/
private function parseTag(int $id): void {
if ($this->tag_dao === null) {
$this->tag_dao = FreshRSS_Factory::createTagDao();
}
$tag = $this->tag_dao->searchById($id);
if ($tag !== null) {
$this->get_name = $tag->name();
} else {
$this->deprecated = true;
}
$this->get_type = 'tag';
}
/**
* Parse the query string when it is a "favorite" query
*/
private function parseFavorite(): void {
$this->get_name = 'favorite';
$this->get_type = 'favorite';
}
/**
* Check if the current user query is deprecated.
* It is deprecated if the category or the feed used in the query are
@ -219,7 +200,7 @@ class FreshRSS_UserQuery {
}
public function getOrder(): string {
return $this->order;
return $this->order ?: FreshRSS_Context::userConf()->sort_order;
}
public function getSearch(): FreshRSS_BooleanSearch {
@ -227,11 +208,74 @@ class FreshRSS_UserQuery {
}
public function getState(): int {
return $this->state;
$state = $this->state;
if (!($state & FreshRSS_Entry::STATE_READ) && !($state & FreshRSS_Entry::STATE_NOT_READ)) {
$state |= FreshRSS_Entry::STATE_READ | FreshRSS_Entry::STATE_NOT_READ;
}
if (!($state & FreshRSS_Entry::STATE_FAVORITE) && !($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) {
$state |= FreshRSS_Entry::STATE_FAVORITE | FreshRSS_Entry::STATE_NOT_FAVORITE;
}
return $state;
}
public function getUrl(): string {
return $this->url;
}
public function getToken(): string {
return $this->token;
}
public function setToken(string $token): void {
$this->token = $token;
}
public function setShareRss(bool $shareRss): void {
$this->shareRss = $shareRss;
}
public function shareRss(): bool {
return $this->shareRss;
}
public function setShareOpml(bool $shareOpml): void {
$this->shareOpml = $shareOpml;
}
public function shareOpml(): bool {
return $this->shareOpml;
}
protected function sharedUrl(bool $xmlEscaped = true): string {
$currentUser = Minz_User::name() ?? '';
return Minz_Url::display("/api/query.php?user={$currentUser}&t={$this->token}", $xmlEscaped ? 'html' : '', true);
}
public function sharedUrlRss(bool $xmlEscaped = true): string {
if ($this->shareRss && $this->token !== '') {
return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=rss';
}
return '';
}
public function sharedUrlHtml(bool $xmlEscaped = true): string {
if ($this->shareRss && $this->token !== '') {
return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=html';
}
return '';
}
/**
* OPML is only safe for some query types, otherwise it risks leaking unwanted feed information.
*/
public function safeForOpml(): bool {
return in_array($this->get_type, ['all', 'category', 'feed'], true);
}
public function sharedUrlOpml(bool $xmlEscaped = true): string {
if ($this->shareOpml && $this->token !== '' && $this->safeForOpml()) {
return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=opml';
}
return '';
}
}

View File

@ -10,7 +10,7 @@ class FreshRSS_View extends Minz_View {
public $callbackBeforeFeeds;
/** @var callable */
public $callbackBeforePagination;
/** @var array<FreshRSS_Category> */
/** @var array<int,FreshRSS_Category> */
public array $categories;
public ?FreshRSS_Category $category;
public ?FreshRSS_Tag $tag;
@ -18,11 +18,11 @@ class FreshRSS_View extends Minz_View {
/** @var iterable<FreshRSS_Entry> */
public $entries;
public FreshRSS_Entry $entry;
public ?FreshRSS_Feed $feed;
/** @var array<FreshRSS_Feed> */
public FreshRSS_Feed $feed;
/** @var array<int,FreshRSS_Feed> */
public array $feeds;
public int $nbUnreadTags;
/** @var array<FreshRSS_Tag> */
/** @var array<int,FreshRSS_Tag> */
public array $tags;
/** @var array<int,array{'id':int,'name':string,'id_entry':string,'checked':bool}> */
public array $tagsForEntry;
@ -100,6 +100,8 @@ class FreshRSS_View extends Minz_View {
public int $nbPage;
// RSS view
public FreshRSS_UserQuery $userQuery;
public string $html_url = '';
public string $rss_title = '';
public string $rss_url = '';
public string $rss_base = '';

View File

@ -3,11 +3,11 @@ declare(strict_types=1);
final class FreshRSS_ViewJavascript extends FreshRSS_View {
/** @var array<FreshRSS_Category> */
/** @var array<int,FreshRSS_Category> */
public array $categories;
/** @var array<FreshRSS_Feed> */
/** @var array<int,FreshRSS_Feed> */
public array $feeds;
/** @var array<FreshRSS_Tag> */
/** @var array<int,FreshRSS_Tag> */
public array $tags;
public string $nonce;

View File

@ -3,10 +3,10 @@ declare(strict_types=1);
final class FreshRSS_ViewStats extends FreshRSS_View {
/** @var array<FreshRSS_Category> */
/** @var array<int,FreshRSS_Category> */
public array $categories;
public ?FreshRSS_Feed $feed;
/** @var array<FreshRSS_Feed> */
public FreshRSS_Feed $feed;
/** @var array<int,FreshRSS_Feed> */
public array $feeds;
public bool $displaySlider = false;

View File

@ -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;
}

View File

@ -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'])

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Zobrazit podle kanálu',
'order' => 'Seřadit podle data',
'search' => 'Výraz',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Stav',
'tags' => 'Zobrazit podle štítku',
'type' => 'Typ',
),
'get_all' => 'Zobrazit všechny články',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Zobrazit kategorii „%s“',
'get_favorite' => 'Zobrazit oblíbené články',
'get_feed' => 'Zobrazit kanál „%s“',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Název',
'no_filter' => 'Žádný filtr',
'number' => 'Dotaz č. %d',
'order_asc' => 'Zobrazit nejdříve nejstarší články',
'order_desc' => 'Zobrazit nejdříve nejnovější články',
'search' => 'Hledat „%s“',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Zobrazit všechny články',
'state_1' => 'Zobrazit přečtené články',
'state_2' => 'Zobrazit nepřečtené články',

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Nach Feed filtern',
'order' => 'Nach Datum sortieren',
'search' => 'Suchbegriff',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Eigenschaft',
'tags' => 'Nach Labels filtern',
'type' => 'Filter-Typ',
),
'get_all' => 'Alle Artikel anzeigen',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Kategorie „%s“ anzeigen',
'get_favorite' => 'Lieblingsartikel anzeigen',
'get_feed' => 'Feed „%s“ anzeigen',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Name', // IGNORE
'no_filter' => 'Kein Filter',
'number' => 'Abfrage Nr. %d',
'order_asc' => 'Älteste Artikel zuerst anzeigen',
'order_desc' => 'Neueste Artikel zuerst anzeigen',
'search' => 'Suche nach „%s“',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Alle Artikel anzeigen',
'state_1' => 'Gelesene Artikel anzeigen',
'state_2' => 'Ungelesene Artikel anzeigen',

View File

@ -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' => 'Επιτρέψτε την μη ασφαλή αυτόματη σύνδεση με την χρήση της μορφής: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Display by feed', // TODO
'order' => 'Sort by date', // TODO
'search' => 'Expression', // TODO
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'State', // TODO
'tags' => 'Display by label', // TODO
'type' => 'Type', // TODO
),
'get_all' => 'Display all articles', // TODO
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Display “%s” category', // TODO
'get_favorite' => 'Display favourite articles', // TODO
'get_feed' => 'Display “%s” feed', // TODO
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Name', // TODO
'no_filter' => 'No filter', // TODO
'number' => 'Query n°%d', // TODO
'order_asc' => 'Display oldest articles first', // TODO
'order_desc' => 'Display newest articles first', // TODO
'search' => 'Search for “%s”', // TODO
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Display all articles', // TODO
'state_1' => 'Display read articles', // TODO
'state_2' => 'Display unread articles', // TODO

View File

@ -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
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Display by feed', // IGNORE
'order' => 'Sort by date', // IGNORE
'search' => 'Expression', // IGNORE
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // IGNORE
'shareRss' => 'Enable sharing by HTML &amp; RSS', // IGNORE
'state' => 'State', // IGNORE
'tags' => 'Display by label', // IGNORE
'type' => 'Type', // IGNORE
),
'get_all' => 'Display all articles', // IGNORE
'get_all_labels' => 'Display articles with any label', // IGNORE
'get_category' => 'Display “%s” category', // IGNORE
'get_favorite' => 'Display favorite articles',
'get_feed' => 'Display “%s” feed', // IGNORE
'get_important' => 'Display articles from important feeds', // IGNORE
'get_label' => 'Display articles with “%s” label', // IGNORE
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // IGNORE
'name' => 'Name', // IGNORE
'no_filter' => 'No filter', // IGNORE
'number' => 'Query n°%d', // IGNORE
'order_asc' => 'Display oldest articles first', // IGNORE
'order_desc' => 'Display newest articles first', // IGNORE
'search' => 'Search for “%s”', // IGNORE
'share' => array(
'_' => 'Share this query by link', // IGNORE
'help' => 'Give this link if you want to share this query with anyone', // IGNORE
'html' => 'Shareable link to the HTML page', // IGNORE
'opml' => 'Shareable link to the OPML list of feeds', // IGNORE
'rss' => 'Shareable link to the RSS feed', // IGNORE
),
'state_0' => 'Display all articles', // IGNORE
'state_1' => 'Display read articles', // IGNORE
'state_2' => 'Display unread articles', // IGNORE

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Display by feed',
'order' => 'Sort by date',
'search' => 'Expression',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds',
'shareRss' => 'Enable sharing by HTML &amp; RSS',
'state' => 'State',
'tags' => 'Display by label',
'type' => 'Type',
),
'get_all' => 'Display all articles',
'get_all_labels' => 'Display articles with any label',
'get_category' => 'Display “%s” category',
'get_favorite' => 'Display favourite articles',
'get_feed' => 'Display “%s” feed',
'get_important' => 'Display articles from important feeds',
'get_label' => 'Display articles with “%s” label',
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.',
'name' => 'Name',
'no_filter' => 'No filter',
'number' => 'Query n°%d',
'order_asc' => 'Display oldest articles first',
'order_desc' => 'Display newest articles first',
'search' => 'Search for “%s”',
'share' => array(
'_' => 'Share this query by link',
'help' => 'Give this link if you want to share this query with anyone',
'html' => 'Shareable link to the HTML page',
'opml' => 'Shareable link to the OPML list of feeds',
'rss' => 'Shareable link to the RSS feed',
),
'state_0' => 'Display all articles',
'state_1' => 'Display read articles',
'state_2' => 'Display unread articles',

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Mostrar por feed',
'order' => 'Ordenar por fecha',
'search' => 'Expresión',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Estado',
'tags' => 'Mostrar por etiqueta',
'type' => 'Tipo',
),
'get_all' => 'Mostrar todos los artículos',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Mostrar la categoría “%s”',
'get_favorite' => 'Mostrar artículos favoritos',
'get_feed' => 'Mostrar fuente “%s”',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Nombre',
'no_filter' => 'Sin filtro',
'number' => 'Consulta n° %d',
'order_asc' => 'Mostrar primero los artículos más antiguos',
'order_desc' => 'Mostrar primero los artículos más recientes',
'search' => 'Buscar “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Mostrar todos los artículos',
'state_1' => 'Mostrar artículos leídos',
'state_2' => 'Mostrar artículos pendientes',

View File

@ -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' => ' اجازه ورود خودکار ناامن را با استفاده از قالب:',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => ' نمایش با فید',
'order' => ' مرتب سازی بر اساس تاریخ',
'search' => ' بیان',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => ' ایالت',
'tags' => ' نمایش بر اساس برچسب',
'type' => ' نوع',
),
'get_all' => ' نمایش همه مقالات',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => ' دسته «%s» را نمایش دهید',
'get_favorite' => ' نمایش مقالات مورد علاقه',
'get_feed' => ' فید "%s" را نمایش دهید',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => ' نام',
'no_filter' => ' بدون فیلتر',
'number' => ' پرس و جو n°%d',
'order_asc' => ' ابتدا قدیمی ترین مقالات را نمایش دهید',
'order_desc' => ' ابتدا جدیدترین مقالات را نمایش دهید',
'search' => ' «%s» را جستجو کنید',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'نمایش همه مقالات',
'state_1' => 'نمایش مقالات خوانده شده',
'state_2' => 'نمایش مقالات خوانده نشده',

View File

@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (pour utilisateurs avancés avec HTTPS)',
'none' => 'Aucune (dangereux)',
'title' => 'Authentification',
'token' => 'Jeton didentification',
'token_help' => 'Permet daccéder à la sortie RSS de lutilisateur par défaut sans besoin de sauthentifier :',
'token' => 'Jeton didentification maître',
'token_help' => 'Permet daccéder à toutes les sorties RSS de lutilisateur et au rafraîchissement des flux sans besoin de sauthentifier :',
'type' => 'Méthode dauthentification',
'unsafe_autologin' => 'Autoriser les connexions automatiques non-sûres au format : ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Afficher par flux',
'order' => 'Tri par date',
'search' => 'Expression', // IGNORE
'shareOpml' => 'Active le partage par OPML des catégories et flux correspondants',
'shareRss' => 'Active le partage par HTML &amp; RSS',
'state' => 'État',
'tags' => 'Afficher par étiquette',
'type' => 'Type', // IGNORE
),
'get_all' => 'Afficher tous les articles',
'get_all_labels' => 'Afficher les articles avec une étiquette',
'get_category' => 'Afficher la catégorie <em>%s<em>',
'get_favorite' => 'Afficher les articles favoris',
'get_feed' => 'Afficher le flux <em>%s</em>',
'get_important' => 'Afficher les articles des flux importants',
'get_label' => 'Afficher les articles avec létiquette “%s”',
'help' => 'Voir la <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation pour les filtres utilisateurs et repartage par HTML / RSS / OPML</a>.',
'name' => 'Nom',
'no_filter' => 'Aucun filtre appliqué',
'number' => 'Filtre n°%d',
'order_asc' => 'Afficher les articles les plus anciens en premier',
'order_desc' => 'Afficher les articles les plus récents en premier',
'search' => 'Recherche de « %s »',
'share' => array(
'_' => 'Partager ce filtre par lien',
'help' => 'Donner ce lien pour partager le contenu du filtre avec dautres 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',

View File

@ -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' => 'הרשאה להתחברות אוטומטית בפורמט: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Display by feed', // TODO
'order' => 'Sort by date', // TODO
'search' => 'Expression', // TODO
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'State', // TODO
'tags' => 'Display by label', // TODO
'type' => 'Type', // TODO
),
'get_all' => 'הצגת כל המאמרים',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'הצגת קטגוריה “%s”',
'get_favorite' => 'הצגת מאמרים מועדפים',
'get_feed' => 'הצגת הזנה %s',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Name', // TODO
'no_filter' => 'ללא סינון',
'number' => 'שאילתה מספר °%d',
'order_asc' => 'הצגת מאמרים ישנים בראש',
'order_desc' => 'הצגת מאמרים חדשים בראש',
'search' => 'חיפוש “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'הצגת כל המאמרים',
'state_1' => 'הצגת מאמרים שנקראו',
'state_2' => 'הצגת מאמרים שלא נקראו',

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Rendezés hírforrás szerint',
'order' => 'Rendezés dátum szerint',
'search' => 'Kifejezés',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Státusz',
'tags' => 'Rendezés címke szerint',
'type' => 'Típus',
),
'get_all' => 'Minden cikk megjelenítése',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Listáz “%s” kategóriát',
'get_favorite' => 'Kedvenc cikkek megjelenítése',
'get_feed' => 'Listáz “%s” hírforrást',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Név',
'no_filter' => 'Nincs szűrés',
'number' => 'Lekérdezés %d',
'order_asc' => 'Régebbi cikkek előre',
'order_desc' => 'Újabb cikkek előre',
'search' => 'Keresse a “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Minden cikk',
'state_1' => 'Olvasott cikkek',
'state_2' => 'Olvasatlan cikkek',

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Display by feed', // TODO
'order' => 'Sort by date', // TODO
'search' => 'Expression', // TODO
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'State', // TODO
'tags' => 'Display by label', // TODO
'type' => 'Type', // TODO
),
'get_all' => 'Display all articles', // TODO
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Display “%s” category', // TODO
'get_favorite' => 'Display favorite articles',
'get_favorite' => 'Display favourite articles', // TODO
'get_feed' => 'Display “%s” feed', // TODO
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Name', // TODO
'no_filter' => 'No filter', // TODO
'number' => 'Query n°%d', // TODO
'order_asc' => 'Display oldest articles first', // TODO
'order_desc' => 'Display newest articles first', // TODO
'search' => 'Search for “%s”', // TODO
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Display all articles', // TODO
'state_1' => 'Display read articles', // TODO
'state_2' => 'Display unread articles', // TODO

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Mostra per feed',
'order' => 'Ordina per data',
'search' => 'Espressione',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Stato',
'tags' => 'Mostra per tag', // DIRTY
'type' => 'Tipo',
),
'get_all' => 'Mostra tutti gli articoli',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Mostra la categoria “%s” ',
'get_favorite' => 'Mostra articoli preferiti',
'get_feed' => 'Mostra feed “%s” ',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Nome',
'no_filter' => 'Nessun filtro',
'number' => 'Ricerca n°%d',
'order_asc' => 'Mostra prima gli articoli più vecchi',
'order_desc' => 'Mostra prima gli articoli più nuovi',
'search' => 'Cerca per “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Mostra tutti gli articoli',
'state_1' => 'Mostra gli articoli letti',
'state_2' => 'Mostra gli articoli non letti',

View File

@ -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' => '危険な自動ログインを有効にします',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'フィードごとに表示する',
'order' => '日付ごとにソートする',
'search' => '式',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => '状態',
'tags' => 'タグごとに表示する',
'type' => 'タイプ',
),
'get_all' => 'すべての著者を表示する',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => '“%s”カテゴリを表示する',
'get_favorite' => 'お気に入りの著者を表示する',
'get_feed' => '“%s”フィードを表示する',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => '名前',
'no_filter' => 'フィルターはありません',
'number' => 'クエリ n°%d',
'order_asc' => '古い著者を最初に表示する',
'order_desc' => '新しい著者を最初に表示する',
'search' => '“%s”で検索する',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'すべての記事を表示する',
'state_1' => '既読の記事を表示する',
'state_2' => '未読の記事を表示する',

View File

@ -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' => '다음과 같은 안전하지 않은 방식의 로그인을 허가합니다: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => '피드별로 표시',
'order' => '날짜순으로 정렬',
'search' => '정규 표현식',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => '상태',
'tags' => '태그별로 표시',
'type' => '유형',
),
'get_all' => '모든 글 표시',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => '“%s” 카테고리 표시',
'get_favorite' => '즐겨찾기에 등록된 글 표시',
'get_feed' => '“%s” 피드 표시',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => '이름',
'no_filter' => '필터가 없습니다',
'number' => '쿼리 #%d',
'order_asc' => '오래된 글 먼저 표시',
'order_desc' => '최근 글 먼저 표시',
'search' => '“%s”의 검색 결과',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => '모든 글 표시',
'state_1' => '읽은 글 표시',
'state_2' => '읽지 않은 글 표시',

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Rādīt pēc barotnes',
'order' => 'Kārtot pēc datuma',
'search' => 'Izteiksme',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Stāvoklis',
'tags' => 'Rādīt pēc birkas',
'type' => 'Veids',
),
'get_all' => 'Rādīt visus rakstus',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Rādīt kategoriju “%s”',
'get_favorite' => 'Rādīt mīļākos rakstus',
'get_feed' => 'Rādīt barotni “%s”',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Vārds',
'no_filter' => 'Bez filtra',
'number' => 'Pieprasījums nr. %d',
'order_asc' => 'Vispirms rādīt vecākos rakstus',
'order_desc' => 'Vispirms rādīt jaunākos rakstus',
'search' => 'Meklēt “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Rādīt visus rakstus',
'state_1' => 'Rādīt lasītos rakstus',
'state_2' => 'Rādīt nelasītos rakstus',

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Weergeven op feed',
'order' => 'Sorteren op datum',
'search' => 'Expressie',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Status',
'tags' => 'Weergeven op label',
'type' => 'Type', // IGNORE
),
'get_all' => 'Toon alle artikelen',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Toon „%s” categorie',
'get_favorite' => 'Toon favoriete artikelen',
'get_feed' => 'Toon „%s” feed',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Naam',
'no_filter' => 'Geen filter',
'number' => 'Query n°%d', // IGNORE
'order_asc' => 'Toon oudste artikelen eerst',
'order_desc' => 'Toon nieuwste artikelen eerst',
'search' => 'Zoek naar „%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Toon alle artikelen',
'state_1' => 'Toon gelezen artikelen',
'state_2' => 'Toon ongelezen artikelen',

View File

@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (per utilizaires avançats amb HTTPS)',
'none' => 'Cap (perilhós)',
'title' => 'Autentificacion',
'token' => 'Geton dautentificacion',
'token_help' => 'Permetre laccès a la sortida RSS de lutilizaire per defaut sens cap dautentificacion:',
'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 dautentification',
'unsafe_autologin' => 'Autorizar las connexions automaticas pas seguras al format: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Afichatge per flux',
'order' => 'Triar per data',
'search' => 'Expression', // IGNORE
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Estat',
'tags' => 'Afichatge per etiqueta',
'type' => 'Tipe',
),
'get_all' => 'Mostrar totes los articles',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Mostrar la categoria « %s »',
'get_favorite' => 'Mostrar los articles favorits',
'get_feed' => 'Mostrar lo flux « %s »',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Nom',
'no_filter' => 'Cap de filtre aplicat',
'number' => 'Filtre n°%d',
'order_asc' => 'Mostrar los articles mai ancians en primièr',
'order_desc' => 'Mostrar los articles mai recents en primièr',
'search' => 'Recèrca de « %s »',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Mostrar totes los articles',
'state_1' => 'Mostrar los articles pas legits',
'state_2' => 'Mostrar los articles pas legits',

View File

@ -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',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Według kanału',
'order' => 'Sortowanie wg daty',
'search' => 'Wyrażenie',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Stan',
'tags' => 'Według tagu',
'type' => 'Rodzaj',
),
'get_all' => 'Wyświetlenie wszystkich wiadomości',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Wyświetlenie kategorii “%s”',
'get_favorite' => 'Wyświetlenie ulubionych wiadomości',
'get_feed' => 'Wyświetlenie kanału “%s”',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Nazwa',
'no_filter' => 'Brak filtrów',
'number' => 'Zapytanie nr %d',
'order_asc' => 'Wyświetl najpierw najstarsze wiadomości',
'order_desc' => 'Wyświetl najpierw najnowsze wiadomości',
'search' => 'Szukaj “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Wyświetl wszystkie wiadomości',
'state_1' => 'Wyświetl przeczytane wiadomości',
'state_2' => 'Wyświetl nieprzeczytane wiadomości',

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Exibir por feed',
'order' => 'Ordenar por data',
'search' => 'Expressão',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Estado',
'tags' => 'Exibir por tag', // DIRTY
'type' => 'Tipo',
),
'get_all' => 'Mostrar todos os artigos',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Visualizar “%s” categoria',
'get_favorite' => 'Visualizar artigos favoritos',
'get_feed' => 'Visualizar “%s” feed',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Nome',
'no_filter' => 'Sem filtro',
'number' => 'Query n°%d', // IGNORE
'order_asc' => 'Exibir artigos mais antigos primeiro',
'order_desc' => 'Exibir artigos mais novos primeiro',
'search' => 'Busca por “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Exibir todos os artigos',
'state_1' => 'Exibir artigos lidos',
'state_2' => 'Exibir artigos não lidos',

View File

@ -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' => 'Разрешить небезопасный автоматический вход с использованием следующего формата: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Отображение по ленте',
'order' => 'Сортировать по дате',
'search' => 'Выражение',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Состояние',
'tags' => 'Отображение по метке',
'type' => 'Тип',
),
'get_all' => 'Показать все статьи',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Показать категорию “%s”',
'get_favorite' => 'Показать избранные статьи',
'get_feed' => 'Показать ленту “%s”',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Название',
'no_filter' => 'Нет фильтров',
'number' => 'Запрос №%d',
'order_asc' => 'Показывать сначала старые статьи',
'order_desc' => 'Показывать сначала новые статьи',
'search' => 'Искать “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Показать все статьи',
'state_1' => 'Показать прочитанные статьи',
'state_2' => 'Показать непрочитанные статьи',

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Zobraziť podľa kanála',
'order' => 'Zobraziť podľa dátumu',
'search' => 'Výraz',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Štát',
'tags' => 'Zobraziť podľa štítku',
'type' => 'Typ',
),
'get_all' => 'Zobraziť všetky články',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => 'Zobraziť kategóriu “%s”',
'get_favorite' => 'Zobraziť obľúbené články',
'get_feed' => 'Zobraziť kanál “%s”',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'Meno',
'no_filter' => 'Žiadny filter',
'number' => 'Dopyt číslo %d',
'order_asc' => 'Zobraziť staršie články hore',
'order_desc' => 'Zobraziť novšie články hore',
'search' => 'Vyhľadáva sa: “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Zobraziť všetky články',
'state_1' => 'Zobraziť prečítané články',
'state_2' => 'Zobraziť neprečítané články',

View File

@ -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: ',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => 'Akışa göre göster',
'order' => 'Tarihe göre göster',
'search' => 'İfade',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => 'Durum',
'tags' => 'Etikete göre göster',
'type' => 'Tür',
),
'get_all' => 'Tüm makaleleri göster',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => '“%s” kategorisini göster',
'get_favorite' => 'Favori makaleleri göster',
'get_feed' => '“%s” akışını göster',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => 'İsim',
'no_filter' => 'Filtre yok',
'number' => 'Sorgu n°%d',
'order_asc' => 'Önce eski makaleleri göster',
'order_desc' => 'Önce yeni makaleleri göster',
'search' => '“%s” için arama',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => 'Tüm makaleleri göster',
'state_1' => 'Okunmuş makaleleri göster',
'state_2' => 'Okunmamış makaleleri göster',

View File

@ -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' => '允许不安全的自动登陆方式:',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => '按订阅源显示',
'order' => '按日期排序',
'search' => '表达式',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => '状态',
'tags' => '按标签显示',
'type' => '类型',
),
'get_all' => '显示所有文章',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => '显示分类 “%s”',
'get_favorite' => '显示收藏文章',
'get_feed' => '显示订阅源 “%s”',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => '名称',
'no_filter' => '无过滤器',
'number' => '查询 n°%d',
'order_asc' => '由旧至新显示文章',
'order_desc' => '由新至旧显示文章',
'search' => '搜索 “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => '显示所有文章',
'state_1' => '显示已读文章',
'state_2' => '显示未读文章',

View File

@ -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' => '允許不安全的自動登入方式:',
),

View File

@ -120,20 +120,33 @@ return array(
'feeds' => '按訂閱源顯示',
'order' => '按日期排序',
'search' => '表達式',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'state' => '狀態',
'tags' => '按標簽顯示',
'type' => '類型',
),
'get_all' => '顯示所有文章',
'get_all_labels' => 'Display articles with any label', // TODO
'get_category' => '顯示分類 “%s”',
'get_favorite' => '顯示收藏文章',
'get_feed' => '顯示訂閱源 “%s”',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'name' => '名稱',
'no_filter' => '無過濾器',
'number' => '查詢 n°%d',
'order_asc' => '由舊至新顯示文章',
'order_desc' => '由新至舊顯示文章',
'search' => '搜尋 “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
),
'state_0' => '顯示所有文章',
'state_1' => '顯示已讀文章',
'state_2' => '顯示未讀文章',

View File

@ -1,9 +1,10 @@
<?php
declare(strict_types=1);
/** @var FreshRSS_View $this */
?>
<header class="header">
<div class="item title">
<a href="<?= _url('index', 'index') ?>">
<a href="<?= Minz_Url::display(['c' => 'index', 'a' => 'index'], 'html', 'root') ?>">
<?php if (FreshRSS_Context::systemConf()->logo_html == '') { ?>
<img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" loading="lazy" />
<?php
@ -16,32 +17,29 @@
<div class="item search">
<?php if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::systemConf()->allow_anonymous) { ?>
<form action="<?= _url('index', 'index') ?>" method="get">
<form action="<?= $this->html_url ?>" method="get">
<div class="stick">
<?php if (Minz_Request::controllerName() === 'index'): ?>
<?php if (in_array(Minz_Request::actionName(), ['normal', 'global', 'reader'], true)) { ?>
<input type="hidden" name="a" value="<?= Minz_Request::actionName() ?>" />
<?php } if (Minz_Request::paramString('get') !== '') { ?>
<input type="hidden" name="get" value="<?= FreshRSS_Context::currentGet() ?>" />
<?php } if (Minz_Request::paramInt('state') !== 0) { ?>
<input type="hidden" name="state" value="<?= Minz_Request::paramInt('state') ?>" />
<?php } ?>
<?php endif; ?>
<?php if (Minz_Request::paramString('user') !== '') { ?>
<input type="hidden" name="user" value="<?= Minz_User::name() ?>" />
<?php } if (ctype_alnum(Minz_Request::paramString('t'))) { ?>
<input type="hidden" name="t" value="<?= Minz_Request::paramString('t') ?>" />
<?php } if (ctype_upper(Minz_Request::paramString('order'))) { ?>
<input type="hidden" name="order" value="<?= FreshRSS_Context::$order ?>" />
<?php } if (ctype_lower(Minz_Request::paramString('f'))) { ?>
<input type="hidden" name="f" value="<?= Minz_Request::paramString('f') ?>" />
<?php } ?>
<input type="search" name="search" id="search"
value="<?= htmlspecialchars(htmlspecialchars_decode(FreshRSS_Context::$search->getRawInput(), ENT_QUOTES), ENT_COMPAT, 'UTF-8') ?>"
value="<?= htmlspecialchars(htmlspecialchars_decode(Minz_Request::paramString('search'), ENT_QUOTES), ENT_COMPAT, 'UTF-8') ?>"
placeholder="<?= _t('gen.menu.search') ?>" />
<?php $param_a = Minz_Request::actionName(); ?>
<?php if (in_array($param_a, ['normal', 'global', 'reader'], true)) { ?>
<input type="hidden" name="a" value="<?= $param_a ?>" />
<?php } ?>
<?php $get = Minz_Request::paramString('get'); ?>
<?php if ($get !== '') { ?>
<input type="hidden" name="get" value="<?= $get ?>" />
<?php } ?>
<?php $order = Minz_Request::paramString('order'); ?>
<?php if ($order !== '') { ?>
<input type="hidden" name="order" value="<?= $order ?>" />
<?php } ?>
<?php $state = Minz_Request::paramString('state'); ?>
<?php if ($state !== '') { ?>
<input type="hidden" name="state" value="<?= $state ?>" />
<?php } ?>
<button class="btn" type="submit"><?= _i('search') ?></button>
</div>
</form>
@ -120,7 +118,7 @@
</nav>
<?php } elseif (FreshRSS_Auth::accessNeedsAction()) { ?>
<div class="item configure">
<a class="signin" href="<?= _url('auth', 'login') ?>"><?= _i('login') ?><?= _t('gen.auth.login') ?></a>
<a class="signin" href="<?= Minz_Url::display(['c' => 'auth', 'a' => 'login'], 'html', 'root') ?>"><?= _i('login') ?><?= _t('gen.auth.login') ?></a>
</div>
<?php } ?>
</header>

View File

@ -2,15 +2,17 @@
declare(strict_types=1);
/** @var FreshRSS_View $this */
FreshRSS::preLayout();
$class = '';
if (_t('gen.dir') === 'rtl') {
echo ' dir="rtl"';
$class = 'rtl ';
}
if (FreshRSS_Context::userConf()->darkMode !== 'no') {
$class .= 'darkMode_' . FreshRSS_Context::userConf()->darkMode;
}
?>
<!DOCTYPE html>
<html lang="<?= FreshRSS_Context::userConf()->language ?>" xml:lang="<?= FreshRSS_Context::userConf()->language ?>"<?php
$class = '';
if (_t('gen.dir') === 'rtl') {
echo ' dir="rtl"';
$class = 'rtl ';
}
?> class="<?= $class ?><?= (FreshRSS_Context::userConf()->darkMode === 'no') ? '' : 'darkMode_' . FreshRSS_Context::userConf()->darkMode ?>">
<html lang="<?= FreshRSS_Context::userConf()->language ?>" xml:lang="<?= FreshRSS_Context::userConf()->language ?>" class="<?= $class ?>">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />

View File

@ -41,26 +41,15 @@
<li class="item">
<span>
<form action="<?= _url('index', 'index') ?>" method="get">
<?php $param_a = Minz_Request::actionName(); ?>
<?php if (in_array($param_a, ['normal', 'global', 'reader'], true)) { ?>
<input type="hidden" name="a" value="<?= $param_a ?>" />
<?php if (in_array(Minz_Request::actionName(), ['normal', 'global', 'reader'], true)) { ?>
<input type="hidden" name="a" value="<?= Minz_Request::actionName() ?>" />
<?php } if (Minz_Request::paramString('get') !== '') { ?>
<input type="hidden" name="get" value="<?= FreshRSS_Context::currentGet() ?>" />
<?php } if (ctype_upper(Minz_Request::paramString('order'))) { ?>
<input type="hidden" name="order" value="<?= FreshRSS_Context::$order ?>" />
<?php } if (Minz_Request::paramInt('state') !== 0) { ?>
<input type="hidden" name="state" value="<?= FreshRSS_Context::$state ?>" />
<?php } ?>
<?php $get = Minz_Request::paramString('get'); ?>
<?php if ($get !== '') { ?>
<input type="hidden" name="get" value="<?= $get ?>" />
<?php } ?>
<?php $order = Minz_Request::paramString('order'); ?>
<?php if ($order !== '') { ?>
<input type="hidden" name="order" value="<?= $order ?>" />
<?php } ?>
<?php $state = Minz_Request::paramString('state'); ?>
<?php if ($state !== '') { ?>
<input type="hidden" name="state" value="<?= $state ?>" />
<?php } ?>
<div class="stick search">
<input type="search" name="search"
value="<?= htmlspecialchars(htmlspecialchars_decode(FreshRSS_Context::$search->getRawInput(), ENT_QUOTES), ENT_COMPAT, 'UTF-8'); ?>"
@ -89,7 +78,7 @@
<?php if (!empty($raw_query['url'])): ?>
<a href="<?= $raw_query['url'] ?>"><?= $raw_query['name'] ?? $raw_query['url'] ?></a>
<?php else: ?>
<?php $query = new FreshRSS_UserQuery($raw_query); ?>
<?php $query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels()); ?>
<a href="<?= $query->getUrl() ?>"><?= $query->getName() ?></a>
<?php endif; ?>
</li>
@ -210,20 +199,6 @@
<?php
}
?>
<?php
$url_output['a'] = 'rss';
if (FreshRSS_Context::userConf()->token) {
$url_output['params']['user'] = Minz_User::name();
$url_output['params']['token'] = FreshRSS_Context::userConf()->token;
}
if (FreshRSS_Context::userConf()->since_hours_posts_per_rss) {
$url_output['params']['hours'] = FreshRSS_Context::userConf()->since_hours_posts_per_rss;
}
?>
<a class="view-rss btn" target="_blank" rel="noreferrer" title="<?= _t('index.menu.rss_view') ?>" href="<?= Minz_Url::display($url_output) ?>">
<?= _i('rss') ?>
</a>
</div>
<?php $nav_menu_hooks = Minz_ExtensionManager::callHookString('nav_menu'); ?>

View File

@ -2,17 +2,27 @@
declare(strict_types=1);
/** @var FreshRSS_View $this */
FreshRSS::preLayout();
$class = '';
if (_t('gen.dir') === 'rtl') {
echo ' dir="rtl"';
$class = 'rtl ';
}
if (FreshRSS_Context::userConf()->darkMode !== 'no') {
$class .= 'darkMode_' . FreshRSS_Context::userConf()->darkMode;
}
?>
<!DOCTYPE html>
<html lang="<?= FreshRSS_Context::userConf()->language ?>" xml:lang="<?= FreshRSS_Context::userConf()->language ?>">
<html lang="<?= FreshRSS_Context::userConf()->language ?>" xml:lang="<?= FreshRSS_Context::userConf()->language ?>" class="<?= $class ?>">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<?= FreshRSS_View::metaThemeColor() ?>
<?= FreshRSS_View::headStyle() ?>
<script id="jsonVars" type="application/json">
<?php $this->renderHelper('javascript_vars'); ?>
</script>
<?= FreshRSS_View::headScript() ?>
<link rel="manifest" href="<?= Minz_Url::display('/themes/manifest.json') ?>" />
<link rel="shortcut icon" id="favicon" type="image/x-icon" sizes="16x16 64x64" href="<?= Minz_Url::display('/favicon.ico') ?>" />
<link rel="icon msapplication-TileImage apple-touch-icon" type="image/png" sizes="256x256" href="<?= Minz_Url::display('/themes/icons/favicon-256.png') ?>" />
<link rel="apple-touch-icon" href="<?= Minz_Url::display('/themes/icons/apple-touch-icon.png') ?>" />
@ -20,9 +30,15 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="<?= FreshRSS_Context::systemConf()->title ?>">
<meta name="msapplication-TileColor" content="#FFF" />
<meta name="theme-color" content="#FFF" />
<?php if (!FreshRSS_Context::systemConf()->allow_referrer) { ?>
<meta name="referrer" content="never" />
<meta name="robots" content="noindex,nofollow" />
<?php } ?>
<?= FreshRSS_View::headTitle() ?>
<?php if ($this->rss_url != ''): ?>
<link rel="alternate" type="application/rss+xml" title="<?= $this->rss_title ?>" href="<?= $this->rss_url ?>" />
<?php endif; ?>
<meta name="robots" content="noindex,nofollow" />
</head>
<body>
@ -30,7 +46,7 @@
<div class="app-layout app-layout-simple">
<div class="header">
<div class="item title">
<a href="<?= _url('index', 'index') ?>">
<a href="<?= Minz_Url::display(['c' => 'index', 'a' => 'index'], 'html', 'root') ?>">
<?php if (FreshRSS_Context::systemConf()->logo_html == '') { ?>
<img class="logo" src="<?= _i('FreshRSS-logo', FreshRSS_Themes::ICON_URL) ?>" alt="FreshRSS" loading="lazy" />
<?php
@ -43,14 +59,20 @@
<div class="item"></div>
<div class="item">
<?php if (FreshRSS_Auth::accessNeedsAction()) { ?>
<a class="signout" href="<?= _url('auth', 'logout') ?>">
<?= _i('logout') . _t('gen.auth.logout') ?>
<?php if (FreshRSS_Auth::accessNeedsAction()): ?>
<div class="item configure">
<?php if (FreshRSS_Auth::hasAccess()): ?>
<a class="signout" href="<?= Minz_Url::display(['c' => 'auth', 'a' => 'logout'], 'html', 'root') ?>">
<?= _i('logout') ?><?= _t('gen.auth.logout') ?>
(<?= htmlspecialchars(Minz_User::name() ?? '', ENT_NOQUOTES, 'UTF-8') ?>)
</a>
<?php } ?>
</div>
<?php else: ?>
<a class="signin" href="<?= Minz_Url::display(['c' => 'auth', 'a' => 'login'], 'html', 'root') ?>">
<?= _i('login') ?><?= _t('gen.auth.login') ?>
</a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php $this->render(); ?>

View File

@ -18,6 +18,9 @@
<div class="box-title">
<a class="configure open-slider" href="<?= _url('configure', 'query', 'id', '' . $key) ?>"><?= _i('configure') ?></a><h2><?= $query->getName() ?></h2>
<input type="hidden" id="queries_<?= $key ?>_name" name="queries[<?= $key ?>][name]" value="<?= $query->getName() ?>"/>
<input type="hidden" id="queries_<?= $key ?>_token" name="queries[<?= $key ?>][token]" value="<?= $query->getToken() ?>"/>
<input type="hidden" id="queries_<?= $key ?>_shareRss" name="queries[<?= $key ?>][token]" value="<?= $query->shareRss() ?>"/>
<input type="hidden" id="queries_<?= $key ?>_shareOpml" name="queries[<?= $key ?>][token]" value="<?= $query->shareOpml() ?>"/>
<input type="hidden" id="queries_<?= $key ?>_url" name="queries[<?= $key ?>][url]" value="<?= $query->getUrl() ?>"/>
<input type="hidden" id="queries_<?= $key ?>_search" name="queries[<?= $key ?>][search]" value="<?= urlencode($query->getSearch()->getRawInput()) ?>"/>
<input type="hidden" id="queries_<?= $key ?>_state" name="queries[<?= $key ?>][state]" value="<?= $query->getState() ?>"/>

View File

@ -7,7 +7,6 @@
?>
<div class="post">
<h2><?= $this->query->getName() ?></h2>
<div>
<a href="<?= $this->query->getUrl() ?>"><?= _i('link') ?> <?= _t('gen.action.filter') ?></a>
</div>
@ -18,15 +17,53 @@
<div class="form-group">
<label class="group-name" for="name"><?= _t('conf.query.name') ?></label>
<div class="group-controls">
<input type="text" name="name" id="name" value="<?= $this->query->getName() ?>" />
<input type="text" name="name" id="name" value="<?= $this->query->getName() ?>" />
<input type="hidden" name="query[token]" id="query_token" value="<?= $this->query->getToken() ?>" />
</div>
</div>
<legend><?= _t('conf.query.filter') ?></legend>
<legend><?= _t('conf.query.share') ?></legend>
<div class="form-group">
<div class="group-controls">
<label class="checkbox" for="shareRss">
<input type="checkbox" name="query[shareRss]" id="shareRss" value="1" <?= $this->query->shareRss() ? 'checked="checked"' : ''?> />
<?= _t('conf.query.filter.shareRss') ?>
</label>
<?php if ($this->query->sharedUrlRss() !== ''): ?>
<ul>
<li><a href="<?= $this->query->sharedUrlHtml() ?>"><?= _i('link') ?> <?= _t('conf.query.share.html') ?></a></li>
<li><a href="<?= $this->query->sharedUrlRss() ?>"><?= _i('link') ?> <?= _t('conf.query.share.rss') ?></a></li>
</ul>
<?php endif; ?>
</div>
<div class="group-controls">
<label class="checkbox" for="shareOpml">
<input type="checkbox" name="query[shareOpml]" id="shareOpml" value="1" <?= $this->query->shareOpml() && $this->query->safeForOpml() ? 'checked="checked"' : '' ?>
<?= $this->query->safeForOpml() ? '' : 'disabled="disabled"' ?> />
<?= _t('conf.query.filter.shareOpml') ?>
</label>
<?php if ($this->query->sharedUrlOpml() !== ''): ?>
<ul>
<li><a href="<?= $this->query->sharedUrlOpml() ?>"><?= _i('link') ?> <?= _t('conf.query.share.opml') ?></a></li>
</ul>
<?php endif; ?>
</div>
<p class="help"><?= _i('help') ?> <?= _t('conf.query.share.help') ?></a></p>
<p class="help"><?= _i('help') ?> <?= _t('conf.query.help') ?></a></p>
</div>
<div class="form-group form-actions">
<div class="group-controls">
<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>
</div>
</div>
<legend><?= _t('conf.query.filter') ?></legend>
<div class="form-group">
<label class="group-name" for=""><?= _t('conf.query.filter.search') ?></label>
<div class="group-controls">
<input type="text" id="query_search" name="query[search]" value="<?= htmlspecialchars($this->query->getSearch()->getRawInput(), ENT_COMPAT, 'UTF-8') ?>"/>
<p class="help"><?= _i('help') ?> <?= _t('gen.menu.search_help') ?></a></p>
</div>
</div>
<div class="form-group">
@ -58,22 +95,24 @@
<label class="group-name" for="query_get"><?= _t('conf.query.filter.type') ?></label>
<div class="group-controls">
<select name="query[get]" id="query_get" size="10">
<option value=""></option>
<option value="s" <?= 's' === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= _t('conf.query.get_favorite') ?></option>
<option value="a" <?= in_array($this->query->getGet(), ['', 'a'], true) ? 'selected="selected"' : '' ?>><?= _t('index.feed.title') ?></option>
<option value="i" <?= 'i' === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= _t('index.menu.important') ?></option>
<option value="s" <?= 's' === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= _t('index.feed.title_fav') ?></option>
<option value="T" <?= 'T' === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= _t('index.menu.tags') ?></option>
<optgroup label="<?= _t('conf.query.filter.tags') ?>">
<?php foreach ($this->tags as $tag): ?>
<option value="t_<?= $tag->id() ?>" <?= "t_{$tag->id()}" === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= $tag->name() ?></option>
<?php endforeach?>
</optgroup>
<optgroup label="<?= _t('conf.query.filter.categories') ?>">
<?php foreach ($this->categories as $category): ?>
<option value="c_<?= $category->id() ?>" <?= "c_{$category->id()}" === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= $category->name() ?></option>
<?php endforeach?>
</optgroup>
<optgroup label="<?= _t('conf.query.filter.feeds') ?>">
<?php foreach ($this->feeds as $feed): ?>
<option value="f_<?= $feed->id() ?>" <?= "f_{$feed->id()}" === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= $feed->name() ?></option>
<?php endforeach?>
</optgroup>
<optgroup label="<?= _t('conf.query.filter.tags') ?>">
<?php foreach ($this->tags as $tag): ?>
<option value="t_<?= $tag->id() ?>" <?= "t_{$tag->id()}" === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= $tag->name() ?></option>
<?php endforeach?>
<?php foreach ($this->feeds as $feed): ?>
<option value="f_<?= $feed->id() ?>" <?= "f_{$feed->id()}" === $this->query->getGet() ? 'selected="selected"' : '' ?>><?= $feed->name() ?></option>
<?php endforeach?>
</optgroup>
</select>
</div>
@ -83,8 +122,8 @@
<div class="group-controls">
<select name="query[order]" id="query_order">
<option value=""></option>
<option value="ASC" <?= 'ASC' === $this->query->getOrder() ? 'selected="selected"' : '' ?>><?= _t('conf.query.order_asc') ?></option>
<option value="DESC" <?= 'DESC' === $this->query->getOrder() ? 'selected="selected"' : '' ?>><?= _t('conf.query.order_desc') ?></option>
<option value="ASC" <?= 'ASC' === $this->query->getOrder() ? 'selected="selected"' : '' ?>><?= _t('conf.query.order_asc') ?></option>
</select>
</div>
</div>

View File

@ -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()] ?? []);

View File

@ -1,9 +1,6 @@
<?php
declare(strict_types=1);
/** @var FreshRSS_View $this */
if ($this->feed === null) {
throw new FreshRSS_Context_Exception('Feed not initialised!');
}
?>
<div class="post" id="feed_update">
<h1><?= $this->feed->name() ?></h1>

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/** @var FreshRSS_View $this */
?>
<nav class="nav-pagination nav-list">
<ul class="pagination">
<?php if (FreshRSS_Context::$offset > 0): ?>
<li class="item pager-first">
<a href="<?= $this->userQuery->sharedUrlHtml() . '&nb=' . FreshRSS_Context::$number ?>">« <?= _t('conf.logs.pagination.first') ?></a>
</li>
<li class="item pager-previous">
<a href="<?= $this->userQuery->sharedUrlHtml() . '&nb=' . FreshRSS_Context::$number .
'&offset=' . max(0, FreshRSS_Context::$offset - FreshRSS_Context::$number) ?>"> <?= _t('conf.logs.pagination.previous') ?></a>
</li>
<?php endif; ?>
<li class="item pager-next">
<a href="<?= $this->userQuery->sharedUrlHtml() . '&nb=' . FreshRSS_Context::$number .
'&offset=' . (FreshRSS_Context::$offset + FreshRSS_Context::$number) ?>"><?= _t('conf.logs.pagination.next') ?> </a>
</li>
</ul>
</nav>

View File

@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
/** @var FreshRSS_View $this */
$entry = $this->entry;
$feed = $this->feed;
?>
<article class="flux_content" dir="auto">
<div class="content <?= FreshRSS_Context::userConf()->content_width ?>">
<header>
<?php
$favoriteUrl = ['c' => 'entry', 'a' => 'bookmark', 'params' => ['id' => $entry->id()]];
if ($entry->isFavorite()) {
$favoriteUrl['params']['is_favorite'] = 0;
}
$readUrl = ['c' => 'entry', 'a' => 'read', 'params' => ['id' => $entry->id()]];
if ($entry->isRead()) {
$readUrl['params']['is_read'] = 0;
}
?>
<div class="article-header-topline">
<?php if (FreshRSS_Auth::hasAccess()) { ?>
<a class="read" href="<?= Minz_Url::display($readUrl) ?>" title="<?= _t('conf.shortcut.mark_read') ?>"><?= _i($entry->isRead() ? 'read' : 'unread') ?></a>
<a class="bookmark" href="<?= Minz_Url::display($favoriteUrl) ?>" title="<?= _t('conf.shortcut.mark_favorite') ?>"><?= _i($entry->isFavorite() ? 'starred' : 'non-starred') ?></a>
<?php } ?>
<?php if (FreshRSS_Context::userConf()->show_feed_name === 't') { ?>
<a class="website" href="<?= _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
<?php if (FreshRSS_Context::userConf()->show_favicons): ?>
<img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
endif; ?><span><?= $feed->name() ?></span></a>
<?php } ?>
</div>
<?php
if (in_array(FreshRSS_Context::userConf()->show_tags, ['b', 'h'], true)) {
$this->renderHelper('index/tags');
}
?>
<h1 class="title"><a target="_blank" rel="noreferrer" class="go_website" href="<?= $entry->link() ?>"><?= $entry->title() ?></a></h1>
<?php if (FreshRSS_Context::userConf()->show_author_date === 'h' || FreshRSS_Context::userConf()->show_author_date === 'b') { ?>
<div class="subtitle">
<?php if (FreshRSS_Context::userConf()->show_feed_name === 'a') { ?>
<div class="website"><a href="<?= $this->internal_rendering ? $feed->website() : _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
<?php if (FreshRSS_Context::userConf()->show_favicons): ?>
<img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
endif; ?><span><?= $feed->name() ?></span></a></div>
<?php } ?>
<div class="author"><?php
$authors = $entry->authors();
if (is_array($authors)) {
if ($this->internal_rendering):
foreach ($authors as $author): ?>
<?= $author ?>
<?php endforeach;
else:
foreach ($authors as $author): ?>
<a href="<?= Minz_Url::display(Minz_Request::modifiedCurrentRequest(['search' => 'author:' . str_replace(' ', '+', htmlspecialchars_decode($author, ENT_QUOTES))])) ?>">
<?= $author ?>
</a>
<?php endforeach;
endif;
} ?>
</div>
<div class="date">
<time datetime="<?= $entry->machineReadableDate() ?>"><?= $entry->date() ?></time>
</div>
</div>
<?php } ?>
</header>
<div class="text">
<?= $entry->content(true) ?>
</div>
<?php
$display_authors_date = in_array(FreshRSS_Context::userConf()->show_author_date, ['b', 'f'], true);
$display_tags = in_array(FreshRSS_Context::userConf()->show_tags, ['b', 'f'], true);
if ($display_authors_date || $display_tags) {
?>
<footer>
<?php if ($display_authors_date) { ?>
<div class="subtitle">
<?php if (FreshRSS_Context::userConf()->show_feed_name === 'a') { ?>
<div class="website"><a href="<?= _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
<?php if (FreshRSS_Context::userConf()->show_favicons): ?>
<img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
endif; ?><span><?= $feed->name() ?></span></a></div>
<?php } ?>
<div class="author"><?php
$authors = $entry->authors();
if (is_array($authors)) {
foreach ($authors as $author) {
?>
<a href="<?= Minz_Url::display(Minz_Request::modifiedCurrentRequest(['search' => 'author:' . str_replace(' ', '+', htmlspecialchars_decode($author, ENT_QUOTES))])) ?>">
<?= $author ?>
</a>
<?php
}
}
?>
</div>
<div class="date">
<time datetime="<?= $entry->machineReadableDate() ?>"><?= $entry->date() ?></time>
</div>
</div>
<?php
}
if ($display_tags) {
$this->renderHelper('index/tags');
}
?>
</footer>
<?php
} ?>
</div>
</article>

View File

@ -1,9 +1,6 @@
<?php
declare(strict_types=1);
/** @var FreshRSS_View $this */
if ($this->feed === null) {
throw new FreshRSS_Context_Exception('Feed not initialised!');
}
$topline_read = FreshRSS_Context::userConf()->topline_read;
$topline_favorite = FreshRSS_Context::userConf()->topline_favorite;
$topline_website = FreshRSS_Context::userConf()->topline_website;

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/** @var FreshRSS_View $this */
[$firstTags,$remainingTags] = $this->entry->tagsFormattingHelper();
?>
<div class="tags">
<?php if (!empty($firstTags)): ?>
<?= _i('tag') ?><ul class="list-tags">
<?php if (Minz_Request::controllerName() === 'index'): ?>
<?php foreach ($firstTags as $tag): ?>
<li class="item tag"><a class="link-tag" href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li>
<?php endforeach; ?>
<?php else: // API public access ?>
<?php foreach ($firstTags as $tag): ?>
<li class="item tag"><a class="link-tag" href="<?= $this->html_url . '&search=%23' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES)) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li>
<?php endforeach; ?>
<?php endif; ?>
<?php if (!empty($remainingTags)): // more than 7 tags: show dropdown menu ?>
<li class="item tag">
<div class="dropdown">
<div id="dropdown-tags2-<?= $this->entry->id() ?>" class="dropdown-target"></div>
<a class="dropdown-toggle" href="#dropdown-tags2-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
<ul class="dropdown-menu">
<li class="dropdown-header"><?= _t('index.tag.related') ?></li>
<?php if (Minz_Request::controllerName() === 'index'): ?>
<?php foreach ($remainingTags as $tag): ?>
<li class="item"><a href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li>
<?php endforeach; ?>
<?php else: ?>
<?php foreach ($remainingTags as $tag): ?>
<li class="item tag"><a class="link-tag" href="<?= $this->html_url . '&search=%23' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES)) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li>
<?php endforeach; ?>
<?php endif; ?>
</ul>
<a class="dropdown-close" href="#close"></a>
</div>
</li>
<?php endif; ?>
</ul>
<?php endif; ?>
</div>

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/** @var FreshRSS_View $this */
// Override some layout preferences for the public API output
FreshRSS_Context::userConf()->content_width = 'large';
FreshRSS_Context::userConf()->show_author_date = FreshRSS_UserConfiguration::default()->show_author_date;
FreshRSS_Context::userConf()->show_favicons = FreshRSS_UserConfiguration::default()->show_favicons;
FreshRSS_Context::userConf()->show_feed_name = FreshRSS_UserConfiguration::default()->show_feed_name;
FreshRSS_Context::userConf()->show_tags = FreshRSS_UserConfiguration::default()->show_tags;
FreshRSS_Context::userConf()->show_tags_max = FreshRSS_UserConfiguration::default()->show_tags_max;
?>
<?php $this->renderHelper('htmlPagination'); ?>
<main id="stream" class="reader api">
<h2>
<a href="<?= $this->html_url ?>"><?= FreshRSS_View::title() ?></a> ·
<a class="view-rss" href="<?= $this->rss_url ?>" title="<?= _t('index.menu.rss_view') ?>">
<?= _i('rss') ?>
</a>
</h2>
<?php
foreach ($this->entries as $entry):
$this->entry = $entry;
$this->feed = $this->feeds[$entry->feedId()] ??
FreshRSS_Category::findFeed($this->categories, $entry->feedId()) ??
FreshRSS_Feed::default();
?>
<div class="flux">
<?php $this->renderHelper('index/article'); ?>
</div>
<?php endforeach; ?>
</main>
<?php $this->renderHelper('htmlPagination'); ?>

View File

@ -11,23 +11,18 @@ call_user_func($this->callbackBeforeEntries, $this);
$display_today = true;
$display_yesterday = true;
$display_others = true;
$hidePosts = !FreshRSS_Context::userConf()->display_posts;
$lazyload = FreshRSS_Context::userConf()->lazyload;
$content_width = FreshRSS_Context::userConf()->content_width;
$MAX_TAGS_DISPLAYED = (int)FreshRSS_Context::userConf()->show_tags_max;
$useKeepUnreadImportant = !FreshRSS_Context::isImportant() && !FreshRSS_Context::isFeed();
$today = @strtotime('today');
?>
<main id="stream" class="normal<?= $hidePosts ? ' hide_posts' : '' ?>">
<main id="stream" class="normal<?= FreshRSS_Context::userConf()->display_posts ? '' : ' hide_posts' ?>">
<h1 class="title_hidden"><?= _t('conf.reading.view.normal') ?></h1>
<div id="new-article">
<a href="<?= Minz_Url::display(Minz_Request::currentRequest()) ?>"><?= _t('gen.js.new_article'); /* TODO: move string in JS*/ ?></a>
</div><?php
$lastEntry = null;
$nbEntries = 0;
/** @var FreshRSS_Entry */
foreach ($this->entries as $item):
$lastEntry = $item;
$nbEntries++;
@ -40,8 +35,8 @@ $today = @strtotime('today');
$this->entry = $item;
// We most likely already have the feed object in cache, otherwise make a request
$this->feed = FreshRSS_CategoryDAO::findFeed($this->categories, $this->entry->feedId()) ??
$this->entry->feed() ?? FreshRSS_Feed::example();
$this->feed = FreshRSS_Category::findFeed($this->categories, $this->entry->feedId()) ??
$this->entry->feed() ?? FreshRSS_Feed::default();
if ($display_today && $this->entry->isDay(FreshRSS_Days::TODAY, $today)) {
?><div class="day" id="day_today"><?php
@ -74,27 +69,8 @@ $today = @strtotime('today');
?>" data-priority="<?= $this->feed->priority()
?>"><?php
$this->renderHelper('index/normal/entry_header');
if ($this->feed === null) {
throw new FreshRSS_Context_Exception('Feed not initialised!');
}
$tags = null;
$firstTags = array();
$remainingTags = array();
if (FreshRSS_Context::userConf()->show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'f' || FreshRSS_Context::userConf()->show_tags === 'b') {
$tags = $this->entry->tags();
if (!empty($tags)) {
if ($MAX_TAGS_DISPLAYED > 0) {
$firstTags = array_slice($tags, 0, $MAX_TAGS_DISPLAYED);
$remainingTags = array_slice($tags, $MAX_TAGS_DISPLAYED);
} else {
$firstTags = $tags;
}
}
}
?><article class="flux_content" dir="auto">
<div class="content <?= $content_width ?>">
<div class="content <?= FreshRSS_Context::userConf()->content_width ?>">
<header>
<?php if (FreshRSS_Context::userConf()->show_feed_name === 't') { ?>
<div class="website"><a href="<?= _url('index', 'index', 'get', 'f_' . $this->feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
@ -103,36 +79,8 @@ $today = @strtotime('today');
endif; ?><span><?= $this->feed->name() ?></span></a>
</div>
<?php } ?>
<?php if (FreshRSS_Context::userConf()->show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'b') { ?>
<div class="tags">
<?php
if (!empty($tags)) {
?><?= _i('tag') ?><ul class="list-tags"><?php
foreach ($firstTags as $tag) {
?><li class="item tag"><a class="link-tag" href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li><?php
}
if (!empty($remainingTags)) { // more than 7 tags: show dropdown menu ?>
<li class="item tag">
<div class="dropdown">
<div id="dropdown-tags2-<?= $this->entry->id() ?>" class="dropdown-target"></div>
<a class="dropdown-toggle" href="#dropdown-tags2-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
<ul class="dropdown-menu">
<li class="dropdown-header"><?= _t('index.tag.related') ?></li>
<?php
foreach ($remainingTags as $tag) {
?><li class="item"><a href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>"><?= $tag ?></a></li><?php
} ?>
</ul>
<a class="dropdown-close" href="#close"></a>
</div>
</li>
<?php
} ?>
</ul><?php
} ?>
</div>
<?php
<?php if (FreshRSS_Context::userConf()->show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'b') {
$this->renderHelper('index/tags');
} ?>
<h1 class="title"><a target="_blank" rel="noreferrer" class="go_website" href="<?= $this->entry->link() ?>" title="<?= _t('conf.shortcut.see_on_website')?>"><?= $this->entry->title() ?></a></h1>
<?php if (FreshRSS_Context::userConf()->show_author_date === 'h' || FreshRSS_Context::userConf()->show_author_date === 'b') { ?>
@ -163,8 +111,8 @@ $today = @strtotime('today');
</div>
<?php } ?>
</header>
<div class="text"><?php
echo $lazyload && $hidePosts ? lazyimg($this->entry->content(true)) : $this->entry->content(true);
<div class="text"><?=
FreshRSS_Context::userConf()->lazyload && !FreshRSS_Context::userConf()->display_posts ? lazyimg($this->entry->content(true)) : $this->entry->content(true)
?></div>
<?php
$display_authors_date = FreshRSS_Context::userConf()->show_author_date === 'f' || FreshRSS_Context::userConf()->show_author_date === 'b';
@ -201,36 +149,10 @@ $today = @strtotime('today');
</div>
<?php
}
if ($display_tags) { ?>
<div class="tags">
<?php
if (!empty($tags)) {
?><?= _i('tag') ?><ul class="list-tags"><?php
foreach ($firstTags as $tag) {
?><li class="item tag"><a class="link-tag" href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li><?php
}
if (!empty($remainingTags)) { ?>
<li class="item tag">
<div class="dropdown">
<div id="dropdown-tags3-<?= $this->entry->id() ?>" class="dropdown-target"></div>
<a class="dropdown-toggle" href="#dropdown-tags3-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
<ul class="dropdown-menu">
<li class="dropdown-header"><?= _t('index.tag.related') ?></li>
<?php
foreach ($remainingTags as $tag) {
?><li class="item"><a href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>"><?= $tag ?></a></li><?php
} ?>
</ul>
<a class="dropdown-close" href="#close"></a>
</div>
</li>
<?php
} ?>
</ul><?php
} ?>
</div>
<?php
} ?>
if ($display_tags) {
$this->renderHelper('index/tags');
}
?>
</footer>
<?php
} ?>

View File

@ -9,8 +9,6 @@ if (!Minz_Request::paramBoolean('ajax')) {
call_user_func($this->callbackBeforeEntries, $this);
$lazyload = FreshRSS_Context::userConf()->lazyload;
$content_width = FreshRSS_Context::userConf()->content_width;
$MAX_TAGS_DISPLAYED = (int)FreshRSS_Context::userConf()->show_tags_max;
?>
<main id="stream" class="reader">
<h1 class="title_hidden"><?= _t('conf.reading.view.reader') ?></h1>
@ -19,197 +17,21 @@ $MAX_TAGS_DISPLAYED = (int)FreshRSS_Context::userConf()->show_tags_max;
</div><?php
$lastEntry = null;
$nbEntries = 0;
/** @var FreshRSS_Entry */
foreach ($this->entries as $item):
$lastEntry = $item;
foreach ($this->entries as $entry):
$lastEntry = $entry;
$nbEntries++;
ob_flush();
/** @var FreshRSS_Entry */
$item = Minz_ExtensionManager::callHook('entry_before_display', $item);
if ($item == null) {
$entry = Minz_ExtensionManager::callHook('entry_before_display', $entry);
if ($entry == null) {
continue;
}
$this->entry = $item;
$tags = null;
$firstTags = array();
$remainingTags = array();
if (FreshRSS_Context::userConf()->show_tags == 'h' || FreshRSS_Context::userConf()->show_tags == 'f' || FreshRSS_Context::userConf()->show_tags == 'b') {
$tags = $this->entry->tags();
if (!empty($tags)) {
if ($MAX_TAGS_DISPLAYED > 0) {
$firstTags = array_slice($tags, 0, $MAX_TAGS_DISPLAYED);
$remainingTags = array_slice($tags, $MAX_TAGS_DISPLAYED);
} else {
$firstTags = $tags;
}
}
}
$this->entry = $entry;
//We most likely already have the feed object in cache, otherwise make a request
$feed = FreshRSS_CategoryDAO::findFeed($this->categories, $item->feedId()) ?? $item->feed() ?? FreshRSS_Feed::example();
?><div class="flux<?= !$item->isRead() ? ' not_read' : '' ?><?= $item->isFavorite() ? ' favorite' : '' ?>" id="flux_<?= $item->id() ?>" data-priority="<?= $feed->priority() ?>">
<article class="flux_content" dir="auto">
<div class="content <?= $content_width ?>">
<header>
<?php
$favoriteUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id()));
if ($item->isFavorite()) {
$favoriteUrl['params']['is_favorite'] = 0;
}
$readUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id()));
if ($item->isRead()) {
$readUrl['params']['is_read'] = 0;
}
?>
<div class="article-header-topline">
<?php if (FreshRSS_Auth::hasAccess()) { ?>
<a class="read" href="<?= Minz_Url::display($readUrl) ?>" title="<?= _t('conf.shortcut.mark_read') ?>"><?= _i($item->isRead() ? 'read' : 'unread') ?></a>
<a class="bookmark" href="<?= Minz_Url::display($favoriteUrl) ?>" title="<?= _t('conf.shortcut.mark_favorite') ?>"><?= _i($item->isFavorite() ? 'starred' : 'non-starred') ?></a>
<?php } ?>
<?php if (FreshRSS_Context::userConf()->show_feed_name === 't') { ?>
<a class="website" href="<?= _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
<?php if (FreshRSS_Context::userConf()->show_favicons): ?>
<img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
endif; ?><span><?= $feed->name() ?></span></a>
<?php } ?>
</div>
<?php if (FreshRSS_Context::userConf()->show_tags === 'h' || FreshRSS_Context::userConf()->show_tags === 'b') { ?>
<div class="tags">
<?php
if (!empty($tags)) {
?><?= _i('tag') ?><ul class="list-tags"><?php
foreach ($firstTags as $tag) {
?><li class="item tag"><a class="link-tag" href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li><?php
}
if (!empty($remainingTags)) { // more than 7 tags: show dropdown menu ?>
<li class="item tag">
<div class="dropdown">
<div id="dropdown-tags-<?= $this->entry->id() ?>" class="dropdown-target"></div>
<a class="dropdown-toggle" href="#dropdown-tags-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
<ul class="dropdown-menu">
<li class="dropdown-header"><?= _t('index.tag.related') ?></li>
<?php
foreach ($remainingTags as $tag) {
?><li class="item"><a href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>"><?= $tag ?></a></li><?php
} ?>
</ul>
<a class="dropdown-close" href="#close"></a>
</div>
</li>
<?php
} ?>
</ul><?php
} ?>
</div>
<?php } ?>
<h1 class="title"><a target="_blank" rel="noreferrer" class="go_website" href="<?= $item->link() ?>"><?= $item->title() ?></a></h1>
<?php if (FreshRSS_Context::userConf()->show_author_date === 'h' || FreshRSS_Context::userConf()->show_author_date === 'b') { ?>
<div class="subtitle">
<?php if (FreshRSS_Context::userConf()->show_feed_name === 'a') { ?>
<div class="website"><a href="<?= _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
<?php if (FreshRSS_Context::userConf()->show_favicons): ?>
<img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
endif; ?><span><?= $feed->name() ?></span></a></div>
<?php } ?>
<div class="author"><?php
$authors = $item->authors();
if (is_array($authors)) {
foreach ($authors as $author) {
?>
<a href="<?= Minz_Url::display(Minz_Request::modifiedCurrentRequest(['search' => 'author:' . str_replace(' ', '+', htmlspecialchars_decode($author, ENT_QUOTES))])) ?>">
<?= $author ?>
</a>
<?php
}
}
?>
</div>
<div class="date">
<time datetime="<?= $item->machineReadableDate() ?>"><?= $item->date() ?></time>
</div>
</div>
<?php } ?>
</header>
<div class="text">
<?= $item->content(true) ?>
</div>
<?php
$display_authors_date = FreshRSS_Context::userConf()->show_author_date === 'f' || FreshRSS_Context::userConf()->show_author_date === 'b';
$display_tags = FreshRSS_Context::userConf()->show_tags === 'f' || FreshRSS_Context::userConf()->show_tags === 'b';
if ($display_authors_date || $display_tags) {
?>
<footer>
<?php if ($display_authors_date) { ?>
<div class="subtitle">
<?php if (FreshRSS_Context::userConf()->show_feed_name === 'a') { ?>
<div class="website"><a href="<?= _url('index', 'reader', 'get', 'f_' . $feed->id()) ?>" title="<?= _t('gen.action.filter') ?>">
<?php if (FreshRSS_Context::userConf()->show_favicons): ?>
<img class="favicon" src="<?= $feed->favicon() ?>" alt="✇" loading="lazy" /><?php
endif; ?><span><?= $feed->name() ?></span></a></div>
<?php } ?>
<div class="author"><?php
$authors = $item->authors();
if (is_array($authors)) {
foreach ($authors as $author) {
?>
<a href="<?= Minz_Url::display(Minz_Request::modifiedCurrentRequest(['search' => 'author:' . str_replace(' ', '+', htmlspecialchars_decode($author, ENT_QUOTES))])) ?>">
<?= $author ?>
</a>
<?php
}
}
?>
</div>
<div class="date">
<time datetime="<?= $item->machineReadableDate() ?>"><?= $item->date() ?></time>
</div>
</div>
<?php
}
if ($display_tags) { ?>
<div class="tags">
<?php
if (!empty($tags)) {
?><?= _i('tag') ?><ul class="list-tags"><?php
foreach ($firstTags as $tag) {
?><li class="item tag"><a class="link-tag" href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>">#<?= $tag ?></a></li><?php
}
if (!empty($remainingTags)) { // more than 7 tags: show dropdown menu ?>
<li class="item tag">
<div class="dropdown">
<div id="dropdown-tags2-<?= $this->entry->id() ?>" class="dropdown-target"></div>
<a class="dropdown-toggle" href="#dropdown-tags2-<?= $this->entry->id() ?>"><?= _i('down') ?></a>
<ul class="dropdown-menu">
<li class="dropdown-header"><?= _t('index.tag.related') ?></li>
<?php
foreach ($remainingTags as $tag) {
?><li class="item"><a href="<?= _url('index', 'index', 'search', '#' . str_replace(' ', '+', htmlspecialchars_decode($tag, ENT_QUOTES))) ?>" title="<?= _t('gen.action.filter') ?>"><?= $tag ?></a></li><?php
} ?>
</ul>
<a class="dropdown-close" href="#close"></a>
</div>
</li>
<?php
} ?>
</ul><?php
} ?>
</div>
<?php } ?>
</footer>
<?php
} ?>
</div>
</article>
$this->feed = FreshRSS_Category::findFeed($this->categories, $entry->feedId()) ?? $entry->feed() ?? FreshRSS_Feed::default();
?><div class="flux<?= !$entry->isRead() ? ' not_read' : '' ?><?= $entry->isFavorite() ? ' favorite' : '' ?>" id="flux_<?= $entry->id() ?>" data-priority="<?= $this->feed->priority() ?>">
<?php $this->renderHelper('index/article'); ?>
</div><?php
endforeach;

View File

@ -8,14 +8,12 @@
>
<channel>
<title><?= $this->rss_title ?></title>
<link><?= $this->internal_rendering ? htmlspecialchars($this->rss_url, ENT_NOQUOTES, 'UTF-8') : Minz_Url::display('', 'html', true) ?></link>
<link><?= $this->html_url ?></link>
<description><?= _t('index.feed.rss_of', $this->rss_title) ?></description>
<pubDate><?= date('D, d M Y H:i:s O') ?></pubDate>
<lastBuildDate><?= gmdate('D, d M Y H:i:s') ?> GMT</lastBuildDate>
<atom:link href="<?= $this->internal_rendering ? htmlspecialchars($this->rss_url, ENT_COMPAT, 'UTF-8') :
Minz_Url::display($this->rss_url, 'html', true) ?>" rel="self" type="application/rss+xml" />
<atom:link href="<?= $this->rss_url ?>" rel="self" type="application/rss+xml" />
<?php
/** @var FreshRSS_Entry */
foreach ($this->entries as $item) {
if (!$this->internal_rendering) {
/** @var FreshRSS_Entry */

View File

@ -62,6 +62,7 @@
<p class="help"><?= _i('help') ?> <?= _t('admin.auth.token_help') ?></p>
<kbd><?= Minz_Url::display(array('a' => 'rss', 'params' => array('user' => Minz_User::name(),
'token' => $token, 'hours' => FreshRSS_Context::userConf()->since_hours_posts_per_rss)), 'html', true) ?></kbd>
<p class="help"><?= _i('help') ?> <?= _t('conf.query.help') ?></a></p>
</div>
</div>
<?php } ?>

View File

@ -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,

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -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)

View File

@ -167,10 +167,7 @@ This means that if you assign a shortcut to more than one action, youll 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

View File

@ -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, youre 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)

View File

@ -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.

View File

@ -162,11 +162,11 @@ class Minz_Request {
* Setteurs
*/
public static function _controllerName(string $controller_name): void {
self::$controller_name = $controller_name;
self::$controller_name = ctype_alnum($controller_name) ? $controller_name : '';
}
public static function _actionName(string $action_name): void {
self::$action_name = $action_name;
self::$action_name = ctype_alnum($action_name) ? $action_name : '';
}
/** @param array<string,string> $params */
@ -187,6 +187,7 @@ class Minz_Request {
* Initialise la Request
*/
public static function init(): void {
self::_params($_GET);
self::initJSON();
}

View File

@ -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();
}

175
p/api/query.php Normal file
View File

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
require(__DIR__ . '/../../constants.php');
require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
Minz_Request::init();
$token = Minz_Request::paramString('t');
if (!ctype_alnum($token)) {
header('HTTP/1.1 422 Unprocessable Entity');
header('Content-Type: text/plain; charset=UTF-8');
die('Invalid token `t`!' . $token);
}
$format = Minz_Request::paramString('f');
if (!in_array($format, ['atom', 'html', 'opml', 'rss'], true)) {
header('HTTP/1.1 422 Unprocessable Entity');
header('Content-Type: text/plain; charset=UTF-8');
die('Invalid format `f`!');
}
$user = Minz_Request::paramString('user');
if (!FreshRSS_user_Controller::checkUsername($user)) {
header('HTTP/1.1 422 Unprocessable Entity');
header('Content-Type: text/plain; charset=UTF-8');
die('Invalid user!');
}
Minz_Session::init('FreshRSS', true);
FreshRSS_Context::initSystem();
if (!FreshRSS_Context::hasSystemConf() || !FreshRSS_Context::systemConf()->api_enabled) {
header('HTTP/1.1 503 Service Unavailable');
header('Content-Type: text/plain; charset=UTF-8');
die('Service Unavailable!');
}
FreshRSS_Context::initUser($user);
if (!FreshRSS_Context::hasUserConf()) {
usleep(rand(100, 10000)); //Primitive mitigation of scanning for users
header('HTTP/1.1 404 Not Found');
header('Content-Type: text/plain; charset=UTF-8');
die('User not found!');
} else {
usleep(rand(20, 200));
}
if (!file_exists(DATA_PATH . '/no-cache.txt')) {
require(LIB_PATH . '/http-conditional.php');
// TODO: Consider taking advantage of $feedMode, only for monotonous queries {all, categories, feeds} and not dynamic ones {read/unread, favourites, user labels}
if (httpConditional(FreshRSS_UserDAO::mtime($user) ?: time(), 0, 0, false, PHP_COMPRESSION, false)) {
exit(); //No need to send anything
}
}
Minz_Translate::init(FreshRSS_Context::userConf()->language);
Minz_ExtensionManager::init();
Minz_ExtensionManager::enableByList(FreshRSS_Context::userConf()->extensions_enabled, 'user');
$query = null;
$userSearch = null;
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
if (!empty($raw_query['token']) && $raw_query['token'] === $token) {
switch ($format) {
case 'atom':
case 'html':
case 'rss':
if (empty($raw_query['shareRss'])) {
continue 2;
}
break;
case 'opml':
if (empty($raw_query['shareOpml'])) {
continue 2;
}
break;
default:
continue 2;
}
$query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
Minz_Request::_param('get', $query->getGet());
if (Minz_Request::paramString('order') === '') {
Minz_Request::_param('order', $query->getOrder());
}
Minz_Request::_param('state', $query->getState());
$search = $query->getSearch()->getRawInput();
// Note: we disallow references to user queries in public user search to avoid sniffing internal user queries
$userSearch = new FreshRSS_BooleanSearch(Minz_Request::paramString('search'), 0, 'AND', false);
if ($userSearch->getRawInput() !== '') {
if ($search === '') {
$search = $userSearch->getRawInput();
} else {
$search .= ' (' . $userSearch->getRawInput() . ')';
}
}
Minz_Request::_param('search', $search);
break;
}
}
if ($query === null || $userSearch === null) {
usleep(rand(100, 10000));
header('HTTP/1.1 404 Not Found');
header('Content-Type: text/plain; charset=UTF-8');
die('User query not found!');
}
$view = new FreshRSS_View();
try {
FreshRSS_Context::updateUsingRequest(false);
Minz_Request::_param('search', $userSearch->getRawInput()); // Restore user search
$view->entries = FreshRSS_index_Controller::listEntriesByContext();
} catch (Minz_Exception $e) {
Minz_Error::error(400, 'Bad user query!');
die();
}
$get = FreshRSS_Context::currentGet(true);
$type = (string)$get[0];
$id = (int)$get[1];
switch ($type) {
case 'c': // Category
$cat = FreshRSS_Context::categories()[$id] ?? null;
if ($cat === null) {
Minz_Error::error(404, "Category {$id} not found!");
die();
}
$view->categories = [ $cat->id() => $cat ];
break;
case 'f': // Feed
$feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
if ($feed === null) {
Minz_Error::error(404, "Feed {$id} not found!");
die();
}
$view->feeds = [ $feed->id() => $feed ];
$view->categories = [];
break;
default:
$view->categories = FreshRSS_Context::categories();
break;
}
$view->disable_aside = true;
$view->excludeMutedFeeds = true;
$view->internal_rendering = true;
$view->userQuery = $query;
$view->html_url = $query->sharedUrlHtml();
$view->rss_url = $query->sharedUrlRss();
$view->rss_title = $query->getName();
if ($query->getName() != '') {
FreshRSS_View::_title($query->getName());
}
FreshRSS_Context::systemConf()->allow_anonymous = true;
if (in_array($format, ['rss', 'atom'], true)) {
header('Content-Type: application/rss+xml; charset=utf-8');
$view->_layout(null);
$view->_path('index/rss.phtml');
} elseif ($format === 'opml') {
if (!$query->safeForOpml()) {
Minz_Error::error(404, 'OPML not allowed for this user query!');
die();
}
header('Content-Type: application/xml; charset=utf-8');
$view->_layout(null);
$view->_path('index/opml.phtml');
} else {
$view->_layout('layout');
$view->_path('index/html.phtml');
}
$view->build();

Some files were not shown because too many files have changed in this diff Show More