<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Security\Logout;

use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Monolog\ContaoContext;
use Contao\System;
use Contao\User;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;

class LogoutHandler implements LogoutHandlerInterface
{
    /**
     * @var ContaoFramework
     */
    private $framework;

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

    public function __construct(ContaoFramework $framework, LoggerInterface $logger = null)
    {
        $this->framework = $framework;
        $this->logger = $logger;
    }

    /**
     * {@inheritdoc}
     */
    public function logout(Request $request, Response $response, TokenInterface $token): void
    {
        $user = $token->getUser();

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

        if (null !== $this->logger) {
            $this->logger->info(
                sprintf('User "%s" has logged out', $user->username),
                ['contao' => new ContaoContext(__METHOD__, ContaoContext::ACCESS, $user->username)]
            );
        }

        $this->triggerPostLogoutHook($user);
    }

    private function triggerPostLogoutHook(User $user): void
    {
        $this->framework->initialize();

        if (empty($GLOBALS['TL_HOOKS']['postLogout']) || !\is_array($GLOBALS['TL_HOOKS']['postLogout'])) {
            return;
        }

        @trigger_error('Using the "postLogout" hook has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

        /** @var System $system */
        $system = $this->framework->getAdapter(System::class);

        $GLOBALS['TL_USERNAME'] = $user->getUsername();

        foreach ($GLOBALS['TL_HOOKS']['postLogout'] as $callback) {
            $system->importStatic($callback[0])->{$callback[1]}($user);
        }
    }
}


<?php

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

namespace Contao;

/**
 * Provide methods to handle form field options.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class OptionWizard extends Widget
{

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

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

	/**
	 * Validate the input and set the value
	 */
	public function validate()
	{
		$mandatory = $this->mandatory;
		$options = $this->getPost($this->strName);

		// Check labels only (values can be empty)
		if (\is_array($options))
		{
			foreach ($options as $key=>$option)
			{
				// Unset empty rows
				if ($option['label'] == '')
				{
					unset($options[$key]);
					continue;
				}

				$options[$key]['label'] = trim($option['label']);
				$options[$key]['value'] = trim($option['value']);

				if ($options[$key]['label'] != '')
				{
					$this->mandatory = false;
				}

				// Strip double quotes (see #6919)
				if ($options[$key]['value'] != '')
				{
					$options[$key]['value'] = str_replace('"', '', $options[$key]['value']);
				}
			}
		}

		$options = array_values($options);
		$varInput = $this->validator($options);

		if (!$this->hasErrors())
		{
			$this->varValue = $varInput;
		}

		// Reset the property
		if ($mandatory)
		{
			$this->mandatory = true;
		}
	}

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

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

		// Begin the table
		$return = '
strId.'" class="tl_optionwizard">

      '.$GLOBALS['TL_LANG']['MSC']['ow_value'].'
      '.$GLOBALS['TL_LANG']['MSC']['ow_label'].'

';

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

      <input type="text" name="'.$this->strId.'['.$i.'][value]" id="'.$this->strId.'_value_'.$i.'" class="tl_text" value="'.StringUtil::specialchars($this->varValue[$i]['value']).'">
      <input type="text" name="'.$this->strId.'['.$i.'][label]" id="'.$this->strId.'_label_'.$i.'" class="tl_text" value="'.StringUtil::specialchars($this->varValue[$i]['label']).'">
      <input type="checkbox" name="'.$this->strId.'['.$i.'][default]" id="'.$this->strId.'_default_'.$i.'" class="fw_checkbox" value="1"'.($this->varValue[$i]['default'] ? ' checked="checked"' : '').'> strId.'_default_'.$i.'">'.$GLOBALS['TL_LANG']['MSC']['ow_default'].'
      <input type="checkbox" name="'.$this->strId.'['.$i.'][group]" id="'.$this->strId.'_group_'.$i.'" class="fw_checkbox" value="1"'.($this->varValue[$i]['group'] ? ' checked="checked"' : '').'> strId.'_group_'.$i.'">'.$GLOBALS['TL_LANG']['MSC']['ow_group'].'	';

			// Add row buttons
			$return .= '
      ';

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

			$return .= '

';
		}

		return $return.'

  ';
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Image;

use Contao\BackendUser;
use Contao\CoreBundle\Event\ContaoCoreEvents;
use Contao\CoreBundle\Event\ImageSizesEvent;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\StringUtil;
use Doctrine\DBAL\Connection;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

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

    /**
     * @var EventDispatcherInterface
     */
    private $eventDispatcher;

    /**
     * @var ContaoFramework
     */
    private $framework;

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

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

    /**
     * Returns the image sizes as options suitable for widgets.
     *
     * @return array<string,string[]>
     */
    public function getAllOptions(): array
    {
        $this->loadOptions();

        $event = new ImageSizesEvent($this->options);

        $this->eventDispatcher->dispatch(ContaoCoreEvents::IMAGE_SIZES_ALL, $event);

        return $event->getImageSizes();
    }

    /**
     * Returns the image sizes for the given user suitable for widgets.
     *
     * @return array<string,string[]>
     */
    public function getOptionsForUser(BackendUser $user): array
    {
        $this->loadOptions();

        if ($user->isAdmin) {
            $event = new ImageSizesEvent($this->options, $user);
        } else {
            $options = array_map(
                static function ($val) {
                    return is_numeric($val) ? (int) $val : $val;
                },
                StringUtil::deserialize($user->imageSizes, true)
            );

            $event = new ImageSizesEvent($this->filterOptions($options), $user);
        }

        $this->eventDispatcher->dispatch(ContaoCoreEvents::IMAGE_SIZES_USER, $event);

        return $event->getImageSizes();
    }

    /**
     * Loads the options from the database.
     */
    private function loadOptions(): void
    {
        if (null !== $this->options) {
            return;
        }

        // The framework is required to have the TL_CROP options available
        $this->framework->initialize();

        $this->options = $GLOBALS['TL_CROP'];

        $rows = $this->connection->fetchAll(
            'SELECT id, name, width, height FROM tl_image_size ORDER BY pid, name'
        );

        foreach ($rows as $imageSize) {
            $this->options['image_sizes'][$imageSize['id']] = sprintf(
                '%s (%sx%s)',
                $imageSize['name'],
                $imageSize['width'],
                $imageSize['height']
            );
        }
    }

    /**
     * Filters the options by the given allowed sizes and returns the result.
     *
     * @return array<string,string[]>
     */
    private function filterOptions(array $allowedSizes): array
    {
        if (empty($allowedSizes)) {
            return [];
        }

        $filteredSizes = [];

        foreach ($this->options as $group => $sizes) {
            if ('image_sizes' === $group) {
                $this->filterImageSizes($sizes, $allowedSizes, $filteredSizes, $group);
            } else {
                $this->filterResizeModes($sizes, $allowedSizes, $filteredSizes, $group);
            }
        }

        return $filteredSizes;
    }

    private function filterImageSizes(array $sizes, array $allowedSizes, array &$filteredSizes, string $group): void
    {
        foreach ($sizes as $key => $size) {
            if (\in_array($key, $allowedSizes, true)) {
                $filteredSizes[$group][$key] = $size;
            }
        }
    }

    private function filterResizeModes(array $sizes, array $allowedSizes, array &$filteredSizes, string $group): void
    {
        foreach ($sizes as $size) {
            if (\in_array($size, $allowedSizes, true)) {
                $filteredSizes[$group][] = $size;
            }
        }
    }
}


<?php

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

namespace Contao;

use Contao\CoreBundle\Exception\NoRootPageFoundException;
use Contao\CoreBundle\Monolog\ContaoContext;
use Psr\Log\LogLevel;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;

