<?php
declare(strict_types = 1);

/**
 * Gestion des catégories et des fichiers
 * dans le mode d'affichage en grille.
 *
 * @license https://www.gnu.org/licenses/gpl-3.0.html
 * @link https://www.igalerie.org/
 */
class AjaxGrid extends Ajax
{
	/**
	 * Récupération des informations des catégories.
	 *
	 * @return void
	 */
	public static function getCategories(): void
	{
		// Quelques vérifications.
		if (!isset($_POST['q']) || !Utility::checkPost('items_per_line', '`^\d{1,2}$`'))
		{
			self::_forbidden();
		}

		// Extraction de la requête.
		$_GET['q'] = $_POST['q'];
		App::request
		([
			'category/{id}',
			'{category}/{id}/search/{search}'
		]);
		if (empty($_GET['section']))
		{
			self::_forbidden();
		}

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

		// On ne peut pas faire de recherche de catégories dans un album.
		if ($_GET['section'] == 'album' && isset($_GET['search']))
		{
			self::_redirectToCatOne();
		}

		// Préférences d'affichage.
		Admin::displayOptions('category');
		$user_prefs = Auth::$infos['user_prefs']['category'];

		// Nombre de fichiers par page.
		$nb_lines = (int) $user_prefs['grid_lines'];
		$nb_lines = $nb_lines > 0 ? $nb_lines : 1;
		$items_per_page = $_POST['items_per_line'] * $nb_lines;

		// Clauses FROM et WHERE.
		$params = [];
		$sql_from = '';
		$sql_where = 'cat.cat_id > 1';

		// Récupération des informations utiles de la catégorie.
		$cat_path = '';
		$cat_id = $_GET['category_id'] ?? 1;
		$cat_infos = self::_getCatInfos((int) $cat_id);
		if ($cat_id > 1)
		{
			$cat_path = $cat_infos['cat_path'] . '/';
		}

		// Recherche.
		Admin::filters();
		if (isset($_GET['search']))
		{
			if ($search = AdminAlbums::sqlWhereCategories())
			{
				$sql_where .= ' AND ' . $search['sql'];
				$params = $search['params'];
			}

			// Limitation de la recherche à la catégorie.
			if ($cat_id > 1)
			{
				$sql_where .= ' AND cat_path LIKE :cat_path';
				$params['cat_path'] = DB::likeEscape($cat_path) . '%';
			}
		}
		else
		{
			$sql_where .= ' AND cat.parent_id = :parent_id';
			$params['parent_id'] = $_GET['category_id'];
		}

		// Récupération du nombre de catégories.
		$sql = "SELECT COUNT(*) FROM {categories} AS cat $sql_from WHERE $sql_where";
		if (!DB::execute($sql, $params))
		{
			self::_error();
		}
		if ((!$nb_cats = DB::fetchVal()) && isset($_GET['search']) && $cat_id > 1)
		{
			self::_redirectToCatOne();
		}

		// Clause ORDER BY.
		$sql_order_by = self::_getCatOrderBy();

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

		// Récupération de l'identifiant de toutes les catégories de la section.
		$sql = "SELECT cat_id FROM {categories} AS cat WHERE $sql_where ORDER BY cat_id";
		if (!DB::execute($sql, $params))
		{
			self::_error();
		}
		$section_all_id = DB::fetchCol('cat_id');

		// Récupération des informations utiles des catégories de la page courante.
		$sql = "SELECT cat.*,
					   CASE WHEN cat.cat_filemtime IS NULL
						    THEN 'category' ELSE 'album'
							 END AS cat_type,
					   i.item_id,
					   i.item_path,
					   i.item_adddt,
					   i.item_type,
					   i.item_width,
					   i.item_height,
					   i.item_orientation,
		               u.user_login,
					   u.user_nickname,
					   u.user_avatar,
					   p.cat_id AS p_cat_id
				  FROM {categories} AS cat
					   $sql_from
			 LEFT JOIN {items} AS i
					ON cat.thumb_id = i.item_id
			 LEFT JOIN {users} AS u
					ON cat.user_id = u.user_id
			 LEFT JOIN {passwords} AS p
				    ON cat.password_id = p.password_id
				 WHERE $sql_where
			  ORDER BY $sql_order_by
				 LIMIT $sql_limit";
		if (!DB::execute($sql, $params))
		{
			self::_error();
		}
		$categories = DB::fetchAll();

		Template::init();
		$html = '';
		$items = [];
		$n = 0;
		$parents = [];
		foreach ($categories as &$i)
		{
			// Code HTML des vignettes.
			$class  = $i['cat_status'] == 0 ? ' obj_deactivated' : '';
			$id     = (int) $i['cat_id'];
			$link   = App::getURL($i['cat_type'] . '/' . $id);
			$rel    = Item::isVideo($i['item_type']) ? " data-video-id='$id'" : '';
			$src    = $i['thumb_id'] == -1
				? Template::$data['admin']['style']['path'] . '/cat-empty.png'
				: App::getThumbSource('cat', $i, Admin::$thumbSize, Admin::$thumbQuality);
			$title  = htmlspecialchars($i['cat_name']);

			$html .= "<dl id='obj_$id' class='obj$class'>";
			$html .=    "<dt>";
			$html .=       "<span><input type='checkbox'></span>";
			$html .=       "<a href='javascript:;'>";
			$html .=          "<img$rel src='$src'>";
			$html .=       "</a>";
			$html .=    "</dt>";
			$html .=    "<dd><a href='$link'>$title</a></dd>";
			$html .= "</dl>";

			// Paramètres dépendant des catégories parentes.
			// On économise les requêtes SQL en mémorisant
			// les informations par catégorie parente.
			if (array_key_exists($i['cat_id'], $parents))
			{
				$parents_infos = $parents[$i['cat_id']]['parents_infos'];
				$i['parent_commentable'] = $parents[$i['cat_id']]['parent_commentable'];
				$i['parent_creatable'] = $parents[$i['cat_id']]['parent_creatable'];
				$i['parent_downloadable'] = $parents[$i['cat_id']]['parent_downloadable'];
				$i['parent_uploadable'] = $parents[$i['cat_id']]['parent_uploadable'];
				$i['parent_votable'] = $parents[$i['cat_id']]['parent_votable'];
			}
			else
			{
				// Informations des catégories parentes.
				$parents_infos = Template::breadcrumb($i);
				$parents_infos[$i['cat_id']] =
				[
					'cat_id' => $i['cat_id'],
					'cat_name' => $i['cat_name'],
					'cat_type' => $i['cat_type']
				];

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

				$parents[$i['parent_id']] =
				[
					'parents_infos' => $parents_infos,
					'parent_commentable' => $i['parent_commentable'],
					'parent_creatable' => $i['parent_creatable'],
					'parent_downloadable' =>  $i['parent_downloadable'],
					'parent_uploadable' =>  $i['parent_uploadable'],
					'parent_votable' =>  $i['parent_votable']
				];
			}

			// Informations formatées.
			HTML::specialchars($i);
			HTML::specialchars($parents_infos);
			$items[$id] = AdminCategory::getFormatedInfos($i, $parents_infos);
			$items[$id]['user_avatar'] = Avatar::getURL(
				(int) $i['user_id'], (bool) $i['user_avatar']);
			$items[$id]['preview_size'] = Image::getResizedSize(
				(int) $i['item_width'], (int) $i['item_height'], 400, 400);

			// On supprime les informations inutiles.
			unset($items[$id]['deactivated']);
			unset($items[$id]['id']);
			unset($items[$id]['link']);
			unset($items[$id]['thumb_src']);
			unset($items[$id]['thumb_size']);

			// Signature des informations.
			$items[$id]['hash'] = md5(json_encode($items[$id]));

			unset($i);
			$n++;
		}

		// Catégories géolocalisées.
		$geocats = AdminCategory::getGeoCategories();

		// Données.
		$data =
		[
			'status' => 'success',
			'cat_stats' => self::_getCatStats($cat_infos),
			'cat_thumb' => self::_getCatThumb($cat_infos),
			'geolocation_categories' => HTML::specialchars($geocats),
			'html' => self::_gridLines($html, $n),
			'items' => $items,
			'lines' => (int) $nb_lines,
			'nb_items' => (int) $nb_cats,
			'section_all_id' => array_map('intval', $section_all_id),
			'thumb_size' => (int) $user_prefs['grid_tb_size']
		];
		if (!empty($_POST['browse']))
		{
			AdminAlbums::getCategoriesLists(TRUE);
			$data['browse'] = HTML::specialchars(Template::$data['categories_browse']);
			$data['browse_levels'] = (int) Template::$data['categories_browse_levels'];
			$data['browse_subcats'] = (int) Template::$data['categories_browse_subcats'];
			$data['move'] = HTML::specialchars(Template::$data['categories_move']);
		}
		self::_printResponse($data, FALSE);
	}

