From 84ceb3297a8f57efd5e3bda73e1f9029018b2807 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 22 Sep 2026 16:35:34 +0200 Subject: [PATCH 1/3] Show implicitly deleted objects in the delete confirmation --- .../Core/Component/Confirmation.ts | 12 ++++++-- .../Core/Component/Interaction/Bulk/Rpc.ts | 6 ++-- .../Component/Interaction/Confirmation.ts | 24 ++++++++++++++- .../Core/Component/Interaction/Rpc.ts | 11 +++++-- .../Core/Component/Confirmation.js | 12 ++++++-- .../Core/Component/Interaction/Bulk/Rpc.js | 6 ++-- .../Component/Interaction/Confirmation.js | 24 +++++++++++++-- .../Core/Component/Interaction/Rpc.js | 6 ++-- ...PreloadPhrasesCollectingListener.class.php | 1 + .../interaction/DeleteInteraction.class.php | 21 +++++++++++++- .../interaction/RpcInteraction.class.php | 25 +++++++++++++++- .../admin/LabelGroupInteractions.class.php | 5 +++- .../bulk/BulkDeleteInteraction.class.php | 19 +++++++++++- .../bulk/BulkRpcInteraction.class.php | 29 +++++++++++++++++-- wcfsetup/install/lang/de.xml | 1 + wcfsetup/install/lang/en.xml | 1 + 16 files changed, 179 insertions(+), 24 deletions(-) diff --git a/ts/WoltLabSuite/Core/Component/Confirmation.ts b/ts/WoltLabSuite/Core/Component/Confirmation.ts index e2fd1945b61..344ee817825 100644 --- a/ts/WoltLabSuite/Core/Component/Confirmation.ts +++ b/ts/WoltLabSuite/Core/Component/Confirmation.ts @@ -26,6 +26,7 @@ import { dialogFactory } from "./Dialog"; import { getPhrase } from "../Language"; import * as DomUtil from "../Dom/Util"; import { ConfirmationCustom } from "./Confirmation/Custom"; +import { escapeHTML } from "../StringUtil"; type ResultSoftDeleteWithoutReason = { result: boolean; @@ -40,8 +41,15 @@ class ConfirmationPrefab { return new ConfirmationCustom(question); } - async delete(title?: string): Promise { - const html = `

${getPhrase("wcf.dialog.confirmation.cannotBeUndone")}

`; + async delete(title?: string, affectedObjects: string[] = []): Promise { + let html = ""; + if (affectedObjects.length > 0) { + const items = affectedObjects.map((name) => `
  • ${escapeHTML(name)}
  • `).join(""); + html += `

    ${getPhrase("wcf.dialog.confirmation.delete.affectedObjects")}

    `; + html += `
      ${items}
    `; + } + html += `

    ${getPhrase("wcf.dialog.confirmation.cannotBeUndone")}

    `; + const dialog = dialogFactory() .fromHtml(html) .asConfirmation({ diff --git a/ts/WoltLabSuite/Core/Component/Interaction/Bulk/Rpc.ts b/ts/WoltLabSuite/Core/Component/Interaction/Bulk/Rpc.ts index ca76359dce1..a0763ef28a5 100644 --- a/ts/WoltLabSuite/Core/Component/Interaction/Bulk/Rpc.ts +++ b/ts/WoltLabSuite/Core/Component/Interaction/Bulk/Rpc.ts @@ -9,7 +9,7 @@ import { deleteObject } from "WoltLabSuite/Core/Api/DeleteObject"; import { postObject } from "WoltLabSuite/Core/Api/PostObject"; -import { ConfirmationType, handleConfirmation } from "../Confirmation"; +import { ConfirmationType, handleConfirmation, parseAffectedObjects } from "../Confirmation"; import { showProgressSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; async function handleRpcInteraction( @@ -19,8 +19,9 @@ async function handleRpcInteraction( label: string, confirmationType: ConfirmationType, customConfirmationMessage: string = "", + affectedObjects: string[] = [], ): Promise { - const confirmationResult = await handleConfirmation("", confirmationType, customConfirmationMessage); + const confirmationResult = await handleConfirmation("", confirmationType, customConfirmationMessage, affectedObjects); if (!confirmationResult.result) { return; } @@ -75,6 +76,7 @@ export function setup(identifier: string, container: HTMLElement): void { event.detail.label, event.detail.confirmationType, event.detail.confirmationMessage, + parseAffectedObjects(event.detail.affectedObjects), ); } }); diff --git a/ts/WoltLabSuite/Core/Component/Interaction/Confirmation.ts b/ts/WoltLabSuite/Core/Component/Interaction/Confirmation.ts index cedc650d658..e06160c80d9 100644 --- a/ts/WoltLabSuite/Core/Component/Interaction/Confirmation.ts +++ b/ts/WoltLabSuite/Core/Component/Interaction/Confirmation.ts @@ -24,10 +24,32 @@ type ResultConfirmationWithReason = { reason?: string; }; +/** + * Parses the JSON encoded list of affected objects from a data attribute. Absent or + * malformed values yield an empty list, the confirmation is then shown without them. + */ +export function parseAffectedObjects(value: string | undefined): string[] { + if (!value) { + return []; + } + + try { + const result: unknown = JSON.parse(value); + if (Array.isArray(result)) { + return result.filter((item): item is string => typeof item === "string"); + } + } catch { + // Ignore malformed values. + } + + return []; +} + export async function handleConfirmation( objectName: string, confirmationType: ConfirmationType, customMessage: string = "", + affectedObjects: string[] = [], ): Promise { if (confirmationType == ConfirmationType.SoftDelete) { return await confirmationFactory().softDelete(objectName ? objectName : undefined); @@ -45,7 +67,7 @@ export async function handleConfirmation( if (confirmationType == ConfirmationType.Delete) { return { - result: await confirmationFactory().delete(objectName ? objectName : undefined), + result: await confirmationFactory().delete(objectName ? objectName : undefined, affectedObjects), }; } diff --git a/ts/WoltLabSuite/Core/Component/Interaction/Rpc.ts b/ts/WoltLabSuite/Core/Component/Interaction/Rpc.ts index 813b714e251..38ecb983907 100644 --- a/ts/WoltLabSuite/Core/Component/Interaction/Rpc.ts +++ b/ts/WoltLabSuite/Core/Component/Interaction/Rpc.ts @@ -9,7 +9,7 @@ import { deleteObject } from "WoltLabSuite/Core/Api/DeleteObject"; import { postObject } from "WoltLabSuite/Core/Api/PostObject"; -import { ConfirmationType, handleConfirmation } from "./Confirmation"; +import { ConfirmationType, handleConfirmation, parseAffectedObjects } from "./Confirmation"; import { showDefaultSuccessSnackbar, showSuccessSnackbar } from "WoltLabSuite/Core/Component/Snackbar"; import { getPhrase } from "WoltLabSuite/Core/Language"; import { InteractionEffect } from "./InteractionEffect"; @@ -24,9 +24,15 @@ async function handleRpcInteraction( confirmationType: ConfirmationType, customConfirmationMessage: string = "", interactionEffect: InteractionEffect = InteractionEffect.ReloadItem, + affectedObjects: string[] = [], detail: Payload, ): Promise { - const confirmationResult = await handleConfirmation(objectName, confirmationType, customConfirmationMessage); + const confirmationResult = await handleConfirmation( + objectName, + confirmationType, + customConfirmationMessage, + affectedObjects, + ); if (!confirmationResult.result) { return; } @@ -78,6 +84,7 @@ export function setup(identifier: string, container: HTMLElement): void { event.detail.confirmationType as ConfirmationType, event.detail.confirmationMessage, event.detail.interactionEffect as InteractionEffect, + parseAffectedObjects(event.detail.affectedObjects), event.detail, ); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Confirmation.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Confirmation.js index 835fb02cb1e..e964e6a9211 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Confirmation.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Confirmation.js @@ -21,7 +21,7 @@ * @license GNU Lesser General Public License * @since 6.0 */ -define(["require", "exports", "tslib", "./Dialog", "../Language", "../Dom/Util", "./Confirmation/Custom"], function (require, exports, tslib_1, Dialog_1, Language_1, DomUtil, Custom_1) { +define(["require", "exports", "tslib", "./Dialog", "../Language", "../Dom/Util", "./Confirmation/Custom", "../StringUtil"], function (require, exports, tslib_1, Dialog_1, Language_1, DomUtil, Custom_1, StringUtil_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.confirmationFactory = confirmationFactory; @@ -30,8 +30,14 @@ define(["require", "exports", "tslib", "./Dialog", "../Language", "../Dom/Util", custom(question) { return new Custom_1.ConfirmationCustom(question); } - async delete(title) { - const html = `

    ${(0, Language_1.getPhrase)("wcf.dialog.confirmation.cannotBeUndone")}

    `; + async delete(title, affectedObjects = []) { + let html = ""; + if (affectedObjects.length > 0) { + const items = affectedObjects.map((name) => `
  • ${(0, StringUtil_1.escapeHTML)(name)}
  • `).join(""); + html += `

    ${(0, Language_1.getPhrase)("wcf.dialog.confirmation.delete.affectedObjects")}

    `; + html += `
      ${items}
    `; + } + html += `

    ${(0, Language_1.getPhrase)("wcf.dialog.confirmation.cannotBeUndone")}

    `; const dialog = (0, Dialog_1.dialogFactory)() .fromHtml(html) .asConfirmation({ diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Bulk/Rpc.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Bulk/Rpc.js index 38cf6616162..fc7b57ceb30 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Bulk/Rpc.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Bulk/Rpc.js @@ -10,8 +10,8 @@ define(["require", "exports", "WoltLabSuite/Core/Api/DeleteObject", "WoltLabSuit "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; - async function handleRpcInteraction(container, objectIds, endpoint, label, confirmationType, customConfirmationMessage = "") { - const confirmationResult = await (0, Confirmation_1.handleConfirmation)("", confirmationType, customConfirmationMessage); + async function handleRpcInteraction(container, objectIds, endpoint, label, confirmationType, customConfirmationMessage = "", affectedObjects = []) { + const confirmationResult = await (0, Confirmation_1.handleConfirmation)("", confirmationType, customConfirmationMessage, affectedObjects); if (!confirmationResult.result) { return; } @@ -47,7 +47,7 @@ define(["require", "exports", "WoltLabSuite/Core/Api/DeleteObject", "WoltLabSuit function setup(identifier, container) { container.addEventListener("bulk-interaction", (event) => { if (event.detail.bulkInteraction === identifier) { - void handleRpcInteraction(container, JSON.parse(event.detail.objectIds), event.detail.endpoint, event.detail.label, event.detail.confirmationType, event.detail.confirmationMessage); + void handleRpcInteraction(container, JSON.parse(event.detail.objectIds), event.detail.endpoint, event.detail.label, event.detail.confirmationType, event.detail.confirmationMessage, (0, Confirmation_1.parseAffectedObjects)(event.detail.affectedObjects)); } }); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Confirmation.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Confirmation.js index dd224e14b81..e336b03685d 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Confirmation.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Confirmation.js @@ -10,6 +10,7 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Confirmation"], funct "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfirmationType = void 0; + exports.parseAffectedObjects = parseAffectedObjects; exports.handleConfirmation = handleConfirmation; var ConfirmationType; (function (ConfirmationType) { @@ -21,7 +22,26 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Confirmation"], funct ConfirmationType["Disable"] = "Disable"; ConfirmationType["Custom"] = "Custom"; })(ConfirmationType || (exports.ConfirmationType = ConfirmationType = {})); - async function handleConfirmation(objectName, confirmationType, customMessage = "") { + /** + * Parses the JSON encoded list of affected objects from a data attribute. Absent or + * malformed values yield an empty list, the confirmation is then shown without them. + */ + function parseAffectedObjects(value) { + if (!value) { + return []; + } + try { + const result = JSON.parse(value); + if (Array.isArray(result)) { + return result.filter((item) => typeof item === "string"); + } + } + catch { + // Ignore malformed values. + } + return []; + } + async function handleConfirmation(objectName, confirmationType, customMessage = "", affectedObjects = []) { if (confirmationType == ConfirmationType.SoftDelete) { return await (0, Confirmation_1.confirmationFactory)().softDelete(objectName ? objectName : undefined); } @@ -35,7 +55,7 @@ define(["require", "exports", "WoltLabSuite/Core/Component/Confirmation"], funct } if (confirmationType == ConfirmationType.Delete) { return { - result: await (0, Confirmation_1.confirmationFactory)().delete(objectName ? objectName : undefined), + result: await (0, Confirmation_1.confirmationFactory)().delete(objectName ? objectName : undefined, affectedObjects), }; } if (confirmationType == ConfirmationType.Disable) { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Rpc.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Rpc.js index e67d466fb56..54ed088004e 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Rpc.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Interaction/Rpc.js @@ -10,8 +10,8 @@ define(["require", "exports", "WoltLabSuite/Core/Api/DeleteObject", "WoltLabSuit "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; - async function handleRpcInteraction(container, element, objectName, endpoint, confirmationType, customConfirmationMessage = "", interactionEffect = InteractionEffect_1.InteractionEffect.ReloadItem, detail) { - const confirmationResult = await (0, Confirmation_1.handleConfirmation)(objectName, confirmationType, customConfirmationMessage); + async function handleRpcInteraction(container, element, objectName, endpoint, confirmationType, customConfirmationMessage = "", interactionEffect = InteractionEffect_1.InteractionEffect.ReloadItem, affectedObjects = [], detail) { + const confirmationResult = await (0, Confirmation_1.handleConfirmation)(objectName, confirmationType, customConfirmationMessage, affectedObjects); if (!confirmationResult.result) { return; } @@ -49,7 +49,7 @@ define(["require", "exports", "WoltLabSuite/Core/Api/DeleteObject", "WoltLabSuit function setup(identifier, container) { container.addEventListener("interaction:execute", (event) => { if (event.detail.interaction === identifier) { - void handleRpcInteraction(container, event.target, event.detail.objectName, event.detail.endpoint, event.detail.confirmationType, event.detail.confirmationMessage, event.detail.interactionEffect, event.detail); + void handleRpcInteraction(container, event.target, event.detail.objectName, event.detail.endpoint, event.detail.confirmationType, event.detail.confirmationMessage, event.detail.interactionEffect, (0, Confirmation_1.parseAffectedObjects)(event.detail.affectedObjects), event.detail); } }); } diff --git a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php index 734b0efb299..02da50bc0ee 100644 --- a/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php +++ b/wcfsetup/install/files/lib/system/event/listener/PreloadPhrasesCollectingListener.class.php @@ -50,6 +50,7 @@ public function __invoke(PreloadPhrasesCollecting $event): void $event->preload('wcf.dialog.button.primary.submit'); $event->preload('wcf.dialog.confirmation.cannotBeUndone'); $event->preload('wcf.dialog.confirmation.delete'); + $event->preload('wcf.dialog.confirmation.delete.affectedObjects'); $event->preload('wcf.dialog.confirmation.delete.indeterminate'); $event->preload('wcf.dialog.confirmation.disable'); $event->preload('wcf.dialog.confirmation.disable.indeterminate'); diff --git a/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php index eb188eb9ea2..86c36b622d1 100644 --- a/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php +++ b/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php @@ -2,6 +2,8 @@ namespace wcf\system\interaction; +use wcf\data\DatabaseObject; + /** * Represents a delete interaction. * @@ -12,9 +14,14 @@ */ class DeleteInteraction extends RpcInteraction { + /** + * @param list $affectedObjects Names of the objects that are implicitly deleted + * along with the object itself. + */ public function __construct( string $endpoint, - ?\Closure $isAvailableCallback = null + ?\Closure $isAvailableCallback = null, + private readonly array $affectedObjects = [] ) { parent::__construct( 'delete', @@ -26,4 +33,16 @@ public function __construct( InteractionEffect::RemoveItem ); } + + #[\Override] + protected function getAdditionalDataAttributes(DatabaseObject $object): array + { + if ($this->affectedObjects === []) { + return []; + } + + return [ + 'data-affected-objects' => \json_encode($this->affectedObjects, \JSON_THROW_ON_ERROR), + ]; + } } diff --git a/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php index 6c756fc0e51..888a78b323a 100644 --- a/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php +++ b/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php @@ -48,6 +48,7 @@ public function render(DatabaseObject $object): string } else { $confirmationMessage = ($this->confirmationMessage)($object); } + $confirmationMessage = StringUtil::encodeHTML($confirmationMessage); $endpoint = StringUtil::encodeHTML( LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) . @@ -68,6 +69,15 @@ public function render(DatabaseObject $object): string } } + $additionalAttributes = ''; + foreach ($this->getAdditionalDataAttributes($object) as $name => $value) { + $additionalAttributes .= \sprintf( + ' %s="%s"', + StringUtil::encodeHTML($name), + StringUtil::encodeHTML($value) + ); + } + return << {$label} HTML; } + /** + * Returns additional data attributes that are added to the rendered button. + * + * The returned names and values are HTML encoded by the caller. + * + * @return array + * @since 6.3 + */ + protected function getAdditionalDataAttributes(DatabaseObject $object): array + { + return []; + } + #[\Override] public function renderInitialization(string $containerId): string { diff --git a/wcfsetup/install/files/lib/system/interaction/admin/LabelGroupInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/admin/LabelGroupInteractions.class.php index 005c9f4354e..6a5cb6e7f14 100644 --- a/wcfsetup/install/files/lib/system/interaction/admin/LabelGroupInteractions.class.php +++ b/wcfsetup/install/files/lib/system/interaction/admin/LabelGroupInteractions.class.php @@ -31,7 +31,10 @@ public function __construct() } $this->addInteractions([ - new DeleteInteraction('core/labels/groups/%s'), + new DeleteInteraction( + 'core/labels/groups/%s', + affectedObjects: [WCF::getLanguage()->get('wcf.label.labels')] + ), new class( "show-labels", ) extends AbstractInteraction { diff --git a/wcfsetup/install/files/lib/system/interaction/bulk/BulkDeleteInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/bulk/BulkDeleteInteraction.class.php index f8f2526bcc8..fc48cf6f3eb 100644 --- a/wcfsetup/install/files/lib/system/interaction/bulk/BulkDeleteInteraction.class.php +++ b/wcfsetup/install/files/lib/system/interaction/bulk/BulkDeleteInteraction.class.php @@ -14,9 +14,14 @@ */ class BulkDeleteInteraction extends BulkRpcInteraction { + /** + * @param list $affectedObjects Names of the objects that are implicitly deleted + * along with the objects themselves. + */ public function __construct( string $endpoint, - ?\Closure $isAvailableCallback = null + ?\Closure $isAvailableCallback = null, + private readonly array $affectedObjects = [] ) { parent::__construct( 'delete', @@ -27,4 +32,16 @@ public function __construct( $isAvailableCallback ); } + + #[\Override] + protected function getAdditionalDataAttributes(array $objects): array + { + if ($this->affectedObjects === []) { + return []; + } + + return [ + 'data-affected-objects' => \json_encode($this->affectedObjects, \JSON_THROW_ON_ERROR), + ]; + } } diff --git a/wcfsetup/install/files/lib/system/interaction/bulk/BulkRpcInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/bulk/BulkRpcInteraction.class.php index efaef4b66e0..b5942aefb9b 100644 --- a/wcfsetup/install/files/lib/system/interaction/bulk/BulkRpcInteraction.class.php +++ b/wcfsetup/install/files/lib/system/interaction/bulk/BulkRpcInteraction.class.php @@ -37,7 +37,9 @@ public function render(array $objects): string $identifier = StringUtil::encodeJS($this->getIdentifier()); $dataLabel = WCF::getLanguage()->get($this->languageItem); $label = WCF::getLanguage()->get($this->languageItem); - $confirmationMessage = WCF::getLanguage()->getDynamicVariable($this->confirmationMessage); + $confirmationMessage = StringUtil::encodeHTML( + WCF::getLanguage()->getDynamicVariable($this->confirmationMessage) + ); $endpoint = StringUtil::encodeHTML( LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) . $this->endpoint ); @@ -48,6 +50,15 @@ public function render(array $objects): string ) ); + $additionalAttributes = ''; + foreach ($this->getAdditionalDataAttributes($objects) as $name => $value) { + $additionalAttributes .= \sprintf( + ' %s="%s"', + StringUtil::encodeHTML($name), + StringUtil::encodeHTML($value) + ); + } + return << {$label} HTML; } + /** + * Returns additional data attributes that are added to the rendered button. + * + * The returned names and values are HTML encoded by the caller. + * + * @param DatabaseObject[] $objects + * @return array + * @since 6.3 + */ + protected function getAdditionalDataAttributes(array $objects): array + { + return []; + } + #[\Override] public function renderInitialization(string $containerId): string { diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 068f5cabf9a..0c492924d0b 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -3733,6 +3733,7 @@ Dateianhänge: + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index dd4bcb36639..217870e1aa0 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -3679,6 +3679,7 @@ Attachments: + From b1b79cd027ede107802508d2d0b2d5cd552fc322 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 22 Sep 2026 16:52:18 +0200 Subject: [PATCH 2/3] Fix HTML encoding of interaction button data attributes --- .../files/lib/system/interaction/RpcInteraction.class.php | 1 - .../system/interaction/bulk/BulkRpcInteraction.class.php | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php index 888a78b323a..b851908c5e1 100644 --- a/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php +++ b/wcfsetup/install/files/lib/system/interaction/RpcInteraction.class.php @@ -48,7 +48,6 @@ public function render(DatabaseObject $object): string } else { $confirmationMessage = ($this->confirmationMessage)($object); } - $confirmationMessage = StringUtil::encodeHTML($confirmationMessage); $endpoint = StringUtil::encodeHTML( LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) . diff --git a/wcfsetup/install/files/lib/system/interaction/bulk/BulkRpcInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/bulk/BulkRpcInteraction.class.php index b5942aefb9b..fe614b7d3a8 100644 --- a/wcfsetup/install/files/lib/system/interaction/bulk/BulkRpcInteraction.class.php +++ b/wcfsetup/install/files/lib/system/interaction/bulk/BulkRpcInteraction.class.php @@ -7,7 +7,6 @@ use wcf\system\interaction\InteractionConfirmationType; use wcf\system\request\LinkHandler; use wcf\system\WCF; - use wcf\util\StringUtil; /** @@ -34,12 +33,10 @@ public function __construct( #[\Override] public function render(array $objects): string { - $identifier = StringUtil::encodeJS($this->getIdentifier()); + $identifier = StringUtil::encodeHTML($this->getIdentifier()); $dataLabel = WCF::getLanguage()->get($this->languageItem); $label = WCF::getLanguage()->get($this->languageItem); - $confirmationMessage = StringUtil::encodeHTML( - WCF::getLanguage()->getDynamicVariable($this->confirmationMessage) - ); + $confirmationMessage = WCF::getLanguage()->getDynamicVariable($this->confirmationMessage); $endpoint = StringUtil::encodeHTML( LinkHandler::getInstance()->getControllerLink(ApiAction::class, ['id' => 'rpc']) . $this->endpoint ); From 5073241e0cf5cb24d01b8f217412c317114d3276 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 22 Sep 2026 17:17:52 +0200 Subject: [PATCH 3/3] Show implicitly deleted objects in category delete confirmations --- .../category/AbstractCategoryType.class.php | 6 ++++++ .../category/ArticleCategoryType.class.php | 6 ++++++ .../lib/system/category/ICategoryType.class.php | 10 ++++++++++ .../category/TrophyCategoryType.class.php | 6 ++++++ .../interaction/DeleteInteraction.class.php | 17 ++++++++++++----- .../admin/CategoryInteractions.class.php | 8 +++++++- 6 files changed, 47 insertions(+), 6 deletions(-) diff --git a/wcfsetup/install/files/lib/system/category/AbstractCategoryType.class.php b/wcfsetup/install/files/lib/system/category/AbstractCategoryType.class.php index 8e5e8ebb263..7ec4dd98458 100644 --- a/wcfsetup/install/files/lib/system/category/AbstractCategoryType.class.php +++ b/wcfsetup/install/files/lib/system/category/AbstractCategoryType.class.php @@ -238,4 +238,10 @@ public function getAddControllerClass(): string { return ''; } + + #[\Override] + public function getAffectedObjects(): array + { + return []; + } } diff --git a/wcfsetup/install/files/lib/system/category/ArticleCategoryType.class.php b/wcfsetup/install/files/lib/system/category/ArticleCategoryType.class.php index ae02d271eb4..28e4c635508 100644 --- a/wcfsetup/install/files/lib/system/category/ArticleCategoryType.class.php +++ b/wcfsetup/install/files/lib/system/category/ArticleCategoryType.class.php @@ -56,6 +56,12 @@ public function beforeDeletion(CategoryEditor $categoryEditor) } } + #[\Override] + public function getAffectedObjects(): array + { + return [WCF::getLanguage()->get('wcf.article.articles')]; + } + #[\Override] public function canAddCategory() { diff --git a/wcfsetup/install/files/lib/system/category/ICategoryType.class.php b/wcfsetup/install/files/lib/system/category/ICategoryType.class.php index c418a0abebd..9ff8b687c68 100644 --- a/wcfsetup/install/files/lib/system/category/ICategoryType.class.php +++ b/wcfsetup/install/files/lib/system/category/ICategoryType.class.php @@ -154,4 +154,14 @@ public function getEditFormLink(Category $category): string; * @since 6.3 */ public function getAddFormLink(?Category $parentCategory = null): string; + + /** + * Returns the names of the objects that are deleted along with a category of this type. + * + * The names are expected to be already localized. + * + * @return list + * @since 6.3 + */ + public function getAffectedObjects(): array; } diff --git a/wcfsetup/install/files/lib/system/category/TrophyCategoryType.class.php b/wcfsetup/install/files/lib/system/category/TrophyCategoryType.class.php index ff197fc0896..72b44b83f35 100644 --- a/wcfsetup/install/files/lib/system/category/TrophyCategoryType.class.php +++ b/wcfsetup/install/files/lib/system/category/TrophyCategoryType.class.php @@ -51,6 +51,12 @@ public function canEditCategory() return WCF::getSession()->hasPermission('admin.trophy.canManageTrophy'); } + #[\Override] + public function getAffectedObjects(): array + { + return [WCF::getLanguage()->get('wcf.user.trophy.trophies')]; + } + #[\Override] public function beforeDeletion(CategoryEditor $categoryEditor) { diff --git a/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php b/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php index 86c36b622d1..5025a5778e8 100644 --- a/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php +++ b/wcfsetup/install/files/lib/system/interaction/DeleteInteraction.class.php @@ -15,13 +15,14 @@ class DeleteInteraction extends RpcInteraction { /** - * @param list $affectedObjects Names of the objects that are implicitly deleted - * along with the object itself. + * @param list|\Closure $affectedObjects Names of the objects that are implicitly deleted + * along with the object itself. A closure receives the `DatabaseObject` the + * interaction is rendered for and must return a list of names. */ public function __construct( string $endpoint, ?\Closure $isAvailableCallback = null, - private readonly array $affectedObjects = [] + private readonly array|\Closure $affectedObjects = [] ) { parent::__construct( 'delete', @@ -37,12 +38,18 @@ public function __construct( #[\Override] protected function getAdditionalDataAttributes(DatabaseObject $object): array { - if ($this->affectedObjects === []) { + if (\is_array($this->affectedObjects)) { + $affectedObjects = $this->affectedObjects; + } else { + $affectedObjects = ($this->affectedObjects)($object); + } + + if ($affectedObjects === []) { return []; } return [ - 'data-affected-objects' => \json_encode($this->affectedObjects, \JSON_THROW_ON_ERROR), + 'data-affected-objects' => \json_encode(\array_values($affectedObjects), \JSON_THROW_ON_ERROR), ]; } } diff --git a/wcfsetup/install/files/lib/system/interaction/admin/CategoryInteractions.class.php b/wcfsetup/install/files/lib/system/interaction/admin/CategoryInteractions.class.php index 57c02e270c9..1735497a29f 100644 --- a/wcfsetup/install/files/lib/system/interaction/admin/CategoryInteractions.class.php +++ b/wcfsetup/install/files/lib/system/interaction/admin/CategoryInteractions.class.php @@ -61,7 +61,13 @@ protected function getLink(DatabaseObject $object): string }, new DeleteInteraction( 'core/categories/%s', - static fn(Category $category) => $category->getObjectType()->getProcessor()->canDeleteCategory() + static fn(Category $category) => $category->getObjectType()->getProcessor()->canDeleteCategory(), + static function (Category $category): array { + $processor = $category->getObjectType()->getProcessor(); + \assert($processor instanceof ICategoryType); + + return $processor->getAffectedObjects(); + } ), ]);