diff --git a/com.woltlab.wcf/package.xml b/com.woltlab.wcf/package.xml index 882df82c685..79930be5e66 100644 --- a/com.woltlab.wcf/package.xml +++ b/com.woltlab.wcf/package.xml @@ -54,5 +54,6 @@ Required order of the following steps for the update to 6.3: acp/database/update_com.woltlab.wcf_6.3_step1.php acp/update_com.woltlab.wcf_6.3_userGroupAssignment.php + acp/update_com.woltlab.wcf_6.3_notice.php --> diff --git a/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl new file mode 100644 index 00000000000..4e204d2fb51 --- /dev/null +++ b/com.woltlab.wcf/templates/shared_categorizedSingleSelectionFormField.tpl @@ -0,0 +1,43 @@ + + +
+
+ + +
+ +
diff --git a/com.woltlab.wcf/templates/shared_cssClassnameFormField.tpl b/com.woltlab.wcf/templates/shared_cssClassnameFormField.tpl new file mode 100644 index 00000000000..e4486cbe060 --- /dev/null +++ b/com.woltlab.wcf/templates/shared_cssClassnameFormField.tpl @@ -0,0 +1,30 @@ + diff --git a/com.woltlab.wcf/templates/shared_timeFormField.tpl b/com.woltlab.wcf/templates/shared_timeFormField.tpl new file mode 100644 index 00000000000..5dc2a443e0b --- /dev/null +++ b/com.woltlab.wcf/templates/shared_timeFormField.tpl @@ -0,0 +1,11 @@ +getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* + *}{if $field->isAutofocused()} autofocus{/if}{* + *}{if $field->isRequired()} required{/if}{* + *}{if $field->isImmutable()} disabled{/if}{* + *}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* +*}> diff --git a/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts new file mode 100644 index 00000000000..f0eecbafa1c --- /dev/null +++ b/ts/WoltLabSuite/Core/Component/ItemList/Categorized.ts @@ -0,0 +1,140 @@ +/** + * Provides a filter input for a categorized item list. + * + * @author Olaf Braun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @sice 6.3 + */ + +import { innerError } from "WoltLabSuite/Core/Dom/Util"; +import { getPhrase } from "WoltLabSuite/Core/Language"; +import { escapeRegExp } from "WoltLabSuite/Core/StringUtil"; + +type Item = { + element: HTMLLIElement; + span: HTMLSpanElement; + text: string; +}; + +type Category = { + items: Item[]; + element: HTMLLIElement; +}; + +export class CategorizedItemList { + readonly #container: HTMLElement; + readonly #elementList: HTMLUListElement; + readonly #input: HTMLInputElement; + #value: string = ""; + readonly #clearButton: HTMLButtonElement; + #categories: Category[] = []; + readonly #fragment: DocumentFragment; + + constructor(elementId: string) { + this.#fragment = document.createDocumentFragment(); + + const container = document.getElementById(elementId); + if (!container) { + throw new Error(`Element with ID ${elementId} not found.`); + } + + this.#container = container; + this.#elementList = this.#container.querySelector(".scrollableCheckboxList")!; + + this.#input = this.#container.querySelector(".inputAddon > input") as HTMLInputElement; + this.#input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + } + }); + this.#input.addEventListener("keyup", () => this.#keyup()); + + this.#clearButton = this.#container.querySelector(".inputAddon > .clearButton")!; + this.#clearButton.addEventListener("click", (event) => { + event.preventDefault(); + + this.#input.value = ""; + this.#keyup(); + }); + + this.#buildItemMap(); + } + + #buildItemMap(): void { + let category: Category | null = null; + for (const li of this.#elementList.querySelectorAll(":scope > li")) { + const input = li.querySelector('input[type="radio"]'); + if (input) { + if (!category) { + throw new Error("Input found without a preceding category."); + } + + category.items.push({ + element: li, + span: li.querySelector("span")!, + text: li.textContent!.trim(), + }); + } else { + const items: Item[] = []; + category = { + items: items, + element: li, + }; + this.#categories.push(category); + } + } + } + + #keyup(): void { + const value = this.#input.value.trim(); + if (this.#value === value) { + return; + } + + this.#value = value; + + if (this.#value) { + this.#clearButton.classList.remove("disabled"); + } else { + this.#clearButton.classList.add("disabled"); + } + + // move list into fragment before editing items, increases performance + // by avoiding the browser to perform repaint/layout over and over again + this.#fragment.appendChild(this.#elementList); + + this.#categories.forEach((category) => { + this.#filterItems(category); + }); + + const hasVisibleItem = this.#elementList.querySelector(".scrollableCheckboxList > li:not([hidden])") !== null; + + this.#container.insertAdjacentElement("beforeend", this.#elementList); + + innerError(this.#container, hasVisibleItem ? false : getPhrase("wcf.global.filter.error.noMatches")); + } + + #filterItems(category: Category): void { + const regexp = new RegExp("(" + escapeRegExp(this.#value) + ")", "i"); + + let hasMatchingItem = false; + for (const item of category.items) { + if (this.#value === "") { + item.span.innerHTML = item.text; // Reset highlighting + + hasMatchingItem = true; + item.element.hidden = false; + } else if (regexp.test(item.text)) { + item.span.innerHTML = item.text.replace(regexp, "$1"); + + item.element.hidden = false; + hasMatchingItem = true; + } else { + item.element.hidden = true; + } + } + + category.element.hidden = !hasMatchingItem; + } +} diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php index 1a24a49c9b2..cf7a3517cd6 100644 --- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php +++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.3_step1.php @@ -18,4 +18,9 @@ MediumtextDatabaseTableColumn::create('conditions'), DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), ]), + PartialDatabaseTable::create('wcf1_notice') + ->columns([ + MediumtextDatabaseTableColumn::create('conditions'), + DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), + ]), ]; diff --git a/wcfsetup/install/files/acp/templates/noticeAdd.tpl b/wcfsetup/install/files/acp/templates/noticeAdd.tpl index 2df7895077e..853b806781b 100644 --- a/wcfsetup/install/files/acp/templates/noticeAdd.tpl +++ b/wcfsetup/install/files/acp/templates/noticeAdd.tpl @@ -17,195 +17,6 @@ -{include file='shared_formNotice'} - -
-
- -
-
- - {if $errorField == 'noticeName'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.acp.notice.noticeName.error.{$errorType}{/lang} - {/if} - - {/if} -
- - - -
-
- - {if $errorField == 'notice'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {elseif $errorType == 'multilingual'} - {lang}wcf.global.form.error.multilingual{/lang} - {else} - {lang}wcf.acp.notice.notice.error.{$errorType}{/lang} - {/if} - - {/if} -
- - {include file='shared_multipleLanguageInputJavascript' elementIdentifier='notice' forceSelection=false} - -
-
-
- -
-
- -
-
-
- - {lang}wcf.acp.notice.showOrder.description{/lang} -
-
- - {event name='dataFields'} -
- -
-

{lang}wcf.global.settings{/lang}

- -
-
-
- {foreach from=$availableCssClassNames item=className} - - {/foreach} - - - - {if $errorField == 'cssClassName'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.acp.notice.cssClassName.error.{$errorType}{/lang} - {/if} - - {/if} - {lang}wcf.acp.notice.cssClassName.description{/lang} - - {lang}wcf.acp.notice.example{/lang} -
-
- -
-
-
- -
-
- -
-
-
- - {lang}wcf.acp.notice.isDismissible.description{/lang} -
-
- - {if $action == 'edit' && $notice->isDismissible} -
-
-
- - {lang}wcf.acp.notice.resetIsDismissed.description{/lang} -
-
- {/if} - - {event name='settingsFields'} -
- - {event name='sections'} - -
-
-

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

-

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

-
- -
-
-

{lang}wcf.acp.notice.conditions.page{/lang}

-

{lang}wcf.acp.notice.conditions.page.description{/lang}

-
- - {foreach from=$groupedConditionObjectTypes['com.woltlab.wcf.page'] item='pageConditionObjectType'} - {unsafe:$pageConditionObjectType->getProcessor()->getHtml()} - {/foreach} -
- -
-
-

{lang}wcf.acp.notice.conditions.pointInTime{/lang}

-

{lang}wcf.acp.notice.conditions.pointInTime.description{/lang}

-
- - {foreach from=$groupedConditionObjectTypes['com.woltlab.wcf.pointInTime'] item='pointInTimeConditionObjectType'} - {unsafe:$pointInTimeConditionObjectType->getProcessor()->getHtml()} - {/foreach} -
- - {event name='conditionTypeSections'} -
- -
-
-

{lang}wcf.acp.notice.conditions.user{/lang}

-

{lang}wcf.acp.notice.conditions.user.description{/lang}

-
- - {include file='shared_userConditions' groupedObjectTypes=$groupedConditionObjectTypes['com.woltlab.wcf.user']} -
- - {event name='conditionContainers'} - -
- - {csrfToken} -
-
- - - +{unsafe:$form->getHtml()} {include file='footer'} diff --git a/wcfsetup/install/files/acp/templates/noticeList.tpl b/wcfsetup/install/files/acp/templates/noticeList.tpl index 5c30518be68..b0e1b98cea0 100644 --- a/wcfsetup/install/files/acp/templates/noticeList.tpl +++ b/wcfsetup/install/files/acp/templates/noticeList.tpl @@ -19,6 +19,12 @@ +{if $hasLegacyObjects} + + {lang}wcf.acp.notice.legacyNotice{/lang} + +{/if} +
{unsafe:$gridView->render()}
diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_notice.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_notice.php new file mode 100644 index 00000000000..7d819ee1d51 --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_notice.php @@ -0,0 +1,25 @@ +exportConditions("com.woltlab.wcf.condition.notice"); +if ($exportedConditions === []) { + return; +} + +$sql = "UPDATE wcf1_notice + SET conditions = ?, + isLegacy = ? + WHERE noticeID = ?"; +$statement = WCF::getDB()->prepare($sql); +foreach ($exportedConditions as $noticeID => $conditionData) { + // TODO handle user option from `com.woltlab.wcf.user.userOptions` + + $statement->execute([ + JSON::encode($conditionData), + 1, + $noticeID, + ]); +} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js new file mode 100644 index 00000000000..92bd82ae7d5 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ItemList/Categorized.js @@ -0,0 +1,112 @@ +/** + * Provides a filter input for a categorized item list. + * + * @author Olaf Braun + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @sice 6.3 + */ +define(["require", "exports", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/StringUtil"], function (require, exports, Util_1, Language_1, StringUtil_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.CategorizedItemList = void 0; + class CategorizedItemList { + #container; + #elementList; + #input; + #value = ""; + #clearButton; + #categories = []; + #fragment; + constructor(elementId) { + this.#fragment = document.createDocumentFragment(); + const container = document.getElementById(elementId); + if (!container) { + throw new Error(`Element with ID ${elementId} not found.`); + } + this.#container = container; + this.#elementList = this.#container.querySelector(".scrollableCheckboxList"); + this.#input = this.#container.querySelector(".inputAddon > input"); + this.#input.addEventListener("keydown", (event) => { + if (event.key === "Enter") { + event.preventDefault(); + } + }); + this.#input.addEventListener("keyup", () => this.#keyup()); + this.#clearButton = this.#container.querySelector(".inputAddon > .clearButton"); + this.#clearButton.addEventListener("click", (event) => { + event.preventDefault(); + this.#input.value = ""; + this.#keyup(); + }); + this.#buildItemMap(); + } + #buildItemMap() { + let category = null; + for (const li of this.#elementList.querySelectorAll(":scope > li")) { + const input = li.querySelector('input[type="radio"]'); + if (input) { + if (!category) { + throw new Error("Input found without a preceding category."); + } + category.items.push({ + element: li, + span: li.querySelector("span"), + text: li.textContent.trim(), + }); + } + else { + const items = []; + category = { + items: items, + element: li, + }; + this.#categories.push(category); + } + } + } + #keyup() { + const value = this.#input.value.trim(); + if (this.#value === value) { + return; + } + this.#value = value; + if (this.#value) { + this.#clearButton.classList.remove("disabled"); + } + else { + this.#clearButton.classList.add("disabled"); + } + // move list into fragment before editing items, increases performance + // by avoiding the browser to perform repaint/layout over and over again + this.#fragment.appendChild(this.#elementList); + this.#categories.forEach((category) => { + this.#filterItems(category); + }); + const hasVisibleItem = this.#elementList.querySelector(".scrollableCheckboxList > li:not([hidden])") !== null; + this.#container.insertAdjacentElement("beforeend", this.#elementList); + (0, Util_1.innerError)(this.#container, hasVisibleItem ? false : (0, Language_1.getPhrase)("wcf.global.filter.error.noMatches")); + } + #filterItems(category) { + const regexp = new RegExp("(" + (0, StringUtil_1.escapeRegExp)(this.#value) + ")", "i"); + let hasMatchingItem = false; + for (const item of category.items) { + if (this.#value === "") { + item.span.innerHTML = item.text; // Reset highlighting + hasMatchingItem = true; + item.element.hidden = false; + } + else if (regexp.test(item.text)) { + item.span.innerHTML = item.text.replace(regexp, "$1"); + item.element.hidden = false; + hasMatchingItem = true; + } + else { + item.element.hidden = true; + } + } + category.element.hidden = !hasMatchingItem; + } + } + exports.CategorizedItemList = CategorizedItemList; +}); diff --git a/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php b/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php index 241a6de6804..39b00b7cb44 100644 --- a/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php @@ -4,308 +4,132 @@ use wcf\data\notice\Notice; use wcf\data\notice\NoticeAction; -use wcf\data\notice\NoticeEditor; -use wcf\data\object\type\ObjectType; -use wcf\data\object\type\ObjectTypeCache; -use wcf\form\AbstractForm; -use wcf\system\condition\ConditionHandler; -use wcf\system\exception\UserInputException; -use wcf\system\language\I18nHandler; -use wcf\system\Regex; -use wcf\system\request\LinkHandler; -use wcf\system\WCF; -use wcf\util\StringUtil; +use wcf\data\notice\NoticeList; +use wcf\form\AbstractFormBuilderForm; +use wcf\system\condition\provider\combined\NoticeConditionProvider; +use wcf\system\form\builder\container\condition\ConditionFormContainer; +use wcf\system\form\builder\container\FormContainer; +use wcf\system\form\builder\data\processor\VoidFormDataProcessor; +use wcf\system\form\builder\field\BooleanFormField; +use wcf\system\form\builder\field\CssClassNameFormField; +use wcf\system\form\builder\field\dependency\NonEmptyFormFieldDependency; +use wcf\system\form\builder\field\MultilineTextFormField; +use wcf\system\form\builder\field\ShowOrderFormField; +use wcf\system\form\builder\field\TextFormField; /** * Shows the form to create a new notice. * - * @author Matthias Schmidt - * @copyright 2001-2019 WoltLab GmbH + * @author Olaf Braun, Matthias Schmidt + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License + * + * @extends AbstractFormBuilderForm */ -class NoticeAddForm extends AbstractForm +class NoticeAddForm extends AbstractFormBuilderForm { /** * @inheritDoc */ public $activeMenuItem = 'wcf.acp.menu.link.notice.add'; - /** - * name of the chosen CSS class name - * @var string - */ - public $cssClassName = 'info'; - - /** - * custom CSS class name - * @var string - */ - public $customCssClassName = ''; - - /** - * grouped notice condition object types - * @var (ObjectType|ObjectType[])[][] - */ - public $groupedConditionObjectTypes = []; - - /** - * 1 if the notice is disabled - * @var int - */ - public $isDisabled = 0; - - /** - * 1 if the notice is dismissible - * @var int - */ - public $isDismissible = 0; - /** * @inheritDoc */ public $neededPermissions = ['admin.notice.canManageNotice']; - /** - * name of the notice - * @var string - */ - public $noticeName = ''; - - /** - * 1 if html is used in the notice text - * @var int - */ - public $noticeUseHtml = 0; - - /** - * order used to the show the notices - * @var int - */ - public $showOrder = 0; - /** * @inheritDoc */ - public function assignVariables() - { - parent::assignVariables(); - - I18nHandler::getInstance()->assignVariables(); - - WCF::getTPL()->assign([ - 'action' => 'add', - 'availableCssClassNames' => Notice::TYPES, - 'cssClassName' => $this->cssClassName, - 'customCssClassName' => $this->customCssClassName, - 'isDisabled' => $this->isDisabled, - 'isDismissible' => $this->isDismissible, - 'groupedConditionObjectTypes' => $this->groupedConditionObjectTypes, - 'noticeName' => $this->noticeName, - 'noticeUseHtml' => $this->noticeUseHtml, - 'showOrder' => $this->showOrder, - ]); - } + public $objectEditLinkController = NoticeEditForm::class; /** * @inheritDoc */ - public function readData() - { - $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.condition.notice'); - foreach ($objectTypes as $objectType) { - if (!$objectType->conditionobject) { - continue; - } - - if (!isset($this->groupedConditionObjectTypes[$objectType->conditionobject])) { - $this->groupedConditionObjectTypes[$objectType->conditionobject] = []; - } + public $objectActionClass = NoticeAction::class; - if ($objectType->conditiongroup) { - if (!isset($this->groupedConditionObjectTypes[$objectType->conditionobject][$objectType->conditiongroup])) { - $this->groupedConditionObjectTypes[$objectType->conditionobject][$objectType->conditiongroup] = []; - } - - $this->groupedConditionObjectTypes[$objectType->conditionobject][$objectType->conditiongroup][$objectType->objectTypeID] = $objectType; - } else { - $this->groupedConditionObjectTypes[$objectType->conditionobject][$objectType->objectTypeID] = $objectType; - } - } - - parent::readData(); - } - - /** - * @inheritDoc - */ - public function readFormParameters() + #[\Override] + protected function createForm() { - parent::readFormParameters(); - - I18nHandler::getInstance()->readValues(); - - if (isset($_POST['cssClassName'])) { - $this->cssClassName = StringUtil::trim($_POST['cssClassName']); - } - if (isset($_POST['customCssClassName'])) { - $this->customCssClassName = StringUtil::trim($_POST['customCssClassName']); - } - if (isset($_POST['isDisabled'])) { - $this->isDisabled = 1; - } - if (isset($_POST['isDismissible'])) { - $this->isDismissible = 1; - } - if (isset($_POST['noticeName'])) { - $this->noticeName = StringUtil::trim($_POST['noticeName']); - } - if (isset($_POST['noticeUseHtml'])) { - $this->noticeUseHtml = 1; - } - if (isset($_POST['showOrder'])) { - $this->showOrder = \intval($_POST['showOrder']); - } - - foreach ($this->groupedConditionObjectTypes as $groupedObjectTypes) { - foreach ($groupedObjectTypes as $objectTypes) { - if (\is_array($objectTypes)) { - foreach ($objectTypes as $objectType) { - $objectType->getProcessor()->readFormParameters(); - } - } else { - $objectTypes->getProcessor()->readFormParameters(); - } - } - } + parent::createForm(); + + $this->form->appendChildren([ + FormContainer::create('generalSection') + ->appendChildren([ + TextFormField::create('noticeName') + ->label('wcf.global.name') + ->required(), + MultilineTextFormField::create('notice') + ->i18n() + ->languageItemPattern('wcf.notice.notice.notice\d+') + ->required(), + BooleanFormField::create('noticeUseHtml') + ->label('wcf.acp.notice.noticeUseHtml'), + ShowOrderFormField::create() + ->description('wcf.acp.notice.showOrder.description') + ->options($this->getNotices()), + ]), + FormContainer::create('settingsSection') + ->label('wcf.global.settings') + ->appendChildren([ + CssClassNameFormField::create('cssClassName') + ->label('wcf.acp.notice.cssClassName') + ->visualTemplate('{$label}') + ->description('wcf.acp.notice.cssClassName.description') + ->options($this->getClassNames()) + ->supportCustomClassName() + ->required(), + BooleanFormField::create('isDisabled') + ->label('wcf.acp.notice.isDisabled'), + BooleanFormField::create('isDismissible') + ->label('wcf.acp.notice.isDismissible') + ->description('wcf.acp.notice.isDismissible.description'), + BooleanFormField::create('resetIsDismissed') + ->label('wcf.acp.notice.resetIsDismissed') + ->description('wcf.acp.notice.resetIsDismissed.description') + ->available($this->formAction === 'edit') + ->addDependency( + NonEmptyFormFieldDependency::create('isDismissibleDependency') + ->fieldId('isDismissible') + ), + ]), + ConditionFormContainer::create() + ->conditionProvider(new NoticeConditionProvider()), + ]); } - /** - * @inheritDoc - */ - public function readParameters() + #[\Override] + public function finalizeForm() { - parent::readParameters(); + parent::finalizeForm(); - I18nHandler::getInstance()->register('notice'); + $this->form->getDataHandler() + ->addProcessor(new VoidFormDataProcessor('resetIsDismissed')); } /** - * @inheritDoc + * @return Notice[] */ - public function save() + private function getNotices(): array { - parent::save(); + $optionList = new NoticeList(); + $optionList->sqlOrderBy = "showOrder ASC"; + $optionList->readObjects(); - $this->objectAction = new NoticeAction([], 'create', [ - 'data' => \array_merge($this->additionalFields, [ - 'cssClassName' => $this->cssClassName == 'custom' ? $this->customCssClassName : $this->cssClassName, - 'isDisabled' => $this->isDisabled, - 'isDismissible' => $this->isDismissible, - 'notice' => I18nHandler::getInstance()->isPlainValue('notice') ? I18nHandler::getInstance()->getValue('notice') : '', - 'noticeName' => $this->noticeName, - 'noticeUseHtml' => $this->noticeUseHtml, - 'showOrder' => $this->showOrder, - ]), - ]); - $returnValues = $this->objectAction->executeAction(); - - if (!I18nHandler::getInstance()->isPlainValue('notice')) { - I18nHandler::getInstance()->save( - 'notice', - 'wcf.notice.notice.notice' . $returnValues['returnValues']->noticeID, - 'wcf.notice', - 1 - ); - - // update notice name - $noticeEditor = new NoticeEditor($returnValues['returnValues']); - $noticeEditor->update([ - 'notice' => 'wcf.notice.notice.notice' . $returnValues['returnValues']->noticeID, - ]); - } - - // transform conditions array into one-dimensional array - $conditions = []; - foreach ($this->groupedConditionObjectTypes as $groupedObjectTypes) { - foreach ($groupedObjectTypes as $objectTypes) { - if (\is_array($objectTypes)) { - $conditions = \array_merge($conditions, $objectTypes); - } else { - $conditions[] = $objectTypes; - } - } - } - - ConditionHandler::getInstance()->createConditions($returnValues['returnValues']->noticeID, $conditions); - - $this->saved(); - - // reset values - $this->cssClassName = ''; - $this->customCssClassName = ''; - $this->isDisabled = 0; - $this->isDismissible = 0; - $this->noticeName = ''; - $this->noticeUseHtml = 0; - $this->showOrder = 0; - I18nHandler::getInstance()->reset(); - - foreach ($conditions as $condition) { - $condition->getProcessor()->reset(); - } - - WCF::getTPL()->assign([ - 'success' => true, - 'objectEditLink' => LinkHandler::getInstance()->getControllerLink( - NoticeEditForm::class, - ['id' => $returnValues['returnValues']->noticeID] - ), - ]); + return $optionList->getObjects(); } /** - * @inheritDoc + * @return array */ - public function validate() + private function getClassNames(): array { - parent::validate(); + $classNames = []; - if (empty($this->noticeName)) { - throw new UserInputException('noticeName'); + foreach (Notice::TYPES as $type) { + $classNames[$type] = 'wcf.acp.notice.cssClassName.' . $type; } - if (!I18nHandler::getInstance()->validateValue('notice')) { - if (I18nHandler::getInstance()->isPlainValue('notice')) { - throw new UserInputException('notice'); - } else { - throw new UserInputException('notice', 'multilingual'); - } - } - - // validate class name - if (empty($this->cssClassName)) { - throw new UserInputException('cssClassName'); - } elseif ($this->cssClassName == 'custom') { - if (empty($this->cssClassName)) { - throw new UserInputException('cssClassName'); - } - if (!Regex::compile('^-?[_a-zA-Z]+[_a-zA-Z0-9-]+$')->match($this->customCssClassName)) { - throw new UserInputException('cssClassName', 'invalid'); - } - } elseif (!\in_array($this->cssClassName, Notice::TYPES)) { - throw new UserInputException('cssClassName', 'invalid'); - } - - foreach ($this->groupedConditionObjectTypes as $groupedObjectTypes) { - foreach ($groupedObjectTypes as $objectTypes) { - if (\is_array($objectTypes)) { - foreach ($objectTypes as $objectType) { - $objectType->getProcessor()->validate(); - } - } else { - $objectTypes->getProcessor()->validate(); - } - } - } + return $classNames; } } diff --git a/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php b/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php index 06fbead5e83..c9a39ceeb25 100644 --- a/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/NoticeEditForm.class.php @@ -4,16 +4,14 @@ use wcf\acp\page\NoticeListPage; use wcf\data\notice\Notice; -use wcf\data\notice\NoticeAction; -use wcf\form\AbstractForm; -use wcf\system\condition\ConditionHandler; use wcf\system\exception\IllegalLinkException; +use wcf\system\exception\NamedUserException; use wcf\system\interaction\admin\NoticeInteractions; use wcf\system\interaction\StandaloneInteractionContextMenuComponent; -use wcf\system\language\I18nHandler; use wcf\system\request\LinkHandler; use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; +use wcf\util\HtmlString; /** * Shows the form to edit an existing notice. @@ -29,186 +27,60 @@ class NoticeEditForm extends NoticeAddForm */ public $activeMenuItem = 'wcf.acp.menu.link.notice.list'; - /** - * edited notice object - * @var Notice - */ - public $notice; - - /** - * id of the edited notice object - * @var int - */ - public $noticeID = 0; - - /** - * 1 if the notice will be displayed for all users again - * @var int - */ - public $resetIsDismissed = 0; - /** * @inheritDoc */ + public $formAction = 'edit'; + + #[\Override] public function assignVariables() { parent::assignVariables(); - I18nHandler::getInstance()->assignVariables(!empty($_POST)); - WCF::getTPL()->assign([ 'action' => 'edit', - 'notice' => $this->notice, - 'resetIsDismissed' => $this->resetIsDismissed, 'interactionContextMenu' => StandaloneInteractionContextMenuComponent::forContentHeaderButton( new NoticeInteractions(), - $this->notice, + $this->formObject, LinkHandler::getInstance()->getControllerLink(NoticeListPage::class) ), ]); } - /** - * @inheritDoc - */ - public function readData() - { - parent::readData(); - - if (empty($_POST)) { - I18nHandler::getInstance()->setOptions('notice', 1, $this->notice->notice, 'wcf.notice.notice.notice\d+'); - - $this->cssClassName = $this->notice->cssClassName; - if (!\in_array($this->cssClassName, Notice::TYPES)) { - $this->customCssClassName = $this->cssClassName; - $this->cssClassName = 'custom'; - } - - $this->isDisabled = $this->notice->isDisabled; - $this->isDismissible = $this->notice->isDismissible; - $this->noticeName = $this->notice->noticeName; - $this->noticeUseHtml = $this->notice->noticeUseHtml; - $this->showOrder = $this->notice->showOrder; - - $conditions = $this->notice->getConditions(); - $conditionsByObjectTypeID = []; - foreach ($conditions as $condition) { - $conditionsByObjectTypeID[$condition->objectTypeID] = $condition; - } - - foreach ($this->groupedConditionObjectTypes as $objectTypes1) { - foreach ($objectTypes1 as $objectTypes2) { - if (\is_array($objectTypes2)) { - foreach ($objectTypes2 as $objectType) { - if (isset($conditionsByObjectTypeID[$objectType->objectTypeID])) { - $conditionsByObjectTypeID[$objectType->objectTypeID]->getObjectType()->getProcessor()->setData($conditionsByObjectTypeID[$objectType->objectTypeID]); - } - } - } elseif (isset($conditionsByObjectTypeID[$objectTypes2->objectTypeID])) { - $conditionsByObjectTypeID[$objectTypes2->objectTypeID]->getObjectType()->getProcessor()->setData($conditionsByObjectTypeID[$objectTypes2->objectTypeID]); - } - } - } - } - } - - /** - * @inheritDoc - */ - public function readFormParameters() - { - parent::readFormParameters(); - - if (isset($_POST['resetIsDismissed'])) { - $this->resetIsDismissed = 1; - } - } - - /** - * @inheritDoc - */ + #[\Override] public function readParameters() { parent::readParameters(); - if (isset($_REQUEST['id'])) { - $this->noticeID = \intval($_REQUEST['id']); + if (!isset($_REQUEST['id'])) { + throw new IllegalLinkException(); } - $this->notice = new Notice($this->noticeID); - if (!$this->notice->noticeID) { + $this->formObject = new Notice(\intval($_REQUEST['id'])); + if (!$this->formObject->noticeID) { throw new IllegalLinkException(); } - } - /** - * @inheritDoc - */ - public function save() - { - AbstractForm::save(); - - $this->objectAction = new NoticeAction([$this->notice], 'update', [ - 'data' => \array_merge($this->additionalFields, [ - 'cssClassName' => $this->cssClassName == 'custom' ? $this->customCssClassName : $this->cssClassName, - 'isDisabled' => $this->isDisabled, - 'isDismissible' => $this->isDismissible, - 'notice' => I18nHandler::getInstance()->isPlainValue('notice') ? I18nHandler::getInstance()->getValue('notice') : 'wcf.notice.notice.notice' . $this->notice->noticeID, - 'noticeName' => $this->noticeName, - 'noticeUseHtml' => $this->noticeUseHtml, - 'showOrder' => $this->showOrder, - ]), - ]); - $this->objectAction->executeAction(); - - if (I18nHandler::getInstance()->isPlainValue('notice')) { - if ($this->notice->notice == 'wcf.notice.notice.notice' . $this->notice->noticeID) { - I18nHandler::getInstance()->remove($this->notice->notice); - } - } else { - I18nHandler::getInstance()->save( - 'notice', - 'wcf.notice.notice.notice' . $this->notice->noticeID, - 'wcf.notice', - 1 + if ($this->formObject->isLegacy) { + throw new NamedUserException( + HtmlString::fromSafeHtml(WCF::getLanguage()->getDynamicVariable('wcf.acp.notice.legacyNotice')) // TODO add language item ); } + } - // transform conditions array into one-dimensional array - $conditions = []; - foreach ($this->groupedConditionObjectTypes as $groupedObjectTypes) { - foreach ($groupedObjectTypes as $objectTypes) { - if (\is_array($objectTypes)) { - $conditions = \array_merge($conditions, $objectTypes); - } else { - $conditions[] = $objectTypes; - } - } - } - - ConditionHandler::getInstance()->updateConditions( - $this->notice->noticeID, - $this->notice->getConditions(), - $conditions - ); - - if ($this->resetIsDismissed) { + #[\Override] + public function saved() + { + if ($this->form->getFormField('resetIsDismissed')->getValue()) { $sql = "DELETE FROM wcf1_notice_dismissed WHERE noticeID = ?"; $statement = WCF::getDB()->prepare($sql); $statement->execute([ - $this->notice->noticeID, + $this->formObject->noticeID, ]); - $this->resetIsDismissed = 0; - UserStorageHandler::getInstance()->resetAll('dismissedNotices'); } - $this->saved(); - - // reload notice object for proper 'isDismissible' value - $this->notice = new Notice($this->noticeID); - - WCF::getTPL()->assign('success', true); + parent::saved(); } } diff --git a/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentAddForm.class.php b/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentAddForm.class.php index 6929fa248ce..133b6c2da3a 100644 --- a/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentAddForm.class.php @@ -7,7 +7,7 @@ use wcf\data\user\group\UserGroup; use wcf\form\AbstractFormBuilderForm; use wcf\system\condition\provider\UserConditionProvider; -use wcf\system\form\builder\container\ConditionFormContainer; +use wcf\system\form\builder\container\condition\ConditionFormContainer; use wcf\system\form\builder\container\FormContainer; use wcf\system\form\builder\field\BooleanFormField; use wcf\system\form\builder\field\SingleSelectionFormField; diff --git a/wcfsetup/install/files/lib/acp/page/NoticeListPage.class.php b/wcfsetup/install/files/lib/acp/page/NoticeListPage.class.php index 6bc2e212bc6..1940cd22639 100644 --- a/wcfsetup/install/files/lib/acp/page/NoticeListPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/NoticeListPage.class.php @@ -4,6 +4,7 @@ use wcf\page\AbstractGridViewPage; use wcf\system\gridView\admin\NoticeGridView; +use wcf\system\WCF; /** * Lists the available notices. @@ -31,4 +32,25 @@ protected function createGridView(): NoticeGridView { return new NoticeGridView(); } + + #[\Override] + public function assignVariables() + { + parent::assignVariables(); + + WCF::getTPL()->assign([ + 'hasLegacyObjects' => $this->hasLegacyObjects(), + ]); + } + + private function hasLegacyObjects(): bool + { + $sql = "SELECT COUNT(*) AS count + FROM wcf1_notice + WHERE isLegacy = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([1]); + + return $statement->fetchColumn() > 0; + } } diff --git a/wcfsetup/install/files/lib/action/ConditionAddAction.class.php b/wcfsetup/install/files/lib/action/ConditionAddAction.class.php index 83c4c2772b7..193571b4347 100644 --- a/wcfsetup/install/files/lib/action/ConditionAddAction.class.php +++ b/wcfsetup/install/files/lib/action/ConditionAddAction.class.php @@ -82,22 +82,14 @@ private function getForm(AbstractConditionProvider $provider): Psr15DialogForm self::class, WCF::getLanguage()->get('wcf.condition.add') ); - $options = \array_map( - static fn (IConditionType $conditionType) => WCF::getLanguage()->get($conditionType->getLabel()), - $provider->getConditionTypes() - ); - $collator = new \Collator(WCF::getLanguage()->getLocale()); - \uasort( - $options, - static fn (string $a, string $b) => $collator->compare($a, $b) - ); $form->appendChild( - SingleSelectionFormField::create('conditionType') + $this->getConditionTypeFormField() + ->id('conditionType') ->label('wcf.condition.condition') ->filterable() ->required() - ->options($options) + ->options($this->getOptions($provider), true, false) ); $form->markRequiredFields(false); @@ -105,4 +97,71 @@ private function getForm(AbstractConditionProvider $provider): Psr15DialogForm return $form; } + + /** + * @param AbstractConditionProvider> $provider + * + * @return array{} + */ + private function getOptions(AbstractConditionProvider $provider): array + { + $conditionTypes = $provider->getConditionTypes(); + + $grouped = []; + foreach ($conditionTypes as $key => $conditionType) { + $category = $conditionType->getCategory(); + $label = $conditionType->getLabel(); + + if (!isset($grouped[$category])) { + $grouped[$category] = [ + 'items' => [], + 'label' => WCF::getLanguage()->get('wcf.condition.category.' . $category), + ]; + } + + $grouped[$category]['items'][$key] = WCF::getLanguage()->get($label); + } + + $collator = new \Collator(WCF::getLanguage()->getLocale()); + + foreach ($grouped as &$category) { + \uasort($category['items'], static function ($labelA, $labelB) use ($collator) { + return $collator->compare($labelA, $labelB); + }); + } + unset($category); + + \uasort($grouped, static function ($catA, $catB) use ($collator) { + return $collator->compare($catA['label'], $catB['label']); + }); + + $options = []; + + foreach ($grouped as $categoryKey => $category) { + $options[] = [ + 'depth' => 0, + 'isSelectable' => false, + 'label' => $category['label'], + 'value' => $categoryKey, + ]; + + foreach ($category['items'] as $key => $label) { + $options[] = [ + 'depth' => 1, + 'isSelectable' => true, + 'label' => $label, + 'value' => $key, + ]; + } + } + + return $options; + } + + private function getConditionTypeFormField(): SingleSelectionFormField + { + return new class extends SingleSelectionFormField { + protected $templateName = 'shared_categorizedSingleSelectionFormField'; + }; + } } diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 6fd9e328f4c..1d3acc96c5f 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -101,6 +101,7 @@ static function (\wcf\event\worker\RebuildWorkerCollecting $event) { $event->register(\wcf\system\worker\FileRebuildDataWorker::class, 475); $event->register(\wcf\system\worker\SitemapRebuildWorker::class, 500); $event->register(\wcf\system\worker\UserGroupAssignmentRebuildDataWorker::class, 600); + $event->register(\wcf\system\worker\NoticeRebuildDataWorker::class, 600); $event->register(\wcf\system\worker\StatDailyRebuildDataWorker::class, 800); } ); diff --git a/wcfsetup/install/files/lib/data/notice/Notice.class.php b/wcfsetup/install/files/lib/data/notice/Notice.class.php index c2eedc27023..fed6c8aad39 100644 --- a/wcfsetup/install/files/lib/data/notice/Notice.class.php +++ b/wcfsetup/install/files/lib/data/notice/Notice.class.php @@ -2,12 +2,11 @@ namespace wcf\data\notice; -use wcf\data\condition\Condition; use wcf\data\DatabaseObject; -use wcf\system\condition\ConditionHandler; use wcf\system\request\IRouteController; use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; +use wcf\util\JSON; use wcf\util\StringUtil; /** @@ -25,6 +24,8 @@ * @property-read int $showOrder position of the notice in relation to the other notices * @property-read int $isDisabled is `1` if the notice is disabled and thus not shown, otherwise `0` * @property-read int $isDismissible is `1` if the notice can be dismissed by users, otherwise `0` + * @property-read string $conditions + * @property-read int $isLegacy */ class Notice extends DatabaseObject implements IRouteController { @@ -65,11 +66,11 @@ public function __toString(): string /** * Returns the conditions of the notice. * - * @return Condition[] + * @return array{identifier: string, value: mixed}[] */ - public function getConditions() + public function getConditions(): array { - return ConditionHandler::getInstance()->getConditions('com.woltlab.wcf.condition.notice', $this->noticeID); + return JSON::decode($this->conditions); } /** diff --git a/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php b/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php index 325174eea6c..c9795c68c4b 100644 --- a/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php +++ b/wcfsetup/install/files/lib/data/notice/NoticeAction.class.php @@ -5,6 +5,7 @@ use wcf\data\AbstractDatabaseObjectAction; use wcf\data\IToggleAction; use wcf\data\TDatabaseObjectToggle; +use wcf\data\TI18nDatabaseObjectAction; use wcf\system\condition\ConditionHandler; use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; @@ -21,6 +22,7 @@ class NoticeAction extends AbstractDatabaseObjectAction implements IToggleAction { use TDatabaseObjectToggle; + use TI18nDatabaseObjectAction; /** * @inheritDoc @@ -56,6 +58,9 @@ public function create() /** @var Notice $notice */ $notice = parent::create(); + + $this->saveI18nValue($notice); + $noticeEditor = new NoticeEditor($notice); $noticeEditor->setShowOrder($showOrder); @@ -69,7 +74,11 @@ public function delete() { ConditionHandler::getInstance()->deleteConditions('com.woltlab.wcf.condition.notice', $this->objectIDs); - return parent::delete(); + $count = parent::delete(); + + $this->deleteI18nValues(); + + return $count; } /** @@ -126,6 +135,10 @@ public function update() { parent::update(); + foreach ($this->getObjects() as $labelEditor) { + $this->saveI18nValue($labelEditor->getDecoratedObject()); + } + if ( \count($this->objects) == 1 && isset($this->parameters['data']['showOrder']) @@ -134,4 +147,22 @@ public function update() \reset($this->objects)->setShowOrder($this->parameters['data']['showOrder']); } } + + #[\Override] + public function getI18nSaveTypes(): array + { + return ['notice' => 'wcf.notice.notice.notice\d+']; + } + + #[\Override] + public function getLanguageCategory(): string + { + return 'wcf.notice'; + } + + #[\Override] + public function getPackageID(): int + { + return 1; + } } diff --git a/wcfsetup/install/files/lib/data/notice/NoticeEditor.class.php b/wcfsetup/install/files/lib/data/notice/NoticeEditor.class.php index c1baac8bf72..194237f9446 100644 --- a/wcfsetup/install/files/lib/data/notice/NoticeEditor.class.php +++ b/wcfsetup/install/files/lib/data/notice/NoticeEditor.class.php @@ -4,9 +4,7 @@ use wcf\data\DatabaseObjectEditor; use wcf\data\IEditableCachedObject; -use wcf\data\object\type\ObjectTypeCache; -use wcf\system\cache\builder\ConditionCacheBuilder; -use wcf\system\cache\builder\NoticeCacheBuilder; +use wcf\system\cache\eager\NoticeCache; use wcf\system\WCF; /** @@ -67,11 +65,6 @@ public function setShowOrder($showOrder = 0) */ public static function resetCache() { - NoticeCacheBuilder::getInstance()->reset(); - ConditionCacheBuilder::getInstance()->reset([ - 'definitionID' => ObjectTypeCache::getInstance() - ->getDefinitionByName('com.woltlab.wcf.condition.notice') - ->definitionID, - ]); + (new NoticeCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php b/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php index c70653a43b8..e5083a6a3ab 100644 --- a/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php +++ b/wcfsetup/install/files/lib/system/acp/dashboard/box/StatusMessageAcpDashboardBox.class.php @@ -283,9 +283,13 @@ private function getMigrationMessage(): array { $event = new MigrationCollecting(); EventHandler::getInstance()->fire($event); + if ($this->userGroupAssignmentHasLegacyObjects()) { $event->migrationNeeded(WCF::getLanguage()->get('wcf.acp.group.assignment')); } + if ($this->noticeHasLegacyObjects()) { + $event->migrationNeeded(WCF::getLanguage()->get('wcf.acp.notice.list')); + } if ($event->needsMigration() === []) { return []; @@ -311,4 +315,15 @@ private function userGroupAssignmentHasLegacyObjects(): bool return $statement->fetchColumn() > 0; } + + private function noticeHasLegacyObjects(): bool + { + $sql = "SELECT COUNT(*) AS count + FROM wcf1_notice + WHERE isLegacy = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([1]); + + return $statement->fetchColumn() > 0; + } } diff --git a/wcfsetup/install/files/lib/system/cache/builder/NoticeCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/NoticeCacheBuilder.class.php index fc0cf38f6fd..9f918197a42 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/NoticeCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/NoticeCacheBuilder.class.php @@ -2,7 +2,7 @@ namespace wcf\system\cache\builder; -use wcf\data\notice\NoticeList; +use wcf\system\cache\eager\NoticeCache; /** * Caches the enabled notices. @@ -10,19 +10,20 @@ * @author Matthias Schmidt * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License + * + * @deprecated 6.2 use `NoticeCache` instead */ -class NoticeCacheBuilder extends AbstractCacheBuilder +final class NoticeCacheBuilder extends AbstractLegacyCacheBuilder { - /** - * @inheritDoc - */ - protected function rebuild(array $parameters) + #[\Override] + protected function rebuild(array $parameters): array { - $noticeList = new NoticeList(); - $noticeList->getConditionBuilder()->add('isDisabled = ?', [0]); - $noticeList->sqlOrderBy = 'showOrder ASC'; - $noticeList->readObjects(); + return (new NoticeCache())->getCache(); + } - return $noticeList->getObjects(); + #[\Override] + public function reset(array $parameters = []) + { + (new NoticeCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/cache/eager/NoticeCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/NoticeCache.class.php new file mode 100644 index 00000000000..6dc16dddf94 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/NoticeCache.class.php @@ -0,0 +1,31 @@ + + * @since 6.3 + * + * @extends AbstractEagerCache> + */ +final class NoticeCache extends AbstractEagerCache +{ + #[\Override] + protected function getCacheData(): array + { + $noticeList = new NoticeList(); + $noticeList->getConditionBuilder()->add('isDisabled = ?', [0]); + $noticeList->getConditionBuilder()->add('isLegacy = ?', [0]); + $noticeList->sqlOrderBy = 'showOrder ASC'; + $noticeList->readObjects(); + + return $noticeList->getObjects(); + } +} diff --git a/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php index 1d26c45869e..0280b6823b2 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php @@ -27,6 +27,10 @@ abstract class AbstractConditionProvider */ public function addCondition(IConditionType $conditionType): void { + if (\array_key_exists($conditionType->getIdentifier(), $this->conditionTypes)) { + throw new \InvalidArgumentException("Condition type with identifier '{$conditionType->getIdentifier()}' already exists."); + } + $this->conditionTypes[$conditionType->getIdentifier()] = $conditionType; } @@ -74,4 +78,13 @@ public function getConditionTypes(): array { return $this->conditionTypes; } + + public function withConditionsFrom(AbstractConditionProvider $provider): self + { + foreach ($provider->getConditionTypes() as $conditionType) { + $this->addCondition($conditionType); + } + + return $this; + } } diff --git a/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php new file mode 100644 index 00000000000..d2464147c8d --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/provider/RequestConditionProvider.class.php @@ -0,0 +1,31 @@ + + * @since 6.3 + * + * @phpstan-type RequestConditionType IContextualConditionType + * @extends AbstractConditionProvider + */ +final class RequestConditionProvider extends AbstractConditionProvider +{ + public function __construct() + { + $this->addCondition(new TimeRequestConditionType()); + $this->addCondition(new ActivePageRequestConditionType()); + $this->addCondition(new NotOnPageRequestConditionType()); + $this->addCondition(new DayOfWeekRequestConditionType()); + $this->addCondition(new NotDayOfWeekRequestConditionType()); + } +} diff --git a/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php index 1e622a03036..b1d3736336b 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php @@ -28,7 +28,8 @@ * @license GNU Lesser General Public License * @since 6.3 * - * @extends AbstractConditionProvider&IObjectConditionType> + * @phpstan-type UserConditionType IDatabaseObjectListConditionType&IObjectConditionType + * @extends AbstractConditionProvider */ final class UserConditionProvider extends AbstractConditionProvider { @@ -38,6 +39,7 @@ public function __construct() new StringUserConditionType( identifier: "username", columnName: "username", + category: "user", migrateKeyName: "username", migrateConditionObjectType: 'com.woltlab.wcf.user.username' ), @@ -46,6 +48,7 @@ public function __construct() new StringUserConditionType( identifier: "email", columnName: "email", + category: "user", migrateKeyName: "email", migrateConditionObjectType: 'com.woltlab.wcf.user.email' ), @@ -69,6 +72,7 @@ public function __construct() new IsNullUserConditionType( identifier: "avatar", columnName: 'avatarFileID', + category: 'userProfile', migrateKeyName: 'userAvatar', migrateConditionObjectType: 'com.woltlab.wcf.user.avatar' ), @@ -80,6 +84,7 @@ public function __construct() new IsNullUserConditionType( identifier: "coverPhoto", columnName: 'coverPhotoFileID', + category: 'userProfile', migrateKeyName: 'userCoverPhoto', migrateConditionObjectType: 'com.woltlab.wcf.coverPhoto' ), @@ -88,6 +93,7 @@ public function __construct() new BooleanUserConditionType( identifier: "isBanned", columnName: 'banned', + category: 'user', migrateKeyName: 'userIsBanned', migrateConditionObjectType: 'com.woltlab.wcf.user.state' ), @@ -99,6 +105,7 @@ public function __construct() new IsNullUserConditionType( identifier: "isEmailConfirmed", columnName: 'emailConfirmed', + category: 'user', migrateKeyName: 'userIsEmailConfirmed', migrateConditionObjectType: 'com.woltlab.wcf.user.state' ), @@ -107,6 +114,7 @@ public function __construct() new BooleanUserConditionType( identifier: "isMultifactorActive", columnName: 'multifactorActive', + category: 'user', migrateKeyName: 'multifactorActive', migrateConditionObjectType: 'com.woltlab.wcf.user.multifactor' ), @@ -121,6 +129,7 @@ public function __construct() new IntegerUserConditionType( identifier: "activityPoints", columnName: "activityPoints", + category: 'userProfile', migrateConditionObjectType: 'com.woltlab.wcf.user.activityPoints' ), ); @@ -128,6 +137,7 @@ public function __construct() new IntegerUserConditionType( identifier: "likesReceived", columnName: "likesReceived", + category: 'userProfile', migrateConditionObjectType: 'com.woltlab.wcf.user.likesReceived' ), ); @@ -135,6 +145,7 @@ public function __construct() new IntegerUserConditionType( identifier: "trophyPoints", columnName: "trophyPoints", + category: 'userProfile', migrateConditionObjectType: 'com.woltlab.wcf.user.trophyPoints' ), ); diff --git a/wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php b/wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php new file mode 100644 index 00000000000..d304f967647 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/provider/combined/NoticeConditionProvider.class.php @@ -0,0 +1,26 @@ + + * @since 6.3 + * + * @phpstan-import-type RequestConditionType from RequestConditionProvider + * @phpstan-import-type UserConditionType from UserConditionProvider + * @extends AbstractConditionProvider + */ +final class NoticeConditionProvider extends AbstractConditionProvider +{ + public function __construct() + { + $this->withConditionsFrom(new UserConditionProvider()); + $this->withConditionsFrom(new RequestConditionProvider()); + } +} diff --git a/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php index 03b9aa4219a..fee6e9f8c03 100644 --- a/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/IConditionType.class.php @@ -36,4 +36,11 @@ public function getLabel(): string; * @param TFilter $filter */ public function setFilter(mixed $filter): void; + + /** + * Get the name of the category for this condition type. + * All condition types with the same category are grouped together. + * The language variable for the category name is `wcf.condition.category.`. + */ + public function getCategory(): string; } diff --git a/wcfsetup/install/files/lib/system/condition/type/IContextualConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/IContextualConditionType.class.php new file mode 100644 index 00000000000..33df27795a8 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/IContextualConditionType.class.php @@ -0,0 +1,20 @@ + + * @since 6.3 + * + * @template TFilter + * @extends IConditionType + */ +interface IContextualConditionType extends IConditionType +{ + /** + * Returns `true` if the condition matches the global context (e.g., the active user via `WCF::getUser()`). + */ + public function matches(): bool; +} diff --git a/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php new file mode 100644 index 00000000000..b1e075568a7 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/request/ActivePageRequestConditionType.class.php @@ -0,0 +1,82 @@ + + * @since 6.3 + * + * @implements IContextualConditionType + * @extends AbstractConditionType + */ +final class ActivePageRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType +{ + #[\Override] + public function getIdentifier(): string + { + return 'activePage'; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.request.activePage"; + } + + #[\Override] + public function getFormField(string $id): MultipleSelectionFormField + { + return MultipleSelectionFormField::create($id) + ->options((new PageNodeTree())->getNodeList(), true) + ->filterable() + ->required(); + } + + #[\Override] + public function matches(): bool + { + return \in_array(RequestHandler::getInstance()->getActivePageID(), $this->filter); + } + + #[\Override] + public function getCategory(): string + { + return "page"; + } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $reverseLogic = $conditionData['pageIDs_reverseLogic'] ?? false; + $pageIDs = $conditionData['pageIDs'] ?? []; + + if ($reverseLogic) { + // `NotOnPageRequestConditionType` should migrate the data. + return []; + } + + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => \array_map('strval', $pageIDs), + ]; + + unset($conditionData['pageIDs'], $conditionData['pageIDs_reverseLogic']); + + return $conditions; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.page'; + } +} diff --git a/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php new file mode 100644 index 00000000000..ef6b2a5f2be --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/request/DayOfWeekRequestConditionType.class.php @@ -0,0 +1,88 @@ + + * @since 6.3 + * + * @implements IContextualConditionType + * @extends AbstractConditionType + */ +final class DayOfWeekRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType +{ + #[\Override] + public function getIdentifier(): string + { + return 'dayOfWeek'; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.request.dayOfWeek"; + } + + #[\Override] + public function getFormField(string $id): SingleSelectionFormField + { + return SingleSelectionFormField::create($id) + ->options( + \array_map( + static fn ($day) => WCF::getLanguage()->get('wcf.date.day.' . $day), + DateUtil::getWeekDays() + ) + ) + ->required(); + } + + #[\Override] + public function matches(): bool + { + $dateTime = new \DateTimeImmutable("@" . TIME_NOW, WCF::getUser()->getTimeZone()); + + return $dateTime->format('w') === $this->filter; + } + + #[\Override] + public function getCategory(): string + { + return "pointInTime"; + } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $daysOfWeeks = $conditionData['daysOfWeek'] ?? []; + if (\count($daysOfWeeks) > 1) { + // `NotDayOfWeekRequestConditionType` should migrate the data. + return []; + } + + $conditions = [ + [ + 'identifier' => $this->getIdentifier(), + 'value' => (string)\reset($daysOfWeeks), + ], + ]; + + unset($conditionData['daysOfWeek']); + + return $conditions; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.pointInTime.daysOfWeek'; + } +} diff --git a/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php new file mode 100644 index 00000000000..c415c74f51c --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/request/NotDayOfWeekRequestConditionType.class.php @@ -0,0 +1,100 @@ + + * @since 6.3 + * + * @implements IContextualConditionType + * @extends AbstractConditionType + */ +final class NotDayOfWeekRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType +{ + #[\Override] + public function getIdentifier(): string + { + return 'notDayOfWeek'; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.request.notDayOfWeek"; + } + + #[\Override] + public function getFormField(string $id): SingleSelectionFormField + { + return SingleSelectionFormField::create($id) + ->options( + \array_map( + static fn ($day) => WCF::getLanguage()->get('wcf.date.day.' . $day), + DateUtil::getWeekDays() + ) + ) + ->required(); + } + + #[\Override] + public function matches(): bool + { + $dateTime = new \DateTimeImmutable("@" . TIME_NOW, WCF::getUser()->getTimeZone()); + + return $dateTime->format('w') !== $this->filter; + } + + #[\Override] + public function getCategory(): string + { + return "pointInTime"; + } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $daysOfWeeks = $conditionData['daysOfWeek'] ?? []; + if (\count($daysOfWeeks) <= 1) { + // `DayOfWeekRequestConditionType` should migrate the data. + return []; + } + + if (\count($daysOfWeeks) === 7) { + // If all days have been selected, this condition is unnecessary and can be removed. + unset($conditionData['daysOfWeek']); + + return []; + } + + $conditions = []; + + // We must remove all selected week of days to convert the previous condition from an “or” to an “and” condition. + $daysOfWeeks = \array_diff_key(DateUtil::getWeekDays(), $daysOfWeeks); + + foreach (\array_keys($daysOfWeeks) as $dayOfWeek) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => (string)$dayOfWeek, + ]; + } + + unset($conditionData['daysOfWeek']); + + return $conditions; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.pointInTime.daysOfWeek'; + } +} diff --git a/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php new file mode 100644 index 00000000000..1b29d932a16 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/request/NotOnPageRequestConditionType.class.php @@ -0,0 +1,100 @@ + + * @since 6.3 + * + * @implements IContextualConditionType + * @extends AbstractConditionType + */ +final class NotOnPageRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType +{ + #[\Override] + public function getIdentifier(): string + { + return 'notOnPage'; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.request.notOnPage"; + } + + #[\Override] + public function getFormField(string $id): SingleSelectionFormField + { + // SelectFormField stores its value as a string, + // so we need to convert it to an integer in the `matches` method. + return SingleSelectionFormField::create($id) + ->options((new PageNodeTree())->getNodeList(), true) + ->required(); + } + + #[\Override] + public function matches(): bool + { + return RequestHandler::getInstance()->getActivePageID() !== (int)$this->filter; + } + + #[\Override] + public function getCategory(): string + { + return "page"; + } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $reverseLogic = $conditionData['pageIDs_reverseLogic'] ?? false; + $pageIDs = $conditionData['pageIDs'] ?? []; + + if (!$reverseLogic) { + // `ActivePageRequestConditionType` should migrate the data. + return []; + } + + $conditions = []; + foreach ($pageIDs as $pageID) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => (string)$pageID, + ]; + } + + unset($conditionData['pageIDs'], $conditionData['pageIDs_reverseLogic']); + + return $conditions; + } + + /** + * @return int[] + */ + private function getPageIDs(): array + { + $sql = "SELECT pageID + FROM wcf1_page"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute(); + + return $statement->fetchAll(\PDO::FETCH_COLUMN); + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.page'; + } +} diff --git a/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php new file mode 100644 index 00000000000..06aa7371f3e --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/request/TimeRequestConditionType.class.php @@ -0,0 +1,145 @@ + + * @since 6.3 + * + * @phpstan-type Filter = array{Condition: string, Value: string, Timezone: string} + * @implements IContextualConditionType + * @extends AbstractConditionType + */ +final class TimeRequestConditionType extends AbstractConditionType implements IContextualConditionType, IMigrateConditionType +{ + public const USER_TIMEZONE = 'userTimezone'; + + #[\Override] + public function getIdentifier(): string + { + return 'time'; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.request.time"; + } + + #[\Override] + public function getFormField(string $id): RowConditionFormFieldContainer + { + return RowConditionFormFieldContainer::create($id) + ->appendChildren([ + SingleSelectionFormField::create("{$id}Condition") + ->options(\array_combine($this->getConditions(), $this->getConditions())) + ->required(), + TimeFormField::create("{$id}Value") + ->required(), + SingleSelectionFormField::create("{$id}Timezone") + ->options($this->getTimezones()) + ->required(), + ]); + } + + #[\Override] + public function matches(): bool + { + ["Condition" => $condition, "Value" => $time, "Timezone" => $timezone] = $this->filter; + if ($timezone === self::USER_TIMEZONE) { + $timezoneObject = WCF::getUser()->getTimezone(); + } else { + $timezoneObject = new \DateTimeZone($timezone); + } + + $dateTime = \DateTimeImmutable::createFromFormat('H:i', $time, $timezoneObject); + + return match ($condition) { + '>' => TIME_NOW > $dateTime->getTimestamp(), + '<' => TIME_NOW < $dateTime->getTimestamp(), + '>=' => TIME_NOW >= $dateTime->getTimestamp(), + '<=' => TIME_NOW <= $dateTime->getTimestamp(), + default => false, + }; + } + + #[\Override] + public function getCategory(): string + { + return "pointInTime"; + } + + /** + * @return array + */ + public function getTimezones(): array + { + return \array_merge( + [self::USER_TIMEZONE => 'wcf.date.timezone.user'], + \array_combine( + DateUtil::getAvailableTimezones(), + \array_map( + static fn (string $timezone): string => WCF::getLanguage()->get( + 'wcf.date.timezone.' . \str_replace( + '/', + '.', + \strtolower($timezone) + ) + ), + DateUtil::getAvailableTimezones() + ) + ) + ); + } + + /** + * @return string[] + */ + protected function getConditions(): array + { + return [">", "<", ">=", "<="]; + } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $startTime = $conditionData['startTime'] ?? null; + $endTime = $conditionData['endTime'] ?? null; + $timezone = $conditionData['timezone'] ?? self::USER_TIMEZONE; + $conditions = []; + + if ($startTime !== null) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => ["Value" => $startTime, 'Condition' => '>', 'Timezone' => $timezone], + ]; + } + if ($endTime !== null) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => ["Value" => $endTime, 'Condition' => '<', 'Timezone' => $timezone], + ]; + } + + unset($conditionData['startTime'], $conditionData['endTime'], $conditionData['timezone']); + + return $conditions; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.pointInTime.time'; + } +} diff --git a/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php index c2ee9504da4..561a9de226c 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php @@ -26,6 +26,7 @@ class BooleanUserConditionType extends AbstractConditionType implements IDatabas public function __construct( public readonly string $identifier, public readonly string $columnName, + public readonly string $category, public readonly ?string $migrateKeyName = null, public readonly ?string $migrateConditionObjectType = null, ) { @@ -69,6 +70,12 @@ public function matches(object $object): bool } } + #[\Override] + public function getCategory(): string + { + return $this->category; + } + #[\Override] public function migrateConditionData(array &$conditionData): array { diff --git a/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php index 2c2306ae1b7..523f2320458 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php @@ -71,6 +71,12 @@ public function matches(object $object): bool return !\in_array((int)$this->filter, $trophyIDs, true); } + #[\Override] + public function getCategory(): string + { + return "userProfile"; + } + /** * @return Trophy[] */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php index 72b710fb7d5..8333af33177 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php @@ -71,6 +71,12 @@ public function matches(object $object): bool return \in_array((int)$this->filter, $trophyIDs, true); } + #[\Override] + public function getCategory(): string + { + return "userProfile"; + } + /** * @return Trophy[] */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php index 3b57a266753..a64510d45bf 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php @@ -71,6 +71,12 @@ public function matches(object $object): bool return \in_array((int)$this->filter, $object->getGroupIDs(), true); } + #[\Override] + public function getCategory(): string + { + return "user"; + } + #[\Override] public function canMigrateConditionData(string $objectType): bool { diff --git a/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php index 196d520968f..9319ece74e0 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php @@ -9,7 +9,7 @@ use wcf\system\condition\type\IDatabaseObjectListConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; -use wcf\system\form\builder\container\PrefixConditionFormFieldContainer; +use wcf\system\form\builder\container\condition\PrefixConditionFormFieldContainer; use wcf\system\form\builder\field\IntegerFormField; use wcf\system\form\builder\field\SingleSelectionFormField; @@ -29,6 +29,7 @@ class IntegerUserConditionType extends AbstractConditionType implements IDatabas public function __construct( public readonly string $identifier, public readonly string $columnName, + public readonly string $category, public readonly ?string $migrateConditionObjectType = null, ) { } @@ -83,6 +84,12 @@ public function matches(object $object): bool }; } + #[\Override] + public function getCategory(): string + { + return $this->category; + } + /** * @return string[] */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php index 1a81c66eb14..84cdcd27b2f 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php @@ -14,7 +14,7 @@ final class IsEnabledConditionType extends BooleanUserConditionType { public function __construct() { - parent::__construct("isEnabled", 'activationCode', 'userIsEnabled', 'com.woltlab.wcf.user.state'); + parent::__construct("isEnabled", 'activationCode', 'user', 'userIsEnabled', 'com.woltlab.wcf.user.state'); } #[\Override] diff --git a/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php index 6f243bc89ff..f032230ec9d 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php @@ -26,6 +26,7 @@ class IsNullUserConditionType extends AbstractConditionType implements IDatabase public function __construct( public readonly string $identifier, public readonly string $columnName, + public readonly string $category, public readonly ?string $migrateKeyName = null, public readonly ?string $migrateConditionObjectType = null, ) {} @@ -68,6 +69,12 @@ public function matches(object $object): bool } } + #[\Override] + public function getCategory(): string + { + return $this->category; + } + #[\Override] public function migrateConditionData(array &$conditionData): array { diff --git a/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php index ba74a10afd4..04ae4fa1d39 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php @@ -61,6 +61,12 @@ public function matches(object $object): bool return (int)$this->filter === $object->languageID; } + #[\Override] + public function getCategory(): string + { + return "user"; + } + #[\Override] public function migrateConditionData(array &$conditionData): array { diff --git a/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php index 12731c3864e..126edf1b97a 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php @@ -71,6 +71,12 @@ public function matches(object $object): bool return !\in_array((int)$this->filter, $object->getGroupIDs(), true); } + #[\Override] + public function getCategory(): string + { + return "user"; + } + #[\Override] public function canMigrateConditionData(string $objectType): bool { diff --git a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php index 80b3f8a95a0..51ecca460a6 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php @@ -9,7 +9,7 @@ use wcf\system\condition\type\IDatabaseObjectListConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; -use wcf\system\form\builder\container\PrefixConditionFormFieldContainer; +use wcf\system\form\builder\container\condition\PrefixConditionFormFieldContainer; use wcf\system\form\builder\field\DateFormField; use wcf\system\form\builder\field\SingleSelectionFormField; @@ -79,6 +79,12 @@ public function matches(object $object): bool }; } + #[\Override] + public function getCategory(): string + { + return "user"; + } + /** * @return string[] */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php index c0e2cfe97f1..dd26a01eec6 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php @@ -3,7 +3,7 @@ namespace wcf\system\condition\type\user; use wcf\data\DatabaseObjectList; -use wcf\system\form\builder\container\PrefixConditionFormFieldContainer; +use wcf\system\form\builder\container\condition\PrefixConditionFormFieldContainer; use wcf\system\form\builder\field\IntegerFormField; use wcf\util\DateUtil; @@ -56,6 +56,12 @@ public function matches(object $object): bool }; } + #[\Override] + public function getCategory(): string + { + return "user"; + } + /** * @return array{condition: string, timestamp: int} */ diff --git a/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php index 30e5e74b8ad..f8b8fffeacd 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php @@ -17,6 +17,7 @@ public function __construct() parent::__construct( 'signature', 'signature', + 'userProfile', 'userSignature', 'com.woltlab.wcf.user.signature' ); diff --git a/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php index 9093decab28..6032f4eb5b8 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php @@ -9,7 +9,7 @@ use wcf\system\condition\type\IDatabaseObjectListConditionType; use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; -use wcf\system\form\builder\container\PrefixConditionFormFieldContainer; +use wcf\system\form\builder\container\condition\PrefixConditionFormFieldContainer; use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\system\form\builder\field\TextFormField; use wcf\system\WCF; @@ -30,6 +30,7 @@ class StringUserConditionType extends AbstractConditionType implements IDatabase public function __construct( public readonly string $identifier, public readonly string $columnName, + public readonly string $category, public readonly ?string $migrateKeyName = null, public readonly ?string $migrateConditionObjectType = null, ) {} @@ -107,6 +108,12 @@ private function getConditions(): array ]; } + #[\Override] + public function getCategory(): string + { + return $this->category; + } + #[\Override] public function canMigrateConditionData(string $objectType): bool { diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/notices/ChangeShowOrder.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/notices/ChangeShowOrder.class.php index 3d4031c2d32..057694261ae 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/notices/ChangeShowOrder.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/notices/ChangeShowOrder.class.php @@ -6,9 +6,8 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use wcf\data\notice\Notice; -use wcf\data\notice\NoticeCache; use wcf\data\notice\NoticeList; -use wcf\system\cache\builder\NoticeCacheBuilder; +use wcf\system\cache\eager\NoticeCache; use wcf\system\endpoint\IController; use wcf\system\endpoint\PostRequest; use wcf\system\showOrder\ShowOrderHandler; @@ -63,6 +62,6 @@ private function saveShowOrder(array $items): void } WCF::getDB()->commitTransaction(); - NoticeCacheBuilder::getInstance()->reset(); + (new NoticeCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/form/builder/container/ConditionFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php similarity index 95% rename from wcfsetup/install/files/lib/system/form/builder/container/ConditionFormContainer.class.php rename to wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php index dad15b6b305..9256854bf1f 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/ConditionFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/condition/ConditionFormContainer.class.php @@ -1,9 +1,10 @@ $identifier, - "value" => $parameters['data'][$fieldId], + "value" => $parameters['data'][$fieldId] ?? $parameters[$fieldId], ]; } diff --git a/wcfsetup/install/files/lib/system/form/builder/container/PrefixConditionFormFieldContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/condition/PrefixConditionFormFieldContainer.class.php similarity index 98% rename from wcfsetup/install/files/lib/system/form/builder/container/PrefixConditionFormFieldContainer.class.php rename to wcfsetup/install/files/lib/system/form/builder/container/condition/PrefixConditionFormFieldContainer.class.php index c1bbf6eb21e..928db183941 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/PrefixConditionFormFieldContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/condition/PrefixConditionFormFieldContainer.class.php @@ -1,8 +1,9 @@ + * @since 6.3 + */ +final class RowConditionFormFieldContainer extends RowFormFieldContainer +{ + #[\Override] + public function populate() + { + $this->getDocument()->getDataHandler() + ->addProcessor( + new CustomFormDataProcessor( + $this->getId() . "DataProcessor", + function (IFormDocument $document, array $parameters) { + $data = []; + + foreach ($this->children() as $child) { + if (!($child instanceof IFormField)) { + continue; + } + + $id = $child->getId(); + $name = $this->getName($child); + + $data[$name] = $parameters['data'][$id]; + unset($parameters['data'][$id]); + } + + if ($data !== []) { + $parameters['data'][$this->getId()] = $data; + } + + return $parameters; + }, + ) + ); + + return parent::populate(); + } + + #[\Override] + public function updatedObject(array $data, IStorableObject $object, $loadValues = true) + { + if ($loadValues && isset($data[$this->getId()])) { + $values = []; + foreach ($data[$this->getId()] as $name => $value) { + $values[$this->getId() . $name] = $value; + } + + foreach ($this->children() as $child) { + if ($child instanceof IFormField || $child instanceof IFormContainer) { + $child->updatedObject($values, $object, $loadValues); + } + } + } + + return $this; + } + + private function getName(IFormField|IFormContainer $child): string + { + $id = $child->getId(); + + return \mb_substr($id, \mb_strlen($this->getId())); + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/field/CssClassNameFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/CssClassNameFormField.class.php new file mode 100644 index 00000000000..579b7f8f64f --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/CssClassNameFormField.class.php @@ -0,0 +1,157 @@ + + * @since 6.3 + */ +final class CssClassNameFormField extends RadioButtonFormField implements IPatternFormField +{ + use TPatternFormField; + + public const CUSTOM_CSS_CLASSNAME = 'custom'; + + /** + * @inheritDoc + */ + protected $templateName = 'shared_cssClassnameFormField'; + + private string $customClassName = ''; + private string $visualTemplate = '
{$label}
'; + + public function __construct() + { + $this + ->addClass('inlineList') + ->addFieldClass('classNameSelection__radio') + ->pattern('^-?[_a-zA-Z]+[_a-zA-Z0-9-]+$'); + } + + #[\Override] + public function readValue() + { + if ($this->getDocument()->hasRequestData($this->getPrefixedId())) { + $this->value = StringUtil::trim($this->getDocument()->getRequestData($this->getPrefixedId())); + + if ($this->supportsCustomClassName() && $this->value === self::CUSTOM_CSS_CLASSNAME) { + $this->customClassName = StringUtil::trim( + $this->getDocument()->getRequestData($this->getPrefixedId() . 'customCssClassName') + ); + } + } + + return $this; + } + + #[\Override] + public function validate() + { + if ($this->supportsCustomClassName() && $this->getValue() === self::CUSTOM_CSS_CLASSNAME) { + if (!Regex::compile($this->getPattern())->match($this->customClassName)) { + $this->addValidationError( + new FormFieldValidationError( + 'invalid', + 'wcf.global.form.error.invalidCssClassName' + ) + ); + } + } else { + parent::validate(); + } + } + + #[\Override] + public function value($value) + { + if ($this->supportsCustomClassName() && !\array_key_exists($value, $this->options)) { + parent::value(self::CUSTOM_CSS_CLASSNAME); + $this->customClassName = $value; + } else { + parent::value($value); + } + + return $this; + } + + #[\Override] + public function getSaveValue() + { + if ($this->hasCustomClassName()) { + return $this->getCustomClassName(); + } + + return $this->getValue(); + } + + public function hasCustomClassName(): bool + { + return $this->supportsCustomClassName() && $this->value === self::CUSTOM_CSS_CLASSNAME; + } + + public function getCustomClassName(): string + { + return $this->customClassName; + } + + /** + * Sets whether the custom class name is supported. + */ + public function supportCustomClassName(bool $supportCustomClassName = true): self + { + $options = $this->options; + + if ($supportCustomClassName) { + // already supported + if ($this->supportsCustomClassName()) { + return $this; + } + + $options[self::CUSTOM_CSS_CLASSNAME] = ''; + } else { + unset($options[self::CUSTOM_CSS_CLASSNAME]); + } + + return $this->options($options); + } + + /** + * Returns whether the custom class name is supported. + */ + public function supportsCustomClassName(): bool + { + return \array_key_exists(self::CUSTOM_CSS_CLASSNAME, $this->options); + } + + public function visualTemplate(string $visualTemplate): self + { + $this->visualTemplate = $visualTemplate; + + return $this; + } + + public function getVisualTemplate(): string + { + return $this->visualTemplate; + } + + public function renderVisualTemplate(string $className, string $label): string + { + return WCF::getTPL()->fetchString( + WCF::getTPL()->getCompiler()->compileString('visualTemplate', $this->visualTemplate)['template'], + [ + 'className' => $className, + 'label' => $label, + ] + ); + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/field/TimeFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/TimeFormField.class.php new file mode 100644 index 00000000000..9be90a48d5e --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/field/TimeFormField.class.php @@ -0,0 +1,124 @@ + + * @since 6.3 + */ +final class TimeFormField extends AbstractFormField implements + IAttributeFormField, + IAutoFocusFormField, + ICssClassFormField, + IImmutableFormField, + INullableFormField +{ + use TInputAttributeFormField; + use TAutoFocusFormField; + use TCssClassFormField; + use TImmutableFormField; + use TNullableFormField; + + public const FORMAT = 'H:i'; + /** + * @inheritDoc + */ + protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Date'; + /** + * @inheritDoc + */ + protected $templateName = 'shared_timeFormField'; + + public function __construct() + { + $this->addFieldClass('medium') + // If no value is set for the time, the time selection cannot be used. + ->value('00:00'); + } + + #[\Override] + public function getSaveValue() + { + if ($this->getValue() === null) { + if ($this->isNullable()) { + return; + } else { + return DateUtil::getDateTimeByTimestamp(0)->format(self::FORMAT); + } + } + + return $this->getValueDateTimeObject()->format(self::FORMAT); + } + + #[\Override] + public function validate() + { + if ($this->getValue() === null) { + if ($this->isRequired()) { + $this->addValidationError(new FormFieldValidationError('empty')); + } + } + } + + #[\Override] + public function value($value) + { + parent::value($value); + + $dateTime = \DateTimeImmutable::createFromFormat( + self::FORMAT, + $this->getValue(), + ); + if ($dateTime === false) { + throw new \InvalidArgumentException( + "Given value does not match format '" . self::FORMAT . "' for field '{$this->getId()}'." + ); + } + + parent::value($dateTime->format(self::FORMAT)); + + return $this; + } + + #[\Override] + public function readValue() + { + if ( + $this->getDocument()->hasRequestData($this->getPrefixedId()) + && \is_string($this->getDocument()->getRequestData($this->getPrefixedId())) + ) { + $value = $this->getDocument()->getRequestData($this->getPrefixedId()); + $this->value = $value; + + if ($this->value === '') { + $this->value = null; + } elseif ($this->getValueDateTimeObject() === null) { + try { + $this->value($value); + } catch (\InvalidArgumentException) { + $this->value = null; + } + } + } + + return $this; + } + + private function getValueDateTimeObject(): ?\DateTimeImmutable + { + $dateTime = \DateTimeImmutable::createFromFormat('H:i', $this->getValue()); + + if ($dateTime === false) { + return null; + } + + return $dateTime; + } +} diff --git a/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php b/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php index f8eb13fc4cb..fea394da139 100644 --- a/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php +++ b/wcfsetup/install/files/lib/system/notice/NoticeHandler.class.php @@ -3,8 +3,12 @@ namespace wcf\system\notice; use wcf\data\notice\Notice; -use wcf\system\cache\builder\NoticeCacheBuilder; +use wcf\system\cache\eager\NoticeCache; +use wcf\system\condition\ConditionHandler; +use wcf\system\condition\provider\combined\NoticeConditionProvider; +use wcf\system\condition\type\IContextualConditionType; use wcf\system\SingletonFactory; +use wcf\system\WCF; /** * Handles notice-related matters. @@ -32,7 +36,7 @@ class NoticeHandler extends SingletonFactory */ protected function init() { - $this->notices = NoticeCacheBuilder::getInstance()->getData(); + $this->notices = (new NoticeCache())->getCache(); } /** @@ -47,14 +51,19 @@ public function getVisibleNotices() } $notices = []; + $provider = new NoticeConditionProvider(); foreach ($this->notices as $notice) { if ($notice->isDismissed()) { continue; } - $conditions = $notice->getConditions(); + $conditions = ConditionHandler::getInstance()->getConditionsWithFilter($provider, $notice->getConditions()); foreach ($conditions as $condition) { - if (!$condition->getObjectType()->getProcessor()->showContent($condition)) { + $matches = $condition instanceof IContextualConditionType + ? $condition->matches() + : $condition->matches(WCF::getUser()); + + if (!$matches) { continue 2; } } diff --git a/wcfsetup/install/files/lib/system/notice/command/MigrateLegacyCondition.class.php b/wcfsetup/install/files/lib/system/notice/command/MigrateLegacyCondition.class.php new file mode 100644 index 00000000000..9db92ba3712 --- /dev/null +++ b/wcfsetup/install/files/lib/system/notice/command/MigrateLegacyCondition.class.php @@ -0,0 +1,49 @@ + + * @since 6.3 + */ +final class MigrateLegacyCondition +{ + public function __construct(public readonly Notice $notice) + { + } + + public function __invoke(): void + { + if (!$this->notice->isLegacy) { + return; + } + + try { + $json = JSON::decode($this->notice->conditions); + } catch (SystemException $ex) { + $ex->getExceptionID(); // Log the exception if JSON decoding fails + + return; + } + + $migratedData = ConditionHandler::getInstance()->migrateConditionData(new NoticeConditionProvider(), $json); + + $editor = new NoticeEditor($this->notice); + $editor->update([ + 'conditions' => JSON::encode($migratedData->conditions), + 'isLegacy' => 0, + 'isDisabled' => $migratedData->isFullyMigrated ? $this->notice->isDisabled : 1, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/worker/NoticeRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/NoticeRebuildDataWorker.class.php new file mode 100644 index 00000000000..23a5a1c0936 --- /dev/null +++ b/wcfsetup/install/files/lib/system/worker/NoticeRebuildDataWorker.class.php @@ -0,0 +1,39 @@ + + * @since 6.3 + * + * @extends AbstractLinearRebuildDataWorker + */ +final class NoticeRebuildDataWorker extends AbstractLinearRebuildDataWorker +{ + /** + * @inheritDoc + */ + protected $objectListClassName = NoticeList::class; + + /** + * @inheritDoc + */ + protected $limit = 100; + + #[\Override] + public function execute() + { + parent::execute(); + + foreach ($this->objectList as $notice) { + (new MigrateLegacyCondition($notice))(); + } + } +} diff --git a/wcfsetup/install/files/style/ui/classNameSelection.scss b/wcfsetup/install/files/style/ui/classNameSelection.scss new file mode 100644 index 00000000000..5c6ed3e58da --- /dev/null +++ b/wcfsetup/install/files/style/ui/classNameSelection.scss @@ -0,0 +1,28 @@ +.classNameSelection.inlineList > li { + flex-basis: 30%; +} + +.classNameSelection > li.custom { + display: flex; +} + +.classNameSelection__label { + display: flex; + flex: 1 1 auto; + align-items: center; + margin-bottom: 10px; +} + +.classNameSelection__radio { + flex: 0 0 auto; + margin-right: 7px; +} + +.classNameSelection > li.custom .classNameSelection__span { + flex: 1 1 auto; +} + +.classNameSelection__label > woltlab-core-notice { + display: inline-flex; + margin-top: 0; +} diff --git a/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss b/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss index 2ef8fb14209..02feb3d11de 100644 --- a/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss +++ b/wcfsetup/install/files/style/ui/scrollableCheckboxList.scss @@ -14,6 +14,28 @@ } } +.scrollableCheckboxList__category:not(:first-child) { + margin-top: 5px; +} + +.scrollableCheckboxList__category__label { + align-items: center; + color: var(--wcfContentDimmedText); + column-gap: 10px; + display: flex; + font-size: 12px; + margin-bottom: 5px; + white-space: nowrap; + + &::after { + border-top: 1px solid currentColor; + content: ""; + display: block; + width: 100%; + opacity: 0.34; + } +} + .dialogContent .scrollableCheckboxList { max-height: 300px; } diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 69319c89e62..3fb5b6c8e0f 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -1296,6 +1296,7 @@ Sie erreichen das Fehlerprotokoll unter: {link controller='ExceptionLogView' isE + Anzeigen aktualisieren durch.]]>
@@ -2705,6 +2706,8 @@ Abschnitte dürfen nicht leer sein und nur folgende Zeichen enthalten: [a-z + + @@ -3562,6 +3565,15 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt + + + + + + + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index e7d1bff1f71..3be6ec4e537 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -1272,6 +1272,7 @@ You can access the error log at: {link controller='ExceptionLogView' isEmail=tru + Rebuild Data.]]> @@ -2632,6 +2633,8 @@ If you have already bought the licenses for the listed apps, th + + @@ -3485,6 +3488,15 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi + + + + + + + + + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 1eb3e7d493e..4bf96a81fae 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -903,7 +903,9 @@ CREATE TABLE wcf1_notice ( cssClassName VARCHAR(255) NOT NULL DEFAULT 'info', showOrder INT(10) NOT NULL DEFAULT 0, isDisabled TINYINT(1) NOT NULL DEFAULT 0, - isDismissible TINYINT(1) NOT NULL DEFAULT 0 + isDismissible TINYINT(1) NOT NULL DEFAULT 0, + conditions MEDIUMTEXT, + isLegacy TINYINT(1) NOT NULL DEFAULT 0 ); DROP TABLE IF EXISTS wcf1_notice_dismissed;