	/**
	 * Récupération des informations des fichiers.
	 *
	 * @return void
	 */
	public static function getItems(): void
	{
		// Quelques vérifications.
		if (!isset($_POST['q']) || !Utility::checkPost('items_per_line', '`^\d{1,2}$`'))
		{
			self::_forbidden();
		}

		// Extraction de la requête.
		$_GET['q'] = $_POST['q'];
		App::request(
		[
			'album/{id}',
			'{category}/{id}/camera-(brand|model)/{id}',
			'{category}/{id}/datetime/{timestamp}',
			'{category}/{id}/selection',
			'{category}/{id}/search/{search}',
			'{category}/{id}/tag/{id}',
			'{category}/{id}/user-(favorites|images|items|videos)/{id}',
			'category-items/{id}'
		]);
		if (empty($_GET['section']))
		{
			self::_forbidden();
		}

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

		// Préférences d'affichage.
		Admin::displayOptions('album');
		$user_prefs = Auth::$infos['user_prefs']['album'];

		// Nombre de fichiers par page.
		$nb_lines = (int) $user_prefs['grid_lines'];
		$nb_lines = $nb_lines > 0 ? $nb_lines : 1;
		$items_per_page = $_POST['items_per_line'] * $nb_lines;

		// Clause WHERE.
		$params = [];
		$sql_where = '';

		// Filtres.
		Admin::filters();
		if (isset($_GET['filter']))
		{
			if (($_GET['filter'] == 'search' && $where = Admin::$search->sql())
			|| $where = SQL::itemsWhere($_GET['filter'], $_GET['filter_value']))
			{
				$sql_where .= ' AND ' . $where['sql'];
				$params = $where['params'];
			}
		}

		// Clause FROM.
		$sql_from = SQL::itemsFrom($_GET['filter'] ?? '');

		// Récupération des informations utiles de la catégorie.
		$cat_id = $_GET['album_id'] ?? $_GET['category_id'] ?? 1;
		$cat_infos = self::_getCatInfos((int) $cat_id);

		// Chemin de la catégorie.
		$cat_path = $cat_id > 1 ? $cat_infos['cat_path'] . '/' : '';
		$params['cat_path'] = DB::likeEscape($cat_path) . '%';

		// Récupération du nombre de fichiers.
		$sql = "SELECT COUNT(*)
				  FROM {items} AS i
					   $sql_from
			 LEFT JOIN {categories} AS cat
					ON i.cat_id = cat.cat_id
				 WHERE item_path LIKE :cat_path
					   $sql_where";
		if (!DB::execute($sql, $params))
		{
			self::_error();
		}
		if ((!$nb_items = DB::fetchVal()) && isset($_GET['search']) && $cat_id > 1)
		{
			self::_redirectToCatOne();
		}

		// Clause ORDER BY.
		$sql_order_by = self::_getItemsOrderBy();

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

		// Récupération de l'identifiant de tous les fichiers de la section.
		$sql = "SELECT i.item_id
				  FROM {items} AS i
					   $sql_from
				 WHERE item_path LIKE :cat_path
					   $sql_where
			  ORDER BY i.item_id";
		if (!DB::execute($sql, $params))
		{
			self::_error();
		}
		$section_all_id = DB::fetchCol('item_id');

		// Récupération des informations utiles des fichiers de la page courante.
		$sql = "SELECT i.*,
					   cat.password_id,
					   cat.cat_parents,
					   cat.cat_name,
					   cat.thumb_id AS cat_thumb_id,
					   CASE WHEN cat.cat_filemtime IS NULL
						    THEN 'category' ELSE 'album'
							 END AS cat_type,
		               u.user_login,
					   u.user_nickname,
					   u.user_avatar,
					   p.cat_id AS p_cat_id
				  FROM {items} AS i
					   $sql_from
			 LEFT JOIN {categories} AS cat
					ON i.cat_id = cat.cat_id
			 LEFT JOIN {users} AS u
					ON i.user_id = u.user_id
			 LEFT JOIN {passwords} AS p
				    ON cat.password_id = p.password_id
				 WHERE item_path LIKE :cat_path
					   $sql_where
			  ORDER BY $sql_order_by
				 LIMIT $sql_limit";
		if (!DB::execute($sql, $params))
		{
			self::_error();
		}
		$items_infos = DB::fetchAll('item_id');

		// Récupération des tags de chaque fichier.
		if ($items_infos && ($items_id = DB::inInt(array_keys($items_infos))))
		{
			$sql = "SELECT item_id,
						   tag_name
					  FROM {tags}
				 LEFT JOIN {tags_items} USING (tag_id)
					 WHERE item_id IN ($items_id)";
			if (!DB::execute($sql))
			{
				return;
			}
			$tags_infos = DB::fetchAll();
			foreach ($tags_infos as &$i)
			{
				$items_infos[$i['item_id']]['tags'][] = $i['tag_name'];
			}
		}

		$html = '';
		$items = [];
		$n = 0;
		$albums = [];
		foreach ($items_infos as &$i)
		{
			// Code HTML des vignettes.
			$class  = $i['item_status'] != '1' ? ' obj_deactivated' : '';
			$file   = App::getFileSource($i['item_path']);
			$id     = (int) $i['item_id'];
			$link   = App::getURL('item-edit/' . $id);
			$rel    = Item::isVideo($i['item_type']) ? " data-video-id='$id'" : '';
			$src    = App::getThumbSource('item', $i, Admin::$thumbSize, Admin::$thumbQuality);
			$title  = htmlspecialchars($i['item_name']);

			$duration = $i['item_duration']
				? App::formatDuration($i['item_duration'])
				: '00:00';
			$duration  = Item::isVideo($i['item_type'])
				? "<span class='duration' id='videoduration_$id'>$duration</span>"
				: "";

			$html .= "<dl id='obj_$id' class='obj$class'>";
			$html .=    "<dt>";
			$html .=       "<span><input type='checkbox'></span>";
			$html .=       "<a href='javascript:;' data-src='$file'>";
			$html .=          "$duration<img$rel src='$src'>";
			$html .=       "</a>";
			$html .=    "</dt>";
			$html .=    "<dd><a href='$link'>$title</a></dd>";
			$html .= "</dl>";

			// Paramètres dépendant des catégories parentes.
			// On économise les requêtes SQL en mémorisant
			// les informations par album parent.
			if (array_key_exists($i['cat_id'], $albums))
			{
				$parents_infos = $albums[$i['cat_id']]['parents_infos'];
				$i['parent_commentable'] = $albums[$i['cat_id']]['parent_commentable'];
				$i['parent_downloadable'] = $albums[$i['cat_id']]['parent_downloadable'];
				$i['parent_votable'] = $albums[$i['cat_id']]['parent_votable'];
			}
			else
			{
				// Informations des catégories parentes.
				$parents_infos = Template::breadcrumb($i);
				$parents_infos[$i['cat_id']] =
				[
					'cat_id' => $i['cat_id'],
					'thumb_id' => $i['cat_thumb_id'],
					'cat_name' => $i['cat_name'],
					'cat_type' => $i['cat_type']
				];

				// Réglages.
				$i['cat_parents'] .= $i['cat_id'] . Parents::SEPARATOR;
				$i = [$i];
				Parents::settings($i);
				$i = $i[0];

				$albums[$i['cat_id']] =
				[
					'parents_infos' => $parents_infos,
					'parent_commentable' => $i['parent_commentable'],
					'parent_downloadable' =>  $i['parent_downloadable'],
					'parent_votable' =>  $i['parent_votable']
				];
			}

			// Informations EXIF.
			$i['item_exif_formated'] = self::_getExif(
				CONF_ALBUMS_PATH . '/' . $i['item_path'],
				(int) $i['item_type'],
				(string) $i['item_exif'],
				$orientation
			);

			// Informations formatées.
			HTML::specialchars($i);
			HTML::specialchars($parents_infos);
			$items[$id] = AdminItems::getFormatedInfos($i, $parents_infos);
			$items[$id]['user_avatar'] = Avatar::getURL(
				(int) $i['user_id'], (bool) $i['user_avatar']);
			$items[$id]['file_source'] = $file;
			$items[$id]['preview_size'] = Image::getResizedSize(
				(int) $i['item_width'], (int) $i['item_height'], 400, 400);
			$items[$id]['exif'] = $i['item_exif_formated'];

			// On supprimes les informations inutiles.
			unset($items[$id]['deactivated']);
			unset($items[$id]['id']);
			unset($items[$id]['link']);
			unset($items[$id]['thumb_src']);
			unset($items[$id]['thumb_size']);

			// Signature des informations.
			$items[$id]['hash'] = md5(json_encode($items[$id]));

			unset($i);
			$n++;
		}

		// Tags de la galerie.
		$tags = AdminAlbums::getTagsList();
		$tags = Utility::UTF8Array($tags);

		// Données.
		$data =
		[
			'status' => 'success',
			'cat_stats' => self::_getCatStats($cat_infos),
			'cat_thumb' => self::_getCatThumb($cat_infos),
			'html' => self::_gridLines($html, $n),
			'items' => $items,
			'lines' => (int) $nb_lines,
			'nb_items' => (int) $nb_items,
			'section_all_id' => array_map('intval', $section_all_id),
			'tags' => $tags,
			'thumb_size' => (int) $user_prefs['grid_tb_size']
		];
		if (!empty($_POST['browse']))
		{
			AdminAlbums::getCategoriesLists();
			$data['browse'] = HTML::specialchars(Template::$data['categories_browse']);
			$data['browse_levels'] = (int) Template::$data['categories_browse_levels'];
			$data['browse_subcats'] = (int) Template::$data['categories_browse_subcats'];
		}
		self::_printResponse($data, FALSE);
	}

