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 $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 */ $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> $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 $params */ + /** @var array $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 $idArray */ + /** @var list $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 + * Fetch extension list from GitHub + * @return list */ 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 $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 $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'])) { - $item['guid'] = $item['id']; - } - if (empty($item['guid'])) { + if (!is_array($item)) { continue; } - if (empty($item['origin'])) { + if (!is_string($item['guid'] ?? null) && is_string($item['id'] ?? null)) { + $item['guid'] = $item['id']; + } + if (!is_string($item['guid'] ?? null)) { + continue; + } + 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 */ + /** @var list */ 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 + * @return list */ 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|null */ + /** @var list|null */ private ?array $feeds = null; /** @var bool|int */ private $hasFeedsWithError = false; @@ -100,7 +100,7 @@ class FreshRSS_Category extends Minz_Model { } /** - * @return array + * @return list * @throws Minz_ConfigurationNamespaceException * @throws Minz_PDOConnectionException */ @@ -142,11 +142,11 @@ class FreshRSS_Category extends Minz_Model { } /** @param array|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 $categories - * @return array + * @return list */ 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 $feeds */ + /** @var list $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 $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} $valuesTmp + * @param array{name:string,id?:int,kind?:int,lastUpdate?:int,error?:int|bool,attributes?:string|array} $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|mixed|null} $valuesTmp + * @param array{name:string,kind:int,attributes?:array|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}> */ + /** @return Traversable}> */ 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} $row */ + /** @var array{id:int,name:string,kind:int,lastUpdate:int,error:int,attributes?:array} $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 $res */ - $categories = self::daoToCategories($res); + /** @var array $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 $res */ - $categories = self::daoToCategories($res); + /** @var array $res */ + $categories = self::daoToCategories($res); // @phpstan-ignore varTag.type return reset($categories) ?: null; } - /** @return array */ + /** @return list */ 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 */ + /** @return list */ 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 $res */ + /** @var list $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 $res */ - return empty($res) ? [] : self::daoToCategories($res); + $res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name') ?? []; + /** @var list $res */ + return empty($res) ? [] : self::daoToCategories($res); // @phpstan-ignore varTag.type } } - /** @return array */ + /** @return list */ 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 $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 $res */ - $categories = self::daoToCategories($res); - if (isset($categories[self::DEFAULTCATEGORYID])) { - return $categories[self::DEFAULTCATEGORYID]; + /** @var array $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 */ + /** @return list */ 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 $res */ + /** @var list $res */ return $res; } /** - * @param array $listDAO - * @return array + * @param array $listDAO + * @return list */ 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 $listDAO - * @return array + * @param array $listDAO + * @return list */ 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 $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 + * @var list */ private static array $categories = []; /** - * @var array + * @var list */ private static array $tags = []; public static string $name = ''; @@ -176,7 +176,7 @@ final class FreshRSS_Context { FreshRSS_Context::$user_conf = null; } - /** @return array */ + /** @return list */ 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 */ + /** @return list */ public static function feeds(): array { return FreshRSS_Category::findFeeds(self::categories()); } - /** @return array */ + /** @return list */ 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 */ + /** @return list */ 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> $listDAO - * @return array + * @return list */ 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 $table - * @param array $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 $table - * @param array $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 */ + /** @return list */ #[\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 */ + /** @return list */ #[\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 $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,'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,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 .= '
'; foreach ($thumbnails as $thumbnail) { - $content .= '

'; + if (is_string($thumbnail)) { + $content .= '

