diff --git a/com.woltlab.wcf/templates/userOptionFieldList.tpl b/com.woltlab.wcf/templates/userOptionFieldList.tpl index 423340d4f60..4af1dcf5bde 100644 --- a/com.woltlab.wcf/templates/userOptionFieldList.tpl +++ b/com.woltlab.wcf/templates/userOptionFieldList.tpl @@ -1,9 +1,9 @@ {foreach from=$options item=optionData} {assign var=option value=$optionData[object]}
- {if $isSearchMode|empty || !$optionData[hideLabelInSearch]}{if $isSearchMode|empty && $option->required} *{/if}{/if} + {if $isSearchMode|empty || !$optionData[hideLabelInSearch]}{if $isSearchMode|empty && $option->required} *{/if}{/if}
{unsafe:$optionData[html]} - {lang __optional=true}{$langPrefix}{$option->optionName}.description{/lang} + {$option->getDescription()} {if $errorType|is_array && $errorType[$option->optionName]|isset} diff --git a/com.woltlab.wcf/templates/userProfileOptionFieldList.tpl b/com.woltlab.wcf/templates/userProfileOptionFieldList.tpl index cb00c78c65c..6d654a1b5a2 100644 --- a/com.woltlab.wcf/templates/userProfileOptionFieldList.tpl +++ b/com.woltlab.wcf/templates/userProfileOptionFieldList.tpl @@ -6,7 +6,7 @@ {assign var=error value=''} {/if}
- +
{unsafe:$optionData[html]} {if $error} @@ -17,7 +17,7 @@ {/if} {/if} - {lang __optional=true}{$langPrefix}{$option->optionName}.description{/lang} + {$option->getDescription()}
{/foreach} diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_captchaQuestion.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_captchaQuestion.php new file mode 100644 index 00000000000..2c5ceb3880b --- /dev/null +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_captchaQuestion.php @@ -0,0 +1,21 @@ +columns([ + NotNullVarchar255DatabaseTableColumn::create('question')->drop(), + MediumtextDatabaseTableColumn::create('answers')->drop(), + ]), +]; 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 bf5144ac108..f2b1265591e 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 @@ -10,11 +10,18 @@ use wcf\system\database\table\column\CharDatabaseTableColumn; use wcf\system\database\table\column\DefaultFalseBooleanDatabaseTableColumn; +use wcf\system\database\table\column\IntDatabaseTableColumn; use wcf\system\database\table\column\JsonDatabaseTableColumn; use wcf\system\database\table\column\MediumintDatabaseTableColumn; +use wcf\system\database\table\column\MediumtextDatabaseTableColumn; +use wcf\system\database\table\column\NotNullInt10DatabaseTableColumn; use wcf\system\database\table\column\NotNullVarchar255DatabaseTableColumn; use wcf\system\database\table\column\SmallintDatabaseTableColumn; use wcf\system\database\table\column\TextDatabaseTableColumn; +use wcf\system\database\table\column\VarcharDatabaseTableColumn; +use wcf\system\database\table\DatabaseTable; +use wcf\system\database\table\index\DatabaseTableForeignKey; +use wcf\system\database\table\index\DatabaseTableIndex; use wcf\system\database\table\PartialDatabaseTable; return [ @@ -74,4 +81,30 @@ ->defaultValue('') ->drop(), ]), + DatabaseTable::create('wcf1_captcha_question_l10n') + ->columns([ + NotNullInt10DatabaseTableColumn::create('questionID'), + IntDatabaseTableColumn::create('languageID'), + VarcharDatabaseTableColumn::create('question') + ->length(255), + MediumtextDatabaseTableColumn::create('answers'), + ]) + ->indices([ + DatabaseTableIndex::create('questionID') + ->columns(['questionID', 'languageID']), + ]) + ->foreignKeys([ + DatabaseTableForeignKey::create() + ->columns(['questionID']) + ->referencedTable('wcf1_captcha_question') + ->referencedColumns(['questionID']) + ->onDelete('CASCADE') + ->onUpdate('NO ACTION'), + DatabaseTableForeignKey::create() + ->columns(['languageID']) + ->referencedTable('wcf1_language') + ->referencedColumns(['languageID']) + ->onDelete('CASCADE') + ->onUpdate('NO ACTION'), + ]), ]; diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_userOption.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_userOption.php new file mode 100644 index 00000000000..4f59eea6b70 --- /dev/null +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_userOption.php @@ -0,0 +1,57 @@ +columns([ + VarcharDatabaseTableColumn::create('l10nIdentifier') + ->length(255), + ]), + DatabaseTable::create('wcf1_user_option_l10n') + ->columns([ + NotNullInt10DatabaseTableColumn::create('optionID'), + IntDatabaseTableColumn::create('languageID'), + VarcharDatabaseTableColumn::create('title') + ->length(255), + MediumtextDatabaseTableColumn::create('description'), + TinyintDatabaseTableColumn::create('isPristine') + ->notNull() + ->defaultValue(1), + ]) + ->indices([ + DatabaseTableIndex::create('optionID') + ->columns(['optionID', 'languageID']), + ]) + ->foreignKeys([ + DatabaseTableForeignKey::create() + ->columns(['optionID']) + ->referencedTable('wcf1_user_option') + ->referencedColumns(['optionID']) + ->onDelete('CASCADE') + ->onUpdate('NO ACTION'), + DatabaseTableForeignKey::create() + ->columns(['languageID']) + ->referencedTable('wcf1_language') + ->referencedColumns(['languageID']) + ->onDelete('CASCADE') + ->onUpdate('NO ACTION'), + ]), +]; diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_captchaQuestionL10n.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_captchaQuestionL10n.php new file mode 100644 index 00000000000..8f4e842459f --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_captchaQuestionL10n.php @@ -0,0 +1,54 @@ +prepare("DELETE FROM wcf1_captcha_question_l10n")->execute(); + +L10nLanguageItemSync::migrate( + CaptchaQuestion::getL10nDefinition(), + static function (array $row): array { + $questionIsPhrase = (bool)\preg_match( + '~^wcf\.captcha\.question\.question\.question\d+$~', + $row['question'] + ); + $answersIsPhrase = $row['answers'] !== null && (bool)\preg_match( + '~^wcf\.captcha\.question\.answers\.question\d+$~', + $row['answers'] + ); + + return [ + 'sources' => [ + 'question' => new L10nLanguageItemSource( + languageItem: $questionIsPhrase ? $row['question'] : null, + literal: $row['question'], + deleteAfterMigration: true, + ), + 'answers' => new L10nLanguageItemSource( + languageItem: $answersIsPhrase ? $row['answers'] : null, + literal: $row['answers'], + deleteAfterMigration: true, + ), + ], + ]; + } +); + +// Cached question objects were created without their localized values. +CaptchaQuestionCacheBuilder::getInstance()->reset(); diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_userOptionL10n.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_userOptionL10n.php new file mode 100644 index 00000000000..7816e70bf12 --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_userOptionL10n.php @@ -0,0 +1,67 @@ +[.description]` language variables into the + * `wcf1_user_option_l10n` table. + * + * System options (shipped by a package) are linked to their language variable + * via `l10nIdentifier`; their localized values are stored as pristine copies + * and kept in sync with the phrases. Options created by an administrator + * (`option`) own their localized value: they stay unlinked and their + * obsolete phrases are removed. + * + * IMPORTANT ordering constraint for package.xml: The database script + * `acp/database/update_com.woltlab.wcf_6.3_userOption.php` (adding the + * `l10nIdentifier` column and creating the `wcf1_user_option_l10n` table) must + * run BEFORE this script. + */ + +use wcf\data\user\option\UserOption; +use wcf\system\cache\builder\UserOptionCacheBuilder; +use wcf\system\l10n\L10nLanguageItemSource; +use wcf\system\l10n\L10nLanguageItemSync; +use wcf\system\WCF; + +$isAdminCreated = static fn(string $optionName): bool => (bool)\preg_match('/^option\d+$/', $optionName); + +// This script owns the table's content at this point (idempotency on re-runs). +WCF::getDB()->prepare("DELETE FROM wcf1_user_option_l10n")->execute(); + +// Link system options to their language variable; administrator created +// options own their localized value and stay unlinked. +$statement = WCF::getDB()->prepare("SELECT optionID, optionName FROM wcf1_user_option"); +$statement->execute(); +$updateStatement = WCF::getDB()->prepare( + "UPDATE wcf1_user_option SET l10nIdentifier = ? WHERE optionID = ?" +); +while ($row = $statement->fetchArray()) { + $updateStatement->execute([ + $isAdminCreated($row['optionName']) ? null : 'wcf.user.option.' . $row['optionName'], + $row['optionID'], + ]); +} + +// Migrate the phrase values into the l10n storage. +L10nLanguageItemSync::migrate( + UserOption::getL10nDefinition(), + static function (array $row) use ($isAdminCreated): array { + $adminCreated = $isAdminCreated($row['optionName']); + $identifier = 'wcf.user.option.' . $row['optionName']; + + return [ + 'sources' => [ + 'title' => new L10nLanguageItemSource( + languageItem: $identifier, + deleteAfterMigration: $adminCreated, + ), + 'description' => new L10nLanguageItemSource( + languageItem: $identifier . '.description', + deleteAfterMigration: $adminCreated, + ), + ], + ]; + } +); + +UserOptionCacheBuilder::getInstance()->reset(); diff --git a/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php b/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php index cb871b98d69..3bc031fe303 100644 --- a/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php @@ -2,12 +2,16 @@ namespace wcf\acp\form; +use wcf\command\captcha\question\CreateCaptchaQuestion; +use wcf\command\captcha\question\UpdateCaptchaQuestion; use wcf\data\captcha\question\CaptchaQuestion; -use wcf\data\captcha\question\CaptchaQuestionAction; +use wcf\data\captcha\question\CaptchaQuestionBuilder; +use wcf\data\DatabaseObjectBuilder; use wcf\data\language\Language; -use wcf\form\AbstractFormBuilderForm; +use wcf\form\AbstractDatabaseObjectBuilderForm; use wcf\system\form\builder\container\FormContainer; use wcf\system\form\builder\field\BooleanFormField; +use wcf\system\form\builder\field\IFormField; use wcf\system\form\builder\field\MultilineTextFormField; use wcf\system\form\builder\field\TextFormField; use wcf\system\form\builder\field\validation\FormFieldValidationError; @@ -19,12 +23,12 @@ * Shows the form to create a new captcha question. * * @author Olaf Braun, Matthias Schmidt - * @copyright 2001-2024 WoltLab GmbH + * @copyright 2001-2026 WoltLab GmbH * @license GNU Lesser General Public License * - * @extends AbstractFormBuilderForm + * @extends AbstractDatabaseObjectBuilderForm */ -class CaptchaQuestionAddForm extends AbstractFormBuilderForm +class CaptchaQuestionAddForm extends AbstractDatabaseObjectBuilderForm { /** * @inheritDoc @@ -39,30 +43,48 @@ class CaptchaQuestionAddForm extends AbstractFormBuilderForm /** * @inheritDoc */ - public $objectActionClass = CaptchaQuestionAction::class; + public string $objectEditLinkController = CaptchaQuestionEditForm::class; - /** - * @inheritDoc - */ - public $objectEditLinkController = CaptchaQuestionEditForm::class; + #[\Override] + protected function getDatabaseObjectBuilder(): CaptchaQuestionBuilder + { + if ($this->formObject !== null) { + return CaptchaQuestionBuilder::forUpdate($this->formObject); + } + + return CaptchaQuestionBuilder::forCreate(); + } #[\Override] - protected function createForm() + protected function getCommand(DatabaseObjectBuilder $builder): callable { - parent::createForm(); + if ($this->formObject !== null) { + return new UpdateCaptchaQuestion($builder); + } + + return new CreateCaptchaQuestion($builder); + } + #[\Override] + protected function createForm(): void + { $this->form->appendChildren([ FormContainer::create('general') ->appendChildren([ TextFormField::create('question') ->label('wcf.acp.captcha.question.question') - ->i18n() - ->languageItemPattern('wcf.captcha.question.question.question\d+') - ->required(), + ->l10n() + ->required() + ->maximumLength(255) + ->saveValueCallback(static function (CaptchaQuestionBuilder $builder, TextFormField $field) { + $builder->setQuestion($field->getL10nValues()); + }) + ->loadValueCallback(static function (CaptchaQuestion $object, IFormField $field) { + $field->value($object->getL10nValues('question')); + }), MultilineTextFormField::create('answers') ->label('wcf.acp.captcha.question.answers') - ->i18n() - ->languageItemPattern('wcf.captcha.question.answers.question\d+') + ->l10n() ->required() ->addValidator( new FormFieldValidator('regexValidator', function (MultilineTextFormField $formField) { @@ -80,10 +102,22 @@ protected function createForm() } } }) - ), + ) + ->saveValueCallback(static function (CaptchaQuestionBuilder $builder, MultilineTextFormField $field) { + $builder->setAnswers($field->getL10nValues()); + }) + ->loadValueCallback(static function (CaptchaQuestion $object, IFormField $field) { + $field->value($object->getL10nValues('answers')); + }), BooleanFormField::create('isDisabled') ->label('wcf.acp.captcha.question.isDisabled') ->value(false) + ->saveValueCallback(static function (CaptchaQuestionBuilder $builder, IFormField $field) { + $builder->setIsDisabled((bool)$field->getSaveValue()); + }) + ->loadValueCallback(static function (CaptchaQuestion $object, IFormField $field) { + $field->value($object->isDisabled); + }), ]) ]); } diff --git a/wcfsetup/install/files/lib/acp/form/CaptchaQuestionEditForm.class.php b/wcfsetup/install/files/lib/acp/form/CaptchaQuestionEditForm.class.php index b81d2afb936..12261d48e50 100644 --- a/wcfsetup/install/files/lib/acp/form/CaptchaQuestionEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/CaptchaQuestionEditForm.class.php @@ -29,7 +29,7 @@ class CaptchaQuestionEditForm extends CaptchaQuestionAddForm /** * @inheritDoc */ - public $formAction = 'edit'; + public string $formAction = 'edit'; #[\Override] public function readParameters() diff --git a/wcfsetup/install/files/lib/acp/form/UserOptionAddForm.class.php b/wcfsetup/install/files/lib/acp/form/UserOptionAddForm.class.php index a4ba4e372e1..2c498b99906 100644 --- a/wcfsetup/install/files/lib/acp/form/UserOptionAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserOptionAddForm.class.php @@ -3,19 +3,20 @@ namespace wcf\acp\form; use Laminas\Diactoros\Response\HtmlResponse; -use wcf\data\IStorableObject; +use wcf\command\user\option\CreateUserOption; +use wcf\command\user\option\UpdateUserOption; +use wcf\data\DatabaseObjectBuilder; use wcf\data\user\option\category\UserOptionCategory; use wcf\data\user\option\category\UserOptionCategoryList; use wcf\data\user\option\UserOption; -use wcf\data\user\option\UserOptionAction; -use wcf\data\user\option\UserOptionEditor; -use wcf\form\AbstractFormBuilderForm; +use wcf\data\user\option\UserOptionBuilder; +use wcf\form\AbstractDatabaseObjectBuilderForm; use wcf\http\error\HtmlErrorRenderer; use wcf\system\form\builder\container\FormContainer; -use wcf\system\form\builder\data\processor\CustomFormDataProcessor; use wcf\system\form\builder\field\BooleanFormField; use wcf\system\form\builder\field\ClassNameFormField; use wcf\system\form\builder\field\dependency\ValueFormFieldDependency; +use wcf\system\form\builder\field\IFormField; use wcf\system\form\builder\field\IntegerFormField; use wcf\system\form\builder\field\ItemListFormField; use wcf\system\form\builder\field\MultilineItemListFormField; @@ -24,8 +25,6 @@ use wcf\system\form\builder\field\TextFormField; use wcf\system\form\builder\field\validation\FormFieldValidationError; use wcf\system\form\builder\field\validation\FormFieldValidator; -use wcf\system\form\builder\IFormDocument; -use wcf\system\language\I18nHandler; use wcf\system\option\user\DateUserOptionOutput; use wcf\system\option\user\IUserOptionOutput; use wcf\system\option\user\LabeledUrlUserOptionOutput; @@ -33,7 +32,6 @@ use wcf\system\option\user\SelectOptionsUserOptionOutput; use wcf\system\option\user\URLUserOptionOutput; use wcf\system\WCF; -use wcf\util\StringUtil; /** * Shows the user option add form. @@ -42,9 +40,9 @@ * @copyright 2001-2024 WoltLab GmbH * @license GNU Lesser General Public License * - * @extends AbstractFormBuilderForm + * @extends AbstractDatabaseObjectBuilderForm */ -class UserOptionAddForm extends AbstractFormBuilderForm +class UserOptionAddForm extends AbstractDatabaseObjectBuilderForm { /** * @inheritDoc @@ -97,12 +95,7 @@ class UserOptionAddForm extends AbstractFormBuilderForm /** * @inheritDoc */ - public $objectActionClass = UserOptionAction::class; - - /** - * @inheritDoc - */ - public $objectEditLinkController = UserOptionEditForm::class; + public string $objectEditLinkController = UserOptionEditForm::class; #[\Override] public function readParameters() @@ -131,26 +124,62 @@ public function readParameters() } #[\Override] - public function createForm() + protected function getDatabaseObjectBuilder(): UserOptionBuilder + { + if ($this->formObject !== null) { + return UserOptionBuilder::forUpdate($this->formObject); + } + + return UserOptionBuilder::forCreate()->setGenericOptionName(); + } + + #[\Override] + protected function getCommand(DatabaseObjectBuilder $builder): callable { - parent::createForm(); + if ($this->formObject !== null) { + return new UpdateUserOption($builder); + } + + return new CreateUserOption($builder); + } + #[\Override] + protected function createForm(): void + { + $formAction = $this->formAction; $this->form->appendChildren([ FormContainer::create('general') ->appendChildren([ + // The localized title and description are stored in the + // `wcf1_user_option_l10n` table via the builder, not in + // columns of `wcf1_user_option`. TextFormField::create('optionName') ->label('wcf.global.name') ->required() - ->i18n() - ->i18nRequired() - ->languageItemPattern('wcf.user.option.(option\d+|\w+)'), + ->l10n() + ->saveValueCallback(static function (UserOptionBuilder $builder, TextFormField $field) { + $builder->setL10nTitle($field->getL10nValues()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->getL10nValues('title')); + }), MultilineTextFormField::create('optionDescription') ->label('wcf.acp.user.option.description') - ->i18n() - ->i18nRequired() - ->languageItemPattern('wcf.user.option.(option\d+|\w+).description'), + ->l10n() + ->saveValueCallback(static function (UserOptionBuilder $builder, MultilineTextFormField $field) { + $builder->setL10nDescription($field->getL10nValues()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->getL10nValues('description')); + }), BooleanFormField::create('isDisabled') - ->label('wcf.global.button.disable'), + ->label('wcf.global.button.disable') + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setIsDisabled((bool)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->isDisabled); + }), SingleSelectionFormField::create('categoryName') ->label('wcf.global.category') ->required() @@ -161,10 +190,22 @@ public function createForm() } return $options; + }) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setCategoryName((string)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->categoryName); }), IntegerFormField::create('showOrder') ->label('wcf.form.field.showOrder') ->value(0) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setShowOrder((int)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->showOrder); + }), ]), FormContainer::create('typeDataContainer') ->label('wcf.acp.user.option.typeData') @@ -175,16 +216,43 @@ public function createForm() ->required() ->immutable($this->formAction !== 'create') ->options(\array_combine(self::$availableOptionTypes, self::$availableOptionTypes)) - ->value('text'), + ->value('text') + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setOptionType((string)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->optionType); + }), TextFormField::create('defaultValue') ->label('wcf.acp.user.option.defaultValue') ->description('wcf.acp.user.option.defaultValue.description') - ->addFieldClass('long'), + ->addFieldClass('long') + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->defaultValue ?? ''); + }) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + // type-cast the default value + $defaultValue = $field->getValue(); + $builder->setDefaultValue( + match ($field->getDocument()->getFormField('optionType')->getValue()) { + 'boolean', 'integer' => \intval($defaultValue), + 'float' => \floatval($defaultValue), + 'date' => \preg_match('/\d{4}-\d{2}-\d{2}/', (string)$defaultValue) ? $defaultValue : '', + default => $defaultValue, + } + ); + }), MultilineItemListFormField::create('selectOptions') ->label('wcf.acp.user.option.selectOptions') ->description('wcf.acp.user.option.selectOptions.description') ->required() ->saveValueType(ItemListFormField::SAVE_VALUE_TYPE_NSV) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setSelectOptions((string)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->selectOptions ?? ''); + }) ->addDependency( ValueFormFieldDependency::create('optionType') ->fieldId('optionType') @@ -207,6 +275,12 @@ public function createForm() } }) ) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setLabeledUrl((string)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->labeledUrl ?? ''); + }) ->addDependency( ValueFormFieldDependency::create('optionType') ->fieldId('optionType') @@ -216,6 +290,28 @@ public function createForm() ->label('wcf.acp.user.option.outputClass') ->description('wcf.acp.user.option.outputClass.description') ->implementedInterface(IUserOptionOutput::class) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) use ($formAction) { + // handle auto-assign of the output class on create + $outputClass = $field->getValue(); + $optionType = $field->getDocument()->getFormField('optionType')->getValue(); + if ($formAction === 'create' && $outputClass === '') { + if (\in_array($optionType, self::$optionTypesUsingSelectOptions)) { + $outputClass = SelectOptionsUserOptionOutput::class; + } else { + $outputClass = match ($optionType) { + 'date' => DateUserOptionOutput::class, + 'URL' => URLUserOptionOutput::class, + 'labeledUrl' => LabeledUrlUserOptionOutput::class, + 'message' => MessageUserOptionOutput::class, + default => '' + }; + } + } + $builder->setOutputClass($outputClass); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->outputClass); + }), ]), FormContainer::create('access') ->label('wcf.acp.user.option.access') @@ -228,7 +324,13 @@ public function createForm() 3 => 'wcf.acp.user.option.editable.3', 6 => 'wcf.acp.user.option.editable.6', ]) - ->value(3), + ->value(3) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setEditable((int)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->editable); + }), SingleSelectionFormField::create('visible') ->label('wcf.acp.user.option.visible') ->options([ @@ -239,10 +341,22 @@ public function createForm() 7 => 'wcf.acp.user.option.visible.7', 15 => 'wcf.acp.user.option.visible.15', ]) - ->value(15), + ->value(15) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setVisible((int)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->visible); + }), TextFormField::create('validationPattern') ->label('wcf.acp.user.option.validationPattern') ->description('wcf.acp.user.option.validationPattern.description') + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setValidationPattern((string)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->validationPattern ?? ''); + }) ->addDependency( ValueFormFieldDependency::create('validationPatternOptionTypeDependency') ->fieldId('optionType') @@ -251,146 +365,73 @@ public function createForm() ), BooleanFormField::create('required') ->label('wcf.acp.user.option.required') - ->value(false), + ->value(false) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setRequired((bool)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->required); + }), BooleanFormField::create('askDuringRegistration') ->label('wcf.acp.user.option.askDuringRegistration') - ->value(false), + ->value(false) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setAskDuringRegistration((bool)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->askDuringRegistration); + }), BooleanFormField::create('searchable') ->label('wcf.acp.user.option.searchable') - ->value(false), + ->value(false) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setSearchable((bool)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->searchable); + }), BooleanFormField::create('showOnUserCard') ->label('wcf.acp.user.option.showOnUserCard') - ->value(false), - ]) + ->value(false) + ->saveValueCallback(static function (UserOptionBuilder $builder, IFormField $field) { + $builder->setShowOnUserCard((bool)$field->getSaveValue()); + }) + ->loadValueCallback(static function (UserOption $object, IFormField $field) { + $field->value($object->showOnUserCard); + }), + ]), ]); } #[\Override] - protected function finalizeForm() - { - parent::finalizeForm(); - - $this->form->getDataHandler() - ->addProcessor( - new CustomFormDataProcessor( - 'optionNameDataProcessor', - function (IFormDocument $document, array $parameters) { - // These values are unconditionally stored in phrases and - // never in actual columns as it is usually the case with - // the `I18nHandler`. - unset($parameters['data']['optionName']); - unset($parameters['data']['optionDescription']); - - return $parameters; - }, - function (IFormDocument $document, array $data, IStorableObject $object) { - \assert($object instanceof UserOption); - $data['optionName'] = 'wcf.user.option.' . $object->optionName; - $data['optionDescription'] = 'wcf.user.option.' . $object->optionName . '.description'; - - return $data; - } - ), - ) - ->addProcessor( - new CustomFormDataProcessor( - 'additionDataProcessor', - function (IFormDocument $document, array $parameters) { - $additionalData = $this->formObject?->additionalData ?: []; - - if ($parameters['data']['optionType'] == 'select') { - $additionalData['allowEmptyValue'] = true; - } elseif ($parameters['data']['optionType'] == 'message') { - $additionalData['messageObjectType'] = 'com.woltlab.wcf.user.option.generic'; - } - - $parameters['data']['additionalData'] = \serialize($additionalData); - - return $parameters; - } - ) - ) - ->addProcessor( - new CustomFormDataProcessor( - 'outputClassDataProcessor', - function (IFormDocument $document, array $parameters) { - if ($this->formAction !== 'create') { - return $parameters; - } - - $outputClass = $parameters['data']['outputClass']; - $optionType = $parameters['data']['optionType']; - - if (empty($outputClass)) { - if (\in_array($optionType, self::$optionTypesUsingSelectOptions)) { - $parameters['data']['outputClass'] = SelectOptionsUserOptionOutput::class; - } else { - $parameters['data']['outputClass'] = match ($optionType) { - 'date' => DateUserOptionOutput::class, - 'URL' => URLUserOptionOutput::class, - 'labeledUrl' => LabeledUrlUserOptionOutput::class, - 'message' => MessageUserOptionOutput::class, - default => '' - }; - } - } - - return $parameters; - } - ) - ) - ->addProcessor( - new CustomFormDataProcessor( - 'defaultValueDataProcessor', - function (IFormDocument $document, array $parameters) { - $optionType = $parameters['data']['optionType']; - $defaultValue = $parameters['data']['defaultValue']; - - $parameters['data']['defaultValue'] = match ($optionType) { - 'boolean', 'integer' => \intval($defaultValue), - 'float' => \floatval($defaultValue), - 'date' => \preg_match('/\d{4}-\d{2}-\d{2}/', $defaultValue) ? $defaultValue : '', - default => $defaultValue, - }; - - return $parameters; - } - ) - ); - } - - #[\Override] - public function save() + public function save(): void { if ($this->formAction === 'create') { - $this->additionalFields['optionName'] = StringUtil::getRandomID(); $this->additionalFields['packageID'] = \PACKAGE_ID; } + $optionType = (string)$this->getFieldValue('optionType'); + + // additionalData + $additionalData = $this->formObject?->additionalData ?: []; + if ($optionType === 'select') { + $additionalData['allowEmptyValue'] = true; + } elseif ($optionType === 'message') { + $additionalData['messageObjectType'] = 'com.woltlab.wcf.user.option.generic'; + } + $this->additionalFields['additionalData'] = \serialize($additionalData); + parent::save(); } - #[\Override] - public function saved() + /** + * Returns the current value of the form field with the given id. + */ + private function getFieldValue(string $id): mixed { - $userOption = $this->objectAction->getReturnValues()['returnValues']; - \assert($userOption instanceof UserOption); - - I18nHandler::getInstance()->save( - 'optionName', - 'wcf.user.option.option' . $userOption->optionID, - 'wcf.user.option' - ); - I18nHandler::getInstance()->save( - 'optionDescription', - 'wcf.user.option.option' . $userOption->optionID . '.description', - 'wcf.user.option' - ); - $editor = new UserOptionEditor($userOption); - $editor->update([ - 'optionName' => 'option' . $userOption->optionID, - ]); + $node = $this->form->getNodeById($id); + \assert($node instanceof IFormField); - parent::saved(); + return $node->getValue(); } } diff --git a/wcfsetup/install/files/lib/acp/form/UserOptionEditForm.class.php b/wcfsetup/install/files/lib/acp/form/UserOptionEditForm.class.php index 3f199f28e6f..bf50f74e90b 100644 --- a/wcfsetup/install/files/lib/acp/form/UserOptionEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserOptionEditForm.class.php @@ -2,16 +2,12 @@ namespace wcf\acp\form; -use CuyZ\Valinor\Mapper\MappingError; use wcf\acp\page\UserOptionListPage; use wcf\data\user\option\UserOption; -use wcf\form\AbstractFormBuilderForm; use wcf\http\Helper; -use wcf\system\exception\IllegalLinkException; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\interaction\admin\UserOptionInteractions; use wcf\system\interaction\StandaloneInteractionContextMenuComponent; -use wcf\system\language\I18nHandler; use wcf\system\request\LinkHandler; use wcf\system\WCF; @@ -32,35 +28,18 @@ class UserOptionEditForm extends UserOptionAddForm /** * @inheritDoc */ - public $formAction = 'edit'; + public string $formAction = 'edit'; #[\Override] public function readParameters() { parent::readParameters(); - try { - $queryParameters = Helper::mapQueryParameters( - $_GET, - <<<'EOT' - array { - id: positive-int - } - EOT - ); - } catch (MappingError) { - throw new IllegalLinkException(); - } - - $this->formObject = new UserOption($queryParameters['id']); - - if (!$this->formObject->getObjectID()) { - throw new IllegalLinkException(); - } + $this->formObject = Helper::fetchObjectFromQueryParameter(UserOption::class); } #[\Override] - public function createForm() + protected function createForm(): void { parent::createForm(); @@ -75,23 +54,6 @@ public function createForm() } } - #[\Override] - public function saved() - { - I18nHandler::getInstance()->save( - 'optionName', - 'wcf.user.option.' . $this->formObject->optionName, - 'wcf.user.option' - ); - I18nHandler::getInstance()->save( - 'optionDescription', - 'wcf.user.option.' . $this->formObject->optionName . '.description', - 'wcf.user.option' - ); - - AbstractFormBuilderForm::saved(); - } - #[\Override] public function assignVariables() { diff --git a/wcfsetup/install/files/lib/acp/page/UserListPage.class.php b/wcfsetup/install/files/lib/acp/page/UserListPage.class.php index a343776ee3f..515c71000f8 100755 --- a/wcfsetup/install/files/lib/acp/page/UserListPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/UserListPage.class.php @@ -419,7 +419,11 @@ protected function readColumnsHeads() } if (isset($this->options[$column]) && $column != 'email') { - $this->columnHeads[$column] = 'wcf.user.option.' . $column; + // system options keep their `wcf.user.option.*` phrase, options + // created by an administrator only have their localized title in + // the l10n storage + $this->columnHeads[$column] = $this->options[$column]->l10nIdentifier + ?? $this->options[$column]->getTitle(); } else { $this->columnHeads[$column] = 'wcf.user.' . $column; } diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 4efb5e9c5cd..25271d8aff0 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -68,6 +68,13 @@ static function () { } } ); + + $eventHandler->register( + \wcf\event\l10n\L10nDefinitionCollecting::class, + static function (\wcf\event\l10n\L10nDefinitionCollecting $event) { + $event->register(\wcf\data\user\option\UserOption::getL10nDefinition()); + } + ); $eventHandler->register( \wcf\event\language\LanguageImported::class, static function (\wcf\event\language\LanguageImported $event) { diff --git a/wcfsetup/install/files/lib/command/captcha/question/CreateCaptchaQuestion.class.php b/wcfsetup/install/files/lib/command/captcha/question/CreateCaptchaQuestion.class.php new file mode 100644 index 00000000000..bc953c05adc --- /dev/null +++ b/wcfsetup/install/files/lib/command/captcha/question/CreateCaptchaQuestion.class.php @@ -0,0 +1,38 @@ + + * @since 6.3 + */ +final class CreateCaptchaQuestion +{ + public function __construct( + private readonly CaptchaQuestionBuilder $builder, + ) {} + + public function __invoke(): CaptchaQuestion + { + $question = $this->builder->create(); + + CaptchaQuestionCacheBuilder::getInstance()->reset(); + + EventHandler::getInstance()->fire(new CaptchaQuestionCreated( + $question, + $this->builder + )); + + return $question; + } +} diff --git a/wcfsetup/install/files/lib/command/captcha/question/UpdateCaptchaQuestion.class.php b/wcfsetup/install/files/lib/command/captcha/question/UpdateCaptchaQuestion.class.php new file mode 100644 index 00000000000..a6b64580411 --- /dev/null +++ b/wcfsetup/install/files/lib/command/captcha/question/UpdateCaptchaQuestion.class.php @@ -0,0 +1,38 @@ + + * @since 6.3 + */ +final class UpdateCaptchaQuestion +{ + public function __construct( + private readonly CaptchaQuestionBuilder $builder, + ) {} + + public function __invoke(): CaptchaQuestion + { + $question = $this->builder->update(); + + CaptchaQuestionCacheBuilder::getInstance()->reset(); + + EventHandler::getInstance()->fire(new CaptchaQuestionUpdated( + $question, + $this->builder + )); + + return $question; + } +} diff --git a/wcfsetup/install/files/lib/command/l10n/SyncL10nLanguageItems.class.php b/wcfsetup/install/files/lib/command/l10n/SyncL10nLanguageItems.class.php new file mode 100644 index 00000000000..839c37fbecf --- /dev/null +++ b/wcfsetup/install/files/lib/command/l10n/SyncL10nLanguageItems.class.php @@ -0,0 +1,31 @@ + + * @since 6.3 + */ +final class SyncL10nLanguageItems +{ + public function __invoke(): void + { + $event = new L10nDefinitionCollecting(); + EventHandler::getInstance()->fire($event); + + foreach ($event->getDefinitions() as $definition) { + L10nLanguageItemSync::sync($definition); + } + } +} diff --git a/wcfsetup/install/files/lib/command/user/option/CreateUserOption.class.php b/wcfsetup/install/files/lib/command/user/option/CreateUserOption.class.php new file mode 100644 index 00000000000..e15e95c7340 --- /dev/null +++ b/wcfsetup/install/files/lib/command/user/option/CreateUserOption.class.php @@ -0,0 +1,38 @@ + + * @since 6.3 + */ +final class CreateUserOption +{ + public function __construct( + private readonly UserOptionBuilder $builder, + ) {} + + public function __invoke(): UserOption + { + $option = $this->builder->create(); + + UserOptionCacheBuilder::getInstance()->reset(); + + EventHandler::getInstance()->fire(new UserOptionCreated( + $option, + $this->builder + )); + + return $option; + } +} diff --git a/wcfsetup/install/files/lib/command/user/option/DeleteOption.class.php b/wcfsetup/install/files/lib/command/user/option/DeleteOption.class.php index 4ba83f75a6e..96fbfcc5204 100644 --- a/wcfsetup/install/files/lib/command/user/option/DeleteOption.class.php +++ b/wcfsetup/install/files/lib/command/user/option/DeleteOption.class.php @@ -3,7 +3,10 @@ namespace wcf\command\user\option; use wcf\data\user\option\UserOption; -use wcf\data\user\option\UserOptionAction; +use wcf\data\user\option\UserOptionBuilder; +use wcf\event\user\option\UserOptionDeleted; +use wcf\system\cache\builder\UserOptionCacheBuilder; +use wcf\system\event\EventHandler; /** * Deletes a user option. @@ -21,7 +24,10 @@ public function __construct( public function __invoke(): void { - $action = new UserOptionAction([$this->option], 'delete'); - $action->executeAction(); + UserOptionBuilder::delete($this->option); + + UserOptionCacheBuilder::getInstance()->reset(); + + EventHandler::getInstance()->fire(new UserOptionDeleted($this->option)); } } diff --git a/wcfsetup/install/files/lib/command/user/option/DisableOption.class.php b/wcfsetup/install/files/lib/command/user/option/DisableOption.class.php index 6a2d48e3f8b..dfeaeafbedb 100644 --- a/wcfsetup/install/files/lib/command/user/option/DisableOption.class.php +++ b/wcfsetup/install/files/lib/command/user/option/DisableOption.class.php @@ -3,7 +3,10 @@ namespace wcf\command\user\option; use wcf\data\user\option\UserOption; -use wcf\data\user\option\UserOptionEditor; +use wcf\data\user\option\UserOptionBuilder; +use wcf\event\user\option\UserOptionDisabled; +use wcf\system\cache\builder\UserOptionCacheBuilder; +use wcf\system\event\EventHandler; /** * Disables a user option. @@ -21,8 +24,12 @@ public function __construct( public function __invoke(): void { - (new UserOptionEditor($this->option))->update([ - 'isDisabled' => 1, - ]); + UserOptionBuilder::forUpdate($this->option) + ->setIsDisabled(true) + ->update(); + + UserOptionCacheBuilder::getInstance()->reset(); + + EventHandler::getInstance()->fire(new UserOptionDisabled($this->option)); } } diff --git a/wcfsetup/install/files/lib/command/user/option/EnableOption.class.php b/wcfsetup/install/files/lib/command/user/option/EnableOption.class.php index 6431f39c044..425c27ff7cd 100644 --- a/wcfsetup/install/files/lib/command/user/option/EnableOption.class.php +++ b/wcfsetup/install/files/lib/command/user/option/EnableOption.class.php @@ -3,7 +3,10 @@ namespace wcf\command\user\option; use wcf\data\user\option\UserOption; -use wcf\data\user\option\UserOptionEditor; +use wcf\data\user\option\UserOptionBuilder; +use wcf\event\user\option\UserOptionEnabled; +use wcf\system\cache\builder\UserOptionCacheBuilder; +use wcf\system\event\EventHandler; /** * Enables a user option. @@ -21,8 +24,12 @@ public function __construct( public function __invoke(): void { - (new UserOptionEditor($this->option))->update([ - 'isDisabled' => 0, - ]); + UserOptionBuilder::forUpdate($this->option) + ->setIsDisabled(false) + ->update(); + + UserOptionCacheBuilder::getInstance()->reset(); + + EventHandler::getInstance()->fire(new UserOptionEnabled($this->option)); } } diff --git a/wcfsetup/install/files/lib/command/user/option/UpdateUserOption.class.php b/wcfsetup/install/files/lib/command/user/option/UpdateUserOption.class.php new file mode 100644 index 00000000000..bdd1339a305 --- /dev/null +++ b/wcfsetup/install/files/lib/command/user/option/UpdateUserOption.class.php @@ -0,0 +1,38 @@ + + * @since 6.3 + */ +final class UpdateUserOption +{ + public function __construct( + private readonly UserOptionBuilder $builder, + ) {} + + public function __invoke(): UserOption + { + $option = $this->builder->update(); + + UserOptionCacheBuilder::getInstance()->reset(); + + EventHandler::getInstance()->fire(new UserOptionUpdated( + $option, + $this->builder + )); + + return $option; + } +} diff --git a/wcfsetup/install/files/lib/data/DatabaseObjectCollection.class.php b/wcfsetup/install/files/lib/data/DatabaseObjectCollection.class.php index f919cece03a..fe07c2c8318 100644 --- a/wcfsetup/install/files/lib/data/DatabaseObjectCollection.class.php +++ b/wcfsetup/install/files/lib/data/DatabaseObjectCollection.class.php @@ -17,10 +17,15 @@ abstract class DatabaseObjectCollection /** * @param TDatabaseObject[] $objects */ - public function __construct(protected readonly array $objects) {} + public function __construct(protected readonly array $objects) + { + if ($objects === []) { + throw new \InvalidArgumentException("A collection must contain at least one object."); + } + } /** - * @return int[] + * @return non-empty-array */ public function getObjectIDs(): array { @@ -28,7 +33,7 @@ public function getObjectIDs(): array } /** - * @return TDatabaseObject[] + * @return non-empty-array */ public function getObjects(): array { diff --git a/wcfsetup/install/files/lib/data/TCollectionL10n.class.php b/wcfsetup/install/files/lib/data/TCollectionL10n.class.php new file mode 100644 index 00000000000..f953d226645 --- /dev/null +++ b/wcfsetup/install/files/lib/data/TCollectionL10n.class.php @@ -0,0 +1,53 @@ + + * @since 6.3 + */ +trait TCollectionL10n +{ + /** + * @var array>> + */ + private array $l10nValues; + + public function getResolvedL10nValue(DatabaseObject $object, string $columnName): string + { + $this->loadL10nValues(); + + return L10nStorage::resolveValue($this->l10nValues[$object->getObjectID()][$columnName] ?? []); + } + + /** + * @return array + */ + public function getL10nValues(DatabaseObject $object, string $columnName): array + { + $this->loadL10nValues(); + + return $this->l10nValues[$object->getObjectID()][$columnName] ?? []; + } + + private function loadL10nValues(): void + { + if (isset($this->l10nValues)) { + return; + } + + $this->l10nValues = (new L10nStorage($this->getL10nDefinition()))->getValuesForObjects( + $this->getObjectIDs() + ); + } + + abstract protected function getL10nDefinition(): L10nDefinition; +} diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php index 1950e5f49a7..92d6db1ee00 100644 --- a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php +++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php @@ -2,48 +2,56 @@ namespace wcf\data\captcha\question; -use wcf\data\DatabaseObject; +use wcf\data\CollectionDatabaseObject; use wcf\data\ITitledObject; +use wcf\system\l10n\L10nDefinition; use wcf\system\Regex; -use wcf\system\WCF; use wcf\util\StringUtil; /** * Represents a captcha question. * - * @author Matthias Schmidt - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License + * The localized values (`question` and `answers`) are stored in the + * `wcf1_captcha_question_l10n` table. + * + * @author Matthias Schmidt, Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License * * @property-read int $questionID unique id of the captcha question - * @property-read string $question question of the captcha or name of language item which contains the question - * @property-read ?string $answers newline-separated list of answers or name of language item which contains the answers * @property-read 0|1 $isDisabled is `1` if the captcha question is disabled and thus not offered to answer, otherwise `0` * @property-read int $views * @property-read int $correctSubmissions * @property-read int $incorrectSubmissions + * + * @extends CollectionDatabaseObject */ -class CaptchaQuestion extends DatabaseObject implements ITitledObject +class CaptchaQuestion extends CollectionDatabaseObject implements ITitledObject { /** * Returns the question in the active user's language. + */ + public function getQuestion(): string + { + return $this->getCollection()->getResolvedL10nValue($this, 'question'); + } + + /** + * Returns the newline-separated list of answers in the active user's language. * - * @return string - * @since 5.2 + * @since 6.3 */ - public function getQuestion() + public function getAnswers(): string { - return WCF::getLanguage()->get($this->question); + return $this->getCollection()->getResolvedL10nValue($this, 'answers'); } /** * Returns true if the given user input is an answer to this question. - * - * @return bool */ - public function isAnswer(string $answer) + public function isAnswer(string $answer): bool { - $answers = \explode("\n", StringUtil::unifyNewlines(WCF::getLanguage()->get($this->answers))); + $answers = \explode("\n", StringUtil::unifyNewlines($this->getAnswers())); foreach ($answers as $__answer) { if (\mb_substr($__answer, 0, 1) == '~' && \mb_substr($__answer, -1, 1) == '~') { if (Regex::compile(\mb_substr($__answer, 1, \mb_strlen($__answer) - 2), Regex::CASE_INSENSITIVE)->match($answer)) { @@ -59,9 +67,37 @@ public function isAnswer(string $answer) return false; } + /** + * Returns the localized values of this question. + * + * @return array + * @since 6.3 + */ + public function getL10nValues(string $columnName): array + { + if ($columnName !== 'question' && $columnName !== 'answers') { + throw new \InvalidArgumentException("Invalid column name given."); + } + + return $this->getCollection()->getL10nValues($this, $columnName); + } + #[\Override] public function getTitle(): string { return $this->getQuestion(); } + + /** + * @since 6.3 + */ + public static function getL10nDefinition(): L10nDefinition + { + return new L10nDefinition( + 'wcf1_captcha_question', + 'wcf1_captcha_question_l10n', + 'questionID', + ['question', 'answers'], + ); + } } diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php index d0b47dc50d8..1280a4fb4c2 100644 --- a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php +++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php @@ -6,11 +6,14 @@ use wcf\command\captcha\question\EnableCaptchaQuestion; use wcf\data\AbstractDatabaseObjectAction; use wcf\data\IToggleAction; -use wcf\data\TI18nDatabaseObjectAction; /** * Executes captcha question-related actions. * + * Captcha questions should be created and updated through the + * `CreateCaptchaQuestion` and `UpdateCaptchaQuestion` commands, the `create` + * and `update` actions are `@deprecated 6.3`. + * * @author Matthias Schmidt * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License @@ -19,8 +22,6 @@ */ class CaptchaQuestionAction extends AbstractDatabaseObjectAction implements IToggleAction { - use TI18nDatabaseObjectAction; - /** * @inheritDoc */ @@ -31,63 +32,6 @@ class CaptchaQuestionAction extends AbstractDatabaseObjectAction implements ITog */ protected $permissionsUpdate = ['admin.captcha.canManageCaptchaQuestion']; - /** - * @return array - */ - #[\Override] - public function getI18nSaveTypes(): array - { - return [ - 'question' => 'wcf.captcha.question.question.question\d+', - 'answers' => 'wcf.captcha.question.answers.question\d+', - ]; - } - - #[\Override] - public function getLanguageCategory(): string - { - return 'wcf.captcha.question'; - } - - #[\Override] - public function getPackageID(): int - { - return \PACKAGE_ID; - } - - #[\Override] - public function update() - { - parent::update(); - - foreach ($this->objects as $object) { - $this->saveI18nValue($object->getDecoratedObject()); - } - } - - #[\Override] - public function create() - { - // Question column doesn't have a default value - $this->parameters['data']['question'] = $this->parameters['data']['question'] ?? ''; - - $captchaQuestion = parent::create(); - - $this->saveI18nValue($captchaQuestion); - - return $captchaQuestion; - } - - #[\Override] - public function delete() - { - $returnValue = parent::delete(); - - $this->deleteI18nValues(); - - return $returnValue; - } - /** * @deprecated 6.3 */ diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionBuilder.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionBuilder.class.php new file mode 100644 index 00000000000..88c732447fe --- /dev/null +++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionBuilder.class.php @@ -0,0 +1,100 @@ + + * @since 6.3 + * + * @extends DatabaseObjectBuilder + */ +final class CaptchaQuestionBuilder extends DatabaseObjectBuilder +{ + /** + * @var array + */ + private array $question; + + /** + * @var array + */ + private array $answers; + + /** + * @param array $question + */ + public function setQuestion(array $question): static + { + $this->question = $question; + + return $this; + } + + /** + * @param array $answers + */ + public function setAnswers(array $answers): static + { + $this->answers = $answers; + + return $this; + } + + public function setIsDisabled(bool $isDisabled): static + { + $this->properties['isDisabled'] = $isDisabled ? 1 : 0; + + return $this; + } + + #[\Override] + protected function afterValidateCreate(): void + { + if (!isset($this->question) || !isset($this->answers)) { + throw new \BadMethodCallException("Missing values for 'question' or 'answers'."); + } + } + + #[\Override] + protected function afterCreate(DatabaseObject $object): void + { + $this->saveL10nValues($object); + } + + #[\Override] + protected function afterUpdate(DatabaseObject $object): void + { + if (isset($this->question) || isset($this->answers)) { + if (!isset($this->question) || !isset($this->answers)) { + // `L10nStorage::setValues()` replaces all rows of the object, + // writing only one of the two columns would wipe the other. + throw new \BadMethodCallException("'question' and 'answers' must be set together."); + } + + $this->saveL10nValues($object); + } + } + + private function saveL10nValues(CaptchaQuestion $question): void + { + (new L10nStorage(CaptchaQuestion::getL10nDefinition()))->setValues( + $question->questionID, + [ + 'question' => $this->question, + 'answers' => $this->answers, + ] + ); + } +} diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionCollection.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionCollection.class.php new file mode 100644 index 00000000000..b308b08a4a5 --- /dev/null +++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionCollection.class.php @@ -0,0 +1,28 @@ + + * @since 6.3 + * + * @extends DatabaseObjectCollection + */ +class CaptchaQuestionCollection extends DatabaseObjectCollection +{ + use TCollectionL10n; + + #[\Override] + protected function getL10nDefinition(): L10nDefinition + { + return CaptchaQuestion::getL10nDefinition(); + } +} diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php index 5cfaa578011..44d39e67ec2 100644 --- a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php +++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php @@ -13,4 +13,10 @@ * * @extends DatabaseObjectList */ -class CaptchaQuestionList extends DatabaseObjectList {} +class CaptchaQuestionList extends DatabaseObjectList +{ + /** + * @inheritDoc + */ + public $className = CaptchaQuestion::class; +} diff --git a/wcfsetup/install/files/lib/data/captcha/question/I18nCaptchaQuestionList.class.php b/wcfsetup/install/files/lib/data/captcha/question/I18nCaptchaQuestionList.class.php deleted file mode 100644 index 4784e731df5..00000000000 --- a/wcfsetup/install/files/lib/data/captcha/question/I18nCaptchaQuestionList.class.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @since 6.2 - * - * @extends I18nDatabaseObjectList - */ -class I18nCaptchaQuestionList extends I18nDatabaseObjectList -{ - /** - * @inheritDoc - */ - public $i18nFields = ['question' => 'questionI18n']; - - /** - * @inheritDoc - */ - public $className = CaptchaQuestion::class; -} diff --git a/wcfsetup/install/files/lib/data/captcha/question/L10nCaptchaQuestionList.class.php b/wcfsetup/install/files/lib/data/captcha/question/L10nCaptchaQuestionList.class.php new file mode 100644 index 00000000000..f86eb606a89 --- /dev/null +++ b/wcfsetup/install/files/lib/data/captcha/question/L10nCaptchaQuestionList.class.php @@ -0,0 +1,27 @@ + + * @since 6.3 + */ +class L10nCaptchaQuestionList extends CaptchaQuestionList +{ + public function __construct() + { + parent::__construct(); + + $storage = new L10nStorage(CaptchaQuestion::getL10nDefinition()); + + $this->sqlSelects .= (!empty($this->sqlSelects) ? ', ' : '') + . $storage->getSubSelect('question', $this->getDatabaseTableAlias()) + . ' AS question'; + } +} diff --git a/wcfsetup/install/files/lib/data/option/Option.class.php b/wcfsetup/install/files/lib/data/option/Option.class.php index e26384616f4..ef0bf320782 100644 --- a/wcfsetup/install/files/lib/data/option/Option.class.php +++ b/wcfsetup/install/files/lib/data/option/Option.class.php @@ -2,7 +2,7 @@ namespace wcf\data\option; -use wcf\data\DatabaseObject; +use wcf\data\CollectionDatabaseObject; use wcf\data\TDatabaseObjectOptions; use wcf\data\TDatabaseObjectPermissions; use wcf\system\WCF; @@ -32,12 +32,20 @@ * @property-read 0|1 $supportI18n is `1` if the option supports different values for all available languages, otherwise `0` * @property-read 0|1 $requireI18n is `1` if `$supportI18n = 1` and the option's value has to explicitly set for all values so that the `monolingual` option is not available, otherwise `0` * @property-read mixed[] $additionalData array with additional data of the option + * + * @extends CollectionDatabaseObject */ -class Option extends DatabaseObject +class Option extends CollectionDatabaseObject { use TDatabaseObjectOptions; use TDatabaseObjectPermissions; + #[\Override] + public function getCollectionClassName(): string + { + return OptionCollection::class; + } + #[\Override] public function __get(string $name) { diff --git a/wcfsetup/install/files/lib/data/option/OptionCollection.class.php b/wcfsetup/install/files/lib/data/option/OptionCollection.class.php new file mode 100644 index 00000000000..433665ef7b4 --- /dev/null +++ b/wcfsetup/install/files/lib/data/option/OptionCollection.class.php @@ -0,0 +1,18 @@ + + * @since 6.3 + * + * @extends DatabaseObjectCollection