<?php
declare(strict_types = 1);

require_once(__DIR__ . '/Gallery.class.php');

/**
 * Opérations concernant les catégories.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class GalleryCategory extends Gallery
{
	/**
	 * Détails du nombre de fichiers de la section.
	 *
	 * @var array
	 */
	public static $count = [];

	/**
	 * Informations en base de données de la catégorie courante.
	 *
	 * @var array
	 */
	public static $infos = [];



	/**
	 * Informations en base de données des catégories.
	 *
	 * @var array
	 */
	private static $_categories = [];

	/**
	 * Nombre de pages de la catégorie courante.
	 *
	 * @var int
	 */
	private static $_pagesCount = 0;



	/**
	 * Vérification du mot de passe entré par l'utilisateur
	 * pour l'accès à une partie protégée de la galerie.
	 *
	 * @param string $type
	 * @param int $object_id
	 * @param string $password
	 * @param bool $remember
	 *
	 * @return string
	 */
	public static function checkPassword(string $type, int $object_id,
	string $password, bool $remember): string
	{
		// Récupération des informations de l'objet demandé.
		if ($type == 'item')
		{
			$sql = 'SELECT i.item_id AS id,
						   i.item_url AS url,
						   p.cat_id AS p_cat_id,
						   p.password_id,
						   p.password_hash,
						   "item" AS type,
						   %s
					  FROM {items} AS i
				 LEFT JOIN {categories} AS cat USING (cat_id)
			     LEFT JOIN {passwords} AS p USING (password_id)
					 WHERE %s
					   AND i.item_id = :item_id';
			DB::params(['item_id' => $object_id]);
		}
		else
		{
			$sql = 'SELECT cat.cat_id AS id,
						   cat.cat_url AS url,
						   p.cat_id AS p_cat_id,
						   p.password_id,
						   p.password_hash,
						   CASE WHEN cat_filemtime IS NULL
								THEN "category"
								ELSE "album"
								 END AS type,
						   %s
					  FROM {categories} AS cat
			     LEFT JOIN {passwords} AS p USING (password_id)
					 WHERE %s
					   AND cat.cat_id = :cat_id';
			DB::params(['cat_id' => $object_id]);
		}
		$sql = sprintf($sql, SQL::catPassword('select'), SQL::catPerms());
		if (!DB::execute($sql))
		{
			return 'db_error';
		}
		$i = DB::fetchRow();
		if (!array_key_exists('password_auth', $i))
		{
			return 'not_found';
		}

		// Si la catégorie n'est pas protégée par un mot de passe,
		// ou si l'utilisateur a déjà entré le bon mot de passe,
		// on redirige vers la page de l'objet.
		if ($i['password_auth'])
		{
			return 'redirect:' . $i['type'] . '/' . $i['id'] . '-' . $i['url'];
		}

		// On arrête là si l'utilisateur n'a pas envoyé de mot de passe.
		if (empty($password))
		{
			return 'no_password';
		}

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

		// Vérification du mot de passe entré par l'utilisateur.
		$callback = function(string $new_password) use (&$i): void
		{
			DB::execute(
				'UPDATE {passwords} SET password_hash = ? WHERE password_id = ?',
				[$new_password, $i['password_id']]
			);
		};
		if (!Security::passwordVerify($password, $i['password_hash'], $callback))
		{
			// Log d'activité.
			App::logActivity('password_rejected');

			return 'password_error';
		}

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

		// On génère un nouveau jeton de session.
		$session = Auth::getNewSessionToken();
		if (!$session['id'])
		{
			return 'db_error';
		}

		// On associe l'identifiant de session avec la catégorie.
		$sql = 'INSERT INTO {sessions_categories} (session_id, cat_id) VALUES (?, ?)';
		if (!DB::execute($sql, [$session['id'], $i['p_cat_id']]))
		{
			return 'db_error';
		}

		// Log d'activité.
		App::logActivity('password');

		// Exécution de la transaction.
		if (!DB::commitTransaction())
		{
			return 'db_error';
		}

		// Mise à jour du cookie de session.
		Auth::$session->add('session_token', $session['token']);
		Auth::$session->add(
			'session_expire', $remember ? (string) CONF_COOKIE_SESSION_EXPIRE : '0'
		);

		// Redirection vers la page demandée.
		return 'redirect:' . $i['type'] . '/' . $i['id'] . '-' . $i['url'];
	}

	/**
	 * Récupération des catégories de la page courante.
	 *
	 * @return int
	 */
	public static function getCategories(): int
	{
		Template::set('objects_count', 0);
		Template::set('objects_count_formated', '0');
		self::_pagination(1);

		// Nombre de catégories par page.
		$cats_per_page = (int) Config::$params['thumbs_cat_nb_per_page'];
		$cats_per_page = $cats_per_page < 1 ? 1000000 : $cats_per_page;
		Template::set('cats_per_page', $cats_per_page);

		// Clause WHERE.
		$sql_where = SQL::catPerms() . '
			AND cat.cat_id > 1
			AND cat_status = "1"
			AND thumb_id > -1
			AND cat_a_images + cat_a_videos > 0
			AND cat.parent_id = :parent_id';
		$params = ['parent_id' => $_GET['category_id']];

		// Clause LIMIT.
		$sql_limit = $cats_per_page * ($_GET['page'] - 1) . ",$cats_per_page";

		// Clause ORDER BY.
		$sql_order_by = SQL::catOrderBy(
			self::$infos['cat_orderby'] ?? Config::$params['categories_sql_order_by'],
			self::$infos
		);

		// Récupération des catégories.
		$sql = "SELECT cat.*,
					   " . SQL::catPassword('select') . ",
					   CASE WHEN cat_filemtime IS NULL
							THEN 'category'
							ELSE 'album'
							 END AS cat_type,
					   i.item_id,
					   i.item_type,
					   i.item_path,
					   i.item_width,
					   i.item_height,
					   i.item_orientation,
					   i.item_adddt,
					   u.user_login,
					   u.user_status
				  FROM {categories} AS cat
			 LEFT JOIN {users} AS u
					ON cat.user_id = u.user_id
			 LEFT JOIN {items} AS i
					ON cat.thumb_id = i.item_id
				 WHERE $sql_where
			  ORDER BY $sql_order_by
			     LIMIT $sql_limit";
		DB::params($params);
		if (!DB::execute($sql))
		{
			return -1;
		}
		if (!self::$_categories = DB::fetchAll('cat_id'))
		{
			if ($_GET['page'] > 1)
			{
				App::redirect($_GET['q_pageless']);
				return 1;
			}
			else
			{
				return (int) ($_GET['category_id'] == 1);
			}
		}

		// Réduction des stats.
		if (!self::changeCatStats(self::$_categories))
		{
			return -1;
		}

		// Nombre de catégories de la section courante.
		$sql = "SELECT COUNT(*) FROM {categories} AS cat WHERE $sql_where";
		if (!DB::execute($sql, $params))
		{
			return -1;
		}
		$count = (int) DB::fetchVal();
		Template::set('objects_count', $count);
		Template::set('objects_count_formated', L10N::formatNumber($count));

		// Nombre de pages.
		self::$_pagesCount = ceil($count / $cats_per_page);
		self::_pagination(self::$_pagesCount);

		// Formatage des informations.
		foreach (self::$_categories as &$i)
		{
			$formated_infos[$i['cat_id']] = self::getFormatedInfos($i, 'thumb');
		}
		Template::set('categories', $formated_infos);

		return 1;
	}

	/**
	 * Formate les informations d'une catégorie.
	 *
	 * @param array $i
	 *   Informations brutes de la catégorie.
	 * @param string $origin
	 *   Origine : 'category' ou 'thumb'.
	 *
	 * @return array
	 */
	public static function getFormatedInfos(array &$i, string $origin = 'category'): array
	{
		$items = $i['cat_a_images'] + $i['cat_a_videos'];
		$link = $i['password_auth']
			? ($i['cat_id'] > 1
				? $i['cat_type'] . '/' . $i['cat_id'] . '-' . $i['cat_url']
				: $i['cat_type'] . '/' . $i['cat_id'] . '-' . __('galerie'))
			: 'password/' . $i['cat_type'] . '/' . $i['cat_id'];
		$filter_link = App::getURL($i['cat_type'] . '/' . $i['cat_id']);

		// Uploadable ?
		$uploadable = FALSE;
		if (!empty($i['parent_uploadable']) && $i['cat_uploadable']
		&& !isset($_GET['filter']) && $i['cat_type'] == 'album' && Config::$params['users']
		&& Auth::$groupPerms['upload']
		&& (!Auth::$groupPerms['upload_owner']
		 || (Auth::$groupPerms['upload_owner']
			&& $i['user_id'] == Auth::$infos['user_id'])))
		{
			$uploadable = TRUE;
		}

		// Statistiques.
		$stats_order = Config::$params['categories_stats_order'];
		if ($origin == 'category' && Config::$params['categories_stats']
		&& in_array($_GET['section'], ['album', 'category']))
		{
			$stats_params = Config::$params['categories_stats_params'];
		}
		$stats = [];
		foreach ($stats_order as &$name)
		{
			if (($origin == 'category' && isset($stats_params) && $stats_params[$name]['status'])
			|| $origin == 'thumb')
			{
				$stats[$name] = [];
				switch ($name)
				{
					case 'albums' :
						if ($i['cat_type'] == 'category' && ($origin == 'category'
						|| Config::$params['thumbs_cat_info_albums']))
						{
							$stats[$name] =
							[
								'link' => NULL,
								'short' => L10N::formatShortNumber((int) $i['cat_a_albums']),
								'text' => $i['cat_a_albums'] > 1
									? sprintf(__('%s albums'),
										L10N::formatNumber((int) $i['cat_a_albums']))
									: sprintf(__('%s album'),
										L10N::formatNumber((int) $i['cat_a_albums'])),
								'value' => (int) $i['cat_a_albums']
							];
						}
						else if ($origin == 'category')
						{
							unset($stats[$name]);
						}
						break;

					case 'comments' :
						if (Config::$params['comments'] && ($origin == 'category'
						|| Config::$params['thumbs_cat_info_comments']))
						{
							if (Config::$params['users'] && !Auth::$groupPerms['comments_read'])
							{
								$stats[$name] =
								[
									'link' => NULL,
									'link_comments' => NULL,
									'short' => L10N::formatShortNumber(0),
									'text' => sprintf(
										__('%s commentaire'), L10N::formatNumber(0)
									),
									'value' => 0
								];
							}
							else
							{
								$stats[$name] =
								[
									'link' => $i['cat_a_comments'] > 0
										? $filter_link . '/comments'
										: NULL,
									'link_comments' => $i['cat_a_comments'] > 0
										? (App::getURL($i['cat_id'] > 1
											? 'comments/' . $i['cat_type'] . '/' . $i['cat_id']
											: 'comments'))
										: NULL,
									'short' => L10N::formatShortNumber(
										(int) $i['cat_a_comments']
									),
									'text' => $i['cat_a_comments'] > 1
										? sprintf(__('%s commentaires'),
											L10N::formatNumber((int) $i['cat_a_comments']))
										: sprintf(__('%s commentaire'),
											L10N::formatNumber((int) $i['cat_a_comments'])),
									'value' => (int) $i['cat_a_comments']
								];
							}
						}
						else if ($origin == 'category')
						{
							unset($stats[$name]);
						}
						break;

					case 'favorites' :
						if (Config::$params['users'] && Config::$params['favorites']
						&& ($origin == 'category'
						|| Config::$params['thumbs_cat_info_favorites']))
						{
							$stats[$name] =
							[
								'link' => $i['cat_a_favorites'] > 0
									? $filter_link . '/favorites'
									: NULL,
								'short' => L10N::formatShortNumber((int) $i['cat_a_favorites']),
								'text' => $i['cat_a_favorites'] > 1
									? sprintf(__('%s favoris'),
										L10N::formatNumber((int) $i['cat_a_favorites']))
									: sprintf(__('%s favori'),
										L10N::formatNumber((int) $i['cat_a_favorites'])),
								'value' => (int) $i['cat_a_favorites']
							];
						}
						else if ($origin == 'category')
						{
							unset($stats[$name]);
						}
						break;

					case 'images' :
						if ($origin == 'category' || Config::$params['thumbs_cat_info_items'])
						{
							$stats[$name] =
							[
								'link' => $i['cat_a_images'] > 0
									? $filter_link . '/images'
									: NULL,
								'short' => L10N::formatShortNumber((int) $i['cat_a_images']),
								'text' => $i['cat_a_images'] > 1
									? sprintf(__('%s photos'),
										L10N::formatNumber((int) $i['cat_a_images']))
									: sprintf(__('%s photo'),
										L10N::formatNumber((int) $i['cat_a_images'])),
								'value' => (int) $i['cat_a_images']
							];
						}
						break;

					case 'items' :
						$stats[$name] =
						[
							'link' => $items > 0 ? $filter_link . '/items' : NULL,
							'short' => L10N::formatShortNumber((int) $items),
							'text' => $items > 1
								? sprintf(__('%s photos et vidéos'),
									L10N::formatNumber((int) $items))
								: sprintf(__('%s photo ou vidéo'),
									L10N::formatNumber((int) $items)),
							'value' => $items
						];
						break;

					case 'filesize' :
						if ($origin == 'category' || Config::$params['thumbs_cat_info_filesize'])
						{
							$stats[$name] =
							[
								'link' => NULL,
								'text' => L10N::formatFilesize($i['cat_a_size']),
								'value' => (int) $i['cat_a_size']
							];
						}
						break;

					case 'rating' :
						if (Config::$params['votes'] && ($origin == 'category'
						|| Config::$params['thumbs_cat_info_rating']))
						{
							$stats['rating'] =
							[
								'array' => Rating::formatArray($i['cat_a_rating'] ?? 0),
								'link' => NULL,
								'text' => L10N::formatRating($i['cat_a_rating'] ?? 0),
								'value' => (float) $i['cat_a_rating']
							];
						}
						else if ($origin == 'category')
						{
							unset($stats[$name]);
						}
						break;

					case 'recent_images' :
					case 'recent_items' :
					case 'recent_videos' :
						if (Config::$params['items_recent'])
						{
							switch (substr($name, 7))
							{
								case 'images' :
									$text = __('%s nouvelle photo');
									break;
								case 'items' :
									$text = __('%s nouvelle photo ou vidéo');
									break;
								case 'videos' :
									$text = __('%s nouvelle vidéo');
									break;
							}
							$stats[$name] =
							[
								'link' => NULL,
								'text' => sprintf($text, 0),
								'value' => 0
							];
						}
						else
						{
							unset($stats[$name]);
						}
						break;

					case 'videos' :
						if ($origin == 'category' || Config::$params['thumbs_cat_info_items'])
						{
							$stats[$name] =
							[
								'link' => $i['cat_a_videos'] > 0 ? $filter_link . '/videos' : 0,
								'short' => L10N::formatShortNumber((int) $i['cat_a_videos']),
								'text' => $i['cat_a_videos'] > 1
									? sprintf(__('%s vidéos'),
										L10N::formatNumber((int) $i['cat_a_videos']))
									: sprintf(__('%s vidéo'),
										L10N::formatNumber((int) $i['cat_a_videos'])),
								'value' => (int) $i['cat_a_videos']
							];
						}
						break;

					case 'views' :
						if ($origin == 'category' || Config::$params['thumbs_cat_info_views'])
						{
							$stats[$name] =
							[
								'link' => $i['cat_a_hits'] > 0 ? $filter_link . '/views' : NULL,
								'short' => L10N::formatShortNumber((int) $i['cat_a_hits']),
								'text' => $i['cat_a_hits'] > 1
									? sprintf(__('%s vues'),
										L10N::formatNumber((int) $i['cat_a_hits']))
									: sprintf(__('%s vue'),
										L10N::formatNumber((int) $i['cat_a_hits'])),
								'value' => (int) $i['cat_a_hits']
							];
						}
						break;

					case 'votes' :
						if (Config::$params['votes'] && ($origin == 'category'
						|| Config::$params['thumbs_cat_info_votes']))
						{
							$stats[$name] =
							[
								'link' => $i['cat_a_votes'] > 0 ? $filter_link . '/votes' : NULL,
								'short' => L10N::formatShortNumber((int) $i['cat_a_votes']),
								'text' => $i['cat_a_votes'] > 1
									? sprintf(__('%s votes'),
										L10N::formatNumber((int) $i['cat_a_votes']))
									: sprintf(__('%s vote'),
										L10N::formatNumber((int) $i['cat_a_votes'])),
								'value' => (int) $i['cat_a_votes']
							];
						}
						else if ($origin == 'category')
						{
							unset($stats[$name]);
						}
				}
			}
		}

		// Fichiers récents.
		$recent = ['value' => 0, 'link' => NULL];
		$cat_type_text = $i['cat_type'] == 'category' ? __('cette catégorie') : __('cet album');
		if (!$i['cat_a_videos'] && $i['cat_a_images'])
		{
			$recent['text'] = sprintf(
				__('Montrer les photos récentes de %s'),
				$cat_type_text
			);
			$recent['link'] = App::getURL($i['cat_type'] . '/' . $i['cat_id'] . '/recent-images');
		}
		if ($i['cat_a_videos'] && !$i['cat_a_images'])
		{
			$recent['text'] = sprintf(
				__('Montrer les vidéos récentes de %s'),
				$cat_type_text
			);
			$recent['link'] = App::getURL($i['cat_type'] . '/' . $i['cat_id']) . '/recent-videos';
		}
		if ($i['cat_a_videos'] && $i['cat_a_images'])
		{
			$recent['text'] = sprintf(
				__('Montrer les photos et vidéos récentes de %s'),
				$cat_type_text
			);
			$recent['link'] = App::getURL($i['cat_type'] . '/' . $i['cat_id'] . '/recent-items');
		}

		// Téléchargement dans une archive Zip.
		$download_album = FALSE;
		$download_favorites = FALSE;
		$download_link = '';
		if ($origin == 'category')
		{
			$key = Security::fileKeyHash([$i['cat_id']]);

			// Téléchargement de l'album.
			if ($download_album = ($i['cat_type'] == 'album')
			&& $i['cat_downloadable'] && $i['parent_downloadable']
			&& !isset($_GET['filter']) && Config::$params['albums_download']
			&& (!Config::$params['users']
			 || (Config::$params['users'] && Auth::$groupPerms['albums_download'])))
			{
				$download_link = CONF_GALLERY_PATH
					. "/download.php?key=$key&cat={$i['cat_id']}";
			}

			// Téléchargement des favoris.
			if ($download_favorites = ($origin == 'category')
			&& Config::$params['favorites_download']
			&& isset(Template::$data['category'])
			&& Template::$data['category']['user_favorites'])
			{
				$download_link = CONF_GALLERY_PATH
					. "/download.php?key=$key&cat={$i['cat_id']}&favorites";
			}
		}

		return
		[
			'id' => $i['cat_id'],
			'type' => $i['cat_type'],
			'link' => App::getURL($link),
			'admin' => Auth::$isAdmin && !isset($_GET['filter'])
				? App::getURLAdmin($i['cat_type']
					. ($i['cat_id'] > 1 ? '-edit' : '') . '/' . $i['cat_id'])
				: '',
			'objects_count' => $items,
			'path' => $i['cat_path'],
			'download_album' => $download_album ? 1 : 0,
			'download_favorites' => $download_favorites ? 1 : 0,
			'download_link' => $download_link,
			'upload' => $uploadable,
			'urlname' => $i['cat_url'],

			// Mot de passe.
			'link_logout' => App::getURL('logout/' . $i['cat_id']),
			'locked' => (bool) !$i['password_auth'],
			'unlocked' => $i['password_id'] && $i['password_auth'],

			// Informations.
			'description' => isset($_GET['filter']) || $_GET['page'] > 1
				? NULL
				: ($i['cat_id'] > 1
					? $i['cat_desc']
					: Config::$params['gallery_description']),
			'title' => ($origin == 'category' || Config::$params['thumbs_cat_info_title'])
				? ($i['cat_id'] > 1 ? $i['cat_name'] : Config::$params['gallery_title'])
				: '',

			// Statistiques.
			'recent' => $recent,
			'stats' => $stats,

			// Vignette.
			'thumb_id' => (int) $i['item_id'],
			'thumb_size' => function(int $max_width, int $max_height) use ($i): array
			{
				return Image::getResizedSize((int) $i['item_width'],
					(int) $i['item_height'], $max_width, $max_height);
			},
			'thumb_src' => function($size = '', $quality = '') use ($i): string
			{
				return htmlspecialchars(($i['thumb_id'] == -1 || !$i['password_auth'])
					? ''
					: App::getThumbSource('cat', $i, (string) $size, (string) $quality));
			},
			'thumb_type' => Item::isVideo($i['item_type']) ? 'video' : 'image'
		];
	}

	/**
	 * Récupération des informations de la catégorie courante.
	 *
	 * @param string $type
	 * @param int $cat_id
	 *
	 * @return int
	 */
	public static function getInfos(string $type, int $cat_id): int
	{
		if ($cat_id == 1 && $type != 'category')
		{
			App::redirect();
			return 1;
		}

		// Récupération des informations.
		$sql = "SELECT cat.*,
					   " . SQL::catPassword('select') . ",
					   CASE WHEN cat_filemtime IS NULL
						    THEN 'category' ELSE 'album'
							 END AS cat_type,
					   i.item_id,
					   i.item_type,
					   i.item_adddt,
					   i.item_path,
					   i.item_orientation
				  FROM {categories} AS cat
			 LEFT JOIN {items} AS i
					ON cat.thumb_id = i.item_id
				 WHERE " . SQL::catPerms() . "
				   AND cat.cat_id = :cat_id
				   AND cat_status = '1'
				   AND cat_filemtime IS ";
		$sql .= $type == 'category' ? 'NULL' : 'NOT NULL';
		DB::params(['cat_id' => $cat_id]);
		if (!DB::execute($sql))
		{
			return -1;
		}
		if (!isset((self::$infos = DB::fetchRow())['cat_id']))
		{
			return 0;
		}

		// L'utilisateur a-t-il entré le bon mot de passe ?
		if (!self::checkCookiePassword(self::$infos))
		{
			return 0;
		}

		// Réduction des statistiques.
		if (self::$infos['cat_type'] == 'category' && !self::changeCatStats(self::$infos))
		{
			return -1;
		}

		// Réglages.
		self::$infos = [self::$infos];
		Parents::settings(self::$infos);
		self::$infos = self::$infos[0];

		// Fil d'Ariane.
		Template::breadcrumb(self::$infos, TRUE);
		if ($_GET['category_id'] == 1)
		{
			unset(Template::$data['breadcrumb'][0]);
		}

		// Numéro de page pour la catégorie parente du fil d'Ariane.
		$cat_per_page = (int) Config::$params['thumbs_cat_nb_per_page'];
		if ($cat_per_page && $_GET['category_id'] > 1 && !isset($_GET['filter']))
		{
			foreach (Template::$data['breadcrumb'] as $k => &$i)
			{
				if ($i['id'] == self::$infos['parent_id'])
				{
					$parent = Template::$data['breadcrumb'][$k];
					if (self::$infos['parent_id'] == 1 || $parent['a_cat'] > $cat_per_page)
					{
						$sql_order_by = SQL::catOrderBy(
							$parent['order_by'] ?? Config::$params['categories_sql_order_by'],
							$parent
						);
						$sql = "SELECT cat_id
								  FROM {categories} AS cat
								 WHERE %s
								   AND parent_id = ?
								   AND cat_id > 1
								   AND cat_status = '1'
								   AND thumb_id > -1
							  ORDER BY $sql_order_by";
						$sql = sprintf($sql, SQL::catPerms());
						if (DB::execute($sql, $parent['id']))
						{
							$categories = DB::fetchCol('cat_id');
							$cat_position = array_search(self::$infos['cat_id'], $categories) + 1;
							$parent_page = ceil($cat_position / $cat_per_page);
							if ($parent_page > 1)
							{
								Template::$data['breadcrumb'][$k]['url']
									.= '/page/' . $parent_page;
							}
						}
					}
					break;
				}
			}
		}
		foreach (Template::$data['breadcrumb'] as &$i)
		{
			if (array_key_exists('a_cat', $i))
			{
				unset($i['a_cat']);
			}
			if (array_key_exists('order_by', $i))
			{
				unset($i['order_by']);
			}
		}

		// Permission d'édition pour les administrateurs
		// et le propriétaire de la catégorie.
		Template::set('category_edit', !isset($_GET['filter'])
			&& (Auth::$isAdmin || (Config::$params['users'] && Auth::$id != 2
			&& self::$infos['user_id'] == Auth::$id)));

		// Chemin de la catégorie pour les requêtes SQL.
		$cat_path = self::$infos['cat_id'] > 1
			? DB::likeEscape(self::$infos['cat_path']) . '/%'
			: '%';

		// L'utilisateur possède-t-il des favoris dans cette catégorie ?
		$in_favorites = 0;
		if (Config::$params['users'] && Config::$params['favorites'] && Auth::$connected
		&& !isset($_GET['filter']) && self::$infos['cat_a_favorites'] > 0)
		{
			$sql = 'SELECT 1
					  FROM {favorites} AS f
				 LEFT JOIN {items} AS i USING (item_id)
				 LEFT JOIN {categories} AS cat USING (cat_id)
					 WHERE ' . SQL::catPerms() . '
					   AND ' . SQL::catPassword() . '
					   AND item_status = "1"
					   AND f.user_id = :user_id
					   AND f.item_id = i.item_id
					   AND item_path LIKE :cat_path
					 LIMIT 1';
			DB::params(['user_id' => Auth::$id, 'cat_path' => $cat_path]);
			if (DB::execute($sql))
			{
				$in_favorites = (int) DB::fetchVal();
			}
		}

		// Est-on sur la page des favoris de l'utilisateur connecté ?
		$user_favorites = Config::$params['users'] && Config::$params['favorites']
			&& in_array($_GET['section'], ['album', 'category'])
			&& isset($_GET['filter']) && $_GET['filter'] == 'user-favorites'
			&& Auth::$connected && $_GET['filter_value'] == Auth::$id;

		// Message de suppression des favoris.
		$category_text = __('la galerie');
		if (self::$infos['cat_id'] > 1)
		{
			$category_text = sprintf(
				self::$infos['cat_type'] == 'album' ? __('l\'album %s') : __('la catégorie %s'),
				'"' . self::$infos['cat_name'] . '"'
			);
		}
		$user_favorites_delete_text = sprintf(
			__('Cette action retirera de vos favoris tous les fichiers de %s.'),
			$category_text
		);

		// Appareils photos et historique.
		foreach (['cameras', 'history'] as $p)
		{
			$$p = Config::$params['pages_params'][$p]['status']
				&& !isset($_GET['filter']) && self::$infos['cat_id'] > 1
				&& (self::$infos['cat_a_images'] + self::$infos['cat_a_videos']);
		}

		// La catégorie contient-elle des photos liées à un appareil photo ?
		if ($cameras)
		{
			$cameras = 0;
			if (self::$infos['cat_a_images'])
			{
				$sql = 'SELECT 1
						  FROM {cameras_models} AS cam_m
					 LEFT JOIN {cameras_brands} AS cam_b USING (camera_brand_id)
					 LEFT JOIN {cameras_items} AS cam_i USING (camera_model_id)
					 LEFT JOIN {items} AS i USING (item_id)
					 LEFT JOIN {categories} AS cat USING (cat_id)
						 WHERE ' . SQL::catPerms() . '
						   AND ' . SQL::catPassword() . '
						   AND item_status = "1"
						   AND item_path LIKE :cat_path
						 LIMIT 1';
				DB::params(['cat_path' => $cat_path]);
				if (DB::execute($sql))
				{
					$cameras = (int) DB::fetchVal();
				}
			}
		}

		Template::set('category',
		[
			'cameras' => $cameras,
			'history' => $history,
			'in_favorites' => $in_favorites,
			'user_favorites' => $user_favorites,
			'user_favorites_delete_text' => $user_favorites_delete_text
		]);

		// Formatage des informations.
		Template::set('category', self::getFormatedInfos(self::$infos, 'category'));

		// URL canonique.
		Template::set('canonical', GALLERY_HOST . App::getURL(self::$infos['cat_id'] > 1
			? self::$infos['cat_type'] . '/'
				. self::$infos['cat_id'] . '-' . self::$infos['cat_url']
			: ''));

		// Description.
		Template::$data['category']['description_edit']
			= Template::$data['category']['description'];

		// Description pour balise <meta>.
		$description = isset($_GET['filter']) ? NULL : (self::$infos['cat_id'] > 1
			? self::$infos['cat_desc']
			: Config::$params['gallery_description']);
		Template::set('meta_description', App::getMetaDescription((string) $description));

		// Tags de catégorie.
		if (Config::$params['tags'])
		{
			$link = substr($_GET['filter'] ?? '', 0, 3) == 'tag'
				? App::getURL('tags/' . $cat_id . '/' . $_GET['value_2'])
				: App::getURL($cat_id > 1 ? 'tags/' . $cat_id : 'tags');
			Template::set('category',
			[
				'tags' => self::getTags(self::$infos, (int) (Config::$params['tags_max'] ?? 15)),
				'tags_link' => $link
			]);
		}

		// Flux RSS.
		if (Config::$params['rss'])
		{
			if (!isset($_GET['filter']))
			{
				self::_rss('category', self::$infos);
			}
			else if (isset($_GET['filter'])
			&& $_GET['filter'] == 'tag' && $_GET['category_id'] == 1)
			{
				self::_rss('tag', self::$infos);
			}
		}

		return 1;
	}

	/**
	 * Récupère le nombre de fichiers récents de la catégorie
	 * et des sous-catégories.
	 *
	 * @return void
	 */
	public static function getRecentItemsCount(): void
	{
		// S'il ne faut pas mettre en évidence les fichiers récents
		// ou s'il n'y a aucun nouveau fichier, on arrête là.
		if (!Config::$params['items_recent'] || empty(self::$infos['cat_lastpubdt'])
		|| strtotime(self::$infos['cat_lastpubdt']) < Item::getRecentTimestamp())
		{
			return;
		}

		// Requête SQL pour déterminer le nombre de fichiers récents d'une catégorie.
		$get_recent_items = function(&$i): array
		{
			$sql = 'SELECT COUNT(*) AS count,
						   item_type
					  FROM {items}
				 LEFT JOIN {categories} AS cat USING (cat_id)
					 WHERE ' . SQL::catPerms() . '
					   AND ' . SQL::catPassword() . '
					   AND item_path LIKE :cat_path
					   AND item_pubdt > :item_pubdt
					   AND item_status = "1"
				  GROUP BY item_type';
			DB::params([
				'cat_path' => $i['cat_id'] > 1 ? DB::likeEscape($i['cat_path']) . '/%' : '%',
				'item_pubdt' => date('Y-m-d H:i:s', Item::getRecentTimestamp())
			]);
			if (!DB::execute($sql))
			{
				return [];
			}

			return Item::countByTypes(DB::fetchAll('item_type'));
		};

		// Formatage des données.
		$tpl = function(&$t, &$r)
		{
			$filter_link = App::getURL($t['type'] . '/' . $t['id']);

			// Images.
			if (isset($t['stats']['recent_images']))
			{
				$formated = L10N::formatNumber((int) $r['images']);
				$t['stats']['recent_images']['formated'] = $formated;
				$t['stats']['recent_images']['link'] = $r['images'] > 0
					? $filter_link . '/recent-images'
					: NULL;
				$t['stats']['recent_images']['text'] = $r['images'] > 1
					? sprintf(__('%s nouvelles photos'), $formated)
					: sprintf(__('%s nouvelle photo'), $formated);
				$t['stats']['recent_images']['value'] = $r['images'];
			}

			// Images + vidéos.
			if (isset($t['stats']['recent_items']))
			{
				$formated = L10N::formatNumber((int) $r['items']);
				$t['stats']['recent_items']['formated'] = $formated;
				$t['stats']['recent_items']['link'] = $r['items'] > 0
					? $filter_link . '/recent-items'
					: NULL;
				$t['stats']['recent_items']['text'] = $r['items'] > 1
					? sprintf(__('%s nouvelles photos et vidéos'), $formated)
					: sprintf(__('%s nouvelle photo ou vidéo'), $formated);
				$t['stats']['recent_items']['value'] = $r['items'];
			}

			// Vidéos.
			if (isset($t['stats']['recent_videos']))
			{
				$formated = L10N::formatNumber((int) $r['videos']);
				$t['stats']['recent_videos']['formated'] = $formated;
				$t['stats']['recent_videos']['link'] = $r['videos'] > 0
					? $filter_link . '/recent-videos'
					: NULL;
				$t['stats']['recent_videos']['text'] = $r['videos'] > 1
					? sprintf(__('%s nouvelles vidéos'), $formated)
					: sprintf(__('%s nouvelle vidéo'), $formated);
				$t['stats']['recent_videos']['value'] = $r['videos'];
			}

			// Fichiers récents.
			$t['recent']['value'] = $t['stats']['recent_items']['value'] ?? NULL;
			$t['recent']['formated'] = $t['stats']['recent_items']['formated'] ?? NULL;
		};

		// On effectue une requête SQL afin de déterminer le
		// nombre total de nouveaux fichiers dans la catégorie.
		// Mais seulement s'il n'y a pas de sous-catégories ou s'il y a plus
		// d'une page. Dans le cas contraire, on additionnera plus loin
		// le nombre de nouveaux fichiers de toutes les sous-catégories.
		$add_subcats = TRUE;
		if (Config::$params['categories_stats']
		&& (!self::$_categories || self::$_pagesCount > 1)
		&& $r = $get_recent_items(self::$infos))
		{
			$add_subcats = FALSE;
			$tpl(Template::$data['category'], $r);
		}

		// S'il n'y a aucune sous-catégorie, on arrête là.
		if (!self::$_categories)
		{
			return;
		}

		// On détermine le nombre de nouveaux fichiers pour chaque sous-catégorie.
		$recent = ['images' => 0, 'items' => 0, 'videos' => 0];
		foreach (self::$_categories as &$i)
		{
			// Si la catégorie ne contient pas de fichiers récents,
			// inutile d'aller plus loin.
			if (strtotime($i['cat_lastpubdt']) < Item::getRecentTimestamp())
			{
				continue;
			}

			if ($r = $get_recent_items($i))
			{
				if ($add_subcats)
				{
					$recent['images'] += $r['images'];
					$recent['items'] += $r['items'];
					$recent['videos'] += $r['videos'];
				}
				$tpl(Template::$data['categories'][$i['cat_id']], $r);
			}
			else
			{
				return;
			}
		}

		// Nombre de nouveaux fichiers pour la catégorie courante.
		if (Config::$params['categories_stats'] && $add_subcats)
		{
			$tpl(Template::$data['category'], $recent);
		}
	}

	/**
	 * Retourne les tags de la catégorie.
	 *
	 * @param array $cat_infos
	 *   Informations de la catégorie.
	 * @param int $limit
	 *   Nombre de tags à récupérer.
	 *
	 * @return array
	 */
	public static function getTags(array &$cat_infos, int $limit = 15): array
	{
		if ($limit === 0)
		{
			return [];
		}
		if ($limit < 0)
		{
			$limit = 0;
		}

		// Tags combinés : paramètres.
		$combined = FALSE;
		if ($_GET['section'] == 'tags' && count($_GET['params']) == 3)
		{
			$combined = TRUE;
			$combined_tags = $_GET['params'][2];
		}
		if (substr($_GET['filter'] ?? '', 0, 3) == 'tag')
		{
			$combined = TRUE;
			$combined_tags = $_GET['filter_value'];
		}

		// Tags combinés : SQL.
		$sql_combined = "";
		if ($combined)
		{
			$tags = explode(',', $combined_tags);
			$tags_count = count($tags);
			$tags = DB::inInt($tags);
			$sql = "SELECT item_id
					  FROM {tags_items}
					 WHERE tag_id IN ($tags)
				  GROUP BY item_id
					HAVING COUNT(DISTINCT tag_id) = $tags_count";
			$sql_combined = " AND i.item_id IN ($sql)
							  AND t.tag_id NOT IN ($tags)";
		}

		// Récupération des informations.
		$sql = "SELECT t.*,
					   COUNT(*) AS count
				  FROM {tags} AS t
			 LEFT JOIN {tags_items} AS ti
					ON t.tag_id = ti.tag_id
			 LEFT JOIN {items} AS i
					ON ti.item_id = i.item_id
			 LEFT JOIN {categories} AS cat
					ON i.cat_id = cat.cat_id
				 WHERE " . SQL::catPerms() . "
				   AND " . SQL::catPassword() . "
				   AND i.item_path LIKE :cat_path
				   AND item_status = '1'
				   $sql_combined
			  GROUP BY t.tag_id";
		DB::params(['cat_path' => $cat_infos['cat_id'] > 1
			? DB::likeEscape($cat_infos['cat_path']) . '/%'
			: '%']);
		if (!DB::execute($sql) || !$tags = DB::fetchAll('tag_name'))
		{
			return [];
		}

		// Pour améliorer les performances, on effectue le tri et la
		// limitation du nombre de tags en PHP plutôt qu'en SQL.
		uasort($tags, function($a, $b) { return $b['count'] - $a['count']; });
		if ($limit)
		{
			$tags = array_slice($tags, 0, $limit);
		}

		// Paramètres pour déterminer le poids des tags.
		$big = current($tags)['count'];
		$small = end($tags)['count'];
		$diff = $big - $small;
		$increment = $diff === 0 ? 1 : $diff / 9;

		// On tri les tags par ordre alphabétique.
		uksort($tags, 'Utility::alphaSort');

		// Textes.
		if (!$cat_infos['cat_a_videos'] && $cat_infos['cat_a_images'])
		{
			$items = __('%s photos');
			$item = __('%s photo');
		}
		if ($cat_infos['cat_a_videos'] && !$cat_infos['cat_a_images'])
		{
			$items = __('%s vidéo');
			$item = __('%s vidéo');
		}
		if ($cat_infos['cat_a_videos'] && $cat_infos['cat_a_images'])
		{
			$items = __('%s photos ou vidéos');
			$item = __('%s photo ou vidéo');
		}

		// Liens.
		$get_link = function(array &$i) use ($cat_infos, $combined): string
		{
			$category = $cat_infos['cat_type'] . '/' . $cat_infos['cat_id'];
			$filter = 'tag';
			$tag = $i['tag_id'] . '-' . $i['tag_url'];

			if ($combined)
			{
				$filter = 'tags';
				$tag = ($_GET['value_2'] ?? $_GET['section_2']) . ',' . $i['tag_id'];
			}

			return App::getURL($category . '/' . $filter . '/' . $tag);
		};

		// Formatage des informations.
		$tags_formated = [];
		foreach (array_values($tags) as &$i)
		{
			$tags_formated[] =
			[
				'count' => $i['count'],
				'link' => $get_link($i),
				'name' => $i['tag_name'],
				'text' => $i['count'] > 1
					? sprintf($items, $i['count'])
					: sprintf($item, $i['count']),
				'weight' => intval(($i['count'] - $small) / $increment) + 1
			];
		}

		return $tags_formated;
	}

	/**
	 * Page de mot de passe.
	 *
	 * @return int
	 */
	public static function password(): int
	{
		$r = self::checkPassword($_GET['section_2'], (int) $_GET['value_1'],
			$_POST['password'] ?? '', isset($_POST['remember']));

		if ($r == 'db_error')
		{
			return -1;
		}
		if ($r == 'not_found')
		{
			return 0;
		}

		if ($r == 'brute_force')
		{
			Report::warning(__('Attaque par force brute détectée.'));
		}
		if ($r == 'password_error')
		{
			Report::warning(__('Mot de passe incorrect.'));
		}
		if (substr($r, 0, 9) == 'redirect:')
		{
			App::redirect(substr($r, 9));
		}

		return 1;
	}
}
?>