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/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/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}> - + {lang}wcf.condition.add{/lang} + + {if $container->isRequired() && $container->isEmpty()} + {lang}wcf.global.form.error.empty{/lang} + {/if} {include file='shared_formContainerDependencies'} 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 @@ + * @since 6.3 + */ + +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); + }); + + container.style.setProperty("color", colorInput.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 e42372471eb..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")}
@@ -356,13 +356,13 @@ class UiColorPicker implements DialogCallbackObject { this.oldColor!.style.backgroundColor = 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/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..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 @@ -9,7 +9,9 @@ */ 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\DatabaseTableForeignKey; use wcf\system\database\table\PartialDatabaseTable; return [ @@ -23,4 +25,17 @@ MediumtextDatabaseTableColumn::create('conditions'), DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), ]), + PartialDatabaseTable::create('wcf1_trophy') + ->columns([ + MediumtextDatabaseTableColumn::create('conditions'), + DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), + IntDatabaseTableColumn::create('imageFileID'), + ]) + ->foreignKeys([ + DatabaseTableForeignKey::create() + ->columns(['imageFileID']) + ->referencedTable('wcf1_file') + ->referencedColumns(['fileID']) + ->onDelete('SET NULL'), + ]), ]; diff --git a/wcfsetup/install/files/acp/templates/trophyAdd.tpl b/wcfsetup/install/files/acp/templates/trophyAdd.tpl index 7188406b92a..2d5344d4d48 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/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..dbd109ea315 --- /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]); + } + } +} 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..df121cd7e4c --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Icon/Badge.js @@ -0,0 +1,31 @@ +/** + * 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.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); + }); + container.style.setProperty("color", colorInput.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); + } + } +}); 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..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")}
@@ -282,14 +282,12 @@ define(["require", "exports", "tslib", "../../Core", "../Dialog", "../../Dom/Uti const colorString = ColorUtil.rgbaToString(color); this.oldColor.style.backgroundColor = 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); } Dialog_1.default.close(this); if (typeof this.options.callbackSubmit === "function") { diff --git a/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php b/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php index 5ba22be8b67..ffe034803fe 100644 --- a/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/TrophyAddForm.class.php @@ -2,30 +2,41 @@ 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\FileProcessorFormField; +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 +53,122 @@ 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(); - - parent::readData(); - } + public $objectActionClass = TrophyAction::class; /** * @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']); - } + public $objectEditLinkController = TrophyEditForm::class; - 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([ + 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') + ->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]) + ), + 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/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/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..b0c57490107 100644 --- a/wcfsetup/install/files/lib/data/trophy/Trophy.class.php +++ b/wcfsetup/install/files/lib/data/trophy/Trophy.class.php @@ -2,17 +2,17 @@ namespace wcf\data\trophy; -use wcf\data\condition\Condition; -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\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; /** @@ -28,7 +28,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 @@ -37,8 +36,13 @@ * @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 + * @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. @@ -172,11 +176,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) : []; } /** @@ -190,4 +194,12 @@ public function getIcon(): ?FontAwesomeIcon return null; } + + /** + * @since 6.3 + */ + public function getFile(): ?File + { + return $this->getCollection()->getFile($this); + } } diff --git a/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php b/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php index b8f10222dff..be5585c8e9a 100644 --- a/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php +++ b/wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php @@ -3,18 +3,15 @@ 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\TI18nDatabaseObjectAction; 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,9 +25,10 @@ * * @extends AbstractDatabaseObjectAction */ -class TrophyAction extends AbstractDatabaseObjectAction implements IToggleAction, IUploadAction +class TrophyAction extends AbstractDatabaseObjectAction implements IToggleAction { use TDatabaseObjectToggle; + use TI18nDatabaseObjectAction; /** * @inheritDoc @@ -60,9 +58,7 @@ public function create() $trophy = parent::create(); - if (isset($this->parameters['tmpHash']) && $this->parameters['data']['type'] === Trophy::TYPE_IMAGE) { - $this->updateTrophyImage($trophy); - } + $this->saveI18nValue($trophy); $trophyEditor = new TrophyEditor($trophy); $trophyEditor->setShowOrder($showOrder); @@ -94,14 +90,21 @@ 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(); + $this->deleteI18nValues(); + UserStorageHandler::getInstance()->resetAll('specialTrophies'); return $returnValues; @@ -114,17 +117,13 @@ 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']); } + + foreach ($this->objects as $object) { + $this->saveI18nValue($object->getDecoratedObject()); + } } /** @@ -197,116 +196,24 @@ 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() + #[\Override] + public function getI18nSaveTypes(): array { - $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), + 'title' => 'wcf.user.trophy.title\d+', + 'description' => 'wcf.user.trophy.description\d+', ]; } - /** - * Updates style preview image. - * - * @return void - */ - protected function updateTrophyImage(Trophy $trophy) + #[\Override] + public function getLanguageCategory(): string { - if (!isset($this->parameters['tmpHash'])) { - return; - } + return 'wcf.user.trophy'; + } - $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); - } - } - } + #[\Override] + public function getPackageID(): int + { + return 1; } } diff --git a/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php b/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php index 23f96d7b9a5..119b52fec2f 100644 --- a/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php +++ b/wcfsetup/install/files/lib/data/trophy/TrophyCache.class.php @@ -2,7 +2,7 @@ namespace wcf\data\trophy; -use wcf\system\cache\builder\TrophyCacheBuilder; +use wcf\system\cache\eager\data\TrophyCacheData; use wcf\system\SingletonFactory; /** @@ -13,53 +13,34 @@ * @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; + private TrophyCollection $trophyCollection; /** * @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 = []; @@ -67,43 +48,35 @@ public function getTrophiesByID(array $trophyIDs) $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; } /** * 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 +95,9 @@ public function getEnabledTrophiesByCategoryID($categoryID) * * @return Trophy[] */ - public function getTrophies() + public function getTrophies(): array { - return $this->trophies; + return $this->trophyCache->trophies; } /** @@ -132,19 +105,16 @@ public function getTrophies() * * @return Trophy[] */ - public function getEnabledTrophies() + public function getEnabledTrophies(): array { - return $this->enabledTrophies; + return $this->trophyCache->enabledTrophies; } /** * 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/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/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/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/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/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; + } } 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/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..b29514a6c4e --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/TrophyCache.class.php @@ -0,0 +1,56 @@ + + * @since 6.3 + * + * @extends AbstractEagerCache + */ +final class TrophyCache extends AbstractEagerCache +{ + #[\Override] + protected function getCacheData(): TrophyCacheData + { + // `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; + } + + $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..ab31b9fe1f6 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/data/TrophyCacheData.class.php @@ -0,0 +1,36 @@ + + * @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] ?? []; + } +} 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/form/builder/container/condition/ConditionFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php index 9256854bf1f..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 @@ -31,6 +31,8 @@ final class ConditionFormContainer extends FormContainer * @phpstan-ignore missingType.generics */ protected AbstractConditionProvider $conditionProvider; + private bool $isRequired = false; + private bool $isEmpty = false; public function __construct() { @@ -38,6 +40,40 @@ public function __construct() $this->label("wcf.form.field.condition"); } + #[\Override] + public function validate() + { + parent::validate(); + + // `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 + { + if ($this->isRequired && $this->isEmpty) { + return true; + } + + return parent::hasValidationErrors(); + } + + #[\Override] protected static function getDefaultId(): string { @@ -165,4 +201,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->isEmpty; + } } 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 */ 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..4140b4eeb6f --- /dev/null +++ b/wcfsetup/install/files/lib/system/trophy/command/MigrateLegacyCondition.class.php @@ -0,0 +1,48 @@ + + * @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 $e) { + // Side-effect: Logs the exception. + $e->getExceptionID(); + + 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, + ]); + } +} 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..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,48 +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() - { - $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. * @@ -139,9 +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(); + $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 @@ -185,8 +144,10 @@ private function getRevocableUserTrophyIDs(Trophy $trophy, $maxTrophyIDs) } // Assign the condition to the pseudo DBOList object + $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. 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/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(); 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..eb01140958e --- /dev/null +++ b/wcfsetup/install/files/lib/system/worker/TrophyRebuildDataWorker.class.php @@ -0,0 +1,67 @@ + + * @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))(); + + $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/files/style/ui/colorPicker.scss b/wcfsetup/install/files/style/ui/colorPicker.scss index c490517c136..94c372131f8 100644 --- a/wcfsetup/install/files/style/ui/colorPicker.scss +++ b/wcfsetup/install/files/style/ui/colorPicker.scss @@ -18,21 +18,18 @@ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEX////MzMw46qqDAAAAD0lEQVQI12P4z4Ad4ZAAAH6/D/Hgw85/AAAAAElFTkSuQmCC); border: 1px solid rgba(0, 0, 0, 1); box-sizing: content-box; - display: block; + display: flex; min-height: 50px; - - > span { - display: block; - } } .colorPickerButton { - height: 32px; + border-radius: var(--wcfBorderRadius); + overflow: hidden; width: 50px; +} - > span { - height: 32px; - } +.colorPickerButton__color { + flex: 1 auto; } .colorPickerComparison { diff --git a/wcfsetup/install/files/style/ui/fontAwesome.scss b/wcfsetup/install/files/style/ui/fontAwesome.scss index 02ebb888fb0..1ddb2fa633c 100644 --- a/wcfsetup/install/files/style/ui/fontAwesome.scss +++ b/wcfsetup/install/files/style/ui/fontAwesome.scss @@ -33,3 +33,14 @@ } } } + +.iconBadge { + align-self: flex-start; + display: inline-flex; + border-radius: 50%; + + fa-icon { + transform: scale(0.5625); + width: var(--icon-size); + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index f9d817fdfb7..f30b25e4ebd 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 + + @@ -5192,7 +5194,7 @@ Sobald {if LANGUAGE_USE_INFORMAL_VARIANT}dein{else}Ihr{/if} Benutzerkonto freige - + @@ -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..e0371976f86 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 + + @@ -5191,7 +5193,7 @@ You also received a list of backup codes to use when your second factor becomes - + @@ -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..71abe873fb3 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1503,6 +1503,9 @@ 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, + imageFileID INT DEFAULT NULL, KEY(categoryID) ); @@ -2220,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;