<?php

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao;

use Contao\CoreBundle\Exception\AccessDeniedException;
use Contao\CoreBundle\Exception\ResponseException;
use Contao\CoreBundle\Picker\PickerInterface;
use Contao\Database\Result;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
use Symfony\Component\HttpFoundation\Session\Session;

/**
 * Provide methods to manage back end controllers.
 *
 * @property Ajax $objAjax
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
abstract class Backend extends Controller
{

	/**
	 * Load the database object
	 */
	protected function __construct()
	{
		parent::__construct();
		$this->import(Database::class, 'Database');
		$this->setStaticUrls();
	}

	/**
	 * Return the current theme as string
	 *
	 * @return string The name of the theme
	 */
	public static function getTheme()
	{
		$theme = Config::get('backendTheme');
		$rootDir = System::getContainer()->getParameter('kernel.project_dir');

		if ($theme != '' && $theme != 'flexible' && is_dir($rootDir . '/system/themes/' . $theme))
		{
			return $theme;
		}

		return 'flexible';
	}

	/**
	 * Return the back end themes as array
	 *
	 * @return array An array of available back end themes
	 */
	public static function getThemes()
	{
		$arrReturn = array();
		$rootDir = System::getContainer()->getParameter('kernel.project_dir');
		$arrThemes = scan($rootDir . '/system/themes');

		foreach ($arrThemes as $strTheme)
		{
			if (strncmp($strTheme, '.', 1) === 0 || !is_dir($rootDir . '/system/themes/' . $strTheme))
			{
				continue;
			}

			$arrReturn[$strTheme] = $strTheme;
		}

		return $arrReturn;
	}

	/**
	 * Return the TinyMCE language
	 *
	 * @return string
	 */
	public static function getTinyMceLanguage()
	{
		$lang = $GLOBALS['TL_LANGUAGE'];

		if ($lang == '')
		{
			return 'en';
		}

		$lang = str_replace('-', '_', $lang);
		$rootDir = System::getContainer()->getParameter('kernel.project_dir');

		// The translation exists
		if (file_exists($rootDir . '/assets/tinymce4/js/langs/' . $lang . '.js'))
		{
			return $lang;
		}

		if (($short = substr($GLOBALS['TL_LANGUAGE'], 0, 2)) != $lang)
		{
			// Try the short tag, e.g. "de" instead of "de_CH"
			if (file_exists($rootDir . '/assets/tinymce4/js/langs/' . $short . '.js'))
			{
				return $short;
			}
		}
		elseif (($long = $short . '_' . strtoupper($short)) != $lang)
		{
			// Try the long tag, e.g. "fr_FR" instead of "fr" (see #6952)
			if (file_exists($rootDir . '/assets/tinymce4/js/langs/' . $long . '.js'))
			{
				return $long;
			}
		}

		// Fallback to English
		return 'en';
	}

	/**
	 * Get the Ace code editor type from a file extension
	 *
	 * @param string $ext
	 *
	 * @return string
	 */
	public static function getAceType($ext)
	{
		switch ($ext)
		{
			case 'css':
			case 'diff':
			case 'html':
			case 'ini':
			case 'java':
			case 'json':
			case 'less':
			case 'mysql':
			case 'php':
			case 'scss':
			case 'sql':
			case 'twig':
			case 'xml':
			case 'yaml':
				return $ext;
				break;

			case 'js':
			case 'javascript':
				return 'javascript';
				break;

			case 'md':
			case 'markdown':
				return 'markdown';
				break;

			case 'cgi':
			case 'pl':
				return 'perl';
				break;

			case 'py':
				return 'python';
				break;

			case 'txt':
				return 'text';
				break;

			case 'c': case 'cc': case 'cpp': case 'c++':
			case 'h': case 'hh': case 'hpp': case 'h++':
				return 'c_cpp';
				break;

			case 'html5':
			case 'xhtml':
				return 'php';
				break;

			case 'svg':
			case 'svgz':
				return 'xml';
				break;

			default:
				return 'text';
				break;
		}
	}

	/**
	 * Return a list of TinyMCE templates as JSON string
	 *
	 * @return string
	 */
	public static function getTinyTemplates()
	{
		$strDir = Config::get('uploadPath') . '/tiny_templates';
		$rootDir = System::getContainer()->getParameter('kernel.project_dir');

		if (!is_dir($rootDir . '/' . $strDir))
		{
			return '';
		}

		$arrFiles = array();
		$arrTemplates = scan($rootDir . '/' . $strDir);

		foreach ($arrTemplates as $strFile)
		{
			if (strncmp('.', $strFile, 1) !== 0 && is_file($rootDir . '/' . $strDir . '/' . $strFile))
			{
				$arrFiles[] = '{ title: "' . $strFile . '", url: "' . $strDir . '/' . $strFile . '" }';
			}
		}

		return implode(",\n", $arrFiles) . "\n";
	}

	/**
	 * Add the request token to the URL
	 *
	 * @param string  $strRequest
	 * @param boolean $blnAddRef
	 * @param array   $arrUnset
	 *
	 * @return string
	 */
	public static function addToUrl($strRequest, $blnAddRef=true, $arrUnset=array())
	{
		// Unset the "no back button" flag
		$arrUnset[] = 'nb';

		return parent::addToUrl($strRequest . (($strRequest != '') ? '&' : '') . 'rt=' . REQUEST_TOKEN, $blnAddRef, $arrUnset);
	}

	/**
	 * Handle "runonce" files
	 *
	 * @throws \Exception
	 */
	public static function handleRunOnce()
	{
		try
		{
			$files = System::getContainer()->get('contao.resource_locator')->locate('config/runonce.php', null, false);
		}
		catch (\InvalidArgumentException $e)
		{
			return;
		}

		foreach ($files as $file)
		{
			try
			{
				include $file;
			}
			catch (\Exception $e) {}

			$strRelpath = StringUtil::stripRootDir($file);

			if (!unlink($file))
			{
				throw new \Exception("The file $strRelpath cannot be deleted. Please remove the file manually and correct the file permission settings on your server.");
			}

			System::log("File $strRelpath ran once and has then been removed successfully", __METHOD__, TL_GENERAL);
		}
	}

	/**
	 * Open a back end module and return it as HTML
	 *
	 * @param string               $module
	 * @param PickerInterface|null $picker
	 *
	 * @return string
	 *
	 * @throws AccessDeniedException
	 */
	protected function getBackendModule($module, PickerInterface $picker = null)
	{
		$arrModule = array();

		foreach ($GLOBALS['BE_MOD'] as &$arrGroup)
		{
			if (isset($arrGroup[$module]))
			{
				$arrModule = &$arrGroup[$module];
				break;
			}
		}

		unset($arrGroup);

		$this->import(BackendUser::class, 'User');
		$blnAccess = (isset($arrModule['disablePermissionChecks']) && $arrModule['disablePermissionChecks'] === true) || $this->User->hasAccess($module, 'modules');

		// Check whether the current user has access to the current module
		if (!$blnAccess)
		{
			throw new AccessDeniedException('Back end module "' . $module . '" is not allowed for user "' . $this->User->username . '".');
		}

		// The module does not exist
		if (empty($arrModule))
		{
			throw new \InvalidArgumentException('Back end module "' . $module . '" is not defined in the BE_MOD array');
		}

		/** @var Session $objSession */
		$objSession = System::getContainer()->get('session');

		$arrTables = (array) $arrModule['tables'];
		$strTable = Input::get('table') ?: $arrTables[0];
		$id = (!Input::get('act') && Input::get('id')) ? Input::get('id') : $objSession->get('CURRENT_ID');

		// Store the current ID in the current session
		if ($id != $objSession->get('CURRENT_ID'))
		{
			$objSession->set('CURRENT_ID', $id);
		}

		\define('CURRENT_ID', (Input::get('table') ? $id : Input::get('id')));
		$this->Template->headline = $GLOBALS['TL_LANG']['MOD'][$module][0];

		// Add the module style sheet
		if (isset($arrModule['stylesheet']))
		{
			foreach ((array) $arrModule['stylesheet'] as $stylesheet)
			{
				$GLOBALS['TL_CSS'][] = $stylesheet;
			}
		}

		// Add module javascript
		if (isset($arrModule['javascript']))
		{
			foreach ((array) $arrModule['javascript'] as $javascript)
			{
				$GLOBALS['TL_JAVASCRIPT'][] = $javascript;
			}
		}

		$dc = null;

		// Create the data container object
		if ($strTable != '')
		{
			if (!\in_array($strTable, $arrTables))
			{
				throw new AccessDeniedException('Table "' . $strTable . '" is not allowed in module "' . $module . '".');
			}

			// Load the language and DCA file
			System::loadLanguageFile($strTable);
			$this->loadDataContainer($strTable);

			// Include all excluded fields which are allowed for the current user
			if ($GLOBALS['TL_DCA'][$strTable]['fields'])
			{
				foreach ($GLOBALS['TL_DCA'][$strTable]['fields'] as $k=>$v)
				{
					if ($v['exclude'])
					{
						if ($this->User->hasAccess($strTable.'::'.$k, 'alexf'))
						{
							if ($strTable == 'tl_user_group')
							{
								$GLOBALS['TL_DCA'][$strTable]['fields'][$k]['orig_exclude'] = $GLOBALS['TL_DCA'][$strTable]['fields'][$k]['exclude'];
							}

							$GLOBALS['TL_DCA'][$strTable]['fields'][$k]['exclude'] = false;
						}
					}
				}
			}

			// Fabricate a new data container object
			if ($GLOBALS['TL_DCA'][$strTable]['config']['dataContainer'] == '')
			{
				$this->log('Missing data container for table "' . $strTable . '"', __METHOD__, TL_ERROR);
				trigger_error('Could not create a data container object', E_USER_ERROR);
			}

			$dataContainer = 'DC_' . $GLOBALS['TL_DCA'][$strTable]['config']['dataContainer'];

			/** @var DataContainer $dc */
			$dc = new $dataContainer($strTable, $arrModule);

			if ($picker !== null && $dc instanceof DataContainer)
			{
				$dc->initPicker($picker);
			}
		}

		// Wrap the existing headline
		$this->Template->headline = '' . $this->Template->headline . '';

		// AJAX request
		if ($_POST && Environment::get('isAjaxRequest'))
		{
			$this->objAjax->executePostActions($dc);
		}

		// Trigger the module callback
		elseif (class_exists($arrModule['callback']))
		{
			/** @var Module $objCallback */
			$objCallback = new $arrModule['callback']($dc);

			$this->Template->main .= $objCallback->generate();
		}

		// Custom action (if key is not defined in config.php the default action will be called)
		elseif (Input::get('key') && isset($arrModule[Input::get('key')]))
		{
			$objCallback = System::importStatic($arrModule[Input::get('key')][0]);
			$response = $objCallback->{$arrModule[Input::get('key')][1]}($dc);

			if ($response instanceof RedirectResponse)
			{
				throw new ResponseException($response);
			}
			elseif ($response instanceof Response)
			{
				$response = $response->getContent();
			}

			$this->Template->main .= $response;

			// Add the name of the parent element
			if (isset($_GET['table']) && !empty($GLOBALS['TL_DCA'][$strTable]['config']['ptable']) && \in_array(Input::get('table'), $arrTables) && Input::get('table') != $arrTables[0])
			{
				$objRow = $this->Database->prepare("SELECT * FROM " . $GLOBALS['TL_DCA'][$strTable]['config']['ptable'] . " WHERE id=(SELECT pid FROM $strTable WHERE id=?)")
										 ->limit(1)
										 ->execute(Input::get('id'));

				if ($objRow->title != '')
				{
					$this->Template->headline .= ' › ' . $objRow->title . '';
				}
				elseif ($objRow->name != '')
				{
					$this->Template->headline .= ' › ' . $objRow->name . '';
				}
			}

			// Add the name of the submodule
			$this->Template->headline .= ' › ' . sprintf($GLOBALS['TL_LANG'][$strTable][Input::get('key')][1], Input::get('id')) . '';
		}

		// Default action
		elseif (\is_object($dc))
		{
			$act = Input::get('act');

			if ($act == '' || $act == 'paste' || $act == 'select')
			{
				$act = ($dc instanceof \listable) ? 'showAll' : 'edit';
			}

			switch ($act)
			{
				case 'delete':
				case 'show':
				case 'showAll':
				case 'undo':
					if (!$dc instanceof \listable)
					{
						$this->log('Data container ' . $strTable . ' is not listable', __METHOD__, TL_ERROR);
						trigger_error('The current data container is not listable', E_USER_ERROR);
					}
					break;

				case 'create':
				case 'cut':
				case 'cutAll':
				case 'copy':
				case 'copyAll':
				case 'move':
				case 'edit':
					if (!$dc instanceof \editable)
					{
						$this->log('Data container ' . $strTable . ' is not editable', __METHOD__, TL_ERROR);
						trigger_error('The current data container is not editable', E_USER_ERROR);
					}
					break;
			}

			// Add the name of the parent elements
			if ($strTable && \in_array($strTable, $arrTables) && $strTable != $arrTables[0])
			{
				$trail = array();

				$pid = $dc->id;
				$table = $strTable;
				$ptable = (Input::get('act') != 'edit') ? $GLOBALS['TL_DCA'][$strTable]['config']['ptable'] : $strTable;

				while ($ptable && !\in_array($GLOBALS['TL_DCA'][$table]['list']['sorting']['mode'], array(5, 6)))
				{
					$objRow = $this->Database->prepare("SELECT * FROM " . $ptable . " WHERE id=?")
											 ->limit(1)
											 ->execute($pid);

					// Add only parent tables to the trail
					if ($table != $ptable)
					{
						// Add table name
						if (isset($GLOBALS['TL_LANG']['MOD'][$table]))
						{
							$trail[] = ' › '. $GLOBALS['TL_LANG']['MOD'][$table] . '';
						}

						// Add object title or name
						if ($objRow->title != '')
						{
							$trail[] = ' › ' . $objRow->title . '';
						}
						elseif ($objRow->name != '')
						{
							$trail[] = ' › ' . $objRow->name . '';
						}
						elseif ($objRow->headline != '')
						{
							$trail[] = ' › ' . $objRow->headline . '';
						}

					}

					$this->loadDataContainer($ptable);

					// Next parent table
					$pid = $objRow->pid;
					$table = $ptable;
					$ptable = ($GLOBALS['TL_DCA'][$ptable]['config']['dynamicPtable']) ? $objRow->ptable : $GLOBALS['TL_DCA'][$ptable]['config']['ptable'];
				}

				// Add the last parent table
				if (isset($GLOBALS['TL_LANG']['MOD'][$table]))
				{
					$trail[] = ' › '. $GLOBALS['TL_LANG']['MOD'][$table] . '';
				}

				// Add the breadcrumb trail in reverse order
				foreach (array_reverse($trail) as $breadcrumb)
				{
					$this->Template->headline .= $breadcrumb;
				}
			}

			// Add the current action
			if (Input::get('act') == 'editAll')
			{
				if (isset($GLOBALS['TL_LANG']['MSC']['all'][0]))
				{
					$this->Template->headline .= ' › ' . $GLOBALS['TL_LANG']['MSC']['all'][0] . '';
				}
			}
			elseif (Input::get('act') == 'overrideAll')
			{
				if (isset($GLOBALS['TL_LANG']['MSC']['all_override'][0]))
				{
					$this->Template->headline .= ' › ' . $GLOBALS['TL_LANG']['MSC']['all_override'][0] . '';
				}
			}
			else
			{
				if (Input::get('id'))
				{
					if (Input::get('do') == 'files' || Input::get('do') == 'tpl_editor')
					{
						// Handle new folders (see #7980)
						if (strpos(Input::get('id'), '__new__') !== false)
						{
							$this->Template->headline .= ' › ' . \dirname(Input::get('id')) . ' › ' . $GLOBALS['TL_LANG'][$strTable]['new'][1] . '';
						}
						else
						{
							$this->Template->headline .= ' › ' . Input::get('id') . '';
						}
					}
					elseif (isset($GLOBALS['TL_LANG'][$strTable][$act][1]))
					{
						$this->Template->headline .= ' › ' . sprintf($GLOBALS['TL_LANG'][$strTable][$act][1], Input::get('id')) . '';
					}
				}
				elseif (Input::get('pid'))
				{
					if (Input::get('do') == 'files' || Input::get('do') == 'tpl_editor')
					{
						if (Input::get('act') == 'move')
						{
							$this->Template->headline .= ' › ' . Input::get('pid') . ' › ' . $GLOBALS['TL_LANG'][$strTable]['move'][1] . '';
						}
						else
						{
							$this->Template->headline .= ' › ' . Input::get('pid') . '';
						}
					}
					elseif (isset($GLOBALS['TL_LANG'][$strTable][$act][1]))
					{
						$this->Template->headline .= ' › ' . sprintf($GLOBALS['TL_LANG'][$strTable][$act][1], Input::get('pid')) . '';
					}
				}
			}

			return $dc->$act();
		}

		return null;
	}

	/**
	 * Get all searchable pages and return them as array
	 *
	 * @param integer $pid
	 * @param string  $domain
	 * @param boolean $blnIsSitemap
	 *
	 * @return array
	 */
	public static function findSearchablePages($pid=0, $domain='', $blnIsSitemap=false)
	{
		$objPages = PageModel::findPublishedByPid($pid, array('ignoreFePreview'=>true));

		if ($objPages === null)
		{
			return array();
		}

		$arrPages = array();

		// Recursively walk through all subpages
		foreach ($objPages as $objPage)
		{
			if ($objPage->type == 'regular')
			{
				// Searchable and not protected
				if ((!$objPage->noSearch || $blnIsSitemap) && (!$objPage->protected || (Config::get('indexProtected') && (!$blnIsSitemap || $objPage->sitemap == 'map_always'))) && (!$blnIsSitemap || $objPage->sitemap != 'map_never') && !$objPage->requireItem)
				{
					$arrPages[] = $objPage->getAbsoluteUrl();

					// Get articles with teaser
					if (($objArticles = ArticleModel::findPublishedWithTeaserByPid($objPage->id, array('ignoreFePreview'=>true))) !== null)
					{
						foreach ($objArticles as $objArticle)
						{
							$arrPages[] = $objPage->getAbsoluteUrl('/articles/' . ($objArticle->alias ?: $objArticle->id));
						}
					}
				}
			}

			// Get subpages
			if ((!$objPage->protected || Config::get('indexProtected')) && ($arrSubpages = static::findSearchablePages($objPage->id, $domain, $blnIsSitemap)))
			{
				$arrPages = array_merge($arrPages, $arrSubpages);
			}
		}

		return $arrPages;
	}

	/**
	 * Add the file meta information to the request
	 *
	 * @param string  $strUuid
	 * @param string  $strPtable
	 * @param integer $intPid
	 *
	 * @deprecated Deprecated since Contao 4.4, to be removed in Contao 5.0.
	 */
	public static function addFileMetaInformationToRequest($strUuid, $strPtable, $intPid)
	{
		@trigger_error('Using Backend::addFileMetaInformationToRequest() has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

		$objFile = FilesModel::findByUuid($strUuid);

		if ($objFile === null)
		{
			return;
		}

		$arrMeta = StringUtil::deserialize($objFile->meta);

		if (empty($arrMeta))
		{
			return;
		}

		$objPage = null;

		if ($strPtable == 'tl_article')
		{
			$objPage = PageModel::findOneBy(array('tl_page.id=(SELECT pid FROM tl_article WHERE id=?)'), $intPid);
		}
		else
		{
			// HOOK: support custom modules
			if (isset($GLOBALS['TL_HOOKS']['addFileMetaInformationToRequest']) && \is_array($GLOBALS['TL_HOOKS']['addFileMetaInformationToRequest']))
			{
				foreach ($GLOBALS['TL_HOOKS']['addFileMetaInformationToRequest'] as $callback)
				{
					if (($val = System::importStatic($callback[0])->{$callback[1]}($strPtable, $intPid)) !== false)
					{
						$objPage = $val;
					}
				}

				if ($objPage instanceof Result && $objPage->numRows < 1)
				{
					return;
				}

				if (\is_object($objPage) && !($objPage instanceof PageModel))
				{
					$objPage = PageModel::findByPk($objPage->id);
				}
			}
		}

		if ($objPage === null)
		{
			return;
		}

		$objPage->loadDetails();

		// Convert the language to a locale (see #5678)
		$strLanguage = str_replace('-', '_', $objPage->rootLanguage);

		if (isset($arrMeta[$strLanguage]))
		{
			if (!empty($arrMeta[$strLanguage]['title']) && Input::post('title') == '')
			{
				Input::setPost('title', $arrMeta[$strLanguage]['title']);
			}

			if (!empty($arrMeta[$strLanguage]['alt']) && Input::post('alt') == '')
			{
				Input::setPost('alt', $arrMeta[$strLanguage]['alt']);
			}

			if (!empty($arrMeta[$strLanguage]['caption']) && Input::post('caption') == '')
			{
				Input::setPost('caption', $arrMeta[$strLanguage]['caption']);
			}
		}
	}

	/**
	 * Add a breadcrumb menu to the page tree
	 *
	 * @param string $strKey
	 *
	 * @throws AccessDeniedException
	 * @throws \RuntimeException
	 */
	public static function addPagesBreadcrumb($strKey='tl_page_node')
	{
		/** @var AttributeBagInterface $objSession */
		$objSession = System::getContainer()->get('session')->getBag('contao_backend');

		// Set a new node
		if (isset($_GET['pn']))
		{
			// Check the path (thanks to Arnaud Buchoux)
			if (Validator::isInsecurePath(Input::get('pn', true)))
			{
				throw new \RuntimeException('Insecure path ' . Input::get('pn', true));
			}

			$objSession->set($strKey, Input::get('pn', true));
			Controller::redirect(preg_replace('/&pn=[^&]*/', '', Environment::get('request')));
		}

		$intNode = $objSession->get($strKey);

		if ($intNode < 1)
		{
			return;
		}

		// Check the path (thanks to Arnaud Buchoux)
		if (Validator::isInsecurePath($intNode))
		{
			throw new \RuntimeException('Insecure path ' . $intNode);
		}

		$arrIds   = array();
		$arrLinks = array();
		$objUser  = BackendUser::getInstance();

		// Generate breadcrumb trail
		if ($intNode)
		{
			$intId = $intNode;
			$objDatabase = Database::getInstance();

			do
			{
				$objPage = $objDatabase->prepare("SELECT * FROM tl_page WHERE id=?")
									   ->limit(1)
									   ->execute($intId);

				if ($objPage->numRows < 1)
				{
					// Currently selected page does not exist
					if ($intId == $intNode)
					{
						$objSession->set($strKey, 0);

						return;
					}

					break;
				}

				$arrIds[] = $intId;

				// No link for the active page
				if ($objPage->id == $intNode)
				{
					$arrLinks[] = self::addPageIcon($objPage->row(), '', null, '', true) . ' ' . $objPage->title;
				}
				else
				{
					$arrLinks[] = self::addPageIcon($objPage->row(), '', null, '', true) . ' id) . '" title="'.StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['selectNode']).'">' . $objPage->title . '';
				}

				// Do not show the mounted pages
				if (!$objUser->isAdmin && $objUser->hasAccess($objPage->id, 'pagemounts'))
				{
					break;
				}

				$intId = $objPage->pid;
			}
			while ($intId > 0 && $objPage->type != 'root');
		}

		// Check whether the node is mounted
		if (!$objUser->hasAccess($arrIds, 'pagemounts'))
		{
			$objSession->set($strKey, 0);
			throw new AccessDeniedException('Page ID ' . $intNode . ' is not mounted.');
		}

		// Limit tree
		$GLOBALS['TL_DCA']['tl_page']['list']['sorting']['root'] = array($intNode);

		// Add root link
		$arrLinks[] = Image::getHtml('pagemounts.svg') . ' ' . $GLOBALS['TL_LANG']['MSC']['filterAll'] . '';
		$arrLinks = array_reverse($arrLinks);

		// Insert breadcrumb menu
		$GLOBALS['TL_DCA']['tl_page']['list']['sorting']['breadcrumb'] .= '

' . implode(' ›

', $arrLinks) . '

';
	}

	/**
	 * Add an image to each page in the tree
	 *
	 * @param array         $row
	 * @param string        $label
	 * @param DataContainer $dc
	 * @param string        $imageAttribute
	 * @param boolean       $blnReturnImage
	 * @param boolean       $blnProtected
	 *
	 * @return string
	 */
	public static function addPageIcon($row, $label, DataContainer $dc=null, $imageAttribute='', $blnReturnImage=false, $blnProtected=false)
	{
		if ($blnProtected)
		{
			$row['protected'] = true;
		}

		$image = Controller::getPageStatusIcon((object) $row);
		$imageAttribute = trim($imageAttribute . ' data-icon="' . Controller::getPageStatusIcon((object) array_merge($row, array('published'=>'1'))) . '" data-icon-disabled="' . Controller::getPageStatusIcon((object) array_merge($row, array('published'=>''))) . '"');

		// Return the image only
		if ($blnReturnImage)
		{
			return Image::getHtml($image, '', $imageAttribute);
		}

		// Mark root pages
		if ($row['type'] == 'root' || Input::get('do') == 'article')
		{
			$label = '' . $label . '';
		}

		// Add the breadcrumb link
		$label = '' . $label . '';

		// Return the image
		return ''.Image::getHtml($image, '', $imageAttribute).' '.$label;
	}

	/**
	 * Return the system messages as HTML
	 *
	 * @return string The messages HTML markup
	 */
	public static function getSystemMessages()
	{
		$strMessages = '';

		// HOOK: add custom messages
		if (isset($GLOBALS['TL_HOOKS']['getSystemMessages']) && \is_array($GLOBALS['TL_HOOKS']['getSystemMessages']))
		{
			$arrMessages = array();

			foreach ($GLOBALS['TL_HOOKS']['getSystemMessages'] as $callback)
			{
				$strBuffer = System::importStatic($callback[0])->{$callback[1]}();

				if ($strBuffer != '')
				{
					$arrMessages[] = $strBuffer;
				}
			}

			if (!empty($arrMessages))
			{
				$strMessages .= implode("\n", $arrMessages);
			}
		}

		return $strMessages;
	}

	/**
	 * Add a breadcrumb menu to the file tree
	 *
	 * @param string $strKey
	 *
	 * @throws AccessDeniedException
	 * @throws \RuntimeException
	 */
	public static function addFilesBreadcrumb($strKey='tl_files_node')
	{
		/** @var AttributeBagInterface $objSession */
		$objSession = System::getContainer()->get('session')->getBag('contao_backend');

		// Set a new node
		if (isset($_GET['fn']))
		{
			// Check the path (thanks to Arnaud Buchoux)
			if (Validator::isInsecurePath(Input::get('fn', true)))
			{
				throw new \RuntimeException('Insecure path ' . Input::get('fn', true));
			}

			$objSession->set($strKey, Input::get('fn', true));
			Controller::redirect(preg_replace('/(&|\?)fn=[^&]*/', '', Environment::get('request')));
		}

		$strNode = $objSession->get($strKey);

		if ($strNode == '')
		{
			return;
		}

		// Check the path (thanks to Arnaud Buchoux)
		if (Validator::isInsecurePath($strNode))
		{
			throw new \RuntimeException('Insecure path ' . $strNode);
		}

		$rootDir = System::getContainer()->getParameter('kernel.project_dir');

		// Currently selected folder does not exist
		if (!is_dir($rootDir . '/' . $strNode))
		{
			$objSession->set($strKey, '');

			return;
		}

		$objUser  = BackendUser::getInstance();
		$strPath  = Config::get('uploadPath');
		$arrNodes = explode('/', preg_replace('/^' . preg_quote(Config::get('uploadPath'), '/') . '\//', '', $strNode));
		$arrLinks = array();

		// Add root link
		$arrLinks[] = Image::getHtml('filemounts.svg') . ' ' . $GLOBALS['TL_LANG']['MSC']['filterAll'] . '';

		// Generate breadcrumb trail
		foreach ($arrNodes as $strFolder)
		{
			$strPath .= '/' . $strFolder;

			// Do not show pages which are not mounted
			if (!$objUser->hasAccess($strPath, 'filemounts'))
			{
				continue;
			}

			// No link for the active folder
			if ($strPath == $strNode)
			{
				$arrLinks[] = Image::getHtml('folderC.svg') . ' ' . $strFolder;
			}
			else
			{
				$arrLinks[] = Image::getHtml('folderC.svg') . ' ' . $strFolder . '';
			}
		}

		// Check whether the node is mounted
		if (!$objUser->hasAccess($strNode, 'filemounts'))
		{
			$objSession->set($strKey, '');
			throw new AccessDeniedException('Folder ID "' . $strNode . '" is not mounted');
		}

		// Limit tree
		$GLOBALS['TL_DCA']['tl_files']['list']['sorting']['root'] = array($strNode);

		// Insert breadcrumb menu
		$GLOBALS['TL_DCA']['tl_files']['list']['sorting']['breadcrumb'] .= '

' . implode(' ›

', $arrLinks) . '

';
	}

	/**
	 * Convert an array of layout section IDs to an associative array with IDs and labels
	 *
	 * @param array $arrSections
	 *
	 * @return array
	 */
	public static function convertLayoutSectionIdsToAssociativeArray($arrSections)
	{
		$arrSections = array_flip(array_values(array_unique($arrSections)));

		foreach (array_keys($arrSections) as $k)
		{
			$arrSections[$k] = $GLOBALS['TL_LANG']['COLS'][$k];
		}

		asort($arrSections);

		return $arrSections;
	}

	/**
	 * Generate the DCA picker wizard
	 *
	 * @param boolean|array $extras
	 * @param string        $table
	 * @param string        $field
	 * @param string        $inputName
	 *
	 * @return string
	 */
	public static function getDcaPickerWizard($extras, $table, $field, $inputName)
	{
		$context = 'link';
		$extras = \is_array($extras) ? $extras : array();
		$providers = (isset($extras['providers']) && \is_array($extras['providers'])) ? $extras['providers'] : null;

		if (isset($extras['context']))
		{
			$context = $extras['context'];
			unset($extras['context']);
		}

		$factory = System::getContainer()->get('contao.picker.builder');

		if (!$factory->supportsContext($context, $providers))
		{
			return '';
		}

		return ' getUrl($context, $extras)) . '" title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['pagepicker']) . '" id="pp_' . $inputName . '">' . Image::getHtml((\is_array($extras) && isset($extras['icon']) ? $extras['icon'] : 'pickpage.svg'), $GLOBALS['TL_LANG']['MSC']['pagepicker']) . '
  ';
	}

	/**
	 * Add the custom layout section references
	 */
	public function addCustomLayoutSectionReferences()
	{
		$objLayout = $this->Database->getInstance()->query("SELECT sections FROM tl_layout WHERE sections!=''");

		while ($objLayout->next())
		{
			$arrCustom = StringUtil::deserialize($objLayout->sections);

			// Add the custom layout sections
			if (!empty($arrCustom) && \is_array($arrCustom))
			{
				foreach ($arrCustom as $v)
				{
					if (!empty($v['id']))
					{
						$GLOBALS['TL_LANG']['COLS'][$v['id']] = $v['title'];
					}
				}
			}
		}
	}

	/**
	 * Get all allowed pages and return them as string
	 *
	 * @return string
	 */
	public function createPageList()
	{
		$this->import(BackendUser::class, 'User');

		if ($this->User->isAdmin)
		{
			return $this->doCreatePageList();
		}

		$return = '';
		$processed = array();

		foreach ($this->eliminateNestedPages($this->User->pagemounts) as $page)
		{
			$objPage = PageModel::findWithDetails($page);

			// Root page mounted
			if ($objPage->type == 'root')
			{
				$title = $objPage->title;
				$start = $objPage->id;
			}

			// Regular page mounted
			else
			{
				$title = $objPage->rootTitle;
				$start = $objPage->rootId;
			}

			// Do not process twice
			if (\in_array($start, $processed))
			{
				continue;
			}

			// Skip websites that run under a different domain (see #2387)
			if ($objPage->domain && $objPage->domain != Environment::get('host'))
			{
				continue;
			}

			$processed[] = $start;
			$return .= '<optgroup label="' . $title . '">' . $this->doCreatePageList($start) . '</optgroup>';
		}

		return $return;
	}

	/**
	 * Recursively get all allowed pages and return them as string
	 *
	 * @param integer $intId
	 * @param integer $level
	 *
	 * @return string
	 */
	protected function doCreatePageList($intId=0, $level=-1)
	{
		$objPages = $this->Database->prepare("SELECT id, title, type, dns FROM tl_page WHERE pid=? ORDER BY sorting")
								   ->execute($intId);

		if ($objPages->numRows < 1)
		{
			return '';
		}

		++$level;
		$strOptions = '';

		while ($objPages->next())
		{
			if ($objPages->type == 'root')
			{
				// Skip websites that run under a different domain
				if ($objPages->dns && $objPages->dns != Environment::get('host'))
				{
					continue;
				}

				$strOptions .= '<optgroup label="' . $objPages->title . '">';
				$strOptions .= $this->doCreatePageList($objPages->id, -1);
				$strOptions .= '</optgroup>';
			}
			else
			{
				$strOptions .= sprintf('<option value="{{link_url::%s}}"%s>%s%s</option>', $objPages->id, (('{{link_url::' . $objPages->id . '}}' == Input::get('value')) ? ' selected="selected"' : ''), str_repeat('     ', $level), StringUtil::specialchars($objPages->title));
				$strOptions .= $this->doCreatePageList($objPages->id, $level);
			}
		}

		return $strOptions;
	}

	/**
	 * Get all allowed files and return them as string
	 *
	 * @param string  $strFilter
	 * @param boolean $filemount
	 *
	 * @return string
	 */
	public function createFileList($strFilter='', $filemount=false)
	{
		// Deprecated since Contao 4.0, to be removed in Contao 5.0
		if ($strFilter === true)
		{
			@trigger_error('Passing "true" to Backend::createFileList() has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

			$strFilter = 'gif,jpg,jpeg,png';
		}

		$this->import(BackendUser::class, 'User');

		if ($this->User->isAdmin)
		{
			return $this->doCreateFileList(Config::get('uploadPath'), -1, $strFilter);
		}

		$return = '';
		$processed = array();

		// Set custom filemount
		if ($filemount)
		{
			$this->User->filemounts = array($filemount);
		}

		// Limit nodes to the filemounts of the user
		foreach ($this->eliminateNestedPaths($this->User->filemounts) as $path)
		{
			if (\in_array($path, $processed))
			{
				continue;
			}

			$processed[] = $path;
			$return .= $this->doCreateFileList($path, -1, $strFilter);
		}

		return $return;
	}

	/**
	 * Recursively get all allowed files and return them as string
	 *
	 * @param string  $strFolder
	 * @param integer $level
	 * @param string  $strFilter
	 *
	 * @return string
	 */
	protected function doCreateFileList($strFolder=null, $level=-1, $strFilter='')
	{
		// Deprecated since Contao 4.0, to be removed in Contao 5.0
		if ($strFilter === true)
		{
			@trigger_error('Passing "true" to Backend::doCreateFileList() has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

			$strFilter = 'gif,jpg,jpeg,png';
		}

		$rootDir = System::getContainer()->getParameter('kernel.project_dir');
		$arrPages = scan($rootDir . '/' . $strFolder);

		// Empty folder
		if (empty($arrPages))
		{
			return '';
		}

		// Protected folder
		if (\in_array('.htaccess', $arrPages))
		{
			return '';
		}

		++$level;
		$strFolders = '';
		$strFiles = '';

		// Recursively list all files and folders
		foreach ($arrPages as $strFile)
		{
			if (strncmp($strFile, '.', 1) === 0)
			{
				continue;
			}

			// Folders
			if (is_dir($rootDir . '/' . $strFolder . '/' . $strFile))
			{
				$strFolders .=  $this->doCreateFileList($strFolder . '/' . $strFile, $level, $strFilter);
			}

			// Files
			else
			{
				// Filter images
				if ($strFilter != '' && !preg_match('/\.(' . str_replace(',', '|', $strFilter) . ')$/i', $strFile))
				{
					continue;
				}

				$strFiles .= sprintf('<option value="%s"%s>%s</option>', $strFolder . '/' . $strFile, (($strFolder . '/' . $strFile == Input::get('value')) ? ' selected="selected"' : ''), StringUtil::specialchars($strFile));
			}
		}

		if (\strlen($strFiles))
		{
			return '<optgroup label="' . StringUtil::specialchars($strFolder) . '">' . $strFiles . $strFolders . '</optgroup>';
		}

		return $strFiles . $strFolders;
	}
}

class_alias(Backend::class, 'Backend');


<?php

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao;

use Contao\Image\Image as NewImage;
use Contao\Image\ImageDimensions;
use Contao\Image\ImportantPart;
use Contao\Image\ResizeConfiguration;
use Contao\Image\ResizeOptions;
use Imagine\Image\Box;
use Imagine\Image\Point;

/**
 * Resizes images
 *
 * The class resizes images and stores them in the image target folder.
 *
 * Usage:
 *
 *     $imageObj = new Image(new File('example.jpg'));
 *
 *     $src = $imageObj->setTargetWidth(640)
 *                     ->setTargetHeight(480)
 *                     ->setResizeMode('center_center')
 *                     ->executeResize()
 *                     ->getResizedPath();
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 * @author Martin Auswöger <https://github.com/ausi>
 * @author Yanick Witschi <https://github.com/Toflar>
 */
class Image
{

	/**
	 * The File instance of the original image
	 *
	 * @var File
	 */
	protected $fileObj;

	/**
	 * The resized image path
	 *
	 * @var string
	 */
	protected $resizedPath = '';

	/**
	 * The target width
	 *
	 * @var integer
	 */
	protected $targetWidth = 0;

	/**
	 * The target height
	 *
	 * @var integer
	 */
	protected $targetHeight = 0;

	/**
	 * The resize mode (defaults to crop for BC)
	 *
	 * @var string
	 */
	protected $resizeMode = 'crop';

	/**
	 * The target path
	 *
	 * @var string
	 */
	protected $targetPath = '';

	/**
	 * Override an existing target
	 *
	 * @var boolean
	 */
	protected $forceOverride = false;

	/**
	 * Zoom level (between 0 and 100)
	 *
	 * @var integer
	 */
	protected $zoomLevel = 0;

	/**
	 * Root dir
	 * @var string
	 */
	protected $strRootDir;

	/**
	 * Important part settings
	 *
	 * @var array
	 */
	protected $importantPart = array();

	/**
	 * Create a new object to handle an image
	 *
	 * @param File $file A file instance of the original image
	 *
	 * @throws \InvalidArgumentException If the file does not exists or cannot be processed
	 *
	 * @deprecated Deprecated since Contao 4.3, to be removed in Contao 5.0.
	 *             Use the contao.image.image_factory service instead.
	 */
	public function __construct(File $file)
	{
		@trigger_error('Using new Contao\Image() has been deprecated and will no longer work in Contao 5.0. Use the contao.image.image_factory service instead.', E_USER_DEPRECATED);

		// Check whether the file exists
		if (!$file->exists())
		{
			$webDir = System::getContainer()->getParameter('contao.web_dir');

			// Handle public bundle resources
			if (file_exists($webDir . '/' . $file->path))
			{
				$file = new File(StringUtil::stripRootDir($webDir) . '/' . $file->path);
			}
			else
			{
				throw new \InvalidArgumentException('Image "' . $file->path . '" could not be found');
			}
		}

		$this->fileObj = $file;
		$arrAllowedTypes = StringUtil::trimsplit(',', strtolower(Config::get('validImageTypes')));

		// Check the file type
		if (!\in_array($this->fileObj->extension, $arrAllowedTypes))
		{
			throw new \InvalidArgumentException('Image type "' . $this->fileObj->extension . '" was not allowed to be processed');
		}

		$this->strRootDir = System::getContainer()->getParameter('kernel.project_dir');
	}

	/**
	 * Override the target image
	 *
	 * @param boolean $forceOverride True to override the target image
	 *
	 * @return $this The image object
	 */
	public function setForceOverride($forceOverride)
	{
		$this->forceOverride = (bool) $forceOverride;

		return $this;
	}

	/**
	 * Get force override setting
	 *
	 * @return boolean True if the target image will be overridden
	 */
	public function getForceOverride()
	{
		return $this->forceOverride;
	}

	/**
	 * Set the important part settings
	 *
	 * @param array $importantPart The settings array
	 *
	 * @return $this The image object
	 *
	 * @throws \InvalidArgumentException If the settings array is malformed
	 */
	public function setImportantPart(array $importantPart = null)
	{
		if ($importantPart !== null)
		{
			if (!isset($importantPart['x']) || !isset($importantPart['y']) || !isset($importantPart['width']) || !isset($importantPart['height']))
			{
				throw new \InvalidArgumentException('Malformed array for setting the important part!');
			}

			$this->importantPart = array
			(
				'x' => max(0, min($this->fileObj->viewWidth - 1, (int) $importantPart['x'])),
				'y' => max(0, min($this->fileObj->viewHeight - 1, (int) $importantPart['y'])),
			);

			$this->importantPart['width'] = max(1, min($this->fileObj->viewWidth - $this->importantPart['x'], (int) $importantPart['width']));
			$this->importantPart['height'] = max(1, min($this->fileObj->viewHeight - $this->importantPart['y'], (int) $importantPart['height']));

		}
		else
		{
			$this->importantPart = null;
		}

		return $this;
	}

	/**
	 * Get the important part settings
	 *
	 * @return array The settings array
	 */
	public function getImportantPart()
	{
		if ($this->importantPart)
		{
			return $this->importantPart;
		}

		return array('x'=>0, 'y'=>0, 'width'=>$this->fileObj->viewWidth, 'height'=>$this->fileObj->viewHeight);
	}

	/**
	 * Set the target height
	 *
	 * @param integer $targetHeight The target height
	 *
	 * @return $this The image object
	 */
	public function setTargetHeight($targetHeight)
	{
		$this->targetHeight = (int) $targetHeight;

		return $this;
	}

	/**
	 * Get the target height
	 *
	 * @return integer The target height
	 */
	public function getTargetHeight()
	{
		return $this->targetHeight;
	}

	/**
	 * Set the target width
	 *
	 * @param integer $targetWidth The target width
	 *
	 * @return $this The image object
	 */
	public function setTargetWidth($targetWidth)
	{
		$this->targetWidth = (int) $targetWidth;

		return $this;
	}

	/**
	 * Get the target width
	 *
	 * @return integer The target width
	 */
	public function getTargetWidth()
	{
		return $this->targetWidth;
	}

	/**
	 * Set the target path
	 *
	 * @param string $targetPath The target path
	 *
	 * @return $this The image object
	 */
	public function setTargetPath($targetPath)
	{
		$this->targetPath = (string) $targetPath;

		return $this;
	}

	/**
	 * Get the target path
	 *
	 * @return string The target path
	 */
	public function getTargetPath()
	{
		return $this->targetPath;
	}

	/**
	 * Set the zoom level
	 *
	 * @param integer $zoomLevel The zoom level
	 *
	 * @return $this The object instance
	 *
	 * @throws \InvalidArgumentException If the zoom level is out of bounds
	 */
	public function setZoomLevel($zoomLevel)
	{
		$zoomLevel = (int) $zoomLevel;

		if ($zoomLevel < 0 || $zoomLevel > 100)
		{
			throw new \InvalidArgumentException('Zoom level must be between 0 and 100!');
		}

		$this->zoomLevel = $zoomLevel;

		return $this;
	}

	/**
	 * Get the zoom level
	 *
	 * @return integer The zoom level
	 */
	public function getZoomLevel()
	{
		return $this->zoomLevel;
	}

	/**
	 * Set the resize mode
	 *
	 * @param string $resizeMode The resize mode
	 *
	 * @return $this The image object
	 */
	public function setResizeMode($resizeMode)
	{
		$this->resizeMode = $resizeMode;

		return $this;
	}

	/**
	 * Get the resize mode
	 *
	 * @return string The resize mode
	 */
	public function getResizeMode()
	{
		return $this->resizeMode;
	}

	/**
	 * Get the path of the original image
	 *
	 * @return string The path of the original image
	 */
	public function getOriginalPath()
	{
		return $this->fileObj->path;
	}

	/**
	 * Get the path of the resized image
	 *
	 * @return string The path of the resized image
	 */
	public function getResizedPath()
	{
		$path = $this->resizedPath;
		$webDir = StringUtil::stripRootDir(System::getContainer()->getParameter('contao.web_dir'));

		// Strip the web/ prefix (see #337)
		if (strncmp($path, $webDir . '/', \strlen($webDir) + 1) === 0)
		{
			$path = substr($path, \strlen($webDir) + 1);
		}

		return $path;
	}

	/**
	 * Get the cache name
	 *
	 * @return string The cache name
	 */
	public function getCacheName()
	{
		$importantPart = $this->getImportantPart();

		$strCacheKey = substr(md5
		(
			  '-w' . $this->getTargetWidth()
			. '-h' . $this->getTargetHeight()
			. '-o' . $this->getOriginalPath()
			. '-m' . $this->getResizeMode()
			. '-z' . $this->getZoomLevel()
			. '-x' . $importantPart['x']
			. '-y' . $importantPart['y']
			. '-i' . $importantPart['width']
			. '-e' . $importantPart['height']
			. '-t' . $this->fileObj->mtime
		), 0, 8);

		return StringUtil::stripRootDir(System::getContainer()->getParameter('contao.image.target_dir')) . '/' . substr($strCacheKey, -1) . '/' . $this->fileObj->filename . '-' . $strCacheKey . '.' . $this->fileObj->extension;
	}

	/**
	 * Resize the image
	 *
	 * @return $this The image object
	 */
	public function executeResize()
	{
		$image = $this->prepareImage();
		$resizeConfig = $this->prepareResizeConfig();

		if (!System::getContainer()->getParameter('contao.image.bypass_cache')
			&& $this->getTargetPath()
			&& !$this->getForceOverride()
			&& file_exists($this->strRootDir . '/' . $this->getTargetPath())
			&& $this->fileObj->mtime <= filemtime($this->strRootDir . '/' . $this->getTargetPath())
		) {
			// HOOK: add custom logic
			if (isset($GLOBALS['TL_HOOKS']['executeResize']) && \is_array($GLOBALS['TL_HOOKS']['executeResize']))
			{
				foreach ($GLOBALS['TL_HOOKS']['executeResize'] as $callback)
				{
					$return = System::importStatic($callback[0])->{$callback[1]}($this);

					if (\is_string($return))
					{
						$this->resizedPath = System::urlEncode($return);

						return $this;
					}
				}
			}

			$this->resizedPath = System::urlEncode($this->getTargetPath());

			return $this;
		}

		$image = System::getContainer()
			->get('contao.image.resizer')
			->resize(
				$image,
				$resizeConfig,
				(new ResizeOptions())
					->setImagineOptions(System::getContainer()->getParameter('contao.image.imagine_options'))
					->setTargetPath($this->targetPath ? $this->strRootDir . '/' . $this->targetPath : null)
					->setBypassCache(System::getContainer()->getParameter('contao.image.bypass_cache'))
			)
		;

		$this->resizedPath = $image->getUrl($this->strRootDir);

		return $this;
	}

	/**
	 * Prepare image object.
	 *
	 * @return NewImage
	 */
	protected function prepareImage()
	{
		if ($this->fileObj->isSvgImage)
		{
			$imagine = System::getContainer()->get('contao.image.imagine_svg');
		}
		else
		{
			$imagine = System::getContainer()->get('contao.image.imagine');
		}

		$image = new NewImage($this->strRootDir . '/' . $this->fileObj->path, $imagine, System::getContainer()->get('filesystem'));
		$image->setImportantPart($this->prepareImportantPart());

		return $image;
	}

	/**
	 * Prepare important part object.
	 *
	 * @return ImportantPart
	 */
	protected function prepareImportantPart()
	{
		$importantPart = $this->getImportantPart();

		if (substr_count($this->resizeMode, '_') === 1)
		{
			$importantPart = array
			(
				'x' => 0,
				'y' => 0,
				'width' => $this->fileObj->viewWidth,
				'height' => $this->fileObj->viewHeight,
			);

			$mode = explode('_', $this->resizeMode);

			if ($mode[0] === 'left')
			{
				$importantPart['width'] = 1;
			}
			elseif ($mode[0] === 'right')
			{
				$importantPart['x'] = $importantPart['width'] - 1;
				$importantPart['width'] = 1;
			}

			if ($mode[1] === 'top')
			{
				$importantPart['height'] = 1;
			}
			elseif ($mode[1] === 'bottom')
			{
				$importantPart['y'] = $importantPart['height'] - 1;
				$importantPart['height'] = 1;
			}
		}

		if (!$importantPart['width'] || !$importantPart['height'])
		{
			return null;
		}

		return new ImportantPart(
			new Point($importantPart['x'], $importantPart['y']),
			new Box($importantPart['width'], $importantPart['height'])
		);
	}

	/**
	 * Prepare resize configuration object.
	 *
	 * @return ResizeConfiguration
	 */
	protected function prepareResizeConfig()
	{
		$resizeConfig = new ResizeConfiguration();
		$resizeConfig->setWidth($this->targetWidth);
		$resizeConfig->setHeight($this->targetHeight);
		$resizeConfig->setZoomLevel($this->zoomLevel);

		if (substr_count($this->resizeMode, '_') === 1)
		{
			$resizeConfig->setMode(ResizeConfiguration::MODE_CROP);
			$resizeConfig->setZoomLevel(0);
		}
		else
		{
			try
			{
				$resizeConfig->setMode($this->resizeMode);
			}
			catch (\InvalidArgumentException $exception)
			{
				$resizeConfig->setMode(ResizeConfiguration::MODE_CROP);
			}
		}

		return $resizeConfig;
	}

	/**
	 * Calculate the resize coordinates
	 *
	 * @return array The resize coordinates (width, height, target_x, target_y, target_width, target_height)
	 */
	public function computeResize()
	{
		$resizeCoordinates = System::getContainer()
			->get('contao.image.resize_calculator')
			->calculate(
				$this->prepareResizeConfig(),
				new ImageDimensions(
					new Box($this->fileObj->viewWidth, $this->fileObj->viewHeight),
					$this->fileObj->viewWidth !== $this->fileObj->width
				),
				$this->prepareImportantPart()
			)
		;

		return array
		(
			'width' => $resizeCoordinates->getCropSize()->getWidth(),
			'height' => $resizeCoordinates->getCropSize()->getHeight(),
			'target_x' => -$resizeCoordinates->getCropStart()->getX(),
			'target_y' => -$resizeCoordinates->getCropStart()->getY(),
			'target_width' => $resizeCoordinates->getSize()->getWidth(),
			'target_height' => $resizeCoordinates->getSize()->getHeight(),
		);
	}

	/**
	 * Get the relative path to an image
	 *
	 * @param string $src The image name or path
	 *
	 * @return string The relative path
	 */
	public static function getPath($src)
	{
		if ($src == '')
		{
			return '';
		}

		$src = rawurldecode($src);

		if (strpos($src, '/') !== false)
		{
			return $src;
		}

		$rootDir = System::getContainer()->getParameter('kernel.project_dir');

		if (strncmp($src, 'icon', 4) === 0)
		{
			if (pathinfo($src, PATHINFO_EXTENSION) == 'svg')
			{
				return 'assets/contao/images/' . $src;
			}

			$filename = pathinfo($src, PATHINFO_FILENAME);

			// Prefer SVG icons
			if (file_exists($rootDir . '/assets/contao/images/' . $filename . '.svg'))
			{
				return 'assets/contao/images/' . $filename . '.svg';
			}

			return 'assets/contao/images/' . $src;
		}
		else
		{
			$theme = Backend::getTheme();

			if (pathinfo($src, PATHINFO_EXTENSION) == 'svg')
			{
				return 'system/themes/' . $theme . '/icons/' . $src;
			}

			$filename = pathinfo($src, PATHINFO_FILENAME);

			// Prefer SVG icons
			if (file_exists($rootDir . '/system/themes/' . $theme . '/icons/' . $filename . '.svg'))
			{
				return 'system/themes/' . $theme . '/icons/' . $filename . '.svg';
			}

			return 'system/themes/' . $theme . '/images/' . $src;
		}
	}

	/**
	 * Generate an image tag and return it as string
	 *
	 * @param string $src        The image path
	 * @param string $alt        An optional alt attribute
	 * @param string $attributes A string of other attributes
	 *
	 * @return string The image HTML tag
	 */
	public static function getHtml($src, $alt='', $attributes='')
	{
		$src = static::getPath($src);

		if ($src == '')
		{
			return '';
		}

		$container = System::getContainer();
		$rootDir = $container->getParameter('kernel.project_dir');
		$webDir = StringUtil::stripRootDir($container->getParameter('contao.web_dir'));

		if (!is_file($rootDir . '/' . $src))
		{
			// Handle public bundle resources
			if (file_exists($rootDir . '/' . $webDir . '/' . $src))
			{
				$src = $webDir . '/' . $src;
			}
			else
			{
				return '';
			}
		}

		$objFile = new File($src);

		// Strip the web/ prefix (see #337)
		if (strncmp($src, $webDir . '/', \strlen($webDir) + 1) === 0)
		{
			$src = substr($src, \strlen($webDir) + 1);
		}

		$context = (strncmp($src, 'assets/', 7) === 0) ? 'assets_context' : 'files_context';

		return '<img src="' . Controller::addStaticUrlTo(System::urlEncode($src), $container->get('contao.assets.'.$context)) . '" width="' . $objFile->width . '" height="' . $objFile->height . '" alt="' . StringUtil::specialchars($alt) . '"' . (($attributes != '') ? ' ' . $attributes : '') . '>';
	}

	/**
	 * Resize or crop an image and replace the original with the resized version
	 *
	 * @param string  $image  The image path
	 * @param integer $width  The target width
	 * @param integer $height The target height
	 * @param string  $mode   The resize mode
	 *
	 * @return boolean True if the image could be resized successfully
	 *
	 * @deprecated Deprecated since Contao 4.3, to be removed in Contao 5.0.
	 *             Use the contao.image.image_factory service instead.
	 */
	public static function resize($image, $width, $height, $mode='')
	{
		@trigger_error('Using Image::resize() has been deprecated and will no longer work in Contao 5.0. Use the contao.image.image_factory service instead.', E_USER_DEPRECATED);

		return static::get($image, $width, $height, $mode, $image, true) ? true : false;
	}

	/**
	 * Create an image instance from the given image path and size
	 *
	 * @param string|File   $image The image path or File instance
	 * @param array|integer $size  The image size as array (width, height, resize mode) or an tl_image_size ID
	 *
	 * @return static The created image instance
	 *
	 * @deprecated Deprecated since Contao 4.3, to be removed in Contao 5.0.
	 *             Use the contao.image.image_factory service instead.
	 */
	public static function create($image, $size=null)
	{
		@trigger_error('Using Image::create() has been deprecated and will no longer work in Contao 5.0. Use the contao.image.image_factory service instead.', E_USER_DEPRECATED);

		if (\is_string($image))
		{
			$image = new File(rawurldecode($image));
		}

		/** @var Image $imageObj */
		$imageObj = new static($image);

		// tl_image_size ID as resize mode
		if (\is_array($size) && !empty($size[2]) && is_numeric($size[2]))
		{
			$size = (int) $size[2];
		}

		if (\is_array($size))
		{
			$size += array(0, 0, 'crop');

			$imageObj
				->setTargetWidth($size[0])
				->setTargetHeight($size[1])
				->setResizeMode($size[2])
			;
		}

		// Load the image size from the database if $size is an ID
		elseif (($imageSize = ImageSizeModel::findByPk($size)) !== null)
		{
			$imageObj
				->setTargetWidth($imageSize->width)
				->setTargetHeight($imageSize->height)
				->setResizeMode($imageSize->resizeMode)
				->setZoomLevel($imageSize->zoom)
			;
		}

		$fileRecord = FilesModel::findByPath($image->path);

		// Set the important part
		if ($fileRecord !== null && $fileRecord->importantPartWidth && $fileRecord->importantPartHeight)
		{
			$imageObj->setImportantPart(array
			(
				'x' => (int) $fileRecord->importantPartX,
				'y' => (int) $fileRecord->importantPartY,
				'width' => (int) $fileRecord->importantPartWidth,
				'height' => (int) $fileRecord->importantPartHeight,
			));
		}

		return $imageObj;
	}

	/**
	 * Resize an image and store the resized version in the image target folder
	 *
	 * @param string  $image  The image path
	 * @param integer $width  The target width
	 * @param integer $height The target height
	 * @param string  $mode   The resize mode
	 * @param string  $target An optional target path
	 * @param boolean $force  Override existing target images
	 *
	 * @return string|null The path of the resized image or null
	 *
	 * @deprecated Deprecated since Contao 4.3, to be removed in Contao 5.0.
	 *             Use the contao.image.image_factory service instead.
	 */
	public static function get($image, $width, $height, $mode='', $target=null, $force=false)
	{
		@trigger_error('Using Image::get() has been deprecated and will no longer work in Contao 5.0. Use the contao.image.image_factory service instead.', E_USER_DEPRECATED);

		if ($image == '')
		{
			return null;
		}

		try
		{
			$imageObj = static::create($image, array($width, $height, $mode));
			$imageObj->setTargetPath($target);
			$imageObj->setForceOverride($force);

			if ($path = $imageObj->executeResize()->getResizedPath())
			{
				return $path;
			}
		}
		catch (\Exception $e)
		{
			System::log('Image "' . $image . '" could not be processed: ' . $e->getMessage(), __METHOD__, 'ERROR');
		}

		return null;
	}

	/**
	 * Convert sizes like 2em, 10cm or 12pt to pixels
	 *
	 * @param string $size The size string
	 *
	 * @return integer The pixel value
	 *
	 * @deprecated Deprecated since Contao 4.3, to be removed in Contao 5.0.
	 *             Use the contao.image.image_factory service instead.
	 */
	public static function getPixelValue($size)
	{
		@trigger_error('Using Image::getPixelValue() has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

		$value = preg_replace('/[^0-9.-]+/', '', $size);
		$unit = preg_replace('/[^acehimnprtvwx%]/', '', $size);

		// Convert 16px = 1em = 2ex = 12pt = 1pc = 1/6in = 2.54/6cm = 25.4/6mm = 100%
		switch ($unit)
		{
			case '':
			case 'px':
				return (int) round($value);
				break;

			case 'em':
				return (int) round($value * 16);
				break;

			case 'ex':
				return (int) round($value * 16 / 2);
				break;

			case 'pt':
				return (int) round($value * 16 / 12);
				break;

			case 'pc':
				return (int) round($value * 16);
				break;

			case 'in':
				return (int) round($value * 16 * 6);
				break;

			case 'cm':
				return (int) round($value * 16 / (2.54 / 6));
				break;

			case 'mm':
				return (int) round($value * 16 / (25.4 / 6));
				break;

			case '%':
				return (int) round($value * 16 / 100);
				break;
		}

		return 0;
	}
}

class_alias(Image::class, 'Image');


<?php

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao;

use Contao\CoreBundle\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;

/**
 * Confirm an invalid token URL.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class BackendConfirm extends Backend
{

	/**
	 * Initialize the controller
	 *
	 * 1. Import the user
	 * 2. Call the parent constructor
	 * 3. Authenticate the user
	 * 4. Load the language files
	 * DO NOT CHANGE THIS ORDER!
	 */
	public function __construct()
	{
		$this->import(BackendUser::class, 'User');
		parent::__construct();

		if (!System::getContainer()->get('security.authorization_checker')->isGranted('ROLE_USER'))
		{
			throw new AccessDeniedException('Access denied');
		}

		System::loadLanguageFile('default');
		System::loadLanguageFile('modules');
	}

	/**
	 * Run the controller
	 *
	 * @return Response
	 */
	public function run()
	{
		/** @var Session $objSession */
		$objSession = System::getContainer()->get('session');

		// Redirect to the back end home page
		if (Input::post('FORM_SUBMIT') == 'invalid_token_url')
		{
			list($strUrl) = explode('?', $objSession->get('INVALID_TOKEN_URL'));
			$this->redirect($strUrl);
		}

		$objTemplate = new BackendTemplate('be_confirm');

		// Prepare the URL
		$url = preg_replace('/(\?|&)rt=[^&]*/', '', $objSession->get('INVALID_TOKEN_URL'));
		$objTemplate->href = ampersand($url . ((strpos($url, '?') !== false) ? '&rt=' : '?rt=') . REQUEST_TOKEN);

		$vars = array();
		list(, $request) = explode('?', $url, 2);

		// Extract the arguments
		foreach (explode('&', $request) as $arg)
		{
			list($key, $value) = explode('=', $arg, 2);
			$vars[$key] = $value;
		}

		$arrInfo = array();

		// Provide more information about the link (see #4007)
		foreach ($vars as $k=>$v)
		{
			switch ($k)
			{
				default:
					$arrInfo[$k] = $v;
					break;

				case 'do':
					$arrInfo['do'] = $GLOBALS['TL_LANG']['MOD'][$v][0];
					break;

				case 'id':
					$arrInfo['id'] = 'ID ' . $v;
					break;
			}
		}

		// Use the first table if none is given
		if (!isset($arrInfo['table']))
		{
			foreach ($GLOBALS['BE_MOD'] as $category=>$modules)
			{
				if (isset($GLOBALS['BE_MOD'][$category][$vars['do']]))
				{
					$arrInfo['table'] = $GLOBALS['BE_MOD'][$category][$vars['do']]['tables'][0];
					break;
				}
			}
		}

		System::loadLanguageFile($arrInfo['table']);

		// Override the action label
		if (isset($arrInfo['clipboard']))
		{
			$arrInfo['act'] = $GLOBALS['TL_LANG']['MSC']['clearClipboard'];
		}
		elseif (isset($arrInfo['mode']) && !isset($arrInfo['act']))
		{
			if ($arrInfo['mode'] == 'create')
			{
				$arrInfo['act'] = $GLOBALS['TL_LANG'][$arrInfo['table']]['new'][0];
			}
			elseif ($arrInfo['mode'] == 'cut' || $arrInfo['mode'] == 'copy')
			{
				$arrInfo['act'] = $GLOBALS['TL_LANG'][$arrInfo['table']][$arrInfo['mode']][0];
			}
		}
		else
		{
			$arrInfo['act'] = $GLOBALS['TL_LANG'][$arrInfo['table']][$arrInfo['act']][0];
		}

		unset($arrInfo['pid']);
		unset($arrInfo['clipboard']);
		unset($arrInfo['ref']);
		unset($arrInfo['mode']);

		// Template variables
		$objTemplate->confirm = true;
		$objTemplate->link = StringUtil::specialchars($url);
		$objTemplate->info = $arrInfo;
		$objTemplate->labels = $GLOBALS['TL_LANG']['CONFIRM'];
		$objTemplate->explain = $GLOBALS['TL_LANG']['MSC']['invalidTokenUrl'];
		$objTemplate->cancel = $GLOBALS['TL_LANG']['MSC']['cancelBT'];
		$objTemplate->continue = $GLOBALS['TL_LANG']['MSC']['continue'];
		$objTemplate->theme = Backend::getTheme();
		$objTemplate->base = Environment::get('base');
		$objTemplate->language = $GLOBALS['TL_LANGUAGE'];
		$objTemplate->h1 = $GLOBALS['TL_LANG']['MSC']['invalidToken'];
		$objTemplate->title = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['invalidToken']);
		$objTemplate->host = Environment::get('host');
		$objTemplate->charset = Config::get('characterSet');

		return $objTemplate->getResponse();
	}
}

class_alias(BackendConfirm::class, 'BackendConfirm');


<?php

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao;

use Symfony\Component\Security\Csrf\CsrfToken;

@trigger_error('Using the Contao\RequestToken class has been deprecated and will no longer work in Contao 5.0. Use the Symfony CSRF service via the container instead.', E_USER_DEPRECATED);

/**
 * Generates and validates request tokens
 *
 * The class tries to read and validate the request token from the user session
 * and creates a new token if there is none.
 *
 * Usage:
 *
 *     echo RequestToken::get();
 *
 *     if (!RequestToken::validate('TOKEN'))
 *     {
 *         throw new Exception("Invalid request token");
 *     }
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 *
 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
 *             Use the Symfony CSRF service via the container instead.
 */
class RequestToken
{

	/**
	 * Read the token from the session or generate a new one
	 */
	public static function initialize()
	{
		// ignore
	}

	/**
	 * Return the token
	 *
	 * @return string The request token
	 */
	public static function get()
	{
		$container = System::getContainer();

		return $container->get('contao.csrf.token_manager')->getToken($container->getParameter('contao.csrf_token_name'))->getValue();
	}

	/**
	 * Validate a token
	 *
	 * @param string $strToken The request token
	 *
	 * @return boolean True if the token matches the stored one
	 */
	public static function validate($strToken)
	{
		// The feature has been disabled
		if (Config::get('disableRefererCheck') || \defined('BYPASS_TOKEN_CHECK'))
		{
			return true;
		}

		// Check against the whitelist (thanks to Tristan Lins) (see #3164)
		if (Config::get('requestTokenWhitelist'))
		{
			$strHostname = gethostbyaddr($_SERVER['REMOTE_ADDR']);

			foreach (Config::get('requestTokenWhitelist') as $strDomain)
			{
				if ($strDomain == $strHostname || preg_match('/\.' . preg_quote($strDomain, '/') . '$/', $strHostname))
				{
					return true;
				}
			}
		}

		$container = System::getContainer();

		return $container->get('contao.csrf.token_manager')->isTokenValid(new CsrfToken($container->getParameter('contao.csrf_token_name'), $strToken));
	}
}

class_alias(RequestToken::class, 'RequestToken');


<?php

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao\Model;

use Contao\Database\Result;
use Contao\Model;

/**
 * The class handles traversing a set of models and lazy loads the database
 * result rows upon their first usage.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class Collection implements \ArrayAccess, \Countable, \IteratorAggregate
{

	/**
	 * Table name
	 * @var string
	 */
	protected $strTable;

	/**
	 * Current index
	 * @var integer
	 */
	protected $intIndex = -1;

	/**
	 * Models
	 * @var Model[]
	 */
	protected $arrModels = array();

	/**
	 * Create a new collection
	 *
	 * @param array  $arrModels An array of models
	 * @param string $strTable  The table name
	 *
	 * @throws \InvalidArgumentException
	 */
	public function __construct(array $arrModels, $strTable)
	{
		$arrModels = array_values($arrModels);

		foreach ($arrModels as $objModel)
		{
			if (!$objModel instanceof Model)
			{
				throw new \InvalidArgumentException('Invalid type: ' . \gettype($objModel));
			}
		}

		$this->arrModels = $arrModels;
		$this->strTable  = $strTable;
	}

	/**
	 * Set an object property
	 *
	 * @param string $strKey   The property name
	 * @param mixed  $varValue The property value
	 */
	public function __set($strKey, $varValue)
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		$this->arrModels[$this->intIndex]->$strKey = $varValue;
	}

	/**
	 * Return an object property
	 *
	 * @param string $strKey The property name
	 *
	 * @return mixed|null The property value or null
	 */
	public function __get($strKey)
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		if (isset($this->arrModels[$this->intIndex]->$strKey))
		{
			return $this->arrModels[$this->intIndex]->$strKey;
		}

		return null;
	}

	/**
	 * Check whether a property is set
	 *
	 * @param string $strKey The property name
	 *
	 * @return boolean True if the property is set
	 */
	public function __isset($strKey)
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		return isset($this->arrModels[$this->intIndex]->$strKey);
	}

	/**
	 * Create a new collection from a database result
	 *
	 * @param Result $objResult The database result object
	 * @param string $strTable  The table name
	 *
	 * @return static The model collection
	 */
	public static function createFromDbResult(Result $objResult, $strTable)
	{
		$arrModels = array();
		$strClass = Model::getClassFromTable($strTable);

		while ($objResult->next())
		{
			/** @var Model $strClass */
			$objModel = Registry::getInstance()->fetch($strTable, $objResult->{$strClass::getPk()});

			if ($objModel !== null)
			{
				$objModel->mergeRow($objResult->row());
				$arrModels[] = $objModel;
			}
			else
			{
				$arrModels[] = new $strClass($objResult);
			}
		}

		return new static($arrModels, $strTable);
	}

	/**
	 * Return the current row as associative array
	 *
	 * @return array The current row as array
	 */
	public function row()
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		return $this->arrModels[$this->intIndex]->row();
	}

	/**
	 * Set the current row from an array
	 *
	 * @param array $arrData The row data as array
	 *
	 * @return static The model collection object
	 */
	public function setRow(array $arrData)
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		$this->arrModels[$this->intIndex]->setRow($arrData);

		return $this;
	}

	/**
	 * Save the current model
	 *
	 * @return static The model collection object
	 */
	public function save()
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		$this->arrModels[$this->intIndex]->save();

		return $this;
	}

	/**
	 * Delete the current model and return the number of affected rows
	 *
	 * @return integer The number of affected rows
	 */
	public function delete()
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		return $this->arrModels[$this->intIndex]->delete();
	}

	/**
	 * Return the models as array
	 *
	 * @return Model[] An array of models
	 */
	public function getModels()
	{
		return $this->arrModels;
	}

	/**
	 * Lazy load related records
	 *
	 * @param string $strKey The property name
	 *
	 * @return Collection|Model The model or a model collection if there are multiple rows
	 */
	public function getRelated($strKey)
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		return $this->arrModels[$this->intIndex]->getRelated($strKey);
	}

	/**
	 * Return the number of rows in the result set
	 *
	 * @return integer The number of rows
	 */
	public function count()
	{
		return \count($this->arrModels);
	}

	/**
	 * Go to the first row
	 *
	 * @return static The model collection object
	 */
	public function first()
	{
		$this->intIndex = 0;

		return $this;
	}

	/**
	 * Go to the previous row
	 *
	 * @return Collection|false The model collection object or false if there is no previous row
	 */
	public function prev()
	{
		if ($this->intIndex < 1)
		{
			return false;
		}

		--$this->intIndex;

		return $this;
	}

	/**
	 * Return the current model
	 *
	 * @return Model The model object
	 */
	public function current()
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		return $this->arrModels[$this->intIndex];
	}

	/**
	 * Go to the next row
	 *
	 * @return Collection|false The model collection object or false if there is no next row
	 */
	public function next()
	{
		if (!isset($this->arrModels[$this->intIndex + 1]))
		{
			return false;
		}

		++$this->intIndex;

		return $this;
	}

	/**
	 * Go to the last row
	 *
	 * @return static The model collection object
	 */
	public function last()
	{
		$this->intIndex = \count($this->arrModels) - 1;

		return $this;
	}

	/**
	 * Reset the model
	 *
	 * @return static The model collection object
	 */
	public function reset()
	{
		$this->intIndex = -1;

		return $this;
	}

	/**
	 * Fetch a column of each row
	 *
	 * @param string $strKey The property name
	 *
	 * @return array An array with all property values
	 */
	public function fetchEach($strKey)
	{
		$this->reset();
		$return = array();

		while ($this->next())
		{
			$strPk = $this->current()->getPk();

			if ($strKey != 'id' && isset($this->$strPk))
			{
				$return[$this->$strPk] = $this->$strKey;
			}
			else
			{
				$return[] = $this->$strKey;
			}
		}

		return $return;
	}

	/**
	 * Fetch all columns of every row
	 *
	 * @return array An array with all rows and columns
	 */
	public function fetchAll()
	{
		$this->reset();
		$return = array();

		while ($this->next())
		{
			$return[] = $this->row();
		}

		return $return;
	}

	/**
	 * Check whether an offset exists
	 *
	 * @param integer $offset The offset
	 *
	 * @return boolean True if the offset exists
	 */
	public function offsetExists($offset)
	{
		return isset($this->arrModels[$offset]);
	}

	/**
	 * Retrieve a particular offset
	 *
	 * @param integer $offset The offset
	 *
	 * @return Model|null The model or null
	 */
	public function offsetGet($offset)
	{
		return $this->arrModels[$offset];
	}

	/**
	 * Set a particular offset
	 *
	 * @param integer $offset The offset
	 * @param mixed   $value  The value to set
	 *
	 * @throws \RuntimeException The collection is immutable
	 */
	public function offsetSet($offset, $value)
	{
		throw new \RuntimeException('This collection is immutable');
	}

	/**
	 * Unset a particular offset
	 *
	 * @param integer $offset The offset
	 *
	 * @throws \RuntimeException The collection is immutable
	 */
	public function offsetUnset($offset)
	{
		throw new \RuntimeException('This collection is immutable');
	}

	/**
	 * Retrieve the iterator object
	 *
	 * @return \ArrayIterator The iterator object
	 */
	public function getIterator()
	{
		return new \ArrayIterator($this->arrModels);
	}
}

