diff --git a/com.woltlab.wcf/objectType.xml b/com.woltlab.wcf/objectType.xml index d6f816dfe49..e6cf1f372f7 100644 --- a/com.woltlab.wcf/objectType.xml +++ b/com.woltlab.wcf/objectType.xml @@ -157,6 +157,10 @@ com.woltlab.wcf.message user.signature.disallowedBBCodes + + com.woltlab.wcf.genericFormOption + com.woltlab.wcf.message + com.woltlab.wcf.user.signature com.woltlab.wcf.attachment.objectType @@ -223,6 +227,11 @@ com.woltlab.wcf.attachment.objectType wcf\system\attachment\ContactAttachmentObjectType + + com.woltlab.wcf.contact.form + com.woltlab.wcf.file + wcf\system\file\processor\ContactFormFileProcessor + com.woltlab.wcf.page.recentActivityEvent @@ -1722,6 +1731,10 @@ com.woltlab.wcf.lostPasswordForm com.woltlab.wcf.floodControl + + com.woltlab.wcf.contactForm + com.woltlab.wcf.floodControl + com.woltlab.wcf.search com.woltlab.wcf.floodControl diff --git a/com.woltlab.wcf/package.xml b/com.woltlab.wcf/package.xml index b986154f945..bbf5922e535 100644 --- a/com.woltlab.wcf/package.xml +++ b/com.woltlab.wcf/package.xml @@ -49,4 +49,11 @@ acp/install_com.woltlab.wcf_step2.php + + diff --git a/com.woltlab.wcf/templates/contact.tpl b/com.woltlab.wcf/templates/contact.tpl index 028be4f70c7..37ce037f882 100644 --- a/com.woltlab.wcf/templates/contact.tpl +++ b/com.woltlab.wcf/templates/contact.tpl @@ -1,96 +1,5 @@ {include file='header'} -{include file='shared_formError'} - -
-
-

{lang}wcf.contact.sender.information{/lang}

- - -
*
-
- - {if $errorField == 'name'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.contact.sender.error.{@$errorType}{/lang} - {/if} - - {/if} -
- - - -
*
-
- - {if $errorField == 'email'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.user.email.error.{@$errorType}{/lang} - {/if} - - {/if} -
- - - {event name='informationFields'} -
- -
-

{lang}wcf.contact.data{/lang}

- - {if $recipientList|count > 1} - -
*
-
- - {if $errorField == 'recipientID'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.contact.recipientID.error.{@$errorType}{/lang} - {/if} - - {/if} -
- - {/if} - - {include file='customOptionFieldList'} - - {event name='optionFields'} - - {if CONTACT_FORM_ENABLE_ATTACHMENTS && !$attachmentHandler|empty && $attachmentHandler->canUpload()} -
- {include file='shared_messageFormAttachments' wysiwygSelector=''} -
- {/if} -
- - {event name='sections'} - - {include file='shared_captcha'} - -
- - {csrfToken} -
-
- -

- * - {lang}wcf.global.form.required{/lang} -

+{unsafe:$form->getHtml()} {include file='footer'} diff --git a/com.woltlab.wcf/templates/shared_selectOptionsFormField.tpl b/com.woltlab.wcf/templates/shared_selectOptionsFormField.tpl new file mode 100644 index 00000000000..bacfd991a7b --- /dev/null +++ b/com.woltlab.wcf/templates/shared_selectOptionsFormField.tpl @@ -0,0 +1,16 @@ + + + diff --git a/ts/WoltLabSuite/Core/Component/Ckeditor/Configuration.ts b/ts/WoltLabSuite/Core/Component/Ckeditor/Configuration.ts index 76507159fce..be6615f857c 100644 --- a/ts/WoltLabSuite/Core/Component/Ckeditor/Configuration.ts +++ b/ts/WoltLabSuite/Core/Component/Ckeditor/Configuration.ts @@ -215,7 +215,7 @@ class ConfigurationBuilder { #setupMention(): void { if (!this.#features.mention) { - this.#removePlugins.push("Mention", "WoltlabMention"); + this.#removePlugins.push("WoltlabMention"); } } diff --git a/ts/WoltLabSuite/Core/Form/Builder/Field/SelectOptions.ts b/ts/WoltLabSuite/Core/Form/Builder/Field/SelectOptions.ts new file mode 100644 index 00000000000..b1ebb8eea60 --- /dev/null +++ b/ts/WoltLabSuite/Core/Form/Builder/Field/SelectOptions.ts @@ -0,0 +1,174 @@ +import { identify } from "WoltLabSuite/Core/Dom/Util"; +import { getPhrase } from "WoltLabSuite/Core/Language"; +import { getValues, init as initI18n } from "WoltLabSuite/Core/Language/Input"; +import Sortable from "sortablejs"; + +type Data = { + key: string; + value: Record; +}; + +type Languages = Record; + +let _languages: Languages; + +export function setup(formField: HTMLInputElement, languages: Languages): void { + _languages = languages; + + const ul = createUi(formField); + + formField.form?.addEventListener("submit", () => { + setHiddenValue(formField); + }); + + new Sortable(ul, { + direction: "vertical", + animation: 150, + fallbackOnBody: true, + draggable: "li", + handle: ".selectOptionsListItem__handle", + }); +} + +function createUi(formField: HTMLInputElement): HTMLUListElement { + const ul = document.createElement("ul"); + ul.classList.add("selectOptionsList"); + formField.parentElement?.append(ul); + + if (formField.value) { + const data = JSON.parse(formField.value) as Data[]; + data.forEach((option) => { + createRow(ul, option); + }); + } else { + createRow(ul); + } + + return ul; +} + +function createRow(ul: HTMLUListElement, option?: Data, autoFocus: boolean = false): void { + const li = document.createElement("li"); + li.classList.add("selectOptionsListItem"); + ul.append(li); + + const addButton = getAddButton(); + addButton.addEventListener("click", () => { + createRow(ul, undefined, true); + }); + + const deleteButton = getDeleteButton(); + deleteButton.addEventListener("click", () => { + li.remove(); + + if (!ul.childElementCount) { + createRow(ul); + } + }); + + const keyInput = getKeyInput(); + keyInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + createRow(ul, undefined, true); + } + }); + keyInput.value = option ? option.key : ""; + + const equalsIcon = document.createElement("fa-icon"); + equalsIcon.setIcon("equals"); + + const valueInput = getValueInput(); + valueInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + createRow(ul, undefined, true); + } + }); + + li.append(getSortableHandle(), addButton, deleteButton, keyInput, equalsIcon, valueInput); + + const hasI18nValues = option && !Object.hasOwn(option.value, 0); + + initI18n(identify(valueInput), hasI18nValues ? option.value : {}, _languages, false); + + if (!hasI18nValues) { + valueInput.value = option?.value[0] ?? ""; + } + + if (autoFocus) { + keyInput.focus(); + } +} + +function getAddButton(): HTMLButtonElement { + const addIcon = document.createElement("fa-icon"); + addIcon.setIcon("plus"); + + const addButton = document.createElement("button"); + addButton.type = "button"; + addButton.append(addIcon); + addButton.classList.add("jsTooltip"); + addButton.title = getPhrase("wcf.global.button.add"); + + return addButton; +} + +function getDeleteButton(): HTMLButtonElement { + const deleteIcon = document.createElement("fa-icon"); + deleteIcon.setIcon("xmark"); + + const deleteButton = document.createElement("button"); + deleteButton.type = "button"; + deleteButton.append(deleteIcon); + deleteButton.classList.add("jsTooltip"); + deleteButton.title = getPhrase("wcf.global.button.delete"); + + return deleteButton; +} + +function getKeyInput(): HTMLInputElement { + const keyInput = document.createElement("input"); + keyInput.classList.add("selectOptionsListItem__key"); + keyInput.placeholder = getPhrase("wcf.form.selectOptions.key"); + keyInput.type = "text"; + keyInput.required = true; + + return keyInput; +} + +function getValueInput(): HTMLInputElement { + const valueInput = document.createElement("input"); + valueInput.classList.add("selectOptionsListItem__value"); + valueInput.placeholder = getPhrase("wcf.form.selectOptions.value"); + valueInput.type = "text"; + valueInput.required = true; + + return valueInput; +} + +function getSortableHandle(): HTMLElement { + const icon = document.createElement("fa-icon"); + icon.setIcon("up-down"); + const handle = document.createElement("span"); + handle.append(icon); + handle.classList.add("selectOptionsListItem__handle"); + + return handle; +} + +function setHiddenValue(formField: HTMLInputElement): void { + const data: Data[] = []; + + formField.parentElement?.querySelectorAll(".selectOptionsListItem").forEach((li) => { + const key = li.querySelector(".selectOptionsListItem__key")!.value; + const valueInput = li.querySelector(".selectOptionsListItem__value")!; + + data.push({ + key, + value: Object.fromEntries(getValues(valueInput.id)), + }); + }); + + formField.value = JSON.stringify(data); +} diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php similarity index 77% rename from wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2.php rename to wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php index 4a587ceebab..144efaffb35 100644 --- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2.php +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php @@ -9,6 +9,9 @@ */ use wcf\system\database\table\column\IntDatabaseTableColumn; +use wcf\system\database\table\column\MediumtextDatabaseTableColumn; +use wcf\system\database\table\column\TextDatabaseTableColumn; +use wcf\system\database\table\column\TinyintDatabaseTableColumn; use wcf\system\database\table\index\DatabaseTableForeignKey; use wcf\system\database\table\PartialDatabaseTable; @@ -46,5 +49,13 @@ ->referencedTable('wcf1_file') ->referencedColumns(['fileID']) ->onDelete('SET NULL'), - ]) + ]), + PartialDatabaseTable::create('wcf1_contact_option') + ->columns([ + MediumtextDatabaseTableColumn::create('configuration'), + ]), + PartialDatabaseTable::create('wcf1_file') + ->columns([ + IntDatabaseTableColumn::create('uploadTime'), + ]), ]; diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step2.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step2.php new file mode 100644 index 00000000000..f10a241c93d --- /dev/null +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step2.php @@ -0,0 +1,28 @@ + + */ + +use wcf\system\database\table\column\MediumtextDatabaseTableColumn; +use wcf\system\database\table\column\TextDatabaseTableColumn; +use wcf\system\database\table\column\TinyintDatabaseTableColumn; +use wcf\system\database\table\PartialDatabaseTable; + +return [ + PartialDatabaseTable::create('wcf1_contact_option') + ->columns([ + MediumtextDatabaseTableColumn::create('defaultValue') + ->drop(), + TextDatabaseTableColumn::create('validationPattern') + ->drop(), + MediumtextDatabaseTableColumn::create('selectOptions') + ->drop(), + TinyintDatabaseTableColumn::create('required') + ->drop(), + ]), +]; diff --git a/wcfsetup/install/files/acp/templates/contactOptionAdd.tpl b/wcfsetup/install/files/acp/templates/contactOptionAdd.tpl index bb9eb7a4a2d..fe7ed672843 100644 --- a/wcfsetup/install/files/acp/templates/contactOptionAdd.tpl +++ b/wcfsetup/install/files/acp/templates/contactOptionAdd.tpl @@ -1,8 +1,10 @@ -{include file='header' pageTitle='wcf.acp.contact.option.'|concat:$action} +{assign var='pageTitle' value='wcf.acp.contact.option.'|concat:$action} + +{include file='header'}
-

{lang}wcf.acp.contact.option.{@$action}{/lang}

+

{lang}{$pageTitle}{/lang}

-{include file='shared_formNotice'} - -
- {include file='customOptionAdd'} - - {event name='sections'} - -
- - {csrfToken} -
-
+{unsafe:$form->getHtml()} {include file='footer'} diff --git a/wcfsetup/install/files/acp/templates/contactSettings.tpl b/wcfsetup/install/files/acp/templates/contactSettings.tpl index 915d9adf2d9..e42632d97dd 100644 --- a/wcfsetup/install/files/acp/templates/contactSettings.tpl +++ b/wcfsetup/install/files/acp/templates/contactSettings.tpl @@ -64,7 +64,7 @@ {@$option->optionID} {$option->getTitle()} - {lang}wcf.acp.customOption.optionType.{$option->optionType}{/lang} + {lang}wcf.form.option.{$option->optionType}{/lang} {#$option->showOrder} {event name='columns'} diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.2_contactOptions.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.2_contactOptions.php new file mode 100644 index 00000000000..c443158e1e1 --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.2_contactOptions.php @@ -0,0 +1,58 @@ +readObjects(); +$contactOptionList->getConditionBuilder()->add('configuration IS NULL'); + +foreach ($contactOptionList as $contactOption) { + $configuration = []; + $optionType = ''; + $optionType = match ($contactOption->optionType) { + 'multiSelect' => 'checkboxes', + 'message' => 'wysiwyg', + 'URL' => 'url', + default => $contactOption->optionType, + }; + + if ($contactOption->required) { + $configuration['required'] = 1; + } + if ($contactOption->defaultValue && $contactOption->optionType == 'text') { + $configuration['defaultValue'] = $contactOption->defaultValue; + } + if ($contactOption->selectOptions) { + $configuration['required'] = convertSelectOptions($contactOption->selectOptions); + } + + $editor = new ContactOptionEditor($contactOption); + $editor->update([ + 'optionType' => $optionType, + 'configuration' => JSON::encode($configuration), + ]); +} + +function convertSelectOptions(string $selectOptions): string +{ + $options = []; + + $parsedSelectOptions = OptionUtil::parseSelectOptions($selectOptions); + foreach ($parsedSelectOptions as $key => $value) { + $options[] = [ + 'key' => $key, + 'value' => [ + 0 => $value + ] + ]; + } + + return JSON::encode($options); +} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Configuration.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Configuration.js index eccdc6ebe03..7d0aae6c8fa 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Configuration.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Configuration.js @@ -197,7 +197,7 @@ define(["require", "exports", "../../Language", "WoltLabSuite/Core/Component/Emo } #setupMention() { if (!this.#features.mention) { - this.#removePlugins.push("Mention", "WoltlabMention"); + this.#removePlugins.push("WoltlabMention"); } } #getToolbar() { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/SelectOptions.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/SelectOptions.js new file mode 100644 index 00000000000..e7a018f1b89 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/SelectOptions.js @@ -0,0 +1,134 @@ +define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Language/Input", "sortablejs"], function (require, exports, tslib_1, Util_1, Language_1, Input_1, sortablejs_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.setup = setup; + sortablejs_1 = tslib_1.__importDefault(sortablejs_1); + let _languages; + function setup(formField, languages) { + _languages = languages; + const ul = createUi(formField); + formField.form?.addEventListener("submit", () => { + setHiddenValue(formField); + }); + new sortablejs_1.default(ul, { + direction: "vertical", + animation: 150, + fallbackOnBody: true, + draggable: "li", + handle: ".selectOptionsListItem__handle", + }); + } + function createUi(formField) { + const ul = document.createElement("ul"); + ul.classList.add("selectOptionsList"); + formField.parentElement?.append(ul); + if (formField.value) { + const data = JSON.parse(formField.value); + data.forEach((option) => { + createRow(ul, option); + }); + } + else { + createRow(ul); + } + return ul; + } + function createRow(ul, option, autoFocus = false) { + const li = document.createElement("li"); + li.classList.add("selectOptionsListItem"); + ul.append(li); + const addButton = getAddButton(); + addButton.addEventListener("click", () => { + createRow(ul, undefined, true); + }); + const deleteButton = getDeleteButton(); + deleteButton.addEventListener("click", () => { + li.remove(); + if (!ul.childElementCount) { + createRow(ul); + } + }); + const keyInput = getKeyInput(); + keyInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + createRow(ul, undefined, true); + } + }); + keyInput.value = option ? option.key : ""; + const equalsIcon = document.createElement("fa-icon"); + equalsIcon.setIcon("equals"); + const valueInput = getValueInput(); + valueInput.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + createRow(ul, undefined, true); + } + }); + li.append(getSortableHandle(), addButton, deleteButton, keyInput, equalsIcon, valueInput); + const hasI18nValues = option && !Object.hasOwn(option.value, 0); + (0, Input_1.init)((0, Util_1.identify)(valueInput), hasI18nValues ? option.value : {}, _languages, false); + if (!hasI18nValues) { + valueInput.value = option?.value[0] ?? ""; + } + if (autoFocus) { + keyInput.focus(); + } + } + function getAddButton() { + const addIcon = document.createElement("fa-icon"); + addIcon.setIcon("plus"); + const addButton = document.createElement("button"); + addButton.type = "button"; + addButton.append(addIcon); + addButton.classList.add("jsTooltip"); + addButton.title = (0, Language_1.getPhrase)("wcf.global.button.add"); + return addButton; + } + function getDeleteButton() { + const deleteIcon = document.createElement("fa-icon"); + deleteIcon.setIcon("xmark"); + const deleteButton = document.createElement("button"); + deleteButton.type = "button"; + deleteButton.append(deleteIcon); + deleteButton.classList.add("jsTooltip"); + deleteButton.title = (0, Language_1.getPhrase)("wcf.global.button.delete"); + return deleteButton; + } + function getKeyInput() { + const keyInput = document.createElement("input"); + keyInput.classList.add("selectOptionsListItem__key"); + keyInput.placeholder = (0, Language_1.getPhrase)("wcf.form.selectOptions.key"); + keyInput.type = "text"; + keyInput.required = true; + return keyInput; + } + function getValueInput() { + const valueInput = document.createElement("input"); + valueInput.classList.add("selectOptionsListItem__value"); + valueInput.placeholder = (0, Language_1.getPhrase)("wcf.form.selectOptions.value"); + valueInput.type = "text"; + valueInput.required = true; + return valueInput; + } + function getSortableHandle() { + const icon = document.createElement("fa-icon"); + icon.setIcon("up-down"); + const handle = document.createElement("span"); + handle.append(icon); + handle.classList.add("selectOptionsListItem__handle"); + return handle; + } + function setHiddenValue(formField) { + const data = []; + formField.parentElement?.querySelectorAll(".selectOptionsListItem").forEach((li) => { + const key = li.querySelector(".selectOptionsListItem__key").value; + const valueInput = li.querySelector(".selectOptionsListItem__value"); + data.push({ + key, + value: Object.fromEntries((0, Input_1.getValues)(valueInput.id)), + }); + }); + formField.value = JSON.stringify(data); + } +}); diff --git a/wcfsetup/install/files/lib/acp/form/AbstractCustomOptionForm.class.php b/wcfsetup/install/files/lib/acp/form/AbstractCustomOptionForm.class.php index 1784a8eb19c..5d263bdba15 100644 --- a/wcfsetup/install/files/lib/acp/form/AbstractCustomOptionForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/AbstractCustomOptionForm.class.php @@ -15,6 +15,7 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @since 3.1 + * @deprecated 6.2 Use `AbstractFormOptionAddForm` instead */ abstract class AbstractCustomOptionForm extends AbstractAcpForm { diff --git a/wcfsetup/install/files/lib/acp/form/AbstractFormOptionAddForm.class.php b/wcfsetup/install/files/lib/acp/form/AbstractFormOptionAddForm.class.php new file mode 100644 index 00000000000..be707388599 --- /dev/null +++ b/wcfsetup/install/files/lib/acp/form/AbstractFormOptionAddForm.class.php @@ -0,0 +1,121 @@ + + * @since 6.2 + */ +abstract class AbstractFormOptionAddForm extends AbstractFormBuilderForm +{ + #[\Override] + public function finalizeForm() + { + parent::finalizeForm(); + + $this->form->getDataHandler()->addProcessor( + new CustomFormDataProcessor( + 'saveOptionProcessor', + function (IFormDocument $document, array $parameters) { + $configuration = []; + + foreach ($this->getConfigurationFormFieldIds() as $parameter) { + if (isset($parameters['data'][$parameter])) { + $configuration[$parameter] = $parameters['data'][$parameter]; + unset($parameters['data'][$parameter]); + } + } + + if ($configuration !== []) { + $parameters['data']['configuration'] = JSON::encode($configuration); + } + + return $parameters; + }, + function (IFormDocument $document, array $data, IStorableObject $object) { + \assert($object instanceof DatabaseObject); + + if ($object->configuration) { + $data = \array_merge($data, JSON::decode($object->configuration)); + } + + return $data; + } + ) + ); + } + + /** + * @return string[] + */ + protected function getConfigurationFormFieldIds(): array + { + $ids = []; + + foreach (FormOptionHandler::getInstance()->getOptions() as $option) { + foreach ($option->getConfigurationFormFields() as $formFieldId) { + $ids[] = $formFieldId; + } + } + + return \array_unique($ids); + } + + /** + * @return IFormField[] + */ + protected function getSharedConfigurationFormFields(): array + { + $sharedConfigurationFormFields = new SharedConfigurationFormFields(); + $matrix = []; + + foreach (FormOptionHandler::getInstance()->getOptions() as $option) { + foreach ($option->getConfigurationFormFields() as $formFieldId) { + if (!isset($matrix[$formFieldId])) { + $matrix[$formFieldId] = []; + } + + $matrix[$formFieldId][] = $option->getId(); + } + } + + $formFields = []; + + foreach ($matrix as $formFieldId => $dependencies) { + $formField = $sharedConfigurationFormFields->getFormField($formFieldId); + $formField->addDependency( + ValueFormFieldDependency::create($formFieldId . 'OptionTypeDependency') + ->fieldId('optionType') + ->values($dependencies) + ); + $formFields[] = $formField; + } + + return $formFields; + } + + protected function getOptionTypeFormField(): SelectFormField + { + return SelectFormField::create('optionType') + ->label('wcf.form.option.optionType') + ->immutable($this->formAction !== 'create') + ->options(FormOptionHandler::getInstance()->getSortedOptionTypes()) + ->required(); + } +} diff --git a/wcfsetup/install/files/lib/acp/form/ContactOptionAddForm.class.php b/wcfsetup/install/files/lib/acp/form/ContactOptionAddForm.class.php index d8e82e73f17..a342d2caf42 100644 --- a/wcfsetup/install/files/lib/acp/form/ContactOptionAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/ContactOptionAddForm.class.php @@ -4,25 +4,25 @@ use wcf\data\contact\option\ContactOption; use wcf\data\contact\option\ContactOptionAction; -use wcf\data\contact\option\ContactOptionEditor; -use wcf\system\request\LinkHandler; -use wcf\system\WCF; +use wcf\data\contact\option\ContactOptionList; +use wcf\form\AbstractFormBuilderForm; +use wcf\system\form\builder\field\BooleanFormField; +use wcf\system\form\builder\field\MultilineTextFormField; +use wcf\system\form\builder\field\ShowOrderFormField; +use wcf\system\form\builder\field\TextFormField; /** * Shows the contact option add form. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License + * @author Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License * @since 3.1 + * + * @extends AbstractFormBuilderForm */ -class ContactOptionAddForm extends AbstractCustomOptionForm +class ContactOptionAddForm extends AbstractFormOptionAddForm { - /** - * @inheritDoc - */ - public $action = 'add'; - /** * @inheritDoc */ @@ -38,51 +38,47 @@ class ContactOptionAddForm extends AbstractCustomOptionForm */ public $neededPermissions = ['admin.contact.canManageContactForm']; - /** - * action class name - * @var string - */ - public $actionClass = ContactOptionAction::class; - - /** - * base class name - * @var string - */ - public $baseClass = ContactOption::class; - - /** - * editor class name - * @var string - */ - public $editorClass = ContactOptionEditor::class; - /** * @inheritDoc */ - public function readParameters() + public $objectActionClass = ContactOptionAction::class; + + #[\Override] + protected function createForm() { - parent::readParameters(); + parent::createForm(); - $this->getI18nValue('optionTitle')->setLanguageItem('wcf.contact.option', 'wcf.contact', 'com.woltlab.wcf'); - $this->getI18nValue('optionDescription')->setLanguageItem( - 'wcf.contact.optionDescription', - 'wcf.contact', - 'com.woltlab.wcf' - ); + $this->form->appendChildren([ + TextFormField::create('optionTitle') + ->label('wcf.global.name') + ->maximumLength(255) + ->i18n() + ->languageItemPattern('wcf.contact.option\d+') + ->required(), + MultilineTextFormField::create('optionDescription') + ->label('wcf.global.description') + ->maximumLength(5000) + ->rows(5) + ->i18n() + ->languageItemPattern('wcf.contact.optionDescription\d+'), + ShowOrderFormField::create('showOrder') + ->options($this->getContactOptions()), + $this->getOptionTypeFormField(), + ...$this->getSharedConfigurationFormFields(), + BooleanFormField::create('isDisabled') + ->label('wcf.acp.customOption.isDisabled'), + ]); } /** - * @inheritDoc + * @return array */ - public function save() + private function getContactOptions(): array { - parent::save(); + $optionList = new ContactOptionList(); + $optionList->sqlOrderBy = 'showOrder ASC'; + $optionList->readObjects(); - WCF::getTPL()->assign([ - 'objectEditLink' => LinkHandler::getInstance()->getControllerLink( - ContactOptionEditForm::class, - ['id' => $this->objectAction->getReturnValues()['returnValues']->getObjectID()] - ), - ]); + return \array_map(static fn($option) => $option->getTitle(), $optionList->getObjects()); } } diff --git a/wcfsetup/install/files/lib/acp/form/ContactOptionEditForm.class.php b/wcfsetup/install/files/lib/acp/form/ContactOptionEditForm.class.php index ebf85862f93..5181668449d 100644 --- a/wcfsetup/install/files/lib/acp/form/ContactOptionEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/ContactOptionEditForm.class.php @@ -2,12 +2,17 @@ namespace wcf\acp\form; +use CuyZ\Valinor\Mapper\MappingError; +use wcf\data\contact\option\ContactOption; +use wcf\http\Helper; +use wcf\system\exception\IllegalLinkException; + /** * Shows the contact option edit form. * - * @author Alexander Ebert + * @author Alexander Ebert * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License + * @license GNU Lesser General Public License * @since 3.1 */ class ContactOptionEditForm extends ContactOptionAddForm @@ -15,13 +20,30 @@ class ContactOptionEditForm extends ContactOptionAddForm /** * @inheritDoc */ - public $action = 'edit'; + public $formAction = 'edit'; - /** - * @inheritDoc - */ - public function save() + #[\Override] + public function readParameters() { - AbstractCustomOptionForm::save(); + parent::readParameters(); + + try { + $queryParameters = Helper::mapQueryParameters( + $_GET, + <<<'EOT' + array { + id: positive-int + } + EOT + ); + } catch (MappingError) { + throw new IllegalLinkException(); + } + + $this->formObject = new ContactOption($queryParameters['id']); + + if (!$this->formObject->getObjectID()) { + throw new IllegalLinkException(); + } } } diff --git a/wcfsetup/install/files/lib/acp/page/ContactAttachmentPage.class.php b/wcfsetup/install/files/lib/acp/page/ContactAttachmentPage.class.php index cd98ba8795c..988b0edeb20 100644 --- a/wcfsetup/install/files/lib/acp/page/ContactAttachmentPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/ContactAttachmentPage.class.php @@ -8,7 +8,6 @@ * @author Alexander Ebert * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License + * @deprecated 6.2 Contact form attachments are using `ContactFormFileProcessor` instead. */ -class ContactAttachmentPage extends \wcf\page\ContactAttachmentPage -{ -} +class ContactAttachmentPage extends \wcf\page\ContactAttachmentPage {} diff --git a/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachment.class.php b/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachment.class.php index 3f0f96d683b..2520fbf4f14 100644 --- a/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachment.class.php +++ b/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachment.class.php @@ -15,6 +15,7 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @since 5.2 + * @deprecated 6.2 Contact form attachments are using `ContactFormFileProcessor` instead. * * @property-read int $attachmentID * @property-read string $accessKey diff --git a/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentAction.class.php b/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentAction.class.php index e566df2a534..cb86fc5c837 100644 --- a/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentAction.class.php +++ b/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentAction.class.php @@ -11,6 +11,7 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @since 5.2 + * @deprecated 6.2 Contact form attachments are using `ContactFormFileProcessor` instead. * * @extends AbstractDatabaseObjectAction */ diff --git a/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentEditor.class.php b/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentEditor.class.php index d84f0e43c93..f9d7bcb0dd6 100644 --- a/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentEditor.class.php +++ b/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentEditor.class.php @@ -11,6 +11,7 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @since 5.2 + * @deprecated 6.2 Contact form attachments are using `ContactFormFileProcessor` instead. * * @mixin ContactAttachment * @extends DatabaseObjectEditor diff --git a/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentList.class.php b/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentList.class.php index 6d2c36963fa..c6db8819c29 100644 --- a/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentList.class.php +++ b/wcfsetup/install/files/lib/data/contact/attachment/ContactAttachmentList.class.php @@ -11,6 +11,7 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @since 5.2 + * @deprecated 6.2 Contact form attachments are using `ContactFormFileProcessor` instead. * * @extends DatabaseObjectList */ diff --git a/wcfsetup/install/files/lib/data/contact/option/ContactOption.class.php b/wcfsetup/install/files/lib/data/contact/option/ContactOption.class.php index b9f871beca0..75ea37865a8 100644 --- a/wcfsetup/install/files/lib/data/contact/option/ContactOption.class.php +++ b/wcfsetup/install/files/lib/data/contact/option/ContactOption.class.php @@ -2,23 +2,79 @@ namespace wcf\data\contact\option; -use wcf\data\custom\option\CustomOption; +use wcf\data\DatabaseObject; +use wcf\data\ITitledObject; +use wcf\data\language\Language; +use wcf\system\form\option\FormOptionHandler; +use wcf\system\form\option\IFormOption; +use wcf\system\WCF; +use wcf\util\JSON; /** * Represents a contact option. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 3.1 + * @author Alexander Ebert + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 3.1 + * + * @property-read int $optionID unique id of the option + * @property-read string $optionTitle title of the option or name of language item which contains the title + * @property-read string $optionDescription description of the option or name of language item which contains the description + * @property-read string $optionType type of the option which determines its input and output + * @property-read string $configuration JSON-encoded configuration information depending on the option type + * @property-read int $showOrder position of the option in relation to the other options + * @property-read int $isDisabled is `1` if the option is disabled, otherwise `0` + * @property-read int $originIsSystem is `1` if the option has been delivered by a package, otherwise `0` (i.e. the option has been created in the ACP) */ -class ContactOption extends CustomOption +class ContactOption extends DatabaseObject implements ITitledObject { - /** - * @inheritDoc - */ + #[\Override] public static function getDatabaseTableAlias() { return 'contact_option'; } + + #[\Override] + public function getTitle(): string + { + return WCF::getLanguage()->get($this->optionTitle); + } + + /** + * Returns the option description in the active user's language. + * + * @since 5.2 + */ + public function getDescription(): string + { + return WCF::getLanguage()->get($this->optionDescription); + } + + public function canDelete(): bool + { + return !$this->originIsSystem; + } + + /** + * @since 6.2 + */ + public function getFormOption(): IFormOption + { + $formOption = FormOptionHandler::getInstance()->getOption($this->optionType); + if ($formOption === null) { + throw new \BadMethodCallException("unknown form option type '{$this->optionType}'"); + } + + return $formOption; + } + + /** + * @return array + * @since 6.2 + */ + public function getConfiguration(): array + { + return $this->configuration ? JSON::decode($this->configuration) : []; + } } diff --git a/wcfsetup/install/files/lib/data/contact/option/ContactOptionAction.class.php b/wcfsetup/install/files/lib/data/contact/option/ContactOptionAction.class.php index af03147be92..1d860f06740 100644 --- a/wcfsetup/install/files/lib/data/contact/option/ContactOptionAction.class.php +++ b/wcfsetup/install/files/lib/data/contact/option/ContactOptionAction.class.php @@ -2,20 +2,9 @@ namespace wcf\data\contact\option; -use wcf\data\attachment\AttachmentEditor; -use wcf\data\contact\attachment\ContactAttachment; -use wcf\data\contact\attachment\ContactAttachmentEditor; -use wcf\data\contact\recipient\ContactRecipient; -use wcf\data\custom\option\CustomOptionAction; +use wcf\data\AbstractDatabaseObjectAction; use wcf\data\ISortableAction; -use wcf\system\attachment\AttachmentHandler; -use wcf\system\email\Email; -use wcf\system\email\Mailbox; -use wcf\system\email\mime\MimePartFacade; -use wcf\system\email\mime\RecipientAwareTextMimePart; use wcf\system\exception\UserInputException; -use wcf\system\language\LanguageFactory; -use wcf\system\option\ContactOptionHandler; use wcf\system\WCF; /** @@ -25,8 +14,10 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @since 3.1 + * + * @extends AbstractDatabaseObjectAction */ -class ContactOptionAction extends CustomOptionAction implements ISortableAction +class ContactOptionAction extends AbstractDatabaseObjectAction implements ISortableAction { /** * @inheritDoc @@ -53,80 +44,6 @@ class ContactOptionAction extends CustomOptionAction implements ISortableAction */ protected $requireACP = ['create', 'delete', 'update', 'updatePosition']; - /** - * Sends an email to the selected recipient. - * - * @return void - */ - public function send() - { - $defaultLanguage = LanguageFactory::getInstance()->getDefaultLanguage(); - - $recipient = new ContactRecipient($this->parameters['recipientID']); - /** @var ContactOptionHandler $optionHandler */ - $optionHandler = $this->parameters['optionHandler']; - - /** @var ?AttachmentHandler $attachmentHandler */ - $attachmentHandler = (!empty($this->parameters['attachmentHandler'])) ? $this->parameters['attachmentHandler'] : null; - - /** @var ContactAttachment[] $attachments */ - $attachments = []; - if ($attachmentHandler !== null) { - foreach ($attachmentHandler->getAttachmentList() as $attachment) { - $attachments[] = ContactAttachmentEditor::create([ - 'attachmentID' => $attachment->attachmentID, - 'accessKey' => ContactAttachment::generateKey(), - ]); - - (new AttachmentEditor($attachment))->update([ - 'objectID' => $attachment->attachmentID, - 'tmpHash' => '', - ]); - } - } - - $options = []; - foreach ($optionHandler->getOptions() as $option) { - /** @var ContactOption $object */ - $object = $option['object']; - if ($object->optionType === 'date' && !$object->getOptionValue()) { - // skip empty dates - continue; - } - - $options[] = [ - 'isMessage' => $object->isMessage(), - 'title' => $object->getLocalizedName($defaultLanguage), - 'value' => $object->getFormattedOptionValue(true), - 'htmlValue' => $object->getFormattedOptionValue(), - ]; - } - - // build message data - $messageData = [ - 'options' => $options, - 'recipient' => $recipient, - 'name' => $this->parameters['name'], - 'emailAddress' => $this->parameters['email'], - 'attachments' => $attachments, - ]; - - // build mail - $email = new Email(); - $email->addRecipient($recipient->getMailbox()); - $email->setSubject($defaultLanguage->get('wcf.contact.mail.subject')); - $email->setBody(new MimePartFacade([ - new RecipientAwareTextMimePart('text/html', 'email_contact', 'wcf', $messageData), - new RecipientAwareTextMimePart('text/plain', 'email_contact', 'wcf', $messageData), - ])); - - // add reply-to tag - $email->setReplyTo(new Mailbox($this->parameters['email'], $this->parameters['name'])); - - // send mail - $email->send(); - } - /** * @inheritDoc */ diff --git a/wcfsetup/install/files/lib/data/contact/option/ContactOptionEditor.class.php b/wcfsetup/install/files/lib/data/contact/option/ContactOptionEditor.class.php index c7a4b70aa2d..70276260def 100644 --- a/wcfsetup/install/files/lib/data/contact/option/ContactOptionEditor.class.php +++ b/wcfsetup/install/files/lib/data/contact/option/ContactOptionEditor.class.php @@ -2,7 +2,7 @@ namespace wcf\data\contact\option; -use wcf\data\custom\option\CustomOptionEditor; +use wcf\data\DatabaseObjectEditor; use wcf\data\IEditableCachedObject; use wcf\system\cache\builder\ContactOptionCacheBuilder; @@ -14,11 +14,11 @@ * @license GNU Lesser General Public License * @since 3.1 * - * @mixin ContactOption - * @extends CustomOptionEditor + * @mixin ContactOption + * @extends DatabaseObjectEditor * @implements IEditableCachedObject */ -class ContactOptionEditor extends CustomOptionEditor implements IEditableCachedObject +class ContactOptionEditor extends DatabaseObjectEditor implements IEditableCachedObject { /** * @inheritDoc diff --git a/wcfsetup/install/files/lib/data/contact/option/ContactOptionList.class.php b/wcfsetup/install/files/lib/data/contact/option/ContactOptionList.class.php index eace172674e..6cfaee88e2f 100644 --- a/wcfsetup/install/files/lib/data/contact/option/ContactOptionList.class.php +++ b/wcfsetup/install/files/lib/data/contact/option/ContactOptionList.class.php @@ -2,26 +2,27 @@ namespace wcf\data\contact\option; -use wcf\data\custom\option\CustomOptionList; +use wcf\data\DatabaseObjectList; /** - * Represents a list of contact recipients. + * Represents a list of contact options. * * @author Alexander Ebert * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @since 3.1 * - * @method ContactOption current() - * @method ContactOption[] getObjects() - * @method ContactOption|null getSingleObject() - * @method ContactOption|null search($objectID) - * @property ContactOption[] $objects + * @extends DatabaseObjectList */ -class ContactOptionList extends CustomOptionList +class ContactOptionList extends DatabaseObjectList { /** * @inheritDoc */ public $className = ContactOption::class; + + /** + * @inheritDoc + */ + public $sqlOrderBy = 'showOrder'; } diff --git a/wcfsetup/install/files/lib/data/custom/option/CustomOption.class.php b/wcfsetup/install/files/lib/data/custom/option/CustomOption.class.php index 3b7328f7aeb..0bf4695a521 100644 --- a/wcfsetup/install/files/lib/data/custom/option/CustomOption.class.php +++ b/wcfsetup/install/files/lib/data/custom/option/CustomOption.class.php @@ -31,6 +31,7 @@ * @property-read int $showOrder position of the option in relation to the other options * @property-read int $isDisabled is `1` if the option is disabled, otherwise `0` * @property-read int $originIsSystem is `1` if the option has been delivered by a package, otherwise `0` (i.e. the option has been created in the ACP) + * @deprecated 6.2 Use `IFormOption` instead */ abstract class CustomOption extends Option implements ITitledObject { diff --git a/wcfsetup/install/files/lib/data/custom/option/CustomOptionAction.class.php b/wcfsetup/install/files/lib/data/custom/option/CustomOptionAction.class.php index b13c7eee1a0..59e3baf005f 100644 --- a/wcfsetup/install/files/lib/data/custom/option/CustomOptionAction.class.php +++ b/wcfsetup/install/files/lib/data/custom/option/CustomOptionAction.class.php @@ -19,6 +19,7 @@ * @template TCustomOptionEditor of CustomOptionEditor|DatabaseObjectDecorator = CustomOptionEditor * @extends AbstractDatabaseObjectAction * @phpstan-ignore generics.notSubtype + * @deprecated 6.2 Use `IFormOption` instead */ abstract class CustomOptionAction extends AbstractDatabaseObjectAction implements IToggleAction { diff --git a/wcfsetup/install/files/lib/data/custom/option/CustomOptionEditor.class.php b/wcfsetup/install/files/lib/data/custom/option/CustomOptionEditor.class.php index 46d7f91221f..d5d7252ae29 100644 --- a/wcfsetup/install/files/lib/data/custom/option/CustomOptionEditor.class.php +++ b/wcfsetup/install/files/lib/data/custom/option/CustomOptionEditor.class.php @@ -15,6 +15,7 @@ * @mixin CustomOption * @template TCustomOption of CustomOption = CustomOption * @extends DatabaseObjectEditor + * @deprecated 6.2 Use `IFormOption` instead */ abstract class CustomOptionEditor extends DatabaseObjectEditor { diff --git a/wcfsetup/install/files/lib/data/custom/option/CustomOptionList.class.php b/wcfsetup/install/files/lib/data/custom/option/CustomOptionList.class.php index 30345774e72..97aa57cf00b 100644 --- a/wcfsetup/install/files/lib/data/custom/option/CustomOptionList.class.php +++ b/wcfsetup/install/files/lib/data/custom/option/CustomOptionList.class.php @@ -13,6 +13,7 @@ * @since 3.1 * * @extends DatabaseObjectList + * @deprecated 6.2 Use `IFormOption` instead */ abstract class CustomOptionList extends DatabaseObjectList { diff --git a/wcfsetup/install/files/lib/data/file/File.class.php b/wcfsetup/install/files/lib/data/file/File.class.php index b72e116f27c..2a80dc1a9ed 100644 --- a/wcfsetup/install/files/lib/data/file/File.class.php +++ b/wcfsetup/install/files/lib/data/file/File.class.php @@ -5,7 +5,7 @@ use wcf\action\FileDownloadAction; use wcf\data\DatabaseObject; use wcf\data\file\thumbnail\FileThumbnail; -use wcf\data\ILinkableObject; +use wcf\data\ITitledLinkObject; use wcf\system\application\ApplicationHandler; use wcf\system\file\processor\FileProcessor; use wcf\system\file\processor\IFileProcessor; @@ -31,8 +31,9 @@ * @property-read int|null $width * @property-read int|null $height * @property-read string|null $fileHashWebp + * @property-read int $uploadTime */ -class File extends DatabaseObject implements ILinkableObject, IImageDataProvider +class File extends DatabaseObject implements ITitledLinkObject, IImageDataProvider { /** * List of common file extensions that are always safe to be served directly @@ -278,4 +279,10 @@ public function getImageData(?int $minWidth = null, ?int $minHeight = null): ?Im return new ImageData($this->getLink(), $this->width, $this->height); } + + #[\Override] + public function getTitle(): string + { + return $this->filename; + } } diff --git a/wcfsetup/install/files/lib/data/file/FileEditor.class.php b/wcfsetup/install/files/lib/data/file/FileEditor.class.php index d7cd358fb32..ff7840ea923 100644 --- a/wcfsetup/install/files/lib/data/file/FileEditor.class.php +++ b/wcfsetup/install/files/lib/data/file/FileEditor.class.php @@ -96,6 +96,7 @@ public static function createFromTemporary(FileTemporary $fileTemporary): File 'mimeType' => $mimeType, 'width' => $width, 'height' => $height, + 'uploadTime' => \TIME_NOW, ]]); $file = $fileAction->executeAction()['returnValues']; \assert($file instanceof File); @@ -117,7 +118,8 @@ public static function createFromExistingFile( string $pathname, string $originalFilename, string $objectTypeName, - bool $copy = false + bool $copy = false, + ?int $uploadTime = null ): ?File { if (!\is_readable($pathname)) { return null; @@ -165,6 +167,7 @@ public static function createFromExistingFile( 'mimeType' => $mimeType, 'width' => $width, 'height' => $height, + 'uploadTime' => $uploadTime, ]]); $file = $fileAction->executeAction()['returnValues']; \assert($file instanceof File); @@ -217,7 +220,7 @@ private static function normalizeImageRotation( ExifUtil::ORIENTATION_180_ROTATE => 180, ExifUtil::ORIENTATION_90_ROTATE => 90, ExifUtil::ORIENTATION_270_ROTATE => 270, - // Any other rotation is unsupported. + // Any other rotation is unsupported. default => null, }; diff --git a/wcfsetup/install/files/lib/event/form/option/FormOptionCollecting.class.php b/wcfsetup/install/files/lib/event/form/option/FormOptionCollecting.class.php new file mode 100644 index 00000000000..17fd255def4 --- /dev/null +++ b/wcfsetup/install/files/lib/event/form/option/FormOptionCollecting.class.php @@ -0,0 +1,38 @@ + + * @since 6.2 + */ +final class FormOptionCollecting implements IPsr14Event +{ + /** + * @var array + */ + private array $options = []; + + /** + * Registers a new form option. + */ + public function register(IFormOption $option): void + { + $this->options[$option->getId()] = $option; + } + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } +} diff --git a/wcfsetup/install/files/lib/event/form/option/SharedConfigurationFormFieldCollecting.class.php b/wcfsetup/install/files/lib/event/form/option/SharedConfigurationFormFieldCollecting.class.php new file mode 100644 index 00000000000..8fa2fcc8f50 --- /dev/null +++ b/wcfsetup/install/files/lib/event/form/option/SharedConfigurationFormFieldCollecting.class.php @@ -0,0 +1,38 @@ + + * @since 6.2 + */ +final class SharedConfigurationFormFieldCollecting implements IPsr14Event +{ + /** + * @var array + */ + private array $formFields = []; + + /** + * Registers a new shared configuration form field. + */ + public function register(IFormField $formField): void + { + $this->formFields[$formField->getId()] = $formField; + } + + /** + * @return array + */ + public function getFormFields(): array + { + return $this->formFields; + } +} diff --git a/wcfsetup/install/files/lib/form/ContactForm.class.php b/wcfsetup/install/files/lib/form/ContactForm.class.php index 6f7e89dc845..67209cf96ea 100644 --- a/wcfsetup/install/files/lib/form/ContactForm.class.php +++ b/wcfsetup/install/files/lib/form/ContactForm.class.php @@ -3,192 +3,157 @@ namespace wcf\form; use wcf\data\contact\option\ContactOption; -use wcf\data\contact\option\ContactOptionAction; +use wcf\data\contact\option\ContactOptionList; +use wcf\data\contact\recipient\ContactRecipient; use wcf\data\contact\recipient\ContactRecipientList; use wcf\event\page\ContactFormSpamChecking; -use wcf\system\attachment\AttachmentHandler; -use wcf\system\email\Mailbox; +use wcf\system\contact\form\SubmitContactForm; use wcf\system\event\EventHandler; -use wcf\system\exception\IllegalLinkException; +use wcf\system\exception\NamedUserException; use wcf\system\exception\PermissionDeniedException; -use wcf\system\exception\UserInputException; -use wcf\system\option\ContactOptionHandler; +use wcf\system\flood\FloodControl; +use wcf\system\form\builder\field\CaptchaFormField; +use wcf\system\form\builder\field\EmailFormField; +use wcf\system\form\builder\field\FileProcessorFormField; +use wcf\system\form\builder\field\IFormField; +use wcf\system\form\builder\field\SelectFormField; +use wcf\system\form\builder\field\TextFormField; use wcf\system\request\LinkHandler; use wcf\system\WCF; use wcf\util\HeaderUtil; -use wcf\util\StringUtil; use wcf\util\UserUtil; /** * Customizable contact form with selectable recipients. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License + * @author Marcel Werk + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License */ -class ContactForm extends AbstractCaptchaForm +class ContactForm extends AbstractFormBuilderForm { - /** - * @var AttachmentHandler - */ - public $attachmentHandler; - - /** - * @var string - */ - public $attachmentObjectType = 'com.woltlab.wcf.contact'; - - /** - * sender email address - * @var string - */ - public $email = ''; - - /** - * sender name - * @var string - */ - public $name = ''; + private const ALLOWED_MAILS_PER_10M = 2; /** * @inheritDoc */ public $neededModules = ['MODULE_CONTACT_FORM']; - /** - * @var ContactOptionHandler - */ - public $optionHandler; - - /** - * recipient id - * @var int - */ - public $recipientID = 0; - - /** - * @var ContactRecipientList - */ - public $recipientList; - - /** - * @var string - */ - public $tmpHash = ''; - - /** - * @inheritDoc - */ - public function readParameters() + #[\Override] + protected function createForm() { - parent::readParameters(); - - $this->optionHandler = new ContactOptionHandler(false); - $this->optionHandler->init(); - - $this->recipientList = new ContactRecipientList(); - $this->recipientList->getConditionBuilder()->add("contact_recipient.isDisabled = ?", [0]); - $this->recipientList->readObjects(); + parent::createForm(); + + $this->form->appendChildren([ + TextFormField::create('name') + ->label('wcf.contact.sender') + ->required() + ->value(WCF::getUser()->username ?: ''), + EmailFormField::create('email') + ->label('wcf.user.email') + ->required() + ->value(WCF::getUser()->email ?: ''), + $this->getRecipientFormField(), + ...$this->getOptionFormFields() + ]); - if (!\count($this->recipientList)) { - throw new IllegalLinkException(); + if (\CONTACT_FORM_ENABLE_ATTACHMENTS) { + $this->form->appendChild($this->getFileUploadFormField()); } - if (isset($_REQUEST['tmpHash'])) { - $this->tmpHash = $_REQUEST['tmpHash']; - } - if (empty($this->tmpHash)) { - $this->tmpHash = StringUtil::getRandomID(); + if (!WCF::getUser()->userID) { + $this->form->appendChild( + CaptchaFormField::create() + ->objectType(\CAPTCHA_TYPE) + ); } } - /** - * @inheritDoc - */ - public function readFormParameters() + #[\Override] + public function validate() { - parent::readFormParameters(); - - $this->optionHandler->readUserInput($_POST); - - if (isset($_POST['email'])) { - $this->email = StringUtil::trim($_POST['email']); - } - if (isset($_POST['name'])) { - $this->name = StringUtil::trim($_POST['name']); - } - if (isset($_POST['recipientID'])) { - $this->recipientID = \intval($_POST['recipientID']); + $requests = FloodControl::getInstance()->countContent( + 'com.woltlab.wcf.contactForm', + new \DateInterval('PT10M') + ); + if ($requests['count'] >= self::ALLOWED_MAILS_PER_10M) { + throw new NamedUserException(WCF::getLanguage()->getDynamicVariable('wcf.page.error.flood')); } + + parent::validate(); } - /** - * @inheritDoc - */ - public function validate() + #[\Override] + public function save() { - // validate file options - $optionHandlerErrors = $this->optionHandler->validate(); + AbstractForm::save(); - parent::validate(); + $formData = $this->form->getData(); + $data = $formData['data']; - if (!empty($optionHandlerErrors)) { - throw new UserInputException('options', $optionHandlerErrors); + $availableRecipients = $this->getAvailableRecipients(); + if (\count($availableRecipients) > 1) { + $recipient = $availableRecipients[$data['recipientID']]; + } else { + $recipient = \reset($availableRecipients); } - if (empty($this->email)) { - throw new UserInputException('email'); - } else { - try { - new Mailbox($this->email); - } catch (\DomainException $e) { - throw new UserInputException('email', 'invalid'); + $optionValues = []; + foreach ($data as $key => $value) { + if (\str_starts_with($key, 'option')) { + $optionValues[\substr($key, 6)] = $value; } } - if (empty($this->name)) { - throw new UserInputException('name'); - } + $this->handleSpamCheck( + $data['email'], + $optionValues + ); - $recipients = $this->recipientList->getObjects(); - if (\count($recipients) === 1) { - $this->recipientID = \reset($recipients)->recipientID; - } else { - if (!$this->recipientID) { - throw new UserInputException('recipientID'); - } + $command = new SubmitContactForm( + $recipient, + $data['name'], + $data['email'], + $optionValues, + $formData['attachments'] ?? [] + ); + $command(); - $isValid = false; - foreach ($recipients as $recipient) { - if ($this->recipientID == $recipient->recipientID) { - $isValid = true; - break; - } - } + $this->saved(); - if (!$isValid) { - throw new UserInputException('recipientID', 'invalid'); - } - } + FloodControl::getInstance()->registerContent('com.woltlab.wcf.contactForm'); + + HeaderUtil::delayedRedirect( + LinkHandler::getInstance()->getLink(), + WCF::getLanguage()->getDynamicVariable('wcf.contact.success') + ); - $this->handleSpamCheck(); + exit; } - private function handleSpamCheck(): void + /** + * @param array $optionValues + */ + private function handleSpamCheck(string $email, array $optionValues): void { $messages = []; - foreach ($this->optionHandler->getOptions() as $option) { - $object = $option['object']; - \assert($object instanceof ContactOption); - if (!$object->isMessage || !$object->getOptionValue()) { + foreach ($this->getAvailableOptions() as $option) { + if (empty($optionValues[$option->optionID])) { + continue; + } + + if (!\in_array($option->optionType, [ + 'text', + 'textarea' + ])) { continue; } - $messages[] = $object->getOptionValue(); + $messages[] = $optionValues[$option->optionID]; } $spamCheckEvent = new ContactFormSpamChecking( - $this->email, + $email, UserUtil::getIpAddress(), $messages, ); @@ -198,72 +163,78 @@ private function handleSpamCheck(): void } } + protected function getRecipientFormField(): SelectFormField + { + $recipients = $this->getAvailableRecipients(); + + return SelectFormField::create('recipientID') + ->label('wcf.contact.recipientID') + ->required() + ->options($recipients) + ->available(\count($recipients) > 1); + } + /** - * @inheritDoc + * @return array */ - public function readData() + protected function getAvailableRecipients(): array { - if (CONTACT_FORM_ENABLE_ATTACHMENTS && $this->attachmentObjectType) { - $this->attachmentHandler = new AttachmentHandler($this->attachmentObjectType, 0, $this->tmpHash, 0); - } + $recipientList = new ContactRecipientList(); + $recipientList->getConditionBuilder()->add("contact_recipient.isDisabled = ?", [0]); + $recipientList->readObjects(); - parent::readData(); + return $recipientList->getObjects(); + } - if (empty($_POST)) { - if (WCF::getUser()->userID) { - $this->email = WCF::getUser()->email; - $this->name = WCF::getUser()->username; - } + /** + * @return array + */ + protected function getAvailableOptions(): array + { + $optionList = new ContactOptionList(); + $optionList->getConditionBuilder()->add("contact_option.isDisabled = ?", [0]); + $optionList->readObjects(); - $this->optionHandler->readData(); - } + return $optionList->getObjects(); } /** - * @inheritDoc + * @return IFormField[] */ - public function save() + protected function getOptionFormFields(): array { - parent::save(); - - $this->objectAction = new ContactOptionAction([], 'send', [ - 'attachmentHandler' => $this->attachmentHandler, - 'email' => $this->email, - 'name' => $this->name, - 'optionHandler' => $this->optionHandler, - 'recipientID' => $this->recipientID, - ]); - $this->objectAction->executeAction(); - - // call saved event - $this->saved(); + $formFields = []; + + foreach ($this->getAvailableOptions() as $option) { + $formField = $option->getFormOption()->getFormField( + 'option' . $option->optionID, + $option->getConfiguration() + ); + $formField->label($option->optionTitle); + $formField->description($option->optionDescription); + + if (!empty($option->getConfiguration()['required'])) { + $formField->required(); + } - HeaderUtil::delayedRedirect( - LinkHandler::getInstance()->getLink(), - WCF::getLanguage()->getDynamicVariable('wcf.contact.success') - ); + $formFields[] = $formField; + } - exit; + return $formFields; } - /** - * @inheritDoc - */ - public function assignVariables() + protected function getFileUploadFormField(): FileProcessorFormField { - parent::assignVariables(); - - WCF::getTPL()->assign([ - 'email' => $this->email, - 'name' => $this->name, - 'options' => $this->optionHandler->getOptions(), - 'recipientList' => $this->recipientList, - 'recipientID' => $this->recipientID, - 'attachmentHandler' => $this->attachmentHandler, - 'attachmentObjectID' => 0, - 'attachmentObjectType' => $this->attachmentObjectType, - 'attachmentParentObjectID' => 0, - 'tmpHash' => $this->tmpHash, - ]); + return FileProcessorFormField::create('attachments') + ->objectType('com.woltlab.wcf.contact.form') + ->label('wcf.contact.attachments') + ->description('wcf.upload.multiple.limits', [ + 'maximumCount' => WCF::getSession()->getPermission('user.contactForm.attachment.maxCount'), + 'maximumSize' => WCF::getSession()->getPermission('user.contactForm.attachment.maxSize'), + 'allowedFileExtensions' => \explode( + "\n", + WCF::getSession()->getPermission('user.contactForm.attachment.allowedExtensions') + ), + ]); } } diff --git a/wcfsetup/install/files/lib/page/ContactAttachmentPage.class.php b/wcfsetup/install/files/lib/page/ContactAttachmentPage.class.php index 7732ac020a9..bad98cca722 100644 --- a/wcfsetup/install/files/lib/page/ContactAttachmentPage.class.php +++ b/wcfsetup/install/files/lib/page/ContactAttachmentPage.class.php @@ -12,6 +12,7 @@ * @author Alexander Ebert * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License + * @deprecated 6.2 Contact form attachments are using `ContactFormFileProcessor` instead. */ class ContactAttachmentPage extends AttachmentPage { diff --git a/wcfsetup/install/files/lib/system/attachment/ContactAttachmentObjectType.class.php b/wcfsetup/install/files/lib/system/attachment/ContactAttachmentObjectType.class.php index 67e047c5e35..01edf79c77f 100644 --- a/wcfsetup/install/files/lib/system/attachment/ContactAttachmentObjectType.class.php +++ b/wcfsetup/install/files/lib/system/attachment/ContactAttachmentObjectType.class.php @@ -14,6 +14,7 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @since 5.2 + * @deprecated 6.2 Contact form attachments are using `ContactFormFileProcessor` instead. * * @extends AbstractAttachmentObjectType */ diff --git a/wcfsetup/install/files/lib/system/contact/form/SubmitContactForm.class.php b/wcfsetup/install/files/lib/system/contact/form/SubmitContactForm.class.php new file mode 100644 index 00000000000..0f68cecd7e8 --- /dev/null +++ b/wcfsetup/install/files/lib/system/contact/form/SubmitContactForm.class.php @@ -0,0 +1,121 @@ + + * @since 6.2 + */ +final class SubmitContactForm +{ + /** + * @param array $optionValues + * @param int[] $fileIDs + */ + public function __construct( + private readonly ContactRecipient $recipient, + private readonly string $senderName, + private readonly string $senderEmail, + private readonly array $optionValues = [], + private readonly array $fileIDs = [] + ) {} + + public function __invoke() + { + $messageData = [ + 'options' => $this->getFormattedOptionValues($this->optionValues), + 'recipient' => $this->recipient, + 'name' => $this->senderName, + 'emailAddress' => $this->senderEmail, + 'attachments' => $this->getFiles($this->fileIDs), + ]; + + $email = new Email(); + $email->addRecipient($this->recipient->getMailbox()); + $email->setSubject(LanguageFactory::getInstance()->getDefaultLanguage()->get('wcf.contact.mail.subject')); + $email->setBody(new MimePartFacade([ + new RecipientAwareTextMimePart('text/html', 'email_contact', 'wcf', $messageData), + new RecipientAwareTextMimePart('text/plain', 'email_contact', 'wcf', $messageData), + ])); + $email->setReplyTo(new Mailbox($this->senderEmail, $this->senderName)); + $email->send(); + } + + /** + * @param array $optionValues + */ + private function getFormattedOptionValues(array $optionValues): array + { + $options = []; + foreach ($this->getAvailableOptions() as $availableOption) { + if (empty($optionValues[$availableOption->optionID])) { + continue; + } + + $value = $optionValues[$availableOption->optionID]; + $configuration = $availableOption->getConfiguration(); + $formOption = $availableOption->getFormOption(); + + $options[] = [ + 'isMessage' => false, // Unused, but is here for backward compatibility in third-party translations. + 'title' => LanguageFactory::getInstance()->getDefaultLanguage()->get($availableOption->optionTitle), + 'value' => $formOption->getPlainTextFormatter()->format( + $value, + LanguageFactory::getInstance()->getDefaultLanguage()->languageID, + $configuration + ), + 'htmlValue' => $formOption->getFormatter()->format( + $value, + LanguageFactory::getInstance()->getDefaultLanguage()->languageID, + $configuration + ), + ]; + } + + return $options; + } + + /** + * @return array + */ + private function getAvailableOptions(): array + { + $optionList = new ContactOptionList(); + $optionList->getConditionBuilder()->add("contact_option.isDisabled = ?", [0]); + $optionList->readObjects(); + + return $optionList->getObjects(); + } + + /** + * @param int[] $fileIDs + * @return array + */ + private function getFiles(array $fileIDs): array + { + if ($fileIDs === []) { + return []; + } + + $list = new FileList(); + $list->setObjectIDs($fileIDs); + $list->readObjects(); + + return $list->getObjects(); + } +} diff --git a/wcfsetup/install/files/lib/system/cronjob/FileCleanUpCronjob.class.php b/wcfsetup/install/files/lib/system/cronjob/FileCleanUpCronjob.class.php index 3df780e0dfb..5a4dbc9c6e7 100644 --- a/wcfsetup/install/files/lib/system/cronjob/FileCleanUpCronjob.class.php +++ b/wcfsetup/install/files/lib/system/cronjob/FileCleanUpCronjob.class.php @@ -4,6 +4,7 @@ use wcf\data\cronjob\Cronjob; use wcf\data\file\FileEditor; +use wcf\data\object\type\ObjectTypeCache; use wcf\system\WCF; /** @@ -28,10 +29,34 @@ public function execute(Cronjob $cronjob) $statement->execute(); $fileIDs = $statement->fetchAll(\PDO::FETCH_COLUMN); + if (\MODULE_CONTACT_FORM && \CONTACT_FORM_PRUNE_ATTACHMENTS > 0) { + $fileIDs = \array_merge($fileIDs, $this->getOldContactFileIDs()); + } + if ($fileIDs === []) { return; } FileEditor::deleteAll($fileIDs); } + + /** + * @return int[] + */ + private function getOldContactFileIDs(): array + { + $sql = "SELECT fileID + FROM wcf1_file + WHERE objectTypeID = ? + AND uploadTime IS NOT NULL + AND uploadTime < ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([ + ObjectTypeCache::getInstance() + ->getObjectTypeIDByName('com.woltlab.wcf.file', 'com.woltlab.wcf.contact.form'), + \TIME_NOW - (\CONTACT_FORM_PRUNE_ATTACHMENTS * 86_400), + ]); + + return $statement->fetchAll(\PDO::FETCH_COLUMN); + } } diff --git a/wcfsetup/install/files/lib/system/file/processor/ContactFormFileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/ContactFormFileProcessor.class.php new file mode 100644 index 00000000000..16e506d4d73 --- /dev/null +++ b/wcfsetup/install/files/lib/system/file/processor/ContactFormFileProcessor.class.php @@ -0,0 +1,88 @@ + + * @since 6.2 + */ +final class ContactFormFileProcessor extends AbstractFileProcessor +{ + private const SESSION_VARIABLE = 'contact_form_file_processor_%d'; + + #[\Override] + public function acceptUpload(string $filename, int $fileSize, array $context): FileProcessorPreflightResult + { + if (!\CONTACT_FORM_ENABLE_ATTACHMENTS) { + return FileProcessorPreflightResult::InsufficientPermissions; + } + + return FileProcessorPreflightResult::Passed; + } + + #[\Override] + public function canAdopt(File $file, array $context): bool + { + return true; + } + + #[\Override] + public function adopt(File $file, array $context): void + { + // Save the `fileID` in the session variable so that the current user can download or delete it. + WCF::getSession()->register(\sprintf(self::SESSION_VARIABLE, $file->fileID), TIME_NOW); + WCF::getSession()->update(); + } + + #[\Override] + public function getMaximumCount(array $context): ?int + { + return WCF::getSession()->getPermission('user.contactForm.attachment.maxCount'); + } + + #[\Override] + public function getAllowedFileExtensions(array $context): array + { + return \explode("\n", WCF::getSession()->getPermission('user.contactForm.attachment.allowedExtensions')); + } + + #[\Override] + public function getMaximumSize(array $context): ?int + { + return WCF::getSession()->getPermission('user.contactForm.attachment.maxSize'); + } + + #[\Override] + public function canDelete(File $file): bool + { + return WCF::getSession()->getVar( + \sprintf(self::SESSION_VARIABLE, $file->fileID) + ) !== null; + } + + #[\Override] + public function canDownload(File $file): bool + { + if (WCF::getSession()->getPermission('admin.contact.canManageContactForm')) { + return true; + } + + return WCF::getSession()->getVar( + \sprintf(self::SESSION_VARIABLE, $file->fileID) + ) !== null; + } + + #[\Override] + public function delete(array $fileIDs, array $thumbnailIDs): void {} + + #[\Override] + public function getObjectTypeName(): string + { + return 'com.woltlab.wcf.contact.form'; + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/field/SelectOptionsFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/SelectOptionsFormField.class.php new file mode 100644 index 00000000000..f6651d6048b --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/SelectOptionsFormField.class.php @@ -0,0 +1,83 @@ + + * @since 6.2 + */ +final class SelectOptionsFormField extends AbstractFormField implements + ICssClassFormField, + IImmutableFormField +{ + use TCssClassFormField; + use TImmutableFormField; + + /** + * @inheritDoc + */ + protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Value'; + + /** + * @inheritDoc + */ + protected $templateName = 'shared_selectOptionsFormField'; + + #[\Override] + public function readValue() + { + if ($this->getDocument()->hasRequestData($this->getPrefixedId())) { + $value = $this->getDocument()->getRequestData($this->getPrefixedId()); + + if (\is_string($value)) { + $this->value = $value !== '' ? $value : null; + } + } + + return $this; + } + + #[\Override] + public function getHtmlVariables() + { + return [ + 'availableLanguages' => LanguageFactory::getInstance()->getLanguages(), + ]; + } + + #[\Override] + public function validate() + { + try { + $mapper = (new MapperBuilder())->mapper(); + $mapper->map( + <<<'EOT' + list, + }> + EOT, + Source::json($this->getValue()) + ); + } catch (MappingError) { + $this->addValidationError(new FormFieldValidationError('empty')); + } + + parent::validate(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/AbstractFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/AbstractFormOption.class.php new file mode 100644 index 00000000000..7fafda27115 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/AbstractFormOption.class.php @@ -0,0 +1,57 @@ + + * @since 6.2 + */ +abstract class AbstractFormOption implements IFormOption +{ + #[\Override] + public function getConfigurationFormFields(): array + { + return ['required']; + } + + #[\Override] + public function getTitle(): string + { + return WCF::getLanguage()->get('wcf.form.option.' . $this->getId()); + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new DefaultFormatter(); + } + + #[\Override] + public function getPlainTextFormatter(): IFormOptionFormatter + { + return new DefaultPlainTextFormatter(); + } + + #[\Override] + public function getFilterFormField(string $id, array $configuration = []): AbstractFormField + { + return $this->getFormField($id, $configuration); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $list, string $columnName, mixed $value): void + { + $list->getConditionBuilder()->add("{$columnName} = ?", [$value]); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/BooleanFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/BooleanFormOption.class.php new file mode 100644 index 00000000000..bfca00989a7 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/BooleanFormOption.class.php @@ -0,0 +1,43 @@ + + * @since 6.2 + */ +class BooleanFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'boolean'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + return BooleanFormField::create($id); + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new BooleanFormatter(); + } + + #[\Override] + public function getPlainTextFormatter(): IFormOptionFormatter + { + return $this->getFormatter(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/CheckboxesFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/CheckboxesFormOption.class.php new file mode 100644 index 00000000000..a6d95198cb2 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/CheckboxesFormOption.class.php @@ -0,0 +1,54 @@ + + * @since 6.2 + */ +class CheckboxesFormOption extends AbstractFormOption +{ + use TSelectOptionsFormOption; + + #[\Override] + public function getId(): string + { + return 'checkboxes'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + $formField = MultipleSelectionFormField::create($id); + $this->setSelectOptions($formField, $configuration); + + return $formField; + } + + #[\Override] + public function getConfigurationFormFields(): array + { + return ['selectOptions', 'required']; + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new MultipleSelectionFormatter(); + } + + #[\Override] + public function getPlainTextFormatter(): IFormOptionFormatter + { + return $this->getFormatter(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/CurrencyFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/CurrencyFormOption.class.php new file mode 100644 index 00000000000..f23b074db50 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/CurrencyFormOption.class.php @@ -0,0 +1,60 @@ + + * @since 6.2 + */ +class CurrencyFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'currency'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + $formField = CurrencyFormField::create($id); + if (isset($configuration['currency'])) { + $formField->currency($configuration['currency']); + } + if (isset($configuration['minFloatValue'])) { + $formField->minimum($configuration['minFloatValue']); + } + if (isset($configuration['maxFloatValue'])) { + $formField->maximum($configuration['maxFloatValue']); + } + + return $formField; + } + + #[\Override] + public function getConfigurationFormFields(): array + { + return ['currency', 'minFloatValue', 'maxFloatValue', 'required']; + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new CurrencyFormatter(); + } + + #[\Override] + public function getPlainTextFormatter(): IFormOptionFormatter + { + return $this->getFormatter(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/DateFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/DateFormOption.class.php new file mode 100644 index 00000000000..de3a22e7780 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/DateFormOption.class.php @@ -0,0 +1,46 @@ + + * @since 6.2 + */ +class DateFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'date'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + $formField = DateFormField::create($id) + ->saveValueFormat('Y-m-d'); + + return $formField; + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new DateFormatter(); + } + + #[\Override] + public function getPlainTextFormatter(): IFormOptionFormatter + { + return $this->getFormatter(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/EmailFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/EmailFormOption.class.php new file mode 100644 index 00000000000..e23e3ef0d96 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/EmailFormOption.class.php @@ -0,0 +1,58 @@ + + * @since 6.2 + */ +class EmailFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'email'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + return EmailFormField::create($id); + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new EmailFormatter(); + } + + #[\Override] + public function getPlainTextFormatter(): IFormOptionFormatter + { + return $this->getFormatter(); + } + + #[\Override] + public function getFilterFormField(string $id, array $configuration = []): AbstractFormField + { + return TextFormField::create($id); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $list, string $columnName, mixed $value): void + { + $list->getConditionBuilder()->add("{$columnName} LIKE ?", ['%' . WCF::getDB()->escapeLikeValue($value) . '%']); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/FloatFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/FloatFormOption.class.php new file mode 100644 index 00000000000..2bfbe762097 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/FloatFormOption.class.php @@ -0,0 +1,57 @@ + + * @since 6.2 + */ +class FloatFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'float'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + $formField = FloatFormField::create($id); + if (isset($configuration['minValue'])) { + $formField->minimum($configuration['minValue']); + } + if (isset($configuration['maxValue'])) { + $formField->maximum($configuration['maxValue']); + } + + return $formField; + } + + #[\Override] + public function getConfigurationFormFields(): array + { + return ['minFloatValue', 'maxFloatValue', 'required']; + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new FloatFormatter(); + } + + #[\Override] + public function getPlainTextFormatter(): IFormOptionFormatter + { + return $this->getFormatter(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/FormOptionHandler.class.php b/wcfsetup/install/files/lib/system/form/option/FormOptionHandler.class.php new file mode 100644 index 00000000000..0f13266d8e4 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/FormOptionHandler.class.php @@ -0,0 +1,103 @@ + + * @since 6.2 + */ +final class FormOptionHandler extends SingletonFactory +{ + /** + * @var array + */ + private array $options; + + #[\Override] + protected function init() + { + $this->options = \array_merge($this->getDefaultFormOptions(), $this->getEventFormOptions()); + } + + /** + * @return array + */ + private function getDefaultFormOptions(): array + { + $options = []; + + foreach ( + [ + new BooleanFormOption(), + new CheckboxesFormOption(), + new CurrencyFormOption(), + new DateFormOption(), + new EmailFormOption(), + new FloatFormOption(), + new IconFormOption(), + new IntegerFormOption(), + new RadioButtonFormOption(), + new RatingFormOption(), + new SelectFormOption(), + new SourceCodeFormOption(), + new TextFormOption(), + new TextareaFormOption(), + new UrlFormOption(), + new WysiwygFormOption(), + ] as $defaultOption + ) { + $options[$defaultOption->getId()] = $defaultOption; + } + + return $options; + } + + /** + * @return array + */ + private function getEventFormOptions(): array + { + $event = new FormOptionCollecting(); + EventHandler::getInstance()->fire($event); + + return $event->getOptions(); + } + + /** + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + public function getOption(string $identifier): ?IFormOption + { + return $this->options[$identifier] ?? null; + } + + /** + * @return array + */ + public function getSortedOptionTypes(): array + { + $optionTypes = \array_map(fn($option) => $option->getTitle(), $this->getOptions()); + + $collator = new \Collator(WCF::getLanguage()->getLocale()); + \uasort( + $optionTypes, + static fn(string $a, string $b) => $collator->compare($a, $b) + ); + + return $optionTypes; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/IFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/IFormOption.class.php new file mode 100644 index 00000000000..ae79d3f20ca --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/IFormOption.class.php @@ -0,0 +1,43 @@ + + * @since 6.2 + */ +interface IFormOption +{ + public function getId(): string; + + public function getTitle(): string; + + /** + * @param array $configuration + */ + public function getFormField(string $id, array $configuration = []): AbstractFormField; + + /** + * @param array $configuration + */ + public function getFilterFormField(string $id, array $configuration = []): AbstractFormField; + + /** + * @return string[] + */ + public function getConfigurationFormFields(): array; + + public function getFormatter(): IFormOptionFormatter; + + public function getPlainTextFormatter(): IFormOptionFormatter; + + public function applyFilter(DatabaseObjectList $list, string $columnName, mixed $value): void; +} diff --git a/wcfsetup/install/files/lib/system/form/option/IconFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/IconFormOption.class.php new file mode 100644 index 00000000000..672e989641c --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/IconFormOption.class.php @@ -0,0 +1,37 @@ + + * @since 6.2 + */ +class IconFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'icon'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + return IconFormField::create($id); + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new IconFormatter(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/IntegerFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/IntegerFormOption.class.php new file mode 100644 index 00000000000..32e0f6350b7 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/IntegerFormOption.class.php @@ -0,0 +1,57 @@ + + * @since 6.2 + */ +class IntegerFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'integer'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + $formField = IntegerFormField::create($id); + if (isset($configuration['minIntegerValue'])) { + $formField->minimum($configuration['minIntegerValue']); + } + if (isset($configuration['maxIntegerValue'])) { + $formField->maximum($configuration['maxIntegerValue']); + } + + return $formField; + } + + #[\Override] + public function getConfigurationFormFields(): array + { + return ['minIntegerValue', 'maxIntegerValue', 'required']; + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new IntegerFormatter(); + } + + #[\Override] + public function getPlainTextFormatter(): IFormOptionFormatter + { + return $this->getFormatter(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/RadioButtonFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/RadioButtonFormOption.class.php new file mode 100644 index 00000000000..9f06399f14c --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/RadioButtonFormOption.class.php @@ -0,0 +1,34 @@ + + * @since 6.2 + */ +class RadioButtonFormOption extends SelectFormOption +{ + use TSelectOptionsFormOption; + + #[\Override] + public function getId(): string + { + return 'radioButton'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + $formField = RadioButtonFormField::create($id); + $this->setSelectOptions($formField, $configuration); + + return $formField; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/RatingFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/RatingFormOption.class.php new file mode 100644 index 00000000000..318b9a02187 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/RatingFormOption.class.php @@ -0,0 +1,37 @@ + + * @since 6.2 + */ +class RatingFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'rating'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + return RatingFormField::create($id); + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new RatingFormatter(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/SelectFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/SelectFormOption.class.php new file mode 100644 index 00000000000..9a520906d6a --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/SelectFormOption.class.php @@ -0,0 +1,54 @@ + + * @since 6.2 + */ +class SelectFormOption extends AbstractFormOption +{ + use TSelectOptionsFormOption; + + #[\Override] + public function getId(): string + { + return 'select'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + $formField = SelectFormField::create($id); + $this->setSelectOptions($formField, $configuration); + + return $formField; + } + + #[\Override] + public function getConfigurationFormFields(): array + { + return ['selectOptions', 'required']; + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new SelectFormatter(); + } + + #[\Override] + public function getPlainTextFormatter(): IFormOptionFormatter + { + return new SelectFormatter(false); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/SharedConfigurationFormFields.class.php b/wcfsetup/install/files/lib/system/form/option/SharedConfigurationFormFields.class.php new file mode 100644 index 00000000000..ad9f0711dcf --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/SharedConfigurationFormFields.class.php @@ -0,0 +1,90 @@ + + * @since 6.2 + */ +final class SharedConfigurationFormFields +{ + /** + * @var array + */ + private array $formFields; + + public function __construct() + { + $this->formFields = \array_merge($this->getDefaultFormFields(), $this->getEventFormFields()); + } + + /** + * @return array + */ + private function getDefaultFormFields(): array + { + return [ + 'currency' => TextFormField::create('currency') + ->label('wcf.form.option.shared.currency') + ->value('EUR') + ->addFieldClass('short') + ->required(), + 'defaultTextValue' => TextFormField::create('defaultTextValue') + ->label('wcf.form.option.shared.defaultValue') + ->addFieldClass('medium'), + 'maxLength' => IntegerFormField::create('maxLength') + ->label('wcf.form.option.shared.maxLength'), + 'minIntegerValue' => IntegerFormField::create('minIntegerValue') + ->label('wcf.form.option.shared.minValue'), + 'maxIntegerValue' => IntegerFormField::create('maxIntegerValue') + ->label('wcf.form.option.shared.maxValue'), + 'minFloatValue' => FloatFormField::create('minFloatValue') + ->label('wcf.form.option.shared.minValue'), + 'maxFloatValue' => FloatFormField::create('maxFloatValue') + ->label('wcf.form.option.shared.maxValue'), + 'selectOptions' => SelectOptionsFormField::create('selectOptions') + ->label('wcf.form.option.shared.selectOptions') + ->required(), + 'required' => BooleanFormField::create('required') + ->label('wcf.form.option.shared.required') + ->value(false), + ]; + } + + /** + * @return array + */ + private function getEventFormFields(): array + { + $event = new SharedConfigurationFormFieldCollecting(); + EventHandler::getInstance()->fire($event); + + return $event->getFormFields(); + } + + /** + * @return array + */ + public function getFormFields(): array + { + return $this->formFields; + } + + public function getFormField(string $identifier): ?IFormField + { + return $this->formFields[$identifier] ?? null; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/SourceCodeFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/SourceCodeFormOption.class.php new file mode 100644 index 00000000000..ec1e5e74bf5 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/SourceCodeFormOption.class.php @@ -0,0 +1,52 @@ + + * @since 6.2 + */ +class SourceCodeFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'sourceCode'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + return SourceCodeFormField::create($id); + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new SourceCodeFormatter(); + } + + #[\Override] + public function getFilterFormField(string $id, array $configuration = []): AbstractFormField + { + return TextFormField::create($id); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $list, string $columnName, mixed $value): void + { + $list->getConditionBuilder()->add("{$columnName} LIKE ?", ['%' . WCF::getDB()->escapeLikeValue($value) . '%']); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/TSelectOptionsFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/TSelectOptionsFormOption.class.php new file mode 100644 index 00000000000..5e0acc9a18b --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/TSelectOptionsFormOption.class.php @@ -0,0 +1,43 @@ + + * @since 6.2 + */ +trait TSelectOptionsFormOption +{ + /** + * @param array $configuration + */ + protected function setSelectOptions(ISelectionFormField $formField, array $configuration): void + { + if (!isset($configuration['selectOptions'])) { + return; + } + + $selectOptions = []; + foreach (JSON::decode($configuration['selectOptions']) as $selectOption) { + if (isset($selectOption['value'][0])) { + $value = $selectOption['value'][0]; + } else if (isset($selectOption['value'][WCF::getLanguage()->languageID])) { + $value = $selectOption['value'][WCF::getLanguage()->languageID]; + } else { + $value = reset($selectOption['value']); + } + + $selectOptions[$selectOption['key']] = $value; + } + + $formField->options($selectOptions); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/TextFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/TextFormOption.class.php new file mode 100644 index 00000000000..158891d1c4a --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/TextFormOption.class.php @@ -0,0 +1,57 @@ + + * @since 6.2 + */ +class TextFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'text'; + } + + #[\Override] + public function getFilterFormField(string $id, array $configuration = []): AbstractFormField + { + return TextFormField::create($id); + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + $formField = TextFormField::create($id); + if (!empty($configuration['maxLength'])) { + $formField->maximumLength($configuration['maxLength']); + } + if (isset($configuration['defaultTextValue'])) { + $formField->value($configuration['defaultTextValue']); + } + + return $formField; + } + + #[\Override] + public function getConfigurationFormFields(): array + { + return ['maxLength', 'defaultTextValue', 'required']; + } + + #[\Override] + public function applyFilter(DatabaseObjectList $list, string $columnName, mixed $value): void + { + $list->getConditionBuilder()->add("{$columnName} LIKE ?", ['%' . WCF::getDB()->escapeLikeValue($value) . '%']); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/TextareaFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/TextareaFormOption.class.php new file mode 100644 index 00000000000..104bbca3362 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/TextareaFormOption.class.php @@ -0,0 +1,63 @@ + + * @since 6.2 + */ +class TextareaFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'textarea'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + $formField = MultilineTextFormField::create($id); + if (!empty($configuration['maxLength'])) { + $formField->maximumLength($configuration['maxLength']); + } + + return $formField; + } + + #[\Override] + public function getConfigurationFormFields(): array + { + return ['maxLength', 'required']; + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new MultilineTextFormatter(); + } + + #[\Override] + public function getFilterFormField(string $id, array $configuration = []): AbstractFormField + { + return TextFormField::create($id); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $list, string $columnName, mixed $value): void + { + $list->getConditionBuilder()->add("{$columnName} LIKE ?", ['%' . WCF::getDB()->escapeLikeValue($value) . '%']); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/UrlFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/UrlFormOption.class.php new file mode 100644 index 00000000000..69666ea5c74 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/UrlFormOption.class.php @@ -0,0 +1,52 @@ + + * @since 6.2 + */ +class UrlFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'url'; + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + return UrlFormField::create($id); + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new UrlFormatter(); + } + + #[\Override] + public function getFilterFormField(string $id, array $configuration = []): AbstractFormField + { + return TextFormField::create($id); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $list, string $columnName, mixed $value): void + { + $list->getConditionBuilder()->add("{$columnName} LIKE ?", ['%' . WCF::getDB()->escapeLikeValue($value) . '%']); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/WysiwygFormOption.class.php b/wcfsetup/install/files/lib/system/form/option/WysiwygFormOption.class.php new file mode 100644 index 00000000000..02b03018e88 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/WysiwygFormOption.class.php @@ -0,0 +1,66 @@ + + * @since 6.2 + */ +class WysiwygFormOption extends AbstractFormOption +{ + #[\Override] + public function getId(): string + { + return 'wysiwyg'; + } + + #[\Override] + public function getFilterFormField(string $id, array $configuration = []): AbstractFormField + { + return TextFormField::create($id); + } + + #[\Override] + public function getFormField(string $id, array $configuration = []): AbstractFormField + { + return WysiwygFormField::create($id) + ->objectType('com.woltlab.wcf.genericFormOption'); + } + + #[\Override] + public function getConfigurationFormFields(): array + { + return ['required']; + } + + #[\Override] + public function applyFilter(DatabaseObjectList $list, string $columnName, mixed $value): void + { + $list->getConditionBuilder()->add("{$columnName} LIKE ?", ['%' . WCF::getDB()->escapeLikeValue($value) . '%']); + } + + #[\Override] + public function getFormatter(): IFormOptionFormatter + { + return new WysiwygFormatter(); + } + + #[\Override] + public function getPlainTextFormatter(): IFormOptionFormatter + { + return new WysiwygPlainTextFormatter(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/BooleanFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/BooleanFormatter.class.php new file mode 100644 index 00000000000..05ff91dc0d9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/BooleanFormatter.class.php @@ -0,0 +1,24 @@ + + * @since 6.2 + */ +final class BooleanFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + if (!$value) { + return ''; + } + + return '✔️'; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/CurrencyFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/CurrencyFormatter.class.php new file mode 100644 index 00000000000..f1a1ad4c9b8 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/CurrencyFormatter.class.php @@ -0,0 +1,36 @@ + + * @since 6.2 + */ +final class CurrencyFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + $showDecimals = $value % 100 !== 0; + $value /= 100; + $language = LanguageFactory::getInstance()->getLanguage($languageID); + $suffix = ''; + if (!empty($configuration['currency'])) { + $suffix = ' ' . StringUtil::encodeHTML($configuration['currency']); + } + + return \number_format( + \round($value, 2), + $showDecimals ? 2 : 0, + $language->get('wcf.global.decimalPoint'), + $language->get('wcf.global.thousandsSeparator') + ) . $suffix; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/DateFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/DateFormatter.class.php new file mode 100644 index 00000000000..f6b2113af87 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/DateFormatter.class.php @@ -0,0 +1,32 @@ + + * @since 6.2 + */ +final class DateFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + $dateTime = new \DateTimeImmutable($value); + $locale = LanguageFactory::getInstance()->getLanguage($languageID)->getLocale(); + + return \IntlDateFormatter::formatObject( + $dateTime, + [ + \IntlDateFormatter::LONG, + \IntlDateFormatter::NONE, + ], + $locale + ); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/DefaultFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/DefaultFormatter.class.php new file mode 100644 index 00000000000..429cede31bd --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/DefaultFormatter.class.php @@ -0,0 +1,22 @@ + + * @since 6.2 + */ +final class DefaultFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + return StringUtil::encodeHTML($value); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/DefaultPlainTextFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/DefaultPlainTextFormatter.class.php new file mode 100644 index 00000000000..576c9fe2b2a --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/DefaultPlainTextFormatter.class.php @@ -0,0 +1,20 @@ + + * @since 6.2 + */ +final class DefaultPlainTextFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + return $value; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/EmailFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/EmailFormatter.class.php new file mode 100644 index 00000000000..d8d2b12456f --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/EmailFormatter.class.php @@ -0,0 +1,24 @@ + + * @since 6.2 + */ +final class EmailFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + $encodedValue = StringUtil::encodeHTML($value); + + return '' . $encodedValue . ''; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/FloatFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/FloatFormatter.class.php new file mode 100644 index 00000000000..a5b0d81af1e --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/FloatFormatter.class.php @@ -0,0 +1,22 @@ + + * @since 6.2 + */ +final class FloatFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + return StringUtil::formatNumeric(\floatval($value)); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/IFormOptionFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/IFormOptionFormatter.class.php new file mode 100644 index 00000000000..20fbe29b9fa --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/IFormOptionFormatter.class.php @@ -0,0 +1,16 @@ + + * @since 6.2 + */ +interface IFormOptionFormatter +{ + public function format(string $value, int $languageID, array $configuration): string; +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/IconFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/IconFormatter.class.php new file mode 100644 index 00000000000..2f8607300cf --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/IconFormatter.class.php @@ -0,0 +1,22 @@ + + * @since 6.2 + */ +final class IconFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + return FontAwesomeIcon::fromString($value)->toHtml(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/IntegerFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/IntegerFormatter.class.php new file mode 100644 index 00000000000..9a6a9d54741 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/IntegerFormatter.class.php @@ -0,0 +1,22 @@ + + * @since 6.2 + */ +final class IntegerFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + return StringUtil::formatNumeric(\intval($value)); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/MultilineTextFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/MultilineTextFormatter.class.php new file mode 100644 index 00000000000..2d16a6af3b9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/MultilineTextFormatter.class.php @@ -0,0 +1,22 @@ + + * @since 6.2 + */ +final class MultilineTextFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + return \nl2br(StringUtil::encodeHTML($value), false); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/MultipleSelectionFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/MultipleSelectionFormatter.class.php new file mode 100644 index 00000000000..c61aad2caf7 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/MultipleSelectionFormatter.class.php @@ -0,0 +1,51 @@ + + * @since 6.2 + */ +final class MultipleSelectionFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + if (!$value) { + return ''; + }; + + $keys = \explode("\n", $value); + $selectOptions = JSON::decode($configuration['selectOptions']); + $html = ''; + + foreach ($keys as $key) { + foreach ($selectOptions as $selectOption) { + if ($selectOption['key'] == $key) { + if (isset($selectOption['value'][0])) { + $value = $selectOption['value'][0]; + } else if (isset($selectOption['value'][$languageID])) { + $value = $selectOption['value'][$languageID]; + } else { + $value = reset($selectOption['value']); + } + + if ($html !== '') { + $html .= ', '; + } + + $html .= StringUtil::encodeHTML($value); + } + } + } + + return $html; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/RatingFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/RatingFormatter.class.php new file mode 100644 index 00000000000..cb06ebf10eb --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/RatingFormatter.class.php @@ -0,0 +1,35 @@ + + * @since 6.2 + */ +final class RatingFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + $rating = \floatval($value); + $html = ''; + + for ($i = 1; $i <= 5; $i++) { + if ($rating >= $i) { + $html .= FontAwesomeIcon::fromString('star;true')->toHtml(); + } else if ($rating + 0.5 >= $i) { + $html .= FontAwesomeIcon::fromString('star-half-stroke;false')->toHtml(); + } else { + $html .= FontAwesomeIcon::fromString('star;false')->toHtml(); + } + } + + return $html; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/SelectFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/SelectFormatter.class.php new file mode 100644 index 00000000000..989d72e0028 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/SelectFormatter.class.php @@ -0,0 +1,43 @@ + + * @since 6.2 + */ +final class SelectFormatter implements IFormOptionFormatter +{ + public function __construct(private readonly bool $encode = true) {} + + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + foreach (JSON::decode($configuration['selectOptions']) as $selectOption) { + if ($selectOption['key'] == $value) { + if (isset($selectOption['value'][0])) { + $value = $selectOption['value'][0]; + } else if (isset($selectOption['value'][$languageID])) { + $value = $selectOption['value'][$languageID]; + } else { + $value = reset($selectOption['value']); + } + + if ($this->encode) { + return StringUtil::encodeHTML($value); + } + + return $value; + } + } + + return ''; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/SourceCodeFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/SourceCodeFormatter.class.php new file mode 100644 index 00000000000..f45786fb5fa --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/SourceCodeFormatter.class.php @@ -0,0 +1,22 @@ + + * @since 6.2 + */ +final class SourceCodeFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + return '
' . StringUtil::encodeHTML($value) . '
'; + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/UrlFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/UrlFormatter.class.php new file mode 100644 index 00000000000..11c7c032da5 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/UrlFormatter.class.php @@ -0,0 +1,22 @@ + + * @since 6.2 + */ +final class UrlFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + return StringUtil::getAnchorTag($value, $value, true, true); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/WysiwygFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/WysiwygFormatter.class.php new file mode 100644 index 00000000000..0803b2db328 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/WysiwygFormatter.class.php @@ -0,0 +1,25 @@ + + * @since 6.2 + */ +final class WysiwygFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + $processor = new HtmlOutputProcessor(); + $processor->process($value, 'com.woltlab.wcf.genericFormOption', 0, true, $languageID); + + return $processor->getHtml(); + } +} diff --git a/wcfsetup/install/files/lib/system/form/option/formatter/WysiwygPlainTextFormatter.class.php b/wcfsetup/install/files/lib/system/form/option/formatter/WysiwygPlainTextFormatter.class.php new file mode 100644 index 00000000000..11945644321 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/option/formatter/WysiwygPlainTextFormatter.class.php @@ -0,0 +1,26 @@ + + * @since 6.2 + */ +final class WysiwygPlainTextFormatter implements IFormOptionFormatter +{ + #[\Override] + public function format(string $value, int $languageID, array $configuration): string + { + $processor = new HtmlOutputProcessor(); + $processor->setOutputType('text/plain'); + $processor->process($value, 'com.woltlab.wcf.genericFormOption', 0, true, $languageID); + + return $processor->getHtml(); + } +} diff --git a/wcfsetup/install/files/lib/system/listView/filter/FormOptionFilter.class.php b/wcfsetup/install/files/lib/system/listView/filter/FormOptionFilter.class.php new file mode 100644 index 00000000000..0bf1c433371 --- /dev/null +++ b/wcfsetup/install/files/lib/system/listView/filter/FormOptionFilter.class.php @@ -0,0 +1,55 @@ + + * @since 6.2 + */ +final class FormOptionFilter extends AbstractFilter +{ + /** + * @param arrayoption->getFilterFormField($this->id, $this->configuration)->label($this->languageItem); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $list, string $value): void + { + $this->option->applyFilter($list, $this->getDatabaseColumnName($list), $value); + } + + #[\Override] + public function renderValue(string $value): string + { + return $this->option->getPlainTextFormatter()->format( + $value, + WCF::getLanguage()->languageID, + $this->configuration + ); + } +} diff --git a/wcfsetup/install/files/lib/system/option/CustomOptionHandler.class.php b/wcfsetup/install/files/lib/system/option/CustomOptionHandler.class.php index fa1e2311174..f1dec15540e 100644 --- a/wcfsetup/install/files/lib/system/option/CustomOptionHandler.class.php +++ b/wcfsetup/install/files/lib/system/option/CustomOptionHandler.class.php @@ -18,6 +18,7 @@ * @template TOption of CustomOption = CustomOption * @extends OptionHandler * @phpstan-import-type ParsedOption from OptionHandler + * @deprecated 6.2 Use `IFormOption` instead */ abstract class CustomOptionHandler extends OptionHandler { diff --git a/wcfsetup/install/files/style/layout/form.scss b/wcfsetup/install/files/style/layout/form.scss index cc78a7fff84..f6cd3f56a32 100644 --- a/wcfsetup/install/files/style/layout/form.scss +++ b/wcfsetup/install/files/style/layout/form.scss @@ -358,3 +358,14 @@ html[data-color-scheme="dark"] .passwordStrengthScore { --score-3: #689f38; --score-4: #1b5e20; } + +.selectOptionsList { + display: flex; + flex-direction: column; + gap: 3px; +} + +.selectOptionsListItem { + display: flex; + align-items: center; +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 00c7421d0f4..72edadeb1cc 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -3529,22 +3529,20 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt - getTitle()} [URL: {@$attachment->getLink()} ] +- {unsafe:$attachment->getTitle()} [URL: {unsafe:$attachment->getLink()} ] {/foreach} {if CONTACT_FORM_PRUNE_ATTACHMENTS}(Dateianhänge werden nach {if CONTACT_FORM_PRUNE_ATTACHMENTS == 1}einem Tag{else}{CONTACT_FORM_PRUNE_ATTACHMENTS} Tagen{/if} gelöscht.){/if} {* this line ends with a space *} {/if}]]> @@ -3554,7 +3552,7 @@ Dateianhänge:


E-Mail-Adresse: {$emailAddress}

{foreach from=$options item=option} -

{@$option['title']}: {@$option['htmlValue']}

+

{unsafe:$option['title']}: {unsafe:$option['htmlValue']}

{/foreach} {if !$attachments|empty}


@@ -3574,9 +3572,8 @@ Dateianhänge: - + - *]]>
@@ -4115,6 +4112,32 @@ Dateianhänge: 1}{#$minimum} Dateien{else}eine Datei{/if} hochladen.]]> 1}{#$maximum} Dateien{else}eine Datei{/if} hochladen.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -5583,6 +5606,11 @@ Benachrichtigungen auf {PAGE_TITLE|phra Ersetzen, um die Datei zu ersetzen.]]> + +Erlaubte Dateiendungen: {', '|implode:$allowedFileExtensions}]]> + +Maximale Dateigröße: {$maximumSize|filesize}
+Erlaubte Dateiendungen: {', '|implode:$allowedFileExtensions}]]>
diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 98961d44d1f..931c376811e 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -3454,22 +3454,20 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi - getTitle()} [URL: {@$attachment->getLink()} ] +- {unsafe:$attachment->getTitle()} [URL: {unsafe:$attachment->getLink()} ] {/foreach} {if CONTACT_FORM_PRUNE_ATTACHMENTS}(Attachments are removed after {if CONTACT_FORM_PRUNE_ATTACHMENTS == 1}one day{else}{CONTACT_FORM_PRUNE_ATTACHMENTS} days{/if}.){/if} {* this line ends with a space *} {/if}]]> @@ -3479,7 +3477,7 @@ Attachments:


