<?php

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

namespace Contao;

/**
 * Converts dates and date format string
 *
 * The class converts arbitrary date strings to Unix timestamps and provides
 * extended information like the begin or end of the day, week, month or year.
 *
 * Usage:
 *
 *     $date = new Date();
 *     echo $date->datim;
 *
 *     $date = new Date('2011-09-18', 'Y-m-d');
 *     echo $date->monthBegin;
 *
 *     Date::formatToJs('m/d/Y H:i');
 *
 * @property integer $tstamp     The Unix timestamp
 * @property string  $date       The formatted date
 * @property string  $time       The formatted time
 * @property string  $datim      The formatted date and time
 * @property integer $dayBegin   The beginning of the current day
 * @property integer $dayEnd     The end of the current day
 * @property integer $monthBegin The beginning of the current month
 * @property integer $monthEnd   The end of the current month
 * @property integer $yearBegin  The beginning of the current year
 * @property integer $yearEnd    The end of the current year
 * @property string  $format     The date format string
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class Date
{

	/**
	 * Date string
	 * @var int
	 */
	protected $strDate;

	/**
	 * Format string
	 * @var string
	 */
	protected $strFormat;

	/**
	 * Date range
	 * @var array
	 */
	protected $arrRange = array();

	/**
	 * Set the object properties
	 *
	 * @param integer $strDate   An optional date string
	 * @param string  $strFormat An optional format string
	 */
	public function __construct($strDate=null, $strFormat=null)
	{
		$this->strDate = $strDate ?? time();
		$this->strFormat = $strFormat ?? static::getNumericDateFormat();

		if (!preg_match('/^-?[0-9]+$/', $this->strDate) || preg_match('/^[a-zA-Z]+$/', $this->strFormat))
		{
			$this->dateToUnix();
		}
	}

	/**
	 * Return an object property
	 *
	 * @param string $strKey The property name
	 *
	 * @return mixed|null The property value
	 */
	public function __get($strKey)
	{
		switch ($strKey)
		{
			case 'tstamp':
			case 'timestamp':
				return $this->strDate;
				break;

			case 'date':
				return static::parse(static::getNumericDateFormat(), $this->strDate);
				break;

			case 'time':
				return static::parse(static::getNumericTimeFormat(), $this->strDate);
				break;

			case 'datim':
				return static::parse(static::getNumericDatimFormat(), $this->strDate);
				break;

			case 'dayBegin':
				$this->createDateRanges();

				return $this->arrRange['day']['begin'];
				break;

			case 'dayEnd':
				$this->createDateRanges();

				return $this->arrRange['day']['end'];
				break;

			case 'monthBegin':
				$this->createDateRanges();

				return $this->arrRange['month']['begin'];
				break;

			case 'monthEnd':
				$this->createDateRanges();

				return $this->arrRange['month']['end'];
				break;

			case 'yearBegin':
				$this->createDateRanges();

				return $this->arrRange['year']['begin'];
				break;

			case 'yearEnd':
				$this->createDateRanges();

				return $this->arrRange['year']['end'];
				break;

			case 'format':
				return $this->strFormat;
				break;
		}

		return null;
	}

	/**
	 * Create the date ranges
	 */
	protected function createDateRanges()
	{
		if (!empty($this->arrRange))
		{
			return;
		}

		$intYear = date('Y', $this->strDate);
		$intMonth = date('m', $this->strDate);
		$intDay = date('d', $this->strDate);

		$this->arrRange['day']['begin'] = mktime(0, 0, 0, $intMonth, $intDay, $intYear);
		$this->arrRange['day']['end'] = mktime(23, 59, 59, $intMonth, $intDay, $intYear);
		$this->arrRange['month']['begin'] = mktime(0, 0, 0, $intMonth, 1, $intYear);
		$this->arrRange['month']['end'] = mktime(23, 59, 59, $intMonth, date('t', $this->strDate), $intYear);
		$this->arrRange['year']['begin'] = mktime(0, 0, 0, 1, 1, $intYear);
		$this->arrRange['year']['end'] = mktime(23, 59, 59, 12, 31, $intYear);
	}

	/**
	 * Return the begin of the week as timestamp
	 *
	 * @param integer $intStartDay The week start day
	 *
	 * @return integer The Unix timestamp
	 */
	public function getWeekBegin($intStartDay=0)
	{
		$intOffset = date('w', $this->strDate) - $intStartDay;

		if ($intOffset < 0)
		{
			$intOffset += 7;
		}

		return strtotime('-' . $intOffset . ' days', $this->strDate);
	}

	/**
	 * Return the end of the week as timestamp
	 *
	 * @param integer $intStartDay The week start day
	 *
	 * @return integer The Unix timestamp
	 */
	public function getWeekEnd($intStartDay=0)
	{
		return strtotime('+1 week', $this->getWeekBegin($intStartDay)) - 1;
	}

	/**
	 * Return a regular expression to check a date
	 *
	 * @param string $strFormat An optional format string
	 *
	 * @return string The regular expression string
	 *
	 * @throws \Exception If $strFormat is invalid
	 */
	public static function getRegexp($strFormat=null)
	{
		if ($strFormat === null)
		{
			$strFormat = static::getNumericDateFormat();
		}

		if (!static::isNumericFormat($strFormat))
		{
			throw new \Exception(sprintf('Invalid date format "%s"', $strFormat));
		}

		return preg_replace_callback('/[a-zA-Z]/', function ($matches)
			{
				// Thanks to Christian Labuda
				$arrRegexp = array
				(
					'a' => '(?Pam|pm)',
					'A' => '(?PAM|PM)',
					'd' => '(?P<d>0[1-9]|[12][0-9]|3[01])',
					'g' => '(?P<g>[1-9]|1[0-2])',
					'G' => '(?P<G>[0-9]|1[0-9]|2[0-3])',
					'h' => '(?P<h>0[1-9]|1[0-2])',
					'H' => '(?P<H>[01][0-9]|2[0-3])',
					'i' => '(?P[0-5][0-9])',
					'j' => '(?P<j>[1-9]|[12][0-9]|3[01])',
					'm' => '(?P<m>0[1-9]|1[0-2])',
					'n' => '(?P<n>[1-9]|1[0-2])',
					's' => '(?P<s>[0-5][0-9])',
					'Y' => '(?P<Y>[0-9]{4})',
					'y' => '(?P<y>[0-9]{2})',
				);

				return $arrRegexp[$matches[0]] ?? $matches[0];
			}, preg_quote($strFormat));
	}

	/**
	 * Return an input format string for a particular date (e.g. YYYY-MM-DD)
	 *
	 * @param string $strFormat An optional format string
	 *
	 * @return string The input format string
	 *
	 * @throws \Exception If $strFormat is invalid
	 */
	public static function getInputFormat($strFormat=null)
	{
		if ($strFormat === null)
		{
			$strFormat = static::getNumericDateFormat();
		}

		if (!static::isNumericFormat($strFormat))
		{
			throw new \Exception(sprintf('Invalid date format "%s"', $strFormat));
		}

		$arrCharacterMapper = array();

		foreach ($GLOBALS['TL_LANG']['DATE'] as $k=>$v)
		{
			$arrCharacterMapper[$k] = $v;
		}

		$arrInputFormat = array();
		$arrCharacters = str_split($strFormat);

		foreach ($arrCharacters as $strCharacter)
		{
			if (isset($arrCharacterMapper[$strCharacter]))
			{
				$arrInputFormat[$strFormat] .= $arrCharacterMapper[$strCharacter];
			}
			else
			{
				$arrInputFormat[$strFormat] .= $strCharacter;
			}
		}

		return $arrInputFormat[$strFormat];
	}

	/**
	 * Convert a date string into a Unix timestamp using the format string
	 *
	 * @throws \Exception            If the format string is invalid
	 * @throws \OutOfBoundsException If the timestamp does not map to a valid date
	 */
	protected function dateToUnix()
	{
		if (!static::isNumericFormat($this->strFormat))
		{
			throw new \Exception(sprintf('Invalid date format "%s"', $this->strFormat));
		}

		$intCount  = 0;
		$intDay    = '';
		$intMonth  = '';
		$intYear   = '';
		$intHour   = '';
		$intMinute = '';
		$intSecond = '';

		$blnMeridiem = false;
		$blnCorrectHour = false;

		$arrCharacterMapper = array
		(
			'd' => 'intDay',
			'j' => 'intDay',
			'm' => 'intMonth',
			'n' => 'intMonth',
			'y' => 'intYear',
			'Y' => 'intYear',
			'h' => 'intHour',
			'H' => 'intHour',
			'g' => 'intHour',
			'G' => 'intHour',
			'i' => 'intMinute',
			's' => 'intSecond'
		);

		$arrCharacters = str_split($this->strFormat);

		foreach ($arrCharacters as $strCharacter)
		{
			$var = $arrCharacterMapper[$strCharacter] ?? 'dummy';

			switch ($strCharacter)
			{
				case 'a':
				case 'A':
					$blnCorrectHour = true;
					$blnMeridiem = (strtolower(substr($this->strDate, $intCount, 2)) == 'pm') ? true : false;
					$intCount += 2;
					break;

				case 'd':
				case 'm':
				case 'y':
				case 'h':
				case 'H':
				case 'i':
				case 's':
					$$var .= substr($this->strDate, $intCount, 2);
					$intCount += 2;
					break;

				case 'j':
				case 'n':
				case 'g':
				case 'G':
					$$var .= substr($this->strDate, $intCount++, 1);

					if (preg_match('/[0-9]+/', substr($this->strDate, $intCount, 1)))
					{
						$$var .= substr($this->strDate, $intCount++, 1);
					}
					break;

				case 'Y':
					$$var .= substr($this->strDate, $intCount, 4);
					$intCount += 4;
					break;

				default:
					++$intCount;
					break;
			}
		}

		$intHour = (int) $intHour;

		if ($blnMeridiem)
		{
			$intHour += 12;
		}

		if ($blnCorrectHour && ($intHour == 12 || $intHour == 24))
		{
			$intHour -= 12;
		}

		if ($intMonth == '')
		{
			$intMonth = 1;
		}

		if ($intDay == '')
		{
			$intDay = 1;
		}

		if ($intYear == '')
		{
			$intYear = 1970;
		}

		// Validate the date (see #5086 and #7955)
		if (!is_numeric($intMonth) || !is_numeric($intDay) || !is_numeric($intYear) || checkdate($intMonth, $intDay, $intYear) === false)
		{
			throw new \OutOfBoundsException(sprintf('Invalid date "%s"', $this->strDate));
		}

		$this->strDate = mktime($intHour, (int) $intMinute, (int) $intSecond, (int) $intMonth, (int) $intDay, (int) $intYear);
	}

	/**
	 * Convert a PHP format string into a JavaScript format string
	 *
	 * @param string $strFormat The PHP format string
	 *
	 * @return mixed The JavaScript format string
	 */
	public static function formatToJs($strFormat)
	{
		$chunks = str_split($strFormat);

		foreach ($chunks as $k=>$v)
		{
			switch ($v)
			{
				case 'D': $chunks[$k] = 'a'; break;
				case 'j': $chunks[$k] = 'e'; break;
				case 'l': $chunks[$k] = 'A'; break;
				case 'S': $chunks[$k] = 'o'; break;
				case 'F': $chunks[$k] = 'B'; break;
				case 'M': $chunks[$k] = 'b'; break;
				case 'a': $chunks[$k] = 'p'; break;
				case 'A': $chunks[$k] = 'p'; break;
				case 'g': $chunks[$k] = 'l'; break;
				case 'G': $chunks[$k] = 'k'; break;
				case 'h': $chunks[$k] = 'I'; break;
				case 'i': $chunks[$k] = 'M'; break;
				case 's': $chunks[$k] = 'S'; break;
				case 'U': $chunks[$k] = 's'; break;
			}
		}

		return preg_replace('/([a-zA-Z])/', '%$1', implode('', $chunks));
	}

	/**
	 * Check for a numeric date format
	 *
	 * @param string $strFormat The PHP format string
	 *
	 * @return boolean True if the date format is numeric
	 */
	public static function isNumericFormat($strFormat)
	{
		return !preg_match('/[BbCcDEeFfIJKkLlMNOoPpQqRrSTtUuVvWwXxZz]+/', $strFormat);
	}

	/**
	 * Return the numeric date format string
	 *
	 * @return string The numeric date format string
	 */
	public static function getNumericDateFormat()
	{
		if (TL_MODE == 'FE')
		{
			/** @var PageModel $objPage */
			global $objPage;

			if ($objPage->dateFormat != '' && static::isNumericFormat($objPage->dateFormat))
			{
				return $objPage->dateFormat;
			}
		}

		return Config::get('dateFormat');
	}

	/**
	 * Return the numeric time format string
	 *
	 * @return string The numeric time format string
	 */
	public static function getNumericTimeFormat()
	{
		if (TL_MODE == 'FE')
		{
			/** @var PageModel $objPage */
			global $objPage;

			if ($objPage->timeFormat != '' && static::isNumericFormat($objPage->timeFormat))
			{
				return $objPage->timeFormat;
			}
		}

		return Config::get('timeFormat');
	}

	/**
	 * Return the numeric datim format string
	 *
	 * @return string The numeric datim format string
	 */
	public static function getNumericDatimFormat()
	{
		if (TL_MODE == 'FE')
		{
			/** @var PageModel $objPage */
			global $objPage;

			if ($objPage->datimFormat != '' && static::isNumericFormat($objPage->datimFormat))
			{
				return $objPage->datimFormat;
			}
		}

		return Config::get('datimFormat');
	}

	/**
	 * Return a numeric format string depending on the regular expression name
	 *
	 * @param string $strRgxp The regular expression name
	 *
	 * @return string The numeric format string
	 */
	public static function getFormatFromRgxp($strRgxp)
	{
		switch ($strRgxp)
		{
			case 'date':
				return static::getNumericDateFormat();
				break;

			case 'time':
				return static::getNumericTimeFormat();
				break;

			case 'datim':
				return static::getNumericDatimFormat();
				break;
		}

		return null;
	}

	/**
	 * Parse a date format string and translate textual representations
	 *
	 * @param string  $strFormat The date format string
	 * @param integer $intTstamp An optional timestamp
	 *
	 * @return string The textual representation of the date
	 */
	public static function parse($strFormat, $intTstamp=null)
	{
		$strModified = str_replace
		(
			array('l', 'D', 'F', 'M'),
			array('w::1', 'w::2', 'n::3', 'n::4'),
			$strFormat
		);

		if ($intTstamp === null)
		{
			$strDate = date($strModified);
		}
		elseif (!is_numeric($intTstamp))
		{
			return '';
		}
		else
		{
			$strDate = date($strModified, $intTstamp);
		}

		$strReturn = static::resolveCustomModifiers($strDate);

		// HOOK: add custom logic (see #4260)
		if (isset($GLOBALS['TL_HOOKS']['parseDate']) && \is_array($GLOBALS['TL_HOOKS']['parseDate']))
		{
			foreach ($GLOBALS['TL_HOOKS']['parseDate'] as $callback)
			{
				$strReturn = System::importStatic($callback[0])->{$callback[1]}($strReturn, $strFormat, $intTstamp);
			}
		}

		return $strReturn;
	}

	/**
	 * Round a UNIX timestamp to the full minute
	 *
	 * @param integer $intTime The timestamp
	 *
	 * @return integer The rounded timestamp
	 */
	public static function floorToMinute($intTime=null)
	{
		if ($intTime === null)
		{
			$intTime = time();
		}

		return $intTime - ($intTime % 60);
	}

	/**
	 * Resolve the custom modifiers
	 *
	 * @param string $strDate The date string
	 *
	 * @return string The resolved date string
	 */
	protected static function resolveCustomModifiers($strDate)
	{
		if (strpos($strDate, '::') === false)
		{
			return $strDate;
		}

		System::loadLanguageFile('default');

		if (!$GLOBALS['TL_LANG']['MSC']['dayShortLength'])
		{
			$GLOBALS['TL_LANG']['MSC']['dayShortLength'] = 3;
		}

		if (!$GLOBALS['TL_LANG']['MSC']['monthShortLength'])
		{
			$GLOBALS['TL_LANG']['MSC']['monthShortLength'] = 3;
		}

		$strReturn = '';
		$chunks = preg_split("/([0-9]{1,2}::[1-4])/", $strDate, -1, PREG_SPLIT_DELIM_CAPTURE);

		foreach ($chunks as $chunk)
		{
			list($index, $flag) = explode('::', $chunk);

			switch ($flag)
			{
				case 1:
					$strReturn .= $GLOBALS['TL_LANG']['DAYS'][$index];
					break;

				case 2:
					$strReturn .= $GLOBALS['TL_LANG']['DAYS_SHORT'][$index];
					break;

				case 3:
					$strReturn .= $GLOBALS['TL_LANG']['MONTHS'][($index - 1)];
					break;

				case 4:
					$strReturn .= $GLOBALS['TL_LANG']['MONTHS_SHORT'][($index - 1)];
					break;

				default:
					$strReturn .= $chunk;
					break;
			}
		}

		return $strReturn;
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\InstallationBundle;

use Contao\Backend;
use Contao\Config;
use Contao\File;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DBALException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

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

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

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

    public function __construct(Connection $connection, string $rootDir, LoggerInterface $logger)
    {
        $this->connection = $connection;
        $this->rootDir = $rootDir;
        $this->logger = $logger;
    }

    public function isLocked(): bool
    {
        $file = $this->rootDir.'/var/install_lock';

        if (!file_exists($file)) {
            return false;
        }

        $count = file_get_contents($this->rootDir.'/var/install_lock');

        return (int) $count >= 3;
    }

    public function canWriteFiles(): bool
    {
        return is_writable(__FILE__);
    }

    public function shouldAcceptLicense(): bool
    {
        return !Config::get('licenseAccepted');
    }

    public function increaseLoginCount(): void
    {
        $count = 0;
        $file = $this->rootDir.'/var/install_lock';

        if (file_exists($file)) {
            $count = file_get_contents($this->rootDir.'/var/install_lock');
        }

        $fs = new Filesystem();
        $fs->dumpFile($file, (int) $count + 1);
    }

    public function resetLoginCount(): void
    {
        File::putContent('system/tmp/login-count.txt', 0);
    }

    public function setConnection(Connection $connection): void
    {
        $this->connection = $connection;
    }

    public function canConnectToDatabase(?string $name): bool
    {
        // Return if there is a working database connection already
        try {
            $this->connection->connect();
            $this->connection->query('SHOW TABLES');

            return true;
        } catch (\Exception $e) {
        }

        if (null === $name || null === $this->connection) {
            return false;
        }

        try {
            $this->connection->connect();
        } catch (\Exception $e) {
            $this->logException($e);

            return false;
        }

        $quotedName = $this->connection->quoteIdentifier($name);

        try {
            $this->connection->query('use '.$quotedName);
        } catch (DBALException $e) {
            $this->logException($e);

            return false;
        }

        return true;
    }

    public function hasTable(string $name): bool
    {
        return $this->connection->getSchemaManager()->tablesExist([$name]);
    }

    public function isFreshInstallation(): bool
    {
        if (!$this->hasTable('tl_page')) {
            return true;
        }

        $statement = $this->connection->query('
            SELECT
                COUNT(*) AS count
            FROM
                tl_page
        ');

        return $statement->fetch(\PDO::FETCH_OBJ)->count < 1;
    }

    /**
     * Checks if the database is older than version 3.2.
     */
    public function hasOldDatabase(): bool
    {
        if (!$this->hasTable('tl_layout')) {
            return false;
        }

        $sql = $this->connection
            ->getDatabasePlatform()
            ->getListTableColumnsSQL('tl_layout', $this->connection->getDatabase())
        ;

        $columns = $this->connection->fetchAll($sql);

        foreach ($columns as $column) {
            if ('sections' === $column['Field']) {
                return !\in_array($column['Type'], ['varchar(1022)', 'blob'], true);
            }
        }

        return false;
    }

    /**
     * Checks the database configuration.
     */
    public function hasConfigurationError(array &$context): bool
    {
        $row = $this->connection
            ->query('SELECT @@version as Version')
            ->fetch(\PDO::FETCH_OBJ)
        ;

        [$version] = explode('-', $row->Version);

        // The database version is too old
        if (version_compare($version, '5.1.0', '<')) {
            $context['errorCode'] = 1;
            $context['version'] = $version;

            return true;
        }

        $options = $this->connection->getParams()['defaultTableOptions'];

        // Check the collation if the user has configured it
        if (isset($options['collate'])) {
            $statement = $this->connection->query("SHOW COLLATION LIKE '".$options['collate']."'");

            // The configured collation is not installed
            if (false === ($row = $statement->fetch(\PDO::FETCH_OBJ))) {
                $context['errorCode'] = 2;
                $context['collation'] = $options['collate'];

                return true;
            }
        }

        // Check the engine if the user has configured it
        if (isset($options['engine'])) {
            $engineFound = false;
            $statement = $this->connection->query('SHOW ENGINES');

            while (false !== ($row = $statement->fetch(\PDO::FETCH_OBJ))) {
                if ($options['engine'] === $row->Engine) {
                    $engineFound = true;
                    break;
                }
            }

            // The configured engine is not available
            if (!$engineFound) {
                $context['errorCode'] = 3;
                $context['engine'] = $options['engine'];

                return true;
            }
        }

        // Check if utf8mb4 can be used if the user has configured it
        if (isset($options['engine'], $options['collate']) && 0 === strncmp($options['collate'], 'utf8mb4', 7)) {
            if ('innodb' !== strtolower($options['engine'])) {
                $context['errorCode'] = 4;
                $context['engine'] = $options['engine'];

                return true;
            }

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

            // The variable no longer exists as of MySQL 8 and MariaDB 10.3
            if (false === $row) {
                return false;
            }

            // As there is no reliable way to get the vendor (see #84), we are
            // guessing based on the version number. The check will not be run
            // as of MySQL 8 and MariaDB 10.3, so this should be safe.
            $vok = version_compare($version, '10', '>=') ? '10.2.2' : '5.7.7';

            // Large prefixes are always enabled as of MySQL 5.7.7 and MariaDB 10.2.2
            if (version_compare($version, $vok, '>=')) {
                return false;
            }

            // The innodb_large_prefix option is disabled
            if (!\in_array(strtolower((string) $row->Value), ['1', 'on'], true)) {
                $context['errorCode'] = 5;

                return true;
            }

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

            // The innodb_file_per_table option is disabled
            if (!\in_array(strtolower((string) $row->Value), ['1', 'on'], true)) {
                $context['errorCode'] = 6;

                return true;
            }

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

            // The InnoDB file format is not Barracuda
            if ('barracuda' !== strtolower((string) $row->Value)) {
                $context['errorCode'] = 6;

                return true;
            }
        }

        return false;
    }

    public function handleRunOnce(): void
    {
        // Wait for the tables to be created (see #5061)
        if (!$this->hasTable('tl_log')) {
            return;
        }

        Backend::handleRunOnce();
    }

    /**
     * Returns the available SQL templates.
     *
     * @return string[]
     */
    public function getTemplates(): array
    {
        /** @var SplFileInfo[] $finder */
        $finder = Finder::create()
            ->files()
            ->name('*.sql')
            ->in($this->rootDir.'/templates')
        ;

        $templates = [];

        foreach ($finder as $file) {
            $templates[] = $file->getRelativePathname();
        }

        return $templates;
    }

    public function importTemplate(string $template, bool $preserveData = false): void
    {
        if (!$preserveData) {
            $tables = $this->connection->getSchemaManager()->listTableNames();

            foreach ($tables as $table) {
                if (0 === strncmp($table, 'tl_', 3)) {
                    $this->connection->query('TRUNCATE TABLE '.$this->connection->quoteIdentifier($table));
                }
            }
        }

        $data = file($this->rootDir.'/templates/'.$template);

        foreach (preg_grep('/^INSERT /', $data) as $query) {
            $this->connection->query($query);
        }
    }

    public function hasAdminUser(): bool
    {
        try {
            $statement = $this->connection->query("
                SELECT
                    COUNT(*) AS count
                FROM
                    tl_user
                WHERE
                    `admin` = '1'
            ");

            if ($statement->fetch(\PDO::FETCH_OBJ)->count > 0) {
                return true;
            }
        } catch (DBALException $e) {
            // ignore
        }

        return false;
    }

    public function persistAdminUser(string $username, string $name, string $email, string $password, string $language): void
    {
        $statement = $this->connection->prepare("
            INSERT INTO
                tl_user
                    (
                        tstamp,
                        name,
                        email,
                        username,
                        password,
                        language,
                        backendTheme,
                        `admin`,
                        showHelp,
                        useRTE,
                        useCE,
                        thumbnails,
                        dateAdded
                    )
                 VALUES
                    (:time, :name, :email, :username, :password, :language, 'flexible', 1, 1, 1, 1, 1, :time)
        ");

        $replace = [
            '#' => '#',
            '<' => '<',
            '>' => '>',
            '(' => '(',
            ')' => ')',
            '\\' => '\',
            '=' => '=',
        ];

        $statement->execute([
            ':time' => time(),
            ':name' => strtr($name, $replace),
            ':email' => $email,
            ':username' => strtr($username, $replace),
            ':password' => password_hash($password, PASSWORD_DEFAULT),
            ':language' => $language,
        ]);
    }

    /**
     * @return mixed|null
     */
    public function getConfig(string $key)
    {
        return Config::get($key);
    }

    public function setConfig(string $key, $value): void
    {
        Config::set($key, $value);
    }

    public function persistConfig(string $key, $value): void
    {
        $config = Config::getInstance();
        $config->persist($key, $value);
        $config->save();
    }

    public function logException(\Exception $e): void
    {
        $this->logger->critical('An exception occurred.', ['exception' => $e]);
    }
}


<?php

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

namespace Contao;

use Contao\Model\Collection;
use FOS\HttpCache\ResponseTagger;

/**
 * Parent class for front end modules.
 *
 * @property integer $id
 * @property integer $pid
 * @property integer $tstamp
 * @property string  $name
 * @property string  $headline
 * @property string  $type
 * @property integer $levelOffset
 * @property integer $showLevel
 * @property boolean $hardLimit
 * @property boolean $showProtected
 * @property boolean $defineRoot
 * @property integer $rootPage
 * @property string  $navigationTpl
 * @property string  $customTpl
 * @property array   $pages
 * @property string  $orderPages
 * @property boolean $showHidden
 * @property string  $customLabel
 * @property boolean $autologin
 * @property integer $jumpTo
 * @property boolean $redirectBack
 * @property string  $cols
 * @property array   $editable
 * @property string  $memberTpl
 * @property integer $form
 * @property string  $queryType
 * @property boolean $fuzzy
 * @property integer $contextLength
 * @property integer $totalLength
 * @property integer $perPage
 * @property string  $searchType
 * @property string  $searchTpl
 * @property string  $inColumn
 * @property integer $skipFirst
 * @property boolean $loadFirst
 * @property string  $singleSRC
 * @property string  $url
 * @property string  $imgSize
 * @property boolean $useCaption
 * @property boolean $fullsize
 * @property string  $multiSRC
 * @property string  $orderSRC
 * @property string  $html
 * @property integer $rss_cache
 * @property string  $rss_feed
 * @property string  $rss_template
 * @property integer $numberOfItems
 * @property boolean $disableCaptcha
 * @property string  $reg_groups
 * @property boolean $reg_allowLogin
 * @property boolean $reg_skipName
 * @property string  $reg_close
 * @property boolean $reg_assignDir
 * @property string  $reg_homeDir
 * @property boolean $reg_activate
 * @property integer $reg_jumpTo
 * @property string  $reg_text
 * @property string  $reg_password
 * @property boolean $protected
 * @property string  $groups
 * @property boolean $guests
 * @property string  $cssID
 * @property string  $hl
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
abstract class Module extends Frontend
{

	/**
	 * Template
	 * @var string
	 */
	protected $strTemplate;

	/**
	 * Column
	 * @var string
	 */
	protected $strColumn;

	/**
	 * Model
	 * @var ModuleModel
	 */
	protected $objModel;

	/**
	 * Current record
	 * @var array
	 */
	protected $arrData = array();

	/**
	 * Style array
	 * @var array
	 */
	protected $arrStyle = array();

	/**
	 * Initialize the object
	 *
	 * @param ModuleModel $objModule
	 * @param string      $strColumn
	 */
	public function __construct($objModule, $strColumn='main')
	{
		if ($objModule instanceof Model || $objModule instanceof Collection)
		{
			/** @var ModuleModel $objModel */
			$objModel = $objModule;

			if ($objModel instanceof Collection)
			{
				$objModel = $objModel->current();
			}

			$this->objModel = $objModel;
		}

		parent::__construct();

		$this->arrData = $objModule->row();
		$this->cssID = StringUtil::deserialize($objModule->cssID, true);

		if ($this->customTpl != '' && TL_MODE == 'FE')
		{
			$this->strTemplate = $this->customTpl;
		}

		$arrHeadline = StringUtil::deserialize($objModule->headline);
		$this->headline = \is_array($arrHeadline) ? $arrHeadline['value'] : $arrHeadline;
		$this->hl = \is_array($arrHeadline) ? $arrHeadline['unit'] : 'h1';
		$this->strColumn = $strColumn;
	}

	/**
	 * Set an object property
	 *
	 * @param string $strKey
	 * @param mixed  $varValue
	 */
	public function __set($strKey, $varValue)
	{
		$this->arrData[$strKey] = $varValue;
	}

	/**
	 * Return an object property
	 *
	 * @param string $strKey
	 *
	 * @return mixed
	 */
	public function __get($strKey)
	{
		if (isset($this->arrData[$strKey]))
		{
			return $this->arrData[$strKey];
		}

		return parent::__get($strKey);
	}

	/**
	 * Check whether a property is set
	 *
	 * @param string $strKey
	 *
	 * @return boolean
	 */
	public function __isset($strKey)
	{
		return isset($this->arrData[$strKey]);
	}

	/**
	 * Return the model
	 *
	 * @return Model
	 */
	public function getModel()
	{
		return $this->objModel;
	}

	/**
	 * Parse the template
	 *
	 * @return string
	 */
	public function generate()
	{
		$this->Template = new FrontendTemplate($this->strTemplate);
		$this->Template->setData($this->arrData);

		$this->compile();

		// Do not change this order (see #6191)
		$this->Template->style = !empty($this->arrStyle) ? implode(' ', $this->arrStyle) : '';
		$this->Template->class = trim('mod_' . $this->type . ' ' . $this->cssID[1]);
		$this->Template->cssID = !empty($this->cssID[0]) ? ' id="' . $this->cssID[0] . '"' : '';

		$this->Template->inColumn = $this->strColumn;

		if ($this->Template->headline == '')
		{
			$this->Template->headline = $this->headline;
		}

		if ($this->Template->hl == '')
		{
			$this->Template->hl = $this->hl;
		}

		if (!empty($this->objModel->classes) && \is_array($this->objModel->classes))
		{
			$this->Template->class .= ' ' . implode(' ', $this->objModel->classes);
		}

		// Tag the response
		if (System::getContainer()->has('fos_http_cache.http.symfony_response_tagger'))
		{
			/** @var ResponseTagger $responseTagger */
			$responseTagger = System::getContainer()->get('fos_http_cache.http.symfony_response_tagger');
			$responseTagger->addTags(array('contao.db.tl_module.' . $this->id));
		}

		return $this->Template->parse();
	}

	/**
	 * Compile the current element
	 */
	abstract protected function compile();

	/**
	 * Recursively compile the navigation menu and return it as HTML string
	 *
	 * @param integer $pid
	 * @param integer $level
	 * @param string  $host
	 * @param string  $language
	 *
	 * @return string
	 */
	protected function renderNavigation($pid, $level=1, $host=null, $language=null)
	{
		// Get all active subpages
		$objSubpages = PageModel::findPublishedSubpagesWithoutGuestsByPid($pid, $this->showHidden, $this instanceof ModuleSitemap);

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

		$items = array();
		$groups = array();

		// Get all groups of the current front end user
		if (FE_USER_LOGGED_IN)
		{
			$this->import(FrontendUser::class, 'User');
			$groups = $this->User->groups;
		}

		// Layout template fallback
		if ($this->navigationTpl == '')
		{
			$this->navigationTpl = 'nav_default';
		}

		$objTemplate = new FrontendTemplate($this->navigationTpl);
		$objTemplate->pid = $pid;
		$objTemplate->type = \get_class($this);
		$objTemplate->cssID = $this->cssID; // see #4897
		$objTemplate->level = 'level_' . $level++;

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

		// Browse subpages
		foreach ($objSubpages as $objSubpage)
		{
			// Skip hidden sitemap pages
			if ($this instanceof ModuleSitemap && $objSubpage->sitemap == 'map_never')
			{
				continue;
			}

			$subitems = '';
			$_groups = StringUtil::deserialize($objSubpage->groups);

			// Override the domain (see #3765)
			if ($host !== null)
			{
				$objSubpage->domain = $host;
			}

			// Do not show protected pages unless a front end user is logged in
			if (!$objSubpage->protected || $this->showProtected || ($this instanceof ModuleSitemap && $objSubpage->sitemap == 'map_always') || (\is_array($_groups) && \is_array($groups) && \count(array_intersect($_groups, $groups))))
			{
				// Check whether there will be subpages
				if ($objSubpage->subpages > 0 && (!$this->showLevel || $this->showLevel >= $level || (!$this->hardLimit && ($objPage->id == $objSubpage->id || \in_array($objPage->id, $this->Database->getChildRecords($objSubpage->id, 'tl_page'))))))
				{
					$subitems = $this->renderNavigation($objSubpage->id, $level, $host, $language);
				}

				$href = null;

				// Get href
				switch ($objSubpage->type)
				{
					case 'redirect':
						$href = $objSubpage->url;

						if (strncasecmp($href, 'mailto:', 7) === 0)
						{
							$href = StringUtil::encodeEmail($href);
						}
						break;

					case 'forward':
						if ($objSubpage->jumpTo)
						{
							$objNext = PageModel::findPublishedById($objSubpage->jumpTo);
						}
						else
						{
							$objNext = PageModel::findFirstPublishedRegularByPid($objSubpage->id);
						}

						// Hide the link if the target page is invisible
						if (!$objNext instanceof PageModel || (!$objNext->isPublic && !BE_USER_LOGGED_IN))
						{
							continue 2;
						}

						$href = $objNext->getFrontendUrl();
						break;

					default:
						$href = $objSubpage->getFrontendUrl();
						break;
				}

				$row = $objSubpage->row();
				$trail = \in_array($objSubpage->id, $objPage->trail);

				// Active page
				if (($objPage->id == $objSubpage->id || ($objSubpage->type == 'forward' && $objPage->id == $objSubpage->jumpTo)) && !($this instanceof ModuleSitemap) && $href == Environment::get('request'))
				{
					// Mark active forward pages (see #4822)
					$strClass = (($objSubpage->type == 'forward' && $objPage->id == $objSubpage->jumpTo) ? 'forward' . ($trail ? ' trail' : '') : 'active') . (($subitems != '') ? ' submenu' : '') . ($objSubpage->protected ? ' protected' : '') . (($objSubpage->cssClass != '') ? ' ' . $objSubpage->cssClass : '');

					$row['isActive'] = true;
					$row['isTrail'] = false;
				}

				// Regular page
				else
				{
					$strClass = (($subitems != '') ? 'submenu' : '') . ($objSubpage->protected ? ' protected' : '') . ($trail ? ' trail' : '') . (($objSubpage->cssClass != '') ? ' ' . $objSubpage->cssClass : '');

					// Mark pages on the same level (see #2419)
					if ($objSubpage->pid == $objPage->pid)
					{
						$strClass .= ' sibling';
					}

					$row['isActive'] = false;
					$row['isTrail'] = $trail;
				}

				$row['subitems'] = $subitems;
				$row['class'] = trim($strClass);
				$row['title'] = StringUtil::specialchars($objSubpage->title, true);
				$row['pageTitle'] = StringUtil::specialchars($objSubpage->pageTitle, true);
				$row['link'] = $objSubpage->title;
				$row['href'] = $href;
				$row['rel'] = '';
				$row['nofollow'] = (strncmp($objSubpage->robots, 'noindex,nofollow', 16) === 0); // backwards compatibility
				$row['target'] = '';
				$row['description'] = str_replace(array("\n", "\r"), array(' ', ''), $objSubpage->description);

				// Override the link target
				if ($objSubpage->type == 'redirect' && $objSubpage->target)
				{
					$row['target'] = ' target="_blank"';
				}

				$arrRel = array();

				if (strncmp($objSubpage->robots, 'noindex,nofollow', 16) === 0)
				{
					$arrRel[] = 'nofollow';
				}

				if ($objSubpage->type == 'redirect' && $objSubpage->target)
				{
					$arrRel[] = 'noreferrer';
					$arrRel[] = 'noopener';
				}

				// Override the rel attribute
				if (!empty($arrRel))
				{
					$row['rel'] = ' rel="' . implode(' ', $arrRel) . '"';
				}

				$items[] = $row;
			}
		}

		// Add classes first and last
		if (!empty($items))
		{
			$last = \count($items) - 1;

			$items[0]['class'] = trim($items[0]['class'] . ' first');
			$items[$last]['class'] = trim($items[$last]['class'] . ' last');
		}

		$objTemplate->items = $items;

		return !empty($items) ? $objTemplate->parse() : '';
	}

	/**
	 * 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
	 */
	public static function findClass($strName)
	{
		foreach ($GLOBALS['FE_MOD'] as $v)
		{
			foreach ($v as $kk=>$vv)
			{
				if ($kk == $strName)
				{
					return $vv;
				}
			}
		}

		return '';
	}
}

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


<?php

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

namespace Contao;

use Patchwork\Utf8;

/**
 * Class ModuleFaqList
 *
 * @property array $faq_categories
 * @property int   $faq_readerModule
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ModuleFaqList extends Module
{

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

	/**
	 * Target pages
	 * @var array
	 */
	protected $arrTargets = array();

	/**
	 * Display a wildcard in the back end
	 *
	 * @return string
	 */
	public function generate()
	{
		if (TL_MODE == 'BE')
		{
			$objTemplate = new BackendTemplate('be_wildcard');
			$objTemplate->wildcard = '### ' . Utf8::strtoupper($GLOBALS['TL_LANG']['FMD']['faqlist'][0]) . ' ###';
			$objTemplate->title = $this->headline;
			$objTemplate->id = $this->id;
			$objTemplate->link = $this->name;
			$objTemplate->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id=' . $this->id;

			return $objTemplate->parse();
		}

		$this->faq_categories = StringUtil::deserialize($this->faq_categories);

		// Return if there are no categories
		if (empty($this->faq_categories) || !\is_array($this->faq_categories))
		{
			return '';
		}

		// Show the FAQ reader if an item has been selected
		if ($this->faq_readerModule > 0 && (isset($_GET['items']) || (Config::get('useAutoItem') && isset($_GET['auto_item']))))
		{
			return $this->getFrontendModule($this->faq_readerModule, $this->strColumn);
		}

		return parent::generate();
	}

	/**
	 * Generate the module
	 */
	protected function compile()
	{
		$objFaq = FaqModel::findPublishedByPids($this->faq_categories);

		if ($objFaq === null)
		{
			$this->Template->faq = array();

			return;
		}

		$arrFaq = array_fill_keys($this->faq_categories, array());

		// Add FAQs
		while ($objFaq->next())
		{
			$arrTemp = $objFaq->row();
			$arrTemp['title'] = StringUtil::specialchars($objFaq->question, true);
			$arrTemp['href'] = $this->generateFaqLink($objFaq);

			/** @var FaqCategoryModel $objPid */
			$objPid = $objFaq->getRelated('pid');

			$arrFaq[$objFaq->pid]['items'][] = $arrTemp;
			$arrFaq[$objFaq->pid]['headline'] = $objPid->headline;
			$arrFaq[$objFaq->pid]['title'] = $objPid->title;
		}

		$arrFaq = array_values(array_filter($arrFaq));

		$cat_count = 0;
		$cat_limit = \count($arrFaq);

		// Add classes
		foreach ($arrFaq as $k=>$v)
		{
			$count = 0;
			$limit = \count($v['items']);

			for ($i=0; $i<$limit; $i++)
			{
				$arrFaq[$k]['items'][$i]['class'] = trim(((++$count == 1) ? ' first' : '') . (($count >= $limit) ? ' last' : '') . ((($count % 2) == 0) ? ' odd' : ' even'));
			}

			$arrFaq[$k]['class'] = trim(((++$cat_count == 1) ? ' first' : '') . (($cat_count >= $cat_limit) ? ' last' : '') . ((($cat_count % 2) == 0) ? ' odd' : ' even'));
		}

		$this->Template->faq = $arrFaq;
	}

	/**
	 * Create links and remember pages that have been processed
	 *
	 * @param FaqModel $objFaq
	 *
	 * @return string
	 *
	 * @throws \Exception
	 */
	protected function generateFaqLink($objFaq)
	{
		/** @var FaqCategoryModel $objCategory */
		$objCategory = $objFaq->getRelated('pid');
		$jumpTo = (int) $objCategory->jumpTo;

		// A jumpTo page is not mandatory for FAQ categories (see #6226) but required for the FAQ list module
		if ($jumpTo < 1)
		{
			throw new \Exception("FAQ categories without redirect page cannot be used in an FAQ list");
		}

		// Get the URL from the jumpTo page of the category
		if (!isset($this->arrTargets[$jumpTo]))
		{
			$this->arrTargets[$jumpTo] = ampersand(Environment::get('request'), true);

			if ($jumpTo > 0 && ($objTarget = PageModel::findByPk($jumpTo)) !== null)
			{
				/** @var PageModel $objTarget */
				$this->arrTargets[$jumpTo] = ampersand($objTarget->getFrontendUrl(Config::get('useAutoItem') ? '/%s' : '/items/%s'));
			}
		}

		return sprintf(preg_replace('/%(?!s)/', '%%', $this->arrTargets[$jumpTo]), ($objFaq->alias ?: $objFaq->id));
	}
}

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


<?php

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

namespace Contao;

use Contao\Model\Collection;

/**
 * Reads and writes forms
 *
 * @property integer $id
 * @property integer $tstamp
 * @property string  $title
 * @property string  $alias
 * @property integer $jumpTo
 * @property boolean $sendViaEmail
 * @property string  $recipient
 * @property string  $subject
 * @property string  $format
 * @property boolean $skipEmpty
 * @property boolean $storeValues
 * @property string  $targetTable
 * @property string  $method
 * @property boolean $novalidate
 * @property string  $attributes
 * @property string  $formID
 * @property boolean $allowTags
 *
 * @method static FormModel|null findById($id, array $opt=array())
 * @method static FormModel|null findByPk($id, array $opt=array())
 * @method static FormModel|null findByIdOrAlias($val, array $opt=array())
 * @method static FormModel|null findOneBy($col, $val, array $opt=array())
 * @method static FormModel|null findOneByTstamp($val, array $opt=array())
 * @method static FormModel|null findOneByTitle($val, array $opt=array())
 * @method static FormModel|null findOneByAlias($val, array $opt=array())
 * @method static FormModel|null findOneByJumpTo($val, array $opt=array())
 * @method static FormModel|null findOneBySendViaEmail($val, array $opt=array())
 * @method static FormModel|null findOneByRecipient($val, array $opt=array())
 * @method static FormModel|null findOneBySubject($val, array $opt=array())
 * @method static FormModel|null findOneByFormat($val, array $opt=array())
 * @method static FormModel|null findOneBySkipEmpty($val, array $opt=array())
 * @method static FormModel|null findOneByStoreValues($val, array $opt=array())
 * @method static FormModel|null findOneByTargetTable($val, array $opt=array())
 * @method static FormModel|null findOneByMethod($val, array $opt=array())
 * @method static FormModel|null findOneByNovalidate($val, array $opt=array())
 * @method static FormModel|null findOneByAttributes($val, array $opt=array())
 * @method static FormModel|null findOneByFormID($val, array $opt=array())
 * @method static FormModel|null findOneByTableless($val, array $opt=array())
 * @method static FormModel|null findOneByAllowTags($val, array $opt=array())
 *
 * @method static Collection|FormModel[]|FormModel|null findByTstamp($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByTitle($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByAlias($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByJumpTo($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findBySendViaEmail($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByRecipient($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findBySubject($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByFormat($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findBySkipEmpty($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByStoreValues($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByTargetTable($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByMethod($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByNovalidate($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByAttributes($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByFormID($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByTableless($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findByAllowTags($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findMultipleByIds($val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findBy($col, $val, array $opt=array())
 * @method static Collection|FormModel[]|FormModel|null findAll(array $opt=array())
 *
 * @method static integer countById($id, array $opt=array())
 * @method static integer countByTstamp($val, array $opt=array())
 * @method static integer countByTitle($val, array $opt=array())
 * @method static integer countByAlias($val, array $opt=array())
 * @method static integer countByJumpTo($val, array $opt=array())
 * @method static integer countBySendViaEmail($val, array $opt=array())
 * @method static integer countByRecipient($val, array $opt=array())
 * @method static integer countBySubject($val, array $opt=array())
 * @method static integer countByFormat($val, array $opt=array())
 * @method static integer countBySkipEmpty($val, array $opt=array())
 * @method static integer countByStoreValues($val, array $opt=array())
 * @method static integer countByTargetTable($val, array $opt=array())
 * @method static integer countByMethod($val, array $opt=array())
 * @method static integer countByNovalidate($val, array $opt=array())
 * @method static integer countByAttributes($val, array $opt=array())
 * @method static integer countByFormID($val, array $opt=array())
 * @method static integer countByTableless($val, array $opt=array())
 * @method static integer countByAllowTags($val, array $opt=array())
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class FormModel extends Model
{

	/**
	 * Table name
	 * @var string
	 */
	protected static $strTable = 'tl_form';

	/**
	 * Get the maximum file size that is allowed for file uploads
	 *
	 * @return integer The maximum file size in bytes
	 */
	public function getMaxUploadFileSize()
	{
		$objResult = Database::getInstance()->prepare("SELECT MAX(maxlength) AS maxlength FROM tl_form_field WHERE pid=? AND invisible='' AND type='upload' AND maxlength>0")
											 ->execute($this->id);

		if ($objResult->numRows > 0 && $objResult->maxlength > 0)
		{
			return $objResult->maxlength;
		}
		else
		{
			return Config::get('maxFileSize');
		}
	}
}

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


