<?php

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

namespace Contao;

/**
 * Provide methodes to render a pagination menu.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class Pagination
{

	/**
	 * Current page number
	 * @var integer
	 */
	protected $intPage;

	/**
	 * Total number of rows
	 * @var integer
	 */
	protected $intRows;

	/**
	 * Number of rows per page
	 * @var integer
	 */
	protected $intRowsPerPage;

	/**
	 * Total number of pages
	 * @var integer
	 */
	protected $intTotalPages;

	/**
	 * Total number of links
	 * @var integer
	 */
	protected $intNumberOfLinks;

	/**
	 * Label for the "<< first" link
	 * @var string
	 */
	protected $lblFirst;

	/**
	 * Label for the "< previous" link
	 * @var string
	 */
	protected $lblPrevious;

	/**
	 * Label for the "next >" link
	 * @var string
	 */
	protected $lblNext;

	/**
	 * Label for the "last >>" link
	 * @var string
	 */
	protected $lblLast;

	/**
	 * Label for "total pages"
	 * @var string
	 */
	protected $lblTotal;

	/**
	 * Template object
	 * @var Template
	 */
	protected $objTemplate;

	/**
	 * Show "<< first" and "last >>" links
	 * @var boolean
	 */
	protected $blnShowFirstLast = true;

	/**
	 * Request url
	 * @var string
	 */
	protected $strUrl = '';

	/**
	 * Page paramenter
	 * @var string
	 */
	protected $strParameter = 'page';

	/**
	 * Variable connector
	 * @var string
	 */
	protected $strVarConnector = '?';

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

	/**
	 * Data array
	 * @var array
	 */
	protected $blnForceParam = false;

	/**
	 * Set the number of rows, the number of results per pages and the number of links
	 *
	 * @param integer  $intRows          The number of rows
	 * @param integer  $intPerPage       The number of items per page
	 * @param integer  $intNumberOfLinks The number of links to generate
	 * @param string   $strParameter     The parameter name
	 * @param Template $objTemplate      The template object
	 * @param boolean  $blnForceParam    Force the URL parameter
	 */
	public function __construct($intRows, $intPerPage, $intNumberOfLinks=7, $strParameter='page', Template $objTemplate=null, $blnForceParam=false)
	{
		$this->intPage = 1;
		$this->intRows = (int) $intRows;
		$this->intRowsPerPage = (int) $intPerPage;
		$this->intNumberOfLinks = (int) $intNumberOfLinks;

		// Initialize default labels
		$this->lblFirst = $GLOBALS['TL_LANG']['MSC']['first'];
		$this->lblPrevious = $GLOBALS['TL_LANG']['MSC']['previous'];
		$this->lblNext = $GLOBALS['TL_LANG']['MSC']['next'];
		$this->lblLast = $GLOBALS['TL_LANG']['MSC']['last'];
		$this->lblTotal = $GLOBALS['TL_LANG']['MSC']['totalPages'];

		if (Input::get($strParameter) > 0)
		{
			$this->intPage = Input::get($strParameter);
		}

		$this->strParameter = $strParameter;

		if ($objTemplate === null)
		{
			$objTemplate = new FrontendTemplate('pagination');
		}

		$this->objTemplate = $objTemplate;
		$this->blnForceParam = $blnForceParam;
	}

	/**
	 * Return true if the pagination menu has a "<< first" link
	 *
	 * @return boolean True if the pagination menu has a "<< first" link
	 */
	public function hasFirst()
	{
		return ($this->blnShowFirstLast && $this->intPage > 2) ? true : false;
	}

	/**
	 * Return true if the pagination menu has a "< previous" link
	 *
	 * @return boolean True if the pagination menu has a "< previous" link
	 */
	public function hasPrevious()
	{
		return ($this->intPage > 1) ? true : false;
	}

	/**
	 * Return true if the pagination menu has a "next >" link
	 *
	 * @return boolean True if the pagination menu has a "next >" link
	 */
	public function hasNext()
	{
		return ($this->intPage < $this->intTotalPages) ? true : false;
	}

	/**
	 * Return true if the pagination menu has a "last >>" link
	 *
	 * @return boolean True if the pagination menu has a "last >>" link
	 */
	public function hasLast()
	{
		return ($this->blnShowFirstLast && $this->intPage < ($this->intTotalPages - 1)) ? true : false;
	}

	/**
	 * Generate the pagination menu and return it as HTML string
	 *
	 * @param string $strSeparator The separator string
	 *
	 * @return string The pagination menu as HTML string
	 */
	public function generate($strSeparator=' ')
	{
		if ($this->intRowsPerPage < 1)
		{
			return '';
		}

		$blnQuery = false;
		list($this->strUrl) = explode('?', Environment::get('request'), 2);

		// Prepare the URL
		foreach (preg_split('/&(amp;)?/', Environment::get('queryString'), -1, PREG_SPLIT_NO_EMPTY) as $fragment)
		{
			if (strpos($fragment, $this->strParameter . '=') === false)
			{
				$this->strUrl .= (!$blnQuery ? '?' : '&') . $fragment;
				$blnQuery = true;
			}
		}

		$this->strVarConnector = $blnQuery ? '&' : '?';
		$this->intTotalPages = ceil($this->intRows / $this->intRowsPerPage);

		// Return if there is only one page
		if ($this->intTotalPages < 2 || $this->intRows < 1)
		{
			return '';
		}

		if ($this->intPage > $this->intTotalPages)
		{
			$this->intPage = $this->intTotalPages;
		}

		$objTemplate = $this->objTemplate;

		$objTemplate->hasFirst = $this->hasFirst();
		$objTemplate->hasPrevious = $this->hasPrevious();
		$objTemplate->hasNext = $this->hasNext();
		$objTemplate->hasLast = $this->hasLast();

		// Deprecated since Contao 4.0, to be removed in Contao 5.0
		$objTemplate->items = function () use ($strSeparator)
		{
			@trigger_error('Using $pagination->items has been deprecated and will no longer work in Contao 5.0. Use $pagination->pages instead.', E_USER_DEPRECATED);

			return $this->getItemsAsString($strSeparator);
		};

		$objTemplate->pages = $this->getItemsAsArray();
		$objTemplate->total = sprintf($this->lblTotal, $this->intPage, $this->intTotalPages);

		$objTemplate->first = array
		(
			'link' => $this->lblFirst,
			'href' => $this->linkToPage(1),
			'title' => sprintf(StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['goToPage']), 1)
		);

		$objTemplate->previous = array
		(
			'link' => $this->lblPrevious,
			'href' => $this->linkToPage($this->intPage - 1),
			'title' => sprintf(StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['goToPage']), ($this->intPage - 1))
		);

		$objTemplate->next = array
		(
			'link' => $this->lblNext,
			'href' => $this->linkToPage($this->intPage + 1),
			'title' => sprintf(StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['goToPage']), ($this->intPage + 1))
		);

		$objTemplate->last = array
		(
			'link' => $this->lblLast,
			'href' => $this->linkToPage($this->intTotalPages),
			'title' => sprintf(StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['goToPage']), $this->intTotalPages)
		);

		$objTemplate->class = 'pagination-' . $this->strParameter;
		$objTemplate->pagination = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['pagination']);

		// Adding rel="prev" and rel="next" links is not possible
		// anymore with unique variable names (see #3515 and #4141)

		return $objTemplate->parse();
	}

	/**
	 * Generate all page links separated with the given argument and return them as string
	 *
	 * @param string $strSeparator The separator string
	 *
	 * @return string The page links as HTML string
	 */
	public function getItemsAsString($strSeparator=' ')
	{
		$arrLinks = array();

		foreach ($this->getItemsAsArray() as $arrItem)
		{
			if ($arrItem['href'] === null)
			{
				$arrLinks[] = sprintf('
%s
', $arrItem['page']);
			}
			else
			{
				$arrLinks[] = sprintf('
%s
', $arrItem['href'], $arrItem['title'], $arrItem['page']);
			}
		}

		return implode($strSeparator, $arrLinks);
	}

	/**
	 * Generate all page links and return them as array
	 *
	 * @return array The page links as array
	 */
	public function getItemsAsArray()
	{
		$arrLinks = array();

		$intNumberOfLinks = floor($this->intNumberOfLinks / 2);
		$intFirstOffset = $this->intPage - $intNumberOfLinks - 1;

		if ($intFirstOffset > 0)
		{
			$intFirstOffset = 0;
		}

		$intLastOffset = $this->intPage + $intNumberOfLinks - $this->intTotalPages;

		if ($intLastOffset < 0)
		{
			$intLastOffset = 0;
		}

		$intFirstLink = $this->intPage - $intNumberOfLinks - $intLastOffset;

		if ($intFirstLink < 1)
		{
			$intFirstLink = 1;
		}

		$intLastLink = $this->intPage + $intNumberOfLinks - $intFirstOffset;

		if ($intLastLink > $this->intTotalPages)
		{
			$intLastLink = $this->intTotalPages;
		}

		for ($i=$intFirstLink; $i<=$intLastLink; $i++)
		{
			if ($i == $this->intPage)
			{
				$arrLinks[] = array
				(
					'page'  => $i,
					'href'  => null,
					'title' => null
				);
			}
			else
			{
				$arrLinks[] = array
				(
					'page'  => $i,
					'href'  => $this->linkToPage($i),
					'title' => StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['goToPage'], $i))
				);
			}
		}

		return $arrLinks;
	}

	/**
	 * Generate a link and return the URL
	 *
	 * @param integer $intPage The page ID
	 *
	 * @return string The URL string
	 */
	protected function linkToPage($intPage)
	{
		if ($intPage <= 1 && !$this->blnForceParam)
		{
			return ampersand($this->strUrl);
		}
		else
		{
			return ampersand($this->strUrl) . $this->strVarConnector . $this->strParameter . '=' . $intPage;
		}
	}
}

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


<?php

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

namespace Contao;

use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;

