Merge branch '5.3'
authorTim Düsterhus <duesterhus@woltlab.com>
Wed, 3 Feb 2021 15:00:12 +0000 (16:00 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Wed, 3 Feb 2021 15:00:12 +0000 (16:00 +0100)
1  2 
wcfsetup/install/files/lib/acp/form/StyleAddForm.class.php

index cefeaaa253306eeeef36e993eb7132ae237e1d6f,a51217184d5071328a5a6a543cd9535a3618bc47..a02e30ddda84580ae8bbe5b8852c06eae6aa102d
@@@ -29,982 -27,890 +29,982 @@@ use wcf\util\StringUtil
  
  /**
   * Shows the style add form.
 - * 
 - * @author    Alexander Ebert
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\Acp\Form
 + *
 + * @author  Alexander Ebert
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\Acp\Form
   */
 -class StyleAddForm extends AbstractForm {
 -      /**
 -       * @inheritDoc
 -       */
 -      public $activeMenuItem = 'wcf.acp.menu.link.style.add';
 -      
 -      /**
 -       * author's name
 -       * @var string
 -       */
 -      public $authorName = '';
 -      
 -      /**
 -       * author's URL
 -       * @var string
 -       */
 -      public $authorURL = '';
 -      
 -      /**
 -       * style api version
 -       * @var string
 -       */
 -      public $apiVersion = Style::API_VERSION;
 -      
 -      /**
 -       * list of available font families
 -       * @var string[]
 -       */
 -      public $availableFontFamilies = [
 -              'Arial, Helvetica, sans-serif' => 'Arial',
 -              'Chicago, Impact, Compacta, sans-serif' => 'Chicago',
 -              '"Comic Sans MS", sans-serif' => 'Comic Sans',
 -              '"Courier New", Courier, monospace' => 'Courier New',
 -              'Geneva, Arial, Helvetica, sans-serif' => 'Geneva',
 -              'Georgia, "Times New Roman", Times, serif' => 'Georgia',
 -              'Helvetica, Verdana, sans-serif' => 'Helvetica',
 -              'Impact, Compacta, Chicago, sans-serif' => 'Impact',
 -              '"Lucida Sans", "Lucida Grande", Monaco, Geneva, sans-serif' => 'Lucida',
 -              '"Segoe UI", "DejaVu Sans", "Lucida Grande", Helvetica, sans-serif' => 'Segoe UI',
 -              'Tahoma, Arial, Helvetica, sans-serif' => 'Tahoma',
 -              '"Times New Roman", Times, Georgia, serif' => 'Times New Roman',
 -              '"Trebuchet MS", Arial, sans-serif' => 'Trebuchet MS',
 -              'Verdana, Helvetica, sans-serif' => 'Verdana'
 -      ];
 -      
 -      /**
 -       * list of available template groups
 -       * @var TemplateGroup[]
 -       */
 -      public $availableTemplateGroups = [];
 -      
 -      /**
 -       * list of available units
 -       * @var string[]
 -       */
 -      public $availableUnits = ['px', 'pt', 'rem', 'em', '%'];
 -      
 -      /**
 -       * @var array
 -       */
 -      public $colorCategories = [];
 -      
 -      /**
 -       * list of color variables
 -       * @var string[][]
 -       */
 -      public $colors = [];
 -      
 -      /**
 -       * copyright message
 -       * @var string
 -       */
 -      public $copyright = '';
 -      
 -      /**
 -       * list of global variables
 -       * @var array
 -       */
 -      public $globals = [];
 -      
 -      /**
 -       * tainted style
 -       * @var boolean
 -       */
 -      public $isTainted = true;
 -      
 -      /**
 -       * license name
 -       * @var string
 -       */
 -      public $license = '';
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public $neededPermissions = ['admin.style.canManageStyle'];
 -      
 -      /**
 -       * list of variables that were added after 3.0
 -       * @var string[]
 -       */
 -      public $newVariables = [
 -              // 3.1
 -              'wcfContentContainerBackground' => '3.1',
 -              'wcfContentContainerBorder' => '3.1',
 -              'wcfEditorButtonBackground' => '3.1',
 -              'wcfEditorButtonBackgroundActive' => '3.1',
 -              'wcfEditorButtonText' => '3.1',
 -              'wcfEditorButtonTextActive' => '3.1',
 -              'wcfEditorButtonTextDisabled' => '3.1',
 -              
 -              // 5.2
 -              'wcfEditorTableBorder' => '5.2',
 -      ];
 -      
 -      /**
 -       * style package name
 -       * @var string
 -       */
 -      public $packageName = '';
 -      
 -      /**
 -       * last change date
 -       * @var string
 -       */
 -      public $styleDate = '0000-00-00';
 -      
 -      /**
 -       * description
 -       * @var string
 -       */
 -      public $styleDescription = '';
 -      
 -      /**
 -       * style name
 -       * @var string
 -       */
 -      public $styleName = '';
 -      
 -      /**
 -       * version number
 -       * @var string
 -       */
 -      public $styleVersion = '';
 -      
 -      /**
 -       * template group id
 -       * @var integer
 -       */
 -      public $templateGroupID = 0;
 -      
 -      /**
 -       * temporary image hash
 -       * @var string
 -       */
 -      public $tmpHash = '';
 -      
 -      /**
 -       * @var string
 -       */
 -      public $styleTestFileDir;
 -      
 -      /**
 -       * list of variables and their value
 -       * @var string[]
 -       */
 -      public $variables = [];
 -      
 -      /**
 -       * list of specialized variables
 -       * @var string[]
 -       */
 -      public $specialVariables = [];
 -      
 -      /**
 -       * current scroll offsets before submitting the form
 -       * @var integer[]
 -       */
 -      public $scrollOffsets = [];
 -      
 -      /**
 -       * @var (null|UploadField)[]
 -       * @since 5.3
 -       */
 -      public $uploads = [];
 -      
 -      /**
 -       * @var (null|UploadField)[]
 -       * @since 5.3
 -       */
 -      public $removedUploads = [];
 -      
 -      /**
 -       * @var UploadField[]
 -       * @since 5.3
 -       */
 -      public $customAssets = [];
 -      
 -      public $supportedApiVersionsCompatibility = [
 -              '3.0' => '3.0',
 -              '3.1' => '3.1',
 -              '5.2' => '5.2 / 5.3',
 -      ];
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function readParameters() {
 -              parent::readParameters();
 -              
 -              I18nHandler::getInstance()->register('styleDescription');
 -              
 -              $this->setVariables();
 -              
 -              $this->rebuildUploadFields();
 -              
 -              if (empty($_POST)) {
 -                      $this->readStyleVariables();
 -              }
 -              
 -              $this->availableTemplateGroups = TemplateGroup::getSelectList([-1], 1);
 -              
 -              if (isset($_REQUEST['tmpHash'])) {
 -                      $this->tmpHash = StringUtil::trim($_REQUEST['tmpHash']);
 -              }
 -              if (empty($this->tmpHash)) {
 -                      $this->tmpHash = StringUtil::getRandomID();
 -              }
 -      }
 -      
 -      /**
 -       * @since       5.3
 -       */
 -      protected function getUploadFields() {
 -              return [
 -                      'image' => [
 -                              'size' => [
 -                                      'maxWidth' => Style::PREVIEW_IMAGE_MAX_WIDTH,
 -                                      'maxHeight' => Style::PREVIEW_IMAGE_MAX_WIDTH,
 -                                      'preserveAspectRatio' => false,
 -                              ],
 -                      ],
 -                      'image2x' => [
 -                              'size' => [
 -                                      'maxWidth' => 2 * Style::PREVIEW_IMAGE_MAX_WIDTH,
 -                                      'maxHeight' => 2 * Style::PREVIEW_IMAGE_MAX_WIDTH,
 -                                      'preserveAspectRatio' => false,
 -                              ],
 -                      ],
 -                      'pageLogo' => [
 -                              'allowSvgImage' => true,
 -                      ],
 -                      'pageLogoMobile' => [
 -                              'allowSvgImage' => true,
 -                      ],
 -                      'coverPhoto' => [
 -                              'size' => [
 -                                      'minWidth' => UserCoverPhoto::MIN_WIDTH,
 -                                      'minHeight' => UserCoverPhoto::MIN_HEIGHT,
 -                              ]
 -                      ],
 -                      'favicon' => [
 -                              'size' => [
 -                                      'resize' => false,
 -                                      'minWidth' => Style::FAVICON_IMAGE_WIDTH,
 -                                      'maxWidth' => Style::FAVICON_IMAGE_WIDTH,
 -                                      'minHeight' => Style::FAVICON_IMAGE_HEIGHT,
 -                                      'maxHeight' => Style::FAVICON_IMAGE_HEIGHT,
 -                              ]
 -                      ],
 -              ];
 -      }
 -      
 -      /**
 -       * @since       5.3
 -       */
 -      protected function rebuildUploadFields() {
 -              $handler = UploadHandler::getInstance();
 -              foreach ($this->getUploadFields() as $name => $options) {
 -                      if ($handler->isRegisteredFieldId($name)) {
 -                              $handler->unregisterUploadField($name);
 -                      }
 -                      $field = new UploadField($name);
 -                      $field->setImageOnly(true);
 -                      if (isset($options['allowSvgImage'])) {
 -                              $field->setAllowSvgImage($options['allowSvgImage']);
 -                      }
 -                      $field->maxFiles = 1;
 -                      $handler->registerUploadField($field);
 -              }
 -              
 -              // This field is special cased, because it may contain arbitrary data.
 -              $name = 'customAssets';
 -              if ($handler->isRegisteredFieldId($name)) {
 -                      $handler->unregisterUploadField($name);
 -              }
 -              $field = new UploadField($name);
 -              $field->setImageOnly(true);
 -              $field->setAllowSvgImage(true);
 -              $field->maxFiles = null;
 -              $handler->registerUploadField($field);
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function readFormParameters() {
 -              parent::readFormParameters();
 -              
 -              I18nHandler::getInstance()->readValues();
 -              
 -              $colors = [];
 -              foreach ($this->colors as $categoryName => $variables) {
 -                      foreach ($variables as $variable) {
 -                              $colors[] = $categoryName . ucfirst($variable);
 -                      }
 -              }
 -              
 -              // ignore everything except well-formed rgba()
 -              $regEx = new Regex('rgba\(\d{1,3}, \d{1,3}, \d{1,3}, (1|1\.00?|0|0?\.[0-9]{1,2})\)');
 -              foreach ($colors as $variableName) {
 -                      if (isset($_POST[$variableName]) && $regEx->match($_POST[$variableName])) {
 -                              $this->variables[$variableName] = $_POST[$variableName];
 -                      }
 -              }
 -              
 -              // read variables with units, e.g. 13px
 -              foreach ($this->globals as $variableName) {
 -                      if (isset($_POST[$variableName]) && is_numeric($_POST[$variableName])) {
 -                              if (isset($_POST[$variableName.'_unit']) && in_array($_POST[$variableName.'_unit'], $this->availableUnits)) {
 -                                      $this->variables[$variableName] = abs($_POST[$variableName]).$_POST[$variableName.'_unit'];
 -                              }
 -                      }
 -                      else {
 -                              // set default value
 -                              $this->variables[$variableName] = '0px';
 -                      }
 -              }
 -              
 -              // read specialized variables
 -              $integerValues = ['pageLogoHeight', 'pageLogoWidth'];
 -              foreach ($this->specialVariables as $variableName) {
 -                      if (isset($_POST[$variableName])) {
 -                              $this->variables[$variableName] = (in_array($variableName, $integerValues)) ? abs(intval($_POST[$variableName])) : StringUtil::trim($_POST[$variableName]);
 -                      }
 -              }
 -              $this->variables['useFluidLayout'] = isset($_POST['useFluidLayout']) ? 1 : 0;
 -              
 -              // style data
 -              if (isset($_POST['authorName'])) $this->authorName = StringUtil::trim($_POST['authorName']);
 -              if (isset($_POST['authorURL'])) $this->authorURL = StringUtil::trim($_POST['authorURL']);
 -              if (isset($_POST['copyright'])) $this->copyright = StringUtil::trim($_POST['copyright']);
 -              if (isset($_POST['license'])) $this->license = StringUtil::trim($_POST['license']);
 -              if (isset($_POST['packageName'])) $this->packageName = StringUtil::trim($_POST['packageName']);
 -              if (isset($_POST['styleDate'])) $this->styleDate = StringUtil::trim($_POST['styleDate']);
 -              if (isset($_POST['styleDescription'])) $this->styleDescription = StringUtil::trim($_POST['styleDescription']);
 -              if (isset($_POST['styleName'])) $this->styleName = StringUtil::trim($_POST['styleName']);
 -              if (isset($_POST['styleVersion'])) $this->styleVersion = StringUtil::trim($_POST['styleVersion']);
 -              if (isset($_POST['templateGroupID'])) $this->templateGroupID = intval($_POST['templateGroupID']);
 -              if (isset($_POST['apiVersion']) && in_array($_POST['apiVersion'], Style::$supportedApiVersions)) $this->apiVersion = $_POST['apiVersion'];
 -              
 -              // codemirror scroll offset
 -              if (isset($_POST['scrollOffsets']) && is_array($_POST['scrollOffsets'])) $this->scrollOffsets = ArrayUtil::toIntegerArray($_POST['scrollOffsets']); 
 -              
 -              $this->uploads = $this->removedUploads = [];
 -              foreach (array_keys($this->getUploadFields()) as $field) {
 -                      $removedFiles = UploadHandler::getInstance()->getRemovedFiledByFieldId($field);
 -                      if (!empty($removedFiles)) {
 -                              $this->removedUploads = array_merge($this->removedUploads, $removedFiles);
 -                      }
 -                      
 -                      $files = UploadHandler::getInstance()->getFilesByFieldId($field);
 -                      if (!empty($files)) {
 -                              $this->uploads[$field] = $files[0];
 -                      }
 -              }
 -              
 -              $this->customAssets = [
 -                      'removed' => UploadHandler::getInstance()->getRemovedFiledByFieldId('customAssets'),
 -                      'added' => UploadHandler::getInstance()->getFilesByFieldId('customAssets'),
 -              ];
 -      }
 -      
 -      /**
 -       * @since       5.3
 -       */
 -      protected function downloadGoogleFont() {
 -              $fontManager = FontManager::getInstance();
 -              $family = $this->variables['wcfFontFamilyGoogle'];
 -              if ($family) {
 -                      if (!$fontManager->isFamilyDownloaded($family)) {
 -                              try {
 -                                      $fontManager->downloadFamily($family);
 -                              }
 -                              catch (FontDownloadFailed $e) {
 -                                      throw new UserInputException('wcfFontFamilyGoogle', 'downloadFailed'.($e->getReason() ? '.'.$e->getReason() : ''));
 -                              }
 -                      }
 -              }
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function validate() {
 -              parent::validate();
 -              
 -              if (empty($this->authorName)) {
 -                      throw new UserInputException('authorName');
 -              }
 -              
 -              // validate date
 -              if (empty($this->styleDate)) {
 -                      throw new UserInputException('styleDate');
 -              }
 -              else {
 -                      try {
 -                              DateUtil::validateDate($this->styleDate);
 -                      }
 -                      catch (SystemException $e) {
 -                              throw new UserInputException('styleDate', 'invalid');
 -                      }
 -              }
 -              
 -              if (empty($this->styleName)) {
 -                      throw new UserInputException('styleName');
 -              }
 -              
 -              // validate version
 -              if (empty($this->styleVersion)) {
 -                      throw new UserInputException('styleVersion');
 -              }
 -              else if (!Package::isValidVersion($this->styleVersion)) {
 -                      throw new UserInputException('styleVersion', 'invalid');
 -              }
 -              
 -              // validate style package name
 -              if (!empty($this->packageName)) {
 -                      if (!Package::isValidPackageName($this->packageName)) {
 -                              throw new UserInputException('packageName', 'invalid');
 -                      }
 -                      
 -                      $this->enforcePackageNameRestriction();
 -              }
 -              
 -              // validate template group id
 -              if ($this->templateGroupID) {
 -                      if (!isset($this->availableTemplateGroups[$this->templateGroupID])) {
 -                              throw new UserInputException('templateGroupID');
 -                      }
 -              }
 -              
 -              if (!empty($this->variables['overrideScss'])) {
 -                      $this->parseOverrides();
 -              }
 -              
 -              $this->downloadGoogleFont();
 -              
 -              $this->validateIndividualScss();
 -              
 -              $this->validateApiVersion();
 -              
 -              $this->validateUploads();
 -      }
 -      
 -      /**
 -       * Validates the individual scss.
 -       * @throws      UserInputException
 -       * @since       5.3
 -       */
 -      public function validateIndividualScss() {
 -              $variables = array_merge(StyleCompiler::getDefaultVariables(), $this->variables);
 -              
 -              $this->styleTestFileDir = FileUtil::getTemporaryFilename('style_');
 -              FileUtil::makePath($this->styleTestFileDir);
 -              
 -              $result = StyleCompiler::getInstance()->testStyle($this->styleTestFileDir, $this->styleName, $this->apiVersion, false, $variables);
 -              
 -              if ($result !== null) {
 -                      rmdir($this->styleTestFileDir);
 -                      
 -                      throw new UserInputException('individualScss', [
 -                              'message' => $result->getMessage(),
 -                      ]);
 -              }
 -      }
 -      
 -      /**
 -       * Disallow the use of `com.woltlab.*` for package names to avoid accidental collisions.
 -       * 
 -       * @throws      UserInputException
 -       */
 -      protected function enforcePackageNameRestriction() {
 -              // 3rd party styles may never have com.woltlab.* as name
 -              if (strpos($this->packageName, 'com.woltlab.') !== false) {
 -                      throw new UserInputException('packageName', 'reserved');
 -              }
 -      }
 -      
 -      /**
 -       * Validates the style API version.
 -       * 
 -       * @throws      UserInputException
 -       * @since       3.1
 -       */
 -      protected function validateApiVersion() {
 -              if (!in_array($this->apiVersion, Style::$supportedApiVersions)) {
 -                      throw new UserInputException('apiVersion', 'invalid');
 -              }
 -      }
 -      
 -      /**
 -       * @since       5.3
 -       */
 -      protected function validateUploads() {
 -              foreach ($this->getUploadFields() as $field => $options) {
 -                      $files = UploadHandler::getInstance()->getFilesByFieldId($field);
 -                      if (count($files) > 1) {
 -                              throw new UserInputException($field, 'invalid');
 -                      }
 -                      if (empty($files)) continue;
 -                      
 -                      if (isset($options['size'])) {
 -                              $fileLocation = $files[0]->getLocation();
 -                              if (($imageData = getimagesize($fileLocation)) === false) {
 -                                      throw new UserInputException($field, 'invalid');
 -                              }
 -                              switch ($imageData[2]) {
 -                                      case IMAGETYPE_PNG:
 -                                      case IMAGETYPE_JPEG:
 -                                      case IMAGETYPE_GIF:
 -                                              // fine
 -                                      break;
 -                                      default:
 -                                              throw new UserInputException($field, 'invalid');
 -                              }
 -                              
 -                              $maxWidth = $options['size']['maxWidth'] ?? PHP_INT_MAX;
 -                              $maxHeight = $options['size']['maxHeight'] ?? PHP_INT_MAX;
 -                              $minWidth = $options['size']['minWidth'] ?? 0;
 -                              $minHeight = $options['size']['minHeight'] ?? 0;
 -                              
 -                              if ($options['size']['resize'] ?? true) {
 -                                      if ($imageData[0] > $maxWidth || $imageData[1] > $maxHeight) {
 -                                              $adapter = ImageHandler::getInstance()->getAdapter();
 -                                              $adapter->loadFile($fileLocation);
 -                                              $thumbnail = $adapter->createThumbnail(
 -                                                      $maxWidth,
 -                                                      $maxHeight,
 -                                                      $options['size']['preserveAspectRatio'] ?? true
 -                                              );
 -                                              $adapter->writeImage($thumbnail, $fileLocation);
 -                                              // Clear thumbnail as soon as possible to free up the memory.
 -                                              $thumbnail = null;
 -                                      }
 -                                      
 -                                      // Check again after scaling
 -                                      if (($imageData = getimagesize($fileLocation)) === false) {
 -                                              throw new UserInputException($field, 'invalid');
 -                                      }
 -                              }
 -                              
 -                              if ($imageData[0] > $maxWidth) {
 -                                      throw new UserInputException($field, 'maxWidth');
 -                              }
 -                              if ($imageData[0] > $maxHeight) {
 -                                      throw new UserInputException($field, 'maxHeight');
 -                              }
 -                              if ($imageData[0] < $minWidth) {
 -                                      throw new UserInputException($field, 'minWidth');
 -                              }
 -                              if ($imageData[0] < $minHeight) {
 -                                      throw new UserInputException($field, 'minHeight');
 -                              }
 -                      }
 -              }
 -      }
 -      
 -      /**
 -       * Validates LESS-variable overrides.
 -       * 
 -       * If an override is invalid, unknown or matches a variable covered by
 -       * the style editor itself, it will be silently discarded.
 -       * 
 -       * @param       string          $variableName
 -       * @throws      UserInputException
 -       */
 -      protected function parseOverrides($variableName = 'overrideScss') {
 -              static $colorNames = null;
 -              if ($colorNames === null) {
 -                      $colorNames = [];
 -                      foreach ($this->colors as $colorPrefix => $colors) {
 -                              foreach ($colors as $color) {
 -                                      $colorNames[] = $colorPrefix . ucfirst($color);
 -                              }
 -                      }
 -              }
 -              
 -              // get available variables
 -              $sql = "SELECT  variableName
 -                      FROM    wcf".WCF_N."_style_variable";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute();
 -              $variables = $statement->fetchAll(\PDO::FETCH_COLUMN);
 -              
 -              $lines = explode("\n", StringUtil::unifyNewlines($this->variables[$variableName]));
 -              $regEx = new Regex('^\$([a-zA-Z]+):\s*([@a-zA-Z0-9 ,\.\(\)\%\#-]+);$');
 -              $errors = [];
 -              foreach ($lines as $index => &$line) {
 -                      $line = StringUtil::trim($line);
 -                      
 -                      // ignore empty lines
 -                      if (empty($line)) {
 -                              unset($lines[$index]);
 -                              continue;
 -                      }
 -                      
 -                      if ($regEx->match($line)) {
 -                              $matches = $regEx->getMatches();
 -                              
 -                              // cannot override variables covered by style editor
 -                              if (in_array($matches[1], $colorNames) || in_array($matches[1], $this->globals) || in_array($matches[1], $this->specialVariables)) {
 -                                      $errors[] = [
 -                                              'error' => 'predefined',
 -                                              'text' => $matches[1]
 -                                      ];
 -                              }
 -                              else if (!in_array($matches[1], $variables)) {
 -                                      // unknown style variable
 -                                      $errors[] = [
 -                                              'error' => 'unknown',
 -                                              'text' => $matches[1]
 -                                      ];
 -                              }
 -                              else {
 -                                      $this->variables[$matches[1]] = $matches[2];
 -                              }
 -                      }
 -                      else {
 -                              // not valid
 -                              $errors[] = [
 -                                      'error' => 'invalid',
 -                                      'text' => $line
 -                              ];
 -                      }
 -              }
 -              
 -              $this->variables[$variableName] = implode("\n", $lines);
 -              
 -              if (!empty($errors)) {
 -                      throw new UserInputException($variableName, $errors);
 -              }
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function readData() {
 -              parent::readData();
 -              
 -              // parse global (unit) variables
 -              foreach ($this->globals as $variableName) {
 -                      if (preg_match('/(.*?)(' . implode('|', $this->availableUnits) . ')$/', $this->variables[$variableName], $match)) {
 -                              $this->variables[$variableName] = $match[1];
 -                              $this->variables[$variableName.'_unit'] = $match[2];
 -                      }
 -              }
 -              
 -              if (empty($_POST)) {
 -                      $this->setDefaultValues();
 -              }
 -      }
 -      
 -      /**
 -       * Sets available variables
 -       */
 -      protected function setVariables() {
 -              $this->colorCategories = [
 -                      'wcfHeader' => ['wcfHeader', 'wcfHeaderSearchBox', 'wcfHeaderMenu', 'wcfHeaderMenuDropdown'],
 -                      'wcfNavigation' => 'wcfNavigation',
 -                      'wcfSidebar' => ['wcfSidebar', 'wcfSidebarDimmed', 'wcfSidebarHeadline'],
 -                      'wcfContent' => ['wcfContent', 'wcfContentContainer', 'wcfContentDimmed', 'wcfContentHeadline'],
 -                      'wcfTabularBox' => 'wcfTabularBox',
 -                      'wcfInput' => ['wcfInput', 'wcfInputDisabled'],
 -                      'wcfButton' => ['wcfButton', 'wcfButtonPrimary', 'wcfButtonDisabled'],
 -                      'wcfEditor' => ['wcfEditorButton', 'wcfEditorTable'],
 -                      'wcfDropdown' => 'wcfDropdown',
 -                      'wcfStatus' => ['wcfStatusInfo', 'wcfStatusSuccess', 'wcfStatusWarning', 'wcfStatusError'],
 -                      'wcfFooterBox' => ['wcfFooterBox', 'wcfFooterBoxHeadline'],
 -                      'wcfFooter' => ['wcfFooter', 'wcfFooterHeadline', 'wcfFooterCopyright']
 -              ];
 -              
 -              $this->colors = [
 -                      'wcfHeader' => ['background', 'text', 'link', 'linkActive'],
 -                      'wcfHeaderSearchBox' => ['background', 'text', 'placeholder', 'placeholderActive', 'backgroundActive', 'textActive'],
 -                      'wcfHeaderMenu' => ['background', 'linkBackground', 'linkBackgroundActive', 'link', 'linkActive'],
 -                      'wcfHeaderMenuDropdown' => ['background', 'link', 'backgroundActive', 'linkActive'],
 -                      'wcfNavigation' => ['background', 'text', 'link', 'linkActive'],
 -                      'wcfSidebar' => ['background', 'text', 'link', 'linkActive'],
 -                      'wcfSidebarDimmed' => ['text', 'link', 'linkActive'],
 -                      'wcfSidebarHeadline' => ['text', 'link', 'linkActive'],
 -                      'wcfContent' => ['background', 'border', 'borderInner', 'text', 'link', 'linkActive'],
 -                      'wcfContentContainer' => ['background', 'border'],
 -                      'wcfContentDimmed' => ['text', 'link', 'linkActive'],
 -                      'wcfContentHeadline' => ['border', 'text', 'link', 'linkActive'],
 -                      'wcfTabularBox' => ['borderInner', 'headline', 'backgroundActive', 'headlineActive'],
 -                      'wcfInput' => ['label', 'background', 'border', 'text', 'placeholder', 'placeholderActive', 'backgroundActive', 'borderActive', 'textActive'],
 -                      'wcfInputDisabled' => ['background', 'border', 'text'],
 -                      'wcfButton' => ['background', 'text', 'backgroundActive', 'textActive'],
 -                      'wcfButtonPrimary' => ['background', 'text', 'backgroundActive', 'textActive'],
 -                      'wcfButtonDisabled' => ['background', 'text'],
 -                      'wcfEditorButton' => ['background', 'backgroundActive', 'text', 'textActive', 'textDisabled'],
 -                      'wcfEditorTable' => ['border'],
 -                      'wcfDropdown' => ['background', 'borderInner', 'text', 'link', 'backgroundActive', 'linkActive'],
 -                      'wcfStatusInfo' => ['background', 'border', 'text', 'link', 'linkActive'],
 -                      'wcfStatusSuccess' => ['background', 'border', 'text', 'link', 'linkActive'],
 -                      'wcfStatusWarning' => ['background', 'border', 'text', 'link', 'linkActive'],
 -                      'wcfStatusError' => ['background', 'border', 'text', 'link', 'linkActive'],
 -                      'wcfFooterBox' => ['background', 'text', 'link', 'linkActive'],
 -                      'wcfFooterBoxHeadline' => ['text', 'link', 'linkActive'],
 -                      'wcfFooter' => ['background', 'text', 'link', 'linkActive'],
 -                      'wcfFooterHeadline' => ['text', 'link', 'linkActive'],
 -                      'wcfFooterCopyright' => ['background', 'text', 'link', 'linkActive']
 -              ];
 -              
 -              // set global variables
 -              $this->globals = [
 -                      'wcfFontSizeSmall',
 -                      'wcfFontSizeDefault',
 -                      'wcfFontSizeHeadline',
 -                      'wcfFontSizeSection',
 -                      'wcfFontSizeTitle',
 -                      
 -                      'wcfLayoutFixedWidth',
 -                      'wcfLayoutMinWidth',
 -                      'wcfLayoutMaxWidth'
 -              ];
 -              
 -              // set specialized variables
 -              $this->specialVariables = [
 -                      'individualScss',
 -                      'overrideScss',
 -                      'pageLogoWidth',
 -                      'pageLogoHeight',
 -                      'useFluidLayout',
 -                      'wcfFontFamilyGoogle',
 -                      'wcfFontFamilyFallback'
 -              ];
 -              
 -              EventHandler::getInstance()->fireAction($this, 'setVariables');
 -      }
 -      
 -      /**
 -       * Reads style variable values.
 -       */
 -      protected function readStyleVariables() {
 -              $sql = "SELECT  variableName, defaultValue
 -                      FROM    wcf".WCF_N."_style_variable";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute();
 -              $this->variables = $statement->fetchMap('variableName', 'defaultValue');
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function save() {
 -              parent::save();
 -              
 -              // Remove control characters that break the SCSS parser, see https://stackoverflow.com/a/23066553
 -              $this->variables['individualScss'] = preg_replace('/[^\PC\s]/u', '', $this->variables['individualScss']);
 -              
 -              $this->objectAction = new StyleAction([], 'create', [
 -                      'data' => array_merge($this->additionalFields, [
 -                              'styleName' => $this->styleName,
 -                              'templateGroupID' => $this->templateGroupID,
 -                              'packageName' => $this->packageName,
 -                              'isDisabled' => 1, // styles are disabled by default
 -                              'isTainted' => 1,
 -                              'styleDescription' => '',
 -                              'styleVersion' => $this->styleVersion,
 -                              'styleDate' => $this->styleDate,
 -                              'copyright' => $this->copyright,
 -                              'license' => $this->license,
 -                              'authorName' => $this->authorName,
 -                              'authorURL' => $this->authorURL,
 -                              'apiVersion' => $this->apiVersion
 -                      ]),
 -                      'uploads' => $this->uploads,
 -                      'removedUploads' => $this->removedUploads,
 -                      'customAssets' => $this->customAssets,
 -                      'tmpHash' => $this->tmpHash,
 -                      'variables' => $this->variables,
 -              ]);
 -              $returnValues = $this->objectAction->executeAction();
 -              $style = $returnValues['returnValues'];
 -              
 -              // save style description
 -              I18nHandler::getInstance()->save('styleDescription', 'wcf.style.styleDescription'.$style->styleID, 'wcf.style');
 -              
 -              $styleEditor = new StyleEditor($style);
 -              $styleEditor->update([
 -                      'styleDescription' => 'wcf.style.styleDescription'.$style->styleID
 -              ]);
 -              
 -              // Do not save the compiled style, because the image path was unknown during the style generation. 
 -              if ($this->styleTestFileDir && file_exists($this->styleTestFileDir . '/style.css') && file_exists($this->styleTestFileDir . '/style-rtl.css')) {
 -                      unlink($this->styleTestFileDir . '/style.css');
 -                      unlink($this->styleTestFileDir . '/style-rtl.css');
 -                      
 -                      rmdir($this->styleTestFileDir);
 -              }
 -              
 -              // call saved event
 -              $this->saved();
 -              
 -              // reset variables
 -              $this->authorName = $this->authorURL = $this->copyright = $this->packageName = '';
 -              $this->license = $this->styleDate = $this->styleDescription = $this->styleName = $this->styleVersion = '';
 -              $this->setDefaultValues();
 -              $this->isTainted = true;
 -              $this->templateGroupID = 0;
 -              $this->styleTestFilename = null;
 -              $this->rebuildUploadFields();
 -              
 -              I18nHandler::getInstance()->reset();
 -              
 -              // reload variables
 -              $this->readStyleVariables();
 -              
 -              WCF::getTPL()->assign([
 -                      'success' => true,
 -                      'objectEditLink' => LinkHandler::getInstance()->getControllerLink(StyleEditForm::class, ['id' => $style->styleID]),
 -              ]);
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function assignVariables() {
 -              parent::assignVariables();
 -              
 -              I18nHandler::getInstance()->assignVariables();
 -              
 -              WCF::getTPL()->assign([
 -                      'action' => 'add',
 -                      'apiVersion' => $this->apiVersion,
 -                      'authorName' => $this->authorName,
 -                      'authorURL' => $this->authorURL,
 -                      'availableFontFamilies' => $this->availableFontFamilies,
 -                      'availableTemplateGroups' => $this->availableTemplateGroups,
 -                      'availableUnits' => $this->availableUnits,
 -                      'colorCategories' => $this->colorCategories,
 -                      'colors' => $this->colors,
 -                      'copyright' => $this->copyright,
 -                      'isTainted' => $this->isTainted,
 -                      'license' => $this->license,
 -                      'packageName' => $this->packageName,
 -                      'recommendedApiVersion' => Style::API_VERSION,
 -                      'styleDate' => $this->styleDate,
 -                      'styleDescription' => $this->styleDescription,
 -                      'styleName' => $this->styleName,
 -                      'styleVersion' => $this->styleVersion,
 -                      'templateGroupID' => $this->templateGroupID,
 -                      'tmpHash' => $this->tmpHash,
 -                      'variables' => $this->variables,
 -                      'supportedApiVersions' => Style::$supportedApiVersions,
 -                      'supportedApiVersionsCompatibility' => $this->supportedApiVersionsCompatibility,
 -                      'newVariables' => $this->newVariables,
 -                      'scrollOffsets' => $this->scrollOffsets,
 -                      'coverPhotoMinHeight' => UserCoverPhoto::MIN_HEIGHT,
 -                      'coverPhotoMinWidth' => UserCoverPhoto::MIN_WIDTH,
 -              ]);
 -      }
 -      
 -      protected function setDefaultValues() {
 -              $this->authorName = WCF::getUser()->username;
 -              $this->styleDate = gmdate('Y-m-d', TIME_NOW);
 -              $this->styleVersion = '1.0.0';
 -      }
 +class StyleAddForm extends AbstractForm
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    public $activeMenuItem = 'wcf.acp.menu.link.style.add';
 +
 +    /**
 +     * author's name
 +     * @var string
 +     */
 +    public $authorName = '';
 +
 +    /**
 +     * author's URL
 +     * @var string
 +     */
 +    public $authorURL = '';
 +
 +    /**
 +     * style api version
 +     * @var string
 +     */
 +    public $apiVersion = Style::API_VERSION;
 +
 +    /**
 +     * list of available font families
 +     * @var string[]
 +     */
 +    public $availableFontFamilies = [
 +        'Arial, Helvetica, sans-serif' => 'Arial',
 +        'Chicago, Impact, Compacta, sans-serif' => 'Chicago',
 +        '"Comic Sans MS", sans-serif' => 'Comic Sans',
 +        '"Courier New", Courier, monospace' => 'Courier New',
 +        'Geneva, Arial, Helvetica, sans-serif' => 'Geneva',
 +        'Georgia, "Times New Roman", Times, serif' => 'Georgia',
 +        'Helvetica, Verdana, sans-serif' => 'Helvetica',
 +        'Impact, Compacta, Chicago, sans-serif' => 'Impact',
 +        '"Lucida Sans", "Lucida Grande", Monaco, Geneva, sans-serif' => 'Lucida',
 +        '"Segoe UI", "DejaVu Sans", "Lucida Grande", Helvetica, sans-serif' => 'Segoe UI',
 +        'Tahoma, Arial, Helvetica, sans-serif' => 'Tahoma',
 +        '"Times New Roman", Times, Georgia, serif' => 'Times New Roman',
 +        '"Trebuchet MS", Arial, sans-serif' => 'Trebuchet MS',
 +        'Verdana, Helvetica, sans-serif' => 'Verdana',
 +    ];
 +
 +    /**
 +     * list of available template groups
 +     * @var TemplateGroup[]
 +     */
 +    public $availableTemplateGroups = [];
 +
 +    /**
 +     * list of available units
 +     * @var string[]
 +     */
 +    public $availableUnits = ['px', 'pt', 'rem', 'em', '%'];
 +
 +    /**
 +     * @var array
 +     */
 +    public $colorCategories = [];
 +
 +    /**
 +     * list of color variables
 +     * @var string[][]
 +     */
 +    public $colors = [];
 +
 +    /**
 +     * copyright message
 +     * @var string
 +     */
 +    public $copyright = '';
 +
 +    /**
 +     * list of global variables
 +     * @var array
 +     */
 +    public $globals = [];
 +
 +    /**
 +     * tainted style
 +     * @var bool
 +     */
 +    public $isTainted = true;
 +
 +    /**
 +     * license name
 +     * @var string
 +     */
 +    public $license = '';
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public $neededPermissions = ['admin.style.canManageStyle'];
 +
 +    /**
 +     * list of variables that were added after 3.0
 +     * @var string[]
 +     */
 +    public $newVariables = [
 +        // 3.1
 +        'wcfContentContainerBackground' => '3.1',
 +        'wcfContentContainerBorder' => '3.1',
 +        'wcfEditorButtonBackground' => '3.1',
 +        'wcfEditorButtonBackgroundActive' => '3.1',
 +        'wcfEditorButtonText' => '3.1',
 +        'wcfEditorButtonTextActive' => '3.1',
 +        'wcfEditorButtonTextDisabled' => '3.1',
 +
 +        // 5.2
 +        'wcfEditorTableBorder' => '5.2',
 +    ];
 +
 +    /**
 +     * style package name
 +     * @var string
 +     */
 +    public $packageName = '';
 +
 +    /**
 +     * last change date
 +     * @var string
 +     */
 +    public $styleDate = '0000-00-00';
 +
 +    /**
 +     * description
 +     * @var string
 +     */
 +    public $styleDescription = '';
 +
 +    /**
 +     * style name
 +     * @var string
 +     */
 +    public $styleName = '';
 +
 +    /**
 +     * version number
 +     * @var string
 +     */
 +    public $styleVersion = '';
 +
 +    /**
 +     * template group id
 +     * @var int
 +     */
 +    public $templateGroupID = 0;
 +
 +    /**
 +     * temporary image hash
 +     * @var string
 +     */
 +    public $tmpHash = '';
 +
 +    /**
 +     * @var string
 +     */
 +    public $styleTestFileDir;
 +
 +    /**
 +     * list of variables and their value
 +     * @var string[]
 +     */
 +    public $variables = [];
 +
 +    /**
 +     * list of specialized variables
 +     * @var string[]
 +     */
 +    public $specialVariables = [];
 +
 +    /**
 +     * current scroll offsets before submitting the form
 +     * @var int[]
 +     */
 +    public $scrollOffsets = [];
 +
 +    /**
 +     * @var (null|UploadField)[]
 +     * @since 5.3
 +     */
 +    public $uploads = [];
 +
 +    /**
 +     * @var (null|UploadField)[]
 +     * @since 5.3
 +     */
 +    public $removedUploads = [];
 +
 +    /**
 +     * @var UploadField[]
 +     * @since 5.3
 +     */
 +    public $customAssets = [];
 +
 +    public $supportedApiVersionsCompatibility = [
 +        '3.0' => '3.0',
 +        '3.1' => '3.1',
 +        '5.2' => '5.2 / 5.3',
 +    ];
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function readParameters()
 +    {
 +        parent::readParameters();
 +
 +        I18nHandler::getInstance()->register('styleDescription');
 +
 +        $this->setVariables();
 +
 +        $this->rebuildUploadFields();
 +
 +        if (empty($_POST)) {
 +            $this->readStyleVariables();
 +        }
 +
 +        $this->availableTemplateGroups = TemplateGroup::getSelectList([-1], 1);
 +
 +        if (isset($_REQUEST['tmpHash'])) {
 +            $this->tmpHash = StringUtil::trim($_REQUEST['tmpHash']);
 +        }
 +        if (empty($this->tmpHash)) {
 +            $this->tmpHash = StringUtil::getRandomID();
 +        }
 +    }
 +
 +    /**
 +     * @since   5.3
 +     */
 +    protected function getUploadFields()
 +    {
 +        return [
 +            'image' => [
 +                'size' => [
 +                    'maxWidth' => Style::PREVIEW_IMAGE_MAX_WIDTH,
 +                    'maxHeight' => Style::PREVIEW_IMAGE_MAX_WIDTH,
 +                    'preserveAspectRatio' => false,
 +                ],
 +            ],
 +            'image2x' => [
 +                'size' => [
 +                    'maxWidth' => 2 * Style::PREVIEW_IMAGE_MAX_WIDTH,
 +                    'maxHeight' => 2 * Style::PREVIEW_IMAGE_MAX_WIDTH,
 +                    'preserveAspectRatio' => false,
 +                ],
 +            ],
 +            'pageLogo' => [
 +                'allowSvgImage' => true,
 +            ],
 +            'pageLogoMobile' => [
 +                'allowSvgImage' => true,
 +            ],
 +            'coverPhoto' => [
 +                'size' => [
 +                    'minWidth' => UserCoverPhoto::MIN_WIDTH,
 +                    'maxWidth' => UserCoverPhoto::MAX_WIDTH,
 +                    'minHeight' => UserCoverPhoto::MIN_HEIGHT,
 +                    'maxHeight' => UserCoverPhoto::MAX_HEIGHT,
 +                ],
 +            ],
 +            'favicon' => [
 +                'size' => [
 +                    'resize' => false,
 +                    'minWidth' => Style::FAVICON_IMAGE_WIDTH,
 +                    'maxWidth' => Style::FAVICON_IMAGE_WIDTH,
 +                    'minHeight' => Style::FAVICON_IMAGE_HEIGHT,
 +                    'maxHeight' => Style::FAVICON_IMAGE_HEIGHT,
 +                ],
 +            ],
 +        ];
 +    }
 +
 +    /**
 +     * @since   5.3
 +     */
 +    protected function rebuildUploadFields()
 +    {
 +        $handler = UploadHandler::getInstance();
 +        foreach ($this->getUploadFields() as $name => $options) {
 +            if ($handler->isRegisteredFieldId($name)) {
 +                $handler->unregisterUploadField($name);
 +            }
 +            $field = new UploadField($name);
 +            $field->setImageOnly(true);
 +            if (isset($options['allowSvgImage'])) {
 +                $field->setAllowSvgImage($options['allowSvgImage']);
 +            }
 +            $field->maxFiles = 1;
 +            $handler->registerUploadField($field);
 +        }
 +
 +        // This field is special cased, because it may contain arbitrary data.
 +        $name = 'customAssets';
 +        if ($handler->isRegisteredFieldId($name)) {
 +            $handler->unregisterUploadField($name);
 +        }
 +        $field = new UploadField($name);
 +        $field->setImageOnly(true);
 +        $field->setAllowSvgImage(true);
 +        $field->maxFiles = null;
 +        $handler->registerUploadField($field);
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function readFormParameters()
 +    {
 +        parent::readFormParameters();
 +
 +        I18nHandler::getInstance()->readValues();
 +
 +        $colors = [];
 +        foreach ($this->colors as $categoryName => $variables) {
 +            foreach ($variables as $variable) {
 +                $colors[] = $categoryName . \ucfirst($variable);
 +            }
 +        }
 +
 +        // ignore everything except well-formed rgba()
 +        $regEx = new Regex('rgba\(\d{1,3}, \d{1,3}, \d{1,3}, (1|1\.00?|0|0?\.[0-9]{1,2})\)');
 +        foreach ($colors as $variableName) {
 +            if (isset($_POST[$variableName]) && $regEx->match($_POST[$variableName])) {
 +                $this->variables[$variableName] = $_POST[$variableName];
 +            }
 +        }
 +
 +        // read variables with units, e.g. 13px
 +        foreach ($this->globals as $variableName) {
 +            if (isset($_POST[$variableName]) && \is_numeric($_POST[$variableName])) {
 +                if (
 +                    isset($_POST[$variableName . '_unit']) && \in_array(
 +                        $_POST[$variableName . '_unit'],
 +                        $this->availableUnits
 +                    )
 +                ) {
 +                    $this->variables[$variableName] = \abs($_POST[$variableName]) . $_POST[$variableName . '_unit'];
 +                }
 +            } else {
 +                // set default value
 +                $this->variables[$variableName] = '0px';
 +            }
 +        }
 +
 +        // read specialized variables
 +        $integerValues = ['pageLogoHeight', 'pageLogoWidth'];
 +        foreach ($this->specialVariables as $variableName) {
 +            if (isset($_POST[$variableName])) {
 +                $this->variables[$variableName] = (\in_array(
 +                    $variableName,
 +                    $integerValues
 +                )) ? \abs(\intval($_POST[$variableName])) : StringUtil::trim($_POST[$variableName]);
 +            }
 +        }
 +        $this->variables['useFluidLayout'] = isset($_POST['useFluidLayout']) ? 1 : 0;
 +
 +        // style data
 +        if (isset($_POST['authorName'])) {
 +            $this->authorName = StringUtil::trim($_POST['authorName']);
 +        }
 +        if (isset($_POST['authorURL'])) {
 +            $this->authorURL = StringUtil::trim($_POST['authorURL']);
 +        }
 +        if (isset($_POST['copyright'])) {
 +            $this->copyright = StringUtil::trim($_POST['copyright']);
 +        }
 +        if (isset($_POST['license'])) {
 +            $this->license = StringUtil::trim($_POST['license']);
 +        }
 +        if (isset($_POST['packageName'])) {
 +            $this->packageName = StringUtil::trim($_POST['packageName']);
 +        }
 +        if (isset($_POST['styleDate'])) {
 +            $this->styleDate = StringUtil::trim($_POST['styleDate']);
 +        }
 +        if (isset($_POST['styleDescription'])) {
 +            $this->styleDescription = StringUtil::trim($_POST['styleDescription']);
 +        }
 +        if (isset($_POST['styleName'])) {
 +            $this->styleName = StringUtil::trim($_POST['styleName']);
 +        }
 +        if (isset($_POST['styleVersion'])) {
 +            $this->styleVersion = StringUtil::trim($_POST['styleVersion']);
 +        }
 +        if (isset($_POST['templateGroupID'])) {
 +            $this->templateGroupID = \intval($_POST['templateGroupID']);
 +        }
 +        if (isset($_POST['apiVersion']) && \in_array($_POST['apiVersion'], Style::$supportedApiVersions)) {
 +            $this->apiVersion = $_POST['apiVersion'];
 +        }
 +
 +        // codemirror scroll offset
 +        if (isset($_POST['scrollOffsets']) && \is_array($_POST['scrollOffsets'])) {
 +            $this->scrollOffsets = ArrayUtil::toIntegerArray($_POST['scrollOffsets']);
 +        }
 +
 +        $this->uploads = $this->removedUploads = [];
 +        foreach (\array_keys($this->getUploadFields()) as $field) {
 +            $removedFiles = UploadHandler::getInstance()->getRemovedFiledByFieldId($field);
 +            if (!empty($removedFiles)) {
 +                $this->removedUploads = \array_merge($this->removedUploads, $removedFiles);
 +            }
 +
 +            $files = UploadHandler::getInstance()->getFilesByFieldId($field);
 +            if (!empty($files)) {
 +                $this->uploads[$field] = $files[0];
 +            }
 +        }
 +
 +        $this->customAssets = [
 +            'removed' => UploadHandler::getInstance()->getRemovedFiledByFieldId('customAssets'),
 +            'added' => UploadHandler::getInstance()->getFilesByFieldId('customAssets'),
 +        ];
 +    }
 +
 +    /**
 +     * @since   5.3
 +     */
 +    protected function downloadGoogleFont()
 +    {
 +        $fontManager = FontManager::getInstance();
 +        $family = $this->variables['wcfFontFamilyGoogle'];
 +        if ($family) {
 +            if (!$fontManager->isFamilyDownloaded($family)) {
 +                try {
 +                    $fontManager->downloadFamily($family);
 +                } catch (FontDownloadFailed $e) {
 +                    throw new UserInputException(
 +                        'wcfFontFamilyGoogle',
 +                        'downloadFailed' . ($e->getReason() ? '.' . $e->getReason() : '')
 +                    );
 +                }
 +            }
 +        }
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function validate()
 +    {
 +        parent::validate();
 +
 +        if (empty($this->authorName)) {
 +            throw new UserInputException('authorName');
 +        }
 +
 +        // validate date
 +        if (empty($this->styleDate)) {
 +            throw new UserInputException('styleDate');
 +        } else {
 +            try {
 +                DateUtil::validateDate($this->styleDate);
 +            } catch (SystemException $e) {
 +                throw new UserInputException('styleDate', 'invalid');
 +            }
 +        }
 +
 +        if (empty($this->styleName)) {
 +            throw new UserInputException('styleName');
 +        }
 +
 +        // validate version
 +        if (empty($this->styleVersion)) {
 +            throw new UserInputException('styleVersion');
 +        } elseif (!Package::isValidVersion($this->styleVersion)) {
 +            throw new UserInputException('styleVersion', 'invalid');
 +        }
 +
 +        // validate style package name
 +        if (!empty($this->packageName)) {
 +            if (!Package::isValidPackageName($this->packageName)) {
 +                throw new UserInputException('packageName', 'invalid');
 +            }
 +
 +            $this->enforcePackageNameRestriction();
 +        }
 +
 +        // validate template group id
 +        if ($this->templateGroupID) {
 +            if (!isset($this->availableTemplateGroups[$this->templateGroupID])) {
 +                throw new UserInputException('templateGroupID');
 +            }
 +        }
 +
 +        if (!empty($this->variables['overrideScss'])) {
 +            $this->parseOverrides();
 +        }
 +
++        $this->downloadGoogleFont();
++
 +        $this->validateIndividualScss();
 +
 +        $this->validateApiVersion();
 +
 +        $this->validateUploads();
-         $this->downloadGoogleFont();
 +    }
 +
 +    /**
 +     * Validates the individual scss.
 +     * @throws      UserInputException
 +     * @since       5.3
 +     */
 +    public function validateIndividualScss()
 +    {
 +        $variables = \array_merge(StyleCompiler::getDefaultVariables(), $this->variables);
 +
 +        $this->styleTestFileDir = FileUtil::getTemporaryFilename('style_');
 +        FileUtil::makePath($this->styleTestFileDir);
 +
 +        $result = StyleCompiler::getInstance()->testStyle(
 +            $this->styleTestFileDir,
 +            $this->styleName,
 +            $this->apiVersion,
 +            false,
 +            $variables
 +        );
 +
 +        if ($result !== null) {
 +            \rmdir($this->styleTestFileDir);
 +
 +            throw new UserInputException('individualScss', [
 +                'message' => $result->getMessage(),
 +            ]);
 +        }
 +    }
 +
 +    /**
 +     * Disallow the use of `com.woltlab.*` for package names to avoid accidental collisions.
 +     *
 +     * @throws      UserInputException
 +     */
 +    protected function enforcePackageNameRestriction()
 +    {
 +        // 3rd party styles may never have com.woltlab.* as name
 +        if (\strpos($this->packageName, 'com.woltlab.') !== false) {
 +            throw new UserInputException('packageName', 'reserved');
 +        }
 +    }
 +
 +    /**
 +     * Validates the style API version.
 +     *
 +     * @throws      UserInputException
 +     * @since       3.1
 +     */
 +    protected function validateApiVersion()
 +    {
 +        if (!\in_array($this->apiVersion, Style::$supportedApiVersions)) {
 +            throw new UserInputException('apiVersion', 'invalid');
 +        }
 +    }
 +
 +    /**
 +     * @since   5.3
 +     */
 +    protected function validateUploads()
 +    {
 +        foreach ($this->getUploadFields() as $field => $options) {
 +            $files = UploadHandler::getInstance()->getFilesByFieldId($field);
 +            if (\count($files) > 1) {
 +                throw new UserInputException($field, 'invalid');
 +            }
 +            if (empty($files)) {
 +                continue;
 +            }
 +
 +            if (isset($options['size'])) {
 +                $fileLocation = $files[0]->getLocation();
 +                if (($imageData = \getimagesize($fileLocation)) === false) {
 +                    throw new UserInputException($field, 'invalid');
 +                }
 +                switch ($imageData[2]) {
 +                    case \IMAGETYPE_PNG:
 +                    case \IMAGETYPE_JPEG:
 +                    case \IMAGETYPE_GIF:
 +                        // fine
 +                        break;
 +                    default:
 +                        throw new UserInputException($field, 'invalid');
 +                }
 +
 +                $maxWidth = $options['size']['maxWidth'] ?? \PHP_INT_MAX;
 +                $maxHeight = $options['size']['maxHeight'] ?? \PHP_INT_MAX;
 +                $minWidth = $options['size']['minWidth'] ?? 0;
 +                $minHeight = $options['size']['minHeight'] ?? 0;
 +
 +                if ($options['size']['resize'] ?? true) {
 +                    if ($imageData[0] > $maxWidth || $imageData[1] > $maxHeight) {
 +                        $adapter = ImageHandler::getInstance()->getAdapter();
 +                        $adapter->loadFile($fileLocation);
 +                        $thumbnail = $adapter->createThumbnail(
 +                            $maxWidth,
 +                            $maxHeight,
 +                            $options['size']['preserveAspectRatio'] ?? true
 +                        );
 +                        $adapter->writeImage($thumbnail, $fileLocation);
 +                        // Clear thumbnail as soon as possible to free up the memory.
 +                        $thumbnail = null;
 +                    }
 +
 +                    // Check again after scaling
 +                    if (($imageData = \getimagesize($fileLocation)) === false) {
 +                        throw new UserInputException($field, 'invalid');
 +                    }
 +                }
 +
 +                if ($imageData[0] > $maxWidth) {
 +                    throw new UserInputException($field, 'maxWidth');
 +                }
 +                if ($imageData[1] > $maxHeight) {
 +                    throw new UserInputException($field, 'maxHeight');
 +                }
 +                if ($imageData[0] < $minWidth) {
 +                    throw new UserInputException($field, 'minWidth');
 +                }
 +                if ($imageData[1] < $minHeight) {
 +                    throw new UserInputException($field, 'minHeight');
 +                }
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Validates LESS-variable overrides.
 +     *
 +     * If an override is invalid, unknown or matches a variable covered by
 +     * the style editor itself, it will be silently discarded.
 +     *
 +     * @param string $variableName
 +     * @throws      UserInputException
 +     */
 +    protected function parseOverrides($variableName = 'overrideScss')
 +    {
 +        static $colorNames = null;
 +        if ($colorNames === null) {
 +            $colorNames = [];
 +            foreach ($this->colors as $colorPrefix => $colors) {
 +                foreach ($colors as $color) {
 +                    $colorNames[] = $colorPrefix . \ucfirst($color);
 +                }
 +            }
 +        }
 +
 +        // get available variables
 +        $sql = "SELECT  variableName
 +                FROM    wcf" . WCF_N . "_style_variable";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute();
 +        $variables = $statement->fetchAll(\PDO::FETCH_COLUMN);
 +
 +        $lines = \explode("\n", StringUtil::unifyNewlines($this->variables[$variableName]));
 +        $regEx = new Regex('^\$([a-zA-Z]+):\s*([@a-zA-Z0-9 ,\.\(\)\%\#-]+);$');
 +        $errors = [];
 +        foreach ($lines as $index => &$line) {
 +            $line = StringUtil::trim($line);
 +
 +            // ignore empty lines
 +            if (empty($line)) {
 +                unset($lines[$index]);
 +                continue;
 +            }
 +
 +            if ($regEx->match($line)) {
 +                $matches = $regEx->getMatches();
 +
 +                // cannot override variables covered by style editor
 +                if (
 +                    \in_array($matches[1], $colorNames) || \in_array(
 +                        $matches[1],
 +                        $this->globals
 +                    ) || \in_array($matches[1], $this->specialVariables)
 +                ) {
 +                    $errors[] = [
 +                        'error' => 'predefined',
 +                        'text' => $matches[1],
 +                    ];
 +                } elseif (!\in_array($matches[1], $variables)) {
 +                    // unknown style variable
 +                    $errors[] = [
 +                        'error' => 'unknown',
 +                        'text' => $matches[1],
 +                    ];
 +                } else {
 +                    $this->variables[$matches[1]] = $matches[2];
 +                }
 +            } else {
 +                // not valid
 +                $errors[] = [
 +                    'error' => 'invalid',
 +                    'text' => $line,
 +                ];
 +            }
 +        }
 +
 +        $this->variables[$variableName] = \implode("\n", $lines);
 +
 +        if (!empty($errors)) {
 +            throw new UserInputException($variableName, $errors);
 +        }
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function readData()
 +    {
 +        parent::readData();
 +
 +        // parse global (unit) variables
 +        foreach ($this->globals as $variableName) {
 +            if (
 +                \preg_match(
 +                    '/(.*?)(' . \implode('|', $this->availableUnits) . ')$/',
 +                    $this->variables[$variableName],
 +                    $match
 +                )
 +            ) {
 +                $this->variables[$variableName] = $match[1];
 +                $this->variables[$variableName . '_unit'] = $match[2];
 +            }
 +        }
 +
 +        if (empty($_POST)) {
 +            $this->setDefaultValues();
 +        }
 +    }
 +
 +    /**
 +     * Sets available variables
 +     */
 +    protected function setVariables()
 +    {
 +        $this->colorCategories = [
 +            'wcfHeader' => ['wcfHeader', 'wcfHeaderSearchBox', 'wcfHeaderMenu', 'wcfHeaderMenuDropdown'],
 +            'wcfNavigation' => 'wcfNavigation',
 +            'wcfSidebar' => ['wcfSidebar', 'wcfSidebarDimmed', 'wcfSidebarHeadline'],
 +            'wcfContent' => ['wcfContent', 'wcfContentContainer', 'wcfContentDimmed', 'wcfContentHeadline'],
 +            'wcfTabularBox' => 'wcfTabularBox',
 +            'wcfInput' => ['wcfInput', 'wcfInputDisabled'],
 +            'wcfButton' => ['wcfButton', 'wcfButtonPrimary', 'wcfButtonDisabled'],
 +            'wcfEditor' => ['wcfEditorButton', 'wcfEditorTable'],
 +            'wcfDropdown' => 'wcfDropdown',
 +            'wcfStatus' => ['wcfStatusInfo', 'wcfStatusSuccess', 'wcfStatusWarning', 'wcfStatusError'],
 +            'wcfFooterBox' => ['wcfFooterBox', 'wcfFooterBoxHeadline'],
 +            'wcfFooter' => ['wcfFooter', 'wcfFooterHeadline', 'wcfFooterCopyright'],
 +        ];
 +
 +        $this->colors = [
 +            'wcfHeader' => ['background', 'text', 'link', 'linkActive'],
 +            'wcfHeaderSearchBox' => [
 +                'background',
 +                'text',
 +                'placeholder',
 +                'placeholderActive',
 +                'backgroundActive',
 +                'textActive',
 +            ],
 +            'wcfHeaderMenu' => ['background', 'linkBackground', 'linkBackgroundActive', 'link', 'linkActive'],
 +            'wcfHeaderMenuDropdown' => ['background', 'link', 'backgroundActive', 'linkActive'],
 +            'wcfNavigation' => ['background', 'text', 'link', 'linkActive'],
 +            'wcfSidebar' => ['background', 'text', 'link', 'linkActive'],
 +            'wcfSidebarDimmed' => ['text', 'link', 'linkActive'],
 +            'wcfSidebarHeadline' => ['text', 'link', 'linkActive'],
 +            'wcfContent' => ['background', 'border', 'borderInner', 'text', 'link', 'linkActive'],
 +            'wcfContentContainer' => ['background', 'border'],
 +            'wcfContentDimmed' => ['text', 'link', 'linkActive'],
 +            'wcfContentHeadline' => ['border', 'text', 'link', 'linkActive'],
 +            'wcfTabularBox' => ['borderInner', 'headline', 'backgroundActive', 'headlineActive'],
 +            'wcfInput' => [
 +                'label',
 +                'background',
 +                'border',
 +                'text',
 +                'placeholder',
 +                'placeholderActive',
 +                'backgroundActive',
 +                'borderActive',
 +                'textActive',
 +            ],
 +            'wcfInputDisabled' => ['background', 'border', 'text'],
 +            'wcfButton' => ['background', 'text', 'backgroundActive', 'textActive'],
 +            'wcfButtonPrimary' => ['background', 'text', 'backgroundActive', 'textActive'],
 +            'wcfButtonDisabled' => ['background', 'text'],
 +            'wcfEditorButton' => ['background', 'backgroundActive', 'text', 'textActive', 'textDisabled'],
 +            'wcfEditorTable' => ['border'],
 +            'wcfDropdown' => ['background', 'borderInner', 'text', 'link', 'backgroundActive', 'linkActive'],
 +            'wcfStatusInfo' => ['background', 'border', 'text', 'link', 'linkActive'],
 +            'wcfStatusSuccess' => ['background', 'border', 'text', 'link', 'linkActive'],
 +            'wcfStatusWarning' => ['background', 'border', 'text', 'link', 'linkActive'],
 +            'wcfStatusError' => ['background', 'border', 'text', 'link', 'linkActive'],
 +            'wcfFooterBox' => ['background', 'text', 'link', 'linkActive'],
 +            'wcfFooterBoxHeadline' => ['text', 'link', 'linkActive'],
 +            'wcfFooter' => ['background', 'text', 'link', 'linkActive'],
 +            'wcfFooterHeadline' => ['text', 'link', 'linkActive'],
 +            'wcfFooterCopyright' => ['background', 'text', 'link', 'linkActive'],
 +        ];
 +
 +        // set global variables
 +        $this->globals = [
 +            'wcfFontSizeSmall',
 +            'wcfFontSizeDefault',
 +            'wcfFontSizeHeadline',
 +            'wcfFontSizeSection',
 +            'wcfFontSizeTitle',
 +
 +            'wcfLayoutFixedWidth',
 +            'wcfLayoutMinWidth',
 +            'wcfLayoutMaxWidth',
 +        ];
 +
 +        // set specialized variables
 +        $this->specialVariables = [
 +            'individualScss',
 +            'overrideScss',
 +            'pageLogoWidth',
 +            'pageLogoHeight',
 +            'useFluidLayout',
 +            'wcfFontFamilyGoogle',
 +            'wcfFontFamilyFallback',
 +        ];
 +
 +        EventHandler::getInstance()->fireAction($this, 'setVariables');
 +    }
 +
 +    /**
 +     * Reads style variable values.
 +     */
 +    protected function readStyleVariables()
 +    {
 +        $sql = "SELECT  variableName, defaultValue
 +                FROM    wcf" . WCF_N . "_style_variable";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute();
 +        $this->variables = $statement->fetchMap('variableName', 'defaultValue');
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function save()
 +    {
 +        parent::save();
 +
 +        // Remove control characters that break the SCSS parser, see https://stackoverflow.com/a/23066553
 +        $this->variables['individualScss'] = \preg_replace('/[^\PC\s]/u', '', $this->variables['individualScss']);
 +
 +        $this->objectAction = new StyleAction([], 'create', [
 +            'data' => \array_merge($this->additionalFields, [
 +                'styleName' => $this->styleName,
 +                'templateGroupID' => $this->templateGroupID,
 +                'packageName' => $this->packageName,
 +                'isDisabled' => 1, // styles are disabled by default
 +                'isTainted' => 1,
 +                'styleDescription' => '',
 +                'styleVersion' => $this->styleVersion,
 +                'styleDate' => $this->styleDate,
 +                'copyright' => $this->copyright,
 +                'license' => $this->license,
 +                'authorName' => $this->authorName,
 +                'authorURL' => $this->authorURL,
 +                'apiVersion' => $this->apiVersion,
 +            ]),
 +            'uploads' => $this->uploads,
 +            'removedUploads' => $this->removedUploads,
 +            'customAssets' => $this->customAssets,
 +            'tmpHash' => $this->tmpHash,
 +            'variables' => $this->variables,
 +        ]);
 +        $returnValues = $this->objectAction->executeAction();
 +        $style = $returnValues['returnValues'];
 +
 +        // save style description
 +        I18nHandler::getInstance()->save(
 +            'styleDescription',
 +            'wcf.style.styleDescription' . $style->styleID,
 +            'wcf.style'
 +        );
 +
 +        $styleEditor = new StyleEditor($style);
 +        $styleEditor->update([
 +            'styleDescription' => 'wcf.style.styleDescription' . $style->styleID,
 +        ]);
 +
 +        // Do not save the compiled style, because the image path was unknown during the style generation.
 +        if ($this->styleTestFileDir && \file_exists($this->styleTestFileDir . '/style.css') && \file_exists($this->styleTestFileDir . '/style-rtl.css')) {
 +            \unlink($this->styleTestFileDir . '/style.css');
 +            \unlink($this->styleTestFileDir . '/style-rtl.css');
 +
 +            \rmdir($this->styleTestFileDir);
 +        }
 +
 +        // call saved event
 +        $this->saved();
 +
 +        // reset variables
 +        $this->authorName = $this->authorURL = $this->copyright = $this->packageName = '';
 +        $this->license = $this->styleDate = $this->styleDescription = $this->styleName = $this->styleVersion = '';
 +        $this->setDefaultValues();
 +        $this->isTainted = true;
 +        $this->templateGroupID = 0;
 +        $this->styleTestFilename = null;
 +        $this->rebuildUploadFields();
 +
 +        I18nHandler::getInstance()->reset();
 +
 +        // reload variables
 +        $this->readStyleVariables();
 +
 +        WCF::getTPL()->assign([
 +            'success' => true,
 +            'objectEditLink' => LinkHandler::getInstance()->getControllerLink(
 +                StyleEditForm::class,
 +                ['id' => $style->styleID]
 +            ),
 +        ]);
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function assignVariables()
 +    {
 +        parent::assignVariables();
 +
 +        I18nHandler::getInstance()->assignVariables();
 +
 +        WCF::getTPL()->assign([
 +            'action' => 'add',
 +            'apiVersion' => $this->apiVersion,
 +            'authorName' => $this->authorName,
 +            'authorURL' => $this->authorURL,
 +            'availableFontFamilies' => $this->availableFontFamilies,
 +            'availableTemplateGroups' => $this->availableTemplateGroups,
 +            'availableUnits' => $this->availableUnits,
 +            'colorCategories' => $this->colorCategories,
 +            'colors' => $this->colors,
 +            'copyright' => $this->copyright,
 +            'isTainted' => $this->isTainted,
 +            'license' => $this->license,
 +            'packageName' => $this->packageName,
 +            'recommendedApiVersion' => Style::API_VERSION,
 +            'styleDate' => $this->styleDate,
 +            'styleDescription' => $this->styleDescription,
 +            'styleName' => $this->styleName,
 +            'styleVersion' => $this->styleVersion,
 +            'templateGroupID' => $this->templateGroupID,
 +            'tmpHash' => $this->tmpHash,
 +            'variables' => $this->variables,
 +            'supportedApiVersions' => Style::$supportedApiVersions,
 +            'supportedApiVersionsCompatibility' => $this->supportedApiVersionsCompatibility,
 +            'newVariables' => $this->newVariables,
 +            'scrollOffsets' => $this->scrollOffsets,
 +            'coverPhotoMinHeight' => UserCoverPhoto::MIN_HEIGHT,
 +            'coverPhotoMaxHeight' => UserCoverPhoto::MAX_HEIGHT,
 +            'coverPhotoMinWidth' => UserCoverPhoto::MIN_WIDTH,
 +            'coverPhotoMaxWidth' => UserCoverPhoto::MAX_WIDTH,
 +        ]);
 +    }
 +
 +    protected function setDefaultValues()
 +    {
 +        $this->authorName = WCF::getUser()->username;
 +        $this->styleDate = \gmdate('Y-m-d', TIME_NOW);
 +        $this->styleVersion = '1.0.0';
 +    }
  }