<?php

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

namespace Contao;

use Contao\Model\Collection;
use Contao\Model\Registry;

/**
 * Reads and writes double opt-in tokens
 *
 * @property integer $id
 * @property integer $tstamp
 * @property string  $token
 * @property integer $createdOn
 * @property integer $confirmedOn
 * @property integer $removeOn
 * @property string  $invalidatedThrough
 * @property string  $email
 * @property string  $emailSubject
 * @property string  $emailText
 *
 * @method static OptInModel|null findById($id, array $opt=array())
 * @method static OptInModel|null findByPk($id, array $opt=array())
 * @method static OptInModel|null findByIdOrAlias($val, array $opt=array())
 * @method static OptInModel|null findOneBy($col, $val, array $opt=array())
 * @method static OptInModel|null findOneByTstamp($val, array $opt=array())
 * @method static OptInModel|null findOneByToken($val, array $opt=array())
 * @method static OptInModel|null findOneByCreatedOn($val, array $opt=array())
 * @method static OptInModel|null findOneByConfirmedOn($val, array $opt=array())
 * @method static OptInModel|null findOneByRemoveOn($val, array $opt=array())
 * @method static OptInModel|null findOneByInvalidatedThrough($val, array $opt=array())
 * @method static OptInModel|null findOneByEmail($val, array $opt=array())
 * @method static OptInModel|null findOneByEmailSubject($val, array $opt=array())
 * @method static OptInModel|null findOneByEmailText($val, array $opt=array())
 *
 * @method static Collection|OptInModel[]|OptInModel|null findByTstamp($val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findByToken($val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findByCreatedOn($val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findByConfirmedOn($val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findByRemoveOn($val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findByInvalidatedThrough($val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findByEmail($val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findByEmailSubject($val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findByEmailText($val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findMultipleByIds($val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findBy($col, $val, array $opt=array())
 * @method static Collection|OptInModel[]|OptInModel|null findAll(array $opt=array())
 *
 * @method static integer countById($id, array $opt=array())
 * @method static integer countByTstamp($val, array $opt=array())
 * @method static integer countByToken($val, array $opt=array())
 * @method static integer countByCreatedOn($val, array $opt=array())
 * @method static integer countByConfirmedOn($val, array $opt=array())
 * @method static integer countByRemoveOn($val, array $opt=array())
 * @method static integer countByInvalidatedThrough($val, array $opt=array())
 * @method static integer countByEmail($val, array $opt=array())
 * @method static integer countByEmailSubject($val, array $opt=array())
 * @method static integer countByEmailText($val, array $opt=array())
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class OptInModel extends Model
{

	/**
	 * Table name
	 * @var string
	 */
	protected static $strTable = 'tl_opt_in';

	/**
	 * Find expired double opt-in tokens
	 *
	 * @param array $arrOptions
	 *
	 * @return Collection|OptInModel[]|OptInModel|null
	 */
	public static function findExpiredTokens(array $arrOptions=array())
	{
		$t = static::$strTable;

		return static::findBy(array("$t.removeOn>0 AND $t.removeOn<?"), time(), $arrOptions);
	}

	/**
	 * Find an opt-in token by its related table and ID
	 *
	 * @param string  $strTable
	 * @param integer $intId
	 * @param array   $arrOptions
	 *
	 * @return static|null
	 *
	 * @deprecated Deprecated since Contao 4.7, to be removed in Contao 5.0; use the
	 *             Contao\OptInModel::findByRelatedTableAndIds() method instead
	 */
	public static function findOneByRelatedTableAndId($strTable, $intId, array $arrOptions=array())
	{
		@trigger_error('Using the Contao\OptInModel::findOneByRelatedTableAndIds() method has been deprecated and will no longer work in Contao 5.0. Use the Contao\OptInModel::findByRelatedTableAndIds() method instead.', E_USER_DEPRECATED);

		$t = static::$strTable;
		$objDatabase = Database::getInstance();

		$objResult = $objDatabase->prepare("SELECT * FROM $t WHERE id IN (SELECT pid FROM tl_opt_in_related WHERE relTable=? AND relId=?)")
								 ->execute($strTable, $intId);

		if ($objResult->numRows < 1)
		{
			return null;
		}

		$objRegistry = Registry::getInstance();

		/** @var OptInModel|Model $objOptIn */
		if ($objOptIn = $objRegistry->fetch($t, $objResult->id))
		{
			return $objOptIn;
		}

		return new static($objResult);
	}

	/**
	 * Find opt-in tokens by their related table and ID
	 *
	 * @param string $strTable
	 * @param array  $arrIds
	 * @param array  $arrOptions
	 *
	 * @return Collection|OptInModel[]|OptInModel|null
	 */
	public static function findByRelatedTableAndIds($strTable, array $arrIds, array $arrOptions=array())
	{
		$t = static::$strTable;
		$objDatabase = Database::getInstance();

		$objResult = $objDatabase->prepare("SELECT * FROM $t WHERE $t.id IN (SELECT pid FROM tl_opt_in_related WHERE relTable=? AND relId IN(" . implode(',', array_map('\intval', $arrIds)) . ")) ORDER BY $t.createdOn DESC")
								 ->execute($strTable, $arrIds);

		if ($objResult->numRows < 1)
		{
			return null;
		}

		$arrModels = array();
		$objRegistry = Registry::getInstance();

		while ($objResult->next())
		{
			/** @var OptInModel|Model $objOptIn */
			if ($objOptIn = $objRegistry->fetch($t, $objResult->id))
			{
				$arrModels[] = $objOptIn;
			}
			else
			{
				$arrModels[] = new static($objResult->row());
			}
		}

		return static::createCollection($arrModels, $t);
	}

	/**
	 * Delete the related records if the model is deleted
	 *
	 * @return integer
	 */
	public function delete()
	{
		Database::getInstance()->prepare("DELETE FROM tl_opt_in_related WHERE pid=?")
								->execute($this->id);

		return parent::delete();
	}

	/**
	 * Returns the related records
	 *
	 * @return array
	 */
	public function getRelatedRecords()
	{
		$arrRelated = array();
		$objDatabase = Database::getInstance();

		$objRelated = $objDatabase->prepare("SELECT * FROM tl_opt_in_related WHERE pid=?")
								  ->execute($this->id);

		while ($objRelated->next())
		{
			$arrRelated[$objRelated->relTable][] = $objRelated->relId;
		}

		return $arrRelated;
	}

	/**
	 * Set the related records
	 *
	 * @param array $arrRelated
	 *
	 * @throws \LogicException
	 */
	public function setRelatedRecords(array $arrRelated)
	{
		$objDatabase = Database::getInstance();

		$objCount = $objDatabase->prepare("SELECT COUNT(*) AS count FROM tl_opt_in_related WHERE pid=?")
								->execute($this->id);

		if ($objCount->count > 0)
		{
			throw new \LogicException(sprintf('Token "%s" already contains related records', $this->token));
		}

		foreach ($arrRelated as $strTable=>$arrIds)
		{
			foreach ($arrIds as $intId)
			{
				$objDatabase->prepare("INSERT INTO tl_opt_in_related (pid, relTable, relId) VALUES (?, ?, ?)")
							->execute($this->id, $strTable, $intId);
			}
		}
	}
}