	/**
	 * Récupération des informations des fichiers en attente.
	 *
	 * @return void
	 */
	public static function getItemsPending(): void
	{
		// Quelques vérifications.
		if (!isset($_POST['q']) || !Utility::checkPost('items_per_line', '`^\d{1,2}$`'))
		{
			self::_forbidden();
		}

		// Extraction de la requête.
		$_GET['q'] = $_POST['q'];
		App::request(['items-pending/{id}']);
		if (empty($_GET['section']))
		{
			self::_forbidden();
		}

		require_once(__DIR__ . '/Admin.class.php');
		require_once(__DIR__ . '/AdminItemsPending.class.php');

		// Préférences d'affichage.
		Admin::displayOptions('pending');
		$user_prefs = Auth::$infos['user_prefs']['pending'];

		// Nombre de fichiers par page.
		$nb_lines = (int) $user_prefs['grid_lines'];
		$nb_lines = $nb_lines > 0 ? $nb_lines : 1;
		$items_per_page = $_POST['items_per_line'] * $nb_lines;

		// Récupération des informations utiles de la catégorie.
		$cat_id = (int) $_GET['value_1'];
		$cat_infos = self::_getCatInfos($cat_id);

		// Chemin de la catégorie.
		$cat_path = $cat_id > 1 ? $cat_infos['cat_path'] . '/' : '';
		$cat_path = DB::likeEscape($cat_path) . '%';

		// Récupération du nombre de fichiers.
		$sql = "SELECT COUNT(*)
				  FROM {items_pending}
			 LEFT JOIN {categories} AS cat USING (cat_id)
				 WHERE cat.cat_id = ? OR cat_path LIKE ?";
		if (!DB::execute($sql, [$_GET['value_1'], $cat_path]))
		{
			self::_error();
		}
		$nb_items = DB::fetchVal();

		// Clause ORDER BY.
		$sql_order_by = sprintf(
			'pending_%s %s',
			$user_prefs['grid_order_by_column'],
			$user_prefs['grid_order_by_order']
		);

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

		// Récupération de l'identifiant de tous les fichiers de la section.
		$sql = 'SELECT pending_id
				  FROM {items_pending}
			 LEFT JOIN {categories} AS cat USING (cat_id)
				 WHERE cat.cat_id = ? OR cat_path LIKE ?
			  ORDER BY pending_id';
		if (!DB::execute($sql, [$_GET['value_1'], $cat_path]))
		{
			self::_error();
		}
		$section_all_id = DB::fetchCol('pending_id');

		// Récupération des informations utiles des fichiers de la page courante.
		$sql = "SELECT ip.*,
					   cat.cat_parents,
					   cat.cat_name,
					   CASE WHEN cat.cat_filemtime IS NULL
						    THEN 'category' ELSE 'album'
							 END AS cat_type,
		               u.user_login,
					   u.user_nickname,
					   u.user_avatar
				  FROM {items_pending} AS ip
			 LEFT JOIN {categories} AS cat
					ON ip.cat_id = cat.cat_id
			 LEFT JOIN {users} AS u
					ON ip.user_id = u.user_id
				 WHERE cat.cat_id = ? OR cat_path LIKE ?
			  ORDER BY $sql_order_by
				 LIMIT $sql_limit";
		if (!DB::execute($sql, [$_GET['value_1'], $cat_path]))
		{
			self::_error();
		}

		$html = '';
		$items = [];
		$n = 0;
		$albums = [];
		foreach (DB::fetchAll() as &$i)
		{
			// Informations EXIF.
			$i['pending_exif_formated'] = self::_getExif(
				GALLERY_ROOT . '/pending/' . $i['pending_file'],
				(int) $i['pending_type'],
				(string) $i['pending_exif'],
				$orientation
			);

			// Code HTML des vignettes.
			$id    = (int) $i['pending_id'];
			$file  = App::getFileSource('/pending/' . $i['pending_file']);
			$rel   = Item::isVideo($i['pending_type']) ? " data-video-id='$id'" : '';
			$src   = App::getThumbSource('pending', [
				'item_adddt' => $i['pending_adddt'],
				'item_id' => $i['pending_id'],
				'item_path' => $i['pending_file'],
				'item_type' => $i['pending_type'],
				'item_orientation' => $orientation
			], Admin::$thumbSize, Admin::$thumbQuality);
			$title = htmlspecialchars($i['pending_name']);

			$duration = $i['pending_duration']
				? App::formatDuration($i['pending_duration'])
				: '00:00';
			$duration  = Item::isVideo($i['pending_type'])
				? "<span class='duration' id='videoduration_$id'>$duration</span>"
				: "";

			$html .= "<dl id='obj_$id' class='obj'>";
			$html .=    "<dt>";
			$html .=       "<span><input type='checkbox'></span>";
			$html .=       "<a href='javascript:;' data-src='$file'>";
			$html .=          "$duration<img $rel src='$src'>";
			$html .=       "</a>";
			$html .=    "</dt>";
			$html .=    "<dd><a href='$file' class='viewer'>$title</a></dd>";
			$html .= "</dl>";

			// Informations formatées.
			HTML::specialchars($i);
			$items[$id] = AdminItemsPending::getFormatedInfos($i);
			$items[$id]['user_avatar'] = Avatar::getURL(
				(int) $i['user_id'], (bool) $i['user_avatar']);
			$items[$id]['file_source'] = $file;
			$items[$id]['preview_size'] = Image::getResizedSize(
				(int) $i['pending_width'], (int) $i['pending_height'], 400, 400);
			$items[$id]['exif'] = $i['pending_exif_formated'];

			// On supprime les informations inutiles.
			unset($items[$id]['id']);
			unset($items[$id]['link']);
			unset($items[$id]['thumb_src']);

			// Signature des informations.
			$items[$id]['hash'] = md5(json_encode($items[$id]));

			unset($i);
			$n++;
		}

		self::_printResponse(
		[
			'status' => 'success',
			'html' => self::_gridLines($html, $n),
			'items' => $items,
			'lines' => (int) $nb_lines,
			'nb_items' => (int) $nb_items,
			'section_all_id' => array_map('intval', $section_all_id),
			'thumb_size' => (int) $user_prefs['grid_tb_size']
		], FALSE);
	}