/**
 * Provide methods to manage front end controllers.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
abstract class Frontend extends Controller
{

	/**
	 * Meta array
	 * @var array
	 */
	protected $arrMeta = array();

	/**
	 * Aux array
	 * @var array
	 */
	protected $arrAux = array();

	/**
	 * Processed files array
	 * @var array
	 */
	protected $arrProcessed = array();

	/**
	 * Load the database object
	 *
	 * Make the constructor public, so pages can be instantiated (see #6182)
	 */
	public function __construct()
	{
		parent::__construct();
		$this->import(Database::class, 'Database');
	}

	/**
	 * Split the current request into fragments, strip the URL suffix, recreate the $_GET array and return the page ID
	 *
	 * @return mixed
	 *
	 * @deprecated Deprecated since Contao 4.7, to be removed in Contao 5.0.
	 *             Use the Symfony routing instead.
	 */
	public static function getPageIdFromUrl()
	{
		@trigger_error('Using Frontend::getPageIdFromUrl() has been deprecated and will no longer work in Contao 5.0. Use the Symfony routing instead.', E_USER_DEPRECATED);

		$strRequest = Environment::get('relativeRequest');

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

		// Get the request without the query string
		list($strRequest) = explode('?', $strRequest, 2);

		// URL decode here (see #6232)
		$strRequest = rawurldecode($strRequest);

		// The request string must not contain "auto_item" (see #4012)
		if (strpos($strRequest, '/auto_item/') !== false)
		{
			return false;
		}

		// Extract the language
		if (Config::get('addLanguageToUrl'))
		{
			$arrMatches = array();

			// Use the matches instead of substr() (thanks to Mario Müller)
			if (preg_match('@^([a-z]{2}(-[A-Z]{2})?)/(.*)$@', $strRequest, $arrMatches))
			{
				Input::setGet('language', $arrMatches[1]);

				// Trigger the root page if only the language was given
				if ($arrMatches[3] == '')
				{
					return null;
				}

				$strRequest = $arrMatches[3];
			}
			else
			{
				return false; // Language not provided
			}
		}

		// Remove the URL suffix if not just a language root (e.g. en/) is requested
		if ($strRequest != '' && (!Config::get('addLanguageToUrl') || !preg_match('@^[a-z]{2}(-[A-Z]{2})?/$@', $strRequest)))
		{
			$intSuffixLength = \strlen(Config::get('urlSuffix'));

			// Return false if the URL suffix does not match (see #2864)
			if ($intSuffixLength > 0)
			{
				if (substr($strRequest, -$intSuffixLength) != Config::get('urlSuffix'))
				{
					return false;
				}

				$strRequest = substr($strRequest, 0, -$intSuffixLength);
			}
		}

		$arrFragments = null;

		// Use folder-style URLs
		if (Config::get('folderUrl') && strpos($strRequest, '/') !== false)
		{
			$strAlias = $strRequest;
			$arrOptions = array($strAlias);

			// Compile all possible aliases by applying dirname() to the request (e.g. news/archive/item, news/archive, news)
			while ($strAlias != '/' && strpos($strAlias, '/') !== false)
			{
				$strAlias = \dirname($strAlias);
				$arrOptions[] = $strAlias;
			}

			/** @var PageModel $objPageModel */
			$objPageModel = System::getContainer()->get('contao.framework')->getAdapter(PageModel::class);

			// Check if there are pages with a matching alias
			$objPages = $objPageModel->findByAliases($arrOptions);

			if ($objPages !== null)
			{
				$arrPages = array();

				// Order by domain and language
				while ($objPages->next())
				{
					/** @var PageModel $objModel */
					$objModel = $objPages->current();
					$objPage  = $objModel->loadDetails();

					$domain = $objPage->domain ?: '*';
					$arrPages[$domain][$objPage->rootLanguage][] = $objPage;

					// Also store the fallback language
					if ($objPage->rootIsFallback)
					{
						$arrPages[$domain]['*'][] = $objPage;
					}
				}

				$strHost = Environment::get('host');

				// Look for a root page whose domain name matches the host name
				if (isset($arrPages[$strHost]))
				{
					$arrLangs = $arrPages[$strHost];
				}
				else
				{
					$arrLangs = $arrPages['*'] ?? array(); // empty domain
				}

				$arrAliases = array();

				// Use the first result (see #4872)
				if (!Config::get('addLanguageToUrl'))
				{
					$arrAliases = current($arrLangs);
				}
				// Try to find a page matching the language parameter
				elseif (($lang = Input::get('language')) && isset($arrLangs[$lang]))
				{
					$arrAliases = $arrLangs[$lang];
				}

				// Return if there are no matches
				if (empty($arrAliases))
				{
					return false;
				}

				$objPage = $arrAliases[0];

				// The request consists of the alias only
				if ($strRequest == $objPage->alias)
				{
					$arrFragments = array($strRequest);
				}
				// Remove the alias from the request string, explode it and then re-insert the alias at the beginning
				else
				{
					$arrFragments = explode('/', substr($strRequest, \strlen($objPage->alias) + 1));
					array_unshift($arrFragments, $objPage->alias);
				}
			}
		}

		// If folderUrl is deactivated or did not find a matching page
		if ($arrFragments === null)
		{
			if ($strRequest == '/')
			{
				return false;
			}
			else
			{
				$arrFragments = explode('/', $strRequest);
			}
		}

		// Add the second fragment as auto_item if the number of fragments is even
		if (\count($arrFragments) % 2 == 0)
		{
			if (!Config::get('useAutoItem'))
			{
				return false; // see #264
			}

			array_insert($arrFragments, 1, array('auto_item'));
		}

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

		// Return if the alias is empty (see #4702 and #4972)
		if ($arrFragments[0] == '' && \count($arrFragments) > 1)
		{
			return false;
		}

		// Add the fragments to the $_GET array
		for ($i=1, $c=\count($arrFragments); $i<$c; $i+=2)
		{
			// Return false if the key is empty (see #4702 and #263)
			if ($arrFragments[$i] == '')
			{
				return false;
			}

			// Return false if there is a duplicate parameter (duplicate content) (see #4277)
			if (isset($_GET[$arrFragments[$i]]))
			{
				return false;
			}

			// Return false if the request contains an auto_item keyword (duplicate content) (see #4012)
			if (Config::get('useAutoItem') && \in_array($arrFragments[$i], $GLOBALS['TL_AUTO_ITEM']))
			{
				return false;
			}

			Input::setGet($arrFragments[$i], $arrFragments[$i+1], true);
		}

		return $arrFragments[0] ?: null;
	}

	/**
	 * Return the root page ID
	 *
	 * @return integer
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Frontend::getRootPageFromUrl()->id instead.
	 */
	public static function getRootIdFromUrl()
	{
		@trigger_error('Using Frontend::getRootIdFromUrl() has been deprecated and will no longer work in Contao 5.0. Use Frontend::getRootPageFromUrl()->id instead.', E_USER_DEPRECATED);

		return static::getRootPageFromUrl()->id;
	}

	/**
	 * Try to find a root page based on language and URL
	 *
	 * @return PageModel
	 */
	public static function getRootPageFromUrl()
	{
		$host = Environment::get('host');
		$logger = System::getContainer()->get('monolog.logger.contao');
		$accept_language = Environment::get('httpAcceptLanguage');

		// The language is set in the URL
		if (!empty($_GET['language']) && Config::get('addLanguageToUrl'))
		{
			$strUri = Environment::get('url') . '/' . Input::get('language') . '/';
			$strError = 'No root page found (host "' . $host . '", language "'. Input::get('language') .'")';
		}

		// No language given
		else
		{
			// Always load the language fall back root if "doNotRedirectEmpty" is enabled
			if (Config::get('addLanguageToUrl') && Config::get('doNotRedirectEmpty'))
			{
				$accept_language = '-';
			}

			$strUri = Environment::get('url') . '/';
			$strError = 'No root page found (host "' . Environment::get('host') . '", languages "' . implode(', ', Environment::get('httpAcceptLanguage')) . '")';
		}

		try
		{
			$objRequest = Request::create($strUri);
			$objRequest->headers->set('Accept-Language', $accept_language);

			$arrParameters = System::getContainer()->get('contao.routing.nested_matcher')->matchRequest($objRequest);
			$objRootPage = $arrParameters['pageModel'] ?? null;

			if (!$objRootPage instanceof PageModel)
			{
				throw new MissingMandatoryParametersException('Every Contao route must have a "pageModel" parameter');
			}
		}
		catch (RoutingExceptionInterface $exception)
		{
			$logger->log(LogLevel::ERROR, $strError, array('contao' => new ContaoContext(__METHOD__, 'ERROR')));

			throw new NoRootPageFoundException('No root page found', 0, $exception);
		}

		// Redirect to the website root or language root (e.g. en/)
		if (Environment::get('relativeRequest') == '')
		{
			if (Config::get('addLanguageToUrl') && !Config::get('doNotRedirectEmpty'))
			{
				$arrParams = array('_locale' => $objRootPage->language);

				$strUrl = System::getContainer()->get('router')->generate('contao_index', $arrParams);
				$strUrl = substr($strUrl, \strlen(Environment::get('path')) + 1);

				static::redirect($strUrl, 301);
			}

			// Redirect if the page alias is not "index" or "/" (see #8498, #8560 and #1210)
			elseif ($objRootPage->type !== 'root' && !\in_array($objRootPage->alias, array('index', '/')))
			{
				static::redirect($objRootPage->getAbsoluteUrl(), 302);
			}
		}

		if ($objRootPage->type != 'root')
		{
			return PageModel::findByPk($objRootPage->rootId);
		}

		return $objRootPage;
	}

	/**
	 * Overwrite the parent method as front end URLs are handled differently
	 *
	 * @param string  $strRequest
	 * @param boolean $blnIgnoreParams
	 * @param array   $arrUnset
	 *
	 * @return string
	 */
	public static function addToUrl($strRequest, $blnIgnoreParams=false, $arrUnset=array())
	{
		$arrGet = $blnIgnoreParams ? array() : $_GET;

		// Clean the $_GET values (thanks to thyon)
		foreach (array_keys($arrGet) as $key)
		{
			$arrGet[$key] = Input::get($key, true, true);
		}

		$arrFragments = preg_split('/&(amp;)?/i', $strRequest);

		// Merge the new request string
		foreach ($arrFragments as $strFragment)
		{
			list($key, $value) = explode('=', $strFragment);

			if ($value == '')
			{
				unset($arrGet[$key]);
			}
			else
			{
				$arrGet[$key] = $value;
			}
		}

		// Unset the language parameter
		if (Config::get('addLanguageToUrl'))
		{
			unset($arrGet['language']);
		}

		$strParams    = '';
		$strConnector = '/';
		$strSeparator = '/';

		// Compile the parameters string
		foreach ($arrGet as $k=>$v)
		{
			// Omit the key if it is an auto_item key (see #5037)
			if (Config::get('useAutoItem') && ($k == 'auto_item' || \in_array($k, $GLOBALS['TL_AUTO_ITEM'])))
			{
				$strParams = $strConnector . urlencode($v) . $strParams;
			}
			else
			{
				$strParams .= $strConnector . urlencode($k) . $strSeparator . urlencode($v);
			}
		}

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

		$pageId = $objPage->alias ?: $objPage->id;

		// Get the page ID from URL if not set
		if (empty($pageId))
		{
			$pageId = static::getPageIdFromUrl();
		}

		$arrParams = array();
		$arrParams['alias'] = $pageId . $strParams;

		if (Config::get('addLanguageToUrl'))
		{
			$arrParams['_locale'] = $objPage->rootLanguage;
		}

		$strUrl = System::getContainer()->get('router')->generate('contao_frontend', $arrParams);
		$strUrl = substr($strUrl, \strlen(Environment::get('path')) + 1);

		return $strUrl;
	}

	/**
	 * Redirect to a jumpTo page or reload the current page
	 *
	 * @param integer|array $intId
	 * @param string        $strParams
	 * @param string        $strForceLang
	 */
	protected function jumpToOrReload($intId, $strParams=null, $strForceLang=null)
	{
		if ($strForceLang !== null)
		{
			@trigger_error('Using Frontend::jumpToOrReload() with $strForceLang has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);
		}

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

		// Always redirect if there are additional arguments (see #5734)
		$blnForceRedirect = ($strParams !== null || $strForceLang !== null);

		if (\is_array($intId))
		{
			$intId = $intId['id'] ?? 0;
		}

		if ($intId > 0 && ($intId != $objPage->id || $blnForceRedirect) && ($objNextPage = PageModel::findPublishedById($intId)) !== null)
		{
			$this->redirect($objNextPage->getFrontendUrl($strParams, $strForceLang));
		}

		$this->reload();
	}

	/**
	 * Check whether a back end or front end user is logged in
	 *
	 * @param string $strCookie
	 *
	 * @return boolean
	 *
	 * @deprecated Deprecated since Contao 4.5, to be removed in Contao 5.0.
	 *             Use Symfony security instead.
	 */
	protected function getLoginStatus($strCookie)
	{
		@trigger_error('Using Frontend::getLoginStatus() has been deprecated and will no longer work in Contao 5.0. Use Symfony security instead.', E_USER_DEPRECATED);

		$objTokenChecker = System::getContainer()->get('contao.security.token_checker');

		if ($strCookie == 'BE_USER_AUTH' && $objTokenChecker->hasBackendUser())
		{
			// Always return false if we are not in preview mode (show hidden elements)
			if (TL_MODE == 'FE' && !$objTokenChecker->isPreviewMode())
			{
				return false;
			}

			return true;
		}

		if ($strCookie == 'FE_USER_AUTH' && $objTokenChecker->hasFrontendUser())
		{
			return true;
		}

		return false;
	}

	/**
	 * Get the meta data from a serialized string
	 *
	 * @param string $strData
	 * @param string $strLanguage
	 *
	 * @return array
	 */
	public static function getMetaData($strData, $strLanguage)
	{
		if (empty($strLanguage))
		{
			return array();
		}

		$arrData = StringUtil::deserialize($strData);

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

		if (!\is_array($arrData) || !isset($arrData[$strLanguage]))
		{
			return array();
		}

		return $arrData[$strLanguage];
	}

	/**
	 * Prepare a text to be used in the meta description tag
	 *
	 * @param string $strText
	 *
	 * @return string
	 */
	protected function prepareMetaDescription($strText)
	{
		$strText = $this->replaceInsertTags($strText, false);
		$strText = strip_tags($strText);
		$strText = str_replace("\n", ' ', $strText);
		$strText = StringUtil::substr($strText, 320);

		return trim($strText);
	}

	/**
	 * Return the cron timeout in seconds
	 *
	 * @return integer
	 */
	public static function getCronTimeout()
	{
		if (!empty($GLOBALS['TL_CRON']['minutely']))
		{
			return 60;
		}
		elseif (!empty($GLOBALS['TL_CRON']['hourly']))
		{
			return 3600;
		}
		else
		{
			return 86400; // daily
		}
	}

	/**
	 * Index a page if applicable
	 *
	 * @param Response $objResponse
	 */
	public static function indexPageIfApplicable(Response $objResponse)
	{
		global $objPage;

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

		// Index page if searching is allowed and there is no back end user
		if (Config::get('enableSearch') && $objResponse->getStatusCode() == 200 && !BE_USER_LOGGED_IN && !$objPage->noSearch)
		{
			// Index protected pages if enabled
			if (Config::get('indexProtected') || (!FE_USER_LOGGED_IN && !$objPage->protected))
			{
				$blnIndex = true;

				// Do not index the page if certain parameters are set
				foreach (array_keys($_GET) as $key)
				{
					if (\in_array($key, $GLOBALS['TL_NOINDEX_KEYS']) || strncmp($key, 'page_', 5) === 0)
					{
						$blnIndex = false;
						break;
					}
				}

				if ($blnIndex)
				{
					$arrData = array(
						'url'       => Environment::get('base') . Environment::get('relativeRequest'),
						'content'   => $objResponse->getContent(),
						'title'     => $objPage->pageTitle ?: $objPage->title,
						'protected' => ($objPage->protected ? '1' : ''),
						'groups'    => $objPage->groups,
						'pid'       => $objPage->id,
						'language'  => $objPage->language
					);

					Search::indexPage($arrData);
				}
			}
		}
	}

	/**
	 * Check whether there is a cached version of the page and return a response object
	 *
	 * @return Response|null
	 *
	 * @deprecated Deprecated since Contao 4.3, to be removed in Contao 5.0.
	 *             Use proper response caching headers instead.
	 */
	public static function getResponseFromCache()
	{
		@trigger_error('Using Frontend::getResponseFromCache() has been deprecated and will no longer work in Contao 5.0. Use proper response caching headers instead.', E_USER_DEPRECATED);

		return null;
	}
}

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


