<?php

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

namespace Contao\Database;

use Contao\Config;
use Contao\Controller;
use Contao\Database;
use Contao\Dbafs;
use Contao\File;
use Contao\Files;
use Contao\FilesModel;
use Contao\StringUtil;
use Contao\System;
use Symfony\Component\Finder\SplFileInfo;

@trigger_error('Using the Contao\Database\Updater class has been deprecated and will no longer work in Contao 5.0.', E_USER_DEPRECATED);

/**
 * Adjust the database if the system is updated.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 *
 * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
 */
class Updater extends Controller
{

	/**
	 * Import the Database object
	 */
	public function __construct()
	{
		parent::__construct();
		$this->import(Database::class, 'Database');
	}

	/**
	 * Version 2.8.0 update
	 */
	public function run28Update()
	{
		// Database changes
		$this->Database->query("ALTER TABLE `tl_layout` ADD `script` text NULL");
		$this->Database->query("ALTER TABLE `tl_member` ADD `dateAdded` int(10) unsigned NOT NULL default 0");
		$this->Database->query("ALTER TABLE `tl_member` ADD `currentLogin` int(10) unsigned NOT NULL default 0");
		$this->Database->query("ALTER TABLE `tl_member` ADD `lastLogin` int(10) unsigned NOT NULL default 0");
		$this->Database->query("ALTER TABLE `tl_user` ADD `dateAdded` int(10) unsigned NOT NULL default 0");
		$this->Database->query("ALTER TABLE `tl_user` ADD `currentLogin` int(10) unsigned NOT NULL default 0");
		$this->Database->query("ALTER TABLE `tl_user` ADD `lastLogin` int(10) unsigned NOT NULL default 0");
		$this->Database->query("ALTER TABLE `tl_comments` ADD `source` varchar(32) NOT NULL default ''");
		$this->Database->query("ALTER TABLE `tl_comments` ADD KEY `source` (`source`)");
		$this->Database->query("ALTER TABLE `tl_layout` CHANGE `mootools` `mootools` text NULL");
		$this->Database->query("ALTER TABLE `tl_comments` CHANGE `pid` `parent` int(10) unsigned NOT NULL default 0");
		$this->Database->query("UPDATE tl_member SET dateAdded=tstamp, currentLogin=tstamp");
		$this->Database->query("UPDATE tl_user SET dateAdded=tstamp, currentLogin=tstamp");
		$this->Database->query("UPDATE tl_layout SET mootools='moo_accordion' WHERE mootools='moo_default'");
		$this->Database->query("UPDATE tl_comments SET source='tl_content'");
		$this->Database->query("UPDATE tl_module SET cal_format='next_365', type='eventlist' WHERE type='upcoming_events'");

		// Get all front end groups
		$objGroups = $this->Database->execute("SELECT id FROM tl_member_group");
		$strGroups = serialize($objGroups->fetchEach('id'));

		// Update protected elements
		$this->Database->prepare("UPDATE tl_page SET `groups`=? WHERE protected=1 AND `groups`=''")->execute($strGroups);
		$this->Database->prepare("UPDATE tl_content SET `groups`=? WHERE protected=1 AND `groups`=''")->execute($strGroups);
		$this->Database->prepare("UPDATE tl_module SET `groups`=? WHERE protected=1 AND `groups`=''")->execute($strGroups);

		// Update layouts
		$objLayout = $this->Database->execute("SELECT id, mootools FROM tl_layout");

		while ($objLayout->next())
		{
			$mootools = array('moo_mediabox');

			if ($objLayout->mootools != '')
			{
				$mootools[] = $objLayout->mootools;
			}

			$this->Database->prepare("UPDATE tl_layout SET mootools=? WHERE id=?")
						   ->execute(serialize($mootools), $objLayout->id);
		}

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

		// Update event reader
		if (!file_exists($rootDir . '/templates/event_default.tpl'))
		{
			$this->Database->execute("UPDATE tl_module SET cal_template='event_full' WHERE cal_template='event_default'");
		}

		// News comments
		$objComment = $this->Database->execute("SELECT * FROM tl_news_comments");

		while ($objComment->next())
		{
			$arrSet = $objComment->row();

			$arrSet['source'] = 'tl_news';
			$arrSet['parent'] = $arrSet['pid'];
			unset($arrSet['id']);
			unset($arrSet['pid']);

			$this->Database->prepare("INSERT INTO tl_comments %s")->set($arrSet)->execute();
		}

		// Delete system/modules/news/Comments.php
		$this->import(Files::class, 'Files');
		$this->Files->delete('system/modules/news/Comments.php');
	}

	/**
	 * Version 2.9.0 update
	 */
	public function run29Update()
	{
		// Create the themes table
		$this->Database->query(
			"CREATE TABLE `tl_theme` (
			  `id` int(10) unsigned NOT NULL auto_increment,
			  `tstamp` int(10) unsigned NOT NULL default 0,
			  `name` varchar(128) NOT NULL default '',
			  `author` varchar(128) NOT NULL default '',
			  `screenshot` varchar(255) NOT NULL default '',
			  `folders` blob NULL,
			  `templates` varchar(255) NOT NULL default '',
			  PRIMARY KEY  (`id`)
			) ENGINE=MyISAM DEFAULT CHARSET=utf8;"
		);

		// Add a PID column to the child tables
		$this->Database->query("ALTER TABLE `tl_module` ADD `pid` int(10) unsigned NOT NULL default 0");
		$this->Database->query("ALTER TABLE `tl_style_sheet` ADD `pid` int(10) unsigned NOT NULL default 0");
		$this->Database->query("ALTER TABLE `tl_layout` ADD `pid` int(10) unsigned NOT NULL default 0");
		$this->Database->query("UPDATE tl_module SET pid=1");
		$this->Database->query("UPDATE tl_style_sheet SET pid=1");
		$this->Database->query("UPDATE tl_layout SET pid=1");

		// Create a theme from the present resources
		$this->Database->prepare("INSERT INTO tl_theme SET tstamp=?, name=?")
					   ->execute(time(), 'Default');

		// Adjust the back end user permissions
		$this->Database->query("ALTER TABLE `tl_user` ADD `themes` blob NULL");
		$this->Database->query("ALTER TABLE `tl_user_group` ADD `themes` blob NULL");

		// Adjust the user and group rights
		$objUser = $this->Database->execute("SELECT id, modules, 'tl_user' AS tbl FROM tl_user WHERE modules!='' UNION SELECT id, modules, 'tl_user_group' AS tbl FROM tl_user_group WHERE modules!=''");

		while ($objUser->next())
		{
			$modules = StringUtil::deserialize($objUser->modules);

			if (empty($modules) || !\is_array($modules))
			{
				continue;
			}

			$themes = array();

			foreach ($modules as $k=>$v)
			{
				if ($v == 'css' || $v == 'modules ' || $v == 'layout')
				{
					$themes[] = $v;
					unset($modules[$k]);
				}
			}

			if (!empty($themes))
			{
				$modules[] = 'themes';
			}

			$modules = array_values($modules);

			$set = array
			(
				'modules' => (!empty($modules) ? serialize($modules) : null),
				'themes'  => (!empty($themes) ? serialize($themes) : null)
			);

			$this->Database->prepare("UPDATE " . $objUser->tbl . " %s WHERE id=?")
						   ->set($set)
						   ->execute($objUser->id);
		}

		// Featured news
		if ($this->Database->fieldExists('news_featured', 'tl_module'))
		{
			$this->Database->query("ALTER TABLE `tl_module` CHANGE `news_featured` `news_featured` varchar(16) NOT NULL default ''");
			$this->Database->query("UPDATE tl_module SET news_featured='featured' WHERE news_featured='1'");
		}

		// Other version 2.9 updates
		$this->Database->query("UPDATE tl_member SET country='gb' WHERE country='uk'");
		$this->Database->query("ALTER TABLE `tl_module` CHANGE `news_jumpToCurrent` `news_jumpToCurrent` varchar(16) NOT NULL default ''");
		$this->Database->query("UPDATE tl_module SET news_jumpToCurrent='show_current' WHERE news_jumpToCurrent=1");
		$this->Database->query("ALTER TABLE `tl_user` ADD `useCE` char(1) NOT NULL default ''");
		$this->Database->query("UPDATE tl_user SET useCE=1");
	}

	/**
	 * Version 2.9.2 update
	 */
	public function run292Update()
	{
		$this->Database->query("ALTER TABLE `tl_calendar_events` CHANGE `startTime` `startTime` int(10) unsigned NULL");
		$this->Database->query("ALTER TABLE `tl_calendar_events` CHANGE `endTime` `endTime` int(10) unsigned NULL");
		$this->Database->query("ALTER TABLE `tl_calendar_events` CHANGE `startDate` `startDate` int(10) unsigned NULL");
		$this->Database->query("ALTER TABLE `tl_calendar_events` CHANGE `endDate` `endDate` int(10) unsigned NULL");
		$this->Database->query("UPDATE tl_calendar_events SET endDate=null WHERE endDate=0");
	}

	/**
	 * Version 2.10.0 update
	 */
	public function run210Update()
	{
		$this->Database->query("ALTER TABLE `tl_style` ADD `positioning` char(1) NOT NULL default ''");
		$this->Database->query("UPDATE `tl_style` SET `positioning`=`size`");
		$this->Database->query("UPDATE `tl_module` SET `guests`=1 WHERE `type`='lostPassword' OR `type`='registration'");
		$this->Database->query("UPDATE `tl_news` SET `teaser`=CONCAT('<p>', teaser, '</p>') WHERE `teaser`!='' AND `teaser` NOT LIKE '<p>%'");
	}

	/**
	 * Version 3.0.0 update
	 */
	public function run300Update()
	{
		// Create the files table
		$this->Database->query(
			"CREATE TABLE `tl_files` (
			  `id` int(10) unsigned NOT NULL auto_increment,
			  `pid` binary(16) NULL,
			  `tstamp` int(10) unsigned NOT NULL default 0,
			  `uuid` binary(16) NULL,
			  `type` varchar(16) NOT NULL default '',
			  `path` varchar(1022) NOT NULL default '',
			  `extension` varchar(16) NOT NULL default '',
			  `hash` varchar(32) NOT NULL default '',
			  `found` char(1) NOT NULL default '1',
			  `name` varchar(255) NOT NULL default '',
			  `meta` blob NULL,
			  PRIMARY KEY  (`id`),
			  KEY `pid` (`pid`),
			  UNIQUE KEY `uuid` (`uuid`),
			  KEY `extension` (`extension`)
			) ENGINE=MyISAM DEFAULT CHARSET=utf8;"
		);

		// Add the "numberOfItems" field
		$this->Database->query("ALTER TABLE `tl_module` ADD `numberOfItems` smallint(5) unsigned NOT NULL default 0");
		$this->Database->query("UPDATE `tl_module` SET `numberOfItems`=`rss_numberOfItems` WHERE `rss_numberOfItems`>0");
		$this->Database->query("UPDATE `tl_module` SET `numberOfItems`=`news_numberOfItems` WHERE `news_numberOfItems`>0");

		// Add the "addMooTools" field
		$this->Database->query("ALTER TABLE `tl_layout` ADD `addMooTools` char(1) NOT NULL default ''");
		$this->Database->query("UPDATE `tl_layout` SET `addMooTools`=1 WHERE `mootools`!=''");

		// Add the "notified" field
		$this->Database->query("ALTER TABLE `tl_comments` ADD `notified` char(1) NOT NULL default ''");
		$this->Database->query("UPDATE `tl_comments` SET `notified`=1");

		// Add the "rows" field
		$this->Database->query("ALTER TABLE `tl_layout` ADD `rows` varchar(8) NOT NULL default ''");
		$this->Database->query("UPDATE `tl_layout` SET `rows`='1rw' WHERE `header`='' AND `footer`=''");
		$this->Database->query("UPDATE `tl_layout` SET `rows`='2rwh' WHERE `header`!='' AND `footer`=''");
		$this->Database->query("UPDATE `tl_layout` SET `rows`='2rwf' WHERE `header`='' AND `footer`!=''");
		$this->Database->query("UPDATE `tl_layout` SET `rows`='3rw' WHERE `header`!='' AND `footer`!=''");

		// Update the "mooType" field
		$this->Database->query("UPDATE `tl_content` SET `mooType`='mooStart' WHERE `mooType`='start'");
		$this->Database->query("UPDATE `tl_content` SET `mooType`='mooStop' WHERE `mooType`='stop'");
		$this->Database->query("UPDATE `tl_content` SET `mooType`='mooSingle' WHERE `mooType`='single'");

		// Add the "framework" field
		$this->Database->query("ALTER TABLE `tl_layout` ADD `framework` varchar(255) NOT NULL default ''");
		$this->Database->query("UPDATE `tl_layout` SET `framework`='a:2:{i:0;s:10:\"layout.css\";i:1;s:11:\"tinymce.css\";}'");
		$this->Database->query("UPDATE `tl_layout` SET `framework`='a:1:{i:0;s:10:\"layout.css\";}' WHERE skipTinymce=1");

		// Make sure the "skipFramework" field exists (see #4624)
		if ($this->Database->fieldExists('skipFramework', 'tl_layout'))
		{
			$this->Database->query("UPDATE `tl_layout` SET `framework`='' WHERE skipFramework=1");
		}

		// Add the "ptable" field
		$this->Database->query("ALTER TABLE `tl_content` ADD ptable varchar(64) NOT NULL default ''");

		// Create a content element for each news article
		$objNews = $this->Database->execute("SELECT * FROM tl_news WHERE text!='' AND source='default'");

		while ($objNews->next())
		{
			$this->createContentElement($objNews, 'tl_news', 'text');
		}

		// Create a content element for each event
		$objEvents = $this->Database->execute("SELECT * FROM tl_calendar_events WHERE details!='' AND source='default'");

		while ($objEvents->next())
		{
			$this->createContentElement($objEvents, 'tl_calendar_events', 'details');
		}

		// Convert the gradient angle syntax (see #4569)
		if ($this->Database->fieldExists('gradientAngle', 'tl_style'))
		{
			$objStyle = $this->Database->execute("SELECT id, gradientAngle FROM tl_style WHERE gradientAngle!=''");

			while ($objStyle->next())
			{
				$angle = '';

				if (strpos($objStyle->gradientAngle, 'deg') !== false)
				{
					$angle = (abs(450 - (int) $objStyle->gradientAngle) % 360) . 'deg';
				}
				else
				{
					switch ($objStyle->gradientAngle)
					{
						case 'top':          $angle = 'to bottom';       break;
						case 'right':        $angle = 'to left';         break;
						case 'bottom':       $angle = 'to top';          break;
						case 'left':         $angle = 'to right';        break;
						case 'top left':     $angle = 'to bottom right'; break;
						case 'top right':    $angle = 'to bottom left';  break;
						case 'bottom left':  $angle = 'to top right';    break;
						case 'bottom right': $angle = 'to top left';     break;
					}
				}

				$this->Database->prepare("UPDATE tl_style SET gradientAngle=? WHERE id=?")
							   ->execute($angle, $objStyle->id);
			}
		}

		// Make unlimited recurrences end on 2038-01-01 00:00:00 (see #4862)
		$this->Database->query("UPDATE `tl_calendar_events` SET `repeatEnd`=2145913200 WHERE `recurring`=1 AND `recurrences`=0");
	}

	/**
	 * Version 3.1.0 update
	 */
	public function run31Update()
	{
		// Get all page layouts that use the CSS framework
		$objLayout = $this->Database->query("SELECT `id`, `framework` FROM `tl_layout` WHERE `framework`!=''");

		// Rename "responsive.css" to "grid.css"
		while ($objLayout->next())
		{
			$arrCss = StringUtil::deserialize($objLayout->framework);

			if (($key = array_search('responsive.css', $arrCss)) !== false)
			{
				$arrCss[$key] = 'grid.css';
			}

			$this->Database->prepare("UPDATE `tl_layout` SET `framework`=? WHERE `id`=?")
						   ->execute(serialize($arrCss), $objLayout->id);
		}

		// Add the jQuery fields if they do not yet exist (see #5689)
		if (!$this->Database->fieldExists('addJQuery', 'tl_layout'))
		{
			$this->Database->query("ALTER TABLE `tl_layout` ADD `addJQuery` char(1) NOT NULL default ''");
			$this->Database->query("ALTER TABLE `tl_layout` ADD `jSource` varchar(16) NOT NULL default ''");
			$this->Database->query("ALTER TABLE `tl_layout` ADD `jquery` text NULL");
		}

		// Get all page layouts that use the moo_mediaelement template
		$objLayout = $this->Database->query("SELECT `id`, `addJQuery`, `jquery`, `mootools` FROM `tl_layout` WHERE `addMooTools`=1 AND `mootools` LIKE '%\"moo_mediaelement\"%'");

		// Activate the "j_mediaelement" template instead
		while ($objLayout->next())
		{
			$arrSet = array();

			// jQuery already activated
			if ($objLayout->addjQuery)
			{
				$arrJQuery = StringUtil::deserialize($objLayout->jquery);

				// Add j_mediaelement
				if (!\is_array($arrJQuery))
				{
					$arrSet['jquery'] = serialize(array('j_mediaelement'));
				}
				elseif (!\in_array('j_mediaelement', $arrJQuery))
				{
					$arrJQuery[] = 'j_mediaelement';
					$arrSet['jquery'] = serialize($arrJQuery);
				}
			}
			else
			{
				$arrSet['addJQuery'] = 1;
				$arrSet['jSource'] = 'j_local';
				$arrSet['jquery'] = serialize(array('j_mediaelement'));
			}

			$arrMooTools = StringUtil::deserialize($objLayout->mootools);

			// Unset the moo_mediaelement template
			if (($key = array_search('moo_mediaelement', $arrMooTools)) !== false)
			{
				unset($arrMooTools[$key]);
			}

			// Update the MooTools templates
			if (empty($arrMooTools))
			{
				$arrSet['mootools'] = '';
			}
			else
			{
				$arrSet['mootools'] = serialize(array_values($arrMooTools));
			}

			$this->Database->prepare("UPDATE `tl_layout` %s WHERE `id`=?")
						   ->set($arrSet)
						   ->execute($objLayout->id);
		}

		// Get all page layouts
		$objLayout = $this->Database->query("SELECT `id`, `modules` FROM `tl_layout`");

		// Add the "enable" flag to all modules
		while ($objLayout->next())
		{
			$arrModules = StringUtil::deserialize($objLayout->modules);

			foreach (array_keys($arrModules) as $key)
			{
				$arrModules[$key]['enable'] = true;
			}

			$this->Database->prepare("UPDATE `tl_layout` SET `modules`=? WHERE `id`=?")
						   ->execute(serialize($arrModules), $objLayout->id);
		}

		// Adjust the accordion elements
		$this->Database->query("UPDATE `tl_content` SET `type`='accordionStart' WHERE `type`='accordion' AND `mooType`='mooStart'");
		$this->Database->query("UPDATE `tl_content` SET `type`='accordionStop' WHERE `type`='accordion' AND `mooType`='mooStop'");
		$this->Database->query("UPDATE `tl_content` SET `type`='accordionSingle' WHERE `type`='accordion' AND `mooType`='mooSingle'");

		// White-space is now in the "alignment" section (see #4519)
		$this->Database->query("UPDATE `tl_style` SET `alignment`=1 WHERE `whitespace`!=''");
		$this->Database->query("ALTER TABLE `tl_style` CHANGE `whitespace` `whitespace` varchar(8) NOT NULL default ''");
		$this->Database->query("UPDATE `tl_style` SET `whitespace`='nowrap' WHERE `whitespace`!=''");

		// Drop the tl_files.path index (see #5598)
		if ($this->Database->indexExists('path', 'tl_files'))
		{
			$this->Database->query("ALTER TABLE `tl_files` DROP INDEX `path`");
		}

		// Remove the "mooType" field (triggers the version 3.1 update)
		$this->Database->query("ALTER TABLE `tl_content` DROP `mooType`");
	}

	/**
	 * Version 3.2.0 update
	 */
	public function run32Update()
	{
		// Adjust the custom layout sections (see #2885)
		$this->Database->query("ALTER TABLE `tl_layout` CHANGE `sections` `sections` varchar(1022) NOT NULL default ''");
		$objLayout = $this->Database->query("SELECT id, sections FROM tl_layout WHERE sections!=''");

		while ($objLayout->next())
		{
			$strSections = '';
			$tmp = StringUtil::deserialize($objLayout->sections);

			if (!empty($tmp) && \is_array($tmp))
			{
				$strSections = implode(', ', $tmp);
			}

			$this->Database->prepare("UPDATE tl_layout SET sections=? WHERE id=?")
						   ->execute($strSections, $objLayout->id);
		}

		// Check whether there are UUIDs
		if (!$this->Database->fieldExists('uuid', 'tl_files'))
		{
			// Adjust the DB structure
			$this->Database->query("ALTER TABLE `tl_files` ADD `uuid` binary(16) NULL");
			$this->Database->query("ALTER TABLE `tl_files` ADD UNIQUE KEY `uuid` (`uuid`)");

			// Backup the pid column and change the column type
			$this->Database->query("ALTER TABLE `tl_files` ADD `pid_backup` int(10) unsigned NOT NULL default 0");
			$this->Database->query("UPDATE `tl_files` SET `pid_backup`=`pid`");
			$this->Database->query("ALTER TABLE `tl_files` CHANGE `pid` `pid` binary(16) NULL");
			$this->Database->query("UPDATE `tl_files` SET `pid`=NULL");
			$this->Database->query("UPDATE `tl_files` SET `pid`=NULL WHERE `pid_backup`=0");

			$objFiles = $this->Database->query("SELECT id FROM tl_files");

			// Generate the UUIDs
			while ($objFiles->next())
			{
				$this->Database->prepare("UPDATE tl_files SET uuid=? WHERE id=?")
							   ->execute($this->Database->getUuid(), $objFiles->id);
			}

			$objFiles = $this->Database->query("SELECT pid_backup FROM tl_files WHERE pid_backup>0 GROUP BY pid_backup");

			// Adjust the parent IDs
			while ($objFiles->next())
			{
				$objParent = $this->Database->prepare("SELECT uuid FROM tl_files WHERE id=?")
											->execute($objFiles->pid_backup);

				if ($objParent->numRows < 1)
				{
					throw new \Exception('Invalid parent ID ' . $objFiles->pid_backup);
				}

				$this->Database->prepare("UPDATE tl_files SET pid=? WHERE pid_backup=?")
							   ->execute($objParent->uuid, $objFiles->pid_backup);
			}

			// Drop the pid_backup column
			$this->Database->query("ALTER TABLE `tl_files` DROP `pid_backup`");
		}

		// Update the fields
		$this->updateFileTreeFields();
	}

	/**
	 * Version 3.3.0 update
	 */
	public function run33Update()
	{
		$objLayout = $this->Database->query("SELECT id, framework FROM tl_layout WHERE framework!=''");

		while ($objLayout->next())
		{
			$strFramework = '';
			$tmp = StringUtil::deserialize($objLayout->framework);

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

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

			$this->Database->prepare("UPDATE tl_layout SET framework=? WHERE id=?")
						   ->execute($strFramework, $objLayout->id);
		}

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

	/**
	 * Version 3.5.0 update
	 */
	public function run35Update()
	{
		$this->Database->query("ALTER TABLE `tl_member` CHANGE `username` `username` varchar(64) COLLATE utf8_bin NULL");
		$this->Database->query("UPDATE `tl_member` SET username=NULL WHERE username=''");
		$this->Database->query("ALTER TABLE `tl_member` DROP INDEX `username`, ADD UNIQUE KEY `username` (`username`)");
	}

	/**
	 * Version 4.0.0 update
	 */
	public function run40Update()
	{
		// Adjust the framework agnostic scripts
		$this->Database->query("ALTER TABLE `tl_layout` ADD `scripts` text NULL");
		$objLayout = $this->Database->query("SELECT id, addJQuery, jquery, addMooTools, mootools FROM tl_layout WHERE framework!=''");

		while ($objLayout->next())
		{
			$arrScripts = array();

			// Check whether j_slider is enabled
			if ($objLayout->addJQuery)
			{
				$jquery = StringUtil::deserialize($objLayout->jquery);

				if (!empty($jquery) && \is_array($jquery))
				{
					if (($key = array_search('j_slider', $jquery)) !== false)
					{
						$arrScripts[] = 'js_slider';
						unset($jquery[$key]);

						$this->Database->prepare("UPDATE tl_layout SET jquery=? WHERE id=?")
									   ->execute(serialize(array_values($jquery)), $objLayout->id);
					}
				}
			}

			// Check whether moo_slider is enabled
			if ($objLayout->addMooTools)
			{
				$mootools = StringUtil::deserialize($objLayout->mootools);

				if (!empty($mootools) && \is_array($mootools))
				{
					if (($key = array_search('moo_slider', $mootools)) !== false)
					{
						$arrScripts[] = 'js_slider';
						unset($mootools[$key]);

						$this->Database->prepare("UPDATE tl_layout SET mootools=? WHERE id=?")
									   ->execute(serialize(array_values($mootools)), $objLayout->id);
					}
				}
			}

			// Enable the js_slider template
			if (!empty($arrScripts))
			{
				$this->Database->prepare("UPDATE tl_layout SET scripts=? WHERE id=?")
							   ->execute(serialize(array_values(array_unique($arrScripts))), $objLayout->id);
			}
		}
	}

	/**
	 * Scan the upload folder and create the database entries
	 *
	 * @param string  $strPath The target folder
	 * @param integer $pid     The parent ID
	 */
	public function scanUploadFolder($strPath=null, $pid=null)
	{
		if ($strPath === null)
		{
			$strPath = Config::get('uploadPath');
		}

		$arrMeta = array();
		$arrMapper = array();
		$arrFolders = array();
		$arrFiles = array();
		$rootDir = System::getContainer()->getParameter('kernel.project_dir');
		$arrScan = scan($rootDir . '/' . $strPath);

		foreach ($arrScan as $strFile)
		{
			if (strncmp($strFile, '.', 1) === 0)
			{
				continue;
			}

			if (is_dir($rootDir . '/' . $strPath . '/' . $strFile))
			{
				$arrFolders[] = $strPath . '/' . $strFile;
			}
			else
			{
				$arrFiles[] = $strPath . '/' . $strFile;
			}
		}

		// Folders
		foreach ($arrFolders as $strFolder)
		{
			$strUuid = $this->Database->getUuid();

			$this->scanUploadFolder($strFolder, $strUuid);

			$this->Database->prepare("INSERT INTO tl_files (pid, tstamp, uuid, name, type, path, hash) VALUES (?, ?, ?, ?, 'folder', ?, ?)")
						   ->execute($pid, time(), $strUuid, basename($strFolder), $strFolder, Dbafs::getFolderHash($strFolder));
		}

		// Files
		foreach ($arrFiles as $strFile)
		{
			$matches = array();

			// Handle meta.txt files
			if (preg_match('/^meta(_([a-z]{2}))?\.txt$/', basename($strFile), $matches))
			{
				$key = $matches[2] ?: 'en';
				$arrData = file($rootDir . '/' . $strFile, FILE_IGNORE_NEW_LINES);

				foreach ($arrData as $line)
				{
					list($name, $info) = explode('=', $line, 2);
					list($title, $link, $caption) = explode('|', $info);
					$arrMeta[trim($name)][$key] = array('title'=>trim($title), 'link'=>trim($link), 'caption'=>trim($caption));
				}
			}

			$objFile = new File($strFile);
			$strUuid = $this->Database->getUuid();

			$this->Database->prepare("INSERT INTO tl_files (pid, tstamp, uuid, name, type, path, extension, hash) VALUES (?, ?, ?, ?, 'file', ?, ?, ?)")
						   ->execute($pid, time(), $strUuid, basename($strFile), $strFile, $objFile->extension, $objFile->hash);

			$arrMapper[basename($strFile)] = $strUuid;
		}

		// Insert the meta data AFTER the file entries have been created
		if (!empty($arrMeta))
		{
			foreach ($arrMeta as $file=>$meta)
			{
				if (isset($arrMapper[$file]))
				{
					$this->Database->prepare("UPDATE tl_files SET meta=? WHERE uuid=?")
								   ->execute(serialize($meta), $arrMapper[$file]);
				}
			}
		}
	}

	/**
	 * Update all FileTree fields
	 */
	public function updateFileTreeFields()
	{
		$processed = array();
		$arrFields = array();

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

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

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

			try
			{
				$this->loadDataContainer($strTable);
			}
			catch (\Exception $e)
			{
				continue;
			}

			// Make sure the table exists (see #8304)
			if (!$this->Database->tableExists($strTable, null, true))
			{
				continue;
			}

			$arrConfig = &$GLOBALS['TL_DCA'][$strTable]['config'];

			// Skip non-database DCAs
			if ($arrConfig['dataContainer'] == 'File')
			{
				continue;
			}
			if ($arrConfig['dataContainer'] == 'Folder' && !$arrConfig['databaseAssisted'])
			{
				continue;
			}

			// Make sure there are fields (see #6437)
			if (\is_array($GLOBALS['TL_DCA'][$strTable]['fields']))
			{
				foreach ($GLOBALS['TL_DCA'][$strTable]['fields'] as $strField=>$arrField)
				{
					if ($arrField['inputType'] == 'fileTree')
					{
						if ($this->Database->fieldExists($strField, $strTable, true))
						{
							$key = $arrField['eval']['multiple'] ? 'multiple' : 'single';
							$arrFields[$key][] = $strTable . '.' . $strField;
						}

						// Convert the order fields as well
						if (isset($arrField['eval']['orderField']) && isset($GLOBALS['TL_DCA'][$strTable]['fields'][$arrField['eval']['orderField']]))
						{
							if ($this->Database->fieldExists($arrField['eval']['orderField'], $strTable, true))
							{
								$arrFields['order'][] = $strTable . '.' . $arrField['eval']['orderField'];
							}
						}
					}
				}
			}
		}

		// Update the existing singleSRC entries
		if (isset($arrFields['single']))
		{
			foreach ($arrFields['single'] as $val)
			{
				list($table, $field) = explode('.', $val);
				static::convertSingleField($table, $field);
			}
		}

		// Update the existing multiSRC entries
		if (isset($arrFields['multiple']))
		{
			foreach ($arrFields['multiple'] as $val)
			{
				list($table, $field) = explode('.', $val);
				static::convertMultiField($table, $field);
			}
		}

		// Update the existing orderField entries
		if (isset($arrFields['order']))
		{
			foreach ($arrFields['order'] as $val)
			{
				list($table, $field) = explode('.', $val);
				static::convertOrderField($table, $field);
			}
		}
	}

	/**
	 * Convert a single source field to UUIDs
	 *
	 * @param string $table The table name
	 * @param string $field The field name
	 */
	public static function convertSingleField($table, $field)
	{
		$objDatabase = Database::getInstance();

		// Get the non-empty rows
		$objRow = $objDatabase->query("SELECT id, $field FROM $table WHERE $field!=''");

		// Check the column type
		$objDesc = $objDatabase->query("DESC $table $field");

		// Change the column type
		if ($objDesc->Type != 'binary(16)')
		{
			$objDatabase->query("ALTER TABLE `$table` CHANGE `$field` `$field` binary(16) NULL");
			$objDatabase->query("UPDATE `$table` SET `$field`=NULL WHERE `$field`='' OR `$field`=0");
		}

		while ($objRow->next())
		{
			$objHelper = static::generateHelperObject($objRow->$field);

			// UUID already
			if ($objHelper->isUuid)
			{
				continue;
			}

			// Numeric ID to UUID
			if ($objHelper->isNumeric)
			{
				$objFile = FilesModel::findByPk($objHelper->value);

				$objDatabase->prepare("UPDATE $table SET $field=? WHERE id=?")
							->execute($objFile->uuid, $objRow->id);
			}

			// Path to UUID
			else
			{
				$objFile = FilesModel::findByPath($objHelper->value);

				$objDatabase->prepare("UPDATE $table SET $field=? WHERE id=?")
							->execute($objFile->uuid, $objRow->id);
			}
		}
	}

	/**
	 * Convert a multi source field to UUIDs
	 *
	 * @param string $table The table name
	 * @param string $field The field name
	 */
	public static function convertMultiField($table, $field)
	{
		$objDatabase = Database::getInstance();

		// Get the non-empty rows
		$objRow = $objDatabase->query("SELECT id, $field FROM $table WHERE $field!=''");

		// Check the column type
		$objDesc = $objDatabase->query("DESC $table $field");

		// Change the column type
		if ($objDesc->Type != 'blob')
		{
			$objDatabase->query("ALTER TABLE `$table` CHANGE `$field` `$field` blob NULL");
			$objDatabase->query("UPDATE `$table` SET `$field`=NULL WHERE `$field`=''");
		}

		while ($objRow->next())
		{
			$arrValues = StringUtil::deserialize($objRow->$field, true);

			if (empty($arrValues))
			{
				continue;
			}

			$objHelper = static::generateHelperObject($arrValues);

			// UUID already
			if ($objHelper->isUuid)
			{
				continue;
			}

			foreach ($arrValues as $k=>$v)
			{
				// Numeric ID to UUID
				if ($objHelper->isNumeric)
				{
					$objFile = FilesModel::findByPk($objHelper->value[$k]);
					$arrValues[$k] = $objFile->uuid;
				}

				// Path to UUID
				else
				{
					$objFile = FilesModel::findByPath($objHelper->value[$k]);
					$arrValues[$k] = $objFile->uuid;
				}
			}

			$objDatabase->prepare("UPDATE $table SET $field=? WHERE id=?")
						->execute(serialize($arrValues), $objRow->id);
		}
	}

	/**
	 * Convert an order source field to UUIDs
	 *
	 * @param string $table The table name
	 * @param string $field The field name
	 */
	public static function convertOrderField($table, $field)
	{
		$objDatabase = Database::getInstance();

		// Get the non-empty rows
		$objRow = $objDatabase->query("SELECT id, $field FROM $table WHERE $field LIKE '%,%'");

		// Convert the comma separated lists into serialized arrays
		while ($objRow->next())
		{
			$objDatabase->prepare("UPDATE $table SET $field=? WHERE id=?")
						->execute(serialize(explode(',', $objRow->$field)), $objRow->id);
		}

		static::convertMultiField($table, $field);
	}

	/**
	 * Generate a helper object based on a field value
	 *
	 * @param mixed $value The field value
	 *
	 * @return \stdClass The helper object
	 */
	protected static function generateHelperObject($value)
	{
		$return = new \stdClass();

		if (!\is_array($value))
		{
			$return->value = rtrim($value, "\x00");
			$return->isUuid = (\strlen($value) == 16 && !is_numeric($return->value) && strncmp($return->value, Config::get('uploadPath') . '/', \strlen(Config::get('uploadPath')) + 1) !== 0);
			$return->isNumeric = (is_numeric($return->value) && $return->value > 0);
		}
		else
		{
			$return->value = array_map(function ($var) { return rtrim($var, "\x00"); }, $value);
			$return->isUuid = (\strlen($value[0]) == 16 && !is_numeric($return->value[0]) && strncmp($return->value[0], Config::get('uploadPath') . '/', \strlen(Config::get('uploadPath')) + 1) !== 0);
			$return->isNumeric = (is_numeric($return->value[0]) && $return->value[0] > 0);
		}

		return $return;
	}

	/**
	 * Create a content element
	 *
	 * @param Result $objElement A database result object
	 * @param string $strPtable  The name of the parent table
	 * @param string $strField   The name of the text column
	 */
	protected function createContentElement(Result $objElement, $strPtable, $strField)
	{
		$set = array
		(
			'pid'         => $objElement->id,
			'ptable'      => $strPtable,
			'sorting'     => 128,
			'tstamp'      => $objElement->tstamp,
			'type'        => 'text',
			'text'        => $objElement->$strField,
			'addImage'    => $objElement->addImage,
			'singleSRC'   => $objElement->singleSRC,
			'alt'         => $objElement->alt,
			'size'        => $objElement->size,
			'imagemargin' => $objElement->imagemargin,
			'imageUrl'    => $objElement->imageUrl,
			'fullsize'    => $objElement->fullsize,
			'caption'     => $objElement->caption,
			'floating'    => $objElement->floating
		);

		$this->Database->prepare("INSERT INTO tl_content %s")->set($set)->execute();
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\InstallationBundle\Controller;

use Contao\Environment;
use Contao\InstallationBundle\Config\ParameterDumper;
use Contao\InstallationBundle\Database\AbstractVersionUpdate;
use Contao\InstallationBundle\Database\ConnectionFactory;
use Contao\InstallationBundle\Event\ContaoInstallationEvents;
use Contao\InstallationBundle\Event\InitializeApplicationEvent;
use Doctrine\DBAL\DBALException;
use Patchwork\Utf8;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/contao", defaults={"_scope" = "backend", "_token_check" = true})
 */
class InstallationController implements ContainerAwareInterface
{
    use ContainerAwareTrait;

    /**
     * @var array
     */
    private $context = [
        'has_admin' => false,
        'hide_admin' => false,
        'sql_message' => '',
    ];

    /**
     * @Route("/install", name="contao_install")
     */
    public function installAction(): Response
    {
        if (null !== ($response = $this->initializeApplication())) {
            return $response;
        }

        if ($this->container->has('contao.framework')) {
            $this->container->get('contao.framework')->initialize();
        }

        $installTool = $this->container->get('contao.install_tool');

        if ($installTool->isLocked()) {
            return $this->render('locked.html.twig');
        }

        if (!$installTool->canWriteFiles()) {
            return $this->render('not_writable.html.twig');
        }

        if ($installTool->shouldAcceptLicense()) {
            return $this->acceptLicense();
        }

        if ('' === $installTool->getConfig('installPassword')) {
            return $this->setPassword();
        }

        if (!$this->container->get('contao.install_tool_user')->isAuthenticated()) {
            return $this->login();
        }

        if (!$installTool->canConnectToDatabase($this->getContainerParameter('database_name'))) {
            return $this->setUpDatabaseConnection();
        }

        $this->warmUpSymfonyCache();

        if ($installTool->hasOldDatabase()) {
            return $this->render('old_database.html.twig');
        }

        if ($installTool->hasConfigurationError($this->context)) {
            return $this->render('configuration_error.html.twig');
        }

        $this->runDatabaseUpdates();

        if (null !== ($response = $this->adjustDatabaseTables())) {
            return $response;
        }

        if (null !== ($response = $this->importExampleWebsite())) {
            return $response;
        }

        if (null !== ($response = $this->createAdminUser())) {
            return $response;
        }

        return $this->render('main.html.twig', $this->context);
    }

    private function initializeApplication(): ?Response
    {
        $event = new InitializeApplicationEvent();

        $this->container->get('event_dispatcher')->dispatch(ContaoInstallationEvents::INITIALIZE_APPLICATION, $event);

        if ($event->hasOutput()) {
            return $this->render('initialize.html.twig', [
                'output' => $event->getOutput(),
            ]);
        }

        return null;
    }

    /**
     * Renders a form to accept the license.
     *
     * @throws \RuntimeException
     *
     * @return Response|RedirectResponse
     */
    private function acceptLicense(): Response
    {
        $request = $this->container->get('request_stack')->getCurrentRequest();

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

        if ('tl_license' !== $request->request->get('FORM_SUBMIT')) {
            return $this->render('license.html.twig');
        }

        $this->container->get('contao.install_tool')->persistConfig('licenseAccepted', true);

        return $this->getRedirectResponse();
    }

    /**
     * Renders a form to set the install tool password.
     *
     * @throws \RuntimeException
     *
     * @return Response|RedirectResponse
     */
    private function setPassword(): Response
    {
        $request = $this->container->get('request_stack')->getCurrentRequest();

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

        if ('tl_password' !== $request->request->get('FORM_SUBMIT')) {
            return $this->render('password.html.twig');
        }

        $password = $request->request->get('password');
        $confirmation = $request->request->get('confirmation');

        // The passwords do not match
        if ($password !== $confirmation) {
            return $this->render('password.html.twig', [
                'error' => $this->trans('password_confirmation_mismatch'),
            ]);
        }

        $installTool = $this->container->get('contao.install_tool');
        $minlength = $installTool->getConfig('minPasswordLength');

        // The passwords is too short
        if (Utf8::strlen($password) < $minlength) {
            return $this->render('password.html.twig', [
                'error' => sprintf($this->trans('password_too_short'), $minlength),
            ]);
        }

        $installTool->persistConfig('installPassword', password_hash($password, PASSWORD_DEFAULT));
        $this->container->get('contao.install_tool_user')->setAuthenticated(true);

        return $this->getRedirectResponse();
    }

    /**
     * Renders a form to log in.
     *
     * @throws \RuntimeException
     *
     * @return Response|RedirectResponse
     */
    private function login(): Response
    {
        $request = $this->container->get('request_stack')->getCurrentRequest();

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

        if ('tl_login' !== $request->request->get('FORM_SUBMIT')) {
            return $this->render('login.html.twig');
        }

        $installTool = $this->container->get('contao.install_tool');

        $verified = password_verify(
            $request->request->get('password'),
            $installTool->getConfig('installPassword')
        );

        if (!$verified) {
            $installTool->increaseLoginCount();

            return $this->render('login.html.twig', [
                'error' => $this->trans('invalid_password'),
            ]);
        }

        $installTool->resetLoginCount();
        $this->container->get('contao.install_tool_user')->setAuthenticated(true);

        return $this->getRedirectResponse();
    }

    /**
     * The method preserves the container directory inside the cache folder,
     * because Symfony will throw a "compile error" exception if it is deleted
     * in the middle of a request.
     */
    private function purgeSymfonyCache(): void
    {
        $filesystem = new Filesystem();
        $cacheDir = $this->getContainerParameter('kernel.cache_dir');
        $ref = new \ReflectionObject($this->container);
        $containerDir = basename(\dirname($ref->getFileName()));

        /** @var SplFileInfo[] $finder */
        $finder = Finder::create()
            ->depth(0)
            ->exclude($containerDir)
            ->in($cacheDir)
        ;

        foreach ($finder as $file) {
            $filesystem->remove($file->getPathname());
        }

        if (\function_exists('opcache_reset')) {
            opcache_reset();
        }

        if (\function_exists('apc_clear_cache') && !ini_get('apc.stat')) {
            apc_clear_cache();
        }
    }

    /**
     * The method runs the optional cache warmers, because the cache will only
     * have the non-optional stuff at this time.
     */
    private function warmUpSymfonyCache(): void
    {
        $cacheDir = $this->getContainerParameter('kernel.cache_dir');

        if (file_exists($cacheDir.'/contao/config/config.php')) {
            return;
        }

        $warmer = $this->container->get('cache_warmer');

        if (!$this->getContainerParameter('kernel.debug')) {
            $warmer->enableOptionalWarmers();
        }

        $warmer->warmUp($cacheDir);

        if (\function_exists('opcache_reset')) {
            opcache_reset();
        }

        if (\function_exists('apc_clear_cache') && !ini_get('apc.stat')) {
            apc_clear_cache();
        }
    }

    /**
     * Renders a form to set up the database connection.
     *
     * @throws \RuntimeException
     *
     * @return Response|RedirectResponse
     */
    private function setUpDatabaseConnection(): Response
    {
        $request = $this->container->get('request_stack')->getCurrentRequest();

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

        // Only warn the user if the connection fails and the env component is used
        if (false !== getenv('DATABASE_URL')) {
            return $this->render('misconfigured_database_url.html.twig');
        }

        $parameters = [
            'parameters' => [
                'database_host' => $this->getContainerParameter('database_host'),
                'database_port' => $this->getContainerParameter('database_port'),
                'database_user' => $this->getContainerParameter('database_user'),
                'database_password' => $this->getContainerParameter('database_password'),
                'database_name' => $this->getContainerParameter('database_name'),
            ],
        ];

        if ('tl_database_login' !== $request->request->get('FORM_SUBMIT')) {
            return $this->render('database.html.twig', $parameters);
        }

        $parameters = [
            'parameters' => [
                'database_host' => $request->request->get('dbHost'),
                'database_port' => $request->request->get('dbPort'),
                'database_user' => $request->request->get('dbUser'),
                'database_password' => $this->getContainerParameter('database_password'),
                'database_name' => $request->request->get('dbName'),
            ],
        ];

        if ('*****' !== $request->request->get('dbPassword')) {
            $parameters['parameters']['database_password'] = $request->request->get('dbPassword');
        }

        if (false !== strpos($parameters['parameters']['database_name'], '.')) {
            return $this->render('database.html.twig', array_merge(
                $parameters,
                ['database_error' => $this->trans('database_dot_in_dbname')]
            ));
        }

        $installTool = $this->container->get('contao.install_tool');
        $installTool->setConnection(ConnectionFactory::create($parameters));

        if (!$installTool->canConnectToDatabase($parameters['parameters']['database_name'])) {
            return $this->render('database.html.twig', array_merge(
                $parameters,
                ['database_error' => $this->trans('database_could_not_connect')]
            ));
        }

        $dumper = new ParameterDumper($this->getContainerParameter('kernel.project_dir'));
        $dumper->setParameters($parameters);
        $dumper->dump();

        $this->purgeSymfonyCache();

        return $this->getRedirectResponse();
    }

    private function runDatabaseUpdates(): void
    {
        if ($this->container->get('contao.install_tool')->isFreshInstallation()) {
            return;
        }

        /** @var SplFileInfo[] $finder */
        $finder = Finder::create()
            ->files()
            ->name('Version*Update.php')
            ->sortByName()
            ->in(__DIR__.'/../Database')
        ;

        $messages = [];

        foreach ($finder as $file) {
            $class = 'Contao\InstallationBundle\Database\\'.$file->getBasename('.php');

            /** @var AbstractVersionUpdate $update */
            $update = new $class($this->container->get('database_connection'));

            if ($update instanceof AbstractVersionUpdate) {
                $update->setContainer($this->container);

                if ($update->shouldBeRun()) {
                    $update->run();
                }

                if ($message = $update->getMessage()) {
                    $messages[] = $message;
                }
            }
        }

        $this->context['sql_message'] = implode('', $messages);
    }

    /**
     * Renders a form to adjust the database tables.
     *
     * @throws \RuntimeException
     */
    private function adjustDatabaseTables(): ?RedirectResponse
    {
        $this->container->get('contao.install_tool')->handleRunOnce();

        $installer = $this->container->get('contao.installer');

        $this->context['sql_form'] = $installer->getCommands();

        $request = $this->container->get('request_stack')->getCurrentRequest();

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

        if ('tl_database_update' !== $request->request->get('FORM_SUBMIT')) {
            return null;
        }

        $sql = $request->request->get('sql');

        if (!empty($sql) && \is_array($sql)) {
            foreach ($sql as $hash) {
                $installer->execCommand($hash);
            }
        }

        return $this->getRedirectResponse();
    }

    /**
     * Renders a form to import the example website.
     *
     * @throws \RuntimeException
     */
    private function importExampleWebsite(): ?RedirectResponse
    {
        $installTool = $this->container->get('contao.install_tool');
        $templates = $installTool->getTemplates();

        $this->context['templates'] = $templates;

        if ($installTool->getConfig('exampleWebsite')) {
            $this->context['import_date'] = date('Y-m-d H:i', $installTool->getConfig('exampleWebsite'));
        }

        $request = $this->container->get('request_stack')->getCurrentRequest();

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

        if ('tl_template_import' !== $request->request->get('FORM_SUBMIT')) {
            return null;
        }

        $template = $request->request->get('template');

        if ('' === $template || !\in_array($template, $templates, true)) {
            $this->context['import_error'] = $this->trans('import_empty_source');

            return null;
        }

        try {
            $installTool->importTemplate($template, '1' === $request->request->get('preserve'));
        } catch (DBALException $e) {
            $installTool->persistConfig('exampleWebsite', null);
            $installTool->logException($e);

            $this->context['import_error'] = $this->trans('import_exception');

            return null;
        }

        $installTool->persistConfig('exampleWebsite', time());

        return $this->getRedirectResponse();
    }

    /**
     * @throws \RuntimeException
     */
    private function createAdminUser(): ?RedirectResponse
    {
        $installTool = $this->container->get('contao.install_tool');

        if (!$installTool->hasTable('tl_user')) {
            $this->context['hide_admin'] = true;

            return null;
        }

        if ($installTool->hasAdminUser()) {
            $this->context['has_admin'] = true;

            return null;
        }

        $request = $this->container->get('request_stack')->getCurrentRequest();

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

        if ('tl_admin' !== $request->request->get('FORM_SUBMIT')) {
            return null;
        }

        $username = $request->request->get('username');
        $name = $request->request->get('name');
        $email = $request->request->get('email');
        $password = $request->request->get('password');
        $confirmation = $request->request->get('confirmation');

        $this->context['admin_username_value'] = $username;
        $this->context['admin_name_value'] = $name;
        $this->context['admin_email_value'] = $email;

        // All fields are mandatory
        if ('' === $username || '' === $name || '' === $email || '' === $password) {
            $this->context['admin_error'] = $this->trans('admin_error');

            return null;
        }

        // Do not allow special characters in usernames
        if (preg_match('/[#()\/<=>]/', $username)) {
            $this->context['admin_username_error'] = $this->trans('admin_error_extnd');

            return null;
        }

        // The username must not contain whitespace characters (see #4006)
        if (false !== strpos($username, ' ')) {
            $this->context['admin_username_error'] = $this->trans('admin_error_no_space');

            return null;
        }

        // Validate the e-mail address (see #6003)
        if (filter_var($email, FILTER_VALIDATE_EMAIL) !== $email) {
            $this->context['admin_email_error'] = $this->trans('admin_error_email');

            return null;
        }

        // The passwords do not match
        if ($password !== $confirmation) {
            $this->context['admin_password_error'] = $this->trans('admin_error_password_match');

            return null;
        }

        $minlength = $installTool->getConfig('minPasswordLength');

        // The password is too short
        if (Utf8::strlen($password) < $minlength) {
            $this->context['admin_password_error'] = sprintf($this->trans('password_too_short'), $minlength);

            return null;
        }

        // Password and username are the same
        if ($password === $username) {
            $this->context['admin_password_error'] = sprintf($this->trans('admin_error_password_user'), $minlength);

            return null;
        }

        $installTool->persistConfig('adminEmail', $email);

        $installTool->persistAdminUser(
            $username,
            $name,
            $email,
            $password,
            $request->getLocale()
        );

        return $this->getRedirectResponse();
    }

    private function render(string $name, array $context = []): Response
    {
        return new Response(
            $this->container->get('twig')->render(
                '@ContaoInstallation/'.$name,
                $this->addDefaultsToContext($context)
            )
        );
    }

    private function trans(string $key): string
    {
        return $this->container->get('translator')->trans($key);
    }

    /**
     * Returns a redirect response to reload the page.
     *
     * @throws \RuntimeException
     */
    private function getRedirectResponse(): RedirectResponse
    {
        $request = $this->container->get('request_stack')->getCurrentRequest();

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

        return new RedirectResponse($request->getRequestUri());
    }

    /**
     * Adds the default values to the context.
     *
     * @throws \RuntimeException
     *
     * @return array<string,string>
     */
    private function addDefaultsToContext(array $context): array
    {
        $context = array_merge($this->context, $context);

        if (!isset($context['request_token'])) {
            $context['request_token'] = $this->getRequestToken();
        }

        if (!isset($context['language'])) {
            $context['language'] = $this->container->get('translator')->getLocale();
        }

        if (!isset($context['ua'])) {
            $context['ua'] = $this->getUserAgentString();
        }

        if (!isset($context['path'])) {
            $request = $this->container->get('request_stack')->getCurrentRequest();

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

            $context['host'] = $request->getHost();
            $context['path'] = $request->getBasePath();
        }

        return $context;
    }

    private function getRequestToken(): string
    {
        $tokenName = $this->getContainerParameter('contao.csrf_token_name');

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

        return $this->container->get('contao.csrf.token_manager')->getToken($tokenName)->getValue();
    }

    /**
     * @return string|bool|null
     */
    private function getContainerParameter(string $name)
    {
        if ($this->container->hasParameter($name)) {
            return $this->container->getParameter($name);
        }

        return null;
    }

    private function getUserAgentString(): string
    {
        if (!$this->container->has('contao.framework') || !$this->container->get('contao.framework')->isInitialized()) {
            return '';
        }

        return Environment::get('agent')->class;
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Command;

use Contao\CoreBundle\Analyzer\HtaccessAnalyzer;
use Contao\CoreBundle\Config\ResourceFinderInterface;
use Contao\CoreBundle\Event\ContaoCoreEvents;
use Contao\CoreBundle\Event\GenerateSymlinksEvent;
use Contao\CoreBundle\Util\SymlinkUtil;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;

/**
 * Symlinks the public resources into the web directory.
 */
class SymlinksCommand extends Command
{
    /**
     * @var SymfonyStyle
     */
    private $io;

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

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

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

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

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

    /**
     * @var ResourceFinderInterface
     */
    private $resourceFinder;

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

    /**
     * @var int
     */
    private $statusCode = 0;

    public function __construct(string $rootDir, string $uploadPath, string $logsDir, ResourceFinderInterface $resourceFinder, EventDispatcherInterface $eventDispatcher)
    {
        $this->rootDir = $rootDir;
        $this->uploadPath = $uploadPath;
        $this->logsDir = $logsDir;
        $this->resourceFinder = $resourceFinder;
        $this->eventDispatcher = $eventDispatcher;

        parent::__construct();
    }

    /**
     * {@inheritdoc}
     */
    protected function configure(): void
    {
        $this
            ->setName('contao:symlinks')
            ->setDefinition([
                new InputArgument('target', InputArgument::OPTIONAL, 'The target directory', 'web'),
            ])
            ->setDescription('Symlinks the public resources into the web directory.')
        ;
    }

    /**
     * {@inheritdoc}
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->io = new SymfonyStyle($input, $output);
        $this->webDir = rtrim($input->getArgument('target'), '/');

        $this->generateSymlinks();

        if (!empty($this->rows)) {
            $this->io->newLine();
            $this->io->table(['', 'Symlink', 'Target / Error'], $this->rows);
        }

        return $this->statusCode;
    }

    /**
     * Generates the symlinks in the web directory.
     */
    private function generateSymlinks(): void
    {
        $fs = new Filesystem();

        // Remove the base folders in the document root
        $fs->remove($this->rootDir.'/'.$this->webDir.'/'.$this->uploadPath);
        $fs->remove($this->rootDir.'/'.$this->webDir.'/system/modules');
        $fs->remove($this->rootDir.'/'.$this->webDir.'/vendor');

        $this->symlinkFiles($this->uploadPath);
        $this->symlinkModules();
        $this->symlinkThemes();

        // Symlink the assets and themes directory
        $this->symlink('assets', $this->webDir.'/assets');
        $this->symlink('system/themes', $this->webDir.'/system/themes');

        // Symlinks the logs directory
        $this->symlink($this->getRelativePath($this->logsDir), 'system/logs');

        $this->triggerSymlinkEvent();
    }

    private function symlinkFiles(string $uploadPath): void
    {
        $this->createSymlinksFromFinder(
            $this->findIn($this->rootDir.'/'.$uploadPath)->files()->depth('> 0')->name('.public'),
            $uploadPath
        );
    }

    private function symlinkModules(): void
    {
        $filter = static function (SplFileInfo $file): bool {
            return HtaccessAnalyzer::create($file)->grantsAccess();
        };

        $this->createSymlinksFromFinder(
            $this->findIn($this->rootDir.'/system/modules')->files()->filter($filter)->name('.htaccess'),
            'system/modules'
        );
    }

    private function symlinkThemes(): void
    {
        /** @var SplFileInfo[] $themes */
        $themes = $this->resourceFinder->findIn('themes')->depth(0)->directories();

        foreach ($themes as $theme) {
            $path = $this->getRelativePath($theme->getPathname());

            if (0 === strncmp($path, 'system/modules/', 15)) {
                continue;
            }

            $this->symlink($path, 'system/themes/'.basename($path));
        }
    }

    private function createSymlinksFromFinder(Finder $finder, string $prepend): void
    {
        $files = $this->filterNestedPaths($finder, $prepend);

        foreach ($files as $file) {
            $path = rtrim($prepend.'/'.$file->getRelativePath(), '/');
            $this->symlink($path, $this->webDir.'/'.$path);
        }
    }

    private function triggerSymlinkEvent(): void
    {
        $event = new GenerateSymlinksEvent();

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

        foreach ($event->getSymlinks() as $target => $link) {
            $this->symlink($target, $link);
        }
    }

    /**
     * The method will try to generate relative symlinks and fall back to generating
     * absolute symlinks if relative symlinks are not supported (see #208).
     */
    private function symlink(string $target, string $link): void
    {
        $target = strtr($target, '\\', '/');
        $link = strtr($link, '\\', '/');

        try {
            SymlinkUtil::symlink($target, $link, $this->rootDir);

            $this->rows[] = [
                sprintf(
                    '<fg=green;options=bold>%s</>',
                    '\\' === \DIRECTORY_SEPARATOR ? 'OK' : "\xE2\x9C\x94" // HEAVY CHECK MARK (U+2714)
                ),
                $link,
                $target,
            ];
        } catch (\Exception $e) {
            $this->statusCode = 1;

            $this->rows[] = [
                sprintf(
                    '<fg=red;options=bold>%s</>',
                    '\\' === \DIRECTORY_SEPARATOR ? 'ERROR' : "\xE2\x9C\x98" // HEAVY BALLOT X (U+2718)
                ),
                $link,
                sprintf('<error>%s</error>', $e->getMessage()),
            ];
        }
    }

    /**
     * Returns a finder instance to find files in the given path.
     */
    private function findIn(string $path): Finder
    {
        return Finder::create()
            ->ignoreDotFiles(false)
            ->sort(
                static function (SplFileInfo $a, SplFileInfo $b): int {
                    $countA = substr_count(strtr($a->getRelativePath(), '\\', '/'), '/');
                    $countB = substr_count(strtr($b->getRelativePath(), '\\', '/'), '/');

                    return $countA <=> $countB;
                }
            )
            ->followLinks()
            ->in($path)
        ;
    }

    /**
     * Filters nested paths so only the top folder is symlinked.
     *
     * @return SplFileInfo[]
     */
    private function filterNestedPaths(Finder $finder, string $prepend): array
    {
        $parents = [];
        $files = iterator_to_array($finder);

        foreach ($files as $key => $file) {
            $path = rtrim(strtr($prepend.'/'.$file->getRelativePath(), '\\', '/'), '/');

            if (!empty($parents)) {
                $parent = \dirname($path);

                while (false !== strpos($parent, '/')) {
                    if (\in_array($parent, $parents, true)) {
                        $this->rows[] = [
                            sprintf(
                                '<fg=yellow;options=bold>%s</>',
                                '\\' === \DIRECTORY_SEPARATOR ? 'WARNING' : '!'
                            ),
                            $this->webDir.'/'.$path,
                            sprintf('<comment>Skipped because %s will be symlinked.</comment>', $parent),
                        ];

                        unset($files[$key]);
                        break;
                    }

                    $parent = \dirname($parent);
                }
            }

            $parents[] = $path;
        }

        return array_values($files);
    }

    private function getRelativePath(string $path): string
    {
        return str_replace(strtr($this->rootDir, '\\', '/').'/', '', strtr($path, '\\', '/'));
    }
}


<?php

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

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

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

	// Config
	'config' => array
	(
		'dataContainer'               => 'Table',
		'ptable'                      => 'tl_image_size',
		'enableVersioning'            => true,
		'markAsCopy'                  => 'media',
		'onload_callback' => array
		(
			array('tl_image_size_item', 'checkPermission'),
			array('tl_image_size_item', 'showJsLibraryHint')
		),
		'sql' => array
		(
			'keys' => array
			(
				'id' => 'primary',
				'pid' => 'index'
			)
		)
	),

	// List
	'list' => array
	(
		'sorting' => array
		(
			'mode'                    => 4,
			'fields'                  => array('sorting'),
			'panelLayout'             => 'limit',
			'headerFields'            => array('name', 'tstamp', 'width', 'height', 'resizeMode', 'zoom'),
			'child_record_callback'   => array('tl_image_size_item', 'listImageSizeItem')
		),
		'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_item']['edit'],
				'href'                => 'act=edit',
				'icon'                => 'edit.svg'
			),
			'copy' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size_item']['copy'],
				'href'                => 'act=paste&mode=copy',
				'icon'                => 'copy.svg',
				'attributes'          => 'onclick="Backend.getScrollOffset()"'
			),
			'cut' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size_item']['cut'],
				'href'                => 'act=paste&mode=cut',
				'icon'                => 'cut.svg',
				'attributes'          => 'onclick="Backend.getScrollOffset()"'
			),
			'delete' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size_item']['delete'],
				'href'                => 'act=delete',
				'icon'                => 'delete.svg',
				'attributes'          => 'onclick="if(!confirm(\'' . $GLOBALS['TL_LANG']['MSC']['deleteConfirm'] . '\'))return false;Backend.getScrollOffset()"'
			),
			'toggle' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size_item']['toggle'],
				'icon'                => 'visible.svg',
				'attributes'          => 'onclick="Backend.getScrollOffset();return AjaxRequest.toggleVisibility(this,%s)"',
				'button_callback'     => array('tl_image_size_item', 'toggleIcon')
			),
			'show' => array
			(
				'label'               => &$GLOBALS['TL_LANG']['tl_image_size_item']['show'],
				'href'                => 'act=show',
				'icon'                => 'show.svg'
			)
		)
	),

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

	// Fields
	'fields' => array
	(
		'id' => array
		(
			'sql'                     => "int(10) unsigned NOT NULL auto_increment"
		),
		'pid' => array
		(
			'foreignKey'              => 'tl_image_size.name',
			'sql'                     => "int(10) unsigned NOT NULL default 0",
			'relation'                => array('type'=>'belongsTo', 'load'=>'lazy')
		),
		'sorting' => array
		(
			'sql'                     => "int(10) unsigned NOT NULL default 0"
		),
		'tstamp' => array
		(
			'sql'                     => "int(10) unsigned NOT NULL default 0"
		),
		'media' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_image_size_item']['media'],
			'inputType'               => 'text',
			'exclude'                 => true,
			'eval'                    => array('maxlength'=>255, 'tl_class'=>'long', 'decodeEntities'=>true),
			'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"
		),
		'invisible' => array
		(
			'label'                   => &$GLOBALS['TL_LANG']['tl_image_size_item']['invisible'],
			'exclude'                 => true,
			'filter'                  => true,
			'inputType'               => 'checkbox',
			'sql'                     => "char(1) NOT NULL default ''"
		)
	)
);

