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/action/ConditionAddAction.class.php b/wcfsetup/install/files/lib/action/ConditionAddAction.class.php index a31d8d85b03..83c4c2772b7 100644 --- a/wcfsetup/install/files/lib/action/ConditionAddAction.class.php +++ b/wcfsetup/install/files/lib/action/ConditionAddAction.class.php @@ -82,18 +82,22 @@ 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') ->label('wcf.condition.condition') ->filterable() ->required() - ->options( - \array_map( - static fn (IConditionType $conditionType) => WCF::getLanguage()->get($conditionType->getLabel()), - $provider->getConditionTypes() - ) - ) + ->options($options) ); $form->markRequiredFields(false); 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 2fdfcc69585..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,11 +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\UserInGroupConditionType; -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\UserUsernameConditionType; +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; /** @@ -26,13 +34,112 @@ final class UserConditionProvider extends AbstractConditionProvider { public function __construct() { - $this->addConditions([ - new UserUsernameConditionType(), - new UserRegistrationDateConditionType(), - new UserRegistrationDaysConditionType(), - new UserInGroupConditionType(), - new UserNotInGroupConditionType(), - ]); + $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` EventHandler::getInstance()->fire( new UserConditionProviderCollecting($this) 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/BooleanUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php new file mode 100644 index 00000000000..c2ee9504da4 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/user/BooleanUserConditionType.class.php @@ -0,0 +1,95 @@ + + * @since 6.3 + * + * @implements IDatabaseObjectListConditionType, bool> + * @implements IObjectConditionType + * @extends AbstractConditionType + */ +class BooleanUserConditionType 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} = ?", [1]); + } else { + $objectList->getConditionBuilder()->add("{$objectList->getDatabaseTableAlias()}.{$this->columnName} = ?", [0]); + } + } + + #[\Override] + public function matches(object $object): bool + { + if ($this->filter) { + return (bool)$object->{$this->columnName}; + } else { + 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/HasNotTrophyUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php new file mode 100644 index 00000000000..2c2306ae1b7 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/user/HasNotTrophyUserConditionType.class.php @@ -0,0 +1,117 @@ + + * @since 6.3 + * + * @implements IDatabaseObjectListConditionType, string> + * @implements IObjectConditionType + * @extends AbstractConditionType + */ +final class HasNotTrophyUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType +{ + #[\Override] + public function getFormField(string $id): SelectFormField + { + // SelectFormField stores its value as a string, + // so we need to convert it to an integer in the `applyFilter`&`matches` method. + return SelectFormField::create($id) + ->options($this->getTrophies()) + ->required(); + } + + #[\Override] + public function getIdentifier(): string + { + return 'hasNotTrophy'; + } + + #[\Override] + public function getLabel(): string + { + return 'wcf.condition.user.hasNotTrophy'; + } + + #[\Override] + public function applyFilter(DatabaseObjectList $objectList): void + { + $objectList->getConditionBuilder()->add( + "{$objectList->getDatabaseTableAlias()}.userID NOT IN ( + SELECT userID + FROM wcf1_user_trophy + WHERE trophyID = ? + )", + [(int)$this->filter] + ); + } + + #[\Override] + public function matches(object $object): bool + { + $userTrophies = UserTrophyList::getUserTrophies([$object->userID], false)[$object->userID]; + $trophyIDs = \array_column($userTrophies, 'trophyID'); + + return !\in_array((int)$this->filter, $trophyIDs, true); + } + + /** + * @return Trophy[] + */ + private function getTrophies(): array + { + $trophyList = new TrophyList(); + $trophyList->readObjects(); + $trophies = $trophyList->getObjects(); + + $collator = new \Collator(WCF::getLanguage()->getLocale()); + \uasort( + $trophies, + static fn (Trophy $a, Trophy $b) => $collator->compare($a->getTitle(), $b->getTitle()) + ); + + 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/HasTrophyUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php new file mode 100644 index 00000000000..72b710fb7d5 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/user/HasTrophyUserConditionType.class.php @@ -0,0 +1,117 @@ + + * @since 6.3 + * + * @implements IDatabaseObjectListConditionType, string> + * @implements IObjectConditionType + * @extends AbstractConditionType + */ +final class HasTrophyUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType +{ + #[\Override] + public function getFormField(string $id): SelectFormField + { + // SelectFormField stores its value as a string, + // so we need to convert it to an integer in the `applyFilter`&`matches` method. + return SelectFormField::create($id) + ->options($this->getTrophies()) + ->required(); + } + + #[\Override] + public function getIdentifier(): string + { + return 'hasTrophy'; + } + + #[\Override] + public function getLabel(): string + { + return 'wcf.condition.user.hasTrophy'; + } + + #[\Override] + public function applyFilter(DatabaseObjectList $objectList): void + { + $objectList->getConditionBuilder()->add( + "{$objectList->getDatabaseTableAlias()}.userID IN ( + SELECT userID + FROM wcf1_user_trophy + WHERE trophyID = ? + )", + [(int)$this->filter] + ); + } + + #[\Override] + public function matches(object $object): bool + { + $userTrophies = UserTrophyList::getUserTrophies([$object->userID], false)[$object->userID]; + $trophyIDs = \array_column($userTrophies, 'trophyID'); + + return \in_array((int)$this->filter, $trophyIDs, true); + } + + /** + * @return Trophy[] + */ + private function getTrophies(): array + { + $trophyList = new TrophyList(); + $trophyList->readObjects(); + $trophies = $trophyList->getObjects(); + + $collator = new \Collator(WCF::getLanguage()->getLocale()); + \uasort( + $trophies, + static fn (Trophy $a, Trophy $b) => $collator->compare($a->getTitle(), $b->getTitle()) + ); + + 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 59% 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 e41849a668e..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; @@ -17,15 +18,17 @@ * @license GNU Lesser General Public License * @since 6.3 * - * @implements IDatabaseObjectListConditionType, int> - * @implements IObjectConditionType - * @extends AbstractConditionType + * @implements IDatabaseObjectListConditionType, string> + * @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 { + // SelectFormField stores its value as a string, + // so we need to convert it to an integer in the `applyFilter`&`matches` method. return SelectFormField::create($id) ->options( UserGroup::getGroupsByType(invalidGroupTypes: [ @@ -57,14 +60,40 @@ public function applyFilter(DatabaseObjectList $objectList): void SELECT userID FROM wcf1_user_to_group WHERE groupID = ? - )", - [$this->filter] + )", + [(int)$this->filter] ); } #[\Override] public function matches(object $object): bool { - return \in_array($this->filter, $object->getGroupIDs(), true); + 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/IntegerUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php new file mode 100644 index 00000000000..196d520968f --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/user/IntegerUserConditionType.class.php @@ -0,0 +1,124 @@ + + * @since 6.3 + * + * @phpstan-type Filter = array{condition: string, value: int} + * @implements IDatabaseObjectListConditionType, Filter> + * @implements IObjectConditionType + * @extends AbstractConditionType + */ +class IntegerUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType +{ + public function __construct( + public readonly string $identifier, + public readonly string $columnName, + public readonly ?string $migrateConditionObjectType = null, + ) { + } + + #[\Override] + public function getFormField(string $id): PrefixConditionFormFieldContainer + { + return PrefixConditionFormFieldContainer::create($id) + ->field( + IntegerFormField::create("{$id}Value") + ->minimum(0) + ->required() + ) + ->prefixField( + SingleSelectionFormField::create("{$id}Condition") + ->options(\array_combine($this->getConditions(), $this->getConditions())) + ->required() + ); + } + + #[\Override] + public function getIdentifier(): string + { + return $this->identifier; + } + + #[\Override] + public function getLabel(): string + { + return "wcf.condition.user.{$this->identifier}"; + } + + #[\Override] + public function applyFilter(DatabaseObjectList $objectList): void + { + $objectList->getConditionBuilder()->add( + "{$objectList->getDatabaseTableAlias()}.{$this->columnName} {$this->filter['condition']} ?", + [$this->filter['value']] + ); + } + + #[\Override] + public function matches(object $object): bool + { + return match ($this->filter['condition']) { + '=' => $object->{$this->columnName} == $this->filter['value'], + '>' => $object->{$this->columnName} > $this->filter['value'], + '<' => $object->{$this->columnName} < $this->filter['value'], + '>=' => $object->{$this->columnName} >= $this->filter['value'], + '<=' => $object->{$this->columnName} <= $this->filter['value'], + default => throw new \InvalidArgumentException("Unknown condition: {$this->filter['condition']}"), + }; + } + + /** + * @return string[] + */ + 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/IsEnabledConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php new file mode 100644 index 00000000000..1a81c66eb14 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/user/IsEnabledConditionType.class.php @@ -0,0 +1,39 @@ + + * @since 6.3 + */ +final class IsEnabledConditionType extends BooleanUserConditionType +{ + public function __construct() + { + parent::__construct("isEnabled", 'activationCode', 'userIsEnabled', 'com.woltlab.wcf.user.state'); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $objectList): void + { + if ($this->filter) { + $objectList->getConditionBuilder()->add("{$objectList->getDatabaseTableAlias()}.activationCode = ?", [0]); + } else { + $objectList->getConditionBuilder()->add("{$objectList->getDatabaseTableAlias()}.activationCode <> ?", [0]); + } + } + + #[\Override] + public function matches(object $object): bool + { + if ($this->filter) { + return $object->activationCode === 0; + } else { + return $object->activationCode !== 0; + } + } +} 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/LanguageUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php new file mode 100644 index 00000000000..ba74a10afd4 --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/user/LanguageUserConditionType.class.php @@ -0,0 +1,89 @@ + + * @since 6.3 + * + * @implements IDatabaseObjectListConditionType, string> + * @implements IObjectConditionType + * @extends AbstractConditionType + */ +final class LanguageUserConditionType extends AbstractConditionType implements IDatabaseObjectListConditionType, IObjectConditionType, IMigrateConditionType +{ + #[\Override] + public function getFormField(string $id): SelectFormField + { + // SelectFormField stores its value as a string, + // so we need to convert it to an integer in the `applyFilter`&`matches` method. + return SelectFormField::create($id) + ->options(LanguageFactory::getInstance()->getLanguages()) + ->required(); + } + + #[\Override] + public function getIdentifier(): string + { + return 'language'; + } + + #[\Override] + public function getLabel(): string + { + return 'wcf.condition.user.language'; + } + + #[\Override] + public function applyFilter(DatabaseObjectList $objectList): void + { + $objectList->getConditionBuilder()->add( + "{$objectList->getDatabaseTableAlias()}.languageID = ?", + [(int)$this->filter] + ); + } + + #[\Override] + 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 59% 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 59da956f17e..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; @@ -17,15 +18,17 @@ * @license GNU Lesser General Public License * @since 6.3 * - * @implements IDatabaseObjectListConditionType, int> - * @implements IObjectConditionType - * @extends AbstractConditionType + * @implements IDatabaseObjectListConditionType, string> + * @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 { + // SelectFormField stores its value as a string, + // so we need to convert it to an integer in the `applyFilter`&`matches` method. return SelectFormField::create($id) ->options( UserGroup::getGroupsByType(invalidGroupTypes: [ @@ -57,14 +60,40 @@ public function applyFilter(DatabaseObjectList $objectList): void SELECT userID FROM wcf1_user_to_group WHERE groupID = ? - )", - [$this->filter] + )", + [(int)$this->filter] ); } #[\Override] public function matches(object $object): bool { - return !\in_array($this->filter, $object->getGroupIDs(), true); + 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 51% 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 9d47d30e0ec..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,40 +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") - ->required() - ) - ->prefixField( - SingleSelectionFormField::create("{$id}Condition") - ->options($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] @@ -71,10 +48,10 @@ public function matches(object $object): bool ["condition" => $condition, "timestamp" => $timestamp] = $this->getParsedFilter(); return match ($condition) { - '>' => $object->registrationDate < $timestamp, - '<' => $object->registrationDate > $timestamp, - '>=' => $object->registrationDate <= $timestamp, - '<=' => $object->registrationDate >= $timestamp, + '>' => $object->registrationDate > $timestamp, + '<' => $object->registrationDate < $timestamp, + '>=' => $object->registrationDate >= $timestamp, + '<=' => $object->registrationDate <= $timestamp, default => throw new \InvalidArgumentException("Unknown condition: {$condition}"), }; } @@ -90,7 +67,7 @@ private function getParsedFilter(): array $date = DateUtil::getDateTimeByTimestamp(TIME_NOW); $date->setTimezone(new \DateTimeZone(TIMEZONE)); - $date->sub(new \DateInterval("P{$this->filter['condition']}D")); + $date->sub(new \DateInterval("P{$this->filter['value']}D")); return [ 'condition' => $this->filter['condition'], @@ -98,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/SignatureUserConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php new file mode 100644 index 00000000000..30e5e74b8ad --- /dev/null +++ b/wcfsetup/install/files/lib/system/condition/type/user/SignatureUserConditionType.class.php @@ -0,0 +1,50 @@ + + * @since 6.3 + */ +final class SignatureUserConditionType extends BooleanUserConditionType +{ + public function __construct() + { + parent::__construct( + 'signature', + 'signature', + 'userSignature', + 'com.woltlab.wcf.user.signature' + ); + } + + #[\Override] + public function applyFilter(DatabaseObjectList $objectList): void + { + if ($this->filter) { + $objectList->getConditionBuilder()->add( + "({$objectList->getDatabaseTableAlias()}.signature <> ? AND {$objectList->getDatabaseTableAlias()}.signature IS NOT NULL)", + [''] + ); + } else { + $objectList->getConditionBuilder()->add( + "({$objectList->getDatabaseTableAlias()}.signature = ? OR {$objectList->getDatabaseTableAlias()}.signature IS NULL)", + [''] + ); + } + } + + #[\Override] + public function matches(object $object): bool + { + if ($this->filter) { + return $object->signature !== '' && $object->signature !== null; + } else { + return $object->signature === '' || $object->signature === null; + } + } +} diff --git a/wcfsetup/install/files/lib/system/condition/type/user/UserUsernameConditionType.class.php b/wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php similarity index 56% rename from wcfsetup/install/files/lib/system/condition/type/user/UserUsernameConditionType.class.php rename to wcfsetup/install/files/lib/system/condition/type/user/StringUserConditionType.class.php index 67952006dd7..9093decab28 100644 --- a/wcfsetup/install/files/lib/system/condition/type/user/UserUsernameConditionType.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,8 +25,15 @@ * @implements IObjectConditionType * @extends AbstractConditionType */ -final class UserUsernameConditionType 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 $migrateKeyName = null, + public readonly ?string $migrateConditionObjectType = null, + ) {} + #[\Override] public function getFormField(string $id): PrefixConditionFormFieldContainer { @@ -43,28 +52,30 @@ public function getFormField(string $id): PrefixConditionFormFieldContainer #[\Override] public function getIdentifier(): string { - return 'username'; + return $this->identifier; } #[\Override] public function getLabel(): string { - return 'wcf.condition.user.username'; + return "wcf.condition.user.{$this->identifier}"; } #[\Override] public function applyFilter(DatabaseObjectList $objectList): void { ["condition" => $condition, "value" => $value] = $this->filter; + $value = WCF::getDB()->escapeLikeValue($value); + $filter = match ($condition) { "_%" => $value . '%', "%_%" => '%' . $value . '%', "%_" => '%' . $value, - default => '', + default => throw new \InvalidArgumentException("Unknown condition: {$condition}"), }; $objectList->getConditionBuilder()->add( - $objectList->getDatabaseTableAlias() . '.username LIKE ?', + $objectList->getDatabaseTableAlias() . ".{$this->columnName} LIKE ?", [$filter] ); } @@ -73,12 +84,14 @@ public function applyFilter(DatabaseObjectList $objectList): void public function matches(object $object): bool { ["condition" => $condition, "value" => $value] = $this->filter; + $value = \mb_strtolower($value); + $objectValue = \mb_strtolower($object->{$this->columnName}); return match ($condition) { - "_%" => \str_starts_with($object->username, $value), - "%_%" => \str_contains($object->username, $value), - "%_" => \str_ends_with($object->username, $value), - default => false, + "_%" => \str_starts_with($objectValue, $value), + "%_%" => \str_contains($objectValue, $value), + "%_" => \str_ends_with($objectValue, $value), + default => throw new \InvalidArgumentException("Unknown condition: {$condition}"), }; } @@ -93,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 68462766792..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,12 +3536,27 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt - - - - + + + + + + + + + + + + + + + + + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index d480e3054fe..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,12 +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;