diff --git a/com.woltlab.wcf/package.xml b/com.woltlab.wcf/package.xml index bbf5922e535..882df82c685 100644 --- a/com.woltlab.wcf/package.xml +++ b/com.woltlab.wcf/package.xml @@ -51,9 +51,8 @@ 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 8e18c78e57a..1a24a49c9b2 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 @@ -8,6 +8,7 @@ * @license GNU Lesser General Public License */ +use wcf\system\database\table\column\DefaultFalseBooleanDatabaseTableColumn; use wcf\system\database\table\column\MediumtextDatabaseTableColumn; use wcf\system\database\table\PartialDatabaseTable; @@ -15,5 +16,6 @@ PartialDatabaseTable::create('wcf1_user_group_assignment') ->columns([ MediumtextDatabaseTableColumn::create('conditions'), + DefaultFalseBooleanDatabaseTableColumn::create('isLegacy'), ]), ]; diff --git a/wcfsetup/install/files/acp/templates/userGroupAssignmentList.tpl b/wcfsetup/install/files/acp/templates/userGroupAssignmentList.tpl index aa70b14a94a..3ea56dd5ab6 100644 --- a/wcfsetup/install/files/acp/templates/userGroupAssignmentList.tpl +++ b/wcfsetup/install/files/acp/templates/userGroupAssignmentList.tpl @@ -14,6 +14,12 @@ +{if $hasLegacyObjects} + + {lang}wcf.acp.group.assignment.legacyNotice{/lang} + +{/if} +
{unsafe:$gridView->render()}
diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_userGroupAssignment.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_userGroupAssignment.php new file mode 100644 index 00000000000..fafe2d74d07 --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_userGroupAssignment.php @@ -0,0 +1,59 @@ +exportConditions("com.woltlab.wcf.condition.userGroupAssignment"); +if ($exportedConditions === []) { + return; +} + +$sql = "UPDATE wcf1_user_group_assignment + SET conditions = ?, + isLegacy = ? + WHERE assignmentID = ?"; +$statement = WCF::getDB()->prepare($sql); +foreach ($exportedConditions as $assignmentID => $conditionData) { + renameObjectTypes($conditionData); + + $statement->execute([ + JSON::encode($conditionData), + 1, + $assignmentID, + ]); +} + +/** + * Rename the object types so that the migration functions can handle them. + * @see \wcf\system\condition\provider\UserConditionProvider + * + * @param array $conditionData + */ +function renameObjectTypes(array &$conditionData): void +{ + $objectTypeMap = [ + 'com.woltlab.wcf.username' => 'com.woltlab.wcf.user.username', + 'com.woltlab.wcf.email' => 'com.woltlab.wcf.user.email', + 'com.woltlab.wcf.userGroup' => 'com.woltlab.wcf.user.userGroup', + 'com.woltlab.wcf.languages' => 'com.woltlab.wcf.user.languages', + 'com.woltlab.wcf.registrationDate' => 'com.woltlab.wcf.user.registrationDate', + 'com.woltlab.wcf.registrationDateInterval' => 'com.woltlab.wcf.user.registrationDateInterval', + 'com.woltlab.wcf.avatar' => 'com.woltlab.wcf.user.avatar', + 'com.woltlab.wcf.signature' => 'com.woltlab.wcf.user.signature', + 'com.woltlab.wcf.coverPhoto' => 'com.woltlab.wcf.user.coverPhoto', + 'com.woltlab.wcf.state' => 'com.woltlab.wcf.user.state', + 'com.woltlab.wcf.activityPoints' => 'com.woltlab.wcf.user.activityPoints', + 'com.woltlab.wcf.likesReceived' => 'com.woltlab.wcf.user.likesReceived', + // TODO 'com.woltlab.wcf.userOptions' + 'com.woltlab.wcf.userTrophyCondition' => 'com.woltlab.wcf.user.trophyCondition', + 'com.woltlab.wcf.trophyPoints' => 'com.woltlab.wcf.user.trophyPoints', + ]; + + foreach ($objectTypeMap as $currentName => $newName) { + if (isset($conditionData[$currentName])) { + $conditionData[$newName] = $conditionData[$currentName]; + unset($conditionData[$currentName]); + } + } +} diff --git a/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentEditForm.class.php b/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentEditForm.class.php index 82afbe37888..ce842bb40f3 100644 --- a/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserGroupAssignmentEditForm.class.php @@ -4,6 +4,9 @@ use wcf\data\user\group\assignment\UserGroupAssignment; use wcf\system\exception\IllegalLinkException; +use wcf\system\exception\NamedUserException; +use wcf\system\WCF; +use wcf\util\HtmlString; /** * Shows the form to edit an existing automatic user group assignment. @@ -39,5 +42,11 @@ public function readParameters() if (!$this->formObject->assignmentID) { throw new IllegalLinkException(); } + + if ($this->formObject->isLegacy) { + throw new NamedUserException( + HtmlString::fromSafeHtml(WCF::getLanguage()->getDynamicVariable('wcf.acp.group.assignment.legacyNotice')) + ); + } } } diff --git a/wcfsetup/install/files/lib/acp/page/UserGroupAssignmentListPage.class.php b/wcfsetup/install/files/lib/acp/page/UserGroupAssignmentListPage.class.php index d296fd146b0..baa50dbcfe9 100644 --- a/wcfsetup/install/files/lib/acp/page/UserGroupAssignmentListPage.class.php +++ b/wcfsetup/install/files/lib/acp/page/UserGroupAssignmentListPage.class.php @@ -4,6 +4,7 @@ use wcf\page\AbstractGridViewPage; use wcf\system\gridView\admin\UserGroupAssignmentGridView; +use wcf\system\WCF; /** * Lists the available automatic user group assignments. @@ -31,4 +32,25 @@ protected function createGridView(): UserGroupAssignmentGridView { return new UserGroupAssignmentGridView(); } + + #[\Override] + public function assignVariables() + { + parent::assignVariables(); + + WCF::getTPL()->assign([ + 'hasLegacyObjects' => $this->hasLegacyObjects(), + ]); + } + + private function hasLegacyObjects(): bool + { + $sql = "SELECT COUNT(*) AS count + FROM wcf1_user_group_assignment + WHERE isLegacy = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([1]); + + return $statement->fetchColumn() > 0; + } } diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index ddc85cf07e0..ddd54414d5c 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -97,6 +97,7 @@ static function (\wcf\event\worker\RebuildWorkerCollecting $event) { $event->register(\wcf\system\worker\UnfurlUrlRebuildDataWorker::class, 450); $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\StatDailyRebuildDataWorker::class, 800); } ); diff --git a/wcfsetup/install/files/lib/data/user/group/UserGroupEditor.class.php b/wcfsetup/install/files/lib/data/user/group/UserGroupEditor.class.php index a2cb30115c1..80fe5869ea8 100644 --- a/wcfsetup/install/files/lib/data/user/group/UserGroupEditor.class.php +++ b/wcfsetup/install/files/lib/data/user/group/UserGroupEditor.class.php @@ -4,9 +4,9 @@ use wcf\data\DatabaseObjectEditor; use wcf\data\IEditableCachedObject; -use wcf\system\cache\builder\UserGroupAssignmentCacheBuilder; use wcf\system\cache\builder\UserGroupCacheBuilder; use wcf\system\cache\builder\UserGroupPermissionCacheBuilder; +use wcf\system\cache\eager\UserGroupAssignmentCache; use wcf\system\exception\SystemException; use wcf\system\user\storage\UserStorageHandler; use wcf\system\WCF; @@ -208,7 +208,7 @@ public static function resetCache() UserGroupPermissionCacheBuilder::getInstance()->reset(); // https://github.com/WoltLab/WCF/issues/4045 - UserGroupAssignmentCacheBuilder::getInstance()->reset(); + (new UserGroupAssignmentCache())->rebuild(); // Clear cached group assignments. UserStorageHandler::getInstance()->resetAll('groupIDs'); diff --git a/wcfsetup/install/files/lib/data/user/group/assignment/UserGroupAssignment.class.php b/wcfsetup/install/files/lib/data/user/group/assignment/UserGroupAssignment.class.php index 545b1a7cd98..e2be3ebac6f 100644 --- a/wcfsetup/install/files/lib/data/user/group/assignment/UserGroupAssignment.class.php +++ b/wcfsetup/install/files/lib/data/user/group/assignment/UserGroupAssignment.class.php @@ -19,6 +19,7 @@ * @property-read string $title title of the automatic user group assignment * @property-read int $isDisabled is `1` if the user group assignment is disabled and thus not checked for automatic assignments, otherwise `0` * @property-read string $conditions JSON-encoded string containing the conditions of the automatic user group assignment + * @property-read bool $isLegacy indicates whether the conditions need to be migrated to the new format */ class UserGroupAssignment extends DatabaseObject implements IRouteController { diff --git a/wcfsetup/install/files/lib/data/user/group/assignment/UserGroupAssignmentEditor.class.php b/wcfsetup/install/files/lib/data/user/group/assignment/UserGroupAssignmentEditor.class.php index 371bbb319c4..ccadb8fb5c5 100644 --- a/wcfsetup/install/files/lib/data/user/group/assignment/UserGroupAssignmentEditor.class.php +++ b/wcfsetup/install/files/lib/data/user/group/assignment/UserGroupAssignmentEditor.class.php @@ -4,7 +4,7 @@ use wcf\data\DatabaseObjectEditor; use wcf\data\IEditableCachedObject; -use wcf\system\cache\builder\UserGroupAssignmentCacheBuilder; +use wcf\system\cache\eager\UserGroupAssignmentCache; /** * Executes user group assignment-related actions. @@ -29,6 +29,6 @@ class UserGroupAssignmentEditor extends DatabaseObjectEditor implements IEditabl */ public static function resetCache() { - UserGroupAssignmentCacheBuilder::getInstance()->reset(); + (new UserGroupAssignmentCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/event/acp/dashboard/box/MigrationCollecting.class.php b/wcfsetup/install/files/lib/event/acp/dashboard/box/MigrationCollecting.class.php new file mode 100644 index 00000000000..d7cc204ece6 --- /dev/null +++ b/wcfsetup/install/files/lib/event/acp/dashboard/box/MigrationCollecting.class.php @@ -0,0 +1,37 @@ + + * @since 6.2 + */ +final class MigrationCollecting implements IPsr14Event +{ + /** + * @var string[] + */ + private array $needsMigration = []; + + /** + * Adds the name of objects that still need to be migrated on the `RebuildDataPage` + */ + public function migrationNeeded(string $title): void + { + $this->needsMigration[] = $title; + } + + /** + * @return string[] + */ + public function needsMigration(): array + { + return $this->needsMigration; + } +} 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 d1a61362972..c8db4cd4c38 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 @@ -3,6 +3,7 @@ namespace wcf\system\acp\dashboard\box; use wcf\data\devtools\missing\language\item\DevtoolsMissingLanguageItemList; +use wcf\event\acp\dashboard\box\MigrationCollecting; use wcf\event\acp\dashboard\box\PHPExtensionCollecting; use wcf\event\acp\dashboard\box\StatusMessageCollecting; use wcf\system\application\ApplicationHandler; @@ -60,6 +61,7 @@ private function getMessages(): array $this->getPHPExtensionMessage(), $this->getEvaluationMessages(), $this->getBasicMessages(), + $this->getMigrationMessage(), $this->getCustomMessages() ); } @@ -271,4 +273,42 @@ private function getPHPExtensionMessage(): array return []; } + + /** + * @return StatusMessage[] + * + * @since 6.2 + */ + private function getMigrationMessage(): array + { + $event = new MigrationCollecting(); + EventHandler::getInstance()->fire($event); + if ($this->userGroupAssignmentHasLegacyObjects()) { + $event->migrationNeeded(WCF::getLanguage()->get('wcf.acp.group.assignment')); + } + + if ($event->needsMigration() === []) { + return []; + } + + return [ + new StatusMessage( + StatusMessageType::Warning, + WCF::getLanguage()->getDynamicVariable('wcf.acp.dashboard.box.migrationNeeded', [ + 'titles' => $event->needsMigration(), + ]) + ), + ]; + } + + private function userGroupAssignmentHasLegacyObjects(): bool + { + $sql = "SELECT COUNT(*) AS count + FROM wcf1_user_group_assignment + WHERE isLegacy = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([1]); + + return $statement->fetchColumn() > 0; + } } diff --git a/wcfsetup/install/files/lib/system/cache/builder/UserGroupAssignmentCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/UserGroupAssignmentCacheBuilder.class.php index 589b86aa876..2283ec66597 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/UserGroupAssignmentCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/UserGroupAssignmentCacheBuilder.class.php @@ -2,7 +2,7 @@ namespace wcf\system\cache\builder; -use wcf\data\user\group\assignment\UserGroupAssignmentList; +use wcf\system\cache\eager\UserGroupAssignmentCache; /** * Caches the enabled automatic user group assignments. @@ -10,18 +10,20 @@ * @author Matthias Schmidt * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License + * + * @deprecated 6.2 use `UserGroupAssignmentCache` instead */ -class UserGroupAssignmentCacheBuilder extends AbstractCacheBuilder +final class UserGroupAssignmentCacheBuilder extends AbstractLegacyCacheBuilder { - /** - * @inheritDoc - */ - protected function rebuild(array $parameters) + #[\Override] + protected function rebuild(array $parameters): array { - $assignmentList = new UserGroupAssignmentList(); - $assignmentList->getConditionBuilder()->add('isDisabled = ?', [0]); - $assignmentList->readObjects(); + return (new UserGroupAssignmentCache())->getCache(); + } - return $assignmentList->getObjects(); + #[\Override] + public function reset(array $parameters = []) + { + (new UserGroupAssignmentCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/cache/eager/UserGroupAssignmentCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/UserGroupAssignmentCache.class.php new file mode 100644 index 00000000000..ee6219b1bca --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/UserGroupAssignmentCache.class.php @@ -0,0 +1,30 @@ + + * @since 6.3 + * + * @extends AbstractEagerCache> + */ +final class UserGroupAssignmentCache extends AbstractEagerCache +{ + #[\Override] + protected function getCacheData(): array + { + $assignmentList = new UserGroupAssignmentList(); + $assignmentList->getConditionBuilder()->add('isDisabled = ?', [0]); + $assignmentList->getConditionBuilder()->add('isLegacy = ?', [0]); + $assignmentList->readObjects(); + + return $assignmentList->getObjects(); + } +} diff --git a/wcfsetup/install/files/lib/system/condition/ConditionHandler.class.php b/wcfsetup/install/files/lib/system/condition/ConditionHandler.class.php index 77b8fe62fa9..cf0e193a276 100644 --- a/wcfsetup/install/files/lib/system/condition/ConditionHandler.class.php +++ b/wcfsetup/install/files/lib/system/condition/ConditionHandler.class.php @@ -10,8 +10,11 @@ use wcf\system\cache\builder\ConditionCacheBuilder; use wcf\system\condition\provider\AbstractConditionProvider; use wcf\system\condition\type\IConditionType; +use wcf\system\condition\type\IMigrateConditionType; +use wcf\system\database\util\PreparedStatementConditionBuilder; use wcf\system\exception\SystemException; use wcf\system\SingletonFactory; +use wcf\system\WCF; /** * Handles general condition-related matters. @@ -172,4 +175,83 @@ public function getConditionsWithFilter(AbstractConditionProvider $provider, arr return $result; } + + /** + * Exports the conditions for all objects that belong to the specified object type definition. + * + * @return array> + */ + public function exportConditions(string $definitionName): array + { + $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes($definitionName); + if ($objectTypes === []) { + return []; + } + + $conditionBuilder = new PreparedStatementConditionBuilder(); + $conditionBuilder->add('objectTypeID IN (?)', [ + \array_column($objectTypes, 'objectTypeID'), + ]); + + $sql = "SELECT * + FROM wcf1_condition + {$conditionBuilder}"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute($conditionBuilder->getParameters()); + + $result = []; + while ($row = $statement->fetchArray()) { + $result[$row['objectID']] ??= []; + $result[$row['objectID']][ObjectTypeCache::getInstance()->getObjectType($row['objectTypeID'])->objectType] = \unserialize($row['conditionData']); + } + + return $result; + } + + /** + * The stored data from the `wcf1_condition` table is migrated to the new format. + * The key of `$conditionData` is the type of condition (objectType), the value is the content of the `wcf1_condition.conditionData` column, unserialize as an array. + * + * @template TCondition of IConditionType + * @param AbstractConditionProvider $provider + * @param array> $conditionData + */ + public function migrateConditionData(AbstractConditionProvider $provider, array $conditionData): ConditionMigration + { + if ($conditionData === []) { + return ConditionMigration::withoutData(); + } + + $migratedData = []; + /** @var IMigrateConditionType[] $conditionTypes */ + $conditionTypes = \array_filter( + $provider->getConditionTypes(), + static fn (IConditionType $condition): bool => $condition instanceof IMigrateConditionType + ); + + if ($conditionTypes === []) { + return ConditionMigration::withoutData(); + } + + foreach ($conditionData as $objectType => &$condition) { + foreach ($conditionTypes as $conditionType) { + if (!$conditionType->canMigrateConditionData($objectType)) { + continue; + } + + \array_push( + $migratedData, + ...$conditionType->migrateConditionData($condition) + ); + + if ($condition === []) { + unset($conditionData[$objectType]); + break; + } + } + } + unset($condition); + + return ConditionMigration::fromData($conditionData, $migratedData); + } } diff --git a/wcfsetup/install/files/lib/system/condition/ConditionMigration.class.php b/wcfsetup/install/files/lib/system/condition/ConditionMigration.class.php new file mode 100644 index 00000000000..e07130dad89 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/ConditionMigration.class.php @@ -0,0 +1,38 @@ + + * @since 6.2 + */ +final class ConditionMigration +{ + private function __construct( + public readonly bool $isFullyMigrated, + /** @var array{identifier: string, value: mixed}[] */ + public readonly array $conditions, + ) { + } + + /** + * Creates a new ConditionMigration instance based on condition data and conditions. + * + * @param array{identifier: string, value: mixed}[] $previousConditionData + * @param array{identifier: string, value: mixed}[] $migratedConditionData + */ + public static function fromData(array $previousConditionData, array $migratedConditionData): self + { + return new self($previousConditionData === [], $migratedConditionData); + } + + /** + * Creates a new ConditionMigration instance for empty data. + */ + public static function withoutData(): self + { + return new self(true, []); + } +} 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 78d0a58f30f..1d26c45869e 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/AbstractConditionProvider.class.php @@ -30,18 +30,6 @@ public function addCondition(IConditionType $conditionType): void $this->conditionTypes[$conditionType->getIdentifier()] = $conditionType; } - /** - * Adds multiple condition types to this provider. - * - * @param TCondition[] $conditionTypes - */ - public function addConditions(array $conditionTypes): void - { - foreach ($conditionTypes as $conditionType) { - $this->addCondition($conditionType); - } - } - final public function getFieldId(string $containerId, string $identifier, int $index): string { return "{$containerId}_{$identifier}_{$index}"; 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 110d3ac3e48..1e622a03036 100644 --- a/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php +++ b/wcfsetup/install/files/lib/system/condition/provider/UserConditionProvider.class.php @@ -7,19 +7,19 @@ use wcf\event\condition\provider\UserConditionProviderCollecting; use wcf\system\condition\type\IDatabaseObjectListConditionType; use wcf\system\condition\type\IObjectConditionType; -use wcf\system\condition\type\user\AbstractUserBooleanConditionType; -use wcf\system\condition\type\user\AbstractUserIntegerConditionType; -use wcf\system\condition\type\user\AbstractUserIsNullConditionType; -use wcf\system\condition\type\user\AbstractUserStringConditionType; -use wcf\system\condition\type\user\UserHasNotTrophyConditionType; -use wcf\system\condition\type\user\UserHasTrophyConditionType; -use wcf\system\condition\type\user\UserInGroupConditionType; -use wcf\system\condition\type\user\UserIsEnabledConditionType; -use wcf\system\condition\type\user\UserLanguageConditionType; -use wcf\system\condition\type\user\UserNotInGroupConditionType; -use wcf\system\condition\type\user\UserRegistrationDateConditionType; -use wcf\system\condition\type\user\UserRegistrationDaysConditionType; -use wcf\system\condition\type\user\UserSignatureConditionType; +use wcf\system\condition\type\user\BooleanUserConditionType; +use wcf\system\condition\type\user\HasNotTrophyUserConditionType; +use wcf\system\condition\type\user\HasTrophyUserConditionType; +use wcf\system\condition\type\user\InGroupUserConditionType; +use wcf\system\condition\type\user\IntegerUserConditionType; +use wcf\system\condition\type\user\IsEnabledConditionType; +use wcf\system\condition\type\user\IsNullUserConditionType; +use wcf\system\condition\type\user\LanguageUserConditionType; +use wcf\system\condition\type\user\NotInGroupUserConditionType; +use wcf\system\condition\type\user\RegistrationDateUserConditionType; +use wcf\system\condition\type\user\RegistrationDaysUserConditionType; +use wcf\system\condition\type\user\SignatureUserConditionType; +use wcf\system\condition\type\user\StringUserConditionType; use wcf\system\event\EventHandler; /** @@ -34,27 +34,110 @@ final class UserConditionProvider extends AbstractConditionProvider { public function __construct() { - $this->addConditions([ - new class("username", "username") extends AbstractUserStringConditionType {}, - new class("email", "email") extends AbstractUserStringConditionType {}, - new UserRegistrationDateConditionType(), - new UserRegistrationDaysConditionType(), - new UserInGroupConditionType(), - new UserNotInGroupConditionType(), - new UserLanguageConditionType(), - new class("avatar", 'avatarFileID') extends AbstractUserIsNullConditionType {}, - new UserSignatureConditionType(), - new class("coverPhoto", 'coverPhotoFileID') extends AbstractUserIsNullConditionType {}, - new class("isBanned", 'banned') extends AbstractUserBooleanConditionType {}, - new UserIsEnabledConditionType(), - new class("isEmailConfirmed", 'emailConfirmed') extends AbstractUserIsNullConditionType {}, - new class("isMultifactorActive", 'multifactorActive') extends AbstractUserBooleanConditionType {}, - new UserHasTrophyConditionType(), - new UserHasNotTrophyConditionType(), - new class("activityPoints", "activityPoints") extends AbstractUserIntegerConditionType {}, - new class("likesReceived", "likesReceived") extends AbstractUserIntegerConditionType {}, - new class("trophyPoints", "trophyPoints") extends AbstractUserIntegerConditionType {}, - ]); + $this->addCondition( + new StringUserConditionType( + identifier: "username", + columnName: "username", + migrateKeyName: "username", + migrateConditionObjectType: 'com.woltlab.wcf.user.username' + ), + ); + $this->addCondition( + new StringUserConditionType( + identifier: "email", + columnName: "email", + migrateKeyName: "email", + migrateConditionObjectType: 'com.woltlab.wcf.user.email' + ), + ); + $this->addCondition( + new RegistrationDateUserConditionType(), + ); + $this->addCondition( + new RegistrationDaysUserConditionType(), + ); + $this->addCondition( + new InGroupUserConditionType(), + ); + $this->addCondition( + new NotInGroupUserConditionType(), + ); + $this->addCondition( + new LanguageUserConditionType(), + ); + $this->addCondition( + new IsNullUserConditionType( + identifier: "avatar", + columnName: 'avatarFileID', + migrateKeyName: 'userAvatar', + migrateConditionObjectType: 'com.woltlab.wcf.user.avatar' + ), + ); + $this->addCondition( + new SignatureUserConditionType(), + ); + $this->addCondition( + new IsNullUserConditionType( + identifier: "coverPhoto", + columnName: 'coverPhotoFileID', + migrateKeyName: 'userCoverPhoto', + migrateConditionObjectType: 'com.woltlab.wcf.coverPhoto' + ), + ); + $this->addCondition( + new BooleanUserConditionType( + identifier: "isBanned", + columnName: 'banned', + migrateKeyName: 'userIsBanned', + migrateConditionObjectType: 'com.woltlab.wcf.user.state' + ), + ); + $this->addCondition( + new IsEnabledConditionType(), + ); + $this->addCondition( + new IsNullUserConditionType( + identifier: "isEmailConfirmed", + columnName: 'emailConfirmed', + migrateKeyName: 'userIsEmailConfirmed', + migrateConditionObjectType: 'com.woltlab.wcf.user.state' + ), + ); + $this->addCondition( + new BooleanUserConditionType( + identifier: "isMultifactorActive", + columnName: 'multifactorActive', + migrateKeyName: 'multifactorActive', + migrateConditionObjectType: 'com.woltlab.wcf.user.multifactor' + ), + ); + $this->addCondition( + new HasTrophyUserConditionType(), + ); + $this->addCondition( + new HasNotTrophyUserConditionType(), + ); + $this->addCondition( + new IntegerUserConditionType( + identifier: "activityPoints", + columnName: "activityPoints", + migrateConditionObjectType: 'com.woltlab.wcf.user.activityPoints' + ), + ); + $this->addCondition( + new IntegerUserConditionType( + identifier: "likesReceived", + columnName: "likesReceived", + migrateConditionObjectType: 'com.woltlab.wcf.user.likesReceived' + ), + ); + $this->addCondition( + new IntegerUserConditionType( + identifier: "trophyPoints", + columnName: "trophyPoints", + migrateConditionObjectType: 'com.woltlab.wcf.user.trophyPoints' + ), + ); // TODO add conditions for user options that implement `ISearchableConditionUserOption` diff --git a/wcfsetup/install/files/lib/system/condition/type/IMigrateConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/IMigrateConditionType.class.php new file mode 100644 index 00000000000..ec0fa98196e --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/IMigrateConditionType.class.php @@ -0,0 +1,35 @@ + + * @since 6.3 + */ +interface IMigrateConditionType +{ + /** + * Migrates old condition data to the new condition format by removing all successfully migrated entries from the `$conditionData` + * and returns a list of condition-data in the new structure. The remaining entries are assumed to be unprocessed and are handled + * by other condition types and must remain untouched. + * + * Note: + * - Remove entries that you have successfully migrated. + * - Leave unrecognized or unsupported entries untouched. + * - If no data can be migrated, return an empty array. + * + * This allows `ConditionHandler::migrateConditionData()` to check whether all data has been migrated correctly and completely. + * + * @param array $conditionData + * + * @return list + */ + public function migrateConditionData(array &$conditionData): array; + + /** + * Returns `true` if the method `migrateConditionData()` can migrate data for the given `$objectType` and `false` otherwise. + */ + public function canMigrateConditionData(string $objectType): bool; +} diff --git a/wcfsetup/install/files/lib/system/condition/type/user/AbstractUserIsNullConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/AbstractUserIsNullConditionType.class.php deleted file mode 100644 index 1638e24a65a..00000000000 --- a/wcfsetup/install/files/lib/system/condition/type/user/AbstractUserIsNullConditionType.class.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @since 6.3 - */ -abstract class AbstractUserIsNullConditionType extends AbstractUserBooleanConditionType -{ - #[\Override] - public function applyFilter(DatabaseObjectList $objectList): void - { - if ($this->filter) { - $objectList->getConditionBuilder()->add("{$objectList->getDatabaseTableAlias()}.{$this->columnName} IS NOT NULL"); - } else { - $objectList->getConditionBuilder()->add("{$objectList->getDatabaseTableAlias()}.{$this->columnName} IS NULL"); - } - } - - #[\Override] - public function matches(object $object): bool - { - if ($this->filter) { - return $object->{$this->columnName} !== null; - } else { - return $object->{$this->columnName} === null; - } - } -} diff --git a/wcfsetup/install/files/lib/system/condition/type/user/AbstractUserBooleanConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php similarity index 65% rename from wcfsetup/install/files/lib/system/condition/type/user/AbstractUserBooleanConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php index ad6ec8aee91..c2ee9504da4 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/AbstractUserBooleanConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php @@ -7,6 +7,7 @@ use wcf\data\user\UserList; use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IDatabaseObjectListConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; use wcf\system\form\builder\field\BooleanFormField; @@ -20,11 +21,13 @@ * @implements IObjectConditionType * @extends AbstractConditionType */ -abstract class AbstractUserBooleanConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType +class BooleanUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType { public function __construct( public readonly string $identifier, - public readonly string $columnName + public readonly string $columnName, + public readonly ?string $migrateKeyName = null, + public readonly ?string $migrateConditionObjectType = null, ) { } @@ -65,4 +68,28 @@ public function matches(object $object): bool return !$object->{$this->columnName}; } } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $value = $conditionData[$this->columnName] ?? null; + if ($value === null) { + return []; + } + + unset($conditionData[$this->migrateKeyName]); + + return [ + [ + 'identifier' => $this->identifier, + 'value' => \boolval($value), + ], + ]; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $this->migrateConditionObjectType === $objectType; + } } diff --git a/wcfsetup/install/files/lib/system/condition/type/user/UserHasNotTrophyConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php similarity index 76% rename from wcfsetup/install/files/lib/system/condition/type/user/UserHasNotTrophyConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php index f19179db225..2c2306ae1b7 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/UserHasNotTrophyConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php @@ -10,6 +10,7 @@ use wcf\data\user\UserList; use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IDatabaseObjectListConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; use wcf\system\form\builder\field\SelectFormField; use wcf\system\WCF; @@ -24,7 +25,7 @@ * @implements IObjectConditionType * @extends AbstractConditionType */ -final class UserHasNotTrophyConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType +final class HasNotTrophyUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType { #[\Override] public function getFormField(string $id): SelectFormField @@ -87,4 +88,30 @@ private function getTrophies(): array return $trophies; } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + if (!isset($conditionData['notUserTrophyIDs'])) { + return []; + } + + $result = []; + foreach ($conditionData['notUserTrophyIDs'] as $trophyID) { + $result[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => $trophyID, + ]; + } + + unset($conditionData['notUserTrophyIDs']); + + return $result; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.user.userTrophyCondition'; + } } diff --git a/wcfsetup/install/files/lib/system/condition/type/user/UserHasTrophyConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php similarity index 76% rename from wcfsetup/install/files/lib/system/condition/type/user/UserHasTrophyConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php index 795f6cc6b12..72b710fb7d5 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/UserHasTrophyConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php @@ -10,6 +10,7 @@ use wcf\data\user\UserList; use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IDatabaseObjectListConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; use wcf\system\form\builder\field\SelectFormField; use wcf\system\WCF; @@ -24,7 +25,7 @@ * @implements IObjectConditionType * @extends AbstractConditionType */ -final class UserHasTrophyConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType +final class HasTrophyUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType { #[\Override] public function getFormField(string $id): SelectFormField @@ -87,4 +88,30 @@ private function getTrophies(): array return $trophies; } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + if (!isset($conditionData['userTrophyIDs'])) { + return []; + } + + $result = []; + foreach ($conditionData['userTrophyIDs'] as $trophyID) { + $result[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => $trophyID, + ]; + } + + unset($conditionData['userTrophyIDs']); + + return $result; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.user.userTrophyCondition'; + } } diff --git a/wcfsetup/install/files/lib/system/condition/type/user/UserInGroupConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php similarity index 72% rename from wcfsetup/install/files/lib/system/condition/type/user/UserInGroupConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php index 8efc55355a6..3b57a266753 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/UserInGroupConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/InGroupUserConditionType.class.php @@ -8,6 +8,7 @@ use wcf\data\user\UserList; use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IDatabaseObjectListConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; use wcf\system\form\builder\field\SelectFormField; @@ -21,7 +22,7 @@ * @implements IObjectConditionType * @extends AbstractConditionType */ -final class UserInGroupConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType +final class InGroupUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType { #[\Override] public function getFormField(string $id): SelectFormField @@ -69,4 +70,30 @@ public function matches(object $object): bool { return \in_array((int)$this->filter, $object->getGroupIDs(), true); } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.user.userGroup'; + } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + if (!isset($conditionData['groupIDs'])) { + return []; + } + + $result = []; + foreach ($conditionData['groupIDs'] as $groupID) { + $result[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => $groupID, + ]; + } + + unset($conditionData['groupIDs']); + + return $result; + } } diff --git a/wcfsetup/install/files/lib/system/condition/type/user/AbstractUserIntegerConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php similarity index 68% rename from wcfsetup/install/files/lib/system/condition/type/user/AbstractUserIntegerConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php index e5ef789c3fc..196d520968f 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/AbstractUserIntegerConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php @@ -7,6 +7,7 @@ use wcf\data\user\UserList; use wcf\system\condition\type\AbstractConditionType; 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\field\IntegerFormField; @@ -23,11 +24,12 @@ * @implements IObjectConditionType * @extends AbstractConditionType */ -abstract class AbstractUserIntegerConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType +class IntegerUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType { public function __construct( public readonly string $identifier, - public readonly string $columnName + public readonly string $columnName, + public readonly ?string $migrateConditionObjectType = null, ) { } @@ -84,8 +86,39 @@ public function matches(object $object): bool /** * @return string[] */ - private function getConditions(): array + protected function getConditions(): array { return ["=", ">", "<", ">=", "<="]; } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === $this->migrateConditionObjectType; + } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $lessThan = $conditionData['lessThan'] ?? null; + $greaterThan = $conditionData['greaterThan'] ?? null; + $conditions = []; + + if ($lessThan !== null) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => ["value" => $lessThan, 'condition' => '<'], + ]; + } + if ($greaterThan !== null) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => ["value" => $greaterThan, 'condition' => '>'], + ]; + } + + unset($conditionData['lessThan'], $conditionData['greaterThan']); + + return $conditions; + } } diff --git a/wcfsetup/install/files/lib/system/condition/type/user/UserIsEnabledConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php similarity index 84% rename from wcfsetup/install/files/lib/system/condition/type/user/UserIsEnabledConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php index f689f18b007..1a81c66eb14 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/UserIsEnabledConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php @@ -10,11 +10,11 @@ * @license GNU Lesser General Public License * @since 6.3 */ -final class UserIsEnabledConditionType extends AbstractUserBooleanConditionType +final class IsEnabledConditionType extends BooleanUserConditionType { public function __construct() { - parent::__construct("isEnabled", 'activationCode'); + parent::__construct("isEnabled", 'activationCode', '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 new file mode 100644 index 00000000000..6f243bc89ff --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/user/IsNullUserConditionType.class.php @@ -0,0 +1,94 @@ + + * @since 6.3 + * + * @implements IDatabaseObjectListConditionType, bool> + * @implements IObjectConditionType + * @extends AbstractConditionType + */ +class IsNullUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType +{ + public function __construct( + public readonly string $identifier, + public readonly string $columnName, + public readonly ?string $migrateKeyName = null, + public readonly ?string $migrateConditionObjectType = null, + ) {} + + #[\Override] + public function getIdentifier(): string + { + return $this->identifier; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.user.{$this->identifier}"; + } + + #[\Override] + public function getFormField(string $id): BooleanFormField + { + return BooleanFormField::create($id); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $objectList): void + { + if ($this->filter) { + $objectList->getConditionBuilder()->add("{$objectList->getDatabaseTableAlias()}.{$this->columnName} IS NOT NULL"); + } else { + $objectList->getConditionBuilder()->add("{$objectList->getDatabaseTableAlias()}.{$this->columnName} IS NULL"); + } + } + + #[\Override] + public function matches(object $object): bool + { + if ($this->filter) { + return $object->{$this->columnName} !== null; + } else { + return $object->{$this->columnName} === null; + } + } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $value = $conditionData[$this->columnName] ?? null; + if ($value === null) { + return []; + } + + unset($conditionData[$this->migrateKeyName]); + + return [ + [ + 'identifier' => $this->identifier, + 'value' => \boolval($value), + ], + ]; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $this->migrateConditionObjectType === $objectType; + } +} diff --git a/wcfsetup/install/files/lib/system/condition/type/user/UserLanguageConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php similarity index 69% rename from wcfsetup/install/files/lib/system/condition/type/user/UserLanguageConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php index 42cd3315b12..ba74a10afd4 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/UserLanguageConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php @@ -7,6 +7,7 @@ use wcf\data\user\UserList; use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IDatabaseObjectListConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; use wcf\system\form\builder\field\SelectFormField; use wcf\system\language\LanguageFactory; @@ -21,7 +22,7 @@ * @implements IObjectConditionType * @extends AbstractConditionType */ -final class UserLanguageConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType +final class LanguageUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType { #[\Override] public function getFormField(string $id): SelectFormField @@ -59,4 +60,30 @@ public function matches(object $object): bool { return (int)$this->filter === $object->languageID; } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + if (!isset($conditionData['languageIDs'])) { + return []; + } + + $result = []; + foreach ($conditionData['languageIDs'] as $languageID) { + $result[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => $languageID, + ]; + } + + unset($conditionData['languageIDs']); + + return $result; + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.user.languages'; + } } diff --git a/wcfsetup/install/files/lib/system/condition/type/user/UserNotInGroupConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php similarity index 72% rename from wcfsetup/install/files/lib/system/condition/type/user/UserNotInGroupConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php index feb39859b47..12731c3864e 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/UserNotInGroupConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/NotInGroupUserConditionType.class.php @@ -8,6 +8,7 @@ use wcf\data\user\UserList; use wcf\system\condition\type\AbstractConditionType; use wcf\system\condition\type\IDatabaseObjectListConditionType; +use wcf\system\condition\type\IMigrateConditionType; use wcf\system\condition\type\IObjectConditionType; use wcf\system\form\builder\field\SelectFormField; @@ -21,7 +22,7 @@ * @implements IObjectConditionType * @extends AbstractConditionType */ -final class UserNotInGroupConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType +final class NotInGroupUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType { #[\Override] public function getFormField(string $id): SelectFormField @@ -69,4 +70,30 @@ public function matches(object $object): bool { return !\in_array((int)$this->filter, $object->getGroupIDs(), true); } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === 'com.woltlab.wcf.user.userGroup'; + } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + if (!isset($conditionData['notGroupIDs'])) { + return []; + } + + $result = []; + foreach ($conditionData['notGroupIDs'] as $groupID) { + $result[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => $groupID, + ]; + } + + unset($conditionData['notGroupIDs']); + + return $result; + } } diff --git a/wcfsetup/install/files/lib/system/condition/type/user/UserRegistrationDateConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php similarity index 60% rename from wcfsetup/install/files/lib/system/condition/type/user/UserRegistrationDateConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php index dfaff458bfb..80b3f8a95a0 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/UserRegistrationDateConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDateUserConditionType.class.php @@ -7,6 +7,7 @@ use wcf\data\user\UserList; use wcf\system\condition\type\AbstractConditionType; 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\field\DateFormField; @@ -23,7 +24,7 @@ * @implements IObjectConditionType * @extends AbstractConditionType */ -final class UserRegistrationDateConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType +final class RegistrationDateUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType { #[\Override] public function getFormField(string $id): PrefixConditionFormFieldContainer @@ -85,4 +86,49 @@ private function getConditions(): array { return [">", "<", ">=", "<="]; } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $registrationDateStart = $conditionData['registrationDateStart'] ?? null; + $registrationDateEnd = $conditionData['registrationDateEnd'] ?? null; + $conditions = []; + + if ($registrationDateStart !== null) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => [ + 'value' => $this->convertDateStringTimestamp($registrationDateStart, 0, 0, 0), + 'condition' => '>=', + ], + ]; + } + if ($registrationDateEnd !== null) { + $conditions[] = [ + 'identifier' => $this->getIdentifier(), + 'value' => [ + 'value' => $this->convertDateStringTimestamp($registrationDateEnd, 23, 59, 59), + 'condition' => '<=', + ], + ]; + } + + unset($conditionData['registrationDateStart'], $conditionData['registrationDateEnd']); + + return $conditions; + } + + private function convertDateStringTimestamp(string $date, int $hour, int $minute, int $seconds): int + { + $dateTime = new \DateTimeImmutable($date, new \DateTimeZone(TIMEZONE)); + $dateTime = $dateTime->setTime($hour, $minute, $seconds); + + return $dateTime->getTimestamp(); + } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === "com.woltlab.wcf.user.registrationDate"; + } } diff --git a/wcfsetup/install/files/lib/system/condition/type/user/UserRegistrationDaysConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php similarity index 50% rename from wcfsetup/install/files/lib/system/condition/type/user/UserRegistrationDaysConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php index 73f5a090596..c0e2cfe97f1 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/UserRegistrationDaysConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/RegistrationDaysUserConditionType.class.php @@ -3,14 +3,8 @@ namespace wcf\system\condition\type\user; use wcf\data\DatabaseObjectList; -use wcf\data\user\User; -use wcf\data\user\UserList; -use wcf\system\condition\type\AbstractConditionType; -use wcf\system\condition\type\IDatabaseObjectListConditionType; -use wcf\system\condition\type\IObjectConditionType; use wcf\system\form\builder\container\PrefixConditionFormFieldContainer; use wcf\system\form\builder\field\IntegerFormField; -use wcf\system\form\builder\field\SingleSelectionFormField; use wcf\util\DateUtil; /** @@ -18,41 +12,23 @@ * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.3 - * - * @phpstan-type Filter = array{condition: string, value: int} - * @implements IDatabaseObjectListConditionType, Filter> - * @implements IObjectConditionType - * @extends AbstractConditionType */ -final class UserRegistrationDaysConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType +final class RegistrationDaysUserConditionType extends IntegerUserConditionType { - #[\Override] - public function getFormField(string $id): PrefixConditionFormFieldContainer + public function __construct() { - return PrefixConditionFormFieldContainer::create($id) - ->field( - IntegerFormField::create("{$id}Value") - ->suffix("wcf.acp.option.suffix.days") - ->minimum(1) - ->required() - ) - ->prefixField( - SingleSelectionFormField::create("{$id}Condition") - ->options(\array_combine($this->getConditions(), $this->getConditions())) - ->required() - ); + parent::__construct('registrationDays', 'registrationDate', 'com.woltlab.wcf.user.registrationDateInterval'); } #[\Override] - public function getIdentifier(): string + public function getFormField(string $id): PrefixConditionFormFieldContainer { - return 'registrationDays'; - } + $container = parent::getFormField($id); + $field = $container->getField(); + \assert($field instanceof IntegerFormField); + $field->suffix("wcf.acp.option.suffix.days"); - #[\Override] - public function getLabel(): string - { - return 'wcf.condition.user.registrationDays'; + return $container; } #[\Override] @@ -61,7 +37,7 @@ public function applyFilter(DatabaseObjectList $objectList): void ["condition" => $condition, "timestamp" => $timestamp] = $this->getParsedFilter(); $objectList->getConditionBuilder()->add( - "? {$condition} {$objectList->getDatabaseTableAlias()}.registrationDate", + "{$objectList->getDatabaseTableAlias()}.registrationDate {$condition} ?", [$timestamp] ); } @@ -72,10 +48,10 @@ public function matches(object $object): bool ["condition" => $condition, "timestamp" => $timestamp] = $this->getParsedFilter(); return match ($condition) { - '>' => $timestamp > $object->registrationDate, - '<' => $timestamp < $object->registrationDate, - '>=' => $timestamp >= $object->registrationDate, - '<=' => $timestamp <= $object->registrationDate, + '>' => $object->registrationDate > $timestamp, + '<' => $object->registrationDate < $timestamp, + '>=' => $object->registrationDate >= $timestamp, + '<=' => $object->registrationDate <= $timestamp, default => throw new \InvalidArgumentException("Unknown condition: {$condition}"), }; } @@ -99,10 +75,8 @@ private function getParsedFilter(): array ]; } - /** - * @return string[] - */ - private function getConditions(): array + #[\Override] + protected function getConditions(): array { return [">", "<", ">=", "<="]; } diff --git a/wcfsetup/install/files/lib/system/condition/type/user/UserSignatureConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php similarity index 83% rename from wcfsetup/install/files/lib/system/condition/type/user/UserSignatureConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php index 28cef22b649..30e5e74b8ad 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/UserSignatureConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php @@ -10,11 +10,16 @@ * @license GNU Lesser General Public License * @since 6.3 */ -final class UserSignatureConditionType extends AbstractUserBooleanConditionType +final class SignatureUserConditionType extends BooleanUserConditionType { public function __construct() { - parent::__construct('signature', 'signature'); + parent::__construct( + 'signature', + 'signature', + 'userSignature', + 'com.woltlab.wcf.user.signature' + ); } #[\Override] diff --git a/wcfsetup/install/files/lib/system/condition/type/user/AbstractUserStringConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php similarity index 66% rename from wcfsetup/install/files/lib/system/condition/type/user/AbstractUserStringConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php index d92f8281e84..9093decab28 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/AbstractUserStringConditionType.class.php +++ b/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php @@ -7,10 +7,12 @@ use wcf\data\user\UserList; use wcf\system\condition\type\AbstractConditionType; 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\field\SingleSelectionFormField; use wcf\system\form\builder\field\TextFormField; +use wcf\system\WCF; /** * @author Olaf Braun @@ -23,13 +25,14 @@ * @implements IObjectConditionType * @extends AbstractConditionType */ -abstract class AbstractUserStringConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType +class StringUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType { public function __construct( public readonly string $identifier, - public readonly string $columnName - ) { - } + public readonly string $columnName, + public readonly ?string $migrateKeyName = null, + public readonly ?string $migrateConditionObjectType = null, + ) {} #[\Override] public function getFormField(string $id): PrefixConditionFormFieldContainer @@ -62,13 +65,13 @@ public function getLabel(): string public function applyFilter(DatabaseObjectList $objectList): void { ["condition" => $condition, "value" => $value] = $this->filter; - $value = \addcslashes($value, '_%'); + $value = WCF::getDB()->escapeLikeValue($value); $filter = match ($condition) { "_%" => $value . '%', "%_%" => '%' . $value . '%', "%_" => '%' . $value, - default => '', + default => throw new \InvalidArgumentException("Unknown condition: {$condition}"), }; $objectList->getConditionBuilder()->add( @@ -81,14 +84,14 @@ public function applyFilter(DatabaseObjectList $objectList): void public function matches(object $object): bool { ["condition" => $condition, "value" => $value] = $this->filter; - $value = \strtolower($value); - $objectValue = \strtolower($object->{$this->columnName}); + $value = \mb_strtolower($value); + $objectValue = \mb_strtolower($object->{$this->columnName}); return match ($condition) { "_%" => \str_starts_with($objectValue, $value), "%_%" => \str_contains($objectValue, $value), "%_" => \str_ends_with($objectValue, $value), - default => false, + default => throw new \InvalidArgumentException("Unknown condition: {$condition}"), }; } @@ -103,4 +106,31 @@ private function getConditions(): array "%_" => "wcf.condition.endsWith", ]; } + + #[\Override] + public function canMigrateConditionData(string $objectType): bool + { + return $objectType === $this->migrateConditionObjectType; + } + + #[\Override] + public function migrateConditionData(array &$conditionData): array + { + $value = $conditionData[$this->migrateKeyName] ?? null; + if ($value === null) { + return []; + } + + unset($conditionData[$this->migrateKeyName]); + + return [ + [ + 'identifier' => $this->identifier, + 'value' => [ + 'condition' => "%_%", + 'value' => $value, + ], + ], + ]; + } } diff --git a/wcfsetup/install/files/lib/system/cronjob/UserGroupAssignmentCronjob.class.php b/wcfsetup/install/files/lib/system/cronjob/UserGroupAssignmentCronjob.class.php index 8714c867ab6..196b5a06be6 100644 --- a/wcfsetup/install/files/lib/system/cronjob/UserGroupAssignmentCronjob.class.php +++ b/wcfsetup/install/files/lib/system/cronjob/UserGroupAssignmentCronjob.class.php @@ -4,7 +4,7 @@ use wcf\data\cronjob\Cronjob; use wcf\data\user\UserAction; -use wcf\system\cache\builder\UserGroupAssignmentCacheBuilder; +use wcf\system\cache\eager\UserGroupAssignmentCache; use wcf\system\user\group\assignment\UserGroupAssignmentHandler; /** @@ -25,7 +25,7 @@ public function execute(Cronjob $cronjob) { parent::execute($cronjob); - $assignments = UserGroupAssignmentCacheBuilder::getInstance()->getData(); + $assignments = (new UserGroupAssignmentCache())->getCache(); $usersToGroup = []; $assignmentCount = 0; diff --git a/wcfsetup/install/files/lib/system/user/group/assignment/UserGroupAssignmentHandler.class.php b/wcfsetup/install/files/lib/system/user/group/assignment/UserGroupAssignmentHandler.class.php index 6d5ef9d5055..b1fdf6df144 100644 --- a/wcfsetup/install/files/lib/system/user/group/assignment/UserGroupAssignmentHandler.class.php +++ b/wcfsetup/install/files/lib/system/user/group/assignment/UserGroupAssignmentHandler.class.php @@ -7,7 +7,7 @@ use wcf\data\user\User; use wcf\data\user\UserAction; use wcf\data\user\UserList; -use wcf\system\cache\builder\UserGroupAssignmentCacheBuilder; +use wcf\system\cache\eager\UserGroupAssignmentCache; use wcf\system\condition\ConditionHandler; use wcf\system\condition\provider\UserConditionProvider; use wcf\system\SingletonFactory; @@ -47,8 +47,7 @@ public function checkUsers(array $userIDs) $userList->setObjectIDs($userIDs); $userList->readObjects(); - /** @var UserGroupAssignment[] $assignments */ - $assignments = UserGroupAssignmentCacheBuilder::getInstance()->getData(); + $assignments = (new UserGroupAssignmentCache())->getCache(); $conditionProvider = new UserConditionProvider(); foreach ($userList as $user) { $groupIDs = $user->getGroupIDs(); diff --git a/wcfsetup/install/files/lib/system/user/group/assignment/command/MigrateLegacyCondition.class.php b/wcfsetup/install/files/lib/system/user/group/assignment/command/MigrateLegacyCondition.class.php new file mode 100644 index 00000000000..797018ea938 --- /dev/null +++ b/wcfsetup/install/files/lib/system/user/group/assignment/command/MigrateLegacyCondition.class.php @@ -0,0 +1,50 @@ + + * @since 6.3 + */ +final class MigrateLegacyCondition +{ + public function __construct( + public readonly UserGroupAssignment $assignment, + ) { + } + + public function __invoke(): void + { + if (!$this->assignment->isLegacy) { + return; + } + + try { + $json = JSON::decode($this->assignment->conditions); + } catch (SystemException $ex) { + $ex->getExceptionID(); // Log the exception if JSON decoding fails + + return; + } + + $migratedData = ConditionHandler::getInstance()->migrateConditionData(new UserConditionProvider(), $json); + + $editor = new UserGroupAssignmentEditor($this->assignment); + $editor->update([ + 'conditions' => JSON::encode($migratedData->conditions), + 'isLegacy' => 0, + 'isDisabled' => $migratedData->isFullyMigrated ? $this->assignment->isDisabled : 1, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/worker/UserGroupAssignmentRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/UserGroupAssignmentRebuildDataWorker.class.php new file mode 100644 index 00000000000..3be1fbaf549 --- /dev/null +++ b/wcfsetup/install/files/lib/system/worker/UserGroupAssignmentRebuildDataWorker.class.php @@ -0,0 +1,38 @@ + + * + * @extends AbstractLinearRebuildDataWorker + */ +final class UserGroupAssignmentRebuildDataWorker extends AbstractLinearRebuildDataWorker +{ + /** + * @inheritDoc + */ + protected $objectListClassName = UserGroupAssignmentList::class; + + /** + * @inheritDoc + */ + protected $limit = 100; + + #[\Override] + public function execute() + { + parent::execute(); + + foreach ($this->objectList as $assignment) { + (new MigrateLegacyCondition($assignment))(); + } + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index a34a5003882..2e90f1eae7c 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -860,6 +860,7 @@ Sie erreichen das Fehlerprotokoll unter: {link controller='ExceptionLogView' isE + @@ -870,6 +871,7 @@ Sie erreichen das Fehlerprotokoll unter: {link controller='ExceptionLogView' isE + Anzeigen aktualisieren durch.]]> @@ -977,6 +979,7 @@ Sie erreichen das Fehlerprotokoll unter: {link controller='ExceptionLogView' isE + {$title}{/implode} wurden noch nicht aktualisiert. Bitte {if LANGUAGE_USE_INFORMAL_VARIANT}führe{else}führen Sie{/if} dies unter Anzeigen-Aktualisierung durch.]]> Apps verwalten.]]> @@ -2698,6 +2701,8 @@ Abschnitte dürfen nicht leer sein und nur folgende Zeichen enthalten: [a-z + + @@ -3531,26 +3536,27 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt - - - - + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 5239983c9d9..09c9226a837 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -836,6 +836,7 @@ You can access the error log at: {link controller='ExceptionLogView' isEmail=tru + @@ -846,6 +847,7 @@ You can access the error log at: {link controller='ExceptionLogView' isEmail=tru + Rebuild Data.]]> @@ -953,6 +955,7 @@ You can access the error log at: {link controller='ExceptionLogView' isEmail=tru + {$title}{/implode} need to be recalculated. Please perform this task by visiting Rebuild Data.]]> Manage Apps.]]> @@ -2625,6 +2628,8 @@ If you have already bought the licenses for the listed apps, th + + @@ -3454,26 +3459,27 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi - - - - - + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index f083967e0e3..274be2d4393 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1685,7 +1685,8 @@ CREATE TABLE wcf1_user_group_assignment ( groupID INT(10) NOT NULL, title VARCHAR(255) NOT NULL, isDisabled TINYINT(1) NOT NULL DEFAULT 0, - conditions MEDIUMTEXT + conditions MEDIUMTEXT, + isLegacy TINYINT(1) NOT NULL DEFAULT 0 ); DROP TABLE IF EXISTS wcf1_user_group_option;