Просмотр исходного кода

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.
Alexandre Alapetite 2 лет назад
Родитель
Сommit
314077a457
41 измененных файлов с 204 добавлено и 164 удалено
  1. 0 1
      app/Controllers/authController.php
  2. 2 2
      app/Controllers/feedController.php
  3. 1 0
      app/Controllers/indexController.php
  4. 1 1
      app/Controllers/tagController.php
  5. 9 9
      app/Controllers/updateController.php
  6. 5 1
      app/Controllers/userController.php
  7. 1 1
      app/Exceptions/AlreadySubscribedException.php
  8. 1 1
      app/Exceptions/ContextException.php
  9. 0 6
      app/Exceptions/DAOException.php
  10. 1 1
      app/Exceptions/EntriesGetterException.php
  11. 1 1
      app/Exceptions/FeedException.php
  12. 1 1
      app/Exceptions/FeedNotAddedException.php
  13. 1 1
      app/Exceptions/ZipException.php
  14. 1 1
      app/Exceptions/ZipMissingException.php
  15. 0 2
      app/Models/CategoryDAO.php
  16. 6 1
      app/Models/Context.php
  17. 2 1
      app/Models/Entry.php
  18. 5 0
      app/Models/EntryDAO.php
  19. 18 9
      app/Models/Feed.php
  20. 0 1
      app/Models/FeedDAO.php
  21. 0 6
      app/Models/UserQuery.php
  22. 71 76
      app/Utils/dotpathUtil.php
  23. 4 1
      app/install.php
  24. 2 1
      lib/Minz/Configuration.php
  25. 5 5
      lib/Minz/Exception.php
  26. 1 0
      lib/Minz/ExtensionManager.php
  27. 4 0
      lib/Minz/Log.php
  28. 1 0
      lib/Minz/Mailer.php
  29. 6 1
      lib/Minz/ModelArray.php
  30. 1 1
      lib/Minz/ModelPdo.php
  31. 13 2
      lib/Minz/Pdo.php
  32. 5 1
      lib/Minz/PdoMysql.php
  33. 4 1
      lib/Minz/PdoPgsql.php
  34. 5 1
      lib/Minz/PdoSqlite.php
  35. 2 0
      lib/Minz/Request.php
  36. 4 7
      lib/Minz/Url.php
  37. 1 0
      lib/Minz/View.php
  38. 6 10
      lib/favicons.php
  39. 3 0
      lib/lib_install.php
  40. 3 10
      lib/lib_rss.php
  41. 7 0
      phpstan.neon

+ 0 - 1
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') === '') {

+ 2 - 2
app/Controllers/feedController.php

@@ -30,8 +30,9 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 	/**
 	 * @param array<string,mixed> $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 {

+ 1 - 0
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<FreshRSS_Entry>
+	 * @throws FreshRSS_EntriesGetter_Exception
 	 */
 	public static function listEntriesByContext(): Traversable {
 		$entryDAO = FreshRSS_Factory::createEntryDao();

+ 1 - 1
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()) {

+ 9 - 9
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);

+ 5 - 1
app/Controllers/userController.php

@@ -208,7 +208,11 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 		}
 	}
 
-	/** @param array<string,mixed> $userConfigOverride */
+	/**
+	 * @param array<string,mixed> $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 = [];

+ 1 - 1
app/Exceptions/AlreadySubscribedException.php

@@ -1,7 +1,7 @@
 <?php
 declare(strict_types=1);
 
-class FreshRSS_AlreadySubscribed_Exception extends Exception {
+class FreshRSS_AlreadySubscribed_Exception extends Minz_Exception {
 
 	private string $feedName = '';
 

+ 1 - 1
app/Exceptions/ContextException.php

@@ -4,6 +4,6 @@ declare(strict_types=1);
 /**
  * An exception raised when a context is invalid
  */