/**
 * Provide methods to handle input field "page tree".
 *
 * @property array  $rootNodes
 * @property string $fieldType
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class PageSelector extends Widget
{

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

	/**
	 * Path nodes
	 * @var array
	 */
	protected $arrNodes = array();

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

	/**
	 * Load the database object
	 *
	 * @param array $arrAttributes
	 */
	public function __construct($arrAttributes=null)
	{
		$this->import(Database::class, 'Database');
		parent::__construct($arrAttributes);
	}

	/**
	 * Generate the widget and return it as string
	 *
	 * @return string
	 */
	public function generate()
	{
		$this->import(BackendUser::class, 'User');

		/** @var AttributeBagInterface $objSessionBag */
		$objSessionBag = System::getContainer()->get('session')->getBag('contao_backend');

		// Store the keyword
		if (Input::post('FORM_SUBMIT') == 'item_selector')
		{
			$strKeyword = ltrim(Input::postRaw('keyword'), '*');

			// Make sure the regular expression is valid
			if ($strKeyword != '')
			{
				try
				{
					$this->Database->prepare("SELECT * FROM tl_page WHERE title REGEXP ?")
								   ->limit(1)
								   ->execute($strKeyword);
				}
				catch (\Exception $e)
				{
					$strKeyword = '';
				}
			}

			$objSessionBag->set('page_selector_search', $strKeyword);
			$this->reload();
		}

		$tree = '';
		$this->getPathNodes();
		$for = $objSessionBag->get('page_selector_search');
		$arrFound = array();

		// Search for a specific page
		if ($for != '')
		{
			// Wrap in a try catch block in case the regular expression is invalid (see #7743)
			try
			{
				$strPattern = "CAST(title AS CHAR) REGEXP ?";

				if (substr(Config::get('dbCollation'), -3) == '_ci')
				{
					$strPattern = "LOWER(CAST(title AS CHAR)) REGEXP LOWER(?)";
				}

				$objRoot = $this->Database->prepare("SELECT id FROM tl_page WHERE $strPattern GROUP BY id")
										  ->execute($for);

				if ($objRoot->numRows < 1)
				{
					$GLOBALS['TL_DCA']['tl_page']['list']['sorting']['root'] = array(0);
				}
				else
				{
					$arrIds = array();

					// Respect existing limitations
					if (\is_array($this->rootNodes))
					{
						while ($objRoot->next())
						{
							// Predefined node set (see #3563)
							if (\count(array_intersect($this->rootNodes, $this->Database->getParentRecords($objRoot->id, 'tl_page'))) > 0)
							{
								$arrFound[] = $objRoot->id;
								$arrIds[] = $objRoot->id;
							}
						}
					}
					elseif ($this->User->isAdmin)
					{
						// Show all pages to admins
						while ($objRoot->next())
						{
							$arrFound[] = $objRoot->id;
							$arrIds[] = $objRoot->id;
						}
					}
					else
					{
						while ($objRoot->next())
						{
							// Show only mounted pages to regular users
							if (\count(array_intersect($this->User->pagemounts, $this->Database->getParentRecords($objRoot->id, 'tl_page'))) > 0)
							{
								$arrFound[] = $objRoot->id;
								$arrIds[] = $objRoot->id;
							}
						}
					}

					$GLOBALS['TL_DCA']['tl_page']['list']['sorting']['root'] = array_unique($arrIds);
				}
			}
			catch (\Exception $e) {}
		}

		$strNode = $objSessionBag->get('tl_page_picker');

		// Unset the node if it is not within the predefined node set (see #5899)
		if ($strNode > 0 && \is_array($this->rootNodes))
		{
			if (!\in_array($strNode, $this->Database->getChildRecords($this->rootNodes, 'tl_page')))
			{
				$objSessionBag->remove('tl_page_picker');
			}
		}

		// Add the breadcrumb menu
		if (Input::get('do') != 'page')
		{
			Backend::addPagesBreadcrumb('tl_page_picker');
		}

		// Root nodes (breadcrumb menu)
		if (!empty($GLOBALS['TL_DCA']['tl_page']['list']['sorting']['root']))
		{
			$root = $GLOBALS['TL_DCA']['tl_page']['list']['sorting']['root'];

			// Allow only those roots that are allowed in root nodes
			if (\is_array($this->rootNodes))
			{
				$root = array_intersect(array_merge($this->rootNodes, $this->Database->getChildRecords($this->rootNodes, 'tl_page')), $root);

				if (empty($root))
				{
					$root = $this->rootNodes;

					// Hide the breadcrumb
					$GLOBALS['TL_DCA']['tl_page']['list']['sorting']['breadcrumb'] = '';
				}
			}

			$nodes = $this->eliminateNestedPages($root);

			foreach ($nodes as $node)
			{
				$tree .= $this->renderPagetree($node, -20, false, false, $arrFound);
			}
		}

		// Predefined node set (see #3563)
		elseif (\is_array($this->rootNodes))
		{
			$nodes = $this->eliminateNestedPages($this->rootNodes);

			foreach ($nodes as $node)
			{
				$tree .= $this->renderPagetree($node, -20, false, false, $arrFound);
			}
		}

		// Show all pages to admins
		elseif ($this->User->isAdmin)
		{
			$objPage = $this->Database->prepare("SELECT id FROM tl_page WHERE pid=? ORDER BY sorting")
									  ->execute(0);

			while ($objPage->next())
			{
				$tree .= $this->renderPagetree($objPage->id, -20, false, false, $arrFound);
			}
		}

		// Show only mounted pages to regular users
		else
		{
			$nodes = $this->eliminateNestedPages($this->User->pagemounts);

			foreach ($nodes as $node)
			{
				$tree .= $this->renderPagetree($node, -20, false, false, $arrFound);
			}
		}

		// Select all checkboxes
		if ($this->fieldType == 'checkbox')
		{
			$strReset = "\n" . '

strId . '" class="tl_change_selected">' . $GLOBALS['TL_LANG']['MSC']['selectAll'] . ' <input type="checkbox" id="check_all_' . $this->strId . '" class="tl_tree_checkbox" value="" onclick="Backend.toggleCheckboxGroup(this,\'' . $this->strName . '\')">

';
		}
		// Reset radio button selection
		else
		{
			$strReset = "\n" . '

strId . '" class="tl_change_selected">' . $GLOBALS['TL_LANG']['MSC']['resetSelected'] . ' <input type="radio" name="' . $this->strName . '" id="reset_' . $this->strName . '" class="tl_tree_radio" value="" onfocus="Backend.getScrollOffset()">

';
		}

		// Return the tree
		return '
strClass != '') ? ' ' . $this->strClass : '').'" id="'.$this->strId.'" data-callback="reloadPagetree" data-inserttag="link_url">

'.Image::getHtml($GLOBALS['TL_DCA']['tl_page']['list']['sorting']['icon'] ?: 'pagemounts.svg').' '.$GLOBALS['TL_LANG']['MOD']['page'][0].'

strId.'_parent">
'.$tree.$strReset.'

';
	}

	/**
	 * Generate a particular subpart of the page tree and return it as HTML string
	 *
	 * @param integer $id
	 * @param string  $strField
	 * @param integer $level
	 *
	 * @return string
	 */
	public function generateAjax($id, $strField, $level)
	{
		if (!Environment::get('isAjaxRequest'))
		{
			return '';
		}

		$this->strField = $strField;
		$this->loadDataContainer($this->strTable);

		// Load current values
		switch ($GLOBALS['TL_DCA'][$this->strTable]['config']['dataContainer'])
		{
			case 'File':
				if (Config::get($this->strField) != '')
				{
					$this->varValue = Config::get($this->strField);
				}
				break;

			case 'Table':
				if (!$this->Database->fieldExists($this->strField, $this->strTable))
				{
					break;
				}

				$objField = $this->Database->prepare("SELECT " . Database::quoteIdentifier($this->strField) . " FROM " . $this->strTable . " WHERE id=?")
										   ->limit(1)
										   ->execute($this->strId);

				if ($objField->numRows)
				{
					$this->varValue = StringUtil::deserialize($objField->{$this->strField});
				}
				break;
		}

		$this->getPathNodes();

		// Load the requested nodes
		$tree = '';
		$level *= 20;

		$objPage = $this->Database->prepare("SELECT id FROM tl_page WHERE pid=? ORDER BY sorting")
								  ->execute($id);

		while ($objPage->next())
		{
			$tree .= $this->renderPagetree($objPage->id, $level);
		}

		return $tree;
	}

	/**
	 * Recursively render the pagetree
	 *
	 * @param integer $id
	 * @param integer $intMargin
	 * @param boolean $protectedPage
	 * @param boolean $blnNoRecursion
	 * @param array   $arrFound
	 *
	 * @return string
	 */
	protected function renderPagetree($id, $intMargin, $protectedPage=false, $blnNoRecursion=false, $arrFound=array())
	{
		/** @var AttributeBagInterface $objSessionBag */
		$objSessionBag = System::getContainer()->get('session')->getBag('contao_backend');

		$session = $objSessionBag->all();

		$flag = substr($this->strField, 0, 2);
		$node = 'tree_' . $this->strTable . '_' . $this->strField;
		$xtnode = 'tree_' . $this->strTable . '_' . $this->strName;

		// Get the session data and toggle the nodes
		if (Input::get($flag.'tg'))
		{
			$session[$node][Input::get($flag.'tg')] = (isset($session[$node][Input::get($flag.'tg')]) && $session[$node][Input::get($flag.'tg')] == 1) ? 0 : 1;
			$objSessionBag->replace($session);
			$this->redirect(preg_replace('/(&(amp;)?|\?)'.$flag.'tg=[^& ]*/i', '', Environment::get('request')));
		}

		$objPage = $this->Database->prepare("SELECT id, alias, type, protected, published, start, stop, hide, title FROM tl_page WHERE id=?")
								  ->limit(1)
								  ->execute($id);

		// Return if there is no result
		if ($objPage->numRows < 1)
		{
			return '';
		}

		$return = '';
		$intSpacing = 20;
		$childs = array();

		// Check whether there are child records
		if (!$blnNoRecursion)
		{
			$objNodes = $this->Database->prepare("SELECT id FROM tl_page WHERE pid=?" . (!empty($arrFound) ? " AND id IN(" . implode(',', array_map('\intval', $arrFound)) . ")" : '') . " ORDER BY sorting")
									   ->execute($id);

			if ($objNodes->numRows)
			{
				$childs = $objNodes->fetchEach('id');
			}
		}

		$return .= "\n    " . '
type == 'root') ? 'tl_folder' : 'tl_file').' toggle_select hover-div">
';

		$folderAttribute = 'style="margin-left:20px"';
		$session[$node][$id] = is_numeric($session[$node][$id]) ? $session[$node][$id] : 0;
		$level = ($intMargin / $intSpacing + 1);
		$blnIsOpen = (!empty($arrFound) || $session[$node][$id] == 1 || \in_array($id, $this->arrNodes));

		if (!empty($childs))
		{
			$folderAttribute = '';
			$img = $blnIsOpen ? 'folMinus.svg' : 'folPlus.svg';
			$alt = $blnIsOpen ? $GLOBALS['TL_LANG']['MSC']['collapseNode'] : $GLOBALS['TL_LANG']['MSC']['expandNode'];
			$return .= 'strField.'\',\''.$this->strName.'\','.$level.')">'.Image::getHtml($img, '', 'style="margin-right:2px"').'';
		}

		// Set the protection status
		$objPage->protected = ($objPage->protected || $protectedPage);

		// Add the current page
		if (!empty($childs))
		{
			$return .= Image::getHtml($this->getPageStatusIcon($objPage), '', $folderAttribute).' id) . '" title="'.StringUtil::specialchars($objPage->title . ' (' . $objPage->alias . Config::get('urlSuffix') . ')').'">'.(($objPage->type == 'root') ? '' : '').$objPage->title.(($objPage->type == 'root') ? '' : '').'

';
		}
		else
		{
			$return .= Image::getHtml($this->getPageStatusIcon($objPage), '', $folderAttribute).' '.(($objPage->type == 'root') ? '' : '').$objPage->title.(($objPage->type == 'root') ? '' : '').'

';
		}

		// Add checkbox or radio button
		switch ($this->fieldType)
		{
			case 'checkbox':
				$return .= '<input type="checkbox" name="'.$this->strName.'[]" id="'.$this->strName.'_'.$id.'" class="tl_tree_checkbox" value="'.StringUtil::specialchars($id).'" onfocus="Backend.getScrollOffset()"'.static::optionChecked($id, $this->varValue).'>';
				break;

			default:
			case 'radio':
				$return .= '<input type="radio" name="'.$this->strName.'" id="'.$this->strName.'_'.$id.'" class="tl_tree_radio" value="'.StringUtil::specialchars($id).'" onfocus="Backend.getScrollOffset()"'.static::optionChecked($id, $this->varValue).'>';
				break;
		}

		$return .= '

