PHPIndex

This page lists files in the current directory. You can view content, get download/execute commands for Wget, Curl, or PowerShell, or filter the list using wildcards (e.g., `*.sh`).

ActionController.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ActionController.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
*/

/**
 * The Minz_ActionController class is a controller in the MVC paradigm
 */
abstract class Minz_ActionController {

	/** @var array<string,string> */
	private static array $csp_default = [
		'default-src' => "'self'",
	];

	/** @var array<string,string> */
	private array $csp_policies;

	/** @var Minz_View */
	protected $view;

	/**
	 * Gives the possibility to override the default view model type.
	 * @var class-string
	 * @deprecated Use constructor with view type instead
	 */
	public static string $defaultViewType = Minz_View::class;

	/**
	 * @phpstan-param class-string|'' $viewType
	 * @param string $viewType Name of the class (inheriting from Minz_View) to use for the view model
	 */
	public function __construct(string $viewType = '') {
		$this->csp_policies = self::$csp_default;
		$view = null;
		if ($viewType !== '' && class_exists($viewType)) {
			$view = new $viewType();
			if (!($view instanceof Minz_View)) {
				$view = null;
			}
		}
		if ($view === null && class_exists(self::$defaultViewType)) {
			$view = new self::$defaultViewType();
			if (!($view instanceof Minz_View)) {
				$view = null;
			}
		}
		$this->view = $view ?? new Minz_View();
		$view_path = Minz_Request::controllerName() . '/' . Minz_Request::actionName() . '.phtml';
		$this->view->_path($view_path);
		$this->view->attributeParams();
	}

	/**
	 * Getteur
	 */
	public function view(): Minz_View {
		return $this->view;
	}

	/**
	 * Set default CSP policies.
	 * @param array<string,string> $policies An array where keys are directives and values are sources.
	 */
	public static function _defaultCsp(array $policies): void {
		if (!isset($policies['default-src'])) {
			Minz_Log::warning('Default CSP policy is not declared', ADMIN_LOG);
		}
		self::$csp_default = $policies;
	}

	/**
	 * Set CSP policies.
	 *
	 * A default-src directive should always be given.
	 *
	 * References:
	 * - https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
	 * - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
	 *
	 * @param array<string,string> $policies An array where keys are directives and values are sources.
	 */
	protected function _csp(array $policies): void {
		if (!isset($policies['default-src'])) {
			$action = Minz_Request::controllerName() . '#' . Minz_Request::actionName();
			Minz_Log::warning(
				"Default CSP policy is not declared for action {$action}.",
				ADMIN_LOG
			);
		}
		$this->csp_policies = $policies;
	}

	/**
	 * Send HTTP Content-Security-Policy header based on declared policies.
	 */
	public function declareCspHeader(): void {
		$policies = [];
		foreach (Minz_ExtensionManager::listExtensions(true) as $extension) {
			$extension->amendCsp($this->csp_policies);
		}
		foreach ($this->csp_policies as $directive => $sources) {
			$policies[] = $directive . ' ' . $sources;
		}
		header('Content-Security-Policy: ' . implode('; ', $policies));
	}

	/**
	 * Méthodes à redéfinir (ou non) par héritage
	 * firstAction est la première méthode exécutée par le Dispatcher
	 * lastAction est la dernière
	 */
	public function init(): void { }
	public function firstAction(): void { }
	public function lastAction(): void { }
}
ActionException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ActionException.php'
View Content
<?php
declare(strict_types=1);

class Minz_ActionException extends Minz_Exception {
	public function __construct(string $controller_name, string $action_name, int $code = self::ERROR) {
		// Just for security, as we are not supposed to get non-alphanumeric characters.
		$action_name = rawurlencode($action_name);

		$message = "Invalid action name “{$action_name}” for controller “{$controller_name}”.";
		parent::__construct($message, $code);
	}
}
Configuration.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Configuration.php'
View Content
<?php
declare(strict_types=1);

/**
 * Manage configuration for the application.
 * @property string $base_url
 * @property array{'type':string,'host':string,'user':string,'password':string,'base':string,'prefix':string,
 *  'connection_uri_params':string,'pdo_options':array<int,int|string|bool>} $db
 * @property bool $disable_update
 * @property string $environment
 * @property array<string,bool> $extensions_enabled
 * @property-read string $mailer
 * @property-read array{'hostname':string,'host':string,'auth':bool,'username':string,'password':string,'secure':string,'port':int,'from':string} $smtp
 * @property string $title
 */
class Minz_Configuration {
	/**
	 * The list of configurations.
	 * @var array<string,static>
	 */
	private static array $config_list = array();

	/**
	 * Add a new configuration to the list of configuration.
	 *
	 * @param string $namespace the name of the current 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 {
		self::$config_list[$namespace] = new static(
			$namespace, $config_filename, $default_filename, $configuration_setter
		);
	}

	/**
	 * Parse a file and return its data.
	 *
	 * @param string $filename the name of the file to parse.
	 * @return array<string,mixed> of values
	 * @throws Minz_FileNotExistException if the file does not exist or is invalid.
	 */
	public static function load(string $filename): array {
		$data = @include($filename);
		if (is_array($data)) {
			return $data;
		} else {
			throw new Minz_FileNotExistException($filename);
		}
	}

	/**
	 * Return the configuration related to a given namespace.
	 *
	 * @param string $namespace the name of the configuration to get.
	 * @return static object
	 * @throws Minz_ConfigurationNamespaceException if the namespace does not exist.
	 */
	public static function get(string $namespace) {
		if (!isset(self::$config_list[$namespace])) {
			throw new Minz_ConfigurationNamespaceException(
				$namespace . ' namespace does not exist'
			);
		}

		return self::$config_list[$namespace];
	}

	/**
	 * The namespace of the current configuration.
	 * Unused.
	 * @phpstan-ignore property.onlyWritten
	 */
	private string $namespace = '';

	/**
	 * The filename for the current configuration.
	 */
	private string $config_filename = '';

	/**
	 * The filename for the current default values, null by default.
	 */
	private ?string $default_filename = null;

	/**
	 * The configuration values, an empty array by default.
	 * @var array<string,mixed>
	 */
	private array $data = [];

	/**
	 * An object which help to set good values in configuration.
	 */
	private ?Minz_ConfigurationSetterInterface $configuration_setter = null;

	/**
	 * Create a new Minz_Configuration object.
	 *
	 * @param string $namespace the name of the current 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
	 */
	final private function __construct(string $namespace, string $config_filename, string $default_filename = null,
		Minz_ConfigurationSetterInterface $configuration_setter = null) {
		$this->namespace = $namespace;
		$this->config_filename = $config_filename;
		$this->default_filename = $default_filename;
		$this->_configurationSetter($configuration_setter);

		if ($this->default_filename != null) {
			$this->data = self::load($this->default_filename);
		}

		try {
			$this->data = array_replace_recursive(
				$this->data, self::load($this->config_filename)
			);
		} catch (Minz_FileNotExistException $e) {
			if ($this->default_filename == null) {
				throw $e;
			}
		}
	}

	/**
	 * Set a configuration setter for the current configuration.
	 * @param Minz_ConfigurationSetterInterface|null $configuration_setter the setter to call when modifying data.
	 */
	public function _configurationSetter(?Minz_ConfigurationSetterInterface $configuration_setter): void {
		if (is_callable(array($configuration_setter, 'handle'))) {
			$this->configuration_setter = $configuration_setter;
		}
	}

	public function configurationSetter(): ?Minz_ConfigurationSetterInterface {
		return $this->configuration_setter;
	}

	/**
	 * Check if a parameter is defined in the configuration
	 */
	public function hasParam(string $key): bool {
		return isset($this->data[$key]);
	}

	/**
	 * Return the value of the given param.
	 *
	 * @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.
	 */
	public function param(string $key, $default = null) {
		if (isset($this->data[$key])) {
			return $this->data[$key];
		} elseif (!is_null($default)) {
			return $default;
		} else {
			Minz_Log::warning($key . ' does not exist in configuration');
			return null;
		}
	}

	/**
	 * A wrapper for param().
	 * @return array|mixed
	 */
	public function __get(string $key) {
		return $this->param($key);
	}

	/**
	 * Set or remove a param.
	 *
	 * @param string $key the param name to set.
	 * @param mixed $value the value to set. If null, the key is removed from the configuration.
	 */
	public function _param(string $key, $value = null): void {
		if ($this->configuration_setter !== null && $this->configuration_setter->support($key)) {
			$this->configuration_setter->handle($this->data, $key, $value);
		} elseif (isset($this->data[$key]) && is_null($value)) {
			unset($this->data[$key]);
		} elseif ($value !== null) {
			$this->data[$key] = $value;
		}
	}

	/**
	 * A wrapper for _param().
	 * @param mixed $value
	 */
	public function __set(string $key, $value): void {
		$this->_param($key, $value);
	}

	/**
	 * Save the current configuration in the configuration file.
	 */
	public function save(): bool {
		$back_filename = $this->config_filename . '.bak.php';
		@rename($this->config_filename, $back_filename);

		if (file_put_contents($this->config_filename,
			"<?php\nreturn " . var_export($this->data, true) . ';', LOCK_EX) === false) {
			return false;
		}

		// Clear PHP cache for include
		if (function_exists('opcache_invalidate')) {
			opcache_invalidate($this->config_filename);
		}

		return true;
	}
}
ConfigurationException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ConfigurationException.php'
View Content
<?php
declare(strict_types=1);
class Minz_ConfigurationException extends Minz_Exception {
	public function __construct(string $error, int $code = self::ERROR) {
		$message = 'Configuration error: ' . $error;
		parent::__construct($message, $code);
	}
}
ConfigurationNamespaceException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ConfigurationNamespaceException.php'
View Content
<?php
declare(strict_types=1);

class Minz_ConfigurationNamespaceException extends Minz_ConfigurationException {
}
ConfigurationParamException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ConfigurationParamException.php'
View Content
<?php
declare(strict_types=1);

class Minz_ConfigurationParamException extends Minz_ConfigurationException {
}
ConfigurationSetterInterface.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ConfigurationSetterInterface.php'
View Content
<?php
declare(strict_types=1);

interface Minz_ConfigurationSetterInterface {

	/**
	 * Return whether the given key is supported by this setter.
	 * @param string $key the key to test.
	 * @return bool true if the key is supported, false otherwise.
	 */
	public function support(string $key): bool;

	/**
	 * Set the given key in data with the current value.
	 * @param array<string,mixed> $data an array containing the list of all configuration data.
	 * @param string $key the key to update.
	 * @param mixed $value the value to set.
	 */
	public function handle(&$data, string $key, $value): void;
}
ControllerNotActionControllerException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ControllerNotActionControllerException.php'
View Content
<?php
declare(strict_types=1);

class Minz_ControllerNotActionControllerException extends Minz_Exception {
	public function __construct(string $controller_name, int $code = self::ERROR) {
		$message = 'Controller `' . $controller_name . '` isn’t instance of ActionController';

		parent::__construct($message, $code);
	}
}
ControllerNotExistException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ControllerNotExistException.php'
View Content
<?php
declare(strict_types=1);

class Minz_ControllerNotExistException extends Minz_Exception {
	public function __construct(int $code = self::ERROR) {
		$message = 'Controller not found!';
		parent::__construct($message, $code);
	}
}
CurrentPagePaginationException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/CurrentPagePaginationException.php'
View Content
<?php
declare(strict_types=1);

class Minz_CurrentPagePaginationException extends Minz_Exception {
	public function __construct(int $page) {
		$message = 'Page number `' . $page . '` doesn’t exist';

		parent::__construct($message, self::ERROR);
	}
}
Dispatcher.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Dispatcher.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
*/

/**
 * The Dispatcher is in charge of initialising the Controller and exectue the action as specified in the Request object.
 * It is a singleton.
 */
final class Minz_Dispatcher {

	/**
	 * Singleton
	 */
	private static ?Minz_Dispatcher $instance = null;
	private static bool $needsReset;
	/** @var array<string,string> */
	private static array $registrations = [];
	private Minz_ActionController $controller;

	/**
	 * Retrieves the Dispatcher instance
	 */
	public static function getInstance(): Minz_Dispatcher {
		if (self::$instance === null) {
			self::$instance = new Minz_Dispatcher();
		}
		return self::$instance;
	}

	/**
	 * Launches the controller specified in Request
	 * Fills the Response body from the View
	 * @throws Minz_Exception
	 */
	public function run(): void {
		do {
			self::$needsReset = false;

			try {
				$this->createController(Minz_Request::controllerName());
				$this->controller->init();
				$this->controller->firstAction();
				// @phpstan-ignore booleanNot.alwaysTrue
				if (!self::$needsReset) {
					$this->launchAction(
						Minz_Request::actionName()
						. 'Action'
					);
				}
				$this->controller->lastAction();

				// @phpstan-ignore booleanNot.alwaysTrue
				if (!self::$needsReset) {
					$this->controller->declareCspHeader();
					$this->controller->view()->build();
				}
			} catch (Minz_Exception $e) {
				throw $e;
			}
			// @phpstan-ignore doWhile.alwaysFalse
		} while (self::$needsReset);
	}

	/**
	 * Informs the controller that it must restart because the request has been modified
	 */
	public static function reset(): void {
		self::$needsReset = true;
	}

	/**
	 * Instantiates the Controller
	 * @param string $base_name the name of the controller to instantiate
	 * @throws Minz_ControllerNotExistException the controller does not exist
	 * @throws Minz_ControllerNotActionControllerException controller is not an instance of ActionController
	 */
	private function createController(string $base_name): void {
		if (self::isRegistered($base_name)) {
			self::loadController($base_name);
			$controller_name = 'FreshExtension_' . $base_name . '_Controller';
		} else {
			$controller_name = 'FreshRSS_' . $base_name . '_Controller';
		}

		if (!class_exists($controller_name)) {
			throw new Minz_ControllerNotExistException(
				Minz_Exception::ERROR
			);
		}
		$controller = new $controller_name();

		if (!($controller instanceof Minz_ActionController)) {
			throw new Minz_ControllerNotActionControllerException(
				$controller_name,
				Minz_Exception::ERROR
			);
		}

		$this->controller = $controller;
	}

	/**
	 * Launch the action on the dispatcher’s controller
	 * @param string $action_name the name of the action
	 * @throws Minz_ActionException if the action cannot be executed on the controller
	 */
	private function launchAction(string $action_name): void {
		$call = [$this->controller, $action_name];
		if (!is_callable($call)) {
			throw new Minz_ActionException(
				get_class($this->controller),
				$action_name,
				Minz_Exception::ERROR
			);
		}
		call_user_func($call);
	}

	/**
	 * Register a controller file.
	 *
	 * @param string $base_name the base name of the controller (i.e. ./?c=<base_name>)
	 * @param string $base_path the base path where we should look into to find info.
	 */
	public static function registerController(string $base_name, string $base_path): void {
		if (!self::isRegistered($base_name)) {
			self::$registrations[$base_name] = $base_path;
		}
	}

	/**
	 * Return if a controller is registered.
	 *
	 * @param string $base_name the base name of the controller.
	 * @return bool true if the controller has been registered, false else.
	 */
	public static function isRegistered(string $base_name): bool {
		return isset(self::$registrations[$base_name]);
	}

	/**
	 * Load a controller file (include).
	 *
	 * @param string $base_name the base name of the controller.
	 */
	private static function loadController(string $base_name): void {
		$base_path = self::$registrations[$base_name];
		$controller_filename = $base_path . '/Controllers/' . $base_name . 'Controller.php';
		include_once $controller_filename;
	}
}
Error.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Error.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
*/