class_alias(Collection::class, 'Model\Collection');


<?php

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao;

/**
 * Provide methods to handle file meta information.
 *
 * @property array $metaFields
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class MetaWizard extends Widget
{

	/**
	 * Submit user input
	 * @var boolean
	 */
	protected $blnSubmitInput = true;

	/**
	 * Template
	 * @var string
	 */
	protected $strTemplate = 'be_widget';

	/**
	 * Set an object property
	 *
	 * @param string $strKey   The property name
	 * @param mixed  $varValue The property value
	 */
	public function __set($strKey, $varValue)
	{
		switch ($strKey)
		{
			case 'metaFields':
				if (!array_is_assoc($varValue))
				{
					$varValue = array_combine($varValue, array_fill(0, \count($varValue), ''));
				}

				foreach($varValue as $strArrKey=>$varArrValue)
				{
					if (!\is_array($varArrValue))
					{
						$varValue[$strArrKey] = array('attributes'=>$varArrValue);
					}
				}

				$this->arrConfiguration['metaFields'] = $varValue;
				break;

			default:
				parent::__set($strKey, $varValue);
				break;
		}
	}

	/**
	 * Trim the values and add new languages if necessary
	 *
	 * @param mixed $varInput
	 *
	 * @return mixed
	 */
	public function validator($varInput)
	{
		if (!\is_array($varInput))
		{
			return null; // see #382
		}

		foreach ($varInput as $k=>$v)
		{
			if ($k != 'language')
			{
				$varInput[$k] = array_map('trim', $v);
			}
			else
			{
				if ($v != '')
				{
					// Take the fields from the DCA (see #4327)
					$varInput[$v] = array_combine(array_keys($this->metaFields), array_fill(0, \count($this->metaFields), ''));
				}

				unset($varInput[$k]);
			}
		}

		return $varInput;
	}

	/**
	 * Generate the widget and return it as string
	 *
	 * @return string
	 */
	public function generate()
	{
		$count = 0;
		$return = '';

		$this->import(Database::class, 'Database');
		$this->import(BackendUser::class, 'User');

		// Only show the root page languages (see #7112, #7667)
		$objRootLangs = $this->Database->query("SELECT REPLACE(language, '-', '_') AS language FROM tl_page WHERE type='root'");
		$existing = $objRootLangs->fetchEach('language');

		foreach ($existing as $lang)
		{
			if (!isset($this->varValue[$lang]))
			{
				$this->varValue[$lang] = array();
			}
		}

		// No languages defined in the site structure
		if (empty($this->varValue) || !\is_array($this->varValue))
		{
			return '
' . $GLOBALS['TL_LANG']['MSC']['metaNoLanguages'] . '
';
		}

		$languages = $this->getLanguages(true);

		// Add the existing entries
		if (!empty($this->varValue))
		{
			$return = '
strId . '" class="tl_metawizard dcapicker">';

			// Add the input fields
			foreach ($this->varValue as $lang=>$meta)
			{
				$return .= '

';

				$return .= '' . ($languages[$lang] ?? $lang) . ' ' . Image::getHtml('delete.svg', '', 'class="tl_metawizard_img" title="' . $GLOBALS['TL_LANG']['MSC']['delete'] . '" onclick="Backend.metaDelete(this)"') . '';

				// Take the fields from the DCA (see #4327)
				foreach ($this->metaFields as $field=>$fieldConfig)
				{
					$return .= '' . $GLOBALS['TL_LANG']['MSC']['aw_' . $field] . ' <input type="text" name="' . $this->strId . '[' . $lang . '][' . $field . ']" id="ctrl_' . $field . '_' . $count . '" class="tl_text" value="' . StringUtil::specialchars($meta[$field]) . '"' . (!empty($fieldConfig['attributes']) ? ' ' . $fieldConfig['attributes'] : '') . '>';

					// DCA picker
					if (isset($fieldConfig['dcaPicker']) && (\is_array($fieldConfig['dcaPicker']) || $fieldConfig['dcaPicker'] === true))
					{
						$return .= Backend::getDcaPickerWizard($fieldConfig['dcaPicker'], $this->strTable, $this->strField, $field . '_' . $count);
					}

					$return .= '
';
				}

				$return .= '

';

				unset($languages[$lang]);
				++$count;
			}

			$return .= '

';
		}

		$options = array('<option value="">-</option>');

		// Add the remaining languages
		foreach ($languages as $k=>$v)
		{
			$options[] = '<option value="' . $k . '">' . $v . '</option>';
		}

		$return .= '

    <select name="' . $this->strId . '[language]" class="tl_select tl_chosen" onchange="Backend.toggleAddLanguageButton(this)">' . implode('', $options) . '</select> <input type="button" class="tl_submit" disabled value="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['aw_new']) . '" onclick="Backend.metaWizard(this,\'ctrl_' . $this->strId . '\')">

';

		return $return;
	}
}

