diff options
| author | 2024-12-27 12:12:49 +0100 | |
|---|---|---|
| committer | 2024-12-27 12:12:49 +0100 | |
| commit | b1d24fbdb7d1cc948c946295035dad6df550fb7e (patch) | |
| tree | 7b4365a04097a779659474fbb9281a9661512522 | |
| 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
102 files changed, 1175 insertions, 1011 deletions
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9585c3842..1e1a9635e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,6 +49,9 @@ jobs: - name: PHPStan run: composer run-script phpstan + - name: PHPStan Next + run: composer run-script phpstan-next + # NPM tests - name: Uses Node.js diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php index 8b42e372a..9e27a5a4d 100644 --- a/app/Controllers/categoryController.php +++ b/app/Controllers/categoryController.php @@ -197,7 +197,6 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController { } // Remove related queries. - /** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries */ $queries = remove_query_by_get('c_' . $id, FreshRSS_Context::userConf()->queries); FreshRSS_Context::userConf()->queries = $queries; FreshRSS_Context::userConf()->save(); @@ -239,7 +238,6 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController { // Remove related queries foreach ($feeds as $feed) { - /** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> */ $queries = remove_query_by_get('f_' . $feed->id(), FreshRSS_Context::userConf()->queries); FreshRSS_Context::userConf()->queries = $queries; } diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 55fd48393..5a60daa55 100644 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -176,10 +176,17 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController { FreshRSS_View::appendScript(Minz_Url::display('/scripts/draggable.js?' . @filemtime(PUBLIC_PATH . '/scripts/draggable.js'))); if (Minz_Request::isPost()) { - $params = $_POST; - FreshRSS_Context::userConf()->sharing = $params['share']; - FreshRSS_Context::userConf()->save(); - invalidateHttpCache(); + $share = $_POST['share'] ?? null; + if (is_array($share)) { + $share = array_filter($share, fn($value, $key): bool => + is_string($key) && is_array($value) && + is_array_values_string($value), + ARRAY_FILTER_USE_BOTH); + /** @var array<string,array<string,string>> $share */ + FreshRSS_Context::userConf()->sharing = $share; + FreshRSS_Context::userConf()->save(); + invalidateHttpCache(); + } Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'integration' ]); } @@ -308,7 +315,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController { FreshRSS_View::appendScript(Minz_Url::display('/scripts/draggable.js?' . @filemtime(PUBLIC_PATH . '/scripts/draggable.js'))); if (Minz_Request::isPost()) { - /** @var array<int,array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $params */ + /** @var array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string}> $params */ $params = Minz_Request::paramArray('queries'); $queries = []; @@ -390,7 +397,7 @@ 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(array_map('intval', $params['state'])); } if (empty($params['token']) || !is_string($params['token'])) { $queryParams['token'] = FreshRSS_UserQuery::generateToken($name); @@ -453,9 +460,10 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController { foreach (FreshRSS_Context::userConf()->queries as $key => $query) { $queries[$key] = (new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray(); } - $params = $_GET; + $params = array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY); unset($params['name']); unset($params['rid']); + /** @var array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,shareRss?:bool,shareOpml?:bool,description?:string,imageUrl?:string} $params */ $params['url'] = Minz_Url::display(['params' => $params]); $params['name'] = _t('conf.query.number', count($queries) + 1); $queries[] = (new FreshRSS_UserQuery($params, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray(); diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index 5637bd101..8e5dbaa80 100644 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -162,7 +162,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController { } } } else { - /** @var array<numeric-string> $idArray */ + /** @var list<numeric-string> $idArray */ $idArray = Minz_Request::paramArrayString('id'); $idString = Minz_Request::paramString('id'); if (count($idArray) > 0) { @@ -177,7 +177,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController { $tagsForEntries = $tagDAO->getTagsForEntries($ids) ?: []; $tags = []; foreach ($tagsForEntries as $line) { - $tags['t_' . $line['id_tag']][] = $line['id_entry']; + $tags['t_' . $line['id_tag']][] = (string)$line['id_entry']; } $this->view->tagsForEntries = $tags; } diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 42538153d..efaee8534 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -39,8 +39,8 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController { } /** - * fetch extension list from GitHub - * @return array<array{'name':string,'author':string,'description':string,'version':string,'entrypoint':string,'type':'system'|'user','url':string,'method':string,'directory':string}> + * Fetch extension list from GitHub + * @return list<array{name:string,author:string,description:string,version:string,entrypoint:string,type:'system'|'user',url:string,method:string,directory:string}> */ protected function getAvailableExtensionList(): array { $extensionListUrl = 'https://raw.githubusercontent.com/FreshRSS/Extensions/master/extensions.json'; @@ -76,17 +76,24 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController { // the current implementation for now, unless it becomes too much effort maintain the extension list manually $extensions = []; foreach ($list['extensions'] as $extension) { + if (!is_array($extension)) { + continue; + } if (isset($extension['version']) && is_numeric($extension['version'])) { $extension['version'] = (string)$extension['version']; } - foreach (['author', 'description', 'directory', 'entrypoint', 'method', 'name', 'type', 'url', 'version'] as $key) { - if (empty($extension[$key]) || !is_string($extension[$key])) { + $keys = ['author', 'description', 'directory', 'entrypoint', 'method', 'name', 'type', 'url', 'version']; + $extension = array_intersect_key($extension, array_flip($keys)); // Keep only valid keys + $extension = array_filter($extension, 'is_string'); + foreach ($keys as $key) { + if (empty($extension[$key])) { continue 2; } } if (!in_array($extension['type'], ['system', 'user'], true)) { continue; } + /** @var array{name:string,author:string,description:string,version:string,entrypoint:string,type:'system'|'user',url:string,method:string,directory:string} $extension */ $extensions[] = $extension; } return $extensions; diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 60ee6d579..bf20f0747 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -799,7 +799,6 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController { } $entryDAO = FreshRSS_Factory::createEntryDao(); - /** @var array<array{id_tag:int,id_entry:string}> $applyLabels */ $applyLabels = []; foreach (FreshRSS_Entry::fromTraversable($entryDAO->selectAll($nbNewEntries)) as $entry) { foreach ($labels as $label) { @@ -1003,7 +1002,6 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController { // TODO: Delete old favicon // Remove related queries - /** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries */ $queries = remove_query_by_get('f_' . $feed_id, FreshRSS_Context::userConf()->queries); FreshRSS_Context::userConf()->queries = $queries; FreshRSS_Context::userConf()->save(); diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index afb1cbfec..1436ffc68 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -169,12 +169,13 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { Minz_Request::forward(['c' => 'importExport', 'a' => 'index'], true); } - $file = $_FILES['file']; - $status_file = $file['error']; + $file = $_FILES['file'] ?? null; + $status_file = is_array($file) ? $file['error'] ?? -1 : -1; - if ($status_file !== 0) { - Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file); + if (!is_array($file) || $status_file !== 0 || !is_string($file['name'] ?? null) || !is_string($file['tmp_name'] ?? null)) { + Minz_Log::warning('File cannot be uploaded. Error code: ' . (is_numeric($status_file) ? $status_file : -1)); Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'), [ 'c' => 'importExport', 'a' => 'index' ]); + return; } if (function_exists('set_time_limit')) { @@ -232,33 +233,36 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { private function ttrssXmlToJson(string $xml): string|false { $table = (array)simplexml_load_string($xml, options: LIBXML_NOBLANKS | LIBXML_NOCDATA); $table['items'] = $table['article'] ?? []; + if (!is_array($table['items'])) { + $table['items'] = []; + } unset($table['article']); for ($i = count($table['items']) - 1; $i >= 0; $i--) { $item = (array)($table['items'][$i]); $item = array_filter($item, static fn($v) => // Filter out empty properties, potentially reported as empty objects (is_string($v) && trim($v) !== '') || !empty($v)); - $item['updated'] = isset($item['updated']) ? strtotime($item['updated']) : ''; + $item['updated'] = is_string($item['updated'] ?? null) ? strtotime($item['updated']) : ''; $item['published'] = $item['updated']; $item['content'] = ['content' => $item['content'] ?? '']; - $item['categories'] = isset($item['tag_cache']) ? [$item['tag_cache']] : []; + $item['categories'] = is_string($item['tag_cache'] ?? null) ? [$item['tag_cache']] : []; if (!empty($item['marked'])) { $item['categories'][] = 'user/-/state/com.google/starred'; } if (!empty($item['published'])) { $item['categories'][] = 'user/-/state/com.google/broadcast'; } - if (!empty($item['label_cache'])) { + if (is_string($item['label_cache'] ?? null)) { $labels_cache = json_decode($item['label_cache'], true); if (is_array($labels_cache)) { foreach ($labels_cache as $label_cache) { - if (!empty($label_cache[1]) && is_string($label_cache[1])) { + if (is_array($label_cache) && !empty($label_cache[1]) && is_string($label_cache[1])) { $item['categories'][] = 'user/-/label/' . trim($label_cache[1]); } } } } - $item['alternate'][0]['href'] = $item['link'] ?? ''; + $item['alternate'] = [['href' => $item['link'] ?? '']]; $item['origin'] = [ 'title' => $item['feed_title'] ?? '', 'feedUrl' => $item['feed_url'] ?? '', @@ -290,6 +294,9 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { return false; } $items = $article_object['items'] ?? $article_object; + if (!is_array($items)) { + $items = []; + } $mark_as_read = FreshRSS_Context::userConf()->mark_when['reception'] ? 1 : 0; @@ -302,29 +309,32 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { // First, we check feeds of articles are in DB (and add them if needed). foreach ($items as &$item) { - if (!isset($item['guid']) && isset($item['id'])) { + if (!is_array($item)) { + continue; + } + if (!is_string($item['guid'] ?? null) && is_string($item['id'] ?? null)) { $item['guid'] = $item['id']; } - if (empty($item['guid'])) { + if (!is_string($item['guid'] ?? null)) { continue; } - if (empty($item['origin'])) { + if (!is_array($item['origin'] ?? null)) { $item['origin'] = []; } - if (empty($item['origin']['title']) || trim($item['origin']['title']) === '') { + if (!is_string($item['origin']['title'] ?? null) || trim($item['origin']['title']) === '') { $item['origin']['title'] = 'Import'; } - if (!empty($item['origin']['feedUrl'])) { + if (is_string($item['origin']['feedUrl'] ?? null)) { $feedUrl = $item['origin']['feedUrl']; - } elseif (!empty($item['origin']['streamId']) && str_starts_with($item['origin']['streamId'], 'feed/')) { + } elseif (is_string($item['origin']['streamId'] ?? null) && str_starts_with($item['origin']['streamId'], 'feed/')) { $feedUrl = substr($item['origin']['streamId'], 5); //Google Reader $item['origin']['feedUrl'] = $feedUrl; - } elseif (!empty($item['origin']['htmlUrl'])) { + } elseif (is_string($item['origin']['htmlUrl'] ?? null)) { $feedUrl = $item['origin']['htmlUrl']; } else { $feedUrl = 'http://import.localhost/import.xml'; $item['origin']['feedUrl'] = $feedUrl; - $item['origin']['disable'] = true; + $item['origin']['disable'] = 'true'; } $feed = new FreshRSS_Feed($feedUrl); $feed = $this->feedDAO->searchByUrl($feed->url()); @@ -335,7 +345,8 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { // Oops, no more place! Minz_Log::warning(_t('feedback.sub.feed.over_max', $limits['max_feeds'])); } else { - $feed = $this->addFeedJson($item['origin']); + $origin = array_filter($item['origin'], fn($value, $key): bool => is_string($key) && is_string($value), ARRAY_FILTER_USE_BOTH); + $feed = $this->addFeedJson($origin); } if ($feed === null) { @@ -375,19 +386,24 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { $newGuids = []; $this->entryDAO->beginTransaction(); foreach ($items as &$item) { - if (empty($item['guid']) || empty($article_to_feed[$item['guid']])) { + if (!is_array($item) || empty($item['guid']) || !is_string($item['guid']) || empty($article_to_feed[$item['guid']])) { // Related feed does not exist for this entry, do nothing. continue; } $feed_id = $article_to_feed[$item['guid']]; - $author = $item['author'] ?? ''; + $author = is_string($item['author'] ?? null) ? $item['author'] : ''; $is_starred = null; // null is used to preserve the current state if that item exists and is already starred $is_read = null; - $tags = empty($item['categories']) ? [] : $item['categories']; + $tags = is_array($item['categories'] ?? null) ? $item['categories'] : []; $labels = []; for ($i = count($tags) - 1; $i >= 0; $i--) { - $tag = trim($tags[$i]); + $tag = $tags[$i]; + if (!is_string($tag)) { + unset($tags[$i]); + continue; + } + $tag = trim($tag); if (preg_match('%^user/[A-Za-z0-9_-]+/%', $tag)) { if (preg_match('%^user/[A-Za-z0-9_-]+/state/com.google/starred$%', $tag)) { $is_starred = true; @@ -401,6 +417,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { unset($tags[$i]); } } + $tags = array_values(array_filter($tags, 'is_string')); if ($starred && !$is_starred) { //If the article has no label, mark it as starred (old format) $is_starred = empty($labels); @@ -409,41 +426,38 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController { $is_read = $mark_as_read; } - if (isset($item['alternate'][0]['href'])) { + if (is_array($item['alternate']) && is_array($item['alternate'][0] ?? null) && is_string($item['alternate'][0]['href'] ?? null)) { $url = $item['alternate'][0]['href']; - } elseif (isset($item['url'])) { + } elseif (is_string($item['url'] ?? null)) { $url = $item['url']; //FeedBin } else { $url = ''; } - if (!is_string($url)) { - $url = ''; - } - $title = empty($item['title']) ? $url : $item['title']; + $title = is_string($item['title'] ?? null) ? $item['title'] : $url; - if (isset($item['content']['content']) && is_string($item['content']['content'])) { + if (is_array($item['content'] ?? null) && is_string($item['content']['content'] ?? null)) { $content = $item['content']['content']; - } elseif (isset($item['summary']['content']) && is_string($item['summary']['content'])) { + } elseif (is_array($item['summary']) && is_string($item['summary']['content'] ?? null)) { $content = $item['summary']['content']; - } elseif (isset($item['content']) && is_string($item['content'])) { + } elseif (is_string($item['content'] ?? null)) { $content = $item['content']; //FeedBin } else { $content = ''; } $content = sanitizeHTML($content, $url); - if (!empty($item['published'])) { - $published = '' . $item['published']; - } elseif (!empty($item['timestampUsec'])) { - $published = substr('' . $item['timestampUsec'], 0, -6); - } elseif (!empty($item['updated'])) { - $published = '' . $item['updated']; + if (is_int($item['published'] ?? null) || is_string($item['published'] ?? null)) { + $published = (string)$item['published']; + } elseif (is_int($item['timestampUsec'] ?? null) || is_string($item['timestampUsec'] ?? null)) { + $published = substr((string)$item['timestampUsec'], 0, -6); + } elseif (is_int($item['updated'] ?? null) || is_string($item['updated'] ?? null)) { + $published = (string)$item['updated']; } else { $published = '0'; } if (!ctype_digit($published)) { - $published = '' . strtotime($published); + $published = (string)strtotime($published); } if (strlen($published) > 10) { // Milliseconds, e.g. Feedly $published = substr($published, 0, -3); diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index 70bb25a77..fffc15b2a 100644 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -170,8 +170,10 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController { $this->view->html_url = Minz_Url::display('', 'html', true); $this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title(); + + $queryString = $_SERVER['QUERY_STRING'] ?? ''; $this->view->rss_url = htmlspecialchars( - PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']), ENT_COMPAT, 'UTF-8'); + PUBLIC_TO_INDEX_PATH . '/' . ($queryString === '' || !is_string($queryString) ? '' : '?' . $queryString), ENT_COMPAT, 'UTF-8'); // No layout for RSS output. $this->view->_layout(null); @@ -216,7 +218,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController { Minz_Error::error(404); return; } - $this->view->categories = [ $cat->id() => $cat ]; + $this->view->categories = [ $cat ]; break; case 'f': // We most likely already have the feed object in cache @@ -229,7 +231,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController { return; } } - $this->view->feeds = [ $feed->id() => $feed ]; + $this->view->feeds = [ $feed ]; break; case 's': case 't': diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index 0cbcd0bd0..f7002cba8 100644 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -5,6 +5,7 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController { /** * @var FreshRSS_ViewJavascript + * @phpstan-ignore property.phpDocType */ protected $view; @@ -53,6 +54,10 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController { header('Pragma: no-cache'); $user = $_GET['user'] ?? ''; + if (!is_string($user) || $user === '') { + Minz_Error::error(400); + return; + } FreshRSS_Context::initUser($user); if (FreshRSS_Context::hasUserConf()) { try { diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php index 062603930..ee3df4ea5 100644 --- a/app/Controllers/statsController.php +++ b/app/Controllers/statsController.php @@ -8,6 +8,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController { /** * @var FreshRSS_ViewStats + * @phpstan-ignore property.phpDocType */ protected $view; diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php index c7623d0a4..f6ed00986 100644 --- a/app/Controllers/updateController.php +++ b/app/Controllers/updateController.php @@ -287,8 +287,8 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController { Minz_Log::notice(_t('feedback.update.finished')); Minz_Request::good(_t('feedback.update.finished')); } else { - Minz_Log::error(_t('feedback.update.error', $res)); - Minz_Request::bad(_t('feedback.update.error', $res), [ 'c' => 'update', 'a' => 'index' ]); + Minz_Log::error(_t('feedback.update.error', is_string($res) ? $res : 'unknown')); + Minz_Request::bad(_t('feedback.update.error', is_string($res) ? $res : 'unknown'), [ 'c' => 'update', 'a' => 'index' ]); } } else { $res = false; @@ -321,8 +321,8 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController { 'params' => ['post_conf' => '1'], ], true); } else { - Minz_Log::error(_t('feedback.update.error', $res)); - Minz_Request::bad(_t('feedback.update.error', $res), [ 'c' => 'update', 'a' => 'index' ]); + Minz_Log::error(_t('feedback.update.error', is_string($res) ? $res : 'unknown')); + Minz_Request::bad(_t('feedback.update.error', is_string($res) ? $res : 'unknown'), [ 'c' => 'update', 'a' => 'index' ]); } } } diff --git a/app/Mailers/UserMailer.php b/app/Mailers/UserMailer.php index 4d657bf69..e319cfbb4 100644 --- a/app/Mailers/UserMailer.php +++ b/app/Mailers/UserMailer.php @@ -8,6 +8,7 @@ class FreshRSS_User_Mailer extends Minz_Mailer { /** * @var FreshRSS_View + * @phpstan-ignore property.phpDocType */ protected $view; 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; } diff --git a/app/Utils/dotNotationUtil.php b/app/Utils/dotNotationUtil.php index 620ed7db1..77ae96c30 100644 --- a/app/Utils/dotNotationUtil.php +++ b/app/Utils/dotNotationUtil.php @@ -65,6 +65,7 @@ final class FreshRSS_dotNotation_Util * Determine if the given key exists in the provided array. * * @param \ArrayAccess<string,mixed>|array<string,mixed>|mixed $array + * @phpstan-assert-if-true \ArrayAccess<string,mixed>|array<string,mixed> $array */ private static function exists($array, string $key): bool { if ($array instanceof \ArrayAccess) { @@ -85,7 +86,7 @@ final class FreshRSS_dotNotation_Util * mapping fields from the JSON object into RSS equivalents * according to the dot-separated paths * - * @param array<string> $jf json feed + * @param array<int|string,mixed> $jf json feed * @param string $feedSourceUrl the source URL for the feed * @param array<string,string> $dotNotation dot notation to map JSON into RSS * @param string $defaultRssTitle Default title of the RSS feed, if not already provided in dotNotation `feedTitle` diff --git a/app/actualize_script.php b/app/actualize_script.php index e55c1e080..d0ca72271 100755 --- a/app/actualize_script.php +++ b/app/actualize_script.php @@ -1,6 +1,6 @@ #!/usr/bin/env php <?php -// declare(strict_types=1); // Need to wait for PHP 8+ due to https://php.net/ob-implicit-flush +declare(strict_types=1); require(__DIR__ . '/../cli/_cli.php'); session_cache_limiter(''); diff --git a/app/install.php b/app/install.php index a7b4ef09c..232d24c7c 100644 --- a/app/install.php +++ b/app/install.php @@ -10,7 +10,7 @@ require(LIB_PATH . '/lib_install.php'); Minz_Session::init('FreshRSS'); -if (isset($_GET['step'])) { +if (isset($_GET['step']) && is_numeric($_GET['step'])) { define('STEP', (int)$_GET['step']); } else { define('STEP', 0); @@ -41,7 +41,7 @@ function initTranslate(): void { } function get_best_language(): string { - $accept = empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? '' : $_SERVER['HTTP_ACCEPT_LANGUAGE']; + $accept = empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) || !is_string($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? '' : $_SERVER['HTTP_ACCEPT_LANGUAGE']; return strtolower(substr($accept, 0, 2)); } @@ -102,19 +102,22 @@ function saveStep2(): void { 'bd_prefix' => false, ]); } else { - if (empty($_POST['type']) || - empty($_POST['host']) || - empty($_POST['user']) || - empty($_POST['base'])) { + if (empty($_POST['type']) || !is_string($_POST['type']) || + empty($_POST['host']) || !is_string($_POST['host']) || + empty($_POST['user']) || !is_string($_POST['user']) || + empty($_POST['base']) || !is_string($_POST['base']) || + !is_string($_POST['pass'] ?? null) || !is_string($_POST['prefix'] ?? null) + ) { Minz_Session::_param('bd_error', 'Missing parameters!'); - } - Minz_Session::_params([ + } else { + Minz_Session::_params([ 'bd_base' => substr($_POST['base'], 0, 64), 'bd_host' => $_POST['host'], 'bd_user' => $_POST['user'], 'bd_password' => $_POST['pass'], 'bd_prefix' => substr($_POST['prefix'], 0, 16), ]); + } } // We use dirname to remove the /i part @@ -143,6 +146,9 @@ function saveStep2(): void { $customConfig = include($customConfigPath); if (is_array($customConfig)) { $config_array = array_merge($customConfig, $config_array); + if (!is_string($config_array['default_user'] ?? null)) { + $config_array['default_user'] = '_'; + } } } @@ -157,6 +163,9 @@ function saveStep2(): void { $ok = false; try { + if (!is_string($config_array['default_user'])) { + throw new Exception('Invalid default user name'); + } Minz_User::change($config_array['default_user']); $error = initDb(); Minz_User::change(); @@ -327,11 +336,11 @@ function checkStep3(): array { $form = Minz_Session::paramString('auth_type') != ''; - $defaultUser = empty($_POST['default_user']) ? null : $_POST['default_user']; - if ($defaultUser === null) { + $defaultUser = is_string($_POST['default_user'] ?? null) ? trim($_POST['default_user']) : ''; + if ($defaultUser === '') { $defaultUser = Minz_Session::paramString('default_user') == '' ? '' : Minz_Session::paramString('default_user'); } - $data = is_writable(join_path(USERS_PATH, $defaultUser, 'config.php')); + $data = is_writable(USERS_PATH . '/' . $defaultUser . '/config.php'); return [ 'conf' => $conf ? 'ok' : 'ko', @@ -445,16 +454,15 @@ function getProcessUsername(): string { /* check system environment */ function printStep1(): void { $res = checkRequirements(); - $processUsername = getProcessUsername(); ?> <h2><?= _t('admin.check_install.php') ?></h2> <noscript><p class="alert alert-warn"><span class="alert-head"><?= _t('gen.short.attention') ?></span> <?= _t('install.javascript_is_better') ?></p></noscript> - <?php - $version = function_exists('curl_version') ? curl_version() : []; printStep1Template('php', $res['php'], [PHP_VERSION, FRESHRSS_MIN_PHP_VERSION]); printStep1Template('pdo', $res['pdo']); - printStep1Template('curl', $res['curl'], [$version['version'] ?? '']); + $curlVersion = function_exists('curl_version') ? curl_version() : []; + $curlVersion = is_string($curlVersion['version'] ?? null) ? $curlVersion['version'] : ''; + printStep1Template('curl', $res['curl'], [$curlVersion]); printStep1Template('json', $res['json']); printStep1Template('pcre', $res['pcre']); printStep1Template('ctype', $res['ctype']); @@ -465,6 +473,7 @@ function printStep1(): void { ?> <h2><?= _t('admin.check_install.files') ?></h2> <?php + $processUsername = getProcessUsername(); printStep1Template('data', $res['data'], [DATA_PATH, $processUsername]); printStep1Template('cache', $res['cache'], [CACHE_PATH, $processUsername]); printStep1Template('tmp', $res['tmp'], [TMP_PATH, $processUsername]); @@ -516,7 +525,7 @@ function printStep2(): void { <p class="alert alert-success"><span class="alert-head"><?= _t('gen.short.ok') ?></span> <?= _t('install.bdd.conf.ok') ?></p> <?php } elseif ($s2['conn'] == 'ko') { ?> <p class="alert alert-error"><span class="alert-head"><?= _t('gen.short.damn') ?></span> <?= _t('install.bdd.conf.ko'), - (empty($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']) ?></p> + (empty($_SESSION['bd_error']) || !is_string($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']) ?></p> <?php } ?> <h2><?= _t('install.bdd.conf') ?></h2> @@ -527,19 +536,19 @@ function printStep2(): void { <select name="type" id="type" tabindex="1"> <?php if (extension_loaded('pdo_sqlite')) {?> <option value="sqlite" - <?= isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'sqlite' ? 'selected="selected"' : '' ?>> + <?= ($_SESSION['bd_type'] ?? null) === 'sqlite' ? 'selected="selected"' : '' ?>> SQLite </option> <?php }?> <?php if (extension_loaded('pdo_mysql')) {?> <option value="mysql" - <?= isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql' ? 'selected="selected"' : '' ?>> + <?= ($_SESSION['bd_type'] ?? null) === 'mysql' ? 'selected="selected"' : '' ?>> MySQL / MariaDB </option> <?php }?> <?php if (extension_loaded('pdo_pgsql')) {?> <option value="pgsql" - <?= isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql' ? 'selected="selected"' : '' ?>> + <?= ($_SESSION['bd_type'] ?? null) === 'pgsql' ? 'selected="selected"' : '' ?>> PostgreSQL </option> <?php }?> @@ -548,11 +557,18 @@ function printStep2(): void { </div> <div id="mysql"> + <?php + $bd_base = is_string($_SESSION['bd_base'] ?? null) ? $_SESSION['bd_base'] : null; + $bd_host = is_string($_SESSION['bd_host'] ?? null) ? $_SESSION['bd_host'] : null; + $bd_password = is_string($_SESSION['bd_password'] ?? null) ? $_SESSION['bd_password'] : null; + $bd_prefix = is_string($_SESSION['bd_prefix'] ?? null) ? $_SESSION['bd_prefix'] : null; + $bd_user = is_string($_SESSION['bd_user'] ?? null) ? $_SESSION['bd_user'] : null; + ?> <div class="form-group"> <label class="group-name" for="host"><?= _t('install.bdd.host') ?></label> <div class="group-controls"> <input type="text" id="host" name="host" pattern="[0-9A-Z\/a-z_.\-]{1,64}(:[0-9]{2,5})?" value="<?= - $_SESSION['bd_host'] ?? $system_default_config->db['host'] ?? '' ?>" tabindex="2" /> + $bd_host ?? $system_default_config->db['host'] ?? '' ?>" tabindex="2" /> </div> </div> @@ -560,7 +576,7 @@ function printStep2(): void { <label class="group-name" for="user"><?= _t('install.bdd.username') ?></label> <div class="group-controls"> <input type="text" id="user" name="user" maxlength="64" pattern="[0-9A-Za-z@_.\-]{1,64}" value="<?= - $_SESSION['bd_user'] ?? '' ?>" tabindex="3" /> + $bd_user ?? '' ?>" tabindex="3" /> </div> </div> @@ -569,7 +585,7 @@ function printStep2(): void { <div class="group-controls"> <div class="stick"> <input type="password" id="pass" name="pass" value="<?= - $_SESSION['bd_password'] ?? '' ?>" tabindex="4" autocomplete="off" /> + $bd_password ?? '' ?>" tabindex="4" autocomplete="off" /> <a class="btn toggle-password" data-toggle="pass" tabindex="5"><?= FreshRSS_Themes::icon('key') ?></a> </div> </div> @@ -579,7 +595,7 @@ function printStep2(): void { <label class="group-name" for="base"><?= _t('install.bdd') ?></label> <div class="group-controls"> <input type="text" id="base" name="base" maxlength="64" pattern="[0-9A-Za-z_\-]{1,64}" value="<?= - $_SESSION['bd_base'] ?? '' ?>" tabindex="6" /> + $bd_base ?? '' ?>" tabindex="6" /> </div> </div> @@ -587,7 +603,7 @@ function printStep2(): void { <label class="group-name" for="prefix"><?= _t('install.bdd.prefix') ?></label> <div class="group-controls"> <input type="text" id="prefix" name="prefix" maxlength="16" pattern="[0-9A-Za-z_]{1,16}" value="<?= - $_SESSION['bd_prefix'] ?? $system_default_config->db['prefix'] ?? '' ?>" tabindex="7" /> + $bd_prefix ?? $system_default_config->db['prefix'] ?? '' ?>" tabindex="7" /> </div> </div> </div> @@ -611,7 +627,8 @@ function no_auth(string $auth_type): bool { /* Create default user */ function printStep3(): void { - $auth_type = $_SESSION['auth_type'] ?? ''; + $auth_type = is_string($_SESSION['auth_type'] ?? null) ? $_SESSION['auth_type'] : ''; + $default_user = is_string($_SESSION['default_user'] ?? null) ? $_SESSION['default_user'] : ''; $s3 = checkStep3(); if ($s3['all'] == 'ok') { ?> <p class="alert alert-success"><span class="alert-head"><?= _t('gen.short.ok') ?></span> <?= _t('install.conf.ok') ?></p> @@ -625,7 +642,7 @@ function printStep3(): void { <label class="group-name" for="default_user"><?= _t('install.default_user') ?></label> <div class="group-controls"> <input type="text" id="default_user" name="default_user" autocomplete="username" required="required" size="16" - pattern="<?= FreshRSS_user_Controller::USERNAME_PATTERN ?>" value="<?= $_SESSION['default_user'] ?? '' ?>" + pattern="<?= FreshRSS_user_Controller::USERNAME_PATTERN ?>" value="<?= $default_user ?>" placeholder="<?= httpAuthUser(false) == '' ? 'alice' : httpAuthUser(false) ?>" tabindex="1" /> <p class="help"><?= _i('help') ?> <?= _t('install.default_user.max_char') ?></p> </div> diff --git a/app/views/entry/bookmark.phtml b/app/views/entry/bookmark.phtml index 81647352c..a124df673 100644 --- a/app/views/entry/bookmark.phtml +++ b/app/views/entry/bookmark.phtml @@ -7,7 +7,7 @@ header('Content-Type: application/json; charset=UTF-8'); $url = [ 'c' => Minz_Request::controllerName(), 'a' => Minz_Request::actionName(), - 'params' => $_GET, + 'params' => array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY), ]; $url['params']['is_favorite'] = (Minz_Request::paramTernary('is_favorite') ?? true) ? '0' : '1'; diff --git a/app/views/helpers/category/update.phtml b/app/views/helpers/category/update.phtml index 8d97a6eec..5b81b6737 100644 --- a/app/views/helpers/category/update.phtml +++ b/app/views/helpers/category/update.phtml @@ -116,7 +116,7 @@ <legend><?= _t('sub.category.archiving') ?></legend> <?php $archiving = $this->category->attributeArray('archiving'); - /** @var array<'default'?:bool,'keep_period'?:string,'keep_max'?:int,'keep_min'?:int,'keep_favourites'?:bool,'keep_labels'?:bool,'keep_unreads'?:bool>|null $archiving */ + /** @var array{default?:bool,keep_period?:string,keep_max?:int,keep_min?:int,keep_favourites?:bool,keep_labels?:bool,keep_unreads?:bool}|null $archiving */ if (empty($archiving)) { $archiving = [ 'default' => true ]; } else { diff --git a/app/views/helpers/export/opml.phtml b/app/views/helpers/export/opml.phtml index c37d8c7c4..37e728470 100644 --- a/app/views/helpers/export/opml.phtml +++ b/app/views/helpers/export/opml.phtml @@ -3,7 +3,7 @@ declare(strict_types=1); /** * @param array<FreshRSS_Feed> $feeds - * @return array<array<string,string|bool|int>> + * @return list<array<string,string|bool|int>> */ function feedsToOutlines(array $feeds, bool $excludeMutedFeeds = false): array { $outlines = []; @@ -112,7 +112,9 @@ function feedsToOutlines(array $feeds, bool $excludeMutedFeeds = false): array { if (!empty($curl_params[CURLOPT_HTTPHEADER]) && is_array($curl_params[CURLOPT_HTTPHEADER])) { $headers = ''; foreach ($curl_params[CURLOPT_HTTPHEADER] as $header) { - $headers .= $header . "\n"; + if (is_string($header)) { + $headers .= $header . "\n"; + } } $headers = trim($headers); $outline['frss:CURLOPT_HTTPHEADER'] = $headers; diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml index 6275d5486..7dd71a0da 100644 --- a/app/views/helpers/feed/update.phtml +++ b/app/views/helpers/feed/update.phtml @@ -305,7 +305,7 @@ </div> <?php $archiving = $this->feed->attributeArray('archiving'); - /** @var array<'default'?:bool,'keep_period'?:string,'keep_max'?:int,'keep_min'?:int,'keep_favourites'?:bool,'keep_labels'?:bool,'keep_unreads'?:bool>|null $archiving */ + /** @var array{default?:bool,keep_period?:string,keep_max?:int,keep_min?:int,keep_favourites?:bool,keep_labels?:bool,keep_unreads?:bool}|null $archiving */ if (empty($archiving)) { $archiving = [ 'default' => true ]; } else { diff --git a/app/views/helpers/logs_pagination.phtml b/app/views/helpers/logs_pagination.phtml index 77e3f3c82..b3c56253b 100644 --- a/app/views/helpers/logs_pagination.phtml +++ b/app/views/helpers/logs_pagination.phtml @@ -3,7 +3,7 @@ /** @var FreshRSS_View $this */ $c = Minz_Request::controllerName(); $a = Minz_Request::actionName(); - $params = $_GET; + $params = array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY); ?> <?php if ($this->nbPage > 1) { ?> <nav class="nav-pagination nav-list"> diff --git a/app/views/index/global.phtml b/app/views/index/global.phtml index 72916f1a0..527c0b9c4 100644 --- a/app/views/index/global.phtml +++ b/app/views/index/global.phtml @@ -39,7 +39,7 @@ <main id="stream" class="global<?= $class ?>"> <h1 class="title_hidden"><?= _t('conf.reading.view.global') ?></h1> <?php - $params = $_GET; + $params = array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY); unset($params['c']); unset($params['a']); $url_base = [ diff --git a/app/views/index/logs.phtml b/app/views/index/logs.phtml index ec8bf3881..60b593344 100644 --- a/app/views/index/logs.phtml +++ b/app/views/index/logs.phtml @@ -11,14 +11,14 @@ <h1><?= _t('index.log') ?></h1> <?php - /** @var array<FreshRSS_Log> $items */ + /** @var list<FreshRSS_Log> $items */ $items = $this->logsPaginator->items(); ?> <?php if (!empty($items)) { ?> <form method="post" action="<?= _url('index', 'logs') ?>"> <?php $this->logsPaginator->render('logs_pagination.phtml', 'page'); ?> - + <div id="loglist-wrapper" class="table-wrapper scrollbar-thin"> <table id="loglist"> <thead> @@ -46,7 +46,7 @@ </table> </div> <?php $this->logsPaginator->render('logs_pagination.phtml', 'page'); ?> - + <div class="form-group form-actions"> <input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" /> <input type="hidden" name="clearLogs" /> diff --git a/app/views/stats/index.phtml b/app/views/stats/index.phtml index 3a5db2fb8..31e6e51f9 100644 --- a/app/views/stats/index.phtml +++ b/app/views/stats/index.phtml @@ -102,7 +102,7 @@ * Generate a color palette. * * @param int $count The number of colors to generate. - * @return array<int, string> An array of HSL color strings. + * @return array<int,string> An array of HSL color strings. */ function generateColorPalette(int $count): array { $colors = []; diff --git a/cli/CliOptionsParser.php b/cli/CliOptionsParser.php index 5a2d353db..933575393 100644 --- a/cli/CliOptionsParser.php +++ b/cli/CliOptionsParser.php @@ -129,7 +129,7 @@ abstract class CliOptionsParser { /** * @param array<string> $userInputs - * @return array<string> + * @return list<string> */ private function getAliasesUsed(array $userInputs, string $regex): array { $foundAliases = []; diff --git a/cli/_cli.php b/cli/_cli.php index 9486405aa..b4df51dd8 100755 --- a/cli/_cli.php +++ b/cli/_cli.php @@ -52,7 +52,8 @@ function accessRights(): void { function done(bool $ok = true): never { if (!$ok) { - fwrite(STDERR, (empty($_SERVER['argv'][0]) ? 'Process' : basename($_SERVER['argv'][0])) . ' failed!' . "\n"); + fwrite(STDERR, (isset($_SERVER['argv']) && is_array($_SERVER['argv']) && !empty($_SERVER['argv'][0]) && is_string($_SERVER['argv'][0]) ? + basename($_SERVER['argv'][0]) : 'Process') . ' failed!' . "\n"); } exit($ok ? 0 : 1); } diff --git a/cli/check.translation.php b/cli/check.translation.php index b452054ed..d209383a8 100755 --- a/cli/check.translation.php +++ b/cli/check.translation.php @@ -82,7 +82,7 @@ if (!$isValidated) { * Iterates through all php and phtml files in the whole project and extracts all * translation keys used. * - * @return array<string> + * @return list<string> */ function findUsedTranslations(): array { $directory = new RecursiveDirectoryIterator(__DIR__ . '/..'); @@ -90,6 +90,9 @@ function findUsedTranslations(): array { $regex = new RegexIterator($iterator, '/^.+\.(php|phtml)$/i', RecursiveRegexIterator::GET_MATCH); $usedI18n = []; foreach (array_keys(iterator_to_array($regex)) as $file) { + if (!is_string($file) || $file === '') { + continue; + } $fileContent = file_get_contents($file); if ($fileContent === false) { continue; diff --git a/cli/db-restore.php b/cli/db-restore.php index 6ea6f4a7d..32f43708f 100755 --- a/cli/db-restore.php +++ b/cli/db-restore.php @@ -34,7 +34,7 @@ try { $_SESSION['bd_error'] = $ex->getMessage(); } if (!$ok) { - fail('FreshRSS database error: ' . (empty($_SESSION['bd_error']) ? 'Unknown error' : $_SESSION['bd_error'])); + fail('FreshRSS database error: ' . (is_string($_SESSION['bd_error'] ?? null) ? $_SESSION['bd_error'] : 'Unknown error')); } foreach (listUsers() as $username) { diff --git a/cli/do-install.php b/cli/do-install.php index 8591f2299..cab4b1462 100755 --- a/cli/do-install.php +++ b/cli/do-install.php @@ -100,7 +100,7 @@ $config = [ $customConfigPath = DATA_PATH . '/config.custom.php'; if (file_exists($customConfigPath)) { $customConfig = include($customConfigPath); - if (is_array($customConfig)) { + if (is_array($customConfig) && is_array_keys_string($customConfig)) { $config = array_merge($customConfig, $config); } } @@ -132,8 +132,14 @@ if ((!empty($config['base_url'])) && is_string($config['base_url']) && Minz_Requ $config['pubsubhubbub_enabled'] = true; } +if (!is_array($config['db'])) { + $config['db'] = []; +} $config['db'] = array_merge($config['db'], array_filter($dbValues, static fn($value) => $value !== null)); +if (!is_string($config['db']['type'] ?? null)) { + $config['db']['type'] = ''; +} performRequirementCheck($config['db']['type']); if (file_put_contents(join_path(DATA_PATH, 'config.php'), @@ -162,9 +168,12 @@ try { if (!$ok) { @unlink(join_path(DATA_PATH, 'config.php')); - fail('FreshRSS database error: ' . (empty($_SESSION['bd_error']) ? 'Unknown error' : $_SESSION['bd_error'])); + fail('FreshRSS database error: ' . (is_string($_SESSION['bd_error'] ?? null) ? $_SESSION['bd_error'] : 'Unknown error')); } +if (!is_string($config['default_user'] ?? null)) { + fail('FreshRSS default user not set!'); +} echo 'ℹ️ Remember to create the default user: ', $config['default_user'], "\t", './cli/create-user.php --user ', $config['default_user'], " --password 'password' --more-options\n"; diff --git a/cli/i18n/I18nData.php b/cli/i18n/I18nData.php index 6f841e947..7170765b7 100644 --- a/cli/i18n/I18nData.php +++ b/cli/i18n/I18nData.php @@ -3,6 +3,7 @@ declare(strict_types=1); class I18nData { + /** @var string */ public const REFERENCE_LANGUAGE = 'en'; /** @param array<string,array<string,array<string,I18nValue>>> $data */ @@ -74,22 +75,21 @@ class I18nData { /** * Return the available languages - * @return array<string> + * @return list<string> */ public function getAvailableLanguages(): array { $languages = array_keys($this->data); sort($languages); - return $languages; } /** * Return all available languages without the reference language - * @return array<string> + * @return list<string> */ private function getNonReferenceLanguages(): array { - return array_filter(array_keys($this->data), - static fn(string $value) => static::REFERENCE_LANGUAGE !== $value); + return array_values(array_filter(array_keys($this->data), + static fn(string $value) => static::REFERENCE_LANGUAGE !== $value)); } /** @@ -129,7 +129,7 @@ class I18nData { * Return the siblings for a specified key. * To get the siblings, we need to find all matches with the parent. * - * @return array<string> + * @return list<string> */ private function getSiblings(string $key): array { if (!array_key_exists($this->getFilenamePrefix($key), $this->data[static::REFERENCE_LANGUAGE])) { diff --git a/cli/i18n/I18nFile.php b/cli/i18n/I18nFile.php index 5d310d6bf..6771dfbff 100644 --- a/cli/i18n/I18nFile.php +++ b/cli/i18n/I18nFile.php @@ -4,6 +4,23 @@ declare(strict_types=1); require_once __DIR__ . '/I18nValue.php'; class I18nFile { + + /** + * @param array<mixed,mixed> $array + * @phpstan-assert-if-true array<string,string|array<string,mixed>> $array + */ + public static function is_array_recursive_string(array $array): bool { + foreach ($array as $key => $value) { + if (!is_string($key)) { + return false; + } + if (!is_string($value) && !(is_array($value) && self::is_array_recursive_string($value))) { + return false; + } + } + return true; + } + /** * @return array<string,array<string,array<string,I18nValue>>> */ @@ -45,7 +62,7 @@ class I18nFile { /** * Process the content of an i18n file - * @return array<string,array<string,I18nValue>> + * @return array<string,string|array<string,mixed>> */ private function process(string $filename): array { $fileContent = file_get_contents($filename) ?: []; @@ -71,7 +88,7 @@ class I18nFile { die(1); } - if (is_array($content)) { + if (is_array($content) && self::is_array_recursive_string($content)) { return $content; } @@ -81,7 +98,7 @@ class I18nFile { /** * Flatten an array of translation * - * @param array<string,I18nValue|array<string,I18nValue>> $translation + * @param array<string,I18nValue|string|array<string,I18nValue>|mixed> $translation * @return array<string,I18nValue> */ private function flatten(array $translation, string $prefix = ''): array { @@ -92,9 +109,9 @@ class I18nFile { } foreach ($translation as $key => $value) { - if (is_array($value)) { + if (is_array($value) && is_array_keys_string($value)) { $a += $this->flatten($value, $prefix . $key); - } else { + } elseif (is_string($value) || $value instanceof I18nValue) { $a[$prefix . $key] = new I18nValue($value); } } diff --git a/composer.json b/composer.json index 22b1a82ff..e94707838 100644 --- a/composer.json +++ b/composer.json @@ -57,9 +57,9 @@ "ext-phar": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "phpstan/phpstan": "^1.12", - "phpstan/phpstan-phpunit": "^1.4", - "phpstan/phpstan-strict-rules": "^1.6", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^10", "squizlabs/php_codesniffer": "^3.11" }, @@ -68,8 +68,8 @@ "phtml-lint": "find . -type d -name 'vendor' -prune -o -name '*.phtml' -print0 | xargs -0 -n1 -P4 php -l 1>/dev/null", "phpcs": "phpcs . -s", "phpcbf": "phpcbf . -p -s", - "phpstan": "phpstan analyse --memory-limit 512M .", - "phpstan-next": "phpstan analyse --memory-limit 512M -c phpstan-next.neon .", + "phpstan": "phpstan analyse .", + "phpstan-next": "phpstan analyse -c phpstan-next.neon .", "phpunit": "phpunit --bootstrap ./tests/bootstrap.php --display-notices --display-phpunit-deprecations ./tests", "translations": "cli/manipulate.translation.php -a format", "test": [ @@ -77,7 +77,8 @@ "@phtml-lint", "@phpunit", "@phpcs", - "@phpstan" + "@phpstan", + "@phpstan-next" ], "fix": [ "@translations", diff --git a/composer.lock b/composer.lock index 7bb45a275..daeaa3d29 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c334a3d4e7b54d959f917e9f171db65f", + "content-hash": "4cf78584eba0020d488de0659d55923b", "packages": [], "packages-dev": [ { @@ -245,20 +245,20 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.12", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0" + "reference": "50d276fc3bf1430ec315f2f109bbde2769821524" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", - "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/50d276fc3bf1430ec315f2f109bbde2769821524", + "reference": "50d276fc3bf1430ec315f2f109bbde2769821524", "shasum": "" }, "require": { - "php": "^7.2|^8.0" + "php": "^7.4|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" @@ -299,34 +299,33 @@ "type": "github" } ], - "time": "2024-11-28T22:13:23+00:00" + "time": "2024-12-17T17:14:01+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.4.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "11d4235fbc6313ecbf93708606edfd3222e44949" + "reference": "e32ac656788a5bf3dedda89e6a2cad5643bf1a18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/11d4235fbc6313ecbf93708606edfd3222e44949", - "reference": "11d4235fbc6313ecbf93708606edfd3222e44949", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/e32ac656788a5bf3dedda89e6a2cad5643bf1a18", + "reference": "e32ac656788a5bf3dedda89e6a2cad5643bf1a18", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.4" }, "conflict": { "phpunit/phpunit": "<7.0" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6" }, "type": "phpstan-extension", "extra": { @@ -349,34 +348,33 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.4.1" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/2.0.3" }, - "time": "2024-11-12T12:43:59+00:00" + "time": "2024-12-19T09:14:43+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.6.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "daeec748b53de80a97498462513066834ec28f8b" + "reference": "ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/daeec748b53de80a97498462513066834ec28f8b", - "reference": "daeec748b53de80a97498462513066834ec28f8b", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3", + "reference": "ed6fea0ad4ad9c7e25f3ad2e7c4d420cf1e67fe3", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.12.4" + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0.4" }, "require-dev": { - "nikic/php-parser": "^4.13.0", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-deprecation-rules": "^1.1", - "phpstan/phpstan-phpunit": "^1.0", - "phpunit/phpunit": "^9.5" + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6" }, "type": "phpstan-extension", "extra": { @@ -398,9 +396,9 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.1" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.1" }, - "time": "2024-09-20T14:04:44+00:00" + "time": "2024-12-12T20:21:10+00:00" }, { "name": "phpunit/php-code-coverage", @@ -725,16 +723,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.38", + "version": "10.5.39", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132" + "reference": "4e89eff200b801db58f3d580ad7426431949eaa9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a86773b9e887a67bc53efa9da9ad6e3f2498c132", - "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4e89eff200b801db58f3d580ad7426431949eaa9", + "reference": "4e89eff200b801db58f3d580ad7426431949eaa9", "shasum": "" }, "require": { @@ -744,7 +742,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", @@ -806,7 +804,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.38" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.39" }, "funding": [ { @@ -822,7 +820,7 @@ "type": "tidelift" } ], - "time": "2024-10-28T13:06:21+00:00" + "time": "2024-12-11T10:51:07+00:00" }, { "name": "sebastian/cli-parser", @@ -1742,16 +1740,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.11.1", + "version": "3.11.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "19473c30efe4f7b3cd42522d0b2e6e7f243c6f87" + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/19473c30efe4f7b3cd42522d0b2e6e7f243c6f87", - "reference": "19473c30efe4f7b3cd42522d0b2e6e7f243c6f87", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1368f4a58c3c52114b86b1abe8f4098869cb0079", + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079", "shasum": "" }, "require": { @@ -1818,7 +1816,7 @@ "type": "open_collective" } ], - "time": "2024-11-16T12:02:36+00:00" + "time": "2024-12-11T16:04:26+00:00" }, { "name": "theseer/tokenizer", diff --git a/constants.php b/constants.php index cb1150bf8..96c770788 100644 --- a/constants.php +++ b/constants.php @@ -3,26 +3,38 @@ declare(strict_types=1); //NB: Do not edit; use ./constants.local.php instead. //<Not customisable> +/** @var string */ const FRESHRSS_MIN_PHP_VERSION = '8.1.0'; +/** @var string */ const FRESHRSS_VERSION = '1.25.1-dev'; +/** @var string */ const FRESHRSS_WEBSITE = 'https://freshrss.org'; +/** @var string */ const FRESHRSS_WIKI = 'https://freshrss.github.io/FreshRSS/'; - +/** @var string */ const APP_NAME = 'FreshRSS'; - +/** @var string */ const FRESHRSS_PATH = __DIR__; +/** @var string */ const PUBLIC_PATH = FRESHRSS_PATH . '/p'; +/** @var string */ const PUBLIC_TO_INDEX_PATH = '/i'; +/** @var string */ const INDEX_PATH = PUBLIC_PATH . PUBLIC_TO_INDEX_PATH; +/** @var string */ const PUBLIC_RELATIVE = '..'; +/** @var string */ const LIB_PATH = FRESHRSS_PATH . '/lib'; +/** @var string */ const APP_PATH = FRESHRSS_PATH . '/app'; +/** @var string */ const I18N_PATH = APP_PATH . '/i18n'; +/** @var string */ const CORE_EXTENSIONS_PATH = LIB_PATH . '/core-extensions'; +/** @var string */ const TESTS_PATH = FRESHRSS_PATH . '/tests'; //</Not customisable> - if (file_exists(__DIR__ . '/constants.local.php')) { //Include custom / local settings: include(__DIR__ . '/constants.local.php'); diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index b2742e249..b56268b4a 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -45,7 +45,7 @@ class Minz_Configuration { */ public static function load(string $filename): array { $data = @include($filename); - if (is_array($data)) { + if (is_array($data) && is_array_keys_string($data)) { return $data; } else { throw new Minz_FileNotExistException($filename); @@ -117,9 +117,10 @@ class Minz_Configuration { } try { - $this->data = array_replace_recursive( + $overloaded = array_replace_recursive( $this->data, self::load($this->config_filename) ); + $this->data = array_filter($overloaded, 'is_string', ARRAY_FILTER_USE_KEY); } catch (Minz_FileNotExistException $e) { if ($this->default_filename == null) { throw $e; diff --git a/lib/Minz/Error.php b/lib/Minz/Error.php index 74a71de0a..e95fd346c 100644 --- a/lib/Minz/Error.php +++ b/lib/Minz/Error.php @@ -15,13 +15,13 @@ class Minz_Error { /** * Permet de lancer une erreur * @param int $code le type de l'erreur, par défaut 404 (page not found) - * @param string|array<'error'|'warning'|'notice',array<string>> $logs logs d'erreurs découpés de la forme + * @param string|array<'error'|'warning'|'notice',list<string>> $logs logs d'erreurs découpés de la forme * > $logs['error'] * > $logs['warning'] * > $logs['notice'] * @param bool $redirect indique s'il faut forcer la redirection (les logs ne seront pas transmis) */ - public static function error(int $code = 404, $logs = [], bool $redirect = true): void { + public static function error(int $code = 404, string|array $logs = [], bool $redirect = true): void { $logs = self::processLogs($logs); $error_filename = APP_PATH . '/Controllers/errorController.php'; @@ -49,8 +49,8 @@ class Minz_Error { /** * Returns filtered logs - * @param string|array<'error'|'warning'|'notice',array<string>> $logs logs sorted by category (error, warning, notice) - * @return array<string> list of matching logs, without the category, according to environment preferences (production / development) + * @param string|array<'error'|'warning'|'notice',list<string>> $logs logs sorted by category (error, warning, notice) + * @return list<string> list of matching logs, without the category, according to environment preferences (production / development) */ private static function processLogs($logs): array { if (is_string($logs)) { @@ -61,13 +61,13 @@ class Minz_Error { $warning = []; $notice = []; - if (isset($logs['error']) && is_array($logs['error'])) { + if (is_array($logs['error'] ?? null)) { $error = $logs['error']; } - if (isset($logs['warning']) && is_array($logs['warning'])) { + if (is_array($logs['warning'] ?? null)) { $warning = $logs['warning']; } - if (isset($logs['notice']) && is_array($logs['notice'])) { + if (is_array($logs['notice'] ?? null)) { $notice = $logs['notice']; } diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index 69b9c569c..25cbe2091 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -26,7 +26,7 @@ abstract class Minz_Extension { private bool $is_enabled; - /** @var string[] */ + /** @var array<string,string> */ protected array $csp_policies = []; /** @@ -411,11 +411,11 @@ abstract class Minz_Extension { } /** - * @param string[] $policies + * @param array<string,string> $policies */ public function amendCsp(array &$policies): void { foreach ($this->csp_policies as $policy => $source) { - if (array_key_exists($policy, $policies)) { + if (isset($policies[$policy])) { $policies[$policy] .= ' ' . $source; } else { $policies[$policy] = $source; diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 90f005d29..84a6fc09f 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -136,7 +136,6 @@ final class Minz_ExtensionManager { array_walk($list_core_extensions, function (&$s) { $s = CORE_EXTENSIONS_PATH . '/' . $s; }); array_walk($list_thirdparty_extensions, function (&$s) { $s = THIRDPARTY_EXTENSIONS_PATH . '/' . $s; }); - /** @var array<string> */ $list_potential_extensions = array_merge($list_core_extensions, $list_thirdparty_extensions); $system_conf = Minz_Configuration::get('system'); @@ -403,7 +402,10 @@ final class Minz_ExtensionManager { public static function callHookString(string $hook_name): string { $result = ''; foreach (self::$hook_list[$hook_name]['list'] ?? [] as $function) { - $result = $result . call_user_func($function); + $return = call_user_func($function); + if (is_scalar($return)) { + $result .= $return; + } } return $result; } diff --git a/lib/Minz/FrontController.php b/lib/Minz/FrontController.php index 3a86d2d6d..756885ffe 100644 --- a/lib/Minz/FrontController.php +++ b/lib/Minz/FrontController.php @@ -42,7 +42,7 @@ class Minz_FrontController { $url = Minz_Url::build(); $url['params'] = array_merge( empty($url['params']) || !is_array($url['params']) ? [] : $url['params'], - $_POST + array_filter($_POST, 'is_string', ARRAY_FILTER_USE_KEY) ); Minz_Request::forward($url); } catch (Minz_Exception $e) { diff --git a/lib/Minz/Migrator.php b/lib/Minz/Migrator.php index 39c834765..47df98e7f 100644 --- a/lib/Minz/Migrator.php +++ b/lib/Minz/Migrator.php @@ -176,6 +176,7 @@ class Minz_Migrator public function migrations(): array { $migrations = $this->migrations; uksort($migrations, 'strnatcmp'); + /** @var array<string,callable> $migrations */ return $migrations; } @@ -237,7 +238,7 @@ class Minz_Migrator * considered as successful. It is considered as good practice to return * true on success though. * - * @return array<string|bool> Return the results of each executed migration. If an + * @return array<string,bool|string> Return the results of each executed migration. If an * exception was raised in a migration, its result is set to * the exception message. */ @@ -251,7 +252,7 @@ class Minz_Migrator try { $migration_result = $callback(); - $result[$version] = $migration_result; + $result[$version] = (bool)$migration_result; } catch (Exception $e) { $migration_result = false; $result[$version] = $e->getMessage(); diff --git a/lib/Minz/ModelArray.php b/lib/Minz/ModelArray.php index 5a1d286cd..89f7f8da4 100644 --- a/lib/Minz/ModelArray.php +++ b/lib/Minz/ModelArray.php @@ -40,7 +40,7 @@ class Minz_ModelArray { if ($data === false) { throw new Minz_PermissionDeniedException($this->filename); - } elseif (!is_array($data)) { + } elseif (!is_array($data) || !is_array_keys_string($data)) { $data = []; } return $data; diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php index 26b5269a5..86f6df306 100644 --- a/lib/Minz/ModelPdo.php +++ b/lib/Minz/ModelPdo.php @@ -176,8 +176,8 @@ class Minz_ModelPdo { /** * @param array<string,int|string|null> $values - * @phpstan-return ($mode is PDO::FETCH_ASSOC ? array<array<string,int|string|null>>|null : array<int|string|null>|null) - * @return array<array<string,int|string|null>>|array<int|string|null>|null + * @phpstan-return ($mode is PDO::FETCH_ASSOC ? list<array<string,int|string|null>>|null : list<int|string|null>|null) + * @return list<array<string,int|string|null>>|list<int|string|null>|null */ private function fetchAny(string $sql, array $values, int $mode, int $column = 0): ?array { $stm = $this->pdo->prepare($sql); @@ -204,15 +204,15 @@ class Minz_ModelPdo { switch ($mode) { case PDO::FETCH_COLUMN: $res = $stm->fetchAll(PDO::FETCH_COLUMN, $column); + /** @var list<int|string|null> $res */ break; case PDO::FETCH_ASSOC: default: $res = $stm->fetchAll(PDO::FETCH_ASSOC); + /** @var list<array<string,int|string|null>> $res */ break; } - if ($res !== false) { - return $res; - } + return $res; } $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6); @@ -231,7 +231,7 @@ class Minz_ModelPdo { /** * @param array<string,int|string|null> $values - * @return array<array<string,int|string|null>>|null + * @return list<array<string,int|string|null>>|null */ public function fetchAssoc(string $sql, array $values = []): ?array { return $this->fetchAny($sql, $values, PDO::FETCH_ASSOC); @@ -239,7 +239,7 @@ class Minz_ModelPdo { /** * @param array<string,int|string|null> $values - * @return array<int|string|null>|null + * @return list<int|string|null>|null */ public function fetchColumn(string $sql, int $column, array $values = []): ?array { return $this->fetchAny($sql, $values, PDO::FETCH_COLUMN, $column); @@ -257,6 +257,6 @@ class Minz_ModelPdo { Minz_Log::error('SQL error ' . json_encode($stm->errorInfo()) . ' during ' . $sql); return null; } - return isset($columns[0]) ? (string)$columns[0] : null; + return is_scalar($columns[0] ?? null) ? (string)$columns[0] : null; } } diff --git a/lib/Minz/Paginator.php b/lib/Minz/Paginator.php index 727fe42d3..7d6892c67 100644 --- a/lib/Minz/Paginator.php +++ b/lib/Minz/Paginator.php @@ -11,7 +11,7 @@ declare(strict_types=1); */ class Minz_Paginator { /** - * @var array<Minz_Model> tableau des éléments à afficher/gérer + * @var list<Minz_Model> tableau des éléments à afficher/gérer */ private array $items = []; @@ -37,7 +37,7 @@ class Minz_Paginator { /** * Constructeur - * @param array<Minz_Model> $items les éléments à gérer + * @param list<Minz_Model> $items les éléments à gérer */ public function __construct(array $items) { $this->_items($items); @@ -116,10 +116,10 @@ class Minz_Paginator { */ /** * @param bool $all si à true, retourne tous les éléments sans prendre en compte la pagination - * @return array<Minz_Model> + * @return list<Minz_Model> */ public function items(bool $all = false): array { - $array = array (); + $array = []; $nbItems = $this->nbItems(); if ($nbItems <= $this->nbItemsPerPage || $all) { @@ -129,9 +129,9 @@ class Minz_Paginator { $counter = 0; $i = 0; - foreach ($this->items as $key => $item) { + foreach ($this->items as $item) { if ($i >= $begin) { - $array[$key] = $item; + $array[] = $item; $counter++; } if ($counter >= $this->nbItemsPerPage) { @@ -159,7 +159,7 @@ class Minz_Paginator { /** * SETTEURS */ - /** @param array<Minz_Model> $items */ + /** @param list<Minz_Model> $items */ public function _items(?array $items): void { $this->items = $items ?? []; $this->_nbPage(); diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index bddb5a276..f441bcabf 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -69,18 +69,33 @@ class Minz_Request { if (empty(self::$params[$key]) || !is_array(self::$params[$key])) { return []; } - return $plaintext ? self::$params[$key] : Minz_Helper::htmlspecialchars_utf8(self::$params[$key]); + $result = []; + foreach (self::$params[$key] as $k => $v) { + if (is_string($v)) { + $result[$k] = $v; + } elseif (is_array($v)) { + $vs = []; + foreach ($v as $k2 => $v2) { + if (is_string($k2) && (is_string($v2) || is_int($v2) || is_bool($v2))) { + $vs[$k2] = $v2; + } + } + $result[$k] = $vs; + } + } + return $plaintext ? $result : Minz_Helper::htmlspecialchars_utf8($result); } /** * @param bool $plaintext `true` to return special characters without any escaping (unsafe), `false` (default) to XML-encode them - * @return array<string> + * @return list<string> */ public static function paramArrayString(string $key, bool $plaintext = false): array { if (empty(self::$params[$key]) || !is_array(self::$params[$key])) { return []; } $result = array_filter(self::$params[$key], 'is_string'); + $result = array_values($result); return $plaintext ? $result : Minz_Helper::htmlspecialchars_utf8($result); } @@ -143,7 +158,7 @@ class Minz_Request { * character is used to break the text into lines. This method is well suited to use * to split textarea content. * @param bool $plaintext `true` to return special characters without any escaping (unsafe), `false` (default) to XML-encode them - * @return array<string> + * @return list<string> */ public static function paramTextToArray(string $key, bool $plaintext = false): array { if (isset(self::$params[$key]) && is_string(self::$params[$key])) { @@ -214,7 +229,7 @@ class Minz_Request { * Initialise la Request */ public static function init(): void { - self::_params($_GET); + self::_params(array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY)); self::initJSON(); } @@ -227,8 +242,8 @@ class Minz_Request { * Return true if the request is over HTTPS, false otherwise (HTTP) */ public static function isHttps(): bool { - $header = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''; - if ('' != $header) { + $header = is_string($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PROTO'] : ''; + if ('' !== $header) { return 'https' === strtolower($header); } return 'on' === ($_SERVER['HTTPS'] ?? ''); @@ -250,34 +265,37 @@ class Minz_Request { } private static function extractProtocol(): string { - if (self::isHttps()) { - return 'https'; - } - return 'http'; + return self::isHttps() ? 'https' : 'http'; } private static function extractHost(): string { - if ('' != $host = ($_SERVER['HTTP_X_FORWARDED_HOST'] ?? '')) { + $host = is_string($_SERVER['HTTP_X_FORWARDED_HOST'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : ''; + if ($host !== '') { return parse_url("http://{$host}", PHP_URL_HOST) ?: 'localhost'; } - if ('' != $host = ($_SERVER['HTTP_HOST'] ?? '')) { + $host = is_string($_SERVER['HTTP_HOST'] ?? null) ? $_SERVER['HTTP_HOST'] : ''; + if ($host !== '') { // Might contain a port number, and mind IPv6 addresses return parse_url("http://{$host}", PHP_URL_HOST) ?: 'localhost'; } - if ('' != $host = ($_SERVER['SERVER_NAME'] ?? '')) { + $host = is_string($_SERVER['SERVER_NAME'] ?? null) ? $_SERVER['SERVER_NAME'] : ''; + if ($host !== '') { return $host; } return 'localhost'; } private static function extractPort(): int { - if ('' != $port = ($_SERVER['HTTP_X_FORWARDED_PORT'] ?? '')) { + $port = is_numeric($_SERVER['HTTP_X_FORWARDED_PORT'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PORT'] : ''; + if ($port !== '') { return intval($port); } - if ('' != $proto = ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) { + $proto = is_string($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PROTO'] : ''; + if ($proto !== '') { return 'https' === strtolower($proto) ? 443 : 80; } - if ('' != $port = ($_SERVER['SERVER_PORT'] ?? '')) { + $port = is_numeric($_SERVER['SERVER_PORT'] ?? null) ? $_SERVER['SERVER_PORT'] : ''; + if ($port !== '') { return intval($port); } return self::isHttps() ? 443 : 80; @@ -294,15 +312,16 @@ class Minz_Request { } private static function extractPrefix(): string { - if ('' != $prefix = ($_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? '')) { + $prefix = is_string($_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PREFIX'] : ''; + if ($prefix !== '') { return rtrim($prefix, '/ '); } return ''; } private static function extractPath(): string { - $path = $_SERVER['REQUEST_URI'] ?? ''; - if ($path != '') { + $path = is_string($_SERVER['REQUEST_URI'] ?? null) ? $_SERVER['REQUEST_URI'] : ''; + if ($path !== '') { $path = parse_url($path, PHP_URL_PATH) ?: ''; return substr($path, -1) === '/' ? rtrim($path, '/') : dirname($path); } @@ -356,7 +375,7 @@ class Minz_Request { } private static function requestId(): string { - if (empty($_GET['rid']) || !ctype_xdigit($_GET['rid'])) { + if (!is_string($_GET['rid'] ?? null) || !ctype_xdigit($_GET['rid'])) { $_GET['rid'] = uniqid(); } return $_GET['rid']; @@ -476,7 +495,8 @@ class Minz_Request { } private static function extractContentType(): string { - return strtolower(trim($_SERVER['CONTENT_TYPE'] ?? '')); + $contentType = is_string($_SERVER['CONTENT_TYPE'] ?? null) ? $_SERVER['CONTENT_TYPE'] : ''; + return strtolower(trim($contentType)); } public static function isPost(): bool { @@ -484,10 +504,11 @@ class Minz_Request { } /** - * @return array<string> + * @return list<string> */ public static function getPreferredLanguages(): array { - if (preg_match_all('/(^|,)\s*(?P<lang>[^;,]+)/', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', $matches) > 0) { + $acceptLanguage = is_string($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : ''; + if (preg_match_all('/(^|,)\s*(?P<lang>[^;,]+)/', $acceptLanguage, $matches) > 0) { return $matches['lang']; } return ['en']; diff --git a/lib/Minz/Session.php b/lib/Minz/Session.php index 9977e62f6..bb2c1a817 100644 --- a/lib/Minz/Session.php +++ b/lib/Minz/Session.php @@ -72,7 +72,13 @@ class Minz_Session { if (empty($_SESSION[$key]) || !is_array($_SESSION[$key])) { return []; } - return $_SESSION[$key]; + $result = []; + foreach ($_SESSION[$key] as $k => $v) { + if (is_string($v) || (is_array($v) && is_array_keys_string($v))) { + $result[$k] = $v; + } + } + return $result; } public static function paramTernary(string $key): ?bool { @@ -97,10 +103,7 @@ class Minz_Session { } public static function paramInt(string $key): int { - if (!empty($_SESSION[$key])) { - return intval($_SESSION[$key]); - } - return 0; + return empty($_SESSION[$key]) || !is_numeric($_SESSION[$key]) ? 0 : (int)$_SESSION[$key]; } public static function paramString(string $key): string { @@ -175,10 +178,10 @@ class Minz_Session { public static function getCookieDir(): string { // Get the script_name (e.g. /p/i/index.php) and keep only the path. $cookie_dir = ''; - if (!empty($_SERVER['HTTP_X_FORWARDED_PREFIX'])) { + if (!empty($_SERVER['HTTP_X_FORWARDED_PREFIX']) && is_string($_SERVER['HTTP_X_FORWARDED_PREFIX'])) { $cookie_dir .= rtrim($_SERVER['HTTP_X_FORWARDED_PREFIX'], '/ '); } - $cookie_dir .= empty($_SERVER['REQUEST_URI']) ? '/' : $_SERVER['REQUEST_URI']; + $cookie_dir .= empty($_SERVER['REQUEST_URI']) || !is_string($_SERVER['REQUEST_URI']) ? '/' : $_SERVER['REQUEST_URI']; if (substr($cookie_dir, -1) !== '/') { $cookie_dir = dirname($cookie_dir) . '/'; } @@ -210,7 +213,7 @@ class Minz_Session { } public static function getLongTermCookie(string $name): string { - return $_COOKIE[$name] ?? ''; + return is_string($_COOKIE[$name] ?? null) ? $_COOKIE[$name] : ''; } } diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index 08b3c91e8..58bf3f424 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -63,7 +63,7 @@ class Minz_Translate { /** * Return the list of available languages. - * @return array<string> containing langs found in different registered paths. + * @return list<string> containing langs found in different registered paths. */ public static function availableLanguages(): array { $list_langs = []; @@ -81,7 +81,7 @@ class Minz_Translate { } } - return array_unique($list_langs); + return array_values(array_unique($list_langs)); } /** @@ -197,7 +197,7 @@ class Minz_Translate { Minz_Log::debug($key . ' is not in a valid format'); $top_level = 'gen'; } else { - $top_level = array_shift($group); + $top_level = array_shift($group) ?? ''; } // If $translates[$top_level] is null it means we have to load the @@ -218,6 +218,9 @@ class Minz_Translate { $level_processed = 0; $translation_value = $key; foreach ($group as $i18n_level) { + if (!is_array($translates)) { + continue; // Not needed. To help PHPStan + } $level_processed++; if (!isset($translates[$i18n_level])) { Minz_Log::debug($key . ' is not a valid key'); @@ -231,10 +234,9 @@ class Minz_Translate { } } - if (is_array($translation_value)) { - if (isset($translation_value['_'])) { - $translation_value = $translation_value['_']; - } else { + if (!is_string($translation_value)) { + $translation_value = is_array($translation_value) ? ($translation_value['_'] ?? null) : null; + if (!is_string($translation_value)) { Minz_Log::debug($key . ' is not a valid key'); return $key; } diff --git a/lib/Minz/Url.php b/lib/Minz/Url.php index 3948414d8..811f0fff7 100644 --- a/lib/Minz/Url.php +++ b/lib/Minz/Url.php @@ -145,10 +145,16 @@ class Minz_Url { * @return array{c?:string,a?:string,params?:array<string,string>} URL representation */ public static function build(): array { + $get = []; + foreach ($_GET as $key => $value) { + if (is_string($key) && is_string($value)) { + $get[$key] = $value; + } + } $url = [ - 'c' => $_GET['c'] ?? Minz_Request::defaultControllerName(), - 'a' => $_GET['a'] ?? Minz_Request::defaultActionName(), - 'params' => $_GET, + 'c' => is_string($_GET['c'] ?? null) ? $_GET['c'] : Minz_Request::defaultControllerName(), + 'a' => is_string($_GET['a'] ?? null) ? $_GET['a'] : Minz_Request::defaultActionName(), + 'params' => $get, ]; // post-traitement @@ -166,7 +172,7 @@ function _url(string $controller, string $action, int|string ...$args): string|f return false; } - $params = array (); + $params = []; for ($i = 0; $i < $nb_args; $i += 2) { $arg = '' . $args[$i]; $params[$arg] = '' . $args[$i + 1]; diff --git a/lib/Minz/View.php b/lib/Minz/View.php index f7dceef0a..01e8501b0 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -19,11 +19,11 @@ class Minz_View { /** @var array<string> */ private static array $base_pathnames = [APP_PATH]; private static string $title = ''; - /** @var array<array{'media':string,'url':string}> */ + /** @var array<array{media:string,url:string}> */ private static array $styles = []; - /** @var array<array{'url':string,'id':string,'defer':bool,'async':bool}> */ + /** @var array<array{url:string,id:string,defer:bool,async:bool}> */ private static array $scripts = []; - /** @var string|array{'dark'?:string,'light'?:string,'default'?:string} */ + /** @var string|array{dark?:string,light?:string,default?:string} */ private static $themeColors; /** @var array<string,mixed> */ private static array $params = []; @@ -245,7 +245,7 @@ class Minz_View { } /** - * @param string|array{'dark'?:string,'light'?:string,'default'?:string} $themeColors + * @param string|array{dark?:string,light?:string,default?:string} $themeColors */ public static function appendThemeColors($themeColors): void { self::$themeColors = $themeColors; diff --git a/lib/favicons.php b/lib/favicons.php index 5df3682b8..bcb05f4f2 100644 --- a/lib/favicons.php +++ b/lib/favicons.php @@ -22,7 +22,7 @@ function isImgMime(string $content): bool { return $isImage; } -/** @param array<int,int|bool> $curlOptions */ +/** @param array<int,int|bool|string> $curlOptions */ function downloadHttp(string &$url, array $curlOptions = []): string { syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $url); $url2 = checkUrl($url); @@ -61,7 +61,7 @@ function downloadHttp(string &$url, array $curlOptions = []): string { $url = $url2; //Possible redirect } } - return $info['http_code'] == 200 ? $response : ''; + return is_array($info) && $info['http_code'] == 200 ? $response : ''; } function searchFavicon(string &$url): string { @@ -103,6 +103,9 @@ function searchFavicon(string &$url): string { } $iri = $href->get_iri(); + if ($iri == false) { + return ''; + } $favicon = downloadHttp($iri, [CURLOPT_REFERER => $url]); if (isImgMime($favicon)) { return $favicon; @@ -115,7 +118,7 @@ function download_favicon(string $url, string $dest): bool { $url = trim($url); $favicon = searchFavicon($url); if ($favicon == '') { - $rootUrl = preg_replace('%^(https?://[^/]+).*$%i', '$1/', $url); + $rootUrl = preg_replace('%^(https?://[^/]+).*$%i', '$1/', $url) ?? $url; if ($rootUrl != $url) { $url = $rootUrl; $favicon = searchFavicon($url); diff --git a/lib/http-conditional.php b/lib/http-conditional.php index f683d4fbf..c08f72f75 100644 --- a/lib/http-conditional.php +++ b/lib/http-conditional.php @@ -29,7 +29,7 @@ declare(strict_types=1); ?> ``` - Version 1.9, 2023-04-08, https://alexandre.alapetite.fr/doc-alex/php-http-304/ + Version 1.10, 2024-12-22, https://alexandre.alapetite.fr/doc-alex/php-http-304/ ------------------------------------------------------------------ Written by Alexandre Alapetite in 2004, https://alexandre.alapetite.fr/cv/ @@ -82,8 +82,8 @@ $_sessionMode = false; function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePrivacy = 0, bool $feedMode = false, bool $compression = false, bool $session = false): bool { if (headers_sent()) return false; - if (isset($_SERVER['SCRIPT_FILENAME'])) $scriptName = $_SERVER['SCRIPT_FILENAME']; - elseif (isset($_SERVER['PATH_TRANSLATED'])) $scriptName = $_SERVER['PATH_TRANSLATED']; + if (is_string($_SERVER['SCRIPT_FILENAME'] ?? null)) $scriptName = $_SERVER['SCRIPT_FILENAME']; + elseif (is_string($_SERVER['PATH_TRANSLATED'] ?? null)) $scriptName = $_SERVER['PATH_TRANSLATED']; else return false; if ((!$feedMode) && (($modifScript = (int)filemtime($scriptName)) > $UnixTimeStamp)) @@ -98,7 +98,7 @@ function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePr $dateCacheClient = 'Thu, 10 Jan 1980 20:30:40 GMT'; //rfc2616-sec14.html#sec14.19 //='"0123456789abcdef0123456789abcdef"' - if (isset($_SERVER['QUERY_STRING'])) $myQuery = '?' . $_SERVER['QUERY_STRING']; + if (is_string($_SERVER['QUERY_STRING'] ?? null)) $myQuery = '?' . $_SERVER['QUERY_STRING']; else $myQuery = ''; if ($session && isset($_SESSION)) { global $_sessionMode; @@ -108,13 +108,13 @@ function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePr $etagServer = '"' . md5($scriptName . $myQuery . '#' . $dateLastModif) . '"'; // @phpstan-ignore booleanNot.alwaysTrue - if ((!$is412) && isset($_SERVER['HTTP_IF_MATCH'])) { //rfc2616-sec14.html#sec14.24 + if ((!$is412) && is_string($_SERVER['HTTP_IF_MATCH'] ?? null)) { //rfc2616-sec14.html#sec14.24 $etagsClient = stripslashes($_SERVER['HTTP_IF_MATCH']); $etagsClient = str_ireplace('-gzip', '', $etagsClient); $is412 = (($etagsClient !== '*') && (strpos($etagsClient, $etagServer) === false)); } // @phpstan-ignore booleanAnd.leftAlwaysTrue - if ($is304 && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { //rfc2616-sec14.html#sec14.25 //rfc1945.txt + if ($is304 && is_string($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? null)) { //rfc2616-sec14.html#sec14.25 //rfc1945.txt $nbCond++; $dateCacheClient = $_SERVER['HTTP_IF_MODIFIED_SINCE']; $p = strpos($dateCacheClient, ';'); @@ -122,13 +122,13 @@ function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePr $dateCacheClient = substr($dateCacheClient, 0, $p); $is304 = ($dateCacheClient == $dateLastModif); } - if ($is304 && isset($_SERVER['HTTP_IF_NONE_MATCH'])) { //rfc2616-sec14.html#sec14.26 + if ($is304 && is_string($_SERVER['HTTP_IF_NONE_MATCH'] ?? null)) { //rfc2616-sec14.html#sec14.26 $nbCond++; $etagClient = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']); $etagClient = str_ireplace('-gzip', '', $etagClient); $is304 = (($etagClient === $etagServer) || ($etagClient === '*')); } - if ((!$is412) && isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) { //rfc2616-sec14.html#sec14.28 + if ((!$is412) && is_string($_SERVER['HTTP_IF_UNMODIFIED_SINCE'] ?? null)) { //rfc2616-sec14.html#sec14.28 $dateCacheClient = $_SERVER['HTTP_IF_UNMODIFIED_SINCE']; $p = strpos($dateCacheClient, ';'); if ($p !== false) @@ -200,13 +200,13 @@ function _httpConditionalCallBack(string $buffer, int $mode = 5): string { function httpConditionalRefresh(int $UnixTimeStamp): void { if (headers_sent()) return; - if (isset($_SERVER['SCRIPT_FILENAME'])) $scriptName = $_SERVER['SCRIPT_FILENAME']; - elseif (isset($_SERVER['PATH_TRANSLATED'])) $scriptName = $_SERVER['PATH_TRANSLATED']; + if (is_string($_SERVER['SCRIPT_FILENAME'] ?? null)) $scriptName = $_SERVER['SCRIPT_FILENAME']; + elseif (is_string($_SERVER['PATH_TRANSLATED'] ?? null)) $scriptName = $_SERVER['PATH_TRANSLATED']; else return; $dateLastModif = gmdate('D, d M Y H:i:s \G\M\T', $UnixTimeStamp); - if (isset($_SERVER['QUERY_STRING'])) $myQuery = '?' . $_SERVER['QUERY_STRING']; + if (is_string($_SERVER['QUERY_STRING'] ?? null)) $myQuery = '?' . $_SERVER['QUERY_STRING']; else $myQuery = ''; global $_sessionMode; if ($_sessionMode && isset($_SESSION)) diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 8eb89c578..c93243eff 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -85,6 +85,32 @@ spl_autoload_register('classAutoloader'); //</Auto-loading> /** + * @param array<mixed,mixed> $array + * @phpstan-assert-if-true array<string,mixed> $array + */ +function is_array_keys_string(array $array): bool { + foreach ($array as $key => $value) { + if (!is_string($key)) { + return false; + } + } + return true; +} + +/** + * @param array<mixed,mixed> $array + * @phpstan-assert-if-true array<mixed,string> $array + */ +function is_array_values_string(array $array): bool { + foreach ($array as $value) { + if (!is_string($value)) { + return false; + } + } + return true; +} + +/** * Memory efficient replacement of `echo json_encode(...)` * @param array<mixed>|mixed $json * @param int $optimisationDepth Number of levels for which to perform memory optimisation @@ -231,6 +257,7 @@ function timestamptodate(int $t, bool $hour = true): string { * Decode HTML entities but preserve XML entities. */ function html_only_entity_decode(?string $text): string { + /** @var array<string,string>|null $htmlEntitiesOnly */ static $htmlEntitiesOnly = null; if ($htmlEntitiesOnly === null) { $htmlEntitiesOnly = array_flip(array_diff( @@ -252,7 +279,7 @@ function sensitive_log($log): array|string { foreach ($log as $k => $v) { if (in_array($k, ['api_key', 'Passwd', 'T'], true)) { $log[$k] = '██'; - } elseif (is_array($v) || is_string($v)) { + } elseif ((is_array($v) && is_array_keys_string($v)) || is_string($v)) { $log[$k] = sensitive_log($v); } else { return ''; @@ -298,7 +325,9 @@ function customSimplePie(array $attributes = [], array $curl_options = []): \Sim } if (!empty($attributes['curl_params']) && is_array($attributes['curl_params'])) { foreach ($attributes['curl_params'] as $co => $v) { - $curl_options[$co] = $v; + if (is_int($co)) { + $curl_options[$co] = $v; + } } } $simplePie->set_curl_options($curl_options); @@ -366,13 +395,18 @@ function sanitizeHTML(string $data, string $base = '', ?int $maxLength = null): if ($maxLength !== null) { $data = mb_strcut($data, 0, $maxLength, 'UTF-8'); } + /** @var \SimplePie\SimplePie|null $simplePie */ static $simplePie = null; - if ($simplePie == null) { + if ($simplePie === null) { $simplePie = customSimplePie(); $simplePie->enable_cache(false); $simplePie->init(); } - $result = html_only_entity_decode($simplePie->sanitize->sanitize($data, \SimplePie\SimplePie::CONSTRUCT_HTML, $base)); + $sanitized = $simplePie->sanitize->sanitize($data, \SimplePie\SimplePie::CONSTRUCT_HTML, $base); + if (!is_string($sanitized)) { + return ''; + } + $result = html_only_entity_decode($sanitized); if ($maxLength !== null && strlen($result) > $maxLength) { //Sanitizing has made the result too long so try again shorter $data = mb_strcut($result, 0, (2 * $maxLength) - strlen($result) - 2, 'UTF-8'); @@ -504,6 +538,9 @@ function httpGet(string $url, string $cachePath, string $type = 'html', array $a // TODO: Implement HTTP 1.1 conditional GET If-Modified-Since $ch = curl_init(); + if ($ch === false) { + return ''; + } curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_HTTPHEADER => ['Accept: ' . $accept], @@ -592,9 +629,10 @@ function lazyimg(string $content): string { /** @return numeric-string */ function uTimeString(): string { $t = @gettimeofday(); - $result = $t['sec'] . str_pad('' . $t['usec'], 6, '0', STR_PAD_LEFT); - /** @var numeric-string @result */ - return $result; + $sec = is_numeric($t['sec']) ? (int)$t['sec'] : 0; + $usec = is_numeric($t['usec']) ? (int)$t['usec'] : 0; + $result = ((string)$sec) . str_pad((string)$usec, 6, '0', STR_PAD_LEFT); + return ctype_digit($result) ? $result : '0'; } function invalidateHttpCache(string $username = ''): bool { @@ -606,7 +644,7 @@ function invalidateHttpCache(string $username = ''): bool { } /** - * @return array<string> + * @return list<string> */ function listUsers(): array { $final_list = []; @@ -712,9 +750,9 @@ function checkCIDR(string $ip, string $range): bool { * Use CONN_REMOTE_ADDR (if available, to be robust even when using Apache mod_remoteip) or REMOTE_ADDR environment variable to determine the connection IP. */ function connectionRemoteAddress(): string { - $remoteIp = $_SERVER['CONN_REMOTE_ADDR'] ?? ''; + $remoteIp = is_string($_SERVER['CONN_REMOTE_ADDR'] ?? null) ? $_SERVER['CONN_REMOTE_ADDR'] : ''; if ($remoteIp == '') { - $remoteIp = $_SERVER['REMOTE_ADDR'] ?? ''; + $remoteIp = is_string($_SERVER['REMOTE_ADDR'] ?? null) ? $_SERVER['REMOTE_ADDR'] : ''; } if ($remoteIp == 0) { $remoteIp = ''; @@ -752,17 +790,17 @@ function checkTrustedIP(): bool { } function httpAuthUser(bool $onlyTrusted = true): string { - if (!empty($_SERVER['REMOTE_USER'])) { + if (!empty($_SERVER['REMOTE_USER']) && is_string($_SERVER['REMOTE_USER'])) { return $_SERVER['REMOTE_USER']; } - if (!empty($_SERVER['REDIRECT_REMOTE_USER'])) { + if (!empty($_SERVER['REDIRECT_REMOTE_USER']) && is_string($_SERVER['REDIRECT_REMOTE_USER'])) { return $_SERVER['REDIRECT_REMOTE_USER']; } if (!$onlyTrusted || checkTrustedIP()) { - if (!empty($_SERVER['HTTP_REMOTE_USER'])) { + if (!empty($_SERVER['HTTP_REMOTE_USER']) && is_string($_SERVER['HTTP_REMOTE_USER'])) { return $_SERVER['HTTP_REMOTE_USER']; } - if (!empty($_SERVER['HTTP_X_WEBAUTH_USER'])) { + if (!empty($_SERVER['HTTP_X_WEBAUTH_USER']) && is_string($_SERVER['HTTP_X_WEBAUTH_USER'])) { return $_SERVER['HTTP_X_WEBAUTH_USER']; } } @@ -872,14 +910,14 @@ function recursive_unlink(string $dir): bool { /** * Remove queries where $get is appearing. * @param string $get the get attribute which should be removed. - * @param array<int,array<string,string|int>> $queries an array of queries. - * @return array<int,array<string,string|int>> without queries where $get is appearing. + * @param array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string}> $queries an array of queries. + * @return array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string}> without queries where $get is appearing. */ function remove_query_by_get(string $get, array $queries): array { $final_queries = []; - foreach ($queries as $key => $query) { + foreach ($queries as $query) { if (empty($query['get']) || $query['get'] !== $get) { - $final_queries[$key] = $query; + $final_queries[] = $query; } } return $final_queries; @@ -901,7 +939,7 @@ const SHORTCUT_KEYS = [ /** * @param array<string> $shortcuts - * @return array<string> + * @return list<string> */ function getNonStandardShortcuts(array $shortcuts): array { $standard = strtolower(implode(' ', SHORTCUT_KEYS)); @@ -911,7 +949,7 @@ function getNonStandardShortcuts(array $shortcuts): array { return $shortcut !== '' && stripos($standard, $shortcut) === false; }); - return $nonStandard; + return array_values($nonStandard); } function errorMessageInfo(string $errorTitle, string $error = ''): string { diff --git a/p/api/fever.php b/p/api/fever.php index 7bab2ce5d..92523db06 100644 --- a/p/api/fever.php +++ b/p/api/fever.php @@ -31,7 +31,7 @@ Minz_Session::init('FreshRSS', true); // ================================================================================================ // <Debug> -$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1_048_576) ?: '';; +$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1_048_576) ?: ''; function debugInfo(): string { if (function_exists('getallheaders')) { @@ -39,7 +39,7 @@ function debugInfo(): string { } else { //nginx http://php.net/getallheaders#84262 $ALL_HEADERS = []; foreach ($_SERVER as $name => $value) { - if (str_starts_with($name, 'HTTP_')) { + if (is_string($name) && str_starts_with($name, 'HTTP_')) { $ALL_HEADERS[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; } } @@ -120,6 +120,8 @@ final class FeverDAO extends Minz_ModelPdo $entries = []; foreach ($result as $dao) { + /** @var 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 */ $entries[] = FreshRSS_Entry::fromArray($dao); } @@ -151,7 +153,7 @@ final class FeverAPI private function authenticate(): bool { FreshRSS_Context::clearUserConf(); Minz_User::change(); - $feverKey = empty($_POST['api_key']) ? '' : substr(trim($_POST['api_key']), 0, 128); + $feverKey = empty($_POST['api_key']) || !is_string($_POST['api_key']) ? '' : substr(trim($_POST['api_key']), 0, 128); if (ctype_xdigit($feverKey)) { $feverKey = strtolower($feverKey); $username = @file_get_contents(DATA_PATH . '/fever/.key-' . sha1(FreshRSS_Context::systemConf()->salt) . '-' . $feverKey . '.txt', false); @@ -223,9 +225,9 @@ final class FeverAPI $response_arr['saved_item_ids'] = $this->getSavedItemIds(); } - if (isset($_REQUEST['mark'], $_REQUEST['as'], $_REQUEST['id']) && ctype_digit($_REQUEST['id'])) { - $id = (string)$_REQUEST['id']; - $before = (int)($_REQUEST['before'] ?? '0'); + if (is_string($_REQUEST['mark'] ?? null) && is_string($_REQUEST['as'] ?? null) && is_string($_REQUEST['id'] ?? null) && ctype_digit($_REQUEST['id'])) { + $id = $_REQUEST['id']; + $before = is_numeric($_REQUEST['before'] ?? null) ? (int)$_REQUEST['before'] : 0; switch (strtolower($_REQUEST['mark'])) { case 'item': switch ($_REQUEST['as']) { @@ -306,7 +308,7 @@ final class FeverAPI return $lastUpdate; } - /** @return array<array<string,string|int>> */ + /** @return list<array{id:int,favicon_id:int,title:string,url:string,site_url:string,is_spark:int,last_updated_on_time:int}> */ private function getFeeds(): array { $feeds = []; $myFeeds = $this->feedDAO->listFeeds(); @@ -328,7 +330,7 @@ final class FeverAPI return $feeds; } - /** @return array<array<string,int|string>> */ + /** @return list<array{id:int,title:string}> */ private function getGroups(): array { $groups = []; @@ -345,7 +347,7 @@ final class FeverAPI return $groups; } - /** @return array<array<string,int|string>> */ + /** @return list<array{id:int,data:string}> */ private function getFavicons(): array { if (!FreshRSS_Context::hasSystemConf()) { return []; @@ -378,7 +380,7 @@ final class FeverAPI } /** - * @return array<array<string,int|string>> + * @return list<array<string,int|string>> */ private function getFeedsGroup(): array { $groups = []; @@ -401,7 +403,7 @@ final class FeverAPI /** * AFAIK there is no 'hot links' alternative in FreshRSS - * @return array<string> + * @return list<string> */ private function getLinks(): array { return []; @@ -452,46 +454,42 @@ final class FeverAPI return $this->entryDAO->markFavorite($id, false); } - /** @return array<array<string,string|int>> */ + /** @return list<array<string,string|int>> */ private function getItems(): array { $feed_ids = []; $entry_ids = []; $max_id = ''; $since_id = ''; - if (isset($_REQUEST['feed_ids']) || isset($_REQUEST['group_ids'])) { - if (isset($_REQUEST['feed_ids'])) { - $feed_ids = explode(',', $_REQUEST['feed_ids']); - } - - if (isset($_REQUEST['group_ids'])) { - $categoryDAO = FreshRSS_Factory::createCategoryDao(); - $group_ids = explode(',', $_REQUEST['group_ids']); - $feeds = []; - foreach ($group_ids as $id) { - $category = $categoryDAO->searchById((int)$id); //TODO: Transform to SQL query without loop! Consider FreshRSS_CategoryDAO::listCategories(true) - if ($category == null) { - continue; - } - foreach ($category->feeds() as $feed) { - $feeds[] = $feed->id(); - } + if (is_string($_REQUEST['feed_ids'] ?? null)) { + $feed_ids = explode(',', $_REQUEST['feed_ids']); + } elseif (is_string($_REQUEST['group_ids'] ?? null)) { + $categoryDAO = FreshRSS_Factory::createCategoryDao(); + $group_ids = explode(',', $_REQUEST['group_ids']); + $feeds = []; + foreach ($group_ids as $id) { + $category = $categoryDAO->searchById((int)$id); //TODO: Transform to SQL query without loop! Consider FreshRSS_CategoryDAO::listCategories(true) + if ($category == null) { + continue; + } + foreach ($category->feeds() as $feed) { + $feeds[] = $feed->id(); } - $feed_ids = array_unique($feeds); } + $feed_ids = array_unique($feeds); } - if (isset($_REQUEST['max_id'])) { + if (is_string($_REQUEST['max_id'] ?? null)) { // use the max_id argument to request the previous $item_limit items - $max_id = '' . $_REQUEST['max_id']; + $max_id = $_REQUEST['max_id']; if (!ctype_digit($max_id)) { $max_id = ''; } - } elseif (isset($_REQUEST['with_ids'])) { + } elseif (is_string($_REQUEST['with_ids'] ?? null)) { $entry_ids = explode(',', $_REQUEST['with_ids']); - } elseif (isset($_REQUEST['since_id'])) { + } elseif (is_string($_REQUEST['since_id'] ?? null)) { // use the since_id argument to request the next $item_limit items - $since_id = '' . $_REQUEST['since_id']; + $since_id = $_REQUEST['since_id']; if (!ctype_digit($since_id)) { $since_id = ''; } diff --git a/p/api/greader.php b/p/api/greader.php index dd3b78235..9769f66cb 100644 --- a/p/api/greader.php +++ b/p/api/greader.php @@ -28,8 +28,6 @@ Server-side API compatible with Google Reader API layer 2 require(__DIR__ . '/../../constants.php'); 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 { @@ -53,13 +51,13 @@ const JSON_OPTIONS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; function headerVariable(string $headerName, string $varName): string { $header = ''; $upName = 'HTTP_' . strtoupper($headerName); - if (isset($_SERVER[$upName])) { + if (is_string($_SERVER[$upName] ?? null)) { $header = '' . $_SERVER[$upName]; - } elseif (isset($_SERVER['REDIRECT_' . $upName])) { + } elseif (is_string($_SERVER['REDIRECT_' . $upName] ?? null)) { $header = '' . $_SERVER['REDIRECT_' . $upName]; } elseif (function_exists('getallheaders')) { $ALL_HEADERS = getallheaders(); - if (isset($ALL_HEADERS[$headerName])) { + if (is_string($ALL_HEADERS[$headerName] ?? null)) { $header = '' . $ALL_HEADERS[$headerName]; } } @@ -70,47 +68,47 @@ function headerVariable(string $headerName, string $varName): string { return is_string($pairs[$varName]) ? $pairs[$varName] : ''; } -/** @return array<string> */ -function multiplePosts(string $name): array { - //https://bugs.php.net/bug.php?id=51633 - global $ORIGINAL_INPUT; - $inputs = explode('&', $ORIGINAL_INPUT); - $result = []; - $prefix = $name . '='; - $prefixLength = strlen($prefix); - foreach ($inputs as $input) { - if (str_starts_with($input, $prefix)) { - $result[] = urldecode(substr($input, $prefixLength)); +final class GReaderAPI { + + private static string $ORIGINAL_INPUT = ''; + + /** @return list<string> */ + private static function multiplePosts(string $name): array { + //https://bugs.php.net/bug.php?id=51633 + $inputs = explode('&', self::$ORIGINAL_INPUT); + $result = []; + $prefix = $name . '='; + $prefixLength = strlen($prefix); + foreach ($inputs as $input) { + if (str_starts_with($input, $prefix)) { + $result[] = urldecode(substr($input, $prefixLength)); + } } + return $result; } - return $result; -} -function debugInfo(): string { - if (function_exists('getallheaders')) { - $ALL_HEADERS = getallheaders(); - } else { //nginx http://php.net/getallheaders#84262 - $ALL_HEADERS = []; - foreach ($_SERVER as $name => $value) { - if (str_starts_with($name, 'HTTP_')) { - $ALL_HEADERS[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + private static function debugInfo(): string { + if (function_exists('getallheaders')) { + $ALL_HEADERS = getallheaders(); + } else { //nginx http://php.net/getallheaders#84262 + $ALL_HEADERS = []; + foreach ($_SERVER as $name => $value) { + if (is_string($name) && str_starts_with($name, 'HTTP_')) { + $ALL_HEADERS[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } } } + $log = sensitive_log([ + 'date' => date('c'), + 'headers' => $ALL_HEADERS, + '_SERVER' => $_SERVER, + '_GET' => $_GET, + '_POST' => $_POST, + '_COOKIE' => $_COOKIE, + 'INPUT' => self::$ORIGINAL_INPUT, + ]); + return print_r($log, true); } - global $ORIGINAL_INPUT; - $log = sensitive_log([ - 'date' => date('c'), - 'headers' => $ALL_HEADERS, - '_SERVER' => $_SERVER, - '_GET' => $_GET, - '_POST' => $_POST, - '_COOKIE' => $_COOKIE, - 'INPUT' => $ORIGINAL_INPUT, - ]); - return print_r($log, true); -} - -final class GReaderAPI { private static function noContent(): never { header('HTTP/1.1 204 No Content'); @@ -119,7 +117,7 @@ final class GReaderAPI { private static function badRequest(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('HTTP/1.1 400 Bad Request'); header('Content-Type: text/plain; charset=UTF-8'); die('Bad Request!'); @@ -127,7 +125,7 @@ final class GReaderAPI { private static function unauthorized(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('HTTP/1.1 401 Unauthorized'); header('Content-Type: text/plain; charset=UTF-8'); header('Google-Bad-Token: true'); @@ -136,7 +134,7 @@ final class GReaderAPI { private static function internalServerError(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('HTTP/1.1 500 Internal Server Error'); header('Content-Type: text/plain; charset=UTF-8'); die('Internal Server Error!'); @@ -144,7 +142,7 @@ final class GReaderAPI { private static function notImplemented(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('HTTP/1.1 501 Not Implemented'); header('Content-Type: text/plain; charset=UTF-8'); die('Not Implemented!'); @@ -152,7 +150,7 @@ final class GReaderAPI { private static function serviceUnavailable(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('HTTP/1.1 503 Service Unavailable'); header('Content-Type: text/plain; charset=UTF-8'); die('Service Unavailable!'); @@ -160,7 +158,7 @@ final class GReaderAPI { private static function checkCompatibility(): never { Minz_Log::warning(__METHOD__, API_LOG); - Minz_Log::debug(__METHOD__ . ' ' . debugInfo(), API_LOG); + Minz_Log::debug(__METHOD__ . ' ' . self::debugInfo(), API_LOG); header('Content-Type: text/plain; charset=UTF-8'); if (PHP_INT_SIZE < 8 && !function_exists('gmp_init')) { die('FAIL 64-bit or GMP extension! Wrong PHP configuration.'); @@ -365,9 +363,9 @@ final class GReaderAPI { } /** - * @param array<string> $streamNames StreamId(s) to operate on. The parameter may be repeated to edit multiple subscriptions at once - * @param array<string> $titles Title(s) to use for the subscription(s). Each title is associated with the corresponding streamName - * @param 'subscribe'|'unsubscribe'|'edit' $action + * @param list<string> $streamNames StreamId(s) to operate on. The parameter may be repeated to edit multiple subscriptions at once + * @param list<string> $titles Title(s) to use for the subscription(s). Each title is associated with the corresponding streamName + * @param string $action 'subscribe'|'unsubscribe'|'edit' * @param string $add StreamId to add the subscription(s) to (generally a category) * @param string $remove StreamId to remove the subscription(s) from (generally a category) */ @@ -544,8 +542,8 @@ final class GReaderAPI { } /** - * @param array<FreshRSS_Entry> $entries - * @return array<array<string,mixed>> + * @param list<FreshRSS_Entry> $entries + * @return list<array<string,mixed>> */ private static function entriesToArray(array $entries): array { if (empty($entries)) { @@ -668,7 +666,7 @@ final class GReaderAPI { $entryDAO = FreshRSS_Factory::createEntryDao(); $entries = $entryDAO->listWhere($type, $include_target, $state, $order === 'o' ? 'ASC' : 'DESC', $count, 0, $continuation, $searches); - $entries = iterator_to_array($entries); //TODO: Improve + $entries = array_values(iterator_to_array($entries)); //TODO: Improve $items = self::entriesToArray($entries); @@ -754,7 +752,7 @@ final class GReaderAPI { } /** - * @param array<string> $e_ids + * @param list<string> $e_ids */ private static function streamContentsItems(array $e_ids, string $order): never { header('Content-Type: application/json; charset=UTF-8'); @@ -765,11 +763,11 @@ final class GReaderAPI { $e_ids[$i] = hex2dec(basename($e_id)); //Strip prefix 'tag:google.com,2005:reader/item/' } } - /** @var array<numeric-string> $e_ids */ + /** @var list<numeric-string> $e_ids */ $entryDAO = FreshRSS_Factory::createEntryDao(); $entries = $entryDAO->listByIds($e_ids, $order === 'o' ? 'ASC' : 'DESC'); - $entries = iterator_to_array($entries); //TODO: Improve + $entries = array_values(iterator_to_array($entries)); //TODO: Improve $items = self::entriesToArray($entries); @@ -785,9 +783,9 @@ final class GReaderAPI { } /** - * @param array<string> $e_ids IDs of the items to edit - * @param array<string> $as tags to add to all the listed items - * @param array<string> $rs tags to remove from all the listed items + * @param list<string> $e_ids IDs of the items to edit + * @param list<string> $as tags to add to all the listed items + * @param list<string> $rs tags to remove from all the listed items */ private static function editTag(array $e_ids, array $as, array $rs): never { foreach ($e_ids as $i => $e_id) { @@ -795,7 +793,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 */ + /** @var list<numeric-string> $e_ids */ $entryDAO = FreshRSS_Factory::createEntryDao(); $tagDAO = FreshRSS_Factory::createTagDao(); @@ -960,8 +958,6 @@ final class GReaderAPI { } public static function parse(): never { - global $ORIGINAL_INPUT; - header('Access-Control-Allow-Headers: Authorization'); header('Access-Control-Allow-Methods: GET, POST'); header('Access-Control-Allow-Origin: *'); @@ -971,8 +967,8 @@ final class GReaderAPI { } $pathInfo = ''; - if (empty($_SERVER['PATH_INFO'])) { - if (!empty($_SERVER['ORIG_PATH_INFO'])) { + if (empty($_SERVER['PATH_INFO']) || !is_string($_SERVER['PATH_INFO'])) { + if (!empty($_SERVER['ORIG_PATH_INFO']) && is_string($_SERVER['ORIG_PATH_INFO'])) { // Compatibility https://php.net/reserved.variables.server $pathInfo = $_SERVER['ORIG_PATH_INFO']; } @@ -992,7 +988,7 @@ final class GReaderAPI { FreshRSS_Context::initSystem(); //Minz_Log::debug('----------------------------------------------------------------', API_LOG); - //Minz_Log::debug(debugInfo(), API_LOG); + //Minz_Log::debug(self::debugInfo(), API_LOG); if (!FreshRSS_Context::hasSystemConf() || !FreshRSS_Context::systemConf()->api_enabled) { self::serviceUnavailable(); @@ -1013,15 +1009,18 @@ final class GReaderAPI { Minz_Translate::init(); } + self::$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576) ?: ''; + if ($pathInfos[1] === 'accounts') { - if (($pathInfos[2] === 'ClientLogin') && isset($_REQUEST['Email']) && isset($_REQUEST['Passwd'])) { + if (($pathInfos[2] === 'ClientLogin') && is_string($_REQUEST['Email'] ?? null) && is_string($_REQUEST['Passwd'] ?? null)) { self::clientLogin($_REQUEST['Email'], $_REQUEST['Passwd']); } } elseif (isset($pathInfos[3], $pathInfos[4]) && $pathInfos[1] === 'reader' && $pathInfos[2] === 'api' && $pathInfos[3] === '0') { if (Minz_User::name() === null) { self::unauthorized(); } - $timestamp = isset($_GET['ck']) ? (int)$_GET['ck'] : 0; //ck=[unix timestamp] : Use the current Unix time here, helps Google with caching. + // ck=[unix timestamp]: Use the current Unix time here, helps Google with caching + $timestamp = is_numeric($_GET['ck'] ?? null) ? (int)$_GET['ck'] : 0; switch ($pathInfos[4]) { case 'stream': /** @@ -1031,41 +1030,41 @@ final class GReaderAPI { * exclude items from a particular feed (obviously not useful in this request, * but xt appears in other listing requests). */ - $exclude_target = $_GET['xt'] ?? ''; - $filter_target = $_GET['it'] ?? ''; + $exclude_target = is_string($_GET['xt'] ?? null) ? $_GET['xt'] : ''; + $filter_target = is_string($_GET['it'] ?? null) ? $_GET['it'] : ''; //n=[integer] : The maximum number of results to return. - $count = isset($_GET['n']) ? (int)$_GET['n'] : 20; + $count = is_numeric($_GET['n'] ?? null) ? (int)$_GET['n'] : 20; //r=[d|n|o] : Sort order of item results. d or n gives items in descending date order, o in ascending order. - $order = $_GET['r'] ?? 'd'; + $order = is_string($_GET['r'] ?? null) ? $_GET['r'] : 'd'; /** * ot=[unix timestamp] : The time from which you want to retrieve items. * Only items that have been crawled by Google Reader after this time will be returned. */ - $start_time = isset($_GET['ot']) ? (int)$_GET['ot'] : 0; - $stop_time = isset($_GET['nt']) ? (int)$_GET['nt'] : 0; + $start_time = is_numeric($_GET['ot'] ?? null) ? (int)$_GET['ot'] : 0; + $stop_time = is_numeric($_GET['nt'] ?? null) ? (int)$_GET['nt'] : 0; /** * Continuation token. If a StreamContents response does not represent * all items in a timestamp range, it will have a continuation attribute. * The same request can be re-issued with the value of that attribute put * in this parameter to get more items */ - $continuation = isset($_GET['c']) ? trim((string)$_GET['c']) : ''; + $continuation = is_string($_GET['c'] ?? null) ? trim($_GET['c']) : ''; if (!ctype_digit($continuation)) { $continuation = ''; } if (isset($pathInfos[5]) && $pathInfos[5] === 'contents') { - if (!isset($pathInfos[6]) && isset($_GET['s'])) { + if (!isset($pathInfos[6]) && is_string($_GET['s'] ?? null)) { // Compatibility BazQux API https://github.com/bazqux/bazqux-api#fetching-streams $streamIdInfos = explode('/', $_GET['s']); foreach ($streamIdInfos as $streamIdInfo) { $pathInfos[] = $streamIdInfo; } } - if (isset($pathInfos[6]) && isset($pathInfos[7])) { + if (isset($pathInfos[6], $pathInfos[7])) { if ($pathInfos[6] === 'feed') { $include_target = $pathInfos[7]; - if ($include_target != '' && !is_numeric($include_target)) { - $include_target = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI']; + if ($include_target !== '' && !is_numeric($include_target)) { + $include_target = empty($_SERVER['REQUEST_URI']) || !is_string($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI']; if (preg_match('#/reader/api/0/stream/contents/feed/([A-Za-z0-9\'!*()%$_.~+-]+)#', $include_target, $matches) === 1) { $include_target = urldecode($matches[1]); } else { @@ -1095,13 +1094,13 @@ final class GReaderAPI { $count, $order, $filter_target, $exclude_target, $continuation); } } elseif ($pathInfos[5] === 'items') { - if ($pathInfos[6] === 'ids' && isset($_GET['s'])) { + if ($pathInfos[6] === 'ids' && is_string($_GET['s'] ?? null)) { // StreamId for which to fetch the item IDs. // TODO: support multiple streams $streamId = $_GET['s']; self::streamContentsItemsIds($streamId, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation); } elseif ($pathInfos[6] === 'contents' && isset($_POST['i'])) { //FeedMe - $e_ids = multiplePosts('i'); //item IDs + $e_ids = self::multiplePosts('i'); //item IDs self::streamContentsItems($e_ids, $order); } } @@ -1120,8 +1119,8 @@ final class GReaderAPI { self::subscriptionExport(); // Always exits case 'import': - if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && $ORIGINAL_INPUT != '') { - self::subscriptionImport($ORIGINAL_INPUT); + if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST' && self::$ORIGINAL_INPUT != '') { + self::subscriptionImport(self::$ORIGINAL_INPUT); } break; case 'list': @@ -1132,23 +1131,23 @@ final class GReaderAPI { case 'edit': if (isset($_REQUEST['s'], $_REQUEST['ac'])) { // StreamId to operate on. The parameter may be repeated to edit multiple subscriptions at once - $streamNames = empty($_POST['s']) && isset($_GET['s']) ? [$_GET['s']] : multiplePosts('s'); + $streamNames = empty($_POST['s']) && is_string($_GET['s'] ?? null) ? [$_GET['s']] : self::multiplePosts('s'); /* Title to use for the subscription. For the `subscribe` action, * if not specified then the feed’s current title will be used. Can * be used with the `edit` action to rename a subscription */ - $titles = empty($_POST['t']) && isset($_GET['t']) ? [$_GET['t']] : multiplePosts('t'); + $titles = empty($_POST['t']) && is_string($_GET['t'] ?? null) ? [$_GET['t']] : self::multiplePosts('t'); // Action to perform on the given StreamId. Possible values are `subscribe`, `unsubscribe` and `edit` - $action = $_REQUEST['ac']; + $action = is_string($_REQUEST['ac'] ?? null) ? $_REQUEST['ac'] : ''; // StreamId to add the subscription to (generally a user label) // (in FreshRSS, we do not support repeated values since a feed can only be in one category) - $add = $_REQUEST['a'] ?? ''; + $add = is_string($_REQUEST['a'] ?? null) ? $_REQUEST['a'] : ''; // StreamId to remove the subscription from (generally a user label) (in FreshRSS, we do not support repeated values) - $remove = $_REQUEST['r'] ?? ''; + $remove = is_string($_REQUEST['r'] ?? null) ? $_REQUEST['r'] : ''; self::subscriptionEdit($streamNames, $titles, $action, $add, $remove); } break; case 'quickadd': //https://github.com/theoldreader/api - if (isset($_REQUEST['quickadd'])) { + if (is_string($_REQUEST['quickadd'] ?? null)) { self::quickadd($_REQUEST['quickadd']); } break; @@ -1161,35 +1160,35 @@ final class GReaderAPI { self::unreadCount(); // Always exits case 'edit-tag': // https://web.archive.org/web/20200616071132/https://blog.martindoms.com/2010/01/20/using-the-google-reader-api-part-3 - $token = isset($_POST['T']) ? trim($_POST['T']) : ''; + $token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : ''; self::checkToken(FreshRSS_Context::userConf(), $token); // Add (Can be repeated to add multiple tags at once): user/-/state/com.google/read user/-/state/com.google/starred - $as = multiplePosts('a'); + $as = self::multiplePosts('a'); // Remove (Can be repeated to remove multiple tags at once): user/-/state/com.google/read user/-/state/com.google/starred - $rs = multiplePosts('r'); - $e_ids = multiplePosts('i'); //item IDs + $rs = self::multiplePosts('r'); + $e_ids = self::multiplePosts('i'); //item IDs self::editTag($e_ids, $as, $rs); // Always exits case 'rename-tag': //https://github.com/theoldreader/api - $token = isset($_POST['T']) ? trim($_POST['T']) : ''; + $token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : ''; self::checkToken(FreshRSS_Context::userConf(), $token); - $s = $_POST['s'] ?? ''; //user/-/label/Folder - $dest = $_POST['dest'] ?? ''; //user/-/label/NewFolder + $s = is_string($_POST['s'] ?? null) ? trim($_POST['s']) : ''; //user/-/label/Folder + $dest = is_string($_POST['dest'] ?? null) ? trim($_POST['dest']) : ''; //user/-/label/NewFolder self::renameTag($s, $dest); // Always exits case 'disable-tag': //https://github.com/theoldreader/api - $token = isset($_POST['T']) ? trim($_POST['T']) : ''; + $token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : ''; self::checkToken(FreshRSS_Context::userConf(), $token); - $s_s = multiplePosts('s'); + $s_s = self::multiplePosts('s'); foreach ($s_s as $s) { self::disableTag($s); //user/-/label/Folder } // Always exits case 'mark-all-as-read': - $token = isset($_POST['T']) ? trim($_POST['T']) : ''; + $token = is_string($_POST['T'] ?? null) ? trim($_POST['T']) : ''; self::checkToken(FreshRSS_Context::userConf(), $token); - $streamId = trim($_POST['s'] ?? ''); - $ts = trim($_POST['ts'] ?? '0'); //Older than timestamp in nanoseconds + $streamId = is_string($_POST['s'] ?? null) ? trim($_POST['s']) : ''; + $ts = is_string($_POST['ts'] ?? null) ? trim($_POST['ts']) : '0'; //Older than timestamp in nanoseconds if (!ctype_digit($ts)) { self::badRequest(); } diff --git a/p/api/pshb.php b/p/api/pshb.php index f8903d385..6b5bda4b5 100644 --- a/p/api/pshb.php +++ b/p/api/pshb.php @@ -19,7 +19,7 @@ FreshRSS_Context::systemConf()->auth_type = 'none'; // avoid necessity to be log // Minz_Log::debug(print_r(['_SERVER' => $_SERVER, '_GET' => $_GET, '_POST' => $_POST, 'INPUT' => $ORIGINAL_INPUT], true), PSHB_LOG); -$key = isset($_GET['k']) ? substr($_GET['k'], 0, 128) : ''; +$key = isset($_GET['k']) && is_string($_GET['k']) ? substr($_GET['k'], 0, 128) : ''; if (!ctype_xdigit($key)) { header('HTTP/1.1 422 Unprocessable Entity'); die('Invalid feed key format!'); @@ -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']) || !is_numeric($_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 00a04d083..1c2bda32d 100644 --- a/p/api/query.php +++ b/p/api/query.php @@ -134,7 +134,7 @@ switch ($type) { Minz_Error::error(404, "Category {$id} not found!"); die(); } - $view->categories = [ $cat->id() => $cat ]; + $view->categories = [ $cat ]; break; case 'f': // Feed $feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id); @@ -142,7 +142,7 @@ switch ($type) { Minz_Error::error(404, "Feed {$id} not found!"); die(); } - $view->feeds = [ $feed->id() => $feed ]; + $view->feeds = [ $feed ]; $view->categories = []; break; default: @@ -15,7 +15,7 @@ function show_default_favicon(int $cacheSeconds = 3600): void { } $id = $_SERVER['QUERY_STRING'] ?? '0'; -if (!ctype_xdigit($id)) { +if (!is_string($id) || !ctype_xdigit($id)) { $id = '0'; } diff --git a/phpstan-next.neon b/phpstan-next.neon index 4899712fb..df4811a7a 100644 --- a/phpstan-next.neon +++ b/phpstan-next.neon @@ -3,6 +3,20 @@ includes: - vendor/phpstan/phpstan/conf/bleedingEdge.neon parameters: + level: max + checkBenevolentUnionTypes: true # TODO pass + checkImplicitMixed: true # TODO pass + strictRules: + strictArrayFilter: false # TODO pass maybe excludePaths: analyse: # TODO: Update files below and remove them from this list + - app/Models/Entry.php + - app/Models/EntryDAO.php + - app/Models/Feed.php + - app/Models/FeedDAO.php + - app/Models/TagDAO.php + - app/Models/Themes.php + - app/Services/ImportService.php + - app/views/helpers/feed/update.phtml + - cli/CliOptionsParser.php diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 994ee6aec..0f7612d8a 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,5 +1,8 @@ parameters: - level: 9 # https://phpstan.org/user-guide/rule-levels + phpVersion: + min: 80100 # PHP 8.1 + max: 80499 # PHP 8.4 + level: 10 # https://phpstan.org/user-guide/rule-levels fileExtensions: - php - phtml @@ -33,23 +36,17 @@ parameters: - STDOUT - TMP_PATH - USERS_PATH + checkBenevolentUnionTypes: false # TODO pass + checkImplicitMixed: false # TODO pass checkMissingOverrideMethodAttribute: true - reportMaybesInPropertyPhpDocTypes: false + checkTooWideReturnTypesInProtectedAndPublicMethods: true + reportAnyTypeWideningInVarTag: true treatPhpDocTypesAsCertain: false strictRules: - allRules: false - booleansInConditions: true - closureUsesThis: true - disallowedConstructs: false + disallowedEmpty: false disallowedLooseComparison: false - matchingInheritedMethodNames: true - noVariableVariables: true - numericOperandsInArithmeticOperators: true - overwriteVariablesWithLoop: true - requireParentConstructorCall: true - strictCalls: true - switchConditionsMatchingType: true - uselessCast: true + disallowedShortTernary: false + strictArrayFilter: false # TODO pass exceptions: check: missingCheckedExceptionInThrows: false # TODO pass diff --git a/tests/app/Models/CategoryTest.php b/tests/app/Models/CategoryTest.php index f9aa1a280..70bed7477 100644 --- a/tests/app/Models/CategoryTest.php +++ b/tests/app/Models/CategoryTest.php @@ -7,17 +7,17 @@ class CategoryTest extends PHPUnit\Framework\TestCase { public static function test__construct_whenNoParameters_createsObjectWithDefaultValues(): void { $category = new FreshRSS_Category(); - self::assertEquals(0, $category->id()); - self::assertEquals('', $category->name()); + self::assertSame(0, $category->id()); + self::assertSame('', $category->name()); } #[DataProvider('provideValidNames')] public static function test_name_whenValidValue_storesModifiedValue(string $input, string $expected): void { $category = new FreshRSS_Category($input); - self::assertEquals($expected, $category->name()); + self::assertSame($expected, $category->name()); } - /** @return array<array{string,string}> */ + /** @return list<array{string,string}> */ public static function provideValidNames(): array { return [ ['', ''], @@ -60,11 +60,11 @@ class CategoryTest extends PHPUnit\Framework\TestCase { self::assertCount(3, $feeds); $feed = reset($feeds) ?: FreshRSS_Feed::default(); - self::assertEquals('AAA', $feed->name()); + self::assertSame('AAA', $feed->name()); $feed = next($feeds) ?: FreshRSS_Feed::default(); - self::assertEquals('lll', $feed->name()); + self::assertSame('lll', $feed->name()); $feed = next($feeds) ?: FreshRSS_Feed::default(); - self::assertEquals('ZZZ', $feed->name()); + self::assertSame('ZZZ', $feed->name()); /** @var FreshRSS_Feed&PHPUnit\Framework\MockObject\MockObject */ $feed_4 = $this->getMockBuilder(FreshRSS_Feed::class) @@ -79,12 +79,12 @@ class CategoryTest extends PHPUnit\Framework\TestCase { self::assertCount(4, $feeds); $feed = reset($feeds) ?: FreshRSS_Feed::default(); - self::assertEquals('AAA', $feed->name()); + self::assertSame('AAA', $feed->name()); $feed = next($feeds) ?: FreshRSS_Feed::default(); - self::assertEquals('BBB', $feed->name()); + self::assertSame('BBB', $feed->name()); $feed = next($feeds) ?: FreshRSS_Feed::default(); - self::assertEquals('lll', $feed->name()); + self::assertSame('lll', $feed->name()); $feed = next($feeds) ?: FreshRSS_Feed::default(); - self::assertEquals('ZZZ', $feed->name()); + self::assertSame('ZZZ', $feed->name()); } } diff --git a/tests/app/Models/FeedDAOTest.php b/tests/app/Models/FeedDAOTest.php index 31cfc57f9..2c7f6488f 100644 --- a/tests/app/Models/FeedDAOTest.php +++ b/tests/app/Models/FeedDAOTest.php @@ -5,7 +5,7 @@ class FeedDAOTest extends PHPUnit\Framework\TestCase { public static function test_ttl_min(): void { $feed = new FreshRSS_Feed('https://example.net/', false); $feed->_ttl(-5); - self::assertEquals(-5, $feed->ttl(true)); - self::assertEquals(true, $feed->mute()); + self::assertSame(-5, $feed->ttl(true)); + self::assertTrue($feed->mute()); } } diff --git a/tests/app/Models/LogDAOTest.php b/tests/app/Models/LogDAOTest.php index 90261ae55..1950a12c8 100644 --- a/tests/app/Models/LogDAOTest.php +++ b/tests/app/Models/LogDAOTest.php @@ -22,15 +22,14 @@ class LogDAOTest extends TestCase { } public function test_lines_is_array_and_truncate_function_work(): void { - self::assertEquals(USERS_PATH . '/' . Minz_User::INTERNAL_USER . '/' . self::LOG_FILE_TEST, $this->logPath); + self::assertSame(USERS_PATH . '/' . Minz_User::INTERNAL_USER . '/' . self::LOG_FILE_TEST, $this->logPath); $line = $this->logDAO::lines(self::LOG_FILE_TEST); self::assertCount(1, $line); - self::assertInstanceOf(FreshRSS_Log::class, $line[0]); - self::assertEquals('Wed, 08 Feb 2023 15:35:05 +0000', $line[0]->date()); - self::assertEquals('notice', $line[0]->level()); - self::assertEquals("Migration 2019_12_22_FooBar: OK", $line[0]->info()); + self::assertSame('Wed, 08 Feb 2023 15:35:05 +0000', $line[0]->date()); + self::assertSame('notice', $line[0]->level()); + self::assertSame("Migration 2019_12_22_FooBar: OK", $line[0]->info()); $this->logDAO::truncate(self::LOG_FILE_TEST); diff --git a/tests/app/Models/SearchTest.php b/tests/app/Models/SearchTest.php index 04da65d3a..81b4ac1fb 100644 --- a/tests/app/Models/SearchTest.php +++ b/tests/app/Models/SearchTest.php @@ -10,7 +10,7 @@ class SearchTest extends PHPUnit\Framework\TestCase { #[DataProvider('provideEmptyInput')] public static function test__construct_whenInputIsEmpty_getsOnlyNullValues(string $input): void { $search = new FreshRSS_Search($input); - self::assertEquals('', $search->getRawInput()); + self::assertSame('', $search->getRawInput()); self::assertNull($search->getIntitle()); self::assertNull($search->getMinDate()); self::assertNull($search->getMaxDate()); @@ -40,12 +40,12 @@ class SearchTest extends PHPUnit\Framework\TestCase { #[DataProvider('provideIntitleSearch')] public static function test__construct_whenInputContainsIntitle_setsIntitleProperty(string $input, ?array $intitle_value, ?array $search_value): void { $search = new FreshRSS_Search($input); - self::assertEquals($intitle_value, $search->getIntitle()); - self::assertEquals($search_value, $search->getSearch()); + self::assertSame($intitle_value, $search->getIntitle()); + self::assertSame($search_value, $search->getSearch()); } /** - * @return array<array<mixed>> + * @return list<list<mixed>> */ public static function provideIntitleSearch(): array { return [ @@ -77,12 +77,12 @@ class SearchTest extends PHPUnit\Framework\TestCase { #[DataProvider('provideAuthorSearch')] public static function test__construct_whenInputContainsAuthor_setsAuthorValue(string $input, ?array $author_value, ?array $search_value): void { $search = new FreshRSS_Search($input); - self::assertEquals($author_value, $search->getAuthor()); - self::assertEquals($search_value, $search->getSearch()); + self::assertSame($author_value, $search->getAuthor()); + self::assertSame($search_value, $search->getSearch()); } /** - * @return array<array<mixed>> + * @return list<list<mixed>> */ public static function provideAuthorSearch(): array { return [ @@ -114,12 +114,12 @@ class SearchTest extends PHPUnit\Framework\TestCase { #[DataProvider('provideInurlSearch')] public static function test__construct_whenInputContainsInurl_setsInurlValue(string $input, ?array $inurl_value, ?array $search_value): void { $search = new FreshRSS_Search($input); - self::assertEquals($inurl_value, $search->getInurl()); - self::assertEquals($search_value, $search->getSearch()); + self::assertSame($inurl_value, $search->getInurl()); + self::assertSame($search_value, $search->getSearch()); } /** - * @return array<array<mixed>> + * @return list<list<mixed>> */ public static function provideInurlSearch(): array { return [ @@ -137,12 +137,12 @@ class SearchTest extends PHPUnit\Framework\TestCase { #[DataProvider('provideDateSearch')] public static function test__construct_whenInputContainsDate_setsDateValues(string $input, ?int $min_date_value, ?int $max_date_value): void { $search = new FreshRSS_Search($input); - self::assertEquals($min_date_value, $search->getMinDate()); - self::assertEquals($max_date_value, $search->getMaxDate()); + self::assertSame($min_date_value, $search->getMinDate()); + self::assertSame($max_date_value, $search->getMaxDate()); } /** - * @return array<array<mixed>> + * @return list<list<mixed>> */ public static function provideDateSearch(): array { return [ @@ -158,12 +158,12 @@ class SearchTest extends PHPUnit\Framework\TestCase { #[DataProvider('providePubdateSearch')] public static function test__construct_whenInputContainsPubdate_setsPubdateValues(string $input, ?int $min_pubdate_value, ?int $max_pubdate_value): void { $search = new FreshRSS_Search($input); - self::assertEquals($min_pubdate_value, $search->getMinPubdate()); - self::assertEquals($max_pubdate_value, $search->getMaxPubdate()); + self::assertSame($min_pubdate_value, $search->getMinPubdate()); + self::assertSame($max_pubdate_value, $search->getMaxPubdate()); } /** - * @return array<array<mixed>> + * @return list<list<mixed>> */ public static function providePubdateSearch(): array { return [ @@ -183,12 +183,12 @@ class SearchTest extends PHPUnit\Framework\TestCase { #[DataProvider('provideTagsSearch')] public static function test__construct_whenInputContainsTags_setsTagsValue(string $input, ?array $tags_value, ?array $search_value): void { $search = new FreshRSS_Search($input); - self::assertEquals($tags_value, $search->getTags()); - self::assertEquals($search_value, $search->getSearch()); + self::assertSame($tags_value, $search->getTags()); + self::assertSame($search_value, $search->getSearch()); } /** - * @return array<array<string|array<string>|null>> + * @return list<list<string|list<string>|null>> */ public static function provideTagsSearch(): array { return [ @@ -215,19 +215,19 @@ class SearchTest extends PHPUnit\Framework\TestCase { ?int $max_date_value, ?array $intitle_value, ?array $inurl_value, ?int $min_pubdate_value, ?int $max_pubdate_value, ?array $tags_value, ?array $search_value): void { $search = new FreshRSS_Search($input); - self::assertEquals($author_value, $search->getAuthor()); - self::assertEquals($min_date_value, $search->getMinDate()); - self::assertEquals($max_date_value, $search->getMaxDate()); - self::assertEquals($intitle_value, $search->getIntitle()); - self::assertEquals($inurl_value, $search->getInurl()); - self::assertEquals($min_pubdate_value, $search->getMinPubdate()); - self::assertEquals($max_pubdate_value, $search->getMaxPubdate()); - self::assertEquals($tags_value, $search->getTags()); - self::assertEquals($search_value, $search->getSearch()); - self::assertEquals($input, $search->getRawInput()); + self::assertSame($author_value, $search->getAuthor()); + self::assertSame($min_date_value, $search->getMinDate()); + self::assertSame($max_date_value, $search->getMaxDate()); + self::assertSame($intitle_value, $search->getIntitle()); + self::assertSame($inurl_value, $search->getInurl()); + self::assertSame($min_pubdate_value, $search->getMinPubdate()); + self::assertSame($max_pubdate_value, $search->getMaxPubdate()); + self::assertSame($tags_value, $search->getTags()); + self::assertSame($search_value, $search->getSearch()); + self::assertSame($input, $search->getRawInput()); } - /** @return array<array<mixed>> */ + /** @return list<list<mixed>> */ public static function provideMultipleSearch(): array { return [ [ @@ -283,10 +283,10 @@ class SearchTest extends PHPUnit\Framework\TestCase { #[DataProvider('provideAddOrParentheses')] public static function test__addOrParentheses(string $input, string $output): void { - self::assertEquals($output, FreshRSS_BooleanSearch::addOrParentheses($input)); + self::assertSame($output, FreshRSS_BooleanSearch::addOrParentheses($input)); } - /** @return array<array{string,string}> */ + /** @return list<list{string,string}> */ public static function provideAddOrParentheses(): array { return [ ['ab', 'ab'], @@ -302,10 +302,10 @@ class SearchTest extends PHPUnit\Framework\TestCase { #[DataProvider('provideconsistentOrParentheses')] public static function test__consistentOrParentheses(string $input, string $output): void { - self::assertEquals($output, FreshRSS_BooleanSearch::consistentOrParentheses($input)); + self::assertSame($output, FreshRSS_BooleanSearch::consistentOrParentheses($input)); } - /** @return array<array{string,string}> */ + /** @return list<list{string,string}> */ public static function provideconsistentOrParentheses(): array { return [ ['ab cd ef', 'ab cd ef'], @@ -332,24 +332,24 @@ class SearchTest extends PHPUnit\Framework\TestCase { #[DataProvider('provideParentheses')] public function test__parentheses(string $input, string $sql, array $values): void { [$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input)); - self::assertEquals(trim($sql), trim($filterSearch)); - self::assertEquals($values, $filterValues); + self::assertSame(trim($sql), trim($filterSearch)); + self::assertSame($values, $filterValues); } - /** @return array<array<mixed>> */ + /** @return list<list<mixed>> */ public static function provideParentheses(): array { return [ [ 'f:1 (f:2 OR f:3 OR f:4) (f:5 OR (f:6 OR f:7))', ' ((e.id_feed IN (?) )) AND ((e.id_feed IN (?) ) OR (e.id_feed IN (?) ) OR (e.id_feed IN (?) )) AND' . ' (((e.id_feed IN (?) )) OR ((e.id_feed IN (?) ) OR (e.id_feed IN (?) ))) ', - ['1', '2', '3', '4', '5', '6', '7'] + [1, 2, 3, 4, 5, 6, 7] ], [ '#tag Hello OR (author:Alice inurl:example) OR (f:3 intitle:World) OR L:12', " ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?) )) OR ((e.author LIKE ? AND e.link LIKE ? )) OR" . ' ((e.id_feed IN (?) AND e.title LIKE ? )) OR ((e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)) )) ', - ['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', '3', '%World%', '12'] + ['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', 3, '%World%', 12] ], [ '#tag Hello (author:Alice inurl:example) (f:3 intitle:World) label:Bleu', @@ -357,7 +357,7 @@ class SearchTest extends PHPUnit\Framework\TestCase { ' ((e.author LIKE ? AND e.link LIKE ? )) AND' . ' ((e.id_feed IN (?) AND e.title LIKE ? )) AND' . ' ((e.id IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN (?)) )) ', - ['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', '3', '%World%', 'Bleu'] + ['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', 3, '%World%', 'Bleu'] ], [ '!((author:Alice intitle:hello) OR (author:Bob intitle:world))', @@ -478,11 +478,11 @@ class SearchTest extends PHPUnit\Framework\TestCase { */ public function test__regex_postgresql(string $input, string $sql, array $values): void { [$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input)); - self::assertEquals(trim($sql), trim($filterSearch)); - self::assertEquals($values, $filterValues); + self::assertSame(trim($sql), trim($filterSearch)); + self::assertSame($values, $filterValues); } - /** @return array<array<mixed>> */ + /** @return list<list<mixed>> */ public static function provideRegexPostreSQL(): array { return [ [ @@ -551,11 +551,11 @@ class SearchTest extends PHPUnit\Framework\TestCase { FreshRSS_DatabaseDAO::$dummyConnection = true; FreshRSS_DatabaseDAO::setStaticVersion('11.4.3-MariaDB-ubu2404'); [$filterValues, $filterSearch] = FreshRSS_EntryDAO::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input)); - self::assertEquals(trim($sql), trim($filterSearch)); - self::assertEquals($values, $filterValues); + self::assertSame(trim($sql), trim($filterSearch)); + self::assertSame($values, $filterValues); } - /** @return array<array<mixed>> */ + /** @return list<list<mixed>> */ public static function provideRegexMariaDB(): array { return [ [ @@ -584,11 +584,11 @@ class SearchTest extends PHPUnit\Framework\TestCase { FreshRSS_DatabaseDAO::$dummyConnection = true; FreshRSS_DatabaseDAO::setStaticVersion('9.0.1'); [$filterValues, $filterSearch] = FreshRSS_EntryDAO::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input)); - self::assertEquals(trim($sql), trim($filterSearch)); - self::assertEquals($values, $filterValues); + self::assertSame(trim($sql), trim($filterSearch)); + self::assertSame($values, $filterValues); } - /** @return array<array<mixed>> */ + /** @return list<list<mixed>> */ public static function provideRegexMySQL(): array { return [ [ @@ -615,11 +615,11 @@ class SearchTest extends PHPUnit\Framework\TestCase { */ public function test__regex_sqlite(string $input, string $sql, array $values): void { [$filterValues, $filterSearch] = FreshRSS_EntryDAOSQLite::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input)); - self::assertEquals(trim($sql), trim($filterSearch)); - self::assertEquals($values, $filterValues); + self::assertSame(trim($sql), trim($filterSearch)); + self::assertSame($values, $filterValues); } - /** @return array<array<mixed>> */ + /** @return list<list<mixed>> */ public static function provideRegexSQLite(): array { return [ [ diff --git a/tests/app/Models/UserQueryTest.php b/tests/app/Models/UserQueryTest.php index 70483aed1..04fb74fd0 100644 --- a/tests/app/Models/UserQueryTest.php +++ b/tests/app/Models/UserQueryTest.php @@ -10,13 +10,13 @@ class UserQueryTest extends TestCase { public static function test__construct_whenAllQuery_storesAllParameters(): void { $query = ['get' => 'a']; $user_query = new FreshRSS_UserQuery($query, [], []); - self::assertEquals('all', $user_query->getGetType()); + self::assertSame('all', $user_query->getGetType()); } public static function test__construct_whenFavoriteQuery_storesFavoriteParameters(): void { $query = ['get' => 's']; $user_query = new FreshRSS_UserQuery($query, [], []); - self::assertEquals('favorite', $user_query->getGetType()); + self::assertSame('favorite', $user_query->getGetType()); } public function test__construct_whenCategoryQuery_storesCategoryParameters(): void { @@ -29,8 +29,8 @@ class UserQueryTest extends TestCase { ->willReturn($category_name); $query = ['get' => 'c_1']; $user_query = new FreshRSS_UserQuery($query, [1 => $cat], []); - self::assertEquals($category_name, $user_query->getGetName()); - self::assertEquals('category', $user_query->getGetType()); + self::assertSame($category_name, $user_query->getGetName()); + self::assertSame('category', $user_query->getGetType()); } public function test__construct_whenFeedQuery_storesFeedParameters(): void { @@ -53,8 +53,8 @@ class UserQueryTest extends TestCase { ->willReturn([1 => $feed]); $query = ['get' => 'f_1']; $user_query = new FreshRSS_UserQuery($query, [1 => $cat], []); - self::assertEquals($feed_name, $user_query->getGetName()); - self::assertEquals('feed', $user_query->getGetType()); + self::assertSame($feed_name, $user_query->getGetName()); + self::assertSame('feed', $user_query->getGetType()); } public static function test__construct_whenUnknownQuery_doesStoreParameters(): void { @@ -68,28 +68,28 @@ class UserQueryTest extends TestCase { $name = 'some name'; $query = ['name' => $name]; $user_query = new FreshRSS_UserQuery($query, [], []); - self::assertEquals($name, $user_query->getName()); + self::assertSame($name, $user_query->getName()); } public static function test__construct_whenOrder_storesOrder(): void { $order = 'some order'; $query = ['order' => $order]; $user_query = new FreshRSS_UserQuery($query, [], []); - self::assertEquals($order, $user_query->getOrder()); + self::assertSame($order, $user_query->getOrder()); } public static function test__construct_whenState_storesState(): void { $state = FreshRSS_Entry::STATE_NOT_READ | FreshRSS_Entry::STATE_FAVORITE; $query = ['state' => $state]; $user_query = new FreshRSS_UserQuery($query, [], []); - self::assertEquals($state, $user_query->getState()); + self::assertSame($state, $user_query->getState()); } public static function test__construct_whenUrl_storesUrl(): void { $url = 'some url'; $query = ['url' => $url]; $user_query = new FreshRSS_UserQuery($query, [], []); - self::assertEquals($url, $user_query->getUrl()); + self::assertSame($url, $user_query->getUrl()); } public static function testToArray_whenNoData_returnsEmptyArray(): void { @@ -108,7 +108,7 @@ class UserQueryTest extends TestCase { ]; $user_query = new FreshRSS_UserQuery($query, [], []); self::assertCount(6, $user_query->toArray()); - self::assertEquals($query, $user_query->toArray()); + self::assertSame($query, $user_query->toArray()); } public static function testHasSearch_whenSearch_returnsTrue(): void { diff --git a/tests/app/Utils/dotNotationUtilTest.php b/tests/app/Utils/dotNotationUtilTest.php index e49220d30..20d4726af 100644 --- a/tests/app/Utils/dotNotationUtilTest.php +++ b/tests/app/Utils/dotNotationUtilTest.php @@ -41,6 +41,6 @@ class dotNotationUtilTest extends PHPUnit\Framework\TestCase { #[DataProvider('provideJsonDots')] public static function testJsonDots(array $array, string $key, string $expected): void { $value = FreshRSS_dotNotation_Util::get($array, $key); - self::assertEquals($expected, $value); + self::assertSame($expected, $value); } } diff --git a/tests/cli/CliOptionsParserTest.php b/tests/cli/CliOptionsParserTest.php index 046617721..7aa9cdac3 100644 --- a/tests/cli/CliOptionsParserTest.php +++ b/tests/cli/CliOptionsParserTest.php @@ -54,131 +54,131 @@ class CliOptionsParserTest extends TestCase { public static function testInvalidOptionSetWithValueReturnsError(): void { $result = self::runOptionalOptions('--invalid=invalid'); - self::assertEquals(['invalid' => 'unknown option: invalid'], $result->errors); + self::assertSame(['invalid' => 'unknown option: invalid'], $result->errors); } public static function testInvalidOptionSetWithoutValueReturnsError(): void { $result = self::runOptionalOptions('--invalid'); - self::assertEquals(['invalid' => 'unknown option: invalid'], $result->errors); + self::assertSame(['invalid' => 'unknown option: invalid'], $result->errors); } public static function testValidOptionSetWithValidValueAndInvalidOptionSetWithValueReturnsValueForValidOptionAndErrorForInvalidOption(): void { $result = self::runOptionalOptions('--string=string --invalid=invalid'); - self::assertEquals('string', $result->string); - self::assertEquals(['invalid' => 'unknown option: invalid'], $result->errors); + self::assertSame('string', $result->string); + self::assertSame(['invalid' => 'unknown option: invalid'], $result->errors); } public static function testOptionWithValueTypeOfStringSetOnceWithValidValueReturnsValueAsString(): void { $result = self::runOptionalOptions('--string=string'); - self::assertEquals('string', $result->string); + self::assertSame('string', $result->string); } public static function testOptionWithRequiredValueTypeOfIntSetOnceWithValidValueReturnsValueAsInt(): void { $result = self::runOptionalOptions('--int=111'); - self::assertEquals(111, $result->int); + self::assertSame(111, $result->int); } public static function testOptionWithRequiredValueTypeOfBoolSetOnceWithValidValueReturnsValueAsBool(): void { $result = self::runOptionalOptions('--bool=on'); - self::assertEquals(true, $result->bool); + self::assertTrue($result->bool); } public static function testOptionWithValueTypeOfArrayOfStringSetOnceWithValidValueReturnsValueAsArrayOfString(): void { $result = self::runOptionalOptions('--array-of-string=string'); - self::assertEquals(['string'], $result->arrayOfString); + self::assertSame(['string'], $result->arrayOfString); } public static function testOptionWithValueTypeOfStringSetMultipleTimesWithValidValueReturnsLastValueSetAsString(): void { $result = self::runOptionalOptions('--string=first --string=second'); - self::assertEquals('second', $result->string); + self::assertSame('second', $result->string); } public static function testOptionWithValueTypeOfIntSetMultipleTimesWithValidValueReturnsLastValueSetAsInt(): void { $result = self::runOptionalOptions('--int=111 --int=222'); - self::assertEquals(222, $result->int); + self::assertSame(222, $result->int); } public static function testOptionWithValueTypeOfBoolSetMultipleTimesWithValidValueReturnsLastValueSetAsBool(): void { $result = self::runOptionalOptions('--bool=on --bool=off'); - self::assertEquals(false, $result->bool); + self::assertFalse($result->bool); } public static function testOptionWithValueTypeOfArrayOfStringSetMultipleTimesWithValidValueReturnsAllSetValuesAsArrayOfString(): void { $result = self::runOptionalOptions('--array-of-string=first --array-of-string=second'); - self::assertEquals(['first', 'second'], $result->arrayOfString); + self::assertSame(['first', 'second'], $result->arrayOfString); } public static function testOptionWithValueTypeOfIntSetWithInvalidValueReturnsAnError(): void { $result = self::runOptionalOptions('--int=one'); - self::assertEquals(['int' => 'invalid input: int must be an integer'], $result->errors); + self::assertSame(['int' => 'invalid input: int must be an integer'], $result->errors); } public static function testOptionWithValueTypeOfBoolSetWithInvalidValuesReturnsAnError(): void { $result = self::runOptionalOptions('--bool=bad'); - self::assertEquals(['bool' => 'invalid input: bool must be a boolean'], $result->errors); + self::assertSame(['bool' => 'invalid input: bool must be a boolean'], $result->errors); } public static function testOptionWithValueTypeOfIntSetMultipleTimesWithValidAndInvalidValuesReturnsLastValidValueSetAsIntAndError(): void { $result = self::runOptionalOptions('--int=111 --int=one --int=222 --int=two'); - self::assertEquals(222, $result->int); - self::assertEquals(['int' => 'invalid input: int must be an integer'], $result->errors); + self::assertSame(222, $result->int); + self::assertSame(['int' => 'invalid input: int must be an integer'], $result->errors); } public static function testOptionWithValueTypeOfBoolSetMultipleTimesWithWithValidAndInvalidValuesReturnsLastValidValueSetAsBoolAndError(): void { $result = self::runOptionalOptions('--bool=on --bool=good --bool=off --bool=bad'); - self::assertEquals(false, $result->bool); - self::assertEquals(['bool' => 'invalid input: bool must be a boolean'], $result->errors); + self::assertFalse($result->bool); + self::assertSame(['bool' => 'invalid input: bool must be a boolean'], $result->errors); } public static function testNotSetOptionWithDefaultInputReturnsDefaultInput(): void { $result = self::runOptionalOptions(''); - self::assertEquals('default', $result->defaultInput); + self::assertSame('default', $result->defaultInput); } public static function testOptionWithDefaultInputSetWithValidValueReturnsCorrectlyTypedValue(): void { $result = self::runOptionalOptions('--default-input=input'); - self::assertEquals('input', $result->defaultInput); + self::assertSame('input', $result->defaultInput); } public static function testOptionWithOptionalValueSetWithoutValueReturnsEmptyString(): void { $result = self::runOptionalOptions('--optional-value'); - self::assertEquals('', $result->optionalValue); + self::assertSame('', $result->optionalValue); } public static function testOptionWithOptionalValueDefaultSetWithoutValueReturnsOptionalValueDefault(): void { $result = self::runOptionalOptions('--optional-value-with-default'); - self::assertEquals(true, $result->optionalValueWithDefault); + self::assertTrue($result->optionalValueWithDefault); } public static function testNotSetOptionWithOptionalValueDefaultAndDefaultInputReturnsDefaultInput(): void { $result = self::runOptionalOptions(''); - self::assertEquals('default', $result->defaultInputAndOptionalValueWithDefault); + self::assertSame('default', $result->defaultInputAndOptionalValueWithDefault); } public static function testOptionWithOptionalValueDefaultAndDefaultInputSetWithoutValueReturnsOptionalValueDefault(): void { $result = self::runOptionalOptions('--default-input-and-optional-value-with-default'); - self::assertEquals('optional', $result->defaultInputAndOptionalValueWithDefault); + self::assertSame('optional', $result->defaultInputAndOptionalValueWithDefault); } public static function testRequiredOptionNotSetReturnsError(): void { $result = self::runOptionalAndRequiredOptions(''); - self::assertEquals(['required' => 'invalid input: required cannot be empty'], $result->errors); + self::assertSame(['required' => 'invalid input: required cannot be empty'], $result->errors); } public static function testOptionSetWithDeprecatedAliasGeneratesDeprecationWarningAndReturnsValue(): void { $result = self::runCommandReadingStandardError('--deprecated-string=string'); - self::assertEquals('FreshRSS deprecation warning: the CLI option(s): deprecated-string are deprecated ' . + self::assertSame('FreshRSS deprecation warning: the CLI option(s): deprecated-string are deprecated ' . 'and will be removed in a future release. Use: string instead', $result ); $result = self::runOptionalOptions('--deprecated-string=string'); - self::assertEquals('string', $result->string); + self::assertSame('string', $result->string); } public static function testAlwaysReturnUsageMessageWithUsageInfoForAllOptions(): void { $result = self::runOptionalAndRequiredOptions(''); - self::assertEquals('Usage: cli-parser-test.php --required=<required> [-s --string=<string>] [-i --int=<int>] [-b --bool=<bool>] [-f --flag]', + self::assertSame('Usage: cli-parser-test.php --required=<required> [-s --string=<string>] [-i --int=<int>] [-b --bool=<bool>] [-f --flag]', $result->usage, ); } diff --git a/tests/cli/i18n/I18nCompletionValidatorTest.php b/tests/cli/i18n/I18nCompletionValidatorTest.php index f16285005..f66d9e04f 100644 --- a/tests/cli/i18n/I18nCompletionValidatorTest.php +++ b/tests/cli/i18n/I18nCompletionValidatorTest.php @@ -17,23 +17,23 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase { public function testDisplayReport(): void { $validator = new I18nCompletionValidator([], []); - self::assertEquals("There is no data.\n", $validator->displayReport()); + self::assertSame("There is no data.\n", $validator->displayReport()); $reflectionTotalEntries = new ReflectionProperty(I18nCompletionValidator::class, 'totalEntries'); $reflectionTotalEntries->setAccessible(true); $reflectionTotalEntries->setValue($validator, 100); - self::assertEquals("Translation is 0.0% complete.\n", $validator->displayReport()); + self::assertSame("Translation is 0.0% complete.\n", $validator->displayReport()); $reflectionPassEntries = new ReflectionProperty(I18nCompletionValidator::class, 'passEntries'); $reflectionPassEntries->setAccessible(true); $reflectionPassEntries->setValue($validator, 25); - self::assertEquals("Translation is 25.0% complete.\n", $validator->displayReport()); + self::assertSame("Translation is 25.0% complete.\n", $validator->displayReport()); $reflectionPassEntries->setValue($validator, 100); - self::assertEquals("Translation is 100.0% complete.\n", $validator->displayReport()); + self::assertSame("Translation is 100.0% complete.\n", $validator->displayReport()); $reflectionPassEntries->setValue($validator, 200); @@ -45,7 +45,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase { public static function testValidateWhenNoData(): void { $validator = new I18nCompletionValidator([], []); self::assertTrue($validator->validate()); - self::assertEquals('', $validator->displayResult()); + self::assertSame('', $validator->displayResult()); } public function testValidateWhenKeyIsMissing(): void { @@ -59,7 +59,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase { ], []); self::assertFalse($validator->validate()); - self::assertEquals("Missing key file1.l1.l2.k1\nMissing key file2.l1.l2.k1\n", $validator->displayResult()); + self::assertSame("Missing key file1.l1.l2.k1\nMissing key file2.l1.l2.k1\n", $validator->displayResult()); } public function testValidateWhenKeyIsIgnored(): void { @@ -84,7 +84,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase { ]); self::assertTrue($validator->validate()); - self::assertEquals('', $validator->displayResult()); + self::assertSame('', $validator->displayResult()); } public function testValidateWhenValueIsEqual(): void { @@ -112,7 +112,7 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase { ]); self::assertFalse($validator->validate()); - self::assertEquals("Untranslated key file1.l1.l2.k1 - \nUntranslated key file2.l1.l2.k1 - \n", $validator->displayResult()); + self::assertSame("Untranslated key file1.l1.l2.k1 - \nUntranslated key file2.l1.l2.k1 - \n", $validator->displayResult()); } public function testValidateWhenValueIsDifferent(): void { @@ -140,6 +140,6 @@ class I18nCompletionValidatorTest extends PHPUnit\Framework\TestCase { ]); self::assertTrue($validator->validate()); - self::assertEquals('', $validator->displayResult()); + self::assertSame('', $validator->displayResult()); } } diff --git a/tests/cli/i18n/I18nDataTest.php b/tests/cli/i18n/I18nDataTest.php index 29ee14802..afdaacf1e 100644 --- a/tests/cli/i18n/I18nDataTest.php +++ b/tests/cli/i18n/I18nDataTest.php @@ -36,7 +36,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { public function testConstructWhenReferenceOnly(): void { $data = new I18nData($this->referenceData); - self::assertEquals($this->referenceData, $data->getData()); + self::assertSame($this->referenceData, $data->getData()); } public function testConstructorWhenLanguageIsMissingFile(): void { @@ -278,7 +278,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { 'nl' => [], ]); $data = new I18nData($rawData); - self::assertEquals([ + self::assertSame([ 'en', 'fr', 'nl', @@ -292,7 +292,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { 'de' => [], ]); $data = new I18nData($rawData); - self::assertEquals([ + self::assertSame([ 'de', 'en', 'fr', @@ -310,7 +310,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { public function testAddLanguageWhenNoReferenceProvided(): void { $data = new I18nData($this->referenceData); $data->addLanguage('fr'); - self::assertEquals([ + self::assertSame([ 'en' => [ 'file1.php' => [ 'file1.l1.l2.k1' => $this->value, @@ -347,7 +347,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { public function testAddLanguageWhenUnknownReferenceProvided(): void { $data = new I18nData($this->referenceData); $data->addLanguage('fr', 'unknown'); - self::assertEquals([ + self::assertSame([ 'en' => [ 'file1.php' => [ 'file1.l1.l2.k1' => $this->value, @@ -384,7 +384,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { public function testAddLanguageWhenKnownReferenceProvided(): void { $data = new I18nData($this->referenceData); $data->addLanguage('fr', 'en'); - self::assertEquals([ + self::assertSame([ 'en' => [ 'file1.php' => [ 'file1.l1.l2.k1' => $this->value, @@ -480,9 +480,9 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { $enValue = $getTargetedValue($data, 'en'); $frValue = $getTargetedValue($data, 'fr'); self::assertInstanceOf(I18nValue::class, $enValue); - self::assertEquals('value', $enValue->getValue()); + self::assertSame('value', $enValue->getValue()); self::assertTrue($enValue->isTodo()); - self::assertEquals($frValue, $enValue); + self::assertSame($frValue, $enValue); } public function testAddValueWhenLanguageDoesNotExist(): void { @@ -520,9 +520,9 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { self::assertEquals($this->value, $beforeEnValue); self::assertEquals($this->value, $beforeFrValue); self::assertInstanceOf(I18nValue::class, $afterEnValue); - self::assertEquals('new value', $afterEnValue->getValue()); + self::assertSame('new value', $afterEnValue->getValue()); self::assertInstanceOf(I18nValue::class, $afterFrValue); - self::assertEquals('new value', $afterFrValue->getValue()); + self::assertSame('new value', $afterFrValue->getValue()); } public function testAddValueWhenLanguageIsReferenceAndValueInOtherLanguageHasChange(): void { @@ -554,7 +554,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { self::assertEquals($this->value, $beforeEnValue); self::assertEquals($value, $beforeFrValue); self::assertInstanceOf(I18nValue::class, $afterEnValue); - self::assertEquals('new value', $afterEnValue->getValue()); + self::assertSame('new value', $afterEnValue->getValue()); self::assertEquals($value, $afterFrValue); } @@ -575,7 +575,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { self::assertEquals($this->value, $beforeFrValue); self::assertEquals($this->value, $afterEnValue); self::assertInstanceOf(I18nValue::class, $afterFrValue); - self::assertEquals('new value', $afterFrValue->getValue()); + self::assertSame('new value', $afterFrValue->getValue()); } public function testRemoveKeyWhenKeyDoesNotExist(): void { @@ -723,7 +723,7 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { 'nl' => [], ]); $data = new I18nData($rawData); - self::assertEquals($this->referenceData['en'], $data->getLanguage('en')); + self::assertSame($this->referenceData['en'], $data->getLanguage('en')); } public function testGetReferenceLanguage(): void { @@ -732,6 +732,6 @@ class I18nDataTest extends PHPUnit\Framework\TestCase { 'nl' => [], ]); $data = new I18nData($rawData); - self::assertEquals($this->referenceData['en'], $data->getReferenceLanguage()); + self::assertSame($this->referenceData['en'], $data->getReferenceLanguage()); } } diff --git a/tests/cli/i18n/I18nFileTest.php b/tests/cli/i18n/I18nFileTest.php index 755ea12f2..34abdb9a3 100644 --- a/tests/cli/i18n/I18nFileTest.php +++ b/tests/cli/i18n/I18nFileTest.php @@ -12,7 +12,7 @@ class I18nFileTest extends PHPUnit\Framework\TestCase { $after = $this->computeFilesHash(); - self::assertEquals($before, $after); + self::assertSame($before, $after); } /** @return array<string,string|false> */ diff --git a/tests/cli/i18n/I18nUsageValidatorTest.php b/tests/cli/i18n/I18nUsageValidatorTest.php index 1c604390f..5a2268141 100644 --- a/tests/cli/i18n/I18nUsageValidatorTest.php +++ b/tests/cli/i18n/I18nUsageValidatorTest.php @@ -17,23 +17,23 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase { public function testDisplayReport(): void { $validator = new I18nUsageValidator([], []); - self::assertEquals("There is no data.\n", $validator->displayReport()); + self::assertSame("There is no data.\n", $validator->displayReport()); $reflectionTotalEntries = new ReflectionProperty(I18nUsageValidator::class, 'totalEntries'); $reflectionTotalEntries->setAccessible(true); $reflectionTotalEntries->setValue($validator, 100); - self::assertEquals(" 0.0% of translation keys are unused.\n", $validator->displayReport()); + self::assertSame(" 0.0% of translation keys are unused.\n", $validator->displayReport()); $reflectionFailedEntries = new ReflectionProperty(I18nUsageValidator::class, 'failedEntries'); $reflectionFailedEntries->setAccessible(true); $reflectionFailedEntries->setValue($validator, 25); - self::assertEquals(" 25.0% of translation keys are unused.\n", $validator->displayReport()); + self::assertSame(" 25.0% of translation keys are unused.\n", $validator->displayReport()); $reflectionFailedEntries->setValue($validator, 100); - self::assertEquals("100.0% of translation keys are unused.\n", $validator->displayReport()); + self::assertSame("100.0% of translation keys are unused.\n", $validator->displayReport()); $reflectionFailedEntries->setValue($validator, 200); @@ -45,7 +45,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase { public static function testValidateWhenNoData(): void { $validator = new I18nUsageValidator([], []); self::assertTrue($validator->validate()); - self::assertEquals('', $validator->displayResult()); + self::assertSame('', $validator->displayResult()); } public function testValidateWhenParentKeyExistsWithoutTransformation(): void { @@ -61,7 +61,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase { 'file2.l1.l2._', ]); self::assertTrue($validator->validate()); - self::assertEquals('', $validator->displayResult()); + self::assertSame('', $validator->displayResult()); } public function testValidateWhenParentKeyExistsWithTransformation(): void { @@ -77,7 +77,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase { 'file2.l1.l2', ]); self::assertTrue($validator->validate()); - self::assertEquals('', $validator->displayResult()); + self::assertSame('', $validator->displayResult()); } public function testValidateWhenParentKeyDoesNotExist(): void { @@ -90,7 +90,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase { ], ], []); self::assertFalse($validator->validate()); - self::assertEquals("Unused key file1.l1.l2._ - \nUnused key file2.l1.l2._ - \n", $validator->displayResult()); + self::assertSame("Unused key file1.l1.l2._ - \nUnused key file2.l1.l2._ - \n", $validator->displayResult()); } public function testValidateWhenChildKeyExists(): void { @@ -106,7 +106,7 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase { 'file2.l1.l2.k1', ]); self::assertTrue($validator->validate()); - self::assertEquals('', $validator->displayResult()); + self::assertSame('', $validator->displayResult()); } public function testValidateWhenChildKeyDoesNotExist(): void { @@ -119,6 +119,6 @@ class I18nUsageValidatorTest extends PHPUnit\Framework\TestCase { ], ], []); self::assertFalse($validator->validate()); - self::assertEquals("Unused key file1.l1.l2.k1 - \nUnused key file2.l1.l2.k1 - \n", $validator->displayResult()); + self::assertSame("Unused key file1.l1.l2.k1 - \nUnused key file2.l1.l2.k1 - \n", $validator->displayResult()); } } diff --git a/tests/cli/i18n/I18nValueTest.php b/tests/cli/i18n/I18nValueTest.php index 44984d7b2..85ff00fee 100644 --- a/tests/cli/i18n/I18nValueTest.php +++ b/tests/cli/i18n/I18nValueTest.php @@ -5,28 +5,28 @@ require_once __DIR__ . '/../../../cli/i18n/I18nValue.php'; class I18nValueTest extends PHPUnit\Framework\TestCase { public static function testConstructorWithoutState(): void { $value = new I18nValue('some value'); - self::assertEquals('some value', $value->getValue()); + self::assertSame('some value', $value->getValue()); self::assertFalse($value->isIgnore()); self::assertFalse($value->isTodo()); } public static function testConstructorWithUnknownState(): void { $value = new I18nValue('some value -> unknown'); - self::assertEquals('some value', $value->getValue()); + self::assertSame('some value', $value->getValue()); self::assertFalse($value->isIgnore()); self::assertFalse($value->isTodo()); } public static function testConstructorWithTodoState(): void { $value = new I18nValue('some value -> todo'); - self::assertEquals('some value', $value->getValue()); + self::assertSame('some value', $value->getValue()); self::assertFalse($value->isIgnore()); self::assertTrue($value->isTodo()); } public static function testConstructorWithIgnoreState(): void { $value = new I18nValue('some value -> ignore'); - self::assertEquals('some value', $value->getValue()); + self::assertSame('some value', $value->getValue()); self::assertTrue($value->isIgnore()); self::assertFalse($value->isTodo()); } @@ -34,8 +34,8 @@ class I18nValueTest extends PHPUnit\Framework\TestCase { public static function testClone(): void { $value = new I18nValue('some value'); $clonedValue = clone $value; - self::assertEquals('some value', $value->getValue()); - self::assertEquals('some value', $clonedValue->getValue()); + self::assertSame('some value', $value->getValue()); + self::assertSame('some value', $clonedValue->getValue()); self::assertFalse($value->isIgnore()); self::assertFalse($clonedValue->isIgnore()); self::assertFalse($value->isTodo()); @@ -63,21 +63,21 @@ class I18nValueTest extends PHPUnit\Framework\TestCase { $value = new I18nValue('some value'); self::assertNull($reflectionProperty->getValue($value)); $value->markAsDirty(); - self::assertEquals('dirty', $reflectionProperty->getValue($value)); + self::assertSame('dirty', $reflectionProperty->getValue($value)); $value->unmarkAsIgnore(); - self::assertEquals('dirty', $reflectionProperty->getValue($value)); + self::assertSame('dirty', $reflectionProperty->getValue($value)); $value->markAsIgnore(); - self::assertEquals('ignore', $reflectionProperty->getValue($value)); + self::assertSame('ignore', $reflectionProperty->getValue($value)); $value->unmarkAsIgnore(); self::assertNull($reflectionProperty->getValue($value)); $value->markAsTodo(); - self::assertEquals('todo', $reflectionProperty->getValue($value)); + self::assertSame('todo', $reflectionProperty->getValue($value)); } public static function testToString(): void { $value = new I18nValue('some value'); - self::assertEquals('some value', $value->__toString()); + self::assertSame('some value', $value->__toString()); $value->markAsTodo(); - self::assertEquals('some value -> todo', $value->__toString()); + self::assertSame('some value -> todo', $value->__toString()); } } |
