<?php
declare(strict_types = 1);

/**
 * Classe mère pour toute la galerie.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class Gallery
{
	/**
	 * Liste des catégories.
	 *
	 * @var array
	 */
	public static $categories = [];

	/**
	 * Paramètres de recherche.
	 *
	 * @var object
	 */
	public static $search;



	/**
	 * Modifie les statistiques de catégories en fonction
	 * des permissions d'accès de l'utilisateur.
	 *
	 * @param array $cat_infos
	 *
	 * @return bool
	 *   Retourne FALSE en cas d'erreur.
	 */
	public static function changeCatStats(array &$cat_infos): bool
	{
		static $reduce_cats;
		if ($reduce_cats === [])
		{
			return TRUE;
		}

		// Catégories parentes.
		$sep = Parents::SEPARATOR;
		if ($multiple = !isset($cat_infos['cat_id']))
		{
			$key = key($cat_infos);
			$cat_id = $cat_infos[$key]['cat_id'];
			$categories = &$cat_infos;
		}
		else
		{
			$cat_id = $cat_infos['cat_id'];
			$categories = [$cat_id => &$cat_infos];
		}

		if (!$reduce_cats)
		{
			$cat_perms = SQL::catPerms();

			// On "inverse" les permissions d'accès aux catégories.
			if (strlen($cat_perms) < 9)
			{
				$cat_perms = '1=2';
			}
			else if (Auth::$groupPerms['perm_list'] == 'black')
			{
				$cat_perms = preg_replace('` (AND|OR) cat_parents.*$`', ')', $cat_perms);
				$cat_perms = str_replace('NOT ', '', $cat_perms);
			}
			else
			{
				$cat_perms = str_replace(
					['IN', 'LIKE', 'OR'], ['NOT IN', 'NOT LIKE', 'AND'], $cat_perms
				);
			}
			$cat_password = preg_replace(
				'`\(cat\.password_id IS NULL OR (.+) = \d\)`s',
				'(cat.password_id IS NOT NULL AND $1 IS NULL)',
				SQL::catPassword()
			);

			// On corrige le nombre d'albums pour
			// ne pas compter les albums vides activés.
			$cat_thumb = '(thumb_id = -1 AND cat_filemtime IS NOT NULL)';

			// On récupère les infos utiles de toutes les catégories protégées
			// non déverrouillées par les permissions de l'utilisateur.
			$sql = 'SELECT cat_id,
						   parent_id,
						   cat_parents,
						   cat_status,
						   cat_filemtime,
						   cat_a_albums,
						   cat_a_comments,
						   cat_a_favorites,
						   cat_a_hits,
						   cat_a_images,
						   cat_a_rating,
						   cat_a_size,
						   cat_a_subalbs,
						   cat_a_subcats,
						   cat_a_videos,
						   cat_a_votes
					  FROM {categories} AS cat
					 WHERE cat.cat_id > 1
					   AND cat_status = "1"
					   AND (%s OR %s OR %s)
				  ORDER BY LENGTH(cat_parents) ASC';
			$sql = sprintf($sql, $cat_perms, $cat_password, $cat_thumb);
			if (!DB::execute($sql))
			{
				return FALSE;
			}
			$reduce_cats = DB::fetchAll('cat_id');
		}
		if (!$reduce_cats)
		{
			return TRUE;
		}

		// Réduction des stats.
		$temp_parents = [];
		foreach ($categories as $id => &$i)
		{
			foreach ($reduce_cats as $cat_id => &$s)
			{
				// On ignore les catégories qui ne sont pas des
				// descendants de la catégorie courante.
				if (isset($cat_infos['cat_parents']) && $cat_infos['cat_id'] > 1
				&& strpos($s['cat_parents'] . $cat_id,
				$cat_infos['cat_parents'] . $cat_infos['cat_id'] . $sep, 0) === FALSE)
				{
					continue;
				}
				if ($id > 1 && $multiple && strpos($s['cat_parents'] . $cat_id,
				$i['cat_parents'] . $id . $sep, 0) === FALSE)
				{
					continue;
				}

				// Il ne faut pas réduire les stats pour une
				// sous-catégorie d'une catégorie qui a déjà été traitée.
				foreach ($temp_parents as &$p)
				{
					if (strpos($s['cat_parents'] . $s['cat_id'], $p . $sep, 0) !== FALSE)
					{
						continue 2;
					}
				}
				$temp_parents[] = $s['cat_parents'] . $s['cat_id'];

				// On réduit le nombre de fichiers.
				$i['cat_a_images'] -= $s['cat_a_images'];
				$i['cat_a_videos'] -= $s['cat_a_videos'];

				if (!isset($i['cat_a_albums']))
				{
					continue;
				}

				// Correction pour le nombre d'albums.
				$s['cat_a_albums'] = $s['cat_filemtime'] === NULL ? $s['cat_a_albums'] : 1;

				// On réduit le nombre d'albums.
				$i['cat_a_albums'] -= $s['cat_filemtime'] === NULL ? $s['cat_a_albums'] : 1;

				// On recalcule la note moyenne.
				if ($i['cat_a_votes'] - $s['cat_a_votes'] > 0)
				{
					$i['cat_a_rating'] =
						(($i['cat_a_rating'] * $i['cat_a_votes'])
						- ($s['cat_a_rating'] * $s['cat_a_votes']))
						/ ($i['cat_a_votes'] - $s['cat_a_votes']);
				}
				else
				{
					$i['cat_a_rating'] = 0;
				}

				// Réduction des autres stats.
				$i['cat_a_comments'] -= $s['cat_a_comments'];
				$i['cat_a_favorites'] -= $s['cat_a_favorites'];
				$i['cat_a_hits'] -= $s['cat_a_hits'];
				$i['cat_a_size'] -= $s['cat_a_size'];
				$i['cat_a_votes'] -= $s['cat_a_votes'];
			}
		}

		return TRUE;
	}

	/**
	 * Détermine si l'utilisateur possède un jeton de session lui
	 * permettant d'accèder à une catégorie. Si ce n'est pas le
	 * cas, on redirige vers la page de demande de mot de passe.
	 *
	 * @params array $i
	 *   Informations de la catégorie.
	 *
	 * @return bool
	 */
	public static function checkCookiePassword(array &$i): bool
	{
		// La catégorie ne possède aucun mot de passe.
		if ($i['password_auth'] == 1)
		{
			return TRUE;
		}

		// La catégorie est protégée par un mot de passe :
		// 1. L'utilisateur a entré le bon mot de passe.
		if ($i['password_auth'] == 2)
		{
			// Mise à jour de la date d'expiration de la session,
			// mais seulement si l'utilisateur n'est pas authentifié.
			if (!Auth::$connected)
			{
				$session_expire = (int) CONF_SESSION_EXPIRE;
				$sql = "UPDATE {sessions}
						   SET session_expire = DATE_ADD(NOW(), INTERVAL $session_expire SECOND)
						 WHERE session_token = ?";
				DB::execute($sql, Auth::getCookieSessionToken());
			}
			return TRUE;
		}

		// 2. Mauvais mot de passe : on redirige.
		if ($_GET['section'] == 'item')
		{
			App::redirect('password/item/' . $i['item_id']);
		}
		else
		{
			App::redirect('password/' . $i['cat_type'] . '/' . $i['cat_id']);
		}
		return FALSE;
	}

	/**
	 * Gestion des filtres.
	 *
	 * @return void
	 */
	public static function filters(): void
	{
		// Filtres.
		if (isset($_GET['filter']))
		{
			// Template : paramètres de chaque section.
			switch ($_GET['section'])
			{
				case 'album' :
				case 'category' :
				case 'item' :
					if (!Template::filter((int) $_GET['filter_value'], $_GET['q_filterless']))
					{
						self::notFound();
					}
					break;
			}
		}
	}

	/**
	 * Initialisation.
	 *
	 * @return void
	 */
	public static function start(): void
	{
		// On redirige vers le bon URL si l'URL demandé
		// ne correspond pas à l'option sur l'URL rewriting choisie.
		if (isset($_GET['q']) && isset($_SERVER['REQUEST_URI']))
		{
			$gallery_base_url = CONF_URL_REWRITE
				? App::getURL('')
				: substr(App::getURL('1'), 0, -1);
			if (preg_match('`/\?q=`', $_SERVER['REQUEST_URI'])
			 != preg_match('`/\?q=`', $gallery_base_url))
			{
				App::redirect($_GET['q'], 301);
			}
		}

		// Changement de page.
		if (isset($_POST['page'])
		&& preg_match('`^\d{1,12}$`', (string) $_POST['page']) && $_POST['page'] > 0)
		{
			$anchor = '';
			if (isset($_POST['anchor'])
			&& preg_match('`^[a-z0-9_-]{1,30}$`i', (string) $_POST['anchor']))
			{
				$anchor = '#' . $_POST['anchor'];
			}
			App::redirect($_GET['q'] . '/page/' . $_POST['page'] . $anchor);
		}

		// Connexion à la base de données.
		if (!DB::connect())
		{
			App::httpResponse(500);
			die('Unable to connect to database.');
		}

		// Récupération de la configuration.
		if (!Config::getDBParams())
		{
			App::httpResponse(500);
			die('No database.');
		}

		// Gestion des paramètres GET.
		self::request();

		// Initialisation du template.
		Template::init();

		// Gestion de membres.
		Template::set('users', (int) Config::$params['users']);

		// Authentification utilisateur par cookie.
		if (Auth::cookie() && (Config::$params['users'] || Auth::$isAdmin))
		{
			Template::set('user',
			[
				'admin' => Auth::$isAdmin,
				'auth' => TRUE,
				'avatar_source' => Avatar::getURL(Auth::$id, (bool) Auth::$infos['user_avatar']),
				'id' => Auth::$id,
				'lang' => &Auth::$lang,
				'nickname' => Auth::$nickname,
				'perms' => Auth::$groupPerms,
				'tz' => &Auth::$tz
			]);

			if (Auth::$isAdmin)
			{
				Config::$params['comments_moderate'] = 0;
			}
		}

		// Invité.
		else if (!Config::$params['gallery_closed'])
		{
			// Authentification par formulaire.
			if (Config::$params['users'] && isset($_POST['connection'])
			&& isset($_POST['login']) && isset($_POST['password']))
			{
				$cause = '';
				if (Auth::form($_POST['login'],
				$_POST['password'], isset($_POST['remember']), $cause))
				{
					App::redirect();
				}
				else
				{
					Report::warning(L10N::getTextLoginRejected($cause));
				}
			}

			// Paramètres par défaut.
			Template::set('user',
			[
				'admin' => Auth::$isAdmin,
				'auth' => FALSE,
				'avatar_source' => Avatar::getURL(2, FALSE),
				'id' => 2,
				'login' => NULL,
				'perms' => Auth::$groupPerms
			]);

			// Galerie autorisée seulement pour les utilisateurs enregistrés ?
			if (Config::$params['users_only_members'])
			{
				if (!in_array($_GET['section'], ['forgot-password', 'login',
				'register', 'validation']))
				{
					App::redirect('login');
					die;
				}

				Config::$params['links'] = 0;
				Config::$params['users_online'] = 0;
				Config::$params['random_item'] = 0;
			}
		}

		// Localisation.
		$lang_switch = !Auth::$connected && Config::$params['lang_switch'];
		Template::set('lang_switch', []);
		if ($lang_switch)
		{
			$params = Config::$params['lang_params']['langs'];
			$langs = [];
			foreach ($params as $code => &$name)
			{
				$langs[$code] = ['code' => $code, 'name' => $name];
			}
			Template::set('lang_switch', $langs);
		}
		if (isset($_POST['lang']) && ($_GET['section'] == 'user-options' || $lang_switch))
		{
			$lang = $_POST['lang'];
		}
		else if ($lang_switch)
		{
			$lang = Auth::$prefs->read('lang');
		}
		if (isset($_POST['tz']) && $_GET['section'] == 'user-options')
		{
			$tz = $_POST['tz'];
		}
		L10N::locale($lang ?? '', $tz ?? '');
		Template::set('lang', str_replace('_', '-', Auth::$lang));

		// La galerie est-elle fermée ?
		if (Config::$params['gallery_closed'])
		{
			Template::set('page_title', function(){ return __('Galerie fermée'); });
			return;
		}

		// Titre de page.
		Template::set('page_title', function(): string
		{
			$page = '';
			$separator = ' - ';
			$section_title = '';
			switch ($_GET['section'])
			{
				case '404' :
					$section_title = __('Page non trouvée');
					break;

				case 'album' :
				case 'category' :
					if (Template::$data['category']['id'] > 1)
					{
						$sql_limit = $_GET['section'] == 'album'
							? Config::$params['thumbs_item_nb_per_page']
							: Config::$params['thumbs_cat_nb_per_page'];
						if (Template::$data['objects_count'] > $sql_limit)
						{
							$page = $separator . sprintf(__('Page %s'), $_GET['page']);
						}
						$section_title = Template::$data['category']['title'] . $page;
					}
					break;

				case 'cameras' :
					$section_title = __('Appareils photos');
					break;

				case 'comments' :
					$section_title = __('Commentaires');
					break;

				case 'contact' :
					$section_title = __('Contact');
					break;

				case 'error' :
					$section_title = __('Oups !');
					break;

				case 'forgot-password' :
					$section_title = __('Mot de passe oublié');
					break;

				case 'history' :
					$section_title = __('Historique');
					break;

				case 'item' :
					$section_title = Template::$data['item']['title'];
					break;

				case 'language' :
					$section_title = __('Choix de la langue');
					break;

				case 'login' :
					$section_title = __('Connexion');
					break;

				case 'logout' :
					$section_title = __('Déconnexion');
					break;

				case 'members' :
					$section_title = __('Liste des membres');
					break;

				case 'password' :
					$section_title = __('Mot de passe');
					break;

				case 'register' :
					$section_title = __('Créer un compte');
					break;

				case 'search' :
					$section_title = __('Recherche');
					break;

				case 'search-advanced' :
					$section_title = __('Recherche avancée');
					break;

				case 'tags' :
					$section_title = __('Tags');
					break;

				case 'user' :
					$section_title = Template::$data['member']['nickname'];
					break;

				case 'user-avatar' :
					$section_title = __('Changer d\'avatar');
					break;

				case 'user-options' :
					$section_title = __('Options');
					break;

				case 'user-password' :
					$section_title = __('Changer de mot de passe');
					break;

				case 'user-profile' :
					$section_title = __('Modifier vos informations');
					break;

				case 'user-upload' :
					$section_title = __('Ajout de fichiers');
					break;
			}
			$section_title = $section_title ? $section_title . $separator : '';

			return $section_title . Config::$params['gallery_title'];
		});

		// Texte de pied de page.
		$footer_text = Config::$params['gallery_footer_text'];
		Template::set('footer_text', Utility::isEmpty($footer_text) ? NULL : $footer_text);

		// Filtres.
		App::filtersGET();

		// Moteur de recherche.
		if (Config::$params['search'])
		{
			self::_search();
		}

		// Pages.
		$pages_order = Config::$params['pages_order'];
		$pages_params = Config::$params['pages_params'];
		if (Config::$params['users'])
		{
			if (!Auth::$groupPerms['members_profile'])
			{
				$pages_params['members']['status'] = 0;
				Config::$params['users_online'] = 0;
			}
			if (!Auth::$connected && Config::$params['users_only_members_contact'])
			{
				$pages_params['contact']['status'] = 0;
			}
		}
		$pages = [];
		foreach ($pages_order as &$name)
		{
			if (($name == 'tags' && !Config::$params['tags'])
			 || ($name == 'comments' && !Config::$params['comments'])
			 || ($name == 'worldmap' && !Config::$params['geolocation'])
			 || ($name == 'members' && !Config::$params['users']))
			{
				continue;
			}
			if ($pages_params[$name]['status'] == 1)
			{
				$id = 0;
				$link = App::getURL($name);
				$title = L10N::getTextPage($name);
				if (substr($name, 0, 7) == 'custom_')
				{
					$id = str_replace('custom_', '', $name);
					$link = App::getURL('page/' . $id . '-' . $pages_params[$name]['url']);
					$title = $pages_params[$name]['title'];
				}

				switch ($name)
				{
					case 'members' :
						$current = $_GET['section'] == $name
							|| substr($_GET['section'], 0, 4) == 'user';
						break;

					default :
						$current = ($_GET['section'] == 'page' && $id)
							? $_GET['page_id'] == $id
							: $_GET['section'] == $name;
						break;
				}

				$pages[$name] =
				[
					'current' => $current,
					'link' => $link,
					'name' => $name,
					'title' => $title
				];
			}
		}
		Template::set('menu_pages', $pages);

		// Liens.
		if (Config::$params['links'])
		{
			$links = [];
			if (is_array(Config::$params['links_params']))
			{
				foreach (Config::$params['links_params'] as &$i)
				{
					if ($i['activated'])
					{
						$links[] = $i;
					}
				}
			}
			Template::set('links', $links);
		}

		// Géolocalisation.
		Template::set('geolocation', 0);
		if (Config::$params['geolocation'] && in_array($_GET['section'], ['item', 'worldmap']))
		{
			Template::set('geolocation', 1);
		}

		// Favoris.
		Template::set('favorites', Config::$params['users'] && Config::$params['favorites']
			&& Auth::$connected);

		// Mode sélection.
		Template::set('selection', $selection = Selection::isActivated());
		Template::set('selection_params', ['start' => 0]);
		if ($selection)
		{
			Template::set('selection_params',
			[
				'link' => App::getURL('category/1/selection'),
				'link_admin' => App::getURLAdmin(
					$_GET['section'] . '/' . $_GET['category_id'] . '/selection'
				),
				'max_items' => Auth::$connected ? 0 : Selection::GUEST_MAX_ITEMS,
				'start' => (int) (bool) Auth::$prefs->read('selection_start'),
				'stats' => Selection::getStats()
			]);
		}

		// Option "rester connecté".
		Template::set('remember', !Auth::$session->read('session_token')
			|| Auth::$session->read('session_expire'));

		// Utilisateurs en ligne.
		if (Config::$params['users'] && Config::$params['users_online']
		&& (int) ($s = Config::$params['users_online_duration']) > 0)
		{
			$sql = "SELECT user_id,
						   user_avatar,
						   user_login,
						   user_nickname,
						   user_lastvstdt
					  FROM {users}
					 WHERE user_id != 2
					   AND user_status = '1'
					   AND (TO_SECONDS(NOW()) - TO_SECONDS(user_lastvstdt)) < $s";
			if (DB::execute($sql))
			{
				$users_online = [];
				foreach (DB::fetchAll() as &$i)
				{
					$id = (int) $i['user_id'];
					$nickname = User::getNickname($i['user_login'], $i['user_nickname']);
					$users_online[$nickname] =
					[
						'avatar_source' => Avatar::getURL($id, (bool) $i['user_avatar']),
						'id' => $id,
						'last_visite' => sprintf(__('Dernière visite à %s'), 
							L10N::dt(__('%H:%M'), $i['user_lastvstdt'])
						),
						'link' => App::getURL('user/' . $id),
						'nickname' => $nickname
					];
				}
				uksort($users_online, 'Utility::alphaSort');
				Template::set('users_online', array_values($users_online));
			}
		}

		// Captures vidéos.
		if (!isset($_POST['upload'])
		&& (Auth::$isAdmin || (Config::$params['users'] && Auth::$groupPerms['upload']))
		&& $captures = Video::captures())
		{
			Template::set('video_captures', $captures);
		}

		// Fichiers à activer ou à désactiver.
		Item::pubexp();

		// Opérations quotidiennes.
		App::dailyUpdate();

		// Traitement de la section courante.
		require_once(__DIR__ . '/../' . str_replace('-', '_', $_GET['section_file']) . '.php');

		// Captures vidéos.
		if (isset($_POST['upload'])
		&& (Auth::$isAdmin || (Config::$params['users'] && Auth::$groupPerms['upload'])))
		{
			Template::set('video_captures', Video::captures());
		}

		// Moteur de recherche.
		Template::set('search', Config::$params['search'] ? [1] : []);
		Template::set('search_advanced', Config::$params['search']
			&& Config::$params['search_advanced']);
		Template::set('search_suggestion', Config::$params['search']
			&& Config::$params['search_suggestion']);

		// Diaporama.
		Template::set('diaporama', 0);
		if (Config::$params['diaporama'])
		{
			self::_diaporama();
		}

		// Sélection.
		if (($_GET['section_2'] ?? '') == 'selection' && !Selection::isActivated())
		{
			self::notFound();
		}

		// Paramètres de thème.
		$theme_params = [];
		$p = Config::$params['theme_params'];
		if (is_array($p))
		{
			$theme_params = $p[Config::$params['theme_template']] ?? [];
		}
		Template::set('theme', ['params' => $theme_params]);

		// Fichier au hasard.
		if (Config::$params['random_item'] && $item = self::randomItem())
		{
			Template::set('random_item', [$item]);
		}

		// A exécuter à la fin.
		if (Config::$params['browse'])
		{
			Template::set('categories_browse', 1);
			if (!Config::$params['browse_ajax'])
			{
				$cat_id = (int) (Template::$data['category']['id'] ?? 0);
				$browse = self::makeCategoriesList($cat_id);
				Template::set('categories_browse', $browse);
			}
		}
	}

	/**
	 * Chargement du fichier de template pour les erreurs.
	 *
	 * @return mixed
	 */
	public static function error()
	{
		Template::set('page_title', function()
		{
			return __('Une erreur s\'est produite') . ' - '
				. Config::$params['gallery_title'];
		});

		include(__DIR__ . '/../error.php');

		return FALSE;
	}

	/**
	 * Génère un lien avec les paramètres de filtres.
	 *
	 * @param string $url
	 * @param bool $cat_id
	 *
	 * @return string
	 */
	public static function getURLFilter(string $url, bool $cat_id = FALSE): string
	{
		if (isset($_GET['filter']))
		{
			// Nom du filtre.
			$url .= '/' . $_GET['filter'];

			// Valeur du filtre.
			$url .= empty($_GET['filter_value'])
			|| (!$cat_id && $_GET['filter_type'] == 'filter')
				? ''
				: '/' . $_GET['filter_value'];

			// Catégorie dans laquelle s'applique le filtre.
			if ($cat_id && ($_GET['section'] != 'item'
			|| ($_GET['section'] == 'item' && isset($_GET['section_3']))))
			{
				$url .= '/' . $_GET['filter_cat_id'];
			}
		}

		return App::getURL($url);
	}

	/**
	 * Construit la liste de toutes les catégories de la galerie.
	 *
	 * @params int $cat_id
	 *
	 * @return void
	 */
	public static function makeCategoriesList(int $cat_id): array
	{
		if (!Auth::$connected && Config::$params['users_only_members'])
		{
			return [];
		}

		$perms = SQL::catPerms()
			. ' AND cat_status = "1"
				AND thumb_id != -1
				AND (cat_a_images + cat_a_videos) > 0';
		$password = SQL::catPassword('select');
		self::$categories = Category::getListJSON($makelist, $cat_id,
			$perms, $password, Config::$params['browse_sql_order_by']);
		if (!self::$categories)
		{
			return [];
		}

		// On supprime les sous-catégories des catégories protégées
		// par mot de passe pour lesquelles l'utilisateur n'a pas
		// l'autorisation d'y accéder.
		foreach (self::$categories as &$i)
		{
			if ($i['p_cat_id'] != $i['cat_id'] && !$i['password_auth'])
			{
				foreach (self::$categories as $id => &$i2)
				{
					if (preg_match('`\D' . $i['p_cat_id'] . '\D`', $i2['cat_parents'])
					&& $i2['p_cat_id'] != $i2['cat_id'])
					{
						unset(self::$categories[$id]);
					}
				}
			}
		}

		// Réduction de la liste.
		$list = NULL;
		$reduce = function() use (&$list, &$makelist)
		{
			if (!(($_GET['filter'] == 'search' && $where = self::$search->sql())
			|| $where = SQL::itemsWhere($_GET['filter'], $_GET['filter_value'])))
			{
				return;
			}

			if (!Config::$params['browse_items_count'])
			{
				$sql = 'SELECT cat.cat_id
						  FROM {items} AS i
							   ' . self::_sqlItemsFrom() . '
					 LEFT JOIN {categories} AS cat
							ON i.cat_id = cat.cat_id
						 WHERE ' . SQL::catPerms() . '
						   AND ' . SQL::catPassword() . '
						   AND ' . $where['sql'];
				DB::params($where['params']);
				if (!DB::execute($sql))
				{
					return;
				}
				$parents_id = array_flip(DB::fetchCol('cat_id'));
				Category::reduceList(self::$categories, $parents_id, FALSE);
				$list = $makelist(self::$categories);
				return;
			}

			$sql = 'SELECT cat.cat_id,
						   COUNT(i.item_id) AS count
					  FROM {items} AS i
						   ' . self::_sqlItemsFrom() . '
				 LEFT JOIN {categories} AS cat
						ON i.cat_id = cat.cat_id
					 WHERE ' . SQL::catPerms() . '
					   AND ' . SQL::catPassword() . '
					   AND item_status = "1"
					   AND ' . $where['sql'] . '
				  GROUP BY cat.cat_id';
			DB::params($where['params']);
			if (!DB::execute($sql))
			{
				return;
			}
			$infos = DB::fetchAll('cat_id', 'count');
			Category::reduceList(self::$categories, $infos);
			$list = $makelist(self::$categories);
		};
		if (in_array($_GET['section'], ['album', 'category', 'item'])
		&& isset($_GET['filter']) && $makelist)
		{
			$reduce();
		}
		else
		{
			if (Config::$params['browse_items_count'] && self::$categories)
			{
				self::changeCatStats(self::$categories);
			}
			$list = $makelist ? $makelist(self::$categories) : NULL;
		}
		return
		[
			'list' => $list,
			'url' => self::getURLFilter('category/id-urlname')
		];
	}

	/**
	 * Chargement du fichier de template pour les pages non trouvées.
	 *
	 * @return bool
	 */
	public static function notFound(): bool
	{
		Template::set('page_title', function()
		{
			return __('Page non trouvée') . ' - '
				. Config::$params['gallery_title'];
		});

		$_GET['q'] = $_GET['q_pageless'] = $_GET['section'] = '404';

		include(__DIR__ . '/../404.php');

		return FALSE;
	}

	/**
	 * Récupère un fichier au hasard.
	 *
	 * @param array $i
	 *
	 * @return array
	 */
	public static function randomItem(&$i = []): array
	{
		$sql = 'SELECT i.*,
					   cat.cat_parents,
					   u.user_avatar,
					   u.user_login,
					   u.user_nickname,
					   u.user_status,
					   (' . Selection::getSQLItem() . ') AS in_selection
				  FROM {items} AS i
			 LEFT JOIN {categories} AS cat
					ON i.cat_id = cat.cat_id
			 LEFT JOIN {users} AS u
					ON i.user_id = u.user_id
				 WHERE item_status = "1"
				   AND ' . SQL::catPerms() . '
				   AND ' . SQL::catPassword() . '
			  ORDER BY RAND()
				 LIMIT 1';
		if (DB::execute($sql) && $i = DB::fetchRow())
		{
			require_once(__DIR__ . '/GalleryItems.class.php');
			return GalleryItems::getFormatedInfos($i, 'thumb');
		}

		return [];
	}

	/**
	 * Gestion des paramètres GET.
	 *
	 * @return void
	 */
	public static function request(): void
	{
		$q = !empty($_GET['q']);

		// Compatibilité avec les URL d'iGalerie 2.
		if ($q)
		{
			$_GET['q'] = str_replace('image/', 'item/', $_GET['q']);
		}

		// Configuration de base.
		$request =
		[
			// Catégories.
			'{category}/{id}',
			'{category}/{id}/camera-(brand|model)/{id}',
			'{category}/{id}/date-(created|published)/{date-flex}',
			'{category}/{id}/(images|items|selection|videos|views)',

			// Fichiers.
			'item/{id}',
			'item/{id}/camera-(brand|model)/{id}/{id}',
			'item/{id}/date-(created|published)/{date-flex}/{id}',
			'item/{id}/(images|items|selection|videos|views)/{id}',

			// Mot de passe de catégorie.
			'logout/{id}',
			'password/{category}/{id}',
			'password/item/{id}',

			// Pages personnalisées.
			'page/{id}'
		];

		// Page des appareils photos.
		if (Config::$params['pages_params']['cameras']['status'])
		{
			$request[] = 'cameras';
			$request[] = 'cameras/{id}';
		}

		// Commentaires.
		if (Config::$params['comments'])
		{
			// Page des commentaires.
			if (Config::$params['pages_params']['comments']['status'])
			{
				$request[] = 'comments';
				$request[] = 'comments/{category}/{id}';
				$request[] = 'comments/{category}/{id}/user/{id}';
				$request[] = 'comments/user/{id}';
			}

			// Filtre.
			$request[] = '{category}/{id}/comments';
			$request[] = 'item/{id}/comments/{id}';
		}

		// Page contact.
		if (Config::$params['pages_params']['contact']['status'])
		{
			$request[] = 'contact';
			$request[] = 'contact/confirm';
		}

		// Fichiers récents.
		if (Config::$params['items_recent'])
		{
			$request[] = '{category}/{id}/recent-(images|items|videos)';
			$request[] = 'item/{id}/recent-(images|items|videos)/{id}';
		}

		// Page de l'historique.
		if (Config::$params['pages_params']['history']['status'])
		{
			$request[] = 'history';
			$request[] = 'history/{id}';
			$request[] = 'history/{id}/date-(created|published)';
			$request[] = 'history/{id}/date-(created|published)/\d{4}';
			$request[] = 'history/{id}/date-(created|published)/\d{4}-\d{2}';
		}

		// Langues.
		if (Config::$params['lang_switch'])
		{
			$request[] = 'language';
		}

		// Moteur de recherche.
		if (Config::$params['search'])
		{
			$request[] = 'search';

			// Recherche avancée.
			if (Config::$params['search_advanced'])
			{
				$request[] = 'search-advanced';
			}

			// Filtre.
			$request[] = '{category}/{id}/search/{search}';
			$request[] = 'item/{id}/search/{search}/{id}';
		}

		// Tags.
		if (Config::$params['tags'])
		{
			// Page des tags.
			$request[] = 'tags';
			$request[] = 'tags/{id}';
			$request[] = 'tags/{id}/{id}';
			$request[] = 'tags/{id}/{ids}';

			// Filtre.
			$request[] = '{category}/{id}/tag/{id}';
			$request[] = '{category}/{id}/tags/{ids}';
			$request[] = 'item/{id}/tag/{id}/{id}';
			$request[] = 'item/{id}/tags/{ids}/{id}';
		}

		// Utilisateurs.
		if (Config::$params['users'])
		{
			// Connexion / déconnexion.
			$request[] = 'login';
			$request[] = 'logout';

			// Inscription.
			if (Config::$params['users_registration'])
			{
				$request[] = 'register';
				$request[] = 'validation/{key}';
			}

			// Procédure pour l'oubli du mot de passe.
			$request[] = 'forgot-password';

			// Page de profil des membres.
			$request[] = 'user/{id}';

			// Pages de modifications du profil.
			$request[] = 'user-avatar';
			$request[] = 'user-options';
			$request[] = 'user-password';
			$request[] = 'user-profile';

			// Pages d'ajout de fichiers et de création d'albums.
			$request[] = 'user-new-cat';
			$request[] = 'user-upload';
			$request[] = 'user-upload/album/{id}';

			// Liste des membres.
			if (Config::$params['pages_params']['members']['status'])
			{
				$request[] = 'members';
				$request[] = 'members/group/{id}';
			}

			// Filtres.
			$request[] = '{category}/{id}/user-(images|items|videos)/{id}';
			$request[] = 'item/{id}/user-(images|items|videos)/{id}/{id}';

			// Filtres : favoris.
			if (Config::$params['favorites'])
			{
				$request[] = '{category}/{id}/favorites';
				$request[] = '{category}/{id}/user-favorites/{id}';
				$request[] = 'item/{id}/favorites/{id}';
				$request[] = 'item/{id}/user-favorites/{id}/{id}';
			}
		}

		// Votes.
		if (Config::$params['votes'])
		{
			// Filtre.
			$request[] = '{category}/{id}/votes';
			$request[] = 'item/{id}/votes/{id}';
		}

		// Géolocalisation.
		if (Config::$params['geolocation'])
		{
			// Page de la carte du monde.
			if (Config::$params['pages_params']['worldmap']['status'])
			{
				$request[] = 'worldmap';
			}
		}

		// Analyse de la requête.
		App::request($request);

		// Paramètres GET par défaut.
		if (!isset($_GET['section']))
		{
			$_GET['q'] = $_GET['q_pageless'] = $_GET['section'] = $q ? '404' : 'category';
		}
		if (isset($_GET['album_id']))
		{
			$_GET['category_id'] = $_GET['album_id'];
		}
		if (!isset($_GET['category_id']))
		{
			$_GET['category_id'] = 1;
		}
		if ($_GET['q_pageless'] == 'category' && $_GET['category_id'] == 1)
		{
			$_GET['q_pageless'] = '';
		}
		$_GET['section_file'] = $_GET['section'];
	}



	/**
	 * Génère les informations de template pour la pagination.
	 *
	 * @param mixed $count
	 *   Nombre de pages.
	 *
	 * @return void
	 */
	protected static function _pagination($count): void
	{
		if (!$q = $_GET['q_pageless'])
		{
			$q = 'category/1-' . __('galerie');
		}
		Template::set('pages',
		[
			'count' => (int) $count,
			'first' =>
			[
				'active' => $_GET['page'] > 1,
				'link' => App::getURL($q . '/page/1')
			],
			'prev' =>
			[
				'active' => $_GET['page'] > 1,
				'link' => App::getURL($q . '/page/'
					. ($_GET['page'] > 1 ? ($_GET['page'] - 1) : 1))
			],
			'next' =>
			[
				'active' => $count > $_GET['page'],
				'link' => App::getURL($q . '/page/'
					. ($count > 1 ? ($_GET['page'] + 1) : 1))
			],
			'last' =>
			[
				'active' => $count > $_GET['page'],
				'link' => App::getURL($q . '/page/' . (int) $count)
			]
		]);
	}

	/**
	 * Informations de template pour générer les liens vers les flux RSS.
	 *
	 * @param string $section
	 * @param array $i
	 *
	 * @return void
	 */
	protected static function _rss(string $section, array &$i): void
	{
		$rss_link = GALLERY_HOST . CONF_GALLERY_PATH
			. '/rss.php?' . $section . '=%s&type=%s&lang=' . Auth::$lang;
		$title_comments = '';
		$title_items = '';

		$text_items = function() use (&$i)
		{
			if ($i['cat_a_images'] && $i['cat_a_videos'])
			{
				return __('photos et vidéos');
			}
			if ($i['cat_a_videos'])
			{
				return __('vidéos');
			}
			return __('photos');
		};

		$text_items_feed = function() use (&$i)
		{
			if ($i['cat_a_images'] && $i['cat_a_videos'])
			{
				return __('Fil des photos et vidéos');
			}
			if ($i['cat_a_videos'])
			{
				return __('Fil des vidéos');
			}
			return __('Fil des photos');
		};

		switch ($section)
		{
			case 'category' :
				$link_id = $i['cat_id'];
				if ($i['cat_id'] > 1)
				{
					if ($i['cat_type'] == 'album')
					{
						$text = __('Flux RSS 2.0 des %s de cet album');
					}
					else
					{
						$text = __('Flux RSS 2.0 des %s de cette catégorie');
					}
				}
				else
				{
					$text = __('Flux RSS 2.0 des %s de la galerie');
				}
				$title_comments = sprintf($text, __('commentaires'));
				$title_items = sprintf($text, $text_items());
				$link_comments_text = __('Fil des commentaires');
				$link_items_text = $text_items_feed();
				break;

			case 'item' :
				$link_id = $i['item_id'];
				$title_comments = (Item::isImage($i['item_type']))
					? __('Flux RSS 2.0 des commentaires de cette photo')
					: __('Flux RSS 2.0 des commentaires de cette vidéo');
				$link_comments_text = __('Fil des commentaires');
				break;

			case 'tag' :
				$link_id = $_GET['tag_id'];
				$title_items = sprintf(__('Flux RSS 2.0 des %s liées à ce tag'), $text_items());
				$link_items_text = $text_items_feed();
				break;
		}

		if ($section != 'tag')
		{
			Template::set('rss',
			[
				'comments' =>
				[
					'link' => sprintf($rss_link, $link_id, 'comments'),
					'link_text' => $link_comments_text,
					'title' => $title_comments
				]
			]);
		}

		if ($section != 'item')
		{
			Template::set('rss',
			[
				'items' =>
				[
					'link' => sprintf($rss_link, $link_id, 'items'),
					'link_text' => $link_items_text,
					'title' => $title_items
				]
			]);
		}
	}



	/**
	 * Paramètres du diaporama.
	 *
	 * @return void
	 */
	private static function _diaporama(): void
	{
		if (isset(Template::$data['category']) && Template::$data['objects_count']
		&& (in_array($_GET['section'], ['album', 'item'])
		|| ($_GET['section'] == 'category' && isset($_GET['filter']))))
		{
			$query = $_GET['q_pageless'];
			if ($_GET['section'] == 'item')
			{
				if (isset($_GET['filter']))
				{
					if ($_GET['filter_cat_id'] == '1')
					{
						$query = 'category/1';
					}
					else
					{
						foreach (Template::$data['breadcrumb'] as &$i)
						{
							if (in_array($i['type'], ['album', 'category'])
							&& $i['id'] == $_GET['filter_cat_id'])
							{
								$query = $i['type'] . '/' . $i['id'];
								break;
							}
						}
					}
					$query .= '/' . $_GET['filter'];
					if ($_GET['filter_type'] == 'filter_p')
					{
						$query .= '/' . $_GET['filter_value'];
					}
				}
				else
				{
					$query = Template::$data['category']['type'] . '/'
						. Template::$data['category']['id'];
				}
				$position = Template::$data['item_position'];
			}
			else
			{
				$position = (($_GET['page'] - 1)
					* (int) Config::$params['thumbs_item_nb_per_page']) + 1;
			}

			$settings = [];

			Template::set('diaporama', 1);
			Template::set('diaporama_carousel_thumbs_ratio',
				Config::$params['diaporama_carousel_thumbs_ratio']);
			Template::set('diaporama_key', function(string $size)
			{
				return md5($size . '|' . CONF_ACCESS_KEY);
			});
			Template::set('diaporama_options',
			[
				'autoDuration' => (int) Config::$params['diaporama_auto_duration'],
				'autoLoop' => (bool) Config::$params['diaporama_auto_loop'],
				'autoStart' => (bool) Config::$params['diaporama_auto_start'],
				'carousel' => (bool) Config::$params['diaporama_carousel'],
				'carouselThumbsSize' => (int) Config::$params['diaporama_carousel_thumbs_size'],
				'controlBars' => (bool) Config::$params['diaporama_control_bars'],
				'fullScreen' => (bool) Config::$params['diaporama_fullscreen'],
				'fullScreenMobile' => (bool) Config::$params['diaporama_fullscreen_mobile'],
				'keyboard' => (bool) Config::$params['diaporama_keyboard'],
				'overImageDescription'
					=> (bool) Config::$params['diaporama_over_image_description'],
				'overImageTitle' => (bool) Config::$params['diaporama_over_image_title'],
				'showInformations' => (bool) Config::$params['diaporama_show_informations'],
				'transitionDuration' => (int) Config::$params['diaporama_transition_duration'],
				'transitionEffect' => (string) Config::$params['diaporama_transition_effect'],
				'zoom' => (bool) Config::$params['diaporama_zoom'],
				'zoomLimit' => (int) Config::$params['diaporama_zoom_limit']
			]);
			Template::set('diaporama_position', (int) $position);
			Template::set('diaporama_query', $query);
		}
	}

	/**
	 * Gestion du moteur de recherche.
	 *
	 * @return void
	 */
	private static function _search(): void
	{
		if (isset($_GET['search']))
		{
			self::$search = new Search();
			if ($search = self::$search->sql($_GET['search'])
			&& !isset(self::$search->options['type']))
			{
				Template::set('search', ['query' => self::$search->query]);
			}
			else
			{
				Template::set('search', ['invalid' => 1]);
			}
		}

		// Options de recherche.
		if (substr($_GET['section'], 0, 6) == 'search')
		{
			Template::set('search_options',
			[
				'comments' => (int) Config::$params['comments'],
				'favorites' => 0,
				'filesize_unit' =>
				[
					['selected' => 0, 'text' => __('octets'), 'value' => 'b'],
					['selected' => 1, 'text' => __('Ko'), 'value' => 'kb'],
					['selected' => 0, 'text' => __('Mo'), 'value' => 'mb'],
					['selected' => 0, 'text' => __('Go'), 'value' => 'gb']
				],
				'type' => 0,
				'tags' => (int) Config::$params['tags']
			]);

			// Favoris.
			if (Config::$params['users'] && Config::$params['favorites'] && Auth::$connected)
			{
				Template::set('search_options', ['favorites' => 1]);
			}

			// Types de fichier.
			$sql = 'SELECT cat_a_images,
						   cat_a_videos
					  FROM {categories} AS cat
					 WHERE cat_id = 1
					   AND ' . SQL::catPerms() . '
					   AND ' . SQL::catPassword();
			if (DB::execute($sql))
			{
				$items = DB::fetchRow();
				if ($items['cat_a_images'] > 0 && $items['cat_a_videos'] > 0)
				{
					Template::set('search_options', ['type' => 1]);
				}
			}
		}

		// Données POST.
		if (isset($_POST['search_query']) && mb_strlen($_POST['search_query']) <= 68
		&& !Utility::isEmpty($_POST['search_query']))
		{
			self::$search = new Search();

			// Options de recherche.
			if (!Config::$params['search_advanced'] || empty($_POST['search_options']))
			{
				$options =
				[
					'columns' => ['item_name', 'item_desc', 'item_path'],
					'item_tags' => 1
				];
			}
			else
			{
				// Vérification des options.
				$check = $_POST['search_options'];
				$options = [];

				// Tous les mots ?
				if (isset($check['all_words']))
				{
					$options['all_words'] = 1;
				}

				// Uniquement dans les favoris de l'utilisateur ?
				if (Config::$params['users'] && Config::$params['favorites']
				&& Auth::$connected && isset($check['favorites']))
				{
					$options['favorites'] = Auth::$id;
				}

				// Type de fichier.
				if (Template::$data['search_options']['type'] && isset($check['type'])
				&& in_array($check['type'], ['all', 'image', 'video']))
				{
					$options['item_type'] = $check['type'];
				}

				// Format d'image.
				if (isset($check['format']) && in_array($check['format'],
				['all', 'landscape', 'panorama', 'portrait', 'square']))
				{
					$options['item_format'] = $check['format'];
				}

				// Champs de recherche.
				if (!isset($check['columns']) || !is_array($check['columns']))
				{
					return;
				}
				$options['columns'] = [];
				foreach ($check['columns'] as &$col)
				{
					if ($col == 'filename')
					{
						$options['columns'][] = 'item_path';
					}
					else if (in_array($col, ['desc', 'name']))
					{
						$options['columns'][] = 'item_' . $col;
					}
					else if (in_array($col, ['comments', 'tags']))
					{
						$options['item_' . $col] = 1;
					}
				}

				// Date.
				if (!empty($check['date']) && isset($check['date_column'])
				&& in_array($check['date_column'], ['crtdt', 'pubdt']))
				{
					$options['date'] = 1;
					$options['date_column'] = 'item_' . $check['date_column'];
					foreach (['start', 'end'] as $p1)
					{
						foreach (['day', 'month', 'year'] as $p2)
						{
							$date = 'date_' . $p1 . '_' . $p2;
							$regexp = '`^' . Search::OPTIONS_PATTERN[$date] . '$`';
							if (!isset($check[$date])
							|| !preg_match($regexp, (string) $check[$date]))
							{
								unset($options['date']);
								break 2;
							}
							$options[$date] = $p2 == 'year'
								? $check[$date]
								: sprintf("%'.02d", (string) $check[$date]);
						}
					}
				}

				// Dimensions.
				if (!empty($check['size']))
				{
					$options['item_size'] = 1;
					foreach (['width', 'height'] as $p1)
					{
						foreach (['start', 'end'] as $p2)
						{
							if (!isset($check['size_' . $p1 . '_' . $p2])
							|| !preg_match('`^\d{1,6}$`', $check['size_' . $p1 . '_' . $p2]))
							{
								unset($options['item_size']);
								break 2;
							}
							$options['item_size_' . $p1 . '_' . $p2]
								= $check['size_' . $p1 . '_' . $p2];
						}
					}
				}

				// Poids.
				if (!empty($check['filesize']))
				{
					$options['item_filesize'] = 1;
					foreach (['start', 'end'] as $p1)
					{
						if (!isset($check['filesize_' . $p1])
						|| !preg_match('`^\d{1,6}$`', $check['filesize_' . $p1]))
						{
							unset($options['item_filesize']);
							break;
						}
						$options['item_filesize_' . $p1] = $check['filesize_' . $p1];
					}

					// Unité de poids.
					if (!empty($options['item_filesize']) && isset($check['filesize_unit'])
					&& preg_match('`^[kmg]?b$`', $check['filesize_unit']))
					{
						$options['item_filesize_unit'] = $check['filesize_unit'];
					}
				}
			}

			// Enregistrement des paramètres de la recherche et redirection.
			if ($search_id = self::$search->insert($_POST['search_query'], $options))
			{
				App::redirect('category/1/search/' . $search_id);
			}
		}
	}



	/**
	 * Retourne la clause FROM pour une recherche
	 * dans la table des fichiers.
	 *
	 * @return string
	 */
	protected static function _sqlItemsFrom(): string
	{
		return SQL::itemsFrom((($filter = $_GET['filter'] ?? '') == 'search'
			&& !empty(self::$search->options['favorites']))
			? 'user-favorites'
			: $filter);
	}
}
?>