diff options
| author | 2023-03-04 13:30:45 +0100 | |
|---|---|---|
| committer | 2023-03-04 13:30:45 +0100 | |
| commit | b3239256dc6d188cda970adab516b3fcf1b86129 (patch) | |
| tree | d8e65dd9784834ba2e82ce7ee94b4718f8af19ea /app/Models | |
| parent | 27b71ffa99f7dff013fb8d51d020ed628e0d2ce6 (diff) | |
| parent | 0fe0ce894cbad09757d719dd4b400b9862c1a12a (diff) | |
Merge branch 'edge' into latest
Diffstat (limited to 'app/Models')
| -rw-r--r-- | app/Models/BooleanSearch.php | 8 | ||||
| -rw-r--r-- | app/Models/Category.php | 15 | ||||
| -rw-r--r-- | app/Models/CategoryDAO.php | 15 | ||||
| -rw-r--r-- | app/Models/ConfigurationSetter.php | 7 | ||||
| -rw-r--r-- | app/Models/Context.php | 20 | ||||
| -rw-r--r-- | app/Models/Days.php | 8 | ||||
| -rw-r--r-- | app/Models/Entry.php | 174 | ||||
| -rw-r--r-- | app/Models/EntryDAO.php | 25 | ||||
| -rw-r--r-- | app/Models/EntryDAOSQLite.php | 4 | ||||
| -rw-r--r-- | app/Models/Feed.php | 142 | ||||
| -rw-r--r-- | app/Models/FeedDAO.php | 6 | ||||
| -rw-r--r-- | app/Models/Searchable.php | 4 | ||||
| -rw-r--r-- | app/Models/SystemConfiguration.php | 6 | ||||
| -rw-r--r-- | app/Models/Tag.php | 55 | ||||
| -rw-r--r-- | app/Models/TagDAO.php | 40 | ||||
| -rw-r--r-- | app/Models/Themes.php | 1 | ||||
| -rw-r--r-- | app/Models/UserConfiguration.php | 8 | ||||
| -rw-r--r-- | app/Models/UserQuery.php | 98 | ||||
| -rw-r--r-- | app/Models/View.php | 1 |
19 files changed, 432 insertions, 205 deletions
diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php index b1c7bbd3b..279040a5a 100644 --- a/app/Models/BooleanSearch.php +++ b/app/Models/BooleanSearch.php @@ -118,8 +118,9 @@ class FreshRSS_BooleanSearch { $nextOperator = 'AND'; while ($i < $length) { $c = $input[$i]; + $backslashed = $i >= 1 ? $input[$i - 1] === '\\' : false; - if ($c === '(') { + if ($c === '(' && !$backslashed) { $hasParenthesis = true; $before = trim($before); @@ -164,11 +165,12 @@ class FreshRSS_BooleanSearch { $i++; while ($i < $length) { $c = $input[$i]; - if ($c === '(') { + $backslashed = $input[$i - 1] === '\\'; + if ($c === '(' && !$backslashed) { // One nested level deeper $parentheses++; $sub .= $c; - } elseif ($c === ')') { + } elseif ($c === ')' && !$backslashed) { $parentheses--; if ($parentheses === 0) { // Found the matching closing parenthesis diff --git a/app/Models/Category.php b/app/Models/Category.php index c4ca12fd3..b23e8da0a 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -103,9 +103,7 @@ class FreshRSS_Category extends Minz_Model { $this->hasFeedsWithError |= $feed->inError(); } - usort($this->feeds, function ($a, $b) { - return strnatcasecmp($a->name(), $b->name()); - }); + $this->sortFeeds(); } return $this->feeds; @@ -144,6 +142,7 @@ class FreshRSS_Category extends Minz_Model { } $this->feeds = $values; + $this->sortFeeds(); } /** @@ -155,6 +154,8 @@ class FreshRSS_Category extends Minz_Model { $this->feeds = []; } $this->feeds[] = $feed; + + $this->sortFeeds(); } public function _attributes($key, $value) { @@ -194,7 +195,7 @@ class FreshRSS_Category extends Minz_Model { } else { $dryRunCategory = new FreshRSS_Category(); $importService = new FreshRSS_Import_Service(); - $importService->importOpml($opml, $dryRunCategory, true, true); + $importService->importOpml($opml, $dryRunCategory, true); if ($importService->lastStatus()) { $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -245,4 +246,10 @@ class FreshRSS_Category extends Minz_Model { return $ok; } + + private function sortFeeds() { + usort($this->feeds, static function ($a, $b) { + return strnatcasecmp($a->name(), $b->name()); + }); + } } diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 20a92d52a..c855f1495 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -265,7 +265,7 @@ SQL; return $categories; } - uasort($categories, function ($a, $b) { + uasort($categories, static function ($a, $b) { $aPosition = $a->attributes('position'); $bPosition = $b->attributes('position'); if ($aPosition === $bPosition) { @@ -310,9 +310,9 @@ SQL; } /** @return array<FreshRSS_Category> */ - public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0) { + 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 ' . intval($limit)); + . ($limit < 1 ? '' : ' LIMIT ' . $limit); $stm = $this->pdo->prepare($sql); if ($stm && $stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) && @@ -387,7 +387,7 @@ SQL; return $res[0]['count']; } - public function countFeed($id) { + public function countFeed(int $id) { $sql = 'SELECT COUNT(*) AS count FROM `_feed` WHERE category=:id'; $stm = $this->pdo->prepare($sql); $stm->bindParam(':id', $id, PDO::PARAM_INT); @@ -396,7 +396,7 @@ SQL; return $res[0]['count']; } - public function countNotRead($id) { + public function countNotRead(int $id) { $sql = 'SELECT COUNT(*) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE category=:id AND e.is_read=0'; $stm = $this->pdo->prepare($sql); $stm->bindParam(':id', $id, PDO::PARAM_INT); @@ -409,7 +409,7 @@ SQL; * @param array<FreshRSS_Category> $categories * @param int $feed_id */ - public static function findFeed($categories, $feed_id) { + public static function findFeed(array $categories, int $feed_id) { foreach ($categories as $category) { foreach ($category->feeds() as $feed) { if ($feed->id() === $feed_id) { @@ -422,9 +422,8 @@ SQL; /** * @param array<FreshRSS_Category> $categories - * @param int $minPriority */ - public static function CountUnreads($categories, $minPriority = 0) { + public static function countUnread(array $categories, int $minPriority = 0): int { $n = 0; foreach ($categories as $category) { foreach ($category->feeds() as $feed) { diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index c822bcf4d..258c2ad58 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -234,6 +234,13 @@ class FreshRSS_ConfigurationSetter { $data['sticky_post'] = $this->handleBool($value); } + private function _darkMode(&$data, $value) { + if (!in_array($value, [ 'no', 'auto'], true)) { + $value = 'no'; + } + $data['darkMode'] = $value; + } + private function _bottomline_date(&$data, $value) { $data['bottomline_date'] = $this->handleBool($value); } diff --git a/app/Models/Context.php b/app/Models/Context.php index fed2a6767..734458d7f 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -58,12 +58,7 @@ class FreshRSS_Context { public static function initSystem($reload = false) { if ($reload || FreshRSS_Context::$system_conf == null) { //TODO: Keep in session what we need instead of always reloading from disk - Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php'); - /** - * @var FreshRSS_SystemConfiguration $system_conf - */ - $system_conf = Minz_Configuration::get('system'); - FreshRSS_Context::$system_conf = $system_conf; + FreshRSS_Context::$system_conf = FreshRSS_SystemConfiguration::init(DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php'); // Register the configuration setter for the system configuration $configurationSetter = new FreshRSS_ConfigurationSetter(); FreshRSS_Context::$system_conf->_configurationSetter($configurationSetter); @@ -88,17 +83,12 @@ class FreshRSS_Context { (!$userMustExist || FreshRSS_user_Controller::userExists($username))) { try { //TODO: Keep in session what we need instead of always reloading from disk - Minz_Configuration::register('user', + FreshRSS_Context::$user_conf = FreshRSS_UserConfiguration::init( USERS_PATH . '/' . $username . '/config.php', FRESHRSS_PATH . '/config-user.default.php', FreshRSS_Context::$system_conf->configurationSetter()); Minz_Session::_param('currentUser', $username); - /** - * @var FreshRSS_UserConfiguration $user_conf - */ - $user_conf = Minz_Configuration::get('user'); - FreshRSS_Context::$user_conf = $user_conf; } catch (Exception $ex) { Minz_Log::warning($ex->getMessage(), USERS_PATH . '/_/' . LOG_FILENAME); } @@ -163,7 +153,7 @@ class FreshRSS_Context { // Update number of read / unread variables. $entryDAO = FreshRSS_Factory::createEntryDao(); self::$total_starred = $entryDAO->countUnreadReadFavorites(); - self::$total_unread = FreshRSS_CategoryDAO::CountUnreads( + self::$total_unread = FreshRSS_CategoryDAO::countUnread( self::$categories, 1 ); @@ -510,4 +500,8 @@ class FreshRSS_Context { return false; } + public static function defaultTimeZone(): string { + $timezone = ini_get('date.timezone'); + return $timezone != '' ? $timezone : 'UTC'; + } } diff --git a/app/Models/Days.php b/app/Models/Days.php index 2d770c30b..d3f1ba075 100644 --- a/app/Models/Days.php +++ b/app/Models/Days.php @@ -1,7 +1,9 @@ <?php +declare(strict_types=1); + class FreshRSS_Days { - const TODAY = 0; - const YESTERDAY = 1; - const BEFORE_YESTERDAY = 2; + public const TODAY = 0; + public const YESTERDAY = 1; + public const BEFORE_YESTERDAY = 2; } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 47fcf3b4a..81ece1ce4 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -17,10 +17,14 @@ class FreshRSS_Entry extends Minz_Model { */ private $guid; + /** @var string */ private $title; private $authors; + /** @var string */ private $content; + /** @var string */ private $link; + /** @var int */ private $date; private $date_added = 0; //In microseconds /** @@ -67,14 +71,16 @@ class FreshRSS_Entry extends Minz_Model { $dao['content'] = ''; } if (!empty($dao['thumbnail'])) { - $dao['content'] .= '<p class="enclosure-content"><img src="' . $dao['thumbnail'] . '" alt="" /></p>'; + $dao['attributes']['thumbnail'] = [ + 'url' => $dao['thumbnail'], + ]; } $entry = new FreshRSS_Entry( $dao['id_feed'] ?? 0, $dao['guid'] ?? '', $dao['title'] ?? '', $dao['author'] ?? '', - $dao['content'] ?? '', + $dao['content'], $dao['link'] ?? '', $dao['date'] ?? 0, $dao['is_read'] ?? false, @@ -116,15 +122,117 @@ class FreshRSS_Entry extends Minz_Model { return $this->authors; } } - public function content(): string { - return $this->content; + + /** + * Basic test without ambition to catch all cases such as unquoted addresses, variants of entities, HTML comments, etc. + */ + private static function containsLink(string $html, string $link): bool { + return preg_match('/(?P<delim>[\'"])' . preg_quote($link, '/') . '(?P=delim)/', $html) == 1; + } + + private static function enclosureIsImage(array $enclosure): bool { + $elink = $enclosure['url'] ?? ''; + $length = $enclosure['length'] ?? 0; + $medium = $enclosure['medium'] ?? ''; + $mime = $enclosure['type'] ?? ''; + + return $elink != '' && $medium === 'image' || strpos($mime, 'image') === 0 || + ($mime == '' && $length == 0 && preg_match('/[.](avif|gif|jpe?g|png|svg|webp)$/i', $elink)); } - /** @return array<array<string,string>> */ - public function enclosures(bool $searchBodyImages = false): array { - $results = []; + /** + * @param bool $withEnclosures Set to true to include the enclosures in the returned HTML, false otherwise. + * @param bool $allowDuplicateEnclosures Set to false to remove obvious enclosure duplicates (based on simple string comparison), true otherwise. + * @return string HTML content + */ + public function content(bool $withEnclosures = true, bool $allowDuplicateEnclosures = false): string { + if (!$withEnclosures) { + return $this->content; + } + + $content = $this->content; + + $thumbnail = $this->attributes('thumbnail'); + if (!empty($thumbnail['url'])) { + $elink = $thumbnail['url']; + if ($allowDuplicateEnclosures || !self::containsLink($content, $elink)) { + $content .= <<<HTML +<figure class="enclosure"> + <p class="enclosure-content"> + <img class="enclosure-thumbnail" src="{$elink}" alt="" /> + </p> +</figure> +HTML; + } + } + + $attributeEnclosures = $this->attributes('enclosures'); + if (empty($attributeEnclosures)) { + return $content; + } + + foreach ($attributeEnclosures as $enclosure) { + $elink = $enclosure['url'] ?? ''; + if ($elink == '') { + continue; + } + if (!$allowDuplicateEnclosures && self::containsLink($content, $elink)) { + continue; + } + $credit = $enclosure['credit'] ?? ''; + $description = $enclosure['description'] ?? ''; + $length = $enclosure['length'] ?? 0; + $medium = $enclosure['medium'] ?? ''; + $mime = $enclosure['type'] ?? ''; + $thumbnails = $enclosure['thumbnails'] ?? []; + $etitle = $enclosure['title'] ?? ''; + + $content .= '<figure class="enclosure">'; + + foreach ($thumbnails as $thumbnail) { + $content .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" title="' . $etitle . '" /></p>'; + } + + if (self::enclosureIsImage($enclosure)) { + $content .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" title="' . $etitle . '" /></p>'; + } elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) { + $content .= '<p class="enclosure-content"><audio preload="none" src="' . $elink + . ($length == null ? '' : '" data-length="' . intval($length)) + . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8')) + . '" controls="controls" title="' . $etitle . '"></audio> <a download="" href="' . $elink . '">💾</a></p>'; + } elseif ($medium === 'video' || strpos($mime, 'video') === 0) { + $content .= '<p class="enclosure-content"><video preload="none" src="' . $elink + . ($length == null ? '' : '" data-length="' . intval($length)) + . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8')) + . '" controls="controls" title="' . $etitle . '"></video> <a download="" href="' . $elink . '">💾</a></p>'; + } else { //e.g. application, text, unknown + $content .= '<p class="enclosure-content"><a download="" href="' . $elink + . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8')) + . ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8')) + . '" title="' . $etitle . '">💾</a></p>'; + } + + if ($credit != '') { + $content .= '<p class="enclosure-credits">© ' . $credit . '</p>'; + } + if ($description != '') { + $content .= '<figcaption class="enclosure-description">' . $description . '</figcaption>'; + } + $content .= "</figure>\n"; + } + + return $content; + } + + /** @return iterable<array<string,string>> */ + public function enclosures(bool $searchBodyImages = false) { + $attributeEnclosures = $this->attributes('enclosures'); + if (is_array($attributeEnclosures)) { + // FreshRSS 1.20.1+: The enclosures are saved as attributes + yield from $attributeEnclosures; + } try { - $searchEnclosures = strpos($this->content, '<p class="enclosure-content') !== false; + $searchEnclosures = !is_array($attributeEnclosures) && (strpos($this->content, '<p class="enclosure-content') !== false); $searchBodyImages &= (stripos($this->content, '<img') !== false); $xpath = null; if ($searchEnclosures || $searchBodyImages) { @@ -133,6 +241,7 @@ class FreshRSS_Entry extends Minz_Model { $xpath = new DOMXpath($dom); } if ($searchEnclosures) { + // Legacy code for database entries < FreshRSS 1.20.1 $enclosures = $xpath->query('//div[@class="enclosure"]/p[@class="enclosure-content"]/*[@src]'); foreach ($enclosures as $enclosure) { $result = [ @@ -148,7 +257,7 @@ class FreshRSS_Entry extends Minz_Model { case 'audio': $result['medium'] = 'audio'; break; } } - $results[] = $result; + yield Minz_Helper::htmlspecialchars_utf8($result); } } if ($searchBodyImages) { @@ -159,26 +268,31 @@ class FreshRSS_Entry extends Minz_Model { $src = $img->getAttribute('data-src'); } if ($src != null) { - $results[] = [ + $result = [ 'url' => $src, - 'alt' => $img->getAttribute('alt'), ]; + yield Minz_Helper::htmlspecialchars_utf8($result); } } } - return $results; } catch (Exception $ex) { - return $results; + Minz_Log::debug(__METHOD__ . ' ' . $ex->getMessage()); } } /** * @return array<string,string>|null */ - public function thumbnail() { - foreach ($this->enclosures(true) as $enclosure) { - if (!empty($enclosure['url']) && empty($enclosure['type'])) { - return $enclosure; + public function thumbnail(bool $searchEnclosures = true) { + $thumbnail = $this->attributes('thumbnail'); + if (!empty($thumbnail['url'])) { + return $thumbnail; + } + if ($searchEnclosures) { + foreach ($this->enclosures(true) as $enclosure) { + if (self::enclosureIsImage($enclosure)) { + return $enclosure; + } } } return null; @@ -188,6 +302,7 @@ class FreshRSS_Entry extends Minz_Model { public function link(): string { return $this->link; } + /** @return string|int */ public function date(bool $raw = false) { if ($raw) { return $this->date; @@ -587,7 +702,7 @@ class FreshRSS_Entry extends Minz_Model { if ($entry) { // l’article existe déjà en BDD, en se contente de recharger ce contenu - $this->content = $entry->content(); + $this->content = $entry->content(false); } else { try { // The article is not yet in the database, so let’s fetch it @@ -629,7 +744,7 @@ class FreshRSS_Entry extends Minz_Model { 'guid' => $this->guid(), 'title' => $this->title(), 'author' => $this->authors(true), - 'content' => $this->content(), + 'content' => $this->content(false), 'link' => $this->link(), 'date' => $this->date(true), 'hash' => $this->hash(), @@ -677,7 +792,6 @@ class FreshRSS_Entry extends Minz_Model { 'published' => $this->date(true), // 'updated' => $this->date(true), 'title' => $this->title(), - 'summary' => ['content' => $this->content()], 'canonical' => [ ['href' => htmlspecialchars_decode($this->link(), ENT_QUOTES)], ], @@ -697,13 +811,16 @@ class FreshRSS_Entry extends Minz_Model { if ($mode === 'compat') { $item['title'] = escapeToUnicodeAlternative($this->title(), false); unset($item['alternate'][0]['type']); - if (mb_strlen($this->content(), 'UTF-8') > self::API_MAX_COMPAT_CONTENT_LENGTH) { - $item['summary']['content'] = mb_strcut($this->content(), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8'); - } - } elseif ($mode === 'freshrss') { + $item['summary'] = [ + 'content' => mb_strcut($this->content(true), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8'), + ]; + } else { + $item['content'] = [ + 'content' => $this->content(false), + ]; + } + if ($mode === 'freshrss') { $item['guid'] = $this->guid(); - unset($item['summary']); - $item['content'] = ['content' => $this->content()]; } if ($category != null && $mode !== 'freshrss') { $item['categories'][] = 'user/-/label/' . htmlspecialchars_decode($category->name(), ENT_QUOTES); @@ -718,10 +835,11 @@ class FreshRSS_Entry extends Minz_Model { } } foreach ($this->enclosures() as $enclosure) { - if (!empty($enclosure['url']) && !empty($enclosure['type'])) { + if (!empty($enclosure['url'])) { $media = [ 'href' => $enclosure['url'], - 'type' => $enclosure['type'], + 'type' => $enclosure['type'] ?? $enclosure['medium'] ?? + (self::enclosureIsImage($enclosure) ? 'image' : ''), ]; if (!empty($enclosure['length'])) { $media['length'] = intval($enclosure['length']); diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index b63515223..3b7c1ac3f 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -10,6 +10,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable { return true; } + protected static function sqlConcat($s1, $s2) { + return 'CONCAT(' . $s1 . ',' . $s2 . ')'; //MySQL + } + public static function sqlHexDecode(string $x): string { return 'unhex(' . $x . ')'; } @@ -943,8 +947,8 @@ SQL; } if ($filter->getTags()) { foreach ($filter->getTags() as $tag) { - $sub_search .= 'AND ' . $alias . 'tags LIKE ? '; - $values[] = "%{$tag}%"; + $sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' LIKE ? '; + $values[] = "%{$tag} #%"; } } if ($filter->getInurl()) { @@ -968,8 +972,8 @@ SQL; } if ($filter->getNotTags()) { foreach ($filter->getNotTags() as $tag) { - $sub_search .= 'AND ' . $alias . 'tags NOT LIKE ? '; - $values[] = "%{$tag}%"; + $sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' NOT LIKE ? '; + $values[] = "%{$tag} #%"; } } if ($filter->getNotInurl()) { @@ -1161,10 +1165,12 @@ SQL; } } - public function listByIds($ids, $order = 'DESC') { + /** @param array<string> $ids */ + public function listByIds(array $ids, string $order = 'DESC') { if (count($ids) < 1) { - yield false; - } elseif (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) { + return; + } + if (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) { // Split a query with too many variables parameters $idsChunks = array_chunk($ids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER); foreach ($idsChunks as $idsChunk) { @@ -1191,15 +1197,16 @@ SQL; /** * For API + * @return array<string> */ public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL, - $order = 'DESC', $limit = 1, $firstId = '', $filters = null) { + $order = 'DESC', $limit = 1, $firstId = '', $filters = null): array { list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters); $stm = $this->pdo->prepare($sql); $stm->execute($values); - return $stm->fetchAll(PDO::FETCH_COLUMN, 0); + return $stm->fetchAll(PDO::FETCH_COLUMN, 0) ?: []; } public function listHashForFeedGuids($id_feed, $guids) { diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index 8039581e6..35f3ef676 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -10,6 +10,10 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { return false; } + protected static function sqlConcat($s1, $s2) { + return $s1 . '||' . $s2; + } + public static function sqlHexDecode(string $x): string { return $x; } diff --git a/app/Models/Feed.php b/app/Models/Feed.php index f24ec1884..7c46199a5 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -18,6 +18,11 @@ class FreshRSS_Feed extends Minz_Model { */ const KIND_HTML_XPATH = 10; /** + * Normal XML with XPath scraping + * @var int + */ + const KIND_XML_XPATH = 15; + /** * Normal JSON with XPath scraping * @var int */ @@ -259,13 +264,14 @@ class FreshRSS_Feed extends Minz_Model { } public function _url(string $value, bool $validate = true) { $this->hash = ''; + $url = $value; if ($validate) { - $value = checkUrl($value); + $url = checkUrl($url); } - if ($value == '') { + if ($url == '') { throw new FreshRSS_BadUrl_Exception($value); } - $this->url = $value; + $this->url = $url; } public function _kind(int $value) { $this->kind = $value; @@ -502,61 +508,46 @@ class FreshRSS_Feed extends Minz_Model { $content = html_only_entity_decode($item->get_content()); - if ($item->get_enclosures() != null) { - $elinks = array(); + $attributeThumbnail = $item->get_thumbnail() ?? []; + if (empty($attributeThumbnail['url'])) { + $attributeThumbnail['url'] = ''; + } + + $attributeEnclosures = []; + if (!empty($item->get_enclosures())) { foreach ($item->get_enclosures() as $enclosure) { $elink = $enclosure->get_link(); - if ($elink != '' && empty($elinks[$elink])) { - $content .= '<div class="enclosure">'; - - if ($enclosure->get_title() != '') { - $content .= '<p class="enclosure-title">' . $enclosure->get_title() . '</p>'; - } - - $enclosureContent = ''; - $elinks[$elink] = true; + if ($elink != '') { + $etitle = $enclosure->get_title() ?? ''; + $credit = $enclosure->get_credit() ?? null; + $description = $enclosure->get_description() ?? ''; $mime = strtolower($enclosure->get_type() ?? ''); $medium = strtolower($enclosure->get_medium() ?? ''); $height = $enclosure->get_height(); $width = $enclosure->get_width(); $length = $enclosure->get_length(); - if ($medium === 'image' || strpos($mime, 'image') === 0 || - ($mime == '' && $length == null && ($width != 0 || $height != 0 || preg_match('/[.](avif|gif|jpe?g|png|svg|webp)$/i', $elink)))) { - $enclosureContent .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" /></p>'; - } elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) { - $enclosureContent .= '<p class="enclosure-content"><audio preload="none" src="' . $elink - . ($length == null ? '' : '" data-length="' . intval($length)) - . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8')) - . '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>'; - } elseif ($medium === 'video' || strpos($mime, 'video') === 0) { - $enclosureContent .= '<p class="enclosure-content"><video preload="none" src="' . $elink - . ($length == null ? '' : '" data-length="' . intval($length)) - . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8')) - . '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>'; - } else { //e.g. application, text, unknown - $enclosureContent .= '<p class="enclosure-content"><a download="" href="' . $elink - . ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8')) - . ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8')) - . '">💾</a></p>'; - } - $thumbnailContent = ''; - if ($enclosure->get_thumbnails() != null) { + $attributeEnclosure = [ + 'url' => $elink, + ]; + if ($etitle != '') $attributeEnclosure['title'] = $etitle; + if ($credit != null) $attributeEnclosure['credit'] = $credit->get_name(); + if ($description != '') $attributeEnclosure['description'] = $description; + if ($mime != '') $attributeEnclosure['type'] = $mime; + if ($medium != '') $attributeEnclosure['medium'] = $medium; + if ($length != '') $attributeEnclosure['length'] = intval($length); + if ($height != '') $attributeEnclosure['height'] = intval($height); + if ($width != '') $attributeEnclosure['width'] = intval($width); + + if (!empty($enclosure->get_thumbnails())) { foreach ($enclosure->get_thumbnails() as $thumbnail) { - if (empty($elinks[$thumbnail])) { - $elinks[$thumbnail] = true; - $thumbnailContent .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" /></p>'; + if ($thumbnail !== $attributeThumbnail['url']) { + $attributeEnclosure['thumbnails'][] = $thumbnail; } } } - $content .= $thumbnailContent; - $content .= $enclosureContent; - - if ($enclosure->get_description() != '') { - $content .= '<p class="enclosure-description">' . $enclosure->get_description() . '</p>'; - } - $content .= "</div>\n"; + $attributeEnclosures[] = $attributeEnclosure; } } } @@ -586,6 +577,10 @@ class FreshRSS_Feed extends Minz_Model { ); $entry->_tags($tags); $entry->_feed($this); + if (!empty($attributeThumbnail['url'])) { + $entry->_attributes('thumbnail', $attributeThumbnail); + } + $entry->_attributes('enclosures', $attributeEnclosures); $entry->hash(); //Must be computed before loading full content $entry->loadCompleteContent(); // Optionally load full content for truncated feeds @@ -596,7 +591,7 @@ class FreshRSS_Feed extends Minz_Model { /** * @return SimplePie|null */ - public function loadHtmlXpath(bool $loadDetails = false, bool $noCache = false) { + public function loadHtmlXpath() { if ($this->url == '') { return null; } @@ -624,8 +619,9 @@ class FreshRSS_Feed extends Minz_Model { return null; } - $cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), FreshRSS_Feed::KIND_HTML_XPATH); - $html = httpGet($feedSourceUrl, $cachePath, 'html', $this->attributes()); + $cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), $this->kind()); + $html = httpGet($feedSourceUrl, $cachePath, + $this->kind() === FreshRSS_Feed::KIND_XML_XPATH ? 'xml' : 'html', $this->attributes()); if (strlen($html) <= 0) { return null; } @@ -640,7 +636,18 @@ class FreshRSS_Feed extends Minz_Model { $doc = new DOMDocument(); $doc->recover = true; $doc->strictErrorChecking = false; - $doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); + + switch ($this->kind()) { + case FreshRSS_Feed::KIND_HTML_XPATH: + $doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); + break; + case FreshRSS_Feed::KIND_XML_XPATH: + $doc->loadXML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); + break; + default: + return null; + } + $xpath = new DOMXPath($doc); $view->rss_title = $xPathFeedTitle == '' ? $this->name() : htmlspecialchars(@$xpath->evaluate('normalize-space(' . $xPathFeedTitle . ')'), ENT_COMPAT, 'UTF-8'); @@ -653,7 +660,23 @@ class FreshRSS_Feed extends Minz_Model { foreach ($nodes as $node) { $item = []; $item['title'] = $xPathItemTitle == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTitle . ')', $node); - $item['content'] = $xPathItemContent == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemContent . ')', $node); + + $item['content'] = ''; + if ($xPathItemContent != '') { + $result = @$xpath->evaluate($xPathItemContent, $node); + if ($result instanceof DOMNodeList) { + // List of nodes, save as HTML + $content = ''; + foreach ($result as $child) { + $content .= $doc->saveHTML($child) . "\n"; + } + $item['content'] = $content; + } else { + // Typed expression, save as-is + $item['content'] = strval($result); + } + } + $item['link'] = $xPathItemUri == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemUri . ')', $node); $item['author'] = $xPathItemAuthor == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemAuthor . ')', $node); $item['timestamp'] = $xPathItemTimestamp == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTimestamp . ')', $node); @@ -679,8 +702,15 @@ class FreshRSS_Feed extends Minz_Model { $item['guid'] = 'urn:sha1:' . sha1($item['title'] . $item['content'] . $item['link']); } - if ($item['title'] . $item['content'] . $item['link'] != '') { - $item = Minz_Helper::htmlspecialchars_utf8($item); + if ($item['title'] != '' || $item['content'] != '' || $item['link'] != '') { + // HTML-encoding/escaping of the relevant fields (all except 'content') + foreach (['author', 'categories', 'guid', 'link', 'thumbnail', 'timestamp', 'title'] as $key) { + if (!empty($item[$key])) { + $item[$key] = Minz_Helper::htmlspecialchars_utf8($item[$key]); + } + } + // CDATA protection + $item['content'] = str_replace(']]>', ']]>', $item['content']); $view->entries[] = FreshRSS_Entry::fromArray($item); } } @@ -763,8 +793,10 @@ class FreshRSS_Feed extends Minz_Model { public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string { $simplePie = customSimplePie($attributes); $filename = $simplePie->get_cache_filename($url); - if ($kind == FreshRSS_Feed::KIND_HTML_XPATH) { + if ($kind === FreshRSS_Feed::KIND_HTML_XPATH) { return CACHE_PATH . '/' . $filename . '.html'; + } elseif ($kind === FreshRSS_Feed::KIND_XML_XPATH) { + return CACHE_PATH . '/' . $filename . '.xml'; } else { return CACHE_PATH . '/' . $filename . '.spc'; } @@ -966,14 +998,14 @@ class FreshRSS_Feed extends Minz_Model { $key = $hubJson['key']; //To renew our lease } } else { - @mkdir($path, 0777, true); + @mkdir($path, 0770, true); $key = sha1($path . FreshRSS_Context::$system_conf->salt); $hubJson = array( 'hub' => $this->hubUrl, 'key' => $key, ); file_put_contents($hubFilename, json_encode($hubJson)); - @mkdir(PSHB_PATH . '/keys/'); + @mkdir(PSHB_PATH . '/keys/', 0770, true); file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', $this->selfUrl); $text = 'WebSub prepared for ' . $this->url; Minz_Log::debug($text); diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 5993f50dc..1aae5fee5 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -49,11 +49,11 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable { } $values = array( - substr($valuesTmp['url'], 0, 511), + $valuesTmp['url'], $valuesTmp['kind'] ?? FreshRSS_Feed::KIND_RSS, $valuesTmp['category'], mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'), - substr($valuesTmp['website'], 0, 255), + $valuesTmp['website'], sanitizeHTML($valuesTmp['description'], '', 1023), $valuesTmp['lastUpdate'], isset($valuesTmp['priority']) ? intval($valuesTmp['priority']) : FreshRSS_Feed::PRIORITY_MAIN_STREAM, @@ -434,7 +434,7 @@ SQL; . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `_entry` e2 WHERE e2.id_feed=`_feed`.id AND e2.is_read=0)' . ($id != 0 ? ' WHERE id=:id' : ''); $stm = $this->pdo->prepare($sql); - if ($id != 0) { + if ($stm && $id != 0) { $stm->bindParam(':id', $id, PDO::PARAM_INT); } diff --git a/app/Models/Searchable.php b/app/Models/Searchable.php index d5bcea49d..a15a44ed7 100644 --- a/app/Models/Searchable.php +++ b/app/Models/Searchable.php @@ -2,5 +2,9 @@ interface FreshRSS_Searchable { + /** + * @param int|string $id + * @return Minz_Model + */ public function searchById($id); } diff --git a/app/Models/SystemConfiguration.php b/app/Models/SystemConfiguration.php index ec5960c0e..9fc79969d 100644 --- a/app/Models/SystemConfiguration.php +++ b/app/Models/SystemConfiguration.php @@ -25,6 +25,10 @@ * @property string $unsafe_autologin_enabled * @property-read array<string> $trusted_sources */ -class FreshRSS_SystemConfiguration extends Minz_Configuration { +final class FreshRSS_SystemConfiguration extends Minz_Configuration { + public static function init($config_filename, $default_filename = null): FreshRSS_SystemConfiguration { + parent::register('system', $config_filename, $default_filename); + return parent::get('system'); + } } diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 589648e26..c1290d192 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -5,40 +5,61 @@ class FreshRSS_Tag extends Minz_Model { * @var int */ private $id = 0; + /** + * @var string + */ private $name; + /** + * @var array<string,mixed> + */ private $attributes = []; + /** + * @var int + */ private $nbEntries = -1; + /** + * @var int + */ private $nbUnread = -1; - public function __construct($name = '') { + public function __construct(string $name = '') { $this->_name($name); } - public function id() { + public function id(): int { return $this->id; } - public function _id($value) { + /** + * @param int|string $value + */ + public function _id($value): void { $this->id = (int)$value; } - public function name() { + public function name(): string { return $this->name; } - public function _name($value) { + public function _name(string $value): void { $this->name = trim($value); } - public function attributes($key = '') { + /** + * @return mixed|string|array<string,mixed>|null + */ + public function attributes(string $key = '') { if ($key == '') { return $this->attributes; } else { - return isset($this->attributes[$key]) ? $this->attributes[$key] : null; + return $this->attributes[$key] ?? null; } } - public function _attributes($key, $value) { + /** + * @param mixed|string|array<string,mixed>|null $value + */ + public function _attributes(string $key, $value = null): void { if ($key == '') { if (is_string($value)) { $value = json_decode($value, true); @@ -53,27 +74,33 @@ class FreshRSS_Tag extends Minz_Model { } } - public function nbEntries() { + public function nbEntries(): int { if ($this->nbEntries < 0) { $tagDAO = FreshRSS_Factory::createTagDao(); - $this->nbEntries = $tagDAO->countEntries($this->id()); + $this->nbEntries = $tagDAO->countEntries($this->id()) ?: 0; } return $this->nbEntries; } - public function _nbEntries($value) { + /** + * @param string|int $value + */ + public function _nbEntries($value): void { $this->nbEntries = (int)$value; } - public function nbUnread() { + public function nbUnread(): int { if ($this->nbUnread < 0) { $tagDAO = FreshRSS_Factory::createTagDao(); - $this->nbUnread = $tagDAO->countNotRead($this->id()); + $this->nbUnread = $tagDAO->countNotRead($this->id()) ?: 0; } return $this->nbUnread; } - public function _nbUnread($value) { + /** + * @param string|int$value + */ + public function _nbUnread($value): void { $this->nbUnread = (int)$value; } } diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php index f232b2f9f..35123606b 100644 --- a/app/Models/TagDAO.php +++ b/app/Models/TagDAO.php @@ -267,12 +267,13 @@ SQL; return $newestItemUsec; } + /** @return int|false */ public function count() { $sql = 'SELECT COUNT(*) AS count FROM `_tag`'; $stm = $this->pdo->query($sql); if ($stm !== false) { $res = $stm->fetchAll(PDO::FETCH_ASSOC); - return $res[0]['count']; + return (int)$res[0]['count']; } else { $info = $this->pdo->errorInfo(); if ($this->autoUpdateDb($info)) { @@ -283,16 +284,27 @@ SQL; } } - public function countEntries($id) { + /** + * @return int|false + */ + public function countEntries(int $id) { $sql = 'SELECT COUNT(*) AS count FROM `_entrytag` WHERE id_tag=?'; - $stm = $this->pdo->prepare($sql); $values = array($id); - $stm->execute($values); - $res = $stm->fetchAll(PDO::FETCH_ASSOC); - return $res[0]['count']; + if (($stm = $this->pdo->prepare($sql)) !== false && + $stm->execute($values) && + ($res = $stm->fetchAll(PDO::FETCH_ASSOC)) !== false) { + return (int)$res[0]['count']; + } else { + $info = is_object($stm) ? $stm->errorInfo() : $this->pdo->errorInfo(); + Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info)); + return false; + } } - public function countNotRead($id = null) { + /** + * @return int|false + */ + public function countNotRead(?int $id = null) { $sql = 'SELECT COUNT(*) AS count FROM `_entrytag` et ' . 'INNER JOIN `_entry` e ON et.id_entry=e.id ' . 'WHERE e.is_read=0'; @@ -303,11 +315,15 @@ SQL; $values = [$id]; } - $stm = $this->pdo->prepare($sql); - - $stm->execute($values); - $res = $stm->fetchAll(PDO::FETCH_ASSOC); - return $res[0]['count']; + if (($stm = $this->pdo->prepare($sql)) !== false && + $stm->execute($values) && + ($res = $stm->fetchAll(PDO::FETCH_ASSOC)) !== false) { + return (int)$res[0]['count']; + } else { + $info = is_object($stm) ? $stm->errorInfo() : $this->pdo->errorInfo(); + Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info)); + return false; + } } public function tagEntry($id_tag, $id_entry, $checked = true) { diff --git a/app/Models/Themes.php b/app/Models/Themes.php index d652ada5b..86125c5f5 100644 --- a/app/Models/Themes.php +++ b/app/Models/Themes.php @@ -79,7 +79,6 @@ class FreshRSS_Themes extends Minz_Model { static $alts = array( 'add' => '➕', //✚ 'all' => '☰', - 'bookmark' => '✨', //★ 'bookmark-add' => '➕', //✚ 'bookmark-tag' => '📑', 'category' => '🗂️', //☷ diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php index 05c3c08ac..53b12cc2e 100644 --- a/app/Models/UserConfiguration.php +++ b/app/Models/UserConfiguration.php @@ -28,6 +28,7 @@ * @property-read string $is_admin * @property int|null $keep_history_default * @property string $language + * @property string $timezone * @property bool $lazyload * @property string $mail_login * @property bool $mark_updated_article_unread @@ -52,6 +53,7 @@ * @property bool $sides_close_article * @property bool $sticky_post * @property string $theme + * @property string $darkMode * @property string $token * @property bool $topline_date * @property bool $topline_display_authors @@ -66,6 +68,10 @@ * @property string $view_mode * @property array<string,mixed> $volatile */ -class FreshRSS_UserConfiguration extends Minz_Configuration { +final class FreshRSS_UserConfiguration extends Minz_Configuration { + public static function init($config_filename, $default_filename = null, $configuration_setter = null): FreshRSS_UserConfiguration { + parent::register('user', $config_filename, $default_filename, $configuration_setter); + return parent::get('user'); + } } diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php index 964324bf7..278074362 100644 --- a/app/Models/UserQuery.php +++ b/app/Models/UserQuery.php @@ -8,26 +8,35 @@ */ class FreshRSS_UserQuery { + /** @var bool */ private $deprecated = false; - private $get; - private $get_name; - private $get_type; - private $name; - private $order; + /** @var string */ + private $get = ''; + /** @var string */ + private $get_name = ''; + /** @var string */ + private $get_type = ''; + /** @var string */ + private $name = ''; + /** @var string */ + private $order = ''; /** @var FreshRSS_BooleanSearch */ private $search; - private $state; - private $url; + /** @var int */ + private $state = 0; + /** @var string */ + private $url = ''; + /** @var FreshRSS_FeedDAO|null */ private $feed_dao; + /** @var FreshRSS_CategoryDAO|null */ private $category_dao; + /** @var FreshRSS_TagDAO|null */ private $tag_dao; /** * @param array<string,string> $query - * @param FreshRSS_Searchable $feed_dao - * @param FreshRSS_Searchable $category_dao */ - public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null, FreshRSS_Searchable $tag_dao = null) { + 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; @@ -53,17 +62,17 @@ class FreshRSS_UserQuery { } // linked too deeply with the search object, need to use dependency injection $this->search = new FreshRSS_BooleanSearch($query['search']); - if (isset($query['state'])) { - $this->state = $query['state']; + if (!empty($query['state'])) { + $this->state = intval($query['state']); } } /** * Convert the current object to an array. * - * @return array<string,string> + * @return array<string,string|int> */ - public function toArray() { + public function toArray(): array { return array_filter(array( 'get' => $this->get, 'name' => $this->name, @@ -75,29 +84,27 @@ class FreshRSS_UserQuery { } /** - * Parse the get parameter in the query string to extract its name and - * type - * - * @param string $get + * Parse the get parameter in the query string to extract its name and type */ - private function parseGet($get) { + private function parseGet(string $get): void { $this->get = $get; if (preg_match('/(?P<type>[acfst])(_(?P<id>\d+))?/', $get, $matches)) { + $id = intval($matches['id'] ?? '0'); switch ($matches['type']) { case 'a': $this->parseAll(); break; case 'c': - $this->parseCategory($matches['id']); + $this->parseCategory($id); break; case 'f': - $this->parseFeed($matches['id']); + $this->parseFeed($id); break; case 's': $this->parseFavorite(); break; case 't': - $this->parseTag($matches['id']); + $this->parseTag($id); break; } } @@ -106,7 +113,7 @@ class FreshRSS_UserQuery { /** * Parse the query string when it is an "all" query */ - private function parseAll() { + private function parseAll(): void { $this->get_name = 'all'; $this->get_type = 'all'; } @@ -114,11 +121,10 @@ class FreshRSS_UserQuery { /** * Parse the query string when it is a "category" query * - * @param integer $id * @throws FreshRSS_DAO_Exception */ - private function parseCategory($id) { - if (is_null($this->category_dao)) { + private function parseCategory(int $id): void { + if ($this->category_dao === null) { throw new FreshRSS_DAO_Exception('Category DAO is not loaded in UserQuery'); } $category = $this->category_dao->searchById($id); @@ -133,11 +139,10 @@ class FreshRSS_UserQuery { /** * Parse the query string when it is a "feed" query * - * @param integer $id * @throws FreshRSS_DAO_Exception */ - private function parseFeed($id) { - if (is_null($this->feed_dao)) { + private function parseFeed(int $id): void { + if ($this->feed_dao === null) { throw new FreshRSS_DAO_Exception('Feed DAO is not loaded in UserQuery'); } $feed = $this->feed_dao->searchById($id); @@ -152,10 +157,9 @@ class FreshRSS_UserQuery { /** * Parse the query string when it is a "tag" query * - * @param integer $id * @throws FreshRSS_DAO_Exception */ - private function parseTag($id) { + private function parseTag(int $id): void { if ($this->tag_dao == null) { throw new FreshRSS_DAO_Exception('Tag DAO is not loaded in UserQuery'); } @@ -171,7 +175,7 @@ class FreshRSS_UserQuery { /** * Parse the query string when it is a "favorite" query */ - private function parseFavorite() { + private function parseFavorite(): void { $this->get_name = 'favorite'; $this->get_type = 'favorite'; } @@ -180,20 +184,16 @@ class FreshRSS_UserQuery { * Check if the current user query is deprecated. * It is deprecated if the category or the feed used in the query are * not existing. - * - * @return boolean */ - public function isDeprecated() { + public function isDeprecated(): bool { return $this->deprecated; } /** * Check if the user query has parameters. * If the type is 'all', it is considered equal to no parameters - * - * @return boolean */ - public function hasParameters() { + public function hasParameters(): bool { if ($this->get_type === 'all') { return false; } @@ -214,42 +214,40 @@ class FreshRSS_UserQuery { /** * Check if there is a search in the search object - * - * @return boolean */ - public function hasSearch() { - return $this->search->getRawInput() != ""; + public function hasSearch(): bool { + return $this->search->getRawInput() !== ''; } - public function getGet() { + public function getGet(): string { return $this->get; } - public function getGetName() { + public function getGetName(): string { return $this->get_name; } - public function getGetType() { + public function getGetType(): string { return $this->get_type; } - public function getName() { + public function getName(): string { return $this->name; } - public function getOrder() { + public function getOrder(): string { return $this->order; } - public function getSearch() { + public function getSearch(): FreshRSS_BooleanSearch { return $this->search; } - public function getState() { + public function getState(): int { return $this->state; } - public function getUrl() { + public function getUrl(): string { return $this->url; } diff --git a/app/Models/View.php b/app/Models/View.php index ab1780405..309773c93 100644 --- a/app/Models/View.php +++ b/app/Models/View.php @@ -39,6 +39,7 @@ class FreshRSS_View extends Minz_View { public $details; public $disable_aside; public $show_email_field; + /** @var string */ public $username; public $users; |
