aboutsummaryrefslogtreecommitdiff
path: root/app/Models
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2023-03-04 13:30:45 +0100
committerGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2023-03-04 13:30:45 +0100
commitb3239256dc6d188cda970adab516b3fcf1b86129 (patch)
treed8e65dd9784834ba2e82ce7ee94b4718f8af19ea /app/Models
parent27b71ffa99f7dff013fb8d51d020ed628e0d2ce6 (diff)
parent0fe0ce894cbad09757d719dd4b400b9862c1a12a (diff)
Merge branch 'edge' into latest
Diffstat (limited to 'app/Models')
-rw-r--r--app/Models/BooleanSearch.php8
-rw-r--r--app/Models/Category.php15
-rw-r--r--app/Models/CategoryDAO.php15
-rw-r--r--app/Models/ConfigurationSetter.php7
-rw-r--r--app/Models/Context.php20
-rw-r--r--app/Models/Days.php8
-rw-r--r--app/Models/Entry.php174
-rw-r--r--app/Models/EntryDAO.php25
-rw-r--r--app/Models/EntryDAOSQLite.php4
-rw-r--r--app/Models/Feed.php142
-rw-r--r--app/Models/FeedDAO.php6
-rw-r--r--app/Models/Searchable.php4
-rw-r--r--app/Models/SystemConfiguration.php6
-rw-r--r--app/Models/Tag.php55
-rw-r--r--app/Models/TagDAO.php40
-rw-r--r--app/Models/Themes.php1
-rw-r--r--app/Models/UserConfiguration.php8
-rw-r--r--app/Models/UserQuery.php98
-rw-r--r--app/Models/View.php1
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(']]>', ']]&gt;', $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;