From d90daa0a7ec031d84fe5c3d4879a394c6e65c56f Mon Sep 17 00:00:00 2001
From: Marcel Werk
Date: Sun, 8 Nov 2026 16:21:20 +0100
Subject: [PATCH] Notify moderators about new content awaiting approval
---
...notification_moderationQueueActivation.tpl | 40 +++++
com.woltlab.wcf/userNotificationEvent.xml | 7 +
.../AbstractModerationQueueManager.class.php | 85 +++++++++-
...ModerationQueueActivationManager.class.php | 15 ++
.../ModerationQueueReportManager.class.php | 75 +--------
...rationQueueUserNotificationEvent.class.php | 154 ++++++++++++++++++
...rationQueueUserNotificationEvent.class.php | 43 +++++
wcfsetup/install/lang/de.xml | 5 +
wcfsetup/install/lang/en.xml | 5 +
9 files changed, 355 insertions(+), 74 deletions(-)
create mode 100644 com.woltlab.wcf/templates/email_notification_moderationQueueActivation.tpl
create mode 100644 wcfsetup/install/files/lib/system/user/notification/event/ActivationModerationQueueUserNotificationEvent.class.php
diff --git a/com.woltlab.wcf/templates/email_notification_moderationQueueActivation.tpl b/com.woltlab.wcf/templates/email_notification_moderationQueueActivation.tpl
new file mode 100644
index 00000000000..f002ae26e20
--- /dev/null
+++ b/com.woltlab.wcf/templates/email_notification_moderationQueueActivation.tpl
@@ -0,0 +1,40 @@
+{if $mimeType === 'text/plain'}
+{lang}wcf.moderation.activation.notification.mail.plaintext{/lang}
+{if $affectedObject}
+
+{unsafe:$affectedObject->getMailText($mimeType)} {* this line ends with a space *}
+{/if}
+{else}
+ {lang}wcf.moderation.activation.notification.mail.html{/lang}
+ {if $affectedObject}
+ {assign var='user' value=$event->getAuthor()}
+
+ {if $notificationType == 'instant'}{assign var='avatarSize' value=48}
+ {else}{assign var='avatarSize' value=32}{/if}
+ {capture assign='userContent'}
+
+ {/capture}
+ {include file='email_paddingHelper' block=true class='box'|concat:$avatarSize content=$userContent sandbox=true}
+ {/if}
+{/if}
diff --git a/com.woltlab.wcf/userNotificationEvent.xml b/com.woltlab.wcf/userNotificationEvent.xml
index 6d92886e137..fc232c304ce 100644
--- a/com.woltlab.wcf/userNotificationEvent.xml
+++ b/com.woltlab.wcf/userNotificationEvent.xml
@@ -48,6 +48,13 @@
mod.general.canUseModeration
1
+
+ activation
+ com.woltlab.wcf.moderation.queue
+ wcf\system\user\notification\event\ActivationModerationQueueUserNotificationEvent
+ mod.general.canUseModeration
+ 0
+
comment
com.woltlab.wcf.moderation.queue.notification
diff --git a/wcfsetup/install/files/lib/system/moderation/queue/AbstractModerationQueueManager.class.php b/wcfsetup/install/files/lib/system/moderation/queue/AbstractModerationQueueManager.class.php
index 11e87fac224..3cb9c56db23 100644
--- a/wcfsetup/install/files/lib/system/moderation/queue/AbstractModerationQueueManager.class.php
+++ b/wcfsetup/install/files/lib/system/moderation/queue/AbstractModerationQueueManager.class.php
@@ -6,10 +6,15 @@
use wcf\data\moderation\queue\ModerationQueue;
use wcf\data\moderation\queue\ModerationQueueAction;
use wcf\data\moderation\queue\ModerationQueueList;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\cache\builder\UserGroupOptionCacheBuilder;
+use wcf\system\cache\runtime\UserProfileRuntimeCache;
use wcf\system\database\util\PreparedStatementConditionBuilder;
use wcf\system\exception\NotImplementedException;
use wcf\system\request\LinkHandler;
use wcf\system\SingletonFactory;
+use wcf\system\user\notification\object\ModerationQueueUserNotificationObject;
+use wcf\system\user\notification\UserNotificationHandler;
use wcf\system\WCF;
/**
@@ -77,7 +82,7 @@ public function removeContent(ModerationQueue $queue, string $message = '')
* Adds an entry to moderation queue.
*
* @param mixed[] $additionalData
- * @return void
+ * @return ModerationQueue
*/
protected function addEntry(int $objectTypeID, int $objectID, int $containerID = 0, array $additionalData = [])
{
@@ -104,6 +109,7 @@ protected function addEntry(int $objectTypeID, int $objectID, int $containerID =
],
]);
$objectAction->executeAction();
+ $queue = $objectAction->getReturnValues()['returnValues'];
} else {
$objectAction = new ModerationQueueAction([$row['queueID']], 'update', [
'data' => [
@@ -115,9 +121,86 @@ protected function addEntry(int $objectTypeID, int $objectID, int $containerID =
],
]);
$objectAction->executeAction();
+ $queue = new ModerationQueue($row['queueID']);
}
ModerationQueueManager::getInstance()->resetModerationCount();
+
+ return $queue;
+ }
+
+ /**
+ * Notifies all moderators affected by the given queue entry by firing the given
+ * user notification event of the `com.woltlab.wcf.moderation.queue` object type.
+ *
+ * @since 6.3
+ */
+ protected function notifyModerators(ModerationQueue $queue, string $eventName): void
+ {
+ /** @see TMultiRecipientModerationQueueCommentUserNotificationObjectType::loadModerators() */
+ $userGroupOptionCache = UserGroupOptionCacheBuilder::getInstance()->getData();
+ $canUseModerationOption = $userGroupOptionCache['options']['mod.general.canUseModeration'];
+
+ $sql = "SELECT DISTINCT userID
+ FROM (
+ SELECT userID
+ FROM wcf1_user_to_group
+ WHERE groupID IN (
+ SELECT groupID
+ FROM wcf1_user_group_option_value
+ WHERE optionID = ?
+ AND optionValue = ?
+ )
+ ) users_in_groups_with_access
+ WHERE userID NOT IN (
+ SELECT userID
+ FROM wcf1_user_to_group
+ WHERE groupID IN (
+ SELECT groupID
+ FROM wcf1_user_group_option_value
+ WHERE optionID = ?
+ AND optionValue = ?
+ )
+ )
+ AND userID NOT IN (
+ SELECT userID
+ FROM wcf1_moderation_queue_to_user
+ WHERE queueID = ?
+ )";
+ $statement = WCF::getDB()->prepare($sql);
+ $statement->execute([
+ $canUseModerationOption->optionID,
+ 1,
+ $canUseModerationOption->optionID,
+ -1,
+ $queue->queueID,
+ ]);
+ $userIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
+ if ($userIDs === []) {
+ return;
+ }
+ UserProfileRuntimeCache::getInstance()->cacheObjectIDs($userIDs);
+ $objectType = ObjectTypeCache::getInstance()->getObjectType($queue->objectTypeID);
+ $processor = $objectType->getProcessor();
+ \assert($processor instanceof IModerationQueueHandler);
+
+ $userIDs = \array_filter($userIDs, function ($userID) use ($processor, $queue) {
+ return $processor->isAffectedUser($queue, $userID);
+ });
+ if ($userIDs === []) {
+ return;
+ }
+ foreach ($userIDs as $userID) {
+ $user = UserProfileRuntimeCache::getInstance()->getObject($userID);
+ ModerationQueueManager::getInstance()->setAssignment([$queue->queueID => true], $user->getDecoratedObject());
+ }
+
+ UserNotificationHandler::getInstance()->fireEvent(
+ $eventName,
+ 'com.woltlab.wcf.moderation.queue',
+ new ModerationQueueUserNotificationObject($queue),
+ $userIDs
+ );
}
/**
diff --git a/wcfsetup/install/files/lib/system/moderation/queue/ModerationQueueActivationManager.class.php b/wcfsetup/install/files/lib/system/moderation/queue/ModerationQueueActivationManager.class.php
index f0c01cfd35a..a3b5c2cb3d0 100644
--- a/wcfsetup/install/files/lib/system/moderation/queue/ModerationQueueActivationManager.class.php
+++ b/wcfsetup/install/files/lib/system/moderation/queue/ModerationQueueActivationManager.class.php
@@ -7,6 +7,7 @@
use wcf\form\ModerationActivationForm;
use wcf\system\exception\InvalidObjectTypeException;
use wcf\system\moderation\queue\activation\IModerationQueueActivationHandler;
+use wcf\system\WCF;
/**
* Moderation queue implementation for moderated content.
@@ -71,6 +72,20 @@ public function addModeratedContent(string $objectType, int $objectID, array $ad
);
}
+ #[\Override]
+ protected function addEntry(int $objectTypeID, int $objectID, int $containerID = 0, array $additionalData = [])
+ {
+ $queue = parent::addEntry($objectTypeID, $objectID, $containerID, $additionalData);
+
+ // Content disabled by a moderator does not require a notification.
+ $user = WCF::getUser();
+ if ($user->isGuest() || !$this->getProcessor(null, $objectTypeID)->isAffectedUser($queue, $user->userID)) {
+ $this->notifyModerators($queue, 'activation');
+ }
+
+ return $queue;
+ }
+
/**
* Adds multiple entries for moderated content at once.
*
diff --git a/wcfsetup/install/files/lib/system/moderation/queue/ModerationQueueReportManager.class.php b/wcfsetup/install/files/lib/system/moderation/queue/ModerationQueueReportManager.class.php
index e8ad035d494..d96a5614793 100644
--- a/wcfsetup/install/files/lib/system/moderation/queue/ModerationQueueReportManager.class.php
+++ b/wcfsetup/install/files/lib/system/moderation/queue/ModerationQueueReportManager.class.php
@@ -5,14 +5,9 @@
use wcf\data\moderation\queue\ModerationQueue;
use wcf\data\moderation\queue\ModerationQueueAction;
use wcf\data\moderation\queue\ViewableModerationQueue;
-use wcf\data\object\type\ObjectTypeCache;
use wcf\form\ModerationReportForm;
-use wcf\system\cache\builder\UserGroupOptionCacheBuilder;
-use wcf\system\cache\runtime\UserProfileRuntimeCache;
use wcf\system\exception\InvalidObjectTypeException;
use wcf\system\moderation\queue\report\IModerationQueueReportHandler;
-use wcf\system\user\notification\object\ModerationQueueUserNotificationObject;
-use wcf\system\user\notification\UserNotificationHandler;
use wcf\system\WCF;
/**
@@ -182,74 +177,8 @@ protected function addEntry(int $objectTypeID, int $objectID, int $containerID =
ModerationQueueManager::getInstance()->resetModerationCount();
- $this->notifyModerators($queue);
- }
-
- private function notifyModerators(ModerationQueue $queue): void
- {
- /** @see TMultiRecipientModerationQueueCommentUserNotificationObjectType::loadModerators() */
- $userGroupOptionCache = UserGroupOptionCacheBuilder::getInstance()->getData();
- $canUseModerationOption = $userGroupOptionCache['options']['mod.general.canUseModeration'];
-
- $sql = "SELECT DISTINCT userID
- FROM (
- SELECT userID
- FROM wcf1_user_to_group
- WHERE groupID IN (
- SELECT groupID
- FROM wcf1_user_group_option_value
- WHERE optionID = ?
- AND optionValue = ?
- )
- ) users_in_groups_with_access
- WHERE userID NOT IN (
- SELECT userID
- FROM wcf1_user_to_group
- WHERE groupID IN (
- SELECT groupID
- FROM wcf1_user_group_option_value
- WHERE optionID = ?
- AND optionValue = ?
- )
- )
- AND userID NOT IN (
- SELECT userID
- FROM wcf1_moderation_queue_to_user
- WHERE queueID = ?
- )";
- $statement = WCF::getDB()->prepare($sql);
- $statement->execute([
- $canUseModerationOption->optionID,
- 1,
- $canUseModerationOption->optionID,
- -1,
- $queue->queueID,
- ]);
- $userIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
- if ($userIDs === []) {
- return;
- }
- UserProfileRuntimeCache::getInstance()->cacheObjectIDs($userIDs);
- $objectType = ObjectTypeCache::getInstance()->getObjectType($queue->objectTypeID);
- $processor = $objectType->getProcessor();
- \assert($processor instanceof IModerationQueueHandler);
-
- $userIDs = \array_filter($userIDs, function ($userID) use ($processor, $queue) {
- return $processor->isAffectedUser($queue, $userID);
- });
- if ($userIDs === []) {
- return;
- }
- foreach ($userIDs as $userID) {
- $user = UserProfileRuntimeCache::getInstance()->getObject($userID);
- ModerationQueueManager::getInstance()->setAssignment([$queue->queueID => true], $user->getDecoratedObject());
- }
+ $this->notifyModerators($queue, 'report');
- UserNotificationHandler::getInstance()->fireEvent(
- 'report',
- 'com.woltlab.wcf.moderation.queue',
- new ModerationQueueUserNotificationObject($queue),
- $userIDs
- );
+ return $queue;
}
}
diff --git a/wcfsetup/install/files/lib/system/user/notification/event/ActivationModerationQueueUserNotificationEvent.class.php b/wcfsetup/install/files/lib/system/user/notification/event/ActivationModerationQueueUserNotificationEvent.class.php
new file mode 100644
index 00000000000..37efa66ff5d
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/user/notification/event/ActivationModerationQueueUserNotificationEvent.class.php
@@ -0,0 +1,154 @@
+
+ * @since 6.3
+ *
+ * @method ModerationQueueUserNotificationObject getUserNotificationObject()
+ */
+final class ActivationModerationQueueUserNotificationEvent extends AbstractUserNotificationEvent implements
+ IRecipientAwareUserNotificationEvent,
+ ITestableUserNotificationEvent
+{
+ use TTestableModerationQueueUserNotificationEvent;
+ use TTestableUserNotificationEvent;
+
+ private ?ViewableModerationQueue $viewableModerationQueue;
+ private User $recipient;
+
+ #[\Override]
+ public function getTitle(): string
+ {
+ return $this->getLanguage()->get('wcf.moderation.activation.notification.title');
+ }
+
+ #[\Override]
+ public function getMessage()
+ {
+ return $this->getLanguage()->getDynamicVariable(
+ 'wcf.moderation.activation.notification.message',
+ $this->getLanguageVariables()
+ );
+ }
+
+ #[\Override]
+ public function getEmailMessage(string $notificationType = 'instant'): array
+ {
+ $affectedObject = $this->getViewableModerationQueue()->getAffectedObject();
+
+ return [
+ 'message-id' => 'com.woltlab.wcf.moderation.queue.notification/'
+ . $this->getUserNotificationObject()->queueID,
+ 'template' => 'email_notification_moderationQueueActivation',
+ 'application' => 'wcf',
+ 'references' => [
+ 'getUserNotificationObject()->queueID . '@' . Email::getHost() . '>',
+ ],
+ 'variables' => [
+ ...$this->getLanguageVariables(),
+ 'affectedObject' => \method_exists($affectedObject, 'getMailText') ? $affectedObject : null,
+ ],
+ ];
+ }
+
+ #[\Override]
+ public function getLink(): string
+ {
+ return LinkHandler::getInstance()->getControllerLink(ModerationActivationForm::class, [
+ 'id' => $this->getUserNotificationObject()->queueID,
+ ]);
+ }
+
+ #[\Override]
+ public function getEventHash()
+ {
+ return \sha1($this->eventID . '-' . $this->getUserNotificationObject()->queueID);
+ }
+
+ #[\Override]
+ public function checkAccess()
+ {
+ if ($this->getViewableModerationQueue() === null) {
+ return false;
+ }
+ $objectType = ObjectTypeCache::getInstance()->getObjectType($this->getUserNotificationObject()->objectTypeID);
+ $processor = $objectType->getProcessor();
+ \assert($processor instanceof IModerationQueueHandler);
+
+ return $processor->isAffectedUser(
+ $this->getUserNotificationObject()->getDecoratedObject(),
+ $this->getRecipient()->userID
+ );
+ }
+
+ #[\Override]
+ public function setRecipient(User $user): void
+ {
+ $this->recipient = $user;
+ }
+
+ /**
+ * @return array
+ */
+ private function getLanguageVariables(): array
+ {
+ return [
+ 'author' => $this->author,
+ 'notification' => $this->notification,
+ 'moderationQueue' => $this->getViewableModerationQueue(),
+ 'title' => $this->getViewableModerationQueue()->getTitle(),
+ 'objectLink' => $this->getViewableModerationQueue()->getLink(),
+ 'typeName' => $this->getLanguage()->getDynamicVariable(
+ "wcf.moderation.type." . $this->getViewableModerationQueue()->getObjectTypeName()
+ ),
+ ];
+ }
+
+ private function getRecipient(): User
+ {
+ return $this->recipient ?? WCF::getUser();
+ }
+
+ private function getViewableModerationQueue(): ?ViewableModerationQueue
+ {
+ if (!isset($this->viewableModerationQueue)) {
+ $this->viewableModerationQueue = ViewableModerationQueue::getViewableModerationQueue(
+ $this->getUserNotificationObject()->queueID,
+ $this->getRecipient(),
+ );
+ }
+ return $this->viewableModerationQueue;
+ }
+
+ #[\Override]
+ public static function canBeTriggeredByGuests()
+ {
+ return true;
+ }
+
+ #[\Override]
+ public static function getTestObjects(UserProfile $recipient, UserProfile $author)
+ {
+ return [
+ new ModerationQueueUserNotificationObject(self::getTestActivationModerationQueueEntry($recipient, $author)),
+ ];
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/user/notification/event/TTestableModerationQueueUserNotificationEvent.class.php b/wcfsetup/install/files/lib/system/user/notification/event/TTestableModerationQueueUserNotificationEvent.class.php
index 9eef370f71b..12d1db4f818 100644
--- a/wcfsetup/install/files/lib/system/user/notification/event/TTestableModerationQueueUserNotificationEvent.class.php
+++ b/wcfsetup/install/files/lib/system/user/notification/event/TTestableModerationQueueUserNotificationEvent.class.php
@@ -2,9 +2,13 @@
namespace wcf\system\user\notification\event;
+use wcf\data\comment\CommentBuilder;
use wcf\data\moderation\queue\ModerationQueue;
+use wcf\data\moderation\queue\ModerationQueueAction;
use wcf\data\object\type\ObjectTypeCache;
use wcf\data\user\UserProfile;
+use wcf\system\comment\CommentHandler;
+use wcf\system\moderation\queue\ModerationQueueActivationManager;
use wcf\system\moderation\queue\ModerationQueueManager;
use wcf\system\moderation\queue\ModerationQueueReportManager;
use wcf\system\WCF;
@@ -52,4 +56,43 @@ public static function getTestUserModerationQueueEntry(UserProfile $reportedUser
return $moderationQueue;
}
+
+ /**
+ * Creates a moderation queue entry for a disabled comment that awaits approval.
+ *
+ * @since 6.3
+ */
+ public static function getTestActivationModerationQueueEntry(
+ UserProfile $recipient,
+ UserProfile $author
+ ): ModerationQueue {
+ $commentObjectTypeID = CommentHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.user.profileComment');
+ $comment = CommentBuilder::forCreate()
+ ->setObjectType(ObjectTypeCache::getInstance()->getObjectType($commentObjectTypeID))
+ ->setObjectID($recipient->userID)
+ ->setTime(\TIME_NOW - 10)
+ ->setUser($author->getDecoratedObject())
+ ->setMessage('Test Comment
')
+ ->setEnableHtml(true)
+ ->setIsDisabled(true)
+ ->create();
+
+ // The queue entry is created directly to avoid notifying the actual moderators.
+ $moderationQueue = new ModerationQueueAction([], 'create', [
+ 'data' => [
+ 'objectTypeID' => ModerationQueueActivationManager::getInstance()->getObjectTypeID(
+ 'com.woltlab.wcf.comment.comment'
+ ),
+ 'objectID' => $comment->commentID,
+ 'userID' => $author->userID,
+ 'time' => \TIME_NOW,
+ 'additionalData' => \serialize([]),
+ ],
+ ])->executeAction()['returnValues'];
+ \assert($moderationQueue instanceof ModerationQueue);
+
+ ModerationQueueManager::getInstance()->setAssignment([$moderationQueue->queueID => true]);
+
+ return $moderationQueue;
+ }
}
diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml
index c4efb9e8948..b8cc82e9057 100644
--- a/wcfsetup/install/lang/de.xml
+++ b/wcfsetup/install/lang/de.xml
@@ -4345,6 +4345,10 @@ Dateianhänge:
- {$moderationQueue} verfasst.]]>
- getAuthor()->userID || $guestTimesTriggered == 0)}hat eine Antwort{else}haben Antworten{/if} zum Kommentar von {@$commentAuthor} zum freizuschaltenden Inhalt {@$moderationQueue->getTitle()} [URL:{@$moderationQueue->getLink()}] verfasst{if $count == 1 && !$guestTimesTriggered}:{else}.{/if}]]>
- {@$authorList} {if $count == 1 && $guestTimesTriggered < 2 && (!$event->getAuthor()->userID || $guestTimesTriggered == 0)}hat eine Antwort{else}haben Antworten{/if} zum Kommentar von {$commentAuthor} zum freizuschaltenden Inhalt {$moderationQueue->getTitle()} verfasst:
]]>
+ - userID}{$author}{else}einem Gast{/if} wartet auf Freischaltung [URL:{$objectLink}].]]>
+ - Der Inhalt {$title} ({$typeName}) von {if $author->userID}{$author}{else}einem Gast{/if} wartet auf Freischaltung{if $affectedObject}:{else}.{/if}]]>
+ - {$title} ({$typeName}) von {if $author->userID}{$author}{else}einem Gast{/if} wartet auf Freischaltung.]]>
+
@@ -5339,6 +5343,7 @@ Benachrichtigungen auf {PAGE_TITLE|phra
+
diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml
index b96b3254cd1..f4ab343ca5a 100644
--- a/wcfsetup/install/lang/en.xml
+++ b/wcfsetup/install/lang/en.xml
@@ -4291,6 +4291,10 @@ Attachments:
- {$moderationQueue} waiting for approval.]]>
- getTitle()} [URL:{@$moderationQueue->getLink()}] waiting for approval{if $count == 1 && !$guestTimesTriggered}:{else}.{/if}]]>
- {@$authorList} replied to {$commentAuthor}’s comment on {$moderationQueue->getTitle()} waiting for approval:]]>
+ - userID}{$author}{else}a guest{/if} is awaiting approval [URL:{$objectLink}].]]>
+ - {$title} ({$typeName}) by {if $author->userID}{$author}{else}a guest{/if} is awaiting approval{if $affectedObject}:{else}.{/if}]]>
+ - {$title} ({$typeName}) by {if $author->userID}{$author}{else}a guest{/if} is awaiting approval.]]>
+
@@ -5334,6 +5338,7 @@ your notifications on {PAGE_TITLE|phras
+