<?php

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

namespace Contao;

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

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

	/**
	 * Generate the content element
	 */
	protected function compile()
	{
		if (substr($this->url, 0, 7) == 'mailto:')
		{
			$this->url = StringUtil::encodeEmail($this->url);
		}
		else
		{
			$this->url = ampersand($this->url);
		}

		$embed = explode('%s', $this->embed);

		// Use an image instead of the title
		if ($this->useImage && $this->singleSRC != '')
		{
			$objModel = FilesModel::findByUuid($this->singleSRC);

			if ($objModel !== null && is_file(System::getContainer()->getParameter('kernel.project_dir') . '/' . $objModel->path))
			{
				$this->singleSRC = $objModel->path;
				$this->addImageToTemplate($this->Template, $this->arrData, null, null, $objModel);
				$this->Template->useImage = true;
			}
		}

		if ($this->rel)
		{
			$this->Template->attribute = ' data-lightbox="'. $this->rel .'"';
		}

		if ($this->linkTitle == '')
		{
			$this->linkTitle = $this->url;
		}

		$this->Template->href = $this->url;
		$this->Template->embed_pre = $embed[0];
		$this->Template->embed_post = $embed[1];
		$this->Template->link = $this->linkTitle;
		$this->Template->target = '';
		$this->Template->rel = '';

		if ($this->titleText)
		{
			$this->Template->linkTitle = StringUtil::specialchars($this->titleText);
		}

		// Override the link target
		if ($this->target)
		{
			$this->Template->target = ' target="_blank"';
			$this->Template->rel = ' rel="noreferrer noopener"';
		}

		// Unset the title attributes in the back end (see #6258)
		if (TL_MODE == 'BE')
		{
			$this->Template->title = '';
			$this->Template->linkTitle = '';
		}
	}
}

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


<?php

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

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

	// Config
	'config' => array
	(
		'dataContainer'               => 'Table',
		'ptable'                      => 'tl_theme',
		'ctable'                      => array('tl_image_size_item'),
		'switchToEdit'                => true,
		'enableVersioning'            => true,
		'markAsCopy'                  => 'name',
		'onload_callback' => array
		(
			array('tl_image_size', 'checkPermission'),
			array('tl_image_size', 'showJsLibraryHint')
		),
		'oncreate_callback' => array
		(
			array('tl_image_size', 'adjustPermissions')
		),
		'oncopy_callback' => array
		(
			array('tl_image_size', 'adjustPermissions')
		),
		'sql' => array
		(
			'keys' => array
			(
				'id' => 'primary',
				'pid' => 'index'
			)
		)
	),

	// List
	'list' => array
	(
		'sorting' => array
		(
			'mode'                    => 4,
			'fields'                  => array('name'),
			'panelLayout'             => 'filter;search,limit',
			'headerFields'            => array('name', 'author', 'tstamp'),
			'child_record_callback'   => array('tl_image_size', 'listImageSize')
		),
		'global_operations' => array
		(
			'all' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['MSC']['all'],
				'href'                => 'act=select',
				'class'               => 'header_edit_all',
				'attributes'          => 'onclick="Backend.getScrollOffset()" accesskey="e"'
			)
		),
		'operations' => array
		(
			'edit' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size']['edit'],
				'href'                => 'table=tl_image_size_item',
				'icon'                => 'edit.svg'
			),
			'editheader' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size']['editheader'],
				'href'                => 'table=tl_image_size&act=edit',
				'icon'                => 'header.svg',
				'button_callback'     => array('tl_image_size', 'editHeader')
			),
			'copy' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size']['copy'],
				'href'                => 'act=paste&mode=copy',
				'icon'                => 'copy.svg'
			),
			'cut' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size']['cut'],
				'href'                => 'act=paste&mode=cut',
				'icon'                => 'cut.svg',
				'attributes'          => 'onclick="Backend.getScrollOffset()"'
			),
			'delete' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size']['delete'],
				'href'                => 'act=delete',
				'icon'                => 'delete.svg',
				'attributes'          => 'onclick="if(!confirm(\'' . $GLOBALS['TL_LANG']['MSC']['deleteConfirm'] . '\'))return false;Backend.getScrollOffset()"'
			),
			'show' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size']['show'],
				'href'                => 'act=show',
				'icon'                => 'show.svg'
			)
		)
	),

	// Palettes
	'palettes' => array
	(
		'default'                     => '{title_legend},name,width,height,resizeMode,zoom;{expert_legend},cssClass,densities,sizes'
	),

	// Fields
	'fields' => array
	(
		'id' => array
		(
			'sql'                     => "int(10) unsigned NOT NULL auto_increment"
		),
		'pid' => array
		(
			'foreignKey'              => 'tl_theme.name',
			'sql'                     => "int(10) unsigned NOT NULL default 0",
			'relation'                => array('type'=>'belongsTo', 'load'=>'lazy')
		),
		'tstamp' => array
		(
			'sql'                     => "int(10) unsigned NOT NULL default 0"
		),
		'name' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_image_size']['name'],
			'inputType'               => 'text',
			'exclude'                 => true,
			'search'                  => true,
			'flag'                    => 1,
			'eval'                    => array('mandatory'=>true, 'maxlength'=>64, 'tl_class'=>'w50'),
			'sql'                     => "varchar(64) NULL"
		),
		'cssClass' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_image_size']['cssClass'],
			'inputType'               => 'text',
			'exclude'                 => true,
			'search'                  => true,
			'eval'                    => array('maxlength'=>64, 'tl_class'=>'w50'),
			'sql'                     => "varchar(255) NOT NULL default ''"
		),
		'densities' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_image_size']['densities'],
			'inputType'               => 'text',
			'explanation'             => 'imageSizeDensities',
			'exclude'                 => true,
			'eval'                    => array('helpwizard'=>true, 'maxlength'=>255, 'tl_class'=>'w50'),
			'sql'                     => "varchar(255) NOT NULL default ''"
		),
		'sizes' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_image_size']['sizes'],
			'inputType'               => 'text',
			'explanation'             => 'imageSizeDensities',
			'exclude'                 => true,
			'eval'                    => array('helpwizard'=>true, 'maxlength'=>255, 'tl_class'=>'clr', 'decodeEntities'=>true),
			'sql'                     => "varchar(255) NOT NULL default ''"
		),
		'width' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_image_size']['width'],
			'inputType'               => 'text',
			'exclude'                 => true,
			'eval'                    => array('rgxp'=>'digit', 'nospace'=>true, 'tl_class'=>'clr w50'),
			'sql'                     => "int(10) NULL"
		),
		'height' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_image_size']['height'],
			'inputType'               => 'text',
			'exclude'                 => true,
			'eval'                    => array('rgxp'=>'digit', 'nospace'=>true, 'tl_class'=>'w50'),
			'sql'                     => "int(10) NULL"
		),
		'resizeMode' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_image_size']['resizeMode'],
			'inputType'               => 'select',
			'options'                 => array('proportional', 'box', 'crop'),
			'reference'               => &$GLOBALS['TL_LANG']['tl_image_size'],
			'exclude'                 => true,
			'eval'                    => array('helpwizard'=>true, 'tl_class'=>'clr w50'),
			'sql'                     => "varchar(255) NOT NULL default ''"
		),
		'zoom' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_image_size']['zoom'],
			'inputType'               => 'text',
			'exclude'                 => true,
			'eval'                    => array('rgxp'=>'prcnt', 'nospace'=>true, 'tl_class'=>'w50'),
			'sql'                     => "int(10) NULL"
		)
	)
);

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

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

	/**
	 * Check permissions to edit the table
	 *
	 * @throws Contao\CoreBundle\Exception\AccessDeniedException
	 */
	public function checkPermission()
	{
		if ($this->User->isAdmin)
		{
			return;
		}

		if (!$this->User->hasAccess('image_sizes', 'themes'))
		{
			throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to access the image sizes module.');
		}
	}

	/**
	 * Add the new image size to the permissions
	 *
	 * @param $insertId
	 */
	public function adjustPermissions($insertId)
	{
		// The oncreate_callback passes $insertId as second argument
		if (\func_num_args() == 4)
		{
			$insertId = func_get_arg(1);
		}

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

		// Set the image sizes
		if (empty($this->User->imageSizes) || !\is_array($this->User->imageSizes))
		{
			$imageSizes = array();
		}
		else
		{
			$imageSizes = $this->User->imageSizes;
		}

		// The image size is enabled already
		if (\in_array($insertId, $imageSizes))
		{
			return;
		}

		/** @var Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface $objSessionBag */
		$objSessionBag = Contao\System::getContainer()->get('session')->getBag('contao_backend');

		$arrNew = $objSessionBag->get('new_records');

		if (\is_array($arrNew['tl_image_size']) && \in_array($insertId, $arrNew['tl_image_size']))
		{
			// Add the permissions on group level
			if ($this->User->inherit != 'custom')
			{
				$objGroup = $this->Database->execute("SELECT id, themes, imageSizes FROM tl_user_group WHERE id IN(" . implode(',', array_map('\intval', $this->User->groups)) . ")");

				while ($objGroup->next())
				{
					$arrThemes = Contao\StringUtil::deserialize($objGroup->themes);

					if (\is_array($arrThemes) && \in_array('image_sizes', $arrThemes))
					{
						$arrImageSizes = Contao\StringUtil::deserialize($objGroup->imageSizes, true);
						$arrImageSizes[] = $insertId;

						$this->Database->prepare("UPDATE tl_user_group SET imageSizes=? WHERE id=?")
									   ->execute(serialize($arrImageSizes), $objGroup->id);
					}
				}
			}

			// Add the permissions on user level
			if ($this->User->inherit != 'group')
			{
				$objUser = $this->Database->prepare("SELECT themes, imageSizes FROM tl_user WHERE id=?")
										   ->limit(1)
										   ->execute($this->User->id);

				$arrThemes = Contao\StringUtil::deserialize($objUser->themes);

				if (\is_array($arrThemes) && \in_array('image_sizes', $arrThemes))
				{
					$arrImageSizes = Contao\StringUtil::deserialize($objUser->imageSizes, true);
					$arrImageSizes[] = $insertId;

					$this->Database->prepare("UPDATE tl_user SET imageSizes=? WHERE id=?")
								   ->execute(serialize($arrImageSizes), $this->User->id);
				}
			}

			// Add the new element to the user object
			$imageSizes[] = $insertId;
			$this->User->imageSizes = $imageSizes;
		}
	}

	/**
	 * List an image size
	 *
	 * @param array $row
	 *
	 * @return string
	 */
	public function listImageSize($row)
	{
		$html = '
';
		$html .= $row['name'];

		if ($row['width'] || $row['height'])
		{
			$html .= ' ' . $row['width'] . 'x' . $row['height'] . '';
		}

		if ($row['zoom'])
		{
			$html .= ' (' . (int) $row['zoom'] . '%)';
		}

		$html .= "
\n";

		return $html;
	}

	/**
	 * Show a hint if a JavaScript library needs to be included in the page layout
	 */
	public function showJsLibraryHint()
	{
		if ($_POST || Contao\Input::get('act') != 'edit')
		{
			return;
		}

		// Return if the user cannot access the layout module (see #6190)
		if (!$this->User->hasAccess('themes', 'modules') || !$this->User->hasAccess('layout', 'themes'))
		{
			return;
		}
	}

	/**
	 * Return the edit header button
	 *
	 * @param array  $row
	 * @param string $href
	 * @param string $label
	 * @param string $title
	 * @param string $icon
	 * @param string $attributes
	 *
	 * @return string
	 */
	public function editHeader($row, $href, $label, $title, $icon, $attributes)
	{
		return $this->User->canEditFieldsOf('tl_image_size') ? 'addToUrl($href.'&id='.$row['id']).'" title="'.Contao\StringUtil::specialchars($title).'"'.$attributes.'>'.Contao\Image::getHtml($icon, $label).' ' : Contao\Image::getHtml(preg_replace('/\.svg$/i', '_.svg', $icon)).' ';
	}
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Routing\Matcher;

