diff options
| author | 2024-12-27 12:12:49 +0100 | |
|---|---|---|
| committer | 2024-12-27 12:12:49 +0100 | |
| commit | b1d24fbdb7d1cc948c946295035dad6df550fb7e (patch) | |
| tree | 7b4365a04097a779659474fbb9281a9661512522 /lib | |
| parent | 897e4a3f4a273d50c28157edb67612b2d7fa2e6f (diff) | |
PHPStan 2.0 (#7131)
* PHPStan 2.0
fix https://github.com/FreshRSS/FreshRSS/issues/6989
https://github.com/phpstan/phpstan/releases/tag/2.0.0
https://github.com/phpstan/phpstan/blob/2.0.x/UPGRADING.md
* More
* More
* Done
* fix i18n CLI
* Restore a PHPStan Next test
For work towards PHPStan Level 10
* 4 more on Level 10
* fix getTagsForEntry
* API at Level 10
* More Level 10
* Finish Minz at Level 10
* Finish CLI at Level 10
* Finish Controllers at Level 10
* More Level 10
* More
* Pass bleedingEdge
* Clean PHPStan options and add TODOs
* Level 10 for main config
* More
* Consitency array vs. list
* Sanitize themes get_infos
* Simplify TagDAO->getTagsForEntries()
* Finish reportAnyTypeWideningInVarTag
* Prepare checkBenevolentUnionTypes and checkImplicitMixed
* Fixes
* Refix
* Another fix
* Casing of __METHOD__ constant
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/Minz/Configuration.php | 5 | ||||
| -rw-r--r-- | lib/Minz/Error.php | 14 | ||||
| -rw-r--r-- | lib/Minz/Extension.php | 6 | ||||
| -rw-r--r-- | lib/Minz/ExtensionManager.php | 6 | ||||
| -rw-r--r-- | lib/Minz/FrontController.php | 2 | ||||
| -rw-r--r-- | lib/Minz/Migrator.php | 5 | ||||
| -rw-r--r-- | lib/Minz/ModelArray.php | 2 | ||||
| -rw-r--r-- | lib/Minz/ModelPdo.php | 16 | ||||
| -rw-r--r-- | lib/Minz/Paginator.php | 14 | ||||
| -rw-r--r-- | lib/Minz/Request.php | 67 | ||||
| -rw-r--r-- | lib/Minz/Session.php | 19 | ||||
| -rw-r--r-- | lib/Minz/Translate.php | 16 | ||||
| -rw-r--r-- | lib/Minz/Url.php | 14 | ||||
| -rw-r--r-- | lib/Minz/View.php | 8 | ||||
| -rw-r--r-- | lib/favicons.php | 9 | ||||
| -rw-r--r-- | lib/http-conditional.php | 22 | ||||
| -rw-r--r-- | lib/lib_rss.php | 78 |
17 files changed, 190 insertions, 113 deletions
diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index b2742e249..b56268b4a 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -45,7 +45,7 @@ class Minz_Configuration { */ public static function load(string $filename): array { $data = @include($filename); - if (is_array($data)) { + if (is_array($data) && is_array_keys_string($data)) { return $data; } else { throw new Minz_FileNotExistException($filename); @@ -117,9 +117,10 @@ class Minz_Configuration { } try { - $this->data = array_replace_recursive( + $overloaded = array_replace_recursive( $this->data, self::load($this->config_filename) ); + $this->data = array_filter($overloaded, 'is_string', ARRAY_FILTER_USE_KEY); } catch (Minz_FileNotExistException $e) { if ($this->default_filename == null) { throw $e; diff --git a/lib/Minz/Error.php b/lib/Minz/Error.php index 74a71de0a..e95fd346c 100644 --- a/lib/Minz/Error.php +++ b/lib/Minz/Error.php @@ -15,13 +15,13 @@ class Minz_Error { /** * Permet de lancer une erreur * @param int $code le type de l'erreur, par défaut 404 (page not found) - * @param string|array<'error'|'warning'|'notice',array<string>> $logs logs d'erreurs découpés de la forme + * @param string|array<'error'|'warning'|'notice',list<string>> $logs logs d'erreurs découpés de la forme * > $logs['error'] * > $logs['warning'] * > $logs['notice'] * @param bool $redirect indique s'il faut forcer la redirection (les logs ne seront pas transmis) */ - public static function error(int $code = 404, $logs = [], bool $redirect = true): void { + public static function error(int $code = 404, string|array $logs = [], bool $redirect = true): void { $logs = self::processLogs($logs); $error_filename = APP_PATH . '/Controllers/errorController.php'; @@ -49,8 +49,8 @@ class Minz_Error { /** * Returns filtered logs - * @param string|array<'error'|'warning'|'notice',array<string>> $logs logs sorted by category (error, warning, notice) - * @return array<string> list of matching logs, without the category, according to environment preferences (production / development) + * @param string|array<'error'|'warning'|'notice',list<string>> $logs logs sorted by category (error, warning, notice) + * @return list<string> list of matching logs, without the category, according to environment preferences (production / development) */ private static function processLogs($logs): array { if (is_string($logs)) { @@ -61,13 +61,13 @@ class Minz_Error { $warning = []; $notice = []; - if (isset($logs['error']) && is_array($logs['error'])) { + if (is_array($logs['error'] ?? null)) { $error = $logs['error']; } - if (isset($logs['warning']) && is_array($logs['warning'])) { + if (is_array($logs['warning'] ?? null)) { $warning = $logs['warning']; } - if (isset($logs['notice']) && is_array($logs['notice'])) { + if (is_array($logs['notice'] ?? null)) { $notice = $logs['notice']; } diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index 69b9c569c..25cbe2091 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -26,7 +26,7 @@ abstract class Minz_Extension { private bool $is_enabled; - /** @var string[] */ + /** @var array<string,string> */ protected array $csp_policies = []; /** @@ -411,11 +411,11 @@ abstract class Minz_Extension { } /** - * @param string[] $policies + * @param array<string,string> $policies */ public function amendCsp(array &$policies): void { foreach ($this->csp_policies as $policy => $source) { - if (array_key_exists($policy, $policies)) { + if (isset($policies[$policy])) { $policies[$policy] .= ' ' . $source; } else { $policies[$policy] = $source; diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 90f005d29..84a6fc09f 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -136,7 +136,6 @@ final class Minz_ExtensionManager { array_walk($list_core_extensions, function (&$s) { $s = CORE_EXTENSIONS_PATH . '/' . $s; }); array_walk($list_thirdparty_extensions, function (&$s) { $s = THIRDPARTY_EXTENSIONS_PATH . '/' . $s; }); - /** @var array<string> */ $list_potential_extensions = array_merge($list_core_extensions, $list_thirdparty_extensions); $system_conf = Minz_Configuration::get('system'); @@ -403,7 +402,10 @@ final class Minz_ExtensionManager { public static function callHookString(string $hook_name): string { $result = ''; foreach (self::$hook_list[$hook_name]['list'] ?? [] as $function) { - $result = $result . call_user_func($function); + $return = call_user_func($function); + if (is_scalar($return)) { + $result .= $return; + } } return $result; } diff --git a/lib/Minz/FrontController.php b/lib/Minz/FrontController.php index 3a86d2d6d..756885ffe 100644 --- a/lib/Minz/FrontController.php +++ b/lib/Minz/FrontController.php @@ -42,7 +42,7 @@ class Minz_FrontController { $url = Minz_Url::build(); $url['params'] = array_merge( empty($url['params']) || !is_array($url['params']) ? [] : $url['params'], - $_POST + array_filter($_POST, 'is_string', ARRAY_FILTER_USE_KEY) ); Minz_Request::forward($url); } catch (Minz_Exception $e) { diff --git a/lib/Minz/Migrator.php b/lib/Minz/Migrator.php index 39c834765..47df98e7f 100644 --- a/lib/Minz/Migrator.php +++ b/lib/Minz/Migrator.php @@ -176,6 +176,7 @@ class Minz_Migrator public function migrations(): array { $migrations = $this->migrations; uksort($migrations, 'strnatcmp'); + /** @var array<string,callable> $migrations */ return $migrations; } @@ -237,7 +238,7 @@ class Minz_Migrator * considered as successful. It is considered as good practice to return * true on success though. * - * @return array<string|bool> Return the results of each executed migration. If an + * @return array<string,bool|string> Return the results of each executed migration. If an * exception was raised in a migration, its result is set to * the exception message. */ @@ -251,7 +252,7 @@ class Minz_Migrator try { $migration_result = $callback(); - $result[$version] = $migration_result; + $result[$version] = (bool)$migration_result; } catch (Exception $e) { $migration_result = false; $result[$version] = $e->getMessage(); diff --git a/lib/Minz/ModelArray.php b/lib/Minz/ModelArray.php index 5a1d286cd..89f7f8da4 100644 --- a/lib/Minz/ModelArray.php +++ b/lib/Minz/ModelArray.php @@ -40,7 +40,7 @@ class Minz_ModelArray { if ($data === false) { throw new Minz_PermissionDeniedException($this->filename); - } elseif (!is_array($data)) { + } elseif (!is_array($data) || !is_array_keys_string($data)) { $data = []; } return $data; diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php index 26b5269a5..86f6df306 100644 --- a/lib/Minz/ModelPdo.php +++ b/lib/Minz/ModelPdo.php @@ -176,8 +176,8 @@ class Minz_ModelPdo { /** * @param array<string,int|string|null> $values - * @phpstan-return ($mode is PDO::FETCH_ASSOC ? array<array<string,int|string|null>>|null : array<int|string|null>|null) - * @return array<array<string,int|string|null>>|array<int|string|null>|null + * @phpstan-return ($mode is PDO::FETCH_ASSOC ? list<array<string,int|string|null>>|null : list<int|string|null>|null) + * @return list<array<string,int|string|null>>|list<int|string|null>|null */ private function fetchAny(string $sql, array $values, int $mode, int $column = 0): ?array { $stm = $this->pdo->prepare($sql); @@ -204,15 +204,15 @@ class Minz_ModelPdo { switch ($mode) { case PDO::FETCH_COLUMN: $res = $stm->fetchAll(PDO::FETCH_COLUMN, $column); + /** @var list<int|string|null> $res */ break; case PDO::FETCH_ASSOC: default: $res = $stm->fetchAll(PDO::FETCH_ASSOC); + /** @var list<array<string,int|string|null>> $res */ break; } - if ($res !== false) { - return $res; - } + return $res; } $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6); @@ -231,7 +231,7 @@ class Minz_ModelPdo { /** * @param array<string,int|string|null> $values - * @return array<array<string,int|string|null>>|null + * @return list<array<string,int|string|null>>|null */ public function fetchAssoc(string $sql, array $values = []): ?array { return $this->fetchAny($sql, $values, PDO::FETCH_ASSOC); @@ -239,7 +239,7 @@ class Minz_ModelPdo { /** * @param array<string,int|string|null> $values - * @return array<int|string|null>|null + * @return list<int|string|null>|null */ public function fetchColumn(string $sql, int $column, array $values = []): ?array { return $this->fetchAny($sql, $values, PDO::FETCH_COLUMN, $column); @@ -257,6 +257,6 @@ class Minz_ModelPdo { Minz_Log::error('SQL error ' . json_encode($stm->errorInfo()) . ' during ' . $sql); return null; } - return isset($columns[0]) ? (string)$columns[0] : null; + return is_scalar($columns[0] ?? null) ? (string)$columns[0] : null; } } diff --git a/lib/Minz/Paginator.php b/lib/Minz/Paginator.php index 727fe42d3..7d6892c67 100644 --- a/lib/Minz/Paginator.php +++ b/lib/Minz/Paginator.php @@ -11,7 +11,7 @@ declare(strict_types=1); */ class Minz_Paginator { /** - * @var array<Minz_Model> tableau des éléments à afficher/gérer + * @var list<Minz_Model> tableau des éléments à afficher/gérer */ private array $items = []; @@ -37,7 +37,7 @@ class Minz_Paginator { /** * Constructeur - * @param array<Minz_Model> $items les éléments à gérer + * @param list<Minz_Model> $items les éléments à gérer */ public function __construct(array $items) { $this->_items($items); @@ -116,10 +116,10 @@ class Minz_Paginator { */ /** * @param bool $all si à true, retourne tous les éléments sans prendre en compte la pagination - * @return array<Minz_Model> + * @return list<Minz_Model> */ public function items(bool $all = false): array { - $array = array (); + $array = []; $nbItems = $this->nbItems(); if ($nbItems <= $this->nbItemsPerPage || $all) { @@ -129,9 +129,9 @@ class Minz_Paginator { $counter = 0; $i = 0; - foreach ($this->items as $key => $item) { + foreach ($this->items as $item) { if ($i >= $begin) { - $array[$key] = $item; + $array[] = $item; $counter++; } if ($counter >= $this->nbItemsPerPage) { @@ -159,7 +159,7 @@ class Minz_Paginator { /** * SETTEURS */ - /** @param array<Minz_Model> $items */ + /** @param list<Minz_Model> $items */ public function _items(?array $items): void { $this->items = $items ?? []; $this->_nbPage(); diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index bddb5a276..f441bcabf 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -69,18 +69,33 @@ class Minz_Request { if (empty(self::$params[$key]) || !is_array(self::$params[$key])) { return []; } - return $plaintext ? self::$params[$key] : Minz_Helper::htmlspecialchars_utf8(self::$params[$key]); + $result = []; + foreach (self::$params[$key] as $k => $v) { + if (is_string($v)) { + $result[$k] = $v; + } elseif (is_array($v)) { + $vs = []; + foreach ($v as $k2 => $v2) { + if (is_string($k2) && (is_string($v2) || is_int($v2) || is_bool($v2))) { + $vs[$k2] = $v2; + } + } + $result[$k] = $vs; + } + } + return $plaintext ? $result : Minz_Helper::htmlspecialchars_utf8($result); } /** * @param bool $plaintext `true` to return special characters without any escaping (unsafe), `false` (default) to XML-encode them - * @return array<string> + * @return list<string> */ public static function paramArrayString(string $key, bool $plaintext = false): array { if (empty(self::$params[$key]) || !is_array(self::$params[$key])) { return []; } $result = array_filter(self::$params[$key], 'is_string'); + $result = array_values($result); return $plaintext ? $result : Minz_Helper::htmlspecialchars_utf8($result); } @@ -143,7 +158,7 @@ class Minz_Request { * character is used to break the text into lines. This method is well suited to use * to split textarea content. * @param bool $plaintext `true` to return special characters without any escaping (unsafe), `false` (default) to XML-encode them - * @return array<string> + * @return list<string> */ public static function paramTextToArray(string $key, bool $plaintext = false): array { if (isset(self::$params[$key]) && is_string(self::$params[$key])) { @@ -214,7 +229,7 @@ class Minz_Request { * Initialise la Request */ public static function init(): void { - self::_params($_GET); + self::_params(array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY)); self::initJSON(); } @@ -227,8 +242,8 @@ class Minz_Request { * Return true if the request is over HTTPS, false otherwise (HTTP) */ public static function isHttps(): bool { - $header = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''; - if ('' != $header) { + $header = is_string($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PROTO'] : ''; + if ('' !== $header) { return 'https' === strtolower($header); } return 'on' === ($_SERVER['HTTPS'] ?? ''); @@ -250,34 +265,37 @@ class Minz_Request { } private static function extractProtocol(): string { - if (self::isHttps()) { - return 'https'; - } - return 'http'; + return self::isHttps() ? 'https' : 'http'; } private static function extractHost(): string { - if ('' != $host = ($_SERVER['HTTP_X_FORWARDED_HOST'] ?? '')) { + $host = is_string($_SERVER['HTTP_X_FORWARDED_HOST'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_HOST'] : ''; + if ($host !== '') { return parse_url("http://{$host}", PHP_URL_HOST) ?: 'localhost'; } - if ('' != $host = ($_SERVER['HTTP_HOST'] ?? '')) { + $host = is_string($_SERVER['HTTP_HOST'] ?? null) ? $_SERVER['HTTP_HOST'] : ''; + if ($host !== '') { // Might contain a port number, and mind IPv6 addresses return parse_url("http://{$host}", PHP_URL_HOST) ?: 'localhost'; } - if ('' != $host = ($_SERVER['SERVER_NAME'] ?? '')) { + $host = is_string($_SERVER['SERVER_NAME'] ?? null) ? $_SERVER['SERVER_NAME'] : ''; + if ($host !== '') { return $host; } return 'localhost'; } private static function extractPort(): int { - if ('' != $port = ($_SERVER['HTTP_X_FORWARDED_PORT'] ?? '')) { + $port = is_numeric($_SERVER['HTTP_X_FORWARDED_PORT'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PORT'] : ''; + if ($port !== '') { return intval($port); } - if ('' != $proto = ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) { + $proto = is_string($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PROTO'] : ''; + if ($proto !== '') { return 'https' === strtolower($proto) ? 443 : 80; } - if ('' != $port = ($_SERVER['SERVER_PORT'] ?? '')) { + $port = is_numeric($_SERVER['SERVER_PORT'] ?? null) ? $_SERVER['SERVER_PORT'] : ''; + if ($port !== '') { return intval($port); } return self::isHttps() ? 443 : 80; @@ -294,15 +312,16 @@ class Minz_Request { } private static function extractPrefix(): string { - if ('' != $prefix = ($_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? '')) { + $prefix = is_string($_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? null) ? $_SERVER['HTTP_X_FORWARDED_PREFIX'] : ''; + if ($prefix !== '') { return rtrim($prefix, '/ '); } return ''; } private static function extractPath(): string { - $path = $_SERVER['REQUEST_URI'] ?? ''; - if ($path != '') { + $path = is_string($_SERVER['REQUEST_URI'] ?? null) ? $_SERVER['REQUEST_URI'] : ''; + if ($path !== '') { $path = parse_url($path, PHP_URL_PATH) ?: ''; return substr($path, -1) === '/' ? rtrim($path, '/') : dirname($path); } @@ -356,7 +375,7 @@ class Minz_Request { } private static function requestId(): string { - if (empty($_GET['rid']) || !ctype_xdigit($_GET['rid'])) { + if (!is_string($_GET['rid'] ?? null) || !ctype_xdigit($_GET['rid'])) { $_GET['rid'] = uniqid(); } return $_GET['rid']; @@ -476,7 +495,8 @@ class Minz_Request { } private static function extractContentType(): string { - return strtolower(trim($_SERVER['CONTENT_TYPE'] ?? '')); + $contentType = is_string($_SERVER['CONTENT_TYPE'] ?? null) ? $_SERVER['CONTENT_TYPE'] : ''; + return strtolower(trim($contentType)); } public static function isPost(): bool { @@ -484,10 +504,11 @@ class Minz_Request { } /** - * @return array<string> + * @return list<string> */ public static function getPreferredLanguages(): array { - if (preg_match_all('/(^|,)\s*(?P<lang>[^;,]+)/', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', $matches) > 0) { + $acceptLanguage = is_string($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : ''; + if (preg_match_all('/(^|,)\s*(?P<lang>[^;,]+)/', $acceptLanguage, $matches) > 0) { return $matches['lang']; } return ['en']; diff --git a/lib/Minz/Session.php b/lib/Minz/Session.php index 9977e62f6..bb2c1a817 100644 --- a/lib/Minz/Session.php +++ b/lib/Minz/Session.php @@ -72,7 +72,13 @@ class Minz_Session { if (empty($_SESSION[$key]) || !is_array($_SESSION[$key])) { return []; } - return $_SESSION[$key]; + $result = []; + foreach ($_SESSION[$key] as $k => $v) { + if (is_string($v) || (is_array($v) && is_array_keys_string($v))) { + $result[$k] = $v; + } + } + return $result; } public static function paramTernary(string $key): ?bool { @@ -97,10 +103,7 @@ class Minz_Session { } public static function paramInt(string $key): int { - if (!empty($_SESSION[$key])) { - return intval($_SESSION[$key]); - } - return 0; + return empty($_SESSION[$key]) || !is_numeric($_SESSION[$key]) ? 0 : (int)$_SESSION[$key]; } public static function paramString(string $key): string { @@ -175,10 +178,10 @@ class Minz_Session { public static function getCookieDir(): string { // Get the script_name (e.g. /p/i/index.php) and keep only the path. $cookie_dir = ''; - if (!empty($_SERVER['HTTP_X_FORWARDED_PREFIX'])) { + if (!empty($_SERVER['HTTP_X_FORWARDED_PREFIX']) && is_string($_SERVER['HTTP_X_FORWARDED_PREFIX'])) { $cookie_dir .= rtrim($_SERVER['HTTP_X_FORWARDED_PREFIX'], '/ '); } - $cookie_dir .= empty($_SERVER['REQUEST_URI']) ? '/' : $_SERVER['REQUEST_URI']; + $cookie_dir .= empty($_SERVER['REQUEST_URI']) || !is_string($_SERVER['REQUEST_URI']) ? '/' : $_SERVER['REQUEST_URI']; if (substr($cookie_dir, -1) !== '/') { $cookie_dir = dirname($cookie_dir) . '/'; } @@ -210,7 +213,7 @@ class Minz_Session { } public static function getLongTermCookie(string $name): string { - return $_COOKIE[$name] ?? ''; + return is_string($_COOKIE[$name] ?? null) ? $_COOKIE[$name] : ''; } } diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index 08b3c91e8..58bf3f424 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -63,7 +63,7 @@ class Minz_Translate { /** * Return the list of available languages. - * @return array<string> containing langs found in different registered paths. + * @return list<string> containing langs found in different registered paths. */ public static function availableLanguages(): array { $list_langs = []; @@ -81,7 +81,7 @@ class Minz_Translate { } } - return array_unique($list_langs); + return array_values(array_unique($list_langs)); } /** @@ -197,7 +197,7 @@ class Minz_Translate { Minz_Log::debug($key . ' is not in a valid format'); $top_level = 'gen'; } else { - $top_level = array_shift($group); + $top_level = array_shift($group) ?? ''; } // If $translates[$top_level] is null it means we have to load the @@ -218,6 +218,9 @@ class Minz_Translate { $level_processed = 0; $translation_value = $key; foreach ($group as $i18n_level) { + if (!is_array($translates)) { + continue; // Not needed. To help PHPStan + } $level_processed++; if (!isset($translates[$i18n_level])) { Minz_Log::debug($key . ' is not a valid key'); @@ -231,10 +234,9 @@ class Minz_Translate { } } - if (is_array($translation_value)) { - if (isset($translation_value['_'])) { - $translation_value = $translation_value['_']; - } else { + if (!is_string($translation_value)) { + $translation_value = is_array($translation_value) ? ($translation_value['_'] ?? null) : null; + if (!is_string($translation_value)) { Minz_Log::debug($key . ' is not a valid key'); return $key; } diff --git a/lib/Minz/Url.php b/lib/Minz/Url.php index 3948414d8..811f0fff7 100644 --- a/lib/Minz/Url.php +++ b/lib/Minz/Url.php @@ -145,10 +145,16 @@ class Minz_Url { * @return array{c?:string,a?:string,params?:array<string,string>} URL representation */ public static function build(): array { + $get = []; + foreach ($_GET as $key => $value) { + if (is_string($key) && is_string($value)) { + $get[$key] = $value; + } + } $url = [ - 'c' => $_GET['c'] ?? Minz_Request::defaultControllerName(), - 'a' => $_GET['a'] ?? Minz_Request::defaultActionName(), - 'params' => $_GET, + 'c' => is_string($_GET['c'] ?? null) ? $_GET['c'] : Minz_Request::defaultControllerName(), + 'a' => is_string($_GET['a'] ?? null) ? $_GET['a'] : Minz_Request::defaultActionName(), + 'params' => $get, ]; // post-traitement @@ -166,7 +172,7 @@ function _url(string $controller, string $action, int|string ...$args): string|f return false; } - $params = array (); + $params = []; for ($i = 0; $i < $nb_args; $i += 2) { $arg = '' . $args[$i]; $params[$arg] = '' . $args[$i + 1]; diff --git a/lib/Minz/View.php b/lib/Minz/View.php index f7dceef0a..01e8501b0 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -19,11 +19,11 @@ class Minz_View { /** @var array<string> */ private static array $base_pathnames = [APP_PATH]; private static string $title = ''; - /** @var array<array{'media':string,'url':string}> */ + /** @var array<array{media:string,url:string}> */ private static array $styles = []; - /** @var array<array{'url':string,'id':string,'defer':bool,'async':bool}> */ + /** @var array<array{url:string,id:string,defer:bool,async:bool}> */ private static array $scripts = []; - /** @var string|array{'dark'?:string,'light'?:string,'default'?:string} */ + /** @var string|array{dark?:string,light?:string,default?:string} */ private static $themeColors; /** @var array<string,mixed> */ private static array $params = []; @@ -245,7 +245,7 @@ class Minz_View { } /** - * @param string|array{'dark'?:string,'light'?:string,'default'?:string} $themeColors + * @param string|array{dark?:string,light?:string,default?:string} $themeColors */ public static function appendThemeColors($themeColors): void { self::$themeColors = $themeColors; diff --git a/lib/favicons.php b/lib/favicons.php index 5df3682b8..bcb05f4f2 100644 --- a/lib/favicons.php +++ b/lib/favicons.php @@ -22,7 +22,7 @@ function isImgMime(string $content): bool { return $isImage; } -/** @param array<int,int|bool> $curlOptions */ +/** @param array<int,int|bool|string> $curlOptions */ function downloadHttp(string &$url, array $curlOptions = []): string { syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $url); $url2 = checkUrl($url); @@ -61,7 +61,7 @@ function downloadHttp(string &$url, array $curlOptions = []): string { $url = $url2; //Possible redirect } } - return $info['http_code'] == 200 ? $response : ''; + return is_array($info) && $info['http_code'] == 200 ? $response : ''; } function searchFavicon(string &$url): string { @@ -103,6 +103,9 @@ function searchFavicon(string &$url): string { } $iri = $href->get_iri(); + if ($iri == false) { + return ''; + } $favicon = downloadHttp($iri, [CURLOPT_REFERER => $url]); if (isImgMime($favicon)) { return $favicon; @@ -115,7 +118,7 @@ function download_favicon(string $url, string $dest): bool { $url = trim($url); $favicon = searchFavicon($url); if ($favicon == '') { - $rootUrl = preg_replace('%^(https?://[^/]+).*$%i', '$1/', $url); + $rootUrl = preg_replace('%^(https?://[^/]+).*$%i', '$1/', $url) ?? $url; if ($rootUrl != $url) { $url = $rootUrl; $favicon = searchFavicon($url); diff --git a/lib/http-conditional.php b/lib/http-conditional.php index f683d4fbf..c08f72f75 100644 --- a/lib/http-conditional.php +++ b/lib/http-conditional.php @@ -29,7 +29,7 @@ declare(strict_types=1); ?> ``` - Version 1.9, 2023-04-08, https://alexandre.alapetite.fr/doc-alex/php-http-304/ + Version 1.10, 2024-12-22, https://alexandre.alapetite.fr/doc-alex/php-http-304/ ------------------------------------------------------------------ Written by Alexandre Alapetite in 2004, https://alexandre.alapetite.fr/cv/ @@ -82,8 +82,8 @@ $_sessionMode = false; function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePrivacy = 0, bool $feedMode = false, bool $compression = false, bool $session = false): bool { if (headers_sent()) return false; - if (isset($_SERVER['SCRIPT_FILENAME'])) $scriptName = $_SERVER['SCRIPT_FILENAME']; - elseif (isset($_SERVER['PATH_TRANSLATED'])) $scriptName = $_SERVER['PATH_TRANSLATED']; + if (is_string($_SERVER['SCRIPT_FILENAME'] ?? null)) $scriptName = $_SERVER['SCRIPT_FILENAME']; + elseif (is_string($_SERVER['PATH_TRANSLATED'] ?? null)) $scriptName = $_SERVER['PATH_TRANSLATED']; else return false; if ((!$feedMode) && (($modifScript = (int)filemtime($scriptName)) > $UnixTimeStamp)) @@ -98,7 +98,7 @@ function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePr $dateCacheClient = 'Thu, 10 Jan 1980 20:30:40 GMT'; //rfc2616-sec14.html#sec14.19 //='"0123456789abcdef0123456789abcdef"' - if (isset($_SERVER['QUERY_STRING'])) $myQuery = '?' . $_SERVER['QUERY_STRING']; + if (is_string($_SERVER['QUERY_STRING'] ?? null)) $myQuery = '?' . $_SERVER['QUERY_STRING']; else $myQuery = ''; if ($session && isset($_SESSION)) { global $_sessionMode; @@ -108,13 +108,13 @@ function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePr $etagServer = '"' . md5($scriptName . $myQuery . '#' . $dateLastModif) . '"'; // @phpstan-ignore booleanNot.alwaysTrue - if ((!$is412) && isset($_SERVER['HTTP_IF_MATCH'])) { //rfc2616-sec14.html#sec14.24 + if ((!$is412) && is_string($_SERVER['HTTP_IF_MATCH'] ?? null)) { //rfc2616-sec14.html#sec14.24 $etagsClient = stripslashes($_SERVER['HTTP_IF_MATCH']); $etagsClient = str_ireplace('-gzip', '', $etagsClient); $is412 = (($etagsClient !== '*') && (strpos($etagsClient, $etagServer) === false)); } // @phpstan-ignore booleanAnd.leftAlwaysTrue - if ($is304 && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { //rfc2616-sec14.html#sec14.25 //rfc1945.txt + if ($is304 && is_string($_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? null)) { //rfc2616-sec14.html#sec14.25 //rfc1945.txt $nbCond++; $dateCacheClient = $_SERVER['HTTP_IF_MODIFIED_SINCE']; $p = strpos($dateCacheClient, ';'); @@ -122,13 +122,13 @@ function httpConditional(int $UnixTimeStamp, int $cacheSeconds = 0, int $cachePr $dateCacheClient = substr($dateCacheClient, 0, $p); $is304 = ($dateCacheClient == $dateLastModif); } - if ($is304 && isset($_SERVER['HTTP_IF_NONE_MATCH'])) { //rfc2616-sec14.html#sec14.26 + if ($is304 && is_string($_SERVER['HTTP_IF_NONE_MATCH'] ?? null)) { //rfc2616-sec14.html#sec14.26 $nbCond++; $etagClient = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']); $etagClient = str_ireplace('-gzip', '', $etagClient); $is304 = (($etagClient === $etagServer) || ($etagClient === '*')); } - if ((!$is412) && isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) { //rfc2616-sec14.html#sec14.28 + if ((!$is412) && is_string($_SERVER['HTTP_IF_UNMODIFIED_SINCE'] ?? null)) { //rfc2616-sec14.html#sec14.28 $dateCacheClient = $_SERVER['HTTP_IF_UNMODIFIED_SINCE']; $p = strpos($dateCacheClient, ';'); if ($p !== false) @@ -200,13 +200,13 @@ function _httpConditionalCallBack(string $buffer, int $mode = 5): string { function httpConditionalRefresh(int $UnixTimeStamp): void { if (headers_sent()) return; - if (isset($_SERVER['SCRIPT_FILENAME'])) $scriptName = $_SERVER['SCRIPT_FILENAME']; - elseif (isset($_SERVER['PATH_TRANSLATED'])) $scriptName = $_SERVER['PATH_TRANSLATED']; + if (is_string($_SERVER['SCRIPT_FILENAME'] ?? null)) $scriptName = $_SERVER['SCRIPT_FILENAME']; + elseif (is_string($_SERVER['PATH_TRANSLATED'] ?? null)) $scriptName = $_SERVER['PATH_TRANSLATED']; else return; $dateLastModif = gmdate('D, d M Y H:i:s \G\M\T', $UnixTimeStamp); - if (isset($_SERVER['QUERY_STRING'])) $myQuery = '?' . $_SERVER['QUERY_STRING']; + if (is_string($_SERVER['QUERY_STRING'] ?? null)) $myQuery = '?' . $_SERVER['QUERY_STRING']; else $myQuery = ''; global $_sessionMode; if ($_sessionMode && isset($_SESSION)) diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 8eb89c578..c93243eff 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -85,6 +85,32 @@ spl_autoload_register('classAutoloader'); //</Auto-loading> /** + * @param array<mixed,mixed> $array + * @phpstan-assert-if-true array<string,mixed> $array + */ +function is_array_keys_string(array $array): bool { + foreach ($array as $key => $value) { + if (!is_string($key)) { + return false; + } + } + return true; +} + +/** + * @param array<mixed,mixed> $array + * @phpstan-assert-if-true array<mixed,string> $array + */ +function is_array_values_string(array $array): bool { + foreach ($array as $value) { + if (!is_string($value)) { + return false; + } + } + return true; +} + +/** * Memory efficient replacement of `echo json_encode(...)` * @param array<mixed>|mixed $json * @param int $optimisationDepth Number of levels for which to perform memory optimisation @@ -231,6 +257,7 @@ function timestamptodate(int $t, bool $hour = true): string { * Decode HTML entities but preserve XML entities. */ function html_only_entity_decode(?string $text): string { + /** @var array<string,string>|null $htmlEntitiesOnly */ static $htmlEntitiesOnly = null; if ($htmlEntitiesOnly === null) { $htmlEntitiesOnly = array_flip(array_diff( @@ -252,7 +279,7 @@ function sensitive_log($log): array|string { foreach ($log as $k => $v) { if (in_array($k, ['api_key', 'Passwd', 'T'], true)) { $log[$k] = '██'; - } elseif (is_array($v) || is_string($v)) { + } elseif ((is_array($v) && is_array_keys_string($v)) || is_string($v)) { $log[$k] = sensitive_log($v); } else { return ''; @@ -298,7 +325,9 @@ function customSimplePie(array $attributes = [], array $curl_options = []): \Sim } if (!empty($attributes['curl_params']) && is_array($attributes['curl_params'])) { foreach ($attributes['curl_params'] as $co => $v) { - $curl_options[$co] = $v; + if (is_int($co)) { + $curl_options[$co] = $v; + } } } $simplePie->set_curl_options($curl_options); @@ -366,13 +395,18 @@ function sanitizeHTML(string $data, string $base = '', ?int $maxLength = null): if ($maxLength !== null) { $data = mb_strcut($data, 0, $maxLength, 'UTF-8'); } + /** @var \SimplePie\SimplePie|null $simplePie */ static $simplePie = null; - if ($simplePie == null) { + if ($simplePie === null) { $simplePie = customSimplePie(); $simplePie->enable_cache(false); $simplePie->init(); } - $result = html_only_entity_decode($simplePie->sanitize->sanitize($data, \SimplePie\SimplePie::CONSTRUCT_HTML, $base)); + $sanitized = $simplePie->sanitize->sanitize($data, \SimplePie\SimplePie::CONSTRUCT_HTML, $base); + if (!is_string($sanitized)) { + return ''; + } + $result = html_only_entity_decode($sanitized); if ($maxLength !== null && strlen($result) > $maxLength) { //Sanitizing has made the result too long so try again shorter $data = mb_strcut($result, 0, (2 * $maxLength) - strlen($result) - 2, 'UTF-8'); @@ -504,6 +538,9 @@ function httpGet(string $url, string $cachePath, string $type = 'html', array $a // TODO: Implement HTTP 1.1 conditional GET If-Modified-Since $ch = curl_init(); + if ($ch === false) { + return ''; + } curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_HTTPHEADER => ['Accept: ' . $accept], @@ -592,9 +629,10 @@ function lazyimg(string $content): string { /** @return numeric-string */ function uTimeString(): string { $t = @gettimeofday(); - $result = $t['sec'] . str_pad('' . $t['usec'], 6, '0', STR_PAD_LEFT); - /** @var numeric-string @result */ - return $result; + $sec = is_numeric($t['sec']) ? (int)$t['sec'] : 0; + $usec = is_numeric($t['usec']) ? (int)$t['usec'] : 0; + $result = ((string)$sec) . str_pad((string)$usec, 6, '0', STR_PAD_LEFT); + return ctype_digit($result) ? $result : '0'; } function invalidateHttpCache(string $username = ''): bool { @@ -606,7 +644,7 @@ function invalidateHttpCache(string $username = ''): bool { } /** - * @return array<string> + * @return list<string> */ function listUsers(): array { $final_list = []; @@ -712,9 +750,9 @@ function checkCIDR(string $ip, string $range): bool { * Use CONN_REMOTE_ADDR (if available, to be robust even when using Apache mod_remoteip) or REMOTE_ADDR environment variable to determine the connection IP. */ function connectionRemoteAddress(): string { - $remoteIp = $_SERVER['CONN_REMOTE_ADDR'] ?? ''; + $remoteIp = is_string($_SERVER['CONN_REMOTE_ADDR'] ?? null) ? $_SERVER['CONN_REMOTE_ADDR'] : ''; if ($remoteIp == '') { - $remoteIp = $_SERVER['REMOTE_ADDR'] ?? ''; + $remoteIp = is_string($_SERVER['REMOTE_ADDR'] ?? null) ? $_SERVER['REMOTE_ADDR'] : ''; } if ($remoteIp == 0) { $remoteIp = ''; @@ -752,17 +790,17 @@ function checkTrustedIP(): bool { } function httpAuthUser(bool $onlyTrusted = true): string { - if (!empty($_SERVER['REMOTE_USER'])) { + if (!empty($_SERVER['REMOTE_USER']) && is_string($_SERVER['REMOTE_USER'])) { return $_SERVER['REMOTE_USER']; } - if (!empty($_SERVER['REDIRECT_REMOTE_USER'])) { + if (!empty($_SERVER['REDIRECT_REMOTE_USER']) && is_string($_SERVER['REDIRECT_REMOTE_USER'])) { return $_SERVER['REDIRECT_REMOTE_USER']; } if (!$onlyTrusted || checkTrustedIP()) { - if (!empty($_SERVER['HTTP_REMOTE_USER'])) { + if (!empty($_SERVER['HTTP_REMOTE_USER']) && is_string($_SERVER['HTTP_REMOTE_USER'])) { return $_SERVER['HTTP_REMOTE_USER']; } - if (!empty($_SERVER['HTTP_X_WEBAUTH_USER'])) { + if (!empty($_SERVER['HTTP_X_WEBAUTH_USER']) && is_string($_SERVER['HTTP_X_WEBAUTH_USER'])) { return $_SERVER['HTTP_X_WEBAUTH_USER']; } } @@ -872,14 +910,14 @@ function recursive_unlink(string $dir): bool { /** * Remove queries where $get is appearing. * @param string $get the get attribute which should be removed. - * @param array<int,array<string,string|int>> $queries an array of queries. - * @return array<int,array<string,string|int>> without queries where $get is appearing. + * @param array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string}> $queries an array of queries. + * @return array<int,array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string}> without queries where $get is appearing. */ function remove_query_by_get(string $get, array $queries): array { $final_queries = []; - foreach ($queries as $key => $query) { + foreach ($queries as $query) { if (empty($query['get']) || $query['get'] !== $get) { - $final_queries[$key] = $query; + $final_queries[] = $query; } } return $final_queries; @@ -901,7 +939,7 @@ const SHORTCUT_KEYS = [ /** * @param array<string> $shortcuts - * @return array<string> + * @return list<string> */ function getNonStandardShortcuts(array $shortcuts): array { $standard = strtolower(implode(' ', SHORTCUT_KEYS)); @@ -911,7 +949,7 @@ function getNonStandardShortcuts(array $shortcuts): array { return $shortcut !== '' && stripos($standard, $shortcut) === false; }); - return $nonStandard; + return array_values($nonStandard); } function errorMessageInfo(string $errorTitle, string $error = ''): string { |
