<?php

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

namespace Contao;

/**
 * Provide methods to handle style sheets.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class StyleSheets extends Backend
{

	/**
	 * @var string
	 */
	protected $strRootDir;

	/**
	 * Import the Files library
	 */
	public function __construct()
	{
		parent::__construct();
		$this->import(Files::class, 'Files');
		$this->strRootDir = System::getContainer()->getParameter('kernel.project_dir');
	}

	/**
	 * Update a particular style sheet
	 *
	 * @param integer $intId
	 */
	public function updateStyleSheet($intId)
	{
		$objStyleSheet = $this->Database->prepare("SELECT * FROM tl_style_sheet WHERE id=?")
										->limit(1)
										->execute($intId);

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

		// Delete the CSS file
		if (Input::get('act') == 'delete')
		{
			$this->import(Files::class, 'Files');
			$this->Files->delete('assets/css/' . $objStyleSheet->name . '.css');
		}

		// Update the CSS file
		else
		{
			$this->writeStyleSheet($objStyleSheet->row());
			$this->log('Generated style sheet "' . $objStyleSheet->name . '.css"', __METHOD__, TL_CRON);
		}
	}

	/**
	 * Update all style sheets in the scripts folder
	 */
	public function updateStyleSheets()
	{
		$objStyleSheets = $this->Database->execute("SELECT * FROM tl_style_sheet");
		$arrStyleSheets = $objStyleSheets->fetchEach('name');

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

		// Delete old style sheets
		foreach (scan($this->strRootDir . '/assets/css', true) as $file)
		{
			// Skip directories
			if (is_dir($this->strRootDir . '/assets/css/' . $file))
			{
				continue;
			}

			// Preserve root files (is this still required now that scripts are in assets/css/scripts?)
			if (\is_array(Config::get('rootFiles')) && \in_array($file, Config::get('rootFiles')))
			{
				continue;
			}

			// Do not delete the combined files (see #3605)
			if (preg_match('/^[a-f0-9]{12}\.css$/', $file))
			{
				continue;
			}

			$objFile = new File('assets/css/' . $file);

			// Delete the old style sheet
			if ($objFile->extension == 'css' && !\in_array($objFile->filename, $arrStyleSheets))
			{
				$objFile->delete();
			}
		}

		$objStyleSheets->reset();

		// Create the new style sheets
		while ($objStyleSheets->next())
		{
			$this->writeStyleSheet($objStyleSheets->row());
			$this->log('Generated style sheet "' . $objStyleSheets->name . '.css"', __METHOD__, TL_CRON);
		}
	}

	/**
	 * Write a style sheet to a file
	 *
	 * @param array $row
	 */
	protected function writeStyleSheet($row)
	{
		if ($row['id'] == '' || $row['name'] == '')
		{
			return;
		}

		$row['name'] = basename($row['name']);

		// Check whether the target file is writeable
		if (file_exists($this->strRootDir . '/assets/css/' . $row['name'] . '.css') && !$this->Files->is_writeable('assets/css/' . $row['name'] . '.css'))
		{
			Message::addError(sprintf($GLOBALS['TL_LANG']['ERR']['notWriteable'], 'assets/css/' . $row['name'] . '.css'));

			return;
		}

		$vars = array();

		// Get the global theme variables
		$objTheme = $this->Database->prepare("SELECT vars FROM tl_theme WHERE id=?")
								   ->limit(1)
								   ->execute($row['pid']);

		if ($objTheme->vars != '')
		{
			if (\is_array(($tmp = StringUtil::deserialize($objTheme->vars))))
			{
				foreach ($tmp as $v)
				{
					$vars[$v['key']] = $v['value'];
				}
			}
		}

		// Merge the global style sheet variables
		if ($row['vars'] != '')
		{
			if (\is_array($tmp = StringUtil::deserialize($row['vars'])))
			{
				foreach ($tmp as $v)
				{
					$vars[$v['key']] = $v['value'];
				}
			}
		}

		// Sort by key length (see #3316)
		uksort($vars, 'length_sort_desc');

		// Create the file
		$objFile = new File('assets/css/' . $row['name'] . '.css');
		$objFile->write('/* ' . $row['name'] . ".css */\n");

		$objDefinitions = $this->Database->prepare("SELECT * FROM tl_style WHERE pid=? AND invisible!='1' ORDER BY sorting")
										 ->execute($row['id']);

		// Append the definition
		while ($objDefinitions->next())
		{
			$objFile->append($this->compileDefinition($objDefinitions->row(), true, $vars, $row), '');
		}

		$objFile->close();
	}

	/**
	 * Compile format definitions and return them as string
	 *
	 * @param array   $row
	 * @param boolean $blnWriteToFile
	 * @param array   $vars
	 * @param array   $parent
	 * @param boolean $export
	 *
	 * @return string
	 */
	public function compileDefinition($row, $blnWriteToFile=false, $vars=array(), $parent=array(), $export=false)
	{
		if ($blnWriteToFile)
		{
			$strGlue = '../../';
			$lb = '';
			$return = '';
		}
		elseif ($export)
		{
			$strGlue = '';
			$lb = "\n    ";
			$return = '';
		}
		else
		{
			$strGlue = '';
			$lb = "\n    ";
			$return = "\n" . '
';
		}

		// Comment
		if ((!$blnWriteToFile || $export) && $row['comment'] != '')
		{
			$search = array('@^\s*/\*+@', '@\*+/\s*$@');
			$comment = preg_replace($search, '', $row['comment']);

			if ($export)
			{
				$return .= "\n/* " . $comment . " */\n";
			}
			else
			{
				$comment = wordwrap(trim($comment), 72);
				$return .= "\n" . '' . $comment . '' . "\n";
			}
		}

		// Selector
		$arrSelector = StringUtil::trimsplit(',', StringUtil::decodeEntities($row['selector']));
		$return .= implode(($blnWriteToFile ? ',' : ",\n"), $arrSelector) . ($blnWriteToFile ? '' : ' ') . '{';

		// Size
		if ($row['size'])
		{
			// Width
			$row['width'] = StringUtil::deserialize($row['width']);

			if (isset($row['width']['value']) && $row['width']['value'] != '')
			{
				$return .= $lb . 'width:' . $row['width']['value'] . (($row['width']['value'] == 'auto') ? '' : $row['width']['unit']) . ';';
			}

			// Height
			$row['height'] = StringUtil::deserialize($row['height']);

			if (isset($row['height']['value']) && $row['height']['value'] != '')
			{
				$return .= $lb . 'height:' . $row['height']['value'] . (($row['height']['value'] == 'auto') ? '' : $row['height']['unit']) . ';';
			}

			// Min-width
			$row['minwidth'] = StringUtil::deserialize($row['minwidth']);

			if (isset($row['minwidth']['value']) && $row['minwidth']['value'] != '')
			{
				$return .= $lb . 'min-width:' . $row['minwidth']['value'] . (($row['minwidth']['value'] == 'inherit') ? '' : $row['minwidth']['unit']) . ';';
			}

			// Min-height
			$row['minheight'] = StringUtil::deserialize($row['minheight']);

			if (isset($row['minheight']['value']) && $row['minheight']['value'] != '')
			{
				$return .= $lb . 'min-height:' . $row['minheight']['value'] . (($row['minheight']['value'] == 'inherit') ? '' : $row['minheight']['unit']) . ';';
			}

			// Max-width
			$row['maxwidth'] = StringUtil::deserialize($row['maxwidth']);

			if (isset($row['maxwidth']['value']) && $row['maxwidth']['value'] != '')
			{
				$return .= $lb . 'max-width:' . $row['maxwidth']['value'] . (($row['maxwidth']['value'] == 'inherit' || $row['maxwidth']['value'] == 'none') ? '' : $row['maxwidth']['unit']) . ';';
			}

			// Max-height
			$row['maxheight'] = StringUtil::deserialize($row['maxheight']);

			if (isset($row['maxheight']['value']) && $row['maxheight']['value'] != '')
			{
				$return .= $lb . 'max-height:' . $row['maxheight']['value'] . (($row['maxheight']['value'] == 'inherit' || $row['maxheight']['value'] == 'none') ? '' : $row['maxheight']['unit']) . ';';
			}
		}

		// Position
		if ($row['positioning'])
		{
			// Top/right/bottom/left
			$row['trbl'] = StringUtil::deserialize($row['trbl']);

			if (\is_array($row['trbl']))
			{
				foreach ($row['trbl'] as $k=>$v)
				{
					if ($v != '' && $k != 'unit')
					{
						$return .= $lb . $k . ':' . $v . (($v == 'auto' || $v === '0') ? '' : $row['trbl']['unit']) . ';';
					}
				}
			}

			// Position
			if ($row['position'] != '')
			{
				$return .= $lb . 'position:' . $row['position'] . ';';
			}

			// Overflow
			if ($row['overflow'] != '')
			{
				$return .= $lb . 'overflow:' . $row['overflow'] . ';';
			}

			// Float
			if ($row['floating'] != '')
			{
				$return .= $lb . 'float:' . $row['floating'] . ';';
			}

			// Clear
			if ($row['clear'] != '')
			{
				$return .= $lb . 'clear:' . $row['clear'] . ';';
			}

			// Display
			if ($row['display'] != '')
			{
				$return .= $lb . 'display:' . $row['display'] . ';';
			}
		}

		// Margin, padding and alignment
		if ($row['alignment'])
		{
			// Margin
			if ($row['margin'] != '' || $row['align'] != '')
			{
				$row['margin'] = StringUtil::deserialize($row['margin']);

				if (\is_array($row['margin']))
				{
					$top = $row['margin']['top'];
					$right = $row['margin']['right'];
					$bottom = $row['margin']['bottom'];
					$left = $row['margin']['left'];

					// Overwrite the left and right margin if an alignment is set
					if ($row['align'] != '')
					{
						if ($row['align'] == 'left' || $row['align'] == 'center')
						{
							$right = 'auto';
						}

						if ($row['align'] == 'right' || $row['align'] == 'center')
						{
							$left = 'auto';
						}
					}

					// Try to shorten the definition
					if ($top != '' && $right != '' && $bottom != '' && $left != '')
					{
						if ($top == $right && $top == $bottom && $top == $left)
						{
							$return .= $lb . 'margin:' . $top . (($top == 'auto' || $top === '0') ? '' : $row['margin']['unit']) . ';';
						}
						elseif ($top == $bottom && $right == $left)
						{
							$return .= $lb . 'margin:' . $top . (($top == 'auto' || $top === '0') ? '' : $row['margin']['unit']) . ' ' . $right . (($right == 'auto' || $right === '0') ? '' : $row['margin']['unit']) . ';';
						}
						elseif ($top != $bottom && $right == $left)
						{
							$return .= $lb . 'margin:' . $top . (($top == 'auto' || $top === '0') ? '' : $row['margin']['unit']) . ' ' . $right . (($right == 'auto' || $right === '0') ? '' : $row['margin']['unit']) . ' ' . $bottom . (($bottom == 'auto' || $bottom === '0') ? '' : $row['margin']['unit']) . ';';
						}
						else
						{
							$return .= $lb . 'margin:' . $top . (($top == 'auto' || $top === '0') ? '' : $row['margin']['unit']) . ' ' . $right . (($right == 'auto' || $right === '0') ? '' : $row['margin']['unit']) . ' ' . $bottom . (($bottom == 'auto' || $bottom === '0') ? '' : $row['margin']['unit']) . ' ' . $left . (($left == 'auto' || $left === '0') ? '' : $row['margin']['unit']) . ';';
						}
					}
					else
					{
						$arrDir = compact('top', 'right', 'bottom', 'left');

						foreach ($arrDir as $k=>$v)
						{
							if ($v != '')
							{
								$return .= $lb . 'margin-' . $k . ':' . $v . (($v == 'auto' || $v === '0') ? '' : $row['margin']['unit']) . ';';
							}
						}
					}
				}
			}

			// Padding
			if ($row['padding'] != '')
			{
				$row['padding'] = StringUtil::deserialize($row['padding']);

				if (\is_array($row['padding']))
				{
					$top = $row['padding']['top'];
					$right = $row['padding']['right'];
					$bottom = $row['padding']['bottom'];
					$left = $row['padding']['left'];

					// Try to shorten the definition
					if ($top != '' && $right != '' && $bottom != '' && $left != '')
					{
						if ($top == $right && $top == $bottom && $top == $left)
						{
							$return .= $lb . 'padding:' . $top . (($top === '0') ? '' : $row['padding']['unit']) . ';';
						}
						elseif ($top == $bottom && $right == $left)
						{
							$return .= $lb . 'padding:' . $top . (($top === '0') ? '' : $row['padding']['unit']) . ' ' . $right . (($right === '0') ? '' : $row['padding']['unit']) . ';';
						}
						elseif ($top != $bottom && $right == $left)
						{
							$return .= $lb . 'padding:' . $top . (($top === '0') ? '' : $row['padding']['unit']) . ' ' . $right . (($right === '0') ? '' : $row['padding']['unit']) . ' ' . $bottom . (($bottom === '0') ? '' : $row['padding']['unit']) . ';';
						}
						else
						{
							$return .= $lb . 'padding:' . $top . (($top === '0') ? '' : $row['padding']['unit']) . ' ' . $right . (($right === '0') ? '' : $row['padding']['unit']) . ' ' . $bottom . (($bottom === '0') ? '' : $row['padding']['unit']) . ' ' . $left . (($left === '0') ? '' : $row['padding']['unit']) . ';';
						}
					}
					else
					{
						$arrDir = compact('top', 'right', 'bottom', 'left');

						foreach ($arrDir as $k=>$v)
						{
							if ($v != '')
							{
								$return .= $lb . 'padding-' . $k . ':' . $v . (($v === '0') ? '' : $row['padding']['unit']) . ';';
							}
						}
					}
				}
			}

			// Vertical alignment
			if ($row['verticalalign'] != '')
			{
				$return .= $lb . 'vertical-align:' . $row['verticalalign'] . ';';
			}

			// Text alignment
			if ($row['textalign'] != '')
			{
				$return .= $lb . 'text-align:' . $row['textalign'] . ';';
			}

			// White space
			if ($row['whitespace'] != '')
			{
				$return .= $lb . 'white-space:' . $row['whitespace'] . ';';
			}
		}

		// Background
		if ($row['background'])
		{
			$bgColor = StringUtil::deserialize($row['bgcolor'], true);

			// Try to shorten the definition
			if ($bgColor[0] != '' && $row['bgimage'] != '' && $row['bgposition'] != '' && $row['bgrepeat'] != '')
			{
				if (($strImage = $this->generateBase64Image($row['bgimage'], $parent)) !== false)
				{
					$return .= $lb . 'background:' . $this->compileColor($bgColor, $blnWriteToFile, $vars) . ' url("' . $strImage . '") ' . $row['bgposition'] . ' ' . $row['bgrepeat'] . ';';
				}
				else
				{
					$glue = (strncmp($row['bgimage'], 'data:', 5) !== 0 && strncmp($row['bgimage'], 'http://', 7) !== 0 && strncmp($row['bgimage'], 'https://', 8) !== 0 && strncmp($row['bgimage'], '/', 1) !== 0) ? $strGlue : '';
					$return .= $lb . 'background:' . $this->compileColor($bgColor, $blnWriteToFile, $vars) . ' url("' . $glue . $row['bgimage'] . '") ' . $row['bgposition'] . ' ' . $row['bgrepeat'] . ';';
				}
			}
			else
			{
				// Background color
				if ($bgColor[0] != '')
				{
					$return .= $lb . 'background-color:' . $this->compileColor($bgColor, $blnWriteToFile, $vars) . ';';
				}

				// Background image
				if ($row['bgimage'] == 'none')
				{
					$return .= $lb . 'background-image:none;';
				}
				elseif ($row['bgimage'] != '')
				{
					if (($strImage = $this->generateBase64Image($row['bgimage'], $parent)) !== false)
					{
						$return .= $lb . 'background-image:url("' . $strImage . '");';
					}
					else
					{
						$glue = (strncmp($row['bgimage'], 'data:', 5) !== 0 && strncmp($row['bgimage'], 'http://', 7) !== 0 && strncmp($row['bgimage'], 'https://', 8) !== 0 && strncmp($row['bgimage'], '/', 1) !== 0) ? $strGlue : '';
						$return .= $lb . 'background-image:url("' . $glue . $row['bgimage'] . '");';
					}
				}

				// Background position
				if ($row['bgposition'] != '')
				{
					$return .= $lb . 'background-position:' .$row['bgposition']. ';';
				}

				// Background repeat
				if ($row['bgrepeat'] != '')
				{
					$return .= $lb . 'background-repeat:' .$row['bgrepeat']. ';';
				}
			}

			// Background gradient
			if ($row['gradientAngle'] != '' && $row['gradientColors'] != '')
			{
				$row['gradientColors'] = StringUtil::deserialize($row['gradientColors']);

				if (\is_array($row['gradientColors']) && \count(array_filter($row['gradientColors'])) > 0)
				{
					$bgImage = '';

					// CSS3 PIE only supports -pie-background, so if there is a background image, include it here, too.
					if ($row['bgimage'] != '' && $row['bgposition'] != '' && $row['bgrepeat'] != '')
					{
						$glue = (strncmp($row['bgimage'], 'data:', 5) !== 0 && strncmp($row['bgimage'], 'http://', 7) !== 0 && strncmp($row['bgimage'], 'https://', 8) !== 0 && strncmp($row['bgimage'], '/', 1) !== 0) ? $strGlue : '';
						$bgImage = 'url("' . $glue . $row['bgimage'] . '") ' . $row['bgposition'] . ' ' . $row['bgrepeat'] . ',';
					}

					// Default starting point
					if ($row['gradientAngle'] == '')
					{
						$row['gradientAngle'] = 'to top';
					}

					$row['gradientColors'] = array_values(array_filter($row['gradientColors']));

					// Add a hash tag to the color values
					foreach ($row['gradientColors'] as $k=>$v)
					{
						$row['gradientColors'][$k] = '#' . $v;
					}

					$angle = '';

					// Convert the angle for the legacy commands (see #4569)
					if (strpos($row['gradientAngle'], 'deg') !== false)
					{
						$angle = (abs((int) $row['gradientAngle'] - 450) % 360) . 'deg';
					}
					else
					{
						switch ($row['gradientAngle'])
						{
							case 'to top':          $angle = 'bottom';       break;
							case 'to right':        $angle = 'left';         break;
							case 'to bottom':       $angle = 'top';          break;
							case 'to left':         $angle = 'right';        break;
							case 'to top left':     $angle = 'bottom right'; break;
							case 'to top right':    $angle = 'bottom left';  break;
							case 'to bottom left':  $angle = 'top right';    break;
							case 'to bottom right': $angle = 'top left';     break;
						}
					}

					$colors = implode(',', $row['gradientColors']);

					$legacy = $angle . ',' . $colors;
					$gradient = $row['gradientAngle'] . ',' . $colors;

					$return .= $lb . 'background:' . $bgImage . '-moz-linear-gradient(' . $legacy . ');';
					$return .= $lb . 'background:' . $bgImage . '-webkit-linear-gradient(' . $legacy . ');';
					$return .= $lb . 'background:' . $bgImage . '-o-linear-gradient(' . $legacy . ');';
					$return .= $lb . 'background:' . $bgImage . '-ms-linear-gradient(' . $legacy . ');';
					$return .= $lb . 'background:' . $bgImage . 'linear-gradient(' . $gradient . ');';
					$return .= $lb . '-pie-background:' . $bgImage . 'linear-gradient(' . $legacy . ');';
				}
			}

			// Box shadow
			if ($row['shadowsize'] != '')
			{
				$shColor = StringUtil::deserialize($row['shadowcolor'], true);
				$row['shadowsize'] = StringUtil::deserialize($row['shadowsize']);

				if (\is_array($row['shadowsize']) && $row['shadowsize']['top'] != '' && $row['shadowsize']['right'] != '')
				{
					$offsetx = $row['shadowsize']['top'];
					$offsety = $row['shadowsize']['right'];
					$blursize = $row['shadowsize']['bottom'];
					$radius = $row['shadowsize']['left'];

					$shadow = $offsetx . (($offsetx === '0') ? '' : $row['shadowsize']['unit']);
					$shadow .= ' ' . $offsety . (($offsety === '0') ? '' : $row['shadowsize']['unit']);
					if ($blursize != '')
					{
						$shadow .= ' ' . $blursize . (($blursize === '0') ? '' : $row['shadowsize']['unit']);
					}
					if ($radius != '')
					{
						$shadow .= ' ' . $radius . (($radius === '0') ? '' : $row['shadowsize']['unit']);
					}
					if ($shColor[0] != '')
					{
						$shadow .= ' ' . $this->compileColor($shColor, $blnWriteToFile, $vars);
					}
					$shadow .= ';';

					// Prefix required in Safari <= 5 and Android
					$return .= $lb . '-webkit-box-shadow:' . $shadow;
					$return .= $lb . 'box-shadow:' . $shadow;
				}
			}
		}

		// Border
		if ($row['border'])
		{
			$bdColor = StringUtil::deserialize($row['bordercolor'], true);
			$row['borderwidth'] = StringUtil::deserialize($row['borderwidth']);

			// Border width
			if (\is_array($row['borderwidth']))
			{
				$top = $row['borderwidth']['top'];
				$right = $row['borderwidth']['right'];
				$bottom = $row['borderwidth']['bottom'];
				$left = $row['borderwidth']['left'];

				// Try to shorten the definition
				if ($top != '' && $right != '' && $bottom != '' && $left != '' && $top == $right && $top == $bottom && $top == $left)
				{
					$return .= $lb . 'border:' . $top . $row['borderwidth']['unit'] . (($row['borderstyle'] != '') ? ' ' .$row['borderstyle'] : '') . (($bdColor[0] != '') ? ' ' . $this->compileColor($bdColor, $blnWriteToFile, $vars) : '') . ';';
				}
				elseif ($top != '' && $right != '' && $bottom != '' && $left != '' && $top == $bottom && $left == $right)
				{
					$return .= $lb . 'border-width:' . $top . $row['borderwidth']['unit'] . ' ' . $right . $row['borderwidth']['unit'] . ';';

					if ($row['borderstyle'] != '')
					{
						$return .= $lb . 'border-style:' . $row['borderstyle'] . ';';
					}

					if ($bdColor[0] != '')
					{
						$return .= $lb . 'border-color:' . $this->compileColor($bdColor, $blnWriteToFile, $vars) . ';';
					}
				}
				elseif ($top == '' && $right == '' && $bottom == '' && $left == '')
				{
					if ($row['borderstyle'] != '')
					{
						$return .= $lb . 'border-style:' . $row['borderstyle'] . ';';
					}

					if ($bdColor[0] != '')
					{
						$return .= $lb . 'border-color:' . $this->compileColor($bdColor, $blnWriteToFile, $vars) . ';';
					}
				}
				else
				{
					$arrDir = compact('top', 'right', 'bottom', 'left');

					foreach ($arrDir as $k=>$v)
					{
						if ($v != '')
						{
							$return .= $lb . 'border-' . $k . ':' . $v . $row['borderwidth']['unit'] . (($row['borderstyle'] != '') ? ' ' . $row['borderstyle'] : '') . (($bdColor[0] != '') ? ' ' . $this->compileColor($bdColor, $blnWriteToFile, $vars) : '') . ';';
						}
					}
				}
			}
			else
			{
				if ($row['borderstyle'] != '')
				{
					$return .= $lb . 'border-style:' . $row['borderstyle'] . ';';
				}

				if ($bdColor[0] != '')
				{
					$return .= $lb . 'border-color:' . $this->compileColor($bdColor, $blnWriteToFile, $vars) . ';';
				}
			}

			// Border radius
			if ($row['borderradius'] != '')
			{
				$row['borderradius'] = StringUtil::deserialize($row['borderradius']);

				if (\is_array($row['borderradius']) && ($row['borderradius']['top'] != '' || $row['borderradius']['right'] != '' || $row['borderradius']['bottom'] != '' || $row['borderradius']['left'] != ''))
				{
					$top = $row['borderradius']['top'];
					$right = $row['borderradius']['right'];
					$bottom = $row['borderradius']['bottom'];
					$left = $row['borderradius']['left'];
					$borderradius = '';

					// Try to shorten the definition
					if ($top != '' && $right != '' && $bottom != '' && $left != '')
					{
						if ($top == $right && $top == $bottom && $top == $left)
						{
							$borderradius = $top . (($top === '0') ? '' : $row['borderradius']['unit']) . ';';
						}
						elseif ($top == $bottom && $right == $left)
						{
							$borderradius = $top . (($top === '0') ? '' : $row['borderradius']['unit']) . ' ' . $right . (($right === '0') ? '' : $row['borderradius']['unit']) . ';';
						}
						elseif ($top != $bottom && $right == $left)
						{
							$borderradius = $top . (($top === '0') ? '' : $row['borderradius']['unit']) . ' ' . $right . (($right === '0') ? '' : $row['borderradius']['unit']) . ' ' . $bottom . (($bottom === '0') ? '' : $row['borderradius']['unit']) . ';';
						}
						else
						{
							$borderradius .= $top . (($top === '0') ? '' : $row['borderradius']['unit']) . ' ' . $right . (($right === '0') ? '' : $row['borderradius']['unit']) . ' ' . $bottom . (($bottom === '0') ? '' : $row['borderradius']['unit']) . ' ' . $left . (($left === '0') ? '' : $row['borderradius']['unit']) . ';';
						}

						$return .= $lb . 'border-radius:' . $borderradius;
					}
					else
					{
						$arrDir = array('top-left'=>$top, 'top-right'=>$right, 'bottom-right'=>$bottom, 'bottom-left'=>$left);

						foreach ($arrDir as $k=>$v)
						{
							if ($v != '')
							{
								$return .= $lb . 'border-' . $k . '-radius:' . $v . (($v === '0') ? '' : $row['borderradius']['unit']) . ';';
							}
						}
					}
				}
			}

			// Border collapse
			if ($row['bordercollapse'] != '')
			{
				$return .= $lb . 'border-collapse:' . $row['bordercollapse'] . ';';
			}

			// Border spacing
			$row['borderspacing'] = StringUtil::deserialize($row['borderspacing']);

			if (isset($row['borderspacing']['value']) && $row['borderspacing']['value'] != '')
			{
				$return .= $lb . 'border-spacing:' . $row['borderspacing']['value'] . $row['borderspacing']['unit'] . ';';
			}
		}

		// Font
		if ($row['font'])
		{
			$row['fontsize'] = StringUtil::deserialize($row['fontsize']);
			$row['lineheight'] = StringUtil::deserialize($row['lineheight']);
			$row['fontfamily'] = str_replace(', ', ',', $row['fontfamily']);

			// Try to shorten the definition
			if ($row['fontfamily'] != '' && $row['fontfamily'] != 'inherit' && isset($row['fontsize']['value']) && $row['fontsize']['value'] != '' && $row['fontsize']['value'] != 'inherit')
			{
				$return .= $lb . 'font:' . $row['fontsize']['value'] . $row['fontsize']['unit'] . ((isset($row['lineheight']['value']) && $row['lineheight']['value'] != '') ? '/' . $row['lineheight']['value'] . $row['lineheight']['unit'] : '') . ' ' . $row['fontfamily'] . ';';
			}
			else
			{
				// Font family
				if ($row['fontfamily'] != '')
				{
					$return .= $lb . 'font-family:' . $row['fontfamily'] . ';';
				}

				// Font size
				if (isset($row['fontsize']['value']) && $row['fontsize']['value'] != '')
				{
					$return .= $lb . 'font-size:' . $row['fontsize']['value'] . $row['fontsize']['unit'] . ';';
				}

				// Line height
				if (isset($row['lineheight']['value']) && $row['lineheight']['value'] != '')
				{
					$return .= $lb . 'line-height:' . $row['lineheight']['value'] . $row['lineheight']['unit'] . ';';
				}
			}

			// Font style
			$row['fontstyle'] = StringUtil::deserialize($row['fontstyle']);

			if (\is_array($row['fontstyle']))
			{
				if (\in_array('bold', $row['fontstyle']))
				{
					$return .= $lb . 'font-weight:bold;';
				}

				if (\in_array('italic', $row['fontstyle']))
				{
					$return .= $lb . 'font-style:italic;';
				}

				if (\in_array('normal', $row['fontstyle']))
				{
					$return .= $lb . 'font-weight:normal;';
				}

				if (\in_array('underline', $row['fontstyle']))
				{
					$return .= $lb . 'text-decoration:underline;';
				}

				if (\in_array('line-through', $row['fontstyle']))
				{
					$return .= $lb . 'text-decoration:line-through;';
				}

				if (\in_array('overline', $row['fontstyle']))
				{
					$return .= $lb. 'text-decoration:overline;';
				}

				if (\in_array('notUnderlined', $row['fontstyle']))
				{
					$return .= $lb . 'text-decoration:none;';
				}

				if (\in_array('small-caps', $row['fontstyle']))
				{
					$return .= $lb . 'font-variant:small-caps;';
				}
			}

			$fnColor = StringUtil::deserialize($row['fontcolor'], true);

			// Font color
			if ($fnColor[0] != '')
			{
				$return .= $lb . 'color:' . $this->compileColor($fnColor, $blnWriteToFile, $vars) . ';';
			}

			// Text transform
			if ($row['texttransform'] != '')
			{
				$return .= $lb . 'text-transform:' . $row['texttransform'] . ';';
			}

			// Text indent
			$row['textindent'] = StringUtil::deserialize($row['textindent']);

			if (isset($row['textindent']['value']) && $row['textindent']['value'] != '')
			{
				$return .= $lb . 'text-indent:' . $row['textindent']['value'] . $row['textindent']['unit'] . ';';
			}

			// Letter spacing
			$row['letterspacing'] = StringUtil::deserialize($row['letterspacing']);

			if (isset($row['letterspacing']['value']) && $row['letterspacing']['value'] != '')
			{
				$return .= $lb . 'letter-spacing:' . $row['letterspacing']['value'] . $row['letterspacing']['unit'] . ';';
			}

			// Word spacing
			$row['wordspacing'] = StringUtil::deserialize($row['wordspacing']);

			if (isset($row['wordspacing']['value']) && $row['wordspacing']['value'] != '')
			{
				$return .= $lb . 'word-spacing:' . $row['wordspacing']['value'] . $row['wordspacing']['unit'] . ';';
			}
		}

		// List
		if ($row['list'])
		{
			// List bullet
			if ($row['liststyletype'] != '')
			{
				$return .= $lb . 'list-style-type:' . $row['liststyletype'] . ';';
			}

			// List image
			if ($row['liststyleimage'] == 'none')
			{
				$return .= $lb . 'list-style-image:none;';
			}
			elseif ($row['liststyleimage'] != '')
			{
				if (($strImage = $this->generateBase64Image($row['liststyleimage'], $parent)) !== false)
				{
					$return .= $lb . 'list-style-image:url("' . $strImage . '");';
				}
				else
				{
					$glue = (strncmp($row['liststyleimage'], 'data:', 5) !== 0 && strncmp($row['liststyleimage'], 'http://', 7) !== 0 && strncmp($row['liststyleimage'], 'https://', 8) !== 0 && strncmp($row['liststyleimage'], '/', 1) !== 0) ? $strGlue : '';
					$return .= $lb . 'list-style-image:url("' . $glue . $row['liststyleimage'] . '");';
				}
			}
		}

		// Optimize floating-point numbers (see #6634)
		$return = preg_replace('/([^0-9.+-])0\.([0-9]+)/', '$1.$2', $return);

		// Custom code
		if ($row['own'] != '')
		{
			$own = trim(StringUtil::decodeEntities($row['own']));
			$own = preg_replace('/url\("(?!data:|\/)/', 'url("' . $strGlue, $own);
			$own = preg_split('/[\n\r]+/', $own);
			$own = implode(($blnWriteToFile ? '' : $lb), $own);
			$return .= $lb . ((!$blnWriteToFile && !$export) ? StringUtil::specialchars($own) : $own);
		}

		// Allow custom definitions
		if (isset($GLOBALS['TL_HOOKS']['compileDefinition']) && \is_array($GLOBALS['TL_HOOKS']['compileDefinition']))
		{
			foreach ($GLOBALS['TL_HOOKS']['compileDefinition'] as $callback)
			{
				$this->import($callback[0]);
				$strTemp = $this->{$callback[0]}->{$callback[1]}($row, $blnWriteToFile, $vars, $parent);

				if ($strTemp != '')
				{
					$return .= $lb . $strTemp;
				}
			}
		}

		// Close the format definition
		if ($blnWriteToFile)
		{
			// Remove the last semi-colon (;) before the closing bracket
			if (substr($return, -1) == ';')
			{
				$return = substr($return, 0, -1);
			}

			$return .= '}';
		}
		elseif ($export)
		{
			$return .= "\n}\n";
		}
		else
		{
			$return .= "\n}
\n";
		}

		// Replace global variables
		if (strpos($return, '$') !== false && !empty($vars))
		{
			$return = str_replace(array_keys($vars), $vars, $return);
		}

		// Replace insert tags (see #5512)
		return $this->replaceInsertTags($return, false);
	}

	/**
	 * Compile a color value and return a hex or rgba color
	 *
	 * @param mixed   $color
	 * @param boolean $blnWriteToFile
	 * @param array   $vars
	 *
	 * @return string
	 */
	protected function compileColor($color, $blnWriteToFile=false, $vars=array())
	{
		if (!\is_array($color))
		{
			return '#' . $this->shortenHexColor($color);
		}
		elseif (!isset($color[1]) || empty($color[1]))
		{
			return '#' . $this->shortenHexColor($color[0]);
		}
		else
		{
			return 'rgba(' . implode(',', $this->convertHexColor($color[0], $blnWriteToFile, $vars)) . ','. ($color[1] / 100) .')';
		}
	}

	/**
	 * Try to shorten a hex color
	 *
	 * @param string $color
	 *
	 * @return string
	 */
	protected function shortenHexColor($color)
	{
		if ($color[0] == $color[1] && $color[2] == $color[3] && $color[4] == $color[5])
		{
			return $color[0] . $color[2] . $color[4];
		}

		return $color;
	}

	/**
	 * Convert hex colors to rgb
	 *
	 * @param string  $color
	 * @param boolean $blnWriteToFile
	 * @param array   $vars
	 *
	 * @return array
	 *
	 * @see http://de3.php.net/manual/de/function.hexdec.php#99478
	 */
	protected function convertHexColor($color, $blnWriteToFile=false, $vars=array())
	{
		// Support global variables
		if (strncmp($color, '$', 1) === 0)
		{
			if (!$blnWriteToFile)
			{
				return array($color);
			}
			else
			{
				$color = str_replace(array_keys($vars), $vars, $color);
			}
		}

		$rgb = array();

		// Try to convert using bitwise operation
		if (\strlen($color) == 6)
		{
			$dec = hexdec($color);
			$rgb['red'] = 0xFF & ($dec >> 0x10);
			$rgb['green'] = 0xFF & ($dec >> 0x8);
			$rgb['blue'] = 0xFF & $dec;
		}

		// Shorthand notation
		elseif (\strlen($color) == 3)
		{
			$rgb['red'] = hexdec(str_repeat(substr($color, 0, 1), 2));
			$rgb['green'] = hexdec(str_repeat(substr($color, 1, 1), 2));
			$rgb['blue'] = hexdec(str_repeat(substr($color, 2, 1), 2));
		}

		return $rgb;
	}

	/**
	 * Return a form to choose an existing style sheet and import it
	 *
	 * @return string
	 *
	 * @throws \Exception
	 */
	public function importStyleSheet()
	{
		if (Input::get('key') != 'import')
		{
			return '';
		}

		/** @var FileUpload $objUploader */
		$objUploader = new FileUpload();

		// Import CSS
		if (Input::post('FORM_SUBMIT') == 'tl_style_sheet_import')
		{
			$arrUploaded = $objUploader->uploadTo('system/tmp');

			if (empty($arrUploaded))
			{
				Message::addError($GLOBALS['TL_LANG']['ERR']['all_fields']);
				$this->reload();
			}

			foreach ($arrUploaded as $strCssFile)
			{
				// Folders cannot be imported
				if (is_dir($this->strRootDir . '/' . $strCssFile))
				{
					Message::addError(sprintf($GLOBALS['TL_LANG']['ERR']['importFolder'], basename($strCssFile)));
					continue;
				}

				$objFile = new File($strCssFile);

				// Check the file extension
				if ($objFile->extension != 'css')
				{
					Message::addError(sprintf($GLOBALS['TL_LANG']['ERR']['filetype'], $objFile->extension));
					continue;
				}

				// Check the file name
				$strName = preg_replace('/\.css$/i', '', basename($strCssFile));
				$strName = $this->checkStyleSheetName($strName);

				// Create the new style sheet
				$objStyleSheet = $this->Database->prepare("INSERT INTO tl_style_sheet (pid, tstamp, name, media) VALUES (?, ?, ?, ?)")
												->execute(Input::get('id'), time(), $strName, array('all'));

				$insertId = $objStyleSheet->insertId;

				if (!is_numeric($insertId) || $insertId < 0)
				{
					throw new \Exception('Invalid insert ID');
				}

				// Read the file and remove carriage returns
				$strFile = $objFile->getContent();
				$strFile = str_replace("\r", '', $strFile);

				$arrTokens   = array();
				$strBuffer   = '';
				$intSorting  = 0;
				$strComment  = '';
				$strCategory = '';
				$intLength   = \strlen($strFile);

				// Tokenize
				for ($i=0; $i<$intLength; $i++)
				{
					$char = $strFile[$i];

					// Whitespace
					if ($char == '' || $char == "\n" || $char == "\t")
					{
						// Ignore
					}

					// Comment
					elseif ($char == '/')
					{
						if ($strFile[$i+1] == '*')
						{
							while ($i<$intLength)
							{
								$strBuffer .= $strFile[$i++];

								if ($strFile[$i] == '/' && $strFile[$i-1] == '*')
								{
									$arrTokens[] = array
									(
										'type'    => 'comment',
										'content' => $strBuffer . $strFile[$i]
									);

									$strBuffer = '';
									break;
								}
							}
						}
					}

					// At block
					elseif ($char == '@')
					{
						$intLevel = 0;
						$strSelector = '';

						while ($i<$intLength)
						{
							$strBuffer .= $strFile[$i++];

							if ($strFile[$i] == '{')
							{
								if (++$intLevel == 1)
								{
									++$i;
									$strSelector = $strBuffer;
									$strBuffer = '';
								}
							}
							elseif ($strFile[$i] == '}')
							{
								if (--$intLevel == 0)
								{
									$arrTokens[] = array
									(
										'type'     => 'atblock',
										'selector' => $strSelector,
										'content'  => $strBuffer
									);

									$strBuffer = '';
									break;
								}
							}
						}
					}

					// Regular block
					else
					{
						$strSelector = '';

						while ($i<$intLength)
						{
							$strBuffer .= $strFile[$i++];

							if ($strFile[$i] == '{')
							{
								++$i;
								$strSelector = $strBuffer;
								$strBuffer = '';
							}
							elseif ($strFile[$i] == '}')
							{
								$arrTokens[] = array
								(
									'type'     => 'block',
									'selector' => $strSelector,
									'content'  => $strBuffer
								);

								$strBuffer = '';
								break;
							}
						}
					}
				}

				foreach ($arrTokens as $arrToken)
				{
					// Comments
					if ($arrToken['type'] == 'comment')
					{
						// Category (comments start with /** and contain only one line)
						if (strncmp($arrToken['content'], '/**', 3) === 0 && substr_count($arrToken['content'], "\n") == 2)
						{
							$strCategory = trim(str_replace(array('/*', '*/', '*'), '', $arrToken['content']));
						}

						// Declaration comment
						elseif (strpos($arrToken['content'], "\n") === false)
						{
							$strComment = trim(str_replace(array('/*', '*/', '*'), '', $arrToken['content']));
						}
					}

					// At blocks like @media or @-webkit-keyframe
					elseif ($arrToken['type'] == 'atblock')
					{
						$arrSet = array
						(
							'pid'        => $insertId,
							'category'   => $strCategory,
							'comment'    => $strComment,
							'sorting'    => $intSorting += 128,
							'selector'   => trim($arrToken['selector']),
							'own'        => $arrToken['content']
						);

						$this->Database->prepare("INSERT INTO tl_style %s")->set($arrSet)->execute();
						$strComment = '';
					}

					// Regular blocks
					else
					{
						$arrDefinition = array
						(
							'pid'        => $insertId,
							'category'   => $strCategory,
							'comment'    => $strComment,
							'sorting'    => $intSorting += 128,
							'selector'   => trim($arrToken['selector']),
							'attributes' => $arrToken['content']
						);

						$this->createDefinition($arrDefinition);
						$strComment = '';
					}
				}

				// Write the style sheet
				$this->updateStyleSheet($insertId);

				// Notify the user
				if ($strName . '.css' != basename($strCssFile))
				{
					Message::addInfo(sprintf($GLOBALS['TL_LANG']['tl_style_sheet']['css_renamed'], basename($strCssFile), $strName . '.css'));
				}
				else
				{
					Message::addConfirmation(sprintf($GLOBALS['TL_LANG']['tl_style_sheet']['css_imported'], $strName . '.css'));
				}
			}

			// Redirect
			System::setCookie('BE_PAGE_OFFSET', 0, 0);
			$this->redirect(str_replace('&key=import', '', Environment::get('request')));
		}

		// Return form
		return Message::generate() . '

' .$GLOBALS['TL_LANG']['MSC']['backBT']. '

<form action="' .ampersand(Environment::get('request'), true). '" id="tl_style_sheet_import" class="tl_form tl_edit_form" method="post" enctype="multipart/form-data">

<input type="hidden" name="FORM_SUBMIT" value="tl_style_sheet_import">
<input type="hidden" name="REQUEST_TOKEN" value="'.REQUEST_TOKEN.'">
<input type="hidden" name="MAX_FILE_SIZE" value="'.Config::get('maxFileSize').'">

'.$GLOBALS['TL_LANG']['tl_style_sheet']['source'][0].'
'.$objUploader->generateMarkup().(isset($GLOBALS['TL_LANG']['tl_style_sheet']['source'][1]) ? '

'.$GLOBALS['TL_LANG']['tl_style_sheet']['source'][1].'
' : '').'

  <button type="submit" name="save" id="save" class="tl_submit" accesskey="s">'.$GLOBALS['TL_LANG']['tl_style_sheet']['import'][0].'</button>

</form>';
	}

	/**
	 * Export a style sheet
	 *
	 * @param DataContainer $dc
	 *
	 * @throws \Exception
	 */
	public function exportStyleSheet(DataContainer $dc)
	{
		$objStyleSheet = $this->Database->prepare("SELECT * FROM tl_style_sheet WHERE id=?")
										->limit(1)
										->execute($dc->id);

		if ($objStyleSheet->numRows < 1)
		{
			throw new \Exception("Invalid style sheet ID {$dc->id}");
		}

		$vars = array();

		// Get the global theme variables
		$objTheme = $this->Database->prepare("SELECT vars FROM tl_theme WHERE id=?")
								   ->limit(1)
								   ->execute($objStyleSheet->pid);

		if ($objTheme->vars != '')
		{
			if (\is_array(($tmp = StringUtil::deserialize($objTheme->vars))))
			{
				foreach ($tmp as $v)
				{
					$vars[$v['key']] = $v['value'];
				}
			}
		}

		// Merge the global style sheet variables
		if ($objStyleSheet->vars != '')
		{
			if (\is_array(($tmp = StringUtil::deserialize($objStyleSheet->vars))))
			{
				foreach ($tmp as $v)
				{
					$vars[$v['key']] = $v['value'];
				}
			}
		}

		// Sort by key length (see #3316)
		uksort($vars, 'length_sort_desc');

		// Create the file
		$objFile = new File('system/tmp/' . md5(uniqid(mt_rand(), true)));
		$objFile->write('');

		// Add the media query (see #7560)
		if ($objStyleSheet->mediaQuery != '')
		{
			$objFile->append('@media ' . $objStyleSheet->mediaQuery . ' {');
		}

		$objDefinitions = $this->Database->prepare("SELECT * FROM tl_style WHERE pid=? AND invisible!='1' ORDER BY sorting")
										 ->execute($objStyleSheet->id);

		// Append the definition
		while ($objDefinitions->next())
		{
			$objFile->append($this->compileDefinition($objDefinitions->row(), false, $vars, $objStyleSheet->row(), true), '');
		}

		// Close the media query
		if ($objStyleSheet->mediaQuery != '')
		{
			$objFile->append('}');
		}

		$objFile->close();
		$objFile->sendToBrowser($objStyleSheet->name . '.css');
		$objFile->delete();
	}

	/**
	 * Check the name of an imported file
	 *
	 * @param string $strName
	 *
	 * @return string
	 */
	public function checkStyleSheetName($strName)
	{
		$objStyleSheet = $this->Database->prepare("SELECT COUNT(*) AS count FROM tl_style_sheet WHERE name=?")
										->limit(1)
										->execute($strName);

		if ($objStyleSheet->count < 1)
		{
			return $strName;
		}

		$chunks = explode('-', $strName);
		$i = (\count($chunks) > 1) ? array_pop($chunks) : 0;
		$strName = implode('-', $chunks) . '-' . ((int) $i + 1);

		return $this->checkStyleSheetName($strName);
	}

	/**
	 * Create a format definition and insert it into the database
	 *
	 * @param array $arrDefinition
	 */
	protected function createDefinition($arrDefinition)
	{
		$arrSet = array
		(
			'pid'      => $arrDefinition['pid'],
			'sorting'  => $arrDefinition['sorting'],
			'tstamp'   => time(),
			'comment'  => $arrDefinition['comment'],
			'category' => $arrDefinition['category'],
			'selector' => $arrDefinition['selector']
		);

		$arrAttributes = StringUtil::trimsplit(';', $arrDefinition['attributes']);

		foreach ($arrAttributes as $strDefinition)
		{
			// Skip empty definitions
			if (trim($strDefinition) == '')
			{
				continue;
			}

			// Handle keywords, variables and functions (see #7448)
			if (strpos($strDefinition, 'important') !== false || strpos($strDefinition, 'transparent') !== false || strpos($strDefinition, 'inherit') !== false || strpos($strDefinition, '$') !== false || strpos($strDefinition, '(') !== false)
			{
				$arrSet['own'][] = $strDefinition;
				continue;
			}

			$arrChunks = array_map('trim', explode(':', $strDefinition, 2));
			$strKey = strtolower($arrChunks[0]);

			switch ($strKey)
			{
				case 'width':
				case 'height':
					if ($arrChunks[1] == 'auto')
					{
						$strUnit = '';
						$varValue = 'auto';
					}
					else
					{
						$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrChunks[1]);
						$varValue = preg_replace('/[^0-9.-]+/', '', $arrChunks[1]);
					}
					$arrSet['size'] = 1;
					$arrSet[$strKey]['value'] = $varValue;
					$arrSet[$strKey]['unit'] = $strUnit;
					break;

				case 'min-width':
				case 'min-height':
					$strName = str_replace('-', '', $strKey);
					if ($arrChunks[1] == 'inherit')
					{
						$strUnit = '';
						$varValue = 'inherit';
					}
					else
					{
						$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrChunks[1]);
						$varValue = preg_replace('/[^0-9.-]+/', '', $arrChunks[1]);
					}
					$arrSet['size'] = 1;
					$arrSet[$strName]['value'] = $varValue;
					$arrSet[$strName]['unit'] = $strUnit;
					break;

				case 'max-width':
				case 'max-height':
					$strName = str_replace('-', '', $strKey);
					if ($arrChunks[1] == 'inherit' || $arrChunks[1] == 'none')
					{
						$strUnit = '';
						$varValue = $arrChunks[1];
					}
					else
					{
						$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrChunks[1]);
						$varValue = preg_replace('/[^0-9.-]+/', '', $arrChunks[1]);
					}
					$arrSet['size'] = 1;
					$arrSet[$strName]['value'] = $varValue;
					$arrSet[$strName]['unit'] = $strUnit;
					break;

				case 'top':
				case 'right':
				case 'bottom':
				case 'left':
					if ($arrChunks[1] == 'auto')
					{
						$strUnit = '';
						$varValue = 'auto';
					}
					elseif (isset($arrSet['trbl']['unit']))
					{
						$arrSet['own'][] = $strDefinition;
						break;
					}
					else
					{
						$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrChunks[1]);
						$varValue = preg_replace('/[^0-9.-]+/', '', $arrChunks[1]);
					}
					$arrSet['positioning'] = 1;
					$arrSet['trbl'][$strKey] = $varValue;
					if ($strUnit != '')
					{
						$arrSet['trbl']['unit'] = $strUnit;
					}
					break;

				case 'position':
				case 'overflow':
				case 'clear':
				case 'display':
					$arrSet['positioning'] = 1;
					$arrSet[$strKey] = $arrChunks[1];
					break;

				case 'float':
					$arrSet['positioning'] = 1;
					$arrSet['floating'] = $arrChunks[1];
					break;

				case 'margin':
				case 'padding':
					$arrSet['alignment'] = 1;
					$arrTRBL = preg_split('/\s+/', $arrChunks[1]);
					$arrUnits = array();
					switch (\count($arrTRBL))
					{
						case 1:
							if ($arrTRBL[0] == 'auto')
							{
								$strUnit = '';
								$varValue = 'auto';
							}
							else
							{
								$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[0]);
								$varValue = preg_replace('/[^0-9.-]+/', '', $arrTRBL[0]);
							}
							$arrSet[$strKey] = array
							(
								'top' => $varValue,
								'right' => $varValue,
								'bottom' => $varValue,
								'left' => $varValue,
								'unit' => $strUnit
							);
							break;

						case 2:
							if ($arrTRBL[0] == 'auto')
							{
								$varValue_1 = 'auto';
							}
							else
							{
								$arrUnits[] = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[0]);
								$varValue_1 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[0]);
							}
							if ($arrTRBL[1] == 'auto')
							{
								$varValue_2 = 'auto';
							}
							else
							{
								$arrUnits[] = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[1]);
								$varValue_2 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[1]);
							}
							// Move to custom section if there are different units
							if (\count(array_filter(array_unique($arrUnits))) > 1)
							{
								$arrSet['alignment'] = '';
								$arrSet['own'][] = $strDefinition;
								break;
							}
							$arrSet[$strKey] = array
							(
								'top' => $varValue_1,
								'right' => $varValue_2,
								'bottom' => $varValue_1,
								'left' => $varValue_2,
								'unit' => ''
							);
							// Overwrite the unit
							foreach ($arrUnits as $strUnit)
							{
								if ($strUnit != '')
								{
									$arrSet[$strKey]['unit'] = $strUnit;
									break;
								}
							}
							break;

						case 3:
							if ($arrTRBL[0] == 'auto')
							{
								$varValue_1 = 'auto';
							}
							else
							{
								$arrUnits[] = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[0]);
								$varValue_1 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[0]);
							}
							if ($arrTRBL[1] == 'auto')
							{
								$varValue_2 = 'auto';
							}
							else
							{
								$arrUnits[] = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[1]);
								$varValue_2 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[1]);
							}
							if ($arrTRBL[2] == 'auto')
							{
								$varValue_3 = 'auto';
							}
							else
							{
								$arrUnits[] = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[2]);
								$varValue_3 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[2]);
							}
							// Move to custom section if there are different units
							if (\count(array_filter(array_unique($arrUnits))) > 1)
							{
								$arrSet['alignment'] = '';
								$arrSet['own'][] = $strDefinition;
								break;
							}
							$arrSet[$strKey] = array
							(
								'top' => $varValue_1,
								'right' => $varValue_2,
								'bottom' => $varValue_3,
								'left' => $varValue_2,
								'unit' => ''
							);
							// Overwrite the unit
							foreach ($arrUnits as $strUnit)
							{
								if ($strUnit != '')
								{
									$arrSet[$strKey]['unit'] = $strUnit;
									break;
								}
							}
							break;

						case 4:
							if ($arrTRBL[0] == 'auto')
							{
								$varValue_1 = 'auto';
							}
							else
							{
								$arrUnits[] = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[0]);
								$varValue_1 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[0]);
							}
							if ($arrTRBL[1] == 'auto')
							{
								$varValue_2 = 'auto';
							}
							else
							{
								$arrUnits[] = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[1]);
								$varValue_2 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[1]);
							}
							if ($arrTRBL[2] == 'auto')
							{
								$varValue_3 = 'auto';
							}
							else
							{
								$arrUnits[] = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[2]);
								$varValue_3 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[2]);
							}
							if ($arrTRBL[3] == 'auto')
							{
								$varValue_4 = 'auto';
							}
							else
							{
								$arrUnits[] = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[3]);
								$varValue_4 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[3]);
							}
							// Move to custom section if there are different units
							if (\count(array_filter(array_unique($arrUnits))) > 1)
							{
								$arrSet['alignment'] = '';
								$arrSet['own'][] = $strDefinition;
								break;
							}
							$arrSet[$strKey] = array
							(
								'top' => $varValue_1,
								'right' => $varValue_2,
								'bottom' => $varValue_3,
								'left' => $varValue_4,
								'unit' => ''
							);
							// Overwrite the unit
							foreach ($arrUnits as $strUnit)
							{
								if ($strUnit != '')
								{
									$arrSet[$strKey]['unit'] = $strUnit;
									break;
								}
							}
							break;
					}
					break;

				case 'margin-top':
				case 'margin-right':
				case 'margin-bottom':
				case 'margin-left':
					$arrSet['alignment'] = 1;
					$strName = str_replace('margin-', '', $strKey);
					if ($arrChunks[1] == 'auto')
					{
						$strUnit = '';
						$varValue = 'auto';
					}
					else
					{
						$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrChunks[1]);
						$varValue = preg_replace('/[^0-9.-]+/', '', $arrChunks[1]);
					}
					$arrSet['margin'][$strName] = $varValue;
					if (empty($arrSet['margin']['unit']))
					{
						$arrSet['margin']['unit'] = $strUnit;
					}
					break;

				case 'padding-top':
				case 'padding-right':
				case 'padding-bottom':
				case 'padding-left':
					$arrSet['alignment'] = 1;
					$strName = str_replace('padding-', '', $strKey);
					$varValue = preg_replace('/[^0-9.-]+/', '', $arrChunks[1]);
					$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrChunks[1]);
					$arrSet['padding'][$strName] = $varValue;
					$arrSet['padding']['unit'] = $strUnit;
					break;

				case 'align':
				case 'text-align':
				case 'vertical-align':
				case 'white-space':
					$arrSet['alignment'] = 1;
					$arrSet[str_replace('-', '', $strKey)] = $arrChunks[1];
					break;

				case 'background-color':
					if (!preg_match('/^#[a-f0-9]+$/i', $arrChunks[1]))
					{
						$arrSet['own'][] = $strDefinition;
					}
					else
					{
						$arrSet['background'] = 1;
						$arrSet['bgcolor'] = str_replace('#', '', $arrChunks[1]);
					}
					break;

				case 'background-image':
					$url = preg_replace('/url\(["\']?([^"\')]+)["\']?\)/i', '$1', $arrChunks[1]);

					if (strncmp($url, '-', 1) === 0)
					{
						// Ignore vendor prefixed commands
					}
					elseif (strncmp($url, 'radial-gradient', 15) === 0)
					{
						$arrSet['own'][] = $strDefinition; // radial gradients (see #4640)
					}
					else
					{
						$arrSet['background'] = 1;

						// Handle linear gradients (see #4640)
						if (strncmp($url, 'linear-gradient', 15) === 0)
						{
							$colors = StringUtil::trimsplit(',', preg_replace('/linear-gradient ?\(([^\)]+)\)/', '$1', $url));
							$arrSet['gradientAngle'] = array_shift($colors);
							$arrSet['gradientColors'] = serialize($colors);
						}
						else
						{
							$arrSet['bgimage'] = $url;
						}
					}
					break;

				case 'background-position':
					$arrSet['background'] = 1;
					if (preg_match('/[0-9]+/', $arrChunks[1]))
					{
						$arrSet['own'][] = $strDefinition;
					}
					else
					{
						$arrSet['bgposition'] = $arrChunks[1];
					}
					break;

				case 'background-repeat':
					$arrSet['background'] = 1;
					$arrSet['bgrepeat'] = $arrChunks[1];
					break;

				case 'border':
					if ($arrChunks[1] == 'none')
					{
						$arrSet['own'][] = $strDefinition;
						break;
					}
					$arrWSC = preg_split('/\s+/', $arrChunks[1]);
					if ($arrWSC[2] != '' && !preg_match('/^#[a-f0-9]+$/i', $arrWSC[2]))
					{
						$arrSet['own'][] = $strDefinition;
						break;
					}
					$arrSet['border'] = 1;
					$varValue = preg_replace('/[^0-9.-]+/', '', $arrWSC[0]);
					$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrWSC[0]);
					$arrSet['borderwidth'] = array
					(
						'top' => $varValue,
						'right' => $varValue,
						'bottom' => $varValue,
						'left' => $varValue,
						'unit' => $strUnit
					);
					if ($arrWSC[1] != '')
					{
						$arrSet['borderstyle'] = $arrWSC[1];
					}
					if ($arrWSC[2] != '')
					{
						$arrSet['bordercolor'] = str_replace('#', '', $arrWSC[2]);
					}
					break;

				case 'border-top':
				case 'border-right':
				case 'border-bottom':
				case 'border-left':
					if ($arrChunks[1] == 'none')
					{
						$arrSet['own'][] = $strDefinition;
						break;
					}
					$arrWSC = preg_split('/\s+/', $arrChunks[1]);
					if ($arrWSC[2] != '' && !preg_match('/^#[a-f0-9]+$/i', $arrWSC[2]))
					{
						$arrSet['own'][] = $strDefinition;
						break;
					}
					$arrSet['border'] = 1;
					$strName = str_replace('border-', '', $strKey);
					$varValue = preg_replace('/[^0-9.-]+/', '', $arrWSC[0]);
					$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrWSC[0]);
					if ((isset($arrSet['borderwidth']['unit']) && $arrSet['borderwidth']['unit'] != $strUnit) || ($arrWSC[1] != '' && isset($arrSet['borderstyle']) && $arrSet['borderstyle'] != $arrWSC[1]) || ($arrWSC[2] != '' && isset($arrSet['bordercolor']) && $arrSet['bordercolor'] != $arrWSC[2]))
					{
						$arrSet['own'][] = $strDefinition;
						break;
					}
					$arrSet['borderwidth'][$strName] = preg_replace('/[^0-9.-]+/', '', $varValue);
					$arrSet['borderwidth']['unit'] = $strUnit;
					if ($arrWSC[1] != '')
					{
						$arrSet['borderstyle'] = $arrWSC[1];
					}
					if ($arrWSC[2] != '')
					{
						$arrSet['bordercolor'] = str_replace('#', '', $arrWSC[2]);
					}
					break;

				case 'border-width':
					$arrSet['border'] = 1;
					$arrTRBL = preg_split('/\s+/', $arrChunks[1]);
					$strUnit = '';
					foreach ($arrTRBL as $v)
					{
						if ($v != 0)
						{
							$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[0]);
						}
					}
					switch (\count($arrTRBL))
					{
						case 1:
							$varValue = preg_replace('/[^0-9.-]+/', '', $arrTRBL[0]);
							$arrSet['borderwidth'] = array
							(
								'top' => $varValue,
								'right' => $varValue,
								'bottom' => $varValue,
								'left' => $varValue,
								'unit' => $strUnit
							);
							break;
						case 2:
							$varValue_1 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[0]);
							$varValue_2 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[1]);
							$arrSet['borderwidth'] = array
							(
								'top' => $varValue_1,
								'right' => $varValue_2,
								'bottom' => $varValue_1,
								'left' => $varValue_2,
								'unit' => $strUnit
							);
							break;
						case 4:
							$arrSet['borderwidth'] = array
							(
								'top' => preg_replace('/[^0-9.-]+/', '', $arrTRBL[0]),
								'right' => preg_replace('/[^0-9.-]+/', '', $arrTRBL[1]),
								'bottom' => preg_replace('/[^0-9.-]+/', '', $arrTRBL[2]),
								'left' => preg_replace('/[^0-9.-]+/', '', $arrTRBL[3]),
								'unit' => $strUnit
							);
							break;
					}
					break;

				case 'border-color':
					if (!preg_match('/^#[a-f0-9]+$/i', $arrChunks[1]))
					{
						$arrSet['own'][] = $strDefinition;
					}
					else
					{
						$arrSet['border'] = 1;
						$arrSet['bordercolor'] = str_replace('#', '', $arrChunks[1]);
					}
					break;

				case 'border-radius':
					$arrSet['border'] = 1;
					$arrTRBL = preg_split('/\s+/', $arrChunks[1]);
					$strUnit = '';
					foreach ($arrTRBL as $v)
					{
						if ($v != 0)
						{
							$strUnit = preg_replace('/[^acehimnprtvwx%]/', '', $arrTRBL[0]);
						}
					}
					switch (\count($arrTRBL))
					{
						case 1:
							$varValue = preg_replace('/[^0-9.-]+/', '', $arrTRBL[0]);
							$arrSet['borderradius'] = array
							(
								'top' => $varValue,
								'right' => $varValue,
								'bottom' => $varValue,
								'left' => $varValue,
								'unit' => $strUnit
							);
							break;
						case 2:
							$varValue_1 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[0]);
							$varValue_2 = preg_replace('/[^0-9.-]+/', '', $arrTRBL[1]);
							$arrSet['borderradius'] = array
							(
								'top' => $varValue_1,
								'right' => $varValue_2,
								'bottom' => $varValue_1,
								'left' => $varValue_2,
								'unit' => $strUnit
							);
							break;
						case 4:
							$arrSet['borderradius'] = array
							(
								'top' => preg_replace('/[^0-9.-]+/', '', $arrTRBL[0]),
								'right' => preg_replace('/[^0-9.-]+/', '', $arrTRBL[1]),
								'bottom' => preg_replace('/[^0-9.-]+/', '', $arrTRBL[2]),
								'left' => preg_replace('/[^0-9.-]+/', '', $arrTRBL[3]),
								'unit' => $strUnit
							);
							break;
					}
					break;

				case '-moz-border-radius':
				case '-webkit-border-radius':
					// Ignore
					break;

				case 'border-style':
				case 'border-collapse':
					$arrSet['border'] = 1;
					$arrSet[str_replace('-', '', $strKey)] = $arrChunks[1];
					break;

				case 'border-spacing':
					$arrSet['border'] = 1;
					$arrSet['borderspacing'] = array
					(
						'value' => preg_replace('/[^0-9.-]+/', '', $arrChunks[1]),
						'unit' => preg_replace('/[^acehimnprtvwx%]/', '', $arrChunks[1])
					);
					break;

				case 'font-family':
					$arrSet['font'] = 1;
					$arrSet[str_replace('-', '', $strKey)] = $arrChunks[1];
					break;

				case 'font-size':
					if (!preg_match('/[0-9]+/', $arrChunks[1]))
					{
						$arrSet['own'][] = $strDefinition;
					}
					else
					{
						$arrSet['font'] = 1;
						$arrSet['fontsize'] = array
						(
							'value' => preg_replace('/[^0-9.-]+/', '', $arrChunks[1]),
							'unit' => preg_replace('/[^acehimnprtvwx%]/', '', $arrChunks[1])
						);
					}
					break;

				case 'font-weight':
					if ($arrChunks[1] == 'bold' || $arrChunks[1] == 'normal')
					{
						$arrSet['font'] = 1;
						$arrSet['fontstyle'][] = $arrChunks[1];
					}
					else
					{
						$arrSet['own'][] = $strDefinition;
					}
					break;

				case 'font-style':
					if ($arrChunks[1] == 'italic')
					{
						$arrSet['font'] = 1;
						$arrSet['fontstyle'][] = 'italic';
					}
					else
					{
						$arrSet['own'][] = $strDefinition;
					}
					break;

				case 'text-decoration':
					$arrSet['font'] = 1;
					switch ($arrChunks[1])
					{
						case 'underline':
							$arrSet['fontstyle'][] = 'underline';
							break;

						case 'none':
							$arrSet['fontstyle'][] = 'notUnderlined';
							break;

						case 'overline':
						case 'line-through':
							$arrSet['fontstyle'][] = $arrChunks[1];
							break;
					}
					break;

				case 'font-variant':
					$arrSet['font'] = 1;
					if ($arrChunks[1] == 'small-caps')
					{
						$arrSet['fontstyle'][] = 'small-caps';
					}
					else
					{
						$arrSet['own'][] = $strDefinition;
					}
					break;

				case 'color':
					if (!preg_match('/^#[a-f0-9]+$/i', $arrChunks[1]))
					{
						$arrSet['own'][] = $strDefinition;
					}
					else
					{
						$arrSet['font'] = 1;
						$arrSet['fontcolor'] = str_replace('#', '', $arrChunks[1]);
					}
					break;

				case 'line-height':
					$arrSet['font'] = 1;
					$arrSet['lineheight'] = array
					(
						'value' => preg_replace('/[^0-9.-]+/', '', $arrChunks[1]),
						'unit' => preg_replace('/[^acehimnprtvwx%]/', '', $arrChunks[1])
					);
					break;

				case 'text-transform':
					$arrSet['font'] = 1;
					$arrSet['texttransform'] = $arrChunks[1];
					break;

				case 'text-indent':
				case 'letter-spacing':
				case 'word-spacing':
					$strName = str_replace('-', '', $strKey);
					$arrSet['font'] = 1;
					$arrSet[$strName] = array
					(
						'value' => preg_replace('/[^0-9.-]+/', '', $arrChunks[1]),
						'unit' => preg_replace('/[^acehimnprtvwx%]/', '', $arrChunks[1])
					);
					break;

				case 'list-style-type':
					$arrSet['list'] = 1;
					$arrSet[str_replace('-', '', $strKey)] = $arrChunks[1];
					break;

				case 'list-style-image':
					$arrSet['list'] = 1;
					$arrSet['liststyleimage'] = preg_replace('/url\(["\']?([^"\')]+)["\']?\)/i', '$1', $arrChunks[1]);
					break;

				case 'behavior':
					$arrSet['own'][] = $strDefinition;
					break;

				default:
					$blnIsOwn = true;

					// Allow custom definitions
					if (isset($GLOBALS['TL_HOOKS']['createDefinition']) && \is_array($GLOBALS['TL_HOOKS']['createDefinition']))
					{
						foreach ($GLOBALS['TL_HOOKS']['createDefinition'] as $callback)
						{
							$this->import($callback[0]);
							$arrTemp = $this->{$callback[0]}->{$callback[1]}($strKey, $arrChunks[1], $strDefinition, $arrSet);

							if ($arrTemp && \is_array($arrTemp))
							{
								$blnIsOwn = false;
								$arrSet = array_merge($arrSet, $arrTemp);
							}
						}
					}

					// Unknown definition
					if ($blnIsOwn)
					{
						$arrSet['own'][] = $strDefinition;
					}
					break;
			}
		}

		if (!empty($arrSet['own']))
		{
			$arrSet['own'] = implode(";\n", $arrSet['own']) . ';';
		}

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

	/**
	 * Return an image as data: string
	 *
	 * @param string $strImage
	 * @param array  $arrParent
	 *
	 * @return string|boolean
	 */
	protected function generateBase64Image($strImage, $arrParent)
	{
		if ($arrParent['embedImages'] > 0 && file_exists($this->strRootDir . '/' . $strImage))
		{
			$objImage = new File($strImage);
			$strMime = $objImage->extension;

			// Adjust the mime types
			if ($strMime == 'jpg')
			{
				$strMime = 'jpeg';
			}
			elseif ($strMime == 'svg')
			{
				$strMime = 'svg+xml';
			}

			// Return the data: string
			if ($objImage->size <= $arrParent['embedImages'])
			{
				return 'data:image/' . $strMime . ';base64,' . base64_encode($objImage->getContent());
			}
		}

		return false;
	}
}

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


