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'} + + + + + +
{unsafe:$user->getAvatar()->getSafeImageTag($avatarSize)} +
+

+ {if $moderationQueue->userID} + {$moderationQueue->username} + {else} + {$moderationQueue->username} + {/if} + · + {$moderationQueue->time|plainTime} +

+
+
+ {unsafe:$affectedObject->getMailText($mimeType)} +
+
+ {/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 +