<?php

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

namespace Contao;

/**
 * Class FormTextArea
 *
 * @property string  $value
 * @property integer $maxlength
 * @property boolean $mandatory
 * @property string  $placeholder
 * @property string  $size
 * @property integer $rows
 * @property integer $cols
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class FormTextArea extends Widget
{

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

	/**
	 * Add a for attribute
	 *
	 * @var boolean
	 */
	protected $blnForAttribute = true;

	/**
	 * Rows
	 *
	 * @var integer
	 */
	protected $intRows = 12;

	/**
	 * Columns
	 *
	 * @var integer
	 */
	protected $intCols = 80;

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

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

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

			case 'mandatory':
				if ($varValue)
				{
					$this->arrAttributes['required'] = 'required';
				}
				else
				{
					unset($this->arrAttributes['required']);
				}
				parent::__set($strKey, $varValue);
				break;

			case 'placeholder':
				$this->arrAttributes['placeholder'] = $varValue;
				break;

			case 'size':
				$arrSize = StringUtil::deserialize($varValue);
				$this->intRows = $arrSize[0];
				$this->intCols = $arrSize[1];
				break;

			case 'rows':
				$this->intRows = $varValue;
				break;

			case 'cols':
				$this->intCols = $varValue;
				break;

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

	/**
	 * Return a parameter
	 *
	 * @param string $strKey The parameter key
	 *
	 * @return mixed The parameter value
	 */
	public function __get($strKey)
	{
		switch ($strKey)
		{
			case 'cols':
				return $this->intCols;
				break;

			case 'rows':
				return $this->intRows;
				break;

			case 'value':
				return StringUtil::specialchars(str_replace('\n', "\n", $this->varValue), false, true);
				break;

			default:
				return parent::__get($strKey);
				break;
		}
	}

	/**
	 * Generate the widget and return it as string
	 *
	 * @return string The widget markup
	 */
	public function generate()
	{
		return sprintf('<textarea name="%s" id="ctrl_%s" class="textarea%s" rows="%s" cols="%s"%s>%s</textarea>',
						$this->strName,
						$this->strId,
						(($this->strClass != '') ? ' ' . $this->strClass : ''),
						$this->intRows,
						$this->intCols,
						$this->getAttributes(),
						$this->value);
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Cache;

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\CacheClearer\CacheClearerInterface;

class ContaoCacheClearer implements CacheClearerInterface
{
    /**
     * @var Filesystem
     */
    private $filesystem;

    public function __construct(Filesystem $filesystem)
    {
        $this->filesystem = $filesystem;
    }

    /**
     * {@inheritdoc}
     */
    public function clear($cacheDir): void
    {
        $this->filesystem->remove($cacheDir.'/contao/config');
        $this->filesystem->remove($cacheDir.'/contao/dca');
        $this->filesystem->remove($cacheDir.'/contao/languages');
        $this->filesystem->remove($cacheDir.'/contao/sql');
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Command;

use Contao\Config;
use Contao\CoreBundle\Framework\ContaoFramework;
use Doctrine\DBAL\Connection;
use Patchwork\Utf8;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * Changes the password of a Contao back end user.
 */
class UserPasswordCommand extends Command
{
    /**
     * @var ContaoFramework
     */
    private $framework;

    /**
     * @var Connection
     */
    private $connection;

    public function __construct(ContaoFramework $framework, Connection $connection)
    {
        $this->framework = $framework;
        $this->connection = $connection;

        parent::__construct();
    }

    /**
     * {@inheritdoc}
     */
    protected function configure(): void
    {
        $this
            ->setName('contao:user:password')
            ->addArgument('username', InputArgument::REQUIRED, 'The username of the back end user')
            ->addOption(
                'password',
                'p',
                InputOption::VALUE_REQUIRED,
                'The new password (using this option is not recommended for security reasons)'
            )
            ->setDescription('Changes the password of a Contao back end user.')
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function interact(InputInterface $input, OutputInterface $output): void
    {
        if (null === $input->getArgument('username')) {
            throw new InvalidArgumentException('Please provide the username as argument.');
        }

        if (null !== $input->getOption('password')) {
            return;
        }

        $password = $this->askForPassword('Please enter the new password:', $input, $output);
        $confirm = $this->askForPassword('Please confirm the password:', $input, $output);

        if ($password !== $confirm) {
            throw new RuntimeException('The passwords do not match.');
        }

        $input->setOption('password', $password);
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        if (null === $input->getArgument('username') || null === $input->getOption('password')) {
            return 1;
        }

        $hash = $this->validateAndHashPassword($input->getOption('password'));

        $affected = $this->connection->update(
            'tl_user',
            ['password' => $hash],
            ['username' => $input->getArgument('username')]
        );

        if (0 === $affected) {
            throw new InvalidArgumentException(sprintf('Invalid username: %s', $input->getArgument('username')));
        }

        $io = new SymfonyStyle($input, $output);
        $io->success('The password has been changed successfully.');

        return 0;
    }

    /**
     * Asks a question with the given label and hides the input.
     */
    private function askForPassword(string $label, InputInterface $input, OutputInterface $output): string
    {
        $question = new Question($label);
        $question->setHidden(true);
        $question->setMaxAttempts(3);

        /** @var QuestionHelper $helper */
        $helper = $this->getHelper('question');

        return $helper->ask($input, $output, $question);
    }

    /**
     * @throws InvalidArgumentException
     */
    private function validateAndHashPassword(string $password): string
    {
        $this->framework->initialize();

        /** @var Config $config */
        $config = $this->framework->getAdapter(Config::class);
        $passwordLength = $config->get('minPasswordLength') ?: 8;

        if (Utf8::strlen($password) < $passwordLength) {
            throw new InvalidArgumentException(
                sprintf('The password must be at least %s characters long.', $passwordLength)
            );
        }

        return password_hash($password, PASSWORD_DEFAULT);
    }
}


<?php

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

namespace Contao;

use Contao\CoreBundle\Exception\ForwardPageNotFoundException;
use Contao\CoreBundle\Exception\PageNotFoundException;
use Symfony\Component\HttpFoundation\Response;

/**
 * Provide methods to handle an error 404 page.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class PageError404 extends Frontend
{

	/**
	 * Generate an error 404 page
	 */
	public function generate()
	{
		/** @var PageModel $objPage */
		global $objPage;

		$obj404 = $this->prepare();
		$objPage = $obj404->loadDetails();

		/** @var PageRegular $objHandler */
		$objHandler = new $GLOBALS['TL_PTY']['regular']();

		header('HTTP/1.1 404 Not Found');
		$objHandler->generate($objPage);
	}

	/**
	 * Return a response object
	 *
	 * @return Response
	 */
	public function getResponse()
	{
		/** @var PageModel $objPage */
		global $objPage;

		$obj404 = $this->prepare();
		$objPage = $obj404->loadDetails();

		/** @var PageRegular $objHandler */
		$objHandler = new $GLOBALS['TL_PTY']['regular']();

		return $objHandler->getResponse($objPage)->setStatusCode(404);
	}

	/**
	 * Prepare the output
	 *
	 * @return PageModel
	 *
	 * @internal Do not call this method in your code. It will be made private in Contao 5.0.
	 */
	protected function prepare()
	{
		// Check the search index (see #3761)
		Search::removeEntry(Environment::get('base') . Environment::get('relativeRequest'));

		// Find the matching root page
		$objRootPage = $this->getRootPageFromUrl();

		// Forward if the language should be but is not set (see #4028)
		if (Config::get('addLanguageToUrl'))
		{
			// Get the request string without the script name
			$strRequest = Environment::get('relativeRequest');

			// Only redirect if there is no language fragment (see #4669)
			if ($strRequest != '' && !preg_match('@^[a-z]{2}(-[A-Z]{2})?/@', $strRequest))
			{
				// Handle language fragments without trailing slash (see #7666)
				if (preg_match('@^[a-z]{2}(-[A-Z]{2})?$@', $strRequest))
				{
					$this->redirect(Environment::get('request') . '/', 301);
				}
				else
				{
					if ($strRequest == Environment::get('request'))
					{
						$strRequest = $objRootPage->language . '/' . $strRequest;
					}
					else
					{
						$strRequest = Environment::get('script') . '/' . $objRootPage->language . '/' . $strRequest;
					}

					$this->redirect($strRequest, 301);
				}
			}
		}

		// Look for a 404 page
		$obj404 = PageModel::find404ByPid($objRootPage->id);

		// Die if there is no page at all
		if (null === $obj404)
		{
			throw new PageNotFoundException('Page not found: ' . Environment::get('uri'));
		}

		// Forward to another page
		if ($obj404->autoforward && $obj404->jumpTo)
		{
			$objNextPage = PageModel::findPublishedById($obj404->jumpTo);

			if (null === $objNextPage)
			{
				$this->log('Forward page ID "' . $obj404->jumpTo . '" does not exist', __METHOD__, TL_ERROR);
				throw new ForwardPageNotFoundException('Forward page not found');
			}

			$this->redirect($objNextPage->getFrontendUrl(), (($obj404->redirect == 'temporary') ? 302 : 301));
		}

		return $obj404;
	}
}

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


<?php

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

namespace Contao;

use Patchwork\Utf8;

/**
 * Front end module "article list".
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ModuleArticlenav extends Module
{

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

	/**
	 * Articles
	 * @var ArticleModel[]
	 */
	protected $objArticles;

	/**
	 * Do not display the module if there are no articles
	 *
	 * @return string
	 */
	public function generate()
	{
		if (TL_MODE == 'BE')
		{
			$objTemplate = new BackendTemplate('be_wildcard');
			$objTemplate->wildcard = '### ' . Utf8::strtoupper($GLOBALS['TL_LANG']['FMD']['articlenav'][0]) . ' ###';
			$objTemplate->title = $this->headline;
			$objTemplate->id = $this->id;
			$objTemplate->link = $this->name;
			$objTemplate->href = 'contao/main.php?do=themes&table=tl_module&act=edit&id=' . $this->id;

			return $objTemplate->parse();
		}

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

		$this->objArticles = ArticleModel::findPublishedWithTeaserByPidAndColumn($objPage->id, $this->strColumn);

		// Return if there are no articles
		if ($this->objArticles === null)
		{
			return '';
		}

		// Redirect to the first article if no article is selected
		if (!Input::get('articles'))
		{
			if (!$this->loadFirst)
			{
				return '';
			}

			/** @var ArticleModel $objArticle */
			$objArticle = $this->objArticles->current();
			$strAlias = $objArticle->alias ?: $objArticle->id;

			$this->redirect($objPage->getFrontendUrl('/articles/' . $strAlias));
		}

		return parent::generate();
	}

	/**
	 * Generate the module
	 */
	protected function compile()
	{
		/** @var PageModel $objPage */
		global $objPage;

		$intActive = null;
		$articles = array();
		$intCount = 1;

		foreach ($this->objArticles as $objArticle)
		{
			$strAlias = $objArticle->alias ?: $objArticle->id;

			// Active article
			if (Input::get('articles') == $strAlias)
			{
				$articles[] = array
				(
					'isActive' => true,
					'href' => $objPage->getFrontendUrl('/articles/' . $strAlias),
					'title' => StringUtil::specialchars($objArticle->title, true),
					'link' => $intCount
				);

				$intActive = ($intCount - 1);
			}

			// Inactive article
			else
			{
				$articles[] = array
				(
					'isActive' => false,
					'href' => $objPage->getFrontendUrl('/articles/' . $strAlias),
					'title' => StringUtil::specialchars($objArticle->title, true),
					'link' => $intCount
				);
			}

			++$intCount;
		}

		$this->Template->articles = $articles;
		$total = \count($articles);

		// Link to first element
		if ($intActive > 1)
		{
			$this->Template->first = array
			(
				'href' => $articles[0]['href'],
				'title' => $articles[0]['title'],
				'link' => $GLOBALS['TL_LANG']['MSC']['first']
			);
		}

		$key = $intActive - 1;

		// Link to previous element
		if ($intCount > 1 && $key >= 0)
		{
			$this->Template->previous = array
			(
				'href' => $articles[$key]['href'],
				'title' => $articles[$key]['title'],
				'link' => $GLOBALS['TL_LANG']['MSC']['previous']
			);
		}

		$key = $intActive + 1;

		// Link to next element
		if ($intCount > 1 && $key < $total)
		{
			$this->Template->next = array
			(
				'href' => $articles[$key]['href'],
				'title' => $articles[$key]['title'],
				'link' => $GLOBALS['TL_LANG']['MSC']['next']
			);
		}

		$key = $total - 1;

		// Link to last element
		if ($intCount > 1 && $intActive < ($key - 1))
		{
			$this->Template->last = array
			(
				'href' => $articles[$key]['href'],
				'title' => $articles[$key]['title'],
				'link' => $GLOBALS['TL_LANG']['MSC']['last']
			);
		}
	}
}

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


<?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\StringUtil;

class Version330Update extends AbstractVersionUpdate
{
    /**
     * {@inheritdoc}
     */
    public function shouldBeRun(): bool
    {
        $schemaManager = $this->connection->getSchemaManager();

        if (!$schemaManager->tablesExist(['tl_layout'])) {
            return false;
        }

        $columns = $schemaManager->listTableColumns('tl_layout');

        return !isset($columns['viewport']);
    }

    /**
     * {@inheritdoc}
     */
    public function run(): void
    {
        $statement = $this->connection->query("
            SELECT
                id, framework
            FROM
                tl_layout
            WHERE
                framework != ''
        ");

        while (false !== ($layout = $statement->fetch(\PDO::FETCH_OBJ))) {
            $framework = '';
            $tmp = StringUtil::deserialize($layout->framework);

            if (!empty($tmp) && \is_array($tmp)) {
                if (false !== ($key = array_search('layout.css', $tmp, true))) {
                    array_insert($tmp, $key + 1, 'responsive.css');
                }

                $framework = serialize(array_values(array_unique($tmp)));
            }

            $stmt = $this->connection->prepare('
                UPDATE
                    tl_layout
                SET
                    framework = :framework
                WHERE
                    id = :id
            ');

            $stmt->execute([':framework' => $framework, ':id' => $layout->id]);
        }

        // Add the "viewport" field (triggers the version 3.3 update)
        $this->connection->query("
            ALTER TABLE
                tl_layout
            ADD
                viewport varchar(255) NOT NULL default ''
        ");
    }
}


<?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\BackendUser;
use Contao\CoreBundle\Event\MenuEvent;
use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

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

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

    /**
     * Adds the back end user navigation.
     */
    public function onBuild(MenuEvent $event): void
    {
        $token = $this->tokenStorage->getToken();

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

        $user = $token->getUser();

        if (!$user instanceof BackendUser) {
            return;
        }

        $factory = $event->getFactory();
        $tree = $event->getTree();
        $modules = $user->navigation();

        foreach ($modules as $categoryName => $categoryData) {
            $categoryNode = $tree->getChild($categoryName);

            if (!$categoryNode) {
                $categoryNode = $this->createNode($factory, $categoryName, $categoryData);

                if (isset($categoryData['class']) && preg_match('/\bnode-collapsed\b/', $categoryData['class'])) {
                    $categoryNode->setDisplayChildren(false);
                }

                $tree->addChild($categoryNode);
            }

            // Create the child nodes
            foreach ($categoryData['modules'] as $moduleName => $moduleData) {
                $moduleNode = $this->createNode($factory, $moduleName, $moduleData);
                $moduleNode->setCurrent((bool) $moduleData['isActive']);

                $categoryNode->addChild($moduleNode);
            }
        }
    }

    private function createNode(FactoryInterface $factory, string $name, array $attributes): ItemInterface
    {
        $classes = [];

        // Remove the default CSS classes and keep potentially existing custom ones (see #1357)
        if (isset($attributes['class'])) {
            $classes = array_flip(array_filter(explode(' ', $attributes['class'])));
            unset($classes['node-expanded'], $classes['node-collapsed'], $classes['trail']);
        }

        return $factory->createItem(
            $name,
            [
                'label' => $attributes['label'],
                'attributes' => [
                    'title' => $attributes['title'],
                    'href' => $attributes['href'],
                    'class' => implode(' ', array_keys($classes)),
                ],
            ]
        );
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Asset;

use Contao\PageModel;
use Symfony\Component\Asset\Context\ContextInterface;
use Symfony\Component\HttpFoundation\RequestStack;

class ContaoContext implements ContextInterface
{
    /**
     * @var RequestStack
     */
    private $requestStack;

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

    /**
     * @var bool
     */
    private $debug;

    public function __construct(RequestStack $requestStack, string $field, bool $debug = false)
    {
        $this->requestStack = $requestStack;
        $this->field = $field;
        $this->debug = $debug;
    }

    /**
     * {@inheritdoc}
     */
    public function getBasePath(): string
    {
        if ($this->debug) {
            return '';
        }

        $request = $this->requestStack->getCurrentRequest();

        if (null === $request || '' === ($staticUrl = $this->getFieldValue($this->getPageModel()))) {
            return '';
        }

        $protocol = $this->isSecure() ? 'https' : 'http';
        $relative = preg_replace('@https?://@', '', $staticUrl);

        return sprintf('%s://%s%s', $protocol, $relative, $request->getBasePath());
    }

    /**
     * {@inheritdoc}
     */
    public function isSecure(): bool
    {
        $page = $this->getPageModel();

        if (null !== $page) {
            return (bool) $page->loadDetails()->rootUseSSL;
        }

        $request = $this->requestStack->getCurrentRequest();

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

        return $request->isSecure();
    }

    /**
     * Returns the base path with a trailing slash if not empty.
     */
    public function getStaticUrl(): string
    {
        if ($path = $this->getBasePath()) {
            return $path.'/';
        }

        return '';
    }

    private function getPageModel(): ?PageModel
    {
        if (isset($GLOBALS['objPage']) && $GLOBALS['objPage'] instanceof PageModel) {
            return $GLOBALS['objPage'];
        }

        return null;
    }

    /**
     * Returns a field value from the page model.
     */
    private function getFieldValue(?PageModel $page): string
    {
        if (null === $page) {
            return '';
        }

        return (string) $page->{$this->field};
    }
}


<?php

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

Contao\System::loadLanguageFile('tl_user');

$GLOBALS['TL_DCA']['tl_user_group'] = array
(

	// Config
	'config' => array
	(
		'dataContainer'               => 'Table',
		'enableVersioning'            => true,
		'onload_callback' => array
		(
			array('tl_user_group', 'addTemplateWarning')
		),
		'sql' => array
		(
			'keys' => array
			(
				'id' => 'primary'
			)
		)
	),

	// List
	'list' => array
	(
		'sorting' => array
		(
			'mode'                    => 1,
			'fields'                  => array('name'),
			'flag'                    => 1,
			'panelLayout'             => 'filter;search,limit',
		),
		'label' => array
		(
			'fields'                  => array('name'),
			'format'                  => '%s',
			'label_callback'          => array('tl_user_group', 'addIcon')
		),
		'global_operations' => array
		(
			'all' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['MSC']['all'],
				'href'                => 'act=select',
				'class'               => 'header_edit_all',
				'attributes'          => 'onclick="Backend.getScrollOffset()" accesskey="e"'
			)
		),
		'operations' => array
		(
			'edit' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_user_group']['edit'],
				'href'                => 'act=edit',
				'icon'                => 'edit.svg'
			),
			'copy' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_user_group']['copy'],
				'href'                => 'act=copy',
				'icon'                => 'copy.svg'
			),
			'delete' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_user_group']['delete'],
				'href'                => 'act=delete',
				'icon'                => 'delete.svg',
				'attributes'          => 'onclick="if(!confirm(\'' . $GLOBALS['TL_LANG']['MSC']['deleteConfirm'] . '\'))return false;Backend.getScrollOffset()"'
			),
			'toggle' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_user_group']['toggle'],
				'icon'                => 'visible.svg',
				'attributes'          => 'onclick="Backend.getScrollOffset();return AjaxRequest.toggleVisibility(this,%s)"',
				'button_callback'     => array('tl_user_group', 'toggleIcon')
			),
			'show' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_user_group']['show'],
				'href'                => 'act=show',
				'icon'                => 'show.svg'
			)
		)
	),

	// Palettes
	'palettes' => array
	(
		'default'                     => '{title_legend},name;{modules_legend},modules,themes;{pagemounts_legend},pagemounts,alpty;{filemounts_legend},filemounts,fop;{imageSizes_legend},imageSizes;{forms_legend},forms,formp;{amg_legend},amg;{alexf_legend:hide},alexf;{account_legend},disable,start,stop',
	),

	// Fields
	'fields' => array
	(
		'id' => array
		(
			'sql'                     => "int(10) unsigned NOT NULL auto_increment"
		),
		'tstamp' => array
		(
			'sql'                     => "int(10) unsigned NOT NULL default 0"
		),
		'name' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user_group']['name'],
			'exclude'                 => true,
			'search'                  => true,
			'inputType'               => 'text',
			'eval'                    => array('mandatory'=>true, 'unique'=>true, 'maxlength'=>255, 'tl_class'=>'w50'),
			'sql'                     => "varchar(255) NOT NULL default ''"
		),
		'modules' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user']['modules'],
			'exclude'                 => true,
			'filter'                  => true,
			'inputType'               => 'checkbox',
			'options_callback'        => array('tl_user_group', 'getModules'),
			'reference'               => &$GLOBALS['TL_LANG']['MOD'],
			'eval'                    => array('multiple'=>true, 'helpwizard'=>true),
			'sql'                     => "blob NULL"
		),
		'themes' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user']['themes'],
			'exclude'                 => true,
			'inputType'               => 'checkbox',
			'options'                 => array('css', 'modules', 'layout', 'image_sizes', 'theme_import', 'theme_export'),
			'reference'               => &$GLOBALS['TL_LANG']['MOD'],
			'eval'                    => array('multiple'=>true),
			'sql'                     => "blob NULL"
		),
		'pagemounts' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user']['pagemounts'],
			'exclude'                 => true,
			'inputType'               => 'pageTree',
			'eval'                    => array('multiple'=>true, 'fieldType'=>'checkbox'),
			'sql'                     => "blob NULL"
		),
		'alpty' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user']['alpty'],
			'default'                 => array('regular', 'redirect', 'forward'),
			'exclude'                 => true,
			'inputType'               => 'checkbox',
			'options'                 => array_keys($GLOBALS['TL_PTY']),
			'reference'               => &$GLOBALS['TL_LANG']['PTY'],
			'eval'                    => array('multiple'=>true, 'helpwizard'=>true),
			'sql'                     => "blob NULL"
		),
		'filemounts' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user']['filemounts'],
			'exclude'                 => true,
			'inputType'               => 'fileTree',
			'eval'                    => array('multiple'=>true, 'fieldType'=>'checkbox'),
			'sql'                     => "blob NULL"
		),
		'fop' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['FOP']['fop'],
			'exclude'                 => true,
			'default'                 => array('f1', 'f2', 'f3'),
			'inputType'               => 'checkbox',
			'options'                 => array('f1', 'f2', 'f3', 'f4', 'f5', 'f6'),
			'reference'               => &$GLOBALS['TL_LANG']['FOP'],
			'eval'                    => array('multiple'=>true),
			'sql'                     => "blob NULL"
		),
		'imageSizes' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user']['imageSizes'],
			'exclude'                 => true,
			'inputType'               => 'checkbox',
			'reference'               => &$GLOBALS['TL_LANG']['MSC'],
			'eval'                    => array('multiple'=>true),
			'options_callback' => function ()
			{
				return Contao\System::getContainer()->get('contao.image.image_sizes')->getAllOptions();
			},
			'sql'                     => "blob NULL"
		),
		'forms' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user']['forms'],
			'exclude'                 => true,
			'inputType'               => 'checkbox',
			'foreignKey'              => 'tl_form.title',
			'eval'                    => array('multiple'=>true),
			'sql'                     => "blob NULL"
		),
		'formp' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user']['formp'],
			'exclude'                 => true,
			'inputType'               => 'checkbox',
			'options'                 => array('create', 'delete'),
			'reference'               => &$GLOBALS['TL_LANG']['MSC'],
			'eval'                    => array('multiple'=>true),
			'sql'                     => "blob NULL"
		),
		'amg' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user']['amg'],
			'exclude'                 => true,
			'inputType'               => 'checkbox',
			'foreignKey'              => 'tl_member_group.name',
			'eval'                    => array('multiple'=>true),
			'sql'                     => "blob NULL"
		),
		'alexf' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user_group']['alexf'],
			'exclude'                 => true,
			'inputType'               => 'checkbox',
			'options_callback'        => array('tl_user_group', 'getExcludedFields'),
			'eval'                    => array('multiple'=>true, 'size'=>36),
			'sql'                     => "blob NULL"
		),
		'disable' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user_group']['disable'],
			'exclude'                 => true,
			'filter'                  => true,
			'inputType'               => 'checkbox',
			'sql'                     => "char(1) NOT NULL default ''"
		),
		'start' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user_group']['start'],
			'exclude'                 => true,
			'inputType'               => 'text',
			'eval'                    => array('rgxp'=>'datim', 'datepicker'=>true, 'tl_class'=>'w50 wizard'),
			'sql'                     => "varchar(10) NOT NULL default ''"
		),
		'stop' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_user_group']['stop'],
			'exclude'                 => true,
			'inputType'               => 'text',
			'eval'                    => array('rgxp'=>'datim', 'datepicker'=>true, 'tl_class'=>'w50 wizard'),
			'sql'                     => "varchar(10) NOT NULL default ''"
		)
	)
);

