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 @@
+getPrefixedId()}" {*
+ *}name="{$field->getPrefixedId()}" {*
+ *}value="{$field->getValue()}"{*
+ *}{if !$field->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'}
-
-
-
-
-
+{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;