class_alias(MetaWizard::class, 'MetaWizard');


<?php

declare(strict_types=1);

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao\CoreBundle\EventListener;

use Contao\CoreBundle\Security\Authentication\Token\TokenChecker;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;

class BypassMaintenanceListener
{
    /**
     * @var TokenChecker
     */
    private $tokenChecker;

    /**
     * @var string
     */
    private $requestAttribute;

    public function __construct(TokenChecker $tokenChecker, string $requestAttribute = '_bypass_maintenance')
    {
        $this->tokenChecker = $tokenChecker;
        $this->requestAttribute = $requestAttribute;
    }

    /**
     * Adds the request attribute to the request.
     */
    public function onKernelRequest(GetResponseEvent $event): void
    {
        if (!$this->tokenChecker->hasBackendUser()) {
            return;
        }

        $request = $event->getRequest();
        $request->attributes->set($this->requestAttribute, true);
    }
}


<?php

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao;

/**
 * Maintenance module "maintenance mode".
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class Maintenance extends Backend implements \executable
{

	/**
	 * Return true if the module is active
	 *
	 * @return boolean
	 */
	public function isActive()
	{
		return false;
	}

	/**
	 * Generate the module
	 *
	 * @return string
	 */
	public function run()
	{
		$objTemplate = new BackendTemplate('be_maintenance_mode');
		$objTemplate->action = ampersand(Environment::get('request'));
		$objTemplate->headline = $GLOBALS['TL_LANG']['tl_maintenance']['maintenanceMode'];
		$objTemplate->isActive = $this->isActive();

		try
		{
			$driver = System::getContainer()->get('lexik_maintenance.driver.factory')->getDriver();
			$isLocked = $driver->isExists();
		}
		catch (\Exception $e)
		{
			return '';
		}

		// Toggle the maintenance mode
		if (Input::post('FORM_SUBMIT') == 'tl_maintenance_mode')
		{
			if ($isLocked)
			{
				$driver->unlock();
			}
			else
			{
				$driver->lock();
			}

			$this->reload();
		}

		if ($isLocked)
		{
			$objTemplate->class= 'tl_error';
			$objTemplate->explain = $GLOBALS['TL_LANG']['MSC']['maintenanceEnabled'];
			$objTemplate->submit = $GLOBALS['TL_LANG']['MSC']['disable'];
		}
		else
		{
			$objTemplate->class= 'tl_info';
			$objTemplate->submit = $GLOBALS['TL_LANG']['MSC']['enable'];
		}

		return $objTemplate->parse();
	}
}