use Contao\Config;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\Input;
use Contao\PageModel;
use Contao\System;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\RequestMatcherInterface;

class LegacyMatcher implements RequestMatcherInterface
{
    /**
     * @var ContaoFramework
     */
    private $framework;

    /**
     * @var RequestMatcherInterface
     */
    private $requestMatcher;

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

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

    public function __construct(ContaoFramework $framework, RequestMatcherInterface $requestMatcher, string $urlSuffix, bool $prependLocale)
    {
        $this->framework = $framework;
        $this->requestMatcher = $requestMatcher;
        $this->urlSuffix = $urlSuffix;
        $this->prependLocale = $prependLocale;
    }

    /**
     * {@inheritdoc}
     */
    public function matchRequest(Request $request): array
    {
        $this->framework->initialize(true);

        $pathInfo = rawurldecode($request->getPathInfo());

        if (
            '/' === $pathInfo
            || empty($GLOBALS['TL_HOOKS']['getPageIdFromUrl'])
            || !\is_array($GLOBALS['TL_HOOKS']['getPageIdFromUrl'])
            || ($this->prependLocale && preg_match('@^/([a-z]{2}(-[A-Z]{2})?)/$@', $pathInfo))
        ) {
            return $this->requestMatcher->matchRequest($request);
        }

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

        $locale = null;
        $fragments = null;

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

        if ($config->get('folderUrl')) {
            try {
                $match = $this->requestMatcher->matchRequest($request);
                $fragments = $this->createFragmentsFromMatch($match);
                $locale = $match['_locale'] ?? null;
            } catch (ResourceNotFoundException $e) {
                // continue and parse fragments from path
            }
        }

        if (null === $fragments) {
            $pathInfo = $this->parseSuffixAndLanguage($pathInfo, $locale);
            $fragments = $this->createFragmentsFromPath($pathInfo);
        }

        if ($this->prependLocale) {
            if (null === $locale) {
                throw new ResourceNotFoundException('Locale is missing');
            }

            /** @var Input $input */
            $input = $this->framework->getAdapter(Input::class);
            $input->setGet('language', $locale);
        }

        $fragments = $this->executeLegacyHook($fragments);
        $pathInfo = $this->createPathFromFragments($fragments, $locale);

        return $this->requestMatcher->matchRequest($this->rebuildRequest($pathInfo, $request));
    }

    private function createFragmentsFromMatch(array $match): array
    {
        $page = $match['pageModel'] ?? null;
        $parameters = $match['parameters'] ?? '';

        if (!$page instanceof PageModel) {
            throw new ResourceNotFoundException('Resource not found');
        }

        if ('' === $parameters) {
            return [$page->alias];
        }

        /** @var Config $config */
        $config = $this->framework->getAdapter(Config::class);
        $fragments = array_merge([$page->alias], explode('/', substr($parameters, 1)));

        // Add the second fragment as auto_item if the number of fragments is even
        if ($config->get('useAutoItem') && 0 === \count($fragments) % 2) {
            array_splice($fragments, 1, 0, ['auto_item']);
        }

        return $fragments;
    }

    private function createFragmentsFromPath(string $pathInfo): array
    {
        /** @var Config $config */
        $config = $this->framework->getAdapter(Config::class);
        $fragments = explode('/', $pathInfo);

        // Add the second fragment as auto_item if the number of fragments is even
        if ($config->get('useAutoItem') && 0 === \count($fragments) % 2) {
            array_splice($fragments, 1, 0, ['auto_item']);
        }

        return $fragments;
    }

    private function executeLegacyHook(array $fragments): array
    {
        /** @var System $system */
        $system = $this->framework->getAdapter(System::class);

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

        // Return if the alias is empty (see #4702 and #4972)
        if ('' === $fragments[0]) {
            throw new ResourceNotFoundException('Page alias is empty');
        }

        return $fragments;
    }

    private function createPathFromFragments(array $fragments, ?string $locale): string
    {
        /** @var Config $config */
        $config = $this->framework->getAdapter(Config::class);

        if (isset($fragments[1]) && 'auto_item' === $fragments[1] && $config->get('useAutoItem')) {
            unset($fragments[1]);
        }

        $pathInfo = implode('/', $fragments).$this->urlSuffix;

        if ($this->prependLocale) {
            $pathInfo = $locale.'/'.$pathInfo;
        }

        return '/'.$pathInfo;
    }

    private function parseSuffixAndLanguage(string $pathInfo, ?string &$locale): string
    {
        $suffixLength = \strlen($this->urlSuffix);

        if (0 !== $suffixLength) {
            if (substr($pathInfo, -$suffixLength) !== $this->urlSuffix) {
                throw new ResourceNotFoundException('URL suffix does not match');
            }

            $pathInfo = substr($pathInfo, 0, -$suffixLength);
        }

        if (0 === strncmp($pathInfo, '/', 1)) {
            $pathInfo = substr($pathInfo, 1);
        }

        if ($this->prependLocale) {
            $matches = [];

            if (!preg_match('@^([a-z]{2}(-[A-Z]{2})?)/(.+)$@', $pathInfo, $matches)) {
                throw new ResourceNotFoundException('Locale does not match');
            }

            [, $locale,, $pathInfo] = $matches;
        }

        return $pathInfo;
    }

    /**
     * @see ChainRouter::rebuildRequest()
     */
    private function rebuildRequest(string $pathinfo, Request $request): Request
    {
        $uri = $pathinfo;
        $server = [];

        if ($request->getBaseUrl()) {
            $uri = $request->getBaseUrl().$pathinfo;
            $server['SCRIPT_FILENAME'] = $request->getBaseUrl();
            $server['PHP_SELF'] = $request->getBaseUrl();
        }

        $host = $request->getHttpHost() ?: 'localhost';
        $scheme = $request->getScheme() ?: 'http';
        $uri = $scheme.'://'.$host.$uri.'?'.$request->getQueryString();

        return Request::create($uri, $request->getMethod(), [], [], [], $server);
    }
}


<?php

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

namespace Contao;

use Contao\Database\Result;
use Contao\Database\Statement;
use Contao\Model\Collection;
use Contao\Model\QueryBuilder;
use Contao\Model\Registry;

