From 314077a457f04cc2f0472e036af029e2676fbf02 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Mon, 15 Jan 2024 10:36:30 +0100 Subject: [PATCH] PHPStan prepare exceptions (#6037) Take advantage of https://phpstan.org/blog/bring-your-exceptions-under-control Minimum changes to pass `tooWideThrowType` and `implicitThrows`. Revert some mistakes from: https://github.com/FreshRSS/FreshRSS/pull/5504 Preparation needed before new PRs of the same type: https://github.com/FreshRSS/FreshRSS/pull/5962 Fix several wrong PHPDocs and catches: > Method ... has ...Exception in PHPDoc @throws tag but it's not thrown. > Dead catch - ...Exception is never thrown in the try block. --- app/Controllers/authController.php | 1 - app/Controllers/feedController.php | 4 +- app/Controllers/indexController.php | 1 + app/Controllers/tagController.php | 2 +- app/Controllers/updateController.php | 18 +- app/Controllers/userController.php | 6 +- app/Exceptions/AlreadySubscribedException.php | 2 +- app/Exceptions/ContextException.php | 2 +- app/Exceptions/DAOException.php | 6 - app/Exceptions/EntriesGetterException.php | 2 +- app/Exceptions/FeedException.php | 2 +- app/Exceptions/FeedNotAddedException.php | 2 +- app/Exceptions/ZipException.php | 2 +- app/Exceptions/ZipMissingException.php | 2 +- app/Models/CategoryDAO.php | 2 - app/Models/Context.php | 7 +- app/Models/Entry.php | 3 +- app/Models/EntryDAO.php | 5 + app/Models/Feed.php | 27 ++- app/Models/FeedDAO.php | 1 - app/Models/UserQuery.php | 6 - app/Utils/dotpathUtil.php | 173 +++++++++--------- app/install.php | 5 +- lib/Minz/Configuration.php | 3 +- lib/Minz/Exception.php | 10 +- lib/Minz/ExtensionManager.php | 1 + lib/Minz/Log.php | 4 + lib/Minz/Mailer.php | 1 + lib/Minz/ModelArray.php | 7 +- lib/Minz/ModelPdo.php | 2 +- lib/Minz/Pdo.php | 15 +- lib/Minz/PdoMysql.php | 6 +- lib/Minz/PdoPgsql.php | 5 +- lib/Minz/PdoSqlite.php | 6 +- lib/Minz/Request.php | 2 + lib/Minz/Url.php | 11 +- lib/Minz/View.php | 1 + lib/favicons.php | 16 +- lib/lib_install.php | 3 + lib/lib_rss.php | 13 +- phpstan.neon | 7 + 41 files changed, 217 insertions(+), 177 deletions(-) delete mode 100644 app/Exceptions/DAOException.php diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index 82dfefddd..7b0462888 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -65,7 +65,6 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController { * * It forwards to the correct login page (form) or main page if * the user is already connected. - * @throws Minz_ConfigurationParamException */ public function loginAction(): void { if (FreshRSS_Auth::hasAccess() && Minz_Request::paramString('u') === '') { diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 018bdb382..c194baf95 100644 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -30,8 +30,9 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController { /** * @param array $attributes * @throws FreshRSS_AlreadySubscribed_Exception - * @throws FreshRSS_FeedNotAdded_Exception + * @throws FreshRSS_BadUrl_Exception * @throws FreshRSS_Feed_Exception + * @throws FreshRSS_FeedNotAdded_Exception * @throws Minz_FileNotExistException */ public static function addFeed(string $url, string $title = '', int $cat_id = 0, string $new_cat_name = '', @@ -874,7 +875,6 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController { /** * @throws Minz_ConfigurationNamespaceException - * @throws JsonException * @throws Minz_PDOConnectionException */ public static function renameFeed(int $feed_id, string $feed_name): bool { diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index a83307714..20223d340 100644 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -241,6 +241,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController { /** * This method returns a list of entries based on the Context object. * @return Traversable + * @throws FreshRSS_EntriesGetter_Exception */ public static function listEntriesByContext(): Traversable { $entryDAO = FreshRSS_Factory::createEntryDao(); diff --git a/app/Controllers/tagController.php b/app/Controllers/tagController.php index da90b3f1c..6233207ed 100644 --- a/app/Controllers/tagController.php +++ b/app/Controllers/tagController.php @@ -160,7 +160,7 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController { /** * @throws Minz_ConfigurationNamespaceException - * @throws Minz_PDOConnectionException|JsonException + * @throws Minz_PDOConnectionException */ public function renameAction(): void { if (!FreshRSS_Auth::hasAccess()) { diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php index 78f28e493..eeac7eb09 100644 --- a/app/Controllers/updateController.php +++ b/app/Controllers/updateController.php @@ -11,18 +11,19 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController { /** * Automatic change to the new name of edge branch since FreshRSS 1.18.0. + * @throws Minz_Exception */ public static function migrateToGitEdge(): bool { $errorMessage = 'Error during git checkout to edge branch. Please change branch manually!'; if (!is_writable(FRESHRSS_PATH . '/.git/config')) { - throw new Exception($errorMessage); + throw new Minz_Exception($errorMessage); } //Note `git branch --show-current` requires git 2.22+ exec('git symbolic-ref --short HEAD', $output, $return); if ($return != 0) { - throw new Exception($errorMessage); + throw new Minz_Exception($errorMessage); } $line = implode('', $output); if ($line !== 'master' && $line !== 'dev') { @@ -33,13 +34,13 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController { unset($output); exec('git checkout edge --guess -f', $output, $return); if ($return != 0) { - throw new Exception($errorMessage); + throw new Minz_Exception($errorMessage); } unset($output); exec('git reset --hard FETCH_HEAD', $output, $return); if ($return != 0) { - throw new Exception($errorMessage); + throw new Minz_Exception($errorMessage); } return true; @@ -64,6 +65,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController { chdir(FRESHRSS_PATH); $output = []; try { + /** @throws ValueError */ exec('git fetch --prune', $output, $return); if ($return == 0) { $output = []; @@ -72,7 +74,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController { $line = implode('; ', $output); Minz_Log::warning('git fetch warning: ' . $line); } - } catch (Exception $e) { + } catch (Throwable $e) { Minz_Log::warning('git fetch error: ' . $e->getMessage()); } chdir($cwd); @@ -101,11 +103,9 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController { $output = []; self::migrateToGitEdge(); - } catch (Exception $e) { + } catch (Throwable $e) { Minz_Log::warning('Git error: ' . $e->getMessage()); - if (empty($output)) { - $output = $e->getMessage(); - } + $output = $e->getMessage(); $return = 1; } chdir($cwd); diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index 126eb60a2..fdf57b5f9 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -208,7 +208,11 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController { } } - /** @param array $userConfigOverride */ + /** + * @param array $userConfigOverride + * @throws Minz_ConfigurationNamespaceException + * @throws Minz_PDOConnectionException + */ public static function createUser(string $new_user_name, ?string $email, string $passwordPlain, array $userConfigOverride = [], bool $insertDefaultFeeds = true): bool { $userConfig = []; diff --git a/app/Exceptions/AlreadySubscribedException.php b/app/Exceptions/AlreadySubscribedException.php index 3802e7d65..8e2eaa13a 100644 --- a/app/Exceptions/AlreadySubscribedException.php +++ b/app/Exceptions/AlreadySubscribedException.php @@ -1,7 +1,7 @@ } $valuesTmp * @return int|false - * @throws JsonException */ public function addCategory(array $valuesTmp) { // TRIM() to provide a type hint as text @@ -155,7 +154,6 @@ SQL; /** * @param array{'name':string,'kind':int,'attributes'?:array|mixed|null} $valuesTmp * @return int|false - * @throws JsonException */ public function updateCategory(int $id, array $valuesTmp) { // No tag of the same name diff --git a/app/Models/Context.php b/app/Models/Context.php index bb6fa4cbd..2d22290bc 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -75,6 +75,9 @@ final class FreshRSS_Context { } } + /** + * @throws FreshRSS_Context_Exception + */ public static function &systemConf(): FreshRSS_SystemConfiguration { if (FreshRSS_Context::$system_conf === null) { throw new FreshRSS_Context_Exception('System configuration not initialised!'); @@ -88,7 +91,6 @@ final class FreshRSS_Context { /** * Initialize the context for the current user. - * @throws Minz_ConfigurationParamException */ public static function initUser(string $username = '', bool $userMustExist = true): void { FreshRSS_Context::$user_conf = null; @@ -153,6 +155,9 @@ final class FreshRSS_Context { } } + /** + * @throws FreshRSS_Context_Exception + */ public static function &userConf(): FreshRSS_UserConfiguration { if (FreshRSS_Context::$user_conf === null) { throw new FreshRSS_Context_Exception('User configuration not initialised!'); diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 16f01483a..d3cfa07de 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -691,6 +691,7 @@ HTML; /** * @param array $attributes + * @throws Minz_Exception */ public static function getContentByParsing(string $url, string $path, array $attributes = [], int $maxRedirs = 3): string { $cachePath = FreshRSS_Feed::cacheFilename($url, $attributes, FreshRSS_Feed::KIND_HTML_XPATH); @@ -741,7 +742,7 @@ HTML; $html = trim(sanitizeHTML($content, $base)); return $html; } else { - throw new Exception(); + throw new Minz_Exception(); } } diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index b809c7475..abcc33a1a 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -1007,6 +1007,7 @@ SQL; /** * @param 'ASC'|'DESC' $order * @return array{0:array,1:string} + * @throws FreshRSS_EntriesGetter_Exception */ protected function sqlListEntriesWhere(string $alias = '', ?FreshRSS_BooleanSearch $filters = null, int $state = FreshRSS_Entry::STATE_ALL, @@ -1059,6 +1060,7 @@ SQL; * @param int $id category/feed/tag ID * @param 'ASC'|'DESC' $order * @return array{0:array,1:string} + * @throws FreshRSS_EntriesGetter_Exception */ private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null, @@ -1126,6 +1128,7 @@ SQL; * @param 'ASC'|'DESC' $order * @param int $id category/feed/tag ID * @return PDOStatement|false + * @throws FreshRSS_EntriesGetter_Exception */ private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null, @@ -1161,6 +1164,7 @@ SQL; * @param int $id category/feed/tag ID * @param 'ASC'|'DESC' $order * @return Traversable + * @throws FreshRSS_EntriesGetter_Exception */ public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, string $order = 'DESC', int $limit = 1, string $firstId = '', @@ -1226,6 +1230,7 @@ SQL; * @param int $id category/feed/tag ID * @param 'ASC'|'DESC' $order * @return array|null + * @throws FreshRSS_EntriesGetter_Exception */ public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null): ?array { diff --git a/app/Models/Feed.php b/app/Models/Feed.php index 40e78bfeb..1de8258e8 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -65,6 +65,9 @@ class FreshRSS_Feed extends Minz_Model { private string $hubUrl = ''; private string $selfUrl = ''; + /** + * @throws FreshRSS_BadUrl_Exception + */ public function __construct(string $url, bool $validate = true) { if ($validate) { $this->_url($url); @@ -248,6 +251,9 @@ class FreshRSS_Feed extends Minz_Model { $this->id = $value; } + /** + * @throws FreshRSS_BadUrl_Exception + */ public function _url(string $value, bool $validate = true): void { $this->hash = ''; $url = $value; @@ -323,9 +329,16 @@ class FreshRSS_Feed extends Minz_Model { $this->nbEntries = $value; } + /** + * @throws Minz_FileNotExistException + * @throws FreshRSS_Feed_Exception + */ public function load(bool $loadDetails = false, bool $noCache = false): ?SimplePie { if ($this->url != '') { - // @phpstan-ignore-next-line + /** + * @phpstan-ignore-next-line + * @throws Minz_FileNotExistException + */ if (CACHE_PATH == '') { throw new Minz_FileNotExistException( 'CACHE_PATH', @@ -615,9 +628,6 @@ class FreshRSS_Feed extends Minz_Model { ]; } - /** - * @throws FreshRSS_Context_Exception - */ public function loadJson(): ?SimplePie { if ($this->url == '') { return null; @@ -654,9 +664,6 @@ class FreshRSS_Feed extends Minz_Model { return $this->simplePieFromContent($feedContent); } - /** - * @throws FreshRSS_Context_Exception - */ public function loadHtmlXpath(): ?SimplePie { if ($this->url == '') { return null; @@ -799,7 +806,6 @@ class FreshRSS_Feed extends Minz_Model { /** * @return int|null The max number of unread articles to keep, or null if disabled. - * @throws JsonException */ public function keepMaxUnread() { $keepMaxUnread = $this->attributeInt('keep_max_n_unread'); @@ -881,7 +887,10 @@ class FreshRSS_Feed extends Minz_Model { return false; } - /** @param array $attributes */ + /** + * @param array $attributes + * @throws FreshRSS_Context_Exception + */ public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string { $simplePie = customSimplePie($attributes); $filename = $simplePie->get_cache_filename($url); diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 895d2d333..0744970de 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -37,7 +37,6 @@ 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 * @return int|false - * @throws JsonException */ public function addFeed(array $valuesTmp) { $sql = 'INSERT INTO `_feed` (url, kind, category, name, website, description, `lastUpdate`, priority, `pathEntries`, `httpAuth`, error, ttl, attributes) diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php index 85df49f1c..000cfbbdd 100644 --- a/app/Models/UserQuery.php +++ b/app/Models/UserQuery.php @@ -109,8 +109,6 @@ class FreshRSS_UserQuery { /** * Parse the query string when it is a "category" query - * - * @throws FreshRSS_DAO_Exception */ private function parseCategory(int $id): void { if ($this->category_dao === null) { @@ -127,8 +125,6 @@ class FreshRSS_UserQuery { /** * Parse the query string when it is a "feed" query - * - * @throws FreshRSS_DAO_Exception */ private function parseFeed(int $id): void { if ($this->feed_dao === null) { @@ -145,8 +141,6 @@ class FreshRSS_UserQuery { /** * Parse the query string when it is a "tag" query - * - * @throws FreshRSS_DAO_Exception */ private function parseTag(int $id): void { if ($this->tag_dao === null) { diff --git a/app/Utils/dotpathUtil.php b/app/Utils/dotpathUtil.php index 299b1dcc4..b4da1506e 100644 --- a/app/Utils/dotpathUtil.php +++ b/app/Utils/dotpathUtil.php @@ -110,99 +110,94 @@ final class FreshRSS_dotpath_Util $view->rss_url = $feedSourceUrl; $view->entries = []; - try { - $view->rss_title = isset($dotPaths['feedTitle']) - ? (htmlspecialchars(FreshRSS_dotpath_Util::getString($jf, $dotPaths['feedTitle']) ?? '', ENT_COMPAT, 'UTF-8') ?: $defaultRssTitle) - : $defaultRssTitle; + $view->rss_title = isset($dotPaths['feedTitle']) + ? (htmlspecialchars(FreshRSS_dotpath_Util::getString($jf, $dotPaths['feedTitle']) ?? '', ENT_COMPAT, 'UTF-8') ?: $defaultRssTitle) + : $defaultRssTitle; - $jsonItems = FreshRSS_dotpath_Util::get($jf, $dotPaths['item']); - if (!is_array($jsonItems) || count($jsonItems) === 0) { - return null; - } - - foreach ($jsonItems as $jsonItem) { - $rssItem = []; - $rssItem['link'] = isset($dotPaths['itemUri']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemUri']) ?? '' : ''; - if (empty($rssItem['link'])) { - continue; - } - $rssItem['title'] = isset($dotPaths['itemTitle']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemTitle']) ?? '' : ''; - $rssItem['author'] = isset($dotPaths['itemAuthor']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemAuthor']) ?? '' : ''; - $rssItem['timestamp'] = isset($dotPaths['itemTimestamp']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemTimestamp']) ?? '' : ''; - - //get simple content, but if a path for HTML content has been provided, replace the simple content with HTML content - $rssItem['content'] = isset($dotPaths['itemContent']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemContent']) ?? '' : ''; - $rssItem['content'] = isset($dotPaths['itemContentHTML']) - ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemContentHTML']) ?? '' - : $rssItem['content']; - - if (isset($dotPaths['itemTimeFormat']) && is_string($dotPaths['itemTimeFormat'])) { - $dateTime = DateTime::createFromFormat($dotPaths['itemTimeFormat'], $rssItem['timestamp']); - if ($dateTime != false) { - $rssItem['timestamp'] = $dateTime->format(DateTime::ATOM); - } - } - - if (isset($dotPaths['itemCategories'])) { - $jsonItemCategories = FreshRSS_dotpath_Util::get($jsonItem, $dotPaths['itemCategories']); - if (is_string($jsonItemCategories) && $jsonItemCategories !== '') { - $rssItem['tags'] = [$jsonItemCategories]; - } elseif (is_array($jsonItemCategories) && count($jsonItemCategories) > 0) { - $rssItem['tags'] = []; - foreach ($jsonItemCategories as $jsonItemCategory) { - if (is_string($jsonItemCategory)) { - $rssItem['tags'][] = $jsonItemCategory; - } - } - } - } - - $rssItem['thumbnail'] = isset($dotPaths['itemThumbnail']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemThumbnail']) ?? '' : ''; - - //Enclosures? - if (isset($dotPaths['itemAttachment'])) { - $jsonItemAttachments = FreshRSS_dotpath_Util::get($jsonItem, $dotPaths['itemAttachment']); - if (is_array($jsonItemAttachments) && count($jsonItemAttachments) > 0) { - $rssItem['attachments'] = []; - foreach ($jsonItemAttachments as $attachment) { - $rssAttachment = []; - $rssAttachment['url'] = isset($dotPaths['itemAttachmentUrl']) - ? FreshRSS_dotpath_Util::getString($attachment, $dotPaths['itemAttachmentUrl']) - : ''; - $rssAttachment['type'] = isset($dotPaths['itemAttachmentType']) - ? FreshRSS_dotpath_Util::getString($attachment, $dotPaths['itemAttachmentType']) - : ''; - $rssAttachment['length'] = isset($dotPaths['itemAttachmentLength']) - ? FreshRSS_dotpath_Util::get($attachment, $dotPaths['itemAttachmentLength']) - : ''; - $rssItem['attachments'][] = $rssAttachment; - } - } - } - - if (isset($dotPaths['itemUid'])) { - $rssItem['guid'] = FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemUid']); - } - - if (empty($rssItem['guid'])) { - $rssItem['guid'] = 'urn:sha1:' . sha1($rssItem['title'] . $rssItem['content'] . $rssItem['link']); - } - - if ($rssItem['title'] != '' || $rssItem['content'] != '' || $rssItem['link'] != '') { - // HTML-encoding/escaping of the relevant fields (all except 'content') - foreach (['author', 'guid', 'link', 'thumbnail', 'timestamp', 'tags', 'title'] as $key) { - if (!empty($rssItem[$key]) && is_string($rssItem[$key])) { - $rssItem[$key] = Minz_Helper::htmlspecialchars_utf8($rssItem[$key]); - } - } - $view->entries[] = FreshRSS_Entry::fromArray($rssItem); - } - } - } catch (Exception $ex) { - Minz_Log::warning($ex->getMessage()); + $jsonItems = FreshRSS_dotpath_Util::get($jf, $dotPaths['item']); + if (!is_array($jsonItems) || count($jsonItems) === 0) { return null; } + foreach ($jsonItems as $jsonItem) { + $rssItem = []; + $rssItem['link'] = isset($dotPaths['itemUri']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemUri']) ?? '' : ''; + if (empty($rssItem['link'])) { + continue; + } + $rssItem['title'] = isset($dotPaths['itemTitle']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemTitle']) ?? '' : ''; + $rssItem['author'] = isset($dotPaths['itemAuthor']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemAuthor']) ?? '' : ''; + $rssItem['timestamp'] = isset($dotPaths['itemTimestamp']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemTimestamp']) ?? '' : ''; + + //get simple content, but if a path for HTML content has been provided, replace the simple content with HTML content + $rssItem['content'] = isset($dotPaths['itemContent']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemContent']) ?? '' : ''; + $rssItem['content'] = isset($dotPaths['itemContentHTML']) + ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemContentHTML']) ?? '' + : $rssItem['content']; + + if (isset($dotPaths['itemTimeFormat']) && is_string($dotPaths['itemTimeFormat'])) { + $dateTime = DateTime::createFromFormat($dotPaths['itemTimeFormat'], $rssItem['timestamp']); + if ($dateTime != false) { + $rssItem['timestamp'] = $dateTime->format(DateTime::ATOM); + } + } + + if (isset($dotPaths['itemCategories'])) { + $jsonItemCategories = FreshRSS_dotpath_Util::get($jsonItem, $dotPaths['itemCategories']); + if (is_string($jsonItemCategories) && $jsonItemCategories !== '') { + $rssItem['tags'] = [$jsonItemCategories]; + } elseif (is_array($jsonItemCategories) && count($jsonItemCategories) > 0) { + $rssItem['tags'] = []; + foreach ($jsonItemCategories as $jsonItemCategory) { + if (is_string($jsonItemCategory)) { + $rssItem['tags'][] = $jsonItemCategory; + } + } + } + } + + $rssItem['thumbnail'] = isset($dotPaths['itemThumbnail']) ? FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemThumbnail']) ?? '' : ''; + + //Enclosures? + if (isset($dotPaths['itemAttachment'])) { + $jsonItemAttachments = FreshRSS_dotpath_Util::get($jsonItem, $dotPaths['itemAttachment']); + if (is_array($jsonItemAttachments) && count($jsonItemAttachments) > 0) { + $rssItem['attachments'] = []; + foreach ($jsonItemAttachments as $attachment) { + $rssAttachment = []; + $rssAttachment['url'] = isset($dotPaths['itemAttachmentUrl']) + ? FreshRSS_dotpath_Util::getString($attachment, $dotPaths['itemAttachmentUrl']) + : ''; + $rssAttachment['type'] = isset($dotPaths['itemAttachmentType']) + ? FreshRSS_dotpath_Util::getString($attachment, $dotPaths['itemAttachmentType']) + : ''; + $rssAttachment['length'] = isset($dotPaths['itemAttachmentLength']) + ? FreshRSS_dotpath_Util::get($attachment, $dotPaths['itemAttachmentLength']) + : ''; + $rssItem['attachments'][] = $rssAttachment; + } + } + } + + if (isset($dotPaths['itemUid'])) { + $rssItem['guid'] = FreshRSS_dotpath_Util::getString($jsonItem, $dotPaths['itemUid']); + } + + if (empty($rssItem['guid'])) { + $rssItem['guid'] = 'urn:sha1:' . sha1($rssItem['title'] . $rssItem['content'] . $rssItem['link']); + } + + if ($rssItem['title'] != '' || $rssItem['content'] != '' || $rssItem['link'] != '') { + // HTML-encoding/escaping of the relevant fields (all except 'content') + foreach (['author', 'guid', 'link', 'thumbnail', 'timestamp', 'tags', 'title'] as $key) { + if (!empty($rssItem[$key]) && is_string($rssItem[$key])) { + $rssItem[$key] = Minz_Helper::htmlspecialchars_utf8($rssItem[$key]); + } + } + $view->entries[] = FreshRSS_Entry::fromArray($rssItem); + } + } + return $view->renderToString(); } } diff --git a/app/install.php b/app/install.php index 7703b3840..3ad69c071 100644 --- a/app/install.php +++ b/app/install.php @@ -508,7 +508,10 @@ function printStep1(): void { data[$key])) { diff --git a/lib/Minz/Exception.php b/lib/Minz/Exception.php index c306a14ca..283b28f4e 100644 --- a/lib/Minz/Exception.php +++ b/lib/Minz/Exception.php @@ -6,13 +6,13 @@ class Minz_Exception extends Exception { const WARNING = 10; const NOTICE = 20; - public function __construct(string $message, int $code = self::ERROR) { - if ($code != Minz_Exception::ERROR - && $code != Minz_Exception::WARNING - && $code != Minz_Exception::NOTICE) { + public function __construct(string $message = '', int $code = self::ERROR, ?Throwable $previous = null) { + if ($code !== Minz_Exception::ERROR + && $code !== Minz_Exception::WARNING + && $code !== Minz_Exception::NOTICE) { $code = Minz_Exception::ERROR; } - parent::__construct ($message, $code); + parent::__construct($message, $code, $previous); } } diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 27e09b97a..d9e38955b 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -118,6 +118,7 @@ final class Minz_ExtensionManager { * extension.php should contain at least a class named Extension where * must match with the entry point in metadata.json. This class must * inherit from Minz_Extension class. + * @throws Minz_ConfigurationNamespaceException */ public static function init(): void { self::reset(); diff --git a/lib/Minz/Log.php b/lib/Minz/Log.php index 272f62f6b..4b884ae3b 100644 --- a/lib/Minz/Log.php +++ b/lib/Minz/Log.php @@ -102,16 +102,20 @@ class Minz_Log { /** * Some helpers to Minz_Log::record() method * Parameters are the same of those of the record() method. + * @throws Minz_PermissionDeniedException */ public static function debug(string $msg, ?string $file_name = null): void { self::record($msg, LOG_DEBUG, $file_name); } + /** @throws Minz_PermissionDeniedException */ public static function notice(string $msg, ?string $file_name = null): void { self::record($msg, LOG_NOTICE, $file_name); } + /** @throws Minz_PermissionDeniedException */ public static function warning(string $msg, ?string $file_name = null): void { self::record($msg, LOG_WARNING, $file_name); } + /** @throws Minz_PermissionDeniedException */ public static function error(string $msg, ?string $file_name = null): void { self::record($msg, LOG_ERR, $file_name); } diff --git a/lib/Minz/Mailer.php b/lib/Minz/Mailer.php index 86c5c33da..8e1211807 100644 --- a/lib/Minz/Mailer.php +++ b/lib/Minz/Mailer.php @@ -41,6 +41,7 @@ class Minz_Mailer { /** * @phpstan-param class-string|'' $viewType * @param string $viewType Name of the class (inheriting from Minz_View) to use for the view model + * @throws Minz_ConfigurationException */ public function __construct(string $viewType = '') { $view = null; diff --git a/lib/Minz/ModelArray.php b/lib/Minz/ModelArray.php index 090536623..f12e23567 100644 --- a/lib/Minz/ModelArray.php +++ b/lib/Minz/ModelArray.php @@ -24,7 +24,11 @@ class Minz_ModelArray { $this->filename = $filename; } - /** @return array */ + /** + * @return array + * @throws Minz_FileNotExistException + * @throws Minz_PermissionDeniedException + */ protected function loadArray(): array { if (!file_exists($this->filename)) { throw new Minz_FileNotExistException($this->filename, Minz_Exception::WARNING); @@ -46,6 +50,7 @@ class Minz_ModelArray { /** * Sauve le tableau $array dans le fichier $filename * @param array $array + * @throws Minz_PermissionDeniedException */ protected function writeArray(array $array): bool { if (file_put_contents($this->filename, "|null $options */ + /** + * @param array|null $options + * @throws PDOException + */ public function __construct(string $dsn, ?string $username = null, ?string $passwd = null, ?array $options = null) { parent::__construct($dsn, $username, $passwd, $options); $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); @@ -38,6 +41,7 @@ abstract class Minz_Pdo extends PDO { /** * @param string|null $name * @return string|false + * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION` */ #[\ReturnTypeWillChange] public function lastInsertId($name = null) { @@ -52,6 +56,7 @@ abstract class Minz_Pdo extends PDO { * @param string $query * @param array $options * @return PDOStatement|false + * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION` * @phpstan-ignore-next-line */ #[\ReturnTypeWillChange] @@ -64,6 +69,8 @@ abstract class Minz_Pdo extends PDO { /** * @param string $statement * @return int|false + * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION` + * @phpstan-ignore-next-line */ #[\ReturnTypeWillChange] public function exec($statement) { @@ -71,7 +78,11 @@ abstract class Minz_Pdo extends PDO { return parent::exec($statement); } - /** @return PDOStatement|false */ + /** + * @return PDOStatement|false + * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION` + * @phpstan-ignore-next-line + */ #[\ReturnTypeWillChange] public function query(string $query, ?int $fetch_mode = null, ...$fetch_mode_args) { $query = $this->preSql($query); diff --git a/lib/Minz/PdoMysql.php b/lib/Minz/PdoMysql.php index 0171141ba..a6d927308 100644 --- a/lib/Minz/PdoMysql.php +++ b/lib/Minz/PdoMysql.php @@ -7,7 +7,10 @@ declare(strict_types=1); */ class Minz_PdoMysql extends Minz_Pdo { - /** @param array|null $options */ + /** + * @param array|null $options + * @throws PDOException + */ public function __construct(string $dsn, ?string $username = null, ?string $passwd = null, ?array $options = null) { parent::__construct($dsn, $username, $passwd, $options); $this->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); @@ -20,6 +23,7 @@ class Minz_PdoMysql extends Minz_Pdo { /** * @param string|null $name * @return string|false + * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION` */ #[\ReturnTypeWillChange] public function lastInsertId($name = null) { diff --git a/lib/Minz/PdoPgsql.php b/lib/Minz/PdoPgsql.php index 2abf168d9..f60dd695f 100644 --- a/lib/Minz/PdoPgsql.php +++ b/lib/Minz/PdoPgsql.php @@ -7,7 +7,10 @@ declare(strict_types=1); */ class Minz_PdoPgsql extends Minz_Pdo { - /** @param array|null $options */ + /** + * @param array|null $options + * @throws PDOException + */ public function __construct(string $dsn, ?string $username = null, ?string $passwd = null, ?array $options = null) { parent::__construct($dsn, $username, $passwd, $options); $this->exec("SET NAMES 'UTF8';"); diff --git a/lib/Minz/PdoSqlite.php b/lib/Minz/PdoSqlite.php index 1fd5c8d7a..cecc68a64 100644 --- a/lib/Minz/PdoSqlite.php +++ b/lib/Minz/PdoSqlite.php @@ -7,7 +7,10 @@ declare(strict_types=1); */ class Minz_PdoSqlite extends Minz_Pdo { - /** @param array|null $options */ + /** + * @param array|null $options + * @throws PDOException + */ public function __construct(string $dsn, ?string $username = null, ?string $passwd = null, ?array $options = null) { parent::__construct($dsn, $username, $passwd, $options); $this->exec('PRAGMA foreign_keys = ON;'); @@ -20,6 +23,7 @@ class Minz_PdoSqlite extends Minz_Pdo { /** * @param string|null $name * @return string|false + * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION` */ #[\ReturnTypeWillChange] public function lastInsertId($name = null) { diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index 80d503048..9bf1ff4fb 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -283,6 +283,7 @@ class Minz_Request { /** * Return the base_url from configuration + * @throws Minz_ConfigurationException */ public static function getBaseUrl(): string { $conf = Minz_Configuration::get('system'); @@ -382,6 +383,7 @@ class Minz_Request { * Restart a request * @param array{'c'?:string,'a'?:string,'params'?:array} $url an array presentation of the URL to route to * @param bool $redirect If true, uses an HTTP redirection, and if false (default), performs an internal dispatcher redirection. + * @throws Minz_ConfigurationException */ public static function forward($url = [], bool $redirect = false): void { if (empty(Minz_Request::originalRequest())) { diff --git a/lib/Minz/Url.php b/lib/Minz/Url.php index 2104759cd..67b927f05 100644 --- a/lib/Minz/Url.php +++ b/lib/Minz/Url.php @@ -15,6 +15,7 @@ class Minz_Url { * @param string $encoding how to encode & (& ou & pour html) * @param bool|string $absolute * @return string Formatted URL + * @throws Minz_ConfigurationException */ public static function display($url = [], string $encoding = 'html', $absolute = false): string { $isArray = is_array($url); @@ -140,13 +141,9 @@ class Minz_Url { * @return array> */ public static function unserialize(string $url = ''): array { - try { - $result = json_decode(base64_decode($url, true) ?: '', true, JSON_THROW_ON_ERROR) ?? []; - /** @var array{'c'?:string,'a'?:string,'params'?:array} $result */ - return $result; - } catch (\Throwable $exception) { - return []; - } + $result = json_decode(base64_decode($url, true) ?: '', true, JSON_THROW_ON_ERROR) ?? []; + /** @var array{'c'?:string,'a'?:string,'params'?:array} $result */ + return $result; } /** diff --git a/lib/Minz/View.php b/lib/Minz/View.php index c215ecdab..5b1518a84 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -30,6 +30,7 @@ class Minz_View { /** * Determines if a layout is used or not + * @throws Minz_ConfigurationException */ public function __construct() { $this->_layout(self::LAYOUT_DEFAULT); diff --git a/lib/favicons.php b/lib/favicons.php index df893b309..48b649c28 100644 --- a/lib/favicons.php +++ b/lib/favicons.php @@ -13,16 +13,12 @@ function isImgMime(string $content): bool { return true; } $isImage = true; - try { - /** @var finfo $fInfo */ - $fInfo = finfo_open(FILEINFO_MIME_TYPE); - /** @var string $content */ - $content = finfo_buffer($fInfo, $content); - $isImage = strpos($content, 'image') !== false; - finfo_close($fInfo); - } catch (Exception $e) { - syslog(LOG_WARNING, 'FreshRSS favicon error: ' . $e->getMessage()); - } + /** @var finfo $fInfo */ + $fInfo = finfo_open(FILEINFO_MIME_TYPE); + /** @var string $content */ + $content = finfo_buffer($fInfo, $content); + $isImage = strpos($content, 'image') !== false; + finfo_close($fInfo); return $isImage; } diff --git a/lib/lib_install.php b/lib/lib_install.php index f36d2eaa6..76ba1b459 100644 --- a/lib/lib_install.php +++ b/lib/lib_install.php @@ -76,6 +76,9 @@ function generateSalt(): string { return sha1(uniqid('' . mt_rand(), true).implode('', stat(__FILE__) ?: [])); } +/** + * @throws FreshRSS_Context_Exception + */ function initDb(): string { $db = FreshRSS_Context::systemConf()->db; if (empty($db['pdo_options'])) { diff --git a/lib/lib_rss.php b/lib/lib_rss.php index fa02c50eb..62fe87375 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -580,6 +580,7 @@ function max_registrations_reached(): bool { * * @param string $username the name of the user of which we want the configuration. * @return FreshRSS_UserConfiguration|null object, or null if the configuration cannot be loaded. + * @throws Minz_ConfigurationNamespaceException */ function get_user_configuration(string $username): ?FreshRSS_UserConfiguration { if (!FreshRSS_user_Controller::checkUsername($username)) { @@ -590,9 +591,6 @@ function get_user_configuration(string $username): ?FreshRSS_UserConfiguration { FreshRSS_UserConfiguration::register($namespace, USERS_PATH . '/' . $username . '/config.php', FRESHRSS_PATH . '/config-user.default.php'); - } catch (Minz_ConfigurationNamespaceException $e) { - // namespace already exists, do nothing. - Minz_Log::warning($e->getMessage(), ADMIN_LOG); } catch (Minz_FileNotExistException $e) { Minz_Log::warning($e->getMessage(), ADMIN_LOG); return null; @@ -706,13 +704,8 @@ function httpAuthUser(bool $onlyTrusted = true): string { } function cryptAvailable(): bool { - try { - $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG'; - return $hash === @crypt('password', $hash); - } catch (Exception $e) { - Minz_Log::warning($e->getMessage()); - } - return false; + $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG'; + return $hash === @crypt('password', $hash); } diff --git a/phpstan.neon b/phpstan.neon index 0d9f1861a..a0f7fe8ef 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -51,6 +51,13 @@ parameters: strictCalls: true switchConditionsMatchingType: true uselessCast: true + exceptions: + check: + missingCheckedExceptionInThrows: false # TODO pass + tooWideThrowType: true + implicitThrows: false + checkedExceptionClasses: + - 'Minz_Exception' ignoreErrors: # - '#Only booleans are allowed in (a negated boolean|a ternary operator condition|an elseif condition|an if condition|&&|\|\|), (bool|false|int(<[0-9, max]+>)?|true|null|\|)+ given.*#' includes: