diff --git a/docs/migration/wsc62/deprecations_removals.md b/docs/migration/wsc62/deprecations_removals.md
index 34aee8fab..f4249ab7d 100644
--- a/docs/migration/wsc62/deprecations_removals.md
+++ b/docs/migration/wsc62/deprecations_removals.md
@@ -126,6 +126,23 @@ With version 6.3, we have deprecated certain components and removed several othe
- `wcf\data\notice\NoticeEditor`
- `wcf\data\ad\AdAction`
- `wcf\data\ad\AdEditor`
+- `wcf\acp\form\UserMergeForm`
+- `wcf\system\event\listener\AbstractUserMergeListener`
+- `wcf\system\event\listener\AbstractUserActionRenameListener`
+- `wcf\data\user\UserEditor`
+- `wcf\data\user\UserAction`
+- `wcf\data\user\UserProfileAction`
+- `wcf\data\user\option\UserOptionAction`
+- `wcf\data\user\option\UserOptionEditor`
+- `wcf\data\user\option\ViewableUserOption`
+- `wcf\data\user\option\category\UserOptionCategoryAction`
+- `wcf\data\user\option\category\UserOptionCategoryEditor`
+- `wcf\system\condition\UserMultifactorCondition`
+- `wcf\system\condition\UserUserIDCondition`
+- `wcf\system\option\ISearchableConditionUserOption`
+- `wcf\system\option\ISearchableUserOption`
+- `wcf\system\option\user\IUserOptionOutput`
+- `wcf\system\worker\SendNewPasswordWorker`
#### Methods
@@ -359,6 +376,11 @@ With version 6.3, we have deprecated certain components and removed several othe
- `calendar\data\event\date\EventDateEditor::rebuildParticipants()`
- `wcf\data\notice\Notice::getConditions()`
- `wcf\data\user\group\assignment\UserGroupAssignment::getConditions()`
+- `wcf\data\user\profile\menu\item\UserProfileMenuItemAction::validateGetContent()`
+- `wcf\data\user\profile\menu\item\UserProfileMenuItemAction::getContent()`
+- `wcf\data\user\UserProfile::setFileAvatar()`
+- `wcf\system\view\user\profile\UserProfileHeaderView::isInAccessibleGroup()`
+- `wcf\util\UserRegistrationUtil::getRandomFieldName()`
#### Properties
@@ -371,6 +393,7 @@ With version 6.3, we have deprecated certain components and removed several othe
#### Object Type Definitions
- `com.woltlab.wcf.sitemap.object`, use the event `wcf\event\sitemap\SitemapObjectCollecting` instead
+- `com.woltlab.wcf.condition.userSearch`, use the event `wcf\event\gridView\admin\UserGridViewInitialized` to add filters to the user list instead
### JavaScript
@@ -378,6 +401,7 @@ With version 6.3, we have deprecated certain components and removed several othe
- `WoltLabSuite/Core/Ui/Sortable/List`
- `WoltLabSuite/Core/Component/GoogleMaps/MarkerLoader::setup()`
- `WoltLabSuite/Core/Ui/User/Sortable/ObjectWatch`
+- `WoltLabSuite/Core/Acp/Ui/User/Action/Handler/SendNewPassword`
## Removals
@@ -449,6 +473,33 @@ With version 6.3, we have deprecated certain components and removed several othe
- `wcf\data\acp\session\ACPSessionList`
- `calendar\data\user\FollowingBirthdayAction`
- `calendar\data\category\CalendarCategoryAction`
+- `wcf\acp\action\UserQuickSearchAction`
+- `wcf\acp\form\UserAssignToGroupForm`
+- `wcf\acp\form\UserContentRevertChangesForm`
+- `wcf\acp\form\UserEmailAddressExportForm`
+- `wcf\acp\form\UserOptionListForm`
+- `wcf\acp\form\UserSearchForm`
+- `wcf\action\EmailValidationAction`
+- `wcf\action\UsernameValidationAction`
+- `wcf\data\user\UserContentAction`
+- `wcf\data\user\UserRegistrationAction`
+- `wcf\form\UserSearchForm`
+- `wcf\system\clipboard\action\UserClipboardAction`
+- `wcf\system\clipboard\action\UserContentClipboardAction`
+- `wcf\system\condition\UserOptionsCondition`
+- `wcf\system\listView\user\UserSearchResultsListView`
+- `wcf\system\option\user\BirthdayUserOptionOutput`
+- `wcf\system\option\user\DateUserOptionOutput`
+- `wcf\system\option\user\FacebookUserOptionOutput`
+- `wcf\system\option\user\FloatUserOptionOutput`
+- `wcf\system\option\user\ImageUserOptionOutput`
+- `wcf\system\option\user\LabeledUrlUserOptionOutput`
+- `wcf\system\option\user\MessageUserOptionOutput`
+- `wcf\system\option\user\NewlineToBreakUserOptionOutput`
+- `wcf\system\option\user\SelectOptionsUserOptionOutput`
+- `wcf\system\option\user\TwitterUserOptionOutput`
+- `wcf\system\option\user\URLUserOptionOutput`
+- `wcf\system\option\user\UserOptionHandler`
#### Methods
@@ -558,6 +609,18 @@ With version 6.3, we have deprecated certain components and removed several othe
- `calendar\data\event\date\participation\EventDateParticipationAction::invite()`
- `wcf\data\package\update\PackageUpdateAction::validateSearchForUpdates()`
- `wcf\data\package\update\PackageUpdateAction::searchForUpdates()`
+- `wcf\data\user\UserAction::validatePrepareRemoveContent()`
+- `wcf\data\user\UserAction::prepareRemoveContent()`
+- `wcf\data\user\UserProfileAction::validateBeginEdit()`
+- `wcf\data\user\UserProfileAction::beginEdit()`
+- `wcf\data\user\UserProfileAction::validateSave()`
+- `wcf\data\user\UserProfileAction::save()`
+- `wcf\data\user\UserProfileAction::getOptionHandler()`
+- `wcf\data\user\option\UserOption::setUser()`
+- `wcf\data\user\option\UserOption::modifyEditableOption()`
+- `wcf\data\user\option\UserOption::modifyVisibleOption()`
+- `wcf\data\user\option\UserOption::setRequired()`
+- `wcf\data\user\option\ViewableUserOption::getOutputObject()`
#### Properties
@@ -574,6 +637,10 @@ With version 6.3, we have deprecated certain components and removed several othe
- `wcf\data\comment\ViewableComment::$userProfile`
- `wcf\data\comment\response\StructuredCommentResponse::$userProfile`
- `wcf\data\comment\response\ViewableCommentResponse::$userProfile`
+- `wcf\data\user\option\UserOption::$optionValue`
+- `wcf\data\user\option\UserOption::$user`
+- `wcf\data\user\option\ViewableUserOption::$outputObjects`
+- `wcf\page\MembersListPage::$searchID`
### JavaScript
@@ -627,3 +694,23 @@ With version 6.3, we have deprecated certain components and removed several othe
- `WCF.ACP.Package.Update.Search`
- `WoltLabSuite/Calendar/Ui/Category/MarkAllAsRead`
- `WCF.ACP.Ad.LocationHandler`
+- `WoltLabSuite/Core/Acp/Component/Tag/SynonymsHandler`
+- `WoltLabSuite/Core/Acp/Controller/User/List`
+- `WoltLabSuite/Core/Acp/Ui/User/Action/Abstract`
+- `WoltLabSuite/Core/Acp/Ui/User/Action/BanAction`
+- `WoltLabSuite/Core/Acp/Ui/User/Action/DeleteAction`
+- `WoltLabSuite/Core/Acp/Ui/User/Action/DisableAction`
+- `WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban`
+- `WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Ban/Dialog`
+- `WoltLabSuite/Core/Acp/Ui/User/Action/Handler/Delete`
+- `WoltLabSuite/Core/Acp/Ui/User/Action/SendNewPasswordAction`
+- `WoltLabSuite/Core/Acp/Ui/User/Action/ToggleConfirmEmailAction`
+- `WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Clipboard`
+- `WoltLabSuite/Core/Acp/Ui/User/Content/Remove/Handler`
+- `WoltLabSuite/Core/Acp/Ui/User/Editor`
+- `WoltLabSuite/Core/Ui/User/Editor`
+- `WCF.User.Profile.Editor`
+- `WCF.User.Registration`
+- `WCF.User.Registration.Validation`
+- `WCF.User.Registration.Validation.EmailAddress`
+- `WCF.User.Registration.Validation.Username`
diff --git a/docs/migration/wsc62/php.md b/docs/migration/wsc62/php.md
index 4d2032ce0..d74f0fe09 100644
--- a/docs/migration/wsc62/php.md
+++ b/docs/migration/wsc62/php.md
@@ -391,3 +391,495 @@ A condition like `userID NOT IN (?)` excludes all events of guests and must be w
Implementations of `IUserActivityEvent` that require a registered user, for example to look up the author of a comment, must handle guests.
The `TCommentResponseUserActivityEvent` trait provides `getCommentAuthor()` that returns a guest profile for comments written by guests.
+
+## User Merging
+
+The ACP form `wcf\acp\form\UserMergeForm` has been replaced by a dialog of the bulk interactions in the user list, the users are merged by the command `wcf\command\user\MergeUsers`.
+`UserMergeForm` and `wcf\system\event\listener\AbstractUserMergeListener` are deprecated.
+Event listeners for the `save` event of `UserMergeForm` are still executed, but should be migrated.
+
+The PSR-14 event `wcf\event\user\UsersMerging` is fired before the merged users are deleted.
+Instead of extending `AbstractUserMergeListener`, a bootstrap script registers the database columns that reference users through `UsersMerging::reassignUserColumn()`.
+Unlike `AbstractUserMergeListener`, the column containing the username is not updated unless it is explicitly passed as `usernameColumn`.
+
+```php
+// Previously
+class FooUserMergeListener extends AbstractUserMergeListener
+{
+ protected $databaseTables = [
+ 'foo1_foo',
+ [
+ 'name' => 'foo1_foo',
+ 'userID' => 'editorID',
+ 'username' => 'editor',
+ ],
+ [
+ 'name' => 'foo1_bar',
+ 'username' => null,
+ 'ignore' => true,
+ ],
+ ];
+}
+
+// Since 6.3
+EventHandler::getInstance()->register(
+ \wcf\event\user\UsersMerging::class,
+ static function (\wcf\event\user\UsersMerging $event) {
+ $event->reassignUserColumn('foo1_foo', usernameColumn: 'username');
+ $event->reassignUserColumn('foo1_foo', 'editorID', 'editor');
+ $event->reassignUserColumn('foo1_bar', ignore: true);
+ }
+);
+```
+
+The registered columns are updated after all event listeners have been executed.
+Listeners that need to perform other changes can access the destination user and the merged users through the properties `destinationUser` and `mergedUsers` of the event.
+
+## User Renaming
+
+Users are renamed through the command `wcf\command\user\UpdateUser`, which fires the PSR-14 event `wcf\event\user\UserRenamed`.
+`wcf\system\event\listener\AbstractUserActionRenameListener` is deprecated.
+Event listeners for the `rename` event of `wcf\data\user\UserAction` are still executed, but should be migrated.
+
+Instead of extending `AbstractUserActionRenameListener`, a bootstrap script registers the database columns containing the username through `UserRenamed::updateUsernameColumn()`.
+
+```php
+// Previously
+class FooUserActionRenameListener extends AbstractUserActionRenameListener
+{
+ protected $databaseTables = [
+ 'foo1_foo',
+ [
+ 'name' => 'foo1_foo',
+ 'userID' => 'editorID',
+ 'username' => 'editor',
+ ],
+ ];
+}
+
+// Since 6.3
+EventHandler::getInstance()->register(
+ \wcf\event\user\UserRenamed::class,
+ static function (\wcf\event\user\UserRenamed $event) {
+ $event->updateUsernameColumn('foo1_foo');
+ $event->updateUsernameColumn('foo1_foo', 'editorID', 'editor');
+ }
+);
+```
+
+The registered columns are updated after all event listeners have been executed.
+Listeners that need to perform other changes can access the renamed user and the previous username through the properties `user` and `oldUsername` of the event.
+
+## Creating and Updating Users
+
+`wcf\data\user\UserAction` and `wcf\data\user\UserEditor` are deprecated, users are created and modified through the [database object builder](../../php/database-object-builder.md) `wcf\data\user\UserBuilder` and the commands in the `wcf\command\user` namespace.
+The commands take care of all side effects, such as updating the user rank, the user online marking and the automatic user group assignments.
+
+```php
+// Previously
+$action = new UserAction([], 'create', [
+ 'data' => [
+ 'username' => 'Foo',
+ 'email' => 'foo@example.com',
+ 'password' => $password,
+ ],
+ 'groups' => [$groupID],
+ 'options' => [
+ User::getUserOptionID('location') => 'Berlin',
+ ],
+]);
+$user = $action->executeAction()['returnValues'];
+
+// Since 6.3
+$user = (new CreateUser(
+ UserBuilder::forCreate()
+ ->setUsername('Foo')
+ ->setEmail('foo@example.com')
+ ->setPassword($password)
+ ->setRegistrationDate(\TIME_NOW)
+ ->setGroupIDs([
+ ...UserGroup::getGroupIDsByType([UserGroup::EVERYONE, UserGroup::USERS]),
+ $groupID,
+ ])
+ ->setUserOption(User::getUserOptionID('location'), 'Berlin')
+))();
+```
+
+```php
+// Previously
+$action = new UserAction([$user], 'update', [
+ 'data' => [
+ 'userTitle' => 'Bar',
+ 'fooColumn' => 1,
+ ],
+]);
+$action->executeAction();
+
+// Since 6.3
+$user = (new UpdateUser(
+ UserBuilder::forUpdate($user)
+ ->setUserTitle('Bar')
+ ->setCustomProperty('fooColumn', 1)
+))();
+```
+
+The user options, the user groups and the content languages are stored in separate tables that are written after the user itself.
+The object returned by `UserBuilder::create()` and `UserBuilder::update()` is therefore loaded before these tables have been written, always use the `CreateUser` and `UpdateUser` commands which return an up-to-date object.
+`UserBuilder::delete()` only removes the database row, use the `DeleteUser` command to delete a user including the associated data.
+
+The methods of `UserAction` and `UserProfileAction` have been replaced as follows:
+
+| Method | Replacement |
+| ------ | ----------- |
+| `UserAction::create()` | `wcf\command\user\CreateUser` |
+| `UserAction::update()` | `wcf\command\user\UpdateUser` |
+| `UserAction::delete()` | `wcf\command\user\DeleteUser` |
+| `UserAction::addToGroups()` | `wcf\command\user\AddUserToGroups` |
+| `UserAction::removeFromGroups()` | `wcf\command\user\RemoveUserFromGroups` |
+| `UserAction::ban()` / `unban()` | `wcf\command\user\BanUser` / `UnbanUser` |
+| `UserAction::enable()` / `disable()` | `wcf\command\user\EnableUser` / `DisableUser` |
+| `UserAction::confirmEmail()` / `unconfirmEmail()` | `wcf\command\user\ConfirmUserEmail` / `UnconfirmUserEmail` |
+| `UserAction::enableAvatar()` / `disableAvatar()` | `wcf\command\user\EnableUserAvatar` / `DisableUserAvatar` |
+| `UserAction::enableCoverPhoto()` / `disableCoverPhoto()` | `wcf\command\user\EnableUserCoverPhoto` / `DisableUserCoverPhoto` |
+| `UserAction::enableSignature()` / `disableSignature()` | `wcf\command\user\EnableUserSignature` / `DisableUserSignature` |
+| `UserAction::resendActivationMail()` | `wcf\command\user\ResendActivationMail` |
+| `UserAction::saveUserConsent()` | RPC endpoint `wcf\system\endpoint\controller\core\users\SaveUserConsent` |
+| `UserAction::devtoolsSetLanguage()` | RPC endpoint `wcf\system\endpoint\controller\core\devtools\SetLanguage` |
+| `UserProfileAction::getMessagePreview()` | RPC endpoint `wcf\system\endpoint\controller\core\users\RenderSignaturePreview` |
+| `UserProfileAction::getDetailedActivityPointList()` | RPC endpoint `wcf\system\endpoint\controller\core\users\GetUserActivityPointList` |
+| `UserProfileAction::updateUserRank()` | `wcf\command\user\RefreshUserRanks` |
+| `UserProfileAction::updateUserOnlineMarking()` | `wcf\command\user\RefreshUserOnlineMarkings` |
+| `UserProfileAction::updateSpecialTrophies()` | `wcf\command\user\SetSpecialTrophies` |
+| `SendNewPasswordWorker` | `wcf\command\user\SendNewPassword` |
+
+Additionally, the commands `DisableUserMultifactor`, `DisconnectThirdPartyLogin` and `MergeUsers` have been added.
+
+!!! warning "WoltLab Suite no longer uses `UserAction` to create, update or delete users. Event listeners for the `initializeAction` and `finalizeAction` events of `UserAction` are no longer executed for these operations and must be migrated to the PSR-14 events below. Conversely, the deprecated methods `UserAction::create()`, `UserAction::update()` and `UserAction::delete()` do not fire the new events."
+
+### Events
+
+The commands fire the following PSR-14 events:
+
+| Event | Description |
+| ----- | ----------- |
+| `wcf\event\user\UserCreated` | Fired after a user has been created, provides the user and the builder. |
+| `wcf\event\user\UserUpdated` | Fired after a user has been updated, provides the updated user and the builder. |
+| `wcf\event\user\UserDeleted` | Fired after a user has been deleted. |
+
+All commands that modify a user, such as `BanUser` or `AddUserToGroups`, use `UpdateUser` internally and therefore fire `UserUpdated`.
+The builder exposes the changes, `getUserOptions()`, `getGroupIDs()` and `getLanguageIDs()` return `null` if the respective data has not been changed.
+The user prior to the update is available through `$event->builder->getObject()`.
+
+```php
+EventHandler::getInstance()->register(
+ \wcf\event\user\UserUpdated::class,
+ static function (\wcf\event\user\UserUpdated $event) {
+ if ($event->builder->getGroupIDs() === null) {
+ return;
+ }
+
+ // The user groups have been changed.
+ }
+);
+```
+
+### Legacy Events of `UserProfile`
+
+The legacy events `getAvatar` and `isAccessible` of `wcf\data\user\UserProfile` have been removed in favor of PSR-14 events.
+
+```php
+// Previously
+final class FooUserProfileListener implements IParameterizedEventListener
+{
+ public function execute($eventObj, $className, $eventName, array &$parameters)
+ {
+ if ($eventName === 'getAvatar') {
+ $parameters['avatar'] = new FooAvatar($eventObj->getDecoratedObject());
+ } elseif ($eventName === 'isAccessible' && $parameters['name'] === 'fooPrivacySetting') {
+ $parameters['result'] = FooPrivacy::isAccessible($eventObj->userID, $parameters['userID']);
+ }
+ }
+}
+
+// Since 6.3
+EventHandler::getInstance()->register(
+ \wcf\event\user\UserAvatarLoading::class,
+ static function (\wcf\event\user\UserAvatarLoading $event) {
+ $event->setAvatar(new FooAvatar($event->user));
+ }
+);
+
+EventHandler::getInstance()->register(
+ \wcf\event\user\UserPrivacySettingChecking::class,
+ static function (\wcf\event\user\UserPrivacySettingChecking $event) {
+ if ($event->name === 'fooPrivacySetting') {
+ $event->setAccessible(FooPrivacy::isAccessible($event->user->userID, $event->viewerUserID));
+ }
+ }
+);
+```
+
+## Permissions and Profile Data of Users
+
+The permission checks for the administration of users are available as methods of `wcf\data\user\User`, which are used by the interactions, the RPC endpoints and the forms:
+
+- `isManageable(bool $allowSelf = false)` returns `true` if all user groups of the user are accessible to the active user and, unless `$allowSelf` is set, the user is not the active user.
+- `canEnable()`, `canConfirmEmail()`, `canBan()`, `canDisableAvatar()`, `canDisableCoverPhoto()`, `canDisableSignature()`, `canSendNewPassword()`, `canDisconnectThirdPartyLogin()`, `canDisableMultifactor()`, `canResendActivationMail()`, `canDelete()`, `canExportGdpr()` and `canMail()` check the permission for the respective action.
+
+The result of `canConfirmEmail()` can be modified through the PSR-14 event `wcf\event\user\UserEmailConfirmationChecking`.
+Do not confuse this method with the existing `canEmailConfirm()`, which checks whether the user can confirm the email address themselves.
+
+The avatar, the rank, the user title, the online status and the privacy settings are also available on `User` through `getAvatar()`, `getRank()`, `getUserTitle()`, `isOnline()` and `isAccessible()`, it is no longer necessary to create a `UserProfile` for these.
+The data required for these methods is loaded for all users of the same collection at once (`wcf\data\user\UserCollection`).
+The corresponding methods of `UserProfile` remain available.
+The constants `UserProfile::ACCESS_*` are aliases of the new constants `User::ACCESS_*`.
+
+## ACP User Add and Edit Form
+
+`wcf\acp\form\UserAddForm` and `wcf\acp\form\UserEditForm` have been migrated to the form builder and are based on `AbstractDatabaseObjectBuilderForm` using `UserBuilder`.
+All properties representing the form values and all template events of the `userAdd` template except for `contentHeaderNavigation` have been removed.
+The options to ban a user, to disable the avatar, cover photo and signature of a user, to disconnect a third party login and to disable the multi-factor authentication have been moved to the interactions on the edit page.
+
+Additional form fields can be added through the `createForm` event of `UserAddForm`, which is inherited by `UserEditForm`.
+The values are written into the builder using `saveValueCallback()` and read from the user using `loadValueCallback()`.
+
+```php
+final class FooUserAddFormListener implements IParameterizedEventListener
+{
+ public function execute($eventObj, $className, $eventName, array &$parameters)
+ {
+ $eventObj->form->getNodeById('general')->appendChild(
+ BooleanFormField::create('fooEnabled')
+ ->label('wcf.acp.user.fooEnabled')
+ ->saveValueCallback(static function (UserBuilder $builder, BooleanFormField $field) {
+ $builder->setCustomProperty('fooEnabled', $field->getSaveValue());
+ })
+ ->loadValueCallback(static function (User $user, BooleanFormField $field) {
+ $field->value($user->fooEnabled);
+ })
+ );
+ }
+}
+```
+
+```xml
+
+ admin
+ wcf\acp\form\UserAddForm
+ createForm
+ wcf\system\event\listener\FooUserAddFormListener
+ 1
+
+```
+
+The form consists of the tab menu `userTabs` with the tabs `generalTab`, `profileTab`, `settingsTab` and `signatureTab`.
+The general data is contained in the form container `general`.
+Data that is stored in other tables can be saved using the `UserCreated` and `UserUpdated` events.
+
+`wcf\form\RegisterForm` and `wcf\form\SettingsForm` have also been migrated to the form builder and are now final.
+The template events `generalFields`, `honeyPotFields` and `sections` of the `register` template and `languageFields`, `styleFields`, `trophyFields` and `sections` of the `settings` template have been removed.
+
+## User List
+
+The user list in the ACP (`wcf\acp\page\UserListPage`) has been migrated to a [grid view](../../php/api/grid_views.md) (`wcf\system\gridView\admin\UserGridView`) and is now final.
+The bulk actions of the user list are provided as [bulk interactions](../../php/api/interactions.md#bulk-interactions) instead of the clipboard.
+The grid view, the interactions and the bulk interactions can be extended through the events `wcf\event\gridView\admin\UserGridViewInitialized`, `wcf\event\interaction\admin\UserInteractionCollecting` and `wcf\event\interaction\bulk\admin\UserBulkInteractionCollecting`.
+
+```php
+EventHandler::getInstance()->register(
+ \wcf\event\gridView\admin\UserGridViewInitialized::class,
+ static function (\wcf\event\gridView\admin\UserGridViewInitialized $event) {
+ $event->gridView->addColumnAfter(
+ GridViewColumn::for('fooCount')
+ ->label('wcf.acp.user.fooCount')
+ ->renderer(new NumberColumnRenderer())
+ ->sortable()
+ ->hidden(),
+ 'lastActivityTime'
+ );
+ $event->gridView->addAvailableFilters([
+ new BooleanFilter('fooEnabled', 'wcf.acp.user.fooEnabled'),
+ ]);
+ }
+);
+
+EventHandler::getInstance()->register(
+ \wcf\event\interaction\bulk\admin\UserBulkInteractionCollecting::class,
+ static function (\wcf\event\interaction\bulk\admin\UserBulkInteractionCollecting $event) {
+ $event->provider->addInteraction(
+ new BulkLinkInteraction('foo', FooForm::class, 'wcf.acp.user.foo')
+ );
+ }
+);
+```
+
+The user search in the ACP (`wcf\acp\form\UserSearchForm`) and in the frontend (`wcf\form\UserSearchForm`) has been removed in favor of the filters of the user list and of the members list.
+Links to a filtered user list can be generated using the `filters` parameter, e.g. `UserList?filters[banned]=1`.
+The object type definition `com.woltlab.wcf.condition.userSearch` is deprecated and its object types are no longer used, use the `UserGridViewInitialized` event to add custom filters instead.
+
+The forms to assign users to groups, to export email addresses, to merge users and to revert content changes have been replaced by dialogs of the bulk interactions.
+
+## User Profile Header in the ACP
+
+The edit page of a user in the ACP shows the profile header of the user (`wcf\system\view\user\profile\AdminUserProfileHeaderView`), which provides the interactions of the user.
+The header fires the existing events `wcf\event\user\profile\UserProfileStatItemCollecting` and `wcf\event\user\profile\UserProfileHeaderSearchContentLinkCollecting`, which means that their listeners are also executed in the ACP.
+Links of stat items must be absolute, e.g. `$user->getLink() . '#foo'` instead of `#foo`.
+
+## User Options
+
+The user options have been migrated to [form options](../../php/api/form_builder/form_options.md), which provide the input field, the formatted output, the database column and the filter of an option.
+`wcf\data\user\option\UserOption` no longer extends `wcf\data\option\Option`.
+
+### Package Installation Plugin
+
+The value of `` in the [userOption PIP](../../package/pip/user-option.md) is the id of a form option.
+The legacy option types `aboutMe`, `message`, `multiSelect`, `URL` and `labeledUrl` are converted automatically, the fields `outputclass`, `enableoptions`, `contentpattern` and `messageObjectType` as well as arbitrary additional fields are ignored.
+Values of `checkboxes` options, including the default value, are separated by commas instead of newlines.
+
+The new option type `socialMedia` replaces `labeledUrl` and plain text options that contain the profile name on a social media platform:
+
+```xml
+
+
+
+
+
+```
+
+Existing user options are converted during the upgrade to 6.3.
+
+### Custom Option Types
+
+Custom option types (`wcf\system\option\IOptionType`) and output classes (`wcf\system\option\user\IUserOptionOutput`) are no longer used for user options, options with an unknown type are treated as `text` options.
+Custom types must be implemented as a form option (`wcf\system\form\option\IFormOption`) and registered through the `wcf\event\form\option\FormOptionCollecting` event.
+The column of the option in `wcf1_user_option_value` is created based on `IFormOption::getDatabaseTableColumn()`.
+
+The new method `IFormOption::matchesFilterValue()` must be implemented by form options that do not extend `AbstractFormOption`.
+
+### Formatted Values
+
+The output classes have been replaced by `UserOption::getFormattedValue()`, `wcf\data\user\option\ViewableUserOption` is deprecated.
+
+```php
+// Previously
+$option = new ViewableUserOption($userOption);
+$option->setOptionValue($userProfile->getDecoratedObject());
+$output = $option->optionValue;
+
+// Since 6.3
+$output = $userProfile->getFormattedUserOption('foo');
+// or
+$output = $userOption->getFormattedValue($user->getUserOption('foo'), $user);
+```
+
+`UserProfile::getFormattedUserOption()` returns an empty string if the option has no value or is not visible to the active user, while `UserOption::getFormattedValue()` does not check the visibility.
+
+### Visibility and Editability
+
+`UserOption::setUser()` has been removed, the user is passed to `isVisible()` and `isEditable()` instead.
+This avoids side effects caused by modifying the shared option objects of the cache.
+
+```php
+// Previously
+$userOption->setUser($user);
+if ($userOption->isVisible()) {
+ // …
+}
+
+// Since 6.3
+if ($userOption->isVisible($user)) {
+ // …
+}
+```
+
+### Forms and Lists
+
+`wcf\system\option\user\UserOptionHandler`, `wcf\acp\form\UserOptionListForm` and the templates `userOptionFieldList` and `userProfileOptionFieldList` have been removed.
+The following traits provide the building blocks to integrate user options:
+
+- `wcf\system\option\user\TUserOptionsByCategory` returns the options of a category tree grouped by the subcategories (`getUserOptionsByCategory()`) and the title of a category (`getUserOptionCategoryTitle()`).
+- `wcf\system\form\option\user\TUserOptionFormFields` creates form containers and form fields for user options in forms based on `AbstractDatabaseObjectBuilderForm` using `UserBuilder`.
+- `wcf\system\option\user\TJoinUserOptionValues` joins the values of the user options to a database object list of users.
+
+```php
+final class FooForm extends AbstractDatabaseObjectBuilderForm
+{
+ use TUserOptionFormFields;
+
+ /**
+ * @var array
+ */
+ private array $options;
+
+ #[\Override]
+ public function readParameters(): void
+ {
+ parent::readParameters();
+
+ $this->formObject = new User(WCF::getUser()->userID);
+ $this->options = $this->getUserOptionsByCategory(
+ 'settings.foo',
+ static fn(UserOption $option) => $option->validateOptions()
+ && $option->validatePermissions()
+ && $option->isEditable()
+ );
+ }
+
+ #[\Override]
+ protected function createForm(): void
+ {
+ parent::createForm();
+
+ $this->form->appendChildren($this->getUserOptionFormContainers($this->options));
+ }
+
+ #[\Override]
+ protected function getDatabaseObjectBuilder(): UserBuilder
+ {
+ return UserBuilder::forUpdate($this->formObject);
+ }
+
+ #[\Override]
+ protected function getCommand(DatabaseObjectBuilder $builder): callable
+ {
+ return new UpdateUser($builder);
+ }
+}
+```
+
+### Builders and Commands
+
+`UserOptionAction` and `UserOptionEditor` are deprecated in favor of `wcf\data\user\option\UserOptionBuilder` and the commands `CreateUserOption`, `UpdateUserOption` and `DeleteOption` in the `wcf\command\user\option` namespace.
+`UserOptionCategoryAction` and `UserOptionCategoryEditor` are deprecated in favor of `wcf\data\user\option\category\UserOptionCategoryBuilder` and the commands `CreateUserOptionCategory` and `UpdateUserOptionCategory`, which fire the events `UserOptionCategoryCreated` and `UserOptionCategoryUpdated`.
+
+The titles of user option categories are stored in the localized storage, use `UserOptionCategory::getTitle()` instead of the language item `wcf.user.option.category.{categoryName}`.
+The descriptions of user option categories are no longer displayed.
+
+### Filters
+
+User options of the `profile` category tree are automatically available as filters, which replace the user search and the `com.woltlab.wcf.userOptions` conditions:
+
+- the filters of the user list in the ACP, which also provides a hidden column for each option,
+- the filters of the members list for options that are `searchable` and visible to other users than the owner,
+- the object filters of ads, notices and automatic user group assignments (`wcf\system\object\filter\user\UserOptionObjectFilter`).
+
+No additional code is required, the filter is provided by the form option of the user option.
+
+## Grid View Column Visibility
+
+Grid views can allow users to select the visible columns, see [Column Visibility](../../php/api/grid_views.md#column-visibility).
+`AbstractGridViewPage` reads the selected columns from the `columns` parameter into the new property `$columns`, which may conflict with an existing property of the same name in a subclass.
diff --git a/docs/package/pip/user-option.md b/docs/package/pip/user-option.md
index 120cf0804..4c47985f3 100644
--- a/docs/package/pip/user-option.md
+++ b/docs/package/pip/user-option.md
@@ -1,27 +1,129 @@
# User Option Package Installation Plugin
Registers new user options (profile fields / user settings).
-The behaviour of this package installation plugin closely follows the [option](option.md) PIP.
+
+The input field and the output of a user option are provided by [form options](../../php/api/form_builder/form_options.md), the same infrastructure that is used for contact form options and other custom fields.
+User options of the `profile` category tree are also automatically available as filters in the user list of the ACP, in the members list (if they are `searchable`) and in object filters, e.g. for ads, notices and automatic user group assignments.
## Category Components
-The category definition works exactly like the option PIP.
+Each category is described as an `` element with the mandatory attribute `name`.
+
+### ``
+
+Optional
+
+The category’s parent category.
+
+### ``
+
+Optional
+
+Specifies the order of this category within the parent category.
+
+### ``
+
+Optional
+
+The options element can contain a comma-separated list of options of which at least one needs to be enabled for the category to be shown.
+
+### ``
+
+Optional
+
+The permissions element can contain a comma-separated list of permissions of which the active user needs to have at least one for the category to be shown.
## Option Components
-The fields `hidden`, `supporti18n` and `requirei18n` do not apply.
-The following extra fields are defined:
+Each option is described as an `