/**
 * Reads objects from and writes them to to the database
 *
 * The class allows you to find and automatically join database records and to
 * convert the result into objects. It also supports creating new objects and
 * persisting them in the database.
 *
 * Usage:
 *
 *     // Write
 *     $user = new UserModel();
 *     $user->name = 'Leo Feyer';
 *     $user->city = 'Wuppertal';
 *     $user->save();
 *
 *     // Read
 *     $user = UserModel::findByCity('Wuppertal');
 *
 *     while ($user->next())
 *     {
 *         echo $user->name;
 *     }
 *
 * @property integer $id        The ID
 * @property string  $customTpl A custom template
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
abstract class Model
{

	/**
	 * Insert flag
	 * @var integer
	 */
	const INSERT = 1;

	/**
	 * Update flag
	 * @var integer
	 */
	const UPDATE = 2;

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

	/**
	 * Primary key
	 * @var string
	 */
	protected static $strPk = 'id';

	/**
	 * Class name cache
	 * @var array
	 */
	protected static $arrClassNames = array();

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

	/**
	 * Modified keys
	 * @var array
	 */
	protected $arrModified = array();

	/**
	 * Relations
	 * @var array
	 */
	protected $arrRelations = array();

	/**
	 * Related
	 * @var array
	 */
	protected $arrRelated = array();

	/**
	 * Prevent saving
	 * @var boolean
	 */
	protected $blnPreventSaving = false;

	/**
	 * Load the relations and optionally process a result set
	 *
	 * @param Result|array $objResult An optional database result or array
	 */
	public function __construct($objResult=null)
	{
		$this->arrModified = array();

		$objDca = DcaExtractor::getInstance(static::$strTable);
		$this->arrRelations = $objDca->getRelations();

		if ($objResult !== null)
		{
			$arrRelated = array();

			if ($objResult instanceof Result)
			{
				$arrData = $objResult->row();
			}
			else
			{
				$arrData = (array) $objResult;
			}

			// Look for joined fields
			foreach ($arrData as $k=>$v)
			{
				if (strpos($k, '__') !== false)
				{
					list($key, $field) = explode('__', $k, 2);

					if (!isset($arrRelated[$key]))
					{
						$arrRelated[$key] = array();
					}

					$arrRelated[$key][$field] = $v;
					unset($arrData[$k]);
				}
			}

			$objRegistry = Registry::getInstance();

			$this->setRow($arrData); // see #5439
			$objRegistry->register($this);

			// Create the related models
			foreach ($arrRelated as $key=>$row)
			{
				$table = $this->arrRelations[$key]['table'];

				/** @var static $strClass */
				$strClass = static::getClassFromTable($table);
				$intPk = $strClass::getPk();

				// If the primary key is empty, set null (see #5356)
				if (!isset($row[$intPk]))
				{
					$this->arrRelated[$key] = null;
				}
				else
				{
					$objRelated = $objRegistry->fetch($table, $row[$intPk]);

					if ($objRelated !== null)
					{
						$objRelated->mergeRow($row);
					}
					else
					{
						/** @var static $objRelated */
						$objRelated = new $strClass();
						$objRelated->setRow($row);

						$objRegistry->register($objRelated);
					}

					$this->arrRelated[$key] = $objRelated;
				}
			}
		}
	}

	/**
	 * Unset the primary key when cloning an object
	 */
	public function __clone()
	{
		$this->arrModified = array();
		$this->blnPreventSaving = false;

		unset($this->arrData[static::$strPk]);
	}

	/**
	 * Clone a model with its original values
	 *
	 * @return static The model
	 */
	public function cloneOriginal()
	{
		$clone = clone $this;
		$clone->setRow($this->originalRow());

		return $clone;
	}

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

		$this->markModified($strKey);
		$this->arrData[$strKey] = $varValue;

		unset($this->arrRelated[$strKey]);
	}

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

		return null;
	}

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

	/**
	 * Return the name of the primary key
	 *
	 * @return string The primary key
	 */
	public static function getPk()
	{
		return static::$strPk;
	}

	/**
	 * Return an array of unique field/column names (without the PK)
	 *
	 * @return array
	 */
	public static function getUniqueFields()
	{
		$objDca = DcaExtractor::getInstance(static::getTable());

		return $objDca->getUniqueFields();
	}

	/**
	 * Return the name of the related table
	 *
	 * @return string The table name
	 */
	public static function getTable()
	{
		return static::$strTable;
	}

	/**
	 * Return the current record as associative array
	 *
	 * @return array The data record
	 */
	public function row()
	{
		return $this->arrData;
	}

	/**
	 * Return the original values as associative array
	 *
	 * @return array The original data
	 */
	public function originalRow()
	{
		$row = $this->row();

		if (!$this->isModified())
		{
			return $row;
		}

		$originalRow = array();

		foreach ($row as $k=>$v)
		{
			$originalRow[$k] = $this->arrModified[$k] ?? $v;
		}

		return $originalRow;
	}

	/**
	 * Return true if the model has been modified
	 *
	 * @return boolean True if the model has been modified
	 */
	public function isModified()
	{
		return !empty($this->arrModified);
	}

	/**
	 * Set the current record from an array
	 *
	 * @param array $arrData The data record
	 *
	 * @return static The model object
	 */
	public function setRow(array $arrData)
	{
		foreach ($arrData as $k=>$v)
		{
			if (strpos($k, '__') !== false)
			{
				unset($arrData[$k]);
			}
		}

		$this->arrData = $arrData;

		return $this;
	}

	/**
	 * Set the current record from an array preserving modified but unsaved fields
	 *
	 * @param array $arrData The data record
	 *
	 * @return static The model object
	 */
	public function mergeRow(array $arrData)
	{
		foreach ($arrData as $k=>$v)
		{
			if (strpos($k, '__') !== false)
			{
				continue;
			}

			if (!isset($this->arrModified[$k]))
			{
				$this->arrData[$k] = $v;
			}
		}

		return $this;
	}

	/**
	 * Mark a field as modified
	 *
	 * @param string $strKey The field key
	 */
	public function markModified($strKey)
	{
		if (!isset($this->arrModified[$strKey]))
		{
			$this->arrModified[$strKey] = $this->arrData[$strKey] ?? null;
		}
	}

	/**
	 * Return the object instance
	 *
	 * @return static The model object
	 */
	public function current()
	{
		return $this;
	}

	/**
	 * Save the current record
	 *
	 * @return static The model object
	 *
	 * @throws \InvalidArgumentException If an argument is passed
	 * @throws \RuntimeException         If the model cannot be saved
	 */
	public function save()
	{
		// Deprecated call
		if (\count(\func_get_args()))
		{
			throw new \InvalidArgumentException('The $blnForceInsert argument has been removed (see system/docs/UPGRADE.md)');
		}

		// The instance cannot be saved
		if ($this->blnPreventSaving)
		{
			throw new \RuntimeException('The model instance has been detached and cannot be saved');
		}

		$objDatabase = Database::getInstance();
		$arrFields = $objDatabase->getFieldNames(static::$strTable);

		// The model is in the registry
		if (Registry::getInstance()->isRegistered($this))
		{
			$arrSet = array();
			$arrRow = $this->row();

			// Only update modified fields
			foreach ($this->arrModified as $k=>$v)
			{
				// Only set fields that exist in the DB
				if (\in_array($k, $arrFields))
				{
					$arrSet[$k] = $arrRow[$k];
				}
			}

			$arrSet = $this->preSave($arrSet);

			// No modified fiels
			if (empty($arrSet))
			{
				return $this;
			}

			$intPk = $this->{static::$strPk};

			// Track primary key changes
			if (isset($this->arrModified[static::$strPk]))
			{
				$intPk = $this->arrModified[static::$strPk];
			}

			if ($intPk === null)
			{
				throw new \RuntimeException('The primary key has not been set');
			}

			// Update the row
			$objDatabase->prepare("UPDATE " . static::$strTable . " %s WHERE " . Database::quoteIdentifier(static::$strPk) . "=?")
						->set($arrSet)
						->execute($intPk);

			$this->postSave(self::UPDATE);
			$this->arrModified = array(); // reset after postSave()
		}

		// The model is not yet in the registry
		else
		{
			$arrSet = $this->row();

			// Remove fields that do not exist in the DB
			foreach ($arrSet as $k=>$v)
			{
				if (!\in_array($k, $arrFields))
				{
					unset($arrSet[$k]);
				}
			}

			$arrSet = $this->preSave($arrSet);

			// No modified fiels
			if (empty($arrSet))
			{
				return $this;
			}

			// Insert a new row
			$stmt = $objDatabase->prepare("INSERT INTO " . static::$strTable . " %s")
								->set($arrSet)
								->execute();

			if (static::$strPk == 'id')
			{
				$this->id = $stmt->insertId;
			}

			$this->postSave(self::INSERT);
			$this->arrModified = array(); // reset after postSave()

			Registry::getInstance()->register($this);
		}

		return $this;
	}

	/**
	 * Modify the current row before it is stored in the database
	 *
	 * @param array $arrSet The data array
	 *
	 * @return array The modified data array
	 */
	protected function preSave(array $arrSet)
	{
		return $arrSet;
	}

	/**
	 * Modify the current row after it has been stored in the database
	 *
	 * @param integer $intType The query type (Model::INSERT or Model::UPDATE)
	 */
	protected function postSave($intType)
	{
		if ($intType == self::INSERT)
		{
			$this->refresh(); // might have been modified by default values or triggers
		}
	}

	/**
	 * Delete the current record and return the number of affected rows
	 *
	 * @return integer The number of affected rows
	 */
	public function delete()
	{
		$intPk = $this->{static::$strPk};

		// Track primary key changes
		if (isset($this->arrModified[static::$strPk]))
		{
			$intPk = $this->arrModified[static::$strPk];
		}

		// Delete the row
		$intAffected = Database::getInstance()->prepare("DELETE FROM " . static::$strTable . " WHERE " . Database::quoteIdentifier(static::$strPk) . "=?")
											   ->execute($intPk)
											   ->affectedRows;

		if ($intAffected)
		{
			// Unregister the model
			Registry::getInstance()->unregister($this);

			// Remove the primary key (see #6162)
			$this->arrData[static::$strPk] = null;
		}

		return $intAffected;
	}

	/**
	 * Lazy load related records
	 *
	 * @param string $strKey     The property name
	 * @param array  $arrOptions An optional options array
	 *
	 * @return static|Collection|null The model or a model collection if there are multiple rows
	 *
	 * @throws \Exception If $strKey is not a related field
	 */
	public function getRelated($strKey, array $arrOptions=array())
	{
		// The related model has been loaded before
		if (\array_key_exists($strKey, $this->arrRelated))
		{
			return $this->arrRelated[$strKey];
		}

		// The relation does not exist
		if (!isset($this->arrRelations[$strKey]))
		{
			throw new \Exception("Field $strKey does not seem to be related");
		}

		// The relation exists but there is no reference yet (see #6161 and #458)
		if (empty($this->$strKey))
		{
			return null;
		}

		$arrRelation = $this->arrRelations[$strKey];

		/** @var static $strClass */
		$strClass = static::getClassFromTable($arrRelation['table']);

		// Load the related record(s)
		if ($arrRelation['type'] == 'hasOne' || $arrRelation['type'] == 'belongsTo')
		{
			$this->arrRelated[$strKey] = $strClass::findOneBy($arrRelation['field'], $this->$strKey, $arrOptions);;
		}
		elseif ($arrRelation['type'] == 'hasMany' || $arrRelation['type'] == 'belongsToMany')
		{
			if (isset($arrRelation['delimiter']))
			{
				$arrValues = StringUtil::trimsplit($arrRelation['delimiter'], $this->$strKey);
			}
			else
			{
				$arrValues = StringUtil::deserialize($this->$strKey, true);
			}

			$objModel = null;

			if (\is_array($arrValues))
			{
				// Handle UUIDs (see #6525 and #8850)
				if ($arrRelation['table'] == 'tl_files' && $arrRelation['field'] == 'uuid')
				{
					/** @var FilesModel $strClass */
					$objModel = $strClass::findMultipleByUuids($arrValues, $arrOptions);
				}
				else
				{
					$strField = $arrRelation['table'] . '.' . Database::quoteIdentifier($arrRelation['field']);

					$arrOptions = array_merge
					(
						array
						(
							'order' => Database::getInstance()->findInSet($strField, $arrValues)
						),
						$arrOptions
					);

					$objModel = $strClass::findBy(array($strField . " IN('" . implode("','", $arrValues) . "')"), null, $arrOptions);
				}
			}

			$this->arrRelated[$strKey] = $objModel;
		}

		return $this->arrRelated[$strKey];
	}

	/**
	 * Reload the data from the database discarding all modifications
	 */
	public function refresh()
	{
		$intPk = $this->{static::$strPk};

		// Track primary key changes
		if (isset($this->arrModified[static::$strPk]))
		{
			$intPk = $this->arrModified[static::$strPk];
		}

		// Reload the database record
		$res = Database::getInstance()->prepare("SELECT * FROM " . static::$strTable . " WHERE " . Database::quoteIdentifier(static::$strPk) . "=?")
									   ->execute($intPk);

		$this->setRow($res->row());
	}

	/**
	 * Detach the model from the registry
	 *
	 * @param boolean $blnKeepClone Keeps a clone of the model in the registry
	 */
	public function detach($blnKeepClone=true)
	{
		$registry = Registry::getInstance();

		if (!$registry->isRegistered($this))
		{
			return;
		}

		$registry->unregister($this);

		if ($blnKeepClone)
		{
			$this->cloneOriginal()->attach();
		}
	}

	/**
	 * Attach the model to the registry
	 */
	public function attach()
	{
		Registry::getInstance()->register($this);
	}

	/**
	 * Called when the model is attached to the model registry
	 *
	 * @param Registry $registry The model registry
	 */
	public function onRegister(Registry $registry)
	{
		// Register aliases to unique fields
		foreach (static::getUniqueFields() as $strColumn)
		{
			$varAliasValue = $this->{$strColumn};

			if (!$registry->isRegisteredAlias($this, $strColumn, $varAliasValue))
			{
				$registry->registerAlias($this, $strColumn, $varAliasValue);
			}
		}
	}

	/**
	 * Called when the model is detached from the model registry
	 *
	 * @param Registry $registry The model registry
	 */
	public function onUnregister(Registry $registry)
	{
		// Unregister aliases to unique fields
		foreach (static::getUniqueFields() as $strColumn)
		{
			$varAliasValue = $this->{$strColumn};

			if ($registry->isRegisteredAlias($this, $strColumn, $varAliasValue))
			{
				$registry->unregisterAlias($this, $strColumn, $varAliasValue);
			}
		}
	}

	/**
	 * Prevent saving the model
	 *
	 * @param boolean $blnKeepClone Keeps a clone of the model in the registry
	 */
	public function preventSaving($blnKeepClone=true)
	{
		$this->detach($blnKeepClone);
		$this->blnPreventSaving = true;
	}

	/**
	 * Find a single record by its primary key
	 *
	 * @param mixed $varValue   The property value
	 * @param array $arrOptions An optional options array
	 *
	 * @return static The model or null if the result is empty
	 */
	public static function findByPk($varValue, array $arrOptions=array())
	{
		// Try to load from the registry
		if (empty($arrOptions))
		{
			$objModel = Registry::getInstance()->fetch(static::$strTable, $varValue);

			if ($objModel !== null)
			{
				return $objModel;
			}
		}

		$arrOptions = array_merge
		(
			array
			(
				'limit'  => 1,
				'column' => static::$strPk,
				'value'  => $varValue,
				'return' => 'Model'
			),

			$arrOptions
		);

		return static::find($arrOptions);
	}

	/**
	 * Find a single record by its ID or alias
	 *
	 * @param mixed $varId      The ID or alias
	 * @param array $arrOptions An optional options array
	 *
	 * @return static The model or null if the result is empty
	 */
	public static function findByIdOrAlias($varId, array $arrOptions=array())
	{
		$isAlias = !preg_match('/^[1-9]\d*$/', $varId);

		// Try to load from the registry
		if (!$isAlias && empty($arrOptions))
		{
			$objModel = Registry::getInstance()->fetch(static::$strTable, $varId);

			if ($objModel !== null)
			{
				return $objModel;
			}
		}

		$t = static::$strTable;

		$arrOptions = array_merge
		(
			array
			(
				'limit'  => 1,
				'column' => $isAlias ? array("$t.alias=?") : array("$t.id=?"),
				'value'  => $varId,
				'return' => 'Model'
			),

			$arrOptions
		);

		return static::find($arrOptions);
	}

	/**
	 * Find multiple records by their IDs
	 *
	 * @param array $arrIds     An array of IDs
	 * @param array $arrOptions An optional options array
	 *
	 * @return Collection|null The model collection or null if there are no records
	 */
	public static function findMultipleByIds($arrIds, array $arrOptions=array())
	{
		if (empty($arrIds) || !\is_array($arrIds))
		{
			return null;
		}

		$arrRegistered = array();
		$arrUnregistered = array();

		// Search for registered models
		foreach ($arrIds as $intId)
		{
			if (empty($arrOptions))
			{
				$arrRegistered[$intId] = Registry::getInstance()->fetch(static::$strTable, $intId);
			}

			if (!isset($arrRegistered[$intId]))
			{
				$arrUnregistered[] = $intId;
			}
		}

		// Fetch only the missing models from the database
		if (!empty($arrUnregistered))
		{
			$t = static::$strTable;

			$arrOptions = array_merge
			(
				array
				(
					'column' => array("$t.id IN(" . implode(',', array_map('\intval', $arrUnregistered)) . ")"),
					'value'  => null,
					'order'  => Database::getInstance()->findInSet("$t.id", $arrIds),
					'return' => 'Collection'
				),

				$arrOptions
			);

			$objMissing = static::find($arrOptions);

			if ($objMissing !== null)
			{
				while ($objMissing->next())
				{
					$intId = $objMissing->{static::$strPk};
					$arrRegistered[$intId] = $objMissing->current();
				}
			}
		}

		$arrRegistered = array_filter(array_values($arrRegistered));

		if (empty($arrRegistered))
		{
			return null;
		}

		return static::createCollection($arrRegistered, static::$strTable);
	}

	/**
	 * Find a single record by various criteria
	 *
	 * @param mixed $strColumn  The property name
	 * @param mixed $varValue   The property value
	 * @param array $arrOptions An optional options array
	 *
	 * @return static The model or null if the result is empty
	 */
	public static function findOneBy($strColumn, $varValue, array $arrOptions=array())
	{
		$arrOptions = array_merge
		(
			array
			(
				'limit'  => 1,
				'column' => $strColumn,
				'value'  => $varValue,
				'return' => 'Model'
			),

			$arrOptions
		);

		return static::find($arrOptions);
	}

	/**
	 * Find records by various criteria
	 *
	 * @param mixed $strColumn  The property name
	 * @param mixed $varValue   The property value
	 * @param array $arrOptions An optional options array
	 *
	 * @return static|Collection|null A model, model collection or null if the result is empty
	 */
	public static function findBy($strColumn, $varValue, array $arrOptions=array())
	{
		$blnModel = false;
		$arrColumn = (array) $strColumn;

		if (\count($arrColumn) == 1 && ($arrColumn[0] === static::getPk() || \in_array($arrColumn[0], static::getUniqueFields())))
		{
			$blnModel = true;
		}

		$arrOptions = array_merge
		(
			array
			(
				'column' => $strColumn,
				'value'  => $varValue,
				'return' => $blnModel ? 'Model' : 'Collection'
			),

			$arrOptions
		);

		return static::find($arrOptions);
	}

	/**
	 * Find all records
	 *
	 * @param array $arrOptions An optional options array
	 *
	 * @return Collection|null The model collection or null if the result is empty
	 */
	public static function findAll(array $arrOptions=array())
	{
		$arrOptions = array_merge
		(
			array
			(
				'return' => 'Collection'
			),

			$arrOptions
		);

		return static::find($arrOptions);
	}

	/**
	 * Magic method to map Model::findByName() to Model::findBy('name')
	 *
	 * @param string $name The method name
	 * @param array  $args The passed arguments
	 *
	 * @return static|Collection|integer|null A model or model collection
	 *
	 * @throws \Exception If the method name is invalid
	 */
	public static function __callStatic($name, $args)
	{
		if (strncmp($name, 'findBy', 6) === 0)
		{
			array_unshift($args, lcfirst(substr($name, 6)));

			return static::findBy(...$args);
		}

		if (strncmp($name, 'findOneBy', 9) === 0)
		{
			array_unshift($args, lcfirst(substr($name, 9)));

			return static::findOneBy(...$args);
		}

		if (strncmp($name, 'countBy', 7) === 0)
		{
			array_unshift($args, lcfirst(substr($name, 7)));

			return static::countBy(...$args);
		}

		throw new \Exception("Unknown method $name");
	}

	/**
	 * Find records and return the model or model collection
	 *
	 * Supported options:
	 *
	 * * column: the field name
	 * * value:  the field value
	 * * limit:  the maximum number of rows
	 * * offset: the number of rows to skip
	 * * order:  the sorting order
	 * * eager:  load all related records eagerly
	 *
	 * @param array $arrOptions The options array
	 *
	 * @return Model|Model[]|Collection|null A model, model collection or null if the result is empty
	 */
	protected static function find(array $arrOptions)
	{
		if (static::$strTable == '')
		{
			return null;
		}

		// Try to load from the registry
		if ($arrOptions['return'] == 'Model')
		{
			$arrColumn = (array) $arrOptions['column'];

			if (\count($arrColumn) == 1)
			{
				// Support table prefixes
				$arrColumn[0] = preg_replace('/^' . preg_quote(static::getTable(), '/') . '\./', '', $arrColumn[0]);

				if ($arrColumn[0] == static::$strPk || \in_array($arrColumn[0], static::getUniqueFields()))
				{
					$varKey = \is_array($arrOptions['value']) ? $arrOptions['value'][0] : $arrOptions['value'];
					$objModel = Registry::getInstance()->fetch(static::$strTable, $varKey, $arrColumn[0]);

					if ($objModel !== null)
					{
						return $objModel;
					}
				}
			}
		}

		$arrOptions['table'] = static::$strTable;
		$strQuery = static::buildFindQuery($arrOptions);

		$objStatement = Database::getInstance()->prepare($strQuery);

		// Defaults for limit and offset
		if (!isset($arrOptions['limit']))
		{
			$arrOptions['limit'] = 0;
		}
		if (!isset($arrOptions['offset']))
		{
			$arrOptions['offset'] = 0;
		}

		// Limit
		if ($arrOptions['limit'] > 0 || $arrOptions['offset'] > 0)
		{
			$objStatement->limit($arrOptions['limit'], $arrOptions['offset']);
		}

		$objStatement = static::preFind($objStatement);
		$objResult = $objStatement->execute($arrOptions['value']);

		if ($objResult->numRows < 1)
		{
			return $arrOptions['return'] == 'Array' ? array() : null;
		}

		$objResult = static::postFind($objResult);

		// Try to load from the registry
		if ($arrOptions['return'] == 'Model')
		{
			$objModel = Registry::getInstance()->fetch(static::$strTable, $objResult->{static::$strPk});

			if ($objModel !== null)
			{
				return $objModel->mergeRow($objResult->row());
			}

			return static::createModelFromDbResult($objResult);
		}
		elseif ($arrOptions['return'] == 'Array')
		{
			return static::createCollectionFromDbResult($objResult, static::$strTable)->getModels();
		}
		else
		{
			return static::createCollectionFromDbResult($objResult, static::$strTable);
		}
	}

	/**
	 * Modify the database statement before it is executed
	 *
	 * @param Statement $objStatement The database statement object
	 *
	 * @return Statement The database statement object
	 */
	protected static function preFind(Statement $objStatement)
	{
		return $objStatement;
	}

	/**
	 * Modify the database result before the model is created
	 *
	 * @param Result $objResult The database result object
	 *
	 * @return Result The database result object
	 */
	protected static function postFind(Result $objResult)
	{
		return $objResult;
	}

	/**
	 * Return the number of records matching certain criteria
	 *
	 * @param mixed $strColumn  An optional property name
	 * @param mixed $varValue   An optional property value
	 * @param array $arrOptions An optional options array
	 *
	 * @return integer The number of matching rows
	 */
	public static function countBy($strColumn=null, $varValue=null, array $arrOptions=array())
	{
		if (static::$strTable == '')
		{
			return 0;
		}

		$arrOptions = array_merge
		(
			array
			(
				'table'  => static::$strTable,
				'column' => $strColumn,
				'value'  => $varValue
			),

			$arrOptions
		);

		$strQuery = static::buildCountQuery($arrOptions);

		return (int) Database::getInstance()->prepare($strQuery)->execute($arrOptions['value'])->count;
	}

	/**
	 * Return the total number of rows
	 *
	 * @return integer The total number of rows
	 */
	public static function countAll()
	{
		return static::countBy();
	}

	/**
	 * Compile a Model class name from a table name (e.g. tl_form_field becomes FormFieldModel)
	 *
	 * @param string $strTable The table name
	 *
	 * @return string The model class name
	 */
	public static function getClassFromTable($strTable)
	{
		if (isset(static::$arrClassNames[$strTable]))
		{
			return static::$arrClassNames[$strTable];
		}

		if (isset($GLOBALS['TL_MODELS'][$strTable]))
		{
			static::$arrClassNames[$strTable] = $GLOBALS['TL_MODELS'][$strTable]; // see 4796

			return static::$arrClassNames[$strTable];
		}
		else
		{
			$arrChunks = explode('_', $strTable);

			if ($arrChunks[0] == 'tl')
			{
				array_shift($arrChunks);
			}

			static::$arrClassNames[$strTable] = implode('', array_map('ucfirst', $arrChunks)) . 'Model';

			return static::$arrClassNames[$strTable];
		}
	}

	/**
	 * Build a query based on the given options
	 *
	 * @param array $arrOptions The options array
	 *
	 * @return string The query string
	 */
	protected static function buildFindQuery(array $arrOptions)
	{
		return QueryBuilder::find($arrOptions);
	}

	/**
	 * Build a query based on the given options to count the number of records
	 *
	 * @param array $arrOptions The options array
	 *
	 * @return string The query string
	 */
	protected static function buildCountQuery(array $arrOptions)
	{
		return QueryBuilder::count($arrOptions);
	}

	/**
	 * Create a model from a database result
	 *
	 * @param Result $objResult The database result object
	 *
	 * @return static The model
	 */
	protected static function createModelFromDbResult(Result $objResult)
	{
		return new static($objResult);
	}

	/**
	 * Create a Collection object
	 *
	 * @param array  $arrModels An array of models
	 * @param string $strTable  The table name
	 *
	 * @return Collection The Collection object
	 */
	protected static function createCollection(array $arrModels, $strTable)
	{
		return new Collection($arrModels, $strTable);
	}

	/**
	 * Create a new collection from a database result
	 *
	 * @param Result $objResult The database result object
	 * @param string $strTable  The table name
	 *
	 * @return Collection The model collection
	 */
	protected static function createCollectionFromDbResult(Result $objResult, $strTable)
	{
		return Collection::createFromDbResult($objResult, $strTable);
	}

	/**
	 * Check if the preview mode is enabled
	 *
	 * @param array $arrOptions The options array
	 *
	 * @return boolean
	 */
	protected static function isPreviewMode(array $arrOptions)
	{
		if (isset($arrOptions['ignoreFePreview']))
		{
			return false;
		}

		return \defined('BE_USER_LOGGED_IN') && BE_USER_LOGGED_IN === true;
	}
}

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