	/**
	 * Récupération des tags des fichiers sélectionnés.
	 *
	 * @return void
	 */
	public static function getTagsItems(): void
	{
		// Récupération des tags à partir de
		// l'identifiant des fichiers.
		if ($id = self::_getIds())
		{
			$sql = 'SELECT tag_name
					  FROM {tags}
				 LEFT JOIN {tags_items} USING (tag_id)
					 WHERE item_id IN (' . DB::inInt($id) . ')
				  ORDER BY LOWER(tag_name) ASC';
			if (DB::execute($sql))
			{
				$tags = DB::fetchCol('tag_name');
			}
		}

		self::_printResponse([
			'tags' => $tags ?? [],
			'text_no_tag' => __('Aucun tag trouvé.'),
			'status' => 'success'
		], FALSE);
	}

	/**
	 * Mise à jour des informations d'une catégorie.
	 *
	 * @return void
	 */
	public static function updateCategories(): void
	{
		// Vérifications.
		if (!isset($_POST['params']) || !is_array($_POST['params'])
		 || !Utility::checkPost('id', '`^\d{1,12}$`'))
		{
			self::_forbidden();
		}

		$id = (int) $_POST['id'];
		$error = FALSE;
		$success = FALSE;

		$r = function($a) use (&$error, &$success)
		{
			$error = ($a == -1) ? TRUE : $error;
			$success = ($a > 0) ? TRUE : $success;
		};

		// État.
		if (isset($_POST['params']['status']))
		{
			$r(Category::status([$id], (int) $_POST['params']['status'],
				!empty($_POST['params']['reset_item_pubdt'])));
		}

		// Déplacement.
		if (isset($_POST['params']['parent']))
		{
			$r(Category::move([$id], (int) $_POST['params']['parent']));
		}

		// Nouveau propriétaire.
		if (isset($_POST['params']['owner']))
		{
			$r(Category::owner([$id], (int) $_POST['params']['owner']));
		}

		// Nombre de vues à zéro.
		if (!empty($_POST['params']['reset_hits']))
		{
			$r(Category::resetHits([$id]));
		}

		// Nouvelle vignette.
		if (!empty($_POST['params']['thumb_new']))
		{
			$r(Category::thumb(
				[$_POST['params']['thumb_new'] => [$id]],
				!empty($_POST['params']['thumb_new_subcats'])
			));
		}

		// Édition des informations.
		if (isset($_POST['params']['cat_name'])
		 || isset($_POST['params']['cat_commentable'])
		 || isset($_POST['params']['cat_creatable'])
		 || isset($_POST['params']['cat_downloadable'])
		 || isset($_POST['params']['cat_votable'])
		 || isset($_POST['params']['cat_uploadable'])
		 || isset($_POST['params']['password_id']))
		{
			$data = [$id => $_POST['params']];
			$r(Category::edit($data));
		}

		// Géolocalisation.
		if (isset($_POST['params']['latitude']))
		{
			self::_checkCoordinates();
			$data = [$id => $_POST['params']];
			$r(Geolocation::editCategories($data));
		}

		// Suppression.
		if (isset($_POST['params']['delete']))
		{
			$r(Category::delete([$id]));
		}

		// Réponse.
		if ($error)
		{
			$report = 'error';
		}
		else if ($success)
		{
			$report = 'success';
		}
		else
		{
			$report = 'nochange';
		}
		self::_printResponse([
			'report' => $report,
			'status' => 'success',
		]);
	}

