Merge branch '5.3' into 5.4
authorTim Düsterhus <duesterhus@woltlab.com>
Thu, 12 Aug 2021 08:09:17 +0000 (10:09 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Thu, 12 Aug 2021 08:09:17 +0000 (10:09 +0200)
1  2 
wcfsetup/install/files/lib/data/style/StyleEditor.class.php

index a0c38dde9c4fb8d697c46c3a16a4d90492f8e469,007cf470e41bb1d44789d571b6c36e58a7566051..045f2fec16e46a7dd582bc69667c7bb7dee25b0e
@@@ -34,1218 -31,1083 +34,1219 @@@ use wcf\util\XMLWriter
  
  /**
   * Provides functions to edit, import, export and delete a style.
 - * 
 - * @author    Marcel Werk
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\Data\Style
 - * 
 - * @method    Style   getDecoratedObject()
 - * @mixin     Style
 + *
 + * @author  Marcel Werk
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\Data\Style
 + *
 + * @method  Style   getDecoratedObject()
 + * @mixin   Style
   */
 -class StyleEditor extends DatabaseObjectEditor implements IEditableCachedObject {
 -      const EXCLUDE_WCF_VERSION = '6.0.0 Alpha 1';
 -      const INFO_FILE = 'style.xml';
 -      const VALID_IMAGE_EXTENSIONS = ['gif', 'jpg', 'jpeg', 'png', 'svg', 'xml', 'json'];
 -      
 -      /**
 -       * list of compatible API versions
 -       * @var integer[]
 -       * @deprecated 5.2
 -       */
 -      public static $compatibilityApiVersions = [2018];
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      protected static $baseClass = Style::class;
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function update(array $parameters = []) {
 -              $variables = null;
 -              if (isset($parameters['variables'])) {
 -                      $variables = $parameters['variables'];
 -                      unset($parameters['variables']);
 -              }
 -              
 -              // update style data
 -              parent::update($parameters);
 -              
 -              // update variables
 -              if ($variables !== null) {
 -                      $this->setVariables($variables);
 -              }
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function delete() {
 -              parent::delete();
 -              
 -              // delete style files
 -              $files = @glob(WCF_DIR.'style/style-'.$this->styleID.'*.css');
 -              if (is_array($files)) {
 -                      foreach ($files as $file) {
 -                              @unlink($file);
 -                      }
 -              }
 -              
 -              // delete preview image
 -              if ($this->image) {
 -                      @unlink(WCF_DIR.'images/'.$this->image);
 -              }
 -              
 -              // delete language items
 -              $sql = "DELETE FROM     wcf".WCF_N."_language_item
 -                      WHERE           languageItem = ?";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute(['wcf.style.styleDescription'.$this->styleID]);
 -      }
 -      
 -      /**
 -       * Sets this style as default style.
 -       */
 -      public function setAsDefault() {
 -              // remove old default
 -              $sql = "UPDATE  wcf".WCF_N."_style
 -                      SET     isDefault = ?
 -                      WHERE   isDefault = ?";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute([0, 1]);
 -              
 -              // set new default
 -              $this->update([
 -                      'isDefault' => 1,
 -                      'isDisabled' => 0
 -              ]);
 -              
 -              self::resetCache();
 -      }
 -      
 -      /**
 -       * Deletes the style's default cover photo.
 -       */
 -      public function deleteCoverPhoto() {
 -              if ($this->coverPhotoExtension) {
 -                      @unlink(WCF_DIR.'images/coverPhotos/'.$this->styleID.'.'.$this->coverPhotoExtension);
 -                      
 -                      $this->update([
 -                              'coverPhotoExtension' => ''
 -                      ]);
 -              }
 -      }
 -      
 -      /**
 -       * Returns the list of variables that exist, but have no explicit values for this style.
 -       * 
 -       * @return      string[]
 -       */
 -      public function getImplicitVariables() {
 -              $sql = "SELECT          variable.variableName
 -                      FROM            wcf".WCF_N."_style_variable variable
 -                      LEFT JOIN       wcf".WCF_N."_style_variable_value variable_value
 -                      ON              (variable_value.variableID = variable.variableID AND variable_value.styleID = ?)
 -                      WHERE           variable.variableName LIKE ?
 -                                      AND variable_value.variableValue IS NULL";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute([
 -                      $this->styleID,
 -                      'wcf%'
 -              ]);
 -              $variableNames = [];
 -              while ($variableName = $statement->fetchColumn()) {
 -                      $variableNames[] = $variableName;
 -              }
 -              
 -              return $variableNames;
 -      }
 -      
 -      /**
 -       * Reads the data of a style exchange format file.
 -       * 
 -       * @param       Tar     $tar
 -       * @return      array
 -       * @throws      SystemException
 -       */
 -      public static function readStyleData(Tar $tar) {
 -              // search style.xml
 -              $index = $tar->getIndexByFilename(self::INFO_FILE);
 -              if ($index === false) {
 -                      throw new SystemException("unable to find required file '".self::INFO_FILE."' in style archive");
 -              }
 -              
 -              // open style.xml
 -              $xml = new XML();
 -              $xml->loadXML(self::INFO_FILE, $tar->extractToString($index));
 -              $xpath = $xml->xpath();
 -              
 -              $data = [
 -                      'name' => '', 'description' => [], 'version' => '', 'image' => '', 'image2x' => '', 'copyright' => '', 'default' => false,
 -                      'license' => '', 'authorName' => '', 'authorURL' => '', 'templates' => '', 'images' => '', 'coverPhoto' => '',
 -                      'variables' => '', 'date' => '0000-00-00', 'imagesPath' => '', 'packageName' => '', 'apiVersion' => '3.0'
 -              ];
 -              
 -              $categories = $xpath->query('/ns:style/*');
 -              foreach ($categories as $category) {
 -                      switch ($category->tagName) {
 -                              case 'author':
 -                                      $elements = $xpath->query('child::*', $category);
 -                                      foreach ($elements as $element) {
 -                                              switch ($element->tagName) {
 -                                                      case 'authorname':
 -                                                              $data['authorName'] = $element->nodeValue;
 -                                                      break;
 -                                                      
 -                                                      case 'authorurl':
 -                                                              $data['authorURL'] = $element->nodeValue;
 -                                                      break;
 -                                              }
 -                                      }
 -                              break;
 -                              
 -                              case 'files':
 -                                      $elements = $xpath->query('child::*', $category);
 -                                      
 -                                      /** @var \DOMElement $element */
 -                                      foreach ($elements as $element) {
 -                                              $data[$element->tagName] = $element->nodeValue;
 -                                              if ($element->hasAttribute('path')) {
 -                                                      $data[$element->tagName.'Path'] = $element->getAttribute('path');
 -                                              }
 -                                      }
 -                              break;
 -                              
 -                              case 'general':
 -                                      $elements = $xpath->query('child::*', $category);
 -                                      
 -                                      /** @var \DOMElement $element */
 -                                      foreach ($elements as $element) {
 -                                              switch ($element->tagName) {
 -                                                      case 'date':
 -                                                              DateUtil::validateDate($element->nodeValue);
 -                                                              
 -                                                              $data['date'] = $element->nodeValue;
 -                                                      break;
 -                                                      
 -                                                      case 'default':
 -                                                              $data['default'] = true;
 -                                                      break;
 -                                                      
 -                                                      case 'description':
 -                                                              if ($element->hasAttribute('language')) {
 -                                                                      $data['description'][$element->getAttribute('language')] = $element->nodeValue;
 -                                                              }
 -                                                      break;
 -                                                      
 -                                                      case 'stylename':
 -                                                              $data['name'] = $element->nodeValue;
 -                                                      break;
 -                                                      
 -                                                      case 'packageName':
 -                                                              $data['packageName'] = $element->nodeValue;
 -                                                      break;
 -                                                      
 -                                                      case 'version':
 -                                                              if (!Package::isValidVersion($element->nodeValue)) {
 -                                                                      throw new SystemException("style version '".$element->nodeValue."' is invalid");
 -                                                              }
 -                                                              
 -                                                              $data['version'] = $element->nodeValue;
 -                                                      break;
 -                                                      
 -                                                      case 'copyright':
 -                                                      case 'image':
 -                                                      case 'image2x':
 -                                                      case 'license':
 -                                                      case 'coverPhoto':
 -                                                              $data[$element->tagName] = $element->nodeValue;
 -                                                      break;
 -                                                      
 -                                                      case 'apiVersion':
 -                                                              if (!in_array($element->nodeValue, Style::$supportedApiVersions)) {
 -                                                                      throw new SystemException("Unknown api version '".$element->nodeValue."'");
 -                                                              }
 -                                                              
 -                                                              $data['apiVersion'] = $element->nodeValue;
 -                                                              break;
 -                                              }
 -                                      }
 -                              break;
 -                      }
 -              }
 -              
 -              if (empty($data['name'])) {
 -                      throw new SystemException("required tag 'stylename' is missing in '".self::INFO_FILE."'");
 -              }
 -              if (empty($data['variables'])) {
 -                      throw new SystemException("required tag 'variables' is missing in '".self::INFO_FILE."'");
 -              }
 -              
 -              // search variables.xml
 -              $index = $tar->getIndexByFilename($data['variables']);
 -              if ($index === false) {
 -                      throw new SystemException("unable to find required file '".$data['variables']."' in style archive");
 -              }
 -              
 -              // open variables.xml
 -              $data['variables'] = self::readVariablesData($data['variables'], $tar->extractToString($index));
 -              
 -              return $data;
 -      }
 -      
 -      /**
 -       * Reads the data of a variables.xml file.
 -       * 
 -       * @param       string          $filename
 -       * @param       string          $content
 -       * @return      array
 -       */
 -      public static function readVariablesData($filename, $content) {
 -              // open variables.xml
 -              $xml = new XML();
 -              $xml->loadXML($filename, $content);
 -              $variables = $xml->xpath()->query('/ns:variables/ns:variable');
 -              
 -              $data = [];
 -              
 -              /** @var \DOMElement $variable */
 -              foreach ($variables as $variable) {
 -                      $data[$variable->getAttribute('name')] = $variable->nodeValue;
 -              }
 -              
 -              return $data;
 -      }
 -      
 -      /**
 -       * Returns the data of a style exchange format file.
 -       * 
 -       * @param       string          $filename
 -       * @return      array
 -       */
 -      public static function getStyleData($filename) {
 -              // open file
 -              $tar = new Tar($filename);
 -              
 -              // get style data
 -              $data = self::readStyleData($tar);
 -              
 -              // export preview image to temporary location
 -              if (!empty($data['image'])) {
 -                      $i = $tar->getIndexByFilename($data['image']);
 -                      if ($i !== false) {
 -                              $path = FileUtil::getTemporaryFilename('stylePreview_', $data['image'], WCF_DIR.'tmp/');
 -                              $data['image'] = basename($path);
 -                              $tar->extract($i, $path);
 -                      }
 -              }
 -              
 -              $tar->close();
 -              
 -              return $data;
 -      }
 -      
 -      /**
 -       * Imports a style.
 -       * 
 -       * @param       string          $filename
 -       * @param       integer         $packageID
 -       * @param       StyleEditor     $style
 -       * @param       boolean         $skipFontDownload
 -       * @return      StyleEditor
 -       */
 -      public static function import($filename, $packageID = 1, StyleEditor $style = null, $skipFontDownload = false) {
 -              // open file
 -              $tar = new Tar($filename);
 -              
 -              // get style data
 -              $data = self::readStyleData($tar);
 -              
 -              $styleData = [
 -                      'styleName' => $data['name'],
 -                      'variables' => $data['variables'],
 -                      'styleVersion' => $data['version'],
 -                      'styleDate' => $data['date'],
 -                      'copyright' => $data['copyright'],
 -                      'license' => $data['license'],
 -                      'authorName' => $data['authorName'],
 -                      'authorURL' => $data['authorURL'],
 -                      'packageName' => $data['packageName'],
 -                      'apiVersion' => $data['apiVersion']
 -              ];
 -              
 -              // check if there is an untainted style with the same package name
 -              if ($style === null && !empty($styleData['packageName'])) {
 -                      $style = StyleHandler::getInstance()->getStyleByName($styleData['packageName'], true);
 -              }
 -              
 -              // handle templates
 -              if (!empty($data['templates'])) {
 -                      $templateGroupFolderName = '';
 -                      if ($style !== null && $style->templateGroupID) {
 -                              $templateGroupFolderName = (new TemplateGroup($style->templateGroupID))->templateGroupFolderName;
 -                              $styleData['templateGroupID'] = $style->templateGroupID;
 -                      }
 -                      
 -                      if (empty($templateGroupFolderName)) {
 -                              // create template group
 -                              $templateGroupName = $originalTemplateGroupName = $data['name'];
 -                              $templateGroupFolderName = preg_replace('/[^a-z0-9_-]/i', '', $templateGroupName);
 -                              if (empty($templateGroupFolderName)) $templateGroupFolderName = 'generic'.mb_substr(StringUtil::getRandomID(), 0, 8);
 -                              $originalTemplateGroupFolderName = $templateGroupFolderName;
 -                              
 -                              // get unique template group name
 -                              $i = 1;
 -                              while (true) {
 -                                      $sql = "SELECT  COUNT(*)
 -                                              FROM    wcf".WCF_N."_template_group
 -                                              WHERE   templateGroupName = ?";
 -                                      $statement = WCF::getDB()->prepareStatement($sql);
 -                                      $statement->execute([$templateGroupName]);
 -                                      if (!$statement->fetchSingleColumn()) break;
 -                                      $templateGroupName = $originalTemplateGroupName . '_' . $i;
 -                                      $i++;
 -                              }
 -                              
 -                              // get unique folder name
 -                              $i = 1;
 -                              while (true) {
 -                                      $sql = "SELECT  COUNT(*)
 -                                              FROM    wcf".WCF_N."_template_group
 -                                              WHERE   templateGroupFolderName = ?";
 -                                      $statement = WCF::getDB()->prepareStatement($sql);
 -                                      $statement->execute([
 -                                              FileUtil::addTrailingSlash($templateGroupFolderName)
 -                                      ]);
 -                                      if (!$statement->fetchSingleColumn()) break;
 -                                      $templateGroupFolderName = $originalTemplateGroupFolderName . '_' . $i;
 -                                      $i++;
 -                              }
 -                              
 -                              $templateGroupAction = new TemplateGroupAction([], 'create', [
 -                                      'data' => [
 -                                              'templateGroupName' => $templateGroupName,
 -                                              'templateGroupFolderName' => FileUtil::addTrailingSlash($templateGroupFolderName)
 -                                      ]
 -                              ]);
 -                              $returnValues = $templateGroupAction->executeAction();
 -                              $styleData['templateGroupID'] = $returnValues['returnValues']->templateGroupID;
 -                      }
 -                      
 -                      // import templates
 -                      $index = $tar->getIndexByFilename($data['templates']);
 -                      if ($index !== false) {
 -                              // extract templates tar
 -                              $destination = FileUtil::getTemporaryFilename('templates_');
 -                              $tar->extract($index, $destination);
 -                              
 -                              // open templates tar and group templates by package
 -                              $templatesTar = new Tar($destination);
 -                              $contentList = $templatesTar->getContentList();
 -                              $packageToTemplates = [];
 -                              foreach ($contentList as $val) {
 -                                      if ($val['type'] == 'file') {
 -                                              $folders = explode('/', $val['filename']);
 -                                              $packageName = array_shift($folders);
 -                                              if (!isset($packageToTemplates[$packageName])) {
 -                                                      $packageToTemplates[$packageName] = [];
 -                                              }
 -                                              $packageToTemplates[$packageName][] = ['index' => $val['index'], 'filename' => implode('/', $folders)];
 -                                      }
 -                              }
 -                              
 -                              $knownTemplates = [];
 -                              if ($style !== null && $style->templateGroupID) {
 -                                      $sql = "SELECT  templateName
 -                                              FROM    wcf".WCF_N."_template
 -                                              WHERE   templateGroupID = ?";
 -                                      $statement = WCF::getDB()->prepareStatement($sql);
 -                                      $statement->execute([$style->templateGroupID]);
 -                                      $knownTemplates = $statement->fetchAll(\PDO::FETCH_COLUMN);
 -                              }
 -                              
 -                              // copy templates
 -                              foreach ($packageToTemplates as $package => $templates) {
 -                                      // try to find package
 -                                      $sql = "SELECT  *
 -                                              FROM    wcf".WCF_N."_package
 -                                              WHERE   package = ?
 -                                                      AND isApplication = ?";
 -                                      $statement = WCF::getDB()->prepareStatement($sql);
 -                                      $statement->execute([
 -                                              $package,
 -                                              1
 -                                      ]);
 -                                      while ($row = $statement->fetchArray()) {
 -                                              // get template path
 -                                              $templatesDir = FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR.$row['packageDir']).'templates/'.$templateGroupFolderName);
 -                                              
 -                                              // create template path
 -                                              if (!file_exists($templatesDir)) {
 -                                                      @mkdir($templatesDir, 0777);
 -                                                      FileUtil::makeWritable($templatesDir);
 -                                              }
 -                                              
 -                                              // copy templates
 -                                              foreach ($templates as $template) {
 -                                                      if (!StringUtil::endsWith($template['filename'], '.tpl')) {
 -                                                              continue;
 -                                                      }
 -                                                      
 -                                                      $templatesTar->extract($template['index'], $templatesDir.$template['filename']);
 -                                                      
 -                                                      $templateName = str_replace('.tpl', '', $template['filename']);
 -                                                      if (!in_array($templateName, $knownTemplates)) {
 -                                                              TemplateEditor::create([
 -                                                                      'application' => Package::getAbbreviation($package),
 -                                                                      'packageID' => $row['packageID'],
 -                                                                      'templateName' => $templateName,
 -                                                                      'templateGroupID' => $styleData['templateGroupID']
 -                                                              ]);
 -                                                      }
 -                                              }
 -                                      }
 -                              }
 -                              
 -                              // delete tmp file
 -                              $templatesTar->close();
 -                              @unlink($destination);
 -                      }
 -              }
 -              
 -              $duplicateLogo = false;
 -              // duplicate logo if logo matches mobile logo
 -              if (!empty($styleData['variables']['pageLogo']) && !empty($styleData['variables']['pageLogoMobile']) && $styleData['variables']['pageLogo'] == $styleData['variables']['pageLogoMobile']) {
 -                      $styleData['variables']['pageLogoMobile'] = 'm-'.basename($styleData['variables']['pageLogo']);
 -                      $duplicateLogo = true;
 -              }
 -              
 -              // save style
 -              if ($style === null) {
 -                      $styleData['packageID'] = $packageID;
 -                      $style = new StyleEditor(self::create($styleData));
 -                      
 -                      // handle descriptions
 -                      if (!empty($data['description'])) {
 -                              self::saveLocalizedDescriptions($style, $data['description']);
 -                              LanguageFactory::getInstance()->deleteLanguageCache();
 -                      }
 -                      
 -                      if ($data['default']) {
 -                              $style->setAsDefault();
 -                      }
 -              }
 -              else {
 -                      unset($styleData['styleName']);
 -                      
 -                      $variables = $style->getVariables();
 -                      if (!isset($styleData['variables']['individualScss'])) $styleData['variables']['individualScss'] = '';
 -                      if (!isset($styleData['variables']['overrideScss'])) $styleData['variables']['overrideScss'] = '';
 -                      
 -                      $individualScss = Style::splitLessVariables($variables['individualScss']);
 -                      $variables['individualScss'] = Style::joinLessVariables($styleData['variables']['individualScss'], $individualScss['custom']);
 -                      unset($styleData['variables']['individualScss']);
 -                      
 -                      $overrideScss = Style::splitLessVariables($variables['overrideScss']);
 -                      $variables['overrideScss'] = Style::joinLessVariables($styleData['variables']['overrideScss'], $overrideScss['custom']);
 -                      unset($styleData['variables']['overrideScss']);
 -                      
 -                      // import variables that have not been explicitly defined before
 -                      $implicitVariables = $style->getImplicitVariables();
 -                      foreach ($styleData['variables'] as $variableName => $variableValue) {
 -                              if (in_array($variableName, $implicitVariables)) {
 -                                      $variables[$variableName] = $variableValue;
 -                              }
 -                      }
 -                      
 -                      $styleData['variables'] = $variables;
 -                      
 -                      $style->update($styleData);
 -              }
 -              
 -              // import images
 -              if (!empty($data['images'])) {
 -                      $index = $tar->getIndexByFilename($data['images']);
 -                      if ($index !== false) {
 -                              // extract images tar
 -                              $destination = FileUtil::getTemporaryFilename('images_');
 -                              $tar->extract($index, $destination);
 -                              
 -                              // open images tar
 -                              $imagesTar = new Tar($destination);
 -                              $contentList = $imagesTar->getContentList();
 -                              foreach ($contentList as $key => $val) {
 -                                      if ($val['type'] == 'file') {
 -                                              $path = FileUtil::getRealPath($val['filename']);
 -                                              $fileExtension = pathinfo($path, PATHINFO_EXTENSION);
 -
 -                                              if (!in_array($fileExtension, self::VALID_IMAGE_EXTENSIONS)) {
 -                                                      continue;
 -                                              }
 -                                              
 -                                              if (strpos($path, '../') !== false) {
 -                                                      continue;
 -                                              }
 -                                              
 -                                              $targetFile = FileUtil::getRealPath($style->getAssetPath().$path);
 -                                              if (strpos(FileUtil::getRelativePath($style->getAssetPath(), $targetFile), '../') !== false) {
 -                                                      continue;
 -                                              }
 -                                              
 -                                              // duplicate pageLogo for mobile version
 -                                              if ($duplicateLogo && $val['filename'] == $styleData['variables']['pageLogo']) {
 -                                                      $imagesTar->extract($key, $style->getAssetPath().'m-'.basename($targetFile));
 -                                              }
 -                                              
 -                                              $imagesTar->extract($key, $targetFile);
 -                                              FileUtil::makeWritable($targetFile);
 -                                      }
 -                              }
 -                              
 -                              // delete tmp file
 -                              $imagesTar->close();
 -                              @unlink($destination);
 -                      }
 -              }
 -              
 -              // import preview image
 -              foreach (['image', 'image2x'] as $type) {
 -                      if (!empty($data[$type])) {
 -                              $fileExtension = pathinfo($data[$type], PATHINFO_EXTENSION);
 -                              if (!in_array($fileExtension, self::VALID_IMAGE_EXTENSIONS)) {
 -                                      continue;
 -                              }
 -                              
 -                              $index = $tar->getIndexByFilename($data[$type]);
 -                              if ($index !== false) {
 -                                      $filename = $style->getAssetPath().'stylePreview' . ($type === 'image2x' ? '@2x' : '') . '.' . $fileExtension;
 -                                      $tar->extract($index, $filename);
 -                                      FileUtil::makeWritable($filename);
 -                                      
 -                                      if (file_exists($filename)) {
 -                                              try {
 -                                                      if (($imageData = getimagesize($filename)) !== false) {
 -                                                              switch ($imageData[2]) {
 -                                                                      case IMAGETYPE_PNG:
 -                                                                      case IMAGETYPE_JPEG:
 -                                                                      case IMAGETYPE_GIF:
 -                                                                              $style->update([$type => 'style-' . $style->styleID . '/stylePreview' . ($type === 'image2x' ? '@2x' : '') . '.' . $fileExtension]);
 -                                                              }
 -                                                      }
 -                                              }
 -                                              catch (SystemException $e) {
 -                                                      // broken image
 -                                              }
 -                                      }
 -                              }
 -                      }
 -              }
 -              
 -              // import cover photo
 -              if (!empty($data['coverPhoto'])) {
 -                      $fileExtension = pathinfo($data['coverPhoto'], PATHINFO_EXTENSION);
 -                      $index = $tar->getIndexByFilename($data['coverPhoto']);
 -                      if ($index !== false && in_array($fileExtension, self::VALID_IMAGE_EXTENSIONS)) {
 -                              $filename = $style->getAssetPath().'coverPhoto.' . $fileExtension;
 -                              $tar->extract($index, $filename);
 -                              FileUtil::makeWritable($filename);
 -                              
 -                              if (file_exists($filename)) {
 -                                      try {
 -                                              if (($imageData = getimagesize($filename)) !== false) {
 -                                                      switch ($imageData[2]) {
 -                                                              case IMAGETYPE_PNG:
 -                                                              case IMAGETYPE_JPEG:
 -                                                              case IMAGETYPE_GIF:
 -                                                                      $style->update(['coverPhotoExtension' => $fileExtension]);
 -                                                      }
 -                                              }
 -                                      }
 -                                      catch (SystemException $e) {
 -                                              // broken image
 -                                      }
 -                              }
 -                      }
 -              }
 -              
 -              if (!$skipFontDownload) {
 -                      // download google fonts
 -                      $fontManager = FontManager::getInstance();
 -                      $style->loadVariables();
 -                      $family = $style->getVariable('wcfFontFamilyGoogle');
 -                      try {
 -                              $fontManager->downloadFamily($family);
 -                      }
 -                      catch (FontDownloadFailed $e) {
 -                              // ignore
 -                      }
 -              }
 -
 -              $tar->close();
 -              
 -              return $style;
 -      }
 -      
 -      /**
 -       * Saves localized style descriptions.
 -       * 
 -       * @param       StyleEditor     $styleEditor
 -       * @param       string[]        $descriptions
 -       */
 -      protected static function saveLocalizedDescriptions(StyleEditor $styleEditor, array $descriptions) {
 -              // localize package information
 -              $sql = "REPLACE INTO    wcf".WCF_N."_language_item
 -                                      (languageID, languageItem, languageItemValue, languageCategoryID, packageID)
 -                      VALUES          (?, ?, ?, ?, ?)";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              
 -              // get language list
 -              $languageList = new LanguageList();
 -              $languageList->readObjects();
 -              
 -              // workaround for WCFSetup
 -              if (!PACKAGE_ID) {
 -                      $sql = "SELECT  *
 -                              FROM    wcf".WCF_N."_language_category
 -                              WHERE   languageCategory = ?";
 -                      $statement2 = WCF::getDB()->prepareStatement($sql);
 -                      $statement2->execute(['wcf.style']);
 -                      $languageCategory = $statement2->fetchObject(LanguageCategory::class);
 -              }
 -              else {
 -                      $languageCategory = LanguageFactory::getInstance()->getCategory('wcf.style');
 -              }
 -              
 -              foreach ($languageList as $language) {
 -                      if (isset($descriptions[$language->languageCode])) {
 -                              $statement->execute([
 -                                      $language->languageID,
 -                                      'wcf.style.styleDescription'.$styleEditor->styleID,
 -                                      $descriptions[$language->languageCode],
 -                                      $languageCategory->languageCategoryID,
 -                                      $styleEditor->packageID
 -                              ]);
 -                      }
 -              }
 -              
 -              $styleEditor->update([
 -                      'styleDescription' => 'wcf.style.styleDescription'.$styleEditor->styleID
 -              ]);
 -      }
 -      
 -      /**
 -       * Returns available location path.
 -       * 
 -       * @param       string          $location
 -       * @return      string
 -       */
 -      protected static function getFileLocation($location) {
 -              $location = FileUtil::removeLeadingSlash(FileUtil::removeTrailingSlash($location));
 -              $location = WCF_DIR.$location;
 -              
 -              $index = null;
 -              do {
 -                      $directory = $location . ($index === null ? '' : $index);
 -                      if (!is_dir($directory)) {
 -                              @mkdir($directory, 0777, true);
 -                              FileUtil::makeWritable($directory);
 -                              
 -                              return FileUtil::addTrailingSlash($directory);
 -                      }
 -                      
 -                      $index = ($index === null ? 2 : ($index + 1));
 -              }
 -              while (true);
 -              
 -              // this should never happen
 -              throw new \LogicException();
 -      }
 -      
 -      /**
 -       * Exports this style.
 -       * 
 -       * @param       boolean         $templates
 -       * @param       boolean         $images
 -       * @param       string          $packageName
 -       */
 -      public function export($templates = false, $images = false, $packageName = '') {
 -              // create style tar
 -              $styleTarName = FileUtil::getTemporaryFilename('style_', '.tgz');
 -              $styleTar = new TarWriter($styleTarName, true);
 -              
 -              // append style preview image
 -              if ($this->image && @file_exists(WCF_DIR.'images/'.$this->image)) {
 -                      $styleTar->add(WCF_DIR.'images/'.$this->image, '', FileUtil::addTrailingSlash(dirname(WCF_DIR.'images/'.$this->image)));
 -              }
 -              if ($this->image2x && @file_exists(WCF_DIR.'images/'.$this->image2x)) {
 -                      $styleTar->add(WCF_DIR.'images/'.$this->image2x, '', FileUtil::addTrailingSlash(dirname(WCF_DIR.'images/'.$this->image2x)));
 -              }
 -              
 -              // append cover photo
 -              $coverPhoto = ($this->coverPhotoExtension) ? $this->getAssetPath().'coverPhoto.'.$this->coverPhotoExtension : '';
 -              if ($coverPhoto && @file_exists($coverPhoto)) {
 -                      $styleTar->add($coverPhoto, '', FileUtil::addTrailingSlash(dirname($coverPhoto)));
 -              }
 -              
 -              // fetch style description
 -              $sql = "SELECT          language.languageCode, language_item.languageItemValue
 -                      FROM            wcf".WCF_N."_language_item language_item
 -                      LEFT JOIN       wcf".WCF_N."_language language
 -                      ON              (language.languageID = language_item.languageID)
 -                      WHERE           language_item.languageItem = ?";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute([$this->styleDescription]);
 -              $styleDescriptions = $statement->fetchMap('languageCode', 'languageItemValue');
 -              
 -              // create style info file
 -              $xml = new XMLWriter();
 -              $xml->beginDocument('style', 'http://www.woltlab.com', 'http://www.woltlab.com/XSD/' . WSC_API_VERSION . '/style.xsd');
 -              
 -              // general block
 -              $xml->startElement('general');
 -              $xml->writeElement('stylename', $this->styleName);
 -              $xml->writeElement('packageName', $this->packageName);
 -              
 -              // style description
 -              foreach ($styleDescriptions as $languageCode => $value) {
 -                      $xml->writeElement('description', $value, ['language' => $languageCode]);
 -              }
 -              
 -              $xml->writeElement('date', $this->styleDate);
 -              $xml->writeElement('version', $this->styleVersion);
 -              $xml->writeElement('apiVersion', $this->apiVersion);
 -              if ($this->image) $xml->writeElement('image', basename($this->image));
 -              if ($this->image2x) $xml->writeElement('image2x', basename($this->image2x));
 -              if ($coverPhoto) $xml->writeElement('coverPhoto', basename(FileUtil::unifyDirSeparator($coverPhoto)));
 -              if ($this->copyright) $xml->writeElement('copyright', $this->copyright);
 -              if ($this->license) $xml->writeElement('license', $this->license);
 -              $xml->endElement();
 -              
 -              // author block
 -              $xml->startElement('author');
 -              $xml->writeElement('authorname', $this->authorName);
 -              if ($this->authorURL) $xml->writeElement('authorurl', $this->authorURL);
 -              $xml->endElement();
 -              
 -              // files block
 -              $xml->startElement('files');
 -              $xml->writeElement('variables', 'variables.xml');
 -              if ($templates) $xml->writeElement('templates', 'templates.tar');
 -              if ($images) $xml->writeElement('images', 'images.tar', ['path' => $this->imagePath]);
 -              $xml->endElement();
 -              
 -              // append style info file to style tar
 -              $styleTar->addString(self::INFO_FILE, $xml->endDocument());
 -              
 -              // create variable list
 -              $xml->beginDocument('variables', 'http://www.woltlab.com', 'http://www.woltlab.com/XSD/' . WSC_API_VERSION . '/styleVariables.xsd');
 -              
 -              // get variables
 -              $sql = "SELECT          variable.variableName, value.variableValue
 -                      FROM            wcf".WCF_N."_style_variable_value value
 -                      LEFT JOIN       wcf".WCF_N."_style_variable variable
 -                      ON              (variable.variableID = value.variableID)
 -                      WHERE           value.styleID = ?";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute([$this->styleID]);
 -              while ($row = $statement->fetchArray()) {
 -                      $xml->writeElement('variable', $row['variableValue'], ['name' => $row['variableName']]);
 -              }
 -              
 -              // append variable list to style tar
 -              $styleTar->addString('variables.xml', $xml->endDocument());
 -              
 -              if ($templates && $this->templateGroupID) {
 -                      $templateGroup = new TemplateGroup($this->templateGroupID);
 -                      
 -                      // create templates tar
 -                      $templatesTarName = FileUtil::getTemporaryFilename('templates', '.tar');
 -                      $templatesTar = new TarWriter($templatesTarName);
 -                      FileUtil::makeWritable($templatesTarName);
 -                      
 -                      // append templates to tar
 -                      // get templates
 -                      $sql = "SELECT          template.*, package.package
 -                              FROM            wcf".WCF_N."_template template
 -                              LEFT JOIN       wcf".WCF_N."_package package
 -                              ON              (package.packageID = template.packageID)
 -                              WHERE           template.templateGroupID = ?";
 -                      $statement = WCF::getDB()->prepareStatement($sql);
 -                      $statement->execute([$this->templateGroupID]);
 -                      while ($row = $statement->fetchArray()) {
 -                              $packageDir = 'com.woltlab.wcf';
 -                              $package = null;
 -                              
 -                              if (Template::isSystemCritical($row['templateName'])) {
 -                                      continue;
 -                              }
 -                              
 -                              if ($row['application'] != 'wcf') {
 -                                      $application = ApplicationHandler::getInstance()->getApplication($row['application']);
 -                                      $package = PackageCache::getInstance()->getPackage($application->packageID);
 -                                      $packageDir = $package->package;
 -                              }
 -                              else {
 -                                      $application = ApplicationHandler::getInstance()->getWCF();
 -                                      $package = PackageCache::getInstance()->getPackage($application->packageID);
 -                              }
 -                              
 -                              $filename = FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR . $package->packageDir . 'templates/' . $templateGroup->templateGroupFolderName)) . $row['templateName'] . '.tpl';
 -                              $templatesTar->add($filename, $packageDir, dirname($filename));
 -                      }
 -                      
 -                      // append templates tar to style tar
 -                      $templatesTar->create();
 -                      $styleTar->add($templatesTarName, 'templates.tar', $templatesTarName);
 -                      @unlink($templatesTarName);
 -              }
 -              
 -              if ($images) {
 -                      // create images tar
 -                      $imagesTarName = FileUtil::getTemporaryFilename('images_', '.tar');
 -                      $imagesTar = new TarWriter($imagesTarName);
 -                      FileUtil::makeWritable($imagesTarName);
 -                      
 -                      $regEx = new Regex('\.(jpg|jpeg|gif|png|svg|ico|json|xml|txt)$', Regex::CASE_INSENSITIVE);
 -                      $iterator = new \RecursiveIteratorIterator(
 -                              new \RecursiveDirectoryIterator(
 -                                      $this->getAssetPath(),
 -                                      \FilesystemIterator::SKIP_DOTS
 -                              ), 
 -                              \RecursiveIteratorIterator::SELF_FIRST
 -                      );
 -                      foreach ($iterator as $file) {
 -                              /** @var \SplFileInfo $file */
 -                              if (!$file->isFile()) continue;
 -                              if (!$regEx->match($file->getPathName())) continue;
 -                              // Skip preview images and cover photos.
 -                              if ($this->image && FileUtil::unifyDirSeparator($file->getPathname()) === FileUtil::unifyDirSeparator(WCF_DIR.'images/'.$this->image)) continue;
 -                              if ($this->image2x && FileUtil::unifyDirSeparator($file->getPathname()) === FileUtil::unifyDirSeparator(WCF_DIR.'images/'.$this->image2x)) continue;
 -                              if ($coverPhoto && FileUtil::unifyDirSeparator($file->getPathname()) === FileUtil::unifyDirSeparator($coverPhoto)) continue;
 -
 -                              $imagesTar->add($file->getPathName(), '', $this->getAssetPath());
 -                      }
 -                      // append images tar to style tar
 -                      $imagesTar->create();
 -                      $styleTar->add($imagesTarName, 'images.tar', $imagesTarName);
 -                      @unlink($imagesTarName);
 -              }
 -              
 -              // output file content
 -              $styleTar->create();
 -              
 -              // export as style package
 -              if (empty($packageName)) {
 -                      readfile($styleTarName);
 -              }
 -              else {
 -                      // export as package
 -                      
 -                      // create package tar
 -                      $packageTarName = FileUtil::getTemporaryFilename('package_', '.tar.gz');
 -                      $packageTar = new TarWriter($packageTarName, true);
 -                      
 -                      // append style tar
 -                      $styleTarName = FileUtil::unifyDirSeparator($styleTarName);
 -                      $packageTar->add($styleTarName, '', FileUtil::addTrailingSlash(dirname($styleTarName)));
 -                      
 -                      // create package.xml
 -                      $xml->beginDocument('package', 'http://www.woltlab.com', 'http://www.woltlab.com/XSD/' . WSC_API_VERSION . '/package.xsd', ['name' => $packageName]);
 -                      
 -                      $xml->startElement('packageinformation');
 -                      $xml->writeElement('packagename', $this->styleName);
 -                      
 -                      // description
 -                      foreach ($styleDescriptions as $languageCode => $value) {
 -                              $xml->writeElement('packagedescription', $value, ['language' => $languageCode]);
 -                      }
 -                      
 -                      $xml->writeElement('version', $this->styleVersion);
 -                      $xml->writeElement('date', $this->styleDate);
 -                      $xml->endElement();
 -                      
 -                      $xml->startElement('authorinformation');
 -                      $xml->writeElement('author', $this->authorName);
 -                      if ($this->authorURL) $xml->writeElement('authorurl', $this->authorURL);
 -                      $xml->endElement();
 -                      
 -                      $xml->startElement('requiredpackages');
 -                      $xml->writeElement('requiredpackage', 'com.woltlab.wcf', ['minversion' => PackageCache::getInstance()->getPackageByIdentifier('com.woltlab.wcf')->packageVersion]);
 -                      $xml->endElement();
 -                      
 -                      $xml->startElement('excludedpackages');
 -                      $xml->writeElement('excludedpackage', 'com.woltlab.wcf', ['version' => self::EXCLUDE_WCF_VERSION]);
 -                      $xml->endElement();
 -                      
 -                      // @deprecated 5.2
 -                      $xml->startElement('compatibility');
 -                      foreach (self::$compatibilityApiVersions as $apiVersion) {
 -                              $xml->writeElement('api', '', ['version' => $apiVersion]);
 -                      }
 -                      $xml->endElement();
 -                      
 -                      $xml->startElement('instructions', ['type' => 'install']);
 -                      $xml->writeElement('instruction', basename($styleTarName), ['type' => 'style']);
 -                      $xml->endElement();
 -                      
 -                      // append package info file to package tar
 -                      $packageTar->addString(PackageArchive::INFO_FILE, $xml->endDocument());
 -                      
 -                      $packageTar->create();
 -                      readfile($packageTarName);
 -                      @unlink($packageTarName);
 -              }
 -              
 -              @unlink($styleTarName);
 -      }
 -      
 -      /**
 -       * Sets the variables of a style.
 -       * 
 -       * @param       string[]                $variables
 -       */
 -      public function setVariables(array $variables = []) {
 -              // delete old variables
 -              $sql = "DELETE FROM     wcf".WCF_N."_style_variable_value
 -                      WHERE           styleID = ?";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute([$this->styleID]);
 -              
 -              // insert new variables
 -              if (!empty($variables)) {
 -                      $sql = "SELECT  *
 -                              FROM    wcf".WCF_N."_style_variable";
 -                      $statement = WCF::getDB()->prepareStatement($sql);
 -                      $statement->execute();
 -                      $styleVariables = [];
 -                      while ($row = $statement->fetchArray()) {
 -                              $variableName = $row['variableName'];
 -                              
 -                              if (isset($variables[$variableName])) {
 -                                      // compare value, save only if differs from default
 -                                      if ($variables[$variableName] != $row['defaultValue']) {
 -                                              $styleVariables[$row['variableID']] = $variables[$variableName];
 -                                      }
 -                              }
 -                      }
 -                      
 -                      if (!empty($styleVariables)) {
 -                              $sql = "INSERT INTO     wcf".WCF_N."_style_variable_value
 -                                                      (styleID, variableID, variableValue)
 -                                      VALUES          (?, ?, ?)";
 -                              $statement = WCF::getDB()->prepareStatement($sql);
 -                              
 -                              WCF::getDB()->beginTransaction();
 -                              foreach ($styleVariables as $variableID => $variableValue) {
 -                                      $statement->execute([
 -                                              $this->styleID,
 -                                              $variableID,
 -                                              $variableValue
 -                                      ]);
 -                              }
 -                              WCF::getDB()->commitTransaction();
 -                      }
 -              }
 -              
 -              $this->writeStyleFile();
 -      }
 -      
 -      /**
 -       * Writes the style-*.css file.
 -       */
 -      public function writeStyleFile() {
 -              StyleCompiler::getInstance()->compile($this->getDecoratedObject());
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       * @return      Style
 -       */
 -      public static function create(array $parameters = []) {
 -              $variables = null;
 -              if (isset($parameters['variables'])) {
 -                      $variables = $parameters['variables'];
 -                      unset($parameters['variables']);
 -              }
 -              
 -              // default values
 -              if (!isset($parameters['packageID'])) $parameters['packageID'] = 1;
 -              if (!isset($parameters['styleDate'])) $parameters['styleDate'] = gmdate('Y-m-d', TIME_NOW);
 -              
 -              // check if no default style is defined
 -              $sql = "SELECT  styleID
 -                      FROM    wcf".WCF_N."_style
 -                      WHERE   isDefault = ?";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute([1]);
 -              $row = $statement->fetchArray();
 -              
 -              // no default style exists
 -              if ($row === false) {
 -                      $parameters['isDefault'] = 1;
 -              }
 -              
 -              /** @var Style $style */
 -              $style = parent::create($parameters);
 -              $styleEditor = new StyleEditor($style);
 -              
 -              // create asset path
 -              FileUtil::makePath($style->getAssetPath());
 -              
 -              $styleEditor->update([
 -                      'imagePath' => FileUtil::getRelativePath(WCF_DIR, $style->getAssetPath()),
 -              ]);
 -              $styleEditor = new StyleEditor(new Style($style->styleID));
 -              
 -              // save variables
 -              if ($variables !== null) {
 -                      $styleEditor->setVariables($variables);
 -              }
 -              
 -              return $style;
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public static function resetCache() {
 -              StyleCacheBuilder::getInstance()->reset();
 -      }
 +class StyleEditor extends DatabaseObjectEditor implements IEditableCachedObject
 +{
 +    const EXCLUDE_WCF_VERSION = '6.0.0 Alpha 1';
 +
 +    const INFO_FILE = 'style.xml';
 +
 +    const VALID_IMAGE_EXTENSIONS = ['gif', 'jpg', 'jpeg', 'png', 'svg', 'xml', 'json', 'webp'];
 +
 +    /**
 +     * list of compatible API versions
 +     * @var int[]
 +     * @deprecated 5.2
 +     */
 +    public static $compatibilityApiVersions = [2018];
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    protected static $baseClass = Style::class;
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function update(array $parameters = [])
 +    {
 +        $variables = null;
 +        if (isset($parameters['variables'])) {
 +            $variables = $parameters['variables'];
 +            unset($parameters['variables']);
 +        }
 +
 +        // update style data
 +        parent::update($parameters);
 +
 +        // update variables
 +        if ($variables !== null) {
 +            $this->setVariables($variables);
 +        }
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function delete()
 +    {
 +        parent::delete();
 +
 +        // delete style files
 +        $files = @\glob(WCF_DIR . 'style/style-' . $this->styleID . '*.css');
 +        if (\is_array($files)) {
 +            foreach ($files as $file) {
 +                @\unlink($file);
 +            }
 +        }
 +
 +        // delete preload data
 +        @\unlink(WCF_DIR . 'style/style-' . $this->styleID . '-preload.json');
 +
 +        // delete preview image
 +        if ($this->image) {
 +            @\unlink(WCF_DIR . 'images/' . $this->image);
 +        }
 +
 +        // delete language items
 +        $sql = "DELETE FROM wcf" . WCF_N . "_language_item
 +                WHERE       languageItem = ?";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute(['wcf.style.styleDescription' . $this->styleID]);
 +    }
 +
 +    /**
 +     * Sets this style as default style.
 +     */
 +    public function setAsDefault()
 +    {
 +        // remove old default
 +        $sql = "UPDATE  wcf" . WCF_N . "_style
 +                SET     isDefault = ?
 +                WHERE   isDefault = ?";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute([0, 1]);
 +
 +        // set new default
 +        $this->update([
 +            'isDefault' => 1,
 +            'isDisabled' => 0,
 +        ]);
 +
 +        self::resetCache();
 +    }
 +
 +    /**
 +     * Deletes the style's default cover photo.
 +     */
 +    public function deleteCoverPhoto()
 +    {
 +        if ($this->coverPhotoExtension) {
 +            @\unlink(WCF_DIR . 'images/coverPhotos/' . $this->styleID . '.' . $this->coverPhotoExtension);
 +
 +            $this->update([
 +                'coverPhotoExtension' => '',
 +            ]);
 +        }
 +    }
 +
 +    /**
 +     * Returns the list of variables that exist, but have no explicit values for this style.
 +     *
 +     * @return      string[]
 +     */
 +    public function getImplicitVariables()
 +    {
 +        $sql = "SELECT      variable.variableName
 +                FROM        wcf" . WCF_N . "_style_variable variable
 +                LEFT JOIN   wcf" . WCF_N . "_style_variable_value variable_value
 +                ON          variable_value.variableID = variable.variableID
 +                        AND variable_value.styleID = ?
 +                WHERE       variable.variableName LIKE ?
 +                        AND variable_value.variableValue IS NULL";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute([
 +            $this->styleID,
 +            'wcf%',
 +        ]);
 +        $variableNames = [];
 +        while ($variableName = $statement->fetchColumn()) {
 +            $variableNames[] = $variableName;
 +        }
 +
 +        return $variableNames;
 +    }
 +
 +    /**
 +     * @return null|bool
 +     * @since 5.4
 +     */
 +    public function createCoverPhotoVariant(?string $sourceLocation = null)
 +    {
 +        if ($sourceLocation === null) {
 +            $sourceLocation = $this->getCoverPhotoLocation(false);
 +        }
 +
 +        $outputFilenameWithoutExtension = \preg_replace('~\.[a-z]+$~', '', $sourceLocation);
 +
 +        return ImageUtil::createWebpVariant($sourceLocation, $outputFilenameWithoutExtension);
 +    }
 +
 +    /**
 +     * Reads the data of a style exchange format file.
 +     *
 +     * @param Tar $tar
 +     * @return  array
 +     * @throws  SystemException
 +     */
 +    public static function readStyleData(Tar $tar)
 +    {
 +        // search style.xml
 +        $index = $tar->getIndexByFilename(self::INFO_FILE);
 +        if ($index === false) {
 +            throw new SystemException("unable to find required file '" . self::INFO_FILE . "' in style archive");
 +        }
 +
 +        // open style.xml
 +        $xml = new XML();
 +        $xml->loadXML(self::INFO_FILE, $tar->extractToString($index));
 +        $xpath = $xml->xpath();
 +
 +        $data = [
 +            'name' => '',
 +            'description' => [],
 +            'version' => '',
 +            'image' => '',
 +            'image2x' => '',
 +            'copyright' => '',
 +            'default' => false,
 +            'license' => '',
 +            'authorName' => '',
 +            'authorURL' => '',
 +            'templates' => '',
 +            'images' => '',
 +            'coverPhoto' => '',
 +            'variables' => '',
 +            'date' => '0000-00-00',
 +            'imagesPath' => '',
 +            'packageName' => '',
 +            'apiVersion' => '3.0',
 +        ];
 +
 +        $categories = $xpath->query('/ns:style/*');
 +        foreach ($categories as $category) {
 +            switch ($category->tagName) {
 +                case 'author':
 +                    $elements = $xpath->query('child::*', $category);
 +                    foreach ($elements as $element) {
 +                        switch ($element->tagName) {
 +                            case 'authorname':
 +                                $data['authorName'] = $element->nodeValue;
 +                                break;
 +
 +                            case 'authorurl':
 +                                $data['authorURL'] = $element->nodeValue;
 +                                break;
 +                        }
 +                    }
 +                    break;
 +
 +                case 'files':
 +                    $elements = $xpath->query('child::*', $category);
 +
 +                    /** @var \DOMElement $element */
 +                    foreach ($elements as $element) {
 +                        $data[$element->tagName] = $element->nodeValue;
 +                        if ($element->hasAttribute('path')) {
 +                            $data[$element->tagName . 'Path'] = $element->getAttribute('path');
 +                        }
 +                    }
 +                    break;
 +
 +                case 'general':
 +                    $elements = $xpath->query('child::*', $category);
 +
 +                    /** @var \DOMElement $element */
 +                    foreach ($elements as $element) {
 +                        switch ($element->tagName) {
 +                            case 'date':
 +                                DateUtil::validateDate($element->nodeValue);
 +
 +                                $data['date'] = $element->nodeValue;
 +                                break;
 +
 +                            case 'default':
 +                                $data['default'] = true;
 +                                break;
 +
 +                            case 'description':
 +                                if ($element->hasAttribute('language')) {
 +                                    $data['description'][$element->getAttribute('language')] = $element->nodeValue;
 +                                }
 +                                break;
 +
 +                            case 'stylename':
 +                                $data['name'] = $element->nodeValue;
 +                                break;
 +
 +                            case 'packageName':
 +                                $data['packageName'] = $element->nodeValue;
 +                                break;
 +
 +                            case 'version':
 +                                if (!Package::isValidVersion($element->nodeValue)) {
 +                                    throw new SystemException("style version '" . $element->nodeValue . "' is invalid");
 +                                }
 +
 +                                $data['version'] = $element->nodeValue;
 +                                break;
 +
 +                            case 'copyright':
 +                            case 'image':
 +                            case 'image2x':
 +                            case 'license':
 +                            case 'coverPhoto':
 +                                $data[$element->tagName] = $element->nodeValue;
 +                                break;
 +
 +                            case 'apiVersion':
 +                                if (!\in_array($element->nodeValue, Style::$supportedApiVersions)) {
 +                                    throw new SystemException("Unknown api version '" . $element->nodeValue . "'");
 +                                }
 +
 +                                $data['apiVersion'] = $element->nodeValue;
 +                                break;
 +                        }
 +                    }
 +                    break;
 +            }
 +        }
 +
 +        if (empty($data['name'])) {
 +            throw new SystemException("required tag 'stylename' is missing in '" . self::INFO_FILE . "'");
 +        }
 +        if (empty($data['variables'])) {
 +            throw new SystemException("required tag 'variables' is missing in '" . self::INFO_FILE . "'");
 +        }
 +
 +        // search variables.xml
 +        $index = $tar->getIndexByFilename($data['variables']);
 +        if ($index === false) {
 +            throw new SystemException("unable to find required file '" . $data['variables'] . "' in style archive");
 +        }
 +
 +        // open variables.xml
 +        $data['variables'] = self::readVariablesData($data['variables'], $tar->extractToString($index));
 +
 +        return $data;
 +    }
 +
 +    /**
 +     * Reads the data of a variables.xml file.
 +     *
 +     * @param string $filename
 +     * @param string $content
 +     * @return  array
 +     */
 +    public static function readVariablesData($filename, $content)
 +    {
 +        // open variables.xml
 +        $xml = new XML();
 +        $xml->loadXML($filename, $content);
 +        $variables = $xml->xpath()->query('/ns:variables/ns:variable');
 +
 +        $data = [];
 +
 +        /** @var \DOMElement $variable */
 +        foreach ($variables as $variable) {
 +            $data[$variable->getAttribute('name')] = $variable->nodeValue;
 +        }
 +
 +        return $data;
 +    }
 +
 +    /**
 +     * Returns the data of a style exchange format file.
 +     *
 +     * @param string $filename
 +     * @return  array
 +     */
 +    public static function getStyleData($filename)
 +    {
 +        // open file
 +        $tar = new Tar($filename);
 +
 +        // get style data
 +        $data = self::readStyleData($tar);
 +
 +        // export preview image to temporary location
 +        if (!empty($data['image'])) {
 +            $i = $tar->getIndexByFilename($data['image']);
 +            if ($i !== false) {
 +                $path = FileUtil::getTemporaryFilename('stylePreview_', $data['image'], WCF_DIR . 'tmp/');
 +                $data['image'] = \basename($path);
 +                $tar->extract($i, $path);
 +            }
 +        }
 +
 +        $tar->close();
 +
 +        return $data;
 +    }
 +
 +    /**
 +     * Imports a style.
 +     *
 +     * @param string $filename
 +     * @param int $packageID
 +     * @param StyleEditor $style
 +     * @param bool $skipFontDownload
 +     * @return  StyleEditor
 +     */
 +    public static function import($filename, $packageID = 1, ?self $style = null, $skipFontDownload = false)
 +    {
 +        // open file
 +        $tar = new Tar($filename);
 +
 +        // get style data
 +        $data = self::readStyleData($tar);
 +
 +        $styleData = [
 +            'styleName' => $data['name'],
 +            'variables' => $data['variables'],
 +            'styleVersion' => $data['version'],
 +            'styleDate' => $data['date'],
 +            'copyright' => $data['copyright'],
 +            'license' => $data['license'],
 +            'authorName' => $data['authorName'],
 +            'authorURL' => $data['authorURL'],
 +            'packageName' => $data['packageName'],
 +            'apiVersion' => $data['apiVersion'],
 +        ];
 +
 +        // check if there is an untainted style with the same package name
 +        if ($style === null && !empty($styleData['packageName'])) {
 +            $style = StyleHandler::getInstance()->getStyleByName($styleData['packageName'], true);
 +        }
 +
 +        // handle templates
 +        if (!empty($data['templates'])) {
 +            $templateGroupFolderName = '';
 +            if ($style !== null && $style->templateGroupID) {
 +                $templateGroupFolderName = (new TemplateGroup($style->templateGroupID))->templateGroupFolderName;
 +                $styleData['templateGroupID'] = $style->templateGroupID;
 +            }
 +
 +            if (empty($templateGroupFolderName)) {
 +                // create template group
 +                $templateGroupName = $originalTemplateGroupName = $data['name'];
 +                $templateGroupFolderName = \preg_replace('/[^a-z0-9_-]/i', '', $templateGroupName);
 +                if (empty($templateGroupFolderName)) {
 +                    $templateGroupFolderName = 'generic' . \mb_substr(StringUtil::getRandomID(), 0, 8);
 +                }
 +                $originalTemplateGroupFolderName = $templateGroupFolderName;
 +
 +                // get unique template group name
 +                $i = 1;
 +                while (true) {
 +                    $sql = "SELECT  COUNT(*)
 +                            FROM    wcf" . WCF_N . "_template_group
 +                            WHERE   templateGroupName = ?";
 +                    $statement = WCF::getDB()->prepareStatement($sql);
 +                    $statement->execute([$templateGroupName]);
 +                    if (!$statement->fetchSingleColumn()) {
 +                        break;
 +                    }
 +                    $templateGroupName = $originalTemplateGroupName . '_' . $i;
 +                    $i++;
 +                }
 +
 +                // get unique folder name
 +                $i = 1;
 +                while (true) {
 +                    $sql = "SELECT  COUNT(*)
 +                            FROM    wcf" . WCF_N . "_template_group
 +                            WHERE   templateGroupFolderName = ?";
 +                    $statement = WCF::getDB()->prepareStatement($sql);
 +                    $statement->execute([
 +                        FileUtil::addTrailingSlash($templateGroupFolderName),
 +                    ]);
 +                    if (!$statement->fetchSingleColumn()) {
 +                        break;
 +                    }
 +                    $templateGroupFolderName = $originalTemplateGroupFolderName . '_' . $i;
 +                    $i++;
 +                }
 +
 +                $templateGroupAction = new TemplateGroupAction([], 'create', [
 +                    'data' => [
 +                        'templateGroupName' => $templateGroupName,
 +                        'templateGroupFolderName' => FileUtil::addTrailingSlash($templateGroupFolderName),
 +                    ],
 +                ]);
 +                $returnValues = $templateGroupAction->executeAction();
 +                $styleData['templateGroupID'] = $returnValues['returnValues']->templateGroupID;
 +            }
 +
 +            // import templates
 +            $index = $tar->getIndexByFilename($data['templates']);
 +            if ($index !== false) {
 +                // extract templates tar
 +                $destination = FileUtil::getTemporaryFilename('templates_');
 +                $tar->extract($index, $destination);
 +
 +                // open templates tar and group templates by package
 +                $templatesTar = new Tar($destination);
 +                $contentList = $templatesTar->getContentList();
 +                $packageToTemplates = [];
 +                foreach ($contentList as $val) {
 +                    if ($val['type'] == 'file') {
 +                        $folders = \explode('/', $val['filename']);
 +                        $packageName = \array_shift($folders);
 +                        if (!isset($packageToTemplates[$packageName])) {
 +                            $packageToTemplates[$packageName] = [];
 +                        }
 +                        $packageToTemplates[$packageName][] = [
 +                            'index' => $val['index'],
 +                            'filename' => \implode('/', $folders),
 +                        ];
 +                    }
 +                }
 +
 +                $knownTemplates = [];
 +                if ($style !== null && $style->templateGroupID) {
 +                    $sql = "SELECT  templateName
 +                            FROM    wcf" . WCF_N . "_template
 +                            WHERE   templateGroupID = ?";
 +                    $statement = WCF::getDB()->prepareStatement($sql);
 +                    $statement->execute([$style->templateGroupID]);
 +                    $knownTemplates = $statement->fetchAll(\PDO::FETCH_COLUMN);
 +                }
 +
 +                // copy templates
 +                foreach ($packageToTemplates as $package => $templates) {
 +                    // try to find package
 +                    $sql = "SELECT  *
 +                            FROM    wcf" . WCF_N . "_package
 +                            WHERE   package = ?
 +                                AND isApplication = ?";
 +                    $statement = WCF::getDB()->prepareStatement($sql);
 +                    $statement->execute([
 +                        $package,
 +                        1,
 +                    ]);
 +                    while ($row = $statement->fetchArray()) {
 +                        // get template path
 +                        $templatesDir = FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR . $row['packageDir']) . 'templates/' . $templateGroupFolderName);
 +
 +                        // create template path
 +                        if (!\file_exists($templatesDir)) {
 +                            @\mkdir($templatesDir, 0777);
 +                            FileUtil::makeWritable($templatesDir);
 +                        }
 +
 +                        // copy templates
 +                        foreach ($templates as $template) {
 +                            if (!StringUtil::endsWith($template['filename'], '.tpl')) {
 +                                continue;
 +                            }
 +
 +                            $templatesTar->extract($template['index'], $templatesDir . $template['filename']);
 +
 +                            $templateName = \str_replace('.tpl', '', $template['filename']);
 +                            if (!\in_array($templateName, $knownTemplates)) {
 +                                TemplateEditor::create([
 +                                    'application' => Package::getAbbreviation($package),
 +                                    'packageID' => $row['packageID'],
 +                                    'templateName' => $templateName,
 +                                    'templateGroupID' => $styleData['templateGroupID'],
 +                                ]);
 +                            }
 +                        }
 +                    }
 +                }
 +
 +                // delete tmp file
 +                $templatesTar->close();
 +                @\unlink($destination);
 +            }
 +        }
 +
 +        $duplicateLogo = false;
 +        // duplicate logo if logo matches mobile logo
 +        if (!empty($styleData['variables']['pageLogo']) && !empty($styleData['variables']['pageLogoMobile']) && $styleData['variables']['pageLogo'] == $styleData['variables']['pageLogoMobile']) {
 +            $styleData['variables']['pageLogoMobile'] = 'm-' . \basename($styleData['variables']['pageLogo']);
 +            $duplicateLogo = true;
 +        }
 +
 +        // save style
 +        if ($style === null) {
 +            $styleData['packageID'] = $packageID;
 +            $style = new self(self::create($styleData));
 +
 +            // handle descriptions
 +            if (!empty($data['description'])) {
 +                self::saveLocalizedDescriptions($style, $data['description']);
 +                LanguageFactory::getInstance()->deleteLanguageCache();
 +            }
 +
 +            if ($data['default']) {
 +                $style->setAsDefault();
 +            }
 +        } else {
 +            unset($styleData['styleName']);
 +
 +            $variables = $style->getVariables();
 +            if (!isset($styleData['variables']['individualScss'])) {
 +                $styleData['variables']['individualScss'] = '';
 +            }
 +            if (!isset($styleData['variables']['overrideScss'])) {
 +                $styleData['variables']['overrideScss'] = '';
 +            }
 +
 +            $individualScss = Style::splitLessVariables($variables['individualScss']);
 +            $variables['individualScss'] = Style::joinLessVariables(
 +                $styleData['variables']['individualScss'],
 +                $individualScss['custom']
 +            );
 +            unset($styleData['variables']['individualScss']);
 +
 +            $overrideScss = Style::splitLessVariables($variables['overrideScss']);
 +            $variables['overrideScss'] = Style::joinLessVariables(
 +                $styleData['variables']['overrideScss'],
 +                $overrideScss['custom']
 +            );
 +            unset($styleData['variables']['overrideScss']);
 +
 +            // import variables that have not been explicitly defined before
 +            $implicitVariables = $style->getImplicitVariables();
 +            foreach ($styleData['variables'] as $variableName => $variableValue) {
 +                if (\in_array($variableName, $implicitVariables)) {
 +                    $variables[$variableName] = $variableValue;
 +                }
 +            }
 +
 +            $styleData['variables'] = $variables;
 +
 +            $style->update($styleData);
 +        }
 +
 +        // import images
 +        if (!empty($data['images'])) {
 +            $index = $tar->getIndexByFilename($data['images']);
 +            if ($index !== false) {
 +                // extract images tar
 +                $destination = FileUtil::getTemporaryFilename('images_');
 +                $tar->extract($index, $destination);
 +
 +                // open images tar
 +                $imagesTar = new Tar($destination);
 +                $contentList = $imagesTar->getContentList();
 +                foreach ($contentList as $key => $val) {
 +                    if ($val['type'] == 'file') {
 +                        $path = FileUtil::getRealPath($val['filename']);
 +                        $fileExtension = \pathinfo($path, \PATHINFO_EXTENSION);
 +
 +                        if (!\in_array($fileExtension, self::VALID_IMAGE_EXTENSIONS)) {
 +                            continue;
 +                        }
 +
 +                        if (\strpos($path, '../') !== false) {
 +                            continue;
 +                        }
 +
 +                        $targetFile = FileUtil::getRealPath($style->getAssetPath() . $path);
 +                        if (\strpos(FileUtil::getRelativePath($style->getAssetPath(), $targetFile), '../') !== false) {
 +                            continue;
 +                        }
 +
 +                        // duplicate pageLogo for mobile version
 +                        if ($duplicateLogo && $val['filename'] == $styleData['variables']['pageLogo']) {
 +                            $imagesTar->extract($key, $style->getAssetPath() . 'm-' . \basename($targetFile));
 +                        }
 +
 +                        $imagesTar->extract($key, $targetFile);
 +                        FileUtil::makeWritable($targetFile);
 +                    }
 +                }
 +
 +                // delete tmp file
 +                $imagesTar->close();
 +                @\unlink($destination);
 +            }
 +        }
 +
 +        // import preview image
 +        foreach (['image', 'image2x'] as $type) {
 +            if (!empty($data[$type])) {
 +                $fileExtension = \pathinfo($data[$type], \PATHINFO_EXTENSION);
 +                if (!\in_array($fileExtension, self::VALID_IMAGE_EXTENSIONS)) {
 +                    continue;
 +                }
 +
 +                $index = $tar->getIndexByFilename($data[$type]);
 +                if ($index !== false) {
 +                    $filename = $style->getAssetPath() . 'stylePreview' . ($type === 'image2x' ? '@2x' : '') . '.' . $fileExtension;
 +                    $tar->extract($index, $filename);
 +                    FileUtil::makeWritable($filename);
 +
 +                    if (\file_exists($filename)) {
 +                        try {
 +                            if (($imageData = \getimagesize($filename)) !== false) {
 +                                switch ($imageData[2]) {
 +                                    case \IMAGETYPE_PNG:
 +                                    case \IMAGETYPE_JPEG:
 +                                    case \IMAGETYPE_GIF:
 +                                        $style->update([$type => 'style-' . $style->styleID . '/stylePreview' . ($type === 'image2x' ? '@2x' : '') . '.' . $fileExtension]);
 +                                }
 +                            }
 +                        } catch (SystemException $e) {
 +                            // broken image
 +                        }
 +                    }
 +                }
 +            }
 +        }
 +
 +        // import cover photo
 +        if (!empty($data['coverPhoto'])) {
 +            $fileExtension = \pathinfo($data['coverPhoto'], \PATHINFO_EXTENSION);
 +            $index = $tar->getIndexByFilename($data['coverPhoto']);
 +            if ($index !== false && \in_array($fileExtension, self::VALID_IMAGE_EXTENSIONS)) {
 +                $coverPhoto = "{$style->getAssetPath()}coverPhoto.{$fileExtension}";
 +                $tar->extract($index, $coverPhoto);
 +                FileUtil::makeWritable($coverPhoto);
 +
 +                if (\file_exists($coverPhoto)) {
 +                    try {
 +                        if (($imageData = \getimagesize($coverPhoto)) !== false) {
 +                            switch ($imageData[2]) {
 +                                case \IMAGETYPE_PNG:
 +                                case \IMAGETYPE_JPEG:
 +                                case \IMAGETYPE_GIF:
 +                                    $style->update(['coverPhotoExtension' => $fileExtension]);
 +
 +                                    // Reload the style editor to include the cover photo.
 +                                    $style = new self(new Style($style->styleID));
 +                                    $style->createCoverPhotoVariant();
 +                                    break;
 +                            }
 +                        }
 +                    } catch (SystemException $e) {
 +                        // broken image
 +                    }
 +                }
 +            }
 +        }
 +
 +        if (!$skipFontDownload) {
 +            // download google fonts
 +            $fontManager = FontManager::getInstance();
++            $style->loadVariables();
 +            $family = $style->getVariable('wcfFontFamilyGoogle');
 +            try {
 +                $fontManager->downloadFamily($family);
 +            } catch (FontDownloadFailed $e) {
 +                // ignore
 +            }
 +        }
 +
 +        $tar->close();
 +
 +        return $style;
 +    }
 +
 +    /**
 +     * Saves localized style descriptions.
 +     *
 +     * @param StyleEditor $styleEditor
 +     * @param string[] $descriptions
 +     */
 +    protected static function saveLocalizedDescriptions(self $styleEditor, array $descriptions)
 +    {
 +        // localize package information
 +        $sql = "REPLACE INTO    wcf" . WCF_N . "_language_item
 +                                (languageID, languageItem, languageItemValue, languageCategoryID, packageID)
 +                VALUES          (?, ?, ?, ?, ?)";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +
 +        // get language list
 +        $languageList = new LanguageList();
 +        $languageList->readObjects();
 +
 +        // workaround for WCFSetup
 +        if (!PACKAGE_ID) {
 +            $sql = "SELECT  *
 +                    FROM    wcf" . WCF_N . "_language_category
 +                    WHERE   languageCategory = ?";
 +            $statement2 = WCF::getDB()->prepareStatement($sql);
 +            $statement2->execute(['wcf.style']);
 +            $languageCategory = $statement2->fetchObject(LanguageCategory::class);
 +        } else {
 +            $languageCategory = LanguageFactory::getInstance()->getCategory('wcf.style');
 +        }
 +
 +        foreach ($languageList as $language) {
 +            if (isset($descriptions[$language->languageCode])) {
 +                $statement->execute([
 +                    $language->languageID,
 +                    'wcf.style.styleDescription' . $styleEditor->styleID,
 +                    $descriptions[$language->languageCode],
 +                    $languageCategory->languageCategoryID,
 +                    $styleEditor->packageID,
 +                ]);
 +            }
 +        }
 +
 +        $styleEditor->update([
 +            'styleDescription' => 'wcf.style.styleDescription' . $styleEditor->styleID,
 +        ]);
 +    }
 +
 +    /**
 +     * Returns available location path.
 +     *
 +     * @param string $location
 +     * @return  string
 +     */
 +    protected static function getFileLocation($location)
 +    {
 +        $location = FileUtil::removeLeadingSlash(FileUtil::removeTrailingSlash($location));
 +        $location = WCF_DIR . $location;
 +
 +        $index = null;
 +        do {
 +            $directory = $location . ($index === null ? '' : $index);
 +            if (!\is_dir($directory)) {
 +                @\mkdir($directory, 0777, true);
 +                FileUtil::makeWritable($directory);
 +
 +                return FileUtil::addTrailingSlash($directory);
 +            }
 +
 +            $index = ($index === null ? 2 : ($index + 1));
 +        } while (true);
 +
 +        // this should never happen
 +        throw new \LogicException();
 +    }
 +
 +    /**
 +     * Exports this style.
 +     *
 +     * @param bool $templates
 +     * @param bool $images
 +     * @param string $packageName
 +     */
 +    public function export($templates = false, $images = false, $packageName = '')
 +    {
 +        // create style tar
 +        $styleTarName = FileUtil::getTemporaryFilename('style_', '.tgz');
 +        $styleTar = new TarWriter($styleTarName, true);
 +
 +        // append style preview image
 +        if ($this->image && @\file_exists(WCF_DIR . 'images/' . $this->image)) {
 +            $styleTar->add(
 +                WCF_DIR . 'images/' . $this->image,
 +                '',
 +                FileUtil::addTrailingSlash(\dirname(WCF_DIR . 'images/' . $this->image))
 +            );
 +        }
 +        if ($this->image2x && @\file_exists(WCF_DIR . 'images/' . $this->image2x)) {
 +            $styleTar->add(
 +                WCF_DIR . 'images/' . $this->image2x,
 +                '',
 +                FileUtil::addTrailingSlash(\dirname(WCF_DIR . 'images/' . $this->image2x))
 +            );
 +        }
 +
 +        // append cover photo
 +        $coverPhoto = $this->coverPhotoExtension ? $this->getCoverPhotoLocation(false) : '';
 +        if ($coverPhoto && @\file_exists($coverPhoto)) {
 +            $styleTar->add($coverPhoto, '', FileUtil::addTrailingSlash(\dirname($coverPhoto)));
 +        }
 +
 +        // fetch style description
 +        $sql = "SELECT      language.languageCode, language_item.languageItemValue
 +                FROM        wcf" . WCF_N . "_language_item language_item
 +                LEFT JOIN   wcf" . WCF_N . "_language language
 +                ON          language.languageID = language_item.languageID
 +                WHERE       language_item.languageItem = ?";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute([$this->styleDescription]);
 +        $styleDescriptions = $statement->fetchMap('languageCode', 'languageItemValue');
 +
 +        // create style info file
 +        $xml = new XMLWriter();
 +        $xml->beginDocument(
 +            'style',
 +            'http://www.woltlab.com',
 +            'http://www.woltlab.com/XSD/5.4/style.xsd'
 +        );
 +
 +        // general block
 +        $xml->startElement('general');
 +        $xml->writeElement('stylename', $this->styleName);
 +        $xml->writeElement('packageName', $this->packageName);
 +
 +        // style description
 +        foreach ($styleDescriptions as $languageCode => $value) {
 +            $xml->writeElement('description', $value, ['language' => $languageCode]);
 +        }
 +
 +        $xml->writeElement('date', $this->styleDate);
 +        $xml->writeElement('version', $this->styleVersion);
 +        $xml->writeElement('apiVersion', $this->apiVersion);
 +        if ($this->image) {
 +            $xml->writeElement('image', \basename($this->image));
 +        }
 +        if ($this->image2x) {
 +            $xml->writeElement('image2x', \basename($this->image2x));
 +        }
 +        if ($coverPhoto) {
 +            $xml->writeElement('coverPhoto', \basename(FileUtil::unifyDirSeparator($coverPhoto)));
 +        }
 +        if ($this->copyright) {
 +            $xml->writeElement('copyright', $this->copyright);
 +        }
 +        if ($this->license) {
 +            $xml->writeElement('license', $this->license);
 +        }
 +        $xml->endElement();
 +
 +        // author block
 +        $xml->startElement('author');
 +        $xml->writeElement('authorname', $this->authorName);
 +        if ($this->authorURL) {
 +            $xml->writeElement('authorurl', $this->authorURL);
 +        }
 +        $xml->endElement();
 +
 +        // files block
 +        $xml->startElement('files');
 +        $xml->writeElement('variables', 'variables.xml');
 +        if ($templates) {
 +            $xml->writeElement('templates', 'templates.tar');
 +        }
 +        if ($images) {
 +            $xml->writeElement('images', 'images.tar', ['path' => $this->imagePath]);
 +        }
 +        $xml->endElement();
 +
 +        // append style info file to style tar
 +        $styleTar->addString(self::INFO_FILE, $xml->endDocument());
 +
 +        // create variable list
 +        $xml->beginDocument(
 +            'variables',
 +            'http://www.woltlab.com',
 +            'http://www.woltlab.com/XSD/5.4/styleVariables.xsd'
 +        );
 +
 +        // get variables
 +        $sql = "SELECT      variable.variableName, value.variableValue
 +                FROM        wcf" . WCF_N . "_style_variable_value value
 +                LEFT JOIN   wcf" . WCF_N . "_style_variable variable
 +                ON          variable.variableID = value.variableID
 +                WHERE       value.styleID = ?";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute([$this->styleID]);
 +        while ($row = $statement->fetchArray()) {
 +            $xml->writeElement('variable', $row['variableValue'], ['name' => $row['variableName']]);
 +        }
 +
 +        // append variable list to style tar
 +        $styleTar->addString('variables.xml', $xml->endDocument());
 +
 +        if ($templates && $this->templateGroupID) {
 +            $templateGroup = new TemplateGroup($this->templateGroupID);
 +
 +            // create templates tar
 +            $templatesTarName = FileUtil::getTemporaryFilename('templates', '.tar');
 +            $templatesTar = new TarWriter($templatesTarName);
 +            FileUtil::makeWritable($templatesTarName);
 +
 +            // append templates to tar
 +            // get templates
 +            $sql = "SELECT      template.*, package.package
 +                    FROM        wcf" . WCF_N . "_template template
 +                    LEFT JOIN   wcf" . WCF_N . "_package package
 +                    ON          package.packageID = template.packageID
 +                    WHERE       template.templateGroupID = ?";
 +            $statement = WCF::getDB()->prepareStatement($sql);
 +            $statement->execute([$this->templateGroupID]);
 +            while ($row = $statement->fetchArray()) {
 +                $packageDir = 'com.woltlab.wcf';
 +                $package = null;
 +
 +                if (Template::isSystemCritical($row['templateName'])) {
 +                    continue;
 +                }
 +
 +                if ($row['application'] != 'wcf') {
 +                    $application = ApplicationHandler::getInstance()->getApplication($row['application']);
 +                    $package = PackageCache::getInstance()->getPackage($application->packageID);
 +                    $packageDir = $package->package;
 +                } else {
 +                    $application = ApplicationHandler::getInstance()->getWCF();
 +                    $package = PackageCache::getInstance()->getPackage($application->packageID);
 +                }
 +
 +                $filename = FileUtil::addTrailingSlash(FileUtil::getRealPath(WCF_DIR . $package->packageDir . 'templates/' . $templateGroup->templateGroupFolderName)) . $row['templateName'] . '.tpl';
 +                $templatesTar->add($filename, $packageDir, \dirname($filename));
 +            }
 +
 +            // append templates tar to style tar
 +            $templatesTar->create();
 +            $styleTar->add($templatesTarName, 'templates.tar', $templatesTarName);
 +            @\unlink($templatesTarName);
 +        }
 +
 +        if ($images) {
 +            // create images tar
 +            $imagesTarName = FileUtil::getTemporaryFilename('images_', '.tar');
 +            $imagesTar = new TarWriter($imagesTarName);
 +            FileUtil::makeWritable($imagesTarName);
 +
 +            $regEx = new Regex('\.(jpg|jpeg|gif|png|svg|ico|json|xml|txt|webp)$', Regex::CASE_INSENSITIVE);
 +            $iterator = new \RecursiveIteratorIterator(
 +                new \RecursiveDirectoryIterator(
 +                    $this->getAssetPath(),
 +                    \FilesystemIterator::SKIP_DOTS
 +                ),
 +                \RecursiveIteratorIterator::SELF_FIRST
 +            );
 +            foreach ($iterator as $file) {
 +                /** @var \SplFileInfo $file */
 +                if (!$file->isFile()) {
 +                    continue;
 +                }
 +                if (!$regEx->match($file->getPathName())) {
 +                    continue;
 +                }
 +
 +                // Skip preview images and cover photos.
 +                if (
 +                    $this->image
 +                    && FileUtil::unifyDirSeparator($file->getPathname()) === FileUtil::unifyDirSeparator(WCF_DIR . 'images/' . $this->image)
 +                ) {
 +                    continue;
 +                }
 +                if (
 +                    $this->image2x
 +                    && FileUtil::unifyDirSeparator($file->getPathname()) === FileUtil::unifyDirSeparator(WCF_DIR . 'images/' . $this->image2x)
 +                ) {
 +                    continue;
 +                }
 +                if (
 +                    $coverPhoto
 +                    && FileUtil::unifyDirSeparator($file->getPathname()) === FileUtil::unifyDirSeparator($coverPhoto)
 +                ) {
 +                    continue;
 +                }
 +
 +                $imagesTar->add($file->getPathName(), '', $this->getAssetPath());
 +            }
 +            // append images tar to style tar
 +            $imagesTar->create();
 +            $styleTar->add($imagesTarName, 'images.tar', $imagesTarName);
 +            @\unlink($imagesTarName);
 +        }
 +
 +        // output file content
 +        $styleTar->create();
 +
 +        // export as style package
 +        if (empty($packageName)) {
 +            \readfile($styleTarName);
 +        } else {
 +            // export as package
 +
 +            // create package tar
 +            $packageTarName = FileUtil::getTemporaryFilename('package_', '.tar.gz');
 +            $packageTar = new TarWriter($packageTarName, true);
 +
 +            // append style tar
 +            $styleTarName = FileUtil::unifyDirSeparator($styleTarName);
 +            $packageTar->add($styleTarName, '', FileUtil::addTrailingSlash(\dirname($styleTarName)));
 +
 +            // create package.xml
 +            $xml->beginDocument(
 +                'package',
 +                'http://www.woltlab.com',
 +                'http://www.woltlab.com/XSD/5.4/package.xsd',
 +                ['name' => $packageName]
 +            );
 +
 +            $xml->startElement('packageinformation');
 +            $xml->writeElement('packagename', $this->styleName);
 +
 +            // description
 +            foreach ($styleDescriptions as $languageCode => $value) {
 +                $xml->writeElement('packagedescription', $value, ['language' => $languageCode]);
 +            }
 +
 +            $xml->writeElement('version', $this->styleVersion);
 +            $xml->writeElement('date', $this->styleDate);
 +            $xml->endElement();
 +
 +            $xml->startElement('authorinformation');
 +            $xml->writeElement('author', $this->authorName);
 +            if ($this->authorURL) {
 +                $xml->writeElement('authorurl', $this->authorURL);
 +            }
 +            $xml->endElement();
 +
 +            $xml->startElement('requiredpackages');
 +            $xml->writeElement(
 +                'requiredpackage',
 +                'com.woltlab.wcf',
 +                ['minversion' => PackageCache::getInstance()->getPackageByIdentifier('com.woltlab.wcf')->packageVersion]
 +            );
 +            $xml->endElement();
 +
 +            $xml->startElement('excludedpackages');
 +            $xml->writeElement('excludedpackage', 'com.woltlab.wcf', ['version' => self::EXCLUDE_WCF_VERSION]);
 +            $xml->endElement();
 +
 +            // @deprecated 5.2
 +            $xml->startElement('compatibility');
 +            foreach (self::$compatibilityApiVersions as $apiVersion) {
 +                $xml->writeElement('api', '', ['version' => $apiVersion]);
 +            }
 +            $xml->endElement();
 +
 +            $xml->startElement('instructions', ['type' => 'install']);
 +            $xml->writeElement('instruction', \basename($styleTarName), ['type' => 'style']);
 +            $xml->endElement();
 +
 +            // append package info file to package tar
 +            $packageTar->addString(PackageArchive::INFO_FILE, $xml->endDocument());
 +
 +            $packageTar->create();
 +            \readfile($packageTarName);
 +            @\unlink($packageTarName);
 +        }
 +
 +        @\unlink($styleTarName);
 +    }
 +
 +    /**
 +     * Sets the variables of a style.
 +     *
 +     * @param string[] $variables
 +     */
 +    public function setVariables(array $variables = [])
 +    {
 +        // delete old variables
 +        $sql = "DELETE FROM wcf" . WCF_N . "_style_variable_value
 +                WHERE       styleID = ?";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute([$this->styleID]);
 +
 +        // insert new variables
 +        if (!empty($variables)) {
 +            $sql = "SELECT  *
 +                    FROM    wcf" . WCF_N . "_style_variable";
 +            $statement = WCF::getDB()->prepareStatement($sql);
 +            $statement->execute();
 +            $styleVariables = [];
 +            while ($row = $statement->fetchArray()) {
 +                $variableName = $row['variableName'];
 +
 +                if (isset($variables[$variableName])) {
 +                    // compare value, save only if differs from default
 +                    if ($variables[$variableName] != $row['defaultValue']) {
 +                        $styleVariables[$row['variableID']] = $variables[$variableName];
 +                    }
 +                }
 +            }
 +
 +            if (!empty($styleVariables)) {
 +                $sql = "INSERT INTO wcf" . WCF_N . "_style_variable_value
 +                                    (styleID, variableID, variableValue)
 +                        VALUES      (?, ?, ?)";
 +                $statement = WCF::getDB()->prepareStatement($sql);
 +
 +                WCF::getDB()->beginTransaction();
 +                foreach ($styleVariables as $variableID => $variableValue) {
 +                    $statement->execute([
 +                        $this->styleID,
 +                        $variableID,
 +                        $variableValue,
 +                    ]);
 +                }
 +                WCF::getDB()->commitTransaction();
 +            }
 +        }
 +
 +        $this->writeStyleFile();
 +    }
 +
 +    /**
 +     * Writes the style-*.css file.
 +     */
 +    public function writeStyleFile()
 +    {
 +        StyleCompiler::getInstance()->compile($this->getDecoratedObject());
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     * @return  Style
 +     */
 +    public static function create(array $parameters = [])
 +    {
 +        $variables = null;
 +        if (isset($parameters['variables'])) {
 +            $variables = $parameters['variables'];
 +            unset($parameters['variables']);
 +        }
 +
 +        // default values
 +        if (!isset($parameters['packageID'])) {
 +            $parameters['packageID'] = 1;
 +        }
 +        if (!isset($parameters['styleDate'])) {
 +            $parameters['styleDate'] = \gmdate('Y-m-d', TIME_NOW);
 +        }
 +
 +        // check if no default style is defined
 +        $sql = "SELECT  styleID
 +                FROM    wcf" . WCF_N . "_style
 +                WHERE   isDefault = ?";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute([1]);
 +        $row = $statement->fetchArray();
 +
 +        // no default style exists
 +        if ($row === false) {
 +            $parameters['isDefault'] = 1;
 +        }
 +
 +        /** @var Style $style */
 +        $style = parent::create($parameters);
 +        $styleEditor = new self($style);
 +
 +        // create asset path
 +        FileUtil::makePath($style->getAssetPath());
 +
 +        $styleEditor->update([
 +            'imagePath' => FileUtil::getRelativePath(WCF_DIR, $style->getAssetPath()),
 +        ]);
 +        $styleEditor = new self(new Style($style->styleID));
 +
 +        // save variables
 +        if ($variables !== null) {
 +            $styleEditor->setVariables($variables);
 +        }
 +
 +        return $style;
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public static function resetCache()
 +    {
 +        StyleCacheBuilder::getInstance()->reset();
 +    }
  }