<?php

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

namespace Contao;

/**
 * Parses a database.sql file.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class SqlFileParser
{

	/**
	 * Parse a database.sql file
	 *
	 * @param string $file The file path
	 *
	 * @return array An array of DCA table settings
	 *
	 * @throws \InvalidArgumentException If the file does not exist
	 */
	public static function parse($file)
	{
		if (!file_exists($file))
		{
			throw new \InvalidArgumentException('Invalid file ' . $file);
		}

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

		$data = file($file);

		foreach ($data as $k=>$v)
		{
			$key_name = array();
			$subpatterns = array();

			// Unset comments and empty lines
			if (preg_match('/^[#-]+/', $v) || !\strlen(trim($v)))
			{
				unset($data[$k]);
				continue;
			}

			// Store the table names
			if (preg_match('/^CREATE TABLE `([^`]+)`/i', $v, $subpatterns))
			{
				$table = $subpatterns[1];
			}
			// Get the table options
			elseif ($table != '' && preg_match('/^\)([^;]+);/', $v, $subpatterns))
			{
				$return[$table]['TABLE_OPTIONS'] = $subpatterns[1];
				$table = '';
			}
			// Add the fields
			elseif ($table != '')
			{
				preg_match('/^[^`]*`([^`]+)`/', trim($v), $key_name);
				$first = preg_replace('/\s[^\n\r]+/', '', $key_name[0]);
				$key = $key_name[1];

				// Create definitions
				if (\in_array($first, array('KEY', 'PRIMARY', 'PRIMARY KEY', 'FOREIGN', 'FOREIGN KEY', 'INDEX', 'UNIQUE', 'FULLTEXT', 'CHECK')))
				{
					if (strncmp($first, 'PRIMARY', 7) === 0)
					{
						$key = 'PRIMARY';
					}

					$return[$table]['TABLE_CREATE_DEFINITIONS'][$key] = preg_replace('/,$/', '', trim($v));
				}
				else
				{
					$return[$table]['TABLE_FIELDS'][$key] = preg_replace('/,$/', '', trim($v));
				}
			}
		}

		// Ignore the table options if there is no primary key
		foreach (array_keys($return) as $table)
		{
			if (!isset($return[$table]['TABLE_CREATE_DEFINITIONS']['PRIMARY']))
			{
				unset($return[$table]['TABLE_OPTIONS']);
			}
		}

		return $return;
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\ManagerBundle\ContaoManager\ApiCommand;

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

class GetConfigCommand extends Command
{
    /**
     * @var ManagerConfig
     */
    private $managerConfig;

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

        $this->managerConfig = $application->getManagerConfig();
    }

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

        $this
            ->setName('config:get')
            ->setDescription('Gets the Contao Manager configuration as JSON string.')
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output): void
    {
        $output->write(json_encode($this->managerConfig->all()));
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\DependencyInjection\Compiler;

use Contao\CoreBundle\Fragment\FragmentConfig;
use Contao\CoreBundle\Fragment\FragmentOptionsAwareInterface;
use Contao\CoreBundle\Fragment\FragmentPreHandlerInterface;
use Contao\CoreBundle\Fragment\Reference\ContentElementReference;
use Contao\CoreBundle\Fragment\Reference\FrontendModuleReference;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

/**
 * Registers Contao fragments in the registry.
 *
 * For custom fragment tags, create your own compiler pass by extending this
 * class and replacing the process() method.
 */
class RegisterFragmentsPass implements CompilerPassInterface
{
    use PriorityTaggedServiceTrait;

    /**
     * Adds the fragments to the registry.
     */
    public function process(ContainerBuilder $container): void
    {
        if (!$container->has('contao.fragment.registry')) {
            return;
        }

        $this->registerFragments($container, ContentElementReference::TAG_NAME);
        $this->registerFragments($container, FrontendModuleReference::TAG_NAME);
    }

    /**
     * @throws InvalidConfigurationException
     */
    protected function registerFragments(ContainerBuilder $container, string $tag): void
    {
        $preHandlers = [];
        $registry = $container->findDefinition('contao.fragment.registry');

        foreach ($this->findAndSortTaggedServices($tag, $container) as $priority => $reference) {
            $definition = $container->findDefinition($reference);
            $definition->setPublic(true);

            $tags = $definition->getTag($tag);
            $definition->clearTag($tag);

            foreach ($tags as $attributes) {
                $attributes['type'] = $this->getFragmentType($definition, $attributes);

                $identifier = sprintf('%s.%s', $tag, $attributes['type']);
                $config = $this->getFragmentConfig($container, $reference, $attributes);

                if (is_a($definition->getClass(), FragmentPreHandlerInterface::class, true)) {
                    $preHandlers[$identifier] = $reference;
                }

                if (is_a($definition->getClass(), FragmentOptionsAwareInterface::class, true)) {
                    $definition->addMethodCall('setFragmentOptions', [$attributes]);
                }

                $registry->addMethodCall('add', [$identifier, $config]);
                $definition->addTag($tag, $attributes);
            }
        }

        $this->addPreHandlers($container, $preHandlers);
    }

    protected function getFragmentConfig(ContainerBuilder $container, Reference $reference, array $attributes): Reference
    {
        $definition = new Definition(
            FragmentConfig::class,
            [
                $this->getControllerName($reference, $attributes),
                $attributes['renderer'] ?? 'forward',
                array_merge(['ignore_errors' => false], $attributes['options'] ?? []),
            ]
        );

        $serviceId = 'contao.fragment._config_'.ContainerBuilder::hash($definition);
        $container->setDefinition($serviceId, $definition);

        return new Reference($serviceId);
    }

    /**
     * Returns the controller name from the service and method name.
     */
    protected function getControllerName(Reference $reference, array $attributes): string
    {
        $controller = (string) $reference;

        // Support a specific method on the controller
        if (isset($attributes['method'])) {
            $controller .= ':'.$attributes['method'];
        }

        return $controller;
    }

    /**
     * @throws \RuntimeException
     */
    protected function addPreHandlers(ContainerBuilder $container, array $handlers): void
    {
        if (!$container->hasDefinition('contao.fragment.pre_handlers')) {
            throw new \RuntimeException('Missing service definition for "contao.fragment.pre_handlers"');
        }

        $definition = $container->getDefinition('contao.fragment.pre_handlers');
        $definition->setArgument(0, array_merge($definition->getArgument(0), $handlers));
    }

    protected function getFragmentType(Definition $definition, array $attributes): string
    {
        if (isset($attributes['type'])) {
            return (string) $attributes['type'];
        }

        $className = $definition->getClass();
        $className = ltrim(strrchr($className, '\\'), '\\');

        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;

use Patchwork\Utf8;

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

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

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

			return $objTemplate->parse();
		}

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

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

		return parent::generate();
	}

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

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

			return;
		}

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

		$arrFaqs = array_fill_keys($this->faq_categories, array());
		$rootDir = System::getContainer()->getParameter('kernel.project_dir');

		// Add FAQs
		while ($objFaq->next())
		{
			/** @var FaqModel $objFaq */
			$objTemp = (object) $objFaq->row();

			// Clean the RTE output
			$objTemp->answer = StringUtil::toHtml5($objFaq->answer);

			$objTemp->answer = StringUtil::encodeEmail($objTemp->answer);
			$objTemp->addImage = false;

			// Add an image
			if ($objFaq->addImage && $objFaq->singleSRC != '')
			{
				$objModel = FilesModel::findByUuid($objFaq->singleSRC);

				if ($objModel !== null && is_file($rootDir . '/' . $objModel->path))
				{
					// Do not override the field now that we have a model registry (see #6303)
					$arrFaq = $objFaq->row();
					$arrFaq['singleSRC'] = $objModel->path;
					$strLightboxId = 'lightbox[' . substr(md5('mod_faqpage_' . $objFaq->id), 0, 6) . ']'; // see #5810

					$this->addImageToTemplate($objTemp, $arrFaq, null, $strLightboxId, $objModel);
				}
			}

			$objTemp->enclosure = array();

			// Add enclosure
			if ($objFaq->addEnclosure)
			{
				$this->addEnclosuresToTemplate($objTemp, $objFaq->row());
			}

			/** @var UserModel $objAuthor */
			$objAuthor = $objFaq->getRelated('author');
			$objTemp->info = sprintf($GLOBALS['TL_LANG']['MSC']['faqCreatedBy'], Date::parse($objPage->dateFormat, $objFaq->tstamp), $objAuthor->name);

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

			// Order by PID
			$arrFaqs[$objFaq->pid]['items'][] = $objTemp;
			$arrFaqs[$objFaq->pid]['headline'] = $objPid->headline;
			$arrFaqs[$objFaq->pid]['title'] = $objPid->title;
		}

		$arrFaqs = array_values(array_filter($arrFaqs));
		$limit_i = \count($arrFaqs) - 1;

		// Add classes first, last, even and odd
		for ($i=0; $i<=$limit_i; $i++)
		{
			$class = (($i == 0) ? 'first ' : '') . (($i == $limit_i) ? 'last ' : '') . (($i%2 == 0) ? 'even' : 'odd');
			$arrFaqs[$i]['class'] = trim($class);
			$limit_j = \count($arrFaqs[$i]['items']) - 1;

			for ($j=0; $j<=$limit_j; $j++)
			{
				$class = (($j == 0) ? 'first ' : '') . (($j == $limit_j) ? 'last ' : '') . (($j%2 == 0) ? 'even' : 'odd');
				$arrFaqs[$i]['items'][$j]->class = trim($class);
			}
		}

		$this->Template->faq = $arrFaqs;
		$this->Template->request = Environment::get('indexFreeRequest');
		$this->Template->topLink = $GLOBALS['TL_LANG']['MSC']['backToTop'];
	}
}

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