<?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 Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher;
use Symfony\Cmf\Component\Routing\NestedMatcher\FinalMatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;

class UrlMatcher extends RedirectableUrlMatcher implements FinalMatcherInterface
{
    /**
     * Initializes the object with an empty route collection and request
     * context, because both will be set in the finalMatch() method.
     */
    public function __construct()
    {
        parent::__construct(new RouteCollection(), new RequestContext());
    }

    /**
     * {@inheritdoc}
     */
    public function finalMatch(RouteCollection $collection, Request $request)
    {
        $this->routes = $collection;

        $context = new RequestContext();
        $context->fromRequest($request);
        $context->setHost($request->getHttpHost());

        $this->setContext($context);

        return $this->matchRequest($request);
    }
}


<?php

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

namespace Contao;

use Contao\CoreBundle\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
use Symfony\Component\HttpFoundation\Session\Session;

/**
 * Back end page picker.
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class BackendPage extends Backend
{

	/**
	 * Current Ajax object
	 * @var Ajax
	 */
	protected $objAjax;

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

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

		System::loadLanguageFile('default');
	}

	/**
	 * Run the controller and parse the template
	 *
	 * @return Response
	 */
	public function run()
	{
		/** @var Session $objSession */
		$objSession = System::getContainer()->get('session');

		$objTemplate = new BackendTemplate('be_picker');
		$objTemplate->main = '';

		// Ajax request
		if ($_POST && Environment::get('isAjaxRequest'))
		{
			$this->objAjax = new Ajax(Input::post('action'));
			$this->objAjax->executePreActions();
		}

		$strTable = Input::get('table');
		$strField = Input::get('field');

		// Define the current ID
		\define('CURRENT_ID', (Input::get('table') ? $objSession->get('CURRENT_ID') : Input::get('id')));

		$this->loadDataContainer($strTable);
		$strDriver = 'DC_' . $GLOBALS['TL_DCA'][$strTable]['config']['dataContainer'];
		$objDca = new $strDriver($strTable);
		$objDca->field = $strField;

		// Set the active record
		if ($this->Database->tableExists($strTable))
		{
			/** @var Model $strModel */
			$strModel = Model::getClassFromTable($strTable);

			if (class_exists($strModel))
			{
				$objModel = $strModel::findByPk(Input::get('id'));

				if ($objModel !== null)
				{
					$objDca->activeRecord = $objModel;
				}
			}
		}

		// AJAX request
		if ($_POST && Environment::get('isAjaxRequest'))
		{
			$this->objAjax->executePostActions($objDca);
		}

		$objSession->set('filePickerRef', Environment::get('request'));
		$arrValues = array_filter(explode(',', Input::get('value')));

		// Call the load_callback
		if (\is_array($GLOBALS['TL_DCA'][$strTable]['fields'][$strField]['load_callback']))
		{
			foreach ($GLOBALS['TL_DCA'][$strTable]['fields'][$strField]['load_callback'] as $callback)
			{
				if (\is_array($callback))
				{
					$this->import($callback[0]);
					$arrValues = $this->{$callback[0]}->{$callback[1]}($arrValues, $objDca);
				}
				elseif (\is_callable($callback))
				{
					$arrValues = $callback($arrValues, $objDca);
				}
			}
		}

		/** @var PageSelector $strClass */
		$strClass = $GLOBALS['BE_FFL']['pageSelector'];

		/** @var PageSelector $objPageTree */
		$objPageTree = new $strClass($strClass::getAttributesFromDca($GLOBALS['TL_DCA'][$strTable]['fields'][$strField], $strField, $arrValues, $strField, $strTable, $objDca));

		/** @var AttributeBagInterface $objSessionBag */
		$objSessionBag = $objSession->getBag('contao_backend');

		$objTemplate->main = $objPageTree->generate();
		$objTemplate->theme = Backend::getTheme();
		$objTemplate->base = Environment::get('base');
		$objTemplate->language = $GLOBALS['TL_LANGUAGE'];
		$objTemplate->title = StringUtil::specialchars($GLOBALS['TL_LANG']['MSC']['pagepicker']);
		$objTemplate->host = Environment::get('host');
		$objTemplate->charset = Config::get('characterSet');
		$objTemplate->addSearch = true;
		$objTemplate->search = $GLOBALS['TL_LANG']['MSC']['search'];
		$objTemplate->action = ampersand(Environment::get('request'));
		$objTemplate->value = $objSessionBag->get('page_selector_search');
		$objTemplate->breadcrumb = $GLOBALS['TL_DCA']['tl_page']['list']['sorting']['breadcrumb'];

		if ($this->User->hasAccess('page', 'modules'))
		{
			$objTemplate->manager = $GLOBALS['TL_LANG']['MSC']['pageManager'];
			$objTemplate->managerHref = 'contao/main.php?do=page&popup=1';
		}

		if (Input::get('switch') && $this->User->hasAccess('files', 'modules'))
		{
			$objTemplate->switch = $GLOBALS['TL_LANG']['MSC']['filePicker'];
			$objTemplate->switchHref = str_replace('contao/page?', 'contao/file?', ampersand(Environment::get('request')));
		}

		return $objTemplate->getResponse();
	}
}

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