/**
 * The Minz_Error class logs and raises framework errors
 */
class Minz_Error {
	public function __construct() {}

	/**
	* Permet de lancer une erreur
	* @param int $code le type de l'erreur, par défaut 404 (page not found)
	* @param string|array<'error'|'warning'|'notice',array<string>> $logs logs d'erreurs découpés de la forme
	*      > $logs['error']
	*      > $logs['warning']
	*      > $logs['notice']
	* @param bool $redirect indique s'il faut forcer la redirection (les logs ne seront pas transmis)
	*/
	public static function error(int $code = 404, $logs = [], bool $redirect = true): void {
		$logs = self::processLogs($logs);
		$error_filename = APP_PATH . '/Controllers/errorController.php';

		if (file_exists($error_filename)) {
			Minz_Session::_params([
				'error_code' => $code,
				'error_logs' => $logs,
			]);

			Minz_Request::forward(['c' => 'error'], $redirect);
		} else {
			echo '<h1>An error occurred</h1>' . "\n";

			if (!empty($logs)) {
				echo '<ul>' . "\n";
				foreach ($logs as $log) {
					echo '<li>' . $log . '</li>' . "\n";
				}
				echo '</ul>' . "\n";
			}

			exit();
		}
	}

	/**
	 * Returns filtered logs
	 * @param string|array<'error'|'warning'|'notice',array<string>> $logs logs sorted by category (error, warning, notice)
	 * @return array<string> list of matching logs, without the category, according to environment preferences (production / development)
	 */
	private static function processLogs($logs): array {
		if (is_string($logs)) {
			return [$logs];
		}

		$error = [];
		$warning = [];
		$notice = [];

		if (isset($logs['error']) && is_array($logs['error'])) {
			$error = $logs['error'];
		}
		if (isset($logs['warning']) && is_array($logs['warning'])) {
			$warning = $logs['warning'];
		}
		if (isset($logs['notice']) && is_array($logs['notice'])) {
			$notice = $logs['notice'];
		}

		switch (Minz_Configuration::get('system')->environment) {
			case 'development':
				return array_merge($error, $warning, $notice);
			case 'production':
			default:
				return $error;
		}
	}
}
Exception.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Exception.php'
View Content
<?php
declare(strict_types=1);

class Minz_Exception extends Exception {
	public const ERROR = 0;
	public const WARNING = 10;
	public const NOTICE = 20;

	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, $previous);
	}
}
Extension.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Extension.php'
View Content
<?php
declare(strict_types=1);

/**
 * The extension base class.
 */
abstract class Minz_Extension {
	private string $name;
	private string $entrypoint;
	private string $path;
	private string $author;
	private string $description;
	private string $version;
	/** @var 'system'|'user' */
	private string $type;
	/** @var array<string,mixed>|null */
	private ?array $user_configuration = null;
	/** @var array<string,mixed>|null */
	private ?array $system_configuration = null;

	/** @var array{0:'system',1:'user'} */
	public static array $authorized_types = [
		'system',
		'user',
	];

	private bool $is_enabled;

	/** @var string[] */
	protected array $csp_policies = [];

	/**
	 * The constructor to assign specific information to the extension.
	 *
	 * Available fields are:
	 * - name: the name of the extension (required).
	 * - entrypoint: the extension class name (required).
	 * - path: the pathname to the extension files (required).
	 * - author: the name and / or email address of the extension author.
	 * - description: a short description to describe the extension role.
	 * - version: a version for the current extension.
	 * - type: "system" or "user" (default).
	 *
	 * @param array{'name':string,'entrypoint':string,'path':string,'author'?:string,'description'?:string,'version'?:string,'type'?:'system'|'user'} $meta_info
	 * contains information about the extension.
	 */
	final public function __construct(array $meta_info) {
		$this->name = $meta_info['name'];
		$this->entrypoint = $meta_info['entrypoint'];
		$this->path = $meta_info['path'];
		$this->author = isset($meta_info['author']) ? $meta_info['author'] : '';
		$this->description = isset($meta_info['description']) ? $meta_info['description'] : '';
		$this->version = isset($meta_info['version']) ? (string)$meta_info['version'] : '0.1';
		$this->setType(isset($meta_info['type']) ? $meta_info['type'] : 'user');

		$this->is_enabled = false;
	}

	/**
	 * Used when installing an extension (e.g. update the database scheme).
	 *
	 * @return string|true true if the extension has been installed or a string explaining the problem.
	 */
	public function install() {
		return true;
	}

	/**
	 * Used when uninstalling an extension (e.g. revert the database scheme to
	 * cancel changes from install).
	 *
	 * @return string|true true if the extension has been uninstalled or a string explaining the problem.
	 */
	public function uninstall() {
		return true;
	}

	/**
	 * Call at the initialization of the extension (i.e. when the extension is
	 * enabled by the extension manager).
	 * @return void
	 */
	public function init() {
		$this->migrateExtensionUserPath();
	}

	/**
	 * Set the current extension to enable.
	 */
	final public function enable(): void {
		$this->is_enabled = true;
	}

	/**
	 * Return if the extension is currently enabled.
	 *
	 * @return bool true if extension is enabled, false otherwise.
	 */
	final public function isEnabled(): bool {
		return $this->is_enabled;
	}

	/**
	 * Return the content of the configure view for the current extension.
	 *
	 * @return string|false html content from ext_dir/configure.phtml, false if it does not exist.
	 */
	final public function getConfigureView() {
		$filename = $this->path . '/configure.phtml';
		if (!file_exists($filename)) {
			return false;
		}

		ob_start();
		include($filename);
		return ob_get_clean();
	}

	/**
	 * Handle the configure action.
	 * @return void
	 */
	public function handleConfigureAction() {
		$this->migrateExtensionUserPath();
	}

	/**
	 * Getters and setters.
	 */
	final public function getName(): string {
		return $this->name;
	}
	final public function getEntrypoint(): string {
		return $this->entrypoint;
	}
	final public function getPath(): string {
		return $this->path;
	}
	final public function getAuthor(): string {
		return $this->author;
	}
	final public function getDescription(): string {
		return $this->description;
	}
	final public function getVersion(): string {
		return $this->version;
	}
	/** @return 'system'|'user' */
	final public function getType() {
		return $this->type;
	}

	/** @param 'user'|'system' $type */
	private function setType(string $type): void {
		if (!in_array($type, ['user', 'system'], true)) {
			throw new Minz_ExtensionException('invalid `type` info', $this->name);
		}
		$this->type = $type;
	}

	/** Return the user-specific, extension-specific, folder where this extension can save user-specific data */
	final protected function getExtensionUserPath(): string {
		$username = Minz_User::name() ?: '_';
		return USERS_PATH . "/{$username}/extensions/{$this->getEntrypoint()}";
	}

	private function migrateExtensionUserPath(): void {
		$username = Minz_User::name() ?: '_';
		$old_extension_user_path = USERS_PATH . "/{$username}/extensions/{$this->getName()}";
		$new_extension_user_path = $this->getExtensionUserPath();
		if (is_dir($old_extension_user_path)) {
			rename($old_extension_user_path, $new_extension_user_path);
		}
	}

	/** Return whether a user-specific, extension-specific, file exists */
	final protected function hasFile(string $filename): bool {
		return file_exists($this->getExtensionUserPath() . '/' . $filename);
	}

	/** Return the user-specific, extension-specific, file content, or null if it does not exist */
	final protected function getFile(string $filename): ?string {
		$content = @file_get_contents($this->getExtensionUserPath() . '/' . $filename);
		return is_string($content) ? $content : null;
	}

	/**
	 * Return the url for a given file.
	 *
	 * @param string $filename name of the file to serve.
	 * @param 'css'|'js'|'svg' $type the type (js or css or svg) of the file to serve.
	 * @param bool $isStatic indicates if the file is a static file or a user file. Default is static.
	 * @return string url corresponding to the file.
	 */
	final public function getFileUrl(string $filename, string $type, bool $isStatic = true): string {
		if ($isStatic) {
			$dir = basename($this->path);
			$file_name_url = urlencode("{$dir}/static/{$filename}");
			$mtime = @filemtime("{$this->path}/static/{$filename}");
		} else {
			$username = Minz_User::name();
			if ($username == null) {
				return '';
			}
			$path = $this->getExtensionUserPath() . "/{$filename}";
			$file_name_url = urlencode("{$username}/extensions/{$this->getEntrypoint()}/{$filename}");
			$mtime = @filemtime($path);
		}

		return Minz_Url::display("/ext.php?f={$file_name_url}&amp;t={$type}&amp;{$mtime}", 'php');
	}

	/**
	 * Register a controller in the Dispatcher.
	 *
	 * @param string $base_name the base name of the controller. Final name will be FreshExtension_<base_name>_Controller.
	 */
	final protected function registerController(string $base_name): void {
		Minz_Dispatcher::registerController($base_name, $this->path);
	}

	/**
	 * Register the views in order to be accessible by the application.
	 */
	final protected function registerViews(): void {
		Minz_View::addBasePathname($this->path);
	}

	/**
	 * Register i18n files from ext_dir/i18n/
	 */
	final protected function registerTranslates(): void {
		$i18n_dir = $this->path . '/i18n';
		Minz_Translate::registerPath($i18n_dir);
	}

	/**
	 * Register a new hook.
	 *
	 * @param string $hook_name the hook name (must exist).
	 * @param callable $hook_function the function name to call (must be callable).
	 */
	final protected function registerHook(string $hook_name, $hook_function): void {
		Minz_ExtensionManager::addHook($hook_name, $hook_function);
	}

	/** @param 'system'|'user' $type */
	private function isConfigurationEnabled(string $type): bool {
		if (!class_exists('FreshRSS_Context', false)) {
			return false;
		}

		switch ($type) {
			case 'system': return FreshRSS_Context::hasSystemConf();
			case 'user': return FreshRSS_Context::hasUserConf();
		}
	}

	/** @param 'system'|'user' $type */
	private function isExtensionConfigured(string $type): bool {
		switch ($type) {
			case 'user':
				$conf = FreshRSS_Context::userConf();
				break;
			case 'system':
				$conf = FreshRSS_Context::systemConf();
				break;
			default:
				return false;
		}

		if (!$conf->hasParam('extensions')) {
			return false;
		}

		return array_key_exists($this->getName(), $conf->extensions);
	}

	/**
	 * @return array<string,mixed>
	 */
	final protected function getSystemConfiguration(): array {
		if ($this->isConfigurationEnabled('system') && $this->isExtensionConfigured('system')) {
			return FreshRSS_Context::systemConf()->extensions[$this->getName()];
		}
		return [];
	}

	/**
	 * @return array<string,mixed>
	 */
	final protected function getUserConfiguration(): array {
		if ($this->isConfigurationEnabled('user') && $this->isExtensionConfigured('user')) {
			return FreshRSS_Context::userConf()->extensions[$this->getName()];
		}
		return [];
	}

	/**
	 * @param mixed $default
	 * @return mixed
	 */
	final public function getSystemConfigurationValue(string $key, $default = null) {
		if (!is_array($this->system_configuration)) {
			$this->system_configuration = $this->getSystemConfiguration();
		}

		if (array_key_exists($key, $this->system_configuration)) {
			return $this->system_configuration[$key];
		}
		return $default;
	}

	/**
	 * @param mixed $default
	 * @return mixed
	 */
	final public function getUserConfigurationValue(string $key, $default = null) {
		if (!is_array($this->user_configuration)) {
			$this->user_configuration = $this->getUserConfiguration();
		}

		if (array_key_exists($key, $this->user_configuration)) {
			return $this->user_configuration[$key];
		}
		return $default;
	}

	/**
	 * @param 'system'|'user' $type
	 * @param array<string,mixed> $configuration
	 */
	private function setConfiguration(string $type, array $configuration): void {
		switch ($type) {
			case 'system':
				$conf = FreshRSS_Context::systemConf();
				break;
			case 'user':
				$conf = FreshRSS_Context::userConf();
				break;
			default:
				return;
		}

		if ($conf->hasParam('extensions')) {
			$extensions = $conf->extensions;
		} else {
			$extensions = [];
		}
		$extensions[$this->getName()] = $configuration;

		$conf->extensions = $extensions;
		$conf->save();
	}

	/** @param array<string,mixed> $configuration */
	final protected function setSystemConfiguration(array $configuration): void {
		$this->setConfiguration('system', $configuration);
		$this->system_configuration = $configuration;
	}

	/** @param array<string,mixed> $configuration */
	final protected function setUserConfiguration(array $configuration): void {
		$this->setConfiguration('user', $configuration);
		$this->user_configuration = $configuration;
	}

	/** @phpstan-param 'system'|'user' $type */
	private function removeConfiguration(string $type): void {
		if (!$this->isConfigurationEnabled($type) || !$this->isExtensionConfigured($type)) {
			return;
		}

		switch ($type) {
			case 'system':
				$conf = FreshRSS_Context::systemConf();
				break;
			case 'user':
				$conf = FreshRSS_Context::userConf();
				break;
			default:
				return;
		}

		$extensions = $conf->extensions;
		unset($extensions[$this->getName()]);
		if (empty($extensions)) {
			$extensions = [];
		}
		$conf->extensions = $extensions;
		$conf->save();
	}

	final protected function removeSystemConfiguration(): void {
		$this->removeConfiguration('system');
		$this->system_configuration = null;
	}

	final protected function removeUserConfiguration(): void {
		$this->removeConfiguration('user');
		$this->user_configuration = null;
	}

	final protected function saveFile(string $filename, string $content): void {
		$path = $this->getExtensionUserPath();

		if (!file_exists($path)) {
			mkdir($path, 0777, true);
		}

		file_put_contents("{$path}/{$filename}", $content);
	}

	final protected function removeFile(string $filename): void {
		$path = $path = $this->getExtensionUserPath() . '/' . $filename;
		if (file_exists($path)) {
			unlink($path);
		}
	}

	/**
	 * @param string[] $policies
	 */
	public function amendCsp(array &$policies): void {
		foreach ($this->csp_policies as $policy => $source) {
			if (array_key_exists($policy, $policies)) {
				$policies[$policy] .= ' ' . $source;
			} else {
				$policies[$policy] = $source;
			}
		}
	}
}
ExtensionException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ExtensionException.php'
View Content
<?php
declare(strict_types=1);

class Minz_ExtensionException extends Minz_Exception {
	public function __construct(string $message, string $extension_name = '', int $code = self::ERROR) {
		if ($extension_name !== '') {
			$message = 'An error occurred in `' . $extension_name . '` extension with the message: ' . $message;
		} else {
			$message = 'An error occurred in an unnamed extension with the message: ' . $message;
		}

		parent::__construct($message, $code);
	}
}
ExtensionManager.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ExtensionManager.php'
View Content
<?php
declare(strict_types=1);

/**
 * An extension manager to load extensions present in CORE_EXTENSIONS_PATH and THIRDPARTY_EXTENSIONS_PATH.
 *
 * @todo see coding style for methods!!
 */
final class Minz_ExtensionManager {

	private static string $ext_metaname = 'metadata.json';
	private static string $ext_entry_point = 'extension.php';
	/** @var array<string,Minz_Extension> */
	private static array $ext_list = [];
	/** @var array<string,Minz_Extension> */
	private static array $ext_list_enabled = [];
	/** @var array<string,bool> */
	private static array $ext_auto_enabled = [];