';

		// Begin a new submenu
		if ($blnIsOpen || (!empty($childs) && $objSessionBag->get('page_selector_search') != ''))
		{
			$return .= '

';

			for ($k=0, $c=\count($childs); $k<$c; $k++)
			{
				$return .= $this->renderPagetree($childs[$k], ($intMargin + $intSpacing), $objPage->protected, $blnNoRecursion, $arrFound);
			}

			$return .= '

';
		}

		return $return;
	}

	/**
	 * Get the IDs of all parent pages of the selected pages, so they are expanded automatically
	 */
	protected function getPathNodes()
	{
		if (!$this->varValue)
		{
			return;
		}

		if (!\is_array($this->varValue))
		{
			$this->varValue = array($this->varValue);
		}

		foreach ($this->varValue as $id)
		{
			$arrPids = $this->Database->getParentRecords($id, 'tl_page');
			array_shift($arrPids); // the first element is the ID of the page itself
			$this->arrNodes = array_merge($this->arrNodes, $arrPids);
		}
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Analyzer;

class HtaccessAnalyzer
{
    /**
     * @var \SplFileInfo
     */
    private $file;

    /**
     * @throws \InvalidArgumentException
     */
    public function __construct(\SplFileInfo $file)
    {
        if (!$file->isFile()) {
            throw new \InvalidArgumentException(sprintf('%s is not a file.', $file));
        }

        $this->file = $file;
    }

    public static function create(\SplFileInfo $file): self
    {
        return new static($file);
    }

    /**
     * Checks whether the .htaccess file grants access via HTTP.
     */
    public function grantsAccess(): bool
    {
        $content = array_filter(file((string) $this->file));

        foreach ($content as $line) {
            if ($this->hasRequireGranted($line)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Scans a line for an access definition.
     */
    private function hasRequireGranted(string $line): bool
    {
        if ($this->isComment($line)) {
            return false;
        }

        return (false !== stripos($line, 'Allow from all')) || (false !== stripos($line, 'Require all granted'));
    }

    /**
     * Checks whether a line is a comment.
     */
    private function isComment(string $line): bool
    {
        return 0 === strncmp('#', ltrim($line), 1);
    }
}


<?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 help wizard.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class BackendHelp extends Backend
{

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

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

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

	/**
	 * Run the controller and parse the template
	 *
	 * @return Response
	 */
	public function run()
	{
		$table = Input::get('table');
		$field = Input::get('field');

		System::loadLanguageFile($table);
		$this->loadDataContainer($table);

		$objTemplate = new BackendTemplate('be_help');
		$objTemplate->rows = array();
		$objTemplate->explanation = '';

		$arrData = $GLOBALS['TL_DCA'][$table]['fields'][$field];

		// Add the reference
		if (!empty($arrData['reference']))
		{
			$rows = array();

			if (\is_array($arrData['options']))
			{
				$options = $arrData['options'];
			}
			elseif (\is_array($arrData['options_callback']))
			{
				$this->import($arrData['options_callback'][0]);
				$options = $this->{$arrData['options_callback'][0]}->{$arrData['options_callback'][1]}(new DC_Table($table));
			}
			elseif (\is_callable($arrData['options_callback']))
			{
				$options = $arrData['options_callback']();
			}
			else
			{
				$options = array_keys($arrData['reference']);
			}

			// Unset the predefined image sizes
			unset($options['image_sizes']);

			foreach ($options as $key=>$option)
			{
				if (\is_array($option))
				{
					if (\is_array($arrData['reference'][$key]))
					{
						$rows[] = array('headspan', $arrData['reference'][$key][0]);
					}
					else
					{
						$rows[] = array('headspan', $arrData['reference'][$key]);
					}

					foreach ($option as $opt)
					{
						$rows[] = $arrData['reference'][$opt];
					}
				}
				else
				{
					if (isset($arrData['reference'][$key]))
					{
						$rows[] = $arrData['reference'][$key];
					}
					elseif (\is_array($arrData['reference'][$option]))
					{
						$rows[] = $arrData['reference'][$option];
					}
					else
					{
						$rows[] = array('headspan', $arrData['reference'][$option]);
					}
				}
			}

			$objTemplate->rows = $rows;
		}

		// Add an explanation
		if (isset($arrData['explanation']))
		{
			System::loadLanguageFile('explain');
			$key = $arrData['explanation'];

			if (!\is_array($GLOBALS['TL_LANG']['XPL'][$key]))
			{
				$objTemplate->explanation = trim($GLOBALS['TL_LANG']['XPL'][$key]);
			}
			else
			{
				$objTemplate->rows = $GLOBALS['TL_LANG']['XPL'][$key];
			}
		}

		$objTemplate->theme = Backend::getTheme();
		$objTemplate->base = Environment::get('base');
		$objTemplate->language = $GLOBALS['TL_LANGUAGE'];
		$objTemplate->title = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['helpWizardTitle']);
		$objTemplate->host = Environment::get('host');
		$objTemplate->charset = Config::get('characterSet');
		$objTemplate->headline = $arrData['label'][0] ?: $field;
		$objTemplate->helpWizard = $GLOBALS['TL_LANG']['MSC']['helpWizard'];

		return $objTemplate->getResponse();
	}
}

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


<?php

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

namespace Contao\Filter;

use Contao\Folder;
use Contao\StringUtil;

/**
 * Filters a directory listing
 *
 * The class filters dot files and folders, which are excluded from
 * being synchronized, from a directory listing.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class SyncExclude extends \RecursiveFilterIterator
{

	/**
	 * Check whether the current element of the iterator is acceptable
	 *
	 * @return boolean True if the element is acceptable
	 */
	public function accept()
	{
		// The resource is to be ignored
		if (strncmp($this->current()->getFilename(), '.', 1) === 0)
		{
			return false;
		}

		$strPath = $this->current()->getPathname();

		if (is_file($strPath))
		{
			$strPath = \dirname($strPath);
		}

		$objFolder = new Folder(StringUtil::stripRootDir($strPath));

		return !$objFolder->isUnsynchronized();
	}
}

class_alias(SyncExclude::class, 'Filter\SyncExclude');


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Security\Authentication;

use Contao\CoreBundle\Monolog\ContaoContext;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler;

class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler
{
    /**
     * Logs the security exception to the Contao back end.
     *
     * @throws \RuntimeException
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
    {
        if (null === $this->logger) {
            return parent::onAuthenticationFailure($request, $exception);
        }

        if ($exception instanceof AccountStatusException && ($user = $exception->getUser()) instanceof UserInterface) {
            $username = $user->getUsername();
        } else {
            $username = $request->request->get('username');
        }

        $this->logger->info(
            $exception->getMessage(),
            ['contao' => new ContaoContext(__METHOD__, ContaoContext::ACCESS, $username)]
        );

        return parent::onAuthenticationFailure($request, $exception);
    }
}


<?php

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

namespace Contao;

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

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

	/**
	 * Files model
	 * @var FilesModel
	 */
	protected $objFilesModel;

	/**
	 * Return if the image does not exist
	 *
	 * @return string
	 */
	public function generate()
	{
		if ($this->singleSRC == '')
		{
			return '';
		}

		$objFile = FilesModel::findByUuid($this->singleSRC);

		if ($objFile === null || !is_file(System::getContainer()->getParameter('kernel.project_dir') . '/' . $objFile->path))
		{
			return '';
		}

		$this->singleSRC = $objFile->path;
		$this->objFilesModel = $objFile;

		return parent::generate();
	}

	/**
	 * Generate the content element
	 */
	protected function compile()
	{
		$this->arrData['floating'] = '';

		$this->addImageToTemplate($this->Template, $this->arrData, null, null, $this->objFilesModel);
	}
}

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


<?php

/**
 * Contao Open Source CMS
 *
 * (c) Leo Feyer
 *
 * @license LGPL-3.0-or-later
 */

namespace Contao;

use Contao\CoreBundle\Fragment\Reference\ContentElementReference;

/**
 * Proxy for new content element fragments so they are accessible via $GLOBALS['TL_CTE'].
 *
 * @author Yanick Witschi <https://github.com/toflar>
 */
class ContentProxy extends ContentElement
{
	/**
	 * {@inheritdoc}
	 */
	public function generate()
	{
		$reference = new ContentElementReference($this->objModel, $this->strColumn);

		if ('BE' === TL_MODE)
		{
			$reference->setBackendScope();
		}

		return System::getContainer()->get('fragment.handler')->render($reference);
	}

	/**
	 * {@inheritdoc}
	 */
	protected function compile()
	{
		// noop
	}
}

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


<?php

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

namespace Contao;

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

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

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

	/**
	 * Parse the template file and return it as string
	 *
	 * @param array $arrAttributes An optional attributes array
	 *
	 * @return string The template markup
	 */
	public function parse($arrAttributes=null)
	{
		if (TL_MODE == 'BE')
		{
			$this->html = htmlspecialchars($this->html);
		}

		return parent::parse($arrAttributes);
	}

	/**
	 * Generate the widget and return it as string
	 *
	 * @return string The widget markup
	 */
	public function generate()
	{
		return (TL_MODE == 'FE') ? $this->html : htmlspecialchars($this->html);
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\ManagerBundle\Command;

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\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

class InstallWebDirCommand extends Command
{
    /**
     * @var Filesystem
     */
    private $fs;

    /**
     * @var SymfonyStyle
     */
    private $io;

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

    public function __construct(string $rootDir)
    {
        $this->rootDir = $rootDir;

        parent::__construct();
    }

    /**
     * {@inheritdoc}
     */
    protected function configure(): void
    {
        $this
            ->setName('contao:install-web-dir')
            ->setDefinition([
                new InputArgument('target', InputArgument::OPTIONAL, 'The target directory', 'web'),
            ])
            ->setDescription('Installs the files in the "web" directory')
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->fs = new Filesystem();
        $this->io = new SymfonyStyle($input, $output);

        $webDir = $this->rootDir.'/'.rtrim($input->getArgument('target'), '/');

        $this->addHtaccess($webDir);
        $this->addFiles($webDir);
        $this->purgeOldFiles($webDir);

        return 0;
    }

    /**
     * Adds the .htaccess file or merges it with an existing one.
     */
    private function addHtaccess(string $webDir): void
    {
        $htaccess = __DIR__.'/../Resources/skeleton/web/.htaccess';

        if (!file_exists($webDir.'/.htaccess')) {
            $this->fs->copy($htaccess, $webDir.'/.htaccess', true);
            $this->io->writeln('Added the <comment>web/.htaccess</comment> file.');

            return;
        }

        $existingContent = file_get_contents($webDir.'/.htaccess');

        // Return if there already is a rewrite rule
        if (preg_match('/^\s*RewriteRule\s/im', $existingContent)) {
            return;
        }

        $this->fs->dumpFile($webDir.'/.htaccess', $existingContent."\n\n".file_get_contents($htaccess));
        $this->io->writeln('Updated the <comment>web/.htaccess</comment> file.');
    }

    /**
     * Adds files from Resources/skeleton/web to the application's web directory.
     */
    private function addFiles(string $webDir): void
    {
        /** @var SplFileInfo[] $finder */
        $finder = Finder::create()->files()->in(__DIR__.'/../Resources/skeleton/web');

        foreach ($finder as $file) {
            if ($this->isExistingOptionalFile($file, $webDir)) {
                continue;
            }

            $this->fs->copy($file->getPathname(), $webDir.'/'.$file->getRelativePathname(), true);
            $this->io->writeln(sprintf('Added the <comment>web/%s</comment> file.', $file->getFilename()));
        }
    }

    /**
     * Purges old entry points.
     */
    private function purgeOldFiles(string $webDir): void
    {
        if (file_exists($webDir.'/app_dev.php')) {
            $this->fs->remove($webDir.'/app_dev.php');
            $this->io->writeln('Deleted the <comment>web/app_dev.php</comment> file.');
        }

        if (file_exists($webDir.'/install.php')) {
            $this->fs->remove($webDir.'/install.php');
            $this->io->writeln('Deleted the <comment>web/install.php</comment> file.');
        }
    }

    /**
     * Checks if an optional file exists.
     */
    private function isExistingOptionalFile(SplFileInfo $file, string $webDir): bool
    {
        $path = $file->getRelativePathname();

        return 'robots.txt' === $path && $this->fs->exists($webDir.'/'.$path);
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Security\User;

use Contao\Config;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Security\Exception\LockedException;
use Contao\Date;
use Contao\FrontendUser;
use Contao\User;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class UserChecker implements UserCheckerInterface
{
    /**
     * @var ContaoFramework
     */
    private $framework;

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

    /**
     * {@inheritdoc}
     */
    public function checkPreAuth(UserInterface $user): void
    {
        if (!$user instanceof User) {
            return;
        }

        $this->checkIfAccountIsLocked($user);
        $this->checkIfAccountIsDisabled($user);
        $this->checkIfLoginIsAllowed($user);
        $this->checkIfAccountIsActive($user);
    }

    /**
     * {@inheritdoc}
     */
    public function checkPostAuth(UserInterface $user): void
    {
    }

    /**
     * @throws LockedException
     */
    private function checkIfAccountIsLocked(User $user): void
    {
        $lockedSeconds = $user->locked - time();

        if ($lockedSeconds <= 0) {
            return;
        }

        $ex = new LockedException(
            $lockedSeconds,
            sprintf('User "%s" has been locked for %s minutes', $user->username, ceil($lockedSeconds / 60))
        );

        $ex->setUser($user);

        throw $ex;
    }

    private function checkIfAccountIsDisabled(User $user): void
    {
        if (!$user->disable) {
            return;
        }

        $ex = new DisabledException('The account has been disabled');
        $ex->setUser($user);

        throw $ex;
    }

    /**
     * Checks wether login is allowed (front end only).
     */
    private function checkIfLoginIsAllowed(User $user): void
    {
        if (!$user instanceof FrontendUser || $user->login) {
            return;
        }

        $ex = new DisabledException(sprintf('User "%s" is not allowed to log in', $user->username));
        $ex->setUser($user);

        throw $ex;
    }

    /**
     * Checks whether the account is not active yet or not anymore.
     */
    private function checkIfAccountIsActive(User $user): void
    {
        /** @var Config $config */
        $config = $this->framework->getAdapter(Config::class);

        $start = (int) $user->start;
        $stop = (int) $user->stop;
        $time = Date::floorToMinute(time());
        $notActiveYet = $start && $start > $time;
        $notActiveAnymore = $stop && $stop <= ($time + 60);
        $logMessage = '';

        if ($notActiveYet) {
            $logMessage = sprintf(
                'The account is not active yet (activation date: %s)',
                Date::parse($config->get('dateFormat'), $start)
            );
        }

        if ($notActiveAnymore) {
            $logMessage = sprintf(
                'The account is not active anymore (deactivation date: %s)',
                Date::parse($config->get('dateFormat'), $stop)
            );
        }

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

        $ex = new DisabledException($logMessage);
        $ex->setUser($user);

        throw $ex;
    }
}


<?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\CoreBundle\Framework\ContaoFramework;
use Contao\FilesModel;
use Contao\Image\Image;
use Contao\Image\ImageInterface;
use Contao\Image\ImportantPart;
use Contao\Image\ImportantPartInterface;
use Contao\Image\ResizeConfiguration;
use Contao\Image\ResizeConfigurationInterface;
use Contao\Image\ResizeOptions;
use Contao\Image\ResizerInterface;
use Contao\ImageSizeModel;
use Imagine\Image\Box;
use Imagine\Image\ImagineInterface;
use Imagine\Image\Point;
use Symfony\Component\Filesystem\Filesystem;

class ImageFactory implements ImageFactoryInterface
{
    /**
     * @var ResizerInterface
     */
    private $resizer;

    /**
     * @var ImagineInterface
     */
    private $imagine;

    /**
     * @var ImagineInterface
     */
    private $imagineSvg;

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

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

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

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

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

    public function __construct(ResizerInterface $resizer, ImagineInterface $imagine, ImagineInterface $imagineSvg, Filesystem $filesystem, ContaoFramework $framework, bool $bypassCache, array $imagineOptions, array $validExtensions)
    {
        $this->resizer = $resizer;
        $this->imagine = $imagine;
        $this->imagineSvg = $imagineSvg;
        $this->filesystem = $filesystem;
        $this->framework = $framework;
        $this->bypassCache = $bypassCache;
        $this->imagineOptions = $imagineOptions;
        $this->validExtensions = $validExtensions;
    }

    /**
     * {@inheritdoc}
     */
    public function create($path, $size = null, $targetPath = null): ImageInterface
    {
        if ($path instanceof ImageInterface) {
            $image = $path;
        } else {
            $fileExtension = strtolower(pathinfo($path, PATHINFO_EXTENSION));

            if (\in_array($fileExtension, ['svg', 'svgz'], true)) {
                $imagine = $this->imagineSvg;
            } else {
                $imagine = $this->imagine;
            }

            if (!\in_array($fileExtension, $this->validExtensions, true)) {
                throw new \InvalidArgumentException(
                    sprintf('Image type "%s" was not allowed to be processed', $fileExtension)
                );
            }

            $image = new Image((string) $path, $imagine, $this->filesystem);
        }

        if ($size instanceof ResizeConfigurationInterface) {
            /** @var ResizeConfigurationInterface $resizeConfig */
            $resizeConfig = $size;
            $importantPart = null;
        } else {
            [$resizeConfig, $importantPart] = $this->createConfig($size, $image);
        }

        if (!\is_object($path) || !($path instanceof ImageInterface)) {
            if (null === $importantPart) {
                $importantPart = $this->createImportantPart($image);
            }

            $image->setImportantPart($importantPart);
        }

        if (null === $targetPath && $resizeConfig->isEmpty()) {
            return $image;
        }

        return $this->resizer->resize(
            $image,
            $resizeConfig,
            (new ResizeOptions())
                ->setImagineOptions($this->imagineOptions)
                ->setTargetPath($targetPath)
                ->setBypassCache($this->bypassCache)
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getImportantPartFromLegacyMode(ImageInterface $image, $mode): ImportantPartInterface
    {
        if (1 !== substr_count($mode, '_')) {
            throw new \InvalidArgumentException(sprintf('"%s" is not a legacy resize mode', $mode));
        }

        $importantPart = [
            0,
            0,
            $image->getDimensions()->getSize()->getWidth(),
            $image->getDimensions()->getSize()->getHeight(),
        ];

        [$modeX, $modeY] = explode('_', $mode);

        if ('left' === $modeX) {
            $importantPart[2] = 1;
        } elseif ('right' === $modeX) {
            $importantPart[0] = $importantPart[2] - 1;
            $importantPart[2] = 1;
        }

        if ('top' === $modeY) {
            $importantPart[3] = 1;
        } elseif ('bottom' === $modeY) {
            $importantPart[1] = $importantPart[3] - 1;
            $importantPart[3] = 1;
        }

        return new ImportantPart(
            new Point($importantPart[0], $importantPart[1]),
            new Box($importantPart[2], $importantPart[3])
        );
    }

    /**
     * Creates a resize configuration object.
     *
     * @param int|array|null $size An image size or an array with width, height and resize mode
     *
     * @return (ResizeConfigurationInterface|ImportantPartInterface|null)[]
     */
    private function createConfig($size, ImageInterface $image): array
    {
        if (!\is_array($size)) {
            $size = [0, 0, $size];
        }

        $config = new ResizeConfiguration();

        if (isset($size[2]) && is_numeric($size[2])) {
            /** @var ImageSizeModel $imageModel */
            $imageModel = $this->framework->getAdapter(ImageSizeModel::class);
            $imageSize = $imageModel->findByPk($size[2]);

            if (null !== $imageSize) {
                $config
                    ->setWidth($imageSize->width)
                    ->setHeight($imageSize->height)
                    ->setMode($imageSize->resizeMode)
                    ->setZoomLevel($imageSize->zoom)
                ;
            }

            return [$config, null];
        }

        if (!empty($size[0])) {
            $config->setWidth($size[0]);
        }
        if (!empty($size[1])) {
            $config->setHeight($size[1]);
        }

        if (!isset($size[2]) || 1 !== substr_count($size[2], '_')) {
            if (!empty($size[2])) {
                $config->setMode($size[2]);
            }

            return [$config, null];
        }

        $config->setMode(ResizeConfigurationInterface::MODE_CROP);

        return [$config, $this->getImportantPartFromLegacyMode($image, $size[2])];
    }

    /**
     * Fetches the important part from the database.
     */
    private function createImportantPart(ImageInterface $image): ?ImportantPart
    {
        /** @var FilesModel $filesModel */
        $filesModel = $this->framework->getAdapter(FilesModel::class);
        $file = $filesModel->findByPath($image->getPath());

        if (null === $file || !$file->importantPartWidth || !$file->importantPartHeight) {
            return null;
        }

        $imageSize = $image->getDimensions()->getSize();

        if (
            $file->importantPartX + $file->importantPartWidth > $imageSize->getWidth()
            || $file->importantPartY + $file->importantPartHeight > $imageSize->getHeight()
        ) {
            return null;
        }

        return new ImportantPart(
            new Point((int) $file->importantPartX, (int) $file->importantPartY),
            new Box((int) $file->importantPartWidth, (int) $file->importantPartHeight)
        );
    }
}


<?php

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

namespace Contao;

/**
 * Class FormSelectMenu
 *
 * @property integer $mSize
 * @property boolean $mandatory
 * @property boolean $multiple
 * @property array   $options
 * @property boolean $chosen
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class FormSelectMenu 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_select';

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

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

			case 'mSize':
				if ($this->multiple)
				{
					$this->arrAttributes['size'] = $varValue;
				}
				break;

			case 'multiple':
				if ($varValue != '')
				{
					$this->arrAttributes['multiple'] = 'multiple';
				}
				break;

			case 'options':
				$this->arrOptions = StringUtil::deserialize($varValue);
				break;

			case 'rgxp':
			case 'minlength':
			case 'maxlength':
				// Ignore
				break;

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

	/**
	 * Check options if the field is mandatory
	 */
	public function validate()
	{
		$mandatory = $this->mandatory;
		$options = $this->getPost($this->strName);

		// Check if there is at least one value
		if ($mandatory && \is_array($options))
		{
			foreach ($options as $option)
			{
				if (\strlen($option))
				{
					$this->mandatory = false;
					break;
				}
			}
		}

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

		// Check for a valid option (see #4383)
		if (!empty($varInput) && !$this->isValidOption($varInput))
		{
			$this->addError($GLOBALS['TL_LANG']['ERR']['invalid']);
		}

		// Add class "error"
		if ($this->hasErrors())
		{
			$this->class = 'error';
		}
		else
		{
			$this->varValue = $varInput;
		}

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

	/**
	 * Return a parameter
	 *
	 * @param string $strKey The parameter name
	 *
	 * @return mixed The parameter value
	 */
	public function __get($strKey)
	{
		if ($strKey == 'options')
		{
			return $this->arrOptions;
		}

		return parent::__get($strKey);
	}

	/**
	 * Parse the template file and return it as string
	 *
	 * @param array $arrAttributes An optional attributes array
	 *
	 * @return string The template markup
	 */
	public function parse($arrAttributes=null)
	{
		$strClass = 'select';

		if ($this->multiple)
		{
			$this->strName .= '[]';
			$strClass = 'multiselect';
		}

		// Make sure there are no multiple options in single mode
		elseif (\is_array($this->varValue))
		{
			$this->varValue = $this->varValue[0];
		}

		// Chosen
		if ($this->chosen)
		{
			$strClass .= ' tl_chosen';
		}

		// Custom class
		if ($this->strClass != '')
		{
			$strClass .= ' ' . $this->strClass;
		}

		$this->strClass = $strClass;

		return parent::parse($arrAttributes);
	}

	/**
	 * Generate the options
	 *
	 * @return array The options array
	 */
	protected function getOptions()
	{
		$arrOptions = array();
		$blnHasGroups = false;

		// Add empty option if there are none
		if (empty($this->arrOptions) || !\is_array($this->arrOptions))
		{
			$this->arrOptions = array(array('value' => '', 'label' => '-'));
		}

		// Generate options
		foreach ($this->arrOptions as $arrOption)
		{
			if ($arrOption['group'])
			{
				if ($blnHasGroups)
				{
					$arrOptions[] = array
					(
						'type' => 'group_end'
					);
				}

				$arrOptions[] = array
				(
					'type'  => 'group_start',
					'label' => StringUtil::specialchars($arrOption['label'])
				);

				$blnHasGroups = true;
			}
			else
			{
				$arrOptions[] = array_replace
				(
					$arrOption,
					array
					(
						'type'     => 'option',
						'value'    => $arrOption['value'],
						'selected' => $this->isSelected($arrOption),
						'label'    => $arrOption['label'],
					)
				);
			}
		}

		if ($blnHasGroups)
		{
			$arrOptions[] = array
			(
				'type' => 'group_end'
			);
		}

		return $arrOptions;
	}

	/**
	 * Generate the widget and return it as string
	 *
	 * @return string The widget markup
	 */
	public function generate()
	{
		$strOptions = '';
		$blnHasGroups = false;

		if ($this->multiple)
		{
			$this->strName .= '[]';
		}

		// Make sure there are no multiple options in single mode
		elseif (\is_array($this->varValue))
		{
			$this->varValue = $this->varValue[0];
		}

		// Add empty option if there are none
		if (empty($this->arrOptions) || !\is_array($this->arrOptions))
		{
			$this->arrOptions = array(array('value'=>'', 'label'=>'-'));
		}

		foreach ($this->arrOptions as $arrOption)
		{
			if ($arrOption['group'])
			{
				if ($blnHasGroups)
				{
					$strOptions .= '</optgroup>';
				}

				$strOptions .= sprintf('<optgroup label="%s">',
										StringUtil::specialchars($arrOption['label']));

				$blnHasGroups = true;
				continue;
			}

			$strOptions .= sprintf('<option value="%s"%s>%s</option>',
									$arrOption['value'],
									$this->isSelected($arrOption),
									$arrOption['label']);
		}

		if ($blnHasGroups)
		{
			$strOptions .= '</optgroup>';
		}

		return sprintf('<select name="%s" id="ctrl_%s" class="%s"%s>%s</select>',
						$this->strName,
						$this->strId,
						$this->class,
						$this->getAttributes(),
						$strOptions);
	}
}

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


<?php

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

namespace Contao\Database;

use Contao\Database;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\Statement as DoctrineStatement;

/**
 * Create and execute queries
 *
 * The class creates the database queries replacing the wildcards and escaping
 * the values. It then executes the query and returns a result object.
 *
 * Usage:
 *
 *     $db = Database::getInstance();
 *     $stmt = $db->prepare("SELECT * FROM tl_member WHERE city=?");
 *     $stmt->limit(10);
 *     $res = $stmt->execute('London');
 *
 * @property string  $query        The query string
 * @property string  $error        The last error message
 * @property integer $affectedRows The number of affected rows
 * @property integer $insertId     The last insert ID
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class Statement
{

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

	/**
	 * Database statement
	 * @var DoctrineStatement
	 */
	protected $statement;

	/**
	 * Query string
	 * @var string
	 */
	protected $strQuery;

	/**
	 * Autocommit indicator
	 * @var boolean
	 */
	protected $blnDisableAutocommit = false;

	/**
	 * Result cache
	 * @var array
	 */
	protected static $arrCache = array();

	/**
	 * Validate the connection resource and store the query string
	 *
	 * @param Connection $resConnection        The connection resource
	 * @param boolean    $blnDisableAutocommit Optionally disable autocommitting
	 */
	public function __construct(Connection $resConnection, $blnDisableAutocommit=false)
	{
		$this->resConnection = $resConnection;
		$this->blnDisableAutocommit = $blnDisableAutocommit;
	}

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

			case 'error':
				$info = $this->statement->errorInfo();

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

			case 'affectedRows':
				return $this->statement->rowCount();
				break;

			case 'insertId':
				return $this->resConnection->lastInsertId();
				break;
		}

		return null;
	}

	/**
	 * Prepare a query string so the following functions can handle it
	 *
	 * @param string $strQuery The query string
	 *
	 * @return Statement The statement object
	 *
	 * @throws \Exception If $strQuery is empty
	 */
	public function prepare($strQuery)
	{
		if ($strQuery == '')
		{
			throw new \Exception('Empty query string');
		}

		$this->strQuery = trim($strQuery);

		// Auto-generate the SET/VALUES subpart
		if (strncasecmp($this->strQuery, 'INSERT', 6) === 0 || strncasecmp($this->strQuery, 'UPDATE', 6) === 0)
		{
			$this->strQuery = str_replace('%s', '%p', $this->strQuery);
		}

		// Replace wildcards
		$arrChunks = preg_split("/('[^']*')/", $this->strQuery, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);

		foreach ($arrChunks as $k=>$v)
		{
			if (substr($v, 0, 1) == "'")
			{
				continue;
			}

			$arrChunks[$k] = str_replace('?', '%s', $v);
		}

		$this->strQuery = implode('', $arrChunks);

		return $this;
	}

	/**
	 * Autogenerate the SET/VALUES subpart of a query from an associative array
	 *
	 * Usage:
	 *
	 *     $set = array(
	 *         'firstname' => 'Leo',
	 *         'lastname'  => 'Feyer'
	 *     );
	 *     $stmt->prepare("UPDATE tl_member %s")->set($set);
	 *
	 * @param array $arrParams The associative array
	 *
	 * @return Statement The statement object
	 */
	public function set($arrParams)
	{
		$strQuery = '';
		$arrParams = $this->escapeParams($arrParams);

		// INSERT
		if (strncasecmp($this->strQuery, 'INSERT', 6) === 0)
		{
			$strQuery = sprintf('(%s) VALUES (%s)',
								implode(', ', array_map('Database::quoteIdentifier', array_keys($arrParams))),
								str_replace('%', '%%', implode(', ', $arrParams)));
		}

		// UPDATE
		elseif (strncasecmp($this->strQuery, 'UPDATE', 6) === 0)
		{
			$arrSet = array();

			foreach ($arrParams as $k=>$v)
			{
				$arrSet[] = Database::quoteIdentifier($k) . '=' . $v;
			}

			$strQuery = 'SET ' . str_replace('%', '%%', implode(', ', $arrSet));
		}

		$this->strQuery = str_replace('%p', $strQuery, $this->strQuery);

		return $this;
	}

	/**
	 * Handle limit and offset
	 *
	 * @param integer $intRows   The maximum number of rows
	 * @param integer $intOffset The number of rows to skip
	 *
	 * @return Statement The statement object
	 */
	public function limit($intRows, $intOffset=0)
	{
		if ($intRows <= 0)
		{
			$intRows = 30;
		}

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

		if (strncasecmp($this->strQuery, 'SELECT', 6) === 0)
		{
			$this->strQuery .= ' LIMIT ' . $intOffset . ',' . $intRows;
		}
		else
		{
			$this->strQuery .= ' LIMIT ' . $intRows;
		}

		return $this;
	}

	/**
	 * Execute the query and return the result object
	 *
	 * @return Result The result object
	 */
	public function execute()
	{
		$arrParams = \func_get_args();

		if (!empty($arrParams) && \is_array($arrParams[0]))
		{
			$arrParams = array_values($arrParams[0]);
		}

		$this->replaceWildcards($arrParams);

		return $this->query();
	}

	/**
	 * Directly send a query string to the database
	 *
	 * @param string $strQuery The query string
	 *
	 * @return Result|Statement The result object or the statement object if there is no result set
	 *
	 * @throws \Exception If the query string is empty
	 */
	public function query($strQuery='')
	{
		if (!empty($strQuery))
		{
			$this->strQuery = trim($strQuery);
		}

		// Make sure there is a query string
		if ($this->strQuery == '')
		{
			throw new \Exception('Empty query string');
		}

		// Execute the query
		$this->statement = $this->resConnection->executeQuery($this->strQuery);

		// No result set available
		if ($this->statement->columnCount() < 1)
		{
			return $this;
		}

		// Instantiate a result object
		return new Result($this->statement, $this->strQuery);
	}

	/**
	 * Replace the wildcards in the query string
	 *
	 * @param array $arrValues The values array
	 *
	 * @throws \Exception If $arrValues has too few values to replace the wildcards in the query string
	 */
	protected function replaceWildcards($arrValues)
	{
		$arrValues = $this->escapeParams($arrValues);
		$this->strQuery = preg_replace('/(?<!%)%([^bcdufosxX%])/', '%%$1', $this->strQuery);

		// Replace wildcards
		if (!$this->strQuery = @vsprintf($this->strQuery, $arrValues))
		{
			throw new \Exception('Too few arguments to build the query string');
		}
	}

	/**
	 * Escape the values and serialize objects and arrays
	 *
	 * @param array $arrValues The values array
	 *
	 * @return array The array with the escaped values
	 */
	protected function escapeParams($arrValues)
	{
		foreach ($arrValues as $k=>$v)
		{
			switch (\gettype($v))
			{
				case 'string':
					$arrValues[$k] = $this->resConnection->quote($v);
					break;

				case 'boolean':
					$arrValues[$k] = ($v === true) ? 1 : 0;
					break;

				case 'object':
					$arrValues[$k] = $this->resConnection->quote(serialize($v));
					break;

				case 'array':
					$arrValues[$k] = $this->resConnection->quote(serialize($v));
					break;

				default:
					$arrValues[$k] = $v ?? 'NULL';
					break;
			}
		}

		return $arrValues;
	}

	/**
	 * Explain the current query
	 *
	 * @return string The explanation string
	 */
	public function explain()
	{
		return $this->resConnection->executeQuery('EXPLAIN ' . $this->strQuery)->fetch();
	}

	/**
	 * Bypass the cache and always execute the query
	 *
	 * @return Result The result object
	 *
	 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
	 *             Use Statement::execute() instead.
	 */
	public function executeUncached()
	{
		@trigger_error('Using Statement::executeUncached() has been deprecated and will no longer work in Contao 5.0. Use Statement::execute() instead.', E_USER_DEPRECATED);

		return \call_user_func_array(array($this, 'execute'), \func_get_args());
	}

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

		return \call_user_func_array(array($this, 'execute'), \func_get_args());
	}
}

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


<?php

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

namespace Contao;

/**
 * Sends HTTP requests and reads the response
 *
 * The class can be used to communicate with services that are available via
 * HTTP (e.g. the Contao Extension Repository or the Live Update Service). It
 * uses some of Drupal's HTTP request class methods ({@see http://drupal.org}).
 *
 * Usage:
 *
 *     $request = new Request();
 *     $request->send('http://www.inetrobots.com/liveupdate/version.txt');
 *
 *     if (!$request->hasError())
 *     {
 *         echo "The latest Contao version is " . $request->response;
 *     }
 *
 * @property string  $error    The error message
 * @property integer $code     The response code
 * @property string  $request  The request string
 * @property string  $response The response string
 * @property array   $headers  The response headers array
 * @property string  $username The auth username
 * @property boolean $redirect The follow redirects status
 * @property integer $rlimit   The maximum number of redirects
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class Request
{

	/**
	 * Request data
	 * @var string
	 */
	protected $strData;

	/**
	 * Request method (defaults to GET)
	 * @var string
	 */
	protected $strMethod;

	/**
	 * Error string
	 * @var string
	 */
	protected $strError;

	/**
	 * Response code
	 * @var integer
	 */
	protected $intCode;

	/**
	 * Response string
	 * @var string
	 */
	protected $strResponse;

	/**
	 * Request string
	 * @var string
	 */
	protected $strRequest;

	/**
	 * Headers array (these headers will be sent)
	 * @var array
	 */
	protected $arrHeaders = array();

	/**
	 * Response headers (these headers are returned)
	 * @var array
	 */
	protected $arrResponseHeaders = array();

	/**
	 * Request username
	 * @var string
	 */
	protected $strUsername;

	/**
	 * Request password
	 * @var string
	 */
	protected $strPassword;

	/**
	 * Follow redirects
	 * @var boolean
	 */
	protected $blnFollowRedirects = false;

	/**
	 * Number of redirects
	 * @var integer
	 */
	protected $intRedirects = 0;

	/**
	 * Maximum number of redirects
	 * @var integer
	 */
	protected $intRedirectLimit = 3;

	/**
	 * Set the default values
	 */
	public function __construct()
	{
		$this->strData = '';
		$this->strMethod = 'get';
	}

	/**
	 * Set an object property
	 *
	 * @param string $strKey   The property name
	 * @param mixed  $varValue The property value
	 *
	 * @throws \Exception If $strKey is unknown
	 */
	public function __set($strKey, $varValue)
	{
		switch ($strKey)
		{
			case 'data':
				$this->strData = $varValue;
				break;

			case 'method':
				$this->strMethod = $varValue;
				break;

			case 'username':
				$this->strUsername = $varValue;
				break;

			case 'password':
				$this->strPassword = $varValue;
				break;

			case 'redirect':
				$this->blnFollowRedirects = $varValue;
				break;

			case 'rlimit':
				$this->intRedirectLimit = $varValue;
				break;

			default:
				throw new \Exception(sprintf('Invalid argument "%s"', $strKey));
				break;
		}
	}

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

			case 'code':
				return $this->intCode;
				break;

			case 'request':
				return $this->strRequest;
				break;

			case 'response':
				return $this->strResponse;
				break;

			case 'headers':
				return $this->arrResponseHeaders;
				break;

			case 'username':
				return $this->strUsername;
				break;

			case 'redirect':
				return $this->blnFollowRedirects;
				break;

			case 'rlimit':
				return $this->intRedirectLimit;
				break;
		}

		return null;
	}

	/**
	 * Add request headers
	 *
	 * @param string $strKey   The header name
	 * @param mixed  $varValue The header value
	 */
	public function setHeader($strKey, $varValue)
	{
		$this->arrHeaders[$strKey] = $varValue;
	}

	/**
	 * Return true if there was an error
	 *
	 * @return boolean True if there was an error
	 */
	public function hasError()
	{
		return $this->strError != '';
	}

	/**
	 * Send the HTTP request
	 *
	 * @param string $strUrl    The target URL
	 * @param string $strData   Optional request data
	 * @param string $strMethod An optional request method
	 */
	public function send($strUrl, $strData=null, $strMethod=null)
	{
		if ($strData !== null)
		{
			$this->strData = $strData;
		}

		if ($strMethod !== null)
		{
			$this->strMethod = $strMethod;
		}

		$errstr = '';
		$errno = null;
		$uri = parse_url($strUrl);

		switch ($uri['scheme'])
		{
			case 'http':
				$port = $uri['port'] ?? 80;
				$host = $uri['host'] . (($port != 80) ? ':' . $port : '');
				$fp = @fsockopen($uri['host'], $port, $errno, $errstr, 10);
				break;

			case 'https':
				$port = $uri['port'] ?? 443;
				$host = $uri['host'] . (($port != 443) ? ':' . $port : '');
				$fp = @fsockopen('ssl://' . $uri['host'], $port, $errno, $errstr, 15);
				break;

			default:
				$this->strError = 'Invalid schema ' . $uri['scheme'];

				return;
				break;
		}

		if (!\is_resource($fp))
		{
			$this->strError = trim($errno .' '. $errstr);

			return;
		}

		$path = $uri['path'] ?? '/';

		if (isset($uri['query']))
		{
			$path .= '?' . $uri['query'];
		}

		$default = array
		(
			'Host' => 'Host: ' . $host,
			'User-Agent' => 'User-Agent: Contao (+https://contao.org/)',
			'Content-Length' => 'Content-Length: '. \strlen($this->strData),
			'Connection' => 'Connection: close'
		);

		if (isset($uri['user']))
		{
			$default['Authorization'] = 'Authorization: Basic ' . base64_encode($uri['user'] . ':' . $uri['pass']);
		}
		elseif ($this->strUsername != '')
		{
			$default['Authorization'] = 'Authorization: Basic ' . base64_encode($this->strUsername . ':' . $this->strPassword);
		}

		foreach ($this->arrHeaders as $header=>$value)
		{
			$default[$header] = $header . ': ' . $value;
		}

		$request = strtoupper($this->strMethod) .' '. $path ." HTTP/1.0\r\n";
		$request .= implode("\r\n", $default);
		$request .= "\r\n\r\n";

		if ($this->strData != '')
		{
			$request .= $this->strData . "\r\n";
		}

		$this->strRequest = $request;
		fwrite($fp, $request);
		$response = '';

		while (!feof($fp) && ($chunk = fread($fp, 1024)) != false)
		{
			$response .= $chunk;
		}

		fclose($fp);

		list($split, $this->strResponse) = explode("\r\n\r\n", $response, 2);
		$split = preg_split("/\r\n|\n|\r/", $split);
		$this->arrResponseHeaders = array();
		list(, $code, $text) = explode(' ', trim(array_shift($split)), 3);

		while (($line = trim(array_shift($split))) != false)
		{
			list($header, $value) = explode(':', $line, 2);

			if (isset($this->arrResponseHeaders[$header]) && $header == 'Set-Cookie')
			{
				$this->arrResponseHeaders[$header] .= ',' . trim($value);
			}
			else
			{
				$this->arrResponseHeaders[$header] = trim($value);
			}
		}

		$responses = array
		(
			100 => 'Continue',
			101 => 'Switching Protocols',
			200 => 'OK',
			201 => 'Created',
			202 => 'Accepted',
			203 => 'Non-Authoritative Information',
			204 => 'No Content',
			205 => 'Reset Content',
			206 => 'Partial Content',
			300 => 'Multiple Choices',
			301 => 'Moved Permanently',
			302 => 'Found',
			303 => 'See Other',
			304 => 'Not Modified',
			305 => 'Use Proxy',
			307 => 'Temporary Redirect',
			400 => 'Bad Request',
			401 => 'Unauthorized',
			402 => 'Payment Required',
			403 => 'Forbidden',
			404 => 'Not Found',
			405 => 'Method Not Allowed',
			406 => 'Not Acceptable',
			407 => 'Proxy Authentication Required',
			408 => 'Request Timeout',
			409 => 'Conflict',
			410 => 'Gone',
			411 => 'Length Required',
			412 => 'Precondition Failed',
			413 => 'Request Entity Too Large',
			414 => 'Request-URI Too Large',
			415 => 'Unsupported Media Type',
			416 => 'Requested Range Not Satisfiable',
			417 => 'Expectation Failed',
			500 => 'Internal Server Error',
			501 => 'Not Implemented',
			502 => 'Bad Gateway',
			503 => 'Service Unavailable',
			504 => 'Gateway Timeout',
			505 => 'HTTP Version Not Supported'
		);

		if (!isset($responses[$code]))
		{
			$code = floor($code / 100) * 100;
		}

		$this->intCode = (int) $code;

		switch ($this->intCode)
		{
			case 200:
			case 304:
				// Ok
				break;

			case 301:
			case 302:
			case 303:
			case 307:
				if ($this->blnFollowRedirects && $this->intRedirects < $this->intRedirectLimit && !empty($this->arrResponseHeaders['Location']))
				{
					++$this->intRedirects;
					$strLocation = $this->arrResponseHeaders['Location'];

					// Make sure the location is an absolute URL (see #7799)
					if (!preg_match('@^https?://@', $strLocation))
					{
						$strLocation = $uri['scheme'] . '://' . $host . $strLocation;
					}

					$this->send($strLocation);
				}
				break;

			default:
				$this->strError = $text ?: $responses[$this->intCode];
				break;
		}
	}
}

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


