diff --git a/com.woltlab.wcf/package.xml b/com.woltlab.wcf/package.xml index bc136faa0db..dc24d8a9d08 100644 --- a/com.woltlab.wcf/package.xml +++ b/com.woltlab.wcf/package.xml @@ -64,6 +64,8 @@ acp/update_com.woltlab.wcf_6.3_styleTemplateGroup.php acp/update_com.woltlab.wcf_6.3_styleVariables.php + acp/update_com.woltlab.wcf_6.3_styleProtection.php + acp/update_com.woltlab.wcf_6.3_styleAuthoredValues.php acp/database/update_com.woltlab.wcf_6.3_step2.php acp/update_com.woltlab.wcf_6.3_embeddedObjects.php diff --git a/ts/WoltLabSuite/Core/Acp/Ui/Style/Editor.ts b/ts/WoltLabSuite/Core/Acp/Ui/Style/Editor.ts index b54c78cc168..828466b368b 100644 --- a/ts/WoltLabSuite/Core/Acp/Ui/Style/Editor.ts +++ b/ts/WoltLabSuite/Core/Acp/Ui/Style/Editor.ts @@ -6,8 +6,8 @@ * @license GNU Lesser General Public License */ -import * as Ajax from "../../../Ajax"; import * as Core from "../../../Core"; +import * as ColorUtil from "../../../ColorUtil"; import DomUtil from "../../../Dom/Util"; import * as EventHandler from "../../../Event/Handler"; import * as UiScreen from "../../../Ui/Screen"; @@ -22,7 +22,24 @@ let _updateRegionMarker: () => void; interface StyleEditorOptions { isTainted: boolean; - styleId: number; +} + +/** + * Compares two colors regardless of their notation, e.g. `.8` and `0.8` for the alpha channel. + */ +function isSameColor(a: string, b: string): boolean { + if (a === b) { + return true; + } + + if (!ColorUtil.isValidColor(a) || !ColorUtil.isValidColor(b)) { + return false; + } + + const colorA = ColorUtil.stringToRgba(a); + const colorB = ColorUtil.stringToRgba(b); + + return colorA.r === colorB.r && colorA.g === colorB.g && colorA.b === colorB.b && colorA.a === colorB.a; } /** @@ -81,28 +98,6 @@ function handleScss(isTainted: boolean): void { } } -function handleProtection(styleId: number): void { - const button = document.getElementById("styleDisableProtectionSubmit") as HTMLButtonElement; - const checkbox = document.getElementById("styleDisableProtectionConfirm") as HTMLInputElement; - - checkbox.addEventListener("change", () => { - button.disabled = !checkbox.checked; - }); - - button.addEventListener("click", () => { - Ajax.apiOnce({ - data: { - actionName: "markAsTainted", - className: "wcf\\data\\style\\StyleAction", - objectIDs: [styleId], - }, - success: () => { - window.location.reload(); - }, - }); - }); -} - /** * Mirrors the selected style options in the preview, e.g. `data-page-header-layout`. */ @@ -206,11 +201,30 @@ function initVisualEditor(): void { const wrapper = document.getElementById("spVariablesWrapper")!; wrapper.querySelectorAll(".styleVariableColor").forEach((colorField: HTMLElement) => { const variableName = colorField.dataset.store!.replace(/_value$/, ""); + const input = document.getElementById(colorField.dataset.store!) as HTMLInputElement; + const restoreButton = wrapper.querySelector( + `.spColorRestore[data-store="${colorField.dataset.store!}"]`, + ); + + const updateRestoreButton = () => { + if (restoreButton !== null) { + restoreButton.hidden = isSameColor(input.value, restoreButton.dataset.authoredColor!); + } + }; + + restoreButton?.addEventListener("click", () => { + const authoredColor = restoreButton.dataset.authoredColor!; + + input.value = authoredColor; + colorField.dataset.color = authoredColor; + colorField.style.setProperty("background-color", authoredColor); + }); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.attributeName === "style") { spWindow.style.setProperty(`--${variableName}`, colorField.style.getPropertyValue("background-color")); + updateRestoreButton(); } }); }); @@ -220,6 +234,7 @@ function initVisualEditor(): void { }); spWindow.style.setProperty(`--${variableName}`, colorField.style.getPropertyValue("background-color")); + updateRestoreButton(); }); // category selection by clicking on the area @@ -281,10 +296,6 @@ export function setup(options: StyleEditorOptions): void { handleLayoutWidth(); handleScss(options.isTainted); - if (!options.isTainted) { - handleProtection(options.styleId); - } - initVisualEditor(); handleStyleOptions(); diff --git a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts index 96991049a6b..824e3150076 100644 --- a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts +++ b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts @@ -191,7 +191,9 @@ class UiColorPicker implements DialogCallbackObject { this.opacityInput!.addEventListener("input", (event) => { this.setOpacity(event); }); - + }, + onShow: () => { + // The stored value can change while the dialog is closed. if (ColorUtil.isValidColor(this.input.value)) { this.setInitialColor(this.input.value); } else if (this.element.dataset.color && ColorUtil.isValidColor(this.element.dataset.color)) { @@ -199,8 +201,7 @@ class UiColorPicker implements DialogCallbackObject { } else { this.setInitialColor("#FFF0"); } - }, - onShow: () => { + this.colorTextInput!.focus(); this.colorTextInput!.select(); }, diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php index ea89f87ca72..2f9ebab65b3 100644 --- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php @@ -253,6 +253,11 @@ ->columns([ IntDatabaseTableColumn::create('templateGroupID'), ]), + PartialDatabaseTable::create('wcf1_style_variable_value') + ->columns([ + MediumtextDatabaseTableColumn::create('authoredValue'), + MediumtextDatabaseTableColumn::create('authoredValueDarkMode'), + ]), PartialDatabaseTable::create('wcf1_user_activity_event') ->columns([ IntDatabaseTableColumn::create('userID'), diff --git a/wcfsetup/install/files/acp/style/acpStyleEditor.css b/wcfsetup/install/files/acp/style/acpStyleEditor.css index 5f571b070ef..370a6d2a05b 100644 --- a/wcfsetup/install/files/acp/style/acpStyleEditor.css +++ b/wcfsetup/install/files/acp/style/acpStyleEditor.css @@ -1055,6 +1055,17 @@ html:not(.touch) .spColorBox { display: flex; flex: 0 0 auto; } +.spColorRestore { + color: var(--wcfContentDimmedText); + flex: 0 0 auto; + margin-left: auto; +} +.spColorRestore:not([hidden]) { + display: flex; +} +.spColorRestore:hover { + color: var(--wcfContentText); +} #spWindow[data-page-header-layout="classic"] + #spSidebar .spColorScope[data-style-option="pageHeaderLayout"], #spWindow[data-page-footer-layout="classic"] + #spSidebar .spColorScope[data-style-option="pageFooterLayout"] { display: none; diff --git a/wcfsetup/install/files/acp/templates/styleAdd.tpl b/wcfsetup/install/files/acp/templates/styleAdd.tpl index dc768ba5b42..448274ab81a 100644 --- a/wcfsetup/install/files/acp/templates/styleAdd.tpl +++ b/wcfsetup/install/files/acp/templates/styleAdd.tpl @@ -8,7 +8,6 @@ require(["WoltLabSuite/Core/Acp/Ui/Style/Editor", "WoltLabSuite/Core/Ui/Color/Picker"], (AcpUiStyleEditor, ColorPicker) => { AcpUiStyleEditor.setup({ isTainted: {if $isTainted}true{else}false{/if}, - styleId: {if $action === 'edit'}{$style->styleID}{else}0{/if}, }); ColorPicker.fromSelector(".jsColorPicker"); @@ -116,105 +115,144 @@ {/if} - -
-
- - {if $errorField == 'authorName'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.acp.style.authorName.error.{$errorType}{/lang} - {/if} - - {/if} -
- - -
-
- - {if $errorField == 'copyright'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.acp.style.copyright.error.{$errorType}{/lang} - {/if} - - {/if} -
- - -
-
- - {if $errorField == 'styleVersion'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.acp.style.styleVersion.error.{$errorType}{/lang} - {/if} - - {/if} -
- - -
-
- - {if $errorField == 'styleDate'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.acp.style.styleDate.error.{$errorType}{/lang} - {/if} - - {/if} -
- - -
-
- - {if $errorField == 'license'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.acp.style.license.error.{$errorType}{/lang} - {/if} - - {/if} -
- - -
-
- - {if $errorField == 'authorURL'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.acp.style.authorURL.error.{$errorType}{/lang} - {/if} - - {/if} -
- - -
-
- - {if $errorField == 'packageName'} - {lang}wcf.acp.style.packageName.error.{$errorType}{/lang} - {/if} -
- + {if $isTainted} + +
+
+ + {if $errorField == 'authorName'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.acp.style.authorName.error.{$errorType}{/lang} + {/if} + + {/if} +
+ + +
+
+ + {if $errorField == 'copyright'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.acp.style.copyright.error.{$errorType}{/lang} + {/if} + + {/if} +
+ + +
+
+ + {if $errorField == 'styleVersion'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.acp.style.styleVersion.error.{$errorType}{/lang} + {/if} + + {/if} +
+ + +
+
+ + {if $errorField == 'styleDate'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.acp.style.styleDate.error.{$errorType}{/lang} + {/if} + + {/if} +
+ + +
+
+ + {if $errorField == 'license'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.acp.style.license.error.{$errorType}{/lang} + {/if} + + {/if} +
+ + +
+
+ + {if $errorField == 'authorURL'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.acp.style.authorURL.error.{$errorType}{/lang} + {/if} + + {/if} +
+ + +
+
+ + {if $errorField == 'packageName'} + {lang}wcf.acp.style.packageName.error.{$errorType}{/lang} + {/if} +
+ + {else} + {if $authorName !== ''} +
+
{lang}wcf.acp.style.authorName{/lang}
+
{$authorName}
+
+ {/if} + {if $copyright !== ''} +
+
{lang}wcf.acp.style.copyright{/lang}
+
{$copyright}
+
+ {/if} + {if $styleVersion !== ''} +
+
{lang}wcf.acp.style.styleVersion{/lang}
+
{$styleVersion}
+
+ {/if} + {if $styleDate !== ''} +
+
{lang}wcf.acp.style.styleDate{/lang}
+
{$styleDate}
+
+ {/if} + {if $license !== ''} +
+
{lang}wcf.acp.style.license{/lang}
+
{$license}
+
+ {/if} + {if $authorURL !== ''} +
+
{lang}wcf.acp.style.authorURL{/lang}
+
{$authorURL}
+
+ {/if} + {/if}
@@ -1012,6 +1050,9 @@ {if $colorScopes[$spColor]|isset} {icon name='circle-info'} {/if} + {if $authoredVariables[$spColor]|isset} + + {/if} {$spType} @@ -1180,17 +1221,4 @@ -
-

{lang}wcf.acp.style.protected.description{/lang}

- -
-
-
-
- -
- -
-
- {include file='footer'} diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_styleAuthoredValues.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_styleAuthoredValues.php new file mode 100644 index 00000000000..a53ed774552 --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_styleAuthoredValues.php @@ -0,0 +1,17 @@ +prepare($sql); +$statement->execute(['com.woltlab.wcf']); diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_styleProtection.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_styleProtection.php new file mode 100644 index 00000000000..022acec7c16 --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_styleProtection.php @@ -0,0 +1,32 @@ +prepare($sql); +$statement->execute([ + "/* WCF_STYLE_CUSTOM_USER_MODIFICATIONS */\n", + 0, + '', + 'individualScss', + 'individualScssDarkMode', + 'overrideScss', +]); + +$sql = "UPDATE wcf1_style + SET isTainted = ? + WHERE isTainted = ? + AND packageName = ?"; +$statement = WCF::getDB()->prepare($sql); +$statement->execute([1, 0, '']); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Editor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Editor.js index ef89d12ef64..99ff57fa542 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Editor.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Editor.js @@ -5,14 +5,14 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License */ -define(["require", "exports", "tslib", "../../../Ajax", "../../../Core", "../../../Dom/Util", "../../../Event/Handler", "../../../Ui/Screen"], function (require, exports, tslib_1, Ajax, Core, Util_1, EventHandler, UiScreen) { +define(["require", "exports", "tslib", "../../../Core", "../../../ColorUtil", "../../../Dom/Util", "../../../Event/Handler", "../../../Ui/Screen"], function (require, exports, tslib_1, Core, ColorUtil, Util_1, EventHandler, UiScreen) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; exports.hideVisualEditor = hideVisualEditor; exports.showVisualEditor = showVisualEditor; - Ajax = tslib_1.__importStar(Ajax); Core = tslib_1.__importStar(Core); + ColorUtil = tslib_1.__importStar(ColorUtil); Util_1 = tslib_1.__importDefault(Util_1); EventHandler = tslib_1.__importStar(EventHandler); UiScreen = tslib_1.__importStar(UiScreen); @@ -22,6 +22,20 @@ define(["require", "exports", "tslib", "../../../Ajax", "../../../Core", "../../ let _isVisible = true; let _isSmartphone = false; let _updateRegionMarker; + /** + * Compares two colors regardless of their notation, e.g. `.8` and `0.8` for the alpha channel. + */ + function isSameColor(a, b) { + if (a === b) { + return true; + } + if (!ColorUtil.isValidColor(a) || !ColorUtil.isValidColor(b)) { + return false; + } + const colorA = ColorUtil.stringToRgba(a); + const colorB = ColorUtil.stringToRgba(b); + return colorA.r === colorB.r && colorA.g === colorB.g && colorA.b === colorB.b && colorA.a === colorB.a; + } /** * Handles the switch between static and fluid layout. */ @@ -74,25 +88,6 @@ define(["require", "exports", "tslib", "../../../Ajax", "../../../Core", "../../ }); } } - function handleProtection(styleId) { - const button = document.getElementById("styleDisableProtectionSubmit"); - const checkbox = document.getElementById("styleDisableProtectionConfirm"); - checkbox.addEventListener("change", () => { - button.disabled = !checkbox.checked; - }); - button.addEventListener("click", () => { - Ajax.apiOnce({ - data: { - actionName: "markAsTainted", - className: "wcf\\data\\style\\StyleAction", - objectIDs: [styleId], - }, - success: () => { - window.location.reload(); - }, - }); - }); - } /** * Mirrors the selected style options in the preview, e.g. `data-page-header-layout`. */ @@ -183,10 +178,24 @@ define(["require", "exports", "tslib", "../../../Ajax", "../../../Core", "../../ const wrapper = document.getElementById("spVariablesWrapper"); wrapper.querySelectorAll(".styleVariableColor").forEach((colorField) => { const variableName = colorField.dataset.store.replace(/_value$/, ""); + const input = document.getElementById(colorField.dataset.store); + const restoreButton = wrapper.querySelector(`.spColorRestore[data-store="${colorField.dataset.store}"]`); + const updateRestoreButton = () => { + if (restoreButton !== null) { + restoreButton.hidden = isSameColor(input.value, restoreButton.dataset.authoredColor); + } + }; + restoreButton?.addEventListener("click", () => { + const authoredColor = restoreButton.dataset.authoredColor; + input.value = authoredColor; + colorField.dataset.color = authoredColor; + colorField.style.setProperty("background-color", authoredColor); + }); const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.attributeName === "style") { spWindow.style.setProperty(`--${variableName}`, colorField.style.getPropertyValue("background-color")); + updateRestoreButton(); } }); }); @@ -194,6 +203,7 @@ define(["require", "exports", "tslib", "../../../Ajax", "../../../Core", "../../ attributes: true, }); spWindow.style.setProperty(`--${variableName}`, colorField.style.getPropertyValue("background-color")); + updateRestoreButton(); }); // category selection by clicking on the area const buttonToggleColorPalette = document.querySelector(".jsButtonToggleColorPalette"); @@ -242,9 +252,6 @@ define(["require", "exports", "tslib", "../../../Ajax", "../../../Core", "../../ function setup(options) { handleLayoutWidth(); handleScss(options.isTainted); - if (!options.isTainted) { - handleProtection(options.styleId); - } initVisualEditor(); handleStyleOptions(); UiScreen.on("screen-sm-down", { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js index 4aac61b166a..14d1ec3ad06 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js @@ -143,6 +143,9 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti this.opacityInput.addEventListener("input", (event) => { this.setOpacity(event); }); + }, + onShow: () => { + // The stored value can change while the dialog is closed. if (ColorUtil.isValidColor(this.input.value)) { this.setInitialColor(this.input.value); } @@ -152,8 +155,6 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti else { this.setInitialColor("#FFF0"); } - }, - onShow: () => { this.colorTextInput.focus(); this.colorTextInput.select(); }, diff --git a/wcfsetup/install/files/lib/acp/form/StyleEditForm.class.php b/wcfsetup/install/files/lib/acp/form/StyleEditForm.class.php index 180c96c5a3a..79db9085d80 100644 --- a/wcfsetup/install/files/lib/acp/form/StyleEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/StyleEditForm.class.php @@ -3,6 +3,8 @@ namespace wcf\acp\form; use wcf\acp\page\StyleListPage; +use wcf\data\package\Package; +use wcf\data\package\PackageCache; use wcf\data\style\Style; use wcf\data\style\StyleAction; use wcf\data\style\StyleEditor; @@ -65,6 +67,23 @@ public function readParameters() parent::readParameters(); } + #[\Override] + public function readFormParameters() + { + parent::readFormParameters(); + + // The fields are only marked as `readonly` for protected styles. + if ($this->style->isTainted === 0) { + $this->authorName = $this->style->authorName; + $this->authorURL = $this->style->authorURL; + $this->copyright = $this->style->copyright; + $this->license = $this->style->license; + $this->packageName = $this->style->packageName; + $this->styleDate = $this->style->styleDate; + $this->styleVersion = $this->style->styleVersion; + } + } + #[\Override] public function validate() { @@ -435,6 +454,10 @@ public function assignVariables() 'style' => $this->style, 'styleID' => $this->styleID, 'isDarkMode' => $this->isDarkMode, + 'authoredVariables' => $this->getAuthoredVariables(), + 'stylePackage' => $this->getStylePackage(), + 'canViewStylePackage' => WCF::getSession()->hasPermission('admin.configuration.package.canInstallPackage') + && WCF::getSession()->hasPermission('admin.configuration.package.canUpdatePackage'), 'interactionContextMenu' => StandaloneInteractionContextMenuComponent::forContentHeaderButton( new StyleInteractions(), $this->style, @@ -442,4 +465,51 @@ public function assignVariables() ), ]); } + + /** + * Returns the package that updates this style. Imported styles carry the + * Core's package id, but are only updated by importing them again. + */ + private function getStylePackage(): ?Package + { + if ($this->style->isTainted !== 0) { + return null; + } + + $package = PackageCache::getInstance()->getPackage($this->style->packageID); + if ($package === null || ($package->packageID === 1 && $this->style->packageName !== 'com.woltlab.wcf')) { + return null; + } + + return $package; + } + + /** + * Returns the authored values of the variables edited in the current mode. + * The dark mode edits the light value of variables without dark mode support. + * + * @return array + */ + private function getAuthoredVariables(): array + { + $authoredVariables = $this->style->getAuthoredVariables(); + $supportsDarkMode = $this->isDarkMode ? Style::getVariablesWithDarkModeSupport() : []; + + $variables = []; + foreach ($authoredVariables as $variableName => $value) { + if (\str_starts_with($variableName, Style::DARK_MODE_PREFIX)) { + continue; + } + + if (\in_array($variableName, $supportsDarkMode, true)) { + $value = $authoredVariables[Style::DARK_MODE_PREFIX . $variableName] ?? null; + } + + if ($value !== null) { + $variables[$variableName] = $value; + } + } + + return $variables; + } } diff --git a/wcfsetup/install/files/lib/command/style/CopyStyle.class.php b/wcfsetup/install/files/lib/command/style/CopyStyle.class.php index e1eb20a5100..03f25223b34 100644 --- a/wcfsetup/install/files/lib/command/style/CopyStyle.class.php +++ b/wcfsetup/install/files/lib/command/style/CopyStyle.class.php @@ -55,6 +55,8 @@ private function copyStyle(Style $style): Style 'coverPhotoExtension' => $style->coverPhotoExtension, 'hasFavicon' => $style->hasFavicon, 'hasDarkMode' => $style->hasDarkMode, + // A copy is never updated, protecting it would only lock the preset declarations. + 'isTainted' => 1, ]); } @@ -89,14 +91,31 @@ private function handleI18n(Style $style): void private function copyVariables(Style $source, Style $destination): void { $sql = "INSERT INTO wcf1_style_variable_value - (styleID, variableID, variableValue, variableValueDarkMode) - SELECT ?, variableID, variableValue, variableValueDarkMode + (styleID, variableID, variableValue, variableValueDarkMode, authoredValue, authoredValueDarkMode) + SELECT ?, variableID, variableValue, variableValueDarkMode, authoredValue, authoredValueDarkMode FROM wcf1_style_variable_value WHERE styleID = ? ON DUPLICATE KEY UPDATE variableValue = VALUES(variableValue), - variableValueDarkMode = VALUES(variableValueDarkMode)"; + variableValueDarkMode = VALUES(variableValueDarkMode), + authoredValue = VALUES(authoredValue), + authoredValueDarkMode = VALUES(authoredValueDarkMode)"; $statement = WCF::getDB()->prepare($sql); $statement->execute([$destination->styleID, $source->styleID]); + + $sql = "UPDATE wcf1_style_variable_value style_variable_value + INNER JOIN wcf1_style_variable style_variable + ON style_variable.variableID = style_variable_value.variableID + SET style_variable_value.variableValue = REPLACE(style_variable_value.variableValue, ?, '') + WHERE style_variable_value.styleID = ? + AND style_variable.variableName IN (?, ?, ?)"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([ + "/* WCF_STYLE_CUSTOM_USER_MODIFICATIONS */\n", + $destination->styleID, + 'individualScss', + 'individualScssDarkMode', + 'overrideScss', + ]); } private function copyAssets(Style $source, Style $destination): void diff --git a/wcfsetup/install/files/lib/data/style/Style.class.php b/wcfsetup/install/files/lib/data/style/Style.class.php index bca3526e7f1..9659303495f 100644 --- a/wcfsetup/install/files/lib/data/style/Style.class.php +++ b/wcfsetup/install/files/lib/data/style/Style.class.php @@ -130,6 +130,37 @@ public function getVariables() return $this->variables; } + /** + * Returns the values shipped by the author of this style, the values for + * the dark mode are prefixed with `DARK_MODE_PREFIX`. Variables without a + * known value are omitted. + * + * @return array + * @since 6.3 + */ + public function getAuthoredVariables(): array + { + $sql = "SELECT variable.variableName, value.authoredValue, value.authoredValueDarkMode + FROM wcf1_style_variable_value value + INNER JOIN wcf1_style_variable variable + ON variable.variableID = value.variableID + WHERE value.styleID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([$this->styleID]); + + $variables = []; + while ($row = $statement->fetchArray()) { + if ($row['authoredValue'] !== null) { + $variables[$row['variableName']] = $row['authoredValue']; + } + if ($row['authoredValueDarkMode'] !== null) { + $variables[self::DARK_MODE_PREFIX . $row['variableName']] = $row['authoredValueDarkMode']; + } + } + + return $variables; + } + /** * Returns a specific style variable or null if not found. * If $toHex is set to true the color defined by the variable diff --git a/wcfsetup/install/files/lib/data/style/StyleAction.class.php b/wcfsetup/install/files/lib/data/style/StyleAction.class.php index dbcb5f977fc..e5dd06a7556 100644 --- a/wcfsetup/install/files/lib/data/style/StyleAction.class.php +++ b/wcfsetup/install/files/lib/data/style/StyleAction.class.php @@ -586,54 +586,20 @@ public function getStyleChooser() } /** - * Validates the mark as tainted action. - * * @return void + * @deprecated 6.3 The protection of a style can no longer be lifted. */ public function validateMarkAsTainted() { - if (!WCF::getSession()->hasPermission('admin.style.canManageStyle')) { - throw new PermissionDeniedException(); - } - - $this->styleEditor = $this->getSingleObject(); + throw new PermissionDeniedException(); } /** - * Marks a style as tainted. + * @deprecated 6.3 The protection of a style can no longer be lifted. */ public function markAsTainted(): void { - // merge definitions - $variables = $this->styleEditor->getVariables(); - $variables['individualScss'] = \str_replace( - "/* WCF_STYLE_CUSTOM_USER_MODIFICATIONS */\n", - '', - $variables['individualScss'] - ); - $variables['overrideScss'] = \str_replace( - "/* WCF_STYLE_CUSTOM_USER_MODIFICATIONS */\n", - '', - $variables['overrideScss'] - ); - - $variablesDarkMode = []; - $variables = \array_filter($variables, static function ($value, $key) use (&$variablesDarkMode) { - if (!\str_starts_with($key, Style::DARK_MODE_PREFIX)) { - return true; - } - - $variablesDarkMode[\str_replace(Style::DARK_MODE_PREFIX, '', $key)] = $value; - - return false; - }, \ARRAY_FILTER_USE_BOTH); - - $this->styleEditor->setVariables($variables, $variablesDarkMode); - - $this->styleEditor->update([ - 'isTainted' => 1, - 'packageName' => '', - ]); + throw new PermissionDeniedException(); } /** diff --git a/wcfsetup/install/files/lib/data/style/StyleEditor.class.php b/wcfsetup/install/files/lib/data/style/StyleEditor.class.php index 31b8afb30f4..18a36480015 100644 --- a/wcfsetup/install/files/lib/data/style/StyleEditor.class.php +++ b/wcfsetup/install/files/lib/data/style/StyleEditor.class.php @@ -645,14 +645,24 @@ public static function import(string $filename, int $packageID = 1, ?self $style $importedOptions[$variableName] = $option::tryFromString($styleData['variables'][$variableName] ?? ''); } + $authoredVariables = $styleData['variables']; + foreach (Style::getAvailableOptions() as $option) { + $variableName = $option::getVariableName(); + $authoredVariables[$variableName] = ($importedOptions[$variableName] ?? $option::getFallback())->toString(); + } + + $authoredVariablesDarkMode = null; + if ($styleData['hasDarkMode'] === 1) { + $authoredVariablesDarkMode = \is_array($styleData['variablesDarkMode']) ? $styleData['variablesDarkMode'] : []; + } + // save style if ($style === null) { - foreach (Style::getAvailableOptions() as $option) { - $variableName = $option::getVariableName(); - $styleData['variables'][$variableName] = ($importedOptions[$variableName] ?? $option::getFallback())->toString(); - } - + $styleData['variables'] = $authoredVariables; $styleData['packageID'] = $packageID; + // A style without a package name is never updated, protecting it + // would only lock the preset declarations. + $styleData['isTainted'] = $styleData['packageName'] === '' ? 1 : 0; $style = new self(self::create($styleData)); // handle descriptions @@ -723,6 +733,9 @@ public static function import(string $filename, int $packageID = 1, ?self $style $style->update($styleData); } + // An update keeps the installed values, but records the ones it ships. + $style->setAuthoredVariables($authoredVariables, $authoredVariablesDarkMode); + // import images if (!empty($data['images'])) { $index = $tar->getIndexByFilename($data['images']); @@ -1299,6 +1312,43 @@ public function setVariables(array $variables, array $variablesDarkMode): void StyleHandler::getInstance()->resetStylesheet($this->getDecoratedObject()); } + /** + * Records the values the style author shipped. Variables missing from the + * archive store the current default, because a later change of the default + * must not alter what the author shipped. + * + * @param array $variables + * @param ?array $variablesDarkMode `null` if the archive has no dark mode + */ + private function setAuthoredVariables(array $variables, ?array $variablesDarkMode): void + { + $sql = "SELECT variableID, variableName, defaultValue, defaultValueDarkMode + FROM wcf1_style_variable"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute(); + $styleVariables = $statement->fetchAll(\PDO::FETCH_ASSOC); + + $sql = "INSERT INTO wcf1_style_variable_value + (styleID, variableID, authoredValue, authoredValueDarkMode) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE authoredValue = VALUES(authoredValue), + authoredValueDarkMode = VALUES(authoredValueDarkMode)"; + $statement = WCF::getDB()->prepare($sql); + + WCF::getDB()->beginTransaction(); + foreach ($styleVariables as $row) { + $variableName = $row['variableName']; + + $statement->execute([ + $this->styleID, + $row['variableID'], + $variables[$variableName] ?? $row['defaultValue'], + $variablesDarkMode === null ? null : ($variablesDarkMode[$variableName] ?? $row['defaultValueDarkMode']), + ]); + } + WCF::getDB()->commitTransaction(); + } + /** * Writes the style-*.css file. */ diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 8eef1973ee0..46cd0dbefd7 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -2898,6 +2898,7 @@ Abschnitte dürfen nicht leer sein und nur folgende Zeichen enthalten: [a-z + @@ -2957,11 +2958,8 @@ Abschnitte dürfen nicht leer sein und nur folgende Zeichen enthalten: [a-z - Schutz aufheben.]]> - -
Dieser Schritt wird nicht empfohlen und ist grundsätzlich nicht notwendig, Stile können weiterhin nach belieben angepasst werden, nur die Vorlage selbst kann nicht verändert werden.

Bitte {if LANGUAGE_USE_INFORMAL_VARIANT}speicher{else}speichern Sie{/if} eventuelle vorgenommene Änderungen bevor {if LANGUAGE_USE_INFORMAL_VARIANT}du{else}Sie{/if} diesen Schritt {if LANGUAGE_USE_INFORMAL_VARIANT}ausführst{else}ausführen{/if}.]]>
+ getLink()}">{$stylePackage->getTitle()}{else}„{$stylePackage->getTitle()}“{/if} bereitgestellt und mit diesem aktualisiert.{elseif $style->packageName !== ''}Dieser Stil wurde importiert und wird durch den Import einer neueren Version aktualisiert.{else}Dieser Stil ist geschützt und kann nur eingeschränkt verändert werden.{/if}]]> - @@ -5867,5 +5865,8 @@ Erlaubte Dateiendungen: {', '|implode:$allowedFileExtensions}]]> + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 2397762b774..616f00a23da 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -2827,6 +2827,7 @@ If you have already bought the licenses for the listed apps, th + @@ -2887,11 +2888,8 @@ If you have already bought the licenses for the listed apps, th - disable this protection.]]> - -
It is neither recommended nor necessary to remove the protection in most cases, styles can still be fully customized while preserving the preset declarations.

Please save any unsaved changes before proceeding.]]>
+ getLink()}">{$stylePackage->getTitle()}{else}“{$stylePackage->getTitle()}”{/if} and receives updates with it.{elseif $style->packageName !== ''}This style was imported and receives updates by importing a newer version of it.{else}This style is protected and editing is limited.{/if}]]> - @@ -5869,5 +5867,8 @@ Allowed extensions: {', '|implode:$allowedFileExtensions}]]>
+ + + diff --git a/wcfsetup/setup/db/install_com.woltlab.wcf.php b/wcfsetup/setup/db/install_com.woltlab.wcf.php index 3f99455bd7d..99b787a0f41 100644 --- a/wcfsetup/setup/db/install_com.woltlab.wcf.php +++ b/wcfsetup/setup/db/install_com.woltlab.wcf.php @@ -3625,6 +3625,8 @@ NotNullInt10DatabaseTableColumn::create('variableID'), MediumtextDatabaseTableColumn::create('variableValue'), MediumtextDatabaseTableColumn::create('variableValueDarkMode'), + MediumtextDatabaseTableColumn::create('authoredValue'), + MediumtextDatabaseTableColumn::create('authoredValueDarkMode'), ]) ->indices([ DatabaseTableIndex::create('styleID')