Selaa lähdekoodia

PHPStan level 5 (#4110)

* Fix most PHPDocs errors
Contributes to https://github.com/FreshRSS/FreshRSS/issues/4103
https://phpstan.org/writing-php-code/phpdoc-types

* Avoid func_get_args
Use variadic syntax instead https://php.net/manual/functions.arguments#functions.variable-arg-list
And avoid dynamic functions names when possible to more easily identify calls and unused functions.
Contributes to https://github.com/FreshRSS/FreshRSS/issues/4103

* PHPStan level 3

* PHPStand level 4

* Update default to PHPStan level 4

* Towards level 5

* Fix level 4 regression

* Towards level 5

* Pass PHPStan level 5

* Towards level 6

* Remove erronenous regression from changelog
https://github.com/FreshRSS/FreshRSS/pull/4116
Alexandre Alapetite 4 vuotta sitten
vanhempi
commit
1335a0e3cf
73 muutettua tiedostoa jossa 403 lisäystä ja 170 poistoa
  1. 0 1
      CHANGELOG.md
  2. 1 1
      app/Controllers/apiController.php
  3. 1 1
      app/Controllers/authController.php
  4. 1 1
      app/Controllers/categoryController.php
  5. 5 4
      app/Controllers/configureController.php
  6. 1 1
      app/Controllers/entryController.php
  7. 1 1
      app/Controllers/errorController.php
  8. 1 1
      app/Controllers/extensionController.php
  9. 9 19
      app/Controllers/feedController.php
  10. 3 7
      app/Controllers/importExportController.php
  11. 1 1
      app/Controllers/indexController.php
  12. 3 3
      app/Controllers/javascriptController.php
  13. 1 1
      app/Controllers/statsController.php
  14. 3 3
      app/Controllers/subscriptionController.php
  15. 1 1
      app/Controllers/tagController.php
  16. 2 2
      app/Controllers/updateController.php
  17. 3 3
      app/Controllers/userController.php
  18. 5 5
      app/Exceptions/FeedNotAddedException.php
  19. 6 0
      app/Mailers/UserMailer.php
  20. 9 0
      app/Models/ActionController.php
  21. 1 1
      app/Models/Auth.php
  22. 1 0
      app/Models/ConfigurationSetter.php
  23. 19 2
      app/Models/Context.php
  24. 11 2
      app/Models/Entry.php
  25. 6 6
      app/Models/EntryDAO.php
  26. 5 5
      app/Models/EntryDAOSQLite.php
  27. 4 3
      app/Models/Feed.php
  28. 1 1
      app/Models/FormAuth.php
  29. 2 2
      app/Models/ReadingMode.php
  30. 1 1
      app/Models/Share.php
  31. 8 8
      app/Models/StatsDAO.php
  32. 4 4
      app/Models/StatsDAOPGSQL.php
  33. 28 0
      app/Models/SystemConfiguration.php
  34. 4 4
      app/Models/TagDAO.php
  35. 64 0
      app/Models/UserConfiguration.php
  36. 2 2
      app/Models/UserQuery.php
  37. 12 0
      app/Models/View.php
  38. 0 4
      app/Services/ImportService.php
  39. 2 0
      app/actualize_script.php
  40. 3 0
      app/install.php
  41. 2 2
      app/views/configure/queries.phtml
  42. 1 0
      app/views/helpers/index/normal/entry_bottom.phtml
  43. 3 3
      app/views/helpers/pagination.phtml
  44. 1 1
      app/views/index/rss.phtml
  45. 1 1
      app/views/subscription/index.phtml
  46. 1 2
      app/views/user/profile.phtml
  47. 3 0
      cli/i18n/I18nCompletionValidator.php
  48. 1 1
      cli/i18n/I18nData.php
  49. 1 1
      cli/i18n/I18nValidatorInterface.php
  50. 1 1
      docs/en/developers/03_Backend/05_Extensions.md
  51. 1 1
      docs/fr/developers/03_Backend/05_Extensions.md
  52. 2 2
      docs/i18n/freshrss.fr.po
  53. 1 1
      docs/i18n/templates/freshrss.pot
  54. 10 0
      lib/Minz/Configuration.php
  55. 3 0
      lib/Minz/Dispatcher.php
  56. 2 2
      lib/Minz/Error.php
  57. 1 1
      lib/Minz/Extension.php
  58. 4 4
      lib/Minz/ExtensionManager.php
  59. 3 0
      lib/Minz/Log.php
  60. 1 1
      lib/Minz/Migrator.php
  61. 0 1
      lib/Minz/ModelPdo.php
  62. 1 1
      lib/Minz/Paginator.php
  63. 4 3
      lib/Minz/Request.php
  64. 3 3
      lib/Minz/Translate.php
  65. 7 6
      lib/Minz/Url.php
  66. 1 1
      lib/Minz/View.php
  67. 2 0
      lib/http-conditional.php
  68. 5 1
      lib/lib_install.php
  69. 9 9
      lib/lib_phpQuery.php
  70. 65 8
      lib/lib_rss.php
  71. 5 8
      p/api/fever.php
  72. 22 3
      p/api/greader.php
  73. 1 1
      phpstan.neon

+ 0 - 1
CHANGELOG.md

@@ -6,7 +6,6 @@
 ## 2022-01-02 FreshRSS 1.19.1
 
 * Bug fixing
-	* Fix regression when creating a new user (only with PostgreSQL, MariaDB/MySQL) [#4116](https://github.com/FreshRSS/FreshRSS/pull/4116)
 	* Fix some filters for automatic article actions (e.g., `!pubdate:P3d`) [#4092](https://github.com/FreshRSS/FreshRSS/pull/4092)
 * Features
 	* New search operator on article IDs (useful to show a single article, extensions) [#4058](https://github.com/FreshRSS/FreshRSS/pull/4058)

+ 1 - 1
app/Controllers/apiController.php

@@ -3,7 +3,7 @@
 /**
  * This controller manage API-related features.
  */
-class FreshRSS_api_Controller extends Minz_ActionController {
+class FreshRSS_api_Controller extends FreshRSS_ActionController {
 
 	/**
 	 * Update the user API password.

+ 1 - 1
app/Controllers/authController.php

@@ -3,7 +3,7 @@
 /**
  * This controller handles action about authentication.
  */
-class FreshRSS_auth_Controller extends Minz_ActionController {
+class FreshRSS_auth_Controller extends FreshRSS_ActionController {
 	/**
 	 * This action handles authentication management page.
 	 *

+ 1 - 1
app/Controllers/categoryController.php

@@ -4,7 +4,7 @@
  * Controller to handle actions relative to categories.
  * User needs to be connected.
  */
-class FreshRSS_category_Controller extends Minz_ActionController {
+class FreshRSS_category_Controller extends FreshRSS_ActionController {
 	/**
 	 * This action is called before every other action in that class. It is
 	 * the common boiler plate for every action. It is triggered by the

+ 5 - 4
app/Controllers/configureController.php

@@ -3,7 +3,7 @@
 /**
  * Controller to handle every configuration options.
  */
-class FreshRSS_configure_Controller extends Minz_ActionController {
+class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 	/**
 	 * This action is called before every other action in that class. It is
 	 * the common boiler plate for every action. It is triggered by the
@@ -249,7 +249,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 			$volatile = [
 				'enable_keep_period' => true,
 				'keep_period_count' => $matches['count'],
-				'keep_period_unit' => str_replace($matches['count'], 1, $keepPeriod),
+				'keep_period_unit' => str_replace($matches['count'], '1', $keepPeriod),
 			];
 		}
 		FreshRSS_Context::$user_conf->volatile = $volatile;
@@ -295,7 +295,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 				if ($query['search']) {
 					$query['search'] = urldecode($query['search']);
 				}
-				$queries[] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
+				$queries[intval($key)] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
 			}
 			FreshRSS_Context::$user_conf->queries = $queries;
 			FreshRSS_Context::$user_conf->save();
@@ -304,7 +304,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 		} else {
 			$this->view->queries = array();
 			foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
-				$this->view->queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
+				$this->view->queries[intval($key)] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
 			}
 		}
 
@@ -315,6 +315,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 		$id = Minz_Request::param('id');
 		$this->view->displaySlider = false;
 		if (false !== $id) {
+			$id = intval($id);
 			$this->view->displaySlider = true;
 			$this->view->query = $this->view->queries[$id];
 			$this->view->queryId = $id;

+ 1 - 1
app/Controllers/entryController.php

@@ -3,7 +3,7 @@
 /**
  * Controller to handle every entry actions.
  */
-class FreshRSS_entry_Controller extends Minz_ActionController {
+class FreshRSS_entry_Controller extends FreshRSS_ActionController {
 
 	/**
 	 * JavaScript request or not.

+ 1 - 1
app/Controllers/errorController.php

@@ -3,7 +3,7 @@
 /**
  * Controller to handle error page.
  */
-class FreshRSS_error_Controller extends Minz_ActionController {
+class FreshRSS_error_Controller extends FreshRSS_ActionController {
 	/**
 	 * This action is the default one for the controller.
 	 *

+ 1 - 1
app/Controllers/extensionController.php

@@ -3,7 +3,7 @@
 /**
  * The controller to manage extensions.
  */
-class FreshRSS_extension_Controller extends Minz_ActionController {
+class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 	/**
 	 * This action is called before every other action in that class. It is
 	 * the common boiler plate for every action. It is triggered by the

+ 9 - 19
app/Controllers/feedController.php

@@ -3,7 +3,7 @@
 /**
  * Controller to handle every feed actions.
  */
-class FreshRSS_feed_Controller extends Minz_ActionController {
+class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 	/**
 	 * This action is called before every other action in that class. It is
 	 * the common boiler plate for every action. It is triggered by the
@@ -46,10 +46,10 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 
 		$url = trim($url);
 
-		/** @var string $url */
+		/** @var string|null $url */
 		$url = Minz_ExtensionManager::callHook('check_url_before_add', $url);
 		if (null === $url) {
-			throw new FreshRSS_FeedNotAdded_Exception($url, $title);
+			throw new FreshRSS_FeedNotAdded_Exception($url);
 		}
 
 		$cat = null;
@@ -77,10 +77,10 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			throw new FreshRSS_AlreadySubscribed_Exception($url, $feed->name());
 		}
 
-		/** @var FreshRSS_Feed $feed */
+		/** @var FreshRSS_Feed|null $feed */
 		$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
 		if ($feed === null) {
-			throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
+			throw new FreshRSS_FeedNotAdded_Exception($url);
 		}
 
 		$values = array(
@@ -97,7 +97,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		$id = $feedDAO->addFeed($values);
 		if (!$id) {
 			// There was an error in database... we cannot say what here.
-			throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
+			throw new FreshRSS_FeedNotAdded_Exception($url);
 		}
 		$feed->_id($id);
 
@@ -186,7 +186,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$attributes['timeout'] = $timeout > 0 ? $timeout : null;
 
 			try {
-				$feed = self::addFeed($url, '', $cat, null, $http_auth, $attributes);
+				$feed = self::addFeed($url, '', $cat, '', $http_auth, $attributes);
 			} catch (FreshRSS_BadUrl_Exception $e) {
 				// Given url was not a valid url!
 				Minz_Log::warning($e->getMessage());
@@ -202,7 +202,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			} catch (FreshRSS_AlreadySubscribed_Exception $e) {
 				return Minz_Request::bad(_t('feedback.sub.feed.already_subscribed', $e->feedName()), $url_redirect);
 			} catch (FreshRSS_FeedNotAdded_Exception $e) {
-				return Minz_Request::bad(_t('feedback.sub.feed.not_added', $e->feedName()), $url_redirect);
+				return Minz_Request::bad(_t('feedback.sub.feed.not_added', $e->url()), $url_redirect);
 			}
 
 			// Entries are in DB, we redirect to feed configuration page.
@@ -296,7 +296,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		$updated_feeds = 0;
 		$nb_new_articles = 0;
 		foreach ($feeds as $feed) {
-			/** @var FreshRSS_Feed $feed */
+			/** @var FreshRSS_Feed|null $feed */
 			$feed = Minz_ExtensionManager::callHook('feed_before_actualize', $feed);
 			if (null === $feed) {
 				continue;
@@ -874,14 +874,4 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$this->view->fatalError = _t('feedback.sub.feed.selector_preview.http_error');
 		}
 	}
-
-	/**
-	 * This method update TTL values for feeds if needed.
-	 * It changes the old default value (-2) to the new default value (0).
-	 * It changes the old disabled value (-1) to the default disabled value.
-	 */
-	private function updateTTL() {
-		$feedDAO = FreshRSS_Factory::createFeedDao();
-		$feedDAO->updateTTL();
-	}
 }

+ 3 - 7
app/Controllers/importExportController.php

@@ -3,9 +3,8 @@
 /**
  * Controller to handle every import and export actions.
  */
-class FreshRSS_importExport_Controller extends Minz_ActionController {
+class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 
-	private $catDAO;
 	private $entryDAO;
 	private $feedDAO;
 
@@ -21,7 +20,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 		require_once(LIB_PATH . '/lib_opml.php');
 
-		$this->catDAO = FreshRSS_Factory::createCategoryDao();
 		$this->entryDAO = FreshRSS_Factory::createEntryDao();
 		$this->feedDAO = FreshRSS_Factory::createFeedDao();
 	}
@@ -54,7 +52,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	public function importFile($name, $path, $username = null) {
 		self::minimumMemory(256);
 
-		$this->catDAO = FreshRSS_Factory::createCategoryDao($username);
 		$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
 		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 
@@ -492,9 +489,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	/**
 	 * This method import a JSON-based feed (Google Reader format).
 	 *
-	 * @param array $origin represents a feed.
-	 * @return FreshRSS_Feed if feed is in database at the end of the process,
-	 *         else null.
+	 * @param array<string,string> $origin represents a feed.
+	 * @return FreshRSS_Feed|null if feed is in database at the end of the process, else null.
 	 */
 	private function addFeedJson($origin) {
 		$return = null;

+ 1 - 1
app/Controllers/indexController.php

@@ -3,7 +3,7 @@
 /**
  * This class handles main actions of FreshRSS.
  */
-class FreshRSS_index_Controller extends Minz_ActionController {
+class FreshRSS_index_Controller extends FreshRSS_ActionController {
 
 	/**
 	 * This action only redirect on the default view mode (normal or global)

+ 3 - 3
app/Controllers/javascriptController.php

@@ -1,6 +1,6 @@
 <?php
 
-class FreshRSS_javascript_Controller extends Minz_ActionController {
+class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
 	public function firstAction() {
 		$this->view->_layout(false);
 	}
@@ -36,7 +36,7 @@ class FreshRSS_javascript_Controller extends Minz_ActionController {
 				if (strlen($s) >= 60) {
 					//CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z".
 					$this->view->salt1 = substr($s, 0, 29);
-					$this->view->nonce = sha1($salt . uniqid(mt_rand(), true));
+					$this->view->nonce = sha1($salt . uniqid('' . mt_rand(), true));
 					Minz_Session::_param('nonce', $this->view->nonce);
 					return;	//Success
 				}
@@ -52,6 +52,6 @@ class FreshRSS_javascript_Controller extends Minz_ActionController {
 		for ($i = 22; $i > 0; $i--) {
 			$this->view->salt1 .= $alphabet[mt_rand(0, 63)];
 		}
-		$this->view->nonce = sha1(mt_rand());
+		$this->view->nonce = sha1('' . mt_rand());
 	}
 }

+ 1 - 1
app/Controllers/statsController.php

@@ -3,7 +3,7 @@
 /**
  * Controller to handle application statistics.
  */
-class FreshRSS_stats_Controller extends Minz_ActionController {
+class FreshRSS_stats_Controller extends FreshRSS_ActionController {
 
 	/**
 	 * This action is called before every other action in that class. It is

+ 3 - 3
app/Controllers/subscriptionController.php

@@ -3,7 +3,7 @@
 /**
  * Controller to handle subscription actions.
  */
-class FreshRSS_subscription_Controller extends Minz_ActionController {
+class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 	/**
 	 * This action is called before every other action in that class. It is
 	 * the common boiler plate for every action. It is triggered by the
@@ -175,7 +175,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 				if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) {
 					$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
 					if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
-						$keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
+						$keepPeriod = str_replace('1', Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
 					}
 				} else {
 					$keepPeriod = false;
@@ -244,7 +244,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 				if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) {
 					$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
 					if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
-						$keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
+						$keepPeriod = str_replace('1', Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
 					}
 				} else {
 					$keepPeriod = false;

+ 1 - 1
app/Controllers/tagController.php

@@ -3,7 +3,7 @@
 /**
  * Controller to handle every tag actions.
  */
-class FreshRSS_tag_Controller extends Minz_ActionController {
+class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 
 	/**
 	 * JavaScript request or not.

+ 2 - 2
app/Controllers/updateController.php

@@ -1,6 +1,6 @@
 <?php
 
-class FreshRSS_update_Controller extends Minz_ActionController {
+class FreshRSS_update_Controller extends FreshRSS_ActionController {
 
 	public static function isGit() {
 		return is_dir(FRESHRSS_PATH . '/.git/');
@@ -261,7 +261,7 @@ class FreshRSS_update_Controller extends Minz_ActionController {
 				Minz_Request::forward(array(
 					'c' => 'update',
 					'a' => 'apply',
-					'params' => array('post_conf' => true)
+					'params' => array('post_conf' => '1')
 				), true);
 			} else {
 				Minz_Request::bad(_t('feedback.update.error', $res), [ 'c' => 'update', 'a' => 'index' ]);

+ 3 - 3
app/Controllers/userController.php

@@ -3,7 +3,7 @@
 /**
  * Controller to handle user actions.
  */
-class FreshRSS_user_Controller extends Minz_ActionController {
+class FreshRSS_user_Controller extends FreshRSS_ActionController {
 	/**
 	 * The username is also used as folder name, file name, and part of SQL table name.
 	 * '_' is a reserved internal username.
@@ -29,7 +29,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 
 			if (FreshRSS_Context::$system_conf->force_email_validation) {
 				$salt = FreshRSS_Context::$system_conf->salt;
-				$userConfig->email_validation_token = sha1($salt . uniqid(mt_rand(), true));
+				$userConfig->email_validation_token = sha1($salt . uniqid('' . mt_rand(), true));
 				$mailer = new FreshRSS_User_Mailer();
 				$mailer->send_email_need_validation($user, $userConfig);
 			}
@@ -536,7 +536,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 
 		if (Minz_Request::isPost()) {
 			$ok = true;
-			if ($ok && $self_deletion) {
+			if ($self_deletion) {
 				// We check the password if it's a self-destruction
 				$nonce = Minz_Session::param('nonce');
 				$challenge = Minz_Request::param('challenge', '');

+ 5 - 5
app/Exceptions/FeedNotAddedException.php

@@ -1,14 +1,14 @@
 <?php
 
 class FreshRSS_FeedNotAdded_Exception extends Exception {
-	private $feedName = '';
+	private $url = '';
 
-	public function __construct($url, $feedName) {
+	public function __construct($url) {
 		parent::__construct('Feed not added! ' . $url, 2147);
-		$this->feedName = $feedName;
+		$this->url = $url;
 	}
 
-	public function feedName() {
-		return $this->feedName;
+	public function url() {
+		return $this->url;
 	}
 }

+ 6 - 0
app/Mailers/UserMailer.php

@@ -4,6 +4,12 @@
  * Manage the emails sent to the users.
  */
 class FreshRSS_User_Mailer extends Minz_Mailer {
+
+	/**
+	 * @var FreshRSS_View
+	 */
+	protected $view;
+
 	public function send_email_need_validation($username, $user_config) {
 		Minz_Translate::reset($user_config->language);
 

+ 9 - 0
app/Models/ActionController.php

@@ -0,0 +1,9 @@
+<?php
+
+class FreshRSS_ActionController extends Minz_ActionController {
+
+	/**
+	 * @var FreshRSS_View
+	 */
+	protected $view;
+}

+ 1 - 1
app/Models/Auth.php

@@ -215,7 +215,7 @@ class FreshRSS_Auth {
 		$csrf = Minz_Session::param('csrf');
 		if ($csrf == '') {
 			$salt = FreshRSS_Context::$system_conf->salt;
-			$csrf = sha1($salt . uniqid(mt_rand(), true));
+			$csrf = sha1($salt . uniqid('' . mt_rand(), true));
 			Minz_Session::_param('csrf', $csrf);
 		}
 		return $csrf;

+ 1 - 0
app/Models/ConfigurationSetter.php

@@ -371,6 +371,7 @@ class FreshRSS_ConfigurationSetter {
 
 			$value = intval($value);
 			$limits = $limits_keys[$key];
+			// @phpstan-ignore-next-line
 			if ((!isset($limits['min']) || $value >= $limits['min']) &&
 				(!isset($limits['max']) || $value <= $limits['max'])
 			) {

+ 19 - 2
app/Models/Context.php

@@ -5,8 +5,17 @@
  * useful functions associated to the current view state.
  */
 class FreshRSS_Context {
+
+	/**
+	 * @var FreshRSS_UserConfiguration|null
+	 */
 	public static $user_conf = null;
+
+	/**
+	 * @var FreshRSS_SystemConfiguration|null
+	 */
 	public static $system_conf = null;
+
 	public static $categories = array();
 	public static $tags = array();
 
@@ -49,7 +58,11 @@ class FreshRSS_Context {
 		if ($reload || FreshRSS_Context::$system_conf == null) {
 			//TODO: Keep in session what we need instead of always reloading from disk
 			Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
-			FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
+			/**
+			 * @var FreshRSS_SystemConfiguration $system_conf
+			 */
+			$system_conf = Minz_Configuration::get('system');
+			FreshRSS_Context::$system_conf = $system_conf;
 			// Register the configuration setter for the system configuration
 			$configurationSetter = new FreshRSS_ConfigurationSetter();
 			FreshRSS_Context::$system_conf->_configurationSetter($configurationSetter);
@@ -80,7 +93,11 @@ class FreshRSS_Context {
 					FreshRSS_Context::$system_conf->configurationSetter());
 
 				Minz_Session::_param('currentUser', $username);
-				FreshRSS_Context::$user_conf = Minz_Configuration::get('user');
+				/**
+				 * @var FreshRSS_UserConfiguration $user_conf
+				 */
+				$user_conf = Minz_Configuration::get('user');
+				FreshRSS_Context::$user_conf = $user_conf;
 			} catch (Exception $ex) {
 				Minz_Log::warning($ex->getMessage(), USERS_PATH . '/_/log.txt');
 			}

+ 11 - 2
app/Models/Entry.php

@@ -260,7 +260,7 @@ class FreshRSS_Entry extends Minz_Model {
 		}
 		foreach ($booleanSearch->searches() as $filter) {
 			$ok = true;
-			if ($ok && $filter->getMinDate()) {
+			if ($filter->getMinDate()) {
 				$ok &= strnatcmp($this->id, $filter->getMinDate() . '000000') >= 0;
 			}
 			if ($ok && $filter->getNotMinDate()) {
@@ -451,12 +451,18 @@ class FreshRSS_Entry extends Minz_Model {
 			Minz_Log::warning('Error fetching content: HTTP code ' . $c_status . ': ' . $c_error . ' ' . $url);
 		}
 
-		if ($html) {
+		if (is_string($html) && strlen($html) > 0) {
 			require_once(LIB_PATH . '/lib_phpQuery.php');
+			/**
+			 * @var phpQueryObject @doc
+			 */
 			$doc = phpQuery::newDocument($html);
 
 			if ($maxRedirs > 0) {
 				//Follow any HTML redirection
+				/**
+				 * @var phpQueryObject @metas
+				 */
 				$metas = $doc->find('meta[http-equiv][content]');
 				foreach ($metas as $meta) {
 					if (strtolower(trim($meta->getAttribute('http-equiv'))) === 'refresh') {
@@ -470,6 +476,9 @@ class FreshRSS_Entry extends Minz_Model {
 				}
 			}
 
+			/**
+			 * @var phpQueryObject @content
+			 */
 			$content = $doc->find($path);
 			$html = trim(sanitizeHTML($content->__toString(), $url));
 			phpQuery::unloadDocuments();

+ 6 - 6
app/Models/EntryDAO.php

@@ -310,7 +310,7 @@ SQL;
 		$hasWhere = false;
 		$values = array();
 		if ($feedId !== false) {
-			$sql .= $hasWhere ? ' AND' : ' WHERE';
+			$sql .= ' WHERE';
 			$hasWhere = true;
 			$sql .= ' f.id=?';
 			$values[] = $feedId;
@@ -342,7 +342,7 @@ SQL;
 	 *
 	 * @param integer|array $ids
 	 * @param boolean $is_read
-	 * @return integer affected rows
+	 * @return integer|false affected rows
 	 */
 	public function markRead($ids, $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -415,7 +415,7 @@ SQL;
 	 * @param integer $idMax fail safe article ID
 	 * @param boolean $onlyFavorites
 	 * @param integer $priorityMin
-	 * @return integer affected rows
+	 * @return integer|false affected rows
 	 */
 	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0, $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -458,7 +458,7 @@ SQL;
 	 *
 	 * @param integer $id category ID
 	 * @param integer $idMax fail safe article ID
-	 * @return integer affected rows
+	 * @return integer|false affected rows
 	 */
 	public function markReadCat($id, $idMax = 0, $filters = null, $state = 0, $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -496,7 +496,7 @@ SQL;
 	 *
 	 * @param integer $id_feed feed ID
 	 * @param integer $idMax fail safe article ID
-	 * @return integer affected rows
+	 * @return integer|false affected rows
 	 */
 	public function markReadFeed($id_feed, $idMax = 0, $filters = null, $state = 0, $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -544,7 +544,7 @@ SQL;
 	 * Mark all the articles in a tag as read.
 	 * @param integer $id tag ID, or empty for targetting any tag
 	 * @param integer $idMax max article ID
-	 * @return integer affected rows
+	 * @return integer|false affected rows
 	 */
 	public function markReadTag($id = 0, $idMax = 0, $filters = null, $state = 0, $is_read = true) {
 		FreshRSS_UserDAO::touch();

+ 5 - 5
app/Models/EntryDAOSQLite.php

@@ -77,7 +77,7 @@ DROP TABLE IF EXISTS `tmp`;
 		$hasWhere = false;
 		$values = array();
 		if ($feedId !== false) {
-			$sql .= $hasWhere ? ' AND' : ' WHERE';
+			$sql .= ' WHERE';
 			$hasWhere = true;
 			$sql .= ' id=?';
 			$values[] = $feedId;
@@ -109,7 +109,7 @@ DROP TABLE IF EXISTS `tmp`;
 	 *
 	 * @param integer|array $ids
 	 * @param boolean $is_read
-	 * @return integer affected rows
+	 * @return integer|false affected rows
 	 */
 	public function markRead($ids, $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -169,7 +169,7 @@ DROP TABLE IF EXISTS `tmp`;
 	 * @param integer $idMax fail safe article ID
 	 * @param boolean $onlyFavorites
 	 * @param integer $priorityMin
-	 * @return integer affected rows
+	 * @return integer|false affected rows
 	 */
 	public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0, $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -210,7 +210,7 @@ DROP TABLE IF EXISTS `tmp`;
 	 *
 	 * @param integer $id category ID
 	 * @param integer $idMax fail safe article ID
-	 * @return integer affected rows
+	 * @return integer|false affected rows
 	 */
 	public function markReadCat($id, $idMax = 0, $filters = null, $state = 0, $is_read = true) {
 		FreshRSS_UserDAO::touch();
@@ -244,7 +244,7 @@ DROP TABLE IF EXISTS `tmp`;
 	 * Mark all the articles in a tag as read.
 	 * @param integer $id tag ID, or empty for targetting any tag
 	 * @param integer $idMax max article ID
-	 * @return integer affected rows
+	 * @return integer|false affected rows
 	 */
 	public function markReadTag($id = 0, $idMax = 0, $filters = null, $state = 0, $is_read = true) {
 		FreshRSS_UserDAO::touch();

+ 4 - 3
app/Models/Feed.php

@@ -259,6 +259,7 @@ class FreshRSS_Feed extends Minz_Model {
 
 	public function load($loadDetails = false, $noCache = false) {
 		if ($this->url !== null) {
+			// @phpstan-ignore-next-line
 			if (CACHE_PATH === false) {
 				throw new Minz_FileNotExistException(
 					'CACHE_PATH',
@@ -462,10 +463,10 @@ class FreshRSS_Feed extends Minz_Model {
 			$entry = new FreshRSS_Entry(
 				$this->id(),
 				$hasBadGuids ? '' : $guid,
-				$title === null ? '' : $title,
+				$title == '' ? '' : $title,
 				$author_names,
-				$content === null ? '' : $content,
-				$link === null ? '' : $link,
+				$content == '' ? '' : $content,
+				$link == '' ? '' : $link,
 				$date ? $date : time()
 			);
 			$entry->_tags($tags);

+ 1 - 1
app/Models/FormAuth.php

@@ -50,7 +50,7 @@ class FreshRSS_FormAuth {
 
 	public static function makeCookie($username, $password_hash) {
 		do {
-			$token = sha1(FreshRSS_Context::$system_conf->salt . $username . uniqid(mt_rand(), true));
+			$token = sha1(FreshRSS_Context::$system_conf->salt . $username . uniqid('' . mt_rand(), true));
 			$token_file = DATA_PATH . '/tokens/' . $token . '.txt';
 		} while (file_exists($token_file));
 

+ 2 - 2
app/Models/ReadingMode.php

@@ -81,14 +81,14 @@ class FreshRSS_ReadingMode {
 	}
 
 	/**
-	 * @return string
+	 * @return array<string>
 	 */
 	public function getUrlParams() {
 		return $this->urlParams;
 	}
 
 	/**
-	 * @param string $urlParams
+	 * @param array<string> $urlParams
 	 * @return FreshRSS_ReadingMode
 	 */
 	public function setUrlParams($urlParams) {

+ 1 - 1
app/Models/Share.php

@@ -11,7 +11,7 @@ class FreshRSS_Share {
 
 	/**
 	 * Register a new sharing option.
-	 * @param array<string,string> $share_options is an array defining the share option.
+	 * @param array<string,string|array<string>> $share_options is an array defining the share option.
 	 */
 	public static function register($share_options) {
 		$type = $share_options['type'];

+ 8 - 8
app/Models/StatsDAO.php

@@ -133,7 +133,7 @@ SQL;
 	 *
 	 * @param string $period format string to use for grouping
 	 * @param integer $feed id
-	 * @return array
+	 * @return array<int,int>
 	 */
 	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
 		$restrict = '';
@@ -178,7 +178,7 @@ SQL;
 	 * Calculates the average number of article per hour per feed
 	 *
 	 * @param integer $feed id
-	 * @return integer
+	 * @return float
 	 */
 	public function calculateEntryAveragePerFeedPerHour($feed = null) {
 		return $this->calculateEntryAveragePerFeedPerPeriod(1 / 24, $feed);
@@ -188,7 +188,7 @@ SQL;
 	 * Calculates the average number of article per day of week per feed
 	 *
 	 * @param integer $feed id
-	 * @return integer
+	 * @return float
 	 */
 	public function calculateEntryAveragePerFeedPerDayOfWeek($feed = null) {
 		return $this->calculateEntryAveragePerFeedPerPeriod(7, $feed);
@@ -198,7 +198,7 @@ SQL;
 	 * Calculates the average number of article per month per feed
 	 *
 	 * @param integer $feed id
-	 * @return integer
+	 * @return float
 	 */
 	public function calculateEntryAveragePerFeedPerMonth($feed = null) {
 		return $this->calculateEntryAveragePerFeedPerPeriod(30, $feed);
@@ -209,7 +209,7 @@ SQL;
 	 *
 	 * @param float $period number used to divide the number of day in the period
 	 * @param integer $feed id
-	 * @return integer
+	 * @return float
 	 */
 	protected function calculateEntryAveragePerFeedPerPeriod($period, $feed = null) {
 		$restrict = '';
@@ -337,7 +337,7 @@ SQL;
 	/**
 	 * Gets days ready for graphs
 	 *
-	 * @return string
+	 * @return array<string>
 	 */
 	public function getDays() {
 		return $this->convertToTranslatedJson(array(
@@ -354,7 +354,7 @@ SQL;
 	/**
 	 * Gets months ready for graphs
 	 *
-	 * @return string
+	 * @return array<string>
 	 */
 	public function getMonths() {
 		return $this->convertToTranslatedJson(array(
@@ -377,7 +377,7 @@ SQL;
 	 * Translates array content
 	 *
 	 * @param array $data
-	 * @return array
+	 * @return array<string>
 	 */
 	private function convertToTranslatedJson($data = array()) {
 		$translated = array_map(function($a) {

+ 4 - 4
app/Models/StatsDAOPGSQL.php

@@ -6,7 +6,7 @@ class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
 	 * Calculates the number of article per hour of the day per feed
 	 *
 	 * @param integer $feed id
-	 * @return string
+	 * @return array
 	 */
 	public function calculateEntryRepartitionPerFeedPerHour($feed = null) {
 		return $this->calculateEntryRepartitionPerFeedPerPeriod('hour', $feed);
@@ -16,7 +16,7 @@ class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
 	 * Calculates the number of article per day of week per feed
 	 *
 	 * @param integer $feed id
-	 * @return string
+	 * @return array
 	 */
 	public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) {
 		return $this->calculateEntryRepartitionPerFeedPerPeriod('day', $feed);
@@ -26,7 +26,7 @@ class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
 	 * Calculates the number of article per month per feed
 	 *
 	 * @param integer $feed
-	 * @return string
+	 * @return array
 	 */
 	public function calculateEntryRepartitionPerFeedPerMonth($feed = null) {
 		return $this->calculateEntryRepartitionPerFeedPerPeriod('month', $feed);
@@ -37,7 +37,7 @@ class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
 	 *
 	 * @param string $period format string to use for grouping
 	 * @param integer $feed id
-	 * @return string
+	 * @return array<int,int>
 	 */
 	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
 		$restrict = '';

+ 28 - 0
app/Models/SystemConfiguration.php

@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @property bool $allow_anonymous
+ * @property bool $allow_anonymous_refresh
+ * @property-read bool $allow_referrer
+ * @property-read bool $allow_robots
+ * @property bool $api_enabled
+ * @property string $archiving
+ * @property string $auth_type
+ * @property string $auto_update_url
+ * @property-read array<int,mixed> $curl_options
+ * @property string $default_user
+ * @property string $email_validation_token
+ * @property bool $force_email_validation
+ * @property-read bool $http_auth_auto_register
+ * @property-read string $http_auth_auto_register_email_field
+ * @property-read string $language
+ * @property array<string,int> $limits
+ * @property-read string $meta_description
+ * @property-read bool $pubsubhubbub_enabled
+ * @property-read string $salt
+ * @property-read bool $simplepie_syslog_enabled
+ * @property string $unsafe_autologin_enabled
+ */
+class FreshRSS_SystemConfiguration extends Minz_Configuration {
+
+}

+ 4 - 4
app/Models/TagDAO.php

@@ -74,8 +74,8 @@ SQL;
 	 * @param FreshRSS_Tag $tag
 	 */
 	public function addTagObject($tag) {
-		$tag = $this->searchByName($tag->name());
-		if (!$tag) {
+		$tag0 = $this->searchByName($tag->name());
+		if (!$tag0) {
 			$values = array(
 				'name' => $tag->name(),
 				'attributes' => $tag->attributes(),
@@ -198,7 +198,7 @@ SQL;
 	}
 
 	/**
-	 * @return FreshRSS_Tag
+	 * @return FreshRSS_Tag|null
 	 */
 	public function searchById($id) {
 		$sql = 'SELECT * FROM `_tag` WHERE id=?';
@@ -211,7 +211,7 @@ SQL;
 	}
 
 	/**
-	 * @return FreshRSS_Tag
+	 * @return FreshRSS_Tag|null
 	 */
 	public function searchByName($name) {
 		$sql = 'SELECT * FROM `_tag` WHERE name=?';

+ 64 - 0
app/Models/UserConfiguration.php

@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @property string $apiPasswordHash
+ * @property array<string,mixed> $archiving
+ * @property bool $auto_load_more
+ * @property bool $auto_remove_article
+ * @property bool $bottomline_date
+ * @property bool $bottomline_favorite
+ * @property bool $bottomline_link
+ * @property bool $bottomline_read
+ * @property bool $bottomline_sharing
+ * @property bool $bottomline_tags
+ * @property string $content_width
+ * @property-read string $default_state
+ * @property string $default_view
+ * @property string|bool $display_categories
+ * @property bool $display_posts
+ * @property string $email_validation_token
+ * @property-read string $enabled
+ * @property string $feverKey
+ * @property bool $hide_read_feeds
+ * @property int $html5_notif_timeout
+ * @property-read string $is_admin
+ * @property int|null $keep_history_default
+ * @property string $language
+ * @property bool $lazyload
+ * @property string $mail_login
+ * @property bool $mark_updated_article_unread
+ * @property array<string,bool> $mark_when
+ * @property int $max_posts_per_rss
+ * @property-read array<string,int> $limits
+ * @property int|null $old_entries
+ * @property bool $onread_jump_next
+ * @property string $passwordHash
+ * @property int $posts_per_page
+ * @property array<int,array<string,string>> $queries
+ * @property bool $reading_confirm
+ * @property int $since_hours_posts_per_rss
+ * @property bool $show_fav_unread
+ * @property bool $show_favicons
+ * @property bool $show_nav_buttons
+ * @property string $sort_order
+ * @property array<string,array<string>> $sharing
+ * @property array<string,string> $shortcuts
+ * @property bool $sides_close_article
+ * @property bool $sticky_post
+ * @property string $theme
+ * @property string $token
+ * @property bool $topline_date
+ * @property bool $topline_display_authors
+ * @property bool $topline_favorite
+ * @property bool $topline_link
+ * @property bool $topline_read
+ * @property bool $topline_summary
+ * @property string $topline_thumbnail
+ * @property int $ttl_default
+ * @property-read bool $unsafe_autologin_enabled
+ * @property string $view_mode
+ * @property array<string,mixed> $volatile
+ */
+class FreshRSS_UserConfiguration extends Minz_Configuration {
+
+}

+ 2 - 2
app/Models/UserQuery.php

@@ -22,7 +22,7 @@ class FreshRSS_UserQuery {
 	private $tag_dao;
 
 	/**
-	 * @param array $query
+	 * @param array<string,string> $query
 	 * @param FreshRSS_Searchable $feed_dao
 	 * @param FreshRSS_Searchable $category_dao
 	 */
@@ -55,7 +55,7 @@ class FreshRSS_UserQuery {
 	/**
 	 * Convert the current object to an array.
 	 *
-	 * @return array
+	 * @return array<string,string>
 	 */
 	public function toArray() {
 		return array_filter(array(

+ 12 - 0
app/Models/View.php

@@ -4,9 +4,11 @@ class FreshRSS_View extends Minz_View {
 
 	// Main views
 	public $callbackBeforeEntries;
+	public $callbackBeforeFeeds;
 	public $callbackBeforePagination;
 	public $categories;
 	public $category;
+	public $current_user;
 	public $entries;
 	public $entry;
 	public $feed;
@@ -33,6 +35,7 @@ class FreshRSS_View extends Minz_View {
 	public $status_files;
 	public $status_php;
 	public $update_to_apply;
+	public $status_database;
 
 	// Archiving
 	public $nb_total;
@@ -46,12 +49,19 @@ class FreshRSS_View extends Minz_View {
 	public $list_keys;
 
 	// User queries
+	/**
+	 * @var array<int,FreshRSS_UserQuery>
+	 */
 	public $queries;
+	/**
+	 * @var FreshRSS_UserQuery|null
+	 */
 	public $query;
 
 	// Export / Import
 	public $content;
 	public $entriesRaw;
+	public $entriesId;
 	public $entryIdsTagNames;
 	public $list_title;
 	public $queryId;
@@ -87,6 +97,7 @@ class FreshRSS_View extends Minz_View {
 	public $selectorSuccess;
 
 	// Extensions
+	public $available_extensions;
 	public $ext_details;
 	public $extension_list;
 	public $extension;
@@ -95,6 +106,7 @@ class FreshRSS_View extends Minz_View {
 	// Errors
 	public $code;
 	public $errorMessage;
+	public $message;
 
 	// Statistics
 	public $average;

+ 0 - 4
app/Services/ImportService.php

@@ -4,9 +4,6 @@
  * Provide methods to import files.
  */
 class FreshRSS_Import_Service {
-	/** @var string */
-	private $username;
-
 	/** @var FreshRSS_CategoryDAO */
 	private $catDAO;
 
@@ -21,7 +18,6 @@ class FreshRSS_Import_Service {
 	public function __construct($username) {
 		require_once(LIB_PATH . '/lib_opml.php');
 
-		$this->username = $username;
 		$this->catDAO = FreshRSS_Factory::createCategoryDao($username);
 		$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
 	}

+ 2 - 0
app/actualize_script.php

@@ -27,9 +27,11 @@ define('SIMPLEPIE_SYSLOG_ENABLED', FreshRSS_Context::$system_conf->simplepie_sys
  */
 function notice($message) {
 	Minz_Log::notice($message, ADMIN_LOG);
+	// @phpstan-ignore-next-line
 	if (!COPY_LOG_TO_SYSLOG && SIMPLEPIE_SYSLOG_ENABLED) {
 		syslog(LOG_NOTICE, $message);
 	}
+	// @phpstan-ignore-next-line
 	if (defined('STDOUT') && !COPY_SYSLOG_TO_STDERR) {
 		fwrite(STDOUT, $message . "\n");	//Unbuffered
 	}

+ 3 - 0
app/install.php

@@ -276,6 +276,9 @@ function freshrss_already_installed() {
 	$system_conf = null;
 	try {
 		Minz_Configuration::register('system', $conf_path);
+		/**
+		 * @var FreshRSS_SystemConfiguration $system_conf
+		 */
 		$system_conf = Minz_Configuration::get('system');
 	} catch (Minz_FileNotExistException $e) {
 		return false;

+ 2 - 2
app/views/configure/queries.phtml

@@ -16,7 +16,7 @@
 		<div class="form-group" id="query-group-<?= $key ?>" draggable="true">
 			<div class="box">
 				<div class="box-title">
-					<a class="configure open-slider" href="<?= _url('configure', 'query', 'id', $key) ?>"><?= _i('configure') ?></a><?= $query->getName() ?>
+					<a class="configure open-slider" href="<?= _url('configure', 'query', 'id', '' . $key) ?>"><?= _i('configure') ?></a><?= $query->getName() ?>
 					<input type="hidden" id="queries_<?= $key ?>_name" name="queries[<?= $key ?>][name]" value="<?= $query->getName() ?>"/>
 					<input type="hidden" id="queries_<?= $key ?>_url" name="queries[<?= $key ?>][url]" value="<?= $query->getUrl() ?>"/>
 					<input type="hidden" id="queries_<?= $key ?>_search" name="queries[<?= $key ?>][search]" value="<?= urlencode($query->getSearch()) ?>"/>
@@ -66,7 +66,7 @@
 	</a>
 	<div id="slider"<?= $class ?>>
 	<?php
-		if (isset($this->query)) {
+		if ($this->query != null) {
 			$this->renderHelper('configure/query');
 		}
 	?>

+ 1 - 0
app/views/helpers/index/normal/entry_bottom.phtml

@@ -35,6 +35,7 @@
 			?></li><?php
 		}
 	}
+	// @phpstan-ignore-next-line
 	if ($bottomline_labels) {
 	?><li class="item">
 		<div class="dropdown dynamictags">

+ 3 - 3
app/views/helpers/pagination.phtml

@@ -20,7 +20,7 @@
 
 <?php
 $hasAccess = FreshRSS_Auth::hasAccess();
-if ($url_mark_read && $hasAccess) { ?>
+if ($hasAccess) { ?>
 <form id="mark-read-pagination" method="post">
 <input type="hidden" name="_csrf" value="<?= FreshRSS_Auth::csrfToken() ?>" />
 <?php } else { ?>
@@ -32,7 +32,7 @@ if ($url_mark_read && $hasAccess) { ?>
 		<a id="load_more" href="<?= Minz_Url::display($url_next) ?>">
 			<?= _t('gen.pagination.load_more') ?>
 		</a>
-	<?php } elseif ($url_mark_read && $hasAccess) { ?>
+	<?php } elseif ($hasAccess) { ?>
 		<button id="bigMarkAsRead"
 			class="as-link <?= FreshRSS_Context::$user_conf->reading_confirm ? 'confirm" disabled="disabled' : '' ?>"
 			form="mark-read-pagination"
@@ -49,7 +49,7 @@ if ($url_mark_read && $hasAccess) { ?>
 	<?php } ?>
 	</li>
 </ul>
-<?php if ($url_mark_read && $hasAccess) { ?>
+<?php if ($hasAccess) { ?>
 </form>
 <?php } else {?>
 </div>

+ 1 - 1
app/views/index/rss.phtml

@@ -3,7 +3,7 @@
 <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
 	<channel>
 		<title><?= $this->rss_title ?></title>
-		<link><?= Minz_Url::display(null, 'html', true) ?></link>
+		<link><?= Minz_Url::display('', 'html', true) ?></link>
 		<description><?= _t('index.feed.rss_of', $this->rss_title) ?></description>
 		<pubDate><?= date('D, d M Y H:i:s O') ?></pubDate>
 		<lastBuildDate><?= gmdate('D, d M Y H:i:s') ?> GMT</lastBuildDate>

+ 1 - 1
app/views/subscription/index.phtml

@@ -22,7 +22,7 @@
 
 	<?php if (!$this->onlyFeedsWithError && $this->signalError){ ?>
 		<div>
-			<a class="btn" href="<?= _url('subscription', 'index', 'error', 1) ?>"><?= _i('look') ?> <?= _t('sub.feed.show.error') ?></a>
+			<a class="btn" href="<?= _url('subscription', 'index', 'error', '1') ?>"><?= _i('look') ?> <?= _t('sub.feed.show.error') ?></a>
 		</div>
 	<?php } ?>
 

+ 1 - 2
app/views/user/profile.phtml

@@ -57,8 +57,7 @@
 			<label class="group-name" for="token"><?= _t('admin.auth.token') ?></label>
 			<?php $token = FreshRSS_Context::$user_conf->token; ?>
 			<div class="group-controls">
-				<input type="text" id="token" name="token" value="<?= $token ?>" placeholder="<?= _t('gen.short.blank_to_disable') ?>"<?php
-					echo FreshRSS_Auth::accessNeedsAction() ? '' : ' disabled="disabled"'; ?> data-leave-validation="<?= $token ?>"/>
+				<input type="text" id="token" name="token" value="<?= $token ?>" placeholder="<?= _t('gen.short.blank_to_disable') ?>" data-leave-validation="<?= $token ?>"/>
 				<p class="help"><?= _i('help') ?> <?= _t('admin.auth.token_help') ?></p>
 				<kbd><?= Minz_Url::display(array('a' => 'rss', 'params' => array('user' => Minz_Session::param('currentUser'),
 					'token' => $token, 'hours' => FreshRSS_Context::$user_conf->since_hours_posts_per_rss)), 'html', true) ?></kbd>

+ 3 - 0
cli/i18n/I18nCompletionValidator.php

@@ -23,6 +23,9 @@ class I18nCompletionValidator implements I18nValidatorInterface {
 		return $this->result;
 	}
 
+	/**
+	 * @param array<string>|null $ignore
+	 */
 	public function validate($ignore) {
 		foreach ($this->reference as $file => $data) {
 			foreach ($data as $key => $value) {

+ 1 - 1
cli/i18n/I18nData.php

@@ -130,7 +130,7 @@ class I18nData {
 		if (array_key_exists($language, $this->data)) {
 			throw new Exception('The selected language already exist.');
 		}
-		if (!is_string($reference) && !array_key_exists($reference, $this->data)) {
+		if (!is_string($reference) || !array_key_exists($reference, $this->data)) {
 			$reference = static::REFERENCE_LANGUAGE;
 		}
 		$this->data[$language] = $this->data[$reference];

+ 1 - 1
cli/i18n/I18nValidatorInterface.php

@@ -19,7 +19,7 @@ interface I18nValidatorInterface {
 	/**
 	 * Display the validation report.
 	 *
-	 * @return array
+	 * @return string
 	 */
 	public function displayReport();
 

+ 1 - 1
docs/en/developers/03_Backend/05_Extensions.md

@@ -48,7 +48,7 @@ Code example:
 ```php
 <?php
 
-class FreshRSS_hello_Controller extends Minz_ActionController {
+class FreshRSS_hello_Controller extends FreshRSS_ActionController {
 	public function indexAction() {
 		$this->view->a_variable = 'FooBar';
 	}

+ 1 - 1
docs/fr/developers/03_Backend/05_Extensions.md

@@ -87,7 +87,7 @@ Exemple de code :
 ```php
 <?php
 
-class FreshRSS_hello_Controller extends Minz_ActionController {
+class FreshRSS_hello_Controller extends FreshRSS_ActionController {
 	public function indexAction() {
 		$this->view->a_variable = 'FooBar';
 	}

+ 2 - 2
docs/i18n/freshrss.fr.po

@@ -1870,7 +1870,7 @@ msgstr "Exemple de code :"
 msgid ""
 "<?php\n"
 "\n"
-"class FreshRSS_hello_Controller extends Minz_ActionController {\n"
+"class FreshRSS_hello_Controller extends FreshRSS_ActionController {\n"
 "\tpublic function indexAction() {\n"
 "\t\t$this->view->a_variable = 'FooBar';\n"
 "\t}\n"
@@ -1884,7 +1884,7 @@ msgid ""
 msgstr ""
 "<?php\n"
 "\n"
-"class FreshRSS_hello_Controller extends Minz_ActionController {\n"
+"class FreshRSS_hello_Controller extends FreshRSS_ActionController {\n"
 "\tpublic function indexAction() {\n"
 "\t\t$this->view->a_variable = 'FooBar';\n"
 "\t}\n"

+ 1 - 1
docs/i18n/templates/freshrss.pot

@@ -1489,7 +1489,7 @@ msgstr ""
 msgid ""
 "<?php\n"
 "\n"
-"class FreshRSS_hello_Controller extends Minz_ActionController {\n"
+"class FreshRSS_hello_Controller extends FreshRSS_ActionController {\n"
 "\tpublic function indexAction() {\n"
 "\t\t$this->view->a_variable = 'FooBar';\n"
 "\t}\n"

+ 10 - 0
lib/Minz/Configuration.php

@@ -2,6 +2,14 @@
 
 /**
  * Manage configuration for the application.
+ * @property-read string $base_url
+ * @property array<string|array<int,string>> $db
+ * @property-read string $disable_update
+ * @property-read string $environment
+ * @property-read array<string> $extensions_enabled
+ * @property-read string $mailer
+ * @property-read string $smtp
+ * @property string $title
  */
 class Minz_Configuration {
 	/**
@@ -58,6 +66,8 @@ class Minz_Configuration {
 
 	/**
 	 * The namespace of the current configuration.
+	 * Unused.
+	 * @phpstan-ignore-next-line
 	 */
 	private $namespace = '';
 

+ 3 - 0
lib/Minz/Dispatcher.php

@@ -41,6 +41,7 @@ class Minz_Dispatcher {
 				$this->createController (Minz_Request::controllerName ());
 				$this->controller->init ();
 				$this->controller->firstAction ();
+				// @phpstan-ignore-next-line
 				if (!self::$needsReset) {
 					$this->launchAction (
 						Minz_Request::actionName ()
@@ -49,6 +50,7 @@ class Minz_Dispatcher {
 				}
 				$this->controller->lastAction ();
 
+				// @phpstan-ignore-next-line
 				if (!self::$needsReset) {
 					$this->controller->declareCspHeader();
 					$this->controller->view ()->build ();
@@ -56,6 +58,7 @@ class Minz_Dispatcher {
 			} catch (Minz_Exception $e) {
 				throw $e;
 			}
+			// @phpstan-ignore-next-line
 		} while (self::$needsReset);
 	}
 

+ 2 - 2
lib/Minz/Error.php

@@ -13,7 +13,7 @@ class Minz_Error {
 	/**
 	* Permet de lancer une erreur
 	* @param int $code le type de l'erreur, par défaut 404 (page not found)
-	* @param array<string> $logs logs d'erreurs découpés de la forme
+	* @param array<string>|array<string,array<string>> $logs logs d'erreurs découpés de la forme
 	*      > $logs['error']
 	*      > $logs['warning']
 	*      > $logs['notice']
@@ -50,7 +50,7 @@ class Minz_Error {
 	/**
 	 * Permet de retourner les logs de façon à n'avoir que
 	 * ceux que l'on veut réellement
-	 * @param array<string> $logs les logs rangés par catégories (error, warning, notice)
+	 * @param array<string,string>|string $logs les logs rangés par catégories (error, warning, notice)
 	 * @return array<string> liste des logs, sans catégorie, en fonction de l'environment
 	 */
 	private static function processLogs ($logs) {

+ 1 - 1
lib/Minz/Extension.php

@@ -192,7 +192,7 @@ abstract class Minz_Extension {
 	 * Register a new hook.
 	 *
 	 * @param string $hook_name the hook name (must exist).
-	 * @param callable-string $hook_function the function name to call (must be callable).
+	 * @param callable-string|array<string> $hook_function the function name to call (must be callable).
 	 */
 	public function registerHook($hook_name, $hook_function) {
 		Minz_ExtensionManager::addHook($hook_name, $hook_function, $this);

+ 4 - 4
lib/Minz/ExtensionManager.php

@@ -210,7 +210,7 @@ class Minz_ExtensionManager {
 	 *
 	 * The extension init() method will be called.
 	 *
-	 * @param Minz_Extension $ext_name is the name of a valid extension present in $ext_list.
+	 * @param string $ext_name is the name of a valid extension present in $ext_list.
 	 */
 	public static function enable($ext_name) {
 		if (isset(self::$ext_list[$ext_name])) {
@@ -295,8 +295,8 @@ class Minz_ExtensionManager {
 	 * array keys.
 	 *
 	 * @param string $hook_name the hook to call.
-	 * @param array<mixed> $args additional parameters (for signature, please see self::$hook_list).
-	 * @return mixed final result of the called hook.
+	 * @param mixed $args additional parameters (for signature, please see self::$hook_list).
+	 * @return mixed|null final result of the called hook.
 	 */
 	public static function callHook($hook_name, ...$args) {
 		if (!isset(self::$hook_list[$hook_name])) {
@@ -328,7 +328,7 @@ class Minz_ExtensionManager {
 	 *
 	 * @param string $hook_name is the hook to call.
 	 * @param mixed $arg is the argument to pass to the first extension hook.
-	 * @return mixed final chained result of the hooks. If nothing is changed,
+	 * @return mixed|null final chained result of the hooks. If nothing is changed,
 	 *         the initial argument is returned.
 	 */
 	private static function callOneToOne($hook_name, $arg) {

+ 3 - 0
lib/Minz/Log.php

@@ -61,6 +61,7 @@ class Minz_Log {
 
 			$log = '[' . date('r') . '] [' . $level_label . '] --- ' . $information . "\n";
 
+			// @phpstan-ignore-next-line
 			if (defined('COPY_LOG_TO_SYSLOG') && COPY_LOG_TO_SYSLOG) {
 				syslog($level, '[' . $username . '] ' . trim($log));
 			}
@@ -84,6 +85,7 @@ class Minz_Log {
 	 */
 	protected static function ensureMaxLogSize($file_name) {
 		$maxSize = defined('MAX_LOG_SIZE') ? MAX_LOG_SIZE : 1048576;
+		// @phpstan-ignore-next-line
 		if ($maxSize > 0 && @filesize($file_name) > $maxSize) {
 			$fp = fopen($file_name, 'c+');
 			if ($fp && flock($fp, LOCK_EX)) {
@@ -98,6 +100,7 @@ class Minz_Log {
 			} else {
 				throw new Minz_PermissionDeniedException($file_name, Minz_Exception::ERROR);
 			}
+			// @phpstan-ignore-next-line
 			if ($fp) {
 				fclose($fp);
 			}

+ 1 - 1
lib/Minz/Migrator.php

@@ -160,7 +160,7 @@ class Minz_Migrator
 	 *
 	 * @param string $version The version of the migration (be careful, migrations
 	 *                        are sorted with the `strnatcmp` function)
-	 * @param callback $callback The migration function to execute, it should
+	 * @param callable $callback The migration function to execute, it should
 	 *                           return true on success and must return false
 	 *                           on error
 	 *

+ 0 - 1
lib/Minz/ModelPdo.php

@@ -15,7 +15,6 @@ class Minz_ModelPdo {
 	 */
 	public static $usesSharedPdo = true;
 	private static $sharedPdo = null;
-	private static $sharedPrefix;
 	private static $sharedCurrentUser;
 
 	protected $pdo;

+ 1 - 1
lib/Minz/Paginator.php

@@ -184,7 +184,7 @@ class Minz_Paginator {
 	}
 	private function _nbPage () {
 		if ($this->nbItemsPerPage > 0) {
-			$this->nbPage = ceil ($this->nbItems () / $this->nbItemsPerPage);
+			$this->nbPage = (int)ceil($this->nbItems() / $this->nbItemsPerPage);
 		}
 	}
 	public function _nbItems ($value) {

+ 4 - 3
lib/Minz/Request.php

@@ -43,6 +43,7 @@ class Minz_Request {
 		if (isset(self::$params[$key])) {
 			$p = self::$params[$key];
 			$tp = trim($p);
+			// @phpstan-ignore-next-line
 			if ($p === null || $tp === '' || $tp === 'null') {
 				return null;
 			} elseif ($p == false || $tp == '0' || $tp === 'false' || $tp === 'no') {
@@ -328,7 +329,7 @@ class Minz_Request {
 
 	/**
 	 * Relance une requête
-	 * @param array<string,string> $url l'url vers laquelle est relancée la requête
+	 * @param array<string,string|array<string,string>> $url l'url vers laquelle est relancée la requête
 	 * @param bool $redirect si vrai, force la redirection http
 	 *                > sinon, le dispatcher recharge en interne
 	 */
@@ -359,7 +360,7 @@ class Minz_Request {
 	/**
 	 * Wrappers good notifications + redirection
 	 * @param string $msg notification content
-	 * @param array<string,string> $url url array to where we should be forwarded
+	 * @param array<string,string|array<string,string>> $url url array to where we should be forwarded
 	 */
 	public static function good($msg, $url = array()) {
 		Minz_Request::setGoodNotification($msg);
@@ -369,7 +370,7 @@ class Minz_Request {
 	/**
 	 * Wrappers bad notifications + redirection
 	 * @param string $msg notification content
-	 * @param array<string,string> $url url array to where we should be forwarded
+	 * @param array<string,string|array<string,mixed>> $url url array to where we should be forwarded
 	 */
 	public static function bad($msg, $url = array()) {
 		Minz_Request::setBadNotification($msg);

+ 3 - 3
lib/Minz/Translate.php

@@ -85,7 +85,7 @@ class Minz_Translate {
 	 * Return the language to use in the application.
 	 * It returns the connected language if it exists then returns the first match from the
 	 * preferred languages then returns the default language
-	 * @param string $user the connected user language (nullable)
+	 * @param string|null $user the connected user language (nullable)
 	 * @param array<string> $preferred an array of the preferred languages
 	 * @param string $default the preferred language to use
 	 * @return string containing the language to use
@@ -179,7 +179,7 @@ class Minz_Translate {
 	/**
 	 * Translate a key into its corresponding value based on selected language.
 	 * @param string $key the key to translate.
-	 * @param string $args additional parameters for variable keys.
+	 * @param mixed $args additional parameters for variable keys.
 	 * @return string value corresponding to the key.
 	 *         If no value is found, return the key itself.
 	 */
@@ -247,7 +247,7 @@ class Minz_Translate {
 /**
  * Alias for Minz_Translate::t()
  * @param string $key
- * @param array<string> $args
+ * @param mixed $args
  */
 function _t($key, ...$args) {
 	return Minz_Translate::t($key, ...$args);

+ 7 - 6
lib/Minz/Url.php

@@ -6,13 +6,13 @@
 class Minz_Url {
 	/**
 	 * Affiche une Url formatée
-	 * @param array<string,string> $url l'url à formater définie comme un tableau :
+	 * @param string|array<string,string|array<string,mixed>> $url l'url à formater définie comme un tableau :
 	 *                    $url['c'] = controller
 	 *                    $url['a'] = action
 	 *                    $url['params'] = tableau des paramètres supplémentaires
 	 *             ou comme une chaîne de caractère
 	 * @param string $encodage pour indiquer comment encoder les & (& ou &amp; pour html)
-	 * @param bool $absolute
+	 * @param bool|string $absolute
 	 * @return string url formatée
 	 */
 	public static function display ($url = array (), $encodage = 'html', $absolute = false) {
@@ -96,8 +96,8 @@ class Minz_Url {
 
 	/**
 	 * Vérifie que les éléments du tableau représentant une url soit ok
-	 * @param array<string,string>|string $url sous forme de tableau (sinon renverra directement $url)
-	 * @return string url vérifié
+	 * @param array<string,array<string,string>> $url sous forme de tableau
+	 * @return array<string,array<string,string>> url vérifié
 	 */
 	public static function checkUrl ($url) {
 		$url_checked = $url;
@@ -121,7 +121,7 @@ class Minz_Url {
 /**
  * @param string $controller
  * @param string $action
- * @param array<string,string> $args
+ * @param string $args
  */
 function _url ($controller, $action, ...$args) {
 	$nb_args = count($args);
@@ -132,7 +132,8 @@ function _url ($controller, $action, ...$args) {
 
 	$params = array ();
 	for ($i = 0; $i < $nb_args; $i += 2) {
-		$params[$args[$i]] = $args[$i + 1];
+		$arg = $args[$i];
+		$params[$arg] = $args[$i + 1];
 	}
 
 	return Minz_Url::display (array ('c' => $controller, 'a' => $action, 'params' => $params));

+ 1 - 1
lib/Minz/View.php

@@ -146,7 +146,7 @@ class Minz_View {
 
 	/**
 	 * Choose the current view layout.
-	 * @param string $layout the layout name to use, false to use no layouts.
+	 * @param string|false $layout the layout name to use, false to use no layouts.
 	 */
 	public function _layout($layout) {
 		if ($layout) {

+ 2 - 0
lib/http-conditional.php

@@ -93,12 +93,14 @@ function httpConditional($UnixTimeStamp,$cacheSeconds=0,$cachePrivacy=0,$feedMod
 	}
 	$etagServer='"'.md5($scriptName.$myQuery.'#'.$dateLastModif).'"';
 
+	// @phpstan-ignore-next-line
 	if ((!$is412)&&isset($_SERVER['HTTP_IF_MATCH']))
 	{//rfc2616-sec14.html#sec14.24
 		$etagsClient=stripslashes($_SERVER['HTTP_IF_MATCH']);
 		$etagsClient=str_ireplace('-gzip','',$etagsClient);
 		$is412=(($etagsClient!=='*')&&(strpos($etagsClient,$etagServer)===false));
 	}
+	// @phpstan-ignore-next-line
 	if ($is304&&isset($_SERVER['HTTP_IF_MODIFIED_SINCE']))
 	{//rfc2616-sec14.html#sec14.25 //rfc1945.txt
 		$nbCond++;

+ 5 - 1
lib/lib_install.php

@@ -41,9 +41,13 @@ function checkRequirements($dbType = '') {
 	$xml = function_exists('xml_parser_create');
 	$json = function_exists('json_encode');
 	$mbstring = extension_loaded('mbstring');
+	// @phpstan-ignore-next-line
 	$data = DATA_PATH && is_writable(DATA_PATH);
+	// @phpstan-ignore-next-line
 	$cache = CACHE_PATH && is_writable(CACHE_PATH);
+	// @phpstan-ignore-next-line
 	$tmp = TMP_PATH && is_writable(TMP_PATH);
+	// @phpstan-ignore-next-line
 	$users = USERS_PATH && is_writable(USERS_PATH);
 	$favicons = is_writable(join_path(DATA_PATH, 'favicons'));
 
@@ -73,7 +77,7 @@ function checkRequirements($dbType = '') {
 }
 
 function generateSalt() {
-	return sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__)));
+	return sha1(uniqid('' . mt_rand(), true).implode('', stat(__FILE__)));
 }
 
 function initDb() {

+ 9 - 9
lib/lib_phpQuery.php

@@ -4642,7 +4642,7 @@ abstract class phpQuery {
 	 * Creates new document from markup.
 	 * Chainable.
 	 *
-	 * @param unknown_type $markup
+	 * @param string $markup
 	 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 	 */
 	public static function newDocument($markup = null, $contentType = null) {
@@ -4655,7 +4655,7 @@ abstract class phpQuery {
 	 * Creates new document from markup.
 	 * Chainable.
 	 *
-	 * @param unknown_type $markup
+	 * @param string $markup
 	 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 	 */
 	public static function newDocumentHTML($markup = null, $charset = null) {
@@ -4668,7 +4668,7 @@ abstract class phpQuery {
 	 * Creates new document from markup.
 	 * Chainable.
 	 *
-	 * @param unknown_type $markup
+	 * @param string $markup
 	 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 	 */
 	public static function newDocumentXML($markup = null, $charset = null) {
@@ -4681,7 +4681,7 @@ abstract class phpQuery {
 	 * Creates new document from markup.
 	 * Chainable.
 	 *
-	 * @param unknown_type $markup
+	 * @param string $markup
 	 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 	 */
 	public static function newDocumentXHTML($markup = null, $charset = null) {
@@ -4694,7 +4694,7 @@ abstract class phpQuery {
 	 * Creates new document from markup.
 	 * Chainable.
 	 *
-	 * @param unknown_type $markup
+	 * @param string $markup
 	 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 	 */
 	public static function newDocumentPHP($markup = null, $contentType = "text/html") {
@@ -4784,7 +4784,7 @@ abstract class phpQuery {
 	 * Creates new document from markup.
 	 * Chainable.
 	 *
-	 * @param unknown_type $markup
+	 * @param string $markup
 	 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 	 */
 	public static function newDocumentFileHTML($file, $charset = null) {
@@ -4797,7 +4797,7 @@ abstract class phpQuery {
 	 * Creates new document from markup.
 	 * Chainable.
 	 *
-	 * @param unknown_type $markup
+	 * @param string $markup
 	 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 	 */
 	public static function newDocumentFileXML($file, $charset = null) {
@@ -4810,7 +4810,7 @@ abstract class phpQuery {
 	 * Creates new document from markup.
 	 * Chainable.
 	 *
-	 * @param unknown_type $markup
+	 * @param string $markup
 	 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 	 */
 	public static function newDocumentFileXHTML($file, $charset = null) {
@@ -4823,7 +4823,7 @@ abstract class phpQuery {
 	 * Creates new document from markup.
 	 * Chainable.
 	 *
-	 * @param unknown_type $markup
+	 * @param string $markup
 	 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 	 */
 	public static function newDocumentFilePHP($file, $contentType = null) {

+ 65 - 8
lib/lib_rss.php

@@ -9,6 +9,7 @@ if (!function_exists('mb_strcut')) {
 	}
 }
 
+// @phpstan-ignore-next-line
 if (COPY_SYSLOG_TO_STDERR) {
 	openlog('FreshRSS', LOG_CONS | LOG_ODELAY | LOG_PID | LOG_PERROR, LOG_USER);
 } else {
@@ -18,7 +19,7 @@ if (COPY_SYSLOG_TO_STDERR) {
 /**
  * Build a directory path by concatenating a list of directory names.
  *
- * @param array<string> $path_parts a list of directory names
+ * @param string $path_parts a list of directory names
  * @return string corresponding to the final pathname
  */
 function join_path(...$path_parts) {
@@ -52,6 +53,10 @@ function classAutoloader($class) {
 spl_autoload_register('classAutoloader');
 //</Auto-loading>
 
+/**
+ * @param string $url
+ * @return string
+ */
 function idn_to_puny($url) {
 	if (function_exists('idn_to_ascii')) {
 		$idn = parse_url($url, PHP_URL_HOST);
@@ -73,6 +78,11 @@ function idn_to_puny($url) {
 	return $url;
 }
 
+/**
+ * @param string $url
+ * @param bool $fixScheme
+ * @return string|false
+ */
 function checkUrl($url, $fixScheme = true) {
 	$url = trim($url);
 	if ($url == '') {
@@ -92,18 +102,39 @@ function checkUrl($url, $fixScheme = true) {
 	}
 }
 
+/**
+ * @param string $text
+ * @return string
+ */
 function safe_ascii($text) {
 	return filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
 }
 
 if (function_exists('mb_convert_encoding')) {
+	/**
+	 * @param string $text
+	 * @return string
+	 */
 	function safe_utf8($text) { return mb_convert_encoding($text, 'UTF-8', 'UTF-8'); }
 } elseif (function_exists('iconv')) {
+	/**
+	 * @param string $text
+	 * @return string
+	 */
 	function safe_utf8($text) { return iconv('UTF-8', 'UTF-8//IGNORE', $text); }
 } else {
+	/**
+	 * @param string $text
+	 * @return string
+	 */
 	function safe_utf8($text) { return $text; }
 }
 
+/**
+ * @param string $text
+ * @param bool $extended
+ * @return string
+ */
 function escapeToUnicodeAlternative($text, $extended = true) {
 	$text = htmlspecialchars_decode($text, ENT_QUOTES);
 
@@ -156,6 +187,10 @@ function timestamptodate ($t, $hour = true) {
 	return @date ($date, $t);
 }
 
+/**
+ * @param string $text
+ * @return string
+ */
 function html_only_entity_decode($text) {
 	static $htmlEntitiesOnly = null;
 	if ($htmlEntitiesOnly === null) {
@@ -167,6 +202,10 @@ function html_only_entity_decode($text) {
 	return $text == '' ? '' : strtr($text, $htmlEntitiesOnly);
 }
 
+/**
+ * @param array<string,mixed> $attributes
+ * @return SimplePie
+ */
 function customSimplePie($attributes = array()) {
 	$limits = FreshRSS_Context::$system_conf->limits;
 	$simplePie = new SimplePie();
@@ -276,7 +315,7 @@ function sanitizeHTML($data, $base = '', $maxLength = false) {
  */
 function validateEmailAddress($email) {
 	$mailer = new PHPMailer\PHPMailer\PHPMailer();
-	$mailer->Charset = 'utf-8';
+	$mailer->CharSet = 'utf-8';
 	$punyemail = $mailer->punyencodeAddress($email);
 	return PHPMailer\PHPMailer\PHPMailer::validateAddress($punyemail, 'html5');
 }
@@ -294,9 +333,12 @@ function lazyimg($content) {
 	);
 }
 
+/**
+ * @return string
+ */
 function uTimeString() {
 	$t = @gettimeofday();
-	return $t['sec'] . str_pad($t['usec'], 6, '0', STR_PAD_LEFT);
+	return $t['sec'] . str_pad('' . $t['usec'], 6, '0', STR_PAD_LEFT);
 }
 
 function invalidateHttpCache($username = '') {
@@ -311,6 +353,9 @@ function invalidateHttpCache($username = '') {
 	return $ok;
 }
 
+/**
+ * @return array<string>
+ */
 function listUsers() {
 	$final_list = array();
 	$base_path = join_path(DATA_PATH, 'users');
@@ -349,7 +394,7 @@ function max_registrations_reached() {
  * objects. If you need a long-time configuration, please don't use this function.
  *
  * @param string $username the name of the user of which we want the configuration.
- * @return Minz_Configuration|null object, or null if the configuration cannot be loaded.
+ * @return FreshRSS_UserConfiguration|null object, or null if the configuration cannot be loaded.
  */
 function get_user_configuration($username) {
 	if (!FreshRSS_user_Controller::checkUsername($username)) {
@@ -368,10 +413,16 @@ function get_user_configuration($username) {
 		return null;
 	}
 
-	return Minz_Configuration::get($namespace);
+	/**
+	 * @var FreshRSS_UserConfiguration $user_conf
+	 */
+	$user_conf = Minz_Configuration::get($namespace);
+	return $user_conf;
 }
 
-
+/**
+ * @return string
+ */
 function httpAuthUser() {
 	if (!empty($_SERVER['REMOTE_USER'])) {
 		return $_SERVER['REMOTE_USER'];
@@ -383,6 +434,9 @@ function httpAuthUser() {
 	return '';
 }
 
+/**
+ * @return bool
+ */
 function cryptAvailable() {
 	try {
 		$hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG';
@@ -425,8 +479,11 @@ function check_install_php() {
  */
 function check_install_files() {
 	return array(
+		// @phpstan-ignore-next-line
 		'data' => DATA_PATH && is_writable(DATA_PATH),
+		// @phpstan-ignore-next-line
 		'cache' => CACHE_PATH && is_writable(CACHE_PATH),
+		// @phpstan-ignore-next-line
 		'users' => USERS_PATH && is_writable(USERS_PATH),
 		'favicons' => is_writable(DATA_PATH . '/favicons'),
 		'tokens' => is_writable(DATA_PATH . '/tokens'),
@@ -497,8 +554,8 @@ function recursive_unlink($dir) {
 /**
  * Remove queries where $get is appearing.
  * @param string $get the get attribute which should be removed.
- * @param array<string,string> $queries an array of queries.
- * @return array<string,string> whithout queries where $get is appearing.
+ * @param array<int,array<string,string>> $queries an array of queries.
+ * @return array<int,array<string,string>> whithout queries where $get is appearing.
  */
 function remove_query_by_get($get, $queries) {
 	$final_queries = array();

+ 5 - 8
p/api/fever.php

@@ -30,6 +30,9 @@ Minz_Session::init('FreshRSS', true);
 // <Debug>
 $ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576);
 
+/**
+ * @return string
+ */
 function debugInfo() {
 	if (function_exists('getallheaders')) {
 		$ALL_HEADERS = getallheaders();
@@ -503,18 +506,12 @@ class FeverAPI
 
 		if (isset($_REQUEST['max_id'])) {
 			// use the max_id argument to request the previous $item_limit items
-			$max_id = '' . $_REQUEST['max_id'];
-			if (!ctype_digit($max_id)) {
-				$max_id = null;
-			}
+			$max_id = ctype_digit('' . $_REQUEST['max_id']) ? intval($_REQUEST['max_id']) : null;
 		} elseif (isset($_REQUEST['with_ids'])) {
 			$entry_ids = explode(',', $_REQUEST['with_ids']);
 		} elseif (isset($_REQUEST['since_id'])) {
 			// use the since_id argument to request the next $item_limit items
-			$since_id = '' . $_REQUEST['since_id'];
-			if (!ctype_digit($since_id)) {
-				$since_id = null;
-			}
+			$since_id = ctype_digit('' . $_REQUEST['since_id']) ? intval($_REQUEST['since_id']) : null;
 		}
 
 		$items = array();

+ 22 - 3
p/api/greader.php

@@ -29,20 +29,36 @@ require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
 $ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576);
 
 if (PHP_INT_SIZE < 8) {	//32-bit
+	/**
+	 * @param string|int $dec
+	 * @return string
+	 */
 	function dec2hex($dec) {
 		return str_pad(gmp_strval(gmp_init($dec, 10), 16), 16, '0', STR_PAD_LEFT);
 	}
+	/**
+	 * @param string $hex
+	 * @return string
+	 */
 	function hex2dec($hex) {
-		if (!ctype_xdigit($hex)) return 0;
+		if (!ctype_xdigit($hex)) return '0';
 		return gmp_strval(gmp_init($hex, 16), 10);
 	}
 } else {	//64-bit
+	/**
+	 * @param string|int $dec
+	 * @return string
+	 */
 	function dec2hex($dec) {	//http://code.google.com/p/google-reader-api/wiki/ItemId
 		return str_pad(dechex($dec), 16, '0', STR_PAD_LEFT);
 	}
+	/**
+	 * @param string $hex
+	 * @return string
+	 */
 	function hex2dec($hex) {
-		if (!ctype_xdigit($hex)) return 0;
-		return hexdec($hex);
+		if (!ctype_xdigit($hex)) return '0';
+		return '' . hexdec($hex);
 	}
 }
 
@@ -79,6 +95,9 @@ function multiplePosts($name) {	//https://bugs.php.net/bug.php?id=51633
 	return $result;
 }
 
+/**
+ * @return string
+ */
 function debugInfo() {
 	if (function_exists('getallheaders')) {
 		$ALL_HEADERS = getallheaders();

+ 1 - 1
phpstan.neon

@@ -1,6 +1,6 @@
 parameters:
 	# TODO: Increase rule-level https://phpstan.org/user-guide/rule-levels
-	level: 1
+	level: 5
 	fileExtensions:
 		- php
 		- phtml