	/**
	 * Mise à jour des informations de plusieurs catégories.
	 *
	 * @return void
	 */
	public static function updateGroupCategories(): void
	{
		// Vérifications.
		if (!isset($_POST['params']) || !is_array($_POST['params']))
		{
			self::_forbidden();
		}

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

		$error = FALSE;
		$success = FALSE;
		$message = NULL;

		// Identifiant des fichiers sélectionnés.
		if (!$id = self::_getIds())
		{
			goto response;
		}

		$r = function($r) use (&$error, &$success)
		{
			$error = $r == -1 ? TRUE : $error;
			$success = $r > 0 ? TRUE : $success;
			return $r;
		};

		// Informations.
		// Propriétaire.
		// Géolocalisation.
		$geolocation = isset($_POST['params']['latitude']);
		$mass_edit = isset($_POST['params']['cat_name']);
		$owner = isset($_POST['params']['owner']);
		if ($geolocation || $mass_edit || $owner)
		{
			if ($geolocation)
			{
				self::_checkCoordinates();
				$coords = [];
				foreach ($id as &$val)
				{
					$coords[$val] = $_POST['params'];
				}
				$count = Geolocation::editCategories($coords);
			}
			else if ($mass_edit)
			{
				$order_by = self::_getCatOrderBy();
				$count = MassEdit::categories($id, $_POST['params'], $order_by);
			}
			else if ($owner)
			{
				$count = Category::owner($id, (int) $_POST['params']['owner']);
			}
			switch ($r($count))
			{
				case -1 :
					break;

				case 0 :
					$message = __('Aucune catégorie n\'a été modifiée.');
					break;

				case 1 :
					$message = __('1 catégorie a été modifiée.');
					break;

				default :
					$message = sprintf(__('%s catégories ont été modifiées.'), $count);
					break;
			}
		}

		// État.
		if (isset($_POST['params']['status']))
		{
			$status = (int) $_POST['params']['status'];
			switch ($r($count = Category::status($id, $status,
			!empty($_POST['params']['reset_item_pubdt']))))
			{
				case -1 :
					break;

				case 0 :
					$message = $status
						? __('Aucune catégorie n\'a été activée.')
						: __('Aucune catégorie n\'a été désactivée.');
					break;

				case 1 :
					$message = $status
						? __('1 catégorie a été activée.')
						: __('1 catégorie a été désactivée.');
					break;

				default :
					$message = $status
						? sprintf(__('%s catégories ont été activées.'), $count)
						: sprintf(__('%s catégories ont été désactivées.'), $count);
					break;
			}
		}

		// Déplacement.
		if (isset($_POST['params']['parent']))
		{
			switch ($r($count = Category::move($id, (int) $_POST['params']['parent'])))
			{
				case -1 :
					break;

				case 0 :
					$message = __('Aucune catégorie n\'a été déplacée.');
					break;

				case 1 :
					$message = __('1 catégorie a été déplacée.');
					break;

				default :
					$message = sprintf(__('%s catégories ont été déplacées.'), $count);
					break;
			}
		}

		// Nombre de vues à zéro.
		if (!empty($_POST['params']['reset_hits']))
		{
			$r(Category::resetHits($id));
		}

		// Nouvelle vignette.
		if (!empty($_POST['params']['thumb_new']))
		{
			$r(Category::thumb(
				[$_POST['params']['thumb_new'] => $id],
				!empty($_POST['params']['thumb_new_subcats'])
			));
		}

		// Suppression.
		if (isset($_POST['params']['delete']))
		{
			switch ($r($count = Category::delete($id)))
			{
				case -1 :
					break;

				case 0 :
					$message = __('Aucune catégorie n\'a été supprimée.');
					break;

				case 1 :
					$message = __('1 catégorie a été supprimée.');
					break;

				default :
					$message = sprintf(__('%s catégories ont été supprimées.'), $count);
					break;
			}
		}

		// Réponse.
		response:
		if ($error)
		{
			$report = 'error';
		}
		else if ($success)
		{
			$report = 'success';
		}
		else
		{
			$report = 'nochange';
		}
		self::_printResponse([
			'message' => $message,
			'report' => $report,
			'status' => 'success',
		]);
	}

