<?php
declare(strict_types = 1);

/**
 * Gestion des utilisateurs.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class User
{
	/**
	 * Longueur des colonnes des informations de profil.
	 *
	 * @param array
	 */
	CONST COLUMNS_LENGTH_MAX =
	[
		'custom_1' => 128,
		'custom_2' => 128,
		'custom_3' => 128,
		'custom_4' => 128,
		'custom_5' => 128,
		'description' => 65535,
		'email' => 128,
		'key' => 128,
		'firstname' => 64,
		'location' => 64,
		'login' => 24,
		'name' => 64,
		'nickname' => 24,

		// Ne doit pas dépasser 72.
		// https://www.php.net/manual/fr/function.password-hash.php
		'password' => 64,

		'website' => 128
	];

	/**
	 * Regexp pour le nom d'utilisateur.
	 *
	 * @param string
	 */
	CONST LOGIN_PATTERN = '[-a-z0-9@_.]{1,24}';



	/**
	 * Change le groupe de plusieurs utilisateurs.
	 *
	 * @param array|int $users_id
	 *   Liste des utilisateurs.
	 * @param int $group_id
	 *   Identifiant du nouveau groupe.
	 *
	 * @return int
	 *   Retourne le nombre d'utilisateurs affectés,
	 *   ou -1 si une erreur est survenue.
	 */
	public static function changeGroup($users_id, int $group_id): int
	{
		if ($group_id < 3 || !$users_id = self::_getSecureUsersId($users_id))
		{
			return 0;
		}

		// On vérifie que le groupe existe.
		if (!DB::execute('SELECT COUNT(*) FROM {groups} WHERE group_id = ?', $group_id))
		{
			return -1;
		}
		if (DB::fetchVal() < 1)
		{
			return 0;
		}

		// Mise à jour des utilisateurs.
		$sql = 'UPDATE {users} SET group_id = ? WHERE user_id IN (' . DB::inInt($users_id) . ')';
		if (!DB::execute($sql, $group_id))
		{
			return -1;
		}

		return DB::rowCount();
	}

	/**
	 * Change l'état de plusieurs utilisateurs.
	 *
	 * @param array|int $users_id
	 *   Liste des utilisateurs.
	 * @param int $status
	 *   État : 0 ou 1.
	 *
	 * @return int
	 *   Retourne le nombre d'utilisateurs affectés,
	 *   ou -1 si une erreur est survenue.
	 */
	public static function changeStatus($users_id, int $status): int
	{
		if (!in_array($status, [0, 1]) || !$users_id = self::_getSecureUsersId($users_id))
		{
			return 0;
		}

		// Début de la transaction.
		if (!DB::beginTransaction())
		{
			return -1;
		}

		// On ne retient que les utilisateurs qui doivent changer d'état.
		$sql = "SELECT user_id,
					   user_email,
					   user_status
				  FROM {users}
				 WHERE user_id IN (" . DB::inInt($users_id) . ")
				   AND user_status != '$status'";
		if (!DB::execute($sql))
		{
			return DB::rollback(-1);
		}
		if (!$users_infos = DB::fetchAll('user_id'))
		{
			return DB::rollback(0);
		}

		// Changement de l'état des utilisateurs.
		$sql_users_id = DB::inInt(array_keys($users_infos));
		$sql = "UPDATE {users} SET user_status = '$status' WHERE user_id IN ($sql_users_id)";
		if (!DB::execute($sql))
		{
			return DB::rollback(-1);
		}

		// Pour les favoris, on écarte les utilisateurs que l'on désactive
		// et qui sont actuellement en attente, ou inversement.
		if ($status === 1)
		{
			$users_favorites = array_keys($users_infos);
		}
		else
		{
			$users_favorites = [];
			foreach ($users_infos as $user_id => &$i)
			{
				if (($status === 0 && $i['user_status'] == -1)
				 || ($status === -1 && $i['user_status'] == 0))
				{
					continue;
				}
				$users_favorites[] = $user_id;
			}
		}

		// Mise à jour du nombre de favoris des fichiers et catégories.
		if (!Item::updateFavoritesCount($users_favorites, $status === 1 ? 1 : 0))
		{
			return DB::rollback(-1);
		}

		if (CONF_DEV_MODE && Maintenance::dbStats() !== 0)
		{
			trigger_error('Gallery stats error.', E_USER_WARNING);
			return DB::rollback(-1);
		}

		// Envoi d'un courriel aux utilisateurs en
		// attente de validation qui ont été activés.
		$new_users_email = [];
		if ($status === 1)
		{
			foreach ($users_infos as &$i)
			{
				if ($i['user_status'] == '-1')
				{
					$new_users_email[] = $i['user_email'];
				}
			}
		}
		if ($new_users_email)
		{
			$message = __('Un administrateur a validé votre inscription à la galerie'
				. ' située à l\'adresse suivante : %s.') . "\n\n";
			$message .= __('Vous pouvez désormais vous connecter avec les informations'
				. ' de connexion que vous avez fournies lors de votre inscription.');
			$message = sprintf($message, GALLERY_HOST . App::getURLGallery());
			$mail = new Mail();
			$mail->messages[] =
			[
				'subject' => '[' . Config::$params['gallery_title'] . '] '
					. __('Validation de votre inscription'),
				'message' => $message,
				'bcc' => implode(', ', $new_users_email)
			];
			$mail->send();
		}

		// Exécution de la transaction.
		if (!DB::commitTransaction())
		{
			return DB::rollback(-1);
		}

		return count($users_infos);
	}

	/**
	 * Vérifie le format de l'adresse de courriel.
	 *
	 * @param string $email
	 *   Chaîne à contrôler.
	 *
	 * @return mixed
	 *   Retourne TRUE si la vérification est OK,
	 *   ou sinon un message explicitant l'erreur.
	 */
	public static function checkEmailFormat(string $email)
	{
		if (!preg_match('`^' . Utility::emailRegexp() . '$`i', $email))
		{
			return __('Format de l\'adresse de courriel incorrect.');
		}

		return TRUE;
	}

	/**
	 * Vérifie qu'une adresse de courriel n'est pas déjà utilisée.
	 *
	 * @param string $email
	 *   Chaîne à contrôler.
	 *
	 * @return mixed
	 *   Retourne TRUE si la vérification est OK,
	 *   ou sinon un message explicitant l'erreur.
	 */
	public static function checkEmailUnique(string $email)
	{
		$sql = 'SELECT COUNT(*) FROM {users} WHERE LOWER(user_email) = LOWER(?)';
		if (!DB::execute($sql, $email) || DB::fetchVal())
		{
			return __('Un utilisateur possède déjà cette adresse de courriel.');
		}

		return TRUE;
	}

	/**
	 * Vérifie le format de l'identifiant de connexion.
	 *
	 * @param string $login
	 *
	 * @return mixed
	 *   Retourne TRUE si la vérification est OK,
	 *   ou sinon un message explicitant l'erreur.
	 */
	public static function checkLoginFormat(string $login)
	{
		if (!preg_match('`^' . self::LOGIN_PATTERN . '$`i', $login))
		{
			return __('Le nom d\'utilisateur ne doit comporter'
				. ' aucun espace, caractère spécial ou accentué.');
		}

		return TRUE;
	}

	/**
	 * Vérifie qu'un identifiant de connexion n'est pas déjà utilisé.
	 *
	 * @param string $login
	 *   Nom d'utilisateur à vérifier.
	 * @param int $exclude_id
	 *   Identifiant de l'utilisateur à exclure de la vérification.
	 *
	 * @return mixed
	 *   Retourne TRUE si la vérification est OK,
	 *   ou sinon un message explicitant l'erreur.
	 */
	public static function checkLoginUnique(string $login, int $exclude_id)
	{
		if (DB::$PDO)
		{
			$params = [$login];
			$sql_where = '';
			if ($exclude_id)
			{
				$sql_where = ' AND user_id != ?';
				$params[] = $exclude_id;
			}
			$sql = "SELECT COUNT(*)
					  FROM {users}
					 WHERE LOWER(user_login) = LOWER(?)
					       $sql_where";
			if (!DB::execute($sql, $params) || DB::fetchVal() != 0)
			{
				return __('Ce nom d\'utilisateur existe déjà.');
			}
		}

		return TRUE;
	}

	/**
	 * Vérifie qu'un pseudonyme n'est pas déjà utilisé.
	 *
	 * @param string $nickname
	 *   Pseudonyme à vérifier.
	 * @param int $exclude_id
	 *   Identifiant de l'utilisateur à exclure de la vérification.
	 *
	 * @return mixed
	 *   Retourne TRUE si la vérification est OK,
	 *   ou sinon un message explicitant l'erreur.
	 */
	public static function checkNicknameUnique(string $nickname, int $exclude_id)
	{
		if (DB::$PDO)
		{
			$params = ['nickname' => $nickname];
			$sql_where = '';
			if ($exclude_id)
			{
				$sql_where = ' AND user_id != :id';
				$params['id'] = $exclude_id;
			}
			$sql = "SELECT COUNT(*)
					  FROM {users}
					 WHERE (LOWER(user_login) = LOWER(:nickname)
					     OR LOWER(user_nickname) = LOWER(:nickname))
					       $sql_where";
			if (!DB::execute($sql, $params) || DB::fetchVal() != 0)
			{
				return __('Ce pseudonyme existe déjà.');
			}
		}

		return TRUE;
	}

	/**
	 * Vérifie le format du mot de passe.
	 *
	 * @param string $password
	 *
	 * @return mixed
	 *   Retourne TRUE si la vérification est OK,
	 *   ou sinon un message explicitant l'erreur.
	 */
	public static function checkPasswordFormat(string $password)
	{
		$minlength = self::getPasswordMinLength();
		if (strlen($password) < $minlength)
		{
			return sprintf(
				__('Le mot de passe doit contenir au moins %s caractères.'), $minlength
			);
		}

		return TRUE;
	}

	/**
	 * Vérifie le format de l'adresse du site Web.
	 *
	 * @param string $website
	 *   Chaîne à contrôler.
	 *
	 * @return mixed
	 *   Retourne TRUE si la vérification est OK,
	 *   ou sinon un message explicitant l'erreur.
	 */
	public static function checkWebsiteFormat(string $website)
	{
		$website = preg_replace('`^' . Utility::URLRegexp('protocol') . '`i', '', $website);
		if (!preg_match('`^' . Utility::URLRegexp('url', FALSE) . '$`i', $website))
		{
			return __('Format de l\'adresse du site Web incorrect.');
		}

		return TRUE;
	}

	/**
	 * Création d'un utilisateur.
	 *
	 * @param array $profile_update
	 *   Informations de profil.
	 * @param array $profile_params
	 *   Paramètres de profil.
	 *
	 * @return mixed
	 *   int   : Identifiant de l'utilisateur créé.
	 *   array : Tableau détaillant l'erreur utilisateur.
	 *   FALSE : Si une erreur s'est produite.
	 */
	public static function create(array $profile_update, array $profile_params)
	{
		// Paramètres de profil indispensables.
		foreach ($profile_params as $param => &$i)
		{
			if (in_array($param, ['group_id', 'login', 'password', 'status']))
			{
				$i['activated'] = 1;
				$i['required'] = 1;
			}
		}

		// Vérification des informations de profil fournies.
		$r = self::_checkProfile($profile_params, $profile_update);
		if (isset($r['error']))
		{
			return $r;
		}
		$params = &$r;

		// Paramètres SQL.
		$params['user_password'] = Security::passwordHash($params['user_password']);
		$params['user_lang'] = $params['user_lang'] ?? Config::$params['lang_default'];
		$params['user_tz'] = $params['user_tz'] ?? Config::$params['tz_default'];
		$params['user_crtip'] = $_SERVER['REMOTE_ADDR'];
		$values = str_repeat('?, ', count($params));
		$columns = implode(', ', array_keys($params));

		// Enregistrement de l'utilisateur.
		$sql = "INSERT INTO {users} ($columns, user_crtdt) VALUES ($values NOW())";
		$seq = ['table' => '{users}', 'column' => 'user_id'];
		if (!DB::execute($sql, array_values($params), $seq))
		{
			return FALSE;
		}

		return DB::lastInsertId();
	}

	/**
	 * Supprime des utilisateurs.
	 *
	 * @param array|int $users_id
	 *
	 * @return int
	 *   Nombre d'utilisateurs supprimés,
	 *   ou -1 si une erreur est survenue.
	 */
	public static function delete($users_id): int
	{
		if (!$users_id = self::_getSecureUsersId($users_id))
		{
			return 0;
		}

		// On démarre une transaction.
		if (!DB::beginTransaction())
		{
			return -1;
		}

		// Mise à jour du nombre de favoris des fichiers et catégories.
		if (!Item::updateFavoritesCount($users_id, 0))
		{
			return DB::rollback(-1);
		}

		// Exécution des requêtes.
		$sql_users_id = DB::inInt($users_id);
		$sql =
		[
			// Mise à jour des tables liées aux utilisateurs.
			"UPDATE {categories} SET user_id = 2 WHERE user_id IN ($sql_users_id)",

			"UPDATE {items} SET user_id = 2 WHERE user_id IN ($sql_users_id)",

			"UPDATE {votes} SET user_id = 2 WHERE user_id IN ($sql_users_id)",

			"UPDATE {comments}
				SET com_author = (SELECT CASE
										 WHEN user_nickname IS NULL
									     THEN user_login ELSE user_nickname
									      END
									FROM {users}
								   WHERE user_id = {comments}.user_id),
					user_id = 2
			  WHERE user_id IN ($sql_users_id)",

			"UPDATE {items_pending} SET user_id = 2 WHERE user_id IN ($sql_users_id)",

			"UPDATE {users_logs} SET user_id = 2 WHERE user_id IN ($sql_users_id)",

			// Suppression des utilisateurs.
			"DELETE FROM {users} WHERE user_id IN ($sql_users_id)"
		];
		foreach ($sql as &$q)
		{
			if (!DB::execute($q))
			{
				return DB::rollback(-1);
			}
		}

		$users_deleted = DB::rowCount();

		if (CONF_DEV_MODE && Maintenance::dbStats() !== 0)
		{
			trigger_error('Gallery stats error.', E_USER_WARNING);
			return DB::rollback(-1);
		}

		// On valide la transaction.
		if (!DB::commitTransaction())
		{
			return DB::rollback(-1);
		}

		// Suppression de l'avatar des utilisateurs.
		foreach ($users_id as &$id)
		{
			// Avatar.
			$avatar_file = Avatar::getFilepath((int) $id);
			if (file_exists($avatar_file))
			{
				File::unlink($avatar_file);
			}

			// Vignette de l'avatar.
			$avatar_thumb = Avatar::getFilepath((int) $id, TRUE);
			if (file_exists($avatar_thumb))
			{
				File::unlink($avatar_thumb);
			}
		}

		return $users_deleted;
	}

	/**
	 * Édition du profil d'un utilisateur.
	 *
	 * @param int $user_id
	 *   Identifiant de l'utilisateur.
	 * @param array $profile_update
	 *   Informations de profil à mettre à jour.
	 * @param array $profile_params
	 *   Paramètres de profil.
	 *
	 * @return mixed
	 *   TRUE : Édition réussie.
	 *   FALSE : Une erreur s'est produite.
	 *   NULL  : Aucun changement effectué.
	 *   array : Tableau détaillant l'erreur utilisateur.
	 */
	public static function edit(int $user_id, array $profile_update, array $profile_params = [])
	{
		// Seul le super-administrateur peut modifier
		// le profil du super-administrateur.
		if ($user_id == 1 && Auth::$id != 1)
		{
			return;
		}

		unset($profile_update['status']);
		unset($profile_params['status']);

		// Récupération des informations de l'utilisateur.
		if (!DB::execute('SELECT * FROM {users} WHERE user_id = ? AND user_id != 2', $user_id))
		{
			return FALSE;
		}
		if (!isset(($user_infos = DB::fetchRow())['user_id']))
		{
			return;
		}

		// Vérification des informations de profil fournies.
		$r = self::_checkProfile($profile_params, $profile_update, $user_infos);
		if (isset($r['error']))
		{
			return $r;
		}
		$params = &$r;
		if (!$params)
		{
			return;
		}

		// Nouveau mot de passe.
		if (isset($params['user_password']))
		{
			$params['user_password'] = Security::passwordHash($params['user_password']);
		}

		// Paramètres SQL.
		$columns = [];
		foreach ($params as $p => &$v)
		{
			$columns[] = $p . ' = ?';
		}
		$columns = implode(', ', $columns);
		$params = array_values($params);
		$params[] = $user_infos['user_id'];

		// Exécution de la requête.
		if (!DB::execute("UPDATE {users} SET $columns WHERE user_id = ?", $params))
		{
			return FALSE;
		}

		return TRUE;
	}

	/**
	 * Retourne le nom d'utilisateur ou le pseudonyme
	 * d'un utilisateur si ce dernier est renseigné et
	 * que l'information de profil "Pseudonyme" est activée.
	 *
	 * @param string $login
	 *   Nom d'utilisateur.
	 * @param mixed $nickname
	 *   Pseudonyme.
	 *
	 * @return string
	 */
	public static function getNickname(string $login, $nickname): string
	{
		static $config;
		if ($config === NULL)
		{
			$config = Config::$params['users_profile_params']['nickname']['activated'];
		}

		if ($config && $nickname)
		{
			return $nickname;
		}

		return $login;
	}

	/**
	 * Retourne la longueur minimum d'un mot de passe.
	 *
	 * @return int
	 */
	public static function getPasswordMinLength(): int
	{
		$minlength = (int) (Config::$params['users_password_minlength'] ?? 6);
		if ($minlength < 6)
		{
			$minlength = 6;
		}

		return $minlength;
	}

	/**
	 * Retourne les informations de profil d'un utilisateur pour édition.
	 *
	 * @param array $i
	 *   Informations brutes de l'utilisateur.
	 * @param array $post
	 *   Données POST.
	 *
	 * @return array
	 */
	public static function getProfileEdit(array &$i, array &$post): array
	{
		// Genre.
		if (isset($post['gender']))
		{
			$current_male = $post['gender'] == 'M';
			$current_female = $post['gender'] == 'F';
			$current_unknown = $post['gender'] == '?';
		}
		else if ($i['user_gender'] !== NULL)
		{
			$current_male = $i['user_gender'] == 'M';
			$current_female = $i['user_gender'] == 'F';
			$current_unknown = $i['user_gender'] == '?';
		}

		return
 		[
			// Informations de connexion.
			'login' => $post['login'] ?? $i['user_login'],
			'password' => $post['password'] ?? '',
			'password_confirm' => $post['password_confirm'] ?? '',
			'password_current' => $post['password_current'] ?? '',

			// Informations personnelles.
			'birthdate_day' => $post['birthdate_day']
				?? ($i['user_birthdate'] ? date('j', strtotime($i['user_birthdate'])) : ''),
			'birthdate_month' => $post['birthdate_month']
				?? ($i['user_birthdate'] ? date('n', strtotime($i['user_birthdate'])) : ''),
			'birthdate_year' => $post['birthdate_year']
				?? ($i['user_birthdate'] ? date('Y', strtotime($i['user_birthdate'])) : ''),
			'description' => $post['description'] ?? $i['user_description'],
			'firstname' => $post['firstname'] ?? $i['user_firstname'],
			'gender' =>
			[
				[
					'value' => '?',
					'text' => '?',
					'current' => $current_unknown ?? FALSE
				],
				[
					'value' => 'M',
					'text' => L10N::getTextUserGender('M'),
					'current' => $current_male ?? FALSE
				],
				[
					'value' => 'F',
					'text' => L10N::getTextUserGender('F'),
					'current' => $current_female ?? FALSE
				]
			],
			'email' => $post['email'] ?? $i['user_email'],
			'location' => $post['location'] ?? $i['user_location'],
			'name' => $post['name'] ?? $i['user_name'],
			'nickname' => $post['nickname'] ?? $i['user_nickname'],
			'custom_1' => $post['custom_1'] ?? $i['user_custom_1'],
			'custom_2' => $post['custom_2'] ?? $i['user_custom_2'],
			'custom_3' => $post['custom_3'] ?? $i['user_custom_3'],
			'custom_4' => $post['custom_4'] ?? $i['user_custom_4'],
			'custom_5' => $post['custom_5'] ?? $i['user_custom_5'],
			'website' => $post['website'] ?? $i['user_website'],

			// Notifications.
			'notification' =>
			[
				(bool) ($post['notification'][0] ?? $i['user_alert'][0]),
				(bool) ($post['notification'][1] ?? $i['user_alert'][1]),
				(bool) ($post['notification'][2] ?? $i['user_alert'][2]),
				(bool) ($post['notification'][3] ?? $i['user_alert'][3]),
				(bool) ($post['notification'][4] ?? $i['user_alert'][4]),
				(bool) ($post['notification'][5] ?? $i['user_alert'][5])
			]
		];
	}

	/**
	 * Retourne les paramètres de profil.
	 *
	 * @return array
	 */
	public static function getProfileParams(): array
	{
		$params = Config::$params['users_profile_params'];
		$params['key'] = ['activated' => 0, 'required' => 0];
		$params['password']['minlength'] = self::getPasswordMinLength();

		foreach ($params as $name => &$i)
		{
			if ($name == 'description')
			{
				$i['maxlength'] = (int) Config::$params['users_description_maxlength'];
			}
			else if (isset(self::COLUMNS_LENGTH_MAX[$name]))
			{
				$i['maxlength'] = self::COLUMNS_LENGTH_MAX[$name];
			}
		}

		return $params;
	}

	/**
	 * Détermine si l'utilisateur connecté a la permission d'effectuer
	 * certains changements (état, groupe et suppression) sur le compte
	 * d'un utilisateur. Un utilisateur connecté ne peut pas effectuer de
	 * changement sur son propre compte ni sur celui du super-administrateur.
	 *
	 * @param int $user_id
	 *   Identifiant de l'utilisateur.
	 *
	 * @return bool
	 */
	public static function isPermChange(int $user_id): bool
	{
		return Auth::$id != $user_id && $user_id > 2;
	}

	/**
	 * Procédure d'oubli de mot de passe.
	 *
	 * @param array $post
	 * @param array $forgot
	 *
	 * @return bool
	 *   Retourn FALSE en cas d'erreur.
	 */
	public static function passwordForgot(array &$post, array &$forgot): bool
	{
		if ($post && (!isset($post['f_email']) || $post['f_email'] !== ''))
		{
			$post = [];
		}

		$forgot = ['step' => $step = $post['step'] ?? 'forgot'];

		if (!empty($post))
		{
			switch ($step)
			{
				case 'forgot' :
					$login = $post['login'] ?? '';
					$email = $post['email'] ?? '';
					if (self::_passwordCode($login, $email))
					{
						$forgot['step'] = 'reset';
						$forgot['login'] = $login;
					}
					else
					{
						return FALSE;
					}
					break;

				case 'reset' :
					$login = $forgot['login'] = $post['login'] ?? '';
					$code = $post['code'] ?? '';
					$password = $post['password'] ?? '';
					$password_confirm = $post['password_confirm'] ?? '';
					if (is_string($r = self::checkPasswordFormat($password)))
					{
						$forgot['warning'] = $r;
						break;
					}
					if ($password !== $password_confirm)
					{
						$forgot['warning'] = L10N::getText('password_confirm_error');
						break;
					}
					switch (self::_passwordReset($code, $login, $password))
					{
						case -1 :
							return FALSE;
						case 0 :
							$forgot['warning'] = __('Code non valide.');
							break;
						case 1 :
							$forgot['step'] = 'confirm';
							break;
					}
					break;
			}
		}

		switch ($forgot['step'] ?? $step)
		{
			case 'confirm' :
				$forgot['text'] = __('Votre mot de passe a été réinitialisé.');
				break;

			case 'forgot' :
				$forgot['text'] = __('Pour réinitialiser votre mot de passe, veuillez'
					. ' entrer les informations suivantes. Un code de vérification'
					. ' vous sera envoyé par courriel.');
				break;

			case 'reset' :
				$forgot['text'] = __('Entrez le code que vous avez reçu par courriel,'
					. ' puis définissez un nouveau mot de passe.');
				break;

			default :
				$forgot['text'] = '';
		}

		return TRUE;
	}



	/**
	 * Vérifie les données fournies ($profile_update)
	 * pour modifier les informations de profil ($profile_params)
	 * d'un utilisateur ($user_infos).
	 *
	 * @param array $profile_params
	 * @param array $profile_update
	 * @param array $user_infos
	 *
	 * @return array
	 *   Retourne un tableau de paramètres à utiliser
	 *   dans une requête de mise à jour de la base de données,
	 *   ou en cas d'erreur un tableau détaillant l'erreur.
	 */
	private static function _checkProfile(array &$profile_params,
	array &$profile_update, array &$user_infos = []): array
	{
		// Retourne la valeur d'un paramètre en faisant quelques vérifications.
		$get_value = function(string $p) use (&$profile_update, &$profile_params)
		{
			$value = (!isset($profile_update[$p])
				|| Utility::isEmpty((string) $profile_update[$p]))
				? NULL
				: Utility::trimAll((string) $profile_update[$p]);

			if ($p == 'birthdate')
			{
				if ($value === NULL)
				{
					$day = sprintf("%'.02d",
						(string) (int) ($profile_update['birthdate_day'] ?? 0));
					$month = sprintf("%'.02d",
						(string) (int) ($profile_update['birthdate_month'] ?? 0));
					$year = sprintf("%'.04d",
						(string) (int) ($profile_update['birthdate_year'] ?? 0));
					if ($day <= 31 && $month <= 12 && $year <= date('Y'))
					{
						$value = "$year-$month-$day";
					}
				}
				$regexp = '`^(?:(?!0000)(\d{4})-(?!00)(\d{2})-(?!00)(\d{2})|0000-00-00)$`';
				$value = (preg_match($regexp, $value, $m) && $value != '0000-00-00')
					? $value
					: NULL;
				if (!$value
				|| (count($m) === 4 && checkdate((int) $m[2], (int) $m[3], (int) $m[1])))
				{
					$profile_update['birthdate'] = $value;
				}
			}
			if ($p == 'description' && $value !== NULL)
			{
				$value = substr($value, 0,
					(int) Config::$params['users_description_maxlength']);
			}
			if ($p == 'gender' && !in_array($value, ['?', 'M', 'F']))
			{
				$value = NULL;
			}
			if ($p == 'group_id' && (int) $value < 3)
			{
				$value = NULL;
			}
			if ($p == 'lang' && !in_array($value,
			array_keys(Config::$params['lang_params']['langs'])))
			{
				$value = NULL;
			}
			if ($p == 'status' && !in_array($value, ['-2', '-1', '0', '1']))
			{
				$value = NULL;
			}
			if ($p == 'tz' && !in_array($value, timezone_identifiers_list()))
			{
				$value = NULL;
			}

			return ($profile_params[$p]['required'] && $value === NULL)
				? FALSE
				: $value;
		};

		// On traite chaque paramètre.
		$params = [];
		foreach ($profile_params as $p => &$i)
		{
			// Le paramètre est-il activé ?
			// Les informations hors profil proprement dit ne peuvent pas être désactivées.
			if (!$i['activated'] && !in_array($p,
			['group_id', 'lang', 'login', 'password', 'status', 'tz']))
			{
				continue;
			}

			// L'utilisateur connecté ne peut pas modifier son propre état
			// et son propre groupe, ni modifier ceux du super-administrateur.
			if (in_array($p, ['status', 'group_id']) &&
			(!$user_infos || ($user_infos['user_id'] > 1
			&& $user_infos['user_id'] != Auth::$id)) === FALSE)
			{
				continue;
			}

			// Récupération de la valeur à mettre à jour.
			if (($value = $get_value($p)) === FALSE)
			{
				return
				[
					'error' => 'required',
					'message' => L10N::getText('required_fields'),
					'param' => $p
				];
			}
			if (!array_key_exists($p, $profile_update))
			{
				continue;
			}

			// Certaines informations ne peuvent pas être à NULL.
			if ($value === NULL
			&& in_array($p, ['gender', 'group_id', 'lang', 'login', 'password', 'status', 'tz']))
			{
				continue;
			}

			// Mise à jour de la valeur si nécessaire.
			$col = ($p == 'group_id') ? $p : "user_$p";
			if (!$user_infos || $value != $user_infos[$col])
			{
				// Vérification de la longueur.
				if (isset(self::COLUMNS_LENGTH_MAX[$p])
				&& mb_strlen((string) $value) > self::COLUMNS_LENGTH_MAX[$p])
				{
					return
					[
						'error' => 'length',
						'message' => sprintf(
							__('La longueur ne doit pas dépassée %s caractères.'),
							self::COLUMNS_LENGTH_MAX[$p]
						),
						'param' => $p
					];
				}

				// Vérification du format.
				if ($value !== NULL && in_array($p, ['email', 'login', 'password', 'website']))
				{
					$method = 'check' . ucfirst($p) . 'Format';
					if (($r = self::$method($value)) !== TRUE)
					{
						return
						[
							'error' => 'format',
							'message' => $r,
							'param' => $p
						];
					}
				}

				// Vérification de l'unicité.
				if ($value !== NULL && in_array($p, ['email', 'login', 'nickname']))
				{
					$method = 'check' . ucfirst($p) . 'Unique';
					$user_id = (int) ($user_infos['user_id'] ?? 0);
					if (($r = self::$method($value, $user_id)) !== TRUE)
					{
						return
						[
							'error' => 'unique',
							'message' => $r,
							'param' => $p
						];
					}
				}

				$params[$col] = $value;
			}
		}

		// Options : notifications.
		if (!empty($profile_update['notification']))
		{
			for ($value = '', $n = 0; $n < 6; $n++)
			{
				$value .= empty($profile_update['notification'][$n]) ? '0' : '1';
			}
			if (!$user_infos || $value != $user_infos['user_alert'])
			{
				$params['user_alert'] = $value;
			}
		}

		return $params;
	}

	/**
	 * Retourne un tableau sécurisé des identifiants d'utilisateur
	 * en vue d'une action sur le compte de ces utilisateurs.
	 *
	 * Il est notamment impossible d'effectuer une action
	 * sur le super-administrateur ou l'utilisateur connecté.
	 *
	 * @param mixed $users_id
	 *
	 * @return array
	 */
	private static function _getSecureUsersId($users_id): array
	{
		$users_id = is_array($users_id) ? $users_id : [$users_id];

		foreach ($users_id as $key => &$id)
		{
			if ((!is_string($id) && !is_int($id)) || !self::isPermChange((int) $id))
			{
				unset($users_id[$key]);
			}
		}

		return array_values($users_id);
	}

	/**
	 * Envoi un code de confirmation pour réinitialiser un mot de passe.
	 *
	 * @param string $login
	 *   Identifiant de connexion.
	 * @param string $email
	 *   Adresse de courriel.
	 *
	 * @return bool
	 *   Retourn FALSE en cas d'erreur.
	 */
	private static function _passwordCode(string &$login, string &$email): bool
	{
		$email = trim($email);
		$login = trim($login);

		// Vérification du format des informations fournies.
		if (strlen($login) > self::COLUMNS_LENGTH_MAX['login']
		 || strlen($email) > self::COLUMNS_LENGTH_MAX['email']
		 || !preg_match('`^' . Utility::emailRegexp() . '$`i', $email)
		 || !preg_match('`^' . self::LOGIN_PATTERN . '$`i', $login))
		{
			$email = $login = '';
			return TRUE;
		}

		// Vérification de l'adresse IP par liste noire.
		if (App::blacklists('forgot_rejected', '', '', '',
		['login' => $login, 'email' => $email]) !== TRUE)
		{
			return TRUE;
		}

		// Prévention contre les attaques par force brute.
		if (Security::bruteForce('forgot\_rejected'))
		{
			return TRUE;
		}

		// On génère un code que l'on enverra par courriel à l'utilisateur.
		$code = Security::key(12);
		$rkey = Security::passwordHash(implode('|', [$code, Auth::getCookieSessionToken()]));

		// Vérification des informations fournies
		// et récupération de l'identifiant de l'utilisateur.
		$sql = 'SELECT user_id,
					   group_admin
				  FROM {users}
			 LEFT JOIN {groups} USING (group_id)
				 WHERE LOWER(user_login) = LOWER(?)
				   AND LOWER(user_email) = LOWER(?)
				   AND user_status = "1"';
		if (!DB::execute($sql, [$login, $email]))
		{
			return FALSE;
		}
		$user_infos = DB::fetchRow();
		if (!$user_infos || ($user_infos['group_admin'] == 1 && !Security::checkAuthAdminIP()))
		{
			App::logActivity('forgot_rejected', '', ['login' => $login, 'email' => $email]);
			return TRUE;
		}

		// Mise à jour de la base de données.
		$sql = 'UPDATE {users}
				   SET user_rkey = ?,
					   user_rdate = DATE_ADD(NOW(), INTERVAL 2 HOUR)
				 WHERE user_id = ?';
		if (!DB::execute($sql, [$rkey, $user_infos['user_id']]) || DB::rowCount() != 1)
		{
			return FALSE;
		}

		// Log d'activité.
		App::logActivity('forgot', '', ['login' => $login, 'email' => $email]);

		// Envoi du courriel.
		$mail = new Mail();
		$message = __('Une demande de réinitialisation de votre mot de passe'
			. ' a été effectuée depuis la galerie suivante :') . "\n\n";
		$message .= '%s' . "\n";
		$message .= '%s' . "\n\n";
		$message .= __('Voici le code de vérification qui vous permettra'
			. ' de définir un nouveau mot de passe :') . "\n\n";
		$message .= '%s' . "\n\n";
		$message .= __('Notez que ce code n\'est valide que pendant 2 heures.');
		$mail->messages[] =
		[
			'to' => $email,
			'subject' => '[' . Config::$params['gallery_title'] . '] '
				. __('Réinitialisation du mot de passe'),
			'message' => sprintf(
				$message,
				Config::$params['gallery_title'],
				GALLERY_HOST . App::getURL(),
				$code
			)
		];
		return $mail->send();
	}

	/**
	 * Réinitialise un mot de passe avec un code de confirmation.
	 *
	 * @param string $code
	 *   Code envoyé par courriel.
	 * @param string $login
	 *   Identifiant de connexion.
	 * @param string $password
	 *   Nouveau mot de passe.
	 *
	 * @return mixed
	 *   Retourne 1 en cas de succès,
	 *   0 si les informations sont incorrectes
	 *   ou -1 en cas d'erreur.
	 */
	private static function _passwordReset(string $code, string $login, string $password): int
	{
		$code = trim($code);
		$login = trim($login);
		$password = trim($password);

		// Vérification du format des informations fournies.
		if (!preg_match('`^[a-z0-9]{10,50}$`i', $code)
		|| strlen($login) > self::COLUMNS_LENGTH_MAX['login']
		|| !preg_match('`^' . self::LOGIN_PATTERN . '$`i', $login)
		|| strlen($password) > self::COLUMNS_LENGTH_MAX['password']
		|| strlen($password) < self::getPasswordMinLength())
		{
			return 0;
		}

		// Vérification de l'adresse IP par liste noire.
		if (App::blacklists('new_password_rejected', '', '', '', ['login' => $login]) !== TRUE)
		{
			return 0;
		}

		// Prévention contre les attaques par force brute.
		if (Security::bruteForce('new\_password\_rejected'))
		{
			return 0;
		}

		// Récupération des informations utiles.
		$sql = 'SELECT user_id,
					   user_rkey,
					   group_admin
				  FROM {users}
			 LEFT JOIN {groups} USING (group_id)
				 WHERE user_status = "1"
				   AND user_rkey IS NOT NULL
				   AND user_rdate >= NOW()
				   AND LOWER(user_login) = LOWER(?)';
		if (!DB::execute($sql, $login))
		{
			return -1;
		}
		$user_infos = DB::fetchRow();

		// Log d'activité en cas de rejet.
		$rejected = function() use ($login)
		{
			App::logActivity('new_password_rejected', '', ['login' => $login]);
			return 0;
		};

		// Vérification du code fourni.
		$code = implode('|', [$code, Auth::getCookieSessionToken()]);
		$rkey = (string) ($user_infos['user_rkey'] ?? '');
		if (!$rkey)
		{
			Security::passwordVerifySimulate();
			return $rejected();
		}
		if (!Security::passwordVerify($code, $rkey))
		{
			return $rejected();
		}

		// Vérification de l'adresse IP par liste blanche.
		if ($user_infos['group_admin'] == 1 && !Security::checkAuthAdminIP())
		{
			return $rejected();
		}

		// Enregistrement du nouveau mot de passe.
		$sql = 'UPDATE {users}
				   SET user_password = ?,
					   user_rkey = NULL,
					   user_rdate = NULL
				 WHERE user_id = ?';
		if (!DB::execute($sql, [Security::passwordHash($password), $user_infos['user_id']]))
		{
			return -1;
		}

		// Log d'activité.
		App::logActivity('new_password', '', ['login' => $login]);

		return 1;
	}
}
?>