diff options
| author | 2024-12-27 12:12:49 +0100 | |
|---|---|---|
| committer | 2024-12-27 12:12:49 +0100 | |
| commit | b1d24fbdb7d1cc948c946295035dad6df550fb7e (patch) | |
| tree | 7b4365a04097a779659474fbb9281a9661512522 /app/Models | |
| parent | 897e4a3f4a273d50c28157edb67612b2d7fa2e6f (diff) | |
PHPStan 2.0 (#7131)
* PHPStan 2.0
fix https://github.com/FreshRSS/FreshRSS/issues/6989
https://github.com/phpstan/phpstan/releases/tag/2.0.0
https://github.com/phpstan/phpstan/blob/2.0.x/UPGRADING.md
* More
* More
* Done
* fix i18n CLI
* Restore a PHPStan Next test
For work towards PHPStan Level 10
* 4 more on Level 10
* fix getTagsForEntry
* API at Level 10
* More Level 10
* Finish Minz at Level 10
* Finish CLI at Level 10
* Finish Controllers at Level 10
* More Level 10
* More
* Pass bleedingEdge
* Clean PHPStan options and add TODOs
* Level 10 for main config
* More
* Consitency array vs. list
* Sanitize themes get_infos
* Simplify TagDAO->getTagsForEntries()
* Finish reportAnyTypeWideningInVarTag
* Prepare checkBenevolentUnionTypes and checkImplicitMixed
* Fixes
* Refix
* Another fix
* Casing of __METHOD__ constant
Diffstat (limited to 'app/Models')
33 files changed, 402 insertions, 420 deletions
diff --git a/app/Models/ActionController.php b/app/Models/ActionController.php index 27fdfa44d..072f1a3d6 100644 --- a/app/Models/ActionController.php +++ b/app/Models/ActionController.php @@ -5,6 +5,7 @@ abstract class FreshRSS_ActionController extends Minz_ActionController { /** * @var FreshRSS_View + * @phpstan-ignore property.phpDocType */ protected $view; diff --git a/app/Models/AttributesTrait.php b/app/Models/AttributesTrait.php index 8795d81d9..f30b11b5d 100644 --- a/app/Models/AttributesTrait.php +++ b/app/Models/AttributesTrait.php @@ -53,6 +53,7 @@ trait FreshRSS_AttributesTrait { $values = json_decode($values, true); } if (is_array($values)) { + $values = array_filter($values, 'is_string', ARRAY_FILTER_USE_KEY); $this->attributes = $values; } } diff --git a/app/Models/Auth.php b/app/Models/Auth.php index f65a59e03..5c861f1db 100644 --- a/app/Models/Auth.php +++ b/app/Models/Auth.php @@ -75,8 +75,8 @@ class FreshRSS_Auth { if (!$login_ok && FreshRSS_Context::systemConf()->http_auth_auto_register) { $email = null; 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]; + is_string($_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field] ?? null)) { + $email = $_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field]; } $language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::systemConf()->language); Minz_Translate::init($language); diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php index 529bcd338..f7273151e 100644 --- a/app/Models/BooleanSearch.php +++ b/app/Models/BooleanSearch.php @@ -7,7 +7,7 @@ declare(strict_types=1); class FreshRSS_BooleanSearch implements \Stringable { private string $raw_input = ''; - /** @var array<FreshRSS_BooleanSearch|FreshRSS_Search> */ + /** @var list<FreshRSS_BooleanSearch|FreshRSS_Search> */ private array $searches = []; /** @@ -400,7 +400,7 @@ class FreshRSS_BooleanSearch implements \Stringable { /** * Either a list of FreshRSS_BooleanSearch combined by implicit AND * or a series of FreshRSS_Search combined by explicit OR - * @return array<FreshRSS_BooleanSearch|FreshRSS_Search> + * @return list<FreshRSS_BooleanSearch|FreshRSS_Search> */ public function searches(): array { return $this->searches; diff --git a/app/Models/Category.php b/app/Models/Category.php index cd8145e0c..5f87335f3 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -19,7 +19,7 @@ class FreshRSS_Category extends Minz_Model { private string $name; private int $nbFeeds = -1; private int $nbNotRead = -1; - /** @var array<FreshRSS_Feed>|null */ + /** @var list<FreshRSS_Feed>|null */ private ?array $feeds = null; /** @var bool|int */ private $hasFeedsWithError = false; @@ -100,7 +100,7 @@ class FreshRSS_Category extends Minz_Model { } /** - * @return array<int,FreshRSS_Feed> + * @return list<FreshRSS_Feed> * @throws Minz_ConfigurationNamespaceException * @throws Minz_PDOConnectionException */ @@ -142,11 +142,11 @@ class FreshRSS_Category extends Minz_Model { } /** @param array<FreshRSS_Feed>|FreshRSS_Feed $values */ - public function _feeds($values): void { + public function _feeds(array|FreshRSS_Feed $values): void { if (!is_array($values)) { $values = [$values]; } - $this->feeds = $values; + $this->feeds = array_values($values); $this->sortFeeds(); } @@ -243,7 +243,7 @@ class FreshRSS_Category extends Minz_Model { if ($this->feeds === null) { return; } - uasort($this->feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name())); + usort($this->feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name())); } /** @@ -265,13 +265,13 @@ class FreshRSS_Category extends Minz_Model { /** * Access cached feeds * @param array<FreshRSS_Category> $categories - * @return array<int,FreshRSS_Feed> + * @return list<FreshRSS_Feed> */ public static function findFeeds(array $categories): array { $result = []; foreach ($categories as $category) { foreach ($category->feeds() as $feed) { - $result[$feed->id()] = $feed; + $result[] = $feed; } } return $result; diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 6b563b0a8..556179800 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -19,7 +19,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if ($this->pdo->inTransaction()) { $this->pdo->commit(); } - Minz_Log::warning(__method__ . ': ' . $name); + Minz_Log::warning(__METHOD__ . ': ' . $name); try { if ($name === 'kind') { //v1.20.0 return $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN kind SMALLINT DEFAULT 0') !== false; @@ -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{'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 */ + /** @var list<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'); @@ -51,15 +51,17 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { if (!is_array($attributes)) { $attributes = []; } + $archiving = is_array($attributes['archiving'] ?? null) ? $attributes['archiving'] : []; if ($keepHistory > 0) { - $attributes['archiving']['keep_min'] = (int)$keepHistory; + $archiving['keep_min'] = (int)$keepHistory; } elseif ($keepHistory == -1) { //Infinite - $attributes['archiving']['keep_period'] = false; - $attributes['archiving']['keep_max'] = false; - $attributes['archiving']['keep_min'] = false; + $archiving['keep_period'] = false; + $archiving['keep_max'] = false; + $archiving['keep_min'] = false; } else { continue; } + $attributes['archiving'] = $archiving; if (!($stm->bindValue(':id', $feed['id'], PDO::PARAM_INT) && $stm->bindValue(':attributes', json_encode($attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) && $stm->execute())) { @@ -78,12 +80,12 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { return $ok; } } catch (Exception $e) { - Minz_Log::error(__method__ . ': ' . $e->getMessage()); + Minz_Log::error(__METHOD__ . ': ' . $e->getMessage()); } return false; } - /** @param array<string|int> $errorInfo */ + /** @param array{0:string,1:int,2:string} $errorInfo */ protected function autoUpdateDb(array $errorInfo): bool { if (isset($errorInfo[0])) { if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) { @@ -99,7 +101,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo { } /** - * @param array{'name':string,'id'?:int,'kind'?:int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string|array<string,mixed>} $valuesTmp + * @param array{name:string,id?:int,kind?:int,lastUpdate?:int,error?:int|bool,attributes?:string|array<string,mixed>} $valuesTmp */ public function addCategory(array $valuesTmp): int|false { // TRIM() to provide a type hint as text @@ -127,6 +129,7 @@ SQL; return $catId === false ? false : (int)$catId; } else { $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->addCategory($valuesTmp); } @@ -150,7 +153,7 @@ SQL; } /** - * @param array{'name':string,'kind':int,'attributes'?:array<string,mixed>|mixed|null} $valuesTmp + * @param array{name:string,kind:int,attributes?:array<string,mixed>|mixed|null} $valuesTmp */ public function updateCategory(int $id, array $valuesTmp): int|false { // No tag of the same name @@ -176,6 +179,7 @@ SQL; return $stm->rowCount(); } else { $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->updateCategory($id, $valuesTmp); } @@ -217,21 +221,22 @@ SQL; } } - /** @return Traversable<array{'id':int,'name':string,'kind':int,'lastUpdate':int,'error':int,'attributes'?:array<string,mixed>}> */ + /** @return Traversable<array{id:int,name:string,kind:int,lastUpdate:int,error:int,attributes?:array<string,mixed>}> */ public function selectAll(): Traversable { $sql = 'SELECT id, name, kind, `lastUpdate`, error, attributes FROM `_category`'; $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 */ + /** @var array{id:int,name:string,kind:int,lastUpdate:int,error:int,attributes?:array<string,mixed>} $row */ yield $row; } } else { $info = $this->pdo->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { yield from $this->selectAll(); } else { - Minz_Log::error(__method__ . ' error: ' . json_encode($info)); + Minz_Log::error(__METHOD__ . ' error: ' . json_encode($info)); } } } @@ -239,24 +244,24 @@ 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 */ - $categories = self::daoToCategories($res); + /** @var array<array{name:string,id:int,kind:int,lastUpdate?:int,error:int|bool,attributes?:string}> $res */ + $categories = self::daoToCategories($res); // @phpstan-ignore varTag.type return reset($categories) ?: null; } public function searchByName(string $name): ?FreshRSS_Category { $sql = 'SELECT * FROM `_category` WHERE name=:name'; $res = $this->fetchAssoc($sql, ['name' => $name]) ?? []; - /** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */ - $categories = self::daoToCategories($res); + /** @var array<array{name:string,id:int,kind:int,lastUpdate:int,error:int|bool,attributes:string}> $res */ + $categories = self::daoToCategories($res); // @phpstan-ignore varTag.type return reset($categories) ?: null; } - /** @return array<int,FreshRSS_Category> */ + /** @return list<FreshRSS_Category> */ public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false): array { $categories = $this->listCategories($prePopulateFeeds, $details); - uasort($categories, static function (FreshRSS_Category $a, FreshRSS_Category $b) { + usort($categories, static function (FreshRSS_Category $a, FreshRSS_Category $b) { $aPosition = $a->attributeInt('position'); $bPosition = $b->attributeInt('position'); if ($aPosition === $bPosition) { @@ -272,7 +277,7 @@ SQL; return $categories; } - /** @return array<int,FreshRSS_Category> */ + /** @return list<FreshRSS_Category> */ public function listCategories(bool $prePopulateFeeds = true, bool $details = false): array { if ($prePopulateFeeds) { $sql = 'SELECT c.id AS c_id, c.name AS c_name, c.kind AS c_kind, c.`lastUpdate` AS c_last_update, c.error AS c_error, c.attributes AS c_attributes, ' @@ -286,11 +291,12 @@ SQL; $values = [ ':priority' => FreshRSS_Feed::PRIORITY_CATEGORY ]; if ($stm !== false && $stm->execute($values)) { $res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: []; - /** @var array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string, - * 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'category'?:int,'website'?:string,'priority'?:int,'error'?:int|bool,'attributes'?:string,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $res */ + /** @var list<array{c_name:string,c_id:int,c_kind:int,c_last_update:int,c_error:int|bool,c_attributes?:string, + * id?:int,name?:string,url?:string,kind?:int,category?:int,website?:string,priority?:int,error?:int|bool,attributes?:string,cache_nbEntries?:int,cache_nbUnreads?:int,ttl?:int}> $res */ return self::daoToCategoriesPrepopulated($res); } else { $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->listCategories($prePopulateFeeds, $details); } @@ -298,13 +304,13 @@ SQL; return []; } } else { - $res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name'); - /** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */ - return empty($res) ? [] : self::daoToCategories($res); + $res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name') ?? []; + /** @var list<array{name:string,id:int,kind:int,lastUpdate?:int,error?:int|bool,attributes?:string}> $res */ + return empty($res) ? [] : self::daoToCategories($res); // @phpstan-ignore varTag.type } } - /** @return array<int,FreshRSS_Category> */ + /** @return list<FreshRSS_Category> */ public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0): array { $sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`' . ($limit < 1 ? '' : ' LIMIT ' . $limit); @@ -313,9 +319,12 @@ SQL; $stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) && $stm->bindValue(':lu', time() - $defaultCacheDuration, PDO::PARAM_INT) && $stm->execute()) { - return self::daoToCategories($stm->fetchAll(PDO::FETCH_ASSOC)); + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + /** @var list<array{name:string,id:int,kind:int,lastUpdate:int,error?:int|bool,attributes?:string}> $res */ + return self::daoToCategories($res); } else { $info = $stm !== false ? $stm->errorInfo() : $this->pdo->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->listCategoriesOrderUpdate($defaultCacheDuration, $limit); } @@ -327,10 +336,10 @@ SQL; public function getDefault(): ?FreshRSS_Category { $sql = 'SELECT * FROM `_category` WHERE id=:id'; $res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]) ?? []; - /** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */ - $categories = self::daoToCategories($res); - if (isset($categories[self::DEFAULTCATEGORYID])) { - return $categories[self::DEFAULTCATEGORYID]; + /** @var array<array{name:string,id:int,kind:int,lastUpdate?:int,error?:int|bool,attributes?:string}> $res */ + $categories = self::daoToCategories($res); // @phpstan-ignore varTag.type + if (isset($categories[0])) { + return $categories[0]; } else { if (FreshRSS_Context::$isCli) { fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n"); @@ -388,7 +397,7 @@ SQL; return isset($res[0]) ? (int)$res[0] : -1; } - /** @return array<int,string> */ + /** @return list<string> */ public function listTitles(int $id, int $limit = 0): array { $sql = <<<'SQL' SELECT e.title FROM `_entry` e @@ -398,15 +407,15 @@ SQL; SQL; $sql .= ($limit < 1 ? '' : ' LIMIT ' . intval($limit)); $res = $this->fetchColumn($sql, 0, [':id_category' => $id]) ?? []; - /** @var array<int,string> $res */ + /** @var list<string> $res */ return $res; } /** - * @param array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string, - * 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'website'?:string,'priority'?:int, - * 'error'?:int|bool,'attributes'?:string,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $listDAO - * @return array<int,FreshRSS_Category> + * @param array<array{c_name:string,c_id:int,c_kind:int,c_last_update:int,c_error:int|bool,c_attributes?:string, + * id?:int,name?:string,url?:string,kind?:int,website?:string,priority?:int, + * error?:int|bool,attributes?:string,cache_nbEntries?:int,cache_nbUnreads?:int,ttl?:int}> $listDAO + * @return list<FreshRSS_Category> */ private static function daoToCategoriesPrepopulated(array $listDAO): array { $list = []; @@ -414,8 +423,6 @@ SQL; $feedsDao = []; $feedDao = FreshRSS_Factory::createFeedDao(); foreach ($listDAO as $line) { - FreshRSS_DatabaseDAO::pdoInt($line, ['c_id', 'c_kind', 'c_last_update', 'c_error', - 'id', 'kind', 'priority', 'error', 'cache_nbEntries', 'cache_nbUnreads', 'ttl']); if (!empty($previousLine['c_id']) && $line['c_id'] !== $previousLine['c_id']) { // End of the current category, we add it to the $list $cat = new FreshRSS_Category( @@ -425,7 +432,7 @@ SQL; ); $cat->_kind($previousLine['c_kind']); $cat->_attributes($previousLine['c_attributes'] ?? '[]'); - $list[$cat->id()] = $cat; + $list[] = $cat; $feedsDao = []; //Prepare for next category } @@ -445,20 +452,19 @@ SQL; $cat->_lastUpdate($previousLine['c_last_update'] ?? 0); $cat->_error($previousLine['c_error'] ?? 0); $cat->_attributes($previousLine['c_attributes'] ?? []); - $list[$cat->id()] = $cat; + $list[] = $cat; } return $list; } /** - * @param array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $listDAO - * @return array<int,FreshRSS_Category> + * @param array<array{name:string,id:int,kind:int,lastUpdate?:int,error?:int|bool,attributes?:string}> $listDAO + * @return list<FreshRSS_Category> */ private static function daoToCategories(array $listDAO): array { $list = []; foreach ($listDAO as $dao) { - FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'lastUpdate', 'error']); $cat = new FreshRSS_Category( $dao['name'], $dao['id'] @@ -467,7 +473,7 @@ SQL; $cat->_lastUpdate($dao['lastUpdate'] ?? 0); $cat->_error($dao['error'] ?? 0); $cat->_attributes($dao['attributes'] ?? ''); - $list[$cat->id()] = $cat; + $list[] = $cat; } return $list; } diff --git a/app/Models/CategoryDAOSQLite.php b/app/Models/CategoryDAOSQLite.php index d13c52550..f4db76299 100644 --- a/app/Models/CategoryDAOSQLite.php +++ b/app/Models/CategoryDAOSQLite.php @@ -3,7 +3,7 @@ declare(strict_types=1); class FreshRSS_CategoryDAOSQLite extends FreshRSS_CategoryDAO { - /** @param array<int|string> $errorInfo */ + /** @param array{0:string,1:int,2:string} $errorInfo */ #[\Override] protected function autoUpdateDb(array $errorInfo): bool { if (($tableInfo = $this->pdo->query("PRAGMA table_info('category')")) !== false) { diff --git a/app/Models/Context.php b/app/Models/Context.php index 6cdda909c..b9cc77498 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -8,11 +8,11 @@ declare(strict_types=1); final class FreshRSS_Context { /** - * @var array<int,FreshRSS_Category> + * @var list<FreshRSS_Category> */ private static array $categories = []; /** - * @var array<int,FreshRSS_Tag> + * @var list<FreshRSS_Tag> */ private static array $tags = []; public static string $name = ''; @@ -176,7 +176,7 @@ final class FreshRSS_Context { FreshRSS_Context::$user_conf = null; } - /** @return array<int,FreshRSS_Category> */ + /** @return list<FreshRSS_Category> */ public static function categories(): array { if (empty(self::$categories)) { $catDAO = FreshRSS_Factory::createCategoryDao(); @@ -185,12 +185,12 @@ final class FreshRSS_Context { return self::$categories; } - /** @return array<int,FreshRSS_Feed> */ + /** @return list<FreshRSS_Feed> */ public static function feeds(): array { return FreshRSS_Category::findFeeds(self::categories()); } - /** @return array<int,FreshRSS_Tag> */ + /** @return list<FreshRSS_Tag> */ public static function labels(bool $precounts = false): array { if (empty(self::$tags) || $precounts) { $tagDAO = FreshRSS_Factory::createTagDao(); @@ -429,7 +429,6 @@ final class FreshRSS_Context { self::$name = _t('index.feed.title_fav'); self::$description = FreshRSS_Context::systemConf()->meta_description; self::$get_unread = self::$total_starred['unread']; - // Update state if favorite is not yet enabled. self::$state = self::$state | FreshRSS_Entry::STATE_FAVORITE; break; @@ -437,11 +436,7 @@ final class FreshRSS_Context { // We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description $feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_Category::findFeed(self::$categories, $id); if ($feed === null) { - $feedDAO = FreshRSS_Factory::createFeedDao(); - $feed = $feedDAO->searchById($id); - if ($feed === null) { - throw new FreshRSS_Context_Exception('Invalid feed: ' . $id); - } + throw new FreshRSS_Context_Exception('Invalid feed: ' . $id); } self::$current_get['feed'] = $id; self::$current_get['category'] = $feed->categoryId(); @@ -452,15 +447,15 @@ final class FreshRSS_Context { case 'c': // We try to find the corresponding category. self::$current_get['category'] = $id; - if (!isset(self::$categories[$id])) { - $catDAO = FreshRSS_Factory::createCategoryDao(); - $cat = $catDAO->searchById($id); - if ($cat === null) { - throw new FreshRSS_Context_Exception('Invalid category: ' . $id); + $cat = null; + foreach (self::$categories as $category) { + if ($category->id() === $id) { + $cat = $category; + break; } - self::$categories[$id] = $cat; - } else { - $cat = self::$categories[$id]; + } + if ($cat === null) { + throw new FreshRSS_Context_Exception('Invalid category: ' . $id); } self::$name = $cat->name(); self::$get_unread = $cat->nbNotRead(); @@ -468,15 +463,15 @@ final class FreshRSS_Context { case 't': // We try to find the corresponding tag. self::$current_get['tag'] = $id; - if (!isset(self::$tags[$id])) { - $tagDAO = FreshRSS_Factory::createTagDao(); - $tag = $tagDAO->searchById($id); - if ($tag === null) { - throw new FreshRSS_Context_Exception('Invalid tag: ' . $id); + $tag = null; + foreach (self::$tags as $t) { + if ($t->id() === $id) { + $tag = $t; + break; } - self::$tags[$id] = $tag; - } else { - $tag = self::$tags[$id]; + } + if ($tag === null) { + throw new FreshRSS_Context_Exception('Invalid tag: ' . $id); } self::$name = $tag->name(); self::$get_unread = $tag->nbUnread(); diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php index c46c91525..5a58ea2ad 100644 --- a/app/Models/DatabaseDAO.php +++ b/app/Models/DatabaseDAO.php @@ -25,10 +25,14 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { $db = FreshRSS_Context::systemConf()->db; try { - $sql = sprintf($GLOBALS['SQL_CREATE_DB'], empty($db['base']) ? '' : $db['base']); + $sql = $GLOBALS['SQL_CREATE_DB']; + if (!is_string($sql)) { + throw new Exception('SQL_CREATE_DB is not a string!'); + } + $sql = sprintf($sql, empty($db['base']) ? '' : $db['base']); return $this->pdo->exec($sql) === false ? 'Error during CREATE DATABASE' : ''; } catch (Exception $e) { - syslog(LOG_DEBUG, __method__ . ' notice: ' . $e->getMessage()); + syslog(LOG_DEBUG, __METHOD__ . ' notice: ' . $e->getMessage()); return $e->getMessage(); } } @@ -43,7 +47,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0); return $res == false ? 'Error during SQL connection fetch test!' : ''; } catch (Exception $e) { - syslog(LOG_DEBUG, __method__ . ' warning: ' . $e->getMessage()); + syslog(LOG_DEBUG, __METHOD__ . ' warning: ' . $e->getMessage()); return $e->getMessage(); } } @@ -81,7 +85,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { return count(array_keys($tables, true, true)) === count($tables); } - /** @return array<array{name:string,type:string,notnull:bool,default:mixed}> */ + /** @return list<array{name:string,type:string,notnull:bool,default:mixed}> */ public function getSchema(string $table): array { $res = $this->fetchAssoc('DESC `_' . $table . '`'); return $res == null ? [] : $this->listDaoToSchema($res); @@ -164,16 +168,16 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { */ public function daoToSchema(array $dao): array { return [ - 'name' => (string)($dao['Field']), - 'type' => strtolower((string)($dao['Type'])), - 'notnull' => (bool)$dao['Null'], - 'default' => $dao['Default'], + 'name' => is_string($dao['Field'] ?? null) ? $dao['Field'] : '', + 'type' => is_string($dao['Type'] ?? null) ? strtolower($dao['Type']) : '', + 'notnull' => empty($dao['Null']), + 'default' => is_scalar($dao['Default'] ?? null) ? $dao['Default'] : null, ]; } /** * @param array<array<string,string|int|bool|null>> $listDAO - * @return array<array{name:string,type:string,notnull:bool,default:mixed}> + * @return list<array{name:string,type:string,notnull:bool,default:mixed}> */ public function listDaoToSchema(array $listDAO): array { $list = []; @@ -198,7 +202,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { return self::$staticVersion; } static $version = null; - if ($version === null) { + if (!is_string($version)) { $version = $this->fetchValue('SELECT version()') ?? ''; } return $version; @@ -256,7 +260,7 @@ SQL; $catDAO->resetDefaultCategoryName(); include_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); - if (!empty($GLOBALS['SQL_UPDATE_MINOR'])) { + if (!empty($GLOBALS['SQL_UPDATE_MINOR']) && is_string($GLOBALS['SQL_UPDATE_MINOR'])) { $sql = $GLOBALS['SQL_UPDATE_MINOR']; $isMariaDB = false; @@ -272,7 +276,7 @@ SQL; if ($this->pdo->exec($sql) === false) { $info = $this->pdo->errorInfo(); if ($this->pdo->dbType() === 'mysql' && - !$isMariaDB && !empty($info[2]) && (stripos($info[2], "Can't DROP ") !== false)) { + !$isMariaDB && is_string($info[2] ?? null) && (stripos($info[2], "Can't DROP ") !== false)) { // Too bad for MySQL, but ignore error return; } @@ -444,7 +448,7 @@ SQL; foreach ($tagFrom->selectEntryTag() as $entryTag) { if (!empty($idMaps['t' . $entryTag['id_tag']])) { $entryTag['id_tag'] = $idMaps['t' . $entryTag['id_tag']]; - if (!$tagTo->tagEntry($entryTag['id_tag'], $entryTag['id_entry'])) { + if (!$tagTo->tagEntry($entryTag['id_tag'], (string)$entryTag['id_entry'])) { $error = 'Error during SQLite copy of entry-tags!'; return self::stdError($error); } @@ -454,31 +458,4 @@ SQL; return true; } - - /** - * Ensure that some PDO columns are `int` and not `string`. - * Compatibility with PHP 7. - * @param array<string|int|null> $table - * @param array<string> $columns - */ - public static function pdoInt(array &$table, array $columns): void { - foreach ($columns as $column) { - if (isset($table[$column]) && is_string($table[$column])) { - $table[$column] = (int)$table[$column]; - } - } - } - - /** - * Ensure that some PDO columns are `string` and not `bigint`. - * @param array<string|int|null> $table - * @param array<string> $columns - */ - public static function pdoString(array &$table, array $columns): void { - foreach ($columns as $column) { - if (isset($table[$column])) { - $table[$column] = (string)$table[$column]; - } - } - } } diff --git a/app/Models/DatabaseDAOPGSQL.php b/app/Models/DatabaseDAOPGSQL.php index 3cce4b062..a183bdee6 100644 --- a/app/Models/DatabaseDAOPGSQL.php +++ b/app/Models/DatabaseDAOPGSQL.php @@ -34,7 +34,7 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite { return count(array_keys($tables, true, true)) === count($tables); } - /** @return array<array{name:string,type:string,notnull:bool,default:mixed}> */ + /** @return list<array{name:string,type:string,notnull:bool,default:mixed}> */ #[\Override] public function getSchema(string $table): array { $sql = <<<'SQL' @@ -52,10 +52,10 @@ SQL; #[\Override] public function daoToSchema(array $dao): array { return [ - 'name' => (string)($dao['field']), - 'type' => strtolower((string)($dao['type'])), - 'notnull' => (bool)$dao['null'], - 'default' => $dao['default'], + 'name' => is_string($dao['field'] ?? null) ? $dao['field'] : '', + 'type' => is_string($dao['type'] ?? null) ? strtolower($dao['type']) : '', + 'notnull' => empty($dao['null']), + 'default' => is_scalar($dao['default'] ?? null) ? $dao['default'] : null, ]; } diff --git a/app/Models/DatabaseDAOSQLite.php b/app/Models/DatabaseDAOSQLite.php index 231616f49..f59f6c9ae 100644 --- a/app/Models/DatabaseDAOSQLite.php +++ b/app/Models/DatabaseDAOSQLite.php @@ -24,18 +24,25 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO { $this->pdo->prefix() . 'entrytag' => false, ]; foreach ($res as $value) { - $tables[$value['name']] = true; + if (is_array($value) && is_string($value['name'] ?? null)) { + $tables[$value['name']] = true; + } } return count(array_keys($tables, true, true)) == count($tables); } - /** @return array<array{name:string,type:string,notnull:bool,default:mixed}> */ + /** @return list<array{name:string,type:string,notnull:bool,default:mixed}> */ #[\Override] public function getSchema(string $table): array { $sql = 'PRAGMA table_info(' . $table . ')'; $stm = $this->pdo->query($sql); - return $stm !== false ? $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC) ?: []) : []; + if ($stm !== false) { + $res = $stm->fetchAll(PDO::FETCH_ASSOC); + /** @var list<array{name:string,type:string,notnull:bool,dflt_value:string|int|bool|null}> $res */ + return $this->listDaoToSchema($res ?: []); + } + return []; } #[\Override] @@ -59,10 +66,10 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO { #[\Override] public function daoToSchema(array $dao): array { return [ - 'name' => (string)$dao['name'], - 'type' => strtolower((string)$dao['type']), - 'notnull' => $dao['notnull'] == '1' ? true : false, - 'default' => $dao['dflt_value'], + 'name' => is_string($dao['name'] ?? null) ? $dao['name'] : '', + 'type' => is_string($dao['type'] ?? null) ? strtolower($dao['type']) : '', + 'notnull' => empty($dao['notnull']), + 'default' => is_scalar($dao['dflt_value'] ?? null) ? $dao['dflt_value'] : null, ]; } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 36ed11b40..c32506319 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -52,12 +52,10 @@ class FreshRSS_Entry extends Minz_Model { $this->_guid($guid); } - /** @param array{'id'?:string,'id_feed'?:int,'guid'?:string,'title'?:string,'author'?:string,'content'?:string,'link'?:string,'date'?:int|string,'lastSeen'?:int, - * 'hash'?:string,'is_read'?:bool|int,'is_favorite'?:bool|int,'tags'?:string|array<string>,'attributes'?:?string,'thumbnail'?:string,'timestamp'?:string} $dao */ + /** @param array{id?:string,id_feed?:int,guid?:string,title?:string,author?:string,content?:string,link?:string,date?:int|string,lastSeen?:int, + * hash?:string,is_read?:bool|int,is_favorite?:bool|int,tags?:string|array<string>,attributes?:?string,thumbnail?:string,timestamp?:string} $dao */ public static function fromArray(array $dao): FreshRSS_Entry { - FreshRSS_DatabaseDAO::pdoInt($dao, ['id_feed', 'date', 'lastSeen', 'is_read', 'is_favorite']); - - if (empty($dao['content'])) { + if (empty($dao['content']) || !is_string($dao['content'])) { $dao['content'] = ''; } @@ -83,7 +81,7 @@ class FreshRSS_Entry extends Minz_Model { $dao['is_favorite'] ?? false, $dao['tags'] ?? '' ); - if (!empty($dao['id'])) { + if (!empty($dao['id']) && is_numeric($dao['id'])) { $entry->_id($dao['id']); } if (!empty($dao['timestamp'])) { @@ -241,7 +239,9 @@ HTML; $content .= '<figure class="enclosure">'; foreach ($thumbnails as $thumbnail) { - $content .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" title="' . $etitle . '" /></p>'; + if (is_string($thumbnail)) { + $content .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" title="' . $etitle . '" /></p>'; + } } if (self::enclosureIsImage($enclosure)) { @@ -283,9 +283,9 @@ HTML; /** @return Traversable<array{'url':string,'type'?:string,'medium'?:string,'length'?:int,'title'?:string,'description'?:string,'credit'?:string|array<string>,'height'?:int,'width'?:int,'thumbnails'?:array<string>}> */ public function enclosures(bool $searchBodyImages = false): Traversable { $attributeEnclosures = $this->attributeArray('enclosures'); - if (is_iterable($attributeEnclosures)) { + if (is_array($attributeEnclosures)) { // FreshRSS 1.20.1+: The enclosures are saved as attributes - /** @var iterable<array{'url':string,'type'?:string,'medium'?:string,'length'?:int,'title'?:string,'description'?:string,'credit'?:string|array<string>,'height'?:int,'width'?:int,'thumbnails'?:array<string>}> $attributeEnclosures */ + /** @var list<array{'url':string,'type'?:string,'medium'?:string,'length'?:int,'title'?:string,'description'?:string,'credit'?:string|array<string>,'height'?:int,'width'?:int,'thumbnails'?:array<string>}> $attributeEnclosures */ yield from $attributeEnclosures; } try { @@ -354,7 +354,7 @@ HTML; public function thumbnail(bool $searchEnclosures = true): ?array { $thumbnail = $this->attributeArray('thumbnail') ?? []; // First, use the provided thumbnail, if any - if (!empty($thumbnail['url'])) { + if (is_string($thumbnail['url'] ?? null)) { /** @var array{'url':string,'height'?:int,'width'?:int,'time'?:string} $thumbnail */ return $thumbnail; } diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index 525687c90..4e7f532ac 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -35,7 +35,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo { return []; } - /** @param array<int|string> $values */ + /** @param list<int|string> $values */ protected static function sqlRegex(string $expression, string $regex, array &$values): string { // The implementation of this function is solely for MySQL and MariaDB static $databaseDAOMySQL = null; @@ -90,7 +90,7 @@ SQL; $ok = $this->pdo->exec($sql) !== false; } catch (Exception $e) { $ok = false; - Minz_Log::error(__method__ . ' error: ' . $e->getMessage()); + Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage()); } return $ok; } @@ -99,7 +99,7 @@ SQL; if ($this->pdo->inTransaction()) { $this->pdo->commit(); } - Minz_Log::warning(__method__ . ': ' . $name); + Minz_Log::warning(__METHOD__ . ': ' . $name); try { if ($name === 'attributes') { //v1.20.0 $sql = <<<'SQL' @@ -109,13 +109,13 @@ SQL; return $this->pdo->exec($sql) !== false; } } catch (Exception $e) { - Minz_Log::error(__method__ . ' error: ' . $e->getMessage()); + Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage()); } return false; } //TODO: Move the database auto-updates to DatabaseDAO - /** @param array<string|int> $errorInfo */ + /** @param array{0:string,1:int,2:string} $errorInfo */ protected function autoUpdateDb(array $errorInfo): bool { if (isset($errorInfo[0])) { if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) { @@ -201,6 +201,7 @@ SQL; return true; } else { $info = $this->addEntryPrepared == false ? $this->pdo->errorInfo() : $this->addEntryPrepared->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { $this->addEntryPrepared = null; return $this->addEntry($valuesTmp); @@ -310,6 +311,7 @@ SQL; return true; } else { $info = $this->updateEntryPrepared == false ? $this->pdo->errorInfo() : $this->updateEntryPrepared->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->updateEntry($valuesTmp); } @@ -336,7 +338,7 @@ SQL; * @todo simplify the query by removing the str_repeat. I am pretty sure * there is an other way to do that. * - * @param numeric-string|array<numeric-string> $ids + * @param numeric-string|list<numeric-string> $ids */ public function markFavorite($ids, bool $is_favorite = true): int|false { if (!is_array($ids)) { @@ -414,7 +416,7 @@ SQL; * Toggle the read marker on one or more article. * Then the cache is updated. * - * @param numeric-string|array<numeric-string> $ids + * @param numeric-string|list<numeric-string> $ids * @return int|false affected rows */ public function markRead(array|string $ids, bool $is_read = true): int|false { @@ -720,16 +722,17 @@ SQL; return $stm->rowCount(); } else { $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->cleanOldEntries($id_feed, $options); } - Minz_Log::error(__method__ . ' error:' . json_encode($info)); + Minz_Log::error(__METHOD__ . ' error:' . json_encode($info)); return false; } } - /** @return Traversable<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':?string}> */ + /** @return Traversable<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:?string}> */ public function selectAll(?int $limit = null): Traversable { $content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content'; $hash = static::sqlHexEncode('hash'); @@ -743,16 +746,17 @@ 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':?string} $row */ + /** @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:?string} $row */ yield $row; } } else { $info = $this->pdo->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { yield from $this->selectAll(); } else { - Minz_Log::error(__method__ . ' error: ' . json_encode($info)); + Minz_Log::error(__METHOD__ . ' error: ' . json_encode($info)); } } } @@ -765,8 +769,8 @@ SELECT id, guid, title, author, link, date, is_read, is_favorite, {$hash} AS has FROM `_entry` WHERE id_feed=:id_feed AND guid=:guid SQL; $res = $this->fetchAssoc($sql, [':id_feed' => $id_feed, ':guid' => $guid]); - /** @var array<array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int, - * 'is_read':int,'is_favorite':int,'tags':string,'attributes':?string}> $res */ + /** @var list<array{id:string,id_feed:int,guid:string,title:string,author:string,content:string,link:string,date:int, + * is_read:int,is_favorite:int,tags:string,attributes:?string}> $res */ return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null; } @@ -778,7 +782,7 @@ SELECT id, guid, title, author, link, date, is_read, is_favorite, {$hash} AS has FROM `_entry` WHERE id=:id SQL; $res = $this->fetchAssoc($sql, [':id' => $id]); - /** @var array<array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int, + /** @var list<array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int, * 'is_read':int,'is_favorite':int,'tags':string,'attributes':?string}> $res */ return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null; } @@ -789,7 +793,7 @@ SQL; return empty($res[0]) ? null : (string)($res[0]); } - /** @return array{0:array<int|string>,1:string} */ + /** @return array{0:list<int|string>,1:string} */ public static function sqlBooleanSearch(string $alias, FreshRSS_BooleanSearch $filters, int $level = 0): array { $search = ''; $values = []; @@ -1104,7 +1108,7 @@ SQL; /** * @param 'ASC'|'DESC' $order - * @return array{0:array<int|string>,1:string} + * @return array{0:list<int|string>,1:string} * @throws FreshRSS_EntriesGetter_Exception */ protected function sqlListEntriesWhere(string $alias = '', ?FreshRSS_BooleanSearch $filters = null, @@ -1173,7 +1177,7 @@ SQL; * @phpstan-param 'a'|'A'|'i'|'s'|'S'|'c'|'f'|'t'|'T'|'ST'|'Z' $type * @param int $id category/feed/tag ID * @param 'ASC'|'DESC' $order - * @return array{0:array<int|string>,1:string} + * @return array{0:list<int|string>,1:string} * @throws FreshRSS_EntriesGetter_Exception */ private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, @@ -1269,6 +1273,7 @@ SQL; return $stm; } else { $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min); } @@ -1347,7 +1352,7 @@ SQL; * @phpstan-param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST'|'Z' $type * @param int $id category/feed/tag ID * @param 'ASC'|'DESC' $order - * @return array<numeric-string>|null + * @return list<numeric-string>|null * @throws FreshRSS_EntriesGetter_Exception */ public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, @@ -1356,7 +1361,8 @@ SQL; [$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters); $stm = $this->pdo->prepare($sql); if ($stm !== false && $stm->execute($values) && ($res = $stm->fetchAll(PDO::FETCH_COLUMN, 0)) !== false) { - /** @var array<numeric-string> $res */ + $res = array_map('strval', $res); + /** @var list<numeric-string> $res */ return $res; } $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); @@ -1366,7 +1372,7 @@ SQL; /** * @param array<string> $guids - * @return array<string>|false + * @return array<string,string>|false */ public function listHashForFeedGuids(int $id_feed, array $guids): array|false { $result = []; @@ -1376,7 +1382,7 @@ SQL; // Split a query with too many variables parameters $guidsChunks = array_chunk($guids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER); foreach ($guidsChunks as $guidsChunk) { - $result += $this->listHashForFeedGuids($id_feed, $guidsChunk); + $result += $this->listHashForFeedGuids($id_feed, $guidsChunk) ?: []; } return $result; } @@ -1394,9 +1400,6 @@ SQL; return $result; } else { $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); - if ($this->autoUpdateDb($info)) { - return $this->listHashForFeedGuids($id_feed, $guids); - } Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info) . ' while querying feed ' . $id_feed); return false; @@ -1430,9 +1433,6 @@ SQL; return $stm->rowCount(); } else { $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); - if ($this->autoUpdateDb($info)) { - return $this->updateLastSeen($id_feed, $guids); - } Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info) . ' while updating feed ' . $id_feed); return false; diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php index fe157308c..1a5266bbd 100644 --- a/app/Models/EntryDAOPGSQL.php +++ b/app/Models/EntryDAOPGSQL.php @@ -49,7 +49,7 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite { // Nothing to do for PostgreSQL } - /** @param array<string|int> $errorInfo */ + /** @param array{0:string,1:int,2:string} $errorInfo */ #[\Override] protected function autoUpdateDb(array $errorInfo): bool { if (isset($errorInfo[0])) { diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index 7cf6eb202..5734ec3b3 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -49,7 +49,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { ); } - /** @param array<string|int> $errorInfo */ + /** @param array{0:string,1:int,2:string} $errorInfo */ #[\Override] protected function autoUpdateDb(array $errorInfo): bool { if (($tableInfo = $this->pdo->query("PRAGMA table_info('entry')")) !== false) { diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 841749312..645dbcf3c 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -122,13 +122,13 @@ class FreshRSS_Feed extends Minz_Model { } /** - * @return array<FreshRSS_Entry>|null + * @return list<FreshRSS_Entry>|null * @deprecated */ public function entries(): ?array { - Minz_Log::warning(__method__ . ' is deprecated since FreshRSS 1.16.1!'); + Minz_Log::warning(__METHOD__ . ' is deprecated since FreshRSS 1.16.1!'); $simplePie = $this->load(false, true); - return $simplePie == null ? [] : iterator_to_array($this->loadEntries($simplePie)); + return $simplePie == null ? [] : array_values(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) ?? ''); @@ -479,7 +479,7 @@ class FreshRSS_Feed extends Minz_Model { * @param float $invalidGuidsTolerance (default 0.05) The maximum ratio (rounded) of invalid GUIDs to tolerate before degrading the unicity criteria. * Example for 0.05 (5% rounded): tolerate 0 invalid GUIDs for up to 9 articles, 1 for 10, 2 for 30, 3 for 50, 4 for 70, 5 for 90, 6 for 110, etc. * The default value of 5% rounded was chosen to allow 1 invalid GUID for feeds of 10 articles, which is a frequently observed amount of articles. - * @return array<string> + * @return list<string> */ public function loadGuids(\SimplePie\SimplePie $simplePie, float $invalidGuidsTolerance = 0.05): array { $invalidGuids = 0; @@ -1077,13 +1077,13 @@ class FreshRSS_Feed extends Minz_Model { $hubFilename = $path . '/!hub.json'; if (($hubFile = @file_get_contents($hubFilename)) != false) { $hubJson = json_decode($hubFile, true); - if (!is_array($hubJson) || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) { + if (!is_array($hubJson) || empty($hubJson['key']) || !is_string($hubJson['key']) || !ctype_xdigit($hubJson['key'])) { $text = 'Invalid JSON for WebSub: ' . $this->url; Minz_Log::warning($text); Minz_Log::warning($text, PSHB_LOG); return false; } - if ((!empty($hubJson['lease_end'])) && ($hubJson['lease_end'] < (time() + (3600 * 23)))) { //TODO: Make a better policy + if (!empty($hubJson['lease_end']) && is_int($hubJson['lease_end']) && $hubJson['lease_end'] < (time() + (3600 * 23))) { //TODO: Make a better policy $text = 'WebSub lease ends at ' . date('c', empty($hubJson['lease_end']) ? time() : $hubJson['lease_end']) . ' and needs renewal: ' . $this->url; @@ -1131,7 +1131,8 @@ class FreshRSS_Feed extends Minz_Model { return false; } $hubJson = json_decode($hubFile, true); - if (!is_array($hubJson) || empty($hubJson['key']) || !ctype_xdigit($hubJson['key']) || empty($hubJson['hub'])) { + if (!is_array($hubJson) || empty($hubJson['key']) || !is_string($hubJson['key']) || !ctype_xdigit($hubJson['key']) || + empty($hubJson['hub']) || !is_string($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 fa52838ca..676b93b7f 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -7,18 +7,18 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { if ($this->pdo->inTransaction()) { $this->pdo->commit(); } - Minz_Log::warning(__method__ . ': ' . $name); + Minz_Log::warning(__METHOD__ . ': ' . $name); try { if ($name === 'kind') { //v1.20.0 return $this->pdo->exec('ALTER TABLE `_feed` ADD COLUMN kind SMALLINT DEFAULT 0') !== false; } } catch (Exception $e) { - Minz_Log::error(__method__ . ' error: ' . $e->getMessage()); + Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage()); } return false; } - /** @param array<int|string> $errorInfo */ + /** @param array{0:string,1:int,2:string} $errorInfo */ protected function autoUpdateDb(array $errorInfo): bool { if (isset($errorInfo[0])) { if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) { @@ -34,8 +34,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { } /** - * @param array{'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|array<string|mixed>} $valuesTmp + * @param array{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|array<string|mixed>} $valuesTmp */ public function addFeed(array $valuesTmp): int|false { $sql = 'INSERT INTO `_feed` (url, kind, category, name, website, description, `lastUpdate`, priority, `pathEntries`, `httpAuth`, error, ttl, attributes) @@ -72,6 +72,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return $feedId === false ? false : (int)$feedId; } else { $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->addFeed($valuesTmp); } @@ -177,6 +178,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { return true; } else { $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->updateFeed($id, $originalValues); } @@ -290,8 +292,8 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { } } - /** @return Traversable<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}> */ + /** @return Traversable<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}> */ public function selectAll(): Traversable { $sql = <<<'SQL' SELECT id, url, kind, category, name, website, description, `lastUpdate`, @@ -301,16 +303,17 @@ SQL; $stm = $this->pdo->query($sql); if ($stm !== false) { 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 */ + /** @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; } } else { $info = $this->pdo->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { yield from $this->selectAll(); } else { - Minz_Log::error(__method__ . ' error: ' . json_encode($info)); + Minz_Log::error(__METHOD__ . ' error: ' . json_encode($info)); } } } @@ -318,40 +321,34 @@ SQL; public function searchById(int $id): ?FreshRSS_Feed { $sql = 'SELECT * FROM `_feed` WHERE id=:id'; $res = $this->fetchAssoc($sql, [':id' => $id]); - if ($res == null) { + if (!is_array($res)) { return null; } - /** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int, - * 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */ - $feeds = self::daoToFeeds($res); - return $feeds[$id] ?? null; + $feeds = self::daoToFeeds($res); // @phpstan-ignore argument.type + return $feeds[0] ?? null; } public function searchByUrl(string $url): ?FreshRSS_Feed { $sql = 'SELECT * FROM `_feed` WHERE url=:url'; $res = $this->fetchAssoc($sql, [':url' => $url]); - /** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int, - * 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */ - return empty($res[0]) ? null : (current(self::daoToFeeds($res)) ?: null); + return empty($res[0]) ? null : (current(self::daoToFeeds($res)) ?: null); // @phpstan-ignore argument.type } - /** @return array<int> */ + /** @return list<int> */ public function listFeedsIds(): array { $sql = 'SELECT id FROM `_feed`'; - /** @var array<int> $res */ + /** @var list<int> $res */ $res = $this->fetchColumn($sql, 0) ?? []; return $res; } /** - * @return array<int,FreshRSS_Feed> + * @return list<FreshRSS_Feed> */ public function listFeeds(): array { $sql = 'SELECT * FROM `_feed` ORDER BY name'; $res = $this->fetchAssoc($sql); - /** @var array<array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int, - * 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'ttl':int,'attributes':string}>|null $res */ - return $res == null ? [] : self::daoToFeeds($res); + return $res == null ? [] : self::daoToFeeds($res); // @phpstan-ignore argument.type } /** @return array<string,string> */ @@ -363,7 +360,7 @@ SQL; $sql .= 'WHERE id_feed=' . intval($id_feed); } $res = $this->fetchAssoc($sql); - /** @var array<array{'id_feed':int,'newest_item_us':string}>|null $res */ + /** @var list<array{'id_feed':int,'newest_item_us':string}>|null $res */ if ($res == null) { return []; } @@ -376,7 +373,7 @@ SQL; /** * @param int $defaultCacheDuration Use -1 to return all feeds, without filtering them by TTL. - * @return array<int,FreshRSS_Feed> + * @return list<FreshRSS_Feed> */ public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0): array { $sql = 'SELECT id, url, kind, category, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes, `cache_nbEntries`, `cache_nbUnreads` ' @@ -391,6 +388,7 @@ SQL; return self::daoToFeeds($stm->fetchAll(PDO::FETCH_ASSOC)); } else { $info = $this->pdo->errorInfo(); + /** @var array{0:string,1:int,2:string} $info */ if ($this->autoUpdateDb($info)) { return $this->listFeedsOrderUpdate($defaultCacheDuration, $limit); } @@ -399,19 +397,19 @@ SQL; } } - /** @return array<int,string> */ + /** @return list<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<int,string> $res */ + /** @var list<string> $res */ return $res; } /** * @param bool|null $muted to include only muted feeds * @param bool|null $errored to include only errored feeds - * @return array<int,FreshRSS_Feed> + * @return list<FreshRSS_Feed> */ public function listByCategory(int $cat, ?bool $muted = null, ?bool $errored = null): array { $sql = 'SELECT * FROM `_feed` WHERE category=:category'; @@ -422,18 +420,11 @@ SQL; $sql .= ' AND error <> 0'; } $res = $this->fetchAssoc($sql, [':category' => $cat]); - if ($res == null) { + if (!is_array($res)) { return []; } - - /** - * @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int, - * 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res - */ - $feeds = self::daoToFeeds($res); - - uasort($feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name())); - + $feeds = self::daoToFeeds($res); // @phpstan-ignore argument.type + usort($feeds, static fn(FreshRSS_Feed $a, FreshRSS_Feed $b) => strnatcasecmp($a->name(), $b->name())); return $feeds; } @@ -576,23 +567,19 @@ SQL; } /** - * @param array<int,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,'cache_nbUnreads'?:int,'cache_nbEntries'?:int}> $listDAO - * @return array<int,FreshRSS_Feed> + * @param array<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,cache_nbUnreads?:int,cache_nbEntries?:int}> $listDAO + * @return list<FreshRSS_Feed> */ public static function daoToFeeds(array $listDAO, ?int $catID = null): array { $list = []; - foreach ($listDAO as $key => $dao) { - FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'category', 'lastUpdate', 'priority', 'error', 'ttl', 'cache_nbUnreads', 'cache_nbEntries']); - if (!isset($dao['name'])) { + foreach ($listDAO as $dao) { + if (!is_string($dao['name'] ?? null)) { continue; } - if (isset($dao['id'])) { - $key = (int)$dao['id']; - } if ($catID === null) { - $category = $dao['category'] ?? 0; + $category = is_numeric($dao['category'] ?? null) ? (int)$dao['category'] : 0; } else { $category = $catID; } @@ -615,7 +602,7 @@ SQL; if (isset($dao['id'])) { $myFeed->_id($dao['id']); } - $list[$key] = $myFeed; + $list[] = $myFeed; } return $list; diff --git a/app/Models/FeedDAOSQLite.php b/app/Models/FeedDAOSQLite.php index 5833a7985..42915a493 100644 --- a/app/Models/FeedDAOSQLite.php +++ b/app/Models/FeedDAOSQLite.php @@ -3,7 +3,7 @@ declare(strict_types=1); class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO { - /** @param array<int|string> $errorInfo */ + /** @param array{0:string,1:int,2:string} $errorInfo */ #[\Override] protected function autoUpdateDb(array $errorInfo): bool { if (($tableInfo = $this->pdo->query("PRAGMA table_info('feed')")) !== false) { diff --git a/app/Models/FilterAction.php b/app/Models/FilterAction.php index eb8ea8502..56c182904 100644 --- a/app/Models/FilterAction.php +++ b/app/Models/FilterAction.php @@ -3,7 +3,7 @@ declare(strict_types=1); class FreshRSS_FilterAction { - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $actions = null; /** @param array<string> $actions */ @@ -15,7 +15,7 @@ class FreshRSS_FilterAction { return $this->booleanSearch; } - /** @return array<string> */ + /** @return list<string> */ public function actions(): array { return $this->actions ?? []; } @@ -23,7 +23,7 @@ class FreshRSS_FilterAction { /** @param array<string> $actions */ public function _actions(?array $actions): void { if (is_array($actions)) { - $this->actions = array_unique($actions); + $this->actions = array_values(array_unique($actions)); } else { $this->actions = null; } @@ -42,7 +42,8 @@ class FreshRSS_FilterAction { /** @param array|mixed|null $json */ public static function fromJSON($json): ?FreshRSS_FilterAction { - if (is_array($json) && !empty($json['search']) && !empty($json['actions']) && is_array($json['actions'])) { + if (is_array($json) && !empty($json['search']) && is_string($json['search']) && + !empty($json['actions']) && is_array($json['actions']) && is_array_values_string($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 9b7ee66d4..3d8257e34 100644 --- a/app/Models/FilterActionsTrait.php +++ b/app/Models/FilterActionsTrait.php @@ -6,11 +6,11 @@ declare(strict_types=1); */ trait FreshRSS_FilterActionsTrait { - /** @var array<FreshRSS_FilterAction>|null $filterActions */ + /** @var list<FreshRSS_FilterAction>|null $filterActions */ private ?array $filterActions = null; /** - * @return array<FreshRSS_FilterAction> + * @return list<FreshRSS_FilterAction> */ private function filterActions(): array { if (empty($this->filterActions)) { @@ -30,7 +30,7 @@ trait FreshRSS_FilterActionsTrait { * @param array<FreshRSS_FilterAction>|null $filterActions */ private function _filterActions(?array $filterActions): void { - $this->filterActions = $filterActions; + $this->filterActions = is_array($filterActions) ? array_values($filterActions) : null; if ($this->filterActions !== null && !empty($this->filterActions)) { $this->_attribute('filters', array_map( static fn(?FreshRSS_FilterAction $af) => $af == null ? null : $af->toJSON(), @@ -40,7 +40,7 @@ trait FreshRSS_FilterActionsTrait { } } - /** @return array<FreshRSS_BooleanSearch> */ + /** @return list<FreshRSS_BooleanSearch> */ public function filtersAction(string $action): array { $action = trim($action); if ($action == '') { @@ -121,6 +121,7 @@ trait FreshRSS_FilterActionsTrait { /** * @param bool $applyLabel Parameter by reference, which will be set to true if the callers needs to apply a label to the article entry. + * @param-out bool $applyLabel */ public function applyFilterActions(FreshRSS_Entry $entry, ?bool &$applyLabel = null): void { $applyLabel = false; diff --git a/app/Models/FormAuth.php b/app/Models/FormAuth.php index 54b468da9..1da03f6d2 100644 --- a/app/Models/FormAuth.php +++ b/app/Models/FormAuth.php @@ -14,7 +14,7 @@ class FreshRSS_FormAuth { return password_verify($nonce . $hash, $challenge); } - /** @return array<string> */ + /** @return list<string> */ public static function getCredentialsFromCookie(): array { $token = Minz_Session::getLongTermCookie('FreshRSS_login'); if (!ctype_alnum($token)) { diff --git a/app/Models/LogDAO.php b/app/Models/LogDAO.php index 3916d2a1e..44cce3ecd 100644 --- a/app/Models/LogDAO.php +++ b/app/Models/LogDAO.php @@ -9,7 +9,7 @@ final class FreshRSS_LogDAO { return USERS_PATH . '/' . (Minz_User::name() ?? Minz_User::INTERNAL_USER) . '/' . $logFileName; } - /** @return array<FreshRSS_Log> */ + /** @return list<FreshRSS_Log> */ public static function lines(?string $logFileName = null): array { $logs = []; $handle = @fopen(self::logPath($logFileName), 'r'); diff --git a/app/Models/ReadingMode.php b/app/Models/ReadingMode.php index 60c7e76e1..01edc6a4c 100644 --- a/app/Models/ReadingMode.php +++ b/app/Models/ReadingMode.php @@ -59,7 +59,7 @@ class FreshRSS_ReadingMode { } /** - * @return array<FreshRSS_ReadingMode> the built-in reading modes + * @return list<FreshRSS_ReadingMode> the built-in reading modes */ public static function getReadingModes(): array { $actualView = Minz_Request::actionName(); diff --git a/app/Models/Search.php b/app/Models/Search.php index a887ec2f7..3eb8b422a 100644 --- a/app/Models/Search.php +++ b/app/Models/Search.php @@ -17,17 +17,17 @@ class FreshRSS_Search implements \Stringable { private string $raw_input = ''; // The following properties are extracted from the raw input - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $entry_ids = null; - /** @var array<int>|null */ + /** @var list<int>|null */ private ?array $feed_ids = null; - /** @var array<int>|'*'|null */ + /** @var list<int>|'*'|null */ private $label_ids = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $label_names = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $intitle = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $intitle_regex = null; /** @var int|false|null */ private $min_date = null; @@ -37,34 +37,34 @@ class FreshRSS_Search implements \Stringable { private $min_pubdate = null; /** @var int|false|null */ private $max_pubdate = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $inurl = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $inurl_regex = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $author = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $author_regex = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $tags = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $tags_regex = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $search = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $search_regex = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_entry_ids = null; - /** @var array<int>|null */ + /** @var list<int>|null */ private ?array $not_feed_ids = null; - /** @var array<int>|'*'|null */ + /** @var list<int>|'*'|null */ private $not_label_ids = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_label_names = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_intitle = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_intitle_regex = null; /** @var int|false|null */ private $not_min_date = null; @@ -74,21 +74,21 @@ class FreshRSS_Search implements \Stringable { private $not_min_pubdate = null; /** @var int|false|null */ private $not_max_pubdate = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_inurl = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_inurl_regex = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_author = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_author_regex = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_tags = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_tags_regex = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_search = null; - /** @var array<string>|null */ + /** @var list<string>|null */ private ?array $not_search_regex = null; public function __construct(string $input) { @@ -137,54 +137,54 @@ class FreshRSS_Search implements \Stringable { return $this->raw_input; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getEntryIds(): ?array { return $this->entry_ids; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotEntryIds(): ?array { return $this->not_entry_ids; } - /** @return array<int>|null */ + /** @return list<int>|null */ public function getFeedIds(): ?array { return $this->feed_ids; } - /** @return array<int>|null */ + /** @return list<int>|null */ public function getNotFeedIds(): ?array { return $this->not_feed_ids; } - /** @return array<int>|'*'|null */ + /** @return list<int>|'*'|null */ public function getLabelIds(): array|string|null { return $this->label_ids; } - /** @return array<int>|'*'|null */ + /** @return list<int>|'*'|null */ public function getNotLabelIds(): array|string|null { return $this->not_label_ids; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getLabelNames(): ?array { return $this->label_names; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotLabelNames(): ?array { return $this->not_label_names; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getIntitle(): ?array { return $this->intitle; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getIntitleRegex(): ?array { return $this->intitle_regex; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotIntitle(): ?array { return $this->not_intitle; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotIntitleRegex(): ?array { return $this->not_intitle_regex; } @@ -223,90 +223,90 @@ class FreshRSS_Search implements \Stringable { return $this->not_max_pubdate ?: null; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getInurl(): ?array { return $this->inurl; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getInurlRegex(): ?array { return $this->inurl_regex; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotInurl(): ?array { return $this->not_inurl; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotInurlRegex(): ?array { return $this->not_inurl_regex; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getAuthor(): ?array { return $this->author; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getAuthorRegex(): ?array { return $this->author_regex; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotAuthor(): ?array { return $this->not_author; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotAuthorRegex(): ?array { return $this->not_author_regex; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getTags(): ?array { return $this->tags; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getTagsRegex(): ?array { return $this->tags_regex; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotTags(): ?array { return $this->not_tags; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotTagsRegex(): ?array { return $this->not_tags_regex; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getSearch(): ?array { return $this->search; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getSearchRegex(): ?array { return $this->search_regex; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotSearch(): ?array { return $this->not_search; } - /** @return array<string>|null */ + /** @return list<string>|null */ public function getNotSearchRegex(): ?array { return $this->not_search_regex; } /** - * @param array<string>|null $anArray - * @return array<string> + * @param list<string>|null $anArray + * @return list<string> */ private static function removeEmptyValues(?array $anArray): array { - return empty($anArray) ? [] : array_filter($anArray, static fn(string $value) => $value !== ''); + return empty($anArray) ? [] : array_values(array_filter($anArray, static fn(string $value) => $value !== '')); } /** - * @param array<string>|string $value - * @return ($value is array ? array<string> : string) + * @param list<string>|string $value + * @return ($value is string ? string : list<string>) */ - private static function decodeSpaces($value): array|string { + private static function decodeSpaces(array|string $value): array|string { if (is_array($value)) { - for ($i = count($value) - 1; $i >= 0; $i--) { - $value[$i] = self::decodeSpaces($value[$i]); + foreach ($value as &$val) { + $val = self::decodeSpaces($val); } } else { $value = trim(str_replace('+', ' ', $value)); @@ -315,8 +315,8 @@ class FreshRSS_Search implements \Stringable { } /** - * @param array<string> $strings - * @return array<string> + * @param list<string> $strings + * @return list<string> */ private static function htmlspecialchars_decodes(array $strings): array { return array_map(static fn(string $s) => htmlspecialchars_decode($s, ENT_QUOTES), $strings); @@ -365,7 +365,7 @@ class FreshRSS_Search implements \Stringable { foreach ($ids_lists as $ids_list) { $feed_ids = explode(',', $ids_list); $feed_ids = self::removeEmptyValues($feed_ids); - /** @var array<int> $feed_ids */ + /** @var list<int> $feed_ids */ $feed_ids = array_map('intval', $feed_ids); if (!empty($feed_ids)) { $this->feed_ids = array_merge($this->feed_ids, $feed_ids); @@ -383,7 +383,7 @@ class FreshRSS_Search implements \Stringable { foreach ($ids_lists as $ids_list) { $feed_ids = explode(',', $ids_list); $feed_ids = self::removeEmptyValues($feed_ids); - /** @var array<int> $feed_ids */ + /** @var list<int> $feed_ids */ $feed_ids = array_map('intval', $feed_ids); if (!empty($feed_ids)) { $this->not_feed_ids = array_merge($this->not_feed_ids, $feed_ids); @@ -408,7 +408,7 @@ class FreshRSS_Search implements \Stringable { } $label_ids = explode(',', $ids_list); $label_ids = self::removeEmptyValues($label_ids); - /** @var array<int> $label_ids */ + /** @var list<int> $label_ids */ $label_ids = array_map('intval', $label_ids); if (!empty($label_ids)) { $this->label_ids = array_merge($this->label_ids, $label_ids); @@ -430,7 +430,7 @@ class FreshRSS_Search implements \Stringable { } $label_ids = explode(',', $ids_list); $label_ids = self::removeEmptyValues($label_ids); - /** @var array<int> $label_ids */ + /** @var list<int> $label_ids */ $label_ids = array_map('intval', $label_ids); if (!empty($label_ids)) { $this->not_label_ids = array_merge($this->not_label_ids, $label_ids); diff --git a/app/Models/Share.php b/app/Models/Share.php index 847127466..140ca0eca 100644 --- a/app/Models/Share.php +++ b/app/Models/Share.php @@ -13,8 +13,8 @@ class FreshRSS_Share { /** * Register a new sharing option. - * @param array{'type':string,'url':string,'transform'?:array<callable>|array<string,array<callable>>,'field'?:string,'help'?:string,'form'?:'simple'|'advanced', - * 'method'?:'GET'|'POST','HTMLtag'?:'button','deprecated'?:bool} $share_options is an array defining the share option. + * @param array{type:string,url:string,transform?:array<callable>|array<string,array<callable>>,field?:string,help?:string,form?:'simple'|'advanced', + * method?:'GET'|'POST',HTMLtag?:'button',deprecated?:bool} $share_options is an array defining the share option. */ public static function register(array $share_options): void { $type = $share_options['type']; @@ -46,7 +46,12 @@ class FreshRSS_Share { } foreach ($shares_from_file as $share_type => $share_options) { + if (!is_array($share_options)) { + continue; + } $share_options['type'] = $share_type; + /** @var array{type:string,url:string,transform?:array<callable>|array<string,array<callable>>,field?:string,help?:string,form?:'simple'|'advanced', + * method?:'GET'|'POST',HTMLtag?:'button',deprecated?:bool} $share_options */ self::register($share_options); } @@ -233,8 +238,8 @@ class FreshRSS_Share { '~LINK~', ]; $replaces = [ - $this->id(), - $this->base_url, + $this->id() ?? '', + $this->base_url ?? '', $this->title(), $this->link(), ]; @@ -298,7 +303,10 @@ class FreshRSS_Share { } foreach ($transform as $action) { - $data = call_user_func($action, $data); + $return = call_user_func($action, $data); + if (is_string($return)) { + $data = $return; + } } return $data; @@ -307,7 +315,7 @@ class FreshRSS_Share { /** * Get the list of transformations for the given attribute. * @param string $attr the attribute of which we want the transformations. - * @return array<callable> containing a list of transformations to apply. + * @return list<callable> containing a list of transformations to apply. */ private function getTransform(string $attr): array { if (array_key_exists($attr, $this->transforms)) { diff --git a/app/Models/StatsDAO.php b/app/Models/StatsDAO.php index 6782bd7ee..d098b81a4 100644 --- a/app/Models/StatsDAO.php +++ b/app/Models/StatsDAO.php @@ -29,7 +29,7 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo { * - unread entries * - favorite entries * - * @return array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false + * @return array{total:int,count_unreads:int,count_reads:int,count_favorites:int}|false */ public function calculateEntryRepartitionPerFeed(?int $feed = null, bool $only_main = false): array|false { $filter = ''; @@ -49,10 +49,9 @@ WHERE e.id_feed = f.id {$filter} SQL; $res = $this->fetchAssoc($sql); - if (!empty($res[0])) { + if (is_array($res) && !empty($res[0])) { $dao = $res[0]; - /** @var array<array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}> $res */ - FreshRSS_DatabaseDAO::pdoInt($dao, ['total', 'count_unreads', 'count_reads', 'count_favorites']); + /** @var array{total:int,count_unreads:int,count_reads:int,count_favorites:int} $dao */ return $dao; } return false; @@ -78,10 +77,10 @@ GROUP BY day ORDER BY day ASC SQL; $res = $this->fetchAssoc($sql); - if ($res == false) { + if (!is_array($res)) { return []; } - /** @var array<array{'day':int,'count':int}> $res */ + /** @var list<array{day:int,count:int}> $res */ foreach ($res as $value) { $count[(int)($value['day'])] = (int)($value['count']); } @@ -123,7 +122,6 @@ SQL; return $monthRepartition; } - /** * Calculates the number of article per period per feed * @param string $period format string to use for grouping @@ -228,7 +226,7 @@ SQL; /** * Calculates feed count per category. - * @return array<array{'label':string,'data':int}> + * @return list<array{'label':string,'data':int}> */ public function calculateFeedByCategory(): array { $sql = <<<SQL @@ -239,14 +237,14 @@ WHERE c.id = f.category GROUP BY label ORDER BY data DESC SQL; - /** @var array<array{'label':string,'data':int}>|null @res */ + /** @var list<array{'label':string,'data':int}>|null @res */ $res = $this->fetchAssoc($sql); return $res == null ? [] : $res; } /** * Calculates entry count per category. - * @return array<array{'label':string,'data':int}> + * @return list<array{'label':string,'data':int}> */ public function calculateEntryByCategory(): array { $sql = <<<SQL @@ -259,13 +257,13 @@ GROUP BY label ORDER BY data DESC SQL; $res = $this->fetchAssoc($sql); - /** @var array<array{'label':string,'data':int}>|null $res */ + /** @var list<array{'label':string,'data':int}>|null $res */ return $res == null ? [] : $res; } /** * Calculates the 10 top feeds based on their number of entries - * @return array<array{'id':int,'name':string,'category':string,'count':int}> + * @return list<array{'id':int,'name':string,'category':string,'count':int}> */ public function calculateTopFeed(): array { $sql = <<<SQL @@ -281,11 +279,8 @@ ORDER BY count DESC LIMIT 10 SQL; $res = $this->fetchAssoc($sql); - /** @var array<array{'id':int,'name':string,'category':string,'count':int}>|null $res */ + /** @var list<array{'id':int,'name':string,'category':string,'count':int}>|null $res */ if (is_array($res)) { - foreach ($res as &$dao) { - FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'count']); - } return $res; } return []; @@ -293,7 +288,7 @@ SQL; /** * Calculates the last publication date for each feed - * @return array<array{'id':int,'name':string,'last_date':int,'nb_articles':int}> + * @return list<array{'id':int,'name':string,'last_date':int,'nb_articles':int}> */ public function calculateFeedLastDate(): array { $sql = <<<SQL @@ -307,11 +302,8 @@ GROUP BY f.id ORDER BY name SQL; $res = $this->fetchAssoc($sql); - /** @var array<array{'id':int,'name':string,'last_date':int,'nb_articles':int}>|null $res */ + /** @var list<array{'id':int,'name':string,'last_date':int,'nb_articles':int}>|null $res */ if (is_array($res)) { - foreach ($res as &$dao) { - FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'last_date', 'nb_articles']); - } return $res; } return []; @@ -319,7 +311,7 @@ SQL; /** * Gets days ready for graphs - * @return array<string> + * @return list<string> */ public function getDays(): array { return $this->convertToTranslatedJson([ @@ -335,7 +327,7 @@ SQL; /** * Gets months ready for graphs - * @return array<string> + * @return list<string> */ public function getMonths(): array { return $this->convertToTranslatedJson([ @@ -356,8 +348,8 @@ SQL; /** * Translates array content - * @param array<string> $data - * @return array<string> + * @param list<string> $data + * @return list<string> */ private function convertToTranslatedJson(array $data = []): array { $translated = array_map(static fn(string $a) => _t('gen.date.' . $a), $data); diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php index e26a73a65..d66899e8f 100644 --- a/app/Models/TagDAO.php +++ b/app/Models/TagDAO.php @@ -117,7 +117,7 @@ SQL; } } - /** @return Traversable<array{'id':int,'name':string,'attributes'?:array<string,mixed>}> */ + /** @return Traversable<array{id:int,name:string,attributes?:array<string,mixed>}> */ public function selectAll(): Traversable { $sql = 'SELECT id, name, attributes FROM `_tag`'; $stm = $this->pdo->query($sql); @@ -126,12 +126,12 @@ SQL; return; } while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { - /** @var array{'id':int,'name':string,'attributes'?:array<string,mixed>} $row */ + /** @var array{id:int,name:string,attributes?:array<string,mixed>} $row */ yield $row; } } - /** @return Traversable<array{'id_tag':int,'id_entry':string}> */ + /** @return Traversable<array{id_tag:int,id_entry:int|numeric-string}> */ public function selectEntryTag(): Traversable { $sql = 'SELECT id_tag, id_entry FROM `_entrytag`'; $stm = $this->pdo->query($sql); @@ -140,9 +140,8 @@ SQL; return; } while ($row = $stm->fetch(PDO::FETCH_ASSOC)) { - FreshRSS_DatabaseDAO::pdoInt($row, ['id_tag']); - FreshRSS_DatabaseDAO::pdoString($row, ['id_entry']); - yield $row; + /** @var array{id_tag:int,id_entry:int|numeric-string}> $row */ + yield $row; // @phpstan-ignore generator.valueType } } @@ -173,17 +172,17 @@ SQL; public function searchById(int $id): ?FreshRSS_Tag { $res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE id=:id', [':id' => $id]); - /** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */ + /** @var list<array{id:int,name:string,attributes?:string}>|null $res */ return $res === null ? null : (current(self::daoToTags($res)) ?: null); } public function searchByName(string $name): ?FreshRSS_Tag { $res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE name=:name', [':name' => $name]); - /** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */ + /** @var list<array{id:int,name:string,attributes?:string}>|null $res */ return $res === null ? null : (current(self::daoToTags($res)) ?: null); } - /** @return array<int,FreshRSS_Tag>|false */ + /** @return list<FreshRSS_Tag>|false */ public function listTags(bool $precounts = false): array|false { if ($precounts) { $sql = <<<'SQL' @@ -291,16 +290,16 @@ SQL; } /** - * @param array<array{id_tag:int,id_entry:string}> $addLabels Labels to insert as batch + * @param iterable<array{id_tag:int,id_entry:numeric-string|int}> $addLabels Labels to insert as batch * @return int|false Number of new entries or false in case of error */ - public function tagEntries(array $addLabels): int|false { + public function tagEntries(iterable $addLabels): int|false { $hasValues = false; $sql = 'INSERT ' . $this->sqlIgnore() . ' INTO `_entrytag`(id_tag, id_entry) VALUES '; foreach ($addLabels as $addLabel) { $id_tag = (int)($addLabel['id_tag'] ?? 0); $id_entry = $addLabel['id_entry'] ?? ''; - if ($id_tag > 0 && ctype_digit($id_entry)) { + if ($id_tag > 0 && (is_int($id_entry) || ctype_digit($id_entry))) { $sql .= "({$id_tag},{$id_entry}),"; $hasValues = true; } @@ -320,7 +319,7 @@ SQL; } /** - * @return array<int,array{'id':int,'name':string,'id_entry':string,'checked':bool}>|false + * @return array<int,array{id:int,name:string,checked:bool}>|false */ public function getTagsForEntry(string $id_entry): array|false { $sql = <<<'SQL' @@ -347,8 +346,8 @@ SQL; } /** - * @param array<FreshRSS_Entry|numeric-string|array<string,string>> $entries - * @return array<array{'id_entry':string,'id_tag':int,'name':string}>|false + * @param list<FreshRSS_Entry|numeric-string> $entries + * @return list<array{id_entry:int|numeric-string,id_tag:int,name:string}>|false */ public function getTagsForEntries(array $entries): array|false { $sql = <<<'SQL' @@ -372,29 +371,16 @@ SQL; return $values; } $sql .= ' AND et.id_entry IN (' . str_repeat('?,', count($entries) - 1) . '?)'; - if (is_array($entries[0])) { - /** @var array<array<string,string>> $entries */ - foreach ($entries as $entry) { - if (!empty($entry['id'])) { - $values[] = $entry['id']; - } - } - } elseif (is_object($entries[0])) { - /** @var array<FreshRSS_Entry> $entries */ - foreach ($entries as $entry) { - $values[] = $entry->id(); - } - } else { - /** @var array<numeric-string> $entries */ - foreach ($entries as $entry) { - $values[] = $entry; - } + foreach ($entries as $entry) { + $values[] = is_object($entry) ? $entry->id() : $entry; } } $stm = $this->pdo->prepare($sql); if ($stm !== false && $stm->execute($values)) { - return $stm->fetchAll(PDO::FETCH_ASSOC); + $result = $stm->fetchAll(PDO::FETCH_ASSOC); + /** @var list<array{id_entry:int|numeric-string,id_tag:int,name:string}> $result; */ + return $result; } $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info)); @@ -404,7 +390,7 @@ SQL; /** * Produces an array: for each entry ID (prefixed by `e_`), associate a list of labels. * Used by API and by JSON export, to speed up queries (would be very expensive to perform a label look-up on each entry individually). - * @param array<FreshRSS_Entry|numeric-string> $entries the list of entries for which to retrieve the labels. + * @param list<FreshRSS_Entry|numeric-string> $entries the list of entries for which to retrieve the labels. * @return array<string,array<string>> An array of the shape `[e_id_entry => ["label 1", "label 2"]]` */ public function getEntryIdsTagNames(array $entries): array { @@ -421,8 +407,8 @@ SQL; } /** - * @param iterable<array{'id':int,'name':string,'attributes'?:string}> $listDAO - * @return array<int,FreshRSS_Tag> + * @param iterable<array{id:int,name:string,attributes?:string}> $listDAO + * @return list<FreshRSS_Tag> */ private static function daoToTags(iterable $listDAO): array { $list = []; @@ -438,7 +424,7 @@ SQL; if (isset($dao['unreads'])) { $tag->_nbUnread($dao['unreads']); } - $list[$tag->id()] = $tag; + $list[] = $tag; } return $list; } diff --git a/app/Models/Themes.php b/app/Models/Themes.php index 2a55a84db..cd66723bf 100644 --- a/app/Models/Themes.php +++ b/app/Models/Themes.php @@ -7,7 +7,7 @@ class FreshRSS_Themes extends Minz_Model { private static string $defaultIconsUrl = '/themes/icons/'; public static string $defaultTheme = 'Origine'; - /** @return array<string> */ + /** @return list<string> */ public static function getList(): array { return array_values(array_diff( scandir(PUBLIC_PATH . self::$themesUrl) ?: [], @@ -15,7 +15,7 @@ class FreshRSS_Themes extends Minz_Model { )); } - /** @return array<string,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 array<string,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}}> */ public static function get(): array { $themes_list = self::getList(); $list = []; @@ -29,7 +29,7 @@ class FreshRSS_Themes extends Minz_Model { } /** - * @return false|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 false|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}} */ public static function get_infos(string $theme_id): array|false { $theme_dir = PUBLIC_PATH . self::$themesUrl . $theme_id; @@ -38,13 +38,24 @@ class FreshRSS_Themes extends Minz_Model { if (file_exists($json_filename)) { $content = file_get_contents($json_filename) ?: ''; $res = json_decode($content, true); - 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; + if (is_array($res)) { + $result = [ + 'id' => $theme_id, + 'name' => is_string($res['name'] ?? null) ? $res['name'] : '', + 'author' => is_string($res['author'] ?? null) ? $res['author'] : '', + 'description' => is_string($res['description'] ?? null) ? $res['description'] : '', + 'version' => is_string($res['version'] ?? null) || is_numeric($res['version'] ?? null) ? $res['version'] : '0', + 'files' => is_array($res['files']) && is_array_values_string($res['files']) ? array_values($res['files']) : [], + 'theme-color' => is_string($res['theme-color'] ?? null) ? $res['theme-color'] : '', + ]; + if (empty($result['theme-color']) && is_array($res['theme-color'])) { + $result['theme-color'] = [ + 'dark' => is_string($res['theme-color']['dark'] ?? null) ? $res['theme-color']['dark'] : '', + 'light' => is_string($res['theme-color']['light'] ?? null) ? $res['theme-color']['light'] : '', + 'default' => is_string($res['theme-color']['default'] ?? null) ? $res['theme-color']['default'] : '', + ]; + } + return $result; } } } @@ -56,7 +67,7 @@ class FreshRSS_Themes extends Minz_Model { private static array $themeIcons; /** - * @return false|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 false|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}} */ public static function load(string $theme_id): array|false { $infos = self::get_infos($theme_id); diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php index 8c2129744..4d465bf67 100644 --- a/app/Models/UserConfiguration.php +++ b/app/Models/UserConfiguration.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** * @property string $apiPasswordHash - * @property array{'keep_period':string|false,'keep_max':int|false,'keep_min':int|false,'keep_favourites':bool,'keep_labels':bool,'keep_unreads':bool} $archiving + * @property array{keep_period:string|false,keep_max:int|false,keep_min:int|false,keep_favourites:bool,keep_labels:bool,keep_unreads:bool} $archiving * @property bool $auto_load_more * @property bool $auto_remove_article * @property bool $bottomline_date @@ -42,7 +42,7 @@ declare(strict_types=1); * @property bool $onread_jump_next * @property string $passwordHash * @property int $posts_per_page - * @property array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $queries + * @property array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string}> $queries * @property bool $reading_confirm * @property int $since_hours_posts_per_rss * @property bool $show_fav_unread @@ -51,7 +51,7 @@ declare(strict_types=1); * @property int $simplify_over_n_feeds * @property bool $show_nav_buttons * @property 'ASC'|'DESC' $sort_order - * @property array<string,array<string>> $sharing + * @property array<string,array<string,string>> $sharing * @property array<string,string> $shortcuts * @property bool $sides_close_article * @property bool $sticky_post @@ -94,8 +94,9 @@ final class FreshRSS_UserConfiguration extends Minz_Configuration { * @throws Minz_FileNotExistException */ public static function default(): FreshRSS_UserConfiguration { + /** @var FreshRSS_UserConfiguration|null $default_user_conf */ static $default_user_conf = null; - if ($default_user_conf == null) { + if ($default_user_conf === null) { $namespace = 'user_default'; FreshRSS_UserConfiguration::register($namespace, '_', FRESHRSS_PATH . '/config-user.default.php'); $default_user_conf = FreshRSS_UserConfiguration::get($namespace); diff --git a/app/Models/UserDAO.php b/app/Models/UserDAO.php index 5ae57dd65..4cbfa7412 100644 --- a/app/Models/UserDAO.php +++ b/app/Models/UserDAO.php @@ -8,6 +8,9 @@ class FreshRSS_UserDAO extends Minz_ModelPdo { try { $sql = $GLOBALS['SQL_CREATE_TABLES']; + if (!is_string($sql)) { + throw new Exception('SQL_CREATE_TABLES is not a string!'); + } $ok = $this->pdo->exec($sql) !== false; //Note: Only exec() can take multiple statements safely. } catch (Exception $e) { $ok = false; @@ -29,7 +32,11 @@ class FreshRSS_UserDAO extends Minz_ModelPdo { } require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php'); - $ok = $this->pdo->exec($GLOBALS['SQL_DROP_TABLES']) !== false; + $sql = $GLOBALS['SQL_DROP_TABLES']; + if (!is_string($sql)) { + throw new Exception('SQL_DROP_TABLES is not a string!'); + } + $ok = $this->pdo->exec($sql) !== false; if ($ok) { $this->close(); diff --git a/app/Models/View.php b/app/Models/View.php index 4ce837922..aad512a39 100644 --- a/app/Models/View.php +++ b/app/Models/View.php @@ -10,7 +10,7 @@ class FreshRSS_View extends Minz_View { public $callbackBeforeFeeds; /** @var callable */ public $callbackBeforePagination; - /** @var array<int,FreshRSS_Category> */ + /** @var list<FreshRSS_Category> */ public array $categories; public ?FreshRSS_Category $category = null; public ?FreshRSS_Tag $tag = null; @@ -19,12 +19,12 @@ class FreshRSS_View extends Minz_View { public $entries; public ?FreshRSS_Entry $entry = null; public ?FreshRSS_Feed $feed = null; - /** @var array<int,FreshRSS_Feed> */ + /** @var list<FreshRSS_Feed> */ public array $feeds; public int $nbUnreadTags; - /** @var array<int,FreshRSS_Tag> */ + /** @var list<FreshRSS_Tag> */ public array $tags; - /** @var array<int,array{'id':int,'name':string,'id_entry':string,'checked':bool}> */ + /** @var array<int,array{id:int,name:string,checked:bool}> */ public array $tagsForEntry; /** @var array<string,array<string>> */ public array $tagsForEntries; @@ -37,12 +37,12 @@ class FreshRSS_View extends Minz_View { public bool $signalError; // Manage users - /** @var array{'feed_count':int,'article_count':int,'database_size':int,'language':string,'mail_login':string,'enabled':bool,'is_admin':bool,'last_user_activity':string,'is_default':bool} */ + /** @var array{feed_count:int,article_count:int,database_size:int,language:string,mail_login:string,enabled:bool,is_admin:bool,last_user_activity:string,is_default:bool} */ public array $details; public bool $disable_aside; public bool $show_email_field; public string $username; - /** @var array<array{'language':string,'enabled':bool,'is_admin':bool,'enabled':bool,'article_count':int,'database_size':int,'last_user_activity':string,'mail_login':string,'feed_count':int,'is_default':bool}> */ + /** @var array<array{language:string,enabled:bool,is_admin:bool,enabled:bool,article_count:int,database_size:int,last_user_activity:string,mail_login:string,feed_count:int,is_default:bool}> */ public array $users; // Updates @@ -62,7 +62,7 @@ class FreshRSS_View extends Minz_View { public int $size_user; // Display - /** @var array<string,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}}> */ + /** @var array<string,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}}> */ public array $themes; // Shortcuts @@ -118,10 +118,10 @@ class FreshRSS_View extends Minz_View { public bool $selectorSuccess; // Extensions - /** @var array<array{'name':string,'author':string,'description':string,'version':string,'entrypoint':string,'type':'system'|'user','url':string,'method':string,'directory':string}> */ + /** @var array<array{name:string,author:string,description:string,version:string,entrypoint:string,type:'system'|'user',url:string,method:string,directory:string}> */ public array $available_extensions; public ?Minz_Extension $ext_details = null; - /** @var array{'system':array<Minz_Extension>,'user':array<Minz_Extension>} */ + /** @var array{system:array<Minz_Extension>,user:array<Minz_Extension>} */ public array $extension_list; public ?Minz_Extension $extension = null; /** @var array<string,string> */ diff --git a/app/Models/ViewJavascript.php b/app/Models/ViewJavascript.php index 2b3c87537..26280627f 100644 --- a/app/Models/ViewJavascript.php +++ b/app/Models/ViewJavascript.php @@ -3,11 +3,11 @@ declare(strict_types=1); final class FreshRSS_ViewJavascript extends FreshRSS_View { - /** @var array<int,FreshRSS_Category> */ + /** @var list<FreshRSS_Category> */ public array $categories; - /** @var array<int,FreshRSS_Feed> */ + /** @var list<FreshRSS_Feed> */ public array $feeds; - /** @var array<int,FreshRSS_Tag> */ + /** @var list<FreshRSS_Tag> */ public array $tags; public string $nonce; diff --git a/app/Models/ViewStats.php b/app/Models/ViewStats.php index 3810312db..e8e0a37bc 100644 --- a/app/Models/ViewStats.php +++ b/app/Models/ViewStats.php @@ -3,10 +3,10 @@ declare(strict_types=1); final class FreshRSS_ViewStats extends FreshRSS_View { - /** @var array<int,FreshRSS_Category> */ + /** @var list<FreshRSS_Category> */ public array $categories; public ?FreshRSS_Feed $feed = null; - /** @var array<int,FreshRSS_Feed> */ + /** @var list<FreshRSS_Feed> */ public array $feeds; public bool $displaySlider = false; @@ -14,7 +14,7 @@ final class FreshRSS_ViewStats extends FreshRSS_View { public float $averageDayOfWeek; public float $averageHour; public float $averageMonth; - /** @var array<string> */ + /** @var list<string> */ public array $days; /** @var array<string,array<int,int|string>> */ public array $entryByCategory; @@ -30,11 +30,11 @@ final class FreshRSS_ViewStats extends FreshRSS_View { public array $last30DaysLabel; /** @var array<int,string> */ public array $last30DaysLabels; - /** @var array<string,string> */ + /** @var list<string> */ public array $months; - /** @var array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false */ + /** @var array{total:int,count_unreads:int,count_reads:int,count_favorites:int}|false */ public $repartition; - /** @var array{'main_stream':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false,'all_feeds':array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}|false} */ + /** @var array{main_stream:array{total:int,count_unreads:int,count_reads:int,count_favorites:int}|false,all_feeds:array{total:int,count_unreads:int,count_reads:int,count_favorites:int}|false} */ public array $repartitions; /** @var array<int,int> */ public array $repartitionDayOfWeek; @@ -42,6 +42,6 @@ final class FreshRSS_ViewStats extends FreshRSS_View { public array $repartitionHour; /** @var array<int,int> */ public array $repartitionMonth; - /** @var array<array{'id':int,'name':string,'category':string,'count':int}> */ + /** @var list<array{id:int,name:string,category:string,count:int}> */ public array $topFeed; } |