/**
 * Provide miscellaneous methods that are used by the data configuration array.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class tl_user_group extends Contao\Backend
{

	/**
	 * Import the back end user object
	 */
	public function __construct()
	{
		parent::__construct();
		$this->import('Contao\BackendUser', 'User');
	}

	/**
	 * Add a warning if there are users with access to the template editor.
	 */
	public function addTemplateWarning()
	{
		if (Contao\Input::get('act') && Contao\Input::get('act') != 'select')
		{
			return;
		}

		$objResult = $this->Database->query("SELECT COUNT(*) AS cnt FROM tl_user_group WHERE modules LIKE '%\"tpl_editor\"%'");

		if ($objResult->cnt > 0)
		{
			Contao\Message::addInfo($GLOBALS['TL_LANG']['MSC']['groupTemplateEditor']);
		}
	}

	/**
	 * Add an image to each record
	 *
	 * @param array  $row
	 * @param string $label
	 *
	 * @return string
	 */
	public function addIcon($row, $label)
	{
		$image = 'group';
		$time = Contao\Date::floorToMinute();

		$disabled = ($row['start'] !== '' && $row['start'] > $time) || ($row['stop'] !== '' && $row['stop'] < $time);

		if ($row['disable'] || $disabled)
		{
			$image .= '_';
		}

		return sprintf('
%s
', Contao\System::getContainer()->get('contao.assets.assets_context')->getStaticUrl(), Contao\Backend::getTheme(), $image, $disabled ? $image : rtrim($image, '_'), rtrim($image, '_') . '_', $label);
	}

	/**
	 * Return all modules except profile modules
	 *
	 * @return array
	 */
	public function getModules()
	{
		$arrModules = array();

		foreach ($GLOBALS['BE_MOD'] as $k=>$v)
		{
			if (!empty($v))
			{
				if ($k == 'accounts')
				{
					unset($v['login']);
				}

				if ($k == 'system')
				{
					unset($v['undo']);
				}

				$arrModules[$k] = array_keys($v);
			}
		}

		return $arrModules;
	}

	/**
	 * Return all excluded fields as HTML drop down menu
	 *
	 * @return array
	 */
	public function getExcludedFields()
	{
		$processed = array();

		/** @var SplFileInfo[] $files */
		$files = Contao\System::getContainer()->get('contao.resource_finder')->findIn('dca')->depth(0)->files()->name('*.php');

		foreach ($files as $file)
		{
			if (\in_array($file->getBasename(), $processed))
			{
				continue;
			}

			$processed[] = $file->getBasename();

			$strTable = $file->getBasename('.php');

			Contao\System::loadLanguageFile($strTable);
			$this->loadDataContainer($strTable);
		}

		$arrReturn = array();

		// Get all excluded fields
		foreach ($GLOBALS['TL_DCA'] as $k=>$v)
		{
			if (\is_array($v['fields']))
			{
				foreach ($v['fields'] as $kk=>$vv)
				{
					// Hide the "admin" field if the user is not an admin (see #184)
					if ($k == 'tl_user' && $kk == 'admin' && !$this->User->isAdmin)
					{
						continue;
					}

					if ($vv['exclude'] || $vv['orig_exclude'])
					{
						$arrReturn[$k][Contao\StringUtil::specialchars($k.'::'.$kk)] = isset($vv['label'][0]) ? $vv['label'][0] . ' [' . $kk . ']' : $kk;
					}
				}
			}
		}

		ksort($arrReturn);

		return $arrReturn;
	}

	/**
	 * Return the "toggle visibility" button
	 *
	 * @param array  $row
	 * @param string $href
	 * @param string $label
	 * @param string $title
	 * @param string $icon
	 * @param string $attributes
	 *
	 * @return string
	 */
	public function toggleIcon($row, $href, $label, $title, $icon, $attributes)
	{
		if (\strlen(Contao\Input::get('tid')))
		{
			$this->toggleVisibility(Contao\Input::get('tid'), (Contao\Input::get('state') == 1), (@func_get_arg(12) ?: null));
			$this->redirect($this->getReferer());
		}

		// Check permissions AFTER checking the tid, so hacking attempts are logged
		if (!$this->User->hasAccess('tl_user_group::disable', 'alexf'))
		{
			return '';
		}

		$href .= '&tid='.$row['id'].'&state='.$row['disable'];

		if ($row['disable'])
		{
			$icon = 'invisible.svg';
		}

		return 'addToUrl($href).'" title="'.Contao\StringUtil::specialchars($title).'"'.$attributes.'>'.Contao\Image::getHtml($icon, $label, 'data-state="' . ($row['disable'] ? 0 : 1) . '"').' ';
	}

	/**
	 * Disable/enable a user group
	 *
	 * @param integer              $intId
	 * @param boolean              $blnVisible
	 * @param Contao\DataContainer $dc
	 *
	 * @throws Contao\CoreBundle\Exception\AccessDeniedException
	 */
	public function toggleVisibility($intId, $blnVisible, Contao\DataContainer $dc=null)
	{
		// Set the ID and action
		Contao\Input::setGet('id', $intId);
		Contao\Input::setGet('act', 'toggle');

		if ($dc)
		{
			$dc->id = $intId; // see #8043
		}

		// Trigger the onload_callback
		if (\is_array($GLOBALS['TL_DCA']['tl_user_group']['config']['onload_callback']))
		{
			foreach ($GLOBALS['TL_DCA']['tl_user_group']['config']['onload_callback'] as $callback)
			{
				if (\is_array($callback))
				{
					$this->import($callback[0]);
					$this->{$callback[0]}->{$callback[1]}($dc);
				}
				elseif (\is_callable($callback))
				{
					$callback($dc);
				}
			}
		}

		// Check the field access permissions
		if (!$this->User->hasAccess('tl_user_group::disable', 'alexf'))
		{
			throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to activate/deactivate user group ID ' . $intId . '.');
		}

		// Set the current record
		if ($dc)
		{
			$objRow = $this->Database->prepare("SELECT * FROM tl_user_group WHERE id=?")
									 ->limit(1)
									 ->execute($intId);

			if ($objRow->numRows)
			{
				$dc->activeRecord = $objRow;
			}
		}

		$objVersions = new Contao\Versions('tl_user_group', $intId);
		$objVersions->initialize();

		// Reverse the logic (user groups have disable=1)
		$blnVisible = !$blnVisible;

		// Trigger the save_callback
		if (\is_array($GLOBALS['TL_DCA']['tl_user_group']['fields']['disable']['save_callback']))
		{
			foreach ($GLOBALS['TL_DCA']['tl_user_group']['fields']['disable']['save_callback'] as $callback)
			{
				if (\is_array($callback))
				{
					$this->import($callback[0]);
					$blnVisible = $this->{$callback[0]}->{$callback[1]}($blnVisible, $dc);
				}
				elseif (\is_callable($callback))
				{
					$blnVisible = $callback($blnVisible, $dc);
				}
			}
		}

		// Trigger the onsubmit_callback
		if (\is_array($GLOBALS['TL_DCA']['tl_user_group']['config']['onsubmit_callback']))
		{
			foreach ($GLOBALS['TL_DCA']['tl_user_group']['config']['onsubmit_callback'] as $callback)
			{
				if (\is_array($callback))
				{
					$this->import($callback[0]);
					$this->{$callback[0]}->{$callback[1]}($dc);
				}
				elseif (\is_callable($callback))
				{
					$callback($dc);
				}
			}
		}

		$time = time();

		// Update the database
		$this->Database->prepare("UPDATE tl_user_group SET tstamp=$time, disable='" . ($blnVisible ? '1' : '') . "' WHERE id=?")
					   ->execute($intId);

		if ($dc)
		{
			$dc->activeRecord->tstamp = $time;
			$dc->activeRecord->disable = ($blnVisible ? '1' : '');
		}

		$objVersions->create();
	}
}