	/**
	 * List of available hooks. Please keep this list sorted.
	 * @var array<string,array{'list':array<callable>,'signature':'NoneToNone'|'NoneToString'|'OneToOne'|'PassArguments'}>
	 */
	private static array $hook_list = [
		'check_url_before_add' => array(	// function($url) -> Url | null
			'list' => array(),
			'signature' => 'OneToOne',
		),
		'entry_auto_read' => array(	// function(FreshRSS_Entry $entry, string $why): void
			'list' => array(),
			'signature' => 'PassArguments',
		),
		'entry_auto_unread' => array(	// function(FreshRSS_Entry $entry, string $why): void
			'list' => array(),
			'signature' => 'PassArguments',
		),
		'entry_before_display' => array(	// function($entry) -> Entry | null
			'list' => array(),
			'signature' => 'OneToOne',
		),
		'entry_before_insert' => array(	// function($entry) -> Entry | null
			'list' => array(),
			'signature' => 'OneToOne',
		),
		'feed_before_actualize' => array(	// function($feed) -> Feed | null
			'list' => array(),
			'signature' => 'OneToOne',
		),
		'feed_before_insert' => array(	// function($feed) -> Feed | null
			'list' => array(),
			'signature' => 'OneToOne',
		),
		'freshrss_init' => array(	// function() -> none
			'list' => array(),
			'signature' => 'NoneToNone',
		),
		'freshrss_user_maintenance' => array(	// function() -> none
			'list' => array(),
			'signature' => 'NoneToNone',
		),
		'js_vars' => array(	// function($vars = array) -> array | null
			'list' => array(),
			'signature' => 'OneToOne',
		),
		'menu_admin_entry' => array(	// function() -> string
			'list' => array(),
			'signature' => 'NoneToString',
		),
		'menu_configuration_entry' => array(	// function() -> string
			'list' => array(),
			'signature' => 'NoneToString',
		),
		'menu_other_entry' => array(	// function() -> string
			'list' => array(),
			'signature' => 'NoneToString',
		),
		'nav_menu' => array(	// function() -> string
			'list' => array(),
			'signature' => 'NoneToString',
		),
		'nav_reading_modes' => array(	// function($readingModes = array) -> array | null
			'list' => array(),
			'signature' => 'OneToOne',
		),
		'post_update' => array(	// function(none) -> none
			'list' => array(),
			'signature' => 'NoneToNone',
		),
		'simplepie_before_init' => array(	// function($simplePie, $feed) -> none
			'list' => array(),
			'signature' => 'PassArguments',
		),
	];

	/** Remove extensions and hooks from a previous initialisation */
	private static function reset(): void {
		$hadAny = !empty(self::$ext_list_enabled);
		self::$ext_list = [];
		self::$ext_list_enabled = [];
		self::$ext_auto_enabled = [];
		foreach (self::$hook_list as $hook_type => $hook_data) {
			$hadAny |= !empty($hook_data['list']);
			$hook_data['list'] = [];
			self::$hook_list[$hook_type] = $hook_data;
		}
		if ($hadAny) {
			gc_collect_cycles();
		}
	}

	/**
	 * Initialize the extension manager by loading extensions in EXTENSIONS_PATH.
	 *
	 * A valid extension is a directory containing metadata.json and
	 * extension.php files.
	 * metadata.json is a JSON structure where the only required fields are
	 * `name` and `entry_point`.
	 * 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();

		$list_core_extensions = array_diff(scandir(CORE_EXTENSIONS_PATH) ?: [], [ '..', '.' ]);
		$list_thirdparty_extensions = array_diff(scandir(THIRDPARTY_EXTENSIONS_PATH) ?: [], [ '..', '.' ], $list_core_extensions);
		array_walk($list_core_extensions, function (&$s) { $s = CORE_EXTENSIONS_PATH . '/' . $s; });
		array_walk($list_thirdparty_extensions, function (&$s) { $s = THIRDPARTY_EXTENSIONS_PATH . '/' . $s; });

		/** @var array<string> */
		$list_potential_extensions = array_merge($list_core_extensions, $list_thirdparty_extensions);

		$system_conf = Minz_Configuration::get('system');
		self::$ext_auto_enabled = $system_conf->extensions_enabled;

		foreach ($list_potential_extensions as $ext_pathname) {
			if (!is_dir($ext_pathname)) {
				continue;
			}
			$metadata_filename = $ext_pathname . '/' . self::$ext_metaname;

			// Try to load metadata file.
			if (!file_exists($metadata_filename)) {
				// No metadata file? Invalid!
				continue;
			}
			$meta_raw_content = file_get_contents($metadata_filename) ?: '';
			/** @var array{'name':string,'entrypoint':string,'path':string,'author'?:string,'description'?:string,'version'?:string,'type'?:'system'|'user'}|null $meta_json */
			$meta_json = json_decode($meta_raw_content, true);
			if (!is_array($meta_json) || !self::isValidMetadata($meta_json)) {
				// metadata.json is not a json file? Invalid!
				// or metadata.json is invalid (no required information), invalid!
				Minz_Log::warning('`' . $metadata_filename . '` is not a valid metadata file');
				continue;
			}

			$meta_json['path'] = $ext_pathname;

			// Try to load extension itself
			$extension = self::load($meta_json);
			if ($extension != null) {
				self::register($extension);
			}
		}
	}

	/**
	 * Indicates if the given parameter is a valid metadata array.
	 *
	 * Required fields are:
	 * - `name`: the name of the extension
	 * - `entry_point`: a class name to load the extension source code
	 * If the extension class name is `TestExtension`, entry point will be `Test`.
	 * `entry_point` must be composed of alphanumeric characters.
	 *
	 * @param array{'name':string,'entrypoint':string,'path':string,'author'?:string,'description'?:string,'version'?:string,'type'?:'system'|'user'} $meta
	 * is an array of values.
	 * @return bool true if the array is valid, false else.
	 */
	private static function isValidMetadata(array $meta): bool {
		$valid_chars = array('_');
		return !(empty($meta['name']) || empty($meta['entrypoint']) || !ctype_alnum(str_replace($valid_chars, '', $meta['entrypoint'])));
	}

	/**
	 * Load the extension source code based on info metadata.
	 *
	 * @param array{'name':string,'entrypoint':string,'path':string,'author'?:string,'description'?:string,'version'?:string,'type'?:'system'|'user'} $info
	 * an array containing information about extension.
	 * @return Minz_Extension|null an extension inheriting from Minz_Extension.
	 */
	private static function load(array $info): ?Minz_Extension {
		$entry_point_filename = $info['path'] . '/' . self::$ext_entry_point;
		$ext_class_name = $info['entrypoint'] . 'Extension';

		include_once($entry_point_filename);

		// Test if the given extension class exists.
		if (!class_exists($ext_class_name)) {
			Minz_Log::warning("`{$ext_class_name}` cannot be found in `{$entry_point_filename}`");
			return null;
		}

		// Try to load the class.
		$extension = null;
		try {
			$extension = new $ext_class_name($info);
		} catch (Exception $e) {
			// We cannot load the extension? Invalid!
			Minz_Log::warning("Invalid extension `{$ext_class_name}`: " . $e->getMessage());
			return null;
		}

		// Test if class is correct.
		if (!($extension instanceof Minz_Extension)) {
			Minz_Log::warning("`{$ext_class_name}` is not an instance of `Minz_Extension`");
			return null;
		}

		return $extension;
	}

	/**
	 * Add the extension to the list of the known extensions ($ext_list).
	 *
	 * If the extension is present in $ext_auto_enabled and if its type is "system",
	 * it will be enabled at the same time.
	 *
	 * @param Minz_Extension $ext a valid extension.
	 */
	private static function register(Minz_Extension $ext): void {
		$name = $ext->getName();
		self::$ext_list[$name] = $ext;

		if ($ext->getType() === 'system' && !empty(self::$ext_auto_enabled[$name])) {
			self::enable($ext->getName(), 'system');
		}
	}

	/**
	 * Enable an extension so it will be called when necessary.
	 *
	 * The extension init() method will be called.
	 *
	 * @param string $ext_name is the name of a valid extension present in $ext_list.
	 * @param 'system'|'user'|null $onlyOfType only enable if the extension matches that type. Set to null to load all.
	 */
	private static function enable(string $ext_name, ?string $onlyOfType = null): void {
		if (isset(self::$ext_list[$ext_name])) {
			$ext = self::$ext_list[$ext_name];

			if ($onlyOfType !== null && $ext->getType() !== $onlyOfType) {
				// Do not enable an extension of the wrong type
				return;
			}

			self::$ext_list_enabled[$ext_name] = $ext;

			if (method_exists($ext, 'autoload')) {
				spl_autoload_register([$ext, 'autoload']);
			}
			$ext->enable();
			$ext->init();
		}
	}

	/**
	 * Enable a list of extensions.
	 *
	 * @param array<string,bool> $ext_list the names of extensions we want to load.
	 * @param 'system'|'user'|null $onlyOfType limit the extensions to load to those of those type. Set to null string to load all.
	 */
	public static function enableByList(?array $ext_list, ?string $onlyOfType = null): void {
		if (empty($ext_list)) {
			return;
		}
		foreach ($ext_list as $ext_name => $ext_status) {
			if ($ext_status && is_string($ext_name)) {
				self::enable($ext_name, $onlyOfType);
			}
		}
	}

	/**
	 * Return a list of extensions.
	 *
	 * @param bool $only_enabled if true returns only the enabled extensions (false by default).
	 * @return Minz_Extension[] an array of extensions.
	 */
	public static function listExtensions(bool $only_enabled = false): array {
		if ($only_enabled) {
			return self::$ext_list_enabled;
		} else {
			return self::$ext_list;
		}
	}

	/**
	 * Return an extension by its name.
	 *
	 * @param string $ext_name the name of the extension.
	 * @return Minz_Extension|null the corresponding extension or null if it doesn't exist.
	 */
	public static function findExtension(string $ext_name): ?Minz_Extension {
		if (!isset(self::$ext_list[$ext_name])) {
			return null;
		}

		return self::$ext_list[$ext_name];
	}

	/**
	 * Add a hook function to a given hook.
	 *
	 * The hook name must be a valid one. For the valid list, see self::$hook_list
	 * array keys.
	 *
	 * @param string $hook_name the hook name (must exist).
	 * @param callable $hook_function the function name to call (must be callable).
	 */
	public static function addHook(string $hook_name, $hook_function): void {
		if (isset(self::$hook_list[$hook_name]) && is_callable($hook_function)) {
			self::$hook_list[$hook_name]['list'][] = $hook_function;
		}
	}

	/**
	 * Call functions related to a given hook.
	 *
	 * The hook name must be a valid one. For the valid list, see self::$hook_list
	 * array keys.
	 *
	 * @param string $hook_name the hook to call.
	 * @param mixed ...$args additional parameters (for signature, please see self::$hook_list).
	 * @return mixed|void|null final result of the called hook.
	 */
	public static function callHook(string $hook_name, ...$args) {
		if (!isset(self::$hook_list[$hook_name])) {
			return;
		}

		$signature = self::$hook_list[$hook_name]['signature'];
		if ($signature === 'OneToOne') {
			return self::callOneToOne($hook_name, $args[0] ?? null);
		} elseif ($signature === 'PassArguments') {
			foreach (self::$hook_list[$hook_name]['list'] as $function) {
				call_user_func($function, ...$args);
			}
		} elseif ($signature === 'NoneToString') {
			return self::callHookString($hook_name);
		} elseif ($signature === 'NoneToNone') {
			self::callHookVoid($hook_name);
		}
		return;
	}

	/**
	 * Call a hook which takes one argument and return a result.
	 *
	 * The result is chained between the extension, for instance, first extension
	 * hook will receive the initial argument and return a result which will be
	 * passed as an argument to the next extension hook and so on.
	 *
	 * If a hook return a null value, the method is stopped and return null.
	 *
	 * @param string $hook_name is the hook to call.
	 * @param mixed $arg is the argument to pass to the first extension hook.
	 * @return mixed|null final chained result of the hooks. If nothing is changed,
	 *         the initial argument is returned.
	 */
	private static function callOneToOne(string $hook_name, $arg) {
		$result = $arg;
		foreach (self::$hook_list[$hook_name]['list'] as $function) {
			$result = call_user_func($function, $arg);

			if ($result === null) {
				break;
			}

			$arg = $result;
		}
		return $result;
	}

	/**
	 * Call a hook which takes no argument and returns a string.
	 *
	 * The result is concatenated between each hook and the final string is
	 * returned.
	 *
	 * @param string $hook_name is the hook to call.
	 * @return string concatenated result of the call to all the hooks.
	 */
	public static function callHookString(string $hook_name): string {
		$result = '';
		foreach (self::$hook_list[$hook_name]['list'] ?? [] as $function) {
			$result = $result . call_user_func($function);
		}
		return $result;
	}

	/**
	 * Call a hook which takes no argument and returns nothing.
	 *
	 * This case is simpler than callOneToOne because hooks are called one by
	 * one, without any consideration of argument nor result.
	 *
	 * @param string $hook_name is the hook to call.
	 */
	public static function callHookVoid(string $hook_name): void {
		foreach (self::$hook_list[$hook_name]['list'] ?? [] as $function) {
			call_user_func($function);
		}
	}
}
FileNotExistException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/FileNotExistException.php'
View Content
<?php
declare(strict_types=1);

class Minz_FileNotExistException extends Minz_Exception {
	public function __construct(string $file_name, int $code = self::ERROR) {
		$message = 'File not found: `' . $file_name . '`';

		parent::__construct($message, $code);
	}
}
FrontController.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/FrontController.php'
View Content
<?php
declare(strict_types=1);

# ***** BEGIN LICENSE BLOCK *****
# MINZ - a free PHP Framework like Zend Framework
# Copyright (C) 2011 Marien Fressinaud
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# ***** END LICENSE BLOCK *****

/**
 * The Minz_FrontController class is the framework Dispatcher.
 * It runs the application.
 * It is generally invoqued by an index.php file at the root.
 */
class Minz_FrontController {

	protected Minz_Dispatcher $dispatcher;

	/**
	 * Constructeur
	 * Initialise le dispatcher, met à jour la Request
	 */
	public function __construct() {
		try {
			$this->setReporting();

			Minz_Request::init();

			$url = Minz_Url::build();
			$url['params'] = array_merge(
				empty($url['params']) || !is_array($url['params']) ? [] : $url['params'],
				$_POST
			);
			Minz_Request::forward($url);
		} catch (Minz_Exception $e) {
			Minz_Log::error($e->getMessage());
			self::killApp($e->getMessage());
		}

		$this->dispatcher = Minz_Dispatcher::getInstance();
	}

	/**
	 * Démarre l'application (lance le dispatcher et renvoie la réponse)
	 */
	public function run(): void {
		try {
			$this->dispatcher->run();
		} catch (Minz_Exception $e) {
			try {
				Minz_Log::error($e->getMessage());
			} catch (Minz_PermissionDeniedException $e) {
				self::killApp($e->getMessage());
			}

			if ($e instanceof Minz_FileNotExistException ||
					$e instanceof Minz_ControllerNotExistException ||
					$e instanceof Minz_ControllerNotActionControllerException ||
					$e instanceof Minz_ActionException) {
				Minz_Error::error(404, ['error' => [$e->getMessage()]], true);
			} else {
				self::killApp($e->getMessage());
			}
		}
	}

	/**
	 * Kills the programme
	 * @return never
	 */
	public static function killApp(string $txt = '') {
		header('HTTP/1.1 500 Internal Server Error', true, 500);
		if (function_exists('errorMessageInfo')) {
			//If the application has defined a custom error message function
			die(errorMessageInfo('Application problem', $txt));
		}
		die('### Application problem ###<br />' . "\n" . $txt);
	}