class_alias(Maintenance::class, 'Maintenance');


<?php

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao;

use Contao\CoreBundle\Exception\ResponseException;
use Symfony\Component\HttpFoundation\RedirectResponse;

/**
 * Provide methods to handle list items.
 *
 * @property integer $maxlength
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ListWizard extends Widget
{

	/**
	 * Submit user input
	 * @var boolean
	 */
	protected $blnSubmitInput = true;

	/**
	 * Template
	 * @var string
	 */
	protected $strTemplate = 'be_widget';

	/**
	 * Add specific attributes
	 *
	 * @param string $strKey
	 * @param mixed  $varValue
	 */
	public function __set($strKey, $varValue)
	{
		switch ($strKey)
		{
			case 'maxlength':
				if ($varValue > 0)
				{
					$this->arrAttributes['maxlength'] = $varValue;
				}
				break;

			default:
				parent::__set($strKey, $varValue);
				break;
		}
	}

	/**
	 * Generate the widget and return it as string
	 *
	 * @return string
	 */
	public function generate()
	{
		$arrButtons = array('copy', 'delete', 'drag');

		// Make sure there is at least an empty array
		if (empty($this->varValue) || !\is_array($this->varValue))
		{
			$this->varValue = array('');
		}

		// Initialize the tab index
		if (!Cache::has('tabindex'))
		{
			Cache::set('tabindex', 1);
		}

		$return = '
strId.'" class="tl_listwizard">';

		// Add input fields
		for ($i=0, $c=\count($this->varValue); $i<$c; $i++)
		{
			$return .= '

<input type="text" name="'.$this->strId.'[]" class="tl_text" value="'.StringUtil::specialchars($this->varValue[$i]).'"' . $this->getAttributes() . '> ';

			// Add buttons
			foreach ($arrButtons as $button)
			{
				if ($button == 'drag')
				{
					$return .= ' <button type="button" class="drag-handle" title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['move']) . '" aria-hidden="true">' . Image::getHtml('drag.svg') . '</button>';
				}
				else
				{
					$return .= ' <button type="button" data-command="' . $button . '" title="' . StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['lw_'.$button]) . '">' . Image::getHtml($button.'.svg') . '</button>';
				}
			}

			$return .= '
';
		}

		return $return.'

  ';
	}

	/**
	 * Return a form to choose a CSV file and import it
	 *
	 * @param DataContainer $dc
	 *
	 * @return string
	 *
	 * @throws \Exception
	 * @throws ResponseException
	 *
	 * @deprecated Deprecated since Contao 4.3 to be removed in 5.0.
	 *             Use the contao.controller.backend_csv_import service instead.
	 */
	public function importList(DataContainer $dc)
	{
		$response = System::getContainer()->get('contao.controller.backend_csv_import')->importListWizardAction($dc);

		if ($response instanceof RedirectResponse)
		{
			throw new ResponseException($response);
		}

		return $response->getContent();
    }
}

