aboutsummaryrefslogtreecommitdiff
path: root/app/Models
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2024-02-26 09:01:03 +0100
committerGravatar GitHub <noreply@github.com> 2024-02-26 09:01:03 +0100
commit39cc1c11ec596176e842cc98e6a54337e3c04d7e (patch)
treedab89beb80268acb5e4bd58dfc55297bd30a8486 /app/Models
parent25166c218be4e1ce1cb098de274a231b623d527e (diff)
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
Diffstat (limited to 'app/Models')
-rw-r--r--app/Models/BooleanSearch.php34
-rw-r--r--app/Models/Category.php54
-rw-r--r--app/Models/CategoryDAO.php72
-rw-r--r--app/Models/Context.php54
-rw-r--r--app/Models/Entry.php22
-rw-r--r--app/Models/EntryDAO.php20
-rw-r--r--app/Models/Feed.php5
-rw-r--r--app/Models/FeedDAO.php20
-rw-r--r--app/Models/TagDAO.php14
-rw-r--r--app/Models/UserConfiguration.php16
-rw-r--r--app/Models/UserQuery.php210
-rw-r--r--app/Models/View.php10
-rw-r--r--app/Models/ViewJavascript.php6
-rw-r--r--app/Models/ViewStats.php6
14 files changed, 333 insertions, 210 deletions
diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php
index 78b7593b2..dd8b95efb 100644
--- a/app/Models/BooleanSearch.php
+++ b/app/Models/BooleanSearch.php
@@ -16,14 +16,12 @@ class FreshRSS_BooleanSearch {
private string $operator;
/** @param 'AND'|'OR'|'AND NOT' $operator */
- public function __construct(string $input, int $level = 0, string $operator = 'AND') {
+ public function __construct(string $input, int $level = 0, string $operator = 'AND', bool $allowUserQueries = true) {
$this->operator = $operator;
$input = trim($input);
if ($input === '') {
return;
}
- $this->raw_input = $input;
-
if ($level === 0) {
$input = preg_replace('/:&quot;(.*?)&quot;/', ':"\1"', $input);
if (!is_string($input)) {
@@ -34,9 +32,11 @@ class FreshRSS_BooleanSearch {
return;
}
- $input = $this->parseUserQueryNames($input);
- $input = $this->parseUserQueryIds($input);
+ $input = $this->parseUserQueryNames($input, $allowUserQueries);
+ $input = $this->parseUserQueryIds($input, $allowUserQueries);
+ $input = trim($input);
}
+ $this->raw_input = $input;
// Either parse everything as a series of BooleanSearch’s combined by implicit AND
// or parse everything as a series of Search’s combined by explicit OR
@@ -46,7 +46,7 @@ class FreshRSS_BooleanSearch {
/**
* Parse the user queries (saved searches) by name and expand them in the input string.
*/
- private function parseUserQueryNames(string $input): string {
+ private function parseUserQueryNames(string $input, bool $allowUserQueries = true): string {
$all_matches = [];
if (preg_match_all('/\bsearch:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matchesFound)) {
$all_matches[] = $matchesFound;
@@ -60,7 +60,7 @@ class FreshRSS_BooleanSearch {
/** @var array<string,FreshRSS_UserQuery> */
$queries = [];
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
- $query = new FreshRSS_UserQuery($raw_query);
+ $query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
$queries[$query->getName()] = $query;
}
@@ -74,7 +74,11 @@ class FreshRSS_BooleanSearch {
$name = trim($matches['search'][$i]);
if (!empty($queries[$name])) {
$fromS[] = $matches[0][$i];
- $toS[] = '(' . trim($queries[$name]->getSearch()->getRawInput()) . ')';
+ if ($allowUserQueries) {
+ $toS[] = '(' . trim($queries[$name]->getSearch()->getRawInput()) . ')';
+ } else {
+ $toS[] = '';
+ }
}
}
}
@@ -87,7 +91,7 @@ class FreshRSS_BooleanSearch {
/**
* Parse the user queries (saved searches) by ID and expand them in the input string.
*/
- private function parseUserQueryIds(string $input): string {
+ private function parseUserQueryIds(string $input, bool $allowUserQueries = true): string {
$all_matches = [];
if (preg_match_all('/\bS:(?P<search>\d+)/', $input, $matchesFound)) {
@@ -95,14 +99,10 @@ class FreshRSS_BooleanSearch {
}
if (!empty($all_matches)) {
- $category_dao = FreshRSS_Factory::createCategoryDao();
- $feed_dao = FreshRSS_Factory::createFeedDao();
- $tag_dao = FreshRSS_Factory::createTagDao();
-
/** @var array<string,FreshRSS_UserQuery> */
$queries = [];
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
- $query = new FreshRSS_UserQuery($raw_query, $feed_dao, $category_dao, $tag_dao);
+ $query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
$queries[] = $query;
}
@@ -117,7 +117,11 @@ class FreshRSS_BooleanSearch {
$id = (int)(trim($matches['search'][$i])) - 1;
if (!empty($queries[$id])) {
$fromS[] = $matches[0][$i];
- $toS[] = '(' . trim($queries[$id]->getSearch()->getRawInput()) . ')';
+ if ($allowUserQueries) {
+ $toS[] = '(' . trim($queries[$id]->getSearch()->getRawInput()) . ')';
+ } else {
+ $toS[] = '';
+ }
}
}
}
diff --git a/app/Models/Category.php b/app/Models/Category.php
index 1f5b4dc61..6674b4e72 100644
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -95,7 +95,7 @@ class FreshRSS_Category extends Minz_Model {
}
/**
- * @return array<FreshRSS_Feed>
+ * @return array<int,FreshRSS_Feed>
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
@@ -110,10 +110,8 @@ class FreshRSS_Category extends Minz_Model {
$this->nbNotRead += $feed->nbNotRead();
$this->hasFeedsWithError |= ($feed->inError() && !$feed->mute());
}
-
$this->sortFeeds();
}
-
return $this->feeds ?? [];
}
@@ -143,7 +141,6 @@ class FreshRSS_Category extends Minz_Model {
if (!is_array($values)) {
$values = [$values];
}
-
$this->feeds = $values;
$this->sortFeeds();
}
@@ -157,7 +154,6 @@ class FreshRSS_Category extends Minz_Model {
}
$feed->_category($this);
$this->feeds[] = $feed;
-
$this->sortFeeds();
}
@@ -243,8 +239,54 @@ class FreshRSS_Category extends Minz_Model {
if ($this->feeds === null) {
return;
}
- usort($this->feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
+ uasort($this->feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
return strnatcasecmp($a->name(), $b->name());
});
}
+
+ /**
+ * Access cached feed
+ * @param array<FreshRSS_Category> $categories
+ */
+ public static function findFeed(array $categories, int $feed_id): ?FreshRSS_Feed {
+ foreach ($categories as $category) {
+ foreach ($category->feeds() as $feed) {
+ if ($feed->id() === $feed_id) {
+ $feed->_category($category); // Should already be done; just to be safe
+ return $feed;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Access cached feeds
+ * @param array<FreshRSS_Category> $categories
+ * @return array<int,FreshRSS_Feed>
+ */
+ public static function findFeeds(array $categories): array {
+ $result = [];
+ foreach ($categories as $category) {
+ foreach ($category->feeds() as $feed) {
+ $result[$feed->id()] = $feed;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * @param array<FreshRSS_Category> $categories
+ */
+ public static function countUnread(array $categories, int $minPriority = 0): int {
+ $n = 0;
+ foreach ($categories as $category) {
+ foreach ($category->feeds() as $feed) {
+ if ($feed->priority() >= $minPriority) {
+ $n += $feed->nbNotRead();
+ }
+ }
+ }
+ return $n;
+ }
}
diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php
index 8ea8090b8..90c3db30d 100644
--- a/app/Models/CategoryDAO.php
+++ b/app/Models/CategoryDAO.php
@@ -245,19 +245,19 @@ SQL;
$sql = 'SELECT * FROM `_category` WHERE id=:id';
$res = $this->fetchAssoc($sql, ['id' => $id]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
- $cat = self::daoToCategory($res);
- return $cat[0] ?? null;
+ $categories = self::daoToCategories($res);
+ return reset($categories) ?: null;
}
public function searchByName(string $name): ?FreshRSS_Category {
$sql = 'SELECT * FROM `_category` WHERE name=:name';
$res = $this->fetchAssoc($sql, ['name' => $name]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
- $cat = self::daoToCategory($res);
- return $cat[0] ?? null;
+ $categories = self::daoToCategories($res);
+ return reset($categories) ?: null;
}
- /** @return array<FreshRSS_Category> */
+ /** @return array<int,FreshRSS_Category> */
public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false): array {
$categories = $this->listCategories($prePopulateFeeds, $details);
@@ -277,7 +277,7 @@ SQL;
return $categories;
}
- /** @return array<FreshRSS_Category> */
+ /** @return array<int,FreshRSS_Category> */
public function listCategories(bool $prePopulateFeeds = true, bool $details = false): array {
if ($prePopulateFeeds) {
$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.kind AS c_kind, c.`lastUpdate` AS c_last_update, c.error AS c_error, c.attributes AS c_attributes, '
@@ -293,7 +293,7 @@ SQL;
$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
/** @var array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'category'?:int,'website'?:string,'priority'?:int,'error'?:int|bool,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $res */
- return self::daoToCategoryPrepopulated($res);
+ return self::daoToCategoriesPrepopulated($res);
} else {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
@@ -305,11 +305,11 @@ SQL;
} else {
$res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name');
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
- return $res == null ? [] : self::daoToCategory($res);
+ return empty($res) ? [] : self::daoToCategories($res);
}
}
- /** @return array<FreshRSS_Category> */
+ /** @return array<int,FreshRSS_Category> */
public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0): array {
$sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`'
. ($limit < 1 ? '' : ' LIMIT ' . $limit);
@@ -318,7 +318,7 @@ SQL;
$stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) &&
$stm->bindValue(':lu', time() - $defaultCacheDuration, PDO::PARAM_INT) &&
$stm->execute()) {
- return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC));
+ return self::daoToCategories($stm->fetchAll(PDO::FETCH_ASSOC));
} else {
$info = $stm ? $stm->errorInfo() : $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
@@ -333,9 +333,9 @@ SQL;
$sql = 'SELECT * FROM `_category` WHERE id=:id';
$res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
- $cat = self::daoToCategory($res);
- if (isset($cat[0])) {
- return $cat[0];
+ $categories = self::daoToCategories($res);
+ if (isset($categories[self::DEFAULTCATEGORYID])) {
+ return $categories[self::DEFAULTCATEGORYID];
} else {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n");
@@ -394,41 +394,13 @@ SQL;
return isset($res[0]) ? (int)$res[0] : -1;
}
- /** @param array<FreshRSS_Category> $categories */
- public static function findFeed(array $categories, int $feed_id): ?FreshRSS_Feed {
- foreach ($categories as $category) {
- foreach ($category->feeds() as $feed) {
- if ($feed->id() === $feed_id) {
- $feed->_category($category); // Should already be done; just to be safe
- return $feed;
- }
- }
- }
- return null;
- }
-
- /**
- * @param array<FreshRSS_Category> $categories
- */
- public static function countUnread(array $categories, int $minPriority = 0): int {
- $n = 0;
- foreach ($categories as $category) {
- foreach ($category->feeds() as $feed) {
- if ($feed->priority() >= $minPriority) {
- $n += $feed->nbNotRead();
- }
- }
- }
- return $n;
- }
-
/**
* @param array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'website'?:string,'priority'?:int,
* 'error'?:int|bool,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $listDAO
* @return array<int,FreshRSS_Category>
*/
- private static function daoToCategoryPrepopulated(array $listDAO): array {
+ private static function daoToCategoriesPrepopulated(array $listDAO): array {
$list = [];
$previousLine = [];
$feedsDao = [];
@@ -441,11 +413,11 @@ SQL;
$cat = new FreshRSS_Category(
$previousLine['c_name'],
$previousLine['c_id'],
- $feedDao::daoToFeed($feedsDao, $previousLine['c_id'])
+ $feedDao::daoToFeeds($feedsDao, $previousLine['c_id'])
);
$cat->_kind($previousLine['c_kind']);
$cat->_attributes($previousLine['c_attributes'] ?? '[]');
- $list[(int)$previousLine['c_id']] = $cat;
+ $list[$cat->id()] = $cat;
$feedsDao = []; //Prepare for next category
}
@@ -459,13 +431,13 @@ SQL;
$cat = new FreshRSS_Category(
$previousLine['c_name'],
$previousLine['c_id'],
- $feedDao::daoToFeed($feedsDao, $previousLine['c_id'])
+ $feedDao::daoToFeeds($feedsDao, $previousLine['c_id'])
);
$cat->_kind($previousLine['c_kind']);
$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
$cat->_error($previousLine['c_error'] ?? 0);
$cat->_attributes($previousLine['c_attributes'] ?? []);
- $list[(int)$previousLine['c_id']] = $cat;
+ $list[$cat->id()] = $cat;
}
return $list;
@@ -473,11 +445,10 @@ SQL;
/**
* @param array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $listDAO
- * @return array<FreshRSS_Category>
+ * @return array<int,FreshRSS_Category>
*/
- private static function daoToCategory(array $listDAO): array {
+ private static function daoToCategories(array $listDAO): array {
$list = [];
-
foreach ($listDAO as $dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'lastUpdate', 'error']);
$cat = new FreshRSS_Category(
@@ -488,9 +459,8 @@ SQL;
$cat->_lastUpdate($dao['lastUpdate'] ?? 0);
$cat->_error($dao['error'] ?? 0);
$cat->_attributes($dao['attributes'] ?? '');
- $list[] = $cat;
+ $list[$cat->id()] = $cat;
}
-
return $list;
}
}
diff --git a/app/Models/Context.php b/app/Models/Context.php
index 2d22290bc..37a2064c6 100644
--- a/app/Models/Context.php
+++ b/app/Models/Context.php
@@ -10,11 +10,11 @@ final class FreshRSS_Context {
/**
* @var array<int,FreshRSS_Category>
*/
- public static array $categories = [];
+ private static array $categories = [];
/**
* @var array<int,FreshRSS_Tag>
*/
- public static array $tags = [];
+ private static array $tags = [];
public static string $name = '';
public static string $description = '';
public static int $total_unread = 0;
@@ -47,6 +47,7 @@ final class FreshRSS_Context {
*/
public static string $order = 'DESC';
public static int $number = 0;
+ public static int $offset = 0;
public static FreshRSS_BooleanSearch $search;
public static string $first_id = '';
public static string $next_id = '';
@@ -173,10 +174,33 @@ final class FreshRSS_Context {
FreshRSS_Context::$user_conf = null;
}
+ /** @return array<int,FreshRSS_Category> */
+ public static function categories(): array {
+ if (empty(self::$categories)) {
+ $catDAO = FreshRSS_Factory::createCategoryDao();
+ self::$categories = $catDAO->listSortedCategories(true, false);
+ }
+ return self::$categories;
+ }
+
+ /** @return array<int,FreshRSS_Feed> */
+ public static function feeds(): array {
+ return FreshRSS_Category::findFeeds(self::categories());
+ }
+
+ /** @return array<int,FreshRSS_Tag> */
+ public static function labels(bool $precounts = false): array {
+ if (empty(self::$tags) || $precounts) {
+ $tagDAO = FreshRSS_Factory::createTagDao();
+ self::$tags = $tagDAO->listTags($precounts) ?: [];
+ }
+ return self::$tags;
+ }
+
/**
* This action updates the Context object by using request parameters.
*
- * Parameters are:
+ * HTTP GET request parameters are:
* - state (default: conf->default_view)
* - search (default: empty string)
* - order (default: conf->sort_order)
@@ -187,18 +211,15 @@ final class FreshRSS_Context {
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
- public static function updateUsingRequest(): void {
- if (empty(self::$categories)) {
- $catDAO = FreshRSS_Factory::createCategoryDao();
- self::$categories = $catDAO->listSortedCategories();
+ public static function updateUsingRequest(bool $computeStatistics): void {
+ if ($computeStatistics && self::$total_unread === 0) {
+ // Update number of read / unread variables.
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ self::$total_starred = $entryDAO->countUnreadReadFavorites();
+ self::$total_unread = FreshRSS_Category::countUnread(self::categories(), FreshRSS_Feed::PRIORITY_MAIN_STREAM);
+ self::$total_important_unread = FreshRSS_Category::countUnread(self::categories(), FreshRSS_Feed::PRIORITY_IMPORTANT);
}
- // Update number of read / unread variables.
- $entryDAO = FreshRSS_Factory::createEntryDao();
- self::$total_starred = $entryDAO->countUnreadReadFavorites();
- self::$total_unread = FreshRSS_CategoryDAO::countUnread(self::$categories, FreshRSS_Feed::PRIORITY_MAIN_STREAM);
- self::$total_important_unread = FreshRSS_CategoryDAO::countUnread(self::$categories, FreshRSS_Feed::PRIORITY_IMPORTANT);
-
self::_get(Minz_Request::paramString('get') ?: 'a');
self::$state = Minz_Request::paramInt('state') ?: FreshRSS_Context::userConf()->default_state;
@@ -224,6 +245,7 @@ final class FreshRSS_Context {
FreshRSS_Context::userConf()->max_posts_per_rss,
FreshRSS_Context::userConf()->posts_per_page);
}
+ self::$offset = Minz_Request::paramInt('offset');
self::$first_id = Minz_Request::paramString('next');
self::$sinceHours = Minz_Request::paramInt('hours');
}
@@ -394,7 +416,7 @@ final class FreshRSS_Context {
break;
case 'f':
// We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description
- $feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
+ $feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_Category::findFeed(self::$categories, $id);
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
@@ -417,7 +439,7 @@ final class FreshRSS_Context {
if ($cat === null) {
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
}
- //self::$categories[$id] = $cat;
+ self::$categories[$id] = $cat;
} else {
$cat = self::$categories[$id];
}
@@ -433,7 +455,7 @@ final class FreshRSS_Context {
if ($tag === null) {
throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
}
- //self::$tags[$id] = $tag;
+ self::$tags[$id] = $tag;
} else {
$tag = self::$tags[$id];
}
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 9caca1fb7..c782f4c94 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -816,6 +816,28 @@ HTML;
}
/**
+ * @return array{array<string>,array<string>} Array of first tags to show, then array of remaining tags
+ */
+ public function tagsFormattingHelper(): array {
+ $firstTags = [];
+ $remainingTags = [];
+
+ if (FreshRSS_Context::hasUserConf() && in_array(FreshRSS_Context::userConf()->show_tags, ['b', 'f', 'h'], true)) {
+ $maxTagsDisplayed = (int)FreshRSS_Context::userConf()->show_tags_max;
+ $tags = $this->tags();
+ if (!empty($tags)) {
+ if ($maxTagsDisplayed > 0) {
+ $firstTags = array_slice($tags, 0, $maxTagsDisplayed);
+ $remainingTags = array_slice($tags, $maxTagsDisplayed);
+ } else {
+ $firstTags = $tags;
+ }
+ }
+ }
+ return [$firstTags,$remainingTags];
+ }
+
+ /**
* Integer format conversion for Google Reader API format
* @param string|int $dec Decimal number
* @return string 64-bit hexa http://code.google.com/p/google-reader-api/wiki/ItemId
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index 2f0e2b919..f770ce400 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -1063,7 +1063,7 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
- string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
+ string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
int $date_min = 0): array {
if (!$state) {
$state = FreshRSS_Entry::STATE_ALL;
@@ -1120,7 +1120,9 @@ SQL;
. 'WHERE ' . $where
. $search
. 'ORDER BY e.id ' . $order
- . ($limit > 0 ? ' LIMIT ' . intval($limit) : '')]; //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
+ . ($limit > 0 ? ' LIMIT ' . $limit : '') // http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
+ . ($offset > 0 ? ' OFFSET ' . $offset : '')
+ ];
}
/**
@@ -1131,9 +1133,9 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
- string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
+ string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
int $date_min = 0) {
- [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
+ [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
if ($order !== 'DESC' && $order !== 'ASC') {
$order = 'DESC';
@@ -1152,7 +1154,7 @@ SQL;
} else {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
- return $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
+ return $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
@@ -1167,9 +1169,9 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
- string $order = 'DESC', int $limit = 1, string $firstId = '',
+ string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '',
?FreshRSS_BooleanSearch $filters = null, int $date_min = 0): Traversable {
- $stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
+ $stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
if ($stm) {
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
if (is_array($row)) {
@@ -1233,9 +1235,9 @@ SQL;
* @throws FreshRSS_EntriesGetter_Exception
*/
public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
- string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null): ?array {
+ string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null): ?array {
- [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
+ [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters);
$stm = $this->pdo->prepare($sql);
if ($stm !== false && $stm->execute($values) && ($res = $stm->fetchAll(PDO::FETCH_COLUMN, 0)) !== false) {
/** @var array<numeric-string> $res */
diff --git a/app/Models/Feed.php b/app/Models/Feed.php
index 2eab0a3cf..b8425e86b 100644
--- a/app/Models/Feed.php
+++ b/app/Models/Feed.php
@@ -76,7 +76,7 @@ class FreshRSS_Feed extends Minz_Model {
}
}
- public static function example(): FreshRSS_Feed {
+ public static function default(): FreshRSS_Feed {
$f = new FreshRSS_Feed('http://example.net/', false);
$f->faviconPrepare();
return $f;
@@ -708,7 +708,8 @@ class FreshRSS_Feed extends Minz_Model {
$view = new FreshRSS_View();
$view->_path('index/rss.phtml');
$view->internal_rendering = true;
- $view->rss_url = $feedSourceUrl;
+ $view->rss_url = htmlspecialchars($feedSourceUrl, ENT_COMPAT, 'UTF-8');
+ $view->html_url = $view->rss_url;
$view->entries = [];
try {
diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php
index 0744970de..417c59da1 100644
--- a/app/Models/FeedDAO.php
+++ b/app/Models/FeedDAO.php
@@ -322,7 +322,7 @@ SQL;
}
/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
- $feeds = self::daoToFeed($res);
+ $feeds = self::daoToFeeds($res);
return $feeds[$id] ?? null;
}
@@ -331,7 +331,7 @@ SQL;
$res = $this->fetchAssoc($sql, [':url' => $url]);
/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
- return empty($res[0]) ? null : (current(self::daoToFeed($res)) ?: null);
+ return empty($res[0]) ? null : (current(self::daoToFeeds($res)) ?: null);
}
/** @return array<int> */
@@ -343,14 +343,14 @@ SQL;
}
/**
- * @return array<FreshRSS_Feed>
+ * @return array<int,FreshRSS_Feed>
*/
public function listFeeds(): array {
$sql = 'SELECT * FROM `_feed` ORDER BY name';
$res = $this->fetchAssoc($sql);
/** @var array<array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'ttl':int,'attributes':string}>|null $res */
- return $res == null ? [] : self::daoToFeed($res);
+ return $res == null ? [] : self::daoToFeeds($res);
}
/** @return array<string,string> */
@@ -375,7 +375,7 @@ SQL;
/**
* @param int $defaultCacheDuration Use -1 to return all feeds, without filtering them by TTL.
- * @return array<FreshRSS_Feed>
+ * @return array<int,FreshRSS_Feed>
*/
public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0): array {
$sql = 'SELECT id, url, kind, category, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes, `cache_nbEntries`, `cache_nbUnreads` '
@@ -387,7 +387,7 @@ SQL;
. ($limit < 1 ? '' : 'LIMIT ' . intval($limit));
$stm = $this->pdo->query($sql);
if ($stm !== false) {
- return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
+ return self::daoToFeeds($stm->fetchAll(PDO::FETCH_ASSOC));
} else {
$info = $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
@@ -409,7 +409,7 @@ SQL;
/**
* @param bool|null $muted to include only muted feeds
- * @return array<FreshRSS_Feed>
+ * @return array<int,FreshRSS_Feed>
*/
public function listByCategory(int $cat, ?bool $muted = null): array {
$sql = 'SELECT * FROM `_feed` WHERE category=:category';
@@ -425,9 +425,9 @@ SQL;
* @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res
*/
- $feeds = self::daoToFeed($res);
+ $feeds = self::daoToFeeds($res);
- usort($feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
+ uasort($feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
return strnatcasecmp($a->name(), $b->name());
});
@@ -585,7 +585,7 @@ SQL;
* 'pathEntries'?:string,'httpAuth'?:string,'error'?:int|bool,'ttl'?:int,'attributes'?:string,'cache_nbUnreads'?:int,'cache_nbEntries'?:int}> $listDAO
* @return array<int,FreshRSS_Feed>
*/
- public static function daoToFeed(array $listDAO, ?int $catID = null): array {
+ public static function daoToFeeds(array $listDAO, ?int $catID = null): array {
$list = [];
foreach ($listDAO as $key => $dao) {
diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php
index 391bde36d..b5611a7d6 100644
--- a/app/Models/TagDAO.php
+++ b/app/Models/TagDAO.php
@@ -184,16 +184,16 @@ SQL;
public function searchById(int $id): ?FreshRSS_Tag {
$res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE id=:id', [':id' => $id]);
/** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */
- return $res === null ? null : self::daoToTag($res)[0] ?? null;
+ return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
public function searchByName(string $name): ?FreshRSS_Tag {
$res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE name=:name', [':name' => $name]);
/** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */
- return $res === null ? null : self::daoToTag($res)[0] ?? null;
+ return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
- /** @return array<FreshRSS_Tag>|false */
+ /** @return array<int,FreshRSS_Tag>|false */
public function listTags(bool $precounts = false) {
if ($precounts) {
$sql = <<<'SQL'
@@ -211,7 +211,7 @@ SQL;
$stm = $this->pdo->query($sql);
if ($stm !== false) {
$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
- return self::daoToTag($res);
+ return self::daoToTags($res);
} else {
$info = $this->pdo->errorInfo();
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
@@ -430,9 +430,9 @@ SQL;
/**
* @param iterable<array{'id':int,'name':string,'attributes'?:string}> $listDAO
- * @return array<FreshRSS_Tag>
+ * @return array<int,FreshRSS_Tag>
*/
- private static function daoToTag(iterable $listDAO): array {
+ private static function daoToTags(iterable $listDAO): array {
$list = [];
foreach ($listDAO as $dao) {
if (empty($dao['id']) || empty($dao['name'])) {
@@ -446,7 +446,7 @@ SQL;
if (isset($dao['unreads'])) {
$tag->_nbUnread($dao['unreads']);
}
- $list[] = $tag;
+ $list[$tag->id()] = $tag;
}
return $list;
}
diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php
index a1e0dbbaa..7ccaa2671 100644
--- a/app/Models/UserConfiguration.php
+++ b/app/Models/UserConfiguration.php
@@ -41,7 +41,7 @@ declare(strict_types=1);
* @property bool $onread_jump_next
* @property string $passwordHash
* @property int $posts_per_page
- * @property array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries
+ * @property array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $queries
* @property bool $reading_confirm
* @property int $since_hours_posts_per_rss
* @property bool $show_fav_unread
@@ -82,6 +82,20 @@ final class FreshRSS_UserConfiguration extends Minz_Configuration {
}
/**
+ * Access the default configuration for users.
+ * @throws Minz_FileNotExistException
+ */
+ public static function default(): FreshRSS_UserConfiguration {
+ static $default_user_conf = null;
+ if ($default_user_conf == null) {
+ $namespace = 'user_default';
+ FreshRSS_UserConfiguration::register($namespace, '_', FRESHRSS_PATH . '/config-user.default.php');
+ $default_user_conf = FreshRSS_UserConfiguration::get($namespace);
+ }
+ return $default_user_conf;
+ }
+
+ /**
* @param non-empty-string $key
* @return array<int|string,mixed>|null
*/
diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php
index 000cfbbdd..156b2df4a 100644
--- a/app/Models/UserQuery.php
+++ b/app/Models/UserQuery.php
@@ -18,17 +18,34 @@ class FreshRSS_UserQuery {
private FreshRSS_BooleanSearch $search;
private int $state = 0;
private string $url = '';
- private ?FreshRSS_FeedDAO $feed_dao;
- private ?FreshRSS_CategoryDAO $category_dao;
- private ?FreshRSS_TagDAO $tag_dao;
+ private string $token = '';
+ private bool $shareRss = false;
+ private bool $shareOpml = false;
+ /** @var array<int,FreshRSS_Category> $categories */
+ private array $categories;
+ /** @var array<int,FreshRSS_Tag> $labels */
+ private array $labels;
+
+ public static function generateToken(string $salt): string {
+ if (!FreshRSS_Context::hasSystemConf()) {
+ return '';
+ }
+ $hash = md5(FreshRSS_Context::systemConf()->salt . $salt . random_bytes(16));
+ if (function_exists('gmp_init')) {
+ // Shorten the hash if possible by converting from base 16 to base 62
+ $hash = gmp_strval(gmp_init($hash, 16), 62);
+ }
+ return $hash;
+ }
/**
- * @param array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string} $query
+ * @param array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,shareRss?:bool,shareOpml?:bool} $query
+ * @param array<int,FreshRSS_Category> $categories
+ * @param array<int,FreshRSS_Tag> $labels
*/
- public function __construct(array $query, FreshRSS_FeedDAO $feed_dao = null, FreshRSS_CategoryDAO $category_dao = null, FreshRSS_TagDAO $tag_dao = null) {
- $this->category_dao = $category_dao;
- $this->feed_dao = $feed_dao;
- $this->tag_dao = $tag_dao;
+ public function __construct(array $query, array $categories, array $labels) {
+ $this->categories = $categories;
+ $this->labels = $labels;
if (isset($query['get'])) {
$this->parseGet($query['get']);
}
@@ -49,8 +66,18 @@ class FreshRSS_UserQuery {
if (!isset($query['search'])) {
$query['search'] = '';
}
+ if (!empty($query['token'])) {
+ $this->token = $query['token'];
+ }
+ if (isset($query['shareRss'])) {
+ $this->shareRss = $query['shareRss'];
+ }
+ if (isset($query['shareOpml'])) {
+ $this->shareOpml = $query['shareOpml'];
+ }
+
// linked too deeply with the search object, need to use dependency injection
- $this->search = new FreshRSS_BooleanSearch($query['search']);
+ $this->search = new FreshRSS_BooleanSearch($query['search'], 0, 'AND', false);
if (!empty($query['state'])) {
$this->state = intval($query['state']);
}
@@ -59,16 +86,19 @@ class FreshRSS_UserQuery {
/**
* Convert the current object to an array.
*
- * @return array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}
+ * @return array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}
*/
public function toArray(): array {
return array_filter([
'get' => $this->get,
'name' => $this->name,
'order' => $this->order,
- 'search' => $this->search->__toString(),
+ 'search' => $this->search->getRawInput(),
'state' => $this->state,
'url' => $this->url,
+ 'token' => $this->token,
+ 'shareRss' => $this->shareRss,
+ 'shareOpml' => $this->shareOpml,
]);
}
@@ -77,93 +107,44 @@ class FreshRSS_UserQuery {
*/
private function parseGet(string $get): void {
$this->get = $get;
- if (preg_match('/(?P<type>[acfst])(_(?P<id>\d+))?/', $get, $matches)) {
+ if (preg_match('/(?P<type>[acfistT])(_(?P<id>\d+))?/', $get, $matches)) {
$id = intval($matches['id'] ?? '0');
switch ($matches['type']) {
case 'a':
- $this->parseAll();
+ $this->get_type = 'all';
break;
case 'c':
- $this->parseCategory($id);
+ $this->get_type = 'category';
+ $c = $this->categories[$id] ?? null;
+ $this->get_name = $c === null ? '' : $c->name();
break;
case 'f':
- $this->parseFeed($id);
+ $this->get_type = 'feed';
+ $f = FreshRSS_Category::findFeed($this->categories, $id);
+ $this->get_name = $f === null ? '' : $f->name();
+ break;
+ case 'i':
+ $this->get_type = 'important';
break;
case 's':
- $this->parseFavorite();
+ $this->get_type = 'favorite';
break;
case 't':
- $this->parseTag($id);
+ $this->get_type = 'label';
+ $l = $this->labels[$id] ?? null;
+ $this->get_name = $l === null ? '' : $l->name();
+ break;
+ case 'T':
+ $this->get_type = 'all_labels';
break;
}
+ if ($this->get_name === '' && in_array($matches['type'], ['c', 'f', 't'], true)) {
+ $this->deprecated = true;
+ }
}
}
/**
- * Parse the query string when it is an "all" query
- */
- private function parseAll(): void {
- $this->get_name = 'all';
- $this->get_type = 'all';
- }
-
- /**
- * Parse the query string when it is a "category" query
- */
- private function parseCategory(int $id): void {
- if ($this->category_dao === null) {
- $this->category_dao = FreshRSS_Factory::createCategoryDao();
- }
- $category = $this->category_dao->searchById($id);
- if ($category !== null) {
- $this->get_name = $category->name();
- } else {
- $this->deprecated = true;
- }
- $this->get_type = 'category';
- }
-
- /**
- * Parse the query string when it is a "feed" query
- */
- private function parseFeed(int $id): void {
- if ($this->feed_dao === null) {
- $this->feed_dao = FreshRSS_Factory::createFeedDao();
- }
- $feed = $this->feed_dao->searchById($id);
- if ($feed !== null) {
- $this->get_name = $feed->name();
- } else {
- $this->deprecated = true;
- }
- $this->get_type = 'feed';
- }
-
- /**
- * Parse the query string when it is a "tag" query
- */
- private function parseTag(int $id): void {
- if ($this->tag_dao === null) {
- $this->tag_dao = FreshRSS_Factory::createTagDao();
- }
- $tag = $this->tag_dao->searchById($id);
- if ($tag !== null) {
- $this->get_name = $tag->name();
- } else {
- $this->deprecated = true;
- }
- $this->get_type = 'tag';
- }
-
- /**
- * Parse the query string when it is a "favorite" query
- */
- private function parseFavorite(): void {
- $this->get_name = 'favorite';
- $this->get_type = 'favorite';
- }
-
- /**
* Check if the current user query is deprecated.
* It is deprecated if the category or the feed used in the query are
* not existing.
@@ -219,7 +200,7 @@ class FreshRSS_UserQuery {
}
public function getOrder(): string {
- return $this->order;
+ return $this->order ?: FreshRSS_Context::userConf()->sort_order;
}
public function getSearch(): FreshRSS_BooleanSearch {
@@ -227,11 +208,74 @@ class FreshRSS_UserQuery {
}
public function getState(): int {
- return $this->state;
+ $state = $this->state;
+ if (!($state & FreshRSS_Entry::STATE_READ) && !($state & FreshRSS_Entry::STATE_NOT_READ)) {
+ $state |= FreshRSS_Entry::STATE_READ | FreshRSS_Entry::STATE_NOT_READ;
+ }
+ if (!($state & FreshRSS_Entry::STATE_FAVORITE) && !($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) {
+ $state |= FreshRSS_Entry::STATE_FAVORITE | FreshRSS_Entry::STATE_NOT_FAVORITE;
+ }
+ return $state;
}
public function getUrl(): string {
return $this->url;
}
+ public function getToken(): string {
+ return $this->token;
+ }
+
+ public function setToken(string $token): void {
+ $this->token = $token;
+ }
+
+ public function setShareRss(bool $shareRss): void {
+ $this->shareRss = $shareRss;
+ }
+
+ public function shareRss(): bool {
+ return $this->shareRss;
+ }
+
+ public function setShareOpml(bool $shareOpml): void {
+ $this->shareOpml = $shareOpml;
+ }
+
+ public function shareOpml(): bool {
+ return $this->shareOpml;
+ }
+
+ protected function sharedUrl(bool $xmlEscaped = true): string {
+ $currentUser = Minz_User::name() ?? '';
+ return Minz_Url::display("/api/query.php?user={$currentUser}&t={$this->token}", $xmlEscaped ? 'html' : '', true);
+ }
+
+ public function sharedUrlRss(bool $xmlEscaped = true): string {
+ if ($this->shareRss && $this->token !== '') {
+ return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=rss';
+ }
+ return '';
+ }
+
+ public function sharedUrlHtml(bool $xmlEscaped = true): string {
+ if ($this->shareRss && $this->token !== '') {
+ return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=html';
+ }
+ return '';
+ }
+
+ /**
+ * OPML is only safe for some query types, otherwise it risks leaking unwanted feed information.
+ */
+ public function safeForOpml(): bool {
+ return in_array($this->get_type, ['all', 'category', 'feed'], true);
+ }
+
+ public function sharedUrlOpml(bool $xmlEscaped = true): string {
+ if ($this->shareOpml && $this->token !== '' && $this->safeForOpml()) {
+ return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=opml';
+ }
+ return '';
+ }
}
diff --git a/app/Models/View.php b/app/Models/View.php
index 4dd0be36a..2595cd1fa 100644
--- a/app/Models/View.php
+++ b/app/Models/View.php
@@ -10,7 +10,7 @@ class FreshRSS_View extends Minz_View {
public $callbackBeforeFeeds;
/** @var callable */
public $callbackBeforePagination;
- /** @var array<FreshRSS_Category> */
+ /** @var array<int,FreshRSS_Category> */
public array $categories;
public ?FreshRSS_Category $category;
public ?FreshRSS_Tag $tag;
@@ -18,11 +18,11 @@ class FreshRSS_View extends Minz_View {
/** @var iterable<FreshRSS_Entry> */
public $entries;
public FreshRSS_Entry $entry;
- public ?FreshRSS_Feed $feed;
- /** @var array<FreshRSS_Feed> */
+ public FreshRSS_Feed $feed;
+ /** @var array<int,FreshRSS_Feed> */
public array $feeds;
public int $nbUnreadTags;
- /** @var array<FreshRSS_Tag> */
+ /** @var array<int,FreshRSS_Tag> */
public array $tags;
/** @var array<int,array{'id':int,'name':string,'id_entry':string,'checked':bool}> */
public array $tagsForEntry;
@@ -100,6 +100,8 @@ class FreshRSS_View extends Minz_View {
public int $nbPage;
// RSS view
+ public FreshRSS_UserQuery $userQuery;
+ public string $html_url = '';
public string $rss_title = '';
public string $rss_url = '';
public string $rss_base = '';
diff --git a/app/Models/ViewJavascript.php b/app/Models/ViewJavascript.php
index 38a0a74f0..2b3c87537 100644
--- a/app/Models/ViewJavascript.php
+++ b/app/Models/ViewJavascript.php
@@ -3,11 +3,11 @@ declare(strict_types=1);
final class FreshRSS_ViewJavascript extends FreshRSS_View {
- /** @var array<FreshRSS_Category> */
+ /** @var array<int,FreshRSS_Category> */
public array $categories;
- /** @var array<FreshRSS_Feed> */
+ /** @var array<int,FreshRSS_Feed> */
public array $feeds;
- /** @var array<FreshRSS_Tag> */
+ /** @var array<int,FreshRSS_Tag> */
public array $tags;
public string $nonce;
diff --git a/app/Models/ViewStats.php b/app/Models/ViewStats.php
index d7bb08c5f..ca98c554a 100644
--- a/app/Models/ViewStats.php
+++ b/app/Models/ViewStats.php
@@ -3,10 +3,10 @@ declare(strict_types=1);
final class FreshRSS_ViewStats extends FreshRSS_View {
- /** @var array<FreshRSS_Category> */
+ /** @var array<int,FreshRSS_Category> */
public array $categories;
- public ?FreshRSS_Feed $feed;
- /** @var array<FreshRSS_Feed> */
+ public FreshRSS_Feed $feed;
+ /** @var array<int,FreshRSS_Feed> */
public array $feeds;
public bool $displaySlider = false;