	private function setReporting(): void {
		$envType = getenv('FRESHRSS_ENV');
		if ($envType == '') {
			$conf = Minz_Configuration::get('system');
			$envType = $conf->environment;
		}
		switch ($envType) {
			case 'development':
				error_reporting(E_ALL);
				ini_set('display_errors', 'On');
				ini_set('log_errors', 'On');
				break;
			case 'silent':
				error_reporting(0);
				break;
			case 'production':
			default:
				error_reporting(E_ALL);
				ini_set('display_errors', 'Off');
				ini_set('log_errors', 'On');
				break;
		}
	}
}
Helper.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Helper.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
*/

/**
 * The Minz_Helper class contains some misc. help functions
 */
final class Minz_Helper {

	/**
	 * Wrapper for htmlspecialchars.
	 * Force UTF-8 value and can be used on array too.
	 *
	 * @phpstan-template T of mixed
	 * @phpstan-param T $var
	 * @phpstan-return T
	 *
	 * @param mixed $var
	 * @return mixed
	 */
	public static function htmlspecialchars_utf8($var) {
		if (is_array($var)) {
			// @phpstan-ignore argument.type, return.type
			return array_map([self::class, 'htmlspecialchars_utf8'], $var);
		} elseif (is_string($var)) {
			// @phpstan-ignore return.type
			return htmlspecialchars($var, ENT_COMPAT, 'UTF-8');
		} else {
			return $var;
		}
	}
}
Log.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Log.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
*/

/**
 * The Minz_Log class is used to log errors and warnings
 */
class Minz_Log {
	/**
	 * Enregistre un message dans un fichier de log spécifique
	 * Message non loggué si
	 * 	- environment = SILENT
	 * 	- level = LOG_WARNING et environment = PRODUCTION
	 * 	- level = LOG_NOTICE et environment = PRODUCTION
	 * @param string $information message d'erreur / information à enregistrer
	 * @param int $level niveau d'erreur https://php.net/function.syslog
	 * @param string $file_name fichier de log
	 * @throws Minz_PermissionDeniedException
	 */
	public static function record(string $information, int $level, ?string $file_name = null): void {
		$env = getenv('FRESHRSS_ENV');
		if ($env == '') {
			try {
				$conf = Minz_Configuration::get('system');
				$env = $conf->environment;
			} catch (Minz_ConfigurationException $e) {
				$env = 'production';
			}
		}

		if (! ($env === 'silent' || ($env === 'production' && ($level >= LOG_NOTICE)))) {
			$username = Minz_User::name() ?? Minz_User::INTERNAL_USER;
			if ($file_name == null) {
				$file_name = join_path(USERS_PATH, $username, LOG_FILENAME);
			}

			switch ($level) {
				case LOG_ERR:
					$level_label = 'error';
					break;
				case LOG_WARNING:
					$level_label = 'warning';
					break;
				case LOG_NOTICE:
					$level_label = 'notice';
					break;
				case LOG_DEBUG:
					$level_label = 'debug';
					break;
				default:
					$level = LOG_INFO;
					$level_label = 'info';
			}

			$log = '[' . date('r') . '] [' . $level_label . '] --- ' . $information . "\n";

			if (defined('COPY_LOG_TO_SYSLOG') && COPY_LOG_TO_SYSLOG) {
				syslog($level, '[' . $username . '] ' . trim($log));
			}

			self::ensureMaxLogSize($file_name);

			if (file_put_contents($file_name, $log, FILE_APPEND | LOCK_EX) === false) {
				throw new Minz_PermissionDeniedException($file_name, Minz_Exception::ERROR);
			}
		}
	}

	/**
	 * Make sure we do not waste a huge amount of disk space with old log messages.
	 *
	 * This method can be called multiple times for one script execution, but its result will not change unless
	 * you call clearstatcache() in between. We won’t do do that for performance reasons.
	 *
	 * @param string $file_name
	 * @throws Minz_PermissionDeniedException
	 */
	protected static function ensureMaxLogSize(string $file_name): void {
		$maxSize = defined('MAX_LOG_SIZE') ? MAX_LOG_SIZE : 1048576;
		if ($maxSize > 0 && @filesize($file_name) > $maxSize) {
			$fp = fopen($file_name, 'c+');
			if (is_resource($fp) && flock($fp, LOCK_EX)) {
				fseek($fp, -(int)($maxSize / 2), SEEK_END);
				$content = fread($fp, $maxSize);
				rewind($fp);
				ftruncate($fp, 0);
				fwrite($fp, $content ?: '');
				fwrite($fp, sprintf("[%s] [notice] --- Log rotate.\n", date('r')));
				fflush($fp);
				flock($fp, LOCK_UN);
			} else {
				throw new Minz_PermissionDeniedException($file_name, Minz_Exception::ERROR);
			}
			fclose($fp);
		}
	}

	/**
	 * 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);
	}
}
Mailer.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Mailer.php'
View Content
<?php
declare(strict_types=1);

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;

/**
 * Allow to send emails.
 *
 * The Minz_Mailer class must be inherited by classes under app/Mailers.
 * They work similarly to the ActionControllers in the way they have a view to
 * which you can pass params (eg. $this->view->foo = 'bar').
 *
 * The view file is not determined automatically, so you have to select one
 * with, for instance:
 *
 * ```
 * $this->view->_path('user_mailer/email_need_validation.txt.php')
 * ```
 *
 * Minz_Mailer uses the PHPMailer library under the hood. The latter requires
 * PHP >= 5.5 to work. If you instantiate a Minz_Mailer with PHP < 5.5, a
 * warning will be logged.
 *
 * The email is sent by calling the `mail` method.
 */
class Minz_Mailer {
	/**
	 * The view attached to the mailer.
	 * You should set its file with `$this->view->_path($path)`
	 *
	 * @var Minz_View
	 */
	protected $view;

	private string $mailer;
	/** @var array{'hostname':string,'host':string,'auth':bool,'username':string,'password':string,'secure':string,'port':int,'from':string} */
	private array $smtp_config;
	private int $debug_level;

	/**
	 * @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;
		if ($viewType !== '' && class_exists($viewType)) {
			$view = new $viewType();
			if (!($view instanceof Minz_View)) {
				$view = null;
			}
		}
		$this->view = $view ?? new Minz_View();
		$this->view->_layout(null);
		$this->view->attributeParams();

		$conf = Minz_Configuration::get('system');
		$this->mailer = $conf->mailer;
		$this->smtp_config = $conf->smtp;

		// According to https://github.com/PHPMailer/PHPMailer/wiki/SMTP-Debugging#debug-levels
		// we should not use debug level above 2 unless if we have big trouble
		// to connect.
		if ($conf->environment === 'development') {
			$this->debug_level = 2;
		} else {
			$this->debug_level = 0;
		}
	}

	/**
	 * Send an email.
	 *
	 * @param string $to The recipient of the email
	 * @param string $subject The subject of the email
	 * @return bool true on success, false if a SMTP error happens
	 */
	public function mail(string $to, string $subject): bool {
		ob_start();
		$this->view->render();
		$body = ob_get_contents() ?: '';
		ob_end_clean();

		PHPMailer::$validator = 'html5';

		$mail = new PHPMailer(true);
		try {
			// Server settings
			$mail->SMTPDebug = $this->debug_level;
			$mail->Debugoutput = 'error_log';

			if ($this->mailer === 'smtp') {
				$mail->isSMTP();
				$mail->Hostname = $this->smtp_config['hostname'];
				$mail->Host = $this->smtp_config['host'];
				$mail->SMTPAuth = $this->smtp_config['auth'];
				$mail->Username = $this->smtp_config['username'];
				$mail->Password = $this->smtp_config['password'];
				$mail->SMTPSecure = $this->smtp_config['secure'];
				$mail->Port = $this->smtp_config['port'];
			} else {
				$mail->isMail();
			}

			// Recipients
			$mail->setFrom($this->smtp_config['from']);
			$mail->addAddress($to);

			// Content
			$mail->isHTML(false);
			$mail->CharSet = 'utf-8';
			$mail->Subject = $subject;
			$mail->Body = $body;

			$mail->send();
			return true;
		} catch (Exception $e) {
			Minz_Log::error('Minz_Mailer cannot send a message: ' . $mail->ErrorInfo);
			return false;
		}
	}
}
Migrator.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Migrator.php'
View Content
<?php
declare(strict_types=1);

/**
 * The Minz_Migrator helps to migrate data (in a database or not) or the
 * architecture of a Minz application.
 *
 * @author Marien Fressinaud <dev@marienfressinaud.fr>
 * @license http://www.gnu.org/licenses/agpl-3.0.en.html AGPL
 */
class Minz_Migrator
{
	/** @var array<string> */
	private array $applied_versions;

	/** @var array<callable> */
	private array $migrations = [];

	/**
	 * Execute a list of migrations, skipping versions indicated in a file
	 *
	 * @param string $migrations_path
	 * @param string $applied_migrations_path
	 *
	 * @return true|string Returns true if execute succeeds to apply
	 *                        migrations, or a string if it fails.
	 * @throws DomainException if there is no migrations corresponding to the
	 *                         given version (can happen if version file has
	 *                         been modified, or migrations path cannot be
	 *                         read).
	 *
	 * @throws BadFunctionCallException if a callback isn’t callable.
	 */
	public static function execute(string $migrations_path, string $applied_migrations_path) {
		$applied_migrations = @file_get_contents($applied_migrations_path);
		if ($applied_migrations === false) {
			return "Cannot open the {$applied_migrations_path} file";
		}
		$applied_migrations = array_filter(explode("\n", $applied_migrations));

		$migration_files = scandir($migrations_path) ?: [];
		$migration_files = array_filter($migration_files, static function (string $filename) {
			$file_extension = pathinfo($filename, PATHINFO_EXTENSION);
			return $file_extension === 'php';
		});
		$migration_versions = array_map(static function (string $filename) {
			return basename($filename, '.php');
		}, $migration_files);

		// We apply a "low-cost" comparison to avoid to include the migration
		// files at each run. It is equivalent to the upToDate method.
		if (count($applied_migrations) === count($migration_versions) &&
			empty(array_diff($applied_migrations, $migration_versions))) {
			// already at the latest version, so there is nothing more to do
			return true;
		}

		$lock_path = $applied_migrations_path . '.lock';
		if (!@mkdir($lock_path, 0770, true)) {
			// Someone is probably already executing the migrations (the folder
			// already exists).
			// We should probably return something else, but we don’t want the
			// user to think there is an error (it’s normal workflow), so let’s
			// stick to this solution for now.
			// Another option would be to show him a maintenance page.
			Minz_Log::warning(
				'A request has been served while the application wasn’t up-to-date. '
				. 'Too many of these errors probably means a previous migration failed.'
			);
			return true;
		}

		$migrator = new self($migrations_path);
		$migrator->setAppliedVersions($applied_migrations);
		$results = $migrator->migrate();

		foreach ($results as $migration => $result) {
			if ($result === true) {
				$result = 'OK';
			} elseif ($result === false) {
				$result = 'KO';
			}

			Minz_Log::notice("Migration {$migration}: {$result}");
		}

		$applied_versions = implode("\n", $migrator->appliedVersions());
		$saved = file_put_contents($applied_migrations_path, $applied_versions);

		if (!@rmdir($lock_path)) {
			Minz_Log::error(
				'We weren’t able to unlink the migration executing folder, '
				. 'you might want to delete yourself: ' . $lock_path
			);
			// we don’t return early because the migrations could have been
			// applied successfully. This file is not "critical" if not removed
			// and more errors will eventually appear in the logs.
		}

		if ($saved === false) {
			return "Cannot save the {$applied_migrations_path} file";
		}

		if (!$migrator->upToDate()) {
			// still not up to date? It means last migration failed.
			return trim('A migration failed to be applied, please see previous logs.' . "\n" . implode("\n", $results));
		}

		return true;
	}

	/**
	 * Create a Minz_Migrator instance. If directory is given, it'll load the
	 * migrations from it.
	 *
	 * All the files in the directory must declare a class named
	 * <app_name>_Migration_<filename> with a static `migrate` method.
	 *
	 * - <app_name> is the application name declared in the APP_NAME constant
	 * - <filename> is the migration file name, without the `.php` extension
	 *
	 * The files starting with a dot are ignored.
	 *
	 * @throws BadFunctionCallException if a callback isn’t callable (i.e. cannot call a migrate method).
	 */
	public function __construct(?string $directory = null) {
		$this->applied_versions = [];

		if ($directory == null || !is_dir($directory)) {
			return;
		}

		foreach (scandir($directory) ?: [] as $filename) {
			$file_extension = pathinfo($filename, PATHINFO_EXTENSION);
			if ($file_extension !== 'php') {
				continue;
			}

			$filepath = $directory . '/' . $filename;
			$migration_version = basename($filename, '.php');
			$migration_class = APP_NAME . "_Migration_" . $migration_version;
			$migration_callback = $migration_class . '::migrate';

			$include_result = @include_once($filepath);
			if (!$include_result) {
				Minz_Log::error(
					"{$filepath} migration file cannot be loaded.",
					ADMIN_LOG
				);
			}

			if (!is_callable($migration_callback)) {
				throw new BadFunctionCallException("{$migration_version} migration cannot be called.");
			}
			$this->addMigration($migration_version, $migration_callback);
		}
	}

	/**
	 * Register a migration into the migration system.
	 *
	 * @param string $version The version of the migration (be careful, migrations
	 *                        are sorted with the `strnatcmp` function)
	 * @param callable $callback The migration function to execute, it should
	 *                           return true on success and must return false
	 *                           on error
	 */
	public function addMigration(string $version, callable $callback): void {
		$this->migrations[$version] = $callback;
	}

	/**
	 * Return the list of migrations, sorted with `strnatcmp`
	 *
	 * @see https://www.php.net/manual/en/function.strnatcmp.php
	 *
	 * @return array<string,callable>
	 */
	public function migrations(): array {
		$migrations = $this->migrations;
		uksort($migrations, 'strnatcmp');
		return $migrations;
	}

	/**
	 * Set the applied versions of the application.
	 *
	 * @param array<string> $versions
	 *
	 * @throws DomainException if there is no migrations corresponding to a version
	 */
	public function setAppliedVersions(array $versions): void {
		foreach ($versions as $version) {
			$version = trim($version);
			if (!isset($this->migrations[$version])) {
				throw new DomainException("{$version} migration does not exist.");
			}
			$this->applied_versions[] = $version;
		}
	}

	/**
	 * @return string[]
	 */
	public function appliedVersions(): array {
		$versions = $this->applied_versions;
		usort($versions, 'strnatcmp');
		return $versions;
	}

	/**
	 * Return the list of available versions, sorted with `strnatcmp`
	 *
	 * @see https://www.php.net/manual/en/function.strnatcmp.php
	 *
	 * @return string[]
	 */
	public function versions(): array {
		$migrations = $this->migrations();
		return array_keys($migrations);
	}

	/**
	 * @return bool Return true if the application is up-to-date, false otherwise.
	 * If no migrations are registered, it always returns true.
	 */
	public function upToDate(): bool {
		// Counting versions is enough since we cannot apply a version which
		// doesn’t exist (see setAppliedVersions method).
		return count($this->versions()) === count($this->applied_versions);
	}

