diff options
author | Alexandre Alapetite <alexandre@alapetite.fr> | 2024-06-09 20:32:12 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-09 20:32:12 +0200 |
commit | 5b28a35003a015e29770094932157f13a3f7f5c0 (patch) | |
tree | 4cbe4100379ca0d148115ad31f5a1c0c95ff7c80 | |
parent | e98c57841b843ed881f06ce6ed1c9c89942c27b8 (diff) | |
download | freshrss-5b28a35003a015e29770094932157f13a3f7f5c0.tar.gz freshrss-5b28a35003a015e29770094932157f13a3f7f5c0.zip |
Pass PHPStan level 9 (#6544)
* More PHPStan
* More, passing
* 4 more files
* Update to PHPStan 1.11.4
Needed for fixed bug: Consider numeric-string types after string concat
https://github.com/phpstan/phpstan/releases/tag/1.11.4
* Pass PHPStan level 9
Start tracking booleansInConditions
* Fix mark as read
* Fix doctype
* ctype_digit
38 files changed, 250 insertions, 184 deletions
diff --git a/app/Controllers/apiController.php b/app/Controllers/apiController.php index 2d9fad535..7c20b630b 100644 --- a/app/Controllers/apiController.php +++ b/app/Controllers/apiController.php @@ -21,7 +21,7 @@ class FreshRSS_api_Controller extends FreshRSS_ActionController { FreshRSS_Context::userConf()->apiPasswordHash = $apiPasswordHash; $feverKey = FreshRSS_fever_Util::updateKey($username, $apiPasswordPlain); - if (!$feverKey) { + if ($feverKey == false) { return _t('feedback.api.password.failed'); } @@ -56,7 +56,7 @@ class FreshRSS_api_Controller extends FreshRSS_ActionController { } $error = self::updatePassword($apiPasswordPlain); - if ($error) { + if (is_string($error)) { Minz_Request::bad($error, $return_url); } else { Minz_Request::good(_t('feedback.api.password.updated'), $return_url); diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index 7b0462888..3fc7036fa 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -191,7 +191,7 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController { $password = Minz_Request::paramString('p'); Minz_Request::_param('p'); - if (!$username) { + if ($username === '') { return; } diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index f28369477..343623d75 100644 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -204,6 +204,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController { $default = Minz_Configuration::load(FRESHRSS_PATH . '/config-user.default.php'); $shortcuts = $default['shortcuts']; } + /** @var array<string,string> $shortcuts */ FreshRSS_Context::userConf()->shortcuts = array_map('trim', $shortcuts); FreshRSS_Context::userConf()->save(); invalidateHttpCache(); @@ -384,27 +385,27 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController { $queryParams['search'] = htmlspecialchars_decode($params['search'], ENT_QUOTES); } if (!empty($params['state']) && is_array($params['state'])) { - $queryParams['state'] = (int)(array_sum($params['state'])); + $queryParams['state'] = (int)array_sum($params['state']); } if (empty($params['token']) || !is_string($params['token'])) { $queryParams['token'] = FreshRSS_UserQuery::generateToken($name); } else { $queryParams['token'] = $params['token']; } - if (!empty($params['shareRss']) && ctype_digit($params['shareRss'])) { - $queryParams['shareRss'] = (bool)$params['shareRss']; - } - if (!empty($params['shareOpml']) && ctype_digit($params['shareOpml'])) { - $queryParams['shareOpml'] = (bool)$params['shareOpml']; - } + $queryParams['url'] = Minz_Url::display(['params' => $queryParams]); + $queryParams['name'] = $name; if (!empty($params['description']) && is_string($params['description'])) { $queryParams['description'] = htmlspecialchars_decode($params['description'], ENT_QUOTES); } if (!empty($params['imageUrl']) && is_string($params['imageUrl'])) { $queryParams['imageUrl'] = $params['imageUrl']; } - $queryParams['url'] = Minz_Url::display(['params' => $queryParams]); - $queryParams['name'] = $name; + if (!empty($params['shareOpml']) && ctype_digit($params['shareOpml'])) { + $queryParams['shareOpml'] = (bool)$params['shareOpml']; + } + if (!empty($params['shareRss']) && ctype_digit($params['shareRss'])) { + $queryParams['shareRss'] = (bool)$params['shareRss']; + } $queries = FreshRSS_Context::userConf()->queries; $queries[$id] = (new FreshRSS_UserQuery($queryParams, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray(); diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index 38dbf8317..9104cefc4 100644 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -44,10 +44,12 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController { * - is_read (default: true) */ public function readAction(): void { - $id = Minz_Request::param('id'); $get = Minz_Request::paramString('get'); $next_get = Minz_Request::paramString('nextGet') ?: $get; $id_max = Minz_Request::paramString('idMax') ?: '0'; + if (!ctype_digit($id_max)) { + $id_max = '0'; + } $is_read = Minz_Request::paramTernary('is_read') ?? true; FreshRSS_Context::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search')); @@ -64,14 +66,14 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController { $this->view->tagsForEntries = []; $entryDAO = FreshRSS_Factory::createEntryDao(); - if ($id == false) { - // id is false? It MUST be a POST request! + if (!Minz_Request::hasParam('id')) { + // No id, then it MUST be a POST request if (!Minz_Request::isPost()) { Minz_Request::bad(_t('feedback.access.not_found'), ['c' => 'index', 'a' => 'index']); return; } - if (!$get) { + if ($get === '') { // No get? Mark all entries as read (from $id_max) $entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Feed::PRIORITY_IMPORTANT, null, 0, $is_read); } else { @@ -111,7 +113,16 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController { } } } else { - $ids = is_array($id) ? $id : [$id]; + /** @var array<numeric-string> $idArray */ + $idArray = Minz_Request::paramArray('id'); + $idString = Minz_Request::paramString('id'); + if (count($idArray) > 0) { + $ids = $idArray; + } elseif (ctype_digit($idString)) { + $ids = [$idString]; + } else { + $ids = []; + } $entryDAO->markRead($ids, $is_read); $tagDAO = FreshRSS_Factory::createTagDao(); $tagsForEntries = $tagDAO->getTagsForEntries($ids) ?: []; @@ -145,7 +156,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController { public function bookmarkAction(): void { $id = Minz_Request::paramString('id'); $is_favourite = Minz_Request::paramTernary('is_favorite') ?? true; - if ($id != '') { + if ($id != '' && ctype_digit($id)) { $entryDAO = FreshRSS_Factory::createEntryDao(); $entryDAO->markFavorite($id, $is_favourite); } diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 131d58d5e..2ecf6c374 100644 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -439,9 +439,8 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController { $nb_new_articles = 0; foreach ($feeds as $feed) { - /** @var FreshRSS_Feed|null $feed */ $feed = Minz_ExtensionManager::callHook('feed_before_actualize', $feed); - if (null === $feed) { + if (!($feed instanceof FreshRSS_Feed)) { continue; } diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index b4be5dd73..52c70ffe0 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -563,7 +563,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { // Call the extension hook $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); - if ($feed != null) { + if ($feed instanceof FreshRSS_Feed) { // addFeedObject checks if feed is already in DB so nothing else to // check here. $id = $this->feedDAO->addFeedObject($feed); diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php index d0938ca62..b57138093 100644 --- a/app/Models/DatabaseDAO.php +++ b/app/Models/DatabaseDAO.php @@ -81,7 +81,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { return count(array_keys($tables, true, true)) === count($tables); } - /** @return array<array<string,string|int|bool|null>> */ + /** @return array<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); @@ -160,7 +160,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { /** * @param array<string,string|int|bool|null> $dao - * @return array{'name':string,'type':string,'notnull':bool,'default':mixed} + * @return array{name:string,type:string,notnull:bool,default:mixed} */ public function daoToSchema(array $dao): array { return [ @@ -173,7 +173,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo { /** * @param array<array<string,string|int|bool|null>> $listDAO - * @return array<array<string,string|int|bool|null>> + * @return array<array{name:string,type:string,notnull:bool,default:mixed}> */ public function listDaoToSchema(array $listDAO): array { $list = []; diff --git a/app/Models/DatabaseDAOPGSQL.php b/app/Models/DatabaseDAOPGSQL.php index e6895e6f1..3cce4b062 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<string,string|int|bool|null>> */ + /** @return array<array{name:string,type:string,notnull:bool,default:mixed}> */ #[\Override] public function getSchema(string $table): array { $sql = <<<'SQL' diff --git a/app/Models/DatabaseDAOSQLite.php b/app/Models/DatabaseDAOSQLite.php index 0ac6ee8f5..3e15578e8 100644 --- a/app/Models/DatabaseDAOSQLite.php +++ b/app/Models/DatabaseDAOSQLite.php @@ -30,7 +30,7 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO { return count(array_keys($tables, true, true)) == count($tables); } - /** @return array<array<string,string|int|bool|null>> */ + /** @return array<array{name:string,type:string,notnull:bool,default:mixed}> */ #[\Override] public function getSchema(string $table): array { $sql = 'PRAGMA table_info(' . $table . ')'; diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 2325252f7..e102c5c21 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -10,6 +10,7 @@ class FreshRSS_Entry extends Minz_Model { public const STATE_FAVORITE = 4; public const STATE_NOT_FAVORITE = 8; + /** @var numeric-string */ private string $id = '0'; private string $guid; private string $title; @@ -110,6 +111,7 @@ class FreshRSS_Entry extends Minz_Model { } } + /** @return numeric-string */ public function id(): string { return $this->id; } @@ -195,8 +197,8 @@ class FreshRSS_Entry extends Minz_Model { $thumbnailAttribute = $this->attributeArray('thumbnail') ?? []; if (!empty($thumbnailAttribute['url'])) { $elink = $thumbnailAttribute['url']; - if ($allowDuplicateEnclosures || !self::containsLink($content, $elink)) { - $content .= <<<HTML + if (is_string($elink) && ($allowDuplicateEnclosures || !self::containsLink($content, $elink))) { + $content .= <<<HTML <figure class="enclosure"> <p class="enclosure-content"> <img class="enclosure-thumbnail" src="{$elink}" alt="" /> @@ -216,7 +218,7 @@ HTML; continue; } $elink = $enclosure['url'] ?? ''; - if ($elink == '') { + if ($elink == '' || !is_string($elink)) { continue; } if (!$allowDuplicateEnclosures && self::containsLink($content, $elink)) { @@ -281,6 +283,7 @@ HTML; $attributeEnclosures = $this->attributeArray('enclosures'); if (is_iterable($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 */ yield from $attributeEnclosures; } try { @@ -296,8 +299,10 @@ HTML; // Legacy code for database entries < FreshRSS 1.20.1 $enclosures = $xpath->query('//div[@class="enclosure"]/p[@class="enclosure-content"]/*[@src]'); if (!empty($enclosures)) { - /** @var DOMElement $enclosure */ foreach ($enclosures as $enclosure) { + if (!($enclosure instanceof DOMElement)) { + continue; + } $result = [ 'url' => $enclosure->getAttribute('src'), 'type' => $enclosure->getAttribute('data-type'), @@ -318,8 +323,10 @@ HTML; if ($searchBodyImages && $xpath !== null) { $images = $xpath->query('//img'); if (!empty($images)) { - /** @var DOMElement $img */ foreach ($images as $img) { + if (!($img instanceof DOMElement)) { + continue; + } $src = $img->getAttribute('src'); if ($src == null) { $src = $img->getAttribute('data-src'); @@ -346,6 +353,7 @@ HTML; $thumbnail = $this->attributeArray('thumbnail') ?? []; // First, use the provided thumbnail, if any if (!empty($thumbnail['url'])) { + /** @var array{'url':string,'height'?:int,'width'?:int,'time'?:string} $thumbnail */ return $thumbnail; } if ($searchEnclosures) { @@ -467,7 +475,7 @@ HTML; return $this->hash; } - /** @param int|string $value String is for compatibility with 32-bit platforms */ + /** @param int|numeric-string $value String is for compatibility with 32-bit platforms */ public function _id($value): void { if (is_int($value)) { $value = (string)$value; @@ -882,7 +890,7 @@ HTML; /** * Integer format conversion for Google Reader API format - * @param string|int $dec Decimal number + * @param numeric-string|int $dec Decimal number * @return string 64-bit hexa http://code.google.com/p/google-reader-api/wiki/ItemId */ private static function dec2hex($dec): string { diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index bb0b9af43..0b289eb41 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -321,7 +321,7 @@ SQL; * @todo simplify the query by removing the str_repeat. I am pretty sure * there is an other way to do that. * - * @param string|array<string> $ids + * @param numeric-string|array<numeric-string> $ids * @return int|false */ public function markFavorite($ids, bool $is_favorite = true) { @@ -399,7 +399,7 @@ SQL; * Toggle the read marker on one or more article. * Then the cache is updated. * - * @param string|array<string> $ids + * @param numeric-string|array<numeric-string> $ids * @param bool $is_read * @return int|false affected rows */ @@ -465,7 +465,7 @@ SQL; * * If $idMax equals 0, a deprecated debug message is logged * - * @param string $idMax fail safe article ID + * @param numeric-string $idMax fail safe article ID * @return int|false affected rows */ public function markReadEntries(string $idMax = '0', bool $onlyFavorites = false, ?int $priorityMin = null, ?int $prioritMax = null, @@ -517,7 +517,7 @@ SQL; * If $idMax equals 0, a deprecated debug message is logged * * @param int $id category ID - * @param string $idMax fail safe article ID + * @param numeric-string $idMax fail safe article ID * @return int|false affected rows */ public function markReadCat(int $id, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) { @@ -558,7 +558,7 @@ SQL; * If $idMax equals 0, a deprecated debug message is logged * * @param int $id_feed feed ID - * @param string $idMax fail safe article ID + * @param numeric-string $idMax fail safe article ID * @return int|false affected rows */ public function markReadFeed(int $id_feed, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) { @@ -612,7 +612,7 @@ SQL; /** * Mark all the articles in a tag as read. * @param int $id tag ID, or empty for targeting any tag - * @param string $idMax max article ID + * @param numeric-string $idMax max article ID * @return int|false affected rows */ public function markReadTag(int $id = 0, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, @@ -1206,7 +1206,7 @@ SQL; } /** - * @param array<string> $ids + * @param array<numeric-string> $ids * @param 'ASC'|'DESC' $order * @return Traversable<FreshRSS_Entry> */ diff --git a/app/Models/Feed.php b/app/Models/Feed.php index ebe69a84c..0274ded0a 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -658,7 +658,7 @@ class FreshRSS_Feed extends Minz_Model { //check if the content is actual JSON $jf = json_decode($json, true); - if (json_last_error() !== JSON_ERROR_NONE) { + if (json_last_error() !== JSON_ERROR_NONE || !is_array($jf)) { return null; } @@ -734,9 +734,14 @@ class FreshRSS_Feed extends Minz_Model { } $xpath = new DOMXPath($doc); + $xpathEvaluateString = function (string $expression, ?DOMNode $contextNode = null) use ($xpath): string { + $result = @$xpath->evaluate('normalize-space(' . $expression . ')', $contextNode); + return is_string($result) ? $result : ''; + }; + $view->rss_title = $xPathFeedTitle == '' ? $this->name() : - htmlspecialchars(@$xpath->evaluate('normalize-space(' . $xPathFeedTitle . ')'), ENT_COMPAT, 'UTF-8'); - $view->rss_base = htmlspecialchars(trim($xpath->evaluate('normalize-space(//base/@href)')), ENT_COMPAT, 'UTF-8'); + htmlspecialchars($xpathEvaluateString($xPathFeedTitle), ENT_COMPAT, 'UTF-8'); + $view->rss_base = htmlspecialchars(trim($xpathEvaluateString('//base/@href')), ENT_COMPAT, 'UTF-8'); $nodes = $xpath->query($xPathItem); if ($nodes === false || $nodes->length === 0) { return null; @@ -744,7 +749,7 @@ class FreshRSS_Feed extends Minz_Model { foreach ($nodes as $node) { $item = []; - $item['title'] = $xPathItemTitle == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTitle . ')', $node); + $item['title'] = $xPathItemTitle == '' ? '' : $xpathEvaluateString($xPathItemTitle, $node); $item['content'] = ''; if ($xPathItemContent != '') { @@ -756,36 +761,35 @@ class FreshRSS_Feed extends Minz_Model { $content .= $doc->saveHTML($child) . "\n"; } $item['content'] = $content; - } else { + } elseif (is_string($result) || is_int($result) || is_bool($result)) { // Typed expression, save as-is $item['content'] = (string)$result; } } - $item['link'] = $xPathItemUri == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemUri . ')', $node); - $item['author'] = $xPathItemAuthor == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemAuthor . ')', $node); - $item['timestamp'] = $xPathItemTimestamp == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTimestamp . ')', $node); + $item['link'] = $xPathItemUri == '' ? '' : $xpathEvaluateString($xPathItemUri, $node); + $item['author'] = $xPathItemAuthor == '' ? '' : $xpathEvaluateString($xPathItemAuthor, $node); + $item['timestamp'] = $xPathItemTimestamp == '' ? '' : $xpathEvaluateString($xPathItemTimestamp, $node); if ($xPathItemTimeFormat != '') { - $dateTime = DateTime::createFromFormat($xPathItemTimeFormat, $item['timestamp'] ?? ''); + $dateTime = DateTime::createFromFormat($xPathItemTimeFormat, $item['timestamp']); if ($dateTime != false) { $item['timestamp'] = $dateTime->format(DateTime::ATOM); } } - $item['thumbnail'] = $xPathItemThumbnail == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemThumbnail . ')', $node); + $item['thumbnail'] = $xPathItemThumbnail == '' ? '' : $xpathEvaluateString($xPathItemThumbnail, $node); if ($xPathItemCategories != '') { $itemCategories = @$xpath->evaluate($xPathItemCategories, $node); if (is_string($itemCategories) && $itemCategories !== '') { $item['tags'] = [$itemCategories]; } elseif ($itemCategories instanceof DOMNodeList && $itemCategories->length > 0) { $item['tags'] = []; - /** @var DOMNode $itemCategory */ foreach ($itemCategories as $itemCategory) { $item['tags'][] = $itemCategory->textContent; } } } if ($xPathItemUid != '') { - $item['guid'] = @$xpath->evaluate('normalize-space(' . $xPathItemUid . ')', $node); + $item['guid'] = $xpathEvaluateString($xPathItemUid, $node); } if (empty($item['guid'])) { $item['guid'] = 'urn:sha1:' . sha1($item['title'] . $item['content'] . $item['link']); @@ -958,7 +962,7 @@ class FreshRSS_Feed extends Minz_Model { $hubFilename = PSHB_PATH . '/feeds/' . sha1($url) . '/!hub.json'; $hubFile = @file_get_contents($hubFilename); $hubJson = is_string($hubFile) ? json_decode($hubFile, true) : null; - if (is_array($hubJson) && !isset($hubJson['error']) || $hubJson['error'] !== $error) { + if (is_array($hubJson) && (!isset($hubJson['error']) || $hubJson['error'] !== $error)) { $hubJson['error'] = $error; file_put_contents($hubFilename, json_encode($hubJson)); Minz_Log::warning('Set error to ' . ($error ? 1 : 0) . ' for ' . $url, PSHB_LOG); diff --git a/app/Models/ReadingMode.php b/app/Models/ReadingMode.php index c8ebe66f3..035b22114 100644 --- a/app/Models/ReadingMode.php +++ b/app/Models/ReadingMode.php @@ -9,13 +9,13 @@ class FreshRSS_ReadingMode { protected string $id; protected string $name; protected string $title; - /** @var array{'c':string,'a':string,'params':array<string,mixed>} */ + /** @var array{c:string,a:string,params:array<string,mixed>} */ protected array $urlParams; protected bool $isActive = false; /** * ReadingMode constructor. - * @param array{'c':string,'a':string,'params':array<string,mixed>} $urlParams + * @param array{c:string,a:string,params:array<string,mixed>} $urlParams */ public function __construct(string $id, string $title, array $urlParams, bool $active) { $this->id = $id; @@ -47,12 +47,12 @@ class FreshRSS_ReadingMode { return $this; } - /** @return array{'c':string,'a':string,'params':array<string,mixed>} */ + /** @return array{c:string,a:string,params:array<string,mixed>} */ public function getUrlParams(): array { return $this->urlParams; } - /** @param array{'c':string,'a':string,'params':array<string,mixed>} $urlParams */ + /** @param array{c:string,a:string,params:array<string,mixed>} $urlParams */ public function setUrlParams(array $urlParams): FreshRSS_ReadingMode { $this->urlParams = $urlParams; return $this; diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php index 0b02960c4..c9d5b07af 100644 --- a/app/Models/UserConfiguration.php +++ b/app/Models/UserConfiguration.php @@ -69,7 +69,7 @@ declare(strict_types=1); * @property int $dynamic_opml_ttl_default * @property-read bool $unsafe_autologin_enabled * @property string $view_mode - * @property array<string,mixed> $volatile + * @property array<string,bool|int|string> $volatile * @property array<string,array<string,mixed>> $extensions */ final class FreshRSS_UserConfiguration extends Minz_Configuration { diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php index 73bf52ef8..3058483d3 100644 --- a/app/Models/UserQuery.php +++ b/app/Models/UserQuery.php @@ -41,7 +41,8 @@ class FreshRSS_UserQuery { } /** - * @param array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,shareRss?:bool,shareOpml?:bool} $query + * @param array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string, + * shareRss?:bool,shareOpml?:bool,description?:string,imageUrl?:string} $query * @param array<int,FreshRSS_Category> $categories * @param array<int,FreshRSS_Tag> $labels */ @@ -61,8 +62,13 @@ class FreshRSS_UserQuery { } if (empty($query['url'])) { if (!empty($query)) { - unset($query['name']); - $this->url = Minz_Url::display(['params' => $query]); + $link = $query; + unset($link['description']); + unset($link['imageUrl']); + unset($link['name']); + unset($link['shareOpml']); + unset($link['shareRss']); + $this->url = Minz_Url::display(['params' => $link]); } } else { $this->url = $query['url']; diff --git a/app/Services/ImportService.php b/app/Services/ImportService.php index 2c23e5d4f..11da49217 100644 --- a/app/Services/ImportService.php +++ b/app/Services/ImportService.php @@ -78,13 +78,13 @@ class FreshRSS_Import_Service { $category_element = $categories_elements[$category_name] ?? null; $category = null; - if ($forced_category) { + if ($forced_category !== null) { // If the category is forced, ignore the actual category name $category = $forced_category; } elseif (isset($categories_by_names[$category_name])) { // If the category already exists, get it from $categories_by_names $category = $categories_by_names[$category_name]; - } elseif ($category_element) { + } elseif (is_array($category_element)) { // Otherwise, create the category (if possible) $limit_reached = $nb_categories >= $limits['max_categories']; $can_create_category = FreshRSS_Context::$isCli || !$limit_reached; @@ -362,11 +362,11 @@ class FreshRSS_Import_Service { * This method is applied to a list of outlines. It merges the different * list of feeds from several outlines into one array. * - * @param array<mixed> $outlines + * @param array<array<mixed>> $outlines * The outlines from which to extract the outlines. * @param string $parent_category_name * The name of the parent category of the current outlines. - * @return array{0:array<mixed>,1:array<mixed>} + * @return array{0:array<string,array<string,string>>,1:array<string,array<array<string,string>>>} */ private function loadFromOutlines(array $outlines, string $parent_category_name): array { $categories_elements = []; @@ -410,13 +410,13 @@ class FreshRSS_Import_Service { * @param string $parent_category_name * The name of the parent category of the current outline. * - * @return array{0:array<string,mixed>,1:array<string,mixed>} + * @return array{0:array<string,array<string,string>>,1:array<array<string,array<string,string>>>} */ - private function loadFromOutline($outline, $parent_category_name): array { + private function loadFromOutline(array $outline, string $parent_category_name): array { $categories_elements = []; $categories_to_feeds = []; - if ($parent_category_name === '' && isset($outline['category'])) { + if ($parent_category_name === '' && isset($outline['category']) && is_array($outline['category'])) { // The outline has no parent category, but its OPML category // attribute is set, so we use it as the category name. // lib_opml parses this attribute as an array of strings, so we @@ -429,9 +429,9 @@ class FreshRSS_Import_Service { if (isset($outline['@outlines'])) { // The outline has children, it’s probably a category - if (!empty($outline['text'])) { + if (!empty($outline['text']) && is_string($outline['text'])) { $category_name = $outline['text']; - } elseif (!empty($outline['title'])) { + } elseif (!empty($outline['title']) && is_string($outline['title'])) { $category_name = $outline['title']; } else { $category_name = $parent_category_name; diff --git a/app/layout/layout.phtml b/app/layout/layout.phtml index e581850bd..685d2d48f 100644 --- a/app/layout/layout.phtml +++ b/app/layout/layout.phtml @@ -41,8 +41,8 @@ if ($this->rss_title != '') { $url_rss = $url_base; $url_rss['a'] = 'rss'; - $url_rss['params']['user'] = Minz_User::name(); - $url_rss['params']['token'] = FreshRSS_Context::userConf()->token ?: null; + $url_rss['params']['user'] = Minz_User::name() ?? ''; + $url_rss['params']['token'] = FreshRSS_Context::userConf()->token; unset($url_rss['params']['rid']); if (FreshRSS_Context::userConf()->since_hours_posts_per_rss) { $url_rss['params']['hours'] = FreshRSS_Context::userConf()->since_hours_posts_per_rss; @@ -52,8 +52,8 @@ <?php } if (FreshRSS_Context::isAll() || FreshRSS_Context::isCategory() || FreshRSS_Context::isFeed()) { $opml_rss = $url_base; $opml_rss['a'] = 'opml'; - $opml_rss['params']['user'] = Minz_User::name(); - $opml_rss['params']['token'] = FreshRSS_Context::userConf()->token ?: null; + $opml_rss['params']['user'] = Minz_User::name() ?? ''; + $opml_rss['params']['token'] = FreshRSS_Context::userConf()->token; unset($opml_rss['params']['rid']); ?> <link rel="outline" type="text/x-opml" title="OPML" href="<?= Minz_Url::display($opml_rss) ?>" /> diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml index 0d9252910..8390fa6bf 100644 --- a/app/views/helpers/feed/update.phtml +++ b/app/views/helpers/feed/update.phtml @@ -421,6 +421,7 @@ <fieldset id="html_xpath"> <?php + /** @var array<string> $xpath */ $xpath = Minz_Helper::htmlspecialchars_utf8($this->feed->attributeArray('xpath') ?? []); ?> <p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.help') ?></p> @@ -516,6 +517,7 @@ <fieldset id="json_dotnotation"> <?php + /** @var array<string,string> $jsonSettings */ $jsonSettings = Minz_Helper::htmlspecialchars_utf8($this->feed->attributeArray('json_dotnotation') ?? []); ?> <p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.json_dotnotation.help') ?></p> @@ -634,17 +636,19 @@ </div> <div class="form-group"> + <?php + /** @var array<int,int|string> $curlParams */ + $curlParams = $this->feed->attributeArray('curl_params') ?? []; + ?> <label class="group-name" for="curl_params_cookie"><?= _t('sub.feed.css_cookie') ?></label> <div class="group-controls"> <input type="text" name="curl_params_cookie" id="curl_params_cookie" class="w100" value="<?= - $this->feed->attributeArray('curl_params') !== null && !empty($this->feed->attributeArray('curl_params')[CURLOPT_COOKIE]) ? - $this->feed->attributeArray('curl_params')[CURLOPT_COOKIE] : '' + !empty($curlParams[CURLOPT_COOKIE]) ? $curlParams[CURLOPT_COOKIE] : '' ?>" placeholder="<?= _t('gen.short.blank_to_disable') ?>" /> <p class="help"><?= _i('help') ?> <?= _t('sub.feed.css_cookie_help') ?></p> <label for="curl_params_cookiefile"> <input type="checkbox" name="curl_params_cookiefile" id="curl_params_cookiefile" value="1"<?= - $this->feed->attributeArray('curl_params') !== null && isset($this->feed->attributeArray('curl_params')[CURLOPT_COOKIEFILE]) ? - ' checked="checked"' : '' + isset($curlParams[CURLOPT_COOKIEFILE]) ? ' checked="checked"' : '' ?> /> <?= _t('sub.feed.accept_cookies') ?> </label> @@ -656,8 +660,7 @@ <label class="group-name" for="curl_params_redirects"><?= _t('sub.feed.max_http_redir') ?></label> <div class="group-controls"> <input type="number" name="curl_params_redirects" id="curl_params_redirects" class="w50" min="-1" value="<?= - $this->feed->attributeArray('curl_params') !== null && !empty($this->feed->attributeArray('curl_params')[CURLOPT_MAXREDIRS]) ? - $this->feed->attributeArray('curl_params')[CURLOPT_MAXREDIRS] : '' + !empty($curlParams[CURLOPT_MAXREDIRS]) ? $curlParams[CURLOPT_MAXREDIRS] : '' ?>" placeholder="<?= _t('gen.short.blank_to_disable') ?>" /> <p class="help"><?= _i('help') ?> <?= _t('sub.feed.max_http_redir_help') ?></p> </div> @@ -678,8 +681,7 @@ <label class="group-name" for="curl_params_useragent"><?= _t('sub.feed.useragent') ?></label> <div class="group-controls"> <input type="text" name="curl_params_useragent" id="curl_params_useragent" class="w100" value="<?= - $this->feed->attributeArray('curl_params') !== null && !empty($this->feed->attributeArray('curl_params')[CURLOPT_USERAGENT]) ? - $this->feed->attributeArray('curl_params')[CURLOPT_USERAGENT] : '' + !empty($curlParams[CURLOPT_USERAGENT]) ? $curlParams[CURLOPT_USERAGENT] : '' ?>" placeholder="<?= _t('gen.short.blank_to_disable') ?>" /> <p class="help"><?= _i('help') ?> <?= _t('sub.feed.useragent_help') ?></p> </div> @@ -689,18 +691,14 @@ <label class="group-name" for="proxy_type"><?= _t('sub.feed.proxy') ?></label> <div class="group-controls"> <select name="proxy_type" id="proxy_type"><?php - $type = ''; - if ($this->feed->attributeArray('curl_params') !== null && isset($this->feed->attributeArray('curl_params')[CURLOPT_PROXYTYPE])) { - $type = $this->feed->attributeArray('curl_params')[CURLOPT_PROXYTYPE]; - } + $type = $curlParams[CURLOPT_PROXYTYPE] ?? ''; foreach(['' => '', 3 => 'NONE', 0 => 'HTTP', 2 => 'HTTPS', 4 => 'SOCKS4', 6 => 'SOCKS4A', 5 => 'SOCKS5', 7 => 'SOCKS5H'] as $k => $v) { echo '<option value="' . $k . ($type === $k ? '" selected="selected' : '' ) . '">' . $v . '</option>'; } ?> </select> <input type="text" name="curl_params" id="curl_params" value="<?= - $this->feed->attributeArray('curl_params') !== null && !empty($this->feed->attributeArray('curl_params')[CURLOPT_PROXY]) ? - $this->feed->attributeArray('curl_params')[CURLOPT_PROXY] : '' + !empty($curlParams[CURLOPT_PROXY]) ? $curlParams[CURLOPT_PROXY] : '' ?>" placeholder="<?= _t('gen.short.blank_to_disable') ?>" /> <p class="help"><?= _i('help') ?> <?= _t('sub.feed.proxy_help') ?></p> </div> diff --git a/app/views/helpers/index/article.phtml b/app/views/helpers/index/article.phtml index 56fd06b4f..bfdfdb8d1 100644 --- a/app/views/helpers/index/article.phtml +++ b/app/views/helpers/index/article.phtml @@ -10,11 +10,11 @@ <?php $favoriteUrl = ['c' => 'entry', 'a' => 'bookmark', 'params' => ['id' => $entry->id()]]; if ($entry->isFavorite()) { - $favoriteUrl['params']['is_favorite'] = 0; + $favoriteUrl['params']['is_favorite'] = '0'; } $readUrl = ['c' => 'entry', 'a' => 'read', 'params' => ['id' => $entry->id()]]; if ($entry->isRead()) { - $readUrl['params']['is_read'] = 0; + $readUrl['params']['is_read'] = '0'; } ?> <div class="article-header-topline"> diff --git a/app/views/helpers/index/normal/entry_bottom.phtml b/app/views/helpers/index/normal/entry_bottom.phtml index e5bfd7fd0..21bd1d355 100644 --- a/app/views/helpers/index/normal/entry_bottom.phtml +++ b/app/views/helpers/index/normal/entry_bottom.phtml @@ -12,9 +12,9 @@ if (FreshRSS_Auth::hasAccess()) { if ($bottomline_read) { ?><li class="item manage"><?php - $arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $this->entry->id())); + $arUrl = ['c' => 'entry', 'a' => 'read', 'params' => ['id' => $this->entry->id()]]; if ($this->entry->isRead()) { - $arUrl['params']['is_read'] = 0; + $arUrl['params']['is_read'] = '0'; } ?><a class="item-element read" href="<?= Minz_Url::display($arUrl) ?>" title="<?= _t('conf.shortcut.mark_read') ?>"><?php echo _i($this->entry->isRead() ? 'read' : 'unread'); ?></a><?php @@ -22,9 +22,9 @@ } if ($bottomline_favorite) { ?><li class="item manage"><?php - $arUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $this->entry->id())); + $arUrl = ['c' => 'entry', 'a' => 'bookmark', 'params' => ['id' => $this->entry->id()]]; if ($this->entry->isFavorite()) { - $arUrl['params']['is_favorite'] = 0; + $arUrl['params']['is_favorite'] = '0'; } ?><a class="item-element bookmark" href="<?= Minz_Url::display($arUrl) ?>" title="<?= _t('conf.shortcut.mark_favorite') ?>"><?php echo _i($this->entry->isFavorite() ? 'starred' : 'non-starred'); ?></a><?php diff --git a/app/views/helpers/index/normal/entry_header.phtml b/app/views/helpers/index/normal/entry_header.phtml index 0db375309..e4befb346 100644 --- a/app/views/helpers/index/normal/entry_header.phtml +++ b/app/views/helpers/index/normal/entry_header.phtml @@ -14,9 +14,9 @@ if (FreshRSS_Auth::hasAccess()) { if ($topline_read) { ?><li class="item manage"><?php - $arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $this->entry->id())); + $arUrl = ['c' => 'entry', 'a' => 'read', 'params' => ['id' => $this->entry->id()]]; if ($this->entry->isRead()) { - $arUrl['params']['is_read'] = 0; + $arUrl['params']['is_read'] = '0'; } ?><a class="item-element read" href="<?= Minz_Url::display($arUrl) ?>" title="<?= _t('conf.shortcut.mark_read') ?>"><?php echo _i($this->entry->isRead() ? 'read' : 'unread'); ?></a><?php @@ -24,9 +24,9 @@ } if ($topline_favorite) { ?><li class="item manage"><?php - $arUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $this->entry->id())); + $arUrl = ['c' => 'entry', 'a' => 'bookmark', 'params' => ['id' => $this->entry->id()]]; if ($this->entry->isFavorite()) { - $arUrl['params']['is_favorite'] = 0; + $arUrl['params']['is_favorite'] = '0'; } ?><a class="item-element bookmark" href="<?= Minz_Url::display($arUrl) ?>" title="<?= _t('conf.shortcut.mark_favorite') ?>"><?php echo _i($this->entry->isFavorite() ? 'starred' : 'non-starred'); ?></a><?php diff --git a/app/views/helpers/stream-footer.phtml b/app/views/helpers/stream-footer.phtml index 0cbab601a..9681cff02 100644 --- a/app/views/helpers/stream-footer.phtml +++ b/app/views/helpers/stream-footer.phtml @@ -3,20 +3,20 @@ /** @var FreshRSS_View $this */ $url_next = Minz_Request::currentRequest(); $url_next['params']['next'] = FreshRSS_Context::$next_id; - $url_next['params']['state'] = FreshRSS_Context::$state; - $url_next['params']['ajax'] = 1; + $url_next['params']['state'] = (string)FreshRSS_Context::$state; + $url_next['params']['ajax'] = '1'; - $url_mark_read = array( + $url_mark_read = [ 'c' => 'entry', 'a' => 'read', - 'params' => array( + 'params' => [ 'get' => FreshRSS_Context::currentGet(), 'nextGet' => FreshRSS_Context::$next_get, 'idMax' => FreshRSS_Context::$id_max, 'search' => htmlspecialchars_decode(FreshRSS_Context::$search->getRawInput(), ENT_QUOTES), 'state' => FreshRSS_Context::$state, - ) - ); + ], + ]; $hasAccess = FreshRSS_Auth::hasAccess(); if ($hasAccess) { diff --git a/app/views/javascript/actualize.phtml b/app/views/javascript/actualize.phtml index bce316d85..314f83012 100644 --- a/app/views/javascript/actualize.phtml +++ b/app/views/javascript/actualize.phtml @@ -5,21 +5,21 @@ declare(strict_types=1); $categories = []; foreach ($this->categories as $category) { $categories[] = [ - 'url' => Minz_Url::display(array('c' => 'category', 'a' => 'refreshOpml', 'params' => array('id' => $category->id(), 'ajax' => '1')), 'php'), + 'url' => Minz_Url::display(['c' => 'category', 'a' => 'refreshOpml', 'params' => ['id' => $category->id(), 'ajax' => '1']], 'php'), 'title' => $category->name(), ]; } -$feeds = array(); +$feeds = []; foreach ($this->feeds as $feed) { - $feeds[] = array( - 'url' => Minz_Url::display(array('c' => 'feed', 'a' => 'actualize', 'params' => array('id' => $feed->id(), 'ajax' => '1')), 'php'), + $feeds[] = [ + 'url' => Minz_Url::display(['c' => 'feed', 'a' => 'actualize', 'params' => ['id' => $feed->id(), 'ajax' => '1']], 'php'), 'title' => $feed->name(), - ); + ]; } -echo json_encode(array( +echo json_encode([ 'categories' => $categories, 'feeds' => $feeds, 'feedback_no_refresh' => _t('feedback.sub.feed.no_refresh'), 'feedback_actualize' => _t('feedback.sub.actualize'), -)); +]); diff --git a/app/views/user/profile.phtml b/app/views/user/profile.phtml index 3a80065ef..b2e8a7d7b 100644 --- a/app/views/user/profile.phtml +++ b/app/views/user/profile.phtml @@ -60,8 +60,8 @@ <div class="group-controls"> <input type="text" id="token" name="token" value="<?= $token ?>" placeholder="<?= _t('gen.short.blank_to_disable') ?>" data-leave-validation="<?= $token ?>"/> <p class="help"><?= _i('help') ?> <?= _t('admin.auth.token_help') ?></p> - <kbd><?= Minz_Url::display(array('a' => 'rss', 'params' => array('user' => Minz_User::name(), - 'token' => $token, 'hours' => FreshRSS_Context::userConf()->since_hours_posts_per_rss)), 'html', true) ?></kbd> + <kbd><?= Minz_Url::display(['a' => 'rss', 'params' => ['user' => Minz_User::name() ?? '', + 'token' => $token, 'hours' => FreshRSS_Context::userConf()->since_hours_posts_per_rss]], 'html', true) ?></kbd> <p class="help"><?= _i('help') ?> <?= _t('conf.query.help') ?></a></p> </div> </div> diff --git a/composer.lock b/composer.lock index 8ee60e728..1b21112a2 100644 --- a/composer.lock +++ b/composer.lock @@ -314,16 +314,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.11.3", + "version": "1.11.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e64220a05c1209fc856d58e789c3b7a32c0bb9a5" + "reference": "9100a76ce8015b9aa7125b9171ae3a76887b6c82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e64220a05c1209fc856d58e789c3b7a32c0bb9a5", - "reference": "e64220a05c1209fc856d58e789c3b7a32c0bb9a5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9100a76ce8015b9aa7125b9171ae3a76887b6c82", + "reference": "9100a76ce8015b9aa7125b9171ae3a76887b6c82", "shasum": "" }, "require": { @@ -368,7 +368,7 @@ "type": "github" } ], - "time": "2024-05-31T13:53:37+00:00" + "time": "2024-06-06T12:19:22+00:00" }, { "name": "phpstan/phpstan-phpunit", diff --git a/lib/Minz/Helper.php b/lib/Minz/Helper.php index 04539ec40..50243ded0 100644 --- a/lib/Minz/Helper.php +++ b/lib/Minz/Helper.php @@ -9,23 +9,25 @@ declare(strict_types=1); /** * The Minz_Helper class contains some misc. help functions */ -class Minz_Helper { +final class Minz_Helper { /** * Wrapper for htmlspecialchars. - * Force UTf-8 value and can be used on array too. + * Force UTF-8 value and can be used on array too. * - * @phpstan-template T of string|array<mixed> + * @phpstan-template T of mixed * @phpstan-param T $var * @phpstan-return T * - * @param string|array<mixed> $var - * @return string|array<mixed> + * @param mixed $var + * @return mixed */ public static function htmlspecialchars_utf8($var) { if (is_array($var)) { - return array_map(['Minz_Helper', 'htmlspecialchars_utf8'], $var); + // @phpstan-ignore argument.type, return.type + return array_map([self::class, 'htmlspecialchars_utf8'], $var); } elseif (is_string($var)) { + // @phpstan-ignore return.type return htmlspecialchars($var, ENT_COMPAT, 'UTF-8'); } else { return $var; diff --git a/lib/Minz/Model.php b/lib/Minz/Model.php index ee65d7d9c..c744b5a52 100644 --- a/lib/Minz/Model.php +++ b/lib/Minz/Model.php @@ -9,6 +9,6 @@ declare(strict_types=1); /** * The Minz_Model class represents a model in the MVC paradigm. */ -class Minz_Model { +abstract class Minz_Model { } diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index 6d099a555..301bd5623 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -19,7 +19,7 @@ class Minz_Request { private static string $default_controller_name = 'index'; private static string $default_action_name = 'index'; - /** @var array{'c'?:string,'a'?:string,'params'?:array<string,mixed>} */ + /** @var array{c?:string,a?:string,params?:array<string,mixed>} */ private static array $originalRequest = []; /** @@ -35,6 +35,7 @@ class Minz_Request { public static function params(): array { return self::$params; } + /** * Read the URL parameter * @param string $key Key name @@ -46,10 +47,8 @@ class Minz_Request { public static function param(string $key, $default = false, bool $specialchars = false) { if (isset(self::$params[$key])) { $p = self::$params[$key]; - if (is_object($p) || $specialchars) { - return $p; - } elseif (is_string($p) || is_array($p)) { - return Minz_Helper::htmlspecialchars_utf8($p); + if (is_string($p) || is_array($p)) { + return $specialchars ? $p : Minz_Helper::htmlspecialchars_utf8($p); } else { return $p; } @@ -58,12 +57,15 @@ class Minz_Request { } } - /** @return array<string|int,string|array<string,string|int>> */ + public static function hasParam(string $key): bool { + return isset(self::$params[$key]); + } + + /** @return array<string|int,string|array<string,string|int|bool>> */ public static function paramArray(string $key, bool $specialchars = false): array { if (empty(self::$params[$key]) || !is_array(self::$params[$key])) { return []; } - return $specialchars ? Minz_Helper::htmlspecialchars_utf8(self::$params[$key]) : self::$params[$key]; } @@ -131,7 +133,7 @@ class Minz_Request { public static function defaultActionName(): string { return self::$default_action_name; } - /** @return array{'c':string,'a':string,'params':array<string,mixed>} */ + /** @return array{c:string,a:string,params:array<string,mixed>} */ public static function currentRequest(): array { return [ 'c' => self::$controller_name, @@ -140,14 +142,14 @@ class Minz_Request { ]; } - /** @return array{'c'?:string,'a'?:string,'params'?:array<string,mixed>} */ + /** @return array{c?:string,a?:string,params?:array<string,mixed>} */ public static function originalRequest() { return self::$originalRequest; } /** * @param array<string,mixed>|null $extraParams - * @return array{'c':string,'a':string,'params':array<string,mixed>} + * @return array{c:string,a:string,params:array<string,mixed>} */ public static function modifiedCurrentRequest(?array $extraParams = null): array { unset(self::$params['ajax']); @@ -169,14 +171,13 @@ class Minz_Request { self::$action_name = ctype_alnum($action_name) ? $action_name : ''; } - /** @param array<string,string> $params */ + /** @param array<string,mixed> $params */ public static function _params(array $params): void { self::$params = $params; } - /** @param array|mixed $value */ - public static function _param(string $key, $value = false): void { - if ($value === false) { + public static function _param(string $key, ?string $value = null): void { + if ($value === null) { unset(self::$params[$key]); } else { self::$params[$key] = $value; @@ -382,7 +383,7 @@ class Minz_Request { /** * Restart a request - * @param array{'c'?:string,'a'?:string,'params'?:array<string,mixed>} $url an array presentation of the URL to route to + * @param array{c?:string,a?:string,params?:array<string,mixed>} $url an array presentation of the URL to route to * @param bool $redirect If true, uses an HTTP redirection, and if false (default), performs an internal dispatcher redirection. * @throws Minz_ConfigurationException */ @@ -400,10 +401,8 @@ class Minz_Request { } else { self::_controllerName($url['c']); self::_actionName($url['a']); - self::_params(array_merge( - self::$params, - $url['params'] - )); + $merge = array_merge(self::$params, $url['params']); + self::_params($merge); Minz_Dispatcher::reset(); } } @@ -411,7 +410,7 @@ class Minz_Request { /** * Wrappers good notifications + redirection * @param string $msg notification content - * @param array{'c'?:string,'a'?:string,'params'?:array<string,mixed>} $url url array to where we should be forwarded + * @param array{c?:string,a?:string,params?:array<string,mixed>} $url url array to where we should be forwarded */ public static function good(string $msg, array $url = []): void { Minz_Request::setGoodNotification($msg); @@ -421,7 +420,7 @@ class Minz_Request { /** * Wrappers bad notifications + redirection * @param string $msg notification content - * @param array{'c'?:string,'a'?:string,'params'?:array<string,mixed>} $url url array to where we should be forwarded + * @param array{c?:string,a?:string,params?:array<string,mixed>} $url url array to where we should be forwarded */ public static function bad(string $msg, array $url = []): void { Minz_Request::setBadNotification($msg); diff --git a/lib/Minz/Url.php b/lib/Minz/Url.php index 67b927f05..73edcf76d 100644 --- a/lib/Minz/Url.php +++ b/lib/Minz/Url.php @@ -7,7 +7,7 @@ declare(strict_types=1); class Minz_Url { /** * Display a formatted URL - * @param string|array<string,string|array<string,mixed>> $url The URL to format, defined as an array: + * @param string|array{c?:string,a?:string,params?:array<string,mixed>} $url The URL to format, defined as an array: * $url['c'] = controller * $url['a'] = action * $url['params'] = array of additional parameters @@ -26,7 +26,7 @@ class Minz_Url { $url_string = ''; - if ($absolute) { + if ($absolute !== false) { $url_string = Minz_Request::getBaseUrl(); if (strlen($url_string) < strlen('http://a.bc')) { $url_string = Minz_Request::guessBaseUrl(); @@ -58,7 +58,7 @@ class Minz_Url { /** * Construit l'URI d'une URL - * @param array<string,mixed> $url l'url sous forme de tableau + * @param array{c:string,a:string,params:array<string,mixed>} $url URL as array definition * @param string $encodage pour indiquer comment encoder les & (& ou & pour html) * @return string uri sous la forme ?key=value&key2=value2 */ @@ -74,17 +74,19 @@ class Minz_Url { } if (!empty($url['params']) && is_array($url['params']) && !empty($url['params']['#'])) { - $anchor = '#' . ($encodage === 'html' ? htmlspecialchars($url['params']['#'], ENT_QUOTES, 'UTF-8') : $url['params']['#']); + if (is_string($url['params']['#'])) { + $anchor = '#' . ($encodage === 'html' ? htmlspecialchars($url['params']['#'], ENT_QUOTES, 'UTF-8') : $url['params']['#']); + } unset($url['params']['#']); } - if (isset($url['c']) + if (isset($url['c']) && is_string($url['c']) && $url['c'] != Minz_Request::defaultControllerName()) { $uri .= $separator . 'c=' . $url['c']; $separator = $and; } - if (isset($url['a']) + if (isset($url['a']) && is_string($url['a']) && $url['a'] != Minz_Request::defaultActionName()) { $uri .= $separator . 'a=' . $url['a']; $separator = $and; @@ -94,7 +96,7 @@ class Minz_Url { unset($url['params']['c']); unset($url['params']['a']); foreach ($url['params'] as $key => $param) { - if (!is_string($key) || (!is_string($param) && !is_int($param))) { + if (!is_string($key) || (!is_string($param) && !is_int($param) && !is_bool($param))) { continue; } $uri .= $separator . urlencode($key) . '=' . urlencode((string)$param); @@ -102,10 +104,6 @@ class Minz_Url { } } - if (!empty($url['#']) && is_string($url['#'])) { - $uri .= '#' . ($encodage === 'html' ? htmlspecialchars($url['#'], ENT_QUOTES, 'UTF-8') : $url['#']); - } - $uri .= $anchor; return $uri; @@ -113,8 +111,8 @@ class Minz_Url { /** * Check that all array elements representing the controller URL are OK - * @param array<string,string|array<string,mixed>> $url controller URL as array - * @return array{'c':string,'a':string,'params':array<string,mixed>} Verified controller URL as array + * @param array{c?:string,a?:string,params?:array<string,mixed>} $url controller URL as array + * @return array{c:string,a:string,params:array<string,mixed>} Verified controller URL as array */ public static function checkControllerUrl(array $url): array { return [ @@ -124,7 +122,7 @@ class Minz_Url { ]; } - /** @param array{'c'?:string,'a'?:string,'params'?:array<string,mixed>} $url */ + /** @param array{c?:string,a?:string,params?:array<string,mixed>} $url */ public static function serialize(?array $url = []): string { if (empty($url)) { return ''; @@ -136,19 +134,16 @@ class Minz_Url { } } - /** - * @phpstan-return array{'c'?:string,'a'?:string,'params'?:array<string,mixed>} - * @return array<string,string|array<string,string>> - */ + /** @return array{c?:string,a?:string,params?:array<string,mixed>} */ public static function unserialize(string $url = ''): array { $result = json_decode(base64_decode($url, true) ?: '', true, JSON_THROW_ON_ERROR) ?? []; - /** @var array{'c'?:string,'a'?:string,'params'?:array<string,mixed>} $result */ + /** @var array{c?:string,a?:string,params?:array<string,mixed>} $result */ return $result; } /** * Returns an array representing the URL as passed in the address bar - * @return array{'c'?:string,'a'?:string,'params'?:array<string,mixed>} URL representation + * @return array{c?:string,a?:string,params?:array<string,string>} URL representation */ public static function build(): array { $url = [ @@ -184,5 +179,5 @@ function _url(string $controller, string $action, ...$args) { $params[$arg] = '' . $args[$i + 1]; } - return Minz_Url::display (array ('c' => $controller, 'a' => $action, 'params' => $params)); + return Minz_Url::display(['c' => $controller, 'a' => $action, 'params' => $params]); } diff --git a/lib/favicons.php b/lib/favicons.php index 53cc9f759..a9ebf9f30 100644 --- a/lib/favicons.php +++ b/lib/favicons.php @@ -75,7 +75,7 @@ function searchFavicon(string &$url): string { $links = $xpath->query('//link[@href][translate(@rel, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="shortcut icon"' . ' or translate(@rel, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")="icon"]'); - if (!$links) { + if (!($links instanceof DOMNodeList)) { return ''; } diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 6bceccc86..ae8744841 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -41,8 +41,7 @@ if (!function_exists('syslog')) { define('STDERR', fopen('php://stderr', 'w')); } function syslog(int $priority, string $message): bool { - // @phpstan-ignore booleanAnd.rightAlwaysTrue - if (COPY_SYSLOG_TO_STDERR && defined('STDERR') && STDERR) { + if (COPY_SYSLOG_TO_STDERR && defined('STDERR') && is_resource(STDERR)) { return fwrite(STDERR, $message . "\n") != false; } return false; @@ -619,9 +618,12 @@ function lazyimg(string $content): string { ) ?? ''; } +/** @return numeric-string */ function uTimeString(): string { $t = @gettimeofday(); - return $t['sec'] . str_pad('' . $t['usec'], 6, '0', STR_PAD_LEFT); + $result = $t['sec'] . str_pad('' . $t['usec'], 6, '0', STR_PAD_LEFT); + /** @var numeric-string @result */ + return $result; } function invalidateHttpCache(string $username = ''): bool { diff --git a/p/api/fever.php b/p/api/fever.php index 9f4766c9e..ec5af189f 100644 --- a/p/api/fever.php +++ b/p/api/fever.php @@ -407,7 +407,7 @@ final class FeverAPI } /** - * @param array<string> $ids + * @param array<numeric-string> $ids */ private function entriesToIdList(array $ids = []): string { return implode(',', array_values($ids)); @@ -424,6 +424,7 @@ final class FeverAPI } /** + * @param numeric-string $id * @return int|false */ private function setItemAsRead(string $id) { @@ -431,6 +432,7 @@ final class FeverAPI } /** + * @param numeric-string $id * @return int|false */ private function setItemAsUnread(string $id) { @@ -438,6 +440,7 @@ final class FeverAPI } /** + * @param numeric-string $id * @return int|false */ private function setItemAsSaved(string $id) { @@ -445,6 +448,7 @@ final class FeverAPI } /** + * @param numeric-string $id * @return int|false */ private function setItemAsUnsaved(string $id) { @@ -527,6 +531,7 @@ final class FeverAPI /** * TODO replace by a dynamic fetch for id <= $before timestamp + * @return numeric-string */ private function convertBeforeToId(int $beforeTimestamp): string { return $beforeTimestamp == 0 ? '0' : $beforeTimestamp . '000000'; diff --git a/p/api/greader.php b/p/api/greader.php index fe6327cb0..71cf40884 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -31,11 +31,15 @@ require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader $ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576) ?: ''; if (PHP_INT_SIZE < 8) { //32-bit + /** @return numeric-string */ function hex2dec(string $hex): string { if (!ctype_xdigit($hex)) return '0'; - return gmp_strval(gmp_init($hex, 16), 10); + $result = gmp_strval(gmp_init($hex, 16), 10); + /** @var numeric-string $result */ + return $result; } } else { //64-bit + /** @return numeric-string */ function hex2dec(string $hex): string { if (!ctype_xdigit($hex)) { return '0'; @@ -794,6 +798,7 @@ final class GReaderAPI { $e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/' } } + /** @var array<numeric-string> $e_ids */ $entryDAO = FreshRSS_Factory::createEntryDao(); $entries = $entryDAO->listByIds($e_ids, $order === 'o' ? 'ASC' : 'DESC'); @@ -822,6 +827,7 @@ final class GReaderAPI { $e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/' } } + /** @var array<numeric-string> $e_ids */ $entryDAO = FreshRSS_Factory::createEntryDao(); $tagDAO = FreshRSS_Factory::createTagDao(); @@ -943,7 +949,10 @@ final class GReaderAPI { self::badRequest(); } - /** @return never */ + /** + * @param numeric-string $olderThanId + * @return never + */ private static function markAllAsRead(string $streamId, string $olderThanId) { $entryDAO = FreshRSS_Factory::createEntryDao(); if (strpos($streamId, 'feed/') === 0) { diff --git a/p/api/pshb.php b/p/api/pshb.php index 18aa0ebbf..9f5b4822c 100644 --- a/p/api/pshb.php +++ b/p/api/pshb.php @@ -67,7 +67,7 @@ if (empty($users)) { } if (!empty($_REQUEST['hub_mode']) && $_REQUEST['hub_mode'] === 'subscribe') { - $leaseSeconds = empty($_REQUEST['hub_lease_seconds']) ? 0 : (int) $_REQUEST['hub_lease_seconds']; + $leaseSeconds = empty($_REQUEST['hub_lease_seconds']) ? 0 : (int)$_REQUEST['hub_lease_seconds']; if ($leaseSeconds > 60) { $hubJson['lease_end'] = time() + $leaseSeconds; } else { diff --git a/p/api/query.php b/p/api/query.php index 20a2d8cdc..c95a2bf43 100644 --- a/p/api/query.php +++ b/p/api/query.php @@ -82,7 +82,7 @@ foreach (FreshRSS_Context::userConf()->queries as $raw_query) { if (Minz_Request::paramString('order') === '') { Minz_Request::_param('order', $query->getOrder()); } - Minz_Request::_param('state', $query->getState()); + Minz_Request::_param('state', (string)$query->getState()); $search = $query->getSearch()->getRawInput(); // Note: we disallow references to user queries in public user search to avoid sniffing internal user queries diff --git a/phpstan-next.neon b/phpstan-next.neon index 0cc7f5eac..aa8dc678a 100644 --- a/phpstan-next.neon +++ b/phpstan-next.neon @@ -2,17 +2,40 @@ includes: - phpstan.neon parameters: - level: 9 + strictRules: + booleansInConditions: true + ignoreErrors: + - '#Only booleans are allowed in (a negated boolean|a ternary operator condition|an elseif condition|an if condition|&&|\|\|), (bool|false|int(<[0-9, max]+>)?|true|null|\|)+ given.*#' excludePaths: analyse: # TODO: Update files below and remove them from this list - - app/Controllers/configureController.php + - app/Controllers/feedController.php - app/Controllers/importExportController.php - - app/Models/DatabaseDAO.php + - app/Controllers/subscriptionController.php + - app/Controllers/tagController.php + - app/FreshRSS.php + - app/Models/CategoryDAO.php + - app/Models/CategoryDAOSQLite.php + - app/Models/DatabaseDAOSQLite.php - app/Models/Entry.php + - app/Models/EntryDAO.php + - app/Models/EntryDAOSQLite.php - app/Models/Feed.php + - app/Models/FeedDAO.php + - app/Models/FeedDAOSQLite.php + - app/Models/FormAuth.php + - app/Models/LogDAO.php + - app/Models/TagDAO.php + - app/Models/Themes.php + - app/Services/ExportService.php - app/Services/ImportService.php - app/views/configure/archiving.phtml - - app/views/helpers/feed/update.phtml - - lib/Minz/Helper.php - - lib/Minz/Request.php + - app/views/configure/queries.phtml + - app/views/configure/query.phtml + - app/views/helpers/stream-footer.phtml + - app/views/stats/repartition.phtml + - app/views/subscription/feed.phtml + - cli/CliOptionsParser.php + - cli/create-user.php + - cli/update-user.php + - lib/lib_rss.php diff --git a/phpstan.neon b/phpstan.neon index 3097afd9c..640e4d9f9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,5 @@ parameters: - # TODO: Increase rule-level https://phpstan.org/user-guide/rule-levels - level: 8 + level: 9 # https://phpstan.org/user-guide/rule-levels phpVersion: 80399 # TODO: Remove line when moving composer.json to PHP 8+ fileExtensions: - php diff --git a/tests/app/Models/SearchTest.php b/tests/app/Models/SearchTest.php index e2dba31a1..e4457fedc 100644 --- a/tests/app/Models/SearchTest.php +++ b/tests/app/Models/SearchTest.php @@ -332,6 +332,11 @@ class SearchTest extends PHPUnit\Framework\TestCase { '(e.title LIKE ? )', ['%(test)%'], ], + [ + 'intitle:\'"hello world"\'', + '(e.title LIKE ? )', + ['%"hello world"%'], + ], ]; } } |