<?php

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

namespace Contao;

/**
 * Loads a set of DCA files
 *
 * The class loads the DCA files of a certain table and stores a combined
 * version in the cache directory.
 *
 * Usage:
 *
 *     $dca = new DcaLoader('tl_user');
 *     $dca->load();
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class DcaLoader extends Controller
{

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

	/**
	 * Store the table name
	 *
	 * @param string $strTable The table name
	 *
	 * @throws \Exception If $strTable is empty
	 */
	public function __construct($strTable)
	{
		if ($strTable == '')
		{
			throw new \Exception('The table name must not be empty');
		}

		if (Validator::isInsecurePath($strTable))
		{
			throw new \InvalidArgumentException('The table name contains invalid characters');
		}

		parent::__construct();

		$this->strTable = $strTable;
	}

	/**
	 * Load a set of DCA files
	 *
	 * @param boolean $blnNoCache If true, the cache will be bypassed
	 */
	public function load($blnNoCache=false)
	{
		// Return if the data has been loaded already
		if (isset($GLOBALS['loadDataContainer'][$this->strTable]) && !$blnNoCache)
		{
			return;
		}

		$GLOBALS['loadDataContainer'][$this->strTable] = true; // see #6145

		$strCacheDir = System::getContainer()->getParameter('kernel.cache_dir');

		// Try to load from cache
		if (file_exists($strCacheDir . '/contao/dca/' . $this->strTable . '.php'))
		{
			include $strCacheDir . '/contao/dca/' . $this->strTable . '.php';
		}
		else
		{
			try
			{
				$files = System::getContainer()->get('contao.resource_locator')->locate('dca/' . $this->strTable . '.php', null, false);
			}
			catch (\InvalidArgumentException $e)
			{
				$files = array();
			}

			foreach ($files as $file)
			{
				include $file;
			}
		}

		// HOOK: allow to load custom settings
		if (isset($GLOBALS['TL_HOOKS']['loadDataContainer']) && \is_array($GLOBALS['TL_HOOKS']['loadDataContainer']))
		{
			foreach ($GLOBALS['TL_HOOKS']['loadDataContainer'] as $callback)
			{
				$this->import($callback[0]);
				$this->{$callback[0]}->{$callback[1]}($this->strTable);
			}
		}

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

		// Local configuration file
		if (file_exists($rootDir . '/system/config/dcaconfig.php'))
		{
			@trigger_error('Using the dcaconfig.php file has been deprecated and will no longer work in Contao 5.0. Create one or more DCA files in app/Resources/contao/dca instead.', E_USER_DEPRECATED);
			include $rootDir . '/system/config/dcaconfig.php';
		}
	}
}

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