	/**
	 * Migrate the system to the latest version.
	 *
	 * It only executes migrations AFTER the current version. If a migration
	 * returns false or fails, it immediately stops the process.
	 *
	 * If the migration doesn’t return false nor raise an exception, it is
	 * considered as successful. It is considered as good practice to return
	 * true on success though.
	 *
	 * @return array<string|bool> Return the results of each executed migration. If an
	 *               exception was raised in a migration, its result is set to
	 *               the exception message.
	 */
	public function migrate(): array {
		$result = [];
		foreach ($this->migrations() as $version => $callback) {
			if (in_array($version, $this->applied_versions, true)) {
				// the version is already applied so we skip this migration
				continue;
			}

			try {
				$migration_result = $callback();
				$result[$version] = $migration_result;
			} catch (Exception $e) {
				$migration_result = false;
				$result[$version] = $e->getMessage();
			}

			if ($migration_result === false) {
				break;
			}

			$this->applied_versions[] = $version;
		}

		return $result;
	}
}
Model.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Model.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
*/

/**
 * The Minz_Model class represents a model in the MVC paradigm.
 */
abstract class Minz_Model {

}
ModelArray.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ModelArray.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
*/

/**
 * The Minz_ModelArray class is the model to interact with text files containing a PHP array
 */
class Minz_ModelArray {
	/**
	 * $filename est le nom du fichier
	 */
	protected string $filename;

	/**
	 * Ouvre le fichier indiqué, charge le tableau dans $array et le $filename
	 * @param string $filename le nom du fichier à ouvrir contenant un tableau
	 * Remarque : $array sera obligatoirement un tableau
	 */
	public function __construct(string $filename) {
		$this->filename = $filename;
	}

	/**
	 * @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);
		} elseif (($handle = $this->getLock()) === false) {
			throw new Minz_PermissionDeniedException($this->filename);
		} else {
			$data = include($this->filename);
			$this->releaseLock($handle);

			if ($data === false) {
				throw new Minz_PermissionDeniedException($this->filename);
			} elseif (!is_array($data)) {
				$data = array();
			}
			return $data;
		}
	}

	/**
	 * 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) {
			throw new Minz_PermissionDeniedException($this->filename);
		}
		if (function_exists('opcache_invalidate')) {
			opcache_invalidate($this->filename);	//Clear PHP cache for include
		}
		return true;
	}

	/** @return resource|false */
	private function getLock() {
		$handle = fopen($this->filename, 'r');
		if ($handle === false) {
			return false;
		}

		$count = 50;
		while (!flock($handle, LOCK_SH) && $count > 0) {
			$count--;
			usleep(1000);
		}

		if ($count > 0) {
			return $handle;
		} else {
			fclose($handle);
			return false;
		}
	}

	/** @param resource $handle */
	private function releaseLock($handle): void {
		flock($handle, LOCK_UN);
		fclose($handle);
	}
}
ModelPdo.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/ModelPdo.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
 */

/**
 * The Model_sql class represents the model for interacting with databases.
 */
class Minz_ModelPdo {

	/**
	 * Shares the connection to the database between all instances.
	 */
	public static bool $usesSharedPdo = true;

	private static ?Minz_Pdo $sharedPdo = null;

	private static string $sharedCurrentUser = '';

	protected Minz_Pdo $pdo;

	protected ?string $current_user;

	/**
	 * @throws Minz_ConfigurationNamespaceException
	 * @throws Minz_PDOConnectionException
	 * @throws PDOException
	 */
	private function dbConnect(): void {
		$db = Minz_Configuration::get('system')->db;
		$driver_options = isset($db['pdo_options']) && is_array($db['pdo_options']) ? $db['pdo_options'] : [];
		$driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_SILENT;
		$dbServer = parse_url('db://' . $db['host']);
		$dsn = '';
		$dsnParams = empty($db['connection_uri_params']) ? '' : (';' . $db['connection_uri_params']);

		switch ($db['type']) {
			case 'mysql':
				$dsn = 'mysql:';
				if (empty($dbServer['host'])) {
					$dsn .= 'unix_socket=' . $db['host'];
				} else {
					$dsn .= 'host=' . $dbServer['host'];
				}
				$dsn .= ';charset=utf8mb4';
				if (!empty($db['base'])) {
					$dsn .= ';dbname=' . $db['base'];
				}
				if (!empty($dbServer['port'])) {
					$dsn .= ';port=' . $dbServer['port'];
				}
				$driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8mb4';
				$this->pdo = new Minz_PdoMysql($dsn . $dsnParams, $db['user'], $db['password'], $driver_options);
				$this->pdo->setPrefix($db['prefix'] . $this->current_user . '_');
				break;
			case 'sqlite':
				$dsn = 'sqlite:' . DATA_PATH . '/users/' . $this->current_user . '/db.sqlite';
				$this->pdo = new Minz_PdoSqlite($dsn . $dsnParams, null, null, $driver_options);
				$this->pdo->setPrefix('');
				break;
			case 'pgsql':
				$dsn = 'pgsql:host=' . (empty($dbServer['host']) ? $db['host'] : $dbServer['host']);
				if (!empty($db['base'])) {
					$dsn .= ';dbname=' . $db['base'];
				}
				if (!empty($dbServer['port'])) {
					$dsn .= ';port=' . $dbServer['port'];
				}
				$this->pdo = new Minz_PdoPgsql($dsn . $dsnParams, $db['user'], $db['password'], $driver_options);
				$this->pdo->setPrefix($db['prefix'] . $this->current_user . '_');
				break;
			default:
				throw new Minz_PDOConnectionException(
					'Invalid database type!',
					$db['user'], Minz_Exception::ERROR
				);
		}
		if (self::$usesSharedPdo) {
			self::$sharedPdo = $this->pdo;
		}
	}

	/**
	 * Create the connection to the database using the variables
	 * HOST, BASE, USER and PASS variables defined in the configuration file
	 * @param string|null $currentUser
	 * @param Minz_Pdo|null $currentPdo
	 * @throws Minz_ConfigurationException
	 * @throws Minz_PDOConnectionException
	 */
	public function __construct(?string $currentUser = null, ?Minz_Pdo $currentPdo = null) {
		if ($currentUser === null) {
			$currentUser = Minz_User::name();
		}
		if ($currentPdo !== null) {
			$this->pdo = $currentPdo;
			return;
		}
		if ($currentUser == null) {
			throw new Minz_PDOConnectionException('Current user must not be empty!', '', Minz_Exception::ERROR);
		}
		if (self::$usesSharedPdo && self::$sharedPdo !== null && $currentUser === self::$sharedCurrentUser) {
			$this->pdo = self::$sharedPdo;
			$this->current_user = self::$sharedCurrentUser;
			return;
		}
		$this->current_user = $currentUser;
		if (self::$usesSharedPdo) {
			self::$sharedCurrentUser = $currentUser;
		}

		$ex = null;
		//Attempt a few times to connect to database
		for ($attempt = 1; $attempt <= 5; $attempt++) {
			try {
				$this->dbConnect();
				return;
			} catch (PDOException $e) {
				$ex = $e;
				if (empty($e->errorInfo[0]) || $e->errorInfo[0] !== '08006') {
					//We are only interested in: SQLSTATE connection exception / connection failure
					break;
				}
			} catch (Exception $e) {
				$ex = $e;
			}
			sleep(2);
		}

		$db = Minz_Configuration::get('system')->db;

		throw new Minz_PDOConnectionException(
				$ex === null ? '' : $ex->getMessage(),
				$db['user'], Minz_Exception::ERROR
			);
	}

	public function beginTransaction(): void {
		$this->pdo->beginTransaction();
	}

	public function inTransaction(): bool {
		return $this->pdo->inTransaction();
	}

	public function commit(): void {
		$this->pdo->commit();
	}

	public function rollBack(): void {
		$this->pdo->rollBack();
	}

	public static function clean(): void {
		self::$sharedPdo = null;
		self::$sharedCurrentUser = '';
	}

	public function close(): void {
		if ($this->current_user === self::$sharedCurrentUser) {
			self::clean();
		}
		$this->current_user = '';
		unset($this->pdo);
		gc_collect_cycles();
	}

	/**
	 * @param array<string,int|string|null> $values
	 * @phpstan-return ($mode is PDO::FETCH_ASSOC ? array<array<string,int|string|null>>|null : array<int|string|null>|null)
	 * @return array<array<string,int|string|null>>|array<int|string|null>|null
	 */
	private function fetchAny(string $sql, array $values, int $mode, int $column = 0): ?array {
		$stm = $this->pdo->prepare($sql);
		$ok = $stm !== false;
		if ($ok && !empty($values)) {
			foreach ($values as $name => $value) {
				if (is_int($value)) {
					$type = PDO::PARAM_INT;
				} elseif (is_string($value)) {
					$type = PDO::PARAM_STR;
				} elseif (is_null($value)) {
					$type = PDO::PARAM_NULL;
				} else {
					$ok = false;
					break;
				}
				if (!$stm->bindValue($name, $value, $type)) {
					$ok = false;
					break;
				}
			}
		}
		if ($ok && $stm !== false && $stm->execute()) {
			switch ($mode) {
				case PDO::FETCH_COLUMN:
					$res = $stm->fetchAll(PDO::FETCH_COLUMN, $column);
					break;
				case PDO::FETCH_ASSOC:
				default:
					$res = $stm->fetchAll(PDO::FETCH_ASSOC);
					break;
			}
			if ($res !== false) {
				return $res;
			}
		}

		$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6);
		$calling = '';
		for ($i = 2; $i < 6; $i++) {
			if (empty($backtrace[$i]['function'])) {
				break;
			}
			$calling .= '|' . $backtrace[$i]['function'];
		}
		$calling = trim($calling, '|');
		$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
		Minz_Log::error('SQL error ' . $calling . ' ' . json_encode($info));
		return null;
	}

	/**
	 * @param array<string,int|string|null> $values
	 * @return array<array<string,int|string|null>>|null
	 */
	public function fetchAssoc(string $sql, array $values = []): ?array {
		return $this->fetchAny($sql, $values, PDO::FETCH_ASSOC);
	}

	/**
	 * @param array<string,int|string|null> $values
	 * @return array<int|string|null>|null
	 */
	public function fetchColumn(string $sql, int $column, array $values = []): ?array {
		return $this->fetchAny($sql, $values, PDO::FETCH_COLUMN, $column);
	}

	/** For retrieving a single value without prepared statement such as `SELECT version()` */
	public function fetchValue(string $sql): ?string {
		$stm = $this->pdo->query($sql);
		if ($stm === false) {
			Minz_Log::error('SQL error ' . json_encode($this->pdo->errorInfo()) . ' during ' . $sql);
			return null;
		}
		$columns = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
		if ($columns === false) {
			Minz_Log::error('SQL error ' . json_encode($stm->errorInfo()) . ' during ' . $sql);
			return null;
		}
		return isset($columns[0]) ? (string)$columns[0] : null;
	}
}
PDOConnectionException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/PDOConnectionException.php'
View Content
<?php
declare(strict_types=1);

class Minz_PDOConnectionException extends Minz_Exception {
	public function __construct(string $error, string $user, int $code = self::ERROR) {
		$message = 'Access to database is denied for `' . $user . '`: ' . $error;

		parent::__construct($message, $code);
	}
}
Paginator.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Paginator.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
*/

/**
 * The Minz_Paginator is used to handle paging
 */
class Minz_Paginator {
	/**
	 * @var array<Minz_Model> tableau des éléments à afficher/gérer
	 */
	private array $items = [];

	/**
	 * le nombre d'éléments par page
	 */
	private int $nbItemsPerPage = 10;

	/**
	 * page actuelle à gérer
	 */
	private int $currentPage = 1;

	/**
	 * le nombre de pages de pagination
	 */
	private int $nbPage = 1;

	/**
	 * le nombre d'éléments
	 */
	private int $nbItems = 0;

	/**
	 * Constructeur
	 * @param array<Minz_Model> $items les éléments à gérer
	 */
	public function __construct(array $items) {
		$this->_items($items);
		$this->_nbItems(count($this->items(true)));
		$this->_nbItemsPerPage($this->nbItemsPerPage);
		$this->_currentPage($this->currentPage);
	}

	/**
	 * Permet d'afficher la pagination
	 * @param string $view nom du fichier de vue situé dans /app/views/helpers/
	 * @param string $getteur variable de type $_GET[] permettant de retrouver la page
	 */
	public function render(string $view, string $getteur = 'page'): void {
		$view = APP_PATH . '/views/helpers/' . $view;

		if (file_exists($view)) {
			include($view);
		}
	}

	/**
	 * Permet de retrouver la page d'un élément donné
	 * @param Minz_Model $item l'élément à retrouver
	 * @return int|false la page à laquelle se trouve l’élément, false si non trouvé
	 */
	public function pageByItem($item) {
		$i = 0;

		do {
			if ($item == $this->items[$i]) {
				return (int)(ceil(($i + 1) / $this->nbItemsPerPage));
			}
			$i++;
		} while ($i < $this->nbItems());

		return false;
	}

	/**
	 * Search the position (index) of a given element
	 * @param Minz_Model $item the element to search
	 * @return int|false the position of the element, or false if not found
	 */
	public function positionByItem($item) {
		$i = 0;

		do {
			if ($item == $this->items[$i]) {
				return $i;
			}
			$i++;
		} while ($i < $this->nbItems());

		return false;
	}

	/**
	 * Permet de récupérer un item par sa position
	 * @param int $pos la position de l'élément
	 * @return Minz_Model item situé à $pos (dernier item si $pos<0, 1er si $pos>=count($items))
	 */
	public function itemByPosition(int $pos): Minz_Model {
		if ($pos < 0) {
			$pos = $this->nbItems() - 1;
		}
		if ($pos >= count($this->items)) {
			$pos = 0;
		}

		return $this->items[$pos];
	}

	/**
	 * GETTEURS
	 */
	/**
	 * @param bool $all si à true, retourne tous les éléments sans prendre en compte la pagination
	 * @return array<Minz_Model>
	 */
	public function items(bool $all = false): array {
		$array = array ();
		$nbItems = $this->nbItems();

		if ($nbItems <= $this->nbItemsPerPage || $all) {
			$array = $this->items;
		} else {
			$begin = ($this->currentPage - 1) * $this->nbItemsPerPage;
			$counter = 0;
			$i = 0;

			foreach ($this->items as $key => $item) {
				if ($i >= $begin) {
					$array[$key] = $item;
					$counter++;
				}
				if ($counter >= $this->nbItemsPerPage) {
					break;
				}
				$i++;
			}
		}

		return $array;
	}
	public function nbItemsPerPage(): int {
		return $this->nbItemsPerPage;
	}
	public function currentPage(): int {
		return $this->currentPage;
	}
	public function nbPage(): int {
		return $this->nbPage;
	}
	public function nbItems(): int {
		return $this->nbItems;
	}

	/**
	 * SETTEURS
	 */
	/** @param array<Minz_Model> $items */
	public function _items(?array $items): void {
		$this->items = $items ?? [];
		$this->_nbPage();
	}
	public function _nbItemsPerPage(int $nbItemsPerPage): void {
		if ($nbItemsPerPage > $this->nbItems()) {
			$nbItemsPerPage = $this->nbItems();
		}
		if ($nbItemsPerPage < 0) {
			$nbItemsPerPage = 0;
		}

		$this->nbItemsPerPage = $nbItemsPerPage;
		$this->_nbPage();
	}
	public function _currentPage(int $page): void {
		if ($page < 1 || ($page > $this->nbPage && $this->nbPage > 0)) {
			throw new Minz_CurrentPagePaginationException($page);
		}

		$this->currentPage = $page;
	}
	private function _nbPage(): void {
		if ($this->nbItemsPerPage > 0) {
			$this->nbPage = (int)ceil($this->nbItems() / $this->nbItemsPerPage);
		}
	}
	public function _nbItems(int $value): void {
		$this->nbItems = $value;
	}
}
Pdo.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Pdo.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
 */