	/**
	 * Mise à jour des informations de plusieurs fichiers.
	 *
	 * @return void
	 */
	public static function updateGroupItems(): void
	{
		// Vérifications.
		if (!isset($_POST['params']) || !is_array($_POST['params']))
		{
			self::_forbidden();
		}

		require_once(__DIR__ . '/AdminItems.class.php');
		Admin::displayOptions('album');

		$error = FALSE;
		$success = FALSE;
		$message = NULL;

		// Identifiant des fichiers sélectionnés.
		if (!$id = self::_getIds())
		{
			goto response;
		}

		$r = function($r) use (&$error, &$success)
		{
			$error = $r == -1 ? TRUE : $error;
			$success = $r > 0 ? TRUE : $success;
			return $r;
		};

		// Géolocalisation, édition en masse, nombre de vues et dates.
		$crtdt = !empty($_POST['params']['item_crtdt_new']);
		$expdt = !empty($_POST['params']['item_expdt_new']);
		$geolocation = isset($_POST['params']['latitude']);
		$mass_edit = isset($_POST['params']['item_name']);
		$owner = isset($_POST['params']['owner']);
		$pubdt = !empty($_POST['params']['item_pubdt_new']);
		$views = isset($_POST['params']['views']);
		$m_data = function() use (&$id)
		{
			$data = [];
			foreach ($id as &$val)
			{
				$data[$val] = $_POST['params'];
			}
			return $data;
		};
		if ($crtdt || $geolocation || $expdt || $mass_edit || $owner || $pubdt || $views)
		{
			if ($crtdt || $expdt || $pubdt)
			{
				$data = [];
				foreach (['crtdt', 'expdt', 'pubdt'] as $col)
				{
					if ($$col && !empty($_POST['params']['item_' . $col]))
					{
						foreach ($id as &$item_id)
						{
							$data[$item_id]['item_' . $col] = $_POST['params']['item_' . $col];
						}
					}
				}
				$count = $data ? Item::edit($data) : 0;
			}
			else if ($geolocation)
			{
				self::_checkCoordinates();
				$data = $m_data();
				$count = Geolocation::editItems($data);
			}
			else if ($mass_edit)
			{
				$count = MassEdit::items($id, $_POST['params'], self::_getItemsOrderBy());
			}
			else if ($owner)
			{
				$count = Item::owner($id, (int) $_POST['params']['owner']);
			}
			else if ($views)
			{
				$views = Utility::trimAll($_POST['params']['views']);
				$count = preg_match('`^\d+$`', $views)
					? Item::views($id, (int) $_POST['params']['views'])
					: 0;
			}
			switch ($r($count))
			{
				case -1 :
					break;

				case 0 :
					$message = __('Aucun fichier n\'a été modifié.');
					break;

				case 1 :
					$message = __('1 fichier a été modifié.');
					break;

				default :
					$message = sprintf(__('%s fichiers ont été modifiés.'), $count);
					break;
			}
		}

		// Tags.
		if (!empty($_POST['params']['tags_delete_all']))
		{
			$r(Tags::itemsDeleteAll($id));
		}
		foreach (['delete', 'add'] as $action)
		{
			if (isset($_POST['params']['tags_' . $action]))
			{
				$tags_str = Tags::sanitize($_POST['params']['tags_' . $action]);
				if (mb_strlen($tags_str))
				{
					$tags_array = explode(',', $tags_str);
					$tags = [];
					foreach ($tags_array as &$tag_name)
					{
						foreach ($id as &$item_id)
						{
							$tags[] = ['item_id' => $item_id, 'tag_name' => $tag_name];
						}
					}
					if ($tags)
					{
						$r(Tags::{'items' . ucfirst($action)}($tags));
					}
				}
			}
		}

		// État.
		if (isset($_POST['params']['status']))
		{
			$status = (int) $_POST['params']['status'];
			switch ($r($count = Item::status($id, $status,
			!empty($_POST['params']['reset_item_pubdt']))))
			{
				case -1 :
					break;

				case 0 :
					$message = $status
						? __('Aucun fichier n\'a été activé.')
						: __('Aucun fichier n\'a été désactivé.');
					break;

				case 1 :
					$message = $status
						? __('1 fichier a été activé.')
						: __('1 fichier a été désactivé.');
					break;

				default :
					$message = $status
						? sprintf(__('%s fichiers ont été activés.'), $count)
						: sprintf(__('%s fichiers ont été désactivés.'), $count);
					break;
			}
		}

		// Déplacement.
		if (isset($_POST['params']['parent']))
		{
			switch ($r($count = Item::move($id, (int) $_POST['params']['parent'])))
			{
				case -1 :
					break;

				case 0 :
					$message = __('Aucun fichier n\'a été déplacé.');
					break;

				case 1 :
					$message = __('1 fichier a été déplacé.');
					break;

				default :
					$message = sprintf(__('%s fichiers ont été déplacés.'), $count);
					break;
			}
		}

		// Mise à jour.
		if (isset($_POST['params']['update']))
		{
			switch ($r($count = Item::update($id)))
			{
				case -1 :
					break;

				case 0 :
					$message = __('Aucun fichier n\'a été mis à jour.');
					break;

				case 1 :
					$message = __('1 fichier a été mis à jour.');
					break;

				default :
					$message = sprintf(__('%s fichiers ont été mis à jour.'), $count);
					break;
			}
		}

		// Téléchargement de fichiers.
		if (isset($_POST['params']['download']))
		{
			if ($id)
			{
				Item::download('selection.zip', $id);
			}
			return;
		}

		// Suppression de fichiers.
		if (isset($_POST['params']['delete'])
		 || isset($_POST['params']['delete_resized']))
		{
			$count = isset($_POST['params']['delete'])
				? Item::delete($id)
				: Item::deleteResized($id);
			switch ($r($count))
			{
				case -1 :
					break;

				case 0 :
					$message = __('Aucun fichier n\'a été supprimé.');
					break;

				case 1 :
					$message = __('1 fichier a été supprimé.');
					break;

				default :
					$message = sprintf(__('%s fichiers ont été supprimés.'), $count);
					break;
			}
			if ($count > 0)
			{
				Config::$params['video_captures'] = 1;
				$captures = Video::captures();
			}
		}

		// Réponse.
		response:
		if ($error)
		{
			$report = 'error';
		}
		else if ($success)
		{
			$report = 'success';
		}
		else
		{
			$report = 'nochange';
		}
		self::_printResponse(
		[
			'captures' => $captures ?? [],
			'message' => $message,
			'report' => $report,
			'status' => 'success'
		]);
	}