<?php

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

namespace Contao;

use Patchwork\Utf8;

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

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

	/**
	 * Do not display the module if there are no menu items
	 *
	 * @return string
	 */
	public function generate()
	{
		if (TL_MODE == 'BE')
		{
			$objTemplate = new BackendTemplate('be_wildcard');
			$objTemplate->wildcard = '### ' . Utf8::strtoupper($GLOBALS['TL_LANG']['FMD']['navigation'][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();
		}

		$strBuffer = parent::generate();

		return ($this->Template->items != '') ? $strBuffer : '';
	}

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

		// Set the trail and level
		if ($this->defineRoot && $this->rootPage > 0)
		{
			$trail = array($this->rootPage);
			$level = 0;
		}
		else
		{
			$trail = $objPage->trail;
			$level = ($this->levelOffset > 0) ? $this->levelOffset : 0;
		}

		$lang = null;
		$host = null;

		// Overwrite the domain and language if the reference page belongs to a differnt root page (see #3765)
		if ($this->defineRoot && $this->rootPage > 0)
		{
			$objRootPage = PageModel::findWithDetails($this->rootPage);

			// Set the language
			if (Config::get('addLanguageToUrl') && $objRootPage->rootLanguage != $objPage->rootLanguage)
			{
				$lang = $objRootPage->rootLanguage;
			}

			// Set the domain
			if ($objRootPage->rootId != $objPage->rootId && $objRootPage->domain != '' && $objRootPage->domain != $objPage->domain)
			{
				$host = $objRootPage->domain;
			}
		}

		$this->Template->request = ampersand(Environment::get('indexFreeRequest'));
		$this->Template->skipId = 'skipNavigation' . $this->id;
		$this->Template->skipNavigation = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['skipNavigation']);
		$this->Template->items = $this->renderNavigation($trail[$level], 1, $host, $lang);
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\HttpKernel;

use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\CoreBundle\Routing\ScopeMatcher;
use Contao\Model;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

class ModelArgumentResolver implements ArgumentValueResolverInterface
{
    /**
     * @var ContaoFramework
     */
    private $framework;

    /**
     * @var ScopeMatcher
     */
    private $scopeMatcher;

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

    /**
     * {@inheritdoc}
     */
    public function supports(Request $request, ArgumentMetadata $argument): bool
    {
        if (!$this->scopeMatcher->isContaoRequest($request)) {
            return false;
        }

        $this->framework->initialize();

        if (!is_a($argument->getType(), Model::class, true)) {
            return false;
        }

        if (!$argument->isNullable() && null === $this->fetchModel($request, $argument)) {
            return false;
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function resolve(Request $request, ArgumentMetadata $argument): \Generator
    {
        yield $this->fetchModel($request, $argument);
    }

    private function fetchModel(Request $request, ArgumentMetadata $argument): ?Model
    {
        $name = $this->getArgumentName($request, $argument);

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

        /** @var Model $model */
        $model = $this->framework->getAdapter($argument->getType());

        return $model->findByPk($request->attributes->getInt($name));
    }

    /**
     * Returns the argument name from the model class.
     */
    private function getArgumentName(Request $request, ArgumentMetadata $argument): ?string
    {
        if ($request->attributes->has($argument->getName())) {
            return $argument->getName();
        }

        $className = lcfirst($this->stripNamespace($argument->getType()));

        if ($request->attributes->has($className)) {
            return $className;
        }

        return null;
    }

    /**
     * Strips the namespace from a class name.
     */
    private function stripNamespace(string $fqcn): string
    {
        if (false !== ($pos = strrpos($fqcn, '\\'))) {
            return substr($fqcn, $pos + 1);
        }

        return $fqcn;
    }
}


<?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\Fragment\FragmentOptionsAwareInterface;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\FrontendTemplate;
use Contao\Model;
use Contao\StringUtil;
use Contao\Template;
use FOS\HttpCacheBundle\Http\SymfonyResponseTagger;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Container;

abstract class AbstractFragmentController extends AbstractController implements FragmentOptionsAwareInterface
{
    /**
     * @var array
     */
    protected $options = [];

    public function setFragmentOptions(array $options): void
    {
        $this->options = $options;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedServices(): array
    {
        return array_merge(
            parent::getSubscribedServices(),
            [
                'contao.framework' => ContaoFramework::class,
                'fos_http_cache.http.symfony_response_tagger' => '?'.SymfonyResponseTagger::class,
            ]
        );
    }

    /**
     * Creates a template by name or from the "customTpl" field of the model.
     */
    protected function createTemplate(Model $model, string $templateName): Template
    {
        if (isset($this->options['template'])) {
            $templateName = $this->options['template'];
        }

        if ($model->customTpl) {
            $templateName = $model->customTpl;
        }

        $template = $this->get('contao.framework')->createInstance(FrontendTemplate::class, [$templateName]);
        $template->setData($model->row());

        return $template;
    }

    /**
     * @param string|array $headline
     */
    protected function addHeadlineToTemplate(Template $template, $headline): void
    {
        $data = StringUtil::deserialize($headline);
        $template->headline = \is_array($data) ? $data['value'] : $data;
        $template->hl = \is_array($data) ? $data['unit'] : 'h1';
    }

    /**
     * @param string|array $cssID
     */
    protected function addCssAttributesToTemplate(Template $template, string $templateName, $cssID, array $classes = null): void
    {
        $data = StringUtil::deserialize($cssID, true);
        $template->class = trim($templateName.' '.($data[1] ?? ''));
        $template->cssID = !empty($data[0]) ? ' id="'.$data[0].'"' : '';

        if (\is_array($classes)) {
            $template->class .= ' '.implode(' ', $classes);
        }
    }

    protected function addSectionToTemplate(Template $template, string $section): void
    {
        $template->inColumn = $section;
    }

    protected function tagResponse(array $tags): void
    {
        if (!$this->has('fos_http_cache.http.symfony_response_tagger')) {
            return;
        }

        $this->get('fos_http_cache.http.symfony_response_tagger')->addTags($tags);
    }

    /**
     * Returns the type from the class name.
     */
    protected function getType(): string
    {
        if (isset($this->options['type'])) {
            return $this->options['type'];
        }

        $className = ltrim(strrchr(static::class, '\\'), '\\');

        if ('Controller' === substr($className, -10)) {
            $className = substr($className, 0, -10);
        }

        return Container::underscore($className);
    }
}


<?php

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

namespace Contao;

/**
 * Back end module "maintenance".
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ModuleMaintenance extends BackendModule
{

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

	/**
	 * Generate the module
	 *
	 * @throws \Exception
	 */
	protected function compile()
	{
		System::loadLanguageFile('tl_maintenance');

		$this->Template->content = '';
		$this->Template->href = $this->getReferer(true);
		$this->Template->title = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['backBTTitle']);
		$this->Template->button = $GLOBALS['TL_LANG']['MSC']['backBT'];

		foreach ($GLOBALS['TL_MAINTENANCE'] as $callback)
		{
			$this->import($callback);

			if (!$this->$callback instanceof \executable)
			{
				throw new \Exception("$callback is not an executable class");
			}

			$buffer = $this->$callback->run();

			if ($this->$callback->isActive())
			{
				$this->Template->content = $buffer;
				break;
			}
			else
			{
				$this->Template->content .= $buffer;
			}
		}
	}
}

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


<?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\BackendAlerts;
use Contao\BackendConfirm;
use Contao\BackendFile;
use Contao\BackendHelp;
use Contao\BackendIndex;
use Contao\BackendMain;
use Contao\BackendPage;
use Contao\BackendPassword;
use Contao\BackendPopup;
use Contao\BackendPreview;
use Contao\BackendSwitch;
use Contao\CoreBundle\Picker\PickerConfig;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route(defaults={"_scope" = "backend", "_token_check" = true})
 */
class BackendController extends AbstractController
{
    /**
     * @Route("/contao", name="contao_backend")
     */
    public function mainAction(): Response
    {
        $this->get('contao.framework')->initialize();

        $controller = new BackendMain();

        return $controller->run();
    }

    /**
     * @Route("/contao/login", name="contao_backend_login")
     */
    public function loginAction(): Response
    {
        $this->get('contao.framework')->initialize();

        if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) {
            return new RedirectResponse($this->get('router')->generate('contao_backend'));
        }

        $controller = new BackendIndex();

        return $controller->run();
    }

    /**
     * Symfony will un-authenticate the user automatically by calling this route.
     *
     * @Route("/contao/logout", name="contao_backend_logout")
     */
    public function logoutAction(): RedirectResponse
    {
        return $this->redirectToRoute('contao_backend_login');
    }

    /**
     * @Route("/contao/password", name="contao_backend_password")
     */
    public function passwordAction(): Response
    {
        $this->get('contao.framework')->initialize();

        $controller = new BackendPassword();

        return $controller->run();
    }

    /**
     * @Route("/contao/preview", name="contao_backend_preview")
     */
    public function previewAction(): Response
    {
        $this->get('contao.framework')->initialize();

        $controller = new BackendPreview();

        return $controller->run();
    }

    /**
     * @Route("/contao/confirm", name="contao_backend_confirm")
     */
    public function confirmAction(): Response
    {
        $this->get('contao.framework')->initialize();

        $controller = new BackendConfirm();

        return $controller->run();
    }

    /**
     * @Route("/contao/file", name="contao_backend_file")
     */
    public function fileAction(): Response
    {
        $this->get('contao.framework')->initialize();

        $controller = new BackendFile();

        return $controller->run();
    }

    /**
     * @Route("/contao/help", name="contao_backend_help")
     */
    public function helpAction(): Response
    {
        $this->get('contao.framework')->initialize();

        $controller = new BackendHelp();

        return $controller->run();
    }

    /**
     * @Route("/contao/page", name="contao_backend_page")
     */
    public function pageAction(): Response
    {
        $this->get('contao.framework')->initialize();

        $controller = new BackendPage();

        return $controller->run();
    }

    /**
     * @Route("/contao/popup", name="contao_backend_popup")
     */
    public function popupAction(): Response
    {
        $this->get('contao.framework')->initialize();

        $controller = new BackendPopup();

        return $controller->run();
    }

    /**
     * @Route("/contao/switch", name="contao_backend_switch")
     */
    public function switchAction(): Response
    {
        $this->get('contao.framework')->initialize();

        $controller = new BackendSwitch();

        return $controller->run();
    }

    /**
     * @Route("/contao/alerts", name="contao_backend_alerts")
     */
    public function alertsAction(): Response
    {
        $this->get('contao.framework')->initialize();

        $controller = new BackendAlerts();

        return $controller->run();
    }

    /**
     * Redirects the user to the Contao back end and adds the picker query parameter.
     * It will determine the current provider URL based on the value, which is usually
     * read dynamically via JavaScript.
     *
     * @throws BadRequestHttpException
     *
     * @Route("/contao/picker", name="contao_backend_picker")
     */
    public function pickerAction(Request $request): RedirectResponse
    {
        $extras = [];

        if ($request->query->has('extras')) {
            $extras = $request->query->get('extras');

            if (!\is_array($extras)) {
                throw new BadRequestHttpException('Invalid picker extras');
            }
        }

        $config = new PickerConfig($request->query->get('context'), $extras, $request->query->get('value'));
        $picker = $this->get('contao.picker.builder')->create($config);

        if (null === $picker) {
            throw new BadRequestHttpException('Unsupported picker context');
        }

        return new RedirectResponse($picker->getCurrentUrl());
    }

    /**
     * Redirects the user to the Contao back end in case they manually call the
     * /contao/two-factor route. Will be intercepted by the two factor bundle otherwise.
     *
     * @Route("/contao/two-factor", name="contao_backend_two_factor")
     */
    public function twoFactorAuthenticationAction(): Response
    {
        return $this->redirectToRoute('contao_backend');
    }
}


<?php

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

namespace Contao;

use Symfony\Component\Finder\Finder;

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

	/**
	 * Return true if the module is active
	 *
	 * @return boolean
	 */
	public function isActive()
	{
		return Input::post('FORM_SUBMIT') == 'tl_purge';
	}

	/**
	 * Generate the module
	 *
	 * @return string
	 */
	public function run()
	{
		$arrJobs = array();

		$objTemplate = new BackendTemplate('be_purge_data');
		$objTemplate->isActive = $this->isActive();
		$objTemplate->message = Message::generateUnwrapped(__CLASS__);

		// Run the jobs
		if (Input::post('FORM_SUBMIT') == 'tl_purge')
		{
			$purge = Input::post('purge');

			if (!empty($purge) && \is_array($purge))
			{
				foreach ($purge as $group=>$jobs)
				{
					foreach ($jobs as $job)
					{
						list($class, $method) = $GLOBALS['TL_PURGE'][$group][$job]['callback'];
						$this->import($class);
						$this->$class->$method();
					}
				}
			}

			Message::addConfirmation($GLOBALS['TL_LANG']['tl_maintenance']['cacheCleared'], __CLASS__);
			$this->reload();
		}

		// Tables
		foreach ($GLOBALS['TL_PURGE']['tables'] as $key=>$config)
		{
			$arrJobs[$key] = array
			(
				'id' => 'purge_' . $key,
				'title' => $GLOBALS['TL_LANG']['tl_maintenance_jobs'][$key][0],
				'description' => $GLOBALS['TL_LANG']['tl_maintenance_jobs'][$key][1],
				'group' => 'tables',
				'affected' => ''
			);

			// Get the current table size
			foreach ($config['affected'] as $table)
			{
				$objCount = $this->Database->execute("SELECT COUNT(*) AS count FROM " . $table);
				$arrJobs[$key]['affected'] .= '
' . $table . ': ' . sprintf($GLOBALS['TL_LANG']['MSC']['entries'], $objCount->count) . ', ' . $this->getReadableSize($this->Database->getSizeOf($table), 0) . '';
			}
		}

		$container = System::getContainer();
		$rootDir = $container->getParameter('kernel.project_dir');
		$strCachePath = StringUtil::stripRootDir($container->getParameter('kernel.cache_dir'));

		// Folders
		foreach ($GLOBALS['TL_PURGE']['folders'] as $key=>$config)
		{
			$arrJobs[$key] = array
			(
				'id' => 'purge_' . $key,
				'title' => $GLOBALS['TL_LANG']['tl_maintenance_jobs'][$key][0],
				'description' => $GLOBALS['TL_LANG']['tl_maintenance_jobs'][$key][1],
				'group' => 'folders',
				'affected' => ''
			);

			// Get the current folder size
			foreach ($config['affected'] as $folder)
			{
				$total = 0;
				$folder = sprintf($folder, $strCachePath);

				// Only check existing folders
				if (is_dir($rootDir . '/' . $folder))
				{
					$objFiles = Finder::create()->in($rootDir . '/' . $folder)->files();
					$total = iterator_count($objFiles);
				}

				$arrJobs[$key]['affected'] .= '
' . $folder . ': ' . sprintf($GLOBALS['TL_LANG']['MSC']['files'], $total) . '';
			}
		}

		// Custom
		foreach ($GLOBALS['TL_PURGE']['custom'] as $key=>$job)
		{
			$arrJobs[$key] = array
			(
				'id' => 'purge_' . $key,
				'title' => $GLOBALS['TL_LANG']['tl_maintenance_jobs'][$key][0],
				'description' => $GLOBALS['TL_LANG']['tl_maintenance_jobs'][$key][1],
				'group' => 'custom'
			);
		}

		$objTemplate->jobs = $arrJobs;
		$objTemplate->action = ampersand(Environment::get('request'));
		$objTemplate->headline = $GLOBALS['TL_LANG']['tl_maintenance']['clearCache'];
		$objTemplate->job = $GLOBALS['TL_LANG']['tl_maintenance']['job'];
		$objTemplate->description = $GLOBALS['TL_LANG']['tl_maintenance']['description'];
		$objTemplate->submit = StringUtil::specialchars($GLOBALS['TL_LANG']['tl_maintenance']['clearCache']);
		$objTemplate->help = (Config::get('showHelp') && ($GLOBALS['TL_LANG']['tl_maintenance']['cacheTables'][1] != '')) ? $GLOBALS['TL_LANG']['tl_maintenance']['cacheTables'][1] : '';

		return $objTemplate->parse();
	}
}

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