abstract class Minz_Pdo extends PDO {
	/**
	 * @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);
	}

	abstract public function dbType(): string;

	private string $prefix = '';
	public function prefix(): string {
		return $this->prefix;
	}
	public function setPrefix(string $prefix): void {
		$this->prefix = $prefix;
	}

	private function autoPrefix(string $sql): string {
		return str_replace('`_', '`' . $this->prefix, $sql);
	}

	protected function preSql(string $statement): string {
		if (preg_match('/^(?:UPDATE|INSERT|DELETE)/i', $statement) === 1) {
			invalidateHttpCache();
		}
		return $this->autoPrefix($statement);
	}

	// PHP8+: PDO::lastInsertId(?string $name = null): string|false
	/**
	 * @param string|null $name
	 * @return string|false
	 * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION`
	 */
	#[\Override]
	#[\ReturnTypeWillChange]
	public function lastInsertId($name = null) {
		if ($name != null) {
			$name = $this->preSql($name);
		}
		return parent::lastInsertId($name);
	}

	// PHP8+: PDO::prepare(string $query, array $options = []): PDOStatement|false
	/**
	 * @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 method.childParameterType, throws.unusedType
	 */
	#[\Override]
	#[\ReturnTypeWillChange]
	public function prepare($query, $options = []) {
		$query = $this->preSql($query);
		return parent::prepare($query, $options);
	}

	// PHP8+: PDO::exec(string $statement): int|false
	/**
	 * @param string $statement
	 * @return int|false
	 * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION`
	 * @phpstan-ignore throws.unusedType
	 */
	#[\Override]
	#[\ReturnTypeWillChange]
	public function exec($statement) {
		$statement = $this->preSql($statement);
		return parent::exec($statement);
	}

	/**
	 * @return PDOStatement|false
	 * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION`
	 * @phpstan-ignore throws.unusedType
	 */
	#[\Override]
	#[\ReturnTypeWillChange]
	public function query(string $query, ?int $fetch_mode = null, ...$fetch_mode_args) {
		$query = $this->preSql($query);
		return $fetch_mode === null ? parent::query($query) : parent::query($query, $fetch_mode, ...$fetch_mode_args);
	}
}
PdoMysql.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/PdoMysql.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
 */

class Minz_PdoMysql extends Minz_Pdo {
	/**
	 * @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);
	}

	#[\Override]
	public function dbType(): string {
		return 'mysql';
	}

	/**
	 * @param string|null $name
	 * @return string|false
	 * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION`
	 */
	#[\Override]
	#[\ReturnTypeWillChange]
	public function lastInsertId($name = null) {
		return parent::lastInsertId();	//We discard the name, only used by PostgreSQL
	}
}
PdoPgsql.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/PdoPgsql.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
 */

class Minz_PdoPgsql extends Minz_Pdo {
	/**
	 * @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';");
	}

	#[\Override]
	public function dbType(): string {
		return 'pgsql';
	}

	#[\Override]
	protected function preSql(string $statement): string {
		$statement = parent::preSql($statement);
		return str_replace(array('`', ' LIKE '), array('"', ' ILIKE '), $statement);
	}
}
PdoSqlite.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/PdoSqlite.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
 */

class Minz_PdoSqlite extends Minz_Pdo {
	/**
	 * @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;');
	}

	#[\Override]
	public function dbType(): string {
		return 'sqlite';
	}

	/**
	 * @param string|null $name
	 * @return string|false
	 * @throws PDOException if the attribute `PDO::ATTR_ERRMODE` is set to `PDO::ERRMODE_EXCEPTION`
	 */
	#[\Override]
	#[\ReturnTypeWillChange]
	public function lastInsertId($name = null) {
		return parent::lastInsertId();	//We discard the name, only used by PostgreSQL
	}
}
PermissionDeniedException.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/PermissionDeniedException.php'
View Content
<?php
declare(strict_types=1);

class Minz_PermissionDeniedException extends Minz_Exception {
	public function __construct(string $file_name, int $code = self::ERROR) {
		$message = 'Permission is denied for `' . $file_name . '`';

		parent::__construct($message, $code);
	}
}
Request.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Request.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
*/

/**
 * Request représente la requête http
 */
class Minz_Request {

	private static string $controller_name = '';
	private static string $action_name = '';
	/** @var array<string,mixed> */
	private static array $params = [];

	private static string $default_controller_name = 'index';
	private static string $default_action_name = 'index';

	/** @var array{c?:string,a?:string,params?:array<string,mixed>} */
	private static array $originalRequest = [];

	/**
	 * Getteurs
	 */
	public static function controllerName(): string {
		return self::$controller_name;
	}
	public static function actionName(): string {
		return self::$action_name;
	}
	/** @return array<string,mixed> */
	public static function params(): array {
		return self::$params;
	}

	/**
	 * Read the URL parameter
	 * @param string $key Key name
	 * @param mixed $default default value, if no parameter is given
	 * @param bool $specialchars special characters
	 * @return mixed value of the parameter
	 * @deprecated use typed versions instead
	 */
	public static function param(string $key, $default = false, bool $specialchars = false) {
		if (isset(self::$params[$key])) {
			$p = self::$params[$key];
			if (is_string($p) || is_array($p)) {
				return $specialchars ? $p : Minz_Helper::htmlspecialchars_utf8($p);
			} else {
				return $p;
			}
		} else {
			return $default;
		}
	}

	public static function hasParam(string $key): bool {
		return isset(self::$params[$key]);
	}

	/** @return array<string|int,string|array<string,string|int|bool>> */
	public static function paramArray(string $key, bool $specialchars = false): array {
		if (empty(self::$params[$key]) || !is_array(self::$params[$key])) {
			return [];
		}
		return $specialchars ? Minz_Helper::htmlspecialchars_utf8(self::$params[$key]) : self::$params[$key];
	}

	/** @return array<string> */
	public static function paramArrayString(string $key, bool $specialchars = false): array {
		if (empty(self::$params[$key]) || !is_array(self::$params[$key])) {
			return [];
		}
		$result = array_filter(self::$params[$key], 'is_string');
		return $specialchars ? Minz_Helper::htmlspecialchars_utf8($result) : $result;
	}

	public static function paramTernary(string $key): ?bool {
		if (isset(self::$params[$key])) {
			$p = self::$params[$key];
			$tp = is_string($p) ? trim($p) : true;
			if ($tp === '' || $tp === 'null') {
				return null;
			} elseif ($p == false || $tp == '0' || $tp === 'false' || $tp === 'no') {
				return false;
			}
			return true;
		}
		return null;
	}

	public static function paramBoolean(string $key): bool {
		if (null === $value = self::paramTernary($key)) {
			return false;
		}
		return $value;
	}

	public static function paramInt(string $key): int {
		if (!empty(self::$params[$key]) && is_numeric(self::$params[$key])) {
			return (int)self::$params[$key];
		}
		return 0;
	}

	public static function paramStringNull(string $key, bool $specialchars = false): ?string {
		if (isset(self::$params[$key])) {
			$s = self::$params[$key];
			if (is_string($s)) {
				$s = trim($s);
				return $specialchars ? $s : htmlspecialchars($s, ENT_COMPAT, 'UTF-8');
			}
			if (is_int($s) || is_bool($s)) {
				return (string)$s;
			}
		}
		return null;
	}

	public static function paramString(string $key, bool $specialchars = false): string {
		return self::paramStringNull($key, $specialchars) ?? '';
	}

	/**
	 * Extract text lines to array.
	 *
	 * It will return an array where each cell contains one line of a text. The new line
	 * character is used to break the text into lines. This method is well suited to use
	 * to split textarea content.
	 * @param array<string> $default
	 * @return array<string>
	 */
	public static function paramTextToArray(string $key, array $default = []): array {
		if (isset(self::$params[$key]) && is_string(self::$params[$key])) {
			return preg_split('/\R/u', self::$params[$key]) ?: [];
		}
		return $default;
	}

	public static function defaultControllerName(): string {
		return self::$default_controller_name;
	}
	public static function defaultActionName(): string {
		return self::$default_action_name;
	}
	/** @return array{c:string,a:string,params:array<string,mixed>} */
	public static function currentRequest(): array {
		return [
			'c' => self::$controller_name,
			'a' => self::$action_name,
			'params' => self::$params,
		];
	}

	/** @return array{c?:string,a?:string,params?:array<string,mixed>} */
	public static function originalRequest() {
		return self::$originalRequest;
	}

	/**
	 * @param array<string,mixed>|null $extraParams
	 * @return array{c:string,a:string,params:array<string,mixed>}
	 */
	public static function modifiedCurrentRequest(?array $extraParams = null): array {
		unset(self::$params['ajax']);
		$currentRequest = self::currentRequest();
		if (null !== $extraParams) {
			$currentRequest['params'] = array_merge($currentRequest['params'], $extraParams);
		}
		return $currentRequest;
	}

	/**
	 * Setteurs
	 */
	public static function _controllerName(string $controller_name): void {
		self::$controller_name = ctype_alnum($controller_name) ? $controller_name : '';
	}

	public static function _actionName(string $action_name): void {
		self::$action_name = ctype_alnum($action_name) ? $action_name : '';
	}

	/** @param array<string,mixed> $params */
	public static function _params(array $params): void {
		self::$params = $params;
	}

	public static function _param(string $key, ?string $value = null): void {
		if ($value === null) {
			unset(self::$params[$key]);
		} else {
			self::$params[$key] = $value;
		}
	}

	/**
	 * Initialise la Request
	 */
	public static function init(): void {
		self::_params($_GET);
		self::initJSON();
	}

	public static function is(string $controller_name, string $action_name): bool {
		return self::$controller_name === $controller_name &&
			self::$action_name === $action_name;
	}

	/**
	 * Return true if the request is over HTTPS, false otherwise (HTTP)
	 */
	public static function isHttps(): bool {
		$header = $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '';
		if ('' != $header) {
			return 'https' === strtolower($header);
		}
		return 'on' === ($_SERVER['HTTPS'] ?? '');
	}

	/**
	 * Try to guess the base URL from $_SERVER information
	 *
	 * @return string base url (e.g. http://example.com)
	 */
	public static function guessBaseUrl(): string {
		$protocol = self::extractProtocol();
		$host = self::extractHost();
		$port = self::extractPortForUrl();
		$prefix = self::extractPrefix();
		$path = self::extractPath();

		return filter_var("{$protocol}://{$host}{$port}{$prefix}{$path}", FILTER_SANITIZE_URL) ?: '';
	}

	private static function extractProtocol(): string {
		if (self::isHttps()) {
			return 'https';
		}
		return 'http';
	}

	private static function extractHost(): string {
		if ('' != $host = ($_SERVER['HTTP_X_FORWARDED_HOST'] ?? '')) {
			return parse_url("http://{$host}", PHP_URL_HOST) ?: 'localhost';
		}
		if ('' != $host = ($_SERVER['HTTP_HOST'] ?? '')) {
			// Might contain a port number, and mind IPv6 addresses
			return parse_url("http://{$host}", PHP_URL_HOST) ?: 'localhost';
		}
		if ('' != $host = ($_SERVER['SERVER_NAME'] ?? '')) {
			return $host;
		}
		return 'localhost';
	}

	private static function extractPort(): int {
		if ('' != $port = ($_SERVER['HTTP_X_FORWARDED_PORT'] ?? '')) {
			return intval($port);
		}
		if ('' != $proto = ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '')) {
			return 'https' === strtolower($proto) ? 443 : 80;
		}
		if ('' != $port = ($_SERVER['SERVER_PORT'] ?? '')) {
			return intval($port);
		}
		return self::isHttps() ? 443 : 80;
	}

	private static function extractPortForUrl(): string {
		if (self::isHttps() && 443 !== $port = self::extractPort()) {
			return ":{$port}";
		}
		if (!self::isHttps() && 80 !== $port = self::extractPort()) {
			return ":{$port}";
		}
		return '';
	}

	private static function extractPrefix(): string {
		if ('' != $prefix = ($_SERVER['HTTP_X_FORWARDED_PREFIX'] ?? '')) {
			return rtrim($prefix, '/ ');
		}
		return '';
	}

	private static function extractPath(): string {
		$path = $_SERVER['REQUEST_URI'] ?? '';
		if ($path != '') {
			$path = parse_url($path, PHP_URL_PATH) ?: '';
			return substr($path, -1) === '/' ? rtrim($path, '/') : dirname($path);
		}
		return '';
	}

	/**
	 * Return the base_url from configuration
	 * @throws Minz_ConfigurationException
	 */
	public static function getBaseUrl(): string {
		$conf = Minz_Configuration::get('system');
		$url = trim($conf->base_url, ' /\\"');
		return filter_var($url, FILTER_SANITIZE_URL) ?: '';
	}

	/**
	 * Test if a given server address is publicly accessible.
	 *
	 * Note: for the moment it tests only if address is corresponding to a
	 * localhost address.
	 *
	 * @param string $address the address to test, can be an IP or a URL.
	 * @return bool true if server is accessible, false otherwise.
	 * @todo improve test with a more valid technique (e.g. test with an external server?)
	 */
	public static function serverIsPublic(string $address): bool {
		if (strlen($address) < strlen('http://a.bc')) {
			return false;
		}
		$host = parse_url($address, PHP_URL_HOST);
		if (!is_string($host)) {
			return false;
		}

		$is_public = !in_array($host, [
			'localhost',
			'localhost.localdomain',
			'[::1]',
			'ip6-localhost',
			'localhost6',
			'localhost6.localdomain6',
		], true);

		if ($is_public) {
			$is_public &= !preg_match('/^(10|127|172[.]16|192[.]168)[.]/', $host);
			$is_public &= !preg_match('/^(\[)?(::1$|fc00::|fe80::)/i', $host);
		}

		return (bool)$is_public;
	}

	private static function requestId(): string {
		if (empty($_GET['rid']) || !ctype_xdigit($_GET['rid'])) {
			$_GET['rid'] = uniqid();
		}
		return $_GET['rid'];
	}

	private static function setNotification(string $type, string $content): void {
		Minz_Session::lock();
		$requests = Minz_Session::paramArray('requests');
		$requests[self::requestId()] = [
				'time' => time(),
				'notification' => [ 'type' => $type, 'content' => $content ],
			];
		Minz_Session::_param('requests', $requests);
		Minz_Session::unlock();
	}

	public static function setGoodNotification(string $content): void {
		self::setNotification('good', $content);
	}

	public static function setBadNotification(string $content): void {
		self::setNotification('bad', $content);
	}

	/**
	 * @param $pop true (default) to remove the notification, false to keep it.
	 * @return array{type:string,content:string}|null
	 */
	public static function getNotification(bool $pop = true): ?array {
		$notif = null;
		Minz_Session::lock();
		/** @var array<string,array{time:int,notification:array{type:string,content:string}}> */
		$requests = Minz_Session::paramArray('requests');
		if (!empty($requests)) {
			//Delete abandoned notifications
			$requests = array_filter($requests, static function (array $r) { return $r['time'] > time() - 3600; });

			$requestId = self::requestId();
			if (!empty($requests[$requestId]['notification'])) {
				$notif = $requests[$requestId]['notification'];
				if ($pop) {
					unset($requests[$requestId]);
				}
			}
			Minz_Session::_param('requests', $requests);
		}
		Minz_Session::unlock();
		return $notif;
	}