'; + } } if (self::enclosureIsImage($enclosure)) { @@ -283,9 +283,9 @@ HTML; /** @return Traversable,'height'?:int,'width'?:int,'thumbnails'?:array}> */ 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,'height'?:int,'width'?:int,'thumbnails'?:array}> $attributeEnclosures */ + /** @var list,'height'?:int,'width'?:int,'thumbnails'?:array}> $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 $values */ + /** @param list $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 $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 $ids + * @param numeric-string|list $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 $ids + * @param numeric-string|list $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 */ + /** @return Traversable */ 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 $res */ + /** @var list $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 $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,1:string} */ + /** @return array{0:list,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,1:string} + * @return array{0:list,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,1:string} + * @return array{0:list,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|null + * @return list|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 $res */ + $res = array_map('strval', $res); + /** @var list $res */ return $res; } $info = $stm === false ? $this->pdo->errorInfo() : $stm->errorInfo(); @@ -1366,7 +1372,7 @@ SQL; /** * @param array $guids - * @return array|false + * @return array|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 $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 $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|null + * @return list|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 + * @return list */ 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 $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} $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} $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 */ + /** @return Traversable */ 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 $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 $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 */ + /** @return list */ public function listFeedsIds(): array { $sql = 'SELECT id FROM `_feed`'; - /** @var array $res */ + /** @var list $res */ $res = $this->fetchColumn($sql, 0) ?? []; return $res; } /** - * @return array + * @return list */ public function listFeeds(): array { $sql = 'SELECT * FROM `_feed` ORDER BY name'; $res = $this->fetchAssoc($sql); - /** @var array|null $res */ - return $res == null ? [] : self::daoToFeeds($res); + return $res == null ? [] : self::daoToFeeds($res); // @phpstan-ignore argument.type } /** @return array */ @@ -363,7 +360,7 @@ SQL; $sql .= 'WHERE id_feed=' . intval($id_feed); } $res = $this->fetchAssoc($sql); - /** @var array|null $res */ + /** @var list|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 + * @return list */ 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 */ + /** @return list */ 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 $res */ + /** @var list $res */ return $res; } /** * @param bool|null $muted to include only muted feeds * @param bool|null $errored to include only errored feeds - * @return array + * @return list */ 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 $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 $listDAO - * @return array + * @param array $listDAO + * @return list */ 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 $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|null */ + /** @var list|null */ private ?array $actions = null; /** @param array $actions */ @@ -15,7 +15,7 @@ class FreshRSS_FilterAction { return $this->booleanSearch; } - /** @return array */ + /** @return list */ public function actions(): array { return $this->actions ?? []; } @@ -23,7 +23,7 @@ class FreshRSS_FilterAction { /** @param array $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|null $filterActions */ + /** @var list|null $filterActions */ private ?array $filterActions = null; /** - * @return array + * @return list */ private function filterActions(): array { if (empty($this->filterActions)) { @@ -30,7 +30,7 @@ trait FreshRSS_FilterActionsTrait { * @param array|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 */ + /** @return list */ 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 */ + /** @return list */ 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 */ + /** @return list */ 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 the built-in reading modes + * @return list 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|null */ + /** @var list|null */ private ?array $entry_ids = null; - /** @var array|null */ + /** @var list|null */ private ?array $feed_ids = null; - /** @var array|'*'|null */ + /** @var list|'*'|null */ private $label_ids = null; - /** @var array|null */ + /** @var list|null */ private ?array $label_names = null; - /** @var array|null */ + /** @var list|null */ private ?array $intitle = null; - /** @var array|null */ + /** @var list|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|null */ + /** @var list|null */ private ?array $inurl = null; - /** @var array|null */ + /** @var list|null */ private ?array $inurl_regex = null; - /** @var array|null */ + /** @var list|null */ private ?array $author = null; - /** @var array|null */ + /** @var list|null */ private ?array $author_regex = null; - /** @var array|null */ + /** @var list|null */ private ?array $tags = null; - /** @var array|null */ + /** @var list|null */ private ?array $tags_regex = null; - /** @var array|null */ + /** @var list|null */ private ?array $search = null; - /** @var array|null */ + /** @var list|null */ private ?array $search_regex = null; - /** @var array|null */ + /** @var list|null */ private ?array $not_entry_ids = null; - /** @var array|null */ + /** @var list|null */ private ?array $not_feed_ids = null; - /** @var array|'*'|null */ + /** @var list|'*'|null */ private $not_label_ids = null; - /** @var array|null */ + /** @var list|null */ private ?array $not_label_names = null; - /** @var array|null */ + /** @var list|null */ private ?array $not_intitle = null; - /** @var array|null */ + /** @var list|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|null */ + /** @var list|null */ private ?array $not_inurl = null; - /** @var array|null */ + /** @var list|null */ private ?array $not_inurl_regex = null; - /** @var array|null */ + /** @var list|null */ private ?array $not_author = null; - /** @var array|null */ + /** @var list|null */ private ?array $not_author_regex = null; - /** @var array|null */ + /** @var list|null */ private ?array $not_tags = null; - /** @var array|null */ + /** @var list|null */ private ?array $not_tags_regex = null; - /** @var array|null */ + /** @var list|null */ private ?array $not_search = null; - /** @var array|null */ + /** @var list|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|null */ + /** @return list|null */ public function getEntryIds(): ?array { return $this->entry_ids; } - /** @return array|null */ + /** @return list|null */ public function getNotEntryIds(): ?array { return $this->not_entry_ids; } - /** @return array|null */ + /** @return list|null */ public function getFeedIds(): ?array { return $this->feed_ids; } - /** @return array|null */ + /** @return list|null */ public function getNotFeedIds(): ?array { return $this->not_feed_ids; } - /** @return array|'*'|null */ + /** @return list|'*'|null */ public function getLabelIds(): array|string|null { return $this->label_ids; } - /** @return array|'*'|null */ + /** @return list|'*'|null */ public function getNotLabelIds(): array|string|null { return $this->not_label_ids; } - /** @return array|null */ + /** @return list|null */ public function getLabelNames(): ?array { return $this->label_names; } - /** @return array|null */ + /** @return list|null */ public function getNotLabelNames(): ?array { return $this->not_label_names; } - /** @return array|null */ + /** @return list|null */ public function getIntitle(): ?array { return $this->intitle; } - /** @return array|null */ + /** @return list|null */ public function getIntitleRegex(): ?array { return $this->intitle_regex; } - /** @return array|null */ + /** @return list|null */ public function getNotIntitle(): ?array { return $this->not_intitle; } - /** @return array|null */ + /** @return list|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|null */ + /** @return list|null */ public function getInurl(): ?array { return $this->inurl; } - /** @return array|null */ + /** @return list|null */ public function getInurlRegex(): ?array { return $this->inurl_regex; } - /** @return array|null */ + /** @return list|null */ public function getNotInurl(): ?array { return $this->not_inurl; } - /** @return array|null */ + /** @return list|null */ public function getNotInurlRegex(): ?array { return $this->not_inurl_regex; } - /** @return array|null */ + /** @return list|null */ public function getAuthor(): ?array { return $this->author; } - /** @return array|null */ + /** @return list|null */ public function getAuthorRegex(): ?array { return $this->author_regex; } - /** @return array|null */ + /** @return list|null */ public function getNotAuthor(): ?array { return $this->not_author; } - /** @return array|null */ + /** @return list|null */ public function getNotAuthorRegex(): ?array { return $this->not_author_regex; } - /** @return array|null */ + /** @return list|null */ public function getTags(): ?array { return $this->tags; } - /** @return array|null */ + /** @return list|null */ public function getTagsRegex(): ?array { return $this->tags_regex; } - /** @return array|null */ + /** @return list|null */ public function getNotTags(): ?array { return $this->not_tags; } - /** @return array|null */ + /** @return list|null */ public function getNotTagsRegex(): ?array { return $this->not_tags_regex; } - /** @return array|null */ + /** @return list|null */ public function getSearch(): ?array { return $this->search; } - /** @return array|null */ + /** @return list|null */ public function getSearchRegex(): ?array { return $this->search_regex; } - /** @return array|null */ + /** @return list|null */ public function getNotSearch(): ?array { return $this->not_search; } - /** @return array|null */ + /** @return list|null */ public function getNotSearchRegex(): ?array { return $this->not_search_regex; } /** - * @param array|null $anArray - * @return array + * @param list|null $anArray + * @return list */ 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 $value - * @return ($value is array ? array : string) + * @param list|string $value + * @return ($value is string ? string : list) */ - 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 $strings - * @return array + * @param list $strings + * @return list */ 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 $feed_ids */ + /** @var list $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 $feed_ids */ + /** @var list $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 $label_ids */ + /** @var list $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 $label_ids */ + /** @var list $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|array>,'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|array>,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|array>,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 containing a list of transformations to apply. + * @return list 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 $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 $res */ + /** @var list $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 + * @return list */ public function calculateFeedByCategory(): array { $sql = <<|null @res */ + /** @var list|null @res */ $res = $this->fetchAssoc($sql); return $res == null ? [] : $res; } /** * Calculates entry count per category. - * @return array + * @return list */ public function calculateEntryByCategory(): array { $sql = <<fetchAssoc($sql); - /** @var array|null $res */ + /** @var list|null $res */ return $res == null ? [] : $res; } /** * Calculates the 10 top feeds based on their number of entries - * @return array + * @return list */ public function calculateTopFeed(): array { $sql = <<fetchAssoc($sql); - /** @var array|null $res */ + /** @var list|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 + * @return list */ public function calculateFeedLastDate(): array { $sql = <<fetchAssoc($sql); - /** @var array|null $res */ + /** @var list|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 + * @return list */ public function getDays(): array { return $this->convertToTranslatedJson([ @@ -335,7 +327,7 @@ SQL; /** * Gets months ready for graphs - * @return array + * @return list */ public function getMonths(): array { return $this->convertToTranslatedJson([ @@ -356,8 +348,8 @@ SQL; /** * Translates array content - * @param array $data - * @return array + * @param list $data + * @return list */ 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}> */ + /** @return Traversable}> */ 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} $row */ + /** @var array{id:int,name:string,attributes?:array} $row */ yield $row; } } - /** @return Traversable */ + /** @return Traversable */ 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|null $res */ + /** @var list|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|null $res */ + /** @var list|null $res */ return $res === null ? null : (current(self::daoToTags($res)) ?: null); } - /** @return array|false */ + /** @return list|false */ public function listTags(bool $precounts = false): array|false { if ($precounts) { $sql = <<<'SQL' @@ -291,16 +290,16 @@ SQL; } /** - * @param array $addLabels Labels to insert as batch + * @param iterable $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|false + * @return array|false */ public function getTagsForEntry(string $id_entry): array|false { $sql = <<<'SQL' @@ -347,8 +346,8 @@ SQL; } /** - * @param array> $entries - * @return array|false + * @param list $entries + * @return list|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> $entries */ - foreach ($entries as $entry) { - if (!empty($entry['id'])) { - $values[] = $entry['id']; - } - } - } elseif (is_object($entries[0])) { - /** @var array $entries */ - foreach ($entries as $entry) { - $values[] = $entry->id(); - } - } else { - /** @var array $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 $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 $entries the list of entries for which to retrieve the labels. + * @param list $entries the list of entries for which to retrieve the labels. * @return array> 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 $listDAO - * @return array + * @param iterable $listDAO + * @return list */ 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 */ + /** @return list */ 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,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}}> */ + /** @return array,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,'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,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,'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,'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,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 $queries + * @property array $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> $sharing + * @property array> $sharing * @property array $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 */ + /** @var list */ 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 */ + /** @var list */ public array $feeds; public int $nbUnreadTags; - /** @var array */ + /** @var list */ public array $tags; - /** @var array */ + /** @var array */ public array $tagsForEntry; /** @var array> */ 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 */ + /** @var array */ public array $users; // Updates @@ -62,7 +62,7 @@ class FreshRSS_View extends Minz_View { public int $size_user; // Display - /** @var array,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}}> */ + /** @var array,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 */ + /** @var array */ public array $available_extensions; public ?Minz_Extension $ext_details = null; - /** @var array{'system':array,'user':array} */ + /** @var array{system:array,user:array} */ public array $extension_list; public ?Minz_Extension $extension = null; /** @var array */ 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 */ + /** @var list */ public array $categories; - /** @var array */ + /** @var list */ public array $feeds; - /** @var array */ + /** @var list */ 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 */ + /** @var list */ public array $categories; public ?FreshRSS_Feed $feed = null; - /** @var array */ + /** @var list */ 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 */ + /** @var list */ public array $days; /** @var array> */ public array $entryByCategory; @@ -30,11 +30,11 @@ final class FreshRSS_ViewStats extends FreshRSS_View { public array $last30DaysLabel; /** @var array */ public array $last30DaysLabels; - /** @var array */ + /** @var list */ 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 */ public array $repartitionDayOfWeek; @@ -42,6 +42,6 @@ final class FreshRSS_ViewStats extends FreshRSS_View { public array $repartitionHour; /** @var array */ public array $repartitionMonth; - /** @var array */ + /** @var list */ 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|array|mixed $array + * @phpstan-assert-if-true \ArrayAccess|array $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 $jf json feed + * @param array $jf json feed * @param string $feedSourceUrl the source URL for the feed * @param array $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 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(); ?>

-

+ (empty($_SESSION['bd_error']) || !is_string($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']) ?>

@@ -527,19 +536,19 @@ function printStep2(): void { + $bd_host ?? $system_default_config->db['host'] ?? '' ?>" tabindex="2" /> @@ -560,7 +576,7 @@ function printStep2(): void {
+ $bd_user ?? '' ?>" tabindex="3" />
@@ -569,7 +585,7 @@ function printStep2(): void {
+ $bd_password ?? '' ?>" tabindex="4" autocomplete="off" />
@@ -579,7 +595,7 @@ function printStep2(): void {
+ $bd_base ?? '' ?>" tabindex="6" />
@@ -587,7 +603,7 @@ function printStep2(): void {
+ $bd_prefix ?? $system_default_config->db['prefix'] ?? '' ?>" tabindex="7" />
@@ -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') { ?>

@@ -625,7 +642,7 @@ function printStep3(): void {

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 @@ 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 $feeds - * @return array> + * @return list> */ 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 @@ 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); ?> nbPage > 1) { ?>