diff options
Diffstat (limited to 'app/Models')
| -rw-r--r-- | app/Models/AttributesTrait.php | 60 | ||||
| -rw-r--r-- | app/Models/Auth.php | 38 | ||||
| -rw-r--r-- | app/Models/BooleanSearch.php | 12 | ||||
| -rw-r--r-- | app/Models/Category.php | 4 | ||||
| -rw-r--r-- | app/Models/CategoryDAO.php | 25 | ||||
| -rw-r--r-- | app/Models/Context.php | 82 | ||||
| -rw-r--r-- | app/Models/DatabaseDAO.php | 4 | ||||
| -rw-r--r-- | app/Models/DatabaseDAOPGSQL.php | 4 | ||||
| -rw-r--r-- | app/Models/DatabaseDAOSQLite.php | 6 | ||||
| -rw-r--r-- | app/Models/Entry.php | 53 | ||||
| -rw-r--r-- | app/Models/EntryDAO.php | 84 | ||||
| -rw-r--r-- | app/Models/EntryDAOSQLite.php | 2 | ||||
| -rw-r--r-- | app/Models/Factory.php | 12 | ||||
| -rw-r--r-- | app/Models/Feed.php | 74 | ||||
| -rw-r--r-- | app/Models/FeedDAO.php | 16 | ||||
| -rw-r--r-- | app/Models/FilterAction.php | 2 | ||||
| -rw-r--r-- | app/Models/FilterActionsTrait.php | 18 | ||||
| -rw-r--r-- | app/Models/FormAuth.php | 8 | ||||
| -rw-r--r-- | app/Models/StatsDAO.php | 2 | ||||
| -rw-r--r-- | app/Models/SystemConfiguration.php | 5 | ||||
| -rw-r--r-- | app/Models/TagDAO.php | 6 | ||||
| -rw-r--r-- | app/Models/Themes.php | 5 | ||||
| -rw-r--r-- | app/Models/UserConfiguration.php | 42 |
23 files changed, 339 insertions, 225 deletions
diff --git a/app/Models/AttributesTrait.php b/app/Models/AttributesTrait.php index 39154182b..e94a973d9 100644 --- a/app/Models/AttributesTrait.php +++ b/app/Models/AttributesTrait.php @@ -10,28 +10,54 @@ trait FreshRSS_AttributesTrait { */ private array $attributes = []; + /** @return array<string,mixed> */ + public function attributes(): array { + return $this->attributes; + } + /** - * @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>) - * @return array<string,mixed>|mixed|null + * @param non-empty-string $key + * @return array<int|string,mixed>|null */ - public function attributes(string $key = '') { - if ($key === '') { - return $this->attributes; - } else { - return $this->attributes[$key] ?? null; + public function attributeArray(string $key): ?array { + $a = $this->attributes[$key] ?? null; + return is_array($a) ? $a : null; + } + + /** @param non-empty-string $key */ + public function attributeBoolean(string $key): ?bool { + $a = $this->attributes[$key] ?? null; + return is_bool($a) ? $a : null; + } + + /** @param non-empty-string $key */ + public function attributeInt(string $key): ?int { + $a = $this->attributes[$key] ?? null; + return is_int($a) ? $a : null; + } + + /** @param non-empty-string $key */ + public function attributeString(string $key): ?string { + $a = $this->attributes[$key] ?? null; + return is_string($a) ? $a : null; + } + + /** @param string|array<string,mixed> $values Values, not HTML-encoded */ + public function _attributes($values): void { + if (is_string($values)) { + $values = json_decode($values, true); + } + if (is_array($values)) { + $this->attributes = $values; } } - /** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */ - public function _attributes(string $key, $value = null): void { - if ($key == '') { - if (is_string($value)) { - $value = json_decode($value, true); - } - if (is_array($value)) { - $this->attributes = $value; - } - } elseif ($value === null) { + /** + * @param non-empty-string $key + * @param array<string,mixed>|mixed|null $value Value, not HTML-encoded + */ + public function _attribute(string $key, $value = null): void { + if ($value === null) { unset($this->attributes[$key]); } else { $this->attributes[$key] = $value; diff --git a/app/Models/Auth.php b/app/Models/Auth.php index e5f7fc0b9..c66bb5016 100644 --- a/app/Models/Auth.php +++ b/app/Models/Auth.php @@ -24,7 +24,7 @@ class FreshRSS_Auth { self::$login_ok = Minz_Session::paramBoolean('loginOk'); $current_user = Minz_User::name(); if ($current_user === null) { - $current_user = FreshRSS_Context::$system_conf->default_user; + $current_user = FreshRSS_Context::systemConf()->default_user; Minz_Session::_params([ Minz_User::CURRENT_USER => $current_user, 'csrf' => false, @@ -51,7 +51,7 @@ class FreshRSS_Auth { * @return bool true if user can be connected, false otherwise. */ private static function accessControl(): bool { - $auth_type = FreshRSS_Context::$system_conf->auth_type; + $auth_type = FreshRSS_Context::systemConf()->auth_type; switch ($auth_type) { case 'form': $credentials = FreshRSS_FormAuth::getCredentialsFromCookie(); @@ -71,13 +71,13 @@ class FreshRSS_Auth { return false; } $login_ok = FreshRSS_UserDAO::exists($current_user); - if (!$login_ok && FreshRSS_Context::$system_conf->http_auth_auto_register) { + if (!$login_ok && FreshRSS_Context::systemConf()->http_auth_auto_register) { $email = null; - if (FreshRSS_Context::$system_conf->http_auth_auto_register_email_field !== '' && - isset($_SERVER[FreshRSS_Context::$system_conf->http_auth_auto_register_email_field])) { - $email = (string)$_SERVER[FreshRSS_Context::$system_conf->http_auth_auto_register_email_field]; + if (FreshRSS_Context::systemConf()->http_auth_auto_register_email_field !== '' && + isset($_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field])) { + $email = (string)$_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field]; } - $language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::$system_conf->language); + $language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::systemConf()->language); Minz_Translate::init($language); $login_ok = FreshRSS_user_Controller::createUser($current_user, $email, '', [ 'language' => $language, @@ -103,17 +103,17 @@ class FreshRSS_Auth { */ public static function giveAccess(): bool { FreshRSS_Context::initUser(); - if (FreshRSS_Context::$user_conf == null) { + if (!FreshRSS_Context::hasUserConf()) { self::$login_ok = false; return false; } - switch (FreshRSS_Context::$system_conf->auth_type) { + switch (FreshRSS_Context::systemConf()->auth_type) { case 'form': - self::$login_ok = Minz_Session::paramString('passwordHash') === FreshRSS_Context::$user_conf->passwordHash; + self::$login_ok = Minz_Session::paramString('passwordHash') === FreshRSS_Context::userConf()->passwordHash; break; case 'http_auth': - $current_user = Minz_User::name(); + $current_user = Minz_User::name() ?? ''; self::$login_ok = strcasecmp($current_user, httpAuthUser()) === 0; break; case 'none': @@ -138,12 +138,12 @@ class FreshRSS_Auth { * @return bool true if user has corresponding access, false else. */ public static function hasAccess(string $scope = 'general'): bool { - if (FreshRSS_Context::$user_conf == null) { + if (!FreshRSS_Context::hasUserConf()) { return false; } $currentUser = Minz_User::name(); - $isAdmin = FreshRSS_Context::$user_conf->is_admin; - $default_user = FreshRSS_Context::$system_conf->default_user; + $isAdmin = FreshRSS_Context::userConf()->is_admin; + $default_user = FreshRSS_Context::systemConf()->default_user; $ok = self::$login_ok; switch ($scope) { case 'general': @@ -180,11 +180,11 @@ class FreshRSS_Auth { } } if ($username == '') { - $username = FreshRSS_Context::$system_conf->default_user; + $username = FreshRSS_Context::systemConf()->default_user; } Minz_User::change($username); - switch (FreshRSS_Context::$system_conf->auth_type) { + switch (FreshRSS_Context::systemConf()->auth_type) { case 'form': Minz_Session::_param('passwordHash'); FreshRSS_FormAuth::deleteCookie(); @@ -202,20 +202,20 @@ class FreshRSS_Auth { * Return if authentication is enabled on this instance of FRSS. */ public static function accessNeedsLogin(): bool { - return FreshRSS_Context::$system_conf->auth_type !== 'none'; + return FreshRSS_Context::systemConf()->auth_type !== 'none'; } /** * Return if authentication requires a PHP action. */ public static function accessNeedsAction(): bool { - return FreshRSS_Context::$system_conf->auth_type === 'form'; + return FreshRSS_Context::systemConf()->auth_type === 'form'; } public static function csrfToken(): string { $csrf = Minz_Session::paramString('csrf'); if ($csrf == '') { - $salt = FreshRSS_Context::$system_conf->salt; + $salt = FreshRSS_Context::systemConf()->salt; $csrf = sha1($salt . uniqid('' . random_int(0, mt_getrandmax()), true)); Minz_Session::_param('csrf', $csrf); } diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php index 8a750a713..78b7593b2 100644 --- a/app/Models/BooleanSearch.php +++ b/app/Models/BooleanSearch.php @@ -19,14 +19,20 @@ class FreshRSS_BooleanSearch { public function __construct(string $input, int $level = 0, string $operator = 'AND') { $this->operator = $operator; $input = trim($input); - if ($input == '') { + if ($input === '') { return; } $this->raw_input = $input; if ($level === 0) { $input = preg_replace('/:"(.*?)"/', ':"\1"', $input); + if (!is_string($input)) { + return; + } $input = preg_replace('/(?<=[\s!-]|^)"(.*?)"/', '"\1"', $input); + if (!is_string($input)) { + return; + } $input = $this->parseUserQueryNames($input); $input = $this->parseUserQueryIds($input); @@ -53,7 +59,7 @@ class FreshRSS_BooleanSearch { if (!empty($all_matches)) { /** @var array<string,FreshRSS_UserQuery> */ $queries = []; - foreach (FreshRSS_Context::$user_conf->queries as $raw_query) { + foreach (FreshRSS_Context::userConf()->queries as $raw_query) { $query = new FreshRSS_UserQuery($raw_query); $queries[$query->getName()] = $query; } @@ -95,7 +101,7 @@ class FreshRSS_BooleanSearch { /** @var array<string,FreshRSS_UserQuery> */ $queries = []; - foreach (FreshRSS_Context::$user_conf->queries as $raw_query) { + foreach (FreshRSS_Context::userConf()->queries as $raw_query) { $query = new FreshRSS_UserQuery($raw_query, $feed_dao, $category_dao, $tag_dao); $queries[] = $query; } diff --git a/app/Models/Category.php b/app/Models/Category.php index b1e35650a..cc25a1ec0 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -169,8 +169,8 @@ class FreshRSS_Category extends Minz_Model { } public function refreshDynamicOpml(): bool { - $url = $this->attributes('opml_url'); - if ($url == '') { + $url = $this->attributeString('opml_url'); + if ($url == null) { return false; } $ok = true; diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 20347e4f2..417ff7a6c 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -30,8 +30,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { } elseif ('attributes' === $name) { //v1.15.0 $ok = $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN attributes TEXT') !== false; - /** @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}> $feeds */ + /** @var array<array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int, + * 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'keep_history':?int,'ttl':int,'attributes':string}> $feeds */ $feeds = $this->fetchAssoc('SELECT * FROM `_feed`') ?? []; $stm = $this->pdo->prepare('UPDATE `_feed` SET attributes = :attributes WHERE id = :id'); @@ -153,7 +153,7 @@ SQL; } /** - * @param array{'name':string,'kind':int,'attributes'?:string|array<string,mixed>} $valuesTmp + * @param array{'name':string,'kind':int,'attributes'?:array<string,mixed>|mixed|null} $valuesTmp * @return int|false * @throws JsonException */ @@ -230,6 +230,7 @@ SQL; $stm = $this->pdo->query($sql); if ($stm !== false) { while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { + /** @var array{'id':int,'name':string,'kind':int,'lastUpdate':int,'error':int,'attributes'?:array<string,mixed>} $row */ yield $row; } } else { @@ -245,7 +246,7 @@ SQL; public function searchById(int $id): ?FreshRSS_Category { $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 */ + /** @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; } @@ -253,7 +254,7 @@ SQL; 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 */ + /** @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; } @@ -263,8 +264,8 @@ SQL; $categories = $this->listCategories($prePopulateFeeds, $details); uasort($categories, static function (FreshRSS_Category $a, FreshRSS_Category $b) { - $aPosition = $a->attributes('position'); - $bPosition = $b->attributes('position'); + $aPosition = $a->attributeInt('position'); + $bPosition = $b->attributeInt('position'); if ($aPosition === $bPosition) { return ($a->name() < $b->name()) ? -1 : 1; } elseif (null === $aPosition) { @@ -332,9 +333,9 @@ SQL; public function getDefault(): ?FreshRSS_Category { $sql = 'SELECT * FROM `_category` WHERE id=:id'; - $res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]); + $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 ?? []); + $cat = self::daoToCategory($res); if (isset($cat[0])) { return $cat[0]; } else { @@ -444,7 +445,7 @@ SQL; $feedDao::daoToFeed($feedsDao, $previousLine['c_id']) ); $cat->_kind($previousLine['c_kind']); - $cat->_attributes('', $previousLine['c_attributes'] ?? '[]'); + $cat->_attributes($previousLine['c_attributes'] ?? '[]'); $list[(int)$previousLine['c_id']] = $cat; $feedsDao = []; //Prepare for next category @@ -464,7 +465,7 @@ SQL; $cat->_kind($previousLine['c_kind']); $cat->_lastUpdate($previousLine['c_last_update'] ?? 0); $cat->_error($previousLine['c_error'] ?? 0); - $cat->_attributes('', $previousLine['c_attributes'] ?? []); + $cat->_attributes($previousLine['c_attributes'] ?? []); $list[(int)$previousLine['c_id']] = $cat; } @@ -487,7 +488,7 @@ SQL; $cat->_kind($dao['kind']); $cat->_lastUpdate($dao['lastUpdate'] ?? 0); $cat->_error($dao['error'] ?? 0); - $cat->_attributes('', $dao['attributes'] ?? ''); + $cat->_attributes($dao['attributes'] ?? ''); $list[] = $cat; } diff --git a/app/Models/Context.php b/app/Models/Context.php index ac5547aa1..3ea5a29eb 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -7,8 +7,6 @@ declare(strict_types=1); */ final class FreshRSS_Context { - public static ?FreshRSS_UserConfiguration $user_conf = null; - public static ?FreshRSS_SystemConfiguration $system_conf = null; /** * @var array<int,FreshRSS_Category> */ @@ -57,21 +55,42 @@ final class FreshRSS_Context { public static bool $isCli = false; /** + * @deprecated Will be made `private`; use `FreshRSS_Context::systemConf()` instead. + * @internal + */ + public static ?FreshRSS_SystemConfiguration $system_conf = null; + /** + * @deprecated Will be made `private`; use `FreshRSS_Context::userConf()` instead. + * @internal + */ + public static ?FreshRSS_UserConfiguration $user_conf = null; + + /** * Initialize the context for the global system. */ - public static function initSystem(bool $reload = false): FreshRSS_SystemConfiguration { - if ($reload || FreshRSS_Context::$system_conf == null) { + public static function initSystem(bool $reload = false): void { + if ($reload || FreshRSS_Context::$system_conf === null) { //TODO: Keep in session what we need instead of always reloading from disk FreshRSS_Context::$system_conf = FreshRSS_SystemConfiguration::init(DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php'); } + } + + public static function &systemConf(): FreshRSS_SystemConfiguration { + if (FreshRSS_Context::$system_conf === null) { + throw new FreshRSS_Context_Exception('System configuration not initialised!'); + } return FreshRSS_Context::$system_conf; } + public static function hasSystemConf(): bool { + return FreshRSS_Context::$system_conf !== null; + } + /** * Initialize the context for the current user. * @throws Minz_ConfigurationParamException */ - public static function initUser(string $username = '', bool $userMustExist = true): ?FreshRSS_UserConfiguration { + public static function initUser(string $username = '', bool $userMustExist = true): void { FreshRSS_Context::$user_conf = null; if (!isset($_SESSION)) { Minz_Session::init('FreshRSS'); @@ -103,14 +122,16 @@ final class FreshRSS_Context { Minz_Session::unlock(); if (FreshRSS_Context::$user_conf == null) { - return null; + return; } FreshRSS_Context::$search = new FreshRSS_BooleanSearch(''); //Legacy - $oldEntries = (int)FreshRSS_Context::$user_conf->param('old_entries', 0); - $keepMin = (int)FreshRSS_Context::$user_conf->param('keep_history_default', -5); + $oldEntries = FreshRSS_Context::$user_conf->param('old_entries', 0); + $oldEntries = is_numeric($oldEntries) ? (int)$oldEntries : 0; + $keepMin = FreshRSS_Context::$user_conf->param('keep_history_default', -5); + $keepMin = is_numeric($keepMin) ? (int)$keepMin : -5; if ($oldEntries > 0 || $keepMin > -5) { //Freshrss < 1.15 $archiving = FreshRSS_Context::$user_conf->archiving; $archiving['keep_max'] = false; @@ -130,10 +151,23 @@ final class FreshRSS_Context { if (!in_array(FreshRSS_Context::$user_conf->display_categories, [ 'active', 'remember', 'all', 'none' ], true)) { FreshRSS_Context::$user_conf->display_categories = FreshRSS_Context::$user_conf->display_categories === true ? 'all' : 'active'; } + } + public static function &userConf(): FreshRSS_UserConfiguration { + if (FreshRSS_Context::$user_conf === null) { + throw new FreshRSS_Context_Exception('User configuration not initialised!'); + } return FreshRSS_Context::$user_conf; } + public static function hasUserConf(): bool { + return FreshRSS_Context::$user_conf !== null; + } + + public static function clearUserConf(): void { + FreshRSS_Context::$user_conf = null; + } + /** * This action updates the Context object by using request parameters. * @@ -162,28 +196,28 @@ final class FreshRSS_Context { self::_get(Minz_Request::paramString('get') ?: 'a'); - self::$state = Minz_Request::paramInt('state') ?: self::$user_conf->default_state; + self::$state = Minz_Request::paramInt('state') ?: FreshRSS_Context::userConf()->default_state; $state_forced_by_user = Minz_Request::paramString('state') !== ''; if (!$state_forced_by_user && !self::isStateEnabled(FreshRSS_Entry::STATE_READ)) { - if (self::$user_conf->default_view === 'all') { + if (FreshRSS_Context::userConf()->default_view === 'all') { self::$state |= FreshRSS_Entry::STATE_ALL; - } elseif (self::$user_conf->default_view === 'adaptive' && self::$get_unread <= 0) { + } elseif (FreshRSS_Context::userConf()->default_view === 'adaptive' && self::$get_unread <= 0) { self::$state |= FreshRSS_Entry::STATE_READ; } - if (self::$user_conf->show_fav_unread && + if (FreshRSS_Context::userConf()->show_fav_unread && (self::isCurrentGet('s') || self::isCurrentGet('T') || self::isTag())) { self::$state |= FreshRSS_Entry::STATE_READ; } } self::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search')); - $order = Minz_Request::paramString('order') ?: self::$user_conf->sort_order; + $order = Minz_Request::paramString('order') ?: FreshRSS_Context::userConf()->sort_order; self::$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC'; - self::$number = Minz_Request::paramInt('nb') ?: self::$user_conf->posts_per_page; - if (self::$number > self::$user_conf->max_posts_per_rss) { + self::$number = Minz_Request::paramInt('nb') ?: FreshRSS_Context::userConf()->posts_per_page; + if (self::$number > FreshRSS_Context::userConf()->max_posts_per_rss) { self::$number = max( - self::$user_conf->max_posts_per_rss, - self::$user_conf->posts_per_page); + FreshRSS_Context::userConf()->max_posts_per_rss, + FreshRSS_Context::userConf()->posts_per_page); } self::$first_id = Minz_Request::paramString('next'); self::$sinceHours = Minz_Request::paramInt('hours'); @@ -335,19 +369,19 @@ final class FreshRSS_Context { case 'a': self::$current_get['all'] = true; self::$name = _t('index.feed.title'); - self::$description = self::$system_conf->meta_description; + self::$description = FreshRSS_Context::systemConf()->meta_description; self::$get_unread = self::$total_unread; break; case 'i': self::$current_get['important'] = true; self::$name = _t('index.menu.important'); - self::$description = self::$system_conf->meta_description; + self::$description = FreshRSS_Context::systemConf()->meta_description; self::$get_unread = self::$total_unread; break; case 's': self::$current_get['starred'] = true; self::$name = _t('index.feed.title_fav'); - self::$description = self::$system_conf->meta_description; + self::$description = FreshRSS_Context::systemConf()->meta_description; self::$get_unread = self::$total_starred['unread']; // Update state if favorite is not yet enabled. @@ -355,7 +389,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::$system_conf->allow_robots ? null : FreshRSS_CategoryDAO::findFeed(self::$categories, $id); + $feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_CategoryDAO::findFeed(self::$categories, $id); if ($feed === null) { $feedDAO = FreshRSS_Factory::createFeedDao(); $feed = $feedDAO->searchById($id); @@ -427,7 +461,7 @@ final class FreshRSS_Context { self::$categories = $catDAO->listCategories(); } - if (self::$user_conf->onread_jump_next && strlen($get) > 2) { + if (FreshRSS_Context::userConf()->onread_jump_next && strlen($get) > 2) { $another_unread_id = ''; $found_current_get = false; switch ($get[0]) { @@ -491,7 +525,7 @@ final class FreshRSS_Context { * - the "unread" state is enable */ public static function isAutoRemoveAvailable(): bool { - if (!self::$user_conf->auto_remove_article) { + if (!FreshRSS_Context::userConf()->auto_remove_article) { return false; } if (self::isStateEnabled(FreshRSS_Entry::STATE_READ)) { @@ -510,7 +544,7 @@ final class FreshRSS_Context { * are read. */ public static function isStickyPostEnabled(): bool { - if (self::$user_conf->sticky_post) { + if (FreshRSS_Context::userConf()->sticky_post) { return true; } if (self::isAutoRemoveAvailable()) { diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php index 7dbe1db3f..cdc74fa12 100644 --- a/app/Models/DatabaseDAO.php +++ b/app/Models/DatabaseDAO.php @@ -22,7 +22,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { public function create(): string { require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); - $db = FreshRSS_Context::$system_conf->db; + $db = FreshRSS_Context::systemConf()->db; try { $sql = sprintf($GLOBALS['SQL_CREATE_DB'], empty($db['base']) ? '' : $db['base']); @@ -174,7 +174,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { } public function size(bool $all = false): int { - $db = FreshRSS_Context::$system_conf->db; + $db = FreshRSS_Context::systemConf()->db; // MariaDB does not refresh size information automatically $sql = <<<'SQL' diff --git a/app/Models/DatabaseDAOPGSQL.php b/app/Models/DatabaseDAOPGSQL.php index 4a92dd184..fe3d6149d 100644 --- a/app/Models/DatabaseDAOPGSQL.php +++ b/app/Models/DatabaseDAOPGSQL.php @@ -11,7 +11,7 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite { public const UNDEFINED_TABLE = '42P01'; public function tablesAreCorrect(): bool { - $db = FreshRSS_Context::$system_conf->db; + $db = FreshRSS_Context::systemConf()->db; $sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=:tableowner'; $res = $this->fetchAssoc($sql, [':tableowner' => $db['user']]); if ($res == null) { @@ -58,7 +58,7 @@ SQL; public function size(bool $all = false): int { if ($all) { - $db = FreshRSS_Context::$system_conf->db; + $db = FreshRSS_Context::systemConf()->db; $res = $this->fetchColumn('SELECT pg_database_size(:base)', 0, [':base' => $db['base']]); } else { $sql = <<<SQL diff --git a/app/Models/DatabaseDAOSQLite.php b/app/Models/DatabaseDAOSQLite.php index 787380637..e72cc74e8 100644 --- a/app/Models/DatabaseDAOSQLite.php +++ b/app/Models/DatabaseDAOSQLite.php @@ -65,12 +65,12 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO { $sum = 0; if ($all) { foreach (glob(DATA_PATH . '/users/*/db.sqlite') ?: [] as $filename) { - $sum += @filesize($filename); + $sum += (@filesize($filename) ?: 0); } } else { - $sum = @filesize(DATA_PATH . '/users/' . $this->current_user . '/db.sqlite'); + $sum = (@filesize(DATA_PATH . '/users/' . $this->current_user . '/db.sqlite') ?: 0); } - return intval($sum); + return $sum; } public function optimize(): bool { diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 186b1f166..62ba91db3 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -90,7 +90,7 @@ class FreshRSS_Entry extends Minz_Model { $entry->_lastSeen($dao['lastSeen']); } if (!empty($dao['attributes'])) { - $entry->_attributes('', $dao['attributes']); + $entry->_attributes($dao['attributes']); } if (!empty($dao['hash'])) { $entry->_hash($dao['hash']); @@ -145,7 +145,7 @@ class FreshRSS_Entry extends Minz_Model { * Provides the original content without additional content potentially added by loadCompleteContent(). */ public function originalContent(): string { - return preg_replace('#<!-- FULLCONTENT start //-->.*<!-- FULLCONTENT end //-->#s', '', $this->content); + return preg_replace('#<!-- FULLCONTENT start //-->.*<!-- FULLCONTENT end //-->#s', '', $this->content) ?? ''; } /** @@ -160,7 +160,7 @@ class FreshRSS_Entry extends Minz_Model { $content = $this->content; - $thumbnailAttribute = $this->attributes('thumbnail'); + $thumbnailAttribute = $this->attributeArray('thumbnail') ?? []; if (!empty($thumbnailAttribute['url'])) { $elink = $thumbnailAttribute['url']; if ($allowDuplicateEnclosures || !self::containsLink($content, $elink)) { @@ -174,12 +174,15 @@ HTML; } } - $attributeEnclosures = $this->attributes('enclosures'); + $attributeEnclosures = $this->attributeArray('enclosures'); if (empty($attributeEnclosures)) { return $content; } foreach ($attributeEnclosures as $enclosure) { + if (!is_array($enclosure)) { + continue; + } $elink = $enclosure['url'] ?? ''; if ($elink == '') { continue; @@ -192,7 +195,10 @@ HTML; $length = $enclosure['length'] ?? 0; $medium = $enclosure['medium'] ?? ''; $mime = $enclosure['type'] ?? ''; - $thumbnails = $enclosure['thumbnails'] ?? []; + $thumbnails = $enclosure['thumbnails'] ?? null; + if (!is_array($thumbnails)) { + $thumbnails = []; + } $etitle = $enclosure['title'] ?? ''; $content .= "\n"; @@ -235,7 +241,7 @@ HTML; /** @return Traversable<array{'url':string,'type'?:string,'medium'?:string,'length'?:int,'title'?:string,'description'?:string,'credit'?:string,'height'?:int,'width'?:int,'thumbnails'?:array<string>}> */ public function enclosures(bool $searchBodyImages = false): Traversable { - $attributeEnclosures = $this->attributes('enclosures'); + $attributeEnclosures = $this->attributeArray('enclosures'); if (is_iterable($attributeEnclosures)) { // FreshRSS 1.20.1+: The enclosures are saved as attributes yield from $attributeEnclosures; @@ -249,7 +255,7 @@ HTML; $dom->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $this->content, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); $xpath = new DOMXPath($dom); } - if ($searchEnclosures) { + if ($searchEnclosures && $xpath !== null) { // Legacy code for database entries < FreshRSS 1.20.1 $enclosures = $xpath->query('//div[@class="enclosure"]/p[@class="enclosure-content"]/*[@src]'); if (!empty($enclosures)) { @@ -272,7 +278,7 @@ HTML; } } } - if ($searchBodyImages) { + if ($searchBodyImages && $xpath !== null) { $images = $xpath->query('//img'); if (!empty($images)) { /** @var DOMElement $img */ @@ -300,7 +306,7 @@ HTML; * @return array{'url':string,'type'?:string,'medium'?:string,'length'?:int,'title'?:string,'description'?:string,'credit'?:string,'height'?:int,'width'?:int,'thumbnails'?:array<string>}|null */ public function thumbnail(bool $searchEnclosures = true): ?array { - $thumbnail = $this->attributes('thumbnail'); + $thumbnail = $this->attributeArray('thumbnail') ?? []; // First, use the provided thumbnail, if any if (!empty($thumbnail['url'])) { return $thumbnail; @@ -558,7 +564,7 @@ HTML; $ok &= in_array($this->feedId, $filter->getFeedIds(), true); } if ($ok && $filter->getNotFeedIds()) { - $ok &= !in_array($this->feedId, $filter->getFeedIds(), true); + $ok &= !in_array($this->feedId, $filter->getNotFeedIds(), true); } if ($ok && $filter->getAuthor()) { foreach ($filter->getAuthor() as $author) { @@ -630,14 +636,15 @@ HTML; return (bool)$ok; } - /** @param array<string,bool> $titlesAsRead */ + /** @param array<string,bool|int> $titlesAsRead */ public function applyFilterActions(array $titlesAsRead = []): void { - if ($this->feed === null) { + $feed = $this->feed; + if ($feed === null) { return; } if (!$this->isRead()) { - if ($this->feed->attributes('read_upon_reception') || - ($this->feed->attributes('read_upon_reception') === null && FreshRSS_Context::$user_conf->mark_when['reception'])) { + if ($feed->attributeBoolean('read_upon_reception') || + ($feed->attributeBoolean('read_upon_reception') === null && FreshRSS_Context::userConf()->mark_when['reception'])) { $this->_isRead(true); Minz_ExtensionManager::callHook('entry_auto_read', $this, 'upon_reception'); } @@ -647,11 +654,11 @@ HTML; Minz_ExtensionManager::callHook('entry_auto_read', $this, 'same_title_in_feed'); } } - FreshRSS_Context::$user_conf->applyFilterActions($this); - if ($this->feed->category() !== null) { - $this->feed->category()->applyFilterActions($this); + FreshRSS_Context::userConf()->applyFilterActions($this); + if ($feed->category() !== null) { + $feed->category()->applyFilterActions($this); } - $this->feed->applyFilterActions($this); + $feed->applyFilterActions($this); } public function isDay(int $day, int $today): bool { @@ -684,10 +691,9 @@ HTML; if ($maxRedirs > 0) { //Follow any HTML redirection - $metas = $xpath->query('//meta[@content]'); - /** @var array<DOMElement> $metas */ + $metas = $xpath->query('//meta[@content]') ?: []; foreach ($metas as $meta) { - if (strtolower(trim($meta->getAttribute('http-equiv'))) === 'refresh') { + if ($meta instanceof DOMElement && strtolower(trim($meta->getAttribute('http-equiv'))) === 'refresh') { $refresh = preg_replace('/^[0-9.; ]*\s*(url\s*=)?\s*/i', '', trim($meta->getAttribute('content'))); $refresh = SimplePie_Misc::absolutize_url($refresh, $url); if ($refresh != false && $refresh !== $url) { @@ -712,6 +718,9 @@ HTML; if (!empty($attributes['path_entries_filter'])) { $filterednodes = $xpath->query((new Gt\CssXPath\Translator($attributes['path_entries_filter']))->asXPath(), $node) ?: []; foreach ($filterednodes as $filterednode) { + if ($filterednode->parentNode === null) { + continue; + } $filterednode->parentNode->removeChild($filterednode); } } @@ -747,7 +756,7 @@ HTML; if ('' !== $fullContent) { $fullContent = "<!-- FULLCONTENT start //-->{$fullContent}<!-- FULLCONTENT end //-->"; $originalContent = $this->originalContent(); - switch ($feed->attributes('content_action')) { + switch ($feed->attributeString('content_action')) { case 'prepend': $this->content = $fullContent . $originalContent; break; diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 23ac3c918..232db8521 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -315,7 +315,7 @@ SQL; $affected = 0; $idsChunks = array_chunk($ids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER); foreach ($idsChunks as $idsChunk) { - $affected += $this->markFavorite($idsChunk, $is_favorite); + $affected += ($this->markFavorite($idsChunk, $is_favorite) ?: 0); } return $affected; } @@ -387,7 +387,7 @@ SQL; if (count($ids) < 6) { //Speed heuristics $affected = 0; foreach ($ids as $id) { - $affected += $this->markRead($id, $is_read); + $affected += ($this->markRead($id, $is_read) ?: 0); } return $affected; } elseif (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) { @@ -395,7 +395,7 @@ SQL; $affected = 0; $idsChunks = array_chunk($ids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER); foreach ($idsChunks as $idsChunk) { - $affected += $this->markRead($idsChunk, $is_read); + $affected += ($this->markRead($idsChunk, $is_read) ?: 0); } return $affected; } @@ -630,7 +630,7 @@ SQL; /** * Remember to call updateCachedValue($id_feed) or updateCachedValues() just after. - * @param array<string,int|bool|string> $options + * @param array<string,bool|int|string> $options * @return int|false */ public function cleanOldEntries(int $id_feed, array $options = []) { @@ -704,6 +704,8 @@ SQL; $stm = $this->pdo->query($sql); if ($stm != false) { while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { + /** @var array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int, + * 'hash':string,'is_read':?bool,'is_favorite':?bool,'id_feed':int,'tags':string,'attributes':array<string,mixed>} $row */ yield $row; } } else { @@ -777,7 +779,7 @@ SQL; } // Searches are combined by OR and are not recursive $sub_search = ''; - if ($filter->getEntryIds()) { + if ($filter->getEntryIds() !== null) { $sub_search .= 'AND ' . $alias . 'id IN ('; foreach ($filter->getEntryIds() as $entry_id) { $sub_search .= '?,'; @@ -786,7 +788,7 @@ SQL; $sub_search = rtrim($sub_search, ','); $sub_search .= ') '; } - if ($filter->getNotEntryIds()) { + if ($filter->getNotEntryIds() !== null) { $sub_search .= 'AND ' . $alias . 'id NOT IN ('; foreach ($filter->getNotEntryIds() as $entry_id) { $sub_search .= '?,'; @@ -796,56 +798,56 @@ SQL; $sub_search .= ') '; } - if ($filter->getMinDate()) { + if ($filter->getMinDate() !== null) { $sub_search .= 'AND ' . $alias . 'id >= ? '; $values[] = "{$filter->getMinDate()}000000"; } - if ($filter->getMaxDate()) { + if ($filter->getMaxDate() !== null) { $sub_search .= 'AND ' . $alias . 'id <= ? '; $values[] = "{$filter->getMaxDate()}000000"; } - if ($filter->getMinPubdate()) { + if ($filter->getMinPubdate() !== null) { $sub_search .= 'AND ' . $alias . 'date >= ? '; $values[] = $filter->getMinPubdate(); } - if ($filter->getMaxPubdate()) { + if ($filter->getMaxPubdate() !== null) { $sub_search .= 'AND ' . $alias . 'date <= ? '; $values[] = $filter->getMaxPubdate(); } //Negation of date intervals must be combined by OR - if ($filter->getNotMinDate() || $filter->getNotMaxDate()) { + if ($filter->getNotMinDate() !== null || $filter->getNotMaxDate() !== null) { $sub_search .= 'AND ('; - if ($filter->getNotMinDate()) { + if ($filter->getNotMinDate() !== null) { $sub_search .= $alias . 'id < ?'; $values[] = "{$filter->getNotMinDate()}000000"; if ($filter->getNotMaxDate()) { $sub_search .= ' OR '; } } - if ($filter->getNotMaxDate()) { + if ($filter->getNotMaxDate() !== null) { $sub_search .= $alias . 'id > ?'; $values[] = "{$filter->getNotMaxDate()}000000"; } $sub_search .= ') '; } - if ($filter->getNotMinPubdate() || $filter->getNotMaxPubdate()) { + if ($filter->getNotMinPubdate() !== null || $filter->getNotMaxPubdate() !== null) { $sub_search .= 'AND ('; - if ($filter->getNotMinPubdate()) { + if ($filter->getNotMinPubdate() !== null) { $sub_search .= $alias . 'date < ?'; $values[] = $filter->getNotMinPubdate(); if ($filter->getNotMaxPubdate()) { $sub_search .= ' OR '; } } - if ($filter->getNotMaxPubdate()) { + if ($filter->getNotMaxPubdate() !== null) { $sub_search .= $alias . 'date > ?'; $values[] = $filter->getNotMaxPubdate(); } $sub_search .= ') '; } - if ($filter->getFeedIds()) { + if ($filter->getFeedIds() !== null) { $sub_search .= 'AND ' . $alias . 'id_feed IN ('; foreach ($filter->getFeedIds() as $feed_id) { $sub_search .= '?,'; @@ -854,7 +856,7 @@ SQL; $sub_search = rtrim($sub_search, ','); $sub_search .= ') '; } - if ($filter->getNotFeedIds()) { + if ($filter->getNotFeedIds() !== null) { $sub_search .= 'AND ' . $alias . 'id_feed NOT IN ('; foreach ($filter->getNotFeedIds() as $feed_id) { $sub_search .= '?,'; @@ -864,7 +866,7 @@ SQL; $sub_search .= ') '; } - if ($filter->getLabelIds()) { + if ($filter->getLabelIds() !== null) { if ($filter->getLabelIds() === '*') { $sub_search .= 'AND EXISTS (SELECT et.id_tag FROM `_entrytag` et WHERE et.id_entry = ' . $alias . 'id) '; } else { @@ -877,7 +879,7 @@ SQL; $sub_search .= ')) '; } } - if ($filter->getNotLabelIds()) { + if ($filter->getNotLabelIds() !== null) { if ($filter->getNotLabelIds() === '*') { $sub_search .= 'AND NOT EXISTS (SELECT et.id_tag FROM `_entrytag` et WHERE et.id_entry = ' . $alias . 'id) '; } else { @@ -891,7 +893,7 @@ SQL; } } - if ($filter->getLabelNames()) { + if ($filter->getLabelNames() !== null) { $sub_search .= 'AND ' . $alias . 'id IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN ('; foreach ($filter->getLabelNames() as $label_name) { $sub_search .= '?,'; @@ -900,7 +902,7 @@ SQL; $sub_search = rtrim($sub_search, ','); $sub_search .= ')) '; } - if ($filter->getNotLabelNames()) { + if ($filter->getNotLabelNames() !== null) { $sub_search .= 'AND ' . $alias . 'id NOT IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN ('; foreach ($filter->getNotLabelNames() as $label_name) { $sub_search .= '?,'; @@ -910,57 +912,57 @@ SQL; $sub_search .= ')) '; } - if ($filter->getAuthor()) { + if ($filter->getAuthor() !== null) { foreach ($filter->getAuthor() as $author) { $sub_search .= 'AND ' . $alias . 'author LIKE ? '; $values[] = "%{$author}%"; } } - if ($filter->getIntitle()) { + if ($filter->getIntitle() !== null) { foreach ($filter->getIntitle() as $title) { $sub_search .= 'AND ' . $alias . 'title LIKE ? '; $values[] = "%{$title}%"; } } - if ($filter->getTags()) { + if ($filter->getTags() !== null) { foreach ($filter->getTags() as $tag) { $sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' LIKE ? '; $values[] = "%{$tag} #%"; } } - if ($filter->getInurl()) { + if ($filter->getInurl() !== null) { foreach ($filter->getInurl() as $url) { $sub_search .= 'AND ' . $alias . 'link LIKE ? '; $values[] = "%{$url}%"; } } - if ($filter->getNotAuthor()) { + if ($filter->getNotAuthor() !== null) { foreach ($filter->getNotAuthor() as $author) { $sub_search .= 'AND ' . $alias . 'author NOT LIKE ? '; $values[] = "%{$author}%"; } } - if ($filter->getNotIntitle()) { + if ($filter->getNotIntitle() !== null) { foreach ($filter->getNotIntitle() as $title) { $sub_search .= 'AND ' . $alias . 'title NOT LIKE ? '; $values[] = "%{$title}%"; } } - if ($filter->getNotTags()) { + if ($filter->getNotTags() !== null) { foreach ($filter->getNotTags() as $tag) { $sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' NOT LIKE ? '; $values[] = "%{$tag} #%"; } } - if ($filter->getNotInurl()) { + if ($filter->getNotInurl() !== null) { foreach ($filter->getNotInurl() as $url) { $sub_search .= 'AND ' . $alias . 'link NOT LIKE ? '; $values[] = "%{$url}%"; } } - if ($filter->getSearch()) { + if ($filter->getSearch() !== null) { foreach ($filter->getSearch() as $search_value) { if (static::isCompressed()) { // MySQL-only $sub_search .= 'AND CONCAT(' . $alias . 'title, UNCOMPRESS(' . $alias . 'content_bin)) LIKE ? '; @@ -972,7 +974,7 @@ SQL; } } } - if ($filter->getNotSearch()) { + if ($filter->getNotSearch() !== null) { foreach ($filter->getNotSearch() as $search_value) { if (static::isCompressed()) { // MySQL-only $sub_search .= 'AND CONCAT(' . $alias . 'title, UNCOMPRESS(' . $alias . 'content_bin)) NOT LIKE ? '; @@ -1163,9 +1165,11 @@ SQL; $stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min); if ($stm) { while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { - /** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int, - * 'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string} $row */ - yield FreshRSS_Entry::fromArray($row); + if (is_array($row)) { + /** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int, + * 'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string} $row */ + yield FreshRSS_Entry::fromArray($row); + } } } } @@ -1206,9 +1210,11 @@ SQL; return; } while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { - /** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int, - * 'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string} $row */ - yield FreshRSS_Entry::fromArray($row); + if (is_array($row)) { + /** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int, + * 'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string} $row */ + yield FreshRSS_Entry::fromArray($row); + } } } @@ -1283,7 +1289,7 @@ SQL; $affected = 0; $guidsChunks = array_chunk($guids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER); foreach ($guidsChunks as $guidsChunk) { - $affected += $this->updateLastSeen($id_feed, $guidsChunk, $mtime); + $affected += ($this->updateLastSeen($id_feed, $guidsChunk, $mtime) ?: 0); } return $affected; } diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index 66feb567b..1a87d03be 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -80,7 +80,7 @@ SQL; //if (true) { //Speed heuristics //TODO: Not implemented yet for SQLite (so always call IDs one by one) $affected = 0; foreach ($ids as $id) { - $affected += $this->markRead($id, $is_read); + $affected += ($this->markRead($id, $is_read) ?: 0); } return $affected; //} diff --git a/app/Models/Factory.php b/app/Models/Factory.php index eef3c81f5..f69c7f6aa 100644 --- a/app/Models/Factory.php +++ b/app/Models/Factory.php @@ -14,7 +14,7 @@ class FreshRSS_Factory { * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException */ public static function createCategoryDao(?string $username = null): FreshRSS_CategoryDAO { - switch (FreshRSS_Context::$system_conf->db['type'] ?? '') { + switch (FreshRSS_Context::systemConf()->db['type'] ?? '') { case 'sqlite': return new FreshRSS_CategoryDAOSQLite($username); default: @@ -26,7 +26,7 @@ class FreshRSS_Factory { * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException */ public static function createFeedDao(?string $username = null): FreshRSS_FeedDAO { - switch (FreshRSS_Context::$system_conf->db['type'] ?? '') { + switch (FreshRSS_Context::systemConf()->db['type'] ?? '') { case 'sqlite': return new FreshRSS_FeedDAOSQLite($username); default: @@ -38,7 +38,7 @@ class FreshRSS_Factory { * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException */ public static function createEntryDao(?string $username = null): FreshRSS_EntryDAO { - switch (FreshRSS_Context::$system_conf->db['type'] ?? '') { + switch (FreshRSS_Context::systemConf()->db['type'] ?? '') { case 'sqlite': return new FreshRSS_EntryDAOSQLite($username); case 'pgsql': @@ -52,7 +52,7 @@ class FreshRSS_Factory { * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException */ public static function createTagDao(?string $username = null): FreshRSS_TagDAO { - switch (FreshRSS_Context::$system_conf->db['type'] ?? '') { + switch (FreshRSS_Context::systemConf()->db['type'] ?? '') { case 'sqlite': return new FreshRSS_TagDAOSQLite($username); case 'pgsql': @@ -66,7 +66,7 @@ class FreshRSS_Factory { * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException */ public static function createStatsDAO(?string $username = null): FreshRSS_StatsDAO { - switch (FreshRSS_Context::$system_conf->db['type'] ?? '') { + switch (FreshRSS_Context::systemConf()->db['type'] ?? '') { case 'sqlite': return new FreshRSS_StatsDAOSQLite($username); case 'pgsql': @@ -80,7 +80,7 @@ class FreshRSS_Factory { * @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException */ public static function createDatabaseDAO(?string $username = null): FreshRSS_DatabaseDAO { - switch (FreshRSS_Context::$system_conf->db['type'] ?? '') { + switch (FreshRSS_Context::systemConf()->db['type'] ?? '') { case 'sqlite': return new FreshRSS_DatabaseDAOSQLite($username); case 'pgsql': diff --git a/app/Models/Feed.php b/app/Models/Feed.php index dbe6aaa73..024a7841a 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -82,7 +82,7 @@ class FreshRSS_Feed extends Minz_Model { public function hash(): string { if ($this->hash == '') { - $salt = FreshRSS_Context::$system_conf->salt; + $salt = FreshRSS_Context::systemConf()->salt; $this->hash = hash('crc32b', $salt . $this->url); } return $this->hash; @@ -126,7 +126,7 @@ class FreshRSS_Feed extends Minz_Model { return $simplePie == null ? [] : iterator_to_array($this->loadEntries($simplePie)); } public function name(bool $raw = false): string { - return $raw || $this->name != '' ? $this->name : preg_replace('%^https?://(www[.])?%i', '', $this->url); + return $raw || $this->name != '' ? $this->name : (preg_replace('%^https?://(www[.])?%i', '', $this->url) ?? ''); } /** @return string HTML-encoded URL of the Web site of the feed */ public function website(): string { @@ -179,7 +179,7 @@ class FreshRSS_Feed extends Minz_Model { if ($raw) { $ttl = $this->ttl; if ($this->mute && FreshRSS_Feed::TTL_DEFAULT === $ttl) { - $ttl = FreshRSS_Context::$user_conf ? FreshRSS_Context::$user_conf->ttl_default : 3600; + $ttl = FreshRSS_Context::userConf()->ttl_default; } return $ttl * ($this->mute ? -1 : 1); } @@ -331,7 +331,7 @@ class FreshRSS_Feed extends Minz_Model { } else { $url = htmlspecialchars_decode($this->url, ENT_QUOTES); if ($this->httpAuth != '') { - $url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url); + $url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url) ?? ''; } $simplePie = customSimplePie($this->attributes()); if (substr($url, -11) === '#force_feed') { @@ -342,7 +342,7 @@ class FreshRSS_Feed extends Minz_Model { if (!$loadDetails) { //Only activates auto-discovery when adding a new feed $simplePie->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE); } - if ($this->attributes('clear_cache')) { + if ($this->attributeBoolean('clear_cache')) { // Do not use `$simplePie->enable_cache(false);` as it would prevent caching in multiuser context $this->clearCache(); } @@ -370,7 +370,7 @@ class FreshRSS_Feed extends Minz_Model { if ($loadDetails) { // si on a utilisé l’auto-discover, notre url va avoir changé - $subscribe_url = $simplePie->subscribe_url(false); + $subscribe_url = $simplePie->subscribe_url(false) ?? ''; if ($this->name(true) === '') { //HTML to HTML-PRE //ENT_COMPAT except '&' @@ -385,11 +385,11 @@ class FreshRSS_Feed extends Minz_Model { } } else { //The case of HTTP 301 Moved Permanently - $subscribe_url = $simplePie->subscribe_url(true); + $subscribe_url = $simplePie->subscribe_url(true) ?? ''; } $clean_url = SimplePie_Misc::url_remove_credentials($subscribe_url); - if ($subscribe_url !== null && $subscribe_url !== $url) { + if ($subscribe_url !== '' && $subscribe_url !== $url) { $this->_url($clean_url); } @@ -411,7 +411,7 @@ class FreshRSS_Feed extends Minz_Model { $testGuids = []; $guids = []; $links = []; - $hadBadGuids = $this->attributes('hasBadGuids'); + $hadBadGuids = $this->attributeBoolean('hasBadGuids'); $items = $simplePie->get_items(); if (empty($items)) { @@ -426,7 +426,10 @@ class FreshRSS_Feed extends Minz_Model { $hasUniqueGuids &= empty($testGuids['_' . $guid]); $testGuids['_' . $guid] = true; $guids[] = $guid; - $links[] = $item->get_permalink(); + $permalink = $item->get_permalink(); + if ($permalink != null) { + $links[] = $permalink; + } } if ($hadBadGuids != !$hasUniqueGuids) { @@ -444,7 +447,7 @@ class FreshRSS_Feed extends Minz_Model { /** @return Traversable<FreshRSS_Entry> */ public function loadEntries(SimplePie $simplePie): Traversable { - $hasBadGuids = $this->attributes('hasBadGuids'); + $hasBadGuids = $this->attributeBoolean('hasBadGuids'); $items = $simplePie->get_items(); if (empty($items)) { @@ -560,15 +563,15 @@ class FreshRSS_Feed extends Minz_Model { $title == '' ? '' : $title, $authorNames, $content == '' ? '' : $content, - $link == '' ? '' : $link, + $link == null ? '' : $link, $date ?: time() ); $entry->_tags($tags); $entry->_feed($this); if (!empty($attributeThumbnail['url'])) { - $entry->_attributes('thumbnail', $attributeThumbnail); + $entry->_attribute('thumbnail', $attributeThumbnail); } - $entry->_attributes('enclosures', $attributeEnclosures); + $entry->_attribute('enclosures', $attributeEnclosures); $entry->hash(); //Must be computed before loading full content $entry->loadCompleteContent(); // Optionally load full content for truncated feeds @@ -587,11 +590,14 @@ class FreshRSS_Feed extends Minz_Model { if ($this->httpAuth != '') { $feedSourceUrl = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $feedSourceUrl); } + if ($feedSourceUrl == null) { + return null; + } // Same naming conventions than https://rss-bridge.github.io/rss-bridge/Bridge_API/XPathAbstract.html // https://rss-bridge.github.io/rss-bridge/Bridge_API/BridgeAbstract.html#collectdata /** @var array<string,string> $xPathSettings */ - $xPathSettings = $this->attributes('xpath'); + $xPathSettings = $this->attributeArray('xpath'); $xPathFeedTitle = $xPathSettings['feedTitle'] ?? ''; $xPathItem = $xPathSettings['item'] ?? ''; $xPathItemTitle = $xPathSettings['itemTitle'] ?? ''; @@ -725,9 +731,9 @@ class FreshRSS_Feed extends Minz_Model { * @throws JsonException */ public function keepMaxUnread() { - $keepMaxUnread = $this->attributes('keep_max_n_unread'); + $keepMaxUnread = $this->attributeInt('keep_max_n_unread'); if ($keepMaxUnread === null) { - $keepMaxUnread = FreshRSS_Context::$user_conf->mark_when['max_n_unread']; + $keepMaxUnread = FreshRSS_Context::userConf()->mark_when['max_n_unread']; } return is_int($keepMaxUnread) && $keepMaxUnread >= 0 ? $keepMaxUnread : null; } @@ -754,9 +760,9 @@ class FreshRSS_Feed extends Minz_Model { * @return int|false the number of lines affected, or false if not applicable */ public function markAsReadUponGone(bool $upstreamIsEmpty, int $maxTimestamp = 0) { - $readUponGone = $this->attributes('read_upon_gone'); + $readUponGone = $this->attributeBoolean('read_upon_gone'); if ($readUponGone === null) { - $readUponGone = FreshRSS_Context::$user_conf->mark_when['gone']; + $readUponGone = FreshRSS_Context::userConf()->mark_when['gone']; } if (!$readUponGone) { return false; @@ -782,13 +788,15 @@ class FreshRSS_Feed extends Minz_Model { * @return int|false */ public function cleanOldEntries() { - $archiving = $this->attributes('archiving'); - if ($archiving == null) { + /** @var array<string,bool|int|string>|null $archiving */ + $archiving = $this->attributeArray('archiving'); + if ($archiving === null) { $catDAO = FreshRSS_Factory::createCategoryDao(); $category = $catDAO->searchById($this->categoryId); - $archiving = $category == null ? null : $category->attributes('archiving'); - if ($archiving == null) { - $archiving = FreshRSS_Context::$user_conf->archiving; + $archiving = $category === null ? null : $category->attributeArray('archiving'); + /** @var array<string,bool|int|string>|null $archiving */ + if ($archiving === null) { + $archiving = FreshRSS_Context::userConf()->archiving; } } if (is_array($archiving)) { @@ -850,7 +858,7 @@ class FreshRSS_Feed extends Minz_Model { $hubFilename = PSHB_PATH . '/feeds/' . sha1($url) . '/!hub.json'; if ($hubFile = @file_get_contents($hubFilename)) { $hubJson = json_decode($hubFile, true); - if ($hubJson && empty($hubJson['error']) && + if (is_array($hubJson) && empty($hubJson['error']) && (empty($hubJson['lease_end']) || $hubJson['lease_end'] > time())) { return true; } @@ -862,8 +870,8 @@ class FreshRSS_Feed extends Minz_Model { $url = $this->selfUrl ?: $this->url; $hubFilename = PSHB_PATH . '/feeds/' . sha1($url) . '/!hub.json'; $hubFile = @file_get_contents($hubFilename); - $hubJson = $hubFile ? json_decode($hubFile, true) : []; - if (!isset($hubJson['error']) || $hubJson['error'] !== $error) { + $hubJson = is_string($hubFile) ? json_decode($hubFile, true) : null; + if (is_array($hubJson) && !isset($hubJson['error']) || $hubJson['error'] !== $error) { $hubJson['error'] = $error; file_put_contents($hubFilename, json_encode($hubJson)); Minz_Log::warning('Set error to ' . ($error ? 1 : 0) . ' for ' . $url, PSHB_LOG); @@ -876,13 +884,13 @@ class FreshRSS_Feed extends Minz_Model { */ public function pubSubHubbubPrepare() { $key = ''; - if (Minz_Request::serverIsPublic(FreshRSS_Context::$system_conf->base_url) && + if (Minz_Request::serverIsPublic(FreshRSS_Context::systemConf()->base_url) && $this->hubUrl && $this->selfUrl && @is_dir(PSHB_PATH)) { $path = PSHB_PATH . '/feeds/' . sha1($this->selfUrl); $hubFilename = $path . '/!hub.json'; if ($hubFile = @file_get_contents($hubFilename)) { $hubJson = json_decode($hubFile, true); - if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) { + if (!is_array($hubJson) || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) { $text = 'Invalid JSON for WebSub: ' . $this->url; Minz_Log::warning($text); Minz_Log::warning($text, PSHB_LOG); @@ -901,7 +909,7 @@ class FreshRSS_Feed extends Minz_Model { } } else { @mkdir($path, 0770, true); - $key = sha1($path . FreshRSS_Context::$system_conf->salt); + $key = sha1($path . FreshRSS_Context::systemConf()->salt); $hubJson = [ 'hub' => $this->hubUrl, 'key' => $key, @@ -913,7 +921,7 @@ class FreshRSS_Feed extends Minz_Model { Minz_Log::debug($text); Minz_Log::debug($text, PSHB_LOG); } - $currentUser = Minz_User::name(); + $currentUser = Minz_User::name() ?? ''; if (FreshRSS_user_Controller::checkUsername($currentUser) && !file_exists($path . '/' . $currentUser . '.txt')) { touch($path . '/' . $currentUser . '.txt'); } @@ -928,7 +936,7 @@ class FreshRSS_Feed extends Minz_Model { } else { $url = $this->url; //Always use current URL during unsubscribe } - if ($url && (Minz_Request::serverIsPublic(FreshRSS_Context::$system_conf->base_url) || !$state)) { + if ($url && (Minz_Request::serverIsPublic(FreshRSS_Context::systemConf()->base_url) || !$state)) { $hubFilename = PSHB_PATH . '/feeds/' . sha1($url) . '/!hub.json'; $hubFile = @file_get_contents($hubFilename); if ($hubFile === false) { @@ -936,7 +944,7 @@ class FreshRSS_Feed extends Minz_Model { return false; } $hubJson = json_decode($hubFile, true); - if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key']) || empty($hubJson['hub'])) { + if (!is_array($hubJson) || empty($hubJson['key']) || !ctype_xdigit($hubJson['key']) || empty($hubJson['hub'])) { Minz_Log::warning('Invalid JSON for WebSub: ' . $this->url); return false; } diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index ac844217a..895d2d333 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -118,7 +118,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { // Merge existing and import attributes $existingAttributes = $feed_search->attributes(); $importAttributes = $feed->attributes(); - $feed->_attributes('', array_replace_recursive($existingAttributes, $importAttributes)); + $feed->_attributes(array_replace_recursive($existingAttributes, $importAttributes)); // Update some values of the existing feed using the import $values = [ @@ -190,11 +190,12 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { } /** + * @param non-empty-string $key * @param string|array<mixed>|bool|int|null $value * @return int|false */ public function updateFeedAttribute(FreshRSS_Feed $feed, string $key, $value) { - $feed->_attributes($key, $value); + $feed->_attribute($key, $value); return $this->updateFeed( $feed->id(), ['attributes' => $feed->attributes()] @@ -236,6 +237,9 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { if ($newCat === null) { $newCat = $catDAO->getDefault(); } + if ($newCat === null) { + return false; + } $sql = 'UPDATE `_feed` SET category=? WHERE category=?'; $stm = $this->pdo->prepare($sql); @@ -305,6 +309,8 @@ SQL; return; } while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { + /** @var array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int, + * 'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string} $row */ yield $row; } } @@ -393,12 +399,12 @@ SQL; } } - /** @return array<string> */ + /** @return array<int,string> */ public function listTitles(int $id, int $limit = 0): array { $sql = 'SELECT title FROM `_entry` WHERE id_feed=:id_feed ORDER BY id DESC' . ($limit < 1 ? '' : ' LIMIT ' . intval($limit)); $res = $this->fetchColumn($sql, 0, [':id_feed' => $id]) ?? []; - /** @var array<string> $res */ + /** @var array<int,string> $res */ return $res; } @@ -609,7 +615,7 @@ SQL; $myFeed->_httpAuth(base64_decode($dao['httpAuth'] ?? '', true) ?: ''); $myFeed->_error($dao['error'] ?? 0); $myFeed->_ttl($dao['ttl'] ?? FreshRSS_Feed::TTL_DEFAULT); - $myFeed->_attributes('', $dao['attributes'] ?? ''); + $myFeed->_attributes($dao['attributes'] ?? ''); $myFeed->_nbNotRead($dao['cache_nbUnreads'] ?? -1); $myFeed->_nbEntries($dao['cache_nbEntries'] ?? -1); if (isset($dao['id'])) { diff --git a/app/Models/FilterAction.php b/app/Models/FilterAction.php index 6487d5100..bf5a79fe7 100644 --- a/app/Models/FilterAction.php +++ b/app/Models/FilterAction.php @@ -44,7 +44,7 @@ class FreshRSS_FilterAction { /** @param array|mixed|null $json */ public static function fromJSON($json): ?FreshRSS_FilterAction { - if (!empty($json['search']) && !empty($json['actions']) && is_array($json['actions'])) { + if (is_array($json) && !empty($json['search']) && !empty($json['actions']) && is_array($json['actions'])) { return new FreshRSS_FilterAction(new FreshRSS_BooleanSearch($json['search']), $json['actions']); } return null; diff --git a/app/Models/FilterActionsTrait.php b/app/Models/FilterActionsTrait.php index 869992b21..aefb549af 100644 --- a/app/Models/FilterActionsTrait.php +++ b/app/Models/FilterActionsTrait.php @@ -15,13 +15,11 @@ trait FreshRSS_FilterActionsTrait { private function filterActions(): array { if (empty($this->filterActions)) { $this->filterActions = []; - $filters = $this->attributes('filters'); - if (is_array($filters)) { - foreach ($filters as $filter) { - $filterAction = FreshRSS_FilterAction::fromJSON($filter); - if ($filterAction != null) { - $this->filterActions[] = $filterAction; - } + $filters = $this->attributeArray('filters') ?? []; + foreach ($filters as $filter) { + $filterAction = FreshRSS_FilterAction::fromJSON($filter); + if ($filterAction != null) { + $this->filterActions[] = $filterAction; } } } @@ -33,12 +31,12 @@ trait FreshRSS_FilterActionsTrait { */ private function _filterActions(?array $filterActions): void { $this->filterActions = $filterActions; - if (is_array($this->filterActions) && !empty($this->filterActions)) { - $this->_attributes('filters', array_map(static function (?FreshRSS_FilterAction $af) { + if ($this->filterActions !== null && !empty($this->filterActions)) { + $this->_attribute('filters', array_map(static function (?FreshRSS_FilterAction $af) { return $af == null ? null : $af->toJSON(); }, $this->filterActions)); } else { - $this->_attributes('filters', null); + $this->_attribute('filters', null); } } diff --git a/app/Models/FormAuth.php b/app/Models/FormAuth.php index 83fb60e3c..a8b4dab8a 100644 --- a/app/Models/FormAuth.php +++ b/app/Models/FormAuth.php @@ -23,7 +23,7 @@ class FreshRSS_FormAuth { $token_file = DATA_PATH . '/tokens/' . $token . '.txt'; $mtime = @filemtime($token_file) ?: 0; - $limits = FreshRSS_Context::$system_conf->limits; + $limits = FreshRSS_Context::systemConf()->limits; $cookie_duration = empty($limits['cookie_duration']) ? FreshRSS_Auth::DEFAULT_COOKIE_DURATION : $limits['cookie_duration']; if ($mtime + $cookie_duration < time()) { // Token has expired (> cookie_duration) or does not exist. @@ -42,7 +42,7 @@ class FreshRSS_FormAuth { private static function renewCookie(string $token) { $token_file = DATA_PATH . '/tokens/' . $token . '.txt'; if (touch($token_file)) { - $limits = FreshRSS_Context::$system_conf->limits; + $limits = FreshRSS_Context::systemConf()->limits; $cookie_duration = empty($limits['cookie_duration']) ? FreshRSS_Auth::DEFAULT_COOKIE_DURATION : $limits['cookie_duration']; $expire = time() + $cookie_duration; Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire); @@ -54,7 +54,7 @@ class FreshRSS_FormAuth { /** @return string|false */ public static function makeCookie(string $username, string $password_hash) { do { - $token = sha1(FreshRSS_Context::$system_conf->salt . $username . uniqid('' . mt_rand(), true)); + $token = sha1(FreshRSS_Context::systemConf()->salt . $username . uniqid('' . mt_rand(), true)); $token_file = DATA_PATH . '/tokens/' . $token . '.txt'; } while (file_exists($token_file)); @@ -78,7 +78,7 @@ class FreshRSS_FormAuth { } public static function purgeTokens(): void { - $limits = FreshRSS_Context::$system_conf->limits; + $limits = FreshRSS_Context::systemConf()->limits; $cookie_duration = empty($limits['cookie_duration']) ? FreshRSS_Auth::DEFAULT_COOKIE_DURATION : $limits['cookie_duration']; $oldest = time() - $cookie_duration; foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $file_info) { diff --git a/app/Models/StatsDAO.php b/app/Models/StatsDAO.php index 0e0bad623..9bdc255e3 100644 --- a/app/Models/StatsDAO.php +++ b/app/Models/StatsDAO.php @@ -248,8 +248,8 @@ WHERE c.id = f.category GROUP BY label ORDER BY data DESC SQL; - $res = $this->fetchAssoc($sql); /** @var array<array{'label':string,'data':int}>|null @res */ + $res = $this->fetchAssoc($sql); return $res == null ? [] : $res; } diff --git a/app/Models/SystemConfiguration.php b/app/Models/SystemConfiguration.php index 294ca1e3a..3efe33b15 100644 --- a/app/Models/SystemConfiguration.php +++ b/app/Models/SystemConfiguration.php @@ -5,7 +5,7 @@ declare(strict_types=1); * @property bool $allow_anonymous * @property bool $allow_anonymous_refresh * @property-read bool $allow_referrer - * @property-read bool $allow_robots + * @property bool $allow_robots * @property bool $api_enabled * @property string $archiving * @property 'form'|'http_auth'|'none' $auth_type @@ -16,7 +16,7 @@ declare(strict_types=1); * @property bool $force_email_validation * @property-read bool $http_auth_auto_register * @property-read string $http_auth_auto_register_email_field - * @property-read string $language + * @property string $language * @property array<string,int> $limits * @property-read string $logo_html * @property-read string $meta_description @@ -25,6 +25,7 @@ declare(strict_types=1); * @property-read bool $simplepie_syslog_enabled * @property bool $unsafe_autologin_enabled * @property array<string> $trusted_sources + * @property array<string,array<string,mixed>> $extensions */ final class FreshRSS_SystemConfiguration extends Minz_Configuration { diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php index 8587e576f..fba27dc7b 100644 --- a/app/Models/TagDAO.php +++ b/app/Models/TagDAO.php @@ -96,11 +96,12 @@ SQL; } /** + * @param non-empty-string $key * @param mixed $value * @return int|false */ public function updateTagAttribute(FreshRSS_Tag $tag, string $key, $value) { - $tag->_attributes($key, $value); + $tag->_attribute($key, $value); return $this->updateTagAttributes($tag->id(), $tag->attributes()); } @@ -134,6 +135,7 @@ SQL; return; } while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { + /** @var array{'id':int,'name':string,'attributes'?:array<string,mixed>} $row */ yield $row; } } @@ -410,7 +412,7 @@ SQL; $tag = new FreshRSS_Tag($dao['name']); $tag->_id($dao['id']); if (!empty($dao['attributes'])) { - $tag->_attributes('', $dao['attributes']); + $tag->_attributes($dao['attributes']); } if (isset($dao['unreads'])) { $tag->_nbUnread($dao['unreads']); diff --git a/app/Models/Themes.php b/app/Models/Themes.php index 53ae1dc27..fab29e986 100644 --- a/app/Models/Themes.php +++ b/app/Models/Themes.php @@ -38,11 +38,12 @@ class FreshRSS_Themes extends Minz_Model { if (file_exists($json_filename)) { $content = file_get_contents($json_filename) ?: ''; $res = json_decode($content, true); - if ($res && + if (is_array($res) && !empty($res['name']) && isset($res['files']) && is_array($res['files'])) { $res['id'] = $theme_id; + /** @var array{'id':string,'name':string,'author':string,'description':string,'version':float|string,'files':array<string>,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}} */ return $res; } } @@ -155,7 +156,7 @@ class FreshRSS_Themes extends Minz_Model { } if ($type == self::ICON_DEFAULT) { - if ((FreshRSS_Context::$user_conf && FreshRSS_Context::$user_conf->icons_as_emojis) + if ((FreshRSS_Context::hasUserConf() && FreshRSS_Context::userConf()->icons_as_emojis) // default to emoji alternate for some icons ) { $type = self::ICON_EMOJI; diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php index 0aec3a05f..a1e0dbbaa 100644 --- a/app/Models/UserConfiguration.php +++ b/app/Models/UserConfiguration.php @@ -70,6 +70,7 @@ declare(strict_types=1); * @property-read bool $unsafe_autologin_enabled * @property string $view_mode * @property array<string,mixed> $volatile + * @property array<string,array<string,mixed>> $extensions */ final class FreshRSS_UserConfiguration extends Minz_Configuration { use FreshRSS_FilterActionsTrait; @@ -81,22 +82,37 @@ final class FreshRSS_UserConfiguration extends Minz_Configuration { } /** - * @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>) - * @return array<string,mixed>|mixed|null + * @param non-empty-string $key + * @return array<int|string,mixed>|null */ - public function attributes(string $key = '') { - if ($key === '') { - return []; // Not implemented for user configuration - } else { - return parent::param($key, null); - } + public function attributeArray(string $key): ?array { + $a = parent::param($key, null); + return is_array($a) ? $a : null; } - /** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */ - public function _attributes(string $key, $value = null): void { - if ($key == '') { - return; // Not implemented for user configuration - } + /** @param non-empty-string $key */ + public function attributeBool(string $key): ?bool { + $a = parent::param($key, null); + return is_bool($a) ? $a : null; + } + + /** @param non-empty-string $key */ + public function attributeInt(string $key): ?int { + $a = parent::param($key, null); + return is_numeric($a) ? (int)$a : null; + } + + /** @param non-empty-string $key */ + public function attributeString(string $key): ?string { + $a = parent::param($key, null); + return is_string($a) ? $a : null; + } + + /** + * @param non-empty-string $key + * @param array<string,mixed>|mixed|null $value Value, not HTML-encoded + */ + public function _attribute(string $key, $value = null): void { parent::_param($key, $value); } } |
