From c1e7571098a7fd6fdd875c0da6bcf234ee415636 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Sun, 8 Nov 2026 14:26:29 +0100 Subject: [PATCH] Queue articles from contributors without publish permission for moderation --- com.woltlab.wcf/objectType.xml | 5 + .../lib/acp/form/ArticleAddForm.class.php | 5 +- .../command/article/CreateArticle.class.php | 7 + .../command/article/DeleteArticle.class.php | 6 + .../command/article/PublishArticle.class.php | 6 + .../article/SoftDeleteArticle.class.php | 6 + .../command/article/UpdateArticle.class.php | 8 + ...ModerationQueueActivationHandler.class.php | 161 ++++++++++++++++++ 8 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 wcfsetup/install/files/lib/system/moderation/queue/activation/ArticleModerationQueueActivationHandler.class.php diff --git a/com.woltlab.wcf/objectType.xml b/com.woltlab.wcf/objectType.xml index e23256e7b78..b5d91b07201 100644 --- a/com.woltlab.wcf/objectType.xml +++ b/com.woltlab.wcf/objectType.xml @@ -86,6 +86,11 @@ com.woltlab.wcf.moderation.report wcf\system\moderation\queue\report\ArticleModerationQueueReportHandler + + com.woltlab.wcf.article + com.woltlab.wcf.moderation.activation + wcf\system\moderation\queue\activation\ArticleModerationQueueActivationHandler + com.woltlab.wcf.article.category com.woltlab.wcf.user.objectWatch diff --git a/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php b/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php index 70b19185c40..01e1be6fd7f 100644 --- a/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php @@ -695,7 +695,10 @@ protected function getCommand(DatabaseObjectBuilder $builder): callable return new UpdateArticle($builder); } - return new CreateArticle($builder); + $canManage = WCF::getSession()->hasPermission('admin.content.article.canManageArticle') + || WCF::getSession()->hasPermission('admin.content.article.canManageOwnArticles'); + + return new CreateArticle($builder, !$canManage); } protected function getAttachmentObjectID(?int $languageID = null): ?int diff --git a/wcfsetup/install/files/lib/command/article/CreateArticle.class.php b/wcfsetup/install/files/lib/command/article/CreateArticle.class.php index f8e9a107227..c78bcf0dd0a 100644 --- a/wcfsetup/install/files/lib/command/article/CreateArticle.class.php +++ b/wcfsetup/install/files/lib/command/article/CreateArticle.class.php @@ -6,6 +6,7 @@ use wcf\data\article\ArticleBuilder; use wcf\event\article\ArticleCreated; use wcf\system\event\EventHandler; +use wcf\system\moderation\queue\ModerationQueueActivationManager; use wcf\system\search\SearchIndexManager; use wcf\system\user\activity\event\UserActivityEventHandler; use wcf\system\user\notification\object\ArticleUserNotificationObject; @@ -23,6 +24,7 @@ final class CreateArticle { public function __construct( private readonly ArticleBuilder $builder, + private readonly bool $addToModerationQueue = false, ) {} public function __invoke(): Article @@ -56,6 +58,11 @@ public function __invoke(): Article ); new MarkArticleAsRead($article)(); + } elseif ($this->addToModerationQueue) { + ModerationQueueActivationManager::getInstance()->addModeratedContent( + 'com.woltlab.wcf.article', + $article->articleID + ); } EventHandler::getInstance()->fire(new ArticleCreated($article, $this->builder)); diff --git a/wcfsetup/install/files/lib/command/article/DeleteArticle.class.php b/wcfsetup/install/files/lib/command/article/DeleteArticle.class.php index 8934d149069..2e5d967d68b 100644 --- a/wcfsetup/install/files/lib/command/article/DeleteArticle.class.php +++ b/wcfsetup/install/files/lib/command/article/DeleteArticle.class.php @@ -10,6 +10,7 @@ use wcf\system\comment\CommentHandler; use wcf\system\event\EventHandler; use wcf\system\message\embedded\object\MessageEmbeddedObjectManager; +use wcf\system\moderation\queue\ModerationQueueActivationManager; use wcf\system\search\SearchIndexManager; use wcf\system\tagging\TagEngine; use wcf\system\user\activity\event\UserActivityEventHandler; @@ -69,6 +70,11 @@ public function __invoke(): void ArticleBuilder::incrementArticleCounter($this->article->userID, -1); } } + // delete moderation queue entries + ModerationQueueActivationManager::getInstance()->removeModeratedContent( + 'com.woltlab.wcf.article', + [$this->article->articleID] + ); // delete attachments if ($attachmentArticleContentIDs !== []) { AttachmentHandler::removeAttachments( diff --git a/wcfsetup/install/files/lib/command/article/PublishArticle.class.php b/wcfsetup/install/files/lib/command/article/PublishArticle.class.php index 9c149ff06d2..d071ed0a6ed 100644 --- a/wcfsetup/install/files/lib/command/article/PublishArticle.class.php +++ b/wcfsetup/install/files/lib/command/article/PublishArticle.class.php @@ -6,6 +6,7 @@ use wcf\data\article\ArticleBuilder; use wcf\event\article\ArticlePublished; use wcf\system\event\EventHandler; +use wcf\system\moderation\queue\ModerationQueueActivationManager; use wcf\system\user\activity\event\UserActivityEventHandler; use wcf\system\user\notification\object\ArticleUserNotificationObject; use wcf\system\user\object\watch\UserObjectWatchHandler; @@ -39,6 +40,11 @@ public function __invoke(): void new ResetUserStorageForUnreadArticles()(); + ModerationQueueActivationManager::getInstance()->removeModeratedContent( + 'com.woltlab.wcf.article', + [$this->article->articleID] + ); + $event = new ArticlePublished($this->article); EventHandler::getInstance()->fire($event); } diff --git a/wcfsetup/install/files/lib/command/article/SoftDeleteArticle.class.php b/wcfsetup/install/files/lib/command/article/SoftDeleteArticle.class.php index 65b1728596a..8894035f452 100644 --- a/wcfsetup/install/files/lib/command/article/SoftDeleteArticle.class.php +++ b/wcfsetup/install/files/lib/command/article/SoftDeleteArticle.class.php @@ -6,6 +6,7 @@ use wcf\data\article\ArticleBuilder; use wcf\event\article\ArticleSoftDeleted; use wcf\system\event\EventHandler; +use wcf\system\moderation\queue\ModerationQueueActivationManager; /** * Soft-deletes an article. @@ -27,6 +28,11 @@ public function __invoke(): void new ResetUserStorageForUnreadArticles()(); + ModerationQueueActivationManager::getInstance()->removeModeratedContent( + 'com.woltlab.wcf.article', + [$this->article->articleID] + ); + $event = new ArticleSoftDeleted($this->article); EventHandler::getInstance()->fire($event); } diff --git a/wcfsetup/install/files/lib/command/article/UpdateArticle.class.php b/wcfsetup/install/files/lib/command/article/UpdateArticle.class.php index a7d3b7328c1..7a130f3eb19 100644 --- a/wcfsetup/install/files/lib/command/article/UpdateArticle.class.php +++ b/wcfsetup/install/files/lib/command/article/UpdateArticle.class.php @@ -8,6 +8,7 @@ use wcf\data\article\content\ArticleContent; use wcf\event\article\ArticleUpdated; use wcf\system\event\EventHandler; +use wcf\system\moderation\queue\ModerationQueueActivationManager; use wcf\system\search\SearchIndexManager; use wcf\system\user\activity\event\UserActivityEventHandler; use wcf\system\user\notification\object\ArticleUserNotificationObject; @@ -119,6 +120,13 @@ private function handlePublicationStatusChange(Article $article, int $oldStatus, [$article->articleID] ); } + + if ($newStatus !== Article::UNPUBLISHED) { + ModerationQueueActivationManager::getInstance()->removeModeratedContent( + 'com.woltlab.wcf.article', + [$article->articleID] + ); + } } private function updateActivityEventAuthor(Article $article): void diff --git a/wcfsetup/install/files/lib/system/moderation/queue/activation/ArticleModerationQueueActivationHandler.class.php b/wcfsetup/install/files/lib/system/moderation/queue/activation/ArticleModerationQueueActivationHandler.class.php new file mode 100644 index 00000000000..9d2d32780ab --- /dev/null +++ b/wcfsetup/install/files/lib/system/moderation/queue/activation/ArticleModerationQueueActivationHandler.class.php @@ -0,0 +1,161 @@ + + * @since 6.3 + */ +final class ArticleModerationQueueActivationHandler extends AbstractModerationQueueHandler implements + IModerationQueueActivationHandler +{ + /** + * @inheritDoc + */ + protected $className = Article::class; + + /** + * @inheritDoc + */ + protected $definitionName = 'com.woltlab.wcf.moderation.activation'; + + /** + * @inheritDoc + */ + protected $objectType = 'com.woltlab.wcf.article'; + + #[\Override] + public function enableContent(ModerationQueue $queue): void + { + $article = $this->getArticle($queue->objectID); + if ($article !== null && $article->publicationStatus === Article::UNPUBLISHED) { + new PublishArticle($article)(); + } + } + + #[\Override] + public function getDisabledContent(ViewableModerationQueue $queue): string + { + return WCF::getTPL()->render('wcf', 'moderationArticle', [ + // @phpstan-ignore property.notFound + 'article' => $this->getArticle($queue->getAffectedObject()->articleID), + ]); + } + + public function getArticle(int $articleID): ?Article + { + return ArticleRuntimeCache::getInstance()->getObject($articleID); + } + + #[\Override] + public function assignQueues(array $queues): void + { + $assignments = $orphanedQueueIDs = []; + + foreach ($queues as $queue) { + ArticleRuntimeCache::getInstance()->cacheObjectID($queue->objectID); + } + + foreach ($queues as $queue) { + $article = ArticleRuntimeCache::getInstance()->getObject($queue->objectID); + + if ($article === null) { + $orphanedQueueIDs[] = $queue->queueID; + } else { + $assignments[$queue->queueID] = $article->canPublish(); + } + } + + ModerationQueueManager::getInstance()->removeOrphans($orphanedQueueIDs); + ModerationQueueManager::getInstance()->setAssignment($assignments); + } + + #[\Override] + public function getContainerID(int $objectID): int + { + if ($this->isValid($objectID)) { + return $this->getArticle($objectID)->getCategory()->categoryID; + } + + return 0; + } + + #[\Override] + public function isValid(int $objectID): bool + { + return $this->getArticle($objectID) !== null; + } + + #[\Override] + public function populate(array $queues): void + { + foreach ($queues as $queue) { + ArticleRuntimeCache::getInstance()->cacheObjectID($queue->objectID); + } + + foreach ($queues as $object) { + $article = ArticleRuntimeCache::getInstance()->getObject($object->objectID); + if ($article !== null) { + $object->setAffectedObject($article); + } else { + $object->setIsOrphaned(); + } + } + } + + #[\Override] + public function canRemoveContent(ModerationQueue $queue): bool + { + if ($this->isValid($queue->objectID)) { + return $this->getArticle($queue->objectID)->canDelete(); + } + + return false; + } + + #[\Override] + public function removeContent(ModerationQueue $queue, string $message): void + { + if ($this->isValid($queue->objectID)) { + new SoftDeleteArticle($this->getArticle($queue->objectID))(); + } + } + + #[\Override] + public function isAffectedUser(ModerationQueue $queue, int $userID): bool + { + if (!parent::isAffectedUser($queue, $userID)) { + return false; + } + + $article = $this->getArticle($queue->objectID); + if ($article === null) { + return false; + } + + /** @see Article::canPublish() */ + $userProfile = UserProfileRuntimeCache::getInstance()->getObject($userID); + if ($userProfile->hasPermission('admin.content.article.canManageArticle')) { + return true; + } + + return $userProfile->hasPermission('admin.content.article.canManageOwnArticles') + && $article->userID === $userProfile->userID; + } +}