<?php

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

namespace Contao;

use Contao\CoreBundle\Asset\ContaoContext;
use Contao\CoreBundle\Exception\AccessDeniedException;
use Contao\CoreBundle\Exception\AjaxRedirectResponseException;
use Contao\CoreBundle\Exception\PageNotFoundException;
use Contao\CoreBundle\Exception\RedirectResponseException;
use Contao\Database\Result;
use Contao\Image\PictureConfigurationInterface;
use Contao\Model\Collection;
use League\Uri\Components\Query;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\Glob;

/**
 * Abstract parent class for Controllers
 *
 * Some of the methods have been made static in Contao 3 and can be used in
 * non-object context as well.
 *
 * Usage:
 *
 *     echo Controller::getTheme();
 *
 * Inside a controller:
 *
 *     public function generate()
 *     {
 *         return $this->getArticle(2);
 *     }
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
abstract class Controller extends System
{

	/**
	 * Find a particular template file and return its path
	 *
	 * @param string $strTemplate The name of the template
	 *
	 * @return string The path to the template file
	 *
	 * @throws \RuntimeException If the template group folder is insecure
	 */
	public static function getTemplate($strTemplate)
	{
		$strTemplate = basename($strTemplate);

		// Check for a theme folder
		if (\defined('TL_MODE') && TL_MODE == 'FE')
		{
			/** @var PageModel $objPage */
			global $objPage;

			if ($objPage->templateGroup != '')
			{
				if (Validator::isInsecurePath($objPage->templateGroup))
				{
					throw new \RuntimeException('Invalid path ' . $objPage->templateGroup);
				}

				return TemplateLoader::getPath($strTemplate, 'html5', $objPage->templateGroup);
			}
		}

		return TemplateLoader::getPath($strTemplate, 'html5');
	}

	/**
	 * Return all template files of a particular group as array
	 *
	 * @param string $strPrefix The template name prefix (e.g. "ce_")
	 *
	 * @return array An array of template names
	 */
	public static function getTemplateGroup($strPrefix)
	{
		$arrTemplates = array();

		// Get the default templates
		foreach (TemplateLoader::getPrefixedFiles($strPrefix) as $strTemplate)
		{
			$arrTemplates[$strTemplate][] = 'root';
		}

		$rootDir = System::getContainer()->getParameter('kernel.project_dir');
		$arrCustomized = self::braceGlob($rootDir . '/templates/' . $strPrefix . '*.html5');

		// Add the customized templates
		if (\is_array($arrCustomized))
		{
			foreach ($arrCustomized as $strFile)
			{
				$strTemplate = basename($strFile, strrchr($strFile, '.'));
				$arrTemplates[$strTemplate][] = $GLOBALS['TL_LANG']['MSC']['global'];
			}
		}

		// Do not look for back end templates in theme folders (see #5379)
		if ($strPrefix != 'be_' && $strPrefix != 'mail_')
		{
			// Try to select the themes (see #5210)
			try
			{
				$objTheme = ThemeModel::findAll(array('order'=>'name'));
			}
			catch (\Exception $e)
			{
				$objTheme = null;
			}

			// Add the theme templates
			if ($objTheme !== null)
			{
				while ($objTheme->next())
				{
					if ($objTheme->templates != '')
					{
						$arrThemeTemplates = self::braceGlob($rootDir . '/' . $objTheme->templates . '/' . $strPrefix . '*.html5');

						if (\is_array($arrThemeTemplates))
						{
							foreach ($arrThemeTemplates as $strFile)
							{
								$strTemplate = basename($strFile, strrchr($strFile, '.'));
								$arrTemplates[$strTemplate][] = $objTheme->name;
							}
						}
					}
				}
			}
		}

		// Show the template sources (see #6875)
		foreach ($arrTemplates as $k=>$v)
		{
			$v = array_filter($v, function ($a) {
				return $a != 'root';
			});

			if (empty($v))
			{
				$arrTemplates[$k] = $k;
			}
			else
			{
				$arrTemplates[$k] = $k . ' (' . implode(', ', $v) . ')';
			}
		}

		// Sort the template names
		ksort($arrTemplates);

		return $arrTemplates;
	}

	/**
	 * Generate a front end module and return it as string
	 *
	 * @param mixed  $intId     A module ID or a Model object
	 * @param string $strColumn The name of the column
	 *
	 * @return string The module HTML markup
	 */
	public static function getFrontendModule($intId, $strColumn='main')
	{
		if (!\is_object($intId) && !\strlen($intId))
		{
			return '';
		}

		/** @var PageModel $objPage */
		global $objPage;

		// Articles
		if (!\is_object($intId) && $intId == 0)
		{
			// Show a particular article only
			if ($objPage->type == 'regular' && Input::get('articles'))
			{
				list($strSection, $strArticle) = explode(':', Input::get('articles'));

				if ($strArticle === null)
				{
					$strArticle = $strSection;
					$strSection = 'main';
				}

				if ($strSection == $strColumn)
				{
					$objArticle = ArticleModel::findPublishedByIdOrAliasAndPid($strArticle, $objPage->id);

					// Send a 404 header if there is no published article
					if (null === $objArticle)
					{
						throw new PageNotFoundException('Page not found: ' . Environment::get('uri'));
					}

					// Send a 403 header if the article cannot be accessed
					if (!static::isVisibleElement($objArticle))
					{
						throw new AccessDeniedException('Access denied: ' . Environment::get('uri'));
					}

					// Add the "first" and "last" classes (see #2583)
					$objArticle->classes = array('first', 'last');

					return static::getArticle($objArticle);
				}
			}

			// HOOK: add custom logic
			if (isset($GLOBALS['TL_HOOKS']['getArticles']) && \is_array($GLOBALS['TL_HOOKS']['getArticles']))
			{
				foreach ($GLOBALS['TL_HOOKS']['getArticles'] as $callback)
				{
					$return = static::importStatic($callback[0])->{$callback[1]}($objPage->id, $strColumn);

					if (\is_string($return))
					{
						return $return;
					}
				}
			}

			// Show all articles (no else block here, see #4740)
			$objArticles = ArticleModel::findPublishedByPidAndColumn($objPage->id, $strColumn);

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

			$return = '';
			$intCount = 0;
			$blnMultiMode = ($objArticles->count() > 1);
			$intLast = $objArticles->count() - 1;

			while ($objArticles->next())
			{
				/** @var ArticleModel $objRow */
				$objRow = $objArticles->current();

				// Add the "first" and "last" classes (see #2583)
				if ($intCount == 0 || $intCount == $intLast)
				{
					$arrCss = array();

					if ($intCount == 0)
					{
						$arrCss[] = 'first';
					}

					if ($intCount == $intLast)
					{
						$arrCss[] = 'last';
					}

					$objRow->classes = $arrCss;
				}

				$return .= static::getArticle($objRow, $blnMultiMode, false, $strColumn);
				++$intCount;
			}

			return $return;
		}

		// Other modules
		else
		{
			if (\is_object($intId))
			{
				$objRow = $intId;
			}
			else
			{
				$objRow = ModuleModel::findByPk($intId);

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

			// Check the visibility (see #6311)
			if (!static::isVisibleElement($objRow))
			{
				return '';
			}

			$strClass = Module::findClass($objRow->type);

			// Return if the class does not exist
			if (!class_exists($strClass))
			{
				static::log('Module class "'.$strClass.'" (module "'.$objRow->type.'") does not exist', __METHOD__, TL_ERROR);

				return '';
			}

			$objRow->typePrefix = 'mod_';

			/** @var Module $objModule */
			$objModule = new $strClass($objRow, $strColumn);
			$strBuffer = $objModule->generate();

			// HOOK: add custom logic
			if (isset($GLOBALS['TL_HOOKS']['getFrontendModule']) && \is_array($GLOBALS['TL_HOOKS']['getFrontendModule']))
			{
				foreach ($GLOBALS['TL_HOOKS']['getFrontendModule'] as $callback)
				{
					$strBuffer = static::importStatic($callback[0])->{$callback[1]}($objRow, $strBuffer, $objModule);
				}
			}

			// Disable indexing if protected
			if ($objModule->protected && !preg_match('/^\s*<!-- indexer::stop/', $strBuffer))
			{
				$strBuffer = "\n<!-- indexer::stop -->". $strBuffer ."<!-- indexer::continue -->\n";
			}

			return $strBuffer;
		}
	}

	/**
	 * Generate an article and return it as string
	 *
	 * @param mixed   $varId          The article ID or a Model object
	 * @param boolean $blnMultiMode   If true, only teasers will be shown
	 * @param boolean $blnIsInsertTag If true, there will be no page relation
	 * @param string  $strColumn      The name of the column
	 *
	 * @return string|boolean The article HTML markup or false
	 */
	public static function getArticle($varId, $blnMultiMode=false, $blnIsInsertTag=false, $strColumn='main')
	{
		/** @var PageModel $objPage */
		global $objPage;

		if (\is_object($varId))
		{
			$objRow = $varId;
		}
		else
		{
			if (!$varId)
			{
				return '';
			}

			$objRow = ArticleModel::findByIdOrAliasAndPid($varId, (!$blnIsInsertTag ? $objPage->id : null));

			if ($objRow === null)
			{
				return false;
			}
		}

		// Check the visibility (see #6311)
		if (!static::isVisibleElement($objRow))
		{
			return '';
		}

		// Print the article as PDF
		if (isset($_GET['pdf']) && Input::get('pdf') == $objRow->id)
		{
			// Deprecated since Contao 4.0, to be removed in Contao 5.0
			if ($objRow->printable == 1)
			{
				@trigger_error('Setting tl_article.printable to "1" has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

				$objArticle = new ModuleArticle($objRow);
				$objArticle->generatePdf();
			}
			elseif ($objRow->printable != '')
			{
				$options = StringUtil::deserialize($objRow->printable);

				if (\is_array($options) && \in_array('pdf', $options))
				{
					$objArticle = new ModuleArticle($objRow);
					$objArticle->generatePdf();
				}
			}
		}

		$objRow->headline = $objRow->title;
		$objRow->multiMode = $blnMultiMode;

		// HOOK: add custom logic
		if (isset($GLOBALS['TL_HOOKS']['getArticle']) && \is_array($GLOBALS['TL_HOOKS']['getArticle']))
		{
			foreach ($GLOBALS['TL_HOOKS']['getArticle'] as $callback)
			{
				static::importStatic($callback[0])->{$callback[1]}($objRow);
			}
		}

		$objArticle = new ModuleArticle($objRow, $strColumn);
		$strBuffer = $objArticle->generate($blnIsInsertTag);

		// Disable indexing if protected
		if ($objArticle->protected && !preg_match('/^\s*<!-- indexer::stop/', $strBuffer))
		{
			$strBuffer = "\n<!-- indexer::stop -->". $strBuffer ."<!-- indexer::continue -->\n";
		}

		return $strBuffer;
	}

	/**
	 * Generate a content element and return it as string
	 *
	 * @param mixed  $intId     A content element ID or a Model object
	 * @param string $strColumn The column the element is in
	 *
	 * @return string The content element HTML markup
	 */
	public static function getContentElement($intId, $strColumn='main')
	{
		if (\is_object($intId))
		{
			$objRow = $intId;
		}
		else
		{
			if (!\strlen($intId) || $intId < 1)
			{
				return '';
			}

			$objRow = ContentModel::findByPk($intId);

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

		// Check the visibility (see #6311)
		if (!static::isVisibleElement($objRow))
		{
			return '';
		}

		$strClass = ContentElement::findClass($objRow->type);

		// Return if the class does not exist
		if (!class_exists($strClass))
		{
			static::log('Content element class "'.$strClass.'" (content element "'.$objRow->type.'") does not exist', __METHOD__, TL_ERROR);

			return '';
		}

		$objRow->typePrefix = 'ce_';

		/** @var ContentElement $objElement */
		$objElement = new $strClass($objRow, $strColumn);
		$strBuffer = $objElement->generate();

		// HOOK: add custom logic
		if (isset($GLOBALS['TL_HOOKS']['getContentElement']) && \is_array($GLOBALS['TL_HOOKS']['getContentElement']))
		{
			foreach ($GLOBALS['TL_HOOKS']['getContentElement'] as $callback)
			{
				$strBuffer = static::importStatic($callback[0])->{$callback[1]}($objRow, $strBuffer, $objElement);
			}
		}

		// Disable indexing if protected
		if ($objElement->protected && !preg_match('/^\s*<!-- indexer::stop/', $strBuffer))
		{
			$strBuffer = "\n<!-- indexer::stop -->". $strBuffer ."<!-- indexer::continue -->\n";
		}

		return $strBuffer;
	}

	/**
	 * Generate a form and return it as string
	 *
	 * @param mixed   $varId     A form ID or a Model object
	 * @param string  $strColumn The column the form is in
	 * @param boolean $blnModule Render the form as module
	 *
	 * @return string The form HTML markup
	 */
	public static function getForm($varId, $strColumn='main', $blnModule=false)
	{
		if (\is_object($varId))
		{
			$objRow = $varId;
		}
		else
		{
			if ($varId == '')
			{
				return '';
			}

			$objRow = FormModel::findByIdOrAlias($varId);

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

		$strClass = $blnModule ? Module::findClass('form') : ContentElement::findClass('form');

		if (!class_exists($strClass))
		{
			static::log('Form class "'.$strClass.'" does not exist', __METHOD__, TL_ERROR);

			return '';
		}

		$objRow->typePrefix = $blnModule ? 'mod_' : 'ce_';
		$objRow->form = $objRow->id;

		/** @var Form $objElement */
		$objElement = new $strClass($objRow, $strColumn);
		$strBuffer = $objElement->generate();

		// HOOK: add custom logic
		if (isset($GLOBALS['TL_HOOKS']['getForm']) && \is_array($GLOBALS['TL_HOOKS']['getForm']))
		{
			foreach ($GLOBALS['TL_HOOKS']['getForm'] as $callback)
			{
				$strBuffer = static::importStatic($callback[0])->{$callback[1]}($objRow, $strBuffer, $objElement);
			}
		}

		return $strBuffer;
	}

	/**
	 * Return the languages for the TinyMCE spellchecker
	 *
	 * @return string The TinyMCE spellchecker language string
	 */
	protected function getSpellcheckerString()
	{
		System::loadLanguageFile('languages');

		$return = array();
		$langs = scan(__DIR__ . '/../../languages');
		array_unshift($langs, $GLOBALS['TL_LANGUAGE']);

		foreach ($langs as $lang)
		{
			$lang = substr($lang, 0, 2);

			if (isset($GLOBALS['TL_LANG']['LNG'][$lang]))
			{
				$return[$lang] = $GLOBALS['TL_LANG']['LNG'][$lang] . '=' . $lang;
			}
		}

		return '+' . implode(',', array_unique($return));
	}

	/**
	 * Calculate the page status icon name based on the page parameters
	 *
	 * @param PageModel|Result|\stdClass $objPage The page object
	 *
	 * @return string The status icon name
	 */
	public static function getPageStatusIcon($objPage)
	{
		$sub = 0;
		$image = $objPage->type.'.svg';

		// Page not published or not active
		if (!$objPage->published || ($objPage->start != '' && $objPage->start > time()) || ($objPage->stop != '' && $objPage->stop < time()))
		{
			++$sub;
		}

		// Page hidden from menu
		if ($objPage->hide && !\in_array($objPage->type, array('root', 'error_401', 'error_403', 'error_404')))
		{
			$sub += 2;
		}

		// Page protected
		if ($objPage->protected && !\in_array($objPage->type, array('root', 'error_401', 'error_403', 'error_404')))
		{
			$sub += 4;
		}

		// Get the image name
		if ($sub > 0)
		{
			$image = $objPage->type.'_'.$sub.'.svg';
		}

		// HOOK: add custom logic
		if (isset($GLOBALS['TL_HOOKS']['getPageStatusIcon']) && \is_array($GLOBALS['TL_HOOKS']['getPageStatusIcon']))
		{
			foreach ($GLOBALS['TL_HOOKS']['getPageStatusIcon'] as $callback)
			{
				$image = static::importStatic($callback[0])->{$callback[1]}($objPage, $image);
			}
		}

		return $image;
	}

	/**
	 * Check whether an element is visible in the front end
	 *
	 * @param Model|ContentModel|ModuleModel $objElement The element model
	 *
	 * @return boolean True if the element is visible
	 */
	public static function isVisibleElement(Model $objElement)
	{
		$blnReturn = true;

		// Only apply the restrictions in the front end
		if (TL_MODE == 'FE')
		{
			// Protected element
			if ($objElement->protected)
			{
				if (!FE_USER_LOGGED_IN)
				{
					$blnReturn = false;
				}
				else
				{
					$objUser = FrontendUser::getInstance();

					if (!\is_array($objUser->groups))
					{
						$blnReturn = false;
					}
					else
					{
						$groups = StringUtil::deserialize($objElement->groups);

						if (empty($groups) || !\is_array($groups) || !\count(array_intersect($groups, $objUser->groups)))
						{
							$blnReturn = false;
						}
					}
				}
			}

			// Show to guests only
			elseif ($objElement->guests && FE_USER_LOGGED_IN)
			{
				$blnReturn = false;
			}
		}

		// HOOK: add custom logic
		if (isset($GLOBALS['TL_HOOKS']['isVisibleElement']) && \is_array($GLOBALS['TL_HOOKS']['isVisibleElement']))
		{
			foreach ($GLOBALS['TL_HOOKS']['isVisibleElement'] as $callback)
			{
				$blnReturn = static::importStatic($callback[0])->{$callback[1]}($objElement, $blnReturn);
			}
		}

		return $blnReturn;
	}

	/**
	 * Replace insert tags with their values
	 *
	 * @param string  $strBuffer The text with the tags to be replaced
	 * @param boolean $blnCache  If false, non-cacheable tags will be replaced
	 *
	 * @return string The text with the replaced tags
	 */
	public static function replaceInsertTags($strBuffer, $blnCache=true)
	{
		$objIt = new InsertTags();

		return $objIt->replace($strBuffer, $blnCache);
	}

	/**
	 * Replace the dynamic script tags (see #4203)
	 *
	 * @param string $strBuffer The string with the tags to be replaced
	 *
	 * @return string The string with the replaced tags
	 */
	public static function replaceDynamicScriptTags($strBuffer)
	{
		// HOOK: add custom logic
		if (isset($GLOBALS['TL_HOOKS']['replaceDynamicScriptTags']) && \is_array($GLOBALS['TL_HOOKS']['replaceDynamicScriptTags']))
		{
			foreach ($GLOBALS['TL_HOOKS']['replaceDynamicScriptTags'] as $callback)
			{
				$strBuffer = static::importStatic($callback[0])->{$callback[1]}($strBuffer);
			}
		}

		$arrReplace = array();
		$strScripts = '';

		// Add the internal jQuery scripts
		if (!empty($GLOBALS['TL_JQUERY']) && \is_array($GLOBALS['TL_JQUERY']))
		{
			foreach (array_unique($GLOBALS['TL_JQUERY']) as $script)
			{
				$strScripts .= $script;
			}
		}

		$arrReplace['[[TL_JQUERY]]'] = $strScripts;
		$strScripts = '';

		// Add the internal MooTools scripts
		if (!empty($GLOBALS['TL_MOOTOOLS']) && \is_array($GLOBALS['TL_MOOTOOLS']))
		{
			foreach (array_unique($GLOBALS['TL_MOOTOOLS']) as $script)
			{
				$strScripts .= $script;
			}
		}

		$arrReplace['[[TL_MOOTOOLS]]'] = $strScripts;
		$strScripts = '';

		// Add the internal <body> tags
		if (!empty($GLOBALS['TL_BODY']) && \is_array($GLOBALS['TL_BODY']))
		{
			foreach (array_unique($GLOBALS['TL_BODY']) as $script)
			{
				$strScripts .= $script;
			}
		}

		// FE preview support
		if (System::getContainer()->get('contao.security.token_checker')->hasBackendUser())
		{
			$strScripts .= "
<script>
  (function(win) {
    if (!win.parent || typeof(win.parent.postMessage) !== 'function') {
      return;
    }
    win.parent.postMessage({
      'contao.preview': {
        'title': win.document.title,
        'uri': win.location.href
      }}, win.location.origin);
  })(window);
</script>";
		}

		global $objPage;

		$objLayout = LayoutModel::findByPk($objPage->layoutId);
		$blnCombineScripts = ($objLayout === null) ? false : $objLayout->combineScripts;

		$arrReplace['[[TL_BODY]]'] = $strScripts;
		$strScripts = '';

		$objCombiner = new Combiner();

		// Add the CSS framework style sheets
		if (!empty($GLOBALS['TL_FRAMEWORK_CSS']) && \is_array($GLOBALS['TL_FRAMEWORK_CSS']))
		{
			foreach (array_unique($GLOBALS['TL_FRAMEWORK_CSS']) as $stylesheet)
			{
				$objCombiner->add($stylesheet);
			}
		}

		// Add the internal style sheets
		if (!empty($GLOBALS['TL_CSS']) && \is_array($GLOBALS['TL_CSS']))
		{
			foreach (array_unique($GLOBALS['TL_CSS']) as $stylesheet)
			{
				$options = StringUtil::resolveFlaggedUrl($stylesheet);

				if ($options->static)
				{
					$objCombiner->add($stylesheet, $options->mtime, $options->media);
				}
				else
				{
					$strScripts .= Template::generateStyleTag(static::addAssetsUrlTo($stylesheet), $options->media, $options->mtime);
				}
			}
		}

		// Add the user style sheets
		if (!empty($GLOBALS['TL_USER_CSS']) && \is_array($GLOBALS['TL_USER_CSS']))
		{
			foreach (array_unique($GLOBALS['TL_USER_CSS']) as $stylesheet)
			{
				$options = StringUtil::resolveFlaggedUrl($stylesheet);

				if ($options->static)
				{
					$objCombiner->add($stylesheet, $options->mtime, $options->media);
				}
				else
				{
					$strScripts .= Template::generateStyleTag(static::addAssetsUrlTo($stylesheet), $options->media, $options->mtime);
				}
			}
		}

		// Create the aggregated style sheet
		if ($objCombiner->hasEntries())
		{
			if ($blnCombineScripts)
			{
				$strScripts .= Template::generateStyleTag($objCombiner->getCombinedFile(), 'all');
			}
			else
			{
				foreach ($objCombiner->getFileUrls() as $strUrl)
				{
					$options = StringUtil::resolveFlaggedUrl($strUrl);
					$strScripts .= Template::generateStyleTag($strUrl, $options->media, $options->mtime);
				}
			}
		}

		$arrReplace['[[TL_CSS]]'] = $strScripts;
		$strScripts = '';

		// Add the internal scripts
		if (!empty($GLOBALS['TL_JAVASCRIPT']) && \is_array($GLOBALS['TL_JAVASCRIPT']))
		{
			$objCombiner = new Combiner();
			$objCombinerAsync = new Combiner();

			foreach (array_unique($GLOBALS['TL_JAVASCRIPT']) as $javascript)
			{
				$options = StringUtil::resolveFlaggedUrl($javascript);

				if ($options->static)
				{
					$options->async ? $objCombinerAsync->add($javascript, $options->mtime) : $objCombiner->add($javascript, $options->mtime);
				}
				else
				{
					$strScripts .= Template::generateScriptTag(static::addAssetsUrlTo($javascript), $options->async, $options->mtime);
				}
			}

			// Create the aggregated script and add it before the non-static scripts (see #4890)
			if ($objCombiner->hasEntries())
			{
				if ($blnCombineScripts)
				{
					$strScripts = Template::generateScriptTag($objCombiner->getCombinedFile()) . $strScripts;
				}
				else
				{
					$arrReversed = array_reverse($objCombiner->getFileUrls());

					foreach ($arrReversed as $strUrl)
					{
						$options = StringUtil::resolveFlaggedUrl($strUrl);
						$strScripts = Template::generateScriptTag($strUrl, false, $options->mtime) . $strScripts;
					}
				}
			}

			if ($objCombinerAsync->hasEntries())
			{
				if ($blnCombineScripts)
				{
					$strScripts = Template::generateScriptTag($objCombinerAsync->getCombinedFile(), true) . $strScripts;
				}
				else
				{
					$arrReversed = array_reverse($objCombinerAsync->getFileUrls());

					foreach ($arrReversed as $strUrl)
					{
						$options = StringUtil::resolveFlaggedUrl($strUrl);
						$strScripts = Template::generateScriptTag($strUrl, true, $options->mtime) . $strScripts;
					}
				}
			}
		}

		// Add the internal <head> tags
		if (!empty($GLOBALS['TL_HEAD']) && \is_array($GLOBALS['TL_HEAD']))
		{
			foreach (array_unique($GLOBALS['TL_HEAD']) as $head)
			{
				$strScripts .= $head;
			}
		}

		$arrReplace['[[TL_HEAD]]'] = $strScripts;

		return str_replace(array_keys($arrReplace), $arrReplace, $strBuffer);
	}

	/**
	 * Compile the margin format definition based on an array of values
	 *
	 * @param array  $arrValues An array of four values and a unit
	 * @param string $strType   Either "margin" or "padding"
	 *
	 * @return string The CSS markup
	 */
	public static function generateMargin($arrValues, $strType='margin')
	{
		// Initialize an empty array (see #5217)
		if (!\is_array($arrValues))
		{
			$arrValues = array('top'=>'', 'right'=>'', 'bottom'=>'', 'left'=>'', 'unit'=>'');
		}

		$top = $arrValues['top'];
		$right = $arrValues['right'];
		$bottom = $arrValues['bottom'];
		$left = $arrValues['left'];

		// Try to shorten the definition
		if ($top != '' && $right != '' && $bottom != '' && $left != '')
		{
			if ($top == $right && $top == $bottom && $top == $left)
			{
				return $strType . ':' . $top . $arrValues['unit'] . ';';
			}
			elseif ($top == $bottom && $right == $left)
			{
				return $strType . ':' . $top . $arrValues['unit'] . ' ' . $left . $arrValues['unit'] . ';';
			}
			elseif ($top != $bottom && $right == $left)
			{
				return $strType . ':' . $top . $arrValues['unit'] . ' ' . $right . $arrValues['unit'] . ' ' . $bottom . $arrValues['unit'] . ';';
			}
			else
			{
				return $strType . ':' . $top . $arrValues['unit'] . ' ' . $right . $arrValues['unit'] . ' ' . $bottom . $arrValues['unit'] . ' ' . $left . $arrValues['unit'] . ';';
			}
		}

		$return = array();
		$arrDir = compact('top', 'right', 'bottom', 'left');

		foreach ($arrDir as $k=>$v)
		{
			if ($v != '')
			{
				$return[] = $strType . '-' . $k . ':' . $v . $arrValues['unit'] . ';';
			}
		}

		return implode($return);
	}

	/**
	 * Add a request string to the current URL
	 *
	 * @param string  $strRequest The string to be added
	 * @param boolean $blnAddRef  Add the referer ID
	 * @param array   $arrUnset   An optional array of keys to unset
	 *
	 * @return string The new URL
	 */
	public static function addToUrl($strRequest, $blnAddRef=true, $arrUnset=array())
	{
		/** @var Query $query */
		$query = new Query(Environment::get('queryString'));

		// Remove the request token and referer ID
		$query = $query->withoutPairs(array_merge(array('rt', 'ref'), $arrUnset));

		// Merge the request string to be added
		$query = $query->merge(str_replace('&', '&', $strRequest));

		// Add the referer ID
		if (isset($_GET['ref']) || ($strRequest != '' && $blnAddRef))
		{
			$query = $query->merge('ref=' . System::getContainer()->get('request_stack')->getCurrentRequest()->attributes->get('_contao_referer_id'));
		}

		$uri = $query->getUriComponent();

		// The query parser automatically converts %2B to +, so re-convert it here
		if (strpos($strRequest, '%2B') !== false)
		{
			$uri = str_replace('+', '%2B', $uri);
		}

		return TL_SCRIPT . ampersand($uri);
	}

	/**
	 * Reload the current page
	 */
	public static function reload()
	{
		static::redirect(Environment::get('uri'));
	}

	/**
	 * Redirect to another page
	 *
	 * @param string  $strLocation The target URL
	 * @param integer $intStatus   The HTTP status code (defaults to 303)
	 */
	public static function redirect($strLocation, $intStatus=303)
	{
		$strLocation = str_replace('&', '&', $strLocation);
		$strLocation = static::replaceOldBePaths($strLocation);

		// Make the location an absolute URL
		if (!preg_match('@^https?://@i', $strLocation))
		{
			$strLocation = Environment::get('base') . ltrim($strLocation, '/');
		}

		// Ajax request
		if (Environment::get('isAjaxRequest'))
		{
			throw new AjaxRedirectResponseException($strLocation);
		}

		throw new RedirectResponseException($strLocation, $intStatus);
	}

	/**
	 * Replace the old back end paths
	 *
	 * @param string $strContext The context
	 *
	 * @return string The modified context
	 */
	protected static function replaceOldBePaths($strContext)
	{
		$router = System::getContainer()->get('router');

		$generate = function ($route) use ($router)
		{
			return substr($router->generate($route), \strlen(Environment::get('path')) + 1);
		};

		$arrMapper = array
		(
			'contao/confirm.php'   => $generate('contao_backend_confirm'),
			'contao/file.php'      => $generate('contao_backend_file'),
			'contao/help.php'      => $generate('contao_backend_help'),
			'contao/index.php'     => $generate('contao_backend_login'),
			'contao/main.php'      => $generate('contao_backend'),
			'contao/page.php'      => $generate('contao_backend_page'),
			'contao/password.php'  => $generate('contao_backend_password'),
			'contao/popup.php'     => $generate('contao_backend_popup'),
			'contao/preview.php'   => $generate('contao_backend_preview'),
			'contao/switch.php'    => $generate('contao_backend_switch')
		);

		return str_replace(array_keys($arrMapper), $arrMapper, $strContext);
	}

	/**
	 * Generate a front end URL
	 *
	 * @param array   $arrRow       An array of page parameters
	 * @param string  $strParams    An optional string of URL parameters
	 * @param string  $strForceLang Force a certain language
	 * @param boolean $blnFixDomain Check the domain of the target page and append it if necessary
	 *
	 * @return string An URL that can be used in the front end
	 *
	 * @deprecated Deprecated since Contao 4.2, to be removed in Contao 5.0.
	 *             Use the contao.routing.url_generator service or PageModel::getFrontendUrl() instead.
	 */
	public static function generateFrontendUrl(array $arrRow, $strParams=null, $strForceLang=null, $blnFixDomain=false)
	{
		@trigger_error('Using Controller::generateFrontendUrl() has been deprecated and will no longer work in Contao 5.0. Use the contao.routing.url_generator service or PageModel::getFrontendUrl() instead.', E_USER_DEPRECATED);

		if (!isset($arrRow['rootId']))
		{
			$row = PageModel::findWithDetails($arrRow['id']);

			$arrRow['rootId'] = $row->rootId;

			foreach (array('domain', 'rootLanguage', 'rootUseSSL') as $key)
			{
				if (!isset($arrRow[$key]))
				{
					$arrRow[$key] = $row->$key;
				}
			}
		}

		$arrParams = array();

		// Set the language
		if ($strForceLang != '')
		{
			$arrParams['_locale'] = $strForceLang;
		}
		elseif (isset($arrRow['rootLanguage']))
		{
			$arrParams['_locale'] = $arrRow['rootLanguage'];
		}
		elseif (isset($arrRow['language']) && $arrRow['type'] == 'root')
		{
			$arrParams['_locale'] = $arrRow['language'];
		}
		elseif (TL_MODE == 'FE')
		{
			/** @var PageModel $objPage */
			global $objPage;

			$arrParams['_locale'] = $objPage->rootLanguage;
		}

		// Add the domain if it differs from the current one (see #3765 and #6927)
		if ($blnFixDomain)
		{
			$arrParams['_domain'] = $arrRow['domain'];
			$arrParams['_ssl'] = (bool) $arrRow['rootUseSSL'];
		}

		$objUrlGenerator = System::getContainer()->get('contao.routing.url_generator');
		$strUrl = $objUrlGenerator->generate(($arrRow['alias'] ?: $arrRow['id']) . $strParams, $arrParams);

		// Remove path from absolute URLs
		if (0 === strncmp($strUrl, '/', 1))
		{
			$strUrl = substr($strUrl, \strlen(Environment::get('path')) + 1);
		}

		// Decode sprintf placeholders
		if (strpos($strParams, '%') !== false)
		{
			$arrMatches = array();
			preg_match_all('/%([sducoxXbgGeEfF])/', $strParams, $arrMatches);

			foreach (array_unique($arrMatches[1]) as $v)
			{
				$strUrl = str_replace('%25' . $v, '%' . $v, $strUrl);
			}
		}

		// HOOK: add custom logic
		if (isset($GLOBALS['TL_HOOKS']['generateFrontendUrl']) && \is_array($GLOBALS['TL_HOOKS']['generateFrontendUrl']))
		{
			foreach ($GLOBALS['TL_HOOKS']['generateFrontendUrl'] as $callback)
			{
				$strUrl = static::importStatic($callback[0])->{$callback[1]}($arrRow, $strParams, $strUrl);
			}
		}

		return $strUrl;
	}

	/**
	 * Convert relative URLs in href and src attributes to absolute URLs
	 *
	 * @param string  $strContent  The text with the URLs to be converted
	 * @param string  $strBase     An optional base URL
	 * @param boolean $blnHrefOnly If true, only href attributes will be converted
	 *
	 * @return string The text with the replaced URLs
	 */
	public static function convertRelativeUrls($strContent, $strBase='', $blnHrefOnly=false)
	{
		if ($strBase == '')
		{
			$strBase = Environment::get('base');
		}

		$search = $blnHrefOnly ? 'href' : 'href|src';
		$arrUrls = preg_split('/(('.$search.')="([^"]+)")/i', $strContent, -1, PREG_SPLIT_DELIM_CAPTURE);
		$strContent = '';

		for ($i=0, $c=\count($arrUrls); $i<$c; $i+=4)
		{
			$strContent .= $arrUrls[$i];

			if (!isset($arrUrls[$i+2]))
			{
				continue;
			}

			$strAttribute = $arrUrls[$i+2];
			$strUrl = $arrUrls[$i+3];

			if (!preg_match('@^(?:[a-z0-9]+:|#)@i', $strUrl))
			{
				$strUrl = $strBase . (($strUrl != '/') ? $strUrl : '');
			}

			$strContent .= $strAttribute . '="' . $strUrl . '"';
		}

		return $strContent;
	}

	/**
	 * Send a file to the browser so the "save as …" dialogue opens
	 *
	 * @param string  $strFile The file path
	 * @param boolean $inline  Show the file in the browser instead of opening the download dialog
	 *
	 * @throws AccessDeniedException
	 */
	public static function sendFileToBrowser($strFile, $inline=false)
	{
		// Make sure there are no attempts to hack the file system
		if (preg_match('@^\.+@', $strFile) || preg_match('@\.+/@', $strFile) || preg_match('@(://)+@', $strFile))
		{
			throw new PageNotFoundException('Invalid file name');
		}

		// Limit downloads to the files directory
		if (!preg_match('@^' . preg_quote(Config::get('uploadPath'), '@') . '@i', $strFile))
		{
			throw new PageNotFoundException('Invalid path');
		}

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

		// Check whether the file exists
		if (!file_exists($rootDir . '/' . $strFile))
		{
			throw new PageNotFoundException('File not found');
		}

		$objFile = new File($strFile);
		$arrAllowedTypes = StringUtil::trimsplit(',', strtolower(Config::get('allowedDownload')));

		// Check whether the file type is allowed to be downloaded
		if (!\in_array($objFile->extension, $arrAllowedTypes))
		{
			throw new AccessDeniedException(sprintf('File type "%s" is not allowed', $objFile->extension));
		}

		// HOOK: post download callback
		if (isset($GLOBALS['TL_HOOKS']['postDownload']) && \is_array($GLOBALS['TL_HOOKS']['postDownload']))
		{
			foreach ($GLOBALS['TL_HOOKS']['postDownload'] as $callback)
			{
				static::importStatic($callback[0])->{$callback[1]}($strFile);
			}
		}

		// Send the file (will stop the script execution)
		$objFile->sendToBrowser('', $inline);
	}

	/**
	 * Load a set of DCA files
	 *
	 * @param string  $strTable   The table name
	 * @param boolean $blnNoCache If true, the cache will be bypassed
	 */
	public static function loadDataContainer($strTable, $blnNoCache=false)
	{
		$loader = new DcaLoader($strTable);
		$loader->load($blnNoCache);
	}

	/**
	 * Redirect to a front end page
	 *
	 * @param integer $intPage    The page ID
	 * @param string  $strArticle An optional article alias
	 * @param boolean $blnReturn  If true, return the URL and don't redirect
	 *
	 * @return string The URL of the target page
	 */
	protected function redirectToFrontendPage($intPage, $strArticle=null, $blnReturn=false)
	{
		if (($intPage = (int) $intPage) <= 0)
		{
			return '';
		}

		$objPage = PageModel::findWithDetails($intPage);

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

		$strParams = null;

		// Add the /article/ fragment (see #673)
		if ($strArticle !== null && ($objArticle = ArticleModel::findByAlias($strArticle)) !== null)
		{
			$strParams = '/articles/' . (($objArticle->inColumn != 'main') ? $objArticle->inColumn . ':' : '') . $strArticle;
		}

		$strUrl = $objPage->getFrontendUrl($strParams);

		// Make sure the URL is absolute (see #4332)
		if (strncmp($strUrl, 'http://', 7) !== 0 && strncmp($strUrl, 'https://', 8) !== 0)
		{
			$strUrl = Environment::get('base') . $strUrl;
		}

		if (!$blnReturn)
		{
			$this->redirect($strUrl);
		}

		return $strUrl;
	}

	/**
	 * Get the parent records of an entry and return them as string which can
	 * be used in a log message
	 *
	 * @param string  $strTable The table name
	 * @param integer $intId    The record ID
	 *
	 * @return string A string that can be used in a log message
	 */
	protected function getParentEntries($strTable, $intId)
	{
		// No parent table
		if (!isset($GLOBALS['TL_DCA'][$strTable]['config']['ptable']))
		{
			return '';
		}

		$arrParent = array();

		do
		{
			// Get the pid
			$objParent = $this->Database->prepare("SELECT pid FROM " . $strTable . " WHERE id=?")
										->limit(1)
										->execute($intId);

			if ($objParent->numRows < 1)
			{
				break;
			}

			// Store the parent table information
			$strTable = $GLOBALS['TL_DCA'][$strTable]['config']['ptable'];
			$intId = $objParent->pid;

			// Add the log entry
			$arrParent[] = $strTable .'.id=' . $intId;

			// Load the data container of the parent table
			$this->loadDataContainer($strTable);
		}
		while ($intId && isset($GLOBALS['TL_DCA'][$strTable]['config']['ptable']));

		if (empty($arrParent))
		{
			return '';
		}

		return ' (parent records: ' . implode(', ', $arrParent) . ')';
	}

	/**
	 * Take an array of file paths and eliminate the nested ones
	 *
	 * @param array $arrPaths The array of file paths
	 *
	 * @return array The file paths array without the nested paths
	 */
	protected function eliminateNestedPaths($arrPaths)
	{
		$arrPaths = array_filter($arrPaths);

		if (empty($arrPaths) || !\is_array($arrPaths))
		{
			return array();
		}

		$nested = array();

		foreach ($arrPaths as $path)
		{
			$nested[] = preg_grep('/^' . preg_quote($path, '/') . '\/.+/', $arrPaths);
		}

		if (!empty($nested))
		{
			$nested = array_merge(...$nested);
		}

		return array_values(array_diff($arrPaths, $nested));
	}

	/**
	 * Take an array of pages and eliminate the nested ones
	 *
	 * @param array   $arrPages   The array of page IDs
	 * @param string  $strTable   The table name
	 * @param boolean $blnSorting True if the table has a sorting field
	 *
	 * @return array The page IDs array without the nested IDs
	 */
	protected function eliminateNestedPages($arrPages, $strTable=null, $blnSorting=false)
	{
		if (empty($arrPages) || !\is_array($arrPages))
		{
			return array();
		}

		if (!$strTable)
		{
			$strTable = 'tl_page';
		}

		// Thanks to Andreas Schempp (see #2475 and #3423)
		$arrPages = array_intersect($arrPages, $this->Database->getChildRecords(0, $strTable, $blnSorting));
		$arrPages = array_values(array_diff($arrPages, $this->Database->getChildRecords($arrPages, $strTable, $blnSorting)));

		return $arrPages;
	}

	/**
	 * Add an image to a template
	 *
	 * @param object     $objTemplate   The template object to add the image to
	 * @param array      $arrItem       The element or module as array
	 * @param integer    $intMaxWidth   An optional maximum width of the image
	 * @param string     $strLightboxId An optional lightbox ID
	 * @param FilesModel $objModel      An optional files model
	 */
	public static function addImageToTemplate($objTemplate, $arrItem, $intMaxWidth=null, $strLightboxId=null, FilesModel $objModel=null)
	{
		try
		{
			$objFile = new File($arrItem['singleSRC']);
		}
		catch (\Exception $e)
		{
			$objFile = null;
		}

		$imgSize = $objFile ? $objFile->imageSize : false;
		$size = StringUtil::deserialize($arrItem['size']);

		if (is_numeric($size))
		{
			$size = array(0, 0, (int) $size);
		}
		elseif (!$size instanceof PictureConfigurationInterface)
		{
			if (!\is_array($size))
			{
				$size = array();
			}

			$size += array(0, 0, 'crop');
		}

		if ($intMaxWidth === null)
		{
			$intMaxWidth = Config::get('maxImageWidth');
		}

		$arrMargin = (TL_MODE == 'BE') ? array() : StringUtil::deserialize($arrItem['imagemargin']);

		// Store the original dimensions
		$objTemplate->width = $imgSize[0];
		$objTemplate->height = $imgSize[1];

		// Adjust the image size
		if ($intMaxWidth > 0)
		{
			@trigger_error('Using a maximum front end width has been deprecated and will no longer work in Contao 5.0. Remove the "maxImageWidth" configuration and use responsive images instead.', E_USER_DEPRECATED);

			// Subtract the margins before deciding whether to resize (see #6018)
			if (\is_array($arrMargin) && $arrMargin['unit'] == 'px')
			{
				$intMargin = (int) $arrMargin['left'] + (int) $arrMargin['right'];

				// Reset the margin if it exceeds the maximum width (see #7245)
				if ($intMaxWidth - $intMargin < 1)
				{
					$arrMargin['left'] = '';
					$arrMargin['right'] = '';
				}
				else
				{
					$intMaxWidth -= $intMargin;
				}
			}

			if (\is_array($size) && ($size[0] > $intMaxWidth || (!$size[0] && !$size[1] && (!$imgSize[0] || $imgSize[0] > $intMaxWidth))))
			{
				// See #2268 (thanks to Thyon)
				$ratio = ($size[0] && $size[1]) ? $size[1] / $size[0] : (($imgSize[0] && $imgSize[1]) ? $imgSize[1] / $imgSize[0] : 0);

				$size[0] = $intMaxWidth;
				$size[1] = floor($intMaxWidth * $ratio);
			}
		}

		try
		{
			$container = System::getContainer();
			$rootDir = $container->getParameter('kernel.project_dir');
			$staticUrl = $container->get('contao.assets.files_context')->getStaticUrl();
			$picture = $container->get('contao.image.picture_factory')->create($rootDir . '/' . $arrItem['singleSRC'], $size);

			$picture = array
			(
				'img' => $picture->getImg($rootDir, $staticUrl),
				'sources' => $picture->getSources($rootDir, $staticUrl)
			);

			$src = $picture['img']['src'];

			if ($src !== $arrItem['singleSRC'])
			{
				$objFile = new File(rawurldecode($src));
			}
		}
		catch (\Exception $e)
		{
			System::log('Image "' . $arrItem['singleSRC'] . '" could not be processed: ' . $e->getMessage(), __METHOD__, TL_ERROR);

			$src = '';
			$picture = array('img'=>array('src'=>'', 'srcset'=>''), 'sources'=>array());
		}

		// Image dimensions
		if ($objFile && $objFile->exists() && ($imgSize = $objFile->imageSize) !== false)
		{
			$objTemplate->arrSize = $imgSize;
			$objTemplate->imgSize = ' width="' . $imgSize[0] . '" height="' . $imgSize[1] . '"';
		}

		$arrMeta = array();

		// Load the meta data
		if ($objModel instanceof FilesModel)
		{
			if (TL_MODE == 'FE')
			{
				global $objPage;

				$arrMeta = Frontend::getMetaData($objModel->meta, $objPage->language);

				if (empty($arrMeta) && $objPage->rootFallbackLanguage !== null)
				{
					$arrMeta = Frontend::getMetaData($objModel->meta, $objPage->rootFallbackLanguage);
				}
			}
			else
			{
				$arrMeta = Frontend::getMetaData($objModel->meta, $GLOBALS['TL_LANGUAGE']);
			}

			self::loadDataContainer('tl_files');

			// Add any missing fields
			foreach (array_keys($GLOBALS['TL_DCA']['tl_files']['fields']['meta']['eval']['metaFields']) as $k)
			{
				if (!isset($arrMeta[$k]))
				{
					$arrMeta[$k] = '';
				}
			}

			$arrMeta['imageTitle'] = $arrMeta['title'];
			$arrMeta['imageUrl'] = $arrMeta['link'];
			unset($arrMeta['title'], $arrMeta['link']);

			// Add the meta data to the item
			if (!$arrItem['overwriteMeta'])
			{
				foreach ($arrMeta as $k=>$v)
				{
					switch ($k)
					{
						case 'alt':
						case 'imageTitle':
							$arrItem[$k] = StringUtil::specialchars($v);
							break;

						default:
							$arrItem[$k] = $v;
							break;
					}
				}
			}
		}

		$picture['alt'] = StringUtil::specialchars($arrItem['alt']);

		// Move the title to the link tag so it is shown in the lightbox
		if (($arrItem['fullsize'] || $arrItem['imageUrl']) && $arrItem['imageTitle'] && !$arrItem['linkTitle'])
		{
			$arrItem['linkTitle'] = $arrItem['imageTitle'];
			unset($arrItem['imageTitle']);
		}

		if (isset($arrItem['imageTitle']))
		{
			$picture['title'] = StringUtil::specialchars($arrItem['imageTitle']);
		}

		$objTemplate->picture = $picture;

		// Provide an ID for single lightbox images in HTML5 (see #3742)
		if ($strLightboxId === null && $arrItem['fullsize'])
		{
			$strLightboxId = substr(md5($objTemplate->getName() . '_' . $arrItem['id']), 0, 6);
		}

		// Float image
		if ($arrItem['floating'])
		{
			$objTemplate->floatClass = ' float_' . $arrItem['floating'];
		}

		// Do not override the "href" key (see #6468)
		$strHrefKey = ($objTemplate->href != '') ? 'imageHref' : 'href';

		// Image link
		if ($arrItem['imageUrl'] && TL_MODE == 'FE')
		{
			$objTemplate->$strHrefKey = $arrItem['imageUrl'];
			$objTemplate->attributes = '';

			if ($arrItem['fullsize'])
			{
				// Open images in the lightbox
				if (preg_match('/\.(jpe?g|gif|png)$/', $arrItem['imageUrl']))
				{
					// Do not add the TL_FILES_URL to external URLs (see #4923)
					if (strncmp($arrItem['imageUrl'], 'http://', 7) !== 0 && strncmp($arrItem['imageUrl'], 'https://', 8) !== 0)
					{
						$objTemplate->$strHrefKey = static::addFilesUrlTo(System::urlEncode($arrItem['imageUrl']));
					}

					$objTemplate->attributes = ' data-lightbox="' . $strLightboxId . '"';
				}
				else
				{
					$objTemplate->attributes = ' target="_blank"';
				}
			}
		}

		// Fullsize view
		elseif ($arrItem['fullsize'] && TL_MODE == 'FE')
		{
			$objTemplate->$strHrefKey = static::addFilesUrlTo(System::urlEncode($arrItem['singleSRC']));
			$objTemplate->attributes = ' data-lightbox="' . $strLightboxId . '"';
		}

		// Add the meta data to the template
		foreach (array_keys($arrMeta) as $k)
		{
			$objTemplate->$k = $arrItem[$k];
		}

		// Do not urlEncode() here because getImage() already does (see #3817)
		$objTemplate->src = static::addFilesUrlTo($src);
		$objTemplate->singleSRC = $arrItem['singleSRC'];
		$objTemplate->linkTitle = StringUtil::specialchars($arrItem['linkTitle'] ?: $arrItem['title']);
		$objTemplate->fullsize = $arrItem['fullsize'] ? true : false;
		$objTemplate->addBefore = ($arrItem['floating'] != 'below');
		$objTemplate->margin = static::generateMargin($arrMargin);
		$objTemplate->addImage = true;
	}

	/**
	 * Add enclosures to a template
	 *
	 * @param object $objTemplate The template object to add the enclosures to
	 * @param array  $arrItem     The element or module as array
	 * @param string $strKey      The name of the enclosures field in $arrItem
	 */
	public static function addEnclosuresToTemplate($objTemplate, $arrItem, $strKey='enclosure')
	{
		$arrEnclosures = StringUtil::deserialize($arrItem[$strKey]);

		if (empty($arrEnclosures) || !\is_array($arrEnclosures))
		{
			return;
		}

		$objFiles = FilesModel::findMultipleByUuids($arrEnclosures);

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

		$file = Input::get('file', true);

		// Send the file to the browser and do not send a 404 header (see #5178)
		if ($file != '')
		{
			while ($objFiles->next())
			{
				if ($file == $objFiles->path)
				{
					static::sendFileToBrowser($file);
				}
			}

			$objFiles->reset();
		}

		/** @var PageModel $objPage */
		global $objPage;

		$arrEnclosures = array();
		$allowedDownload = StringUtil::trimsplit(',', strtolower(Config::get('allowedDownload')));

		// Add download links
		while ($objFiles->next())
		{
			if ($objFiles->type == 'file')
			{
				$rootDir = System::getContainer()->getParameter('kernel.project_dir');

				if (!\in_array($objFiles->extension, $allowedDownload) || !is_file($rootDir . '/' . $objFiles->path))
				{
					continue;
				}

				$objFile = new File($objFiles->path);
				$strHref = Environment::get('request');

				// Remove an existing file parameter (see #5683)
				if (preg_match('/(&(amp;)?|\?)file=/', $strHref))
				{
					$strHref = preg_replace('/(&(amp;)?|\?)file=[^&]+/', '', $strHref);
				}

				$strHref .= ((strpos($strHref, '?') !== false) ? '&' : '?') . 'file=' . System::urlEncode($objFiles->path);

				$arrMeta = Frontend::getMetaData($objFiles->meta, $objPage->language);

				if (empty($arrMeta) && $objPage->rootFallbackLanguage !== null)
				{
					$arrMeta = Frontend::getMetaData($objFiles->meta, $objPage->rootFallbackLanguage);
				}

				// Use the file name as title if none is given
				if ($arrMeta['title'] == '')
				{
					$arrMeta['title'] = StringUtil::specialchars($objFile->basename);
				}

				$arrEnclosures[] = array
				(
					'id'        => $objFiles->id,
					'uuid'      => $objFiles->uuid,
					'name'      => $objFile->basename,
					'title'     => StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['download'], $objFile->basename)),
					'link'      => $arrMeta['title'],
					'caption'   => $arrMeta['caption'],
					'href'      => $strHref,
					'filesize'  => static::getReadableSize($objFile->filesize),
					'icon'      => Image::getPath($objFile->icon),
					'mime'      => $objFile->mime,
					'meta'      => $arrMeta,
					'extension' => $objFile->extension,
					'path'      => $objFile->dirname,
					'enclosure' => $objFiles->path // backwards compatibility
				);
			}
		}

		// Order the enclosures
		if (!empty($arrItem['orderEnclosure']))
		{
			$tmp = StringUtil::deserialize($arrItem['orderEnclosure']);

			if (!empty($tmp) && \is_array($tmp))
			{
				// Remove all values
				$arrOrder = array_map(function () {}, array_flip($tmp));

				// Move the matching elements to their position in $arrOrder
				foreach ($arrEnclosures as $k=>$v)
				{
					if (\array_key_exists($v['uuid'], $arrOrder))
					{
						$arrOrder[$v['uuid']] = $v;
						unset($arrEnclosures[$k]);
					}
				}

				// Append the left-over enclosures at the end
				if (!empty($arrEnclosures))
				{
					$arrOrder = array_merge($arrOrder, array_values($arrEnclosures));
				}

				// Remove empty (unreplaced) entries
				$arrEnclosures = array_values(array_filter($arrOrder));
				unset($arrOrder);
			}
		}

		$objTemplate->enclosure = $arrEnclosures;
	}

	/**
	 * Set the static URL constants
	 */
	public static function setStaticUrls()
	{
		if (\defined('TL_FILES_URL'))
		{
			return;
		}

		if (\func_num_args() > 0)
		{
			@trigger_error('Using Controller::setStaticUrls() has been deprecated and will no longer work in Contao 5.0. Use the asset contexts instead.', E_USER_DEPRECATED);

			if (!isset($GLOBALS['objPage']))
			{
				$GLOBALS['objPage'] = func_get_arg(0);
			}
		}

		\define('TL_ASSETS_URL', System::getContainer()->get('contao.assets.assets_context')->getStaticUrl());
		\define('TL_FILES_URL', System::getContainer()->get('contao.assets.files_context')->getStaticUrl());

		// Deprecated since Contao 4.0, to be removed in Contao 5.0
		\define('TL_SCRIPT_URL', TL_ASSETS_URL);
		\define('TL_PLUGINS_URL', TL_ASSETS_URL);
	}

	/**
	 * Add a static URL to a script
	 *
	 * @param string             $script  The script path
	 * @param ContaoContext|null $context
	 *
	 * @return string The script path with the static URL
	 */
	public static function addStaticUrlTo($script, ContaoContext $context = null)
	{
		// Absolute URLs
		if (preg_match('@^https?://@', $script))
		{
			return $script;
		}

		if ($context === null)
		{
			$context = System::getContainer()->get('contao.assets.assets_context');
		}

		if ($strStaticUrl = $context->getStaticUrl())
		{
			return $strStaticUrl . $script;
		}

		return $script;
	}

	/**
	 * Add the assets URL to a script
	 *
	 * @param string $script The script path
	 *
	 * @return string The script path with the assets URL
	 */
	public static function addAssetsUrlTo($script)
	{
		return static::addStaticUrlTo($script, System::getContainer()->get('contao.assets.assets_context'));
	}

	/**
	 * Add the files URL to a script
	 *
	 * @param string $script The script path
	 *
	 * @return string The script path with the files URL
	 */
	public static function addFilesUrlTo($script)
	{
		return static::addStaticUrlTo($script, System::getContainer()->get('contao.assets.files_context'));
	}

	/**
	 * Return the current theme as string
	 *
	 * @return string The name of the theme
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Backend::getTheme() instead.
	 */
	public static function getTheme()
	{
		@trigger_error('Using Controller::getTheme() has been deprecated and will no longer work in Contao 5.0. Use Backend::getTheme() instead.', E_USER_DEPRECATED);

		return Backend::getTheme();
	}

	/**
	 * Return the back end themes as array
	 *
	 * @return array An array of available back end themes
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Backend::getThemes() instead.
	 */
	public static function getBackendThemes()
	{
		@trigger_error('Using Controller::getBackendThemes() has been deprecated and will no longer work in Contao 5.0. Use Backend::getThemes() instead.', E_USER_DEPRECATED);

		return Backend::getThemes();
	}

	/**
	 * Get the details of a page including inherited parameters
	 *
	 * @param mixed $intId A page ID or a Model object
	 *
	 * @return PageModel The page model or null
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use PageModel::findWithDetails() or PageModel->loadDetails() instead.
	 */
	public static function getPageDetails($intId)
	{
		@trigger_error('Using Controller::getPageDetails() has been deprecated and will no longer work in Contao 5.0. Use PageModel::findWithDetails() or PageModel->loadDetails() instead.', E_USER_DEPRECATED);

		if ($intId instanceof PageModel)
		{
			return $intId->loadDetails();
		}
		elseif ($intId instanceof Collection)
		{
			/** @var PageModel $objPage */
			$objPage = $intId->current();

			return $objPage->loadDetails();
		}
		elseif (\is_object($intId))
		{
			$strKey = __METHOD__ . '-' . $intId->id;

			// Try to load from cache
			if (Cache::has($strKey))
			{
				return Cache::get($strKey);
			}

			// Create a model from the database result
			$objPage = new PageModel();
			$objPage->setRow($intId->row());
			$objPage->loadDetails();

			Cache::set($strKey, $objPage);

			return $objPage;
		}
		else
		{
			// Invalid ID
			if (!\strlen($intId) || $intId < 1)
			{
				return null;
			}

			$strKey = __METHOD__ . '-' . $intId;

			// Try to load from cache
			if (Cache::has($strKey))
			{
				return Cache::get($strKey);
			}

			$objPage = PageModel::findWithDetails($intId);

			Cache::set($strKey, $objPage);

			return $objPage;
		}
	}

	/**
	 * Remove old XML files from the share directory
	 *
	 * @param boolean $blnReturn If true, only return the finds and don't delete
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Automator::purgeXmlFiles() instead.
	 */
	protected function removeOldFeeds($blnReturn=false)
	{
		@trigger_error('Using Controller::removeOldFeeds() has been deprecated and will no longer work in Contao 5.0. Use Automator::purgeXmlFiles() instead.', E_USER_DEPRECATED);

		$this->import(Automator::class, 'Automator');
		$this->Automator->purgeXmlFiles($blnReturn);
	}

	/**
	 * Return true if a class exists (tries to autoload the class)
	 *
	 * @param string $strClass The class name
	 *
	 * @return boolean True if the class exists
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use the PHP function class_exists() instead.
	 */
	protected function classFileExists($strClass)
	{
		@trigger_error('Using Controller::classFileExists() has been deprecated and will no longer work in Contao 5.0. Use the PHP function class_exists() instead.', E_USER_DEPRECATED);

		return class_exists($strClass);
	}

	/**
	 * Restore basic entities
	 *
	 * @param string $strBuffer The string with the tags to be replaced
	 *
	 * @return string The string with the original entities
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use StringUtil::restoreBasicEntities() instead.
	 */
	public static function restoreBasicEntities($strBuffer)
	{
		@trigger_error('Using Controller::restoreBasicEntities() has been deprecated and will no longer work in Contao 5.0. Use StringUtil::restoreBasicEntities() instead.', E_USER_DEPRECATED);

		return StringUtil::restoreBasicEntities($strBuffer);
	}

	/**
	 * Resize an image and crop it if necessary
	 *
	 * @param string  $image  The image path
	 * @param integer $width  The target width
	 * @param integer $height The target height
	 * @param string  $mode   An optional resize mode
	 *
	 * @return boolean True if the image has been resized correctly
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Image::resize() instead.
	 */
	protected function resizeImage($image, $width, $height, $mode='')
	{
		@trigger_error('Using Controller::resizeImage() has been deprecated and will no longer work in Contao 5.0. Use Image::resize() instead.', E_USER_DEPRECATED);

		return Image::resize($image, $width, $height, $mode);
	}

	/**
	 * Resize an image and crop it if necessary
	 *
	 * @param string  $image  The image path
	 * @param integer $width  The target width
	 * @param integer $height The target height
	 * @param string  $mode   An optional resize mode
	 * @param string  $target An optional target to be replaced
	 * @param boolean $force  Override existing target images
	 *
	 * @return string|null The image path or null
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Image::get() instead.
	 */
	protected function getImage($image, $width, $height, $mode='', $target=null, $force=false)
	{
		@trigger_error('Using Controller::getImage() has been deprecated and will no longer work in Contao 5.0. Use Image::get() instead.', E_USER_DEPRECATED);

		return Image::get($image, $width, $height, $mode, $target, $force);
	}

	/**
	 * 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
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Image::getHtml() instead.
	 */
	public static function generateImage($src, $alt='', $attributes='')
	{
		@trigger_error('Using Controller::generateImage() has been deprecated and will no longer work in Contao 5.0. Use Image::getHtml() instead.', E_USER_DEPRECATED);

		return Image::getHtml($src, $alt, $attributes);
	}

	/**
	 * Return the date picker string (see #3218)
	 *
	 * @return boolean
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Specify "datepicker"=>true in your DCA file instead.
	 */
	protected function getDatePickerString()
	{
		@trigger_error('Using Controller::getDatePickerString() has been deprecated and will no longer work in Contao 5.0. Specify "datepicker"=>true in your DCA file instead.', E_USER_DEPRECATED);

		return true;
	}

	/**
	 * Return the installed back end languages as array
	 *
	 * @return array An array of available back end languages
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use System::getLanguages(true) instead.
	 */
	protected function getBackendLanguages()
	{
		@trigger_error('Using Controller::getBackendLanguages() has been deprecated and will no longer work in Contao 5.0. Use System::getLanguages(true) instead.', E_USER_DEPRECATED);

		return $this->getLanguages(true);
	}

	/**
	 * Parse simple tokens that can be used to personalize newsletters
	 *
	 * @param string $strBuffer The text with the tokens to be replaced
	 * @param array  $arrData   The replacement data as array
	 *
	 * @return string The text with the replaced tokens
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use StringUtil::parseSimpleTokens() instead.
	 */
	protected function parseSimpleTokens($strBuffer, $arrData)
	{
		@trigger_error('Using Controller::parseSimpleTokens() has been deprecated and will no longer work in Contao 5.0. Use StringUtil::parseSimpleTokens() instead.', E_USER_DEPRECATED);

		return StringUtil::parseSimpleTokens($strBuffer, $arrData);
	}

	/**
	 * Convert a DCA file configuration to be used with widgets
	 *
	 * @param array  $arrData  The field configuration array
	 * @param string $strName  The field name in the form
	 * @param mixed  $varValue The field value
	 * @param string $strField The field name in the database
	 * @param string $strTable The table name
	 *
	 * @return array An array that can be passed to a widget
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Widget::getAttributesFromDca() instead.
	 */
	protected function prepareForWidget($arrData, $strName, $varValue=null, $strField='', $strTable='')
	{
		@trigger_error('Using Controller::prepareForWidget() has been deprecated and will no longer work in Contao 5.0. Use Widget::getAttributesFromDca() instead.', E_USER_DEPRECATED);

		return Widget::getAttributesFromDca($arrData, $strName, $varValue, $strField, $strTable);
	}

	/**
	 * Return the IDs of all child records of a particular record (see #2475)
	 *
	 * @author Andreas Schempp
	 *
	 * @param mixed   $arrParentIds An array of parent IDs
	 * @param string  $strTable     The table name
	 * @param boolean $blnSorting   True if the table has a sorting field
	 * @param array   $arrReturn    The array to be returned
	 * @param string  $strWhere     Additional WHERE condition
	 *
	 * @return array An array of child record IDs
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Database::getChildRecords() instead.
	 */
	protected function getChildRecords($arrParentIds, $strTable, $blnSorting=false, $arrReturn=array(), $strWhere='')
	{
		@trigger_error('Using Controller::getChildRecords() has been deprecated and will no longer work in Contao 5.0. Use Database::getChildRecords() instead.', E_USER_DEPRECATED);

		return $this->Database->getChildRecords($arrParentIds, $strTable, $blnSorting, $arrReturn, $strWhere);
	}

	/**
	 * Return the IDs of all parent records of a particular record
	 *
	 * @param integer $intId    The ID of the record
	 * @param string  $strTable The table name
	 *
	 * @return array An array of parent record IDs
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Database::getParentRecords() instead.
	 */
	protected function getParentRecords($intId, $strTable)
	{
		@trigger_error('Using Controller::getParentRecords() has been deprecated and will no longer work in Contao 5.0. Use Database::getParentRecords() instead.', E_USER_DEPRECATED);

		return $this->Database->getParentRecords($intId, $strTable);
	}

	/**
	 * Print an article as PDF and stream it to the browser
	 *
	 * @param ModuleModel $objArticle An article object
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use ModuleArticle->generatePdf() instead.
	 */
	protected function printArticleAsPdf($objArticle)
	{
		@trigger_error('Using Controller::printArticleAsPdf() has been deprecated and will no longer work in Contao 5.0. Use ModuleArticle->generatePdf() instead.', E_USER_DEPRECATED);

		$objArticle = new ModuleArticle($objArticle);
		$objArticle->generatePdf();
	}

	/**
	 * Return all page sections as array
	 *
	 * @return array An array of active page sections
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             See https://github.com/contao/core/issues/4693.
	 */
	public static function getPageSections()
	{
		@trigger_error('Using Controller::getPageSections() has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

		return array('header', 'left', 'right', 'main', 'footer');
	}

	/**
	 * Return a "selected" attribute if the option is selected
	 *
	 * @param string $strOption The option to check
	 * @param mixed  $varValues One or more values to check against
	 *
	 * @return string The attribute or an empty string
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Widget::optionSelected() instead.
	 */
	public static function optionSelected($strOption, $varValues)
	{
		@trigger_error('Using Controller::optionSelected() has been deprecated and will no longer work in Contao 5.0. Use Widget::optionSelected() instead.', E_USER_DEPRECATED);

		return Widget::optionSelected($strOption, $varValues);
	}

	/**
	 * Return a "checked" attribute if the option is checked
	 *
	 * @param string $strOption The option to check
	 * @param mixed  $varValues One or more values to check against
	 *
	 * @return string The attribute or an empty string
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Widget::optionChecked() instead.
	 */
	public static function optionChecked($strOption, $varValues)
	{
		@trigger_error('Using Controller::optionChecked() has been deprecated and will no longer work in Contao 5.0. Use Widget::optionChecked() instead.', E_USER_DEPRECATED);

		return Widget::optionChecked($strOption, $varValues);
	}

	/**
	 * Find a content element in the TL_CTE array and return the class name
	 *
	 * @param string $strName The content element name
	 *
	 * @return string The class name
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use ContentElement::findClass() instead.
	 */
	public static function findContentElement($strName)
	{
		@trigger_error('Using Controller::findContentElement() has been deprecated and will no longer work in Contao 5.0. Use ContentElement::findClass() instead.', E_USER_DEPRECATED);

		return ContentElement::findClass($strName);
	}

	/**
	 * Find a front end module in the FE_MOD array and return the class name
	 *
	 * @param string $strName The front end module name
	 *
	 * @return string The class name
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Module::findClass() instead.
	 */
	public static function findFrontendModule($strName)
	{
		@trigger_error('Using Controller::findFrontendModule() has been deprecated and will no longer work in Contao 5.0. Use Module::findClass() instead.', E_USER_DEPRECATED);

		return Module::findClass($strName);
	}

	/**
	 * Create an initial version of a record
	 *
	 * @param string  $strTable The table name
	 * @param integer $intId    The ID of the element to be versioned
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Versions->initialize() instead.
	 */
	protected function createInitialVersion($strTable, $intId)
	{
		@trigger_error('Using Controller::createInitialVersion() has been deprecated and will no longer work in Contao 5.0. Use Versions->initialize() instead.', E_USER_DEPRECATED);

		$objVersions = new Versions($strTable, $intId);
		$objVersions->initialize();
	}

	/**
	 * Create a new version of a record
	 *
	 * @param string  $strTable The table name
	 * @param integer $intId    The ID of the element to be versioned
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Versions->create() instead.
	 */
	protected function createNewVersion($strTable, $intId)
	{
		@trigger_error('Using Controller::createNewVersion() has been deprecated and will no longer work in Contao 5.0. Use Versions->create() instead.', E_USER_DEPRECATED);

		$objVersions = new Versions($strTable, $intId);
		$objVersions->create();
	}

	/**
	 * Return the files matching a GLOB pattern
	 *
	 * @param string $pattern
	 *
	 * @return array
	 */
	protected static function braceGlob($pattern)
	{
		// Use glob() if possible
		if (false === strpos($pattern, '/**/') && (\defined('GLOB_BRACE') || false === strpos($pattern, '{')))
		{
			return glob($pattern, \defined('GLOB_BRACE') ? GLOB_BRACE : 0);
		}

		$finder = new Finder();
		$regex = Glob::toRegex($pattern);

		// All files in the given template folder
		$filesIterator = $finder
			->files()
			->followLinks()
			->sortByName()
			->in(\dirname($pattern))
		;

		// Match the actual regex and filter the files
		$filesIterator = $filesIterator->filter(function (\SplFileInfo $info) use ($regex)
		{
			$path = $info->getPathname();

			return preg_match($regex, $path) && $info->isFile();
		});

		$files = iterator_to_array($filesIterator);

		return array_keys($files);
	}
}

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


<?php

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

namespace Contao;

use Contao\Database\Result;
use Contao\Database\Statement;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;

/**
 * Handle the database communication
 *
 * The class is responsible for connecting to the database, listing tables and
 * fields, handling transactions and locking tables. It also creates the related
 * Statement and Result objects.
 *
 * Usage:
 *
 *     $db   = Database::getInstance();
 *     $stmt = $db->prepare("SELECT * FROM tl_user WHERE id=?");
 *     $res  = $stmt->execute(4);
 *
 * @property string $error The last error message
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class Database
{

	/**
	 * Object instances (Singleton)
	 * @var array
	 */
	protected static $arrInstances = array();

	/**
	 * Connection ID
	 * @var Connection
	 */
	protected $resConnection;

	/**
	 * Disable autocommit
	 * @var boolean
	 */
	protected $blnDisableAutocommit = false;

	/**
	 * Cache
	 * @var array
	 */
	protected $arrCache = array();

	/**
	 * Establish the database connection
	 *
	 * @param array $arrConfig The configuration array
	 *
	 * @throws \Exception If a connection cannot be established
	 */
	protected function __construct(array $arrConfig)
	{
		// Deprecated since Contao 4.0, to be removed in Contao 5.0
		if (!empty($arrConfig))
		{
			@trigger_error('Passing a custom configuration to Database::__construct() has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

			$arrParams = array
			(
				'driver'    => 'pdo_mysql',
				'host'      => $arrConfig['dbHost'],
				'port'      => $arrConfig['dbPort'],
				'user'      => $arrConfig['dbUser'],
				'password'  => $arrConfig['dbPass'],
				'dbname'    => $arrConfig['dbDatabase'],
				'charset'   => $arrConfig['dbCharset']
			);

			$this->resConnection = DriverManager::getConnection($arrParams);
		}
		else
		{
			$this->resConnection = System::getContainer()->get('database_connection');
		}

		if (!\is_object($this->resConnection))
		{
			throw new \Exception(sprintf('Could not connect to database (%s)', $this->error));
		}
	}

	/**
	 * Close the database connection
	 */
	public function __destruct()
	{
		unset($this->resConnection);
	}

	/**
	 * Prevent cloning of the object (Singleton)
	 */
	final public function __clone() {}

	/**
	 * Return an object property
	 *
	 * @param string $strKey The property name
	 *
	 * @return string|null The property value
	 */
	public function __get($strKey)
	{
		if ($strKey == 'error')
		{
			$info = $this->resConnection->errorInfo();

			return 'SQLSTATE ' . $info[0] . ': error ' . $info[1] . ': ' . $info[2];
		}

		return null;
	}

	/**
	 * Instantiate the Database object (Factory)
	 *
	 * @param array $arrCustomConfig A configuration array
	 *
	 * @return Database The Database object
	 */
	public static function getInstance(array $arrCustomConfig=null)
	{
		$arrConfig = array();

		if (\is_array($arrCustomConfig))
		{
			$arrDefaultConfig = array
			(
				'dbHost'     => Config::get('dbHost'),
				'dbPort'     => Config::get('dbPort'),
				'dbUser'     => Config::get('dbUser'),
				'dbPass'     => Config::get('dbPass'),
				'dbDatabase' => Config::get('dbDatabase')
			);

			$arrConfig = array_merge($arrDefaultConfig, $arrCustomConfig);
		}

		// Sort the array before generating the key
		ksort($arrConfig);
		$strKey = md5(implode('', $arrConfig));

		if (!isset(static::$arrInstances[$strKey]))
		{
			static::$arrInstances[$strKey] = new static($arrConfig);
		}

		return static::$arrInstances[$strKey];
	}

	/**
	 * Prepare a query and return a Statement object
	 *
	 * @param string $strQuery The query string
	 *
	 * @return Statement The Statement object
	 */
	public function prepare($strQuery)
	{
		$objStatement = new Statement($this->resConnection, $this->blnDisableAutocommit);

		return $objStatement->prepare($strQuery);
	}

	/**
	 * Execute a query and return a Result object
	 *
	 * @param string $strQuery The query string
	 *
	 * @return Result The Result object
	 */
	public function execute($strQuery)
	{
		return $this->prepare($strQuery)->execute();
	}

	/**
	 * Execute a raw query and return a Result object
	 *
	 * @param string $strQuery The query string
	 *
	 * @return Result The Result object
	 */
	public function query($strQuery)
	{
		$objStatement = new Statement($this->resConnection, $this->blnDisableAutocommit);

		return $objStatement->query($strQuery);
	}

	/**
	 * Auto-generate a FIND_IN_SET() statement
	 *
	 * @param string  $strKey     The field name
	 * @param mixed   $varSet     The set to find the key in
	 * @param boolean $blnIsField If true, the set will not be quoted
	 *
	 * @return string The FIND_IN_SET() statement
	 */
	public function findInSet($strKey, $varSet, $blnIsField=false)
	{
		if (\is_array($varSet))
		{
			$varSet = implode(',', $varSet);
		}

		if ($blnIsField)
		{
			$varSet = static::quoteIdentifier($varSet);
		}
		else
		{
			$varSet = $this->resConnection->quote($varSet);
		}

		return "FIND_IN_SET(" . static::quoteIdentifier($strKey) . ", " . $varSet . ")";
	}

	/**
	 * Return all tables as array
	 *
	 * @param string  $strDatabase The database name
	 * @param boolean $blnNoCache  If true, the cache will be bypassed
	 *
	 * @return array An array of table names
	 */
	public function listTables($strDatabase=null, $blnNoCache=false)
	{
		if ($blnNoCache || !isset($this->arrCache[$strDatabase]))
		{
			$strOldDatabase = $this->resConnection->getDatabase();

			// Change the database
			if ($strDatabase !== null && $strDatabase != $strOldDatabase)
			{
				$this->setDatabase($strDatabase);
			}

			$this->arrCache[$strDatabase] = $this->resConnection->getSchemaManager()->listTableNames();

			// Restore the database
			if ($strDatabase !== null && $strDatabase != $strOldDatabase)
			{
				$this->setDatabase($strOldDatabase);
			}
		}

		return $this->arrCache[$strDatabase];
	}

	/**
	 * Determine if a particular database table exists
	 *
	 * @param string  $strTable    The table name
	 * @param string  $strDatabase The optional database name
	 * @param boolean $blnNoCache  If true, the cache will be bypassed
	 *
	 * @return boolean True if the table exists
	 */
	public function tableExists($strTable, $strDatabase=null, $blnNoCache=false)
	{
		if ($strTable == '')
		{
			return false;
		}

		return \in_array($strTable, $this->listTables($strDatabase, $blnNoCache));
	}

	/**
	 * Return all columns of a particular table as array
	 *
	 * @param string  $strTable   The table name
	 * @param boolean $blnNoCache If true, the cache will be bypassed
	 *
	 * @return array An array of column names
	 */
	public function listFields($strTable, $blnNoCache=false)
	{
		if ($blnNoCache || !isset($this->arrCache[$strTable]))
		{
			$arrReturn = array();
			$objFields = $this->query("SHOW FULL COLUMNS FROM $strTable");

			while ($objFields->next())
			{
				$arrTmp = array();
				$arrChunks = preg_split('/(\([^\)]+\))/', $objFields->Type, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);

				$arrTmp['name'] = $objFields->Field;
				$arrTmp['type'] = $arrChunks[0];

				if (!empty($arrChunks[1]))
				{
					$arrChunks[1] = str_replace(array('(', ')'), '', $arrChunks[1]);

					// Handle enum fields (see #6387)
					if ($arrChunks[0] == 'enum')
					{
						$arrTmp['length'] = $arrChunks[1];
					}
					else
					{
						$arrSubChunks = explode(',', $arrChunks[1]);
						$arrTmp['length'] = trim($arrSubChunks[0]);

						if (!empty($arrSubChunks[1]))
						{
							$arrTmp['precision'] = trim($arrSubChunks[1]);
						}
					}
				}

				if (!empty($arrChunks[2]))
				{
					$arrTmp['attributes'] = trim($arrChunks[2]);
				}

				if ($objFields->Key != '')
				{
					switch ($objFields->Key)
					{
						case 'PRI':
							$arrTmp['index'] = 'PRIMARY';
							break;

						case 'UNI':
							$arrTmp['index'] = 'UNIQUE';
							break;

						case 'MUL':
							// Ignore
							break;

						default:
							$arrTmp['index'] = 'KEY';
							break;
					}
				}

				// Do not modify the order!
				$arrTmp['collation'] = $objFields->Collation;
				$arrTmp['null'] = ($objFields->Null == 'YES') ? 'NULL' : 'NOT NULL';
				$arrTmp['default'] = $objFields->Default;
				$arrTmp['extra'] = $objFields->Extra;
				$arrTmp['origtype'] = $objFields->Type;

				$arrReturn[] = $arrTmp;
			}

			$objIndex = $this->query("SHOW INDEXES FROM `$strTable`");

			while ($objIndex->next())
			{
				$strColumnName = $objIndex->Column_name;

				if ($objIndex->Sub_part != '')
				{
					$strColumnName .= '(' . $objIndex->Sub_part . ')';
				}

				$arrReturn[$objIndex->Key_name]['name'] = $objIndex->Key_name;
				$arrReturn[$objIndex->Key_name]['type'] = 'index';
				$arrReturn[$objIndex->Key_name]['index_fields'][] = $strColumnName;
				$arrReturn[$objIndex->Key_name]['index'] = (($objIndex->Non_unique == 0) ? 'UNIQUE' : 'KEY');
			}

			$this->arrCache[$strTable] = $arrReturn;
		}

		return $this->arrCache[$strTable];
	}

	/**
	 * Determine if a particular column exists
	 *
	 * @param string  $strField   The field name
	 * @param string  $strTable   The table name
	 * @param boolean $blnNoCache If true, the cache will be bypassed
	 *
	 * @return boolean True if the field exists
	 */
	public function fieldExists($strField, $strTable, $blnNoCache=false)
	{
		if ($strField == '' || $strTable == '')
		{
			return false;
		}

		foreach ($this->listFields($strTable, $blnNoCache) as $arrField)
		{
			if ($arrField['name'] == $strField && $arrField['type'] != 'index')
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Determine if a particular index exists
	 *
	 * @param string  $strName    The index name
	 * @param string  $strTable   The table name
	 * @param boolean $blnNoCache If true, the cache will be bypassed
	 *
	 * @return boolean True if the index exists
	 */
	public function indexExists($strName, $strTable, $blnNoCache=false)
	{
		if ($strName == '' || $strTable == '')
		{
			return false;
		}

		foreach ($this->listFields($strTable, $blnNoCache) as $arrField)
		{
			if ($arrField['name'] == $strName && $arrField['type'] == 'index')
			{
				return true;
			}
		}

		return false;
	}

	/**
	 * Return the field names of a particular table as array
	 *
	 * @param string  $strTable   The table name
	 * @param boolean $blnNoCache If true, the cache will be bypassed
	 *
	 * @return array An array of field names
	 */
	public function getFieldNames($strTable, $blnNoCache=false)
	{
		$arrNames = array();
		$arrFields = $this->listFields($strTable, $blnNoCache);

		foreach ($arrFields as $arrField)
		{
			if ($arrField['type'] != 'index')
			{
				$arrNames[] = $arrField['name'];
			}
		}

		return $arrNames;
	}

	/**
	 * Check whether a field value in the database is unique
	 *
	 * @param string  $strTable The table name
	 * @param string  $strField The field name
	 * @param mixed   $varValue The field value
	 * @param integer $intId    The ID of a record to exempt
	 *
	 * @return boolean True if the field value is unique
	 */
	public function isUniqueValue($strTable, $strField, $varValue, $intId=null)
	{
		$strQuery = "SELECT * FROM $strTable WHERE " . static::quoteIdentifier($strField) . "=?";

		if ($intId !== null)
		{
			$strQuery .= " AND id!=?";
		}

		$objUnique = $this->prepare($strQuery)
						  ->limit(1)
						  ->execute($varValue, $intId);

		return $objUnique->numRows ? false : true;
	}

	/**
	 * Return the IDs of all child records of a particular record (see #2475)
	 *
	 * @author Andreas Schempp
	 *
	 * @param mixed   $arrParentIds An array of parent IDs
	 * @param string  $strTable     The table name
	 * @param boolean $blnSorting   True if the table has a sorting field
	 * @param array   $arrReturn    The array to be returned
	 * @param string  $strWhere     Additional WHERE condition
	 *
	 * @return array An array of child record IDs
	 */
	public function getChildRecords($arrParentIds, $strTable, $blnSorting=false, $arrReturn=array(), $strWhere='')
	{
		if (!\is_array($arrParentIds))
		{
			$arrParentIds = array($arrParentIds);
		}

		if (empty($arrParentIds))
		{
			return $arrReturn;
		}

		$arrParentIds = array_map('\intval', $arrParentIds);
		$objChilds = $this->query("SELECT id, pid FROM " . $strTable . " WHERE pid IN(" . implode(',', $arrParentIds) . ")" . ($strWhere ? " AND $strWhere" : "") . ($blnSorting ? " ORDER BY " . $this->findInSet('pid', $arrParentIds) . ", sorting" : ""));

		if ($objChilds->numRows > 0)
		{
			if ($blnSorting)
			{
				$arrChilds = array();
				$arrOrdered = array();

				while ($objChilds->next())
				{
					$arrChilds[] = $objChilds->id;
					$arrOrdered[$objChilds->pid][] = $objChilds->id;
				}

				foreach (array_reverse(array_keys($arrOrdered)) as $pid)
				{
					$pos = (int) array_search($pid, $arrReturn);
					array_insert($arrReturn, $pos+1, $arrOrdered[$pid]);
				}

				$arrReturn = $this->getChildRecords($arrChilds, $strTable, $blnSorting, $arrReturn, $strWhere);
			}
			else
			{
				$arrChilds = $objChilds->fetchEach('id');
				$arrReturn = array_merge($arrChilds, $this->getChildRecords($arrChilds, $strTable, $blnSorting, $arrReturn, $strWhere));
			}
		}

		return $arrReturn;
	}

	/**
	 * Return the IDs of all parent records of a particular record
	 *
	 * @param integer $intId    The ID of the record
	 * @param string  $strTable The table name
	 *
	 * @return array An array of parent record IDs
	 */
	public function getParentRecords($intId, $strTable)
	{
		$arrReturn = array();

		// Currently supports a nesting-level of 10
		$objPages = $this->prepare("SELECT id, @pid:=pid FROM $strTable WHERE id=?" . str_repeat(" UNION SELECT id, @pid:=pid FROM $strTable WHERE id=@pid", 9))
						 ->execute($intId);

		while ($objPages->next())
		{
			$arrReturn[] = $objPages->id;
		}

		return $arrReturn;
	}

	/**
	 * Change the current database
	 *
	 * @param string $strDatabase The name of the target database
	 */
	public function setDatabase($strDatabase)
	{
		$this->resConnection->exec("USE $strDatabase");
	}

	/**
	 * Begin a transaction
	 */
	public function beginTransaction()
	{
		$this->resConnection->beginTransaction();
	}

	/**
	 * Commit a transaction
	 */
	public function commitTransaction()
	{
		$this->resConnection->commit();
	}

	/**
	 * Rollback a transaction
	 */
	public function rollbackTransaction()
	{
		$this->resConnection->rollBack();
	}

	/**
	 * Lock one or more tables
	 *
	 * @param array $arrTables An array of table names to be locked
	 */
	public function lockTables($arrTables)
	{
		$arrLocks = array();

		foreach ($arrTables as $table=>$mode)
		{
			$arrLocks[] = $this->resConnection->quoteIdentifier($table) . ' ' . $mode;
		}

		$this->resConnection->exec('LOCK TABLES ' . implode(', ', $arrLocks) . ';');
	}

	/**
	 * Unlock all tables
	 */
	public function unlockTables()
	{
		$this->resConnection->exec('UNLOCK TABLES;');
	}

	/**
	 * Return the table size in bytes
	 *
	 * @param string $strTable The table name
	 *
	 * @return integer The table size in bytes
	 */
	public function getSizeOf($strTable)
	{
		$statement = $this->resConnection->executeQuery('SHOW TABLE STATUS LIKE ' . $this->resConnection->quote($strTable));
		$status = $statement->fetch(\PDO::FETCH_ASSOC);

		return $status['Data_length'] + $status['Index_length'];
	}

	/**
	 * Return the next autoincrement ID of a table
	 *
	 * @param string $strTable The table name
	 *
	 * @return integer The autoincrement ID
	 */
	public function getNextId($strTable)
	{
		$statement = $this->resConnection->executeQuery('SHOW TABLE STATUS LIKE ' . $this->resConnection->quote($strTable));
		$status = $statement->fetch(\PDO::FETCH_ASSOC);

		return $status['Auto_increment'];
	}

	/**
	 * Return a universal unique identifier
	 *
	 * @return string The UUID string
	 */
	public function getUuid()
	{
		static $ids;

		if (empty($ids))
		{
			$statement = $this->resConnection->executeQuery(implode(' UNION ALL ', array_fill(0, 10, "SELECT UNHEX(REPLACE(UUID(), '-', '')) AS uuid")));
			$ids = $statement->fetchAll(\PDO::FETCH_COLUMN);
		}

		return array_pop($ids);
	}

	/**
	 * Quote the column name if it is a reserved word
	 *
	 * @param string $strName
	 *
	 * @return string
	 */
	public static function quoteIdentifier($strName)
	{
		static $strQuoteCharacter = null;

		if ($strQuoteCharacter === null)
		{
			$strQuoteCharacter = System::getContainer()->get('database_connection')->getDatabasePlatform()->getIdentifierQuoteCharacter();
		}

		// The identifier is quoted already
		if (strncmp($strName, $strQuoteCharacter, 1) === 0)
		{
			return $strName;
		}

		// Not an identifier (AbstractPlatform::quoteIdentifier() handles table.column so also allow . here)
		if (!preg_match('/^[A-Za-z0-9_$.]+$/', $strName))
		{
			return $strName;
		}

		return System::getContainer()->get('database_connection')->quoteIdentifier($strName);
	}

	/**
	 * Execute a query and do not cache the result
	 *
	 * @param string $strQuery The query string
	 *
	 * @return Result The Result object
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Database::execute() instead.
	 */
	public function executeUncached($strQuery)
	{
		@trigger_error('Using Database::executeUncached() has been deprecated and will no longer work in Contao 5.0. Use Database::execute() instead.', E_USER_DEPRECATED);

		return $this->execute($strQuery);
	}

	/**
	 * Always execute the query and add or replace an existing cache entry
	 *
	 * @param string $strQuery The query string
	 *
	 * @return Result The Result object
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Database::execute() instead.
	 */
	public function executeCached($strQuery)
	{
		@trigger_error('Using Database::executeCached() has been deprecated and will no longer work in Contao 5.0. Use Database::execute() instead.', E_USER_DEPRECATED);

		return $this->execute($strQuery);
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\ManagerBundle\Api\Command;

use Contao\ManagerBundle\Api\Application;
use Contao\ManagerPlugin\Api\ApiPluginInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class VersionCommand extends Command
{
    /**
     * @var Application
     */
    private $application;

    public function __construct(Application $application)
    {
        parent::__construct();

        $this->application = $application;
    }

    /**
     * {@inheritdoc}
     */
    protected function configure(): void
    {
        parent::configure();

        $this
            ->setName('version')
            ->setDescription('Gets the Contao Manager API version and features.')
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output): void
    {
        $output->write(json_encode([
            'version' => Application::VERSION,
            'commands' => $this->getCommandNames(),
            'features' => $this->getFeatures(),
        ]));
    }

    /**
     * @return string[]
     */
    private function getCommandNames(): array
    {
        return array_keys($this->application->all());
    }

    /**
     * @return array<string,string[]>
     */
    private function getFeatures(): array
    {
        /** @var ApiPluginInterface[] $plugins */
        $plugins = $this->application->getPluginLoader()->getInstancesOf(ApiPluginInterface::class);

        $features = [];

        foreach ($plugins as $packageName => $plugin) {
            $features[$packageName] = $plugin->getApiFeatures();
        }

        return $features;
    }
}


<?php

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

namespace Contao;

/**
 * Class FormExplanation
 *
 * @property string $text
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class FormExplanation extends Widget
{

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

	/**
	 * The CSS class prefix
	 *
	 * @var string
	 */
	protected $strPrefix = 'widget widget-explanation';

	/**
	 * Do not validate
	 */
	public function validate() {}

	/**
	 * Generate the widget and return it as string
	 *
	 * @return string The widget markup
	 */
	public function generate()
	{
		$this->text = StringUtil::toHtml5($this->text);

		// Add the static files URL to images
		if ($staticUrl = System::getContainer()->get('contao.assets.files_context')->getStaticUrl())
		{
			$path = Config::get('uploadPath') . '/';
			$this->text = str_replace(' src="' . $path, ' src="' . $staticUrl . $path, $this->text);
		}

		return StringUtil::encodeEmail($this->text);
	}
}

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