	/**
	 * Gestion des fichiers en attente.
	 *
	 * @return void
	 */
	public static function updateGroupItemsPending(): void
	{
		// Vérifications.
		if (!isset($_POST['params']) || !is_array($_POST['params']))
		{
			self::_forbidden();
		}

		$error = FALSE;
		$success = FALSE;
		$message = NULL;

		// Identifiant des fichiers sélectionnés.
		if (!$id = self::_getIds())
		{
			goto response;
		}

		$r = function($r) use (&$error, &$success)
		{
			$error = $r == -1 ? TRUE : $error;
			$success = $r > 0 ? TRUE : $success;
			return $r;
		};

		// État.
		if (isset($_POST['params']['status']))
		{
			$status = (int) $_POST['params']['status'];
			switch ($r($count = Pending::add($id, $status)))
			{
				case -1 :
					break;

				case 0 :
					$message = __('Aucun fichier n\'a été ajouté à la galerie.');
					break;

				case 1 :
					$message = __('1 fichier a été ajouté à la galerie.');
					break;

				default :
					$message = sprintf(__('%s fichiers ont été ajoutés à la galerie.'), $count);
					break;
			}
		}

		// Suppression.
		if (isset($_POST['params']['delete']))
		{
			switch ($r($count = Pending::delete($id)))
			{
				case -1 :
					break;

				case 0 :
					$message = __('Aucun fichier n\'a été supprimé.');
					break;

				case 1 :
					$message = __('1 fichier a été supprimé.');
					break;

				default :
					$message = sprintf(__('%s fichiers ont été supprimés.'), $count);
					break;
			}
		}

		// Réponse.
		response:
		if ($error)
		{
			$report = 'error';
		}
		else if ($success)
		{
			$report = 'success';
		}
		else
		{
			$report = 'nochange';
		}
		self::_printResponse(
		[
			'message' => $message,
			'report' => $report,
			'status' => 'success'
		]);
	}

	/**
	 * Mise à jour des informations d'un fichier.
	 *
	 * @return void
	 */
	public static function updateItems(): void
	{
		// Vérifications.
		if (!isset($_POST['params']) || !is_array($_POST['params'])
		|| !Utility::checkPost('id', '`^\d{1,12}$`'))
		{
			self::_forbidden();
		}

		$id = (int) $_POST['id'];
		$error = FALSE;
		$success = FALSE;

		$r = function($r) use (&$error, &$success)
		{
			$error = $r == -1 ? TRUE : $error;
			$success = $r > 0 ? TRUE : $success;
		};

		// État.
		if (isset($_POST['params']['status']))
		{
			$r(Item::status([$id], (int) $_POST['params']['status'],
				!empty($_POST['params']['reset_item_pubdt'])));
		}

		// Album.
		if (isset($_POST['params']['parent']))
		{
			$r(Item::move([$id], (int) $_POST['params']['parent']));
		}

		// Nombre de vues.
		if (isset($_POST['params']['hits']))
		{
			$r(Item::views([$id], (int) $_POST['params']['hits']));
		}

		// Nouveau propriétaire.
		if (isset($_POST['params']['owner']))
		{
			$r(Item::owner([$id], (int) $_POST['params']['owner']));
		}

		// Édition des informations, tags et réglages.
		if (isset($_POST['params']['item_name'])
		 || isset($_POST['params']['item_crtdt'])
		 || isset($_POST['params']['item_commentable'])
		 || isset($_POST['params']['item_downloadable'])
		 || isset($_POST['params']['item_votable'])
		 || isset($_POST['params']['tags']))
		{
			$data = [$id => $_POST['params']];
			$r(Item::edit($data));
		}

		// Vignette des catégories parentes.
		if (isset($_POST['params']['thumb']))
		{
			$cat_thumbs = ['item' => [$id => []], 'random' => [$id => []]];
			foreach ($_POST['params']['thumb'] as $cat_id => &$value)
			{
				$cat_thumbs[$value ? 'item' : 'random'][$id][] = $cat_id;
			}
			$r(Category::thumb($cat_thumbs));
		}

		// Géolocalisation.
		if (isset($_POST['params']['latitude']))
		{
			self::_checkCoordinates();
			$data = [$id => $_POST['params']];
			$r(Geolocation::editItems($data));
		}

		// Mise à jour.
		if (isset($_POST['params']['update']))
		{
			$r(Item::update([$id]));
		}

		// Suppression des vignettes.
		if (isset($_POST['params']['delete_resized']))
		{
			$r($count = Item::deleteResized([$id]));
			if ($count > 0)
			{
				Config::$params['video_captures'] = 1;
				$captures = Video::captures();
			}
		}

		// Suppression.
		if (isset($_POST['params']['delete']))
		{
			$r(Item::delete([$id]));
		}

		// Réponse.
		if ($error)
		{
			$report = 'error';
		}
		else if ($success)
		{
			$report = 'success';
		}
		else
		{
			$report = 'nochange';
		}
		self::_printResponse(
		[
			'captures' => $captures ?? [],
			'report' => $report,
			'status' => 'success',
		]);
	}