/**
 * Provide miscellaneous methods that are used by the data configuration array.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class tl_image_size_item 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.');
		}
	}

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

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

		if ($row['zoom'])
		{
			$html .= ' (' . $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 "toggle visibility" button
	 *
	 * @param array  $row
	 * @param string $href
	 * @param string $label
	 * @param string $title
	 * @param string $icon
	 * @param string $attributes
	 *
	 * @return string
	 */
	public function toggleIcon($row, $href, $label, $title, $icon, $attributes)
	{
		if (\strlen(Contao\Input::get('tid')))
		{
			$this->toggleVisibility(Contao\Input::get('tid'), (Contao\Input::get('state') == 1), (@func_get_arg(12) ?: null));
			$this->redirect($this->getReferer());
		}

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

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

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

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

	/**
	 * Toggle the visibility of a format definition
	 *
	 * @param integer              $intId
	 * @param boolean              $blnVisible
	 * @param Contao\DataContainer $dc
	 */
	public function toggleVisibility($intId, $blnVisible, Contao\DataContainer $dc=null)
	{
		// Set the ID and action
		Contao\Input::setGet('id', $intId);
		Contao\Input::setGet('act', 'toggle');

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

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

		// Check the field access
		if (!$this->User->hasAccess('tl_image_size_item::invisible', 'alexf'))
		{
			throw new Contao\CoreBundle\Exception\AccessDeniedException('Not enough permissions to publish/unpublish image size item ID ' . $intId . '.');
		}

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

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

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

		// Reverse the logic (image sizes have invisible=1)
		$blnVisible = !$blnVisible;

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

		$time = time();

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

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

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

		$objVersions->create();
	}
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Menu;

use Contao\CoreBundle\Event\ContaoCoreEvents;
use Contao\CoreBundle\Event\MenuEvent;
use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class BackendMenuBuilder
{
    /**
     * @var FactoryInterface
     */
    private $factory;

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

    public function __construct(FactoryInterface $factory, EventDispatcherInterface $eventDispatcher)
    {
        $this->factory = $factory;
        $this->eventDispatcher = $eventDispatcher;
    }

    /**
     * Creates a new root node and dispatches an event to fill it with child nodes.
     */
    public function create(): ItemInterface
    {
        $tree = $this->factory->createItem('root');

        $this->eventDispatcher->dispatch(ContaoCoreEvents::BACKEND_MENU_BUILD, new MenuEvent($this->factory, $tree));

        return $tree;
    }
}


<?php

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

namespace Contao;

use Contao\CoreBundle\Exception\PageNotFoundException;
use Patchwork\Utf8;

/**
 * Front end module "news archive".
 *
 * @property int    $news_startDay
 * @property bool   $news_showQuantity
 * @property array  $news_archives
 * @property string $news_order
 * @property string $news_format
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ModuleNewsMenu extends ModuleNews
{

	/**
	 * Current date object
	 * @var Date
	 */
	protected $Date;

	/**
	 * Current URL
	 * @var string
	 */
	protected $strUrl;

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

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

		if (empty($this->news_archives) || !\is_array($this->news_archives))
		{
			return '';
		}

		$this->strUrl = preg_replace('/\?.*$/', '', Environment::get('request'));

		if (($objTarget = $this->objModel->getRelated('jumpTo')) instanceof PageModel)
		{
			/** @var PageModel $objTarget */
			$this->strUrl = $objTarget->getFrontendUrl();
		}

		return parent::generate();
	}

	/**
	 * Generate the module
	 */
	protected function compile()
	{
		switch ($this->news_format)
		{
			case 'news_year':
				$this->compileYearlyMenu();
				break;

			default:
			case 'news_month':
				$this->compileMonthlyMenu();
				break;

			case 'news_day':
				$this->compileDailyMenu();
				break;
		}

		$this->Template->empty = $GLOBALS['TL_LANG']['MSC']['emptyList'];
	}

	/**
	 * Generate the yearly menu
	 */
	protected function compileYearlyMenu()
	{
		$arrData = array();
		$time = Date::floorToMinute();

		// Get the dates
		$objDates = $this->Database->query("SELECT FROM_UNIXTIME(date, '%Y') AS year, COUNT(*) AS count FROM tl_news WHERE pid IN(" . implode(',', array_map('\intval', $this->news_archives)) . ")" . ((!BE_USER_LOGGED_IN || TL_MODE == 'BE') ? " AND (start='' OR start<='$time') AND (stop='' OR stop>'" . ($time + 60) . "') AND published='1'" : "") . " GROUP BY year ORDER BY year DESC");

		while ($objDates->next())
		{
			$arrData[$objDates->year] = $objDates->count;
		}

		// Sort the data
		($this->news_order == 'order_date_asc') ? ksort($arrData) : krsort($arrData);

		$arrItems = array();
		$count = 0;
		$limit = \count($arrData);

		// Prepare the navigation
		foreach ($arrData as $intYear=>$intCount)
		{
			$intDate = $intYear;
			$quantity = sprintf((($intCount < 2) ? $GLOBALS['TL_LANG']['MSC']['entry'] : $GLOBALS['TL_LANG']['MSC']['entries']), $intCount);

			$arrItems[$intYear]['date'] = $intDate;
			$arrItems[$intYear]['link'] = $intYear;
			$arrItems[$intYear]['href'] = $this->strUrl . '?year=' . $intDate;
			$arrItems[$intYear]['title'] = StringUtil::specialchars($intYear . ' (' . $quantity . ')');
			$arrItems[$intYear]['class'] = trim(((++$count == 1) ? 'first ' : '') . (($count == $limit) ? 'last' : ''));
			$arrItems[$intYear]['isActive'] = (Input::get('year') == $intDate);
			$arrItems[$intYear]['quantity'] = $quantity;
		}

		$this->Template->yearly = true;
		$this->Template->items = $arrItems;
		$this->Template->showQuantity = ($this->news_showQuantity != '');
	}

	/**
	 * Generate the monthly menu
	 */
	protected function compileMonthlyMenu()
	{
		$arrData = array();
		$time = Date::floorToMinute();

		// Get the dates
		$objDates = $this->Database->query("SELECT FROM_UNIXTIME(date, '%Y') AS year, FROM_UNIXTIME(date, '%m') AS month, COUNT(*) AS count FROM tl_news WHERE pid IN(" . implode(',', array_map('\intval', $this->news_archives)) . ")" . ((!BE_USER_LOGGED_IN || TL_MODE == 'BE') ? " AND (start='' OR start<='$time') AND (stop='' OR stop>'" . ($time + 60) . "') AND published='1'" : "") . " GROUP BY year, month ORDER BY year DESC, month DESC");

		while ($objDates->next())
		{
			$arrData[$objDates->year][$objDates->month] = $objDates->count;
		}

		// Sort the data
		foreach (array_keys($arrData) as $key)
		{
			($this->news_order == 'order_date_asc') ? ksort($arrData[$key]) : krsort($arrData[$key]);
		}

		($this->news_order == 'order_date_asc') ? ksort($arrData) : krsort($arrData);

		$arrItems = array();

		// Prepare the navigation
		foreach ($arrData as $intYear=>$arrMonth)
		{
			$count = 0;
			$limit = \count($arrMonth);

			foreach ($arrMonth as $intMonth=>$intCount)
			{
				$intDate = $intYear . $intMonth;
				$intMonth = (int) $intMonth - 1;

				$quantity = sprintf((($intCount < 2) ? $GLOBALS['TL_LANG']['MSC']['entry'] : $GLOBALS['TL_LANG']['MSC']['entries']), $intCount);

				$arrItems[$intYear][$intMonth]['date'] = $intDate;
				$arrItems[$intYear][$intMonth]['link'] = $GLOBALS['TL_LANG']['MONTHS'][$intMonth] . ' ' . $intYear;
				$arrItems[$intYear][$intMonth]['href'] = $this->strUrl . '?month=' . $intDate;
				$arrItems[$intYear][$intMonth]['title'] = StringUtil::specialchars($GLOBALS['TL_LANG']['MONTHS'][$intMonth].' '.$intYear . ' (' . $quantity . ')');
				$arrItems[$intYear][$intMonth]['class'] = trim(((++$count == 1) ? 'first ' : '') . (($count == $limit) ? 'last' : ''));
				$arrItems[$intYear][$intMonth]['isActive'] = (Input::get('month') == $intDate);
				$arrItems[$intYear][$intMonth]['quantity'] = $quantity;
			}
		}

		$this->Template->items = $arrItems;
		$this->Template->showQuantity = ($this->news_showQuantity != '') ? true : false;
		$this->Template->url = $this->strUrl . '?';
		$this->Template->activeYear = Input::get('year');
	}

	/**
	 * Generate the dayil menu
	 */
	protected function compileDailyMenu()
	{
		$arrData = array();
		$time = Date::floorToMinute();

		// Get the dates
		$objDates = $this->Database->query("SELECT FROM_UNIXTIME(date, '%Y%m%d') AS day, COUNT(*) AS count FROM tl_news WHERE pid IN(" . implode(',', array_map('\intval', $this->news_archives)) . ")" . ((!BE_USER_LOGGED_IN || TL_MODE == 'BE') ? " AND (start='' OR start<='$time') AND (stop='' OR stop>'" . ($time + 60) . "') AND published='1'" : "") . " GROUP BY day ORDER BY day DESC");

		while ($objDates->next())
		{
			$arrData[$objDates->day] = $objDates->count;
		}

		// Sort the data
		krsort($arrData);

		// Create the date object
		try
		{
			$this->Date = Input::get('day') ? new Date(Input::get('day'), 'Ymd') : new Date();
		}
		catch (\OutOfBoundsException $e)
		{
			throw new PageNotFoundException('Page not found: ' . Environment::get('uri'));
		}

		$intYear = date('Y', $this->Date->tstamp);
		$intMonth = date('m', $this->Date->tstamp);

		$this->Template->intYear = $intYear;
		$this->Template->intMonth = $intMonth;

		// Previous month
		$prevMonth = ($intMonth == 1) ? 12 : ($intMonth - 1);
		$prevYear = ($intMonth == 1) ? ($intYear - 1) : $intYear;
		$lblPrevious = $GLOBALS['TL_LANG']['MONTHS'][($prevMonth - 1)] . ' ' . $prevYear;

		$this->Template->prevHref = $this->strUrl . '?day=' . $prevYear . ((\strlen($prevMonth) < 2) ? '0' : '') . $prevMonth . '01';
		$this->Template->prevTitle = StringUtil::specialchars($lblPrevious);
		$this->Template->prevLink = $GLOBALS['TL_LANG']['MSC']['news_previous'] . ' ' . $lblPrevious;
		$this->Template->prevLabel = $GLOBALS['TL_LANG']['MSC']['news_previous'];

		// Current month
		$this->Template->current = $GLOBALS['TL_LANG']['MONTHS'][(date('m', $this->Date->tstamp) - 1)] .  ' ' . date('Y', $this->Date->tstamp);

		// Next month
		$nextMonth = ($intMonth == 12) ? 1 : ($intMonth + 1);
		$nextYear = ($intMonth == 12) ? ($intYear + 1) : $intYear;
		$lblNext = $GLOBALS['TL_LANG']['MONTHS'][($nextMonth - 1)] . ' ' . $nextYear;

		$this->Template->nextHref = $this->strUrl . '?day=' . $nextYear . ((\strlen($nextMonth) < 2) ? '0' : '') . $nextMonth . '01';
		$this->Template->nextTitle = StringUtil::specialchars($lblNext);
		$this->Template->nextLink = $lblNext . ' ' . $GLOBALS['TL_LANG']['MSC']['news_next'];
		$this->Template->nextLabel = $GLOBALS['TL_LANG']['MSC']['news_next'];

		// Set week start day
		if (!$this->news_startDay)
		{
			$this->news_startDay = 0;
		}

		$this->Template->daily = true;
		$this->Template->days = $this->compileDays();
		$this->Template->weeks = $this->compileWeeks($arrData);
		$this->Template->showQuantity = ($this->news_showQuantity != '') ? true : false;
	}

	/**
	 * Return the week days and labels as array
	 *
	 * @return array
	 */
	protected function compileDays()
	{
		$arrDays = array();

		for ($i=0; $i<7; $i++)
		{
			$intCurrentDay = ($i + $this->news_startDay) % 7;
			$arrDays[$intCurrentDay] = $GLOBALS['TL_LANG']['DAYS'][$intCurrentDay];
		}

		return array_values($arrDays);
	}

	/**
	 * Return all weeks of the current month as array
	 *
	 * @param array $arrData
	 *
	 * @return array
	 */
	protected function compileWeeks($arrData)
	{
		$intDaysInMonth = date('t', $this->Date->monthBegin);
		$intFirstDayOffset = date('w', $this->Date->monthBegin) - $this->news_startDay;

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

		$intColumnCount = -1;
		$intNumberOfRows = ceil(($intDaysInMonth + $intFirstDayOffset) / 7);
		$arrDays = array();

		// Compile days
		for ($i=1; $i<=($intNumberOfRows * 7); $i++)
		{
			$intWeek = floor(++$intColumnCount / 7);
			$intDay = $i - $intFirstDayOffset;
			$intCurrentDay = ($i + $this->news_startDay) % 7;

			$strWeekClass = 'week_' . $intWeek;
			$strWeekClass .= ($intWeek == 0) ? ' first' : '';
			$strWeekClass .= ($intWeek == ($intNumberOfRows - 1)) ? ' last' : '';

			$strClass = ($intCurrentDay < 2) ? ' weekend' : '';
			$strClass .= ($i == 1 || $i == 8 || $i == 15 || $i == 22 || $i == 29 || $i == 36) ? ' col_first' : '';
			$strClass .= ($i == 7 || $i == 14 || $i == 21 || $i == 28 || $i == 35 || $i == 42) ? ' col_last' : '';

			// Empty cell
			if ($intDay < 1 || $intDay > $intDaysInMonth)
			{
				$arrDays[$strWeekClass][$i]['label'] = ' ';
				$arrDays[$strWeekClass][$i]['class'] = 'days empty' . $strClass;
				$arrDays[$strWeekClass][$i]['events'] = array();

				continue;
			}

			$intKey = date('Ym', $this->Date->tstamp) . ((\strlen($intDay) < 2) ? '0' . $intDay : $intDay);
			$strClass .= ($intKey == date('Ymd')) ? ' today' : '';

			// Inactive days
			if (empty($intKey) || !isset($arrData[$intKey]))
			{
				$arrDays[$strWeekClass][$i]['label'] = $intDay;
				$arrDays[$strWeekClass][$i]['class'] = 'days' . $strClass;
				$arrDays[$strWeekClass][$i]['events'] = array();

				continue;
			}

			$arrDays[$strWeekClass][$i]['label'] = $intDay;
			$arrDays[$strWeekClass][$i]['class'] = 'days active' . $strClass;
			$arrDays[$strWeekClass][$i]['href'] = $this->strUrl . '?day=' . $intKey;
			$arrDays[$strWeekClass][$i]['title'] = sprintf(StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['news_items']), $arrData[$intKey]);
		}

		return $arrDays;
	}
}

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