<?php

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

namespace Contao;

/**
 * Creates RSS or Atom feeds
 *
 * The class provides an interface to create RSS or Atom feeds. You can add the
 * feed item objects and the class will generate the XML markup.
 *
 * Usage:
 *
 *     $feed = new Feed('news');
 *     $feed->title = 'News feed';
 *
 *     $item = new FeedItem();
 *     $item->title = 'Latest news';
 *     $item->author = 'Leo Feyer';
 *
 *     $feed->addItem($item);
 *     echo $feed->generateRss();
 *
 * @property string  $title       The feed title
 * @property string  $description The feed description
 * @property string  $language    The feed language
 * @property string  $link        The feed link
 * @property integer $published   The publication date
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class Feed
{

	/**
	 * Feed name
	 * @var string
	 */
	protected $strName;

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

	/**
	 * Items
	 * @var array
	 */
	protected $arrItems = array();

	/**
	 * Store the feed name (without file extension)
	 *
	 * @param string $strName The feed name
	 */
	public function __construct($strName)
	{
		$this->strName = $strName;
	}

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

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

		return null;
	}

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

	/**
	 * Add an item
	 *
	 * @param FeedItem $objItem The feed item object
	 */
	public function addItem(FeedItem $objItem)
	{
		$this->arrItems[] = $objItem;
	}

	/**
	 * Generate an RSS 2.0 feed and return it as XML string
	 *
	 * @return string The RSS feed markup
	 */
	public function generateRss()
	{
		$this->adjustPublicationDate();

		$xml  = '<?xml version="1.0" encoding="' . Config::get('characterSet') . '"?>';
		$xml .= '<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/" xmlns:atom="http://www.w3.org/2005/Atom">';
		$xml .= '<channel>';
		$xml .= '<title>' . StringUtil::specialchars($this->title) . '</title>';
		$xml .= '<description>' . StringUtil::specialchars($this->description) . '</description>';
		$xml .= '<link>' . StringUtil::specialchars($this->link) . '</link>';
		$xml .= '<language>' . $this->language . '</language>';
		$xml .= '<pubDate>' . date('r', $this->published) . '</pubDate>';
		$xml .= '<generator>Contao Open Source CMS</generator>';
		$xml .= '<atom:link href="' . StringUtil::specialchars(Environment::get('base') . 'share/' . $this->strName) . '.xml" rel="self" type="application/rss+xml" />';

		foreach ($this->arrItems as $objItem)
		{
			$xml .= '<item>';
			$xml .= '<title>' . StringUtil::specialchars(strip_tags(StringUtil::stripInsertTags($objItem->title))) . '</title>';
			$xml .= '<description><![CDATA[' . preg_replace('/[\n\r]+/', ' ', $objItem->description) . ']]></description>';
			$xml .= '<link>' . StringUtil::specialchars($objItem->link) . '</link>';
			$xml .= '<pubDate>' . date('r', $objItem->published) . '</pubDate>';

			// Add the GUID
			if ($objItem->guid)
			{
				// Add the isPermaLink attribute if the guid is not a link (see #4930)
				if (strncmp($objItem->guid, 'http://', 7) !== 0 && strncmp($objItem->guid, 'https://', 8) !== 0)
				{
					$xml .= '<guid isPermaLink="false">' . $objItem->guid . '</guid>';
				}
				else
				{
					$xml .= '<guid>' . $objItem->guid . '</guid>';
				}
			}
			else
			{
				$xml .= '<guid>' . StringUtil::specialchars($objItem->link) . '</guid>';
			}

			// Enclosures
			if (\is_array($objItem->enclosure))
			{
				foreach ($objItem->enclosure as $arrEnclosure)
				{
					if (!empty($arrEnclosure['media']) && $arrEnclosure['media'] == 'media:content')
					{
						$xml .= '<media:content url="' . $arrEnclosure['url'] . '" type="' . $arrEnclosure['type'] . '" />';
					}
					else
					{
						$xml .= '<enclosure url="' . $arrEnclosure['url'] . '" length="' . $arrEnclosure['length'] . '" type="' . $arrEnclosure['type'] . '" />';
					}
				}
			}

			$xml .= '</item>';
		}

		$xml .= '</channel>';
		$xml .= '</rss>';

		return $xml;
	}

	/**
	 * Generate an Atom feed and return it as XML string
	 *
	 * @return string The Atom feed markup
	 */
	public function generateAtom()
	{
		$this->adjustPublicationDate();

		$xml  = '<?xml version="1.0" encoding="' . Config::get('characterSet') . '"?>';
		$xml .= '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="' . $this->language . '">';
		$xml .= '<title>' . StringUtil::specialchars($this->title) . '</title>';
		$xml .= '<subtitle>' . StringUtil::specialchars($this->description) . '</subtitle>';
		$xml .= '<link rel="alternate" href="' . StringUtil::specialchars($this->link) . '" />';
		$xml .= '<id>' . StringUtil::specialchars($this->link) . '</id>';
		$xml .= '<updated>' . preg_replace('/00$/', ':00', date('Y-m-d\TH:i:sO', $this->published)) . '</updated>';
		$xml .= '<generator>Contao Open Source CMS</generator>';
		$xml .= '<link href="' . StringUtil::specialchars(Environment::get('base') . 'share/' . $this->strName) . '.xml" rel="self" />';

		foreach ($this->arrItems as $objItem)
		{
			$xml .= '<entry>';
			$xml .= '<title>' . StringUtil::specialchars(strip_tags(StringUtil::stripInsertTags($objItem->title))) . '</title>';
			$xml .= '<content type="xhtml">
' . preg_replace('/[\n\r]+/', ' ', StringUtil::toXhtml($objItem->description)) . '
</content>';
			$xml .= '<link rel="alternate" href="' . StringUtil::specialchars($objItem->link) . '" />';
			$xml .= '<updated>' . preg_replace('/00$/', ':00', date('Y-m-d\TH:i:sO', $objItem->published)) . '</updated>';
			$xml .= '<id>' . ($objItem->guid ?: StringUtil::specialchars($objItem->link)) . '</id>';

			if ($objItem->author)
			{
				$xml .= '<author><name>' . $objItem->author . '</name></author>';
			}

			// Enclosures
			if (\is_array($objItem->enclosure))
			{
				foreach ($objItem->enclosure as $arrEnclosure)
				{
					if (!empty($arrEnclosure['media']) && $arrEnclosure['media'] == 'media:content')
					{
						$xml .= '<media:content url="' . $arrEnclosure['url'] . '" type="' . $arrEnclosure['type'] . '" />';
					}
					else
					{
						$xml .= '<link rel="enclosure" type="' . $arrEnclosure['type'] . '" href="' . $arrEnclosure['url'] . '" length="' . $arrEnclosure['length'] . '" />';
					}
				}
			}

			$xml .= '</entry>';
		}

		return $xml . '</feed>';
	}

	/**
	 * Adjust the publication date
	 */
	protected function adjustPublicationDate()
	{
		if (!empty($this->arrItems) && $this->arrItems[0]->published > $this->published)
		{
			$this->published = $this->arrItems[0]->published;
		}
	}
}

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