<?php

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

namespace Contao;

use Patchwork\Utf8;

/**
 * Class FormCaptcha
 *
 * @property string $name
 * @property string $question
 * @property string $placeholder
 * @property string $rowClass
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class FormCaptcha extends Widget
{

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

	/**
	 * Captcha key
	 *
	 * @var string
	 */
	protected $strCaptchaKey;

	/**
	 * Captcha values
	 *
	 * @var array
	 */
	protected $arrCaptcha = array();

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

	/**
	 * Initialize the object
	 *
	 * @param array $arrAttributes An optional attributes array
	 */
	public function __construct($arrAttributes=null)
	{
		parent::__construct($arrAttributes);

		$this->arrAttributes['maxlength'] = 2;
		$this->strCaptchaKey = 'captcha_' . $this->strId;
		$this->arrAttributes['required'] = true;
		$this->arrConfiguration['mandatory'] = true;
	}

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

			case 'required':
			case 'mandatory':
			case 'minlength':
			case 'maxlength':
				// Ignore
				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 'name':
				return $this->strCaptchaKey;
				break;

			case 'question':
				return $this->getQuestion();
				break;

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

	/**
	 * Validate the input and set the value
	 */
	public function validate()
	{
		if (!isset($_POST[$this->strCaptchaKey]) || !\in_array(Input::post($this->strCaptchaKey.'_hash'), $this->generateHashes((int) Input::post($this->strCaptchaKey)), true) || (isset($_POST[$this->strCaptchaKey.'_name']) && Input::post($this->strCaptchaKey.'_name')))
		{
			$this->class = 'error';
			$this->addError($GLOBALS['TL_LANG']['ERR']['captcha']);
		}
	}

	/**
	 * Generate the captcha values
	 */
	protected function generateCaptcha()
	{
		if ($this->arrCaptcha)
		{
			return;
		}

		$int1 = random_int(1, 9);
		$int2 = random_int(1, 9);

		$this->arrCaptcha = array
		(
			'int1' => $int1,
			'int2' => $int2,
			'sum' => $int1 + $int2,
			'key' => $this->strCaptchaKey,
			'hashes' => $this->generateHashes($int1 + $int2)
		);
	}

	/**
	 * Generate hashes for the current time and the specified sum
	 *
	 * @param integer $sum
	 *
	 * @return array
	 */
	protected function generateHashes($sum)
	{
		// Round the time to 30 minutes
		$time = (int) round(time() / 60 / 30);

		return array_map(
			function ($hashTime) use ($sum)
			{
				return hash_hmac('sha256', $sum . "\0" . $hashTime, System::getContainer()->getParameter('kernel.secret'));
			},
			array($time, $time - 1)
		);
	}

	/**
	 * Generate the captcha question
	 *
	 * @return string The question string
	 */
	protected function getQuestion()
	{
		$this->generateCaptcha();

		$question = $GLOBALS['TL_LANG']['SEC']['question' . random_int(1, 3)];
		$question = sprintf($question, $this->arrCaptcha['int1'], $this->arrCaptcha['int2']);

		$strEncoded = '';
		$arrCharacters = Utf8::str_split($question);

		foreach ($arrCharacters as $strCharacter)
		{
			$strEncoded .= sprintf('&#%s;', Utf8::ord($strCharacter));
		}

		return $strEncoded;
	}

	/**
	 * Get the correct sum for the current captcha
	 *
	 * @return int The sum
	 */
	protected function getSum()
	{
		$this->generateCaptcha();

		return $this->arrCaptcha['sum'];
	}

	/**
	 * Get the correct hash for the current captcha
	 *
	 * @return string The hash
	 */
	protected function getHash()
	{
		$this->generateCaptcha();

		return $this->arrCaptcha['hashes'][0];
	}

	/**
	 * Generate the label and return it as string
	 *
	 * @return string The label markup
	 */
	public function generateLabel()
	{
		if ($this->strLabel == '')
		{
			return '';
		}

		return sprintf('%s %s* %s',
						$this->strId,
						(($this->strClass != '') ? ' ' . $this->strClass : ''),
						$GLOBALS['TL_LANG']['MSC']['mandatory'],
						$this->strLabel,
						$this->getQuestion());
	}

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

	/**
	 * Return the captcha question as string
	 *
	 * @return string The question markup
	 */
	public function generateQuestion()
	{
		return sprintf('%s',
						$this->strId,
						(($this->strClass != '') ? ' ' . $this->strClass : ''),
						$this->getQuestion());
	}
}

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