class_alias(ListWizard::class, 'ListWizard');


<?php

declare(strict_types=1);

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao\CoreBundle\EventListener;

use Contao\CoreBundle\Monolog\ContaoContext;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;

class SwitchUserListener
{
    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(TokenStorageInterface $tokenStorage, LoggerInterface $logger)
    {
        $this->tokenStorage = $tokenStorage;
        $this->logger = $logger;
    }

    /**
     * Logs successful user impersonations.
     *
     * @throws \RuntimeException
     */
    public function onSwitchUser(SwitchUserEvent $event): void
    {
        $token = $this->tokenStorage->getToken();

        if (null === $token) {
            throw new \RuntimeException('The token storage did not contain a token.');
        }

        $sourceUser = $token->getUser();

        if ($sourceUser instanceof UserInterface) {
            $sourceUser = $sourceUser->getUsername();
        }

        $targetUser = $event->getTargetUser();

        if ($targetUser instanceof UserInterface) {
            $targetUser = $targetUser->getUsername();
        }

        $this->logger->info(
            sprintf('User "%s" has switched to user "%s"', $sourceUser, $targetUser),
            ['contao' => new ContaoContext(__METHOD__, ContaoContext::ACCESS, $sourceUser)]
        );
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao\CoreBundle\EventListener;

use Contao\CoreBundle\Csrf\MemoryTokenStorage;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;

class CsrfTokenCookieListener
{
    /**
     * @var MemoryTokenStorage
     */
    private $tokenStorage;

    /**
     * @var string
     */
    private $cookiePrefix;

    public function __construct(MemoryTokenStorage $tokenStorage, string $cookiePrefix = 'csrf_')
    {
        $this->tokenStorage = $tokenStorage;
        $this->cookiePrefix = $cookiePrefix;
    }

    /**
     * Reads the cookies from the request and injects them into the storage.
     */
    public function onKernelRequest(GetResponseEvent $event): void
    {
        if (!$event->isMasterRequest()) {
            return;
        }

        $this->tokenStorage->initialize($this->getTokensFromCookies($event->getRequest()->cookies));
    }

    /**
     * Adds the token cookies to the response.
     */
    public function onKernelResponse(FilterResponseEvent $event): void
    {
        if (!$event->isMasterRequest()) {
            return;
        }

        $request = $event->getRequest();
        $response = $event->getResponse();
        $isSecure = $request->isSecure();
        $basePath = $request->getBasePath() ?: '/';

        foreach ($this->tokenStorage->getUsedTokens() as $key => $value) {
            $cookieKey = $this->cookiePrefix.$key;

            // The cookie already exists
            if ($request->cookies->has($cookieKey) && $value === $request->cookies->get($cookieKey)) {
                continue;
            }

            $expires = null === $value ? 1 : 0;

            $response->headers->setCookie(
                new Cookie($cookieKey, $value, $expires, $basePath, null, $isSecure, true, false, Cookie::SAMESITE_LAX)
            );
        }
    }

    /**
     * @return array<string,string>
     */
    private function getTokensFromCookies(ParameterBag $cookies): array
    {
        $tokens = [];

        foreach ($cookies as $key => $value) {
            if (!\is_string($key)) {
                continue;
            }

            if (0 === strpos($key, $this->cookiePrefix) && preg_match('/^[a-z0-9_-]+$/i', $value)) {
                $tokens[substr($key, \strlen($this->cookiePrefix))] = $value;
            }
        }

        return $tokens;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao\NewsBundle\Picker;

use Contao\CoreBundle\Framework\FrameworkAwareInterface;
use Contao\CoreBundle\Framework\FrameworkAwareTrait;
use Contao\CoreBundle\Picker\AbstractPickerProvider;
use Contao\CoreBundle\Picker\DcaPickerProviderInterface;
use Contao\CoreBundle\Picker\PickerConfig;
use Contao\NewsArchiveModel;
use Contao\NewsModel;

class NewsPickerProvider extends AbstractPickerProvider implements DcaPickerProviderInterface, FrameworkAwareInterface
{
    use FrameworkAwareTrait;

    /**
     * {@inheritdoc}
     */
    public function getName(): string
    {
        return 'newsPicker';
    }

    /**
     * {@inheritdoc}
     */
    public function supportsContext($context): bool
    {
        return 'link' === $context && $this->getUser()->hasAccess('news', 'modules');
    }

    /**
     * {@inheritdoc}
     */
    public function supportsValue(PickerConfig $config): bool
    {
        return false !== strpos($config->getValue(), '{{news_url::');
    }

    /**
     * {@inheritdoc}
     */
    public function getDcaTable(): string
    {
        return 'tl_news';
    }

    /**
     * {@inheritdoc}
     */
    public function getDcaAttributes(PickerConfig $config): array
    {
        $attributes = ['fieldType' => 'radio'];

        if ($source = $config->getExtra('source')) {
            $attributes['preserveRecord'] = $source;
        }

        if ($this->supportsValue($config)) {
            $attributes['value'] = str_replace(['{{news_url::', '}}'], '', $config->getValue());
        }

        return $attributes;
    }

    /**
     * {@inheritdoc}
     */
    public function convertDcaValue(PickerConfig $config, $value): string
    {
        return '{{news_url::'.$value.'}}';
    }

    /**
     * {@inheritdoc}
     */
    protected function getRouteParameters(PickerConfig $config = null): array
    {
        $params = ['do' => 'news'];

        if (null === $config || !$config->getValue() || false === strpos($config->getValue(), '{{news_url::')) {
            return $params;
        }

        $value = str_replace(['{{news_url::', '}}'], '', $config->getValue());

        if (null !== ($newsArchiveId = $this->getNewsArchiveId($value))) {
            $params['table'] = 'tl_news';
            $params['id'] = $newsArchiveId;
        }

        return $params;
    }

    /**
     * @param int|string $id
     */
    private function getNewsArchiveId($id): ?int
    {
        /** @var NewsModel $newsAdapter */
        $newsAdapter = $this->framework->getAdapter(NewsModel::class);

        if (!($newsModel = $newsAdapter->findById($id)) instanceof NewsModel) {
            return null;
        }

        if (!($newsArchive = $newsModel->getRelated('pid')) instanceof NewsArchiveModel) {
            return null;
        }

        return (int) $newsArchive->id;
    }
}


<?php

declare(strict_types=1);

/*
 * This file is part of Contao.
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao\InstallationBundle\Database;

use Contao\CoreBundle\Doctrine\Schema\DcaSchemaProvider;
use Contao\System;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;

class Installer
{
    /**
     * @var Connection
     */
    private $connection;

    /**
     * @var array
     */
    private $commands;

    /**
     * @var DcaSchemaProvider
     */
    private $schemaProvider;

    public function __construct(Connection $connection, DcaSchemaProvider $schemaProvider)
    {
        $this->connection = $connection;
        $this->schemaProvider = $schemaProvider;
    }

    /**
     * @return string[]
     */
    public function getCommands(): array
    {
        if (null === $this->commands) {
            $this->compileCommands();
        }

        return $this->commands;
    }

    /**
     * @throws \InvalidArgumentException
     */
    public function execCommand(string $hash): void
    {
        if (null === $this->commands) {
            $this->compileCommands();
        }

        foreach ($this->commands as $commands) {
            if (isset($commands[$hash])) {
                $this->connection->query($commands[$hash]);

                return;
            }
        }

        throw new \InvalidArgumentException(sprintf('Invalid hash: %s', $hash));
    }

    /**
     * Compiles the command required to update the database.
     */
    private function compileCommands(): void
    {
        $return = [
            'CREATE' => [],
            'ALTER_TABLE' => [],
            'ALTER_CHANGE' => [],
            'ALTER_ADD' => [],
            'DROP' => [],
            'ALTER_DROP' => [],
        ];

        $config = $this->connection->getConfiguration();

        // Overwrite the schema filter (see #78)
        $previousFilter = $config->getFilterSchemaAssetsExpression();
        $config->setFilterSchemaAssetsExpression('/^tl_/');

        // Create the from and to schema
        $fromSchema = $this->connection->getSchemaManager()->createSchema();
        $toSchema = $this->schemaProvider->createSchema();

        // Reset the schema filter
        $config->setFilterSchemaAssetsExpression($previousFilter);

        $diff = $fromSchema->getMigrateToSql($toSchema, $this->connection->getDatabasePlatform());

        foreach ($diff as $sql) {
            switch (true) {
                case 0 === strncmp($sql, 'CREATE TABLE ', 13):
                    $return['CREATE'][md5($sql)] = $sql;
                    break;

                case 0 === strncmp($sql, 'DROP TABLE ', 11):
                    $return['DROP'][md5($sql)] = $sql;
                    break;

                case 0 === strncmp($sql, 'CREATE INDEX ', 13):
                case 0 === strncmp($sql, 'CREATE UNIQUE INDEX ', 20):
                case 0 === strncmp($sql, 'CREATE FULLTEXT INDEX ', 22):
                    $return['ALTER_ADD'][md5($sql)] = $sql;
                    break;

                case 0 === strncmp($sql, 'DROP INDEX', 10):
                    $return['ALTER_CHANGE'][md5($sql)] = $sql;
                    break;

                case preg_match('/^(ALTER TABLE [^ ]+) /', $sql, $matches):
                    $prefix = $matches[1];
                    $sql = substr($sql, \strlen($prefix));
                    $parts = array_reverse(array_map('trim', explode(',', $sql)));

                    for ($i = 0, $count = \count($parts); $i < $count; ++$i) {
                        $part = $parts[$i];
                        $command = $prefix.' '.$part;

                        switch (true) {
                            case 0 === strncmp($part, 'DROP ', 5):
                                $return['ALTER_DROP'][md5($command)] = $command;
                                break;

                            case 0 === strncmp($part, 'ADD ', 4):
                                $return['ALTER_ADD'][md5($command)] = $command;
                                break;

                            case 0 === strncmp($part, 'CHANGE ', 7):
                            case 0 === strncmp($part, 'RENAME ', 7):
                                $return['ALTER_CHANGE'][md5($command)] = $command;
                                break;

                            default:
                                $parts[$i + 1] = $parts[$i + 1].','.$part;
                                break;
                        }
                    }
                    break;

                default:
                    throw new \RuntimeException(sprintf('Unsupported SQL schema diff: %s', $sql));
            }
        }

        $this->checkEngineAndCollation($return, $fromSchema, $toSchema);

        $return = array_filter($return);

        // HOOK: allow third-party developers to modify the array (see #3281)
        if (isset($GLOBALS['TL_HOOKS']['sqlCompileCommands']) && \is_array($GLOBALS['TL_HOOKS']['sqlCompileCommands'])) {
            foreach ($GLOBALS['TL_HOOKS']['sqlCompileCommands'] as $callback) {
                $return = System::importStatic($callback[0])->{$callback[1]}($return);
            }
        }

        $this->commands = $return;
    }

    /**
     * Checks engine and collation and adds the ALTER TABLE queries.
     */
    private function checkEngineAndCollation(array &$sql, Schema $fromSchema, Schema $toSchema): void
    {
        $tables = $toSchema->getTables();
        $dynamic = $this->hasDynamicRowFormat();

        foreach ($tables as $table) {
            $tableName = $table->getName();
            $alterTables = [];
            $deleteIndexes = false;

            if (0 !== strncmp($tableName, 'tl_', 3)) {
                continue;
            }

            $this->setLegacyOptions($table);

            $tableOptions = $this->connection
                ->query("SHOW TABLE STATUS LIKE '".$tableName."'")
                ->fetch(\PDO::FETCH_OBJ)
            ;

            // The table does not yet exist
            if (false === $tableOptions) {
                continue;
            }

            $engine = $table->getOption('engine');
            $innodb = 'innodb' === strtolower($engine);

            if (strtolower($tableOptions->Engine) !== strtolower($engine)) {
                if ($innodb && $dynamic) {
                    $command = 'ALTER TABLE '.$tableName.' ENGINE = '.$engine.' ROW_FORMAT = DYNAMIC';
                } else {
                    $command = 'ALTER TABLE '.$tableName.' ENGINE = '.$engine;
                }

                $deleteIndexes = true;
                $alterTables[md5($command)] = $command;
            } elseif ($innodb && $dynamic) {
                $rowFormat = $table->getOption('row_format');

                if ($rowFormat && strtolower($tableOptions->Row_format) !== strtolower($rowFormat)) {
                    $command = 'ALTER TABLE '.$tableName.' ENGINE = '.$engine.' ROW_FORMAT = DYNAMIC';
                    $alterTables[md5($command)] = $command;
                }
            }

            $collate = $table->getOption('collate');

            if ($tableOptions->Collation !== $collate) {
                $charset = $table->getOption('charset');
                $command = 'ALTER TABLE '.$tableName.' CONVERT TO CHARACTER SET '.$charset.' COLLATE '.$collate;
                $deleteIndexes = true;
                $alterTables[md5($command)] = $command;
            }

            // Delete the indexes if the engine changes in case the existing
            // indexes are too long. The migration then needs to be run muliple
            // times to re-create the indexes with the correct length.
            if ($deleteIndexes) {
                if (!$fromSchema->hasTable($tableName)) {
                    continue;
                }

                $platform = $this->connection->getDatabasePlatform();

                foreach ($fromSchema->getTable($tableName)->getIndexes() as $index) {
                    $indexName = $index->getName();

                    if ('primary' === strtolower($indexName)) {
                        continue;
                    }

                    $indexCommand = $platform->getDropIndexSQL($indexName, $tableName);
                    $strKey = md5($indexCommand);

                    if (isset($sql['ALTER_CHANGE'][$strKey])) {
                        unset($sql['ALTER_CHANGE'][$strKey]);
                    }

                    $sql['ALTER_TABLE'][$strKey] = $indexCommand;
                }
            }

            foreach ($alterTables as $k => $alterTable) {
                $sql['ALTER_TABLE'][$k] = $alterTable;
            }
        }
    }

    private function hasDynamicRowFormat(): bool
    {
        $filePerTable = $this->connection
            ->query("SHOW VARIABLES LIKE 'innodb_file_per_table'")
            ->fetch(\PDO::FETCH_OBJ)
        ;

        // Dynamic rows require innodb_file_per_table to be enabled
        if (!\in_array(strtolower((string) $filePerTable->Value), ['1', 'on'], true)) {
            return false;
        }

        $fileFormat = $this->connection
            ->query("SHOW VARIABLES LIKE 'innodb_file_format'")
            ->fetch(\PDO::FETCH_OBJ)
        ;

        // MySQL 8 and MariaDB 10.3 no longer have the "innodb_file_format" setting
        if (false === $fileFormat) {
            return true;
        }

        // Dynamic rows require the Barracuda file format in MySQL <8 and MariaDB <10.3
        return 'barracuda' === strtolower((string) $fileFormat->Value);
    }

    /**
     * Adds the legacy table options to remain backwards compatibility with database.sql files.
     */
    private function setLegacyOptions(Table $table): void
    {
        if (!$table->hasOption('engine')) {
            $table->addOption('engine', 'MyISAM');
        }

        if (!$table->hasOption('charset')) {
            $table->addOption('charset', 'utf8');
        }

        if (!$table->hasOption('collate')) {
            $table->addOption('collate', 'utf8_general_ci');
        }
    }
}