<?php

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

namespace Contao;

/**
 * Provide methods regarding calendars.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class Calendar extends Frontend
{

	/**
	 * Current events
	 * @var array
	 */
	protected $arrEvents = array();

	/**
	 * Update a particular RSS feed
	 *
	 * @param integer $intId
	 */
	public function generateFeed($intId)
	{
		$objCalendar = CalendarFeedModel::findByPk($intId);

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

		$objCalendar->feedName = $objCalendar->alias ?: 'calendar' . $objCalendar->id;

		// Delete XML file
		if (Input::get('act') == 'delete')
		{
			$this->import(Files::class, 'Files');
			$this->Files->delete('web/share/' . $objCalendar->feedName . '.xml');
		}

		// Update XML file
		else
		{
			$this->generateFiles($objCalendar->row());
			$this->log('Generated calendar feed "' . $objCalendar->feedName . '.xml"', __METHOD__, TL_CRON);
		}
	}

	/**
	 * Delete old files and generate all feeds
	 */
	public function generateFeeds()
	{
		$this->import(Automator::class, 'Automator');
		$this->Automator->purgeXmlFiles();

		$objCalendar = CalendarFeedModel::findAll();

		if ($objCalendar !== null)
		{
			while ($objCalendar->next())
			{
				$objCalendar->feedName = $objCalendar->alias ?: 'calendar' . $objCalendar->id;
				$this->generateFiles($objCalendar->row());
				$this->log('Generated calendar feed "' . $objCalendar->feedName . '.xml"', __METHOD__, TL_CRON);
			}
		}
	}

	/**
	 * Generate all feeds including a certain calendar
	 *
	 * @param integer $intId
	 */
	public function generateFeedsByCalendar($intId)
	{
		$objFeed = CalendarFeedModel::findByCalendar($intId);

		if ($objFeed !== null)
		{
			while ($objFeed->next())
			{
				$objFeed->feedName = $objFeed->alias ?: 'calendar' . $objFeed->id;

				// Update the XML file
				$this->generateFiles($objFeed->row());
				$this->log('Generated calendar feed "' . $objFeed->feedName . '.xml"', __METHOD__, TL_CRON);
			}
		}
	}

	/**
	 * Generate an XML file and save it to the root directory
	 *
	 * @param array $arrFeed
	 */
	protected function generateFiles($arrFeed)
	{
		$arrCalendars = StringUtil::deserialize($arrFeed['calendars']);

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

		$strType = ($arrFeed['format'] == 'atom') ? 'generateAtom' : 'generateRss';
		$strLink = $arrFeed['feedBase'] ?: Environment::get('base');
		$strFile = $arrFeed['feedName'];

		$objFeed = new Feed($strFile);
		$objFeed->link = $strLink;
		$objFeed->title = $arrFeed['title'];
		$objFeed->description = $arrFeed['description'];
		$objFeed->language = $arrFeed['language'];
		$objFeed->published = $arrFeed['tstamp'];

		$arrUrls = array();
		$this->arrEvents = array();
		$time = time();

		// Get the upcoming events
		$objArticle = CalendarEventsModel::findUpcomingByPids($arrCalendars, $arrFeed['maxItems']);

		// Parse the items
		if ($objArticle !== null)
		{
			while ($objArticle->next())
			{
				$jumpTo = $objArticle->getRelated('pid')->jumpTo;

				// No jumpTo page set (see #4784)
				if (!$jumpTo)
				{
					continue;
				}

				// Get the jumpTo URL
				if (!isset($arrUrls[$jumpTo]))
				{
					$objParent = PageModel::findWithDetails($jumpTo);

					// A jumpTo page is set but does no longer exist (see #5781)
					if ($objParent === null)
					{
						$arrUrls[$jumpTo] = false;
					}
					else
					{
						$arrUrls[$jumpTo] = $objParent->getAbsoluteUrl(Config::get('useAutoItem') ? '/%s' : '/events/%s');
					}
				}

				// Skip the event if it requires a jumpTo URL but there is none
				if ($arrUrls[$jumpTo] === false && $objArticle->source == 'default')
				{
					continue;
				}

				$strUrl = $arrUrls[$jumpTo];
				$this->addEvent($objArticle, $objArticle->startTime, $objArticle->endTime, $strUrl);

				// Recurring events
				if ($objArticle->recurring)
				{
					$arrRepeat = StringUtil::deserialize($objArticle->repeatEach);

					if (!\is_array($arrRepeat) || !isset($arrRepeat['unit']) || !isset($arrRepeat['value']) || $arrRepeat['value'] < 1)
					{
						continue;
					}

					$count = 0;
					$intStartTime = $objArticle->startTime;
					$intEndTime = $objArticle->endTime;
					$strtotime = '+ ' . $arrRepeat['value'] . ' ' . $arrRepeat['unit'];

					// Do not include more than 20 recurrences
					while ($count++ < 20)
					{
						if ($objArticle->recurrences > 0 && $count >= $objArticle->recurrences)
						{
							break;
						}

						$intStartTime = strtotime($strtotime, $intStartTime);
						$intEndTime = strtotime($strtotime, $intEndTime);

						if ($intStartTime >= $time)
						{
							$this->addEvent($objArticle, $intStartTime, $intEndTime, $strUrl);
						}
					}
				}
			}
		}

		$count = 0;
		ksort($this->arrEvents);

		// Add the feed items
		foreach ($this->arrEvents as $days)
		{
			foreach ($days as $events)
			{
				foreach ($events as $event)
				{
					if ($arrFeed['maxItems'] > 0 && $count++ >= $arrFeed['maxItems'])
					{
						break 3;
					}

					$objItem = new FeedItem();

					$objItem->title = $event['title'];
					$objItem->link = $event['link'];
					$objItem->published = $event['tstamp'];
					$objItem->begin = $event['startTime'];
					$objItem->end = $event['endTime'];

					if (($objAuthor = UserModel::findById($event['author'])) !== null)
					{
						$objItem->author = $objAuthor->name;
					}

					// Prepare the description
					if ($arrFeed['source'] == 'source_text')
					{
						$strDescription = '';
						$objElement = ContentModel::findPublishedByPidAndTable($event['id'], 'tl_calendar_events');

						if ($objElement !== null)
						{
							// Overwrite the request (see #7756)
							$strRequest = Environment::get('request');
							Environment::set('request', $objItem->link);

							while ($objElement->next())
							{
								$strDescription .= $this->getContentElement($objElement->current());
							}

							Environment::set('request', $strRequest);
						}
					}
					else
					{
						$strDescription = $event['teaser'];
					}

					$strDescription = $this->replaceInsertTags($strDescription, false);
					$objItem->description = $this->convertRelativeUrls($strDescription, $strLink);

					if (\is_array($event['media:content']))
					{
						foreach ($event['media:content'] as $enclosure)
						{
							$objItem->addEnclosure($enclosure, $strLink, 'media:content');
						}
					}

					if (\is_array($event['enclosure']))
					{
						foreach ($event['enclosure'] as $enclosure)
						{
							$objItem->addEnclosure($enclosure, $strLink);
						}
					}

					$objFeed->addItem($objItem);
				}
			}
		}

		// Create the file
		File::putContent('web/share/' . $strFile . '.xml', $this->replaceInsertTags($objFeed->$strType(), false));
	}

	/**
	 * Add events to the indexer
	 *
	 * @param array   $arrPages
	 * @param integer $intRoot
	 * @param boolean $blnIsSitemap
	 *
	 * @return array
	 */
	public function getSearchablePages($arrPages, $intRoot=0, $blnIsSitemap=false)
	{
		$arrRoot = array();

		if ($intRoot > 0)
		{
			$arrRoot = $this->Database->getChildRecords($intRoot, 'tl_page');
		}

		$arrProcessed = array();
		$time = Date::floorToMinute();

		// Get all calendars
		$objCalendar = CalendarModel::findByProtected('');

		// Walk through each calendar
		if ($objCalendar !== null)
		{
			while ($objCalendar->next())
			{
				// Skip calendars without target page
				if (!$objCalendar->jumpTo)
				{
					continue;
				}

				// Skip calendars outside the root nodes
				if (!empty($arrRoot) && !\in_array($objCalendar->jumpTo, $arrRoot))
				{
					continue;
				}

				// Get the URL of the jumpTo page
				if (!isset($arrProcessed[$objCalendar->jumpTo]))
				{
					$objParent = PageModel::findWithDetails($objCalendar->jumpTo);

					// The target page does not exist
					if ($objParent === null)
					{
						continue;
					}

					// The target page has not been published (see #5520)
					if (!$objParent->published || ($objParent->start != '' && $objParent->start > $time) || ($objParent->stop != '' && $objParent->stop <= ($time + 60)))
					{
						continue;
					}

					if ($blnIsSitemap)
					{
						// The target page is protected (see #8416)
						if ($objParent->protected)
						{
							continue;
						}

						// The target page is exempt from the sitemap (see #6418)
						if ($objParent->sitemap == 'map_never')
						{
							continue;
						}
					}

					// Generate the URL
					$arrProcessed[$objCalendar->jumpTo] = $objParent->getAbsoluteUrl(Config::get('useAutoItem') ? '/%s' : '/events/%s');
				}

				$strUrl = $arrProcessed[$objCalendar->jumpTo];

				// Get the items
				$objEvents = CalendarEventsModel::findPublishedDefaultByPid($objCalendar->id);

				if ($objEvents !== null)
				{
					while ($objEvents->next())
					{
						$arrPages[] = sprintf(preg_replace('/%(?!s)/', '%%', $strUrl), ($objEvents->alias ?: $objEvents->id));
					}
				}
			}
		}

		return $arrPages;
	}

	/**
	 * Add an event to the array of active events
	 *
	 * @param CalendarEventsModel $objEvent
	 * @param integer             $intStart
	 * @param integer             $intEnd
	 * @param string              $strUrl
	 * @param string              $strBase
	 */
	protected function addEvent($objEvent, $intStart, $intEnd, $strUrl, $strBase='')
	{
		if ($intEnd < time()) // see #3917
		{
			return;
		}

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

		// Called in the back end (see #4026)
		if ($objPage === null)
		{
			$objPage = new \stdClass();
			$objPage->dateFormat = Config::get('dateFormat');
			$objPage->datimFormat = Config::get('datimFormat');
			$objPage->timeFormat = Config::get('timeFormat');
		}

		$intKey = date('Ymd', $intStart);
		$span = self::calculateSpan($intStart, $intEnd);
		$format = $objEvent->addTime ? 'datimFormat' : 'dateFormat';

		// Add date
		if ($span > 0)
		{
			$title = Date::parse($objPage->$format, $intStart) . $GLOBALS['TL_LANG']['MSC']['cal_timeSeparator'] . Date::parse($objPage->$format, $intEnd);
		}
		else
		{
			$title = Date::parse($objPage->dateFormat, $intStart) . ($objEvent->addTime ? ' (' . Date::parse($objPage->timeFormat, $intStart) . (($intStart < $intEnd) ? $GLOBALS['TL_LANG']['MSC']['cal_timeSeparator'] . Date::parse($objPage->timeFormat, $intEnd) : '') . ')' : '');
		}

		// Add title and link
		$title .= ' ' . $objEvent->title;

		// Backwards compatibility (see #8329)
		if ($strBase != '' && !preg_match('#^https?://#', $strUrl))
		{
			$strUrl = $strBase . $strUrl;
		}

		$link = '';

		switch ($objEvent->source)
		{
			case 'external':
				$link = $objEvent->url;
				break;

			case 'internal':
				if (($objTarget = $objEvent->getRelated('jumpTo')) instanceof PageModel)
				{
					/** @var PageModel $objTarget */
					$link = $objTarget->getAbsoluteUrl();
				}
				break;

			case 'article':
				if (($objArticle = ArticleModel::findByPk($objEvent->articleId)) instanceof ArticleModel && ($objPid = $objArticle->getRelated('pid')) instanceof PageModel)
				{
					/** @var PageModel $objPid */
					$link = ampersand($objPid->getAbsoluteUrl('/articles/' . ($objArticle->alias ?: $objArticle->id)));
				}
				break;

			default:
				$link = sprintf(preg_replace('/%(?!s)/', '%%', $strUrl), ($objEvent->alias ?: $objEvent->id));
				break;
		}

		// Store the whole row (see #5085)
		$arrEvent = $objEvent->row();

		// Override link and title
		$arrEvent['link'] = $link;
		$arrEvent['title'] = $title;

		// Clean the RTE output
		$arrEvent['teaser'] = StringUtil::toHtml5($objEvent->teaser);

		// Reset the enclosures (see #5685)
		$arrEvent['enclosure'] = array();
		$arrEvent['media:content'] = array();

		// Add the article image as enclosure
		if ($objEvent->addImage)
		{
			$objFile = FilesModel::findByUuid($objEvent->singleSRC);

			if ($objFile !== null)
			{
				$arrEvent['media:content'][] = $objFile->path;
			}
		}

		// Enclosures
		if ($objEvent->addEnclosure)
		{
			$arrEnclosure = StringUtil::deserialize($objEvent->enclosure, true);

			if (\is_array($arrEnclosure))
			{
				$objFile = FilesModel::findMultipleByUuids($arrEnclosure);

				if ($objFile !== null)
				{
					while ($objFile->next())
					{
						$arrEvent['enclosure'][] = $objFile->path;
					}
				}
			}
		}

		$this->arrEvents[$intKey][$intStart][] = $arrEvent;
	}

	/**
	 * Calculate the span between two timestamps in days
	 *
	 * @param integer $intStart
	 * @param integer $intEnd
	 *
	 * @return integer
	 */
	public static function calculateSpan($intStart, $intEnd)
	{
		return self::unixToJd($intEnd) - self::unixToJd($intStart);
	}

	/**
	 * Convert a UNIX timestamp to a Julian day
	 *
	 * @param integer $tstamp
	 *
	 * @return integer
	 */
	public static function unixToJd($tstamp)
	{
		list($year, $month, $day) = explode(',', date('Y,m,d', $tstamp));

		// Make year a positive number
		$year += ($year < 0 ? 4801 : 4800);

		// Adjust the start of the year
		if ($month > 2)
		{
			$month -= 3;
		}
		else
		{
			$month += 9;
			--$year;
		}

		$sdn  = floor((floor($year / 100) * 146097) / 4);
		$sdn += floor((($year % 100) * 1461) / 4);
		$sdn += floor(($month * 153 + 2) / 5);
		$sdn += $day - 32045;

		return $sdn;
	}

	/**
	 * Return the names of the existing feeds so they are not removed
	 *
	 * @return array
	 */
	public function purgeOldFeeds()
	{
		$arrFeeds = array();
		$objFeeds = CalendarFeedModel::findAll();

		if ($objFeeds !== null)
		{
			while ($objFeeds->next())
			{
				$arrFeeds[] = $objFeeds->alias ?: 'calendar' . $objFeeds->id;
			}
		}

		return $arrFeeds;
	}
}

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


