From 45d74b9ce2464fa6d8f4bc699aa2ee223008f5f9 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 29 Jul 2025 16:42:18 +0200 Subject: [PATCH 1/4] Refactor command for copying user groups --- .../group/command/CopyUserGroup.class.php | 198 +++++++++++------- 1 file changed, 121 insertions(+), 77 deletions(-) diff --git a/wcfsetup/install/files/lib/system/user/group/command/CopyUserGroup.class.php b/wcfsetup/install/files/lib/system/user/group/command/CopyUserGroup.class.php index 0ee849fcb77..b8ab40657dd 100644 --- a/wcfsetup/install/files/lib/system/user/group/command/CopyUserGroup.class.php +++ b/wcfsetup/install/files/lib/system/user/group/command/CopyUserGroup.class.php @@ -13,7 +13,7 @@ * Copies a user group. * * @author Olaf Braun - * @copyright 2001-2024 WoltLab GmbH + * @copyright 2001-2025 WoltLab GmbH * @license GNU Lesser General Public License */ final class CopyUserGroup @@ -22,53 +22,39 @@ public function __construct( public readonly UserGroup $userGroup, public readonly bool $copyUserGroupOptions, public readonly bool $copyMembers, - public readonly bool $copyACLOptions - ) { - } + public readonly bool $copyACLOptions, + ) {} public function __invoke(): UserGroup { - // fetch user group option values if ($this->copyUserGroupOptions) { - $sql = "SELECT optionID, optionValue - FROM wcf1_user_group_option_value - WHERE groupID = ?"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute([$this->userGroup->groupID]); + $optionValues = $this->getOptionValues($this->userGroup); } else { - $sql = "SELECT optionID, defaultValue AS optionValue - FROM wcf1_user_group_option"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute(); + $optionValues = $this->getDefaultOptionValues(); } - $optionValues = $statement->fetchMap('optionID', 'optionValue'); + $group = $this->copyUserGroup($this->userGroup, $optionValues); + $groupEditor = new UserGroupEditor($group); + $groupName = $this->updateGroupName($this->userGroup, $group); + $groupDescription = $this->updateGroupDescription($this->userGroup, $group); + $groupEditor->update([ + 'groupDescription' => $groupDescription, + 'groupName' => $groupName, + ]); - $groupType = $this->userGroup->groupType; - // When copying special user groups of which only one may exist, - // change the group type to 'other'. - if (\in_array($groupType, [UserGroup::EVERYONE, UserGroup::GUESTS, UserGroup::USERS, UserGroup::OWNER])) { - $groupType = UserGroup::OTHER; - } + $this->copyMembers($this->userGroup, $group); + $this->copyACLOptions($this->userGroup, $group); - /** @var UserGroup $group */ - $group = (new UserGroupAction([], 'create', [ - 'data' => [ - 'groupName' => $this->userGroup->groupName, - 'groupDescription' => $this->userGroup->groupDescription, - 'priority' => $this->userGroup->priority, - 'userOnlineMarking' => $this->userGroup->userOnlineMarking, - 'showOnTeamPage' => $this->userGroup->showOnTeamPage, - 'groupType' => $groupType, - ], - 'options' => $optionValues, - ]))->executeAction()['returnValues']; - $groupEditor = new UserGroupEditor($group); + LanguageFactory::getInstance()->deleteLanguageCache(); + UserGroupEditor::resetCache(); + + return $group; + } - // update group name - $groupName = $this->userGroup->groupName; - if (\preg_match('~^wcf\.acp\.group\.group\d+$~', $this->userGroup->groupName)) { - $groupName = 'wcf.acp.group.group' . $group->groupID; + private function updateGroupName(UserGroup $oldGroup, UserGroup $newGroup): string + { + if (\preg_match('~^wcf\.acp\.group\.group\d+$~', $oldGroup->groupName)) { + $groupName = 'wcf.acp.group.group' . $newGroup->groupID; // create group name language item $sql = "INSERT INTO wcf1_language_item @@ -77,15 +63,18 @@ public function __invoke(): UserGroup FROM wcf1_language_item WHERE languageItem = ?"; $statement = WCF::getDB()->prepare($sql); - $statement->execute([$this->userGroup->groupName]); + $statement->execute([$oldGroup->groupName]); } else { - $groupName .= ' (2)'; + $groupName = $oldGroup->groupName . ' (2)'; } - // update group name - $groupDescription = $this->userGroup->groupName; - if (\preg_match('~^wcf\.acp\.group\.groupDescription\d+$~', $this->userGroup->groupDescription)) { - $groupDescription = 'wcf.acp.group.groupDescription' . $group->groupID; + return $groupName; + } + + private function updateGroupDescription(UserGroup $oldGroup, UserGroup $newGroup): string + { + if (\preg_match('~^wcf\.acp\.group\.groupDescription\d+$~', $oldGroup->groupDescription)) { + $groupDescription = 'wcf.acp.group.groupDescription' . $newGroup->groupID; // create group name language item $sql = "INSERT INTO wcf1_language_item @@ -94,48 +83,103 @@ public function __invoke(): UserGroup FROM wcf1_language_item WHERE languageItem = ?"; $statement = WCF::getDB()->prepare($sql); - $statement->execute([$this->userGroup->groupDescription]); + $statement->execute([$oldGroup->groupDescription]); + } else { + $groupDescription = $oldGroup->groupDescription; } - $groupEditor->update([ - 'groupDescription' => $groupDescription, - 'groupName' => $groupName, - ]); + return $groupDescription; + } - // copy members - if ($this->copyMembers) { - $sql = "INSERT INTO wcf1_user_to_group - (userID, groupID) - SELECT userID, " . $group->groupID . " - FROM wcf1_user_to_group - WHERE groupID = ?"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute([$this->userGroup->groupID]); + private function copyMembers(UserGroup $oldGroup, UserGroup $newGroup): void + { + if (!$this->copyMembers) { + return; } - // copy acl options - if ($this->copyACLOptions) { - $sql = "INSERT INTO wcf1_acl_option_to_group - (optionID, objectID, groupID, optionValue) - SELECT optionID, objectID, " . $group->groupID . ", optionValue - FROM wcf1_acl_option_to_group - WHERE groupID = ?"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute([$this->userGroup->groupID]); - - // it is likely that applications or plugins use caches - // for acl option values like for the labels which have - // to be renewed after copying the acl options; because - // there is no other way to delete these caches, we simply - // delete all caches - CacheHandler::getInstance()->flushAll(); + $sql = "INSERT INTO wcf1_user_to_group + (userID, groupID) + SELECT userID, " . $newGroup->groupID . " + FROM wcf1_user_to_group + WHERE groupID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([$oldGroup->groupID]); + } + + private function copyACLOptions(UserGroup $oldGroup, UserGroup $newGroup): void + { + if (!$this->copyACLOptions) { + return; } - // reset language cache - LanguageFactory::getInstance()->deleteLanguageCache(); + $sql = "INSERT INTO wcf1_acl_option_to_group + (optionID, objectID, groupID, optionValue) + SELECT optionID, objectID, " . $newGroup->groupID . ", optionValue + FROM wcf1_acl_option_to_group + WHERE groupID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([$oldGroup->groupID]); + + // it is likely that applications or plugins use caches + // for acl option values like for the labels which have + // to be renewed after copying the acl options; because + // there is no other way to delete these caches, we simply + // delete all caches + CacheHandler::getInstance()->flushAll(); + } - UserGroupEditor::resetCache(); + /** + * @param array $optionValues + */ + private function copyUserGroup(UserGroup $group, array $optionValues): UserGroup + { + $groupType = $group->groupType; + // When copying special user groups of which only one may exist, + // change the group type to 'other'. + if (\in_array($groupType, [UserGroup::EVERYONE, UserGroup::GUESTS, UserGroup::USERS, UserGroup::OWNER])) { + $groupType = UserGroup::OTHER; + } + + $group = (new UserGroupAction([], 'create', [ + 'data' => [ + 'groupName' => $group->groupName, + 'groupDescription' => $group->groupDescription, + 'priority' => $group->priority, + 'userOnlineMarking' => $group->userOnlineMarking, + 'showOnTeamPage' => $group->showOnTeamPage, + 'groupType' => $groupType, + ], + 'options' => $optionValues, + ]))->executeAction()['returnValues']; + \assert($group instanceof UserGroup); return $group; } + + /** + * @return array + */ + private function getOptionValues(UserGroup $group): array + { + $sql = "SELECT optionID, optionValue + FROM wcf1_user_group_option_value + WHERE groupID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([$group->groupID]); + + return $statement->fetchMap('optionID', 'optionValue'); + } + + /** + * @return array + */ + private function getDefaultOptionValues(): array + { + $sql = "SELECT optionID, defaultValue AS optionValue + FROM wcf1_user_group_option"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute(); + + return $statement->fetchMap('optionID', 'optionValue'); + } } From 91017fb4af6e0ab6bd7ba5581b37b48c5efd74d3 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 29 Jul 2025 17:07:29 +0200 Subject: [PATCH 2/4] Re-add old copy method for backward compatibility --- .../data/user/group/UserGroupAction.class.php | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/wcfsetup/install/files/lib/data/user/group/UserGroupAction.class.php b/wcfsetup/install/files/lib/data/user/group/UserGroupAction.class.php index ecf053f1b83..150d106ec7c 100644 --- a/wcfsetup/install/files/lib/data/user/group/UserGroupAction.class.php +++ b/wcfsetup/install/files/lib/data/user/group/UserGroupAction.class.php @@ -3,6 +3,10 @@ namespace wcf\data\user\group; use wcf\data\AbstractDatabaseObjectAction; +use wcf\system\exception\PermissionDeniedException; +use wcf\system\request\LinkHandler; +use wcf\system\user\group\command\CopyUserGroup; +use wcf\system\WCF; /** * Executes user group-related actions. @@ -20,6 +24,12 @@ class UserGroupAction extends AbstractDatabaseObjectAction */ public $className = UserGroupEditor::class; + /** + * editor object for the copied user group + * @var UserGroupEditor + */ + public $groupEditor; + /** * @inheritDoc */ @@ -38,7 +48,7 @@ class UserGroupAction extends AbstractDatabaseObjectAction /** * @inheritDoc */ - protected $requireACP = ['create', 'delete', 'update']; + protected $requireACP = ['copy', 'create', 'delete', 'update']; /** * @inheritDoc @@ -71,4 +81,47 @@ public function update() $object->updateGroupOptions($this->parameters['options']); } } + + /** + * Validates the 'copy' action. + * @deprecated 6.2 Use `CopyUserGroup` instead. + */ + public function validateCopy() + { + WCF::getSession()->checkPermissions([ + 'admin.user.canAddGroup', + 'admin.user.canEditGroup', + ]); + + $this->readBoolean('copyACLOptions'); + $this->readBoolean('copyMembers'); + $this->readBoolean('copyUserGroupOptions'); + + $this->groupEditor = $this->getSingleObject(); + if (!$this->groupEditor->canCopy()) { + throw new PermissionDeniedException(); + } + } + + /** + * Copies a user group. + * @deprecated 6.2 Use `CopyUserGroup` instead. + */ + public function copy() + { + $command = new CopyUserGroup( + $this->groupEditor->getDecoratedObject(), + $this->parameters['copyUserGroupOptions'], + $this->parameters['copyMembers'], + $this->parameters['copyACLOptions'] + ); + $group = $command(); + + return [ + 'groupID' => $group->groupID, + 'redirectURL' => LinkHandler::getInstance()->getLink('UserGroupEdit', [ + 'id' => $group->groupID, + ]), + ]; + } } From cb9c30e5e52b76ff412d5040d8d9936fd7bea405 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 29 Jul 2025 17:15:26 +0200 Subject: [PATCH 3/4] Remove obsolete language variable --- wcfsetup/install/lang/de.xml | 1 - wcfsetup/install/lang/en.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 7becf2f4799..27f7d4680b2 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -877,7 +877,6 @@ Sie erreichen das Fehlerprotokoll unter: {link controller='ExceptionLogView' isE - {$group->getName()} wirklich kopieren?]]> diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index c7c65a30999..be0ed65f21b 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -853,7 +853,6 @@ You can access the error log at: {link controller='ExceptionLogView' isEmail=tru - {$group->getName()}?]]> From 80819f009400c5f9887f3050bab829e41adbd65a Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 29 Jul 2025 17:33:20 +0200 Subject: [PATCH 4/4] Migrate user group copy to `FormBuilderDialogInteraction` --- .../Core/Acp/Component/User/Group/Copy.ts | 28 ------------------- .../Interaction/FormBuilderDialog.ts | 5 +++- .../files/acp/templates/userGroupAdd.tpl | 22 ++++++++------- .../Core/Acp/Component/User/Group/Copy.js | 25 ----------------- .../Interaction/FormBuilderDialog.js | 5 +++- .../acp/action/UserGroupCopyAction.class.php | 2 +- .../admin/UserGroupInteractions.class.php | 15 +++++++++- wcfsetup/install/lang/de.xml | 3 +- wcfsetup/install/lang/en.xml | 3 +- 9 files changed, 39 insertions(+), 69 deletions(-) delete mode 100644 ts/WoltLabSuite/Core/Acp/Component/User/Group/Copy.ts delete mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Component/User/Group/Copy.js diff --git a/ts/WoltLabSuite/Core/Acp/Component/User/Group/Copy.ts b/ts/WoltLabSuite/Core/Acp/Component/User/Group/Copy.ts deleted file mode 100644 index 45866e4ad70..00000000000 --- a/ts/WoltLabSuite/Core/Acp/Component/User/Group/Copy.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Handles the dialog to copy a user group. - * - * @author Olaf Braun - * @copyright 2001-2024 WoltLab GmbH - * @license GNU Lesser General Public License - */ - -import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog"; - -interface CopyResponse { - groupID: number; - redirectURL: string; -} - -export function init() { - const button = document.querySelector(".jsButtonUserGroupCopy"); - button?.addEventListener("click", () => { - void dialogFactory() - .usingFormBuilder() - .fromEndpoint(button.dataset.endpoint!) - .then((result) => { - if (result.ok) { - window.location.href = result.result.redirectURL; - } - }); - }); -} diff --git a/ts/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.ts b/ts/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.ts index c48f6e2b811..9e50bb6d84a 100644 --- a/ts/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.ts +++ b/ts/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.ts @@ -20,7 +20,7 @@ async function handleFormBuilderDialogAction( interactionEffect: InteractionEffect = InteractionEffect.ReloadItem, detail: Payload, ): Promise { - const { ok } = await dialogFactory().usingFormBuilder().fromEndpoint(endpoint); + const { ok, result } = await dialogFactory().usingFormBuilder().fromEndpoint(endpoint); if (!ok) { return; @@ -32,6 +32,7 @@ async function handleFormBuilderDialogAction( bubbles: true, detail: { ...detail, + ...result, _reloadPage: String(interactionEffect === InteractionEffect.ReloadPage), }, }), @@ -41,6 +42,7 @@ async function handleFormBuilderDialogAction( new CustomEvent("interaction:invalidate-all", { detail: { ...detail, + ...result, }, }), ); @@ -50,6 +52,7 @@ async function handleFormBuilderDialogAction( bubbles: true, detail: { ...detail, + ...result, }, }), ); diff --git a/wcfsetup/install/files/acp/templates/userGroupAdd.tpl b/wcfsetup/install/files/acp/templates/userGroupAdd.tpl index 34408f14cc6..714054d75ab 100644 --- a/wcfsetup/install/files/acp/templates/userGroupAdd.tpl +++ b/wcfsetup/install/files/acp/templates/userGroupAdd.tpl @@ -1,12 +1,6 @@ {include file='header' pageTitle='wcf.acp.group.'|concat:$action} {/if} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Component/User/Group/Copy.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Component/User/Group/Copy.js deleted file mode 100644 index 0fca6a79f2b..00000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Component/User/Group/Copy.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Handles the dialog to copy a user group. - * - * @author Olaf Braun - * @copyright 2001-2024 WoltLab GmbH - * @license GNU Lesser General Public License - */ -define(["require", "exports", "WoltLabSuite/Core/Component/Dialog"], function (require, exports, Dialog_1) { - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); - exports.init = init; - function init() { - const button = document.querySelector(".jsButtonUserGroupCopy"); - button?.addEventListener("click", () => { - void (0, Dialog_1.dialogFactory)() - .usingFormBuilder() - .fromEndpoint(button.dataset.endpoint) - .then((result) => { - if (result.ok) { - window.location.href = result.result.redirectURL; - } - }); - }); - } -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.js index 070ec5abb7f..44996918670 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/FormBuilderDialog.js @@ -11,7 +11,7 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuit Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; async function handleFormBuilderDialogAction(container, element, endpoint, interactionEffect = InteractionEffect_1.InteractionEffect.ReloadItem, detail) { - const { ok } = await (0, Dialog_1.dialogFactory)().usingFormBuilder().fromEndpoint(endpoint); + const { ok, result } = await (0, Dialog_1.dialogFactory)().usingFormBuilder().fromEndpoint(endpoint); if (!ok) { return; } @@ -20,6 +20,7 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuit bubbles: true, detail: { ...detail, + ...result, _reloadPage: String(interactionEffect === InteractionEffect_1.InteractionEffect.ReloadPage), }, })); @@ -28,6 +29,7 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuit container.dispatchEvent(new CustomEvent("interaction:invalidate-all", { detail: { ...detail, + ...result, }, })); } @@ -36,6 +38,7 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuit bubbles: true, detail: { ...detail, + ...result, }, })); } diff --git a/wcfsetup/install/files/lib/acp/action/UserGroupCopyAction.class.php b/wcfsetup/install/files/lib/acp/action/UserGroupCopyAction.class.php index 68557d5ba2b..a77b10ab0fb 100644 --- a/wcfsetup/install/files/lib/acp/action/UserGroupCopyAction.class.php +++ b/wcfsetup/install/files/lib/acp/action/UserGroupCopyAction.class.php @@ -89,7 +89,7 @@ private function getForm(): Psr15DialogForm { $form = new Psr15DialogForm( UserGroupCopyAction::class, - WCF::getLanguage()->get('wcf.acp.dashboard.configure') + WCF::getLanguage()->get('wcf.acp.group.copy') ); $form->appendChildren([ FormContainer::create('section') diff --git a/wcfsetup/install/files/lib/system/interaction/admin/UserGroupInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/admin/UserGroupInteractions.class.php index 39933891adf..d2e223adad1 100644 --- a/wcfsetup/install/files/lib/system/interaction/admin/UserGroupInteractions.class.php +++ b/wcfsetup/install/files/lib/system/interaction/admin/UserGroupInteractions.class.php @@ -2,11 +2,15 @@ namespace wcf\system\interaction\admin; +use wcf\acp\action\UserGroupCopyAction; use wcf\data\user\group\UserGroup; use wcf\event\interaction\admin\UserGroupInteractionCollecting; use wcf\system\event\EventHandler; use wcf\system\interaction\AbstractInteractionProvider; use wcf\system\interaction\DeleteInteraction; +use wcf\system\interaction\FormBuilderDialogInteraction; +use wcf\system\interaction\InteractionEffect; +use wcf\system\request\LinkHandler; /** * Interaction provider for user groups. @@ -21,7 +25,16 @@ final class UserGroupInteractions extends AbstractInteractionProvider public function __construct() { $this->addInteractions([ - new DeleteInteraction("core/users/groups/%s", static fn(UserGroup $group) => $group->isDeletable()) + new DeleteInteraction("core/users/groups/%s", static fn(UserGroup $group) => $group->isDeletable()), + new FormBuilderDialogInteraction( + 'copy', + LinkHandler::getInstance()->getControllerLink(UserGroupCopyAction::class, ['id' => '%s']), + 'wcf.acp.group.button.copy', + static function (UserGroup $group): bool { + return $group->canCopy(); + }, + InteractionEffect::ReloadList + ) ]); EventHandler::getInstance()->fire( diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 27f7d4680b2..641303d4862 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -876,7 +876,8 @@ Sie erreichen das Fehlerprotokoll unter: {link controller='ExceptionLogView' isE - + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index be0ed65f21b..1e35d6471cd 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -852,7 +852,8 @@ You can access the error log at: {link controller='ExceptionLogView' isEmail=tru - + +