	/**
	 * 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())) {
			self::$originalRequest = $url;
		}

		$url = Minz_Url::checkControllerUrl($url);
		$url['params']['rid'] = self::requestId();

		if ($redirect) {
			header('Location: ' . Minz_Url::display($url, 'php', 'root'));
			exit();
		} else {
			self::_controllerName($url['c']);
			self::_actionName($url['a']);
			$merge = array_merge(self::$params, $url['params']);
			self::_params($merge);
			Minz_Dispatcher::reset();
		}
	}

	/**
	 * Wrappers good notifications + redirection
	 * @param string $msg notification content
	 * @param array{c?:string,a?:string,params?:array<string,mixed>} $url url array to where we should be forwarded
	 */
	public static function good(string $msg, array $url = []): void {
		Minz_Request::setGoodNotification($msg);
		Minz_Request::forward($url, true);
	}

	/**
	 * Wrappers bad notifications + redirection
	 * @param string $msg notification content
	 * @param array{c?:string,a?:string,params?:array<string,mixed>} $url url array to where we should be forwarded
	 */
	public static function bad(string $msg, array $url = []): void {
		Minz_Request::setBadNotification($msg);
		Minz_Request::forward($url, true);
	}

	/**
	 * Allows receiving POST data as application/json
	 */
	private static function initJSON(): void {
		if (!str_starts_with(self::extractContentType(), 'application/json')) {
			return;
		}
		$ORIGINAL_INPUT = file_get_contents('php://input', false, null, 0, 1048576);
		if ($ORIGINAL_INPUT == false) {
			return;
		}
		if (!is_array($json = json_decode($ORIGINAL_INPUT, true))) {
			return;
		}

		foreach ($json as $k => $v) {
			if (!isset($_POST[$k])) {
				$_POST[$k] = $v;
			}
		}
	}

	private static function extractContentType(): string {
		return strtolower(trim($_SERVER['CONTENT_TYPE'] ?? ''));
	}

	public static function isPost(): bool {
		return 'POST' === ($_SERVER['REQUEST_METHOD'] ?? '');
	}

	/**
	 * @return array<string>
	 */
	public static function getPreferredLanguages(): array {
		if (preg_match_all('/(^|,)\s*(?P<lang>[^;,]+)/', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '', $matches) > 0) {
			return $matches['lang'];
		}
		return array('en');
	}
}
Session.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Session.php'
View Content
<?php
declare(strict_types=1);

/**
 * The Minz_Session class handles user’s session
 */
class Minz_Session {

	private static bool $volatile = false;

	/**
	 * For mutual exclusion.
	 */
	private static bool $locked = false;

	public static function lock(): bool {
		if (!self::$volatile && !self::$locked) {
			session_start();
			self::$locked = true;
		}
		return self::$locked;
	}

	public static function unlock(): bool {
		if (!self::$volatile) {
			session_write_close();
			self::$locked = false;
		}
		return self::$locked;
	}

	/**
	 * Initialize the session, with a name
	 * The session name is used as the name for cookies and URLs (i.e. PHPSESSID).
	 * It should contain only alphanumeric characters; it should be short and descriptive
	 * If the volatile parameter is true, then no cookie and not session storage are used.
	 * Volatile is especially useful for API calls without cookie / Web session.
	 */
	public static function init(string $name, bool $volatile = false): void {
		self::$volatile = $volatile;
		if (self::$volatile) {
			$_SESSION = [];
			return;
		}

		$cookie = session_get_cookie_params();
		self::keepCookie($cookie['lifetime']);

		// start session
		session_name($name);
		//When using cookies (default value), session_stars() sends HTTP headers
		session_start();
		session_write_close();
		//Use cookie only the first time the session is started to avoid resending HTTP headers
		ini_set('session.use_cookies', '0');
	}


	/**
	 * Allows you to retrieve a session variable
	 * @param string $p the parameter to retrieve
	 * @param mixed|false $default the default value if the parameter doesn’t exist
	 * @return mixed|false the value of the session variable, false if doesn’t exist
	 * @deprecated Use typed versions instead
	 */
	public static function param(string $p, $default = false) {
		return $_SESSION[$p] ?? $default;
	}

	/** @return array<string|int,string|array<string,mixed>> */
	public static function paramArray(string $key): array {
		if (empty($_SESSION[$key]) || !is_array($_SESSION[$key])) {
			return [];
		}
		return $_SESSION[$key];
	}

	public static function paramTernary(string $key): ?bool {
		if (isset($_SESSION[$key])) {
			$p = $_SESSION[$key];
			$tp = is_string($p) ? trim($p) : true;
			if ($tp === '' || $tp === 'null') {
				return null;
			} elseif ($p == false || $tp == '0' || $tp === 'false' || $tp === 'no') {
				return false;
			}
			return true;
		}
		return null;
	}

	public static function paramBoolean(string $key): bool {
		if (null === $value = self::paramTernary($key)) {
			return false;
		}
		return $value;
	}

	public static function paramInt(string $key): int {
		if (!empty($_SESSION[$key])) {
			return intval($_SESSION[$key]);
		}
		return 0;
	}

	public static function paramString(string $key): string {
		if (isset($_SESSION[$key])) {
			$s = $_SESSION[$key];
			if (is_string($s)) {
				return $s;
			}
			if (is_int($s) || is_bool($s)) {
				return (string)$s;
			}
		}
		return '';
	}

	/**
	 * Allows you to create or update a session variable
	 * @param string $parameter the parameter to create or modify
	 * @param mixed|false $value the value to assign, false to delete
	 */
	public static function _param(string $parameter, $value = false): void {
		if (!self::$volatile && !self::$locked) {
			session_start();
		}
		if ($value === false) {
			unset($_SESSION[$parameter]);
		} else {
			$_SESSION[$parameter] = $value;
		}
		if (!self::$volatile && !self::$locked) {
			session_write_close();
		}
	}

	/**
	 * @param array<string,string|bool|int|array<string>> $keyValues
	 */
	public static function _params(array $keyValues): void {
		if (!self::$volatile && !self::$locked) {
			session_start();
		}
		foreach ($keyValues as $key => $value) {
			if ($value === false) {
				unset($_SESSION[$key]);
			} else {
				$_SESSION[$key] = $value;
			}
		}
		if (!self::$volatile && !self::$locked) {
			session_write_close();
		}
	}

	/**
	 * Allows to delete a session
	 * @param bool $force if false, does not clear the language parameter
	 */
	public static function unset_session(bool $force = false): void {
		$language = self::paramString('language');

		if (!self::$volatile) {
			session_destroy();
		}
		$_SESSION = array();

		if (!$force) {
			self::_param('language', $language);
			Minz_Translate::reset($language);
		}
	}

	public static function getCookieDir(): string {
		// Get the script_name (e.g. /p/i/index.php) and keep only the path.
		$cookie_dir = '';
		if (!empty($_SERVER['HTTP_X_FORWARDED_PREFIX'])) {
			$cookie_dir .= rtrim($_SERVER['HTTP_X_FORWARDED_PREFIX'], '/ ');
		}
		$cookie_dir .= empty($_SERVER['REQUEST_URI']) ? '/' : $_SERVER['REQUEST_URI'];
		if (substr($cookie_dir, -1) !== '/') {
			$cookie_dir = dirname($cookie_dir) . '/';
		}
		return $cookie_dir;
	}

	/**
	 * Specifies the lifetime of the cookies
	 * @param int $l the lifetime
	 */
	public static function keepCookie(int $l): void {
		session_set_cookie_params($l, self::getCookieDir(), '', Minz_Request::isHttps(), true);
	}

	/**
	 * Regenerate a session id.
	 * Useful to call session_set_cookie_params after session_start()
	 */
	public static function regenerateID(): void {
		session_regenerate_id(true);
	}

	public static function deleteLongTermCookie(string $name): void {
		setcookie($name, '', 1, '', '', Minz_Request::isHttps(), true);
	}

	public static function setLongTermCookie(string $name, string $value, int $expire): void {
		setcookie($name, $value, $expire, '', '', Minz_Request::isHttps(), true);
	}

	public static function getLongTermCookie(string $name): string {
		return $_COOKIE[$name] ?? '';
	}

}
Translate.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Translate.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
 */

/**
 * This class is used for the internationalization.
 * It uses files in `./app/i18n/`
 */
class Minz_Translate {
	/**
	 * $path_list is the list of registered base path to search translations.
	 * @var array<string>
	 */
	private static array $path_list = [];

	/**
	 * $lang_name is the name of the current language to use.
	 */
	private static string $lang_name = '';

	/**
	 * $lang_files is a list of registered i18n files.
	 * @var array<string,array<string>>
	 */
	private static array $lang_files = [];

	/**
	 * $translates is a cache for i18n translation.
	 * @var array<string,mixed>
	 */
	private static array $translates = [];

	/**
	 * Init the translation object.
	 * @param string $lang_name the lang to show.
	 */
	public static function init(string $lang_name = ''): void {
		self::$lang_name = $lang_name;
		self::$lang_files = array();
		self::$translates = array();
		self::registerPath(APP_PATH . '/i18n');
		foreach (self::$path_list as $path) {
			self::loadLang($path);
		}
	}

	/**
	 * Reset the translation object with a new language.
	 * @param string $lang_name the new language to use
	 */
	public static function reset(string $lang_name): void {
		self::$lang_name = $lang_name;
		self::$lang_files = array();
		self::$translates = array();
		foreach (self::$path_list as $path) {
			self::loadLang($path);
		}
	}

	/**
	 * Return the list of available languages.
	 * @return array<string> containing langs found in different registered paths.
	 */
	public static function availableLanguages(): array {
		$list_langs = array();

		self::registerPath(APP_PATH . '/i18n');

		foreach (self::$path_list as $path) {
			$scan = scandir($path);
			if (is_array($scan)) {
				$path_langs = array_values(array_diff(
					$scan,
					array('..', '.')
				));
				$list_langs = array_merge($list_langs, $path_langs);
			}
		}

		return array_unique($list_langs);
	}

	/**
	 * 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|null $user the connected user language (nullable)
	 * @param array<string> $preferred an array of the preferred languages
	 * @param string|null $default the preferred language to use
	 * @return string containing the language to use
	 */
	public static function getLanguage(?string $user, array $preferred, ?string $default): string {
		if (null !== $user) {
			return $user;
		}

		$languages = Minz_Translate::availableLanguages();
		foreach ($preferred as $language) {
			$language = strtolower($language);
			if (in_array($language, $languages, true)) {
				return $language;
			}
		}

		return $default == null ? 'en' : $default;
	}

	/**
	 * Register a new path.
	 * @param string $path a path containing i18n directories (e.g. ./en/, ./fr/).
	 */
	public static function registerPath(string $path): void {
		if (!in_array($path, self::$path_list, true) && is_dir($path)) {
			self::$path_list[] = $path;
			self::loadLang($path);
		}
	}

	/**
	 * Load translations of the current language from the given path.
	 * @param string $path the path containing i18n directories.
	 */
	private static function loadLang(string $path): void {
		$lang_path = $path . '/' . self::$lang_name;
		if (self::$lang_name === '' || !is_dir($lang_path)) {
			// The lang path does not exist, fallback to English ('en')
			$lang_path = $path . '/en';
			if (!is_dir($lang_path)) {
				// English ('en') i18n files not provided. Stop here. The keys will be shown.
				return;
			}
		}

		$list_i18n_files = array_values(array_diff(
			scandir($lang_path) ?: [],
			['..', '.']
		));

		// Each file basename correspond to a top-level i18n key. For each of
		// these keys we store the file pathname and mark translations must be
		// reloaded (by setting $translates[$i18n_key] to null).
		foreach ($list_i18n_files as $i18n_filename) {
			$i18n_key = basename($i18n_filename, '.php');
			if (!isset(self::$lang_files[$i18n_key])) {
				self::$lang_files[$i18n_key] = array();
			}
			self::$lang_files[$i18n_key][] = $lang_path . '/' . $i18n_filename;
			self::$translates[$i18n_key] = null;
		}
	}

	/**
	 * Load the files associated to $key into $translates.
	 * @param string $key the top level i18n key we want to load.
	 */
	private static function loadKey(string $key): bool {
		// The top level key is not in $lang_files, it means it does not exist!
		if (!isset(self::$lang_files[$key])) {
			Minz_Log::debug($key . ' is not a valid top level key');
			return false;
		}

		self::$translates[$key] = array();

		foreach (self::$lang_files[$key] as $lang_pathname) {
			$i18n_array = include($lang_pathname);
			if (!is_array($i18n_array)) {
				Minz_Log::warning('`' . $lang_pathname . '` does not contain a PHP array');
				continue;
			}

			// We must avoid to erase previous data so we just override them if
			// needed.
			self::$translates[$key] = array_replace_recursive(
				self::$translates[$key], $i18n_array
			);
		}

		return true;
	}

	/**
	 * Translate a key into its corresponding value based on selected language.
	 * @param string $key the key to translate.
	 * @param bool|float|int|string ...$args additional parameters for variable keys.
	 * @return string value corresponding to the key.
	 *         If no value is found, return the key itself.
	 */
	public static function t(string $key, ...$args): string {
		$group = explode('.', $key);

		if (count($group) < 2) {
			Minz_Log::debug($key . ' is not in a valid format');
			$top_level = 'gen';
		} else {
			$top_level = array_shift($group);
		}

		// If $translates[$top_level] is null it means we have to load the
		// corresponding files.
		if (empty(self::$translates[$top_level])) {
			$res = self::loadKey($top_level);
			if (!$res) {
				return $key;
			}
		}

		// Go through the i18n keys to get the correct translation value.
		$translates = self::$translates[$top_level];
		if (!is_array($translates)) {
			$translates = [];
		}
		$size_group = count($group);
		$level_processed = 0;
		$translation_value = $key;
		foreach ($group as $i18n_level) {
			$level_processed++;
			if (!isset($translates[$i18n_level])) {
				Minz_Log::debug($key . ' is not a valid key');
				return $key;
			}

			if ($level_processed < $size_group) {
				$translates = $translates[$i18n_level];
			} else {
				$translation_value = $translates[$i18n_level];
			}
		}

		if (is_array($translation_value)) {
			if (isset($translation_value['_'])) {
				$translation_value = $translation_value['_'];
			} else {
				Minz_Log::debug($key . ' is not a valid key');
				return $key;
			}
		}

		// Get the facultative arguments to replace i18n variables.
		return empty($args) ? $translation_value : vsprintf($translation_value, $args);
	}

	/**
	 * Return the current language.
	 */
	public static function language(): string {
		return self::$lang_name;
	}
}


/**
 * Alias for Minz_Translate::t()
 * @param string $key
 * @param bool|float|int|string ...$args
 */
function _t(string $key, ...$args): string {
	return Minz_Translate::t($key, ...$args);
}
Url.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/Url.php'
View Content
<?php
declare(strict_types=1);

/**
 * The Minz_Url class handles URLs across the MINZ framework
 */
class Minz_Url {
	/**
	 * Display a formatted URL
	 * @param string|array{c?:string,a?:string,params?:array<string,mixed>} $url The URL to format, defined as an array:
	 *                    $url['c'] = controller
	 *                    $url['a'] = action
	 *                    $url['params'] = array of additional parameters
	 *             or as a string
	 * @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);

		if ($isArray) {
			$url = self::checkControllerUrl($url);
		}

		$url_string = '';

		if ($absolute !== false) {
			$url_string = Minz_Request::getBaseUrl();
			if (strlen($url_string) < strlen('http://a.bc')) {
				$url_string = Minz_Request::guessBaseUrl();
				if (PUBLIC_RELATIVE === '..' && preg_match('%' . PUBLIC_TO_INDEX_PATH . '(/|$)%', $url_string)) {
					//TODO: Implement proper resolver of relative parts such as /test/./../
					$url_string = dirname($url_string);
				}
			}
			if ($isArray) {
				$url_string .= PUBLIC_TO_INDEX_PATH;
			}
			if ($absolute === 'root') {
				$url_string = parse_url($url_string, PHP_URL_PATH);
			}
		} else {
			$url_string = $isArray ? '.' : PUBLIC_RELATIVE;
		}

		if ($isArray) {
			$url_string .= '/' . self::printUri($url, $encoding);
		} elseif ($encoding === 'html') {
			$url_string = Minz_Helper::htmlspecialchars_utf8($url_string . $url);
		} else {
			$url_string .= $url;
		}

		return $url_string;
	}

	/**
	 * Construit l'URI d'une URL
	 * @param array{c:string,a:string,params:array<string,mixed>} $url URL as array definition
	 * @param string $encodage pour indiquer comment encoder les & (& ou &amp; pour html)
	 * @return string uri sous la forme ?key=value&key2=value2
	 */
	private static function printUri(array $url, string $encodage): string {
		$uri = '';
		$separator = '?';
		$anchor = '';

		if ($encodage === 'html') {
			$and = '&amp;';
		} else {
			$and = '&';
		}

		if (!empty($url['params']) && is_array($url['params']) && !empty($url['params']['#'])) {
			if (is_string($url['params']['#'])) {
				$anchor = '#' . ($encodage === 'html' ? htmlspecialchars($url['params']['#'], ENT_QUOTES, 'UTF-8') : $url['params']['#']);
			}
			unset($url['params']['#']);
		}

		if (isset($url['c']) && is_string($url['c'])
			&& $url['c'] != Minz_Request::defaultControllerName()) {
			$uri .= $separator . 'c=' . $url['c'];
			$separator = $and;
		}

		if (isset($url['a']) && is_string($url['a'])
			&& $url['a'] != Minz_Request::defaultActionName()) {
			$uri .= $separator . 'a=' . $url['a'];
			$separator = $and;
		}

		if (isset($url['params']) && is_array($url['params'])) {
			unset($url['params']['c']);
			unset($url['params']['a']);
			foreach ($url['params'] as $key => $param) {
				if (!is_string($key) || (!is_string($param) && !is_int($param) && !is_bool($param))) {
					continue;
				}
				$uri .= $separator . urlencode($key) . '=' . urlencode((string)$param);
				$separator = $and;
			}
		}

		$uri .= $anchor;

		return $uri;
	}

	/**
	 * Check that all array elements representing the controller URL are OK
	 * @param array{c?:string,a?:string,params?:array<string,mixed>} $url controller URL as array
	 * @return array{c:string,a:string,params:array<string,mixed>} Verified controller URL as array
	 */
	public static function checkControllerUrl(array $url): array {
		return [
			'c' => empty($url['c']) || !is_string($url['c']) ? Minz_Request::defaultControllerName() : $url['c'],
			'a' => empty($url['a']) || !is_string($url['a']) ? Minz_Request::defaultActionName() : $url['a'],
			'params' => empty($url['params']) || !is_array($url['params']) ? [] : $url['params'],
		];
	}

	/** @param array{c?:string,a?:string,params?:array<string,mixed>} $url */
	public static function serialize(?array $url = []): string {
		if (empty($url)) {
			return '';
		}
		try {
			return base64_encode(json_encode($url, JSON_THROW_ON_ERROR));
		} catch (\Throwable $exception) {
			return '';
		}
	}

	/** @return array{c?:string,a?:string,params?:array<string,mixed>} */
	public static function unserialize(string $url = ''): array {
		$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;
	}

	/**
	 * Returns an array representing the URL as passed in the address bar
	 * @return array{c?:string,a?:string,params?:array<string,string>} URL representation
	 */
	public static function build(): array {
		$url = [
			'c' => $_GET['c'] ?? Minz_Request::defaultControllerName(),
			'a' => $_GET['a'] ?? Minz_Request::defaultActionName(),
			'params' => $_GET,
		];

		// post-traitement
		unset($url['params']['c']);
		unset($url['params']['a']);

		return $url;
	}
}

/**
 * @param string $controller
 * @param string $action
 * @param string|int ...$args
 * @return string|false
 */
function _url(string $controller, string $action, ...$args) {
	$nb_args = count($args);

	if ($nb_args % 2 !== 0) {
		return false;
	}

	$params = array ();
	for ($i = 0; $i < $nb_args; $i += 2) {
		$arg = '' . $args[$i];
		$params[$arg] = '' . $args[$i + 1];
	}

	return Minz_Url::display(['c' => $controller, 'a' => $action, 'params' => $params]);
}
User.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/User.php'
View Content
<?php
declare(strict_types=1);

/**
 * The Minz_User class handles the user information.
 */
final class Minz_User {

	public const INTERNAL_USER = '_';

	public const CURRENT_USER = 'currentUser';

	/**
	 * @return string the name of the current user, or null if there is none
	 */
	public static function name(): ?string {
		$currentUser = trim(Minz_Session::paramString(Minz_User::CURRENT_USER));
		return $currentUser === '' ? null : $currentUser;
	}

	/**
	 * @param string $name the name of the new user. Set to empty string to clear the user.
	 */
	public static function change(string $name = ''): void {
		$name = trim($name);
		Minz_Session::_param(Minz_User::CURRENT_USER, $name === '' ? false : $name);
	}
}
View.php
wget 'https://sme10.lists2.roe3.org/FreshRSS/lib/Minz/View.php'
View Content
<?php
declare(strict_types=1);

/**
 * MINZ - Copyright 2011 Marien Fressinaud
 * Sous licence AGPL3 <http://www.gnu.org/licenses/>
*/

/**
 * The Minz_View represents a view in the MVC paradigm
 */
class Minz_View {
	private const VIEWS_PATH_NAME = '/views';
	private const LAYOUT_PATH_NAME = '/layout/';
	private const LAYOUT_DEFAULT = 'layout';

	private string $view_filename = '';
	private string $layout_filename = '';
	/** @var array<string> */
	private static array $base_pathnames = [APP_PATH];
	private static string $title = '';
	/** @var array<array{'media':string,'url':string}> */
	private static array $styles = [];
	/** @var array<array{'url':string,'id':string,'defer':bool,'async':bool}> */
	private static array $scripts = [];
	/** @var string|array{'dark'?:string,'light'?:string,'default'?:string} */
	private static $themeColors;
	/** @var array<string,mixed> */
	private static array $params = [];

	/**
	 * Determines if a layout is used or not
	 * @throws Minz_ConfigurationException
	 */
	public function __construct() {
		$this->_layout(self::LAYOUT_DEFAULT);
		$conf = Minz_Configuration::get('system');
		self::$title = $conf->title;
	}

	/**
	 * @deprecated Change the view file based on controller and action.
	 */
	public function change_view(string $controller_name, string $action_name): void {
		Minz_Log::warning('Minz_View::change_view is deprecated, it will be removed in a future version. Please use Minz_View::_path instead.');
		$this->_path($controller_name . '/' . $action_name . '.phtml');
	}

	/**
	 * Change the view file based on a pathname relative to VIEWS_PATH_NAME.
	 *
	 * @param string $path the new path
	 */
	public function _path(string $path): void {
		$this->view_filename = self::VIEWS_PATH_NAME . '/' . $path;
	}

	/**
	 * Add a base pathname to search views.
	 *
	 * New pathnames will be added at the beginning of the list.
	 *
	 * @param string $base_pathname the new base pathname.
	 */
	public static function addBasePathname(string $base_pathname): void {
		array_unshift(self::$base_pathnames, $base_pathname);
	}

	/**
	 * Builds the view filename based on controller and action.
	 */
	public function build(): void {
		if ($this->layout_filename !== '') {
			$this->buildLayout();
		} else {
			$this->render();
		}
	}

	/**
	 * Include a view file.
	 *
	 * The file is searched inside list of $base_pathnames.
	 *
	 * @param string $filename the name of the file to include.
	 * @return bool true if the file has been included, false else.
	 */
	private function includeFile(string $filename): bool {
		// We search the filename in the list of base pathnames. Only the first view
		// found is considered.
		foreach (self::$base_pathnames as $base) {
			$absolute_filename = $base . $filename;
			if (file_exists($absolute_filename)) {
				include $absolute_filename;
				return true;
			}
		}

		return false;
	}

	/**
	 * Builds the layout
	 */
	public function buildLayout(): void {
		header('Content-Type: text/html; charset=UTF-8');
		if (!$this->includeFile($this->layout_filename)) {
			Minz_Log::notice('File not found: `' . $this->layout_filename . '`');
		}
	}

	/**
	 * Displays the View itself
	 */
	public function render(): void {
		if (!$this->includeFile($this->view_filename)) {
			Minz_Log::notice('File not found: `' . $this->view_filename . '`');
		}
	}

	public function renderToString(): string {
		ob_start();
		$this->render();
		return ob_get_clean() ?: '';
	}

	/**
	 * Adds a layout element
	 * @param string $part the partial element to be added
	 */
	public function partial(string $part): void {
		$fic_partial = self::LAYOUT_PATH_NAME . '/' . $part . '.phtml';
		if (!$this->includeFile($fic_partial)) {
			Minz_Log::warning('File not found: `' . $fic_partial . '`');
		}
	}

	/**
	 * Displays a graphic element located in APP./views/helpers/
	 * @param string $helper the element to be displayed
	 */
	public function renderHelper(string $helper): void {
		$fic_helper = '/views/helpers/' . $helper . '.phtml';
		if (!$this->includeFile($fic_helper)) {
			Minz_Log::warning('File not found: `' . $fic_helper . '`');
		}
	}

	/**
	 * Returns renderHelper() in a string
	 * @param string $helper the element to be treated
	 */
	public function helperToString(string $helper): string {
		ob_start();
		$this->renderHelper($helper);
		return ob_get_clean() ?: '';
	}

	/**
	 * Choose the current view layout.
	 * @param string|null $layout the layout name to use, null to use no layouts.
	 */
	public function _layout(?string $layout): void {
		if ($layout != null) {
			$this->layout_filename = self::LAYOUT_PATH_NAME . $layout . '.phtml';
		} else {
			$this->layout_filename = '';
		}
	}

	/**
	 * Choose if we want to use the layout or not.
	 * @deprecated Please use the `_layout` function instead.
	 * @param bool $use true if we want to use the layout, false else
	 */
	public function _useLayout(bool $use): void {
		Minz_Log::warning('Minz_View::_useLayout is deprecated, it will be removed in a future version. Please use Minz_View::_layout instead.');
		if ($use) {
			$this->_layout(self::LAYOUT_DEFAULT);
		} else {
			$this->_layout(null);
		}
	}

	/**
	 * Title management
	 */
	public static function title(): string {
		return self::$title;
	}
	public static function headTitle(): string {
		return '<title>' . self::$title . '</title>' . "\n";
	}
	public static function _title(string $title): void {
		self::$title = $title;
	}
	public static function prependTitle(string $title): void {
		self::$title = $title . self::$title;
	}
	public static function appendTitle(string $title): void {
		self::$title = self::$title . $title;
	}

	/**
	 * Style sheet management
	 */
	public static function headStyle(): string {
		$styles = '';
		foreach (self::$styles as $style) {
			$styles .= '<link rel="stylesheet" ' .
				($style['media'] === 'all' ? '' : 'media="' . $style['media'] . '" ') .
				'href="' . $style['url'] . '" />';
			$styles .= "\n";
		}

		return $styles;
	}

	/**
	 * Prepends a <link> element referencing stylesheet.
	 * @param bool $cond Conditional comment for IE, now deprecated and ignored @deprecated
	 */
	public static function prependStyle(string $url, string $media = 'all', bool $cond = false): void {
		if ($url === '') {
			return;
		}
		array_unshift(self::$styles, [
			'url' => $url,
			'media' => $media,
		]);
	}

	/**
	 * Append a `<link>` element referencing stylesheet.
	 * @param string $url
	 * @param string $media
	 * @param bool $cond Conditional comment for IE, now deprecated and ignored @deprecated
	 */
	public static function appendStyle(string $url, string $media = 'all', bool $cond = false): void {
		if ($url === '') {
			return;
		}
		self::$styles[] = [
			'url' => $url,
			'media' => $media,
		];
	}

	/**
	 * @param string|array{'dark'?:string,'light'?:string,'default'?:string} $themeColors
	 */
	public static function appendThemeColors($themeColors): void {
		self::$themeColors = $themeColors;
	}

	/**
	 * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color
	 */
	public static function metaThemeColor(): string {
		$meta = '';
		if (is_array(self::$themeColors)) {
			if (!empty(self::$themeColors['light'])) {
				$meta .= '<meta name="theme-color" media="(prefers-color-scheme: light)" content="' . htmlspecialchars(self::$themeColors['light']) . '" />';
			}
			if (!empty(self::$themeColors['dark'])) {
				$meta .= '<meta name="theme-color" media="(prefers-color-scheme: dark)" content="' . htmlspecialchars(self::$themeColors['dark']) . '" />';
			}
			if (!empty(self::$themeColors['default'])) {
				$meta .= '<meta name="theme-color" content="' . htmlspecialchars(self::$themeColors['default']) . '" />';
			}
		} elseif (is_string(self::$themeColors)) {
			$meta .= '<meta name="theme-color" content="' . htmlspecialchars(self::$themeColors) . '" />';
		}
		return $meta;
	}

	/**
	 * JS script management
	 */
	public static function headScript(): string {
		$scripts = '';
		foreach (self::$scripts as $script) {
			$scripts .= '<script src="' . $script['url'] . '"';
			if (!empty($script['id'])) {
				$scripts .= ' id="' . $script['id'] . '"';
			}
			if ($script['defer']) {
				$scripts .= ' defer="defer"';
			}
			if ($script['async']) {
				$scripts .= ' async="async"';
			}
			$scripts .= '></script>';
			$scripts .= "\n";
		}

		return $scripts;
	}
	/**
	 * Prepend a `<script>` element.
	 * @param string $url
	 * @param bool $cond Conditional comment for IE, now deprecated and ignored @deprecated
	 * @param bool $defer Use `defer` flag
	 * @param bool $async Use `async` flag
	 * @param string $id Add a script `id` attribute
	 */
	public static function prependScript(string $url, bool $cond = false, bool $defer = true, bool $async = true, string $id = ''): void {
		if ($url === '') {
			return;
		}
		array_unshift(self::$scripts, [
			'url' => $url,
			'defer' => $defer,
			'async' => $async,
			'id' => $id,
		]);
	}

	/**
	 * Append a `<script>` element.
	 * @param string $url
	 * @param bool $cond Conditional comment for IE, now deprecated and ignored @deprecated
	 * @param bool $defer Use `defer` flag
	 * @param bool $async Use `async` flag
	 * @param string $id Add a script `id` attribute
	 */
	public static function appendScript(string $url, bool $cond = false, bool $defer = true, bool $async = true, string $id = ''): void {
		if ($url === '') {
			return;
		}
		self::$scripts[] = [
			'url' => $url,
			'defer' => $defer,
			'async' => $async,
			'id' => $id,
		];
	}

	/**
	 * Management of parameters added to the view
	 * @param mixed $value
	 */
	public static function _param(string $key, $value): void {
		self::$params[$key] = $value;
	}

	public function attributeParams(): void {
		foreach (Minz_View::$params as $key => $value) {
			// @phpstan-ignore property.dynamicName
			$this->$key = $value;
		}
	}
}