<?php

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

namespace Contao;

/**
 * Front end content element "list".
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ContentList extends ContentElement
{

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

	/**
	 * Generate the content element
	 */
	protected function compile()
	{
		$arrItems = array();
		$items = StringUtil::deserialize($this->listitems, true);
		$limit = \count($items) - 1;

		for ($i=0, $c=\count($items); $i<$c; $i++)
		{
			$arrItems[] = array
			(
				'class' => (($i == 0) ? 'first' : (($i == $limit) ? 'last' : '')),
				'content' => $items[$i]
			);
		}

		$this->Template->items = $arrItems;
		$this->Template->tag = ($this->listtype == 'ordered') ? 'ol' : 'ul';
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\InstallationBundle\Config;

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Yaml\Yaml;

class ParameterDumper
{
    /**
     * @var string
     */
    private $rootDir;

    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * @var array
     */
    private $parameters = ['parameters' => []];

    public function __construct(string $rootDir, Filesystem $filesystem = null)
    {
        $this->rootDir = $rootDir;
        $this->filesystem = $filesystem ?: new Filesystem();
        $parameters = [];

        foreach (['app/config/parameters.yml.dist', 'app/config/parameters.yml'] as $file) {
            if (file_exists($rootDir.'/'.$file)) {
                $parameters[] = Yaml::parse(file_get_contents($rootDir.'/'.$file));
            }
        }

        if (!empty($parameters)) {
            $this->parameters = array_merge($this->parameters, ...$parameters);
        }
    }

    public function setParameter(string $name, $value): void
    {
        $this->parameters['parameters'][$name] = $value;
    }

    public function setParameters(array $params): void
    {
        foreach ($params['parameters'] as $name => $value) {
            $this->setParameter($name, $value);
        }
    }

    /**
     * Dumps the parameters into the parameters.yml file.
     */
    public function dump(): void
    {
        if (
            empty($this->parameters['parameters']['secret']) ||
            'ThisTokenIsNotSoSecretChangeIt' === $this->parameters['parameters']['secret']
        ) {
            $this->parameters['parameters']['secret'] = bin2hex(random_bytes(32));
        }

        if (isset($this->parameters['parameters']['database_port'])) {
            $this->parameters['parameters']['database_port'] = (int) $this->parameters['parameters']['database_port'];
        }

        $this->filesystem->dumpFile(
            $this->rootDir.'/app/config/parameters.yml',
            "# This file has been auto-generated during installation\n".Yaml::dump($this->getEscapedValues())
        );
    }

    /**
     * Escapes % and @.
     *
     * @return array<string,string[]>
     *
     * @see http://symfony.com/doc/current/service_container/parameters.html#parameters-in-configuration-files
     */
    private function getEscapedValues(): array
    {
        $parameters = [];

        foreach ($this->parameters['parameters'] as $key => $value) {
            if (\is_string($value)) {
                if (0 === strncmp($value, '@', 1)) {
                    $value = '@'.$value;
                }

                if (false !== strpos($value, '%')) {
                    $value = str_replace('%', '%%', $value);
                }
            }

            $parameters[$key] = $value;
        }

        return ['parameters' => $parameters];
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\InstallationBundle\Translation;

use Symfony\Component\HttpFoundation\RequestStack;

class LanguageResolver
{
    /**
     * @var RequestStack
     */
    private $requestStack;

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

    public function __construct(RequestStack $requestStack, string $translationsDir)
    {
        $this->requestStack = $requestStack;
        $this->translationsDir = $translationsDir;
    }

    /**
     * Returns the first available locale.
     */
    public function getLocale(): string
    {
        foreach ($this->getAcceptedLocales() as $locale) {
            if (file_exists($this->translationsDir.'/messages.'.$locale.'.xlf')) {
                return $locale;
            }
        }

        return 'en';
    }

    /**
     * Returns the first eight accepted locales.
     *
     * @return string[]
     */
    private function getAcceptedLocales(): array
    {
        $accepted = [];
        $locales = [];

        // The implementation differs from the original implementation and also works with .jp browsers
        preg_match_all(
            '/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.\d+))?/i',
            $this->requestStack->getCurrentRequest()->headers->get('accept-language'),
            $accepted
        );

        // Remove all invalid locales
        foreach ($accepted[1] as $v) {
            $chunks = explode('-', $v);

            // Language plus dialect, e.g. "en-US" or "fr-FR"
            if (isset($chunks[1])) {
                $locale = $chunks[0].'-'.strtoupper($chunks[1]);

                if (preg_match('/^[a-z]{2}(\-[A-Z]{2})?$/', $locale)) {
                    $locales[] = $locale;
                }
            }

            // Language only, e.g. "en" or "fr"
            if (preg_match('/^[a-z]{2}$/', $chunks[0])) {
                $locales[] = $chunks[0];
            }
        }

        return \array_slice(array_unique($locales), 0, 8);
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Controller;

use Contao\BackendTemplate;
use Contao\Config;
use Contao\CoreBundle\Exception\InternalServerErrorException;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\DataContainer;
use Contao\FileUpload;
use Contao\Message;
use Doctrine\DBAL\Connection;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Translation\TranslatorInterface;

class BackendCsvImportController
{
    public const SEPARATOR_COMMA = 'comma';
    public const SEPARATOR_LINEBREAK = 'linebreak';
    public const SEPARATOR_SEMICOLON = 'semicolon';
    public const SEPARATOR_TABULATOR = 'tabulator';

    /**
     * @var ContaoFramework
     */
    private $framework;

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

    /**
     * @var RequestStack
     */
    private $requestStack;

    /**
     * @var TranslatorInterface
     */
    private $translator;

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

    public function __construct(ContaoFramework $framework, Connection $connection, RequestStack $requestStack, TranslatorInterface $translator, string $projectDir)
    {
        $this->framework = $framework;
        $this->connection = $connection;
        $this->requestStack = $requestStack;
        $this->translator = $translator;
        $this->projectDir = $projectDir;
    }

    public function importListWizardAction(DataContainer $dc): Response
    {
        return $this->importFromTemplate(
            static function (array $data, array $row): array {
                return array_merge($data, $row);
            },
            $dc->table,
            'listitems',
            (int) $dc->id,
            $this->translator->trans('MSC.lw_import.0', [], 'contao_default'),
            true
        );
    }

    public function importTableWizardAction(DataContainer $dc): Response
    {
        return $this->importFromTemplate(
            static function (array $data, array $row): array {
                $data[] = $row;

                return $data;
            },
            $dc->table,
            'tableitems',
            (int) $dc->id,
            $this->translator->trans('MSC.tw_import.0', [], 'contao_default')
        );
    }

    public function importOptionWizardAction(DataContainer $dc): Response
    {
        return $this->importFromTemplate(
            static function (array $data, array $row): array {
                $data[] = [
                    'value' => $row[0],
                    'label' => $row[1],
                    'default' => !empty($row[2]) ? 1 : '',
                    'group' => !empty($row[3]) ? 1 : '',
                ];

                return $data;
            },
            $dc->table,
            'options',
            (int) $dc->id,
            $this->translator->trans('MSC.ow_import.0', [], 'contao_default')
        );
    }

    /**
     * @throws InternalServerErrorException
     */
    private function importFromTemplate(callable $callback, string $table, string $field, int $id, string $submitLabel = null, bool $allowLinebreak = false): Response
    {
        $request = $this->requestStack->getCurrentRequest();

        if (null === $request) {
            throw new InternalServerErrorException('No request object given.');
        }

        $this->framework->initialize();

        /** @var FileUpload $uploader */
        $uploader = $this->framework->createInstance(FileUpload::class);
        $template = $this->prepareTemplate($request, $uploader, $allowLinebreak);

        if (null !== $submitLabel) {
            $template->submitLabel = $submitLabel;
        }

        if ($request->request->get('FORM_SUBMIT') === $this->getFormId($request)) {
            try {
                $data = $this->fetchData($uploader, $request->request->get('separator', ''), $callback);
            } catch (\RuntimeException $e) {
                /** @var Message $message */
                $message = $this->framework->getAdapter(Message::class);
                $message->addError($e->getMessage());

                return new RedirectResponse($request->getUri(), 303);
            }

            $this->connection->update(
                $table,
                [$field => serialize($data)],
                ['id' => $id]
            );

            if (method_exists(Cookie::class, 'create')) {
                $cookie = Cookie::create('BE_PAGE_OFFSET', null, 0, $request->getBasePath(), null, null, false);
            } else {
                // Backwards compatibility with symfony/http-foundation <4.2
                $cookie = new Cookie('BE_PAGE_OFFSET', null, 0, $request->getBasePath(), null, false, false);
            }

            $response = new RedirectResponse($this->getBackUrl($request));
            $response->headers->setCookie($cookie);

            return $response;
        }

        return new Response($template->parse());
    }

    private function prepareTemplate(Request $request, FileUpload $uploader, bool $allowLinebreak = false): BackendTemplate
    {
        $template = new BackendTemplate('be_csv_import');

        /** @var Config $config */
        $config = $this->framework->getAdapter(Config::class);

        $template->formId = $this->getFormId($request);
        $template->backUrl = $this->getBackUrl($request);
        $template->action = $request->getRequestUri();
        $template->fileMaxSize = $config->get('maxFileSize');
        $template->uploader = $uploader->generateMarkup();
        $template->separators = $this->getSeparators($allowLinebreak);
        $template->submitLabel = $this->translator->trans('MSC.apply', [], 'contao_default');
        $template->backBT = $this->translator->trans('MSC.backBT', [], 'contao_default');
        $template->backBTTitle = $this->translator->trans('MSC.backBTTitle', [], 'contao_default');
        $template->separatorLabel = $this->translator->trans('MSC.separator.0', [], 'contao_default');
        $template->separatorHelp = $this->translator->trans('MSC.separator.1', [], 'contao_default');
        $template->sourceLabel = $this->translator->trans('MSC.source.0', [], 'contao_default');
        $template->sourceLabelHelp = $this->translator->trans('MSC.source.1', [], 'contao_default');

        return $template;
    }

    /**
     * Returns an array of data from the imported CSV files.
     *
     * @return string[]
     */
    private function fetchData(FileUpload $uploader, string $separator, callable $callback): array
    {
        $data = [];
        $files = $this->getFiles($uploader);
        $delimiter = $this->getDelimiter($separator);

        foreach ($files as $file) {
            $fp = fopen($file, 'r');

            while (false !== ($row = fgetcsv($fp, 0, $delimiter))) {
                $data = $callback($data, $row);
            }
        }

        return $data;
    }

    private function getFormId(Request $request): string
    {
        return 'tl_csv_import_'.$request->query->get('key');
    }

    private function getBackUrl(Request $request): string
    {
        return str_replace('&key='.$request->query->get('key'), '', $request->getRequestUri());
    }

    /**
     * @return array<string,array<string,string>>
     */
    private function getSeparators(bool $allowLinebreak = false): array
    {
        $separators = [
            self::SEPARATOR_COMMA => [
                'delimiter' => ',',
                'value' => self::SEPARATOR_COMMA,
                'label' => $this->translator->trans('MSC.comma', [], 'contao_default'),
            ],
            self::SEPARATOR_SEMICOLON => [
                'delimiter' => ';',
                'value' => self::SEPARATOR_SEMICOLON,
                'label' => $this->translator->trans('MSC.semicolon', [], 'contao_default'),
            ],
            self::SEPARATOR_TABULATOR => [
                'delimiter' => "\t",
                'value' => self::SEPARATOR_TABULATOR,
                'label' => $this->translator->trans('MSC.tabulator', [], 'contao_default'),
            ],
        ];

        if ($allowLinebreak) {
            $separators[self::SEPARATOR_LINEBREAK] = [
                'delimiter' => "\n",
                'value' => self::SEPARATOR_LINEBREAK,
                'label' => $this->translator->trans('MSC.linebreak', [], 'contao_default'),
            ];
        }

        return $separators;
    }

    /**
     * @throws \RuntimeException
     */
    private function getDelimiter(string $separator): string
    {
        $separators = $this->getSeparators(true);

        if (!isset($separators[$separator])) {
            throw new \RuntimeException($this->translator->trans('MSC.separator.1', [], 'contao_default'));
        }

        return $separators[$separator]['delimiter'];
    }

    /**
     * Returns the uploaded files from a FileUpload instance.
     *
     * @throws \RuntimeException
     *
     * @return string[]
     */
    private function getFiles(FileUpload $uploader): array
    {
        $files = $uploader->uploadTo('system/tmp');

        if (\count($files) < 1) {
            throw new \RuntimeException($this->translator->trans('ERR.all_fields', [], 'contao_default'));
        }

        foreach ($files as &$file) {
            $extension = pathinfo($file, PATHINFO_EXTENSION);

            if ('csv' !== $extension) {
                throw new \RuntimeException(
                    sprintf($this->translator->trans('ERR.filetype', [], 'contao_default'), $extension)
                );
            }

            $file = $this->projectDir.'/'.$file;
        }

        return $files;
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Util;

use PackageVersions\Versions;

class PackageUtil
{
    /**
     * Returns the version number of a package.
     */
    public static function getVersion(string $packageName): string
    {
        $version = Versions::getVersion($packageName);

        return static::parseVersion($version);
    }

    /**
     * Returns the version number as "major.minor.patch".
     */
    public static function getNormalizedVersion(string $packageName): string
    {
        $chunks = explode('.', static::getVersion($packageName));
        $chunks += [0, 0, 0];

        if (\count($chunks) > 3) {
            $chunks = \array_slice($chunks, 0, 3);
        }

        return implode('.', $chunks);
    }

    /**
     * Parses a version number.
     *
     * The method either returns a version number such as 1.0.0 (a leading "v"
     * will be stripped) or a branch name such as dev-master.
     */
    public static function parseVersion(string $version): string
    {
        return ltrim(strstr($version, '@', true), 'v');
    }
}


<?php

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

namespace Contao;

/**
 * Provide methods to handle CHMOD tables.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ChmodTable extends Widget
{

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

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

	/**
	 * Generate the widget and return it as string
	 *
	 * @return string
	 */
	public function generate()
	{
		$arrObjects = array('u'=>'cuser', 'g'=>'cgroup', 'w'=>'cworld');

		$return = '

      '.$GLOBALS['TL_LANG']['CHMOD']['editpage'].'
      '.$GLOBALS['TL_LANG']['CHMOD']['editnavigation'].'
      '.$GLOBALS['TL_LANG']['CHMOD']['deletepage'].'
      '.$GLOBALS['TL_LANG']['CHMOD']['editarticles'].'
      '.$GLOBALS['TL_LANG']['CHMOD']['movearticles'].'
      '.$GLOBALS['TL_LANG']['CHMOD']['deletearticles'].'

';

		// Build rows for user, group and world
		foreach ($arrObjects as $k=>$v)
		{
			$return .= '

      '.$GLOBALS['TL_LANG']['CHMOD'][$v].'	';

			// Add checkboxes
			for ($j=1; $j<=6; $j++)
			{
				$return .= '
      <input type="checkbox" name="'.$this->strName.'[]" value="'.StringUtil::specialchars($k.$j).'"'.static::optionChecked($k.$j, $this->varValue).' onfocus="Backend.getScrollOffset()">	';
			}

			$return .= '

';
		}

		return $return.'

';
	}
}

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


<?php

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

namespace Contao;

/**
 * Front end content element "table".
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ContentTable extends ContentElement
{

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

	/**
	 * Generate the content element
	 */
	protected function compile()
	{
		$rows = StringUtil::deserialize($this->tableitems, true);

		$this->Template->id = 'table_' . $this->id;
		$this->Template->summary = StringUtil::specialchars($this->summary);
		$this->Template->useHeader = $this->thead ? true : false;
		$this->Template->useFooter = $this->tfoot ? true : false;
		$this->Template->useLeftTh = $this->tleft ? true : false;
		$this->Template->sortable = $this->sortable ? true : false;

		$arrHeader = array();
		$arrBody = array();
		$arrFooter = array();

		// Table header
		if ($this->thead)
		{
			foreach ($rows[0] as $i=>$v)
			{
				// Set table sort cookie
				if ($this->sortable && $i == $this->sortIndex)
				{
					$co = 'TS_TABLE_' . $this->id;
					$so = ($this->sortOrder == 'descending') ? 'desc' : 'asc';

					if (Input::cookie($co) == '')
					{
						System::setCookie($co, $i . '|' . $so, 0);
					}
				}

				// Add cell
				$arrHeader[] = array
				(
					'class' => 'head_'.$i . (($i == 0) ? ' col_first' : '') . (($i == (\count($rows[0]) - 1)) ? ' col_last' : '') . (($i == 0 && $this->tleft) ? ' unsortable' : ''),
					'content' => (($v != '') ? $this->nl2br($v) : ' ')
				);
			}

			array_shift($rows);
		}

		$this->Template->header = $arrHeader;
		$limit = $this->tfoot ? (\count($rows)-1) : \count($rows);

		// Table body
		for ($j=0; $j<$limit; $j++)
		{
			$class_tr = '';

			if ($j == 0)
			{
				$class_tr .= ' row_first';
			}

			if ($j == ($limit - 1))
			{
				$class_tr .= ' row_last';
			}

			$class_eo = (($j % 2) == 0) ? ' odd' : ' even';

			foreach ($rows[$j] as $i=>$v)
			{
				$class_td = '';

				if ($i == 0)
				{
					$class_td .= ' col_first';
				}

				if ($i == (\count($rows[$j]) - 1))
				{
					$class_td .= ' col_last';
				}

				$arrBody['row_' . $j . $class_tr . $class_eo][] = array
				(
					'class' => 'col_'.$i . $class_td,
					'content' => (($v != '') ? $this->nl2br($v) : ' ')
				);
			}
		}

		$this->Template->body = $arrBody;

		// Table footer
		if ($this->tfoot)
		{
			foreach ($rows[(\count($rows)-1)] as $i=>$v)
			{
				$arrFooter[] = array
				(
					'class' => 'foot_'.$i . (($i == 0) ? ' col_first' : '') . (($i == (\count($rows[(\count($rows)-1)]) - 1)) ? ' col_last' : ''),
					'content' => (($v != '') ? $this->nl2br($v) : ' ')
				);
			}
		}

		$this->Template->footer = $arrFooter;
	}

	/**
	 * Convert new lines to <br> tags if there are no HTML block elements
	 *
	 * @param string $strString
	 *
	 * @return string
	 */
	private function nl2br($strString)
	{
		if (preg_match('#<(?:address|blockquote|dd|div|dl|dt|figcaption|figure|h[1-6]|hr|li|ol|p|pre|ul)[ >]#', $strString))
		{
			return $strString;
		}

		return nl2br($strString, false);
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\ManagerBundle\ContaoManager\ApiCommand;

use Contao\ManagerBundle\Api\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Dotenv\Dotenv;

class GetDotEnvCommand extends Command
{
    /**
     * @var string
     */
    private $projectDir;

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

        $this->projectDir = $application->getProjectDir();
    }

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

        $this
            ->setName('dot-env:get')
            ->setDescription('Reads a parameter from the .env file.')
            ->addArgument('key', InputArgument::OPTIONAL, 'The variable name')
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output): void
    {
        $path = $this->projectDir.'/.env';

        if (!file_exists($path)) {
            return;
        }

        $vars = (new Dotenv())->parse(file_get_contents($path));
        $key = $input->getArgument('key');

        if (!$key) {
            $output->write(json_encode($vars));
        }

        if (isset($vars[$key])) {
            $output->write($vars[$key]);
        }
    }
}


<?php

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

namespace Contao;

/**
 * Class FormTextField
 *
 * @property string  $value
 * @property string  $type
 * @property integer $maxlength
 * @property boolean $mandatory
 * @property integer $min
 * @property integer $max
 * @property integer $step
 * @property string  $placeholder
 * @property boolean $hideInput
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class FormTextField extends Widget
{

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

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

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

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

	/**
	 * Add specific attributes
	 *
	 * @param string $strKey   The attribute key
	 * @param mixed  $varValue The attribute value
	 */
	public function __set($strKey, $varValue)
	{
		switch ($strKey)
		{
			// Treat minlength/minval as min for number type field (#1622)
			case 'minlength':
			case 'minval':
				if ($this->type == 'number')
				{
					$this->min = $varValue;
				}
				else
				{
					$this->arrConfiguration[$strKey] = $varValue;
				}
				break;

			// Treat maxlength/maxval as max for number type field (#1622)
			case 'maxlength':
			case 'maxval':
				if ($varValue > 0)
				{
					if ($this->type == 'number')
					{
						$this->max = $varValue;
					}
					elseif ($strKey == 'maxlength')
					{
						$this->arrAttributes[$strKey] = $varValue;
					}
					else
					{
						$this->arrConfiguration[$strKey] = $varValue;
					}
				}
				break;

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

			case 'min':
			case 'max':
				$this->arrAttributes[$strKey] = $varValue;
				$this->arrConfiguration[$strKey . 'val'] = $varValue;
				unset($this->arrAttributes[$strKey . 'length']);
				break;

			case 'step':
			case 'placeholder':
				$this->arrAttributes[$strKey] = $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 'value':
				// Hide the Punycode format (see #2750)
				if ($this->rgxp == 'url')
				{
					try
					{
						return Idna::decodeUrl($this->varValue);
					}
					catch (\InvalidArgumentException $e)
					{
						return $this->varValue;
					}
				}
				elseif ($this->rgxp == 'email' || $this->rgxp == 'friendly')
				{
					return Idna::decodeEmail($this->varValue);
				}
				else
				{
					return $this->varValue;
				}
				break;

			case 'type':
				if ($this->hideInput)
				{
					return 'password';
				}

				// Use the HTML5 types (see #4138) but not the date, time and datetime types (see #5918)
				switch ($this->rgxp)
				{
					case 'digit':
						// Allow floats (see #7257)
						if (!isset($this->arrAttributes['step']))
						{
							$this->addAttribute('step', 'any');
						}
						// NO break; here

					case 'natural':
						return 'number';
						break;

					case 'phone':
						return 'tel';
						break;

					case 'email':
						return 'email';
						break;

					case 'url':
						return 'url';
						break;
				}

				return 'text';
				break;

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

	/**
	 * Re-add some attributes if the field type is a number
	 */
	public function addAttributes($arrAttributes)
	{
		parent::addAttributes($arrAttributes);

		if ($this->type != 'number')
		{
			return;
		}

		foreach (array('minlength', 'minval', 'maxlength', 'maxval') as $name)
		{
			if (isset($arrAttributes[$name]))
			{
				$this->$name = $arrAttributes[$name];
			}
		}
	}

	/**
	 * Trim the values
	 *
	 * @param mixed $varInput The user input
	 *
	 * @return mixed The validated user input
	 */
	protected function validator($varInput)
	{
		if (\is_array($varInput))
		{
			return parent::validator($varInput);
		}

		// Convert to Punycode format (see #5571)
		if ($this->rgxp == 'url')
		{
			try
			{
				$varInput = Idna::encodeUrl($varInput);
			}
			catch (\InvalidArgumentException $e) {}
		}
		elseif ($this->rgxp == 'email' || $this->rgxp == 'friendly')
		{
			$varInput = Idna::encodeEmail($varInput);
		}

		return parent::validator($varInput);
	}

	/**
	 * Generate the widget and return it as string
	 *
	 * @return string The widget markup
	 */
	public function generate()
	{
		return sprintf('<input type="%s" name="%s" id="ctrl_%s" class="text%s%s" value="%s"%s%s',
						$this->type,
						$this->strName,
						$this->strId,
						($this->hideInput ? ' password' : ''),
						(($this->strClass != '') ? ' ' . $this->strClass : ''),
						StringUtil::specialchars($this->value),
						$this->getAttributes(),
						$this->strTagEnding);
	}
}

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


<?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 Version440Update extends AbstractVersionUpdate
{
    /**
     * {@inheritdoc}
     */
    public function shouldBeRun(): bool
    {
        $schemaManager = $this->connection->getSchemaManager();

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

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

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

    /**
     * {@inheritdoc}
     */
    public function run(): void
    {
        // Add the js_autofocus.html5 template
        $statement = $this->connection->query('
            SELECT
                id, scripts
            FROM
                tl_layout
        ');

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

            if (!empty($scripts) && \is_array($scripts)) {
                $scripts[] = 'js_autofocus';

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

                $stmt->execute([':scripts' => serialize(array_values(array_unique($scripts))), ':id' => $layout->id]);
            }
        }

        $schemaManager = $this->connection->getSchemaManager();

        if ($schemaManager->tablesExist(['tl_calendar_events'])) {
            $this->enableOverwriteMeta('tl_calendar_events');
        }

        if ($schemaManager->tablesExist(['tl_faq'])) {
            $this->enableOverwriteMeta('tl_faq');
        }

        if ($schemaManager->tablesExist(['tl_news'])) {
            $this->enableOverwriteMeta('tl_news');
        }

        $this->connection->query("
            ALTER TABLE
                tl_content
            CHANGE
                title imageTitle varchar(255) NOT NULL DEFAULT ''
        ");

        $this->connection->query("
            ALTER TABLE
                tl_content
            ADD
                overwriteMeta CHAR(1) DEFAULT '' NOT NULL
        ");

        $this->connection->query("
            UPDATE
                tl_content
            SET
                overwriteMeta = '1'
            WHERE
                alt != '' OR imageTitle != '' OR imageUrl != '' OR caption != ''
        ");
    }

    private function enableOverwriteMeta(string $table): void
    {
        $this->connection->query("
            ALTER TABLE
                $table
            ADD
                overwriteMeta CHAR(1) DEFAULT '' NOT NULL
        ");

        $this->connection->query("
            UPDATE
                $table
            SET
                overwriteMeta = '1'
            WHERE
                alt != '' OR imageUrl != '' OR caption != ''
        ");
    }
}


<?php

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

namespace Contao;

use Patchwork\Utf8;

/**
 * Front end module "newsletter unsubscribe".
 *
 * @property bool   $nl_hideChannels
 * @property string $nl_unsubscribe
 * @property array  $nl_channels
 * @property string $nl_template
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ModuleUnsubscribe extends Module
{

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

	/**
	 * 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']['unsubscribe'][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->nl_channels = StringUtil::deserialize($this->nl_channels);

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

		return parent::generate();
	}

	/**
	 * Generate the module
	 */
	protected function compile()
	{
		// Overwrite default template
		if ($this->nl_template)
		{
			$this->Template = new FrontendTemplate($this->nl_template);
			$this->Template->setData($this->arrData);
		}

		$this->Template->email = '';
		$this->Template->captcha = '';

		$objWidget = null;

		// Set up the captcha widget
		if (!$this->disableCaptcha)
		{
			$arrField = array
			(
				'name' => 'unsubscribe_'.$this->id,
				'label' => $GLOBALS['TL_LANG']['MSC']['securityQuestion'],
				'inputType' => 'captcha',
				'eval' => array('mandatory'=>true)
			);

			/** @var Widget $objWidget */
			$objWidget = new FormCaptcha(FormCaptcha::getAttributesFromDca($arrField, $arrField['name']));
		}

		$strFormId = 'tl_unsubscribe_' . $this->id;

		// Unsubscribe
		if (Input::post('FORM_SUBMIT') == $strFormId)
		{
			$varSubmitted = $this->validateForm($objWidget);

			if ($varSubmitted !== false)
			{
				$this->removeRecipient(...$varSubmitted);
			}
		}

		// Add the captcha widget to the template
		if ($objWidget !== null)
		{
			$this->Template->captcha = $objWidget->parse();
		}

		$session = System::getContainer()->get('session');
		$flashBag = $session->getFlashBag();

		// Confirmation message
		if ($session->isStarted() && $flashBag->has('nl_removed'))
		{
			$arrMessages = $flashBag->get('nl_removed');

			$this->Template->mclass = 'confirm';
			$this->Template->message = $arrMessages[0];
		}

		$arrChannels = array();
		$objChannel = NewsletterChannelModel::findByIds($this->nl_channels);

		// Get the titles
		if ($objChannel !== null)
		{
			while ($objChannel->next())
			{
				$arrChannels[$objChannel->id] = $objChannel->title;
			}
		}

		// Default template variables
		$this->Template->channels = $arrChannels;
		$this->Template->showChannels = !$this->nl_hideChannels;
		$this->Template->email = Input::get('email');
		$this->Template->submit = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['unsubscribe']);
		$this->Template->channelsLabel = $GLOBALS['TL_LANG']['MSC']['nl_channels'];
		$this->Template->emailLabel = $GLOBALS['TL_LANG']['MSC']['emailAddress'];
		$this->Template->action = Environment::get('indexFreeRequest');
		$this->Template->formId = $strFormId;
		$this->Template->id = $this->id;
	}

	/**
	 * Validate the subscription form
	 *
	 * @param Widget $objWidget
	 *
	 * @return array|bool
	 */
	protected function validateForm(Widget $objWidget=null)
	{
		// Validate the e-mail address
		$varInput = Idna::encodeEmail(Input::post('email', true));

		if (!Validator::isEmail($varInput))
		{
			$this->Template->mclass = 'error';
			$this->Template->message = $GLOBALS['TL_LANG']['ERR']['email'];

			return false;
		}

		$this->Template->email = $varInput;

		// Validate the channel selection
		$arrChannels = Input::post('channels');

		if (!\is_array($arrChannels))
		{
			$this->Template->mclass = 'error';
			$this->Template->message = $GLOBALS['TL_LANG']['ERR']['noChannels'];

			return false;
		}

		$arrChannels = array_intersect($arrChannels, $this->nl_channels); // see #3240

		if (empty($arrChannels) || !\is_array($arrChannels))
		{
			$this->Template->mclass = 'error';
			$this->Template->message = $GLOBALS['TL_LANG']['ERR']['noChannels'];

			return false;
		}

		$this->Template->selectedChannels = $arrChannels;

		// Check if there are any new subscriptions
		$arrSubscriptions = array();

		if (($objSubscription = NewsletterRecipientsModel::findBy(array("email=? AND active='1'"), $varInput)) !== null)
		{
			$arrSubscriptions = $objSubscription->fetchEach('pid');
		}

		$arrChannels = array_intersect($arrChannels, $arrSubscriptions);

		if (empty($arrChannels))
		{
			$this->Template->mclass = 'error';
			$this->Template->message = $GLOBALS['TL_LANG']['ERR']['unsubscribed'];

			return false;
		}

		// Validate the captcha
		if ($objWidget !== null)
		{
			$objWidget->validate();

			if ($objWidget->hasErrors())
			{
				return false;
			}
		}

		return array($varInput, $arrChannels);
	}

	/**
	 * Remove the recipient
	 *
	 * @param string $strEmail
	 * @param array  $arrRemove
	 */
	protected function removeRecipient($strEmail, $arrRemove)
	{
		// Remove the subscriptions
		if (($objRemove = NewsletterRecipientsModel::findByEmailAndPids($strEmail, $arrRemove)) !== null)
		{
			while ($objRemove->next())
			{
				$strHash = md5($objRemove->email);

				// Add a blacklist entry (see #4999)
				if (($objBlacklist = NewsletterBlacklistModel::findByHashAndPid($strHash, $objRemove->pid)) === null)
				{
					$objBlacklist = new NewsletterBlacklistModel();
					$objBlacklist->pid = $objRemove->pid;
					$objBlacklist->hash = $strHash;
					$objBlacklist->save();
				}

				$objRemove->delete();
			}
		}

		// Get the channels
		$objChannels = NewsletterChannelModel::findByIds($arrRemove);
		$arrChannels = $objChannels->fetchEach('title');

		// HOOK: post unsubscribe callback
		if (isset($GLOBALS['TL_HOOKS']['removeRecipient']) && \is_array($GLOBALS['TL_HOOKS']['removeRecipient']))
		{
			foreach ($GLOBALS['TL_HOOKS']['removeRecipient'] as $callback)
			{
				$this->import($callback[0]);
				$this->{$callback[0]}->{$callback[1]}($strEmail, $arrRemove);
			}
		}

		// Prepare the simple token data
		$arrData = array();
		$arrData['domain'] = Idna::decode(Environment::get('host'));
		$arrData['channel'] = $arrData['channels'] = implode("\n", $arrChannels);

		// Confirmation e-mail
		$objEmail = new Email();
		$objEmail->from = $GLOBALS['TL_ADMIN_EMAIL'];
		$objEmail->fromName = $GLOBALS['TL_ADMIN_NAME'];
		$objEmail->subject = sprintf($GLOBALS['TL_LANG']['MSC']['nl_subject'], Idna::decode(Environment::get('host')));
		$objEmail->text = StringUtil::parseSimpleTokens($this->nl_unsubscribe, $arrData);
		$objEmail->sendTo($strEmail);

		// Redirect to the jumpTo page
		if (($objTarget = $this->objModel->getRelated('jumpTo')) instanceof PageModel)
		{
			/** @var PageModel $objTarget */
			$this->redirect($objTarget->getFrontendUrl());
		}

		System::getContainer()->get('session')->getFlashBag()->set('nl_removed', $GLOBALS['TL_LANG']['MSC']['nl_removed']);

		$this->reload();
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\EventListener;

use Contao\CoreBundle\Exception\AccessDeniedException;
use Contao\CoreBundle\Exception\ForwardPageNotFoundException;
use Contao\CoreBundle\Exception\IncompleteInstallationException;
use Contao\CoreBundle\Exception\InsecureInstallationException;
use Contao\CoreBundle\Exception\InsufficientAuthenticationException;
use Contao\CoreBundle\Exception\InternalServerErrorException;
use Contao\CoreBundle\Exception\InternalServerErrorHttpException;
use Contao\CoreBundle\Exception\InvalidRequestTokenException;
use Contao\CoreBundle\Exception\NoActivePageFoundException;
use Contao\CoreBundle\Exception\NoLayoutSpecifiedException;
use Contao\CoreBundle\Exception\NoRootPageFoundException;
use Contao\CoreBundle\Exception\PageNotFoundException;
use Contao\CoreBundle\Exception\ServiceUnavailableException as ContaoServiceUnavailableException;
use Doctrine\DBAL\Connection;
use Lexik\Bundle\MaintenanceBundle\Exception\ServiceUnavailableException;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;

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

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

    /**
     * Maps known exceptions to HTTP exceptions.
     */
    public function onKernelException(GetResponseForExceptionEvent $event): void
    {
        $exception = $event->getException();

        if ($exception->getPrevious() instanceof ResourceNotFoundException && !$this->hasRootPages()) {
            $exception = new NoRootPageFoundException('No root page found', 0, $exception);
        }

        $class = $this->getTargetClass($exception);

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

        if (null !== ($httpException = $this->convertToHttpException($exception, $class))) {
            $event->setException($httpException);
        }
    }

    private function getTargetClass(\Exception $exception): ?string
    {
        static $mapper = [
            AccessDeniedException::class => 'AccessDeniedHttpException',
            ForwardPageNotFoundException::class => 'InternalServerErrorHttpException',
            IncompleteInstallationException::class => 'InternalServerErrorHttpException',
            InsecureInstallationException::class => 'InternalServerErrorHttpException',
            InsufficientAuthenticationException::class => 'UnauthorizedHttpException',
            InternalServerErrorException::class => 'InternalServerErrorHttpException',
            InvalidRequestTokenException::class => 'BadRequestHttpException',
            NoActivePageFoundException::class => 'InternalServerErrorHttpException',
            NoLayoutSpecifiedException::class => 'InternalServerErrorHttpException',
            NoRootPageFoundException::class => 'InternalServerErrorHttpException',
            PageNotFoundException::class => 'NotFoundHttpException',
            ServiceUnavailableException::class => 'ServiceUnavailableHttpException',
            ContaoServiceUnavailableException::class => 'ServiceUnavailableHttpException',
        ];

        foreach ($mapper as $source => $target) {
            if ($exception instanceof $source) {
                return $target;
            }
        }

        return null;
    }

    private function convertToHttpException(\Exception $exception, string $target): ?HttpException
    {
        switch ($target) {
            case 'AccessDeniedHttpException':
                return new AccessDeniedHttpException($exception->getMessage(), $exception);

            case 'BadRequestHttpException':
                return new BadRequestHttpException($exception->getMessage(), $exception);

            case 'InternalServerErrorHttpException':
                return new InternalServerErrorHttpException($exception->getMessage(), $exception);

            case 'NotFoundHttpException':
                return new NotFoundHttpException($exception->getMessage(), $exception);

            case 'ServiceUnavailableHttpException':
                return new ServiceUnavailableHttpException(null, $exception->getMessage(), $exception);

            case 'UnauthorizedHttpException':
                return new UnauthorizedHttpException('', $exception->getMessage(), $exception);
        }

        return null;
    }

    private function hasRootPages(): bool
    {
        $qb = $this->connection->createQueryBuilder();
        $qb
            ->select('COUNT(*)')
            ->from('tl_page')
            ->where('type = :type')
            ->setParameter('type', 'root')
        ;

        return $qb->execute()->fetchColumn() > 0;
    }
}


<?php

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

namespace Contao;

/**
 * Class FormHidden
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class FormHidden extends Widget
{

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

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

	/**
	 * Generate the widget and return it as string
	 *
	 * @return string The widget markup
	 */
	public function generate()
	{
		return sprintf('<input type="hidden" name="%s" value="%s"%s',
						$this->strName,
						StringUtil::specialchars($this->varValue),
						$this->strTagEnding);
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Translation;

use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\System;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Component\Translation\TranslatorInterface;

class Translator implements TranslatorInterface, TranslatorBagInterface
{
    /**
     * @var TranslatorInterface|TranslatorBagInterface
     */
    private $translator;

    /**
     * @var ContaoFramework
     */
    private $framework;

    public function __construct(TranslatorInterface $translator, ContaoFramework $framework)
    {
        $this->translator = $translator;
        $this->framework = $framework;
    }

    /**
     * {@inheritdoc}
     *
     * Gets the translation from Contao’s $GLOBALS['TL_LANG'] array if the message domain starts with
     * "contao_". The locale parameter is ignored in this case.
     */
    public function trans($id, array $parameters = [], $domain = null, $locale = null): string
    {
        // Forward to the default translator
        if (null === $domain || 0 !== strncmp($domain, 'contao_', 7)) {
            return $this->translator->trans($id, $parameters, $domain, $locale);
        }

        $this->framework->initialize();
        $this->loadLanguageFile(substr($domain, 7));

        $translated = $this->getFromGlobals($id);

        if (null === $translated) {
            return $id;
        }

        if (!empty($parameters)) {
            $translated = vsprintf($translated, $parameters);
        }

        return $translated;
    }

    /**
     * {@inheritdoc}
     */
    public function transChoice($id, $number, array $parameters = [], $domain = null, $locale = null): string
    {
        return $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
    }

    /**
     * {@inheritdoc}
     */
    public function setLocale($locale): ?string
    {
        return $this->translator->setLocale($locale);
    }

    /**
     * {@inheritdoc}
     */
    public function getLocale(): string
    {
        return $this->translator->getLocale();
    }

    /**
     * {@inheritdoc}
     */
    public function getCatalogue($locale = null): MessageCatalogueInterface
    {
        return $this->translator->getCatalogue($locale);
    }

    /**
     * Returns the labels from $GLOBALS['TL_LANG'] based on a message ID like "MSC.view".
     */
    private function getFromGlobals(string $id): ?string
    {
        // Split the ID into chunks allowing escaped dots (\.) and backslashes (\\)
        preg_match_all('/(?:\\\\[\.\\\\]|[^\.])++/', $id, $matches);
        $parts = preg_replace('/\\\\([\.\\\\])/', '$1', $matches[0]);

        $item = &$GLOBALS['TL_LANG'];

        foreach ($parts as $part) {
            if (!isset($item[$part])) {
                return null;
            }

            $item = &$item[$part];
        }

        return $item;
    }

    /**
     * Loads a Contao framework language file.
     */
    private function loadLanguageFile(string $name): void
    {
        /** @var System $system */
        $system = $this->framework->getAdapter(System::class);
        $system->loadLanguageFile($name);
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\EventListener;

class GlobalsMapListener
{
    /**
     * @var array
     */
    private $globals;

    public function __construct(array $globals)
    {
        $this->globals = $globals;
    }

    /**
     * Maps fragments to the globals array.
     */
    public function onInitializeSystem(): void
    {
        foreach ($this->globals as $key => $value) {
            if (\is_array($value) && isset($GLOBALS[$key]) && \is_array($GLOBALS[$key])) {
                $GLOBALS[$key] = array_replace_recursive($GLOBALS[$key], $value);
            } else {
                $GLOBALS[$key] = $value;
            }
        }
    }
}


<?php

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

namespace Contao;

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

/**
 * Back end alerts wizard.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class BackendAlerts extends Backend
{

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

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

		System::loadLanguageFile('default');
	}

	/**
	 * Run the controller and parse the template
	 *
	 * @return Response
	 */
	public function run()
	{
		$objTemplate = new BackendTemplate('be_alerts');
		$objTemplate->theme = Backend::getTheme();
		$objTemplate->base = Environment::get('base');
		$objTemplate->language = $GLOBALS['TL_LANGUAGE'];
		$objTemplate->title = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['systemMessages']);
		$objTemplate->host = Environment::get('host');
		$objTemplate->charset = Config::get('characterSet');
		$objTemplate->messages = Message::generateUnwrapped() . Backend::getSystemMessages();
		$objTemplate->noMessages = $GLOBALS['TL_LANG']['MSC']['noSystemMessages'];

		return $objTemplate->getResponse();
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Config\Dumper;

use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Filesystem\Filesystem;

/**
 * Combines multiple files into one PHP file.
 */
class CombinedFileDumper implements DumperInterface
{
    /**
     * @var Filesystem
     */
    private $filesystem;

    /**
     * @var LoaderInterface
     */
    private $loader;

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

    /**
     * @var string
     */
    private $header = "<?php\n"; // add a line-break to prevent the "unexpected $end" error

    public function __construct(Filesystem $filesystem, LoaderInterface $loader, string $cacheDir)
    {
        $this->filesystem = $filesystem;
        $this->loader = $loader;
        $this->cacheDir = $cacheDir;
    }

    /**
     * @throws \InvalidArgumentException
     */
    public function setHeader(string $header): void
    {
        if (0 !== strncmp($header, '<?php', 5)) {
            throw new \InvalidArgumentException('The file header must start with an opening PHP tag.');
        }

        $this->header = $header;
    }

    /**
     * {@inheritdoc}
     */
    public function dump($files, $cacheFile, array $options = []): void
    {
        $buffer = $this->header;
        $type = $options['type'] ?? null;

        foreach ((array) $files as $file) {
            $buffer .= $this->loader->load($file, $type);
        }

        $this->filesystem->dumpFile($this->cacheDir.'/'.$cacheFile, $buffer);
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Controller;

use Contao\CoreBundle\Response\InitializeControllerResponse;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Custom controller to support legacy entry points.
 *
 * @deprecated Deprecated in Contao 4.0, to be removed in Contao 5.0
 */
class InitializeController extends AbstractController
{
    /**
     * Initializes the Contao framework.
     *
     * @throws \RuntimeException
     *
     * @Route("/_contao/initialize", name="contao_initialize")
     */
    public function indexAction(): InitializeControllerResponse
    {
        @trigger_error('Custom entry points are deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

        $masterRequest = $this->get('request_stack')->getMasterRequest();

        if (null === $masterRequest) {
            throw new \RuntimeException('The request stack did not contain a master request.');
        }

        $realRequest = Request::createFromGlobals();

        // Necessary to generate the correct base path
        foreach (['REQUEST_URI', 'SCRIPT_NAME', 'SCRIPT_FILENAME', 'PHP_SELF'] as $name) {
            $realRequest->server->set(
                $name,
                str_replace(TL_SCRIPT, 'index.php', $realRequest->server->get($name))
            );
        }

        $realRequest->attributes->replace($masterRequest->attributes->all());

        // Initialize the framework with the real request
        $this->get('request_stack')->push($realRequest);
        $this->get('contao.framework')->initialize();

        // Add the master request again. When Kernel::handle() is finished,
        // it will pop the current request, resulting in the real request being active.
        $this->get('request_stack')->push($masterRequest);

        return new InitializeControllerResponse('', 204);
    }
}


<?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 calendar feeds
 *
 * @property integer $id
 * @property integer $tstamp
 * @property string  $title
 * @property string  $alias
 * @property string  $language
 * @property string  $calendars
 * @property string  $format
 * @property string  $source
 * @property integer $maxItems
 * @property string  $feedBase
 * @property string  $description
 * @property string  $feedName
 *
 * @method static CalendarFeedModel|null findById($id, array $opt=array())
 * @method static CalendarFeedModel|null findByPk($id, array $opt=array())
 * @method static CalendarFeedModel|null findByIdOrAlias($val, array $opt=array())
 * @method static CalendarFeedModel|null findOneBy($col, $val, array $opt=array())
 * @method static CalendarFeedModel|null findOneByTstamp($val, array $opt=array())
 * @method static CalendarFeedModel|null findOneByTitle($val, array $opt=array())
 * @method static CalendarFeedModel|null findOneByAlias($val, array $opt=array())
 * @method static CalendarFeedModel|null findOneByLanguage($val, array $opt=array())
 * @method static CalendarFeedModel|null findOneByCalendars($val, array $opt=array())
 * @method static CalendarFeedModel|null findOneByFormat($val, array $opt=array())
 * @method static CalendarFeedModel|null findOneBySource($val, array $opt=array())
 * @method static CalendarFeedModel|null findOneByMaxItems($val, array $opt=array())
 * @method static CalendarFeedModel|null findOneByFeedBase($val, array $opt=array())
 * @method static CalendarFeedModel|null findOneByDescription($val, array $opt=array())
 *
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findByTstamp($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findByTitle($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findByAlias($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findByLanguage($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findByCalendars($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findByFormat($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findBySource($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findByMaxItems($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findByFeedBase($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findByDescription($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findMultipleByIds($val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|null findBy($col, $val, array $opt=array())
 * @method static Collection|CalendarFeedModel[]|CalendarFeedModel|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 countByLanguage($val, array $opt=array())
 * @method static integer countByCalendars($val, array $opt=array())
 * @method static integer countByFormat($val, array $opt=array())
 * @method static integer countBySource($val, array $opt=array())
 * @method static integer countByMaxItems($val, array $opt=array())
 * @method static integer countByFeedBase($val, array $opt=array())
 * @method static integer countByDescription($val, array $opt=array())
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class CalendarFeedModel extends Model
{

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

	/**
	 * Find all feeds which include a certain calendar
	 *
	 * @param integer $intId      The calendar ID
	 * @param array   $arrOptions An optional options array
	 *
	 * @return Collection|CalendarFeedModel[]|CalendarFeedModel|null A collection of models or null if the calendar is not part of a feed
	 */
	public static function findByCalendar($intId, array $arrOptions=array())
	{
		$t = static::$strTable;

		return static::findBy(array("$t.calendars LIKE '%\"" . (int) $intId . "\"%'"), null, $arrOptions);
	}

	/**
	 * Find calendar feeds by their IDs
	 *
	 * @param array $arrIds     An array of calendar feed IDs
	 * @param array $arrOptions An optional options array
	 *
	 * @return Collection|CalendarFeedModel[]|CalendarFeedModel|null A collection of models or null if there are no feeds
	 */
	public static function findByIds($arrIds, array $arrOptions=array())
	{
		if (empty($arrIds) || !\is_array($arrIds))
		{
			return null;
		}

		$t = static::$strTable;

		return static::findBy(array("$t.id IN(" . implode(',', array_map('\intval', $arrIds)) . ")"), null, $arrOptions);
	}
}

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