	/**
	 * Vérifie le format des coordonnées de géolocalisation.
	 *
	 * @return void
	 */
	private static function _checkCoordinates(): void
	{
		$lat = Utility::trimAll($_POST['params']['latitude'] ?? '');
		$long = Utility::trimAll($_POST['params']['longitude'] ?? '');
		if (($lat !== '' || $long !== '')
		&& is_string($coords = Geolocation::checkCoordinates($lat, $long)))
		{
			self::_printResponse(
			[
				'message' => $coords,
				'report' => 'warning',
				'status' => 'success'
			]);
		}
	}

	/**
	 * Retourne la clause ORDER BY pour la récupération des catégories.
	 *
	 * @return string
	 */
	private static function _getCatOrderBy(): string
	{
		$prefs = Auth::$infos['user_prefs']['category'];

		return AdminCategory::getOrderBy(
			$prefs['grid_order_by_column'],
			$prefs['grid_order_by_order']
		);
	}

	/**
	 * Récupération des informations de la catégorie $cat_id.
	 *
	 * @param int $cat_id
	 *
	 * @return array
	 */
	private static function _getCatInfos(int $cat_id): array
	{
		$sql = 'SELECT cat.*,
					   CASE WHEN cat.cat_filemtime IS NULL
					        THEN "category" ELSE "album"
						     END AS cat_type,
					   i.item_id,
					   i.item_width,
					   i.item_height,
					   i.item_orientation,
					   i.item_type,
					   i.item_path,
					   i.item_adddt
				  FROM {categories} AS cat
			 LEFT JOIN {items} AS i
					ON cat.thumb_id = i.item_id
				 WHERE cat.cat_id = ?';
		if (!DB::execute($sql, $cat_id))
		{
			self::_error();
		}
		return DB::fetchRow();
	}

	/**
	 * Formatage des informations d'une catégorie.
	 *
	 * @param array $cat_infos
	 *
	 * @return array
	 */
	private static function _getCatStats(array $cat_infos): array
	{
		require_once(__DIR__ . '/AdminCategory.class.php');

		return AdminCategory::getFormatedStats($cat_infos);
	}

	/**
	 * Informations de vignette d'une catégorie.
	 *
	 * @param array $cat_infos
	 *
	 * @return array
	 */
	private static function _getCatThumb(array $cat_infos): array
	{
		return
		[
			'link' => $cat_infos['thumb_id'] == 0
				? App::getFileSource('/cache/external/'
					. App::hashFilename((string) $cat_infos['cat_id'], ['ext']))
				: App::getFileSource((string) $cat_infos['item_path']),
			'source' => $cat_infos['thumb_id'] == -1
				? ''
				: App::getThumbSource('cat', $cat_infos, Admin::$thumbSize, Admin::$thumbQuality),
			'status' => (int) $cat_infos['cat_status'],
			'type' => Item::isVideo($cat_infos['item_type']) ? 'video' : 'image',
			'video_vertical' => Item::isVideo($cat_infos['item_type'])
				&& $cat_infos['item_height'] > $cat_infos['item_width'],
		];
	}

	/**
	 * Retourne les informations EXIF formatées d'un fichier.
	 *
	 * @param string $file
	 * @param int $type
	 * @param string $db_exif
	 * @param int $orientation
	 *
	 * @return mixed
	 */
	private static function _getExif(string $file, int $type, string $db_exif, &$orientation)
	{
		static $exif_order, $exif_params;
		if (!$exif_order)
		{
			$exif_order = Config::getDBParamDefault('exif_order');
			$exif_params = Config::getDBParamDefault('exif_params');
			foreach ($exif_params as &$p)
			{
				$p['status'] = 1;
			}
		}

		$exif = $metadata = NULL;
		$orientation = 1;
		if (Utility::isJson($db_exif))
		{
			$metadata = new Metadata($file, ['exif' => Utility::jsonDecode($db_exif)]);
		}
		else if (in_array($type, [Item::TYPE_JPEG, Item::TYPE_WEBP]) && file_exists($file))
		{
			$metadata = new Metadata($file);
			$orientation = (int) $metadata->getExifValue('orientation');
		}
		if ($metadata)
		{
			$exif = $metadata->getExifFormated($exif_params);
			$exif = Metadata::getSorted($exif, $exif_order);
		}
		return $exif;
	}

	/**
	 * Retourne les identifiants envoyés sous forme d'un tableau.
	 *
	 * @return array
	 */
	private static function _getIds(): array
	{
		if (!isset($_POST['id']) || !is_string($_POST['id']))
		{
			return [];
		}

		$id = explode(',', $_POST['id']);
		$id = array_unique(array_map('intval', $id));
		if (($key = array_search(0, $id)) !== FALSE)
		{
			unset($id[$key]);
		}
		sort($id);
		if (count($id) < 1)
		{
			return [];
		}

		return $id;
	}

	/**
	 * Retourne la clause ORDER BY pour la récupération des fichiers.
	 *
	 * @return string
	 */
	private static function _getItemsOrderBy(): string
	{
		$prefs = Auth::$infos['user_prefs']['album'];

		return AdminItems::getOrderBy(
			$prefs['grid_order_by_column'],
			$prefs['grid_order_by_order']
		);
	}

	/**
	 * Dans le mode d'affichage "grille", comble la dernière
	 * ligne de vignettes avec de "fausses vignettes", de manière
	 * à toujours avoir des vignettes bien alignées verticalement.
	 *
	 * @param string $html
	 * @param int $n
	 *
	 * @return string
	 */
	private static function _gridLines(string $html, int $n): string
	{
		$items_per_line = $_POST['items_per_line'] < 1 ? 1 : $_POST['items_per_line'];
		$items_lines = ceil($n / $items_per_line);
		$diff = ($items_lines * $items_per_line) - $n;
		if ($diff > 0)
		{
			$html .= str_repeat('<dl><dt><a></a></dt></dl>', (int) $diff);
		}
		return $html;
	}

	/**
	 * Redirection vers la racine de la galerie.
	 *
	 * @return void
	 */
	private static function _redirectToCatOne(): void
	{
		self::_printResponse([
			'status' => 'success',
			'redirect' => App::getURL(AdminAlbums::getURLSectionCatOne())
		]);
	}
}
?>