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