Email: {$emailAddress}

{foreach from=$options item=option} -

{@$option['title']}: {@$option['htmlValue']}

+

{unsafe:$option['title']}: {unsafe:$option['htmlValue']}

{/foreach} {if !$attachments|empty}


@@ -3499,9 +3497,8 @@ Attachments: - + - *]]> @@ -4061,6 +4058,32 @@ Attachments: 1}{#$minimum} files{else}one file{/if}.]]> 1}{#$maximum} files{else}one file{/if}.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -5585,6 +5608,11 @@ your notifications on {PAGE_TITLE|phras Replace instead to swap out the file.]]> + +Allowed extensions: {', '|implode:$allowedFileExtensions}]]> + +Maximum file size: {$maximumSize|filesize}
+Allowed extensions: {', '|implode:$allowedFileExtensions}]]>
diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index e273b4af331..185ebbf105a 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -466,10 +466,7 @@ CREATE TABLE wcf1_contact_option ( optionTitle VARCHAR(255) NOT NULL DEFAULT '', optionDescription TEXT, optionType VARCHAR(255) NOT NULL DEFAULT '', - defaultValue MEDIUMTEXT, - validationPattern TEXT, - selectOptions MEDIUMTEXT, - required TINYINT(1) NOT NULL DEFAULT 0, + configuration MEDIUMTEXT, showOrder INT(10) NOT NULL DEFAULT 0, isDisabled TINYINT(1) NOT NULL DEFAULT 0, originIsSystem TINYINT(1) NOT NULL DEFAULT 0 @@ -609,7 +606,8 @@ CREATE TABLE wcf1_file ( mimeType VARCHAR(255) NOT NULL, width INT, height INT, - fileHashWebp CHAR(64) + fileHashWebp CHAR(64), + uploadTime INT ); DROP TABLE IF EXISTS wcf1_file_temporary; @@ -2574,8 +2572,8 @@ INSERT INTO wcf1_template_group (parentTemplateGroupID, templateGroupName, templ INSERT INTO wcf1_template_group (parentTemplateGroupID, templateGroupName, templateGroupFolderName) VALUES (NULL, 'wcf.acp.template.group.shared', '_wcf_shared/'); -- default options: subject and message -INSERT INTO wcf1_contact_option (optionID, optionTitle, optionDescription, optionType, required, showOrder, originIsSystem) VALUES (1, 'wcf.contact.option1', 'wcf.contact.optionDescription1', 'text', 1, 1, 1); -INSERT INTO wcf1_contact_option (optionID, optionTitle, optionDescription, optionType, required, showOrder, originIsSystem) VALUES (2, 'wcf.contact.option2', '', 'textarea', 1, 1, 1); +INSERT INTO wcf1_contact_option (optionID, optionTitle, optionDescription, optionType, configuration, showOrder, originIsSystem) VALUES (1, 'wcf.contact.option1', 'wcf.contact.optionDescription1', 'text', '{\"required\":1}', 1, 1); +INSERT INTO wcf1_contact_option (optionID, optionTitle, optionDescription, optionType, configuration, showOrder, originIsSystem) VALUES (2, 'wcf.contact.option2', '', 'textarea', '{\"required\":1}', 1, 1); -- default recipient: site administrator INSERT INTO wcf1_contact_recipient (recipientID, name, email, isAdministrator, originIsSystem) VALUES (1, 'wcf.contact.recipient.name1', '', 1, 1);