<?php

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

namespace Contao;

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

/**
 * Parent class for news modules.
 *
 * @property string $news_template
 * @property mixed  $news_metaFields
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
abstract class ModuleNews extends Module
{

	/**
	 * Sort out protected archives
	 *
	 * @param array $arrArchives
	 *
	 * @return array
	 */
	protected function sortOutProtected($arrArchives)
	{
		if (empty($arrArchives) || !\is_array($arrArchives))
		{
			return $arrArchives;
		}

		$this->import(FrontendUser::class, 'User');
		$objArchive = NewsArchiveModel::findMultipleByIds($arrArchives);
		$arrArchives = array();

		if ($objArchive !== null)
		{
			while ($objArchive->next())
			{
				if ($objArchive->protected)
				{
					if (!FE_USER_LOGGED_IN || !\is_array($this->User->groups))
					{
						continue;
					}

					$groups = StringUtil::deserialize($objArchive->groups);

					if (empty($groups) || !\is_array($groups) || !\count(array_intersect($groups, $this->User->groups)))
					{
						continue;
					}
				}

				$arrArchives[] = $objArchive->id;
			}
		}

		return $arrArchives;
	}

	/**
	 * Parse an item and return it as string
	 *
	 * @param NewsModel $objArticle
	 * @param boolean   $blnAddArchive
	 * @param string    $strClass
	 * @param integer   $intCount
	 *
	 * @return string
	 */
	protected function parseArticle($objArticle, $blnAddArchive=false, $strClass='', $intCount=0)
	{
		$objTemplate = new FrontendTemplate($this->news_template);
		$objTemplate->setData($objArticle->row());

		if ($objArticle->cssClass != '')
		{
			$strClass = ' ' . $objArticle->cssClass . $strClass;
		}

		if ($objArticle->featured)
		{
			$strClass = ' featured' . $strClass;
		}

		$objTemplate->class = $strClass;
		$objTemplate->newsHeadline = $objArticle->headline;
		$objTemplate->subHeadline = $objArticle->subheadline;
		$objTemplate->hasSubHeadline = $objArticle->subheadline ? true : false;
		$objTemplate->linkHeadline = $this->generateLink($objArticle->headline, $objArticle, $blnAddArchive);
		$objTemplate->more = $this->generateLink($GLOBALS['TL_LANG']['MSC']['more'], $objArticle, $blnAddArchive, true);
		$objTemplate->link = News::generateNewsUrl($objArticle, $blnAddArchive);
		$objTemplate->archive = $objArticle->getRelated('pid');
		$objTemplate->count = $intCount; // see #5708
		$objTemplate->text = '';
		$objTemplate->hasText = false;
		$objTemplate->hasTeaser = false;

		// Clean the RTE output
		if ($objArticle->teaser != '')
		{
			$objTemplate->hasTeaser = true;
			$objTemplate->teaser = StringUtil::toHtml5($objArticle->teaser);
			$objTemplate->teaser = StringUtil::encodeEmail($objTemplate->teaser);
		}

		// Display the "read more" button for external/article links
		if ($objArticle->source != 'default')
		{
			$objTemplate->text = true;
			$objTemplate->hasText = true;
		}

		// Compile the news text
		else
		{
			$id = $objArticle->id;

			$objTemplate->text = function () use ($id)
			{
				$strText = '';
				$objElement = ContentModel::findPublishedByPidAndTable($id, 'tl_news');

				if ($objElement !== null)
				{
					while ($objElement->next())
					{
						$strText .= $this->getContentElement($objElement->current());
					}
				}

				return $strText;
			};

			$objTemplate->hasText = function () use ($objArticle)
			{
				return ContentModel::countPublishedByPidAndTable($objArticle->id, 'tl_news') > 0;
			};
		}

		$arrMeta = $this->getMetaFields($objArticle);

		// Add the meta information
		$objTemplate->date = $arrMeta['date'];
		$objTemplate->hasMetaFields = !empty($arrMeta);
		$objTemplate->numberOfComments = $arrMeta['ccount'];
		$objTemplate->commentCount = $arrMeta['comments'];
		$objTemplate->timestamp = $objArticle->date;
		$objTemplate->author = $arrMeta['author'];
		$objTemplate->datetime = date('Y-m-d\TH:i:sP', $objArticle->date);

		$objTemplate->addImage = false;

		// Add an image
		if ($objArticle->addImage && $objArticle->singleSRC != '')
		{
			$objModel = FilesModel::findByUuid($objArticle->singleSRC);

			if ($objModel !== null && is_file(System::getContainer()->getParameter('kernel.project_dir') . '/' . $objModel->path))
			{
				// Do not override the field now that we have a model registry (see #6303)
				$arrArticle = $objArticle->row();

				// Override the default image size
				if ($this->imgSize != '')
				{
					$size = StringUtil::deserialize($this->imgSize);

					if ($size[0] > 0 || $size[1] > 0 || is_numeric($size[2]))
					{
						$arrArticle['size'] = $this->imgSize;
					}
				}

				$arrArticle['singleSRC'] = $objModel->path;
				$this->addImageToTemplate($objTemplate, $arrArticle, null, null, $objModel);

				// Link to the news article if no image link has been defined (see #30)
				if (!$objTemplate->fullsize && !$objTemplate->imageUrl)
				{
					// Unset the image title attribute
					$picture = $objTemplate->picture;
					unset($picture['title']);
					$objTemplate->picture = $picture;

					// Link to the news article
					$objTemplate->href = $objTemplate->link;
					$objTemplate->linkTitle = StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['readMore'], $objArticle->headline), true);

					// If the external link is opened in a new window, open the image link in a new window, too (see #210)
					if ($objTemplate->source == 'external' && $objTemplate->target && strpos($objTemplate->attributes, 'target="_blank"') === false)
					{
						$objTemplate->attributes .= ' target="_blank"';
					}
				}
			}
		}

		$objTemplate->enclosure = array();

		// Add enclosures
		if ($objArticle->addEnclosure)
		{
			$this->addEnclosuresToTemplate($objTemplate, $objArticle->row());
		}

		// HOOK: add custom logic
		if (isset($GLOBALS['TL_HOOKS']['parseArticles']) && \is_array($GLOBALS['TL_HOOKS']['parseArticles']))
		{
			foreach ($GLOBALS['TL_HOOKS']['parseArticles'] as $callback)
			{
				$this->import($callback[0]);
				$this->{$callback[0]}->{$callback[1]}($objTemplate, $objArticle->row(), $this);
			}
		}

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

		return $objTemplate->parse();
	}

	/**
	 * Parse one or more items and return them as array
	 *
	 * @param Collection $objArticles
	 * @param boolean    $blnAddArchive
	 *
	 * @return array
	 */
	protected function parseArticles($objArticles, $blnAddArchive=false)
	{
		$limit = $objArticles->count();

		if ($limit < 1)
		{
			return array();
		}

		$count = 0;
		$arrArticles = array();

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

			$arrArticles[] = $this->parseArticle($objArticle, $blnAddArchive, ((++$count == 1) ? ' first' : '') . (($count == $limit) ? ' last' : '') . ((($count % 2) == 0) ? ' odd' : ' even'), $count);
		}

		return $arrArticles;
	}

	/**
	 * Return the meta fields of a news article as array
	 *
	 * @param NewsModel $objArticle
	 *
	 * @return array
	 */
	protected function getMetaFields($objArticle)
	{
		$meta = StringUtil::deserialize($this->news_metaFields);

		if (!\is_array($meta))
		{
			return array();
		}

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

		$return = array();

		foreach ($meta as $field)
		{
			switch ($field)
			{
				case 'date':
					$return['date'] = Date::parse($objPage->datimFormat, $objArticle->date);
					break;

				case 'author':
					/** @var UserModel $objAuthor */
					if (($objAuthor = $objArticle->getRelated('author')) instanceof UserModel)
					{
						$return['author'] = $GLOBALS['TL_LANG']['MSC']['by'] . ' ' . $objAuthor->name . '';
					}
					break;

				case 'comments':
					if ($objArticle->noComments || $objArticle->source != 'default')
					{
						break;
					}

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

					if (!isset($bundles['ContaoCommentsBundle']))
					{
						break;
					}

					$intTotal = CommentsModel::countPublishedBySourceAndParent('tl_news', $objArticle->id);
					$return['ccount'] = $intTotal;
					$return['comments'] = sprintf($GLOBALS['TL_LANG']['MSC']['commentCount'], $intTotal);
					break;
			}
		}

		return $return;
	}

	/**
	 * Generate a URL and return it as string
	 *
	 * @param NewsModel $objItem
	 * @param boolean   $blnAddArchive
	 *
	 * @return string
	 *
	 * @deprecated Deprecated since Contao 4.1, to be removed in Contao 5.
	 *             Use News::generateNewsUrl() instead.
	 */
	protected function generateNewsUrl($objItem, $blnAddArchive=false)
	{
		@trigger_error('Using ModuleNews::generateNewsUrl() has been deprecated and will no longer work in Contao 5.0. Use News::generateNewsUrl() instead.', E_USER_DEPRECATED);

		return News::generateNewsUrl($objItem, $blnAddArchive);
	}

	/**
	 * Generate a link and return it as string
	 *
	 * @param string    $strLink
	 * @param NewsModel $objArticle
	 * @param boolean   $blnAddArchive
	 * @param boolean   $blnIsReadMore
	 *
	 * @return string
	 */
	protected function generateLink($strLink, $objArticle, $blnAddArchive=false, $blnIsReadMore=false)
	{
		// Internal link
		if ($objArticle->source != 'external')
		{
			return sprintf('%s%s',
							News::generateNewsUrl($objArticle, $blnAddArchive),
							StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['readMore'], $objArticle->headline), true),
							$strLink,
							($blnIsReadMore ? ' '.$objArticle->headline.'' : ''));
		}

		// Encode e-mail addresses
		if (substr($objArticle->url, 0, 7) == 'mailto:')
		{
			$strArticleUrl = StringUtil::encodeEmail($objArticle->url);
		}

		// Ampersand URIs
		else
		{
			$strArticleUrl = ampersand($objArticle->url);
		}

		// External link
		return sprintf('%s',
						$strArticleUrl,
						StringUtil::specialchars(sprintf($GLOBALS['TL_LANG']['MSC']['open'], $strArticleUrl)),
						($objArticle->target ? ' target="_blank"' : ''),
						$strLink);
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class PickerProviderPass implements CompilerPassInterface
{
    use PriorityTaggedServiceTrait;

    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container): void
    {
        if (!$container->has('contao.picker.builder')) {
            return;
        }

        $definition = $container->findDefinition('contao.picker.builder');
        $references = $this->findAndSortTaggedServices('contao.picker_provider', $container);

        foreach ($references as $reference) {
            $definition->addMethodCall('addProvider', [$reference]);
        }
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\InstallationBundle\Database;

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

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

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

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

    /**
     * {@inheritdoc}
     */
    public function run(): void
    {
        $crop = $GLOBALS['TL_CROP'];

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

        $options = [];

        foreach ($crop as $group => $modes) {
            $options[] = array_values($modes);
        }

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

        $rows = $this->connection->fetchAll('
            SELECT
                id
            FROM
                tl_image_size
        ');

        foreach ($rows as $imageSize) {
            $options[] = $imageSize['id'];
        }

        // Add the database fields
        $this->connection->query('
            ALTER TABLE
                tl_user
            ADD
                imageSizes blob NULL
        ');

        $this->connection->query('
            ALTER TABLE
                tl_user_group
            ADD
                imageSizes blob NULL
        ');

        // Grant access to all existing image sizes at group level
        $stmt = $this->connection->prepare('
            UPDATE
                tl_user_group
            SET
                imageSizes = :options
        ');

        $stmt->execute([':options' => serialize($options)]);
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Fragment;

use Contao\CoreBundle\Exception\ResponseException;
use Contao\CoreBundle\Fragment\Reference\FragmentReference;
use Contao\PageModel;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Fragment\FragmentHandler as BaseFragmentHandler;

class FragmentHandler extends BaseFragmentHandler
{
    /**
     * @var ContainerInterface
     */
    private $renderers;

    /**
     * @var BaseFragmentHandler
     */
    private $fragmentHandler;

    /**
     * @var FragmentRegistryInterface
     */
    private $fragmentRegistry;

    /**
     * @var ContainerInterface
     */
    private $preHandlers;

    /**
     * @var array
     */
    private $initialized = [];

    public function __construct(ContainerInterface $renderers, BaseFragmentHandler $fragmentHandler, RequestStack $requestStack, FragmentRegistryInterface $fragmentRegistry, ContainerInterface $preHandlers, bool $debug = false)
    {
        $this->renderers = $renderers;
        $this->fragmentHandler = $fragmentHandler;
        $this->fragmentRegistry = $fragmentRegistry;
        $this->preHandlers = $preHandlers;

        parent::__construct($requestStack, [], $debug);
    }

    /**
     * {@inheritdoc}
     */
    public function render($uri, $renderer = 'inline', array $options = []): ?string
    {
        if (!$uri instanceof FragmentReference) {
            return $this->fragmentHandler->render($uri, $renderer, $options);
        }

        $config = $this->fragmentRegistry->get($uri->controller);

        if (null === $config) {
            throw new UnknownFragmentException(sprintf('Invalid fragment identifier "%s"', $uri->controller));
        }

        $this->preHandleFragment($uri, $config);

        $renderer = $config->getRenderer();

        if (!isset($this->initialized[$renderer]) && $this->renderers->has($renderer)) {
            $this->addRenderer($this->renderers->get($renderer));
            $this->initialized[$renderer] = true;
        }

        return parent::render($uri, $renderer, $config->getOptions());
    }

    /**
     * {@inheritdoc}
     */
    protected function deliver(Response $response): ?string
    {
        try {
            return parent::deliver($response);
        } catch (\RuntimeException $e) {
            throw new ResponseException($response, $e);
        }
    }

    /**
     * Adds generic attributes and query parameters before rendering.
     */
    private function preHandleFragment(FragmentReference $uri, FragmentConfig $config): void
    {
        if (!isset($uri->attributes['pageModel']) && $this->hasGlobalPageObject()) {
            $uri->attributes['pageModel'] = $GLOBALS['objPage']->id;
        }

        if ($this->preHandlers->has($uri->controller)) {
            /** @var FragmentPreHandlerInterface $preHandler */
            $preHandler = $this->preHandlers->get($uri->controller);
            $preHandler->preHandleFragment($uri, $config);
        }
    }

    private function hasGlobalPageObject(): bool
    {
        return isset($GLOBALS['objPage']) && $GLOBALS['objPage'] instanceof PageModel;
    }
}


<?php

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

namespace Contao;

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

/**
 * Provide methods to handle a website root page.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 * @author Yanick Witschi <https://github.com/Toflar>
 */
class PageRoot extends Frontend
{

	/**
	 * Redirect to the first active regular page
	 *
	 * @param integer $rootPageId
	 * @param boolean $blnReturn
	 * @param boolean $blnPreferAlias
	 *
	 * @return integer
	 */
	public function generate($rootPageId, $blnReturn=false, $blnPreferAlias=false)
	{
		if (!$blnReturn)
		{
			$this->redirect($this->getRedirectUrl($rootPageId), 302);
		}

		$objNextPage = $this->getNextPage($rootPageId);

		return ($blnPreferAlias && $objNextPage->alias != '') ? $objNextPage->alias : $objNextPage->id;
	}

	/**
	 * Return a response object
	 *
	 * @param integer $rootPageId
	 *
	 * @return RedirectResponse
	 */
	public function getResponse($rootPageId)
	{
		return new RedirectResponse($this->getRedirectUrl($rootPageId), 303);
	}

	/**
	 * Prepare the page object and redirect URL
	 *
	 * @param integer $rootPageId
	 *
	 * @return PageModel
	 *
	 * @throws NoActivePageFoundException
	 */
	protected function getNextPage($rootPageId)
	{
		$objNextPage = PageModel::findFirstPublishedByPid($rootPageId);

		// No published pages yet
		if (null === $objNextPage)
		{
			$this->log('No active page found under root page "' . $rootPageId . '")', __METHOD__, TL_ERROR);
			throw new NoActivePageFoundException('No active page found under root page.');
		}

		return $objNextPage;
	}

	/**
	 * Prepare the page object and redirect URL
	 *
	 * @param integer $rootPageId
	 *
	 * @return string
	 */
	protected function getRedirectUrl($rootPageId)
	{
		return $this->getNextPage($rootPageId)->getFrontendUrl();
	}
}

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


<?php

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

namespace Contao;

/**
 * Class FormRadioButton
 *
 * @property boolean $mandatory
 * @property array   $options
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class FormRadioButton extends Widget
{

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

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

	/**
	 * Error message
	 *
	 * @var string
	 */
	protected $strError = '';

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

	/**
	 * Add specific attributes
	 *
	 * @param string $strKey   The attribute key
	 * @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 'options':
				$this->arrOptions = StringUtil::deserialize($varValue);
				break;

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

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

	/**
	 * 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);
	}

	/**
	 * Check for a valid option (see #4383)
	 */
	public function validate()
	{
		$varValue = $this->getPost($this->strName);

		if (!empty($varValue) && !$this->isValidOption($varValue))
		{
			$this->addError($GLOBALS['TL_LANG']['ERR']['invalid']);
		}

		parent::validate();
	}

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

		foreach ($this->arrOptions as $i=>$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',
						'name'       => $this->strName,
						'id'         => $this->strId . '_' . $i,
						'value'      => $arrOption['value'],
						'checked'    => $this->isChecked($arrOption),
						'attributes' => $this->getAttributes(),
						'label'      => $arrOption['label']
					)
				);
			}
		}

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

		return $arrOptions;
	}

	/**
	 * Override the parent method and inject the error message inside the fieldset (see #3392)
	 *
	 * @param boolean $blnSwitchOrder If true, the error message will be shown below the field
	 *
	 * @return string The form field markup
	 */
	public function generateWithError($blnSwitchOrder=false)
	{
		$this->strError = $this->getErrorAsHTML();

		return $this->generate();
	}

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

		foreach ($this->arrOptions as $i=>$arrOption)
		{
			$strOptions .= sprintf('<input type="radio" name="%s" id="opt_%s" class="radio" value="%s"%s%s%s %s ',
									$this->strName,
									$this->strId.'_'.$i,
									$arrOption['value'],
									$this->isChecked($arrOption),
									$this->getAttributes(),
									$this->strTagEnding,
									$this->strId.'_'.$i,
									$this->strId.'_'.$i,
									$arrOption['label']);
		}

		if ($this->strLabel != '')
		{
			return sprintf('<fieldset id="ctrl_%s" class="radio_container%s"><legend>%s%s%s</legend>%s<input type="hidden" name="%s" value=""%s%s</fieldset>',
							$this->strId,
							(($this->strClass != '') ? ' ' . $this->strClass : ''),
							($this->mandatory ? ''.$GLOBALS['TL_LANG']['MSC']['mandatory'].' ' : ''),
							$this->strLabel,
							($this->mandatory ? '*' : ''),
							$this->strError,
							$this->strName,
							$this->strTagEnding,
							$strOptions);
		}
		else
		{
	        return sprintf('<fieldset id="ctrl_%s" class="radio_container%s">%s<input type="hidden" name="%s" value=""%s%s</fieldset>',
							$this->strId,
							(($this->strClass != '') ? ' ' . $this->strClass : ''),
							$this->strError,
							$this->strName,
							$this->strTagEnding,
							$strOptions);
		}
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\EventListener\InsertTags;

use Symfony\Component\Translation\TranslatorInterface;

class TranslationListener
{
    /**
     * @var TranslatorInterface
     */
    private $translator;

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

    /**
     * Replaces the "trans" insert tag.
     *
     * @return string|false
     */
    public function onReplaceInsertTags(string $tag)
    {
        $chunks = explode('::', $tag);

        if ('trans' !== $chunks[0]) {
            return false;
        }

        $parameters = isset($chunks[3]) ? explode(':', $chunks[3]) : [];

        return $this->translator->trans($chunks[1], $parameters, $chunks[2] ?? null);
    }
}