From: Tim Düsterhus Date: Thu, 12 Aug 2021 08:09:17 +0000 (+0200) Subject: Merge branch '5.3' into 5.4 X-Git-Tag: 5.4.5_RC_1~51 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=d30a32f753e400e3e817ec830493021f8526a95a;p=GitHub%2FWoltLab%2FWCF.git Merge branch '5.3' into 5.4 --- d30a32f753e400e3e817ec830493021f8526a95a diff --cc wcfsetup/install/files/lib/data/style/StyleEditor.class.php index a0c38dde9c,007cf470e4..045f2fec16 --- a/wcfsetup/install/files/lib/data/style/StyleEditor.class.php +++ b/wcfsetup/install/files/lib/data/style/StyleEditor.class.php @@@ -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 - * @package WoltLabSuite\Core\Data\Style - * - * @method Style getDecoratedObject() - * @mixin Style + * + * @author Marcel Werk + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @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(); + } }