-class FreshRSS_Context_Exception extends Exception {
+class FreshRSS_Context_Exception extends Minz_Exception {
 
 }

+ 0 - 6
app/Exceptions/DAOException.php

@@ -1,6 +0,0 @@
-<?php
-declare(strict_types=1);
-
-class FreshRSS_DAO_Exception extends Exception {
-
-}

+ 1 - 1
app/Exceptions/EntriesGetterException.php

@@ -1,6 +1,6 @@
 <?php
 declare(strict_types=1);
 
-class FreshRSS_EntriesGetter_Exception extends Exception {
+class FreshRSS_EntriesGetter_Exception extends Minz_Exception {
 
 }

+ 1 - 1
app/Exceptions/FeedException.php

@@ -1,6 +1,6 @@
 <?php
 declare(strict_types=1);
 
-class FreshRSS_Feed_Exception extends Exception {
+class FreshRSS_Feed_Exception extends Minz_Exception {
 
 }

+ 1 - 1
app/Exceptions/FeedNotAddedException.php

@@ -1,7 +1,7 @@
 <?php
 declare(strict_types=1);
 
-class FreshRSS_FeedNotAdded_Exception extends Exception {
+class FreshRSS_FeedNotAdded_Exception extends Minz_Exception {
 
 	private string $url = '';
 

+ 1 - 1
app/Exceptions/ZipException.php

@@ -1,7 +1,7 @@
 <?php
 declare(strict_types=1);
 
-class FreshRSS_Zip_Exception extends Exception {
+class FreshRSS_Zip_Exception extends Minz_Exception {
 
 	private int $zipErrorCode = 0;
 

+ 1 - 1
app/Exceptions/ZipMissingException.php

@@ -1,5 +1,5 @@
 <?php
 declare(strict_types=1);
 
-class FreshRSS_ZipMissing_Exception extends Exception {
+class FreshRSS_ZipMissing_Exception extends Minz_Exception {
 }

+ 0 - 2
app/Models/CategoryDAO.php

@@ -101,7 +101,6 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
 	/**
 	 * @param array{'name':string,'id'?:int,'kind'?:int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string|array<string,mixed>} $valuesTmp
 	 * @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<string,mixed>|mixed|null} $valuesTmp
 	 * @return int|false
-	 * @throws JsonException
 	 */
 	public function updateCategory(int $id, array $valuesTmp) {
 		// No tag of the same name

+ 6 - 1
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!');

+ 2 - 1
app/Models/Entry.php

@@ -691,6 +691,7 @@ HTML;
 
 	/**
 	 * @param array<string,mixed> $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();
 		}
 	}
 

+ 5 - 0
app/Models/EntryDAO.php

@@ -1007,6 +1007,7 @@ SQL;
 	/**
 	 * @param 'ASC'|'DESC' $order
 	 * @return array{0:array<int|string>,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<int|string>,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<FreshRSS_Entry>
+	 * @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<numeric-string>|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 {

+ 18 - 9
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<string,mixed> $attributes */
+	/**
+	 * @param array<string,mixed> $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);

+ 0 - 1
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<string|mixed>} $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)

+ 0 - 6
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) {

+ 71 - 76
app/Utils/dotpathUtil.php

@@ -110,97 +110,92 @@ 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;
-
-			$jsonItems = FreshRSS_dotpath_Util::get($jf, $dotPaths['item']);
-			if (!is_array($jsonItems) || count($jsonItems) === 0) {
-				return null;
-			}
+		$view->rss_title = isset($dotPaths['feedTitle'])
+			? (htmlspecialchars(FreshRSS_dotpath_Util::getString($jf, $dotPaths['feedTitle']) ?? '', ENT_COMPAT, 'UTF-8') ?: $defaultRssTitle)
+			: $defaultRssTitle;
 
-			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);
-					}
+		$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;
-							}
+			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;
-						}
+			$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 (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 (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]);
-						}
+			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);
 				}
+				$view->entries[] = FreshRSS_Entry::fromArray($rssItem);
 			}
-		} catch (Exception $ex) {
-			Minz_Log::warning($ex->getMessage());
-			return null;
 		}
 
 		return $view->renderToString();

+ 4 - 1
app/install.php

@@ -508,7 +508,10 @@ function printStep1(): void {
 <?php
 }
 
-/* Select database & configuration */
+/**
+ * Select database & configuration
+ * @throws Minz_ConfigurationNamespaceException
+ */
 function printStep2(): void {
 	$system_default_config = FreshRSS_SystemConfiguration::get('default_system');
 	$s2 = checkStep2();

+ 2 - 1
lib/Minz/Configuration.php

@@ -27,6 +27,7 @@ class Minz_Configuration {
 	 * @param string $config_filename the filename of the configuration
 	 * @param string $default_filename a filename containing default values for the configuration
 	 * @param Minz_ConfigurationSetterInterface $configuration_setter an optional helper to set values in configuration
+	 * @throws Minz_FileNotExistException
 	 */
 	public static function register(string $namespace, string $config_filename, string $default_filename = null,
 		Minz_ConfigurationSetterInterface $configuration_setter = null): void {
@@ -103,6 +104,7 @@ class Minz_Configuration {
 	 * @param string $config_filename the file containing configuration values.
 	 * @param string $default_filename the file containing default values, null by default.
 	 * @param Minz_ConfigurationSetterInterface $configuration_setter an optional helper to set values in configuration
+	 * @throws Minz_FileNotExistException
 	 */
 	private final function __construct(string $namespace, string $config_filename, string $default_filename = null,
 		Minz_ConfigurationSetterInterface $configuration_setter = null) {
@@ -153,7 +155,6 @@ class Minz_Configuration {
 	 * @param string $key the name of the param.
 	 * @param mixed $default default value to return if key does not exist.
 	 * @return array|mixed value corresponding to the key.
-	 * @throws Minz_ConfigurationParamException if the param does not exist
 	 */
 	public function param(string $key, $default = null) {
 		if (isset($this->data[$key])) {

+ 5 - 5
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);
 	}
 }

+ 1 - 0
lib/Minz/ExtensionManager.php

@@ -118,6 +118,7 @@ final class Minz_ExtensionManager {
 	 * extension.php should contain at least a class named <name>Extension where
 	 * <name> 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();

+ 4 - 0
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);
 	}

+ 1 - 0
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;

+ 6 - 1
lib/Minz/ModelArray.php

@@ -24,7 +24,11 @@ class Minz_ModelArray {
 		$this->filename = $filename;
 	}
 
-	/** @return array<string,mixed> */
+	/**
+	 * @return array<string,mixed>
+	 * @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<string,mixed> $array
+	 * @throws Minz_PermissionDeniedException
 	 */
 	protected function writeArray(array $array): bool {
 		if (file_put_contents($this->filename, "<?php\n return " . var_export($array, true) . ';', LOCK_EX) === false) {

+ 1 - 1
lib/Minz/ModelPdo.php

@@ -86,7 +86,7 @@ class Minz_ModelPdo {
 	 * HOST, BASE, USER and PASS variables defined in the configuration file
 	 * @param string|null $currentUser
 	 * @param Minz_Pdo|null $currentPdo
-	 * @throws Minz_ConfigurationNamespaceException
+	 * @throws Minz_ConfigurationException
 	 * @throws Minz_PDOConnectionException
 	 */
 	public function __construct(?string $currentUser = null, ?Minz_Pdo $currentPdo = null) {

+ 13 - 2
lib/Minz/Pdo.php

@@ -7,7 +7,10 @@ declare(strict_types=1);
  */
 
 abstract class Minz_Pdo extends PDO {
-	/** @param array<int,int|string|bool>|null $options */
+	/**
+	 * @param array<int,int|string|bool>|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<int,string> $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);

+ 5 - 1
lib/Minz/PdoMysql.php

@@ -7,7 +7,10 @@ declare(strict_types=1);
  */
 
 class Minz_PdoMysql extends Minz_Pdo {
-	/** @param array<int,int|string|bool>|null $options */
+	/**
+	 * @param array<int,int|string|bool>|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) {

+ 4 - 1
lib/Minz/PdoPgsql.php

@@ -7,7 +7,10 @@ declare(strict_types=1);
  */
 
 class Minz_PdoPgsql extends Minz_Pdo {
-	/** @param array<int,int|string|bool>|null $options */
+	/**
+	 * @param array<int,int|string|bool>|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';");

+ 5 - 1
lib/Minz/PdoSqlite.php

@@ -7,7 +7,10 @@ declare(strict_types=1);
  */
 
 class Minz_PdoSqlite extends Minz_Pdo {
-	/** @param array<int,int|string|bool>|null $options */
+	/**
+	 * @param array<int,int|string|bool>|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) {

+ 2 - 0
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<string,mixed>} $url an array presentation of the URL to route to
 	 * @param bool $redirect If true, uses an HTTP redirection, and if false (default), performs an internal dispatcher redirection.
+	 * @throws Minz_ConfigurationException
 	 */
 	public static function forward($url = [], bool $redirect = false): void {
 		if (empty(Minz_Request::originalRequest())) {

+ 4 - 7
lib/Minz/Url.php

@@ -15,6 +15,7 @@ class Minz_Url {
 	 * @param string $encoding how to encode & (& ou &amp; 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<string,string|array<string,string>>
 	 */
 	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<string,mixed>} $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<string,mixed>} $result */
+		return $result;
 	}
 
 	/**

+ 1 - 0
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);

+ 6 - 10
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;
 }
 

+ 3 - 0
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'])) {

+ 3 - 10
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);
 }
 
 

+ 7 - 0
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: