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`).
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ActionController.php'
<?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 { }
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ActionException.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Configuration.php'
<?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;
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ConfigurationException.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ConfigurationNamespaceException.php'
<?php
declare(strict_types=1);
class Minz_ConfigurationNamespaceException extends Minz_ConfigurationException {
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ConfigurationParamException.php'
<?php
declare(strict_types=1);
class Minz_ConfigurationParamException extends Minz_ConfigurationException {
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ConfigurationSetterInterface.php'
<?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;
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ControllerNotActionControllerException.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ControllerNotExistException.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/CurrentPagePaginationException.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Dispatcher.php'
<?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;
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Error.php'
<?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;
}
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Exception.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Extension.php'
<?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}&t={$type}&{$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;
}
}
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ExtensionException.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ExtensionManager.php'
<?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);
}
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/FileNotExistException.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/FrontController.php'
<?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;
}
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Helper.php'
<?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;
}
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Log.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Mailer.php'
<?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;
}
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Migrator.php'
<?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;
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Model.php'
<?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 {
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ModelArray.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/ModelPdo.php'
<?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;
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/PDOConnectionException.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Paginator.php'
<?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;
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Pdo.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/PdoMysql.php'
<?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
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/PdoPgsql.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/PdoSqlite.php'
<?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
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/PermissionDeniedException.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Request.php'
<?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');
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Session.php'
<?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] ?? '';
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Translate.php'
<?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);
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/Url.php'
<?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 & 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 & 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 = '&';
} 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]);
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/User.php'
<?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);
}
}
wget 'https://lists2.roe3.org/FreshRSS/lib/Minz/View.php'
<?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;
}
}
}