From 248bc7156d1cecc556f205c0c9af6d3b08454449 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 28 Jul 2025 17:27:40 +0200 Subject: [PATCH 01/19] Add the option to define the query selector for the header that should be reloaded. --- .../shared_standaloneInteractionButton.tpl | 4 +++- .../Component/Interaction/StandaloneButton.ts | 14 +++++++++++--- .../Component/Interaction/StandaloneButton.js | 15 ++++++++++----- ...aloneInteractionContextMenuComponent.class.php | 4 ++++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/com.woltlab.wcf/templates/shared_standaloneInteractionButton.tpl b/com.woltlab.wcf/templates/shared_standaloneInteractionButton.tpl index f673f25166..b7affbd2f1 100644 --- a/com.woltlab.wcf/templates/shared_standaloneInteractionButton.tpl +++ b/com.woltlab.wcf/templates/shared_standaloneInteractionButton.tpl @@ -22,7 +22,9 @@ '{unsafe:$providerClassName|encodeJS}', '{unsafe:$objectID|encodeJS}', '{unsafe:$redirectUrl|encodeJS}', - '{unsafe:$reloadHeaderEndpoint|encodeJS}' + '{unsafe:$reloadHeaderEndpoint|encodeJS}', + '{unsafe:$headerCssClassName|encodeJS}', + {if $reloadContextMenu}true{else}false{/if} ); }); diff --git a/ts/WoltLabSuite/Core/Component/Interaction/StandaloneButton.ts b/ts/WoltLabSuite/Core/Component/Interaction/StandaloneButton.ts index 5b2a4aa4a0..206683396e 100644 --- a/ts/WoltLabSuite/Core/Component/Interaction/StandaloneButton.ts +++ b/ts/WoltLabSuite/Core/Component/Interaction/StandaloneButton.ts @@ -10,6 +10,7 @@ import { getObject } from "WoltLabSuite/Core/Api/GetObject"; import { getContextMenuOptions } from "WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions"; import UiDropdownSimple from "WoltLabSuite/Core/Ui/Dropdown/Simple"; +import { insertHtml } from "WoltLabSuite/Core/Dom/Util"; interface HeaderContent { template: string; @@ -23,6 +24,8 @@ export class StandaloneButton { #objectId: string | number; #redirectUrl: string; #reloadHeaderEndpoint: string; + #headerCssClassName: string; + #reloadContextMenu: boolean; constructor( container: HTMLElement, @@ -30,12 +33,16 @@ export class StandaloneButton { objectId: string | number, redirectUrl: string, reloadHeaderEndpoint: string, + headerCssClassName: string, + reloadContextMenu: boolean, ) { this.#container = container; this.#providerClassName = providerClassName; this.#objectId = objectId; this.#redirectUrl = redirectUrl; this.#reloadHeaderEndpoint = reloadHeaderEndpoint; + this.#headerCssClassName = headerCssClassName; + this.#reloadContextMenu = reloadContextMenu; this.#initInteractions(); this.#initEventListeners(); @@ -55,11 +62,11 @@ export class StandaloneButton { } async #refreshHeader(): Promise { - if (!this.#reloadHeaderEndpoint) { + if (!this.#reloadContextMenu || !this.#reloadHeaderEndpoint || !this.#headerCssClassName) { return; } - const header = document.querySelector(".contentHeaderTitle"); + const header = document.querySelector(`${this.#headerCssClassName}`); if (!header) { return; } @@ -69,7 +76,8 @@ export class StandaloneButton { return; } - header.outerHTML = result.value.template; + insertHtml(result.value.template, header, "before"); + header.remove(); } #getDropdownMenu(): HTMLElement | undefined { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/StandaloneButton.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/StandaloneButton.js index ae76832889..5478be4b68 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/StandaloneButton.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/StandaloneButton.js @@ -6,7 +6,7 @@ * @license GNU Lesser General Public License * @since 6.2 */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/GetObject", "WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions", "WoltLabSuite/Core/Ui/Dropdown/Simple"], function (require, exports, tslib_1, GetObject_1, GetContextMenuOptions_1, Simple_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/GetObject", "WoltLabSuite/Core/Api/Interactions/GetContextMenuOptions", "WoltLabSuite/Core/Ui/Dropdown/Simple", "WoltLabSuite/Core/Dom/Util"], function (require, exports, tslib_1, GetObject_1, GetContextMenuOptions_1, Simple_1, Util_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.StandaloneButton = void 0; @@ -17,12 +17,16 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/GetObject", "WoltL #objectId; #redirectUrl; #reloadHeaderEndpoint; - constructor(container, providerClassName, objectId, redirectUrl, reloadHeaderEndpoint) { + #headerCssClassName; + #reloadContextMenu; + constructor(container, providerClassName, objectId, redirectUrl, reloadHeaderEndpoint, headerCssClassName, reloadContextMenu) { this.#container = container; this.#providerClassName = providerClassName; this.#objectId = objectId; this.#redirectUrl = redirectUrl; this.#reloadHeaderEndpoint = reloadHeaderEndpoint; + this.#headerCssClassName = headerCssClassName; + this.#reloadContextMenu = reloadContextMenu; this.#initInteractions(); this.#initEventListeners(); } @@ -36,10 +40,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/GetObject", "WoltL this.#initInteractions(); } async #refreshHeader() { - if (!this.#reloadHeaderEndpoint) { + if (!this.#reloadContextMenu || !this.#reloadHeaderEndpoint || !this.#headerCssClassName) { return; } - const header = document.querySelector(".contentHeaderTitle"); + const header = document.querySelector(`${this.#headerCssClassName}`); if (!header) { return; } @@ -47,7 +51,8 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Api/GetObject", "WoltL if (!result.ok) { return; } - header.outerHTML = result.value.template; + (0, Util_1.insertHtml)(result.value.template, header, "before"); + header.remove(); } #getDropdownMenu() { const button = this.#container.querySelector(".dropdownToggle"); diff --git a/wcfsetup/install/files/lib/system/interaction/StandaloneInteractionContextMenuComponent.class.php b/wcfsetup/install/files/lib/system/interaction/StandaloneInteractionContextMenuComponent.class.php index c60147ccb8..c3263b1731 100644 --- a/wcfsetup/install/files/lib/system/interaction/StandaloneInteractionContextMenuComponent.class.php +++ b/wcfsetup/install/files/lib/system/interaction/StandaloneInteractionContextMenuComponent.class.php @@ -25,6 +25,8 @@ public function __construct( protected readonly string $redirectUrl, protected readonly string $reloadHeaderEndpoint = '', protected ?InteractionContextMenuComponentConfiguration $configuration = null, + protected readonly string $headerCssClassName = '.contentHeaderTitle', + protected readonly bool $reloadContextMenu = true, ) { parent::__construct($provider, $configuration); @@ -49,6 +51,8 @@ public function render(): string 'objectID' => $this->object->getObjectID(), 'redirectUrl' => $this->redirectUrl, 'reloadHeaderEndpoint' => $this->reloadHeaderEndpoint, + 'headerCssClassName' => $this->headerCssClassName, + 'reloadContextMenu' => $this->reloadContextMenu, 'configuration' => $this->configuration, ], ); From 78ac201087065006f609932e0af2037aad8b0acf Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 28 Jul 2025 17:28:10 +0200 Subject: [PATCH 02/19] Use `$view->user` instead of `$user` --- .../templates/userProfileHeader.tpl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/com.woltlab.wcf/templates/userProfileHeader.tpl b/com.woltlab.wcf/templates/userProfileHeader.tpl index a37f894b00..dbbe025249 100644 --- a/com.woltlab.wcf/templates/userProfileHeader.tpl +++ b/com.woltlab.wcf/templates/userProfileHeader.tpl @@ -30,7 +30,7 @@ {if $view->canEditCoverPhoto()}
  • - @@ -68,7 +68,7 @@

    {$view->user->username} {if $view->user->banned} - + {icon name='lock'} {/if} @@ -108,13 +108,13 @@
    {event name='beforeButtons'} - {if $__wcf->user->userID && $user->userID != $__wcf->user->userID} - {if !$__wcf->getUserProfileHandler()->isIgnoredByUser($user->userID)} - {if $__wcf->getUserProfileHandler()->isFollowing($user->userID)} + {if $__wcf->user->userID && $view->user->userID != $__wcf->user->userID} + {if !$__wcf->getUserProfileHandler()->isIgnoredByUser($view->user->userID)} + {if $__wcf->getUserProfileHandler()->isFollowing($view->user->userID)} @@ -122,7 +122,7 @@ @@ -164,9 +164,9 @@ {if $view->user->canViewOnlineStatus() && $view->user->getLastActivityTime()}
    {icon name='clock'} {lang}wcf.user.usersOnline.lastActivity{/lang}
    {time time=$view->user->getLastActivityTime()}
    - {if $user->getCurrentLocation()} + {if $view->user->getCurrentLocation()}
    {icon name='location-arrow'} {lang}wcf.user.usersOnline.location{/lang}
    -
    {unsafe:$user->getCurrentLocation()}
    +
    {unsafe:$view->user->getCurrentLocation()}
    {/if} {/if} {if $__wcf->session->getPermission('admin.user.canViewIpAddress') && $view->user->registrationIpAddress} From 26f53bb95ba65a1b7de39f9f83df4094549f905b Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 28 Jul 2025 17:29:24 +0200 Subject: [PATCH 03/19] Adding functions for ban/unban users via RPC endpoints --- .../files/lib/action/UserBanAction.class.php | 127 ++++++++++++++++++ .../files/lib/bootstrap/com.woltlab.wcf.php | 2 + .../core/users/GetUserProfileHeader.class.php | 52 +++++++ .../controller/core/users/UnbanUser.class.php | 55 ++++++++ .../user/UserManagementInteractions.class.php | 89 ++++++------ .../lib/system/user/command/Ban.class.php | 34 +++++ .../lib/system/user/command/Unban.class.php | 31 +++++ .../profile/UserProfileHeaderView.class.php | 6 +- 8 files changed, 354 insertions(+), 42 deletions(-) create mode 100644 wcfsetup/install/files/lib/action/UserBanAction.class.php create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/users/GetUserProfileHeader.class.php create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/users/UnbanUser.class.php create mode 100644 wcfsetup/install/files/lib/system/user/command/Ban.class.php create mode 100644 wcfsetup/install/files/lib/system/user/command/Unban.class.php diff --git a/wcfsetup/install/files/lib/action/UserBanAction.class.php b/wcfsetup/install/files/lib/action/UserBanAction.class.php new file mode 100644 index 0000000000..8a6ab89efd --- /dev/null +++ b/wcfsetup/install/files/lib/action/UserBanAction.class.php @@ -0,0 +1,127 @@ + + * @since 6.3 + */ +final class UserBanAction implements RequestHandlerInterface +{ + #[\Override] + public function handle(ServerRequestInterface $request): ResponseInterface + { + $parameters = Helper::mapQueryParameters( + $request->getQueryParams(), + <<<'EOT' + array { + id: positive-int + } + EOT + ); + + $user = UserProfileRuntimeCache::getInstance()->getObject($parameters['id']); + $this->assertUserCanBeBanned($user); + + $form = $this->getForm(); + + if ($request->getMethod() === 'GET') { + return $form->toResponse(); + } elseif ($request->getMethod() === 'POST') { + $response = $form->validateRequest($request); + if ($response !== null) { + return $response; + } + + $data = $form->getData()['data']; + $reason = $data['reason']; + if ($data['neverExpires']) { + $expires = null; + } else { + $expires = $data['expires']; + } + + (new Ban($user->getDecoratedObject(), $reason, $expires))(); + + return new JsonResponse([]); + } else { + throw new \LogicException('Unreachable'); + } + } + + private function assertUserCanBeBanned(?UserProfile $userProfile): void + { + if (!$userProfile) { + throw new IllegalLinkException(); + } + + if ($userProfile->userID === WCF::getUser()->userID) { + throw new IllegalLinkException(); + } + + if (!WCF::getSession()->getPermission('admin.user.canBanUser')) { + throw new PermissionDeniedException(); + } + + if (!UserGroup::isAccessibleGroup($userProfile->getGroupIDs())) { + throw new PermissionDeniedException(); + } + + if ($userProfile->banned !== 0) { + throw new IllegalLinkException(); + } + } + + private function getForm(): Psr15DialogForm + { + $form = new Psr15DialogForm( + static::class, + WCF::getLanguage()->getDynamicVariable('wcf.user.ban.confirmMessage') + ); + $form->appendChildren([ + MultilineTextFormField::create('reason') + ->rows(3) + ->label('wcf.global.reason') + ->description('wcf.user.ban.reason.description'), + BooleanFormField::create('neverExpires') + ->label('wcf.user.ban.neverExpires') + ->value(true), + DateFormField::create('expires') + ->label('wcf.user.ban.expires') + ->description('wcf.user.ban.expires.description') + ->earliestDate(TIME_NOW) + ->required() + ->addDependency( + EmptyFormFieldDependency::create('neverExpires') + ->fieldId('neverExpires') + ), + ]); + + $form->markRequiredFields(false); + $form->build(); + + return $form; + } +} diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 3e860c3274..8e4585d58f 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -174,6 +174,8 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\users\options\EnableOption()); $event->register(new \wcf\system\endpoint\controller\core\users\ranks\DeleteUserRank()); $event->register(new \wcf\system\endpoint\controller\core\users\trophies\DeleteUserTrophy()); + $event->register(new \wcf\system\endpoint\controller\core\users\GetUserProfileHeader()); + $event->register(new \wcf\system\endpoint\controller\core\users\UnbanUser()); $event->register(new \wcf\system\endpoint\controller\core\interactions\GetBulkContextMenuOptions()); $event->register(new \wcf\system\endpoint\controller\core\interactions\GetContextMenuOptions()); $event->register(new \wcf\system\endpoint\controller\core\articles\DeleteArticle()); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/users/GetUserProfileHeader.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/GetUserProfileHeader.class.php new file mode 100644 index 0000000000..977a695e11 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/GetUserProfileHeader.class.php @@ -0,0 +1,52 @@ + + * @since 6.3 + */ +#[GetRequest('/core/users/{id:\d+}/profile-header')] +final class GetUserProfileHeader implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $userProfile = UserProfileRuntimeCache::getInstance()->getObject((int)$variables['id']); + + $this->assertUserProfileCanBeViewed($userProfile); + + $view = new UserProfileHeaderView($userProfile); + + return new JsonResponse([ + 'template' => $view->__toString(), + ]); + } + + private function assertUserProfileCanBeViewed(?UserProfile $userProfile): void + { + if ($userProfile === null) { + throw new UserInputException('id'); + } + + if ($userProfile->userID !== WCF::getUser()->userID && !WCF::getSession()->getPermission('user.profile.canViewUserProfile')) { + throw new PermissionDeniedException(); + } + } +} diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/users/UnbanUser.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/UnbanUser.class.php new file mode 100644 index 0000000000..a47fe70634 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/UnbanUser.class.php @@ -0,0 +1,55 @@ + + * @since 6.3 + */ +#[PostRequest('/core/users/{id:\d+}/unban')] +final class UnbanUser implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $user = Helper::fetchObjectFromRequestParameter($variables['id'], User::class); + + $this->assertUserCanUnbanned($user); + + if ($user->banned) { + (new Unban($user))(); + } + + return new JsonResponse([]); + } + + private function assertUserCanUnbanned(User $user): void + { + if (WCF::getUser()->userID === $user->userID) { + throw new IllegalLinkException(); + } + if (!WCF::getSession()->getPermission('admin.user.canBanUser')) { + throw new PermissionDeniedException(); + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + throw new PermissionDeniedException(); + } + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php index dcdc71bbee..c003145c41 100644 --- a/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php +++ b/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php @@ -3,6 +3,7 @@ namespace wcf\system\interaction\user; use wcf\acp\form\UserEditForm; +use wcf\action\UserBanAction; use wcf\data\DatabaseObject; use wcf\data\user\group\UserGroup; use wcf\data\user\UserProfile; @@ -10,6 +11,8 @@ use wcf\system\event\EventHandler; use wcf\system\interaction\AbstractInteraction; use wcf\system\interaction\AbstractInteractionProvider; +use wcf\system\interaction\FormBuilderDialogInteraction; +use wcf\system\interaction\RpcInteraction; use wcf\system\request\LinkHandler; use wcf\system\WCF; use wcf\util\StringUtil; @@ -27,34 +30,52 @@ final class UserManagementInteractions extends AbstractInteractionProvider { public function __construct() { - if (WCF::getSession()->getPermission('admin.user.canBanUser')) { - $this->addInteraction( - new class( - 'ban', - static fn(UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID - ) extends AbstractInteraction { - #[\Override] - public function render(DatabaseObject $object): string - { - \assert($object instanceof UserProfile); - $title = WCF::getLanguage()->get($object->banned ? 'wcf.user.unban' : 'wcf.user.ban'); + $this->addInteraction( + new FormBuilderDialogInteraction( + "ban", + LinkHandler::getInstance()->getControllerLink(UserBanAction::class, [ + 'id' => '%s', + ]), + 'wcf.user.ban', + static function (UserProfile $user): bool { + if (WCF::getUser()->userID === $user->userID) { + return false; + } + if (!WCF::getSession()->getPermission('admin.user.canBanUser')) { + return false; + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + return false; + } - return <<{$title} - HTML; + return $user->banned === 0; + }, + ) + ); + $this->addInteraction( + new RpcInteraction( + "unban", + 'core/users/%s/unban', + 'wcf.user.unban', + isAvailableCallback: static function (UserProfile $user): bool { + if (WCF::getUser()->userID === $user->userID) { + return false; } - } - ); - } + if (!WCF::getSession()->getPermission('admin.user.canBanUser')) { + return false; + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + return false; + } + + return $user->banned === 1; + }, + ) + ); + if (WCF::getSession()->getPermission('admin.user.canDisableAvatar')) { $this->addInteraction( - new class( - 'disable-avatar', - static fn(UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID - ) extends AbstractInteraction { + new class('disable-avatar', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { #[\Override] public function render(DatabaseObject $object): string { @@ -73,10 +94,7 @@ class="jsButtonUserDisableAvatar" } if (WCF::getSession()->getPermission('admin.user.canDisableSignature')) { $this->addInteraction( - new class( - 'disable-signature', - static fn(UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID - ) extends AbstractInteraction { + new class('disable-signature', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { #[\Override] public function render(DatabaseObject $object): string { @@ -95,10 +113,7 @@ class="jsButtonUserDisableSignature" } if (WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto')) { $this->addInteraction( - new class( - 'disable-cover-photo', - static fn(UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID - ) extends AbstractInteraction { + new class('disable-cover-photo', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { #[\Override] public function render(DatabaseObject $object): string { @@ -117,10 +132,7 @@ class="jsButtonUserDisableCoverPhoto" } if (WCF::getSession()->getPermission('admin.user.canEnableUser')) { $this->addInteraction( - new class( - 'enable', - static fn(UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID - ) extends AbstractInteraction { + new class('enable', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { #[\Override] public function render(DatabaseObject $object): string { @@ -139,10 +151,7 @@ class="jsButtonUserEnable" } if (WCF::getSession()->getPermission('admin.general.canUseAcp') && WCF::getSession()->getPermission('admin.user.canEditUser')) { $this->addInteraction( - new class( - 'edit', - static fn(UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID - ) extends AbstractInteraction { + new class('edit', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { #[\Override] public function render(DatabaseObject $object): string { diff --git a/wcfsetup/install/files/lib/system/user/command/Ban.class.php b/wcfsetup/install/files/lib/system/user/command/Ban.class.php new file mode 100644 index 0000000000..53596bad07 --- /dev/null +++ b/wcfsetup/install/files/lib/system/user/command/Ban.class.php @@ -0,0 +1,34 @@ + + * @since 6.3 + */ +final class Ban +{ + public function __construct( + private readonly User $user, + private readonly string $reason, + private readonly ?int $banExpires = null, + ) { + } + + public function __invoke(): void + { + $editor = new UserEditor($this->user); + $editor->update([ + 'banned' => 1, + 'banReason' => $this->reason, + 'banExpires' => $this->banExpires ?? 0, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/user/command/Unban.class.php b/wcfsetup/install/files/lib/system/user/command/Unban.class.php new file mode 100644 index 0000000000..c3c0963e3c --- /dev/null +++ b/wcfsetup/install/files/lib/system/user/command/Unban.class.php @@ -0,0 +1,31 @@ + + * @since 6.3 + */ +final class Unban +{ + public function __construct( + private readonly User $user, + ) { + } + + public function __invoke(): void + { + $editor = new UserEditor($this->user); + $editor->update([ + 'banned' => 0, + 'banExpires' => 0, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/view/user/profile/UserProfileHeaderView.class.php b/wcfsetup/install/files/lib/system/view/user/profile/UserProfileHeaderView.class.php index 2aa4497c6b..a2c7668ace 100644 --- a/wcfsetup/install/files/lib/system/view/user/profile/UserProfileHeaderView.class.php +++ b/wcfsetup/install/files/lib/system/view/user/profile/UserProfileHeaderView.class.php @@ -146,13 +146,15 @@ private function initManagementContextMenu(): void new UserManagementInteractions(), $this->user, LinkHandler::getInstance()->getControllerLink(MembersListPage::class), + reloadHeaderEndpoint: "core/users/{$this->user->userID}/profile-header", configuration: new InteractionContextMenuComponentConfiguration( cssClassName: 'userProfileHeader__button', buttonCssClassName: 'button small', dropdownMenuCssClassName: 'userProfileHeader__managementOptions', + tooltip: 'wcf.user.profile.management', icon: FontAwesomeIcon::fromValues('gear'), - tooltip: 'wcf.user.profile.management' - ) + ), + headerCssClassName: '.userProfileHeader' ); if (!$this->isInAccessibleGroup() || $this->user->userID == WCF::getUser()->userID) { From e5921d18c28fe8555f4a9afa3817ba03bff45eb9 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 28 Jul 2025 17:42:03 +0200 Subject: [PATCH 04/19] Adding functions for enable/disable users avtar via RPC endpoints --- .../action/UserDisableAvatarAction.class.php | 126 ++++++++++++++++++ .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../core/users/EnableUserAvatar.class.php | 55 ++++++++ .../user/UserManagementInteractions.class.php | 64 ++++++--- .../user/command/DisableAvatar.class.php | 34 +++++ .../user/command/EnableAvatar.class.php | 31 +++++ 6 files changed, 291 insertions(+), 20 deletions(-) create mode 100644 wcfsetup/install/files/lib/action/UserDisableAvatarAction.class.php create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserAvatar.class.php create mode 100644 wcfsetup/install/files/lib/system/user/command/DisableAvatar.class.php create mode 100644 wcfsetup/install/files/lib/system/user/command/EnableAvatar.class.php diff --git a/wcfsetup/install/files/lib/action/UserDisableAvatarAction.class.php b/wcfsetup/install/files/lib/action/UserDisableAvatarAction.class.php new file mode 100644 index 0000000000..7923008ed9 --- /dev/null +++ b/wcfsetup/install/files/lib/action/UserDisableAvatarAction.class.php @@ -0,0 +1,126 @@ + + * @since 6.3 + */ +final class UserDisableAvatarAction implements RequestHandlerInterface +{ + #[\Override] + public function handle(ServerRequestInterface $request): ResponseInterface + { + $parameters = Helper::mapQueryParameters( + $request->getQueryParams(), + <<<'EOT' + array { + id: positive-int + } + EOT + ); + + $user = UserProfileRuntimeCache::getInstance()->getObject($parameters['id']); + $this->assertAvatarCanBeDisabled($user); + + $form = $this->getForm(); + + if ($request->getMethod() === 'GET') { + return $form->toResponse(); + } elseif ($request->getMethod() === 'POST') { + $response = $form->validateRequest($request); + if ($response !== null) { + return $response; + } + + $data = $form->getData()['data']; + $reason = $data['reason']; + if ($data['neverExpires']) { + $expires = null; + } else { + $expires = $data['expires']; + } + + (new DisableAvatar($user->getDecoratedObject(), $reason, $expires))(); + + return new JsonResponse([]); + } else { + throw new \LogicException('Unreachable'); + } + } + + private function assertAvatarCanBeDisabled(?UserProfile $userProfile): void + { + if (!$userProfile) { + throw new IllegalLinkException(); + } + + if ($userProfile->userID === WCF::getUser()->userID) { + throw new IllegalLinkException(); + } + + if (!WCF::getSession()->getPermission('admin.user.canDisableAvatar')) { + throw new PermissionDeniedException(); + } + + if (!UserGroup::isAccessibleGroup($userProfile->getGroupIDs())) { + throw new PermissionDeniedException(); + } + + if ($userProfile->disableAvatar !== 0) { + throw new IllegalLinkException(); + } + } + + private function getForm(): Psr15DialogForm + { + $form = new Psr15DialogForm( + static::class, + WCF::getLanguage()->getDynamicVariable('wcf.user.disableAvatar.confirmMessage') + ); + $form->appendChildren([ + MultilineTextFormField::create('reason') + ->rows(3) + ->label('wcf.global.reason'), + BooleanFormField::create('neverExpires') + ->label('wcf.user.disableAvatar.neverExpires') + ->value(true), + DateFormField::create('expires') + ->label('wcf.user.disableAvatar.expires') + ->description('wcf.user.disableAvatar.expires.description') + ->earliestDate(TIME_NOW) + ->required() + ->addDependency( + EmptyFormFieldDependency::create('neverExpires') + ->fieldId('neverExpires') + ), + ]); + + $form->markRequiredFields(false); + $form->build(); + + return $form; + } +} diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 8e4585d58f..1733a19ed5 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -176,6 +176,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\users\trophies\DeleteUserTrophy()); $event->register(new \wcf\system\endpoint\controller\core\users\GetUserProfileHeader()); $event->register(new \wcf\system\endpoint\controller\core\users\UnbanUser()); + $event->register(new \wcf\system\endpoint\controller\core\users\EnableUserAvatar()); $event->register(new \wcf\system\endpoint\controller\core\interactions\GetBulkContextMenuOptions()); $event->register(new \wcf\system\endpoint\controller\core\interactions\GetContextMenuOptions()); $event->register(new \wcf\system\endpoint\controller\core\articles\DeleteArticle()); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserAvatar.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserAvatar.class.php new file mode 100644 index 0000000000..4f10c296b0 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserAvatar.class.php @@ -0,0 +1,55 @@ + + * @since 6.3 + */ +#[PostRequest('/core/users/{id:\d+}/enable-avatar')] +final class EnableUserAvatar implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $user = Helper::fetchObjectFromRequestParameter($variables['id'], User::class); + + $this->assertAvatarCanBeEnabled($user); + + if ($user->disableAvatar) { + (new EnableAvatar($user))(); + } + + return new JsonResponse([]); + } + + private function assertAvatarCanBeEnabled(User $user): void + { + if (WCF::getUser()->userID === $user->userID) { + throw new IllegalLinkException(); + } + if (!WCF::getSession()->getPermission('admin.user.canDisableAvatar')) { + throw new PermissionDeniedException(); + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + throw new PermissionDeniedException(); + } + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php index c003145c41..dbe12344df 100644 --- a/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php +++ b/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php @@ -4,6 +4,7 @@ use wcf\acp\form\UserEditForm; use wcf\action\UserBanAction; +use wcf\action\UserDisableAvatarAction; use wcf\data\DatabaseObject; use wcf\data\user\group\UserGroup; use wcf\data\user\UserProfile; @@ -32,7 +33,7 @@ public function __construct() { $this->addInteraction( new FormBuilderDialogInteraction( - "ban", + 'ban', LinkHandler::getInstance()->getControllerLink(UserBanAction::class, [ 'id' => '%s', ]), @@ -54,7 +55,7 @@ static function (UserProfile $user): bool { ); $this->addInteraction( new RpcInteraction( - "unban", + 'unban', 'core/users/%s/unban', 'wcf.user.unban', isAvailableCallback: static function (UserProfile $user): bool { @@ -72,26 +73,49 @@ static function (UserProfile $user): bool { }, ) ); + $this->addInteraction( + new FormBuilderDialogInteraction( + 'disable-avatar', + LinkHandler::getInstance()->getControllerLink(UserDisableAvatarAction::class, [ + 'id' => '%s', + ]), + 'wcf.user.disableAvatar', + static function (UserProfile $user): bool { + if (WCF::getUser()->userID === $user->userID) { + return false; + } + if (!WCF::getSession()->getPermission('admin.user.canDisableAvatar')) { + return false; + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + return false; + } - if (WCF::getSession()->getPermission('admin.user.canDisableAvatar')) { - $this->addInteraction( - new class('disable-avatar', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { - #[\Override] - public function render(DatabaseObject $object): string - { - \assert($object instanceof UserProfile); - $title = WCF::getLanguage()->get($object->disableAvatar ? 'wcf.user.enableAvatar' : 'wcf.user.disableAvatar'); - - return <<{$title} - HTML; + return $user->disableAvatar === 0; + }, + ) + ); + $this->addInteraction( + new RpcInteraction( + 'enable-avatar', + 'core/users/%s/enable-avatar', + 'wcf.user.enableAvatar', + isAvailableCallback: static function (UserProfile $user): bool { + if (WCF::getUser()->userID === $user->userID) { + return false; } - } - ); - } + if (!WCF::getSession()->getPermission('admin.user.canDisableAvatar')) { + return false; + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + return false; + } + + return $user->disableAvatar === 1; + }, + ) + ); + if (WCF::getSession()->getPermission('admin.user.canDisableSignature')) { $this->addInteraction( new class('disable-signature', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { diff --git a/wcfsetup/install/files/lib/system/user/command/DisableAvatar.class.php b/wcfsetup/install/files/lib/system/user/command/DisableAvatar.class.php new file mode 100644 index 0000000000..34dc18b102 --- /dev/null +++ b/wcfsetup/install/files/lib/system/user/command/DisableAvatar.class.php @@ -0,0 +1,34 @@ + + * @since 6.3 + */ +final class DisableAvatar +{ + public function __construct( + private readonly User $user, + private readonly string $reason, + private readonly ?int $banExpires = null, + ) { + } + + public function __invoke(): void + { + $editor = new UserEditor($this->user); + $editor->update([ + 'disableAvatar' => 1, + 'disableAvatarReason' => $this->reason, + 'disableAvatarExpires' => $this->banExpires ?? 0, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/user/command/EnableAvatar.class.php b/wcfsetup/install/files/lib/system/user/command/EnableAvatar.class.php new file mode 100644 index 0000000000..5a207f7fb8 --- /dev/null +++ b/wcfsetup/install/files/lib/system/user/command/EnableAvatar.class.php @@ -0,0 +1,31 @@ + + * @since 6.3 + */ +final class EnableAvatar +{ + public function __construct( + private readonly User $user, + ) { + } + + public function __invoke(): void + { + $editor = new UserEditor($this->user); + $editor->update([ + 'disableAvatar' => 0, + 'disableAvatarExpires' => 0, + ]); + } +} From 48092ea0a8f1cd9004e0986e02bc328d1d8a47b0 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 28 Jul 2025 17:47:14 +0200 Subject: [PATCH 05/19] Adding functions for enable/disable users signature via RPC endpoints --- .../UserDisableSignatureAction.class.php | 126 ++++++++++++++++++ .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../core/users/EnableUserSignature.class.php | 55 ++++++++ .../user/UserManagementInteractions.class.php | 60 ++++++--- .../user/command/DisableSignature.class.php | 34 +++++ .../user/command/EnableSignature.class.php | 31 +++++ 6 files changed, 289 insertions(+), 18 deletions(-) create mode 100644 wcfsetup/install/files/lib/action/UserDisableSignatureAction.class.php create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserSignature.class.php create mode 100644 wcfsetup/install/files/lib/system/user/command/DisableSignature.class.php create mode 100644 wcfsetup/install/files/lib/system/user/command/EnableSignature.class.php diff --git a/wcfsetup/install/files/lib/action/UserDisableSignatureAction.class.php b/wcfsetup/install/files/lib/action/UserDisableSignatureAction.class.php new file mode 100644 index 0000000000..8ad1805006 --- /dev/null +++ b/wcfsetup/install/files/lib/action/UserDisableSignatureAction.class.php @@ -0,0 +1,126 @@ + + * @since 6.3 + */ +final class UserDisableSignatureAction implements RequestHandlerInterface +{ + #[\Override] + public function handle(ServerRequestInterface $request): ResponseInterface + { + $parameters = Helper::mapQueryParameters( + $request->getQueryParams(), + <<<'EOT' + array { + id: positive-int + } + EOT + ); + + $user = UserProfileRuntimeCache::getInstance()->getObject($parameters['id']); + $this->assertSignatureCanBeDisabled($user); + + $form = $this->getForm(); + + if ($request->getMethod() === 'GET') { + return $form->toResponse(); + } elseif ($request->getMethod() === 'POST') { + $response = $form->validateRequest($request); + if ($response !== null) { + return $response; + } + + $data = $form->getData()['data']; + $reason = $data['reason']; + if ($data['neverExpires']) { + $expires = null; + } else { + $expires = $data['expires']; + } + + (new DisableSignature($user->getDecoratedObject(), $reason, $expires))(); + + return new JsonResponse([]); + } else { + throw new \LogicException('Unreachable'); + } + } + + private function assertSignatureCanBeDisabled(?UserProfile $userProfile): void + { + if (!$userProfile) { + throw new IllegalLinkException(); + } + + if ($userProfile->userID === WCF::getUser()->userID) { + throw new IllegalLinkException(); + } + + if (!WCF::getSession()->getPermission('admin.user.canDisableSignature')) { + throw new PermissionDeniedException(); + } + + if (!UserGroup::isAccessibleGroup($userProfile->getGroupIDs())) { + throw new PermissionDeniedException(); + } + + if ($userProfile->disableSignature !== 0) { + throw new IllegalLinkException(); + } + } + + private function getForm(): Psr15DialogForm + { + $form = new Psr15DialogForm( + static::class, + WCF::getLanguage()->getDynamicVariable('wcf.user.disableSignature.confirmMessage') + ); + $form->appendChildren([ + MultilineTextFormField::create('reason') + ->rows(3) + ->label('wcf.global.reason'), + BooleanFormField::create('neverExpires') + ->label('wcf.user.disableSignature.neverExpires') + ->value(true), + DateFormField::create('expires') + ->label('wcf.user.disableSignature.expires') + ->description('wcf.user.disableSignature.expires.description') + ->earliestDate(TIME_NOW) + ->required() + ->addDependency( + EmptyFormFieldDependency::create('neverExpires') + ->fieldId('neverExpires') + ), + ]); + + $form->markRequiredFields(false); + $form->build(); + + return $form; + } +} diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 1733a19ed5..7247f3fa2e 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -177,6 +177,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\users\GetUserProfileHeader()); $event->register(new \wcf\system\endpoint\controller\core\users\UnbanUser()); $event->register(new \wcf\system\endpoint\controller\core\users\EnableUserAvatar()); + $event->register(new \wcf\system\endpoint\controller\core\users\EnableUserSignature()); $event->register(new \wcf\system\endpoint\controller\core\interactions\GetBulkContextMenuOptions()); $event->register(new \wcf\system\endpoint\controller\core\interactions\GetContextMenuOptions()); $event->register(new \wcf\system\endpoint\controller\core\articles\DeleteArticle()); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserSignature.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserSignature.class.php new file mode 100644 index 0000000000..912eff6f01 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserSignature.class.php @@ -0,0 +1,55 @@ + + * @since 6.3 + */ +#[PostRequest('/core/users/{id:\d+}/enable-signature')] +final class EnableUserSignature implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $user = Helper::fetchObjectFromRequestParameter($variables['id'], User::class); + + $this->assertSignatureCanBeEnabled($user); + + if ($user->disableSignature) { + (new EnableSignature($user))(); + } + + return new JsonResponse([]); + } + + private function assertSignatureCanBeEnabled(User $user): void + { + if (WCF::getUser()->userID === $user->userID) { + throw new IllegalLinkException(); + } + if (!WCF::getSession()->getPermission('admin.user.canDisableSignature')) { + throw new PermissionDeniedException(); + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + throw new PermissionDeniedException(); + } + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php index dbe12344df..34b11ee380 100644 --- a/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php +++ b/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php @@ -5,6 +5,7 @@ use wcf\acp\form\UserEditForm; use wcf\action\UserBanAction; use wcf\action\UserDisableAvatarAction; +use wcf\action\UserDisableSignatureAction; use wcf\data\DatabaseObject; use wcf\data\user\group\UserGroup; use wcf\data\user\UserProfile; @@ -115,26 +116,49 @@ static function (UserProfile $user): bool { }, ) ); + $this->addInteraction( + new FormBuilderDialogInteraction( + 'disable-signature', + LinkHandler::getInstance()->getControllerLink(UserDisableSignatureAction::class, [ + 'id' => '%s', + ]), + 'wcf.user.disableSignature', + static function (UserProfile $user): bool { + if (WCF::getUser()->userID === $user->userID) { + return false; + } + if (!WCF::getSession()->getPermission('admin.user.canDisableSignature')) { + return false; + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + return false; + } - if (WCF::getSession()->getPermission('admin.user.canDisableSignature')) { - $this->addInteraction( - new class('disable-signature', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { - #[\Override] - public function render(DatabaseObject $object): string - { - \assert($object instanceof UserProfile); - $title = WCF::getLanguage()->get($object->disableSignature ? 'wcf.user.enableSignature' : 'wcf.user.disableSignature'); - - return <<{$title} - HTML; + return $user->disableSignature === 0; + }, + ) + ); + $this->addInteraction( + new RpcInteraction( + 'enable-signature', + 'core/users/%s/enable-signature', + 'wcf.user.enableSignature', + isAvailableCallback: static function (UserProfile $user): bool { + if (WCF::getUser()->userID === $user->userID) { + return false; } - } - ); - } + if (!WCF::getSession()->getPermission('admin.user.canDisableSignature')) { + return false; + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + return false; + } + + return $user->disableSignature === 1; + }, + ) + ); + if (WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto')) { $this->addInteraction( new class('disable-cover-photo', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { diff --git a/wcfsetup/install/files/lib/system/user/command/DisableSignature.class.php b/wcfsetup/install/files/lib/system/user/command/DisableSignature.class.php new file mode 100644 index 0000000000..60bbeeb540 --- /dev/null +++ b/wcfsetup/install/files/lib/system/user/command/DisableSignature.class.php @@ -0,0 +1,34 @@ + + * @since 6.3 + */ +final class DisableSignature +{ + public function __construct( + private readonly User $user, + private readonly string $reason, + private readonly ?int $banExpires = null, + ) { + } + + public function __invoke(): void + { + $editor = new UserEditor($this->user); + $editor->update([ + 'disableSignature' => 1, + 'disableSignatureReason' => $this->reason, + 'disableSignatureExpires' => $this->banExpires ?? 0, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/user/command/EnableSignature.class.php b/wcfsetup/install/files/lib/system/user/command/EnableSignature.class.php new file mode 100644 index 0000000000..7f9b3d7753 --- /dev/null +++ b/wcfsetup/install/files/lib/system/user/command/EnableSignature.class.php @@ -0,0 +1,31 @@ + + * @since 6.3 + */ +final class EnableSignature +{ + public function __construct( + private readonly User $user, + ) { + } + + public function __invoke(): void + { + $editor = new UserEditor($this->user); + $editor->update([ + 'disableSignature' => 0, + 'disableSignatureExpires' => 0, + ]); + } +} From bfb3eb4f98fe0dd8c001565ca1918c287babcc79 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 28 Jul 2025 17:53:18 +0200 Subject: [PATCH 06/19] Adding functions for enable/disable users cover photo via RPC endpoints --- .../UserDisableCoverPhotoAction.class.php | 126 ++++++++++++++++++ .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../core/users/EnableUserCoverPhoto.class.php | 55 ++++++++ .../user/UserManagementInteractions.class.php | 61 ++++++--- .../user/command/DisableCoverPhoto.class.php | 34 +++++ .../user/command/EnableCoverPhoto.class.php | 31 +++++ 6 files changed, 291 insertions(+), 17 deletions(-) create mode 100644 wcfsetup/install/files/lib/action/UserDisableCoverPhotoAction.class.php create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserCoverPhoto.class.php create mode 100644 wcfsetup/install/files/lib/system/user/command/DisableCoverPhoto.class.php create mode 100644 wcfsetup/install/files/lib/system/user/command/EnableCoverPhoto.class.php diff --git a/wcfsetup/install/files/lib/action/UserDisableCoverPhotoAction.class.php b/wcfsetup/install/files/lib/action/UserDisableCoverPhotoAction.class.php new file mode 100644 index 0000000000..71981574fb --- /dev/null +++ b/wcfsetup/install/files/lib/action/UserDisableCoverPhotoAction.class.php @@ -0,0 +1,126 @@ + + * @since 6.3 + */ +final class UserDisableCoverPhotoAction implements RequestHandlerInterface +{ + #[\Override] + public function handle(ServerRequestInterface $request): ResponseInterface + { + $parameters = Helper::mapQueryParameters( + $request->getQueryParams(), + <<<'EOT' + array { + id: positive-int + } + EOT + ); + + $user = UserProfileRuntimeCache::getInstance()->getObject($parameters['id']); + $this->assertCoverPhotoCanBeDisabled($user); + + $form = $this->getForm(); + + if ($request->getMethod() === 'GET') { + return $form->toResponse(); + } elseif ($request->getMethod() === 'POST') { + $response = $form->validateRequest($request); + if ($response !== null) { + return $response; + } + + $data = $form->getData()['data']; + $reason = $data['reason']; + if ($data['neverExpires']) { + $expires = null; + } else { + $expires = $data['expires']; + } + + (new DisableCoverPhoto($user->getDecoratedObject(), $reason, $expires))(); + + return new JsonResponse([]); + } else { + throw new \LogicException('Unreachable'); + } + } + + private function assertCoverPhotoCanBeDisabled(?UserProfile $userProfile): void + { + if (!$userProfile) { + throw new IllegalLinkException(); + } + + if ($userProfile->userID === WCF::getUser()->userID) { + throw new IllegalLinkException(); + } + + if (!WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto')) { + throw new PermissionDeniedException(); + } + + if (!UserGroup::isAccessibleGroup($userProfile->getGroupIDs())) { + throw new PermissionDeniedException(); + } + + if ($userProfile->disableCoverPhoto !== 0) { + throw new IllegalLinkException(); + } + } + + private function getForm(): Psr15DialogForm + { + $form = new Psr15DialogForm( + static::class, + WCF::getLanguage()->getDynamicVariable('wcf.user.disableCoverPhoto.confirmMessage') + ); + $form->appendChildren([ + MultilineTextFormField::create('reason') + ->rows(3) + ->label('wcf.global.reason'), + BooleanFormField::create('neverExpires') + ->label('wcf.user.disableCoverPhoto.neverExpires') + ->value(true), + DateFormField::create('expires') + ->label('wcf.user.disableCoverPhoto.expires') + ->description('wcf.user.disableCoverPhoto.expires.description') + ->earliestDate(TIME_NOW) + ->required() + ->addDependency( + EmptyFormFieldDependency::create('neverExpires') + ->fieldId('neverExpires') + ), + ]); + + $form->markRequiredFields(false); + $form->build(); + + return $form; + } +} diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 7247f3fa2e..e7bb2a1da3 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -178,6 +178,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\users\UnbanUser()); $event->register(new \wcf\system\endpoint\controller\core\users\EnableUserAvatar()); $event->register(new \wcf\system\endpoint\controller\core\users\EnableUserSignature()); + $event->register(new \wcf\system\endpoint\controller\core\users\EnableUserCoverPhoto()); $event->register(new \wcf\system\endpoint\controller\core\interactions\GetBulkContextMenuOptions()); $event->register(new \wcf\system\endpoint\controller\core\interactions\GetContextMenuOptions()); $event->register(new \wcf\system\endpoint\controller\core\articles\DeleteArticle()); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserCoverPhoto.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserCoverPhoto.class.php new file mode 100644 index 0000000000..66682a9c32 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUserCoverPhoto.class.php @@ -0,0 +1,55 @@ + + * @since 6.3 + */ +#[PostRequest('/core/users/{id:\d+}/enable-cover-photo')] +final class EnableUserCoverPhoto implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $user = Helper::fetchObjectFromRequestParameter($variables['id'], User::class); + + $this->assertCoverPhotoCanBeEnabled($user); + + if ($user->disableCoverPhoto) { + (new EnableCoverPhoto($user))(); + } + + return new JsonResponse([]); + } + + private function assertCoverPhotoCanBeEnabled(User $user): void + { + if (WCF::getUser()->userID === $user->userID) { + throw new IllegalLinkException(); + } + if (!WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto')) { + throw new PermissionDeniedException(); + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + throw new PermissionDeniedException(); + } + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php index 34b11ee380..6432dc0ce9 100644 --- a/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php +++ b/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php @@ -5,6 +5,7 @@ use wcf\acp\form\UserEditForm; use wcf\action\UserBanAction; use wcf\action\UserDisableAvatarAction; +use wcf\action\UserDisableCoverPhotoAction; use wcf\action\UserDisableSignatureAction; use wcf\data\DatabaseObject; use wcf\data\user\group\UserGroup; @@ -74,6 +75,7 @@ static function (UserProfile $user): bool { }, ) ); + $this->addInteraction( new FormBuilderDialogInteraction( 'disable-avatar', @@ -116,6 +118,7 @@ static function (UserProfile $user): bool { }, ) ); + $this->addInteraction( new FormBuilderDialogInteraction( 'disable-signature', @@ -159,25 +162,49 @@ static function (UserProfile $user): bool { ) ); - if (WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto')) { - $this->addInteraction( - new class('disable-cover-photo', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { - #[\Override] - public function render(DatabaseObject $object): string - { - \assert($object instanceof UserProfile); - $title = WCF::getLanguage()->get($object->disableCoverPhoto ? 'wcf.user.enableCoverPhoto' : 'wcf.user.disableCoverPhoto'); + $this->addInteraction( + new FormBuilderDialogInteraction( + 'disable-cover-photo', + LinkHandler::getInstance()->getControllerLink(UserDisableCoverPhotoAction::class, [ + 'id' => '%s', + ]), + 'wcf.user.disableCoverPhoto', + static function (UserProfile $user): bool { + if (WCF::getUser()->userID === $user->userID) { + return false; + } + if (!WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto')) { + return false; + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + return false; + } - return <<{$title} - HTML; + return $user->disableCoverPhoto === 0; + }, + ) + ); + $this->addInteraction( + new RpcInteraction( + 'enable-cover-photo', + 'core/users/%s/enable-cover-photo', + 'wcf.user.enableCoverPhoto', + isAvailableCallback: static function (UserProfile $user): bool { + if (WCF::getUser()->userID === $user->userID) { + return false; } - } - ); - } + if (!WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto')) { + return false; + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + return false; + } + + return $user->disableCoverPhoto === 1; + }, + ) + ); + if (WCF::getSession()->getPermission('admin.user.canEnableUser')) { $this->addInteraction( new class('enable', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { diff --git a/wcfsetup/install/files/lib/system/user/command/DisableCoverPhoto.class.php b/wcfsetup/install/files/lib/system/user/command/DisableCoverPhoto.class.php new file mode 100644 index 0000000000..647a44d3c3 --- /dev/null +++ b/wcfsetup/install/files/lib/system/user/command/DisableCoverPhoto.class.php @@ -0,0 +1,34 @@ + + * @since 6.3 + */ +final class DisableCoverPhoto +{ + public function __construct( + private readonly User $user, + private readonly string $reason, + private readonly ?int $banExpires = null, + ) { + } + + public function __invoke(): void + { + $editor = new UserEditor($this->user); + $editor->update([ + 'disableCoverPhoto' => 1, + 'disableCoverPhotoReason' => $this->reason, + 'disableCoverPhotoExpires' => $this->banExpires ?? 0, + ]); + } +} diff --git a/wcfsetup/install/files/lib/system/user/command/EnableCoverPhoto.class.php b/wcfsetup/install/files/lib/system/user/command/EnableCoverPhoto.class.php new file mode 100644 index 0000000000..8289fe1329 --- /dev/null +++ b/wcfsetup/install/files/lib/system/user/command/EnableCoverPhoto.class.php @@ -0,0 +1,31 @@ + + * @since 6.3 + */ +final class EnableCoverPhoto +{ + public function __construct( + private readonly User $user, + ) { + } + + public function __invoke(): void + { + $editor = new UserEditor($this->user); + $editor->update([ + 'disableCoverPhoto' => 0, + 'disableCoverPhotoExpires' => 0, + ]); + } +} From 15c69503e6037af71b295c6a517182b19befe8e0 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 28 Jul 2025 18:03:21 +0200 Subject: [PATCH 07/19] Adding functions for enable/disable users via RPC endpoints --- .../files/lib/bootstrap/com.woltlab.wcf.php | 2 + .../core/users/DisableUser.class.php | 55 ++++++++++++++++++ .../core/users/EnableUser.class.php | 55 ++++++++++++++++++ .../user/UserManagementInteractions.class.php | 56 +++++++++++++------ 4 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/users/DisableUser.class.php create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUser.class.php diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index e7bb2a1da3..85180ea839 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -179,6 +179,8 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\users\EnableUserAvatar()); $event->register(new \wcf\system\endpoint\controller\core\users\EnableUserSignature()); $event->register(new \wcf\system\endpoint\controller\core\users\EnableUserCoverPhoto()); + $event->register(new \wcf\system\endpoint\controller\core\users\EnableUser()); + $event->register(new \wcf\system\endpoint\controller\core\users\DisableUser()); $event->register(new \wcf\system\endpoint\controller\core\interactions\GetBulkContextMenuOptions()); $event->register(new \wcf\system\endpoint\controller\core\interactions\GetContextMenuOptions()); $event->register(new \wcf\system\endpoint\controller\core\articles\DeleteArticle()); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/users/DisableUser.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/DisableUser.class.php new file mode 100644 index 0000000000..01fda88514 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/DisableUser.class.php @@ -0,0 +1,55 @@ + + * @since 6.3 + */ +#[PostRequest('/core/users/{id:\d+}/disable')] +final class DisableUser implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $user = Helper::fetchObjectFromRequestParameter($variables['id'], User::class); + + $this->assertUserCanBeDisabled($user); + + if (!$user->pendingActivation()) { + (new UserAction([$user], 'disable'))->executeAction(); + } + + return new JsonResponse([]); + } + + private function assertUserCanBeDisabled(User $user): void + { + if (WCF::getUser()->userID === $user->userID) { + throw new IllegalLinkException(); + } + if (!WCF::getSession()->getPermission('admin.user.canEnableUser')) { + throw new PermissionDeniedException(); + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + throw new PermissionDeniedException(); + } + } +} diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUser.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUser.class.php new file mode 100644 index 0000000000..5b9ef9b014 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/users/EnableUser.class.php @@ -0,0 +1,55 @@ + + * @since 6.3 + */ +#[PostRequest('/core/users/{id:\d+}/enable')] +final class EnableUser implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $user = Helper::fetchObjectFromRequestParameter($variables['id'], User::class); + + $this->assertUserCanBeEnabled($user); + + if ($user->pendingActivation()) { + (new UserAction([$user], 'enable'))->executeAction(); + } + + return new JsonResponse([]); + } + + private function assertUserCanBeEnabled(User $user): void + { + if (WCF::getUser()->userID === $user->userID) { + throw new IllegalLinkException(); + } + if (!WCF::getSession()->getPermission('admin.user.canEnableUser')) { + throw new PermissionDeniedException(); + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + throw new PermissionDeniedException(); + } + } +} diff --git a/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php index 6432dc0ce9..6cb7740c8f 100644 --- a/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php +++ b/wcfsetup/install/files/lib/system/interaction/user/UserManagementInteractions.class.php @@ -205,25 +205,47 @@ static function (UserProfile $user): bool { ) ); - if (WCF::getSession()->getPermission('admin.user.canEnableUser')) { - $this->addInteraction( - new class('enable', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { - #[\Override] - public function render(DatabaseObject $object): string - { - \assert($object instanceof UserProfile); - $title = WCF::getLanguage()->get($object->pendingActivation() ? 'wcf.acp.user.enable' : 'wcf.acp.user.disable'); + $this->addInteraction( + new RpcInteraction( + 'disable', + 'core/users/%s/disable', + 'wcf.acp.user.disable', + isAvailableCallback: static function (UserProfile $user): bool { + if (WCF::getUser()->userID === $user->userID) { + return false; + } + if (!WCF::getSession()->getPermission('admin.user.canEnableUser')) { + return false; + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + return false; + } - return <<{$title} - HTML; + return !$user->pendingActivation(); + }, + ) + ); + $this->addInteraction( + new RpcInteraction( + 'enable', + 'core/users/%s/enable', + 'wcf.acp.user.enable', + isAvailableCallback: static function (UserProfile $user): bool { + if (WCF::getUser()->userID === $user->userID) { + return false; } - } - ); - } + if (!WCF::getSession()->getPermission('admin.user.canEnableUser')) { + return false; + } + if (!UserGroup::isAccessibleGroup($user->getGroupIDs())) { + return false; + } + + return $user->pendingActivation(); + }, + ) + ); + if (WCF::getSession()->getPermission('admin.general.canUseAcp') && WCF::getSession()->getPermission('admin.user.canEditUser')) { $this->addInteraction( new class('edit', static fn (UserProfile $user) => UserGroup::isAccessibleGroup($user->getGroupIDs()) && WCF::getUser()->userID !== $user->userID) extends AbstractInteraction { From 4427cb1979e72006fb09d38f248a92486d81f32f Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 28 Jul 2025 18:10:39 +0200 Subject: [PATCH 08/19] Remove unused `Ui/User/Editor` --- com.woltlab.wcf/fileDelete.xml | 1 + com.woltlab.wcf/templates/user.tpl | 40 --- ts/WoltLabSuite/Core/Ui/User/Editor.ts | 270 ------------------ .../js/WoltLabSuite/Core/Ui/User/Editor.js | 224 --------------- .../user/UserManagementInteractions.class.php | 5 +- 5 files changed, 2 insertions(+), 538 deletions(-) delete mode 100644 ts/WoltLabSuite/Core/Ui/User/Editor.ts delete mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Editor.js diff --git a/com.woltlab.wcf/fileDelete.xml b/com.woltlab.wcf/fileDelete.xml index b75e6e2cbe..069f85ab84 100644 --- a/com.woltlab.wcf/fileDelete.xml +++ b/com.woltlab.wcf/fileDelete.xml @@ -572,5 +572,6 @@ icon/font-awesome/v6/brands/yoast.svg icon/font-awesome/v6/brands/youtube.svg icon/font-awesome/v6/brands/zhihu.svg + js/WoltLabSuite/Core/Ui/User/Editor.js diff --git a/com.woltlab.wcf/templates/user.tpl b/com.woltlab.wcf/templates/user.tpl index 10684c4cd8..2d3dfbd3a7 100644 --- a/com.woltlab.wcf/templates/user.tpl +++ b/com.woltlab.wcf/templates/user.tpl @@ -3,46 +3,6 @@ {capture assign='headContent'} {event name='javascriptInclude'}