<?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 "newsletter reader".
 *
 * @property array $nl_channels
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ModuleNewsletterReader extends Module
{

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

	/**
	 * 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']['newsletterreader'][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();
		}

		// Set the item from the auto_item parameter
		if (!isset($_GET['items']) && Config::get('useAutoItem') && isset($_GET['auto_item']))
		{
			Input::setGet('items', Input::get('auto_item'));
		}

		// Do not index or cache the page if no news item has been specified
		if (!Input::get('items'))
		{
			/** @var PageModel $objPage */
			global $objPage;

			$objPage->noSearch = 1;
			$objPage->cache = 0;

			return '';
		}

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

		// Do not index or cache the page if there are no channels
		if (empty($this->nl_channels) || !\is_array($this->nl_channels))
		{
			/** @var PageModel $objPage */
			global $objPage;

			$objPage->noSearch = 1;
			$objPage->cache = 0;

			return '';
		}

		return parent::generate();
	}

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

		$this->Template->content = '';
		$this->Template->referer = 'javascript:history.go(-1)';
		$this->Template->back = $GLOBALS['TL_LANG']['MSC']['goBack'];

		$objNewsletter = NewsletterModel::findSentByParentAndIdOrAlias(Input::get('items'), $this->nl_channels);

		if (null === $objNewsletter)
		{
			throw new PageNotFoundException('Page not found: ' . Environment::get('uri'));
		}

		// Overwrite the page title (see #2853 and #4955)
		if ($objNewsletter->subject != '')
		{
			$objPage->pageTitle = strip_tags(StringUtil::stripInsertTags($objNewsletter->subject));
		}

		// Add enclosure
		if ($objNewsletter->addFile)
		{
			$this->addEnclosuresToTemplate($this->Template, $objNewsletter->row(), 'files');
		}

		// Support plain text newsletters (thanks to Hagen Klemp)
		if ($objNewsletter->sendText)
		{
			$strContent = nl2br_html5($objNewsletter->text);
		}
		else
		{
			$strContent = str_ireplace(' align="center"', '', $objNewsletter->content);
		}

		// Parse simple tokens and insert tags
		$strContent = $this->replaceInsertTags($strContent);
		$strContent = StringUtil::parseSimpleTokens($strContent, array());

		// Encode e-mail addresses
		$strContent = StringUtil::encodeEmail($strContent);

		$this->Template->content = $strContent;
		$this->Template->subject = $objNewsletter->subject;
	}
}

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


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

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

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

        return isset($columns['username']) && true === $columns['username']->getNotnull();
    }

    /**
     * {@inheritdoc}
     */
    public function run(): void
    {
        $this->connection->query('
            ALTER TABLE
                tl_member
            CHANGE
                username username varchar(64) COLLATE utf8_bin NULL
        ');

        $this->connection->query("
            UPDATE
                tl_member
            SET
                username = NULL
            WHERE
                username = ''
        ");

        $this->connection->query('
            ALTER TABLE
                tl_member
            DROP INDEX
                username,
            ADD UNIQUE KEY
                username (username)
        ');
    }
}


<?php

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

namespace Contao;

use Contao\Model\Collection;

/**
 * Reads and writes style sheets
 *
 * The class reads from and writes to the style sheet table. It does not create
 * .css files on the hard disk. This is done by the StyleSheet class.
 *
 * @property integer $id
 * @property integer $pid
 * @property integer $tstamp
 * @property string  $name
 * @property boolean $disablePie
 * @property integer $embedImages
 * @property string  $cc
 * @property string  $media
 * @property string  $mediaQuery
 * @property string  $vars
 * @property string  $type
 * @property boolean $hasFontFace
 * @property string  $singleSRC
 * @property integer $tstamp2
 * @property integer $tstamp3
 *
 * @method static StyleSheetModel|null findById($id, array $opt=array())
 * @method static StyleSheetModel|null findByPk($id, array $opt=array())
 * @method static StyleSheetModel|null findByIdOrAlias($val, array $opt=array())
 * @method static StyleSheetModel|null findOneBy($col, $val, array $opt=array())
 * @method static StyleSheetModel|null findByName($val, array $opt=array())
 * @method static StyleSheetModel|null findOneByPid($val, array $opt=array())
 * @method static StyleSheetModel|null findOneByTstamp($val, array $opt=array())
 * @method static StyleSheetModel|null findOneByDisablePie($val, array $opt=array())
 * @method static StyleSheetModel|null findOneByEmbedImages($val, array $opt=array())
 * @method static StyleSheetModel|null findOneByCc($val, array $opt=array())
 * @method static StyleSheetModel|null findOneByMedia($val, array $opt=array())
 * @method static StyleSheetModel|null findOneByMediaQuery($val, array $opt=array())
 * @method static StyleSheetModel|null findOneByVars($val, array $opt=array())
 *
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findByPid($val, array $opt=array())
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findByTstamp($val, array $opt=array())
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findByDisablePie($val, array $opt=array())
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findByEmbedImages($val, array $opt=array())
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findByCc($val, array $opt=array())
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findByMedia($val, array $opt=array())
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findByMediaQuery($val, array $opt=array())
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findByVars($val, array $opt=array())
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findMultipleByIds($val, array $opt=array())
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findBy($col, $val, array $opt=array())
 * @method static Collection|StyleSheetModel[]|StyleSheetModel|null findAll(array $opt=array())
 *
 * @method static integer countById($id, array $opt=array())
 * @method static integer countByPid($val, array $opt=array())
 * @method static integer countByTstamp($val, array $opt=array())
 * @method static integer countByName($val, array $opt=array())
 * @method static integer countByDisablePie($val, array $opt=array())
 * @method static integer countByEmbedImages($val, array $opt=array())
 * @method static integer countByCc($val, array $opt=array())
 * @method static integer countByMedia($val, array $opt=array())
 * @method static integer countByMediaQuery($val, array $opt=array())
 * @method static integer countByVars($val, array $opt=array())
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class StyleSheetModel extends Model
{

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

	/**
	 * Find multiple style sheets by their IDs
	 *
	 * @param array $arrIds An array of style sheet IDs
	 *
	 * @return Collection|StyleSheetModel[]|StyleSheetModel|null A collection of models or null if there are no style sheets
	 */
	public static function findByIds($arrIds)
	{
		if (empty($arrIds) || !\is_array($arrIds))
		{
			return null;
		}

		$objDatabase = Database::getInstance();
		$arrIds = array_map('\intval', $arrIds);

		$objResult = $objDatabase->execute("SELECT *, (SELECT tstamp FROM tl_theme WHERE tl_theme.id=tl_style_sheet.pid) AS tstamp3, (SELECT MAX(tstamp) FROM tl_style WHERE tl_style.pid=tl_style_sheet.id) AS tstamp2, (SELECT COUNT(*) FROM tl_style WHERE tl_style.selector='@font-face' AND tl_style.invisible='' AND tl_style.pid=tl_style_sheet.id) AS hasFontFace FROM tl_style_sheet WHERE id IN (" . implode(',', $arrIds) . ") ORDER BY " . $objDatabase->findInSet('id', $arrIds));

		return static::createCollectionFromDbResult($objResult, 'tl_style_sheet');
	}
}

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


<?php

declare(strict_types=1);

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

namespace Contao\CalendarBundle\EventListener;

use Contao\CalendarEventsModel;
use Contao\CoreBundle\Event\PreviewUrlConvertEvent;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\Events;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

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

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

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

    /**
     * Adds the front end preview URL to the event.
     */
    public function onPreviewUrlConvert(PreviewUrlConvertEvent $event): void
    {
        if (!$this->framework->isInitialized()) {
            return;
        }

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

        if (null === $request || null === ($eventModel = $this->getEventModel($request))) {
            return;
        }

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

        $event->setUrl($request->getSchemeAndHttpHost().'/'.$eventsAdapter->generateEventUrl($eventModel));
    }

    private function getEventModel(Request $request): ?CalendarEventsModel
    {
        if (!$request->query->has('calendar')) {
            return null;
        }

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

        return $adapter->findByPk($request->query->get('calendar'));
    }
}


<?php

declare(strict_types=1);

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

namespace Contao\CoreBundle\Security\Authentication;

use Contao\BackendUser;
use Contao\CoreBundle\Monolog\ContaoContext;
use Contao\CoreBundle\Security\Authentication\Token\FrontendPreviewToken;
use Contao\FrontendUser;
use Contao\StringUtil;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class FrontendPreviewAuthenticator
{
    /**
     * @var SessionInterface
     */
    private $session;

    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * @var UserProviderInterface
     */
    private $userProvider;

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

    public function __construct(SessionInterface $session, TokenStorageInterface $tokenStorage, UserProviderInterface $userProvider, LoggerInterface $logger = null)
    {
        $this->session = $session;
        $this->tokenStorage = $tokenStorage;
        $this->userProvider = $userProvider;
        $this->logger = $logger;
    }

    public function authenticateFrontendUser(string $username, bool $showUnpublished): bool
    {
        $backendUser = $this->loadBackendUser();

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

        // The back end user does not have permission to log in front end users
        if (!$backendUser->isAdmin && (empty($backendUser->amg) || !\is_array($backendUser->amg))) {
            return false;
        }

        $frontendUser = $this->loadFrontendUser($username, $backendUser);

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

        $token = new FrontendPreviewToken($frontendUser, $showUnpublished);

        $this->session->set(FrontendUser::SECURITY_SESSION_KEY, serialize($token));

        return true;
    }

    public function authenticateFrontendGuest(bool $showUnpublished): bool
    {
        $backendUser = $this->loadBackendUser();

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

        $token = new FrontendPreviewToken(null, $showUnpublished);

        $this->session->set(FrontendUser::SECURITY_SESSION_KEY, serialize($token));

        return true;
    }

    /**
     * Removes a front end authentication from the session.
     */
    public function removeFrontendAuthentication(): bool
    {
        if (!$this->session->isStarted() || !$this->session->has(FrontendUser::SECURITY_SESSION_KEY)) {
            return false;
        }

        $this->session->remove(FrontendUser::SECURITY_SESSION_KEY);

        return true;
    }

    private function loadBackendUser(): ?BackendUser
    {
        if (!$this->session->isStarted()) {
            return null;
        }

        $token = $this->tokenStorage->getToken();

        // Check if a back end user is authenticated
        if (null === $token || !$token->isAuthenticated()) {
            return null;
        }

        $backendUser = $token->getUser();

        if (!$backendUser instanceof BackendUser) {
            return null;
        }

        return $backendUser;
    }

    /**
     * Loads the front end user and checks its group access permissions.
     */
    private function loadFrontendUser(string $username, BackendUser $backendUser): ?FrontendUser
    {
        try {
            $frontendUser = $this->userProvider->loadUserByUsername($username);

            // Make sure the user provider returned a front end user
            if (!$frontendUser instanceof FrontendUser) {
                throw new UsernameNotFoundException('User is not a front end user');
            }
        } catch (UsernameNotFoundException $e) {
            if (null !== $this->logger) {
                $this->logger->info(
                    sprintf('Could not find a front end user with the username "%s"', $username),
                    ['contao' => new ContaoContext(__METHOD__, ContaoContext::ACCESS, '')]
                );
            }

            return null;
        }

        $allowedGroups = StringUtil::deserialize($backendUser->amg, true);
        $frontendGroups = StringUtil::deserialize($frontendUser->groups, true);

        // The front end user does not belong to a group that the back end user is allowed to log in
        if (!$backendUser->isAdmin && !\count(array_intersect($frontendGroups, $allowedGroups))) {
            return null;
        }

        return $frontendUser;
    }
}


<?php

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

namespace Contao;

/**
 * Reads .zip files and unpacks their content
 *
 * Usage:
 *
 *     $zip = new ZipReader('test.zip');
 *
 *     while ($zip->next())
 *     {
 *         echo $zip->->file_name;
 *     }
 *
 * @property integer $number_of_this_disk       The number of this disk
 * @property integer $number_of_disk_with_cd    The number of the disk with the start of the central directory
 * @property integer $total_cd_entries_disk     The total number of entries in the central directory on this disk
 * @property integer $total_cd_entries          The total number of entries in the central directory
 * @property integer $size_of_cd                The size of the central directory
 * @property integer $offset_start_cd           The offset of the start of the central directory with respect to the starting disk number
 * @property integer $zipfile_comment_length    The ZIP file comment length
 * @property string  $zipfile_comment           The ZIP file comment
 * @property integer $version_made_by           The version made by
 * @property integer $version_needed_to_extract The version needed to extract
 * @property integer $general_purpose_bit_flag  General purpose bit flag
 * @property integer $compression_method        The compression method
 * @property integer $last_mod_file_time        The last modification file time
 * @property integer $last_mod_file_date        The last modification file date
 * @property integer $last_mod_file_unix        The last modification file unix timestamp
 * @property integer $crc-32                    The CRC32 checksum
 * @property integer $compressed_size           The compressed size
 * @property integer $uncompressed_size         The uncompressed size
 * @property integer $file_name_length          The file name length
 * @property integer $extra_field_length        The extra field length
 * @property integer $file_comment_length       The file comment length
 * @property integer $disk_number_start         Disk number start
 * @property integer $internal_file_attributes  Internal file attributes
 * @property integer $external_file_attributes  External file attributes
 * @property integer $offset_of_local_header    The relative offset of local header
 * @property string  $file_name                 The file name
 * @property string  $file_basename             The file basename
 * @property string  $file_dirname              The file dirname
 * @property string  $extra_field               The extra field
 * @property string  $file_comment              The file comment
 *
 * @author Leo Feyer <https://github.com/leofeyer>
 */
class ZipReader
{

	/**
	 * File signatur
	 * @var string
	 */
	const FILE_SIGNATURE = "\x50\x4b\x03\x04";

	/**
	 * Central directory begin marker
	 * @var string
	 */
	const CENTRAL_DIR_START = "\x50\x4b\x01\x02";

	/**
	 * Central directory end marker
	 * @var string
	 */
	const CENTRAL_DIR_END = "\x50\x4b\x05\x06";

	/**
	 * File handle
	 * @var resource
	 */
	protected $resFile;

	/**
	 * File name
	 * @var string
	 */
	protected $strFile;

	/**
	 * Root dir
	 * @var string
	 */
	protected $strRootDir;

	/**
	 * Current file index
	 * @var integer
	 */
	private $intIndex = -1;

	/**
	 * Last file index
	 * @var integer
	 */
	private $intLast = 0;

	/**
	 * Archive header
	 * @var array
	 */
	protected $arrHeader = array();

	/**
	 * File list
	 * @var array
	 */
	protected $arrFiles = array();

	/**
	 * Open the archive and return the file handle
	 *
	 * @param string $strFile The file path
	 *
	 * @throws \Exception If $strFile does not exist or cannot be opened
	 */
	public function __construct($strFile)
	{
		// Handle open_basedir restrictions
		if ($strFile == '.')
		{
			$strFile = '';
		}

		$this->strFile = $strFile;
		$this->strRootDir = System::getContainer()->getParameter('kernel.project_dir');

		// Check if file exists
		if (!file_exists($this->strRootDir . '/' . $strFile))
		{
			throw new \Exception("Could not find file $strFile");
		}

		$this->resFile = @fopen($this->strRootDir . '/' . $strFile, 'rb');

		// Could not open file
		if (!\is_resource($this->resFile))
		{
			throw new \Exception("Could not open file $strFile");
		}

		$this->readCentralDirectory();
	}

	/**
	 * Close the file handle
	 */
	public function __destruct()
	{
		fclose($this->resFile);
	}

	/**
	 * Return a property of the archive header or the current file
	 *
	 * @param string $strKey The property name
	 *
	 * @return mixed|null The property value or null
	 */
	public function __get($strKey)
	{
		switch ($strKey)
		{
			// Header
			case 'number_of_this_disk':
			case 'number_of_disk_with_cd':
			case 'total_cd_entries_disk':
			case 'total_cd_entries':
			case 'size_of_cd':
			case 'offset_start_cd':
			case 'zipfile_comment_length':
			case 'zipfile_comment':
				return $this->arrHeader[$strKey];
				break;

			// Current file
			default:
				if ($this->intIndex < 0)
				{
					$this->first();
				}
				if (isset($this->arrFiles[$this->intIndex][$strKey]))
				{
					return $this->arrFiles[$this->intIndex][$strKey];
				}
				break;
		}

		return null;
	}

	/**
	 * Return a list of all files in the archive
	 *
	 * @return array The files array
	 */
	public function getFileList()
	{
		$arrFiles = array();

		foreach ($this->arrFiles as $arrFile)
		{
			$arrFiles[] = $arrFile['file_name'];
		}

		return $arrFiles;
	}

	/**
	 * Set the internal pointer to a particular file
	 *
	 * @param string $strName The file name
	 *
	 * @return boolean True if the file was found
	 */
	public function getFile($strName)
	{
		foreach ($this->arrFiles as $k=>$v)
		{
			if ($strName == $v['file_name'])
			{
				$this->intIndex = $k;

				return true;
			}
		}

		return false;
	}

	/**
	 * Go to the first file of the archive
	 *
	 * @return ZipReader The object instance
	 */
	public function first()
	{
		$this->intIndex = 0;

		return $this;
	}

	/**
	 * Go to the next file of the archive
	 *
	 * @return ZipReader|boolean The object instance or false if there is no next file
	 */
	public function next()
	{
		if ($this->intIndex >= $this->intLast)
		{
			return false;
		}

		++$this->intIndex;

		return $this;
	}

	/**
	 * Go to the previous file of the archive
	 *
	 * @return ZipReader|boolean The object instance or false if there is no previous file
	 */
	public function prev()
	{
		if ($this->intIndex <= 0)
		{
			return false;
		}

		--$this->intIndex;

		return $this;
	}

	/**
	 * Go to the last file of the archive
	 *
	 * @return ZipReader The object instance
	 */
	public function last()
	{
		$this->intIndex = $this->intLast;

		return $this;
	}

	/**
	 * Return the current file as array
	 *
	 * @return array The current file array
	 */
	public function current()
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		return $this->arrFiles[$this->intIndex];
	}

	/**
	 * Reset the archive
	 *
	 * @return ZipReader The object instance
	 */
	public function reset()
	{
		$this->intIndex = -1;

		return $this;
	}

	/**
	 * Unzip the current file and return its contents as string
	 *
	 * @return string The file content
	 *
	 * @throws \Exception If the current file is encrypted or not a compressed file
	 */
	public function unzip()
	{
		if ($this->intIndex < 0)
		{
			$this->first();
		}

		$strName = $this->arrFiles[$this->intIndex]['file_name'];

		// Encrypted files are not supported
		if ($this->arrFiles[$this->intIndex]['general_purpose_bit_flag'] & 0x0001)
		{
			throw new \Exception("File $strName is encrypted");
		}

		// Reposition pointer
		if (@fseek($this->resFile, $this->arrFiles[$this->intIndex]['offset_of_local_header']) !== 0)
		{
			throw new \Exception("Cannot reposition pointer");
		}

		$strSignature = @fread($this->resFile, 4);

		// Not a file
		if ($strSignature != self::FILE_SIGNATURE)
		{
			throw new \Exception("$strName is not a compressed file");
		}

		// Get extra field length
		fseek($this->resFile, 24, SEEK_CUR);
		$arrEFL = unpack('v', @fread($this->resFile, 2));

		// Reposition pointer
		fseek($this->resFile, ($this->arrFiles[$this->intIndex]['file_name_length'] + $arrEFL[1]), SEEK_CUR);

		// Empty file
		if ($this->arrFiles[$this->intIndex]['compressed_size'] < 1)
		{
			return '';
		}

		// Read data
		$strBuffer = @fread($this->resFile, $this->arrFiles[$this->intIndex]['compressed_size']);

		// Decompress data
		switch ($this->arrFiles[$this->intIndex]['compression_method'])
		{
			// Stored
			case 0:
				break;

			// Deflated
			case 8:
				$strBuffer = gzinflate($strBuffer);
				break;

			// BZIP2
			case 12:
				if (!\extension_loaded('bz2'))
				{
					throw new \Exception('PHP extension "bz2" required to decompress BZIP2 files');
				}
				$strBuffer = bzdecompress($strBuffer);
				break;

			// Unknown
			default:
				throw new \Exception('Unknown compression method');
				break;
		}

		// Check uncompressed data
		if ($strBuffer === false)
		{
			throw new \Exception('Could not decompress data');
		}

		// Check uncompressed size
		if (\strlen($strBuffer) != $this->arrFiles[$this->intIndex]['uncompressed_size'])
		{
			throw new \Exception('Size of the uncompressed file does not match header value');
		}

		return $strBuffer;
	}

	/**
	 * Return a list of all files in the archive
	 *
	 * @throws \Exception If the central directory cannot be found
	 */
	protected function readCentralDirectory()
	{
		$strMbCharset = null;

		// Set the mbstring encoding to ASCII (see #5842)
		if (ini_get('mbstring.func_overload') > 0)
		{
			$strMbCharset = mb_internal_encoding();

			if (mb_internal_encoding('ASCII') === false)
			{
				$strMbCharset = null;
			}
		}

		$intOffset = 0;
		$pos = 0;
		$intInterval = min(filesize($this->strRootDir . '/' . $this->strFile), 1024);
		$strBuffer = '';

		// Read to delimiter
		do
		{
			$intOffset -= $intInterval;

			$fseek = @fseek($this->resFile, $intOffset, SEEK_END);
			$strBuffer = @fread($this->resFile, abs($intOffset)) . $strBuffer;
		}
		while ($fseek != -1 && ($pos = strpos($strBuffer, self::CENTRAL_DIR_END)) === false);

		// Reposition pointer
		fseek($this->resFile, ($intOffset + $pos), SEEK_END);
		$strSignature = @fread($this->resFile, 4);

		// Read archive header
		if ($strSignature != self::CENTRAL_DIR_END)
		{
			throw new \Exception('Error reading central directory');
		}

		$arrHeader = array();

		$arrHeader['number_of_this_disk']    = unpack('v', @fread($this->resFile, 2));
		$arrHeader['number_of_disk_with_cd'] = unpack('v', @fread($this->resFile, 2));
		$arrHeader['total_cd_entries_disk']  = unpack('v', @fread($this->resFile, 2));
		$arrHeader['total_cd_entries']       = unpack('v', @fread($this->resFile, 2));
		$arrHeader['size_of_cd']             = unpack('V', @fread($this->resFile, 4));
		$arrHeader['offset_start_cd']        = unpack('V', @fread($this->resFile, 4));
		$arrHeader['zipfile_comment_length'] = unpack('v', @fread($this->resFile, 2));
		$arrHeader['zipfile_comment']        = $arrHeader['zipfile_comment_length'][1] ? @fread($this->resFile, $arrHeader['zipfile_comment_length'][1]) : '';

		// Eliminate nested arrays
		foreach ($arrHeader as $k=>$v)
		{
			$arrHeader[$k] = \is_array($v) ? $v[1] : $v;
		}

		$this->arrHeader = $arrHeader;

		// Reposition pointer to begin of the central directory
		fseek($this->resFile, $this->arrHeader['offset_start_cd'], SEEK_SET);
		$strSignature = @fread($this->resFile, 4);

		// Build file list
		while ($strSignature == self::CENTRAL_DIR_START)
		{
			$arrFile = array();

			$arrFile['version_made_by']           = unpack('v', @fread($this->resFile, 2));
			$arrFile['version_needed_to_extract'] = unpack('v', @fread($this->resFile, 2));
			$arrFile['general_purpose_bit_flag']  = unpack('v', @fread($this->resFile, 2));
			$arrFile['compression_method']        = unpack('v', @fread($this->resFile, 2));
			$arrFile['last_mod_file_time']        = unpack('v', @fread($this->resFile, 2));
			$arrFile['last_mod_file_date']        = unpack('v', @fread($this->resFile, 2));
			$arrFile['crc-32']                    = unpack('V', @fread($this->resFile, 4));
			$arrFile['compressed_size']           = unpack('V', @fread($this->resFile, 4));
			$arrFile['uncompressed_size']         = unpack('V', @fread($this->resFile, 4));
			$arrFile['file_name_length']          = unpack('v', @fread($this->resFile, 2));
			$arrFile['extra_field_length']        = unpack('v', @fread($this->resFile, 2));
			$arrFile['file_comment_length']       = unpack('v', @fread($this->resFile, 2));
			$arrFile['disk_number_start']         = unpack('v', @fread($this->resFile, 2));
			$arrFile['internal_file_attributes']  = unpack('v', @fread($this->resFile, 2));
			$arrFile['external_file_attributes']  = unpack('V', @fread($this->resFile, 4));
			$arrFile['offset_of_local_header']    = unpack('V', @fread($this->resFile, 4));
			$arrFile['file_name']                 = @fread($this->resFile, $arrFile['file_name_length'][1]);
			$arrFile['extra_field']               = $arrFile['extra_field_length'][1] ? @fread($this->resFile, $arrFile['extra_field_length'][1]) : '';
			$arrFile['file_comment']              = $arrFile['file_comment_length'][1] ? @fread($this->resFile, $arrFile['file_comment_length'][1]) : '';

			// Skip directories
			if (substr($arrFile['file_name'], -1) == '/')
			{
				$strSignature = @fread($this->resFile, 4);
				continue;
			}

			// Eliminate nested arrays
			foreach ($arrFile as $k=>$v)
			{
				$arrFile[$k] = \is_array($v) ? $v[1] : $v;
			}

			// Split file path
			$arrFile['file_basename'] = basename($arrFile['file_name']);
			$arrFile['file_dirname'] = (($path = \dirname($arrFile['file_name'])) != '.' ? $path : '');

			// Add UNIX time
			$arrFile['last_mod_file_unix'] = $this->decToUnix((int) $arrFile['last_mod_file_time'], (int) $arrFile['last_mod_file_date']);

			// Read next signature
			$strSignature = @fread($this->resFile, 4);
			$this->arrFiles[] = $arrFile;
		}

		$this->intLast = (\count($this->arrFiles) - 1);

		// Restore the mbstring encoding (see #5842)
		$strMbCharset && mb_internal_encoding($strMbCharset);
	}

	/**
	 * Calculate the Unix timestamp from two hexadecimal values
	 *
	 * @param integer $intTime The time integer
	 * @param integer $intDate The date integer
	 *
	 * @return integer The Unix timestamp
	 */
	protected function decToUnix($intTime, $intDate)
	{
		return mktime
		(
			 ($intTime & 0xf800) >> 11,
			 ($intTime & 0x07e0) >>  5,
			 ($intTime & 0x001f) <<  1,
			 ($intDate & 0x01e0) >>  5,
			 ($intDate & 0x001f),
			(($intDate & 0xfe00) >>  9) + 1980
		);
	}
}

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