From 4e9be42508d1678341f5ab9e5ac431c58a229eb3 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 21 Jul 2025 10:05:58 +0200 Subject: [PATCH 01/18] Add trophy conditions migration --- com.woltlab.wcf/package.xml | 1 + .../update_com.woltlab.wcf_6.3_step1.php | 5 ++ .../files/acp/templates/trophyList.tpl | 6 ++ .../acp/update_com.woltlab.wcf_6.3_trophy.php | 59 +++++++++++++++++++ .../lib/acp/page/TrophyListPage.class.php | 22 +++++++ .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../files/lib/data/trophy/Trophy.class.php | 11 ++-- .../command/MigrateLegacyCondition.class.php | 49 +++++++++++++++ .../TrophyConditionHandler.class.php | 3 + .../worker/TrophyRebuildDataWorker.class.php | 39 ++++++++++++ wcfsetup/install/lang/de.xml | 3 + wcfsetup/install/lang/en.xml | 3 + wcfsetup/setup/db/install.sql | 2 + 13 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_trophy.php create mode 100644 wcfsetup/install/files/lib/system/trophy/command/MigrateLegacyCondition.class.php create mode 100644 wcfsetup/install/files/lib/system/worker/TrophyRebuildDataWorker.class.php diff --git a/com.woltlab.wcf/package.xml b/com.woltlab.wcf/package.xml index 79930be5e66..b3281619196 100644 --- a/com.woltlab.wcf/package.xml +++ b/com.woltlab.wcf/package.xml @@ -55,5 +55,6 @@ acp/database/update_com.woltlab.wcf_6.3_step1.php acp/update_com.woltlab.wcf_6.3_userGroupAssignment.php acp/update_com.woltlab.wcf_6.3_notice.php + acp/update_com.woltlab.wcf_6.3_trophy.php --> 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 cf7a3517cd6..12fa2efe069 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 @@ -23,4 +23,9 @@ MediumtextDatabaseTableColumn::create('conditions'), DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), ]), + PartialDatabaseTable::create('wcf1_trophy') + ->columns([ + MediumtextDatabaseTableColumn::create('conditions'), + DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), + ]), ]; diff --git a/wcfsetup/install/files/acp/templates/trophyList.tpl b/wcfsetup/install/files/acp/templates/trophyList.tpl index a59c8c05912..d056eb69250 100644 --- a/wcfsetup/install/files/acp/templates/trophyList.tpl +++ b/wcfsetup/install/files/acp/templates/trophyList.tpl @@ -19,6 +19,12 @@ +{if $hasLegacyObjects} + + {lang}wcf.acp.trophy.legacyTrophies{/lang} + +{/if} +
{unsafe:$gridView->render()}
diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_trophy.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_trophy.php new file mode 100644 index 00000000000..851a59a0795 --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_trophy.php @@ -0,0 +1,59 @@ +exportConditions("com.woltlab.wcf.condition.trophy"); +if ($exportedConditions === []) { + return; +} + +$sql = "UPDATE wcf1_trophy + SET conditions = ?, + isLegacy = ? + WHERE trophyID = ?"; +$statement = WCF::getDB()->prepare($sql); +foreach ($exportedConditions as $trophyID => $conditionData) { + renameObjectTypes($conditionData); + + $statement->execute([ + JSON::encode($conditionData), + 1, + $trophyID, + ]); +} + +/** + * Rename the object types so that the migration functions can handle them. + * @see \wcf\system\condition\provider\UserConditionProvider + * + * @param array $conditionData + */ +function renameObjectTypes(array &$conditionData): void +{ + $objectTypeMap = [ + 'com.woltlab.wcf.username' => 'com.woltlab.wcf.user.username', + 'com.woltlab.wcf.email' => 'com.woltlab.wcf.user.email', + 'com.woltlab.wcf.userGroup' => 'com.woltlab.wcf.user.userGroup', + 'com.woltlab.wcf.languages' => 'com.woltlab.wcf.user.languages', + 'com.woltlab.wcf.registrationDate' => 'com.woltlab.wcf.user.registrationDate', + 'com.woltlab.wcf.registrationDateInterval' => 'com.woltlab.wcf.user.registrationDateInterval', + 'com.woltlab.wcf.avatar' => 'com.woltlab.wcf.user.avatar', + 'com.woltlab.wcf.signature' => 'com.woltlab.wcf.user.signature', + 'com.woltlab.wcf.coverPhoto' => 'com.woltlab.wcf.user.coverPhoto', + 'com.woltlab.wcf.state' => 'com.woltlab.wcf.user.state', + 'com.woltlab.wcf.activityPoints' => 'com.woltlab.wcf.user.activityPoints', + 'com.woltlab.wcf.likesReceived' => 'com.woltlab.wcf.user.likesReceived', + // TODO 'com.woltlab.wcf.userOptions' + 'com.woltlab.wcf.userTrophyCondition' => 'com.woltlab.wcf.user.trophyCondition', + 'com.woltlab.wcf.trophyPoints' => 'com.woltlab.wcf.user.trophyPoints', + ]; + + foreach ($objectTypeMap as $currentName => $newName) { + if (isset($conditionData[$currentName])) { + $conditionData[$newName] = $conditionData[$currentName]; + unset($conditionData[$currentName]); + } + } +} \ No newline at end of file diff --git a/wcfsetup/install/files/lib/acp/page/TrophyListPage.class.php b/wcfsetup/install/files/lib/acp/page/TrophyListPage.class.php index 49b501bc111..35a83ed9c8c 100644 --- a/wcfsetup/install/files/lib/acp/page/TrophyListPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/TrophyListPage.class.php @@ -4,6 +4,7 @@ use wcf\page\AbstractGridViewPage; use wcf\system\gridView\admin\TrophyGridView; +use wcf\system\WCF; /** * Trophy list page. @@ -37,4 +38,25 @@ protected function createGridView(): TrophyGridView { return new TrophyGridView(); } + + #[\Override] + public function assignVariables() + { + parent::assignVariables(); + + WCF::getTPL()->assign([ + 'hasLegacyObjects' => $this->hasLegacyObjects(), + ]); + } + + private function hasLegacyObjects(): bool + { + $sql = "SELECT COUNT(*) AS count + FROM wcf1_trophy + WHERE isLegacy = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([1]); + + return $statement->fetchColumn() > 0; + } } diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 1d3acc96c5f..fa759714a8b 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -102,6 +102,7 @@ static function (\wcf\event\worker\RebuildWorkerCollecting $event) { $event->register(\wcf\system\worker\SitemapRebuildWorker::class, 500); $event->register(\wcf\system\worker\UserGroupAssignmentRebuildDataWorker::class, 600); $event->register(\wcf\system\worker\NoticeRebuildDataWorker::class, 600); + $event->register(\wcf\system\worker\TrophyRebuildDataWorker::class, 600); $event->register(\wcf\system\worker\StatDailyRebuildDataWorker::class, 800); } ); diff --git a/wcfsetup/install/files/lib/data/trophy/Trophy.class.php b/wcfsetup/install/files/lib/data/trophy/Trophy.class.php index 0ac0c2ec3e0..378dc8b2433 100644 --- a/wcfsetup/install/files/lib/data/trophy/Trophy.class.php +++ b/wcfsetup/install/files/lib/data/trophy/Trophy.class.php @@ -2,17 +2,16 @@ namespace wcf\data\trophy; -use wcf\data\condition\Condition; use wcf\data\DatabaseObject; use wcf\data\ITitledLinkObject; use wcf\data\trophy\category\TrophyCategory; use wcf\data\trophy\category\TrophyCategoryCache; -use wcf\system\condition\ConditionHandler; use wcf\system\event\EventHandler; use wcf\system\request\IRouteController; use wcf\system\request\LinkHandler; use wcf\system\style\FontAwesomeIcon; use wcf\system\WCF; +use wcf\util\JSON; use wcf\util\StringUtil; /** @@ -37,6 +36,8 @@ * @property-read int $revokeAutomatically `1` if the trophy should be automatically revoked once the conditions are no longer met. * @property-read int $trophyUseHtml `1` if the trophy use a html description * @property-read int $showOrder position of the trophy in relation to the other trophies at the same location + * @property-read string|null $conditions + * @property-read int $isLegacy */ class Trophy extends DatabaseObject implements ITitledLinkObject, IRouteController { @@ -172,11 +173,11 @@ public function getDescription() /** * Returns the conditions of the trophy. * - * @return Condition[] + * @return array{identifier: string, value: mixed}[] */ - public function getConditions() + public function getConditions(): array { - return ConditionHandler::getInstance()->getConditions('com.woltlab.wcf.condition.trophy', $this->trophyID); + return $this->conditions !== null ? JSON::decode($this->conditions) : []; } /** diff --git a/wcfsetup/install/files/lib/system/trophy/command/MigrateLegacyCondition.class.php b/wcfsetup/install/files/lib/system/trophy/command/MigrateLegacyCondition.class.php new file mode 100644 index 00000000000..fb92f017aed --- /dev/null +++ b/wcfsetup/install/files/lib/system/trophy/command/MigrateLegacyCondition.class.php @@ -0,0 +1,49 @@ + + * @since 6.3 + */ +final class MigrateLegacyCondition +{ + public function __construct(public readonly Trophy $trophy) + { + } + + public function __invoke(): void + { + if (!$this->trophy->isLegacy) { + return; + } + + try { + $json = JSON::decode($this->trophy->conditions); + } catch (SystemException $ex) { + $ex->getExceptionID(); // Log the exception if JSON decoding fails + + return; + } + + $migratedData = ConditionHandler::getInstance()->migrateConditionData(new UserConditionProvider(), $json); + + $editor = new TrophyEditor($this->trophy); + $editor->update([ + 'conditions' => JSON::encode($migratedData->conditions), + 'isLegacy' => 0, + 'isDisabled' => $migratedData->isFullyMigrated ? $this->trophy->isDisabled : 1, + ]); + } +} \ No newline at end of file diff --git a/wcfsetup/install/files/lib/system/trophy/condition/TrophyConditionHandler.class.php b/wcfsetup/install/files/lib/system/trophy/condition/TrophyConditionHandler.class.php index 259065e8d90..01eba01a0e2 100644 --- a/wcfsetup/install/files/lib/system/trophy/condition/TrophyConditionHandler.class.php +++ b/wcfsetup/install/files/lib/system/trophy/condition/TrophyConditionHandler.class.php @@ -37,6 +37,7 @@ class TrophyConditionHandler extends SingletonFactory */ protected function init() { + // TODO $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes(self::CONDITION_DEFINITION_NAME); foreach ($objectTypes as $objectType) { @@ -140,6 +141,7 @@ private function getUserIDs(Trophy $trophy) ON user_option_value.userID = user_table.userID"; $conditions = $trophy->getConditions(); + // TODO foreach ($conditions as $condition) { $condition->getObjectType()->getProcessor()->addUserCondition($condition, $userList); } @@ -185,6 +187,7 @@ private function getRevocableUserTrophyIDs(Trophy $trophy, $maxTrophyIDs) } // Assign the condition to the pseudo DBOList object + // TODO foreach ($conditions as $condition) { $condition->getObjectType()->getProcessor()->addUserCondition($condition, $pseudoUserList); } diff --git a/wcfsetup/install/files/lib/system/worker/TrophyRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/TrophyRebuildDataWorker.class.php new file mode 100644 index 00000000000..0cbbaceba26 --- /dev/null +++ b/wcfsetup/install/files/lib/system/worker/TrophyRebuildDataWorker.class.php @@ -0,0 +1,39 @@ + + * @since 6.3 + * + * @extends AbstractLinearRebuildDataWorker + */ +final class TrophyRebuildDataWorker extends AbstractLinearRebuildDataWorker +{ + /** + * @inheritDoc + */ + protected $objectListClassName = TrophyList::class; + + /** + * @inheritDoc + */ + protected $limit = 100; + + #[\Override] + public function execute() + { + parent::execute(); + + foreach ($this->objectList as $trophy) { + (new MigrateLegacyCondition($trophy))(); + } + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index f9d817fdfb7..93d29b8c2d5 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -2708,6 +2708,8 @@ Abschnitte dürfen nicht leer sein und nur folgende Zeichen enthalten: [a-z + + @@ -5214,6 +5216,7 @@ Sobald {if LANGUAGE_USE_INFORMAL_VARIANT}dein{else}Ihr{/if} Benutzerkonto freige Trophäe hinzufügen, welche nicht automatisch durch das System vergeben wird.]]> + Anzeigen aktualisieren durch.]]> diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index bd2e8c60d78..ed0b9518e18 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -2635,6 +2635,8 @@ If you have already bought the licenses for the listed apps, th + + @@ -5213,6 +5215,7 @@ You also received a list of backup codes to use when your second factor becomes add a trophy that is not automatically awarded before you award trophies.]]> + Rebuild Data.]]> diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 4bf96a81fae..055bc1b0fa9 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1503,6 +1503,8 @@ CREATE TABLE wcf1_trophy( revokeAutomatically TINYINT(1) NOT NULL DEFAULT 0, trophyUseHtml TINYINT(1) NOT NULL DEFAULT 0, showOrder INT(10) NOT NULL DEFAULT 0, + conditions MEDIUMTEXT, + isLegacy TINYINT(1) NOT NULL DEFAULT 0, KEY(categoryID) ); From 538dc45c0db5f77d1afd55f878b19a89a50f0730 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 21 Jul 2025 12:09:59 +0200 Subject: [PATCH 02/18] Migrate `TrophyAddFrom` to FormBuilder --- ts/WoltLabSuite/Core/Component/Icon/Badge.ts | 52 ++ ts/WoltLabSuite/Core/Ui/Color/Picker.ts | 3 +- .../install/files/acp/templates/trophyAdd.tpl | 260 +-------- .../WoltLabSuite/Core/Component/Icon/Badge.js | 49 ++ .../js/WoltLabSuite/Core/Ui/Color/Picker.js | 3 +- .../lib/acp/form/TrophyAddForm.class.php | 505 +++++------------- .../lib/acp/form/TrophyEditForm.class.php | 221 +------- .../install/files/style/ui/fontAwesome.scss | 12 + 8 files changed, 252 insertions(+), 853 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Component/Icon/Badge.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Component/Icon/Badge.js diff --git a/ts/WoltLabSuite/Core/Component/Icon/Badge.ts b/ts/WoltLabSuite/Core/Component/Icon/Badge.ts new file mode 100644 index 00000000000..d267c318eed --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/Icon/Badge.ts @@ -0,0 +1,52 @@ +/** + * Handles the display of an icon badge with customizable colors. + * + * @author Olaf Braun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ + +export class IconBadge { + #iconContainer: HTMLElement; + #colorInput?: HTMLInputElement; + #backgroundColorInput?: HTMLInputElement; + + constructor(iconFieldId: string, colorFieldId?: string, backgroundColorFieldId?: string) { + this.#iconContainer = document.getElementById(`${iconFieldId}_icon`)!; + this.#iconContainer.classList.add("iconBadge"); + + const observer = new MutationObserver((mutationsList) => { + for (const mutation of mutationsList) { + if (mutation.type === "attributes" && mutation.attributeName === "value") { + this.#updateIcon(); + } + } + }); + + if (colorFieldId) { + this.#colorInput = document.getElementById(colorFieldId) as HTMLInputElement; + + observer.observe(this.#colorInput, { + attributes: true, + attributeFilter: ["value"], + }); + } + + if (backgroundColorFieldId) { + this.#backgroundColorInput = document.getElementById(backgroundColorFieldId) as HTMLInputElement; + + observer.observe(this.#backgroundColorInput, { + attributes: true, + attributeFilter: ["value"], + }); + } + + this.#updateIcon(); + } + + #updateIcon(): void { + this.#iconContainer.style.color = this.#colorInput?.value || ""; + this.#iconContainer.style.backgroundColor = this.#backgroundColorInput?.value || ""; + } +} diff --git a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts index e42372471eb..3191f459f6c 100644 --- a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts +++ b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts @@ -354,7 +354,8 @@ class UiColorPicker implements DialogCallbackObject { const colorString = ColorUtil.rgbaToString(color); this.oldColor!.style.backgroundColor = colorString; - this.input.value = colorString; + // The change in value via `this.input.value = colorString;` cannot be detected by a MutationObserver. + this.input.setAttribute("value", colorString); if (!(this.element instanceof HTMLButtonElement)) { const span = this.element.querySelector("span"); diff --git a/wcfsetup/install/files/acp/templates/trophyAdd.tpl b/wcfsetup/install/files/acp/templates/trophyAdd.tpl index 7188406b92a..69bc3979822 100644 --- a/wcfsetup/install/files/acp/templates/trophyAdd.tpl +++ b/wcfsetup/install/files/acp/templates/trophyAdd.tpl @@ -1,22 +1,5 @@ {include file='header' pageTitle='wcf.acp.menu.link.trophy.'|concat:$action} -{include file='shared_colorPickerJavaScript'} -{include file='shared_fontAwesomeJavaScript'} - - -

{lang}wcf.acp.menu.link.trophy.{$action}{/lang}

@@ -34,243 +17,12 @@
-{include file='shared_formNotice'} - -{if $trophyCategories|count} -
-
- -
-
- - {if $errorField == 'title'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {elseif $errorType == 'multilingual'} - {lang}wcf.global.form.error.multilingual{/lang} - {/if} - - {/if} -
- - {include file='shared_multipleLanguageInputJavascript' elementIdentifier='title' forceSelection=false} - - -
-
- - {if $errorField == 'description'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {elseif $errorType == 'multilingual'} - {lang}wcf.global.form.error.multilingual{/lang} - {/if} - - {/if} -
- - {include file='shared_multipleLanguageInputJavascript' elementIdentifier='description' forceSelection=false} - -
-
-
- -
-
- - -
-
- - {if $errorField == 'categoryID'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {/if} - - {/if} -
- - -
-
-
- - {lang}wcf.acp.trophy.showOrder.description{/lang} -
-
- -
-
-
- -
-
- -
-
-
- -
-
- -
-
-
- -
-
- -
-
-
- -
-
- - {event name='dataFields'} -
- -
-
-

{lang}wcf.acp.trophy.type.imageUpload{/lang}

-
- - -
{lang}wcf.acp.trophy.type.imageUpload{/lang}
-
- -
-
-
- {if $errorField == 'imageUpload'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {/if} - - {/if} - {lang}wcf.acp.trophy.type.imageUpload.description{/lang} -
-
-
{if $action == 'add'}{if !$uploadedImageURL|empty}{/if}{else}{if $trophy->type == 1}{/if}{/if}
-
-
- - -
- -
- -
-
-

{lang}wcf.acp.trophy.type.badge{/lang}

-
- -
-
{lang}wcf.acp.trophy.type.badge{/lang}
-
-
- - {unsafe:$icon->toHtml(64)} - -
- - - -
-
- - - - -
-
-
- - {event name='sections'} - -
-
-

{lang}wcf.acp.trophy.conditions{/lang}

-

{lang}wcf.acp.trophy.conditions.description{/lang}

-
- - {if $errorField == 'conditions'} - {lang}wcf.acp.trophy.conditions.error.noConditions{/lang} - {/if} - - {include file='shared_userConditions'} -
- - {event name='conditionSections'} - -
- - {csrfToken} -
-
-{else} - {lang}wcf.acp.trophy.error.noCategories{/lang} -{/if} - - + {include file='footer'} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Icon/Badge.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Icon/Badge.js new file mode 100644 index 00000000000..1d884d5fa83 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Icon/Badge.js @@ -0,0 +1,49 @@ +/** + * Handles the display of an icon badge with customizable colors. + * + * @author Olaf Braun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ +define(["require", "exports"], function (require, exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.IconBadge = void 0; + class IconBadge { + #iconContainer; + #colorInput; + #backgroundColorInput; + constructor(iconFieldId, colorFieldId, backgroundColorFieldId) { + this.#iconContainer = document.getElementById(`${iconFieldId}_icon`); + this.#iconContainer.classList.add("iconBadge"); + const observer = new MutationObserver((mutationsList) => { + for (const mutation of mutationsList) { + if (mutation.type === "attributes" && mutation.attributeName === "value") { + this.#updateIcon(); + } + } + }); + if (colorFieldId) { + this.#colorInput = document.getElementById(colorFieldId); + observer.observe(this.#colorInput, { + attributes: true, + attributeFilter: ["value"], + }); + } + if (backgroundColorFieldId) { + this.#backgroundColorInput = document.getElementById(backgroundColorFieldId); + observer.observe(this.#backgroundColorInput, { + attributes: true, + attributeFilter: ["value"], + }); + } + this.#updateIcon(); + } + #updateIcon() { + this.#iconContainer.style.color = this.#colorInput?.value || ""; + this.#iconContainer.style.backgroundColor = this.#backgroundColorInput?.value || ""; + } + } + exports.IconBadge = IconBadge; +}); 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 3ab78f29276..de403be9615 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js @@ -281,7 +281,8 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti } const colorString = ColorUtil.rgbaToString(color); this.oldColor.style.backgroundColor = colorString; - this.input.value = colorString; + // The change in value via `this.input.value = colorString;` cannot be detected by a MutationObserver. + this.input.setAttribute("value", colorString); if (!(this.element instanceof HTMLButtonElement)) { const span = this.element.querySelector("span"); if (span) { diff --git a/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php b/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php index 5ba22be8b67..6244b601b45 100644 --- a/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php @@ -2,30 +2,40 @@ namespace wcf\acp\form; -use wcf\data\object\type\ObjectType; -use wcf\data\trophy\category\TrophyCategory; use wcf\data\trophy\category\TrophyCategoryCache; use wcf\data\trophy\Trophy; use wcf\data\trophy\TrophyAction; -use wcf\data\trophy\TrophyEditor; -use wcf\system\condition\ConditionHandler; -use wcf\system\exception\UserInputException; -use wcf\system\language\I18nValue; -use wcf\system\request\LinkHandler; -use wcf\system\style\FontAwesomeIcon; -use wcf\system\trophy\condition\TrophyConditionHandler; +use wcf\data\trophy\TrophyList; +use wcf\form\AbstractFormBuilderForm; +use wcf\system\condition\provider\UserConditionProvider; +use wcf\system\exception\NamedUserException; +use wcf\system\form\builder\container\condition\ConditionFormContainer; +use wcf\system\form\builder\container\FormContainer; +use wcf\system\form\builder\container\RowFormContainer; +use wcf\system\form\builder\field\BooleanFormField; +use wcf\system\form\builder\field\ColorFormField; +use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency; +use wcf\system\form\builder\field\dependency\ValueFormFieldDependency; +use wcf\system\form\builder\field\DescriptionFormField; +use wcf\system\form\builder\field\IconFormField; +use wcf\system\form\builder\field\RadioButtonFormField; +use wcf\system\form\builder\field\ShowOrderFormField; +use wcf\system\form\builder\field\SingleSelectionFormField; +use wcf\system\form\builder\field\TitleFormField; use wcf\system\WCF; -use wcf\util\StringUtil; +use wcf\util\HtmlString; /** * Represents the trophy add form. * - * @author Joshua Ruesweg - * @copyright 2001-2019 WoltLab GmbH + * @author Olaf Braun, Joshua Ruesweg + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License * @since 3.1 + * + * @extends AbstractFormBuilderForm */ -class TrophyAddForm extends AbstractAcpForm +class TrophyAddForm extends AbstractFormBuilderForm { /** * @inheritDoc @@ -42,382 +52,109 @@ class TrophyAddForm extends AbstractAcpForm */ public $neededModules = ['MODULE_TROPHY']; - /** - * category id for the trophy. - * @var int - */ - public $categoryID = 0; - - /** - * Category object. - * @var ?TrophyCategory - */ - public $category; - - /** - * Trophy description. - * @var string - */ - public $description = ''; - - /** - * Trophy title. - * @var string - */ - public $title = ''; - - /** - * All available trophy types. - * @var array - */ - public $availableTypes = [ - Trophy::TYPE_IMAGE => 'imageUpload', - Trophy::TYPE_BADGE => 'badge', - ]; - - /** - * Type of the trophy (whether this is an image or not) - * @var int - */ - public $type = Trophy::TYPE_BADGE; - - /** - * temporary hash for image icon - * @var string - */ - public $tmpHash = ''; - - /** - * the url for the uploaded image - * @var string - */ - public $uploadedImageURL = ''; - - /** - * the icon name for CSS icons (FA-Icon) - * @var string - */ - public $iconName = "trophy;false"; - - /** - * The icon color (rgba format with rgba prefix) - * @var string - */ - public $iconColor = 'rgba(255, 255, 255, 1)'; - - /** - * The badge color (rgba format with rgba prefix) - * @var string - */ - public $badgeColor = 'rgba(50, 92, 132, 1)'; - - /** - * `1` if the trophy is disabled. - * @var int - */ - public $isDisabled = 0; - - /** - * `1` if the trophy has conditions to reward automatically trophies. - * @var int - */ - public $awardAutomatically = 0; - - /** - * `1` if the trophy should be automatically revoked once the conditions are no longer met. - * @var int - */ - public $revokeAutomatically = 0; - - /** - * `1` if the trophy contains html in the description - * @var int - */ - public $trophyUseHtml = 0; - - /** - * list of grouped user group assignment condition object types - * @var (ObjectType|ObjectType[])[][] - */ - public $conditions = []; - - /** - * the showOrder value of the trophy - * @var int - */ - public $showOrder = 0; - /** * @inheritDoc */ - public function readData() - { - $this->conditions = TrophyConditionHandler::getInstance()->getGroupedObjectTypes(); + public $objectActionClass = TrophyAction::class; - parent::readData(); - } - - /** - * @inheritDoc - */ - public function readParameters() - { - parent::readParameters(); - - $titleI18n = new I18nValue('title'); - $titleI18n->setLanguageItem('wcf.user.trophy.title', 'wcf.user.trophy', 'com.woltlab.wcf'); - $this->registerI18nValue($titleI18n); - - $descriptionI18n = new I18nValue('description'); - $descriptionI18n->setLanguageItem('wcf.user.trophy.description', 'wcf.user.trophy', 'com.woltlab.wcf'); - $descriptionI18n->setFlags(I18nValue::ALLOW_EMPTY); - $this->registerI18nValue($descriptionI18n); - - if (isset($_POST['tmpHash'])) { - $this->tmpHash = StringUtil::trim($_POST['tmpHash']); - } - - if (empty($this->tmpHash)) { - $this->tmpHash = StringUtil::getRandomID(); - } - } - - /** - * @inheritDoc - */ - public function readFormParameters() + #[\Override] + public function createForm() { - parent::readFormParameters(); - - if (isset($_POST['categoryID'])) { - $this->categoryID = \intval($_POST['categoryID']); - } - if (isset($_POST['type'])) { - $this->type = \intval($_POST['type']); - } - if (isset($_POST['isDisabled'])) { - $this->isDisabled = \intval($_POST['isDisabled']); - } - if (isset($_POST['iconName'])) { - $this->iconName = StringUtil::trim($_POST['iconName']); - } - if (isset($_POST['iconColor'])) { - $this->iconColor = $_POST['iconColor']; - } - if (isset($_POST['badgeColor'])) { - $this->badgeColor = $_POST['badgeColor']; - } - if (isset($_POST['awardAutomatically'])) { - $this->awardAutomatically = 1; - } - if (isset($_POST['revokeAutomatically']) && $this->awardAutomatically) { - $this->revokeAutomatically = 1; - } - if (isset($_POST['trophyUseHtml'])) { - $this->trophyUseHtml = 1; - } - if (isset($_POST['showOrder'])) { - $this->showOrder = \intval($_POST['showOrder']); - } - - // read file upload - $fileExtension = WCF::getSession()->getVar('trophyImage-' . $this->tmpHash); - - if ($fileExtension !== null && \file_exists(WCF_DIR . 'images/trophy/tmp_' . $this->tmpHash . '.' . $fileExtension)) { - $this->uploadedImageURL = WCF::getPath() . 'images/trophy/tmp_' . $this->tmpHash . '.' . $fileExtension; - } - - $this->category = TrophyCategoryCache::getInstance()->getCategoryByID($this->categoryID); - - foreach ($this->conditions as $conditions) { - /** @var ObjectType $condition */ - foreach ($conditions as $condition) { - $condition->getProcessor()->readFormParameters(); - } - } - } - - /** - * @inheritDoc - */ - public function validate() - { - parent::validate(); - - if (!\in_array($this->type, \array_keys($this->availableTypes))) { - throw new UserInputException('type'); - } - - if (!$this->categoryID) { - throw new UserInputException('categoryID'); - } - - if (!$this->category->getObjectID()) { - throw new UserInputException('categoryID'); - } - - $this->validateType(); - - if ($this->awardAutomatically) { - $hasData = false; - foreach ($this->conditions as $conditions) { - foreach ($conditions as $condition) { - $condition->getProcessor()->validate(); - - if (!$hasData && $condition->getProcessor()->getData() !== null) { - $hasData = true; - } - } - } - - if (!$hasData) { - throw new UserInputException('conditions'); - } - } - } - - /** - * Validates the trophy type. - * - * @return void - */ - protected function validateType() - { - switch ($this->type) { - case Trophy::TYPE_IMAGE: - $fileExtension = WCF::getSession()->getVar('trophyImage-' . $this->tmpHash); - - if ($fileExtension === null) { - throw new UserInputException('imageUpload'); - } - - if (!\file_exists(WCF_DIR . 'images/trophy/tmp_' . $this->tmpHash . '.' . $fileExtension)) { - throw new UserInputException('imageUpload'); - } - break; - - case Trophy::TYPE_BADGE: - if (empty($this->iconName)) { - throw new UserInputException('iconName'); - } - - if (!FontAwesomeIcon::isValidString($this->iconName)) { - throw new UserInputException('iconName'); - } - - if (empty($this->iconColor)) { - throw new UserInputException('iconColor'); - } - - if (empty($this->badgeColor)) { - throw new UserInputException('badgeColor'); - } - break; - } - } - - /** - * @inheritDoc - */ - public function save() - { - parent::save(); - - $data = []; - if ($this->type == Trophy::TYPE_BADGE) { - $data['iconName'] = $this->iconName; - $data['iconColor'] = $this->iconColor; - $data['badgeColor'] = $this->badgeColor; - } - - $this->objectAction = new TrophyAction([], 'create', [ - 'data' => \array_merge($this->additionalFields, $data, [ - 'title' => $this->title, - 'description' => $this->description, - 'categoryID' => $this->categoryID, - 'type' => $this->type, - 'isDisabled' => $this->isDisabled, - 'awardAutomatically' => $this->awardAutomatically, - 'revokeAutomatically' => $this->revokeAutomatically, - 'trophyUseHtml' => $this->trophyUseHtml, - 'showOrder' => $this->showOrder, - ]), - 'tmpHash' => $this->tmpHash, - ]); - $returnValues = $this->objectAction->executeAction(); - - $this->saveI18n($returnValues['returnValues'], TrophyEditor::class); - - // transform conditions array into one-dimensional array - $conditions = []; - foreach ($this->conditions as $groupedObjectTypes) { - foreach ($groupedObjectTypes as $objectTypes) { - if (\is_array($objectTypes)) { - $conditions = \array_merge($conditions, $objectTypes); - } else { - $conditions[] = $objectTypes; - } - } - } - - ConditionHandler::getInstance()->createConditions($returnValues['returnValues']->trophyID, $conditions); - - $this->reset(); - - WCF::getTPL()->assign([ - 'objectEditLink' => LinkHandler::getInstance()->getControllerLink( - TrophyEditForm::class, - ['id' => $returnValues['returnValues']->trophyID] - ), - ]); - } - - /** - * @inheritDoc - */ - public function reset() - { - parent::reset(); - - $this->isDisabled = $this->awardAutomatically = $this->categoryID = $this->trophyUseHtml = $this->showOrder = $this->revokeAutomatically = 0; - $this->type = Trophy::TYPE_BADGE; - $this->iconName = $this->uploadedImageURL = ''; - $this->iconColor = 'rgba(255, 255, 255, 1)'; - $this->badgeColor = 'rgba(50, 92, 132, 1)'; - $this->iconName = 'trophy;false'; - $this->tmpHash = StringUtil::getRandomID(); - - foreach ($this->conditions as $conditions) { - foreach ($conditions as $condition) { - $condition->getProcessor()->reset(); - } - } - } - - /** - * @inheritDoc - */ - public function assignVariables() - { - parent::assignVariables(); - - WCF::getTPL()->assign([ - 'categoryID' => $this->categoryID, - 'type' => $this->type, - 'isDisabled' => $this->isDisabled, - 'iconName' => $this->iconName, - 'iconColor' => $this->iconColor, - 'badgeColor' => $this->badgeColor, - 'icon' => FontAwesomeIcon::fromString($this->iconName), - 'trophyCategories' => TrophyCategoryCache::getInstance()->getCategories(), - 'groupedObjectTypes' => $this->conditions, - 'awardAutomatically' => $this->awardAutomatically, - 'revokeAutomatically' => $this->revokeAutomatically, - 'availableTypes' => $this->availableTypes, - 'tmpHash' => $this->tmpHash, - 'uploadedImageURL' => $this->uploadedImageURL, - 'trophyUseHtml' => $this->trophyUseHtml, - 'showOrder' => $this->showOrder, + parent::createForm(); + + $categories = TrophyCategoryCache::getInstance()->getCategories(); + if ($categories === []) { + throw new NamedUserException(HtmlString::fromSafeHtml(WCF::getLanguage()->getDynamicVariable('wcf.acp.trophy.error.noCategories'))); + } + + $this->form->appendChildren([ + FormContainer::create('generalContainer') + ->appendChildren([ + TitleFormField::create() + ->i18n() + ->languageItemPattern('wcf.user.trophy.title\d+') + ->required(), + DescriptionFormField::create() + ->i18n() + ->languageItemPattern('wcf.user.trophy.description\d+'), + BooleanFormField::create('trophyUseHtml') + ->label('wcf.acp.trophy.trophyUseHtml') + ->value(false), + SingleSelectionFormField::create('categoryID') + ->options($categories) + ->label('wcf.global.category') + ->filterable(\count($categories) > 20) + ->required(), + ShowOrderFormField::create() + ->options(new TrophyList()) + ->description('wcf.acp.trophy.showOrder.description') + ->required(), + BooleanFormField::create('isDisabled') + ->value(false) + ->label('wcf.acp.trophy.isDisabled'), + BooleanFormField::create('awardAutomatically') + ->value(false) + ->label('wcf.acp.trophy.awardAutomatically'), + BooleanFormField::create('revokeAutomatically') + ->value(false) + ->label('wcf.acp.trophy.revokeAutomatically') + ->addDependency( + NonEmptyFormFieldDependency::create('awardAutomaticallyDependency') + ->fieldId('awardAutomatically') + ), + RadioButtonFormField::create('type') + ->label('wcf.acp.trophy.type') + ->value(Trophy::TYPE_BADGE) + ->required() + ->options([ + Trophy::TYPE_IMAGE => 'wcf.acp.trophy.type.imageUpload', + Trophy::TYPE_BADGE => 'wcf.acp.trophy.type.badge', + ]), + ]), + FormContainer::create('imageUploadContainer') + ->label('wcf.acp.trophy.type.imageUpload') + ->appendChildren([ + // TODO + ]) + ->addDependency( + ValueFormFieldDependency::create('typeDependency') + ->fieldId('type') + ->values([Trophy::TYPE_IMAGE]) + ), + RowFormContainer::create('badgeContainer') + ->addClass('section') + ->label('wcf.acp.trophy.type.badge') + ->appendChildren([ + IconFormField::create('iconName') + ->addClasses(['col-xs-12', 'col-md-4']) + ->label('wcf.acp.trophy.type.badge') + ->value('trophy;false') + ->required(), + ColorFormField::create('iconColor') + ->label('wcf.acp.trophy.badge.iconColor') + ->addClasses(['col-xs-12', 'col-md-4']) + ->value('rgba(255, 255, 255, 1)') + ->required(), + ColorFormField::create('badgeColor') + ->label('wcf.acp.trophy.badge.badgeColor') + ->addClasses(['col-xs-12', 'col-md-4']) + ->value('rgba(50, 92, 132, 1)') + ->required(), + ]) + ->addDependency( + ValueFormFieldDependency::create('typeDependency') + ->fieldId('type') + ->values([Trophy::TYPE_BADGE]) + ), + // TODO make it required + ConditionFormContainer::create() + ->label('wcf.acp.trophy.conditions') + ->description('wcf.acp.trophy.conditions.description') + ->conditionProvider(new UserConditionProvider()) + ->addDependency( + NonEmptyFormFieldDependency::create('awardAutomaticallyDependency') + ->fieldId('awardAutomatically') + ), ]); } } diff --git a/wcfsetup/install/files/lib/acp/form/TrophyEditForm.class.php b/wcfsetup/install/files/lib/acp/form/TrophyEditForm.class.php index 21652cd93c0..3450a53878d 100644 --- a/wcfsetup/install/files/lib/acp/form/TrophyEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/TrophyEditForm.class.php @@ -4,18 +4,10 @@ use wcf\acp\page\TrophyListPage; use wcf\data\trophy\Trophy; -use wcf\data\trophy\TrophyAction; -use wcf\data\user\UserAction; -use wcf\system\condition\ConditionHandler; -use wcf\system\database\util\PreparedStatementConditionBuilder; use wcf\system\exception\IllegalLinkException; -use wcf\system\exception\UserInputException; use wcf\system\interaction\admin\TrophyInteractions; use wcf\system\interaction\StandaloneInteractionContextMenuComponent; -use wcf\system\language\I18nHandler; use wcf\system\request\LinkHandler; -use wcf\system\trophy\condition\TrophyConditionHandler; -use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; /** @@ -36,216 +28,22 @@ class TrophyEditForm extends TrophyAddForm /** * @inheritDoc */ - public $action = 'edit'; + public $formAction = 'edit'; - /** - * trophy id - * @var int - */ - public $trophyID = 0; - - /** - * trophy object - * @var Trophy - */ - public $trophy; - - /** - * @inheritDoc - */ + #[\Override] public function readParameters() { - if (!empty($_REQUEST['id'])) { - $this->trophyID = \intval($_REQUEST['id']); - } - $this->trophy = new Trophy($this->trophyID); - - if (!$this->trophy->trophyID) { + if (!isset($_REQUEST['id'])) { throw new IllegalLinkException(); } - parent::readParameters(); - } - - /** - * @inheritDoc - */ - public function readData() - { - parent::readData(); - - if (empty($_POST)) { - $this->readDataI18n($this->trophy); - - $this->categoryID = $this->trophy->categoryID; - $this->type = $this->trophy->type; - $this->isDisabled = $this->trophy->isDisabled; - $this->iconName = $this->trophy->iconName; - $this->iconColor = $this->trophy->iconColor; - $this->badgeColor = $this->trophy->badgeColor; - $this->awardAutomatically = $this->trophy->awardAutomatically; - $this->revokeAutomatically = $this->trophy->revokeAutomatically; - $this->trophyUseHtml = $this->trophy->trophyUseHtml; - $this->showOrder = $this->trophy->showOrder; - - // reset badge values for non badge trophies - if ($this->trophy->type != Trophy::TYPE_BADGE) { - $this->iconName = 'trophy;false'; - $this->iconColor = 'rgba(255, 255, 255, 1)'; - $this->badgeColor = 'rgba(50, 92, 132, 1)'; - } - - $conditions = $this->trophy->getConditions(); - $conditionsByObjectTypeID = []; - foreach ($conditions as $condition) { - $conditionsByObjectTypeID[$condition->objectTypeID] = $condition; - } - - foreach ($this->conditions as $objectTypes1) { - foreach ($objectTypes1 as $objectTypes2) { - if (\is_array($objectTypes2)) { - foreach ($objectTypes2 as $objectType) { - if (isset($conditionsByObjectTypeID[$objectType->objectTypeID])) { - $conditionsByObjectTypeID[$objectType->objectTypeID]->getObjectType()->getProcessor()->setData($conditionsByObjectTypeID[$objectType->objectTypeID]); - } - } - } elseif (isset($conditionsByObjectTypeID[$objectTypes2->objectTypeID])) { - $conditionsByObjectTypeID[$objectTypes2->objectTypeID]->getObjectType()->getProcessor()->setData($conditionsByObjectTypeID[$objectTypes2->objectTypeID]); - } - } - } - } - } - - /** - * @inheritDoc - */ - protected function validateType() - { - switch ($this->type) { - case Trophy::TYPE_IMAGE: - if (empty($this->trophy->iconFile) || !\file_exists(WCF_DIR . 'images/trophy/' . $this->trophy->iconFile)) { - throw new UserInputException('imageUpload'); - } - break; - - case Trophy::TYPE_BADGE: - if (empty($this->iconName)) { - throw new UserInputException('iconName'); - } - - if (empty($this->iconColor)) { - throw new UserInputException('iconColor'); - } - - if (empty($this->badgeColor)) { - throw new UserInputException('badgeColor'); - } - break; - } - } - - /** - * @inheritDoc - */ - public function save() - { - AbstractAcpForm::save(); - - $this->beforeSaveI18n($this->trophy); - - $data = []; - if ($this->type == Trophy::TYPE_IMAGE) { - $data['iconName'] = ''; - $data['iconColor'] = ''; - $data['badgeColor'] = ''; - } elseif ($this->type == Trophy::TYPE_BADGE) { - // delete old image icon - if (\is_file(WCF_DIR . 'images/trophy/' . $this->trophy->iconFile)) { - @\unlink(WCF_DIR . 'images/trophy/' . $this->trophy->iconFile); - } - - $data['iconName'] = $this->iconName; - $data['iconColor'] = $this->iconColor; - $data['badgeColor'] = $this->badgeColor; - $data['iconFile'] = ''; - } + $this->formObject = new Trophy(\intval($_REQUEST['id'])); - $this->objectAction = new TrophyAction([$this->trophy], 'update', [ - 'data' => \array_merge($this->additionalFields, $data, [ - 'title' => $this->title, - 'description' => $this->description, - 'categoryID' => $this->categoryID, - 'type' => $this->type, - 'isDisabled' => $this->isDisabled, - 'awardAutomatically' => $this->awardAutomatically, - 'revokeAutomatically' => $this->revokeAutomatically, - 'trophyUseHtml' => $this->trophyUseHtml, - 'showOrder' => $this->showOrder, - ]), - ]); - $this->objectAction->executeAction(); - - // transform conditions array into one-dimensional array - $conditions = []; - foreach ($this->conditions as $groupedObjectTypes) { - foreach ($groupedObjectTypes as $objectTypes) { - if (\is_array($objectTypes)) { - $conditions = \array_merge($conditions, $objectTypes); - } else { - $conditions[] = $objectTypes; - } - } - } - - if ($this->awardAutomatically) { - ConditionHandler::getInstance()->updateConditions( - $this->trophy->trophyID, - $this->trophy->getConditions(), - $conditions - ); - } else { - ConditionHandler::getInstance()->deleteConditions( - TrophyConditionHandler::CONDITION_DEFINITION_NAME, - [$this->trophy->trophyID] - ); - } - - // reset special trophies, if trophy is disabled - if ($this->isDisabled) { - $sql = "DELETE FROM wcf1_user_special_trophy - WHERE trophyID = ?"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute([$this->trophyID]); - - UserStorageHandler::getInstance()->resetAll('specialTrophies'); - } - - if ($this->isDisabled != $this->trophy->isDisabled) { - // update trophy points - $conditionBuilder = new PreparedStatementConditionBuilder(); - $conditionBuilder->add('trophyID = ?', [$this->trophyID]); - $sql = "SELECT COUNT(*) as count, userID - FROM wcf1_user_trophy - " . $conditionBuilder . " - GROUP BY userID"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute($conditionBuilder->getParameters()); - - while ($row = $statement->fetchArray()) { - $userAction = new UserAction([$row['userID']], 'update', [ - 'counters' => [ - 'trophyPoints' => $row['count'] * ($this->isDisabled) ? -1 : 1, - ], - ]); - $userAction->executeAction(); - } + if (!$this->formObject->trophyID) { + throw new IllegalLinkException(); } - $this->saved(); - - // show success message - WCF::getTPL()->assign('success', true); + parent::readParameters(); } /** @@ -255,13 +53,10 @@ public function assignVariables() { parent::assignVariables(); - I18nHandler::getInstance()->assignVariables(!empty($_POST)); - WCF::getTPL()->assign([ - 'trophy' => $this->trophy, 'interactionContextMenu' => StandaloneInteractionContextMenuComponent::forContentHeaderButton( new TrophyInteractions(), - $this->trophy, + $this->formObject, LinkHandler::getInstance()->getControllerLink(TrophyListPage::class) ), ]); diff --git a/wcfsetup/install/files/style/ui/fontAwesome.scss b/wcfsetup/install/files/style/ui/fontAwesome.scss index 02ebb888fb0..7274ab91ab9 100644 --- a/wcfsetup/install/files/style/ui/fontAwesome.scss +++ b/wcfsetup/install/files/style/ui/fontAwesome.scss @@ -33,3 +33,15 @@ } } } + +.iconBadge { + align-self: flex-start; + display: inline-block; + border-radius: 50%; + + > fa-icon { + transform: scale(0.5625); + width: var(--icon-size); + position: static !important; + } +} From 2b424efeeb1fdce2b76c359ac34d30df8704f582 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 21 Jul 2025 12:37:48 +0200 Subject: [PATCH 03/18] File upload processor for trophy image --- com.woltlab.wcf/fileDelete.xml | 1 + com.woltlab.wcf/objectType.xml | 5 + .../templates/shared_trophyImage.tpl | 2 +- .../update_com.woltlab.wcf_6.3_step1.php | 7 + .../lib/acp/form/TrophyAddForm.class.php | 16 +- .../files/lib/data/trophy/Trophy.class.php | 12 +- .../lib/data/trophy/TrophyAction.class.php | 142 +------------- .../files/lib/page/TrophyPage.class.php | 2 +- .../processor/TrophyFileProcessor.class.php | 179 ++++++++++++++++++ ...mageUploadFileValidationStrategy.class.php | 66 ------- .../worker/TrophyRebuildDataWorker.class.php | 28 +++ wcfsetup/install/lang/de.xml | 2 +- wcfsetup/install/lang/en.xml | 2 +- wcfsetup/setup/db/install.sql | 4 +- 14 files changed, 262 insertions(+), 206 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/file/processor/TrophyFileProcessor.class.php delete mode 100644 wcfsetup/install/files/lib/system/upload/TrophyImageUploadFileValidationStrategy.class.php diff --git a/com.woltlab.wcf/fileDelete.xml b/com.woltlab.wcf/fileDelete.xml index 10520b1ddc4..acb51b66fde 100644 --- a/com.woltlab.wcf/fileDelete.xml +++ b/com.woltlab.wcf/fileDelete.xml @@ -62,5 +62,6 @@ lib/system/upload/AvatarUploadFileValidationStrategy.class.php lib/system/upload/UserCoverPhotoUploadFileSaveStrategy.class.php lib/system/upload/UserCoverPhotoUploadFileValidationStrategy.class.php + lib/system/upload/TrophyImageUploadFileValidationStrategy.class.php diff --git a/com.woltlab.wcf/objectType.xml b/com.woltlab.wcf/objectType.xml index e6cf1f372f7..f08cd2d53cd 100644 --- a/com.woltlab.wcf/objectType.xml +++ b/com.woltlab.wcf/objectType.xml @@ -1767,6 +1767,11 @@ com.woltlab.wcf.file wcf\system\file\processor\UnfurlUrlImageFileProcessor + + com.woltlab.wcf.trophy + com.woltlab.wcf.file + wcf\system\file\processor\TrophyFileProcessor + com.woltlab.wcf.page.controller diff --git a/com.woltlab.wcf/templates/shared_trophyImage.tpl b/com.woltlab.wcf/templates/shared_trophyImage.tpl index 22d9ebb7840..798a77114c9 100644 --- a/com.woltlab.wcf/templates/shared_trophyImage.tpl +++ b/com.woltlab.wcf/templates/shared_trophyImage.tpl @@ -1,5 +1,5 @@ columns([ MediumtextDatabaseTableColumn::create('conditions'), DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), + IntDatabaseTableColumn::create('imageFileID'), + ]) + ->indices([ + DatabaseTableIndex::create('imageFileID') + ->columns(['imageFileID']), ]), ]; diff --git a/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php b/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php index 6244b601b45..248f622307f 100644 --- a/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php @@ -17,6 +17,7 @@ use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency; use wcf\system\form\builder\field\dependency\ValueFormFieldDependency; use wcf\system\form\builder\field\DescriptionFormField; +use wcf\system\form\builder\field\FileProcessorFormField; use wcf\system\form\builder\field\IconFormField; use wcf\system\form\builder\field\RadioButtonFormField; use wcf\system\form\builder\field\ShowOrderFormField; @@ -57,6 +58,11 @@ class TrophyAddForm extends AbstractFormBuilderForm */ public $objectActionClass = TrophyAction::class; + /** + * @inheritDoc + */ + public $objectEditLinkController = TrophyEditForm::class; + #[\Override] public function createForm() { @@ -114,7 +120,15 @@ public function createForm() FormContainer::create('imageUploadContainer') ->label('wcf.acp.trophy.type.imageUpload') ->appendChildren([ - // TODO + FileProcessorFormField::create('imageFileID') + ->label('wcf.acp.trophy.type.imageUpload') + ->description('wcf.acp.trophy.type.imageUpload.description') + ->objectType('com.woltlab.wcf.trophy') + ->singleFileUpload() + ->simpleReplace() + ->bigPreview() + ->hideDeleteButton() + ->required(), ]) ->addDependency( ValueFormFieldDependency::create('typeDependency') diff --git a/wcfsetup/install/files/lib/data/trophy/Trophy.class.php b/wcfsetup/install/files/lib/data/trophy/Trophy.class.php index 378dc8b2433..b71622e11a6 100644 --- a/wcfsetup/install/files/lib/data/trophy/Trophy.class.php +++ b/wcfsetup/install/files/lib/data/trophy/Trophy.class.php @@ -3,9 +3,11 @@ namespace wcf\data\trophy; use wcf\data\DatabaseObject; +use wcf\data\file\File; use wcf\data\ITitledLinkObject; use wcf\data\trophy\category\TrophyCategory; use wcf\data\trophy\category\TrophyCategoryCache; +use wcf\system\cache\runtime\FileRuntimeCache; use wcf\system\event\EventHandler; use wcf\system\request\IRouteController; use wcf\system\request\LinkHandler; @@ -27,7 +29,6 @@ * @property-read string $description the trophy description * @property-read int $categoryID the categoryID of the trophy * @property-read int $type the trophy type - * @property-read string $iconFile the file location of the icon * @property-read string $iconName the icon name * @property-read string $iconColor the icon color * @property-read string $badgeColor the icon badge color @@ -38,6 +39,7 @@ * @property-read int $showOrder position of the trophy in relation to the other trophies at the same location * @property-read string|null $conditions * @property-read int $isLegacy + * @property-read int|null $imageFileID */ class Trophy extends DatabaseObject implements ITitledLinkObject, IRouteController { @@ -191,4 +193,12 @@ public function getIcon(): ?FontAwesomeIcon return null; } + + /** + * @since 6.3 + */ + public function getFile(): ?File + { + return FileRuntimeCache::getInstance()->getObject($this->imageFileID); + } } diff --git a/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php b/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php index b8f10222dff..db190ea9897 100644 --- a/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php +++ b/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php @@ -3,18 +3,14 @@ namespace wcf\data\trophy; use wcf\data\AbstractDatabaseObjectAction; +use wcf\data\file\FileEditor; use wcf\data\IToggleAction; -use wcf\data\IUploadAction; use wcf\data\TDatabaseObjectToggle; use wcf\data\user\trophy\UserTrophyAction; use wcf\data\user\trophy\UserTrophyList; use wcf\data\user\UserAction; use wcf\system\database\util\PreparedStatementConditionBuilder; -use wcf\system\exception\IllegalLinkException; -use wcf\system\exception\UserInputException; -use wcf\system\image\ImageHandler; use wcf\system\upload\TrophyImageUploadFileValidationStrategy; -use wcf\system\upload\UploadFile; use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; @@ -28,7 +24,7 @@ * * @extends AbstractDatabaseObjectAction */ -class TrophyAction extends AbstractDatabaseObjectAction implements IToggleAction, IUploadAction +class TrophyAction extends AbstractDatabaseObjectAction implements IToggleAction { use TDatabaseObjectToggle; @@ -60,10 +56,6 @@ public function create() $trophy = parent::create(); - if (isset($this->parameters['tmpHash']) && $this->parameters['data']['type'] === Trophy::TYPE_IMAGE) { - $this->updateTrophyImage($trophy); - } - $trophyEditor = new TrophyEditor($trophy); $trophyEditor->setShowOrder($showOrder); @@ -94,12 +86,17 @@ public function delete() $userTrophyAction = new UserTrophyAction($userTrophyList->getObjects(), 'delete'); $userTrophyAction->executeAction(); + $fileIDs = []; foreach ($this->getObjects() as $trophy) { - if ($trophy->iconFile) { - @\unlink(WCF_DIR . 'images/trophy/' . $trophy->iconFile); + if ($trophy->imageFileID) { + $fileIDs[] = $trophy->imageFileID; } } + if ($fileIDs !== []) { + FileEditor::deleteAll($fileIDs); + } + $returnValues = parent::delete(); UserStorageHandler::getInstance()->resetAll('specialTrophies'); @@ -114,14 +111,6 @@ public function update() { parent::update(); - if (isset($this->parameters['data']['type']) && $this->parameters['data']['type'] === Trophy::TYPE_IMAGE) { - foreach ($this->getObjects() as $trophy) { - if (isset($this->parameters['tmpHash'])) { - $this->updateTrophyImage($trophy->getDecoratedObject()); - } - } - } - if (\count($this->objects) == 1 && isset($this->parameters['data']['showOrder']) && $this->parameters['data']['showOrder'] != \reset($this->objects)->showOrder) { \reset($this->objects)->setShowOrder($this->parameters['data']['showOrder']); } @@ -196,117 +185,4 @@ public function toggle() UserStorageHandler::getInstance()->resetAll('specialTrophies'); } - - /** - * @inheritDoc - */ - public function validateUpload() - { - WCF::getSession()->checkPermissions(['admin.trophy.canManageTrophy']); - - $this->readString('tmpHash'); - $this->readInteger('trophyID', true); - - if ($this->parameters['trophyID']) { - $this->parameters['trophy'] = new Trophy($this->parameters['trophyID']); - - if (!$this->parameters['trophy']->trophyID) { - throw new IllegalLinkException(); - } - } - - $this->parameters['__files']->validateFiles(new TrophyImageUploadFileValidationStrategy()); - - /** @var UploadFile[] $files */ - $files = $this->parameters['__files']->getFiles(); - - // only one file is allowed - if (\count($files) !== 1) { - throw new UserInputException('file'); - } - - $this->parameters['file'] = \reset($files); - - if ($this->parameters['file']->getValidationErrorType()) { - throw new UserInputException('file', $this->parameters['file']->getValidationErrorType()); - } - } - - /** - * @inheritDoc - */ - public function upload() - { - $fileName = WCF_DIR . 'images/trophy/tmp_' . $this->parameters['tmpHash'] . '.' . $this->parameters['file']->getFileExtension(); - if ($this->parameters['file']->getImageData()['height'] > 128) { - $adapter = ImageHandler::getInstance()->getAdapter(); - $adapter->loadFile($this->parameters['file']->getLocation()); - $adapter->resize( - 0, - 0, - $this->parameters['file']->getImageData()['height'], - $this->parameters['file']->getImageData()['height'], - 128, - 128 - ); - $adapter->writeImage($adapter->getImage(), $fileName); - } else { - \copy($this->parameters['file']->getLocation(), $fileName); - } - - // remove old image - @\unlink($this->parameters['file']->getLocation()); - - // store extension within session variables - WCF::getSession()->register( - 'trophyImage-' . $this->parameters['tmpHash'], - $this->parameters['file']->getFileExtension() - ); - - if ($this->parameters['trophyID']) { - $this->updateTrophyImage($this->parameters['trophy']); - - return [ - 'url' => WCF::getPath() . 'images/trophy/trophyImage-' . $this->parameters['trophyID'] . '.' . $this->parameters['file']->getFileExtension(), - ]; - } - - return [ - 'url' => WCF::getPath() . 'images/trophy/' . \basename($fileName), - ]; - } - - /** - * Updates style preview image. - * - * @return void - */ - protected function updateTrophyImage(Trophy $trophy) - { - if (!isset($this->parameters['tmpHash'])) { - return; - } - - $fileExtension = WCF::getSession()->getVar('trophyImage-' . $this->parameters['tmpHash']); - if ($fileExtension !== null) { - $oldFilename = WCF_DIR . 'images/trophy/tmp_' . $this->parameters['tmpHash'] . '.' . $fileExtension; - if (\file_exists($oldFilename)) { - $filename = 'trophyImage-' . $trophy->trophyID . '.' . $fileExtension; - if (@\rename($oldFilename, WCF_DIR . 'images/trophy/' . $filename)) { - // delete old file if it has a different file extension - if ($trophy->iconFile != $filename) { - @\unlink(WCF_DIR . 'images/trophy/' . $trophy->iconFile); - - $trophyEditor = new TrophyEditor($trophy); - $trophyEditor->update([ - 'iconFile' => $filename, - ]); - } - } else { - // remove temp file - @\unlink($oldFilename); - } - } - } - } } diff --git a/wcfsetup/install/files/lib/page/TrophyPage.class.php b/wcfsetup/install/files/lib/page/TrophyPage.class.php index 03ebe9e4188..970911842ca 100644 --- a/wcfsetup/install/files/lib/page/TrophyPage.class.php +++ b/wcfsetup/install/files/lib/page/TrophyPage.class.php @@ -114,7 +114,7 @@ public function readData() MetaTagHandler::getInstance()->addTag( 'og:image', 'og:image', - WCF::getPath() . 'images/trophy/' . $this->trophy->iconFile, + $this->trophy->getFile()->getFullSizeImageSource(), true ); } diff --git a/wcfsetup/install/files/lib/system/file/processor/TrophyFileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/TrophyFileProcessor.class.php new file mode 100644 index 00000000000..b7879d40535 --- /dev/null +++ b/wcfsetup/install/files/lib/system/file/processor/TrophyFileProcessor.class.php @@ -0,0 +1,179 @@ + + * @since 6.3 + */ +final class TrophyFileProcessor extends AbstractFileProcessor +{ + public const IMAGE_MIN_SIZE = 64; + + public const IMAGE_MAX_SIZE = 128; + + #[\Override] + public function getObjectTypeName(): string + { + return 'com.woltlab.wcf.trophy'; + } + + #[\Override] + public function getAllowedFileExtensions(array $context): array + { + return [ + 'gif', + 'jpg', + 'jpeg', + 'png', + 'webp', + ]; + } + + #[\Override] + public function canAdopt(File $file, array $context): bool + { + $trophyFromContext = $this->getTrophy($context); + $trophyFromCoreFile = $this->getTrophyByFile($file); + + if ($trophyFromCoreFile === null) { + return true; + } + + if ($trophyFromContext->trophyID === $trophyFromCoreFile->trophyID) { + return true; + } + + return false; + } + + #[\Override] + public function adopt(File $file, array $context): void + { + $trophy = $this->getTrophy($context); + if ($trophy === null) { + return; + } + + (new TrophyEditor($trophy))->update([ + 'imageFileID' => $file->fileID, + ]); + } + + #[\Override] + public function acceptUpload(string $filename, int $fileSize, array $context): FileProcessorPreflightResult + { + if (!WCF::getSession()->getPermission('admin.trophy.canManageTrophy')) { + return FileProcessorPreflightResult::InsufficientPermissions; + } + + if (isset($context['objectID'])) { + $trophy = $this->getTrophy($context); + if ($trophy === null) { + return FileProcessorPreflightResult::InvalidContext; + } + } + + if (!FileUtil::endsWithAllowedExtension($filename, $this->getAllowedFileExtensions($context))) { + return FileProcessorPreflightResult::FileExtensionNotPermitted; + } + + return FileProcessorPreflightResult::Passed; + } + + #[\Override] + public function validateUpload(File $file): void + { + $imageData = @\getimagesize($file->getPathname()); + if ($imageData === false) { + throw new UserInputException('file', 'noImage'); + } + + if ($imageData[0] !== $imageData[1]) { + throw new UserInputException('file', 'notSquare'); + } + + if ( + $imageData[0] != self::IMAGE_MIN_SIZE + && $imageData[0] != self::IMAGE_MAX_SIZE + ) { + throw new UserInputException('file', 'wrongSize'); + } + } + + #[\Override] + public function canDelete(File $file): bool + { + return WCF::getSession()->getPermission('admin.trophy.canManageTrophy'); + } + + #[\Override] + public function canDownload(File $file): bool + { + return true; + } + + #[\Override] + public function delete(array $fileIDs, array $thumbnailIDs): void + { + $conditionBuilder = new PreparedStatementConditionBuilder(); + $conditionBuilder->add('imageFileID IN (?)', [$fileIDs]); + + $sql = "UPDATE wcf1_trophy + SET imageFileID = ? + " . $conditionBuilder; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([null, ...$conditionBuilder->getParameters()]); + } + + #[\Override] + public function countExistingFiles(array $context): int + { + return $this->getTrophy($context)?->imageFileID === null ? 0 : 1; + } + + #[\Override] + public function getImageCropperConfiguration(): ImageCropperConfiguration + { + return ImageCropperConfiguration::forMinMax( + new ImageCropSize(self::IMAGE_MIN_SIZE, self::IMAGE_MIN_SIZE), + new ImageCropSize(self::IMAGE_MAX_SIZE, self::IMAGE_MAX_SIZE), + ); + } + + /** + * @param array $context + */ + private function getTrophy(array $context): ?Trophy + { + $trophyID = $context['objectID'] ?? null; + if ($trophyID === null) { + return null; + } + + return new Trophy($trophyID); + } + + private function getTrophyByFile(File $file): ?Trophy + { + $sql = "SELECT * + FROM wcf1_trophy + WHERE imageFileID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([$file->fileID]); + + return $statement->fetchObject(Trophy::class); + } +} diff --git a/wcfsetup/install/files/lib/system/upload/TrophyImageUploadFileValidationStrategy.class.php b/wcfsetup/install/files/lib/system/upload/TrophyImageUploadFileValidationStrategy.class.php deleted file mode 100644 index a0ab4b5361c..00000000000 --- a/wcfsetup/install/files/lib/system/upload/TrophyImageUploadFileValidationStrategy.class.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @since 3.1 - */ -class TrophyImageUploadFileValidationStrategy implements IUploadFileValidationStrategy -{ - /** - * minimum trophy image width and height - * @var int - */ - const MIN_TROPHY_IMAGE_SIZE = 64; - - /** - * @inheritDoc - */ - public function validate(UploadFile $uploadFile) - { - if ($uploadFile->getErrorCode()) { - $uploadFile->setValidationErrorType('uploadFailed'); - - return false; - } - - if ($uploadFile->getImageData() === null) { - $uploadFile->setValidationErrorType('noImage'); - - return false; - } - - if ($uploadFile->getImageData()['width'] != $uploadFile->getImageData()['height']) { - $uploadFile->setValidationErrorType('notSquared'); - - return false; - } - - if ($uploadFile->getImageData()['width'] < self::MIN_TROPHY_IMAGE_SIZE) { - $uploadFile->setValidationErrorType('tooSmall'); - - return false; - } - - if (!ImageUtil::checkImageContent($uploadFile->getLocation())) { - $uploadFile->setValidationErrorType('noImage'); - - return false; - } - - if (!ImageUtil::isImage($uploadFile->getLocation(), $uploadFile->getFilename())) { - $uploadFile->setValidationErrorType('noImage'); - - return false; - } - - return true; - } -} diff --git a/wcfsetup/install/files/lib/system/worker/TrophyRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/TrophyRebuildDataWorker.class.php index 0cbbaceba26..eb01140958e 100644 --- a/wcfsetup/install/files/lib/system/worker/TrophyRebuildDataWorker.class.php +++ b/wcfsetup/install/files/lib/system/worker/TrophyRebuildDataWorker.class.php @@ -2,6 +2,9 @@ namespace wcf\system\worker; +use wcf\data\file\FileEditor; +use wcf\data\trophy\Trophy; +use wcf\data\trophy\TrophyEditor; use wcf\data\trophy\TrophyList; use wcf\system\trophy\command\MigrateLegacyCondition; @@ -34,6 +37,31 @@ public function execute() foreach ($this->objectList as $trophy) { (new MigrateLegacyCondition($trophy))(); + + $this->migrateFile($trophy); + } + } + + private function migrateFile(Trophy $trophy): void + { + // @phpstan-ignore property.notFound + if ($trophy->type !== Trophy::TYPE_IMAGE || $trophy->imageFileID !== null || $trophy->iconFile === '') { + return; } + + $file = FileEditor::createFromExistingFile( + WCF_DIR . 'images/trophy/' . $trophy->iconFile, + $trophy->iconFile, + 'com.woltlab.wcf.trophy' + ); + + if ($file === null) { + return; + } + + (new TrophyEditor($trophy))->update([ + 'imageFileID' => $file->fileID, + 'iconFile' => '', + ]); } } diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 93d29b8c2d5..f30b25e4ebd 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -5194,7 +5194,7 @@ Sobald {if LANGUAGE_USE_INFORMAL_VARIANT}dein{else}Ihr{/if} Benutzerkonto freige - + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index ed0b9518e18..e0371976f86 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -5193,7 +5193,7 @@ You also received a list of backup codes to use when your second factor becomes - + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 055bc1b0fa9..f1850d9d8b8 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1505,7 +1505,9 @@ CREATE TABLE wcf1_trophy( showOrder INT(10) NOT NULL DEFAULT 0, conditions MEDIUMTEXT, isLegacy TINYINT(1) NOT NULL DEFAULT 0, - KEY(categoryID) + imageFileID INT DEFAULT NULL, + KEY(categoryID), + KEY imageFileID(imageFileID) ); DROP TABLE IF EXISTS wcf1_unfurl_url; From 66727f6c99f5b6a80b4f876c34bceae1af4998ce Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 21 Jul 2025 13:25:51 +0200 Subject: [PATCH 04/18] Migrate trophy cache to an eager cache --- .../lib/data/trophy/I18nTrophyList.class.php | 8 ++ .../lib/data/trophy/TrophyCache.class.php | 91 +++++++------------ .../files/lib/data/user/UserProfile.class.php | 1 + .../data/user/trophy/UserTrophyList.class.php | 15 +++ .../files/lib/page/TrophyListPage.class.php | 3 + .../builder/TrophyCacheBuilder.class.php | 25 ++--- .../system/cache/eager/TrophyCache.class.php | 47 ++++++++++ .../eager/data/TrophyCacheData.class.php | 37 ++++++++ ...rTrophyReceivedNotificationEvent.class.php | 3 +- 9 files changed, 159 insertions(+), 71 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/cache/eager/TrophyCache.class.php create mode 100644 wcfsetup/install/files/lib/system/cache/eager/data/TrophyCacheData.class.php diff --git a/wcfsetup/install/files/lib/data/trophy/I18nTrophyList.class.php b/wcfsetup/install/files/lib/data/trophy/I18nTrophyList.class.php index 27d595da41c..6d6643ff764 100644 --- a/wcfsetup/install/files/lib/data/trophy/I18nTrophyList.class.php +++ b/wcfsetup/install/files/lib/data/trophy/I18nTrophyList.class.php @@ -25,4 +25,12 @@ class I18nTrophyList extends I18nDatabaseObjectList * @inheritDoc */ public $className = Trophy::class; + + #[\Override] + public function readObjects() + { + parent::readObjects(); + + TrophyCache::getInstance()->cacheFileIDs($this->getObjects()); + } } diff --git a/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php b/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php index 23f96d7b9a5..b6206ee8204 100644 --- a/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php +++ b/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php @@ -2,7 +2,8 @@ namespace wcf\data\trophy; -use wcf\system\cache\builder\TrophyCacheBuilder; +use wcf\system\cache\eager\data\TrophyCacheData; +use wcf\system\cache\runtime\FileRuntimeCache; use wcf\system\SingletonFactory; /** @@ -13,53 +14,33 @@ * @license GNU Lesser General Public License * @since 3.1 */ -class TrophyCache extends SingletonFactory +final class TrophyCache extends SingletonFactory { - /** - * Contains all trophies. - * @var Trophy[] - */ - protected $trophies; - - /** - * Contains all enabled trophies. - * @var Trophy[] - */ - protected $enabledTrophies; - - /** - * Contains all trophies sorted by the category. - * @var ?array> - */ - protected $categorySortedTrophies; + private TrophyCacheData $trophyCache; /** * @inheritDoc */ public function init() { - $this->trophies = TrophyCacheBuilder::getInstance()->getData(); - $this->enabledTrophies = TrophyCacheBuilder::getInstance()->getData(['onlyEnabled' => 1]); + $this->trophyCache = (new \wcf\system\cache\eager\TrophyCache())->getCache(); } /** * Returns the trophy with the given trophyID. - * - * @param int $trophyID - * @return Trophy|null */ - public function getTrophyByID($trophyID) + public function getTrophyByID(int $trophyID): ?Trophy { - return $this->trophies[$trophyID] ?? null; + return $this->trophyCache->getTrophyByID($trophyID); } /** * Returns the trophy with the given trophyID. * * @param int[] $trophyIDs - * @return (Trophy|null)[] + * @return Trophy[] */ - public function getTrophiesByID(array $trophyIDs) + public function getTrophiesByID(array $trophyIDs): array { $returnValues = []; @@ -73,37 +54,19 @@ public function getTrophiesByID(array $trophyIDs) /** * Returns all trophies for a specific category. * - * @param int $categoryID * @return Trophy[] */ - public function getTrophiesByCategoryID($categoryID) + public function getTrophiesByCategoryID(int $categoryID): array { - if (!\is_array($this->categorySortedTrophies)) { - $this->categorySortedTrophies = []; - - foreach ($this->trophies as $trophy) { - if (!isset($this->categorySortedTrophies[$trophy->categoryID])) { - $this->categorySortedTrophies[$trophy->categoryID] = []; - } - - $this->categorySortedTrophies[$trophy->categoryID][$trophy->getObjectID()] = $trophy; - } - } - - if (!isset($this->categorySortedTrophies[$categoryID])) { - return []; - } - - return $this->categorySortedTrophies[$categoryID]; + return $this->trophyCache->getTrophiesByCategoryID($categoryID); } /** * Returns all enabled trophies for a specific category. * - * @param int $categoryID * @return Trophy[] */ - public function getEnabledTrophiesByCategoryID($categoryID) + public function getEnabledTrophiesByCategoryID(int $categoryID): array { $trophies = $this->getTrophiesByCategoryID($categoryID); @@ -122,9 +85,9 @@ public function getEnabledTrophiesByCategoryID($categoryID) * * @return Trophy[] */ - public function getTrophies() + public function getTrophies(): array { - return $this->trophies; + return $this->trophyCache->trophies; } /** @@ -132,19 +95,31 @@ public function getTrophies() * * @return Trophy[] */ - public function getEnabledTrophies() + public function getEnabledTrophies(): array { - return $this->enabledTrophies; + return $this->trophyCache->enabledTrophies; + } + + /** + * @param Trophy[] $trophies + */ + public function cacheFileIDs(array $trophies): void + { + $fileIDs = []; + foreach ($trophies as $trophy) { + if ($trophy->imageFileID) { + $fileIDs[] = $trophy->imageFileID; + } + } + + FileRuntimeCache::getInstance()->cacheObjectIDs($fileIDs); } /** * Resets the cache for the trophies. - * - * @return void */ - public function clearCache() + public function clearCache(): void { - TrophyCacheBuilder::getInstance()->reset(); - TrophyCacheBuilder::getInstance()->reset(['onlyEnabled' => 1]); + (new \wcf\system\cache\eager\TrophyCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/data/user/UserProfile.class.php b/wcfsetup/install/files/lib/data/user/UserProfile.class.php index 3913e010668..a2bdec6279d 100644 --- a/wcfsetup/install/files/lib/data/user/UserProfile.class.php +++ b/wcfsetup/install/files/lib/data/user/UserProfile.class.php @@ -552,6 +552,7 @@ public function getSpecialTrophies() } Trophy::sort($trophies, 'showOrder'); + TrophyCache::getInstance()->cacheFileIDs($trophies); return $trophies; } diff --git a/wcfsetup/install/files/lib/data/user/trophy/UserTrophyList.class.php b/wcfsetup/install/files/lib/data/user/trophy/UserTrophyList.class.php index ad1a73a2c14..5480b6ab464 100644 --- a/wcfsetup/install/files/lib/data/user/trophy/UserTrophyList.class.php +++ b/wcfsetup/install/files/lib/data/user/trophy/UserTrophyList.class.php @@ -3,6 +3,7 @@ namespace wcf\data\user\trophy; use wcf\data\DatabaseObjectList; +use wcf\system\cache\runtime\FileRuntimeCache; /** * Provides a user trophy list. @@ -71,4 +72,18 @@ public static function getUserTrophies(array $userIDs, $includeDisabled = false) return $returnValues; } + + #[\Override] + public function readObjects() + { + parent::readObjects(); + + $fileIDs = []; + foreach ($this->getObjects() as $trophy) { + if ($trophy->getTrophy()->imageFileID !== null) { + $fileIDs[] = $trophy->getTrophy()->imageFileID; + } + } + FileRuntimeCache::getInstance()->cacheObjectIDs(\array_unique($fileIDs)); + } } diff --git a/wcfsetup/install/files/lib/page/TrophyListPage.class.php b/wcfsetup/install/files/lib/page/TrophyListPage.class.php index 580de3694f3..88f339754ee 100644 --- a/wcfsetup/install/files/lib/page/TrophyListPage.class.php +++ b/wcfsetup/install/files/lib/page/TrophyListPage.class.php @@ -4,6 +4,7 @@ use wcf\data\trophy\category\TrophyCategory; use wcf\data\trophy\category\TrophyCategoryCache; +use wcf\data\trophy\TrophyCache; use wcf\data\trophy\TrophyList; use wcf\system\exception\IllegalLinkException; use wcf\system\request\LinkHandler; @@ -111,6 +112,8 @@ public function assignVariables() { parent::assignVariables(); + TrophyCache::getInstance()->cacheFileIDs($this->objectList->getObjects()); + WCF::getTPL()->assign([ 'category' => $this->category, 'categoryID' => $this->categoryID, diff --git a/wcfsetup/install/files/lib/system/cache/builder/TrophyCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/TrophyCacheBuilder.class.php index 61687aa7b16..64a0ec9bafc 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/TrophyCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/TrophyCacheBuilder.class.php @@ -2,7 +2,7 @@ namespace wcf\system\cache\builder; -use wcf\data\trophy\TrophyList; +use wcf\system\cache\eager\TrophyCache; /** * Caches the trophies. @@ -11,23 +11,26 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @since 3.1 + * + * @deprecated since 6.3, use `wcf\system\cache\eager\TrophyCache` instead. */ -class TrophyCacheBuilder extends AbstractCacheBuilder +class TrophyCacheBuilder extends AbstractLegacyCacheBuilder { - /** - * @inheritDoc - */ - public function rebuild(array $parameters) + #[\Override] + protected function rebuild(array $parameters): array { - $trophyList = new TrophyList(); + $cache = (new TrophyCache())->getCache(); if (isset($parameters['onlyEnabled']) && $parameters['onlyEnabled']) { - $trophyList->getConditionBuilder()->add('isDisabled = ?', [0]); + return $cache->enabledTrophies; } - $trophyList->sqlOrderBy = 'trophy.showOrder ASC'; - $trophyList->readObjects(); + return $cache->trophies; + } - return $trophyList->getObjects(); + #[\Override] + public function reset(array $parameters = []) + { + (new TrophyCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/cache/eager/TrophyCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/TrophyCache.class.php new file mode 100644 index 00000000000..74d376634c2 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/TrophyCache.class.php @@ -0,0 +1,47 @@ + + * @since 6.3 + * + * @extends AbstractEagerCache + */ +final class TrophyCache extends AbstractEagerCache +{ + #[\Override] + protected function getCacheData(): TrophyCacheData + { + $trophyList = new TrophyList(); + $trophyList->sqlOrderBy = 'showOrder ASC'; + $trophyList->readObjects(); + + $trophies = $trophyList->getObjects(); + $enabledTrophies = \array_filter($trophies, static function ($trophy) { + return !$trophy->isDisabled; + }); + + $categorySortedTrophies = []; + foreach ($trophies as $trophy) { + if (!isset($categorySortedTrophies[$trophy->categoryID])) { + $categorySortedTrophies[$trophy->categoryID] = []; + } + + $categorySortedTrophies[$trophy->categoryID][$trophy->getObjectID()] = $trophy; + } + + return new TrophyCacheData( + $trophies, + $enabledTrophies, + $categorySortedTrophies + ); + } +} diff --git a/wcfsetup/install/files/lib/system/cache/eager/data/TrophyCacheData.class.php b/wcfsetup/install/files/lib/system/cache/eager/data/TrophyCacheData.class.php new file mode 100644 index 00000000000..15805c1c841 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/data/TrophyCacheData.class.php @@ -0,0 +1,37 @@ + + * @since 6.3 + */ +final class TrophyCacheData +{ + public function __construct( + /** @var array */ + public readonly array $trophies, + /** @var array */ + public readonly array $enabledTrophies, + /** @var array> */ + public readonly array $categorySortedTrophies = [], + ) { + } + + public function getTrophyByID(int $trophyID): ?Trophy + { + return $this->trophies[$trophyID] ?? null; + } + + /** + * @return Trophy[] + */ + public function getTrophiesByCategoryID(int $categoryID): array + { + return $this->categorySortedTrophies[$categoryID] ?? []; + } +} \ No newline at end of file diff --git a/wcfsetup/install/files/lib/system/user/notification/event/UserTrophyReceivedNotificationEvent.class.php b/wcfsetup/install/files/lib/system/user/notification/event/UserTrophyReceivedNotificationEvent.class.php index 82eb6e820f9..76a2e43a87c 100644 --- a/wcfsetup/install/files/lib/system/user/notification/event/UserTrophyReceivedNotificationEvent.class.php +++ b/wcfsetup/install/files/lib/system/user/notification/event/UserTrophyReceivedNotificationEvent.class.php @@ -10,7 +10,6 @@ use wcf\data\user\trophy\UserTrophy; use wcf\data\user\trophy\UserTrophyAction; use wcf\data\user\UserProfile; -use wcf\system\cache\builder\TrophyCacheBuilder; use wcf\system\cache\eager\CategoryCache; use wcf\system\category\CategoryHandler; use wcf\system\style\FontAwesomeIcon; @@ -97,7 +96,7 @@ public static function getTestObjects(UserProfile $recipient, UserProfile $autho ], ]))->executeAction()['returnValues']; - TestableUserNotificationEventHandler::getInstance()->resetCacheBuilder(TrophyCacheBuilder::getInstance()); + TestableUserNotificationEventHandler::getInstance()->resetCacheHandler(new \wcf\system\cache\eager\TrophyCache()); TrophyCache::getInstance()->clearCache(); TrophyCache::getInstance()->init(); From 71b4566c3f7e9fd92ff302347f18e3b5ac5e28a1 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 21 Jul 2025 14:17:22 +0200 Subject: [PATCH 05/18] Save i18n values --- .../lib/data/trophy/TrophyAction.class.php | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php b/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php index db190ea9897..be5585c8e9a 100644 --- a/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php +++ b/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php @@ -6,6 +6,7 @@ use wcf\data\file\FileEditor; use wcf\data\IToggleAction; use wcf\data\TDatabaseObjectToggle; +use wcf\data\TI18nDatabaseObjectAction; use wcf\data\user\trophy\UserTrophyAction; use wcf\data\user\trophy\UserTrophyList; use wcf\data\user\UserAction; @@ -27,6 +28,7 @@ class TrophyAction extends AbstractDatabaseObjectAction implements IToggleAction { use TDatabaseObjectToggle; + use TI18nDatabaseObjectAction; /** * @inheritDoc @@ -56,6 +58,8 @@ public function create() $trophy = parent::create(); + $this->saveI18nValue($trophy); + $trophyEditor = new TrophyEditor($trophy); $trophyEditor->setShowOrder($showOrder); @@ -99,6 +103,8 @@ public function delete() $returnValues = parent::delete(); + $this->deleteI18nValues(); + UserStorageHandler::getInstance()->resetAll('specialTrophies'); return $returnValues; @@ -114,6 +120,10 @@ public function update() if (\count($this->objects) == 1 && isset($this->parameters['data']['showOrder']) && $this->parameters['data']['showOrder'] != \reset($this->objects)->showOrder) { \reset($this->objects)->setShowOrder($this->parameters['data']['showOrder']); } + + foreach ($this->objects as $object) { + $this->saveI18nValue($object->getDecoratedObject()); + } } /** @@ -185,4 +195,25 @@ public function toggle() UserStorageHandler::getInstance()->resetAll('specialTrophies'); } + + #[\Override] + public function getI18nSaveTypes(): array + { + return [ + 'title' => 'wcf.user.trophy.title\d+', + 'description' => 'wcf.user.trophy.description\d+', + ]; + } + + #[\Override] + public function getLanguageCategory(): string + { + return 'wcf.user.trophy'; + } + + #[\Override] + public function getPackageID(): int + { + return 1; + } } From ffbd755c7176f36d86bcc17e2adcde82f31dd113 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 21 Jul 2025 14:25:36 +0200 Subject: [PATCH 06/18] Use the new condition types to check whether the user meets the condition. --- .../TrophyConditionHandler.class.php | 58 +++---------------- 1 file changed, 8 insertions(+), 50 deletions(-) diff --git a/wcfsetup/install/files/lib/system/trophy/condition/TrophyConditionHandler.class.php b/wcfsetup/install/files/lib/system/trophy/condition/TrophyConditionHandler.class.php index 01eba01a0e2..baca6b3be8f 100644 --- a/wcfsetup/install/files/lib/system/trophy/condition/TrophyConditionHandler.class.php +++ b/wcfsetup/install/files/lib/system/trophy/condition/TrophyConditionHandler.class.php @@ -2,12 +2,12 @@ namespace wcf\system\trophy\condition; -use wcf\data\object\type\ObjectType; -use wcf\data\object\type\ObjectTypeCache; use wcf\data\trophy\Trophy; use wcf\data\trophy\TrophyList; use wcf\data\user\trophy\UserTrophyAction; use wcf\data\user\UserList; +use wcf\system\condition\ConditionHandler; +use wcf\system\condition\provider\UserConditionProvider; use wcf\system\SingletonFactory; /** @@ -20,49 +20,6 @@ */ class TrophyConditionHandler extends SingletonFactory { - /** - * definition name for trophy conditions - * @var string - */ - const CONDITION_DEFINITION_NAME = 'com.woltlab.wcf.condition.trophy'; - - /** - * list of grouped trophy condition object types - * @var ObjectType[][] - */ - protected $groupedObjectTypes = []; - - /** - * @inheritDoc - */ - protected function init() - { - // TODO - $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes(self::CONDITION_DEFINITION_NAME); - - foreach ($objectTypes as $objectType) { - if (!$objectType->conditiongroup) { - continue; - } - - if (!isset($this->groupedObjectTypes[$objectType->conditiongroup])) { - $this->groupedObjectTypes[$objectType->conditiongroup] = []; - } - - $this->groupedObjectTypes[$objectType->conditiongroup][$objectType->objectTypeID] = $objectType; - } - } - - /** - * Returns the list of grouped trophy condition object types. - * - * @return ObjectType[][] - */ - public function getGroupedObjectTypes() - { - return $this->groupedObjectTypes; - } - /** * Assign trophies based on rules. * @@ -140,10 +97,10 @@ private function getUserIDs(Trophy $trophy) LEFT JOIN wcf1_user_option_value user_option_value ON user_option_value.userID = user_table.userID"; - $conditions = $trophy->getConditions(); - // TODO + $provider = new UserConditionProvider(); + $conditions = ConditionHandler::getInstance()->getConditionsWithFilter($provider, $trophy->getConditions()); foreach ($conditions as $condition) { - $condition->getObjectType()->getProcessor()->addUserCondition($condition, $userList); + $condition->applyFilter($userList); } // prevent multiple awards from a trophy for a user @@ -187,9 +144,10 @@ private function getRevocableUserTrophyIDs(Trophy $trophy, $maxTrophyIDs) } // Assign the condition to the pseudo DBOList object - // TODO + $provider = new UserConditionProvider(); + $conditions = ConditionHandler::getInstance()->getConditionsWithFilter($provider, $trophy->getConditions()); foreach ($conditions as $condition) { - $condition->getObjectType()->getProcessor()->addUserCondition($condition, $pseudoUserList); + $condition->applyFilter($pseudoUserList); } // Now we create our own query to find out which users no longer meet the conditions. From 6009099a1b16b948796dffb37c179bfe7a815f17 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 21 Jul 2025 14:39:24 +0200 Subject: [PATCH 07/18] Add required validation for condition form fields --- .../shared_conditionFormContainer.tpl | 4 +++ .../lib/acp/form/TrophyAddForm.class.php | 2 +- .../ConditionFormContainer.class.php | 28 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/com.woltlab.wcf/templates/shared_conditionFormContainer.tpl b/com.woltlab.wcf/templates/shared_conditionFormContainer.tpl index bf7fabb2f1f..941edf0fef8 100644 --- a/com.woltlab.wcf/templates/shared_conditionFormContainer.tpl +++ b/com.woltlab.wcf/templates/shared_conditionFormContainer.tpl @@ -21,6 +21,10 @@ + + {if $container->isRequired() && $container->isEmpty()} + {lang}wcf.global.form.error.empty{/lang} + {/if} {include file='shared_formContainerDependencies'} diff --git a/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php b/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php index 248f622307f..ffe034803fe 100644 --- a/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php @@ -160,11 +160,11 @@ public function createForm() ->fieldId('type') ->values([Trophy::TYPE_BADGE]) ), - // TODO make it required ConditionFormContainer::create() ->label('wcf.acp.trophy.conditions') ->description('wcf.acp.trophy.conditions.description') ->conditionProvider(new UserConditionProvider()) + ->required() ->addDependency( NonEmptyFormFieldDependency::create('awardAutomaticallyDependency') ->fieldId('awardAutomatically') diff --git a/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php index 9256854bf1f..11a617cd62c 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php @@ -31,6 +31,7 @@ final class ConditionFormContainer extends FormContainer * @phpstan-ignore missingType.generics */ protected AbstractConditionProvider $conditionProvider; + private bool $isRequired = false; public function __construct() { @@ -38,6 +39,16 @@ public function __construct() $this->label("wcf.form.field.condition"); } + public function hasValidationErrors(): bool + { + if ($this->isRequired && $this->isEmpty()) { + return true; + } + + return parent::hasValidationErrors(); + } + + #[\Override] protected static function getDefaultId(): string { @@ -165,4 +176,21 @@ public function getConditionProviderClass(): string return $this->conditionProvider::class; } + + public function required(bool $isRequired = true): self + { + $this->isRequired = $isRequired; + + return $this; + } + + public function isRequired(): bool + { + return $this->isRequired; + } + + public function isEmpty(): bool + { + return !$this->hasChildren(); + } } From a883fcfa25f56c7cb67e89a5a331a9f3b5ffe80e Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 22 Jul 2025 09:16:31 +0200 Subject: [PATCH 08/18] Adjustment of the importer for trophy to save the image with FileProcessor --- .../system/importer/TrophyImporter.class.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/wcfsetup/install/files/lib/system/importer/TrophyImporter.class.php b/wcfsetup/install/files/lib/system/importer/TrophyImporter.class.php index 7e3cc223f39..c5827deb82f 100644 --- a/wcfsetup/install/files/lib/system/importer/TrophyImporter.class.php +++ b/wcfsetup/install/files/lib/system/importer/TrophyImporter.class.php @@ -2,11 +2,11 @@ namespace wcf\system\importer; +use wcf\data\file\FileEditor; use wcf\data\object\type\ObjectTypeCache; use wcf\data\trophy\Trophy; use wcf\data\trophy\TrophyEditor; use wcf\system\WCF; -use wcf\util\StringUtil; /** * Represents a trophy importer. @@ -49,15 +49,18 @@ public function import($oldID, array $data, array $additionalData = []) } $filename = \basename($additionalData['fileLocation']); - while (\file_exists(WCF_DIR . 'images/trophy/' . $filename)) { - $filename = \substr(StringUtil::getRandomID(), 0, 5) . '_' . \basename($additionalData['fileLocation']); - } - - if (!@\copy($additionalData['fileLocation'], WCF_DIR . 'images/trophy/' . $filename)) { + $file = FileEditor::createFromExistingFile( + $additionalData['fileLocation'], + $filename, + 'com.woltlab.wcf.trophy', + true + ); + + if ($file === null) { return 0; } - $data['iconFile'] = $filename; + $data['imageFileID'] = $file->fileID; } /** @var Trophy $trophy */ From fef6d72e0dccf946a11eabf1465ddae34d20f29e Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 22 Jul 2025 09:27:50 +0200 Subject: [PATCH 09/18] Display a message in the ACP dashboard that trophies need to be updated via RebuildData. --- .../box/StatusMessageAcpDashboardBox.class.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php b/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php index e5083a6a3ab..78b6966d056 100644 --- a/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php +++ b/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php @@ -290,6 +290,9 @@ private function getMigrationMessage(): array if ($this->noticeHasLegacyObjects()) { $event->migrationNeeded(WCF::getLanguage()->get('wcf.acp.notice.list')); } + if ($this->trophyHasLegacyObjects()) { + $event->migrationNeeded(WCF::getLanguage()->get('wcf.user.trophy.trophies')); + } if ($event->needsMigration() === []) { return []; @@ -326,4 +329,15 @@ private function noticeHasLegacyObjects(): bool return $statement->fetchColumn() > 0; } + + private function trophyHasLegacyObjects(): bool + { + $sql = "SELECT COUNT(*) AS count + FROM wcf1_trophy + WHERE isLegacy = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([1]); + + return $statement->fetchColumn() > 0; + } } From 71142228678af706d8fc3cfd0476d2e4b01dc6bc Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 24 Jul 2025 12:00:42 +0200 Subject: [PATCH 10/18] Add the missing foreign key --- .../acp/database/update_com.woltlab.wcf_6.3_step1.php | 9 ++++++--- .../files/acp/update_com.woltlab.wcf_6.3_trophy.php | 2 +- wcfsetup/setup/db/install.sql | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) 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 3f20a04f540..1dd4ff41c84 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 @@ -11,7 +11,7 @@ use wcf\system\database\table\column\DefaultFalseBooleanDatabaseTableColumn; use wcf\system\database\table\column\IntDatabaseTableColumn; use wcf\system\database\table\column\MediumtextDatabaseTableColumn; -use wcf\system\database\table\index\DatabaseTableIndex; +use wcf\system\database\table\index\DatabaseTableForeignKey; use wcf\system\database\table\PartialDatabaseTable; return [ @@ -32,7 +32,10 @@ IntDatabaseTableColumn::create('imageFileID'), ]) ->indices([ - DatabaseTableIndex::create('imageFileID') - ->columns(['imageFileID']), + DatabaseTableForeignKey::create('imageFileID') + ->columns(['imageFileID']) + ->referencedTable('wcf1_file') + ->referencedColumns(['fileID']) + ->onDelete('SET NULL'), ]), ]; diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_trophy.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_trophy.php index 851a59a0795..dbd109ea315 100644 --- a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_trophy.php +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_trophy.php @@ -56,4 +56,4 @@ function renameObjectTypes(array &$conditionData): void unset($conditionData[$currentName]); } } -} \ No newline at end of file +} diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index f1850d9d8b8..71abe873fb3 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1506,8 +1506,7 @@ CREATE TABLE wcf1_trophy( conditions MEDIUMTEXT, isLegacy TINYINT(1) NOT NULL DEFAULT 0, imageFileID INT DEFAULT NULL, - KEY(categoryID), - KEY imageFileID(imageFileID) + KEY(categoryID) ); DROP TABLE IF EXISTS wcf1_unfurl_url; @@ -2224,6 +2223,7 @@ ALTER TABLE wcf1_template_group ADD FOREIGN KEY (parentTemplateGroupID) REFERENC ALTER TABLE wcf1_template_listener ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; ALTER TABLE wcf1_trophy ADD FOREIGN KEY (categoryID) REFERENCES wcf1_category (categoryID) ON DELETE CASCADE; +ALTER TABLE wcf1_trophy ADD FOREIGN KEY (imageFileID) REFERENCES wcf1_file (fileID) ON DELETE SET NULL; ALTER TABLE wcf1_user_collapsible_content ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; ALTER TABLE wcf1_user_collapsible_content ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE; From aa7fb790e04737ead0bcb9891254c5547414b1f0 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 24 Jul 2025 12:39:44 +0200 Subject: [PATCH 11/18] Fix the foreign key definition --- .../acp/database/update_com.woltlab.wcf_6.3_step1.php | 4 ++-- .../system/cache/eager/data/TrophyCacheData.class.php | 5 ++--- .../trophy/command/MigrateLegacyCondition.class.php | 11 +++++------ 3 files changed, 9 insertions(+), 11 deletions(-) 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 1dd4ff41c84..eca3fbe033c 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 @@ -31,8 +31,8 @@ DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), IntDatabaseTableColumn::create('imageFileID'), ]) - ->indices([ - DatabaseTableForeignKey::create('imageFileID') + ->foreignKeys([ + DatabaseTableForeignKey::create() ->columns(['imageFileID']) ->referencedTable('wcf1_file') ->referencedColumns(['fileID']) diff --git a/wcfsetup/install/files/lib/system/cache/eager/data/TrophyCacheData.class.php b/wcfsetup/install/files/lib/system/cache/eager/data/TrophyCacheData.class.php index 15805c1c841..ab31b9fe1f6 100644 --- a/wcfsetup/install/files/lib/system/cache/eager/data/TrophyCacheData.class.php +++ b/wcfsetup/install/files/lib/system/cache/eager/data/TrophyCacheData.class.php @@ -19,8 +19,7 @@ public function __construct( public readonly array $enabledTrophies, /** @var array> */ public readonly array $categorySortedTrophies = [], - ) { - } + ) {} public function getTrophyByID(int $trophyID): ?Trophy { @@ -34,4 +33,4 @@ public function getTrophiesByCategoryID(int $categoryID): array { return $this->categorySortedTrophies[$categoryID] ?? []; } -} \ No newline at end of file +} diff --git a/wcfsetup/install/files/lib/system/trophy/command/MigrateLegacyCondition.class.php b/wcfsetup/install/files/lib/system/trophy/command/MigrateLegacyCondition.class.php index fb92f017aed..4140b4eeb6f 100644 --- a/wcfsetup/install/files/lib/system/trophy/command/MigrateLegacyCondition.class.php +++ b/wcfsetup/install/files/lib/system/trophy/command/MigrateLegacyCondition.class.php @@ -19,9 +19,7 @@ */ final class MigrateLegacyCondition { - public function __construct(public readonly Trophy $trophy) - { - } + public function __construct(public readonly Trophy $trophy) {} public function __invoke(): void { @@ -31,8 +29,9 @@ public function __invoke(): void try { $json = JSON::decode($this->trophy->conditions); - } catch (SystemException $ex) { - $ex->getExceptionID(); // Log the exception if JSON decoding fails + } catch (SystemException $e) { + // Side-effect: Logs the exception. + $e->getExceptionID(); return; } @@ -46,4 +45,4 @@ public function __invoke(): void 'isDisabled' => $migratedData->isFullyMigrated ? $this->trophy->isDisabled : 1, ]); } -} \ No newline at end of file +} From c23d8288866edf1365788d19f0a4ee2270823142 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 24 Jul 2025 13:11:34 +0200 Subject: [PATCH 12/18] Just check whether there are any entries when `validate` is called. This prevents the form from immediately displaying an error message that the Conditions field has not been filled in. --- .../condition/ConditionFormContainer.class.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php index 11a617cd62c..df66bb9128c 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php @@ -32,6 +32,7 @@ final class ConditionFormContainer extends FormContainer */ protected AbstractConditionProvider $conditionProvider; private bool $isRequired = false; + private bool $isEmpty = false; public function __construct() { @@ -39,9 +40,18 @@ public function __construct() $this->label("wcf.form.field.condition"); } + #[\Override] + public function validate() + { + parent::validate(); + + $this->isEmpty = !$this->hasChildren(); + } + + public function hasValidationErrors(): bool { - if ($this->isRequired && $this->isEmpty()) { + if ($this->isRequired && $this->isEmpty) { return true; } @@ -191,6 +201,6 @@ public function isRequired(): bool public function isEmpty(): bool { - return !$this->hasChildren(); + return $this->isEmpty; } } From d20ee99ba4d2b190aad5be244d8376af2ad015aa Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 24 Jul 2025 14:59:13 +0200 Subject: [PATCH 13/18] Improve the behavior and visuals of the color picker button --- .../templates/shared_colorFormField.tpl | 6 +++--- wcfsetup/install/files/style/ui/colorPicker.scss | 15 ++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/com.woltlab.wcf/templates/shared_colorFormField.tpl b/com.woltlab.wcf/templates/shared_colorFormField.tpl index 94b941f6429..c3b4451ccd4 100644 --- a/com.woltlab.wcf/templates/shared_colorFormField.tpl +++ b/com.woltlab.wcf/templates/shared_colorFormField.tpl @@ -3,9 +3,9 @@ getValue()} style="background-color: {$field->getValue()}"{/if}> {else} - - getValue()} style="background-color: {$field->getValue()}"{/if}> - + span { - display: block; - } } .colorPickerButton { - height: 32px; + border-radius: var(--wcfBorderRadius); + overflow: hidden; width: 50px; +} - > span { - height: 32px; - } +.colorPickerButton__color { + flex: 1 auto; } .colorPickerComparison { From f83eff7d52e58dd36f2e95cb7ab98a2673345e9e Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 24 Jul 2025 15:34:27 +0200 Subject: [PATCH 14/18] Simplify the interaction with the color picker --- ts/WoltLabSuite/Core/Component/Icon/Badge.ts | 53 ++++++------------- ts/WoltLabSuite/Core/Ui/Color/Picker.ts | 21 ++++---- .../install/files/acp/templates/trophyAdd.tpl | 4 +- .../WoltLabSuite/Core/Component/Icon/Badge.js | 48 ++++++----------- .../js/WoltLabSuite/Core/Ui/Color/Picker.js | 21 ++++---- 5 files changed, 52 insertions(+), 95 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Icon/Badge.ts b/ts/WoltLabSuite/Core/Component/Icon/Badge.ts index d267c318eed..4fa17d3e0b7 100644 --- a/ts/WoltLabSuite/Core/Component/Icon/Badge.ts +++ b/ts/WoltLabSuite/Core/Component/Icon/Badge.ts @@ -7,46 +7,25 @@ * @since 6.3 */ -export class IconBadge { - #iconContainer: HTMLElement; - #colorInput?: HTMLInputElement; - #backgroundColorInput?: HTMLInputElement; - - constructor(iconFieldId: string, colorFieldId?: string, backgroundColorFieldId?: string) { - this.#iconContainer = document.getElementById(`${iconFieldId}_icon`)!; - this.#iconContainer.classList.add("iconBadge"); - - const observer = new MutationObserver((mutationsList) => { - for (const mutation of mutationsList) { - if (mutation.type === "attributes" && mutation.attributeName === "value") { - this.#updateIcon(); - } - } +export function setup(iconFieldId: string, colorFieldId: string, backgroundColorFieldId: string) { + const container = document.getElementById(`${iconFieldId}_icon`)!; + container.classList.add("iconBadge"); + + if (colorFieldId !== "") { + const colorInput = document.getElementById(colorFieldId) as HTMLInputElement; + colorInput.addEventListener("color-picker:submit", () => { + container.style.setProperty("color", colorInput.value); }); - if (colorFieldId) { - this.#colorInput = document.getElementById(colorFieldId) as HTMLInputElement; - - observer.observe(this.#colorInput, { - attributes: true, - attributeFilter: ["value"], - }); - } - - if (backgroundColorFieldId) { - this.#backgroundColorInput = document.getElementById(backgroundColorFieldId) as HTMLInputElement; - - observer.observe(this.#backgroundColorInput, { - attributes: true, - attributeFilter: ["value"], - }); - } - - this.#updateIcon(); + container.style.setProperty("color", colorInput.value); } - #updateIcon(): void { - this.#iconContainer.style.color = this.#colorInput?.value || ""; - this.#iconContainer.style.backgroundColor = this.#backgroundColorInput?.value || ""; + if (backgroundColorFieldId !== "") { + const backgroundColorInput = document.getElementById(backgroundColorFieldId) as HTMLInputElement; + backgroundColorInput.addEventListener("color-picker:submit", () => { + container.style.setProperty("background-color", backgroundColorInput.value); + }); + + container.style.setProperty("background-color", backgroundColorInput.value); } } diff --git a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts index 3191f459f6c..dfdfb36ef19 100644 --- a/ts/WoltLabSuite/Core/Ui/Color/Picker.ts +++ b/ts/WoltLabSuite/Core/Ui/Color/Picker.ts @@ -131,10 +131,10 @@ class UiColorPicker implements DialogCallbackObject {
${Language.get("wcf.style.colorPicker.new")}
- +
- +
${Language.get("wcf.style.colorPicker.current")}
@@ -354,16 +354,15 @@ class UiColorPicker implements DialogCallbackObject { const colorString = ColorUtil.rgbaToString(color); this.oldColor!.style.backgroundColor = colorString; - // The change in value via `this.input.value = colorString;` cannot be detected by a MutationObserver. - this.input.setAttribute("value", colorString); + this.input.value = colorString; - if (!(this.element instanceof HTMLButtonElement)) { - const span = this.element.querySelector("span"); - if (span) { - span.style.backgroundColor = colorString; - } else { - this.element.style.backgroundColor = colorString; - } + const event = new CustomEvent("color-picker:submit"); + this.input.dispatchEvent(event); + + const span = this.element.querySelector("span") + if (span !== null) { + span.classList.add("colorPickerButton__color"); + span.style.setProperty("background-color", colorString); } UiDialog.close(this); diff --git a/wcfsetup/install/files/acp/templates/trophyAdd.tpl b/wcfsetup/install/files/acp/templates/trophyAdd.tpl index 69bc3979822..2d5344d4d48 100644 --- a/wcfsetup/install/files/acp/templates/trophyAdd.tpl +++ b/wcfsetup/install/files/acp/templates/trophyAdd.tpl @@ -20,8 +20,8 @@ {unsafe:$form->getHtml()} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Icon/Badge.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Icon/Badge.js index 1d884d5fa83..df121cd7e4c 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Icon/Badge.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Icon/Badge.js @@ -9,41 +9,23 @@ define(["require", "exports"], function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.IconBadge = void 0; - class IconBadge { - #iconContainer; - #colorInput; - #backgroundColorInput; - constructor(iconFieldId, colorFieldId, backgroundColorFieldId) { - this.#iconContainer = document.getElementById(`${iconFieldId}_icon`); - this.#iconContainer.classList.add("iconBadge"); - const observer = new MutationObserver((mutationsList) => { - for (const mutation of mutationsList) { - if (mutation.type === "attributes" && mutation.attributeName === "value") { - this.#updateIcon(); - } - } + exports.setup = setup; + function setup(iconFieldId, colorFieldId, backgroundColorFieldId) { + const container = document.getElementById(`${iconFieldId}_icon`); + container.classList.add("iconBadge"); + if (colorFieldId !== "") { + const colorInput = document.getElementById(colorFieldId); + colorInput.addEventListener("color-picker:submit", () => { + container.style.setProperty("color", colorInput.value); }); - if (colorFieldId) { - this.#colorInput = document.getElementById(colorFieldId); - observer.observe(this.#colorInput, { - attributes: true, - attributeFilter: ["value"], - }); - } - if (backgroundColorFieldId) { - this.#backgroundColorInput = document.getElementById(backgroundColorFieldId); - observer.observe(this.#backgroundColorInput, { - attributes: true, - attributeFilter: ["value"], - }); - } - this.#updateIcon(); + container.style.setProperty("color", colorInput.value); } - #updateIcon() { - this.#iconContainer.style.color = this.#colorInput?.value || ""; - this.#iconContainer.style.backgroundColor = this.#backgroundColorInput?.value || ""; + if (backgroundColorFieldId !== "") { + const backgroundColorInput = document.getElementById(backgroundColorFieldId); + backgroundColorInput.addEventListener("color-picker:submit", () => { + container.style.setProperty("background-color", backgroundColorInput.value); + }); + container.style.setProperty("background-color", backgroundColorInput.value); } } - exports.IconBadge = IconBadge; }); 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 de403be9615..07275ab9eca 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Color/Picker.js @@ -94,10 +94,10 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti
${Language.get("wcf.style.colorPicker.new")}
- +
- +
${Language.get("wcf.style.colorPicker.current")}
@@ -281,16 +281,13 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti } const colorString = ColorUtil.rgbaToString(color); this.oldColor.style.backgroundColor = colorString; - // The change in value via `this.input.value = colorString;` cannot be detected by a MutationObserver. - this.input.setAttribute("value", colorString); - if (!(this.element instanceof HTMLButtonElement)) { - const span = this.element.querySelector("span"); - if (span) { - span.style.backgroundColor = colorString; - } - else { - this.element.style.backgroundColor = colorString; - } + this.input.value = colorString; + const event = new CustomEvent("color-picker:submit"); + this.input.dispatchEvent(event); + const span = this.element.querySelector("span"); + if (span !== null) { + span.classList.add("colorPickerButton__color"); + span.style.setProperty("background-color", colorString); } Dialog_1.default.close(this); if (typeof this.options.callbackSubmit === "function") { From 23891fe031131d701ee92529ae07a76f480f0148 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 24 Jul 2025 15:42:56 +0200 Subject: [PATCH 15/18] Check whether there are conditions only if this container is available --- .../container/condition/ConditionFormContainer.class.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php index df66bb9128c..fb55b083a44 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php @@ -45,7 +45,9 @@ public function validate() { parent::validate(); - $this->isEmpty = !$this->hasChildren(); + if ($this->isAvailable() && $this->checkDependencies()) { + $this->isEmpty = !$this->hasChildren(); + } } From ccb2ed78224c3f94925b7801e914856da3208b7a Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 24 Jul 2025 15:52:51 +0200 Subject: [PATCH 16/18] `TFormNode::checkDependencies` cannot be used because no child element (no added condition) returns `false` for this function either. --- .../condition/ConditionFormContainer.class.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php index fb55b083a44..5b7ba72fa75 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php @@ -45,11 +45,24 @@ public function validate() { parent::validate(); - if ($this->isAvailable() && $this->checkDependencies()) { + // `TFormNode::checkDependencies` also checks whether the container has at least one child element. + // We only want to know whether the container is available; we will check later whether a child element is present. + if ($this->isAvailable() && $this->checkDependency()) { $this->isEmpty = !$this->hasChildren(); } } + private function checkDependency(): bool + { + foreach ($this->dependencies as $dependency) { + if (!$dependency->checkDependency() || !$dependency->getField()->checkDependencies()) { + return false; + } + } + + return true; + } + public function hasValidationErrors(): bool { From 3c601350197beadbccb442d61ba41548b42b9861 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 24 Jul 2025 16:10:45 +0200 Subject: [PATCH 17/18] Change the CSS to saner values --- wcfsetup/install/files/style/ui/fontAwesome.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wcfsetup/install/files/style/ui/fontAwesome.scss b/wcfsetup/install/files/style/ui/fontAwesome.scss index 7274ab91ab9..1ddb2fa633c 100644 --- a/wcfsetup/install/files/style/ui/fontAwesome.scss +++ b/wcfsetup/install/files/style/ui/fontAwesome.scss @@ -36,12 +36,11 @@ .iconBadge { align-self: flex-start; - display: inline-block; + display: inline-flex; border-radius: 50%; - > fa-icon { + fa-icon { transform: scale(0.5625); width: var(--icon-size); - position: static !important; } } From 3844eb9e66c79cb3cdbe564ecaec9b14212f956b Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 25 Jul 2025 12:32:12 +0200 Subject: [PATCH 18/18] Implement a collection for `Trophy` and `UserTrophy` --- .../lib/data/trophy/I18nTrophyList.class.php | 8 -- .../files/lib/data/trophy/Trophy.class.php | 9 ++- .../lib/data/trophy/TrophyCache.class.php | 27 +++---- .../data/trophy/TrophyCollection.class.php | 55 +++++++++++++ .../files/lib/data/user/UserProfile.class.php | 1 - .../lib/data/user/trophy/UserTrophy.class.php | 19 ++--- .../trophy/UserTrophyCollection.class.php | 79 +++++++++++++++++++ .../data/user/trophy/UserTrophyList.class.php | 15 ---- .../files/lib/form/SettingsForm.class.php | 7 +- .../files/lib/page/TrophyListPage.class.php | 3 - .../box/UserTrophyListBoxController.class.php | 9 --- .../system/cache/eager/TrophyCache.class.php | 19 +++-- 12 files changed, 174 insertions(+), 77 deletions(-) create mode 100644 wcfsetup/install/files/lib/data/trophy/TrophyCollection.class.php create mode 100644 wcfsetup/install/files/lib/data/user/trophy/UserTrophyCollection.class.php diff --git a/wcfsetup/install/files/lib/data/trophy/I18nTrophyList.class.php b/wcfsetup/install/files/lib/data/trophy/I18nTrophyList.class.php index 6d6643ff764..27d595da41c 100644 --- a/wcfsetup/install/files/lib/data/trophy/I18nTrophyList.class.php +++ b/wcfsetup/install/files/lib/data/trophy/I18nTrophyList.class.php @@ -25,12 +25,4 @@ class I18nTrophyList extends I18nDatabaseObjectList * @inheritDoc */ public $className = Trophy::class; - - #[\Override] - public function readObjects() - { - parent::readObjects(); - - TrophyCache::getInstance()->cacheFileIDs($this->getObjects()); - } } diff --git a/wcfsetup/install/files/lib/data/trophy/Trophy.class.php b/wcfsetup/install/files/lib/data/trophy/Trophy.class.php index b71622e11a6..b0c57490107 100644 --- a/wcfsetup/install/files/lib/data/trophy/Trophy.class.php +++ b/wcfsetup/install/files/lib/data/trophy/Trophy.class.php @@ -2,12 +2,11 @@ namespace wcf\data\trophy; -use wcf\data\DatabaseObject; +use wcf\data\CollectionDatabaseObject; use wcf\data\file\File; use wcf\data\ITitledLinkObject; use wcf\data\trophy\category\TrophyCategory; use wcf\data\trophy\category\TrophyCategoryCache; -use wcf\system\cache\runtime\FileRuntimeCache; use wcf\system\event\EventHandler; use wcf\system\request\IRouteController; use wcf\system\request\LinkHandler; @@ -40,8 +39,10 @@ * @property-read string|null $conditions * @property-read int $isLegacy * @property-read int|null $imageFileID + * + * @extends CollectionDatabaseObject */ -class Trophy extends DatabaseObject implements ITitledLinkObject, IRouteController +class Trophy extends CollectionDatabaseObject implements ITitledLinkObject, IRouteController { /** * The type value, if this trophy is an image trophy. @@ -199,6 +200,6 @@ public function getIcon(): ?FontAwesomeIcon */ public function getFile(): ?File { - return FileRuntimeCache::getInstance()->getObject($this->imageFileID); + return $this->getCollection()->getFile($this); } } diff --git a/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php b/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php index b6206ee8204..119b52fec2f 100644 --- a/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php +++ b/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php @@ -3,7 +3,6 @@ namespace wcf\data\trophy; use wcf\system\cache\eager\data\TrophyCacheData; -use wcf\system\cache\runtime\FileRuntimeCache; use wcf\system\SingletonFactory; /** @@ -17,6 +16,7 @@ final class TrophyCache extends SingletonFactory { private TrophyCacheData $trophyCache; + private TrophyCollection $trophyCollection; /** * @inheritDoc @@ -48,6 +48,16 @@ public function getTrophiesByID(array $trophyIDs): array $returnValues[] = $this->getTrophyByID($trophyID); } + if (!isset($this->trophyCollection)) { + $this->trophyCollection = new TrophyCollection($returnValues); + } else { + $this->trophyCollection = $this->trophyCollection->withTrophies($returnValues); + } + + foreach ($returnValues as $trophy) { + $trophy->setCollection($this->trophyCollection); + } + return $returnValues; } @@ -100,21 +110,6 @@ public function getEnabledTrophies(): array return $this->trophyCache->enabledTrophies; } - /** - * @param Trophy[] $trophies - */ - public function cacheFileIDs(array $trophies): void - { - $fileIDs = []; - foreach ($trophies as $trophy) { - if ($trophy->imageFileID) { - $fileIDs[] = $trophy->imageFileID; - } - } - - FileRuntimeCache::getInstance()->cacheObjectIDs($fileIDs); - } - /** * Resets the cache for the trophies. */ diff --git a/wcfsetup/install/files/lib/data/trophy/TrophyCollection.class.php b/wcfsetup/install/files/lib/data/trophy/TrophyCollection.class.php new file mode 100644 index 00000000000..db38cf9da26 --- /dev/null +++ b/wcfsetup/install/files/lib/data/trophy/TrophyCollection.class.php @@ -0,0 +1,55 @@ + + * @since 6.3 + * + * @extends DatabaseObjectCollection + */ +final class TrophyCollection extends DatabaseObjectCollection +{ + private bool $filesLoaded = false; + + /** + * @param Trophy[] $objects + */ + public function withTrophies(array $objects): self + { + return new self(\array_unique(\array_merge($this->getObjects(), $objects))); + } + + public function getFile(Trophy $trophy): ?File + { + $this->loadFiles(); + + return FileRuntimeCache::getInstance()->getObject($trophy->imageFileID); + } + + private function loadFiles(): void + { + if ($this->filesLoaded) { + return; + } + + $this->filesLoaded = true; + + $fileIDs = []; + foreach ($this->getObjects() as $object) { + if ($object->imageFileID) { + $fileIDs[] = $object->imageFileID; + } + } + + if ($fileIDs !== []) { + FileRuntimeCache::getInstance()->cacheObjectIDs($fileIDs); + } + } +} diff --git a/wcfsetup/install/files/lib/data/user/UserProfile.class.php b/wcfsetup/install/files/lib/data/user/UserProfile.class.php index a2bdec6279d..3913e010668 100644 --- a/wcfsetup/install/files/lib/data/user/UserProfile.class.php +++ b/wcfsetup/install/files/lib/data/user/UserProfile.class.php @@ -552,7 +552,6 @@ public function getSpecialTrophies() } Trophy::sort($trophies, 'showOrder'); - TrophyCache::getInstance()->cacheFileIDs($trophies); return $trophies; } diff --git a/wcfsetup/install/files/lib/data/user/trophy/UserTrophy.class.php b/wcfsetup/install/files/lib/data/user/trophy/UserTrophy.class.php index 57c74b7bbe7..5a60b3851fc 100644 --- a/wcfsetup/install/files/lib/data/user/trophy/UserTrophy.class.php +++ b/wcfsetup/install/files/lib/data/user/trophy/UserTrophy.class.php @@ -2,9 +2,8 @@ namespace wcf\data\user\trophy; -use wcf\data\DatabaseObject; +use wcf\data\CollectionDatabaseObject; use wcf\data\trophy\Trophy; -use wcf\data\trophy\TrophyCache; use wcf\data\user\User; use wcf\data\user\UserProfile; use wcf\system\cache\runtime\UserProfileRuntimeCache; @@ -27,8 +26,10 @@ * @property-read string $description the custom trophy description * @property-read int $useCustomDescription `1`, if the trophy use a custom description * @property-read int $trophyUseHtml `1`, if the trophy use a html description + * + * @extends CollectionDatabaseObject */ -class UserTrophy extends DatabaseObject +class UserTrophy extends CollectionDatabaseObject { /** * @inheritDoc @@ -43,22 +44,18 @@ class UserTrophy extends DatabaseObject /** * Returns the trophy for the user trophy. - * - * @return Trophy */ - public function getTrophy() + public function getTrophy(): Trophy { - return TrophyCache::getInstance()->getTrophyByID($this->trophyID); + return $this->getCollection()->getTrophy($this); } /** * Returns the user profile for the user trophy. - * - * @return UserProfile */ - public function getUserProfile() + public function getUserProfile(): UserProfile { - return UserProfileRuntimeCache::getInstance()->getObject($this->userID); + return $this->getCollection()->getUserProfile($this); } /** diff --git a/wcfsetup/install/files/lib/data/user/trophy/UserTrophyCollection.class.php b/wcfsetup/install/files/lib/data/user/trophy/UserTrophyCollection.class.php new file mode 100644 index 00000000000..8990ea6037d --- /dev/null +++ b/wcfsetup/install/files/lib/data/user/trophy/UserTrophyCollection.class.php @@ -0,0 +1,79 @@ + + * @since 6.3 + * + * @extends DatabaseObjectCollection + */ +final class UserTrophyCollection extends DatabaseObjectCollection +{ + /** + * @var array + */ + private array $trophies; + + private bool $userProfilesLoaded = false; + + public function getUserProfile(UserTrophy $trophy): UserProfile + { + $this->loadUserProfiles(); + + return UserProfileRuntimeCache::getInstance()->getObject($trophy->userID); + } + + public function getTrophy(UserTrophy $trophy): Trophy + { + $this->loadTrophies(); + + return $this->trophies[$trophy->trophyID]; + } + + private function loadUserProfiles(): void + { + if ($this->userProfilesLoaded) { + return; + } + + $this->userProfilesLoaded = true; + + $userIDs = []; + foreach ($this->getObjects() as $object) { + if ($object->userID) { + $userIDs[] = $object->userID; + } + } + + if ($userIDs !== []) { + UserProfileRuntimeCache::getInstance()->cacheObjectIDs($userIDs); + } + } + + private function loadTrophies(): void + { + if (isset($this->trophies)) { + return; + } + + $this->trophies = []; + + $trophies = TrophyCache::getInstance()->getTrophiesByID(\array_unique(\array_map( + static fn (UserTrophy $userTrophy) => $userTrophy->trophyID, + $this->getObjects() + ))); + + foreach ($trophies as $trophy) { + $this->trophies[$trophy->trophyID] = $trophy; + } + } +} diff --git a/wcfsetup/install/files/lib/data/user/trophy/UserTrophyList.class.php b/wcfsetup/install/files/lib/data/user/trophy/UserTrophyList.class.php index 5480b6ab464..ad1a73a2c14 100644 --- a/wcfsetup/install/files/lib/data/user/trophy/UserTrophyList.class.php +++ b/wcfsetup/install/files/lib/data/user/trophy/UserTrophyList.class.php @@ -3,7 +3,6 @@ namespace wcf\data\user\trophy; use wcf\data\DatabaseObjectList; -use wcf\system\cache\runtime\FileRuntimeCache; /** * Provides a user trophy list. @@ -72,18 +71,4 @@ public static function getUserTrophies(array $userIDs, $includeDisabled = false) return $returnValues; } - - #[\Override] - public function readObjects() - { - parent::readObjects(); - - $fileIDs = []; - foreach ($this->getObjects() as $trophy) { - if ($trophy->getTrophy()->imageFileID !== null) { - $fileIDs[] = $trophy->getTrophy()->imageFileID; - } - } - FileRuntimeCache::getInstance()->cacheObjectIDs(\array_unique($fileIDs)); - } } diff --git a/wcfsetup/install/files/lib/form/SettingsForm.class.php b/wcfsetup/install/files/lib/form/SettingsForm.class.php index 7e08aa2c5ec..a4c16100c4e 100644 --- a/wcfsetup/install/files/lib/form/SettingsForm.class.php +++ b/wcfsetup/install/files/lib/form/SettingsForm.class.php @@ -5,7 +5,6 @@ use wcf\data\language\Language; use wcf\data\style\Style; use wcf\data\trophy\Trophy; -use wcf\data\trophy\TrophyCache; use wcf\data\user\option\category\UserOptionCategory; use wcf\data\user\trophy\UserTrophyList; use wcf\data\user\UserAction; @@ -129,12 +128,10 @@ public function readParameters() $this->availableStyles = StyleHandler::getInstance()->getAvailableStyles(); // read available trophies - $trophyIDs = \array_unique(\array_map(static function ($userTrophy) { - return $userTrophy->trophyID; + $this->availableTrophies = \array_unique(\array_map(static function ($userTrophy) { + return $userTrophy->getTrophy(); }, UserTrophyList::getUserTrophies([WCF::getUser()->userID])[WCF::getUser()->userID])); - $this->availableTrophies = TrophyCache::getInstance()->getTrophiesByID($trophyIDs); - Trophy::sort($this->availableTrophies, 'showOrder'); } elseif (!$this->optionHandler->countCategoryOptions('settings.' . $this->category)) { throw new IllegalLinkException(); diff --git a/wcfsetup/install/files/lib/page/TrophyListPage.class.php b/wcfsetup/install/files/lib/page/TrophyListPage.class.php index 88f339754ee..580de3694f3 100644 --- a/wcfsetup/install/files/lib/page/TrophyListPage.class.php +++ b/wcfsetup/install/files/lib/page/TrophyListPage.class.php @@ -4,7 +4,6 @@ use wcf\data\trophy\category\TrophyCategory; use wcf\data\trophy\category\TrophyCategoryCache; -use wcf\data\trophy\TrophyCache; use wcf\data\trophy\TrophyList; use wcf\system\exception\IllegalLinkException; use wcf\system\request\LinkHandler; @@ -112,8 +111,6 @@ public function assignVariables() { parent::assignVariables(); - TrophyCache::getInstance()->cacheFileIDs($this->objectList->getObjects()); - WCF::getTPL()->assign([ 'category' => $this->category, 'categoryID' => $this->categoryID, diff --git a/wcfsetup/install/files/lib/system/box/UserTrophyListBoxController.class.php b/wcfsetup/install/files/lib/system/box/UserTrophyListBoxController.class.php index f1963413bd5..f1d055478af 100644 --- a/wcfsetup/install/files/lib/system/box/UserTrophyListBoxController.class.php +++ b/wcfsetup/install/files/lib/system/box/UserTrophyListBoxController.class.php @@ -4,7 +4,6 @@ use wcf\data\user\trophy\UserTrophyList; use wcf\data\user\User; -use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\database\util\PreparedStatementConditionBuilder; use wcf\system\WCF; @@ -142,14 +141,6 @@ protected function getObjectList() */ public function getTemplate() { - $userIDs = []; - - foreach ($this->objectList->getObjects() as $trophy) { - $userIDs[] = $trophy->userID; - } - - UserProfileRuntimeCache::getInstance()->cacheObjectIDs(\array_unique($userIDs)); - $templateName = match ($this->getBox()->position) { 'sidebarLeft', 'sidebarRight' => 'boxUserTrophyListSidebar', default => 'boxUserTrophyList', diff --git a/wcfsetup/install/files/lib/system/cache/eager/TrophyCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/TrophyCache.class.php index 74d376634c2..b29514a6c4e 100644 --- a/wcfsetup/install/files/lib/system/cache/eager/TrophyCache.class.php +++ b/wcfsetup/install/files/lib/system/cache/eager/TrophyCache.class.php @@ -2,8 +2,9 @@ namespace wcf\system\cache\eager; -use wcf\data\trophy\TrophyList; +use wcf\data\trophy\Trophy; use wcf\system\cache\eager\data\TrophyCacheData; +use wcf\system\WCF; /** * Caches for trophies. @@ -20,11 +21,19 @@ final class TrophyCache extends AbstractEagerCache #[\Override] protected function getCacheData(): TrophyCacheData { - $trophyList = new TrophyList(); - $trophyList->sqlOrderBy = 'showOrder ASC'; - $trophyList->readObjects(); + // `TrophyList` cannot be used, otherwise calling `Trophy::getFile()` would load the files for all existing trophies + // and not just those that are actually needed. + $sql = "SELECT * + FROM wcf1_trophy + ORDER BY showOrder ASC"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute(); + + $trophies = []; + while ($trophy = $statement->fetchObject(Trophy::class)) { + $trophies[$trophy->trophyID] = $trophy; + } - $trophies = $trophyList->getObjects(); $enabledTrophies = \array_filter($trophies, static function ($trophy) { return !$trophy->isDisabled; });