From: Matthias Schmidt Date: Fri, 23 Apr 2021 06:14:28 +0000 (+0200) Subject: Merge branch '5.3' X-Git-Tag: 5.4.0_Alpha_1~55 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=5903c698563fa9ef662c0b55f0decf156e157494;p=GitHub%2FWoltLab%2FWCF.git Merge branch '5.3' --- 5903c698563fa9ef662c0b55f0decf156e157494 diff --cc wcfsetup/install/files/lib/data/article/ArticleAction.class.php index 9ff32064d3,d44c02325c..2e47b398ca --- a/wcfsetup/install/files/lib/data/article/ArticleAction.class.php +++ b/wcfsetup/install/files/lib/data/article/ArticleAction.class.php @@@ -29,889 -27,810 +29,889 @@@ use wcf\system\WCF /** * Executes article related actions. - * - * @author Marcel Werk - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @package WoltLabSuite\Core\Data\Article - * @since 3.0 - * - * @method ArticleEditor[] getObjects() - * @method ArticleEditor getSingleObject() + * + * @author Marcel Werk + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @package WoltLabSuite\Core\Data\Article + * @since 3.0 + * + * @method ArticleEditor[] getObjects() + * @method ArticleEditor getSingleObject() */ -class ArticleAction extends AbstractDatabaseObjectAction { - /** - * article editor instance - * @var ArticleEditor - */ - public $articleEditor; - - /** - * language object - * @var Language - */ - public $language; - - /** - * @inheritDoc - */ - protected $className = ArticleEditor::class; - - /** - * @inheritDoc - */ - protected $permissionsCreate = ['admin.content.article.canManageArticle']; - - /** - * @inheritDoc - */ - protected $permissionsDelete = ['admin.content.article.canManageArticle']; - - /** - * @inheritDoc - */ - protected $permissionsUpdate = ['admin.content.article.canManageArticle']; - - /** - * @inheritDoc - */ - protected $requireACP = ['create', 'update']; - - /** - * @inheritDoc - */ - protected $allowGuestAccess = ['markAllAsRead']; - - /** - * @inheritDoc - * @return Article - */ - public function create() { - /** @var Article $article */ - $article = parent::create(); - - // save article content - if (!empty($this->parameters['content'])) { - foreach ($this->parameters['content'] as $languageID => $content) { - if (!empty($content['htmlInputProcessor'])) { - /** @noinspection PhpUndefinedMethodInspection */ - $content['content'] = $content['htmlInputProcessor']->getHtml(); - } - - /** @var ArticleContent $articleContent */ - $articleContent = ArticleContentEditor::create([ - 'articleID' => $article->articleID, - 'languageID' => $languageID ?: null, - 'title' => $content['title'], - 'teaser' => $content['teaser'], - 'content' => $content['content'], - 'imageID' => $content['imageID'], - 'teaserImageID' => $content['teaserImageID'], - 'metaTitle' => $content['metaTitle'] ?? '', - 'metaDescription' => $content['metaDescription'] ?? '', - ]); - $articleContentEditor = new ArticleContentEditor($articleContent); - - // save tags - if (!empty($content['tags'])) { - TagEngine::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, $content['tags'], ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID())); - } - - // update search index - SearchIndexManager::getInstance()->set( - 'com.woltlab.wcf.article', - $articleContent->articleContentID, - $articleContent->content, - $articleContent->title, - $article->time, - $article->userID, - $article->username, - $languageID ?: null, - $articleContent->teaser - ); - - // save embedded objects - if (!empty($content['htmlInputProcessor'])) { - /** @noinspection PhpUndefinedMethodInspection */ - $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID); - if (MessageEmbeddedObjectManager::getInstance()->registerObjects($content['htmlInputProcessor'])) { - $articleContentEditor->update(['hasEmbeddedObjects' => 1]); - } - } - } - } - - // reset storage - if (ARTICLE_ENABLE_VISIT_TRACKING) { - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - } - - if ($article->publicationStatus == Article::PUBLISHED) { - ArticleEditor::updateArticleCounter([$article->userID => 1]); - - UserObjectWatchHandler::getInstance()->updateObject( - 'com.woltlab.wcf.article.category', - $article->getCategory()->categoryID, - 'article', - 'com.woltlab.wcf.article.notification', - new ArticleUserNotificationObject($article) - ); - - UserActivityEventHandler::getInstance()->fireEvent('com.woltlab.wcf.article.recentActivityEvent', $article->articleID, null, $article->userID, $article->time); - } - - return $article; - } - - /** - * @inheritDoc - */ - public function update() { - parent::update(); - - $isRevert = (!empty($this->parameters['isRevert'])); - - // update article content - if (!empty($this->parameters['content'])) { - foreach ($this->getObjects() as $article) { - $versionData = []; - $hasChanges = false; - - foreach ($this->parameters['content'] as $languageID => $content) { - if (!empty($content['htmlInputProcessor'])) { - /** @noinspection PhpUndefinedMethodInspection */ - $content['content'] = $content['htmlInputProcessor']->getHtml(); - } - - $articleContent = ArticleContent::getArticleContent($article->articleID, ($languageID ?: null)); - $articleContentEditor = null; - if ($articleContent !== null) { - // update - $articleContentEditor = new ArticleContentEditor($articleContent); - $articleContentEditor->update([ - 'title' => $content['title'], - 'teaser' => $content['teaser'], - 'content' => $content['content'], - 'imageID' => ($isRevert) ? $articleContent->imageID : $content['imageID'], - 'teaserImageID' => ($isRevert) ? $articleContent->teaserImageID : $content['teaserImageID'], - 'metaTitle' => $content['metaTitle'] ?? '', - 'metaDescription' => $content['metaDescription'] ?? '', - ]); - - $versionData[] = $articleContent; - if ($articleContent->content != $content['content'] || $articleContent->teaser != $content['teaser'] || $articleContent->title != $content['title']) { - $hasChanges = true; - } - - // delete tags - if (!$isRevert && empty($content['tags'])) { - TagEngine::getInstance()->deleteObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, ($languageID ?: null)); - } - } - else { - /** @var ArticleContent $articleContent */ - $articleContent = ArticleContentEditor::create([ - 'articleID' => $article->articleID, - 'languageID' => $languageID ?: null, - 'title' => $content['title'], - 'teaser' => $content['teaser'], - 'content' => $content['content'], - 'imageID' => ($isRevert) ? null : $content['imageID'], - 'teaserImageID' => ($isRevert) ? null : $content['teaserImageID'], - 'metaTitle' => $content['metaTitle'] ?? '', - 'metaDescription' => $content['metaDescription'] ?? '', - ]); - $articleContentEditor = new ArticleContentEditor($articleContent); - - $versionData[] = $articleContent; - $hasChanges = true; - } - - // save tags - if (!$isRevert && !empty($content['tags'])) { - TagEngine::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, $content['tags'], ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID())); - } - - // update search index - SearchIndexManager::getInstance()->set( - 'com.woltlab.wcf.article', - $articleContent->articleContentID, - isset($content['content']) ? $content['content'] : $articleContent->content, - isset($content['title']) ? $content['title'] : $articleContent->title, - $this->parameters['data']['time'] ?? $article->time, - $this->parameters['data']['userID'] ?? $article->userID, - $this->parameters['data']['username'] ?? $article->username, - $languageID ?: null, - isset($content['teaser']) ? $content['teaser'] : $articleContent->teaser - ); - - // save embedded objects - if (!empty($content['htmlInputProcessor'])) { - /** @noinspection PhpUndefinedMethodInspection */ - $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID); - if ($articleContent->hasEmbeddedObjects != MessageEmbeddedObjectManager::getInstance()->registerObjects($content['htmlInputProcessor'])) { - $articleContentEditor->update(['hasEmbeddedObjects' => $articleContent->hasEmbeddedObjects ? 0 : 1]); - } - } - } - - if ($hasChanges) { - $articleObj = new ArticleVersionTracker($article->getDecoratedObject()); - $articleObj->setContent($versionData); - VersionTracker::getInstance()->add('com.woltlab.wcf.article', $articleObj); - } - } - } - - // reset storage - if (ARTICLE_ENABLE_VISIT_TRACKING) { - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - } - - $publicationStatus = (isset($this->parameters['data']['publicationStatus'])) ? $this->parameters['data']['publicationStatus'] : null; - if ($publicationStatus !== null) { - $usersToArticles = $resetArticleIDs = []; - /** @var ArticleEditor $articleEditor */ - foreach ($this->objects as $articleEditor) { - if ($publicationStatus != $articleEditor->publicationStatus) { - // The article was published before or was now published. - if ($publicationStatus == Article::PUBLISHED || $articleEditor->publicationStatus == Article::PUBLISHED) { - if (!isset($usersToArticles[$articleEditor->userID])) { - $usersToArticles[$articleEditor->userID] = 0; - } - - $usersToArticles[$articleEditor->userID] += ($publicationStatus == Article::PUBLISHED) ? 1 : -1; - } - - if ($publicationStatus == Article::PUBLISHED) { - UserObjectWatchHandler::getInstance()->updateObject( - 'com.woltlab.wcf.article.category', - $articleEditor->getCategory()->categoryID, - 'article', - 'com.woltlab.wcf.article.notification', - new ArticleUserNotificationObject($articleEditor->getDecoratedObject()) - ); - - UserActivityEventHandler::getInstance()->fireEvent( - 'com.woltlab.wcf.article.recentActivityEvent', - $articleEditor->articleID, - null, - $this->parameters['data']['userID'] ?? $articleEditor->userID, - $this->parameters['data']['time'] ?? $articleEditor->time - ); - } - else { - $resetArticleIDs[] = $articleEditor->articleID; - } - } - } - - if (!empty($resetArticleIDs)) { - // delete user notifications - UserNotificationHandler::getInstance()->removeNotifications('com.woltlab.wcf.article.notification', $resetArticleIDs); - // delete recent activity events - UserActivityEventHandler::getInstance()->removeEvents('com.woltlab.wcf.article.recentActivityEvent', $resetArticleIDs); - } - - if (!empty($usersToArticles)) { - ArticleEditor::updateArticleCounter($usersToArticles); - } - } - - // update author in recent activities - if (isset($this->parameters['data']['userID'])) { - $sql = "UPDATE wcf".WCF_N."_user_activity_event SET userID = ? WHERE objectTypeID = ? AND objectID = ?"; - $statement = WCF::getDB()->prepareStatement($sql); - - foreach ($this->objects as $articleEditor) { - if ($articleEditor->userID != $this->parameters['data']['userID']) { - $statement->execute([ - $this->parameters['data']['userID'], - UserActivityEventHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article.recentActivityEvent'), - $articleEditor->articleID, - ]); - } - } - } - } - - /** - * Validates parameters to delete articles. - * - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validateDelete() { - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - foreach ($this->getObjects() as $article) { - if (!$article->canDelete()) { - throw new PermissionDeniedException(); - } - - if (!$article->isDeleted) { - throw new UserInputException('objectIDs'); - } - } - } - - /** - * @inheritDoc - */ - public function delete() { - $usersToArticles = $articleIDs = $articleContentIDs = []; - foreach ($this->getObjects() as $article) { - $articleIDs[] = $article->articleID; - foreach ($article->getArticleContents() as $articleContent) { - $articleContentIDs[] = $articleContent->articleContentID; - } - - if ($article->publicationStatus == Article::PUBLISHED) { - if (!isset($usersToArticles[$article->userID])) { - $usersToArticles[$article->userID] = 0; - } - $usersToArticles[$article->userID]--; - } - } - - // delete articles - parent::delete(); - - if (!empty($articleIDs)) { - // delete like data - ReactionHandler::getInstance()->removeReactions('com.woltlab.wcf.likeableArticle', $articleIDs); - // delete comments - CommentHandler::getInstance()->deleteObjects('com.woltlab.wcf.articleComment', $articleContentIDs); - // delete tag to object entries - TagEngine::getInstance()->deleteObjects('com.woltlab.wcf.article', $articleContentIDs); - // delete entry from search index - SearchIndexManager::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs); - // delete user notifications - UserNotificationHandler::getInstance()->removeNotifications('com.woltlab.wcf.article.notification', $articleIDs); - // delete recent activity events - UserActivityEventHandler::getInstance()->removeEvents('com.woltlab.wcf.article.recentActivityEvent', $articleIDs); - // delete embedded object references - MessageEmbeddedObjectManager::getInstance()->removeObjects('com.woltlab.wcf.article.content', $articleContentIDs); - // update wcf1_user.articles - ArticleEditor::updateArticleCounter($usersToArticles); - } - - $this->unmarkItems(); - - return [ - 'objectIDs' => $this->objectIDs, - 'redirectURL' => LinkHandler::getInstance()->getLink('ArticleList', ['isACP' => true]) - ]; - } - - /** - * Validates parameters to move articles to the trash bin. - * - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validateTrash() { - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - foreach ($this->getObjects() as $article) { - if (!$article->canDelete()) { - throw new PermissionDeniedException(); - } - - if ($article->isDeleted) { - throw new UserInputException('objectIDs'); - } - } - } - - /** - * Moves articles to the trash bin. - */ - public function trash() { - foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['isDeleted' => 1]); - } - - $this->unmarkItems(); - - // reset storage - if (ARTICLE_ENABLE_VISIT_TRACKING) { - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - } - - return ['objectIDs' => $this->objectIDs]; - } - - /** - * Validates parameters to restore articles. - * - * @throws UserInputException - */ - public function validateRestore() { - $this->validateDelete(); - } - - /** - * Restores articles. - */ - public function restore() { - foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['isDeleted' => 0]); - } - - $this->unmarkItems(); - - // reset storage - if (ARTICLE_ENABLE_VISIT_TRACKING) { - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - } - - return ['objectIDs' => $this->objectIDs]; - } - - /** - * Validates parameters to toggle between i18n and monolingual mode. - * - * @throws UserInputException - */ - public function validateToggleI18n() { - WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']); - - $this->articleEditor = $this->getSingleObject(); - if ($this->articleEditor->getDecoratedObject()->isMultilingual) { - $this->readInteger('languageID'); - $this->language = LanguageFactory::getInstance()->getLanguage($this->parameters['languageID']); - if ($this->language === null) { - throw new UserInputException('languageID'); - } - - $contents = $this->articleEditor->getArticleContents(); - if (!isset($contents[$this->language->languageID])) { - // there is no content - throw new UserInputException('languageID'); - } - } - } - - /** - * Toggles between i18n and monolingual mode. - */ - public function toggleI18n() { - $removeContent = []; - - // i18n -> monolingual - if ($this->articleEditor->getDecoratedObject()->isMultilingual) { - foreach ($this->articleEditor->getArticleContents() as $articleContent) { - if ($articleContent->languageID == $this->language->languageID) { - $articleContentEditor = new ArticleContentEditor($articleContent); - $articleContentEditor->update(['languageID' => null]); - } - else { - $removeContent[] = $articleContent; - } - } - } - else { - // monolingual -> i18n - $articleContent = $this->articleEditor->getArticleContent(); - $data = []; - foreach (LanguageFactory::getInstance()->getLanguages() as $language) { - $data[$language->languageID] = [ - 'title' => $articleContent->title, - 'teaser' => $articleContent->teaser, - 'content' => $articleContent->content, - 'imageID' => $articleContent->imageID ?: null, - 'teaserImageID' => $articleContent->teaserImageID ?: null - ]; - } - - $action = new ArticleAction([$this->articleEditor], 'update', ['content' => $data]); - $action->executeAction(); - - $removeContent[] = $articleContent; - } - - if (!empty($removeContent)) { - $action = new ArticleContentAction($removeContent, 'delete'); - $action->executeAction(); - } - - // flush edit history - VersionTracker::getInstance()->reset('com.woltlab.wcf.article', $this->articleEditor->getDecoratedObject()->articleID); - - // update article's i18n state - $this->articleEditor->update([ - 'isMultilingual' => ($this->articleEditor->getDecoratedObject()->isMultilingual) ? 0 : 1 - ]); - } - - /** - * Marks articles as read. - */ - public function markAsRead() { - if (empty($this->parameters['visitTime'])) { - $this->parameters['visitTime'] = TIME_NOW; - } - - if (empty($this->objects)) { - $this->readObjects(); - } - - foreach ($this->getObjects() as $article) { - VisitTracker::getInstance()->trackObjectVisit('com.woltlab.wcf.article', $article->articleID, $this->parameters['visitTime']); - } - - // reset storage - if (WCF::getUser()->userID) { - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); - } - } - - /** - * Marks all articles as read. - */ - public function markAllAsRead() { - VisitTracker::getInstance()->trackTypeVisit('com.woltlab.wcf.article'); - - // reset storage - if (WCF::getUser()->userID) { - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); - UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); - } - } - - /** - * Validates the mark all as read action. - */ - public function validateMarkAllAsRead() { - // does nothing - } - - /** - * Validates the `setCategory` action. - * - * @throws UserInputException - */ - public function validateSetCategory() { - WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']); - - $this->readBoolean('useMarkedArticles', true); - - // if no object ids are given, use clipboard handler - if (empty($this->objectIDs) && $this->parameters['useMarkedArticles']) { - $this->objectIDs = array_keys(ClipboardHandler::getInstance()->getMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article'))); - } - - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - $this->readInteger('categoryID'); - if (ArticleCategory::getCategory($this->parameters['categoryID']) === null) { - throw new UserInputException('categoryID'); - } - } - - /** - * Sets the category of articles. - */ - public function setCategory() { - foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['categoryID' => $this->parameters['categoryID']]); - } - - $this->unmarkItems(); - } - - /** - * Validates the `publish` action. - * - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validatePublish() { - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - foreach ($this->getObjects() as $article) { - if (!$article->canPublish()) { - throw new PermissionDeniedException(); - } - - if ($article->publicationStatus == Article::PUBLISHED) { - throw new UserInputException('objectIDs'); - } - } - } - - /** - * Publishes articles. - */ - public function publish() { - $usersToArticles = []; - foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update([ - 'time' => TIME_NOW, - 'publicationStatus' => Article::PUBLISHED, - 'publicationDate' => 0 - ]); - - if (!isset($usersToArticles[$articleEditor->userID])) { - $usersToArticles[$articleEditor->userID] = 0; - } - - $usersToArticles[$articleEditor->userID]++; - - UserObjectWatchHandler::getInstance()->updateObject( - 'com.woltlab.wcf.article.category', - $articleEditor->getCategory()->categoryID, - 'article', - 'com.woltlab.wcf.article.notification', - new ArticleUserNotificationObject($articleEditor->getDecoratedObject()) - ); - - UserActivityEventHandler::getInstance()->fireEvent('com.woltlab.wcf.article.recentActivityEvent', $articleEditor->articleID, null, $articleEditor->userID, TIME_NOW); - } - - ArticleEditor::updateArticleCounter($usersToArticles); - - // reset storage - if (ARTICLE_ENABLE_VISIT_TRACKING) { - UserStorageHandler::getInstance()->resetAll('unreadArticles'); - UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); - UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); - } - - $this->unmarkItems(); - } - - /** - * Validates the `unpublish` action. - * - * @throws PermissionDeniedException - * @throws UserInputException - */ - public function validateUnpublish() { - if (empty($this->objects)) { - $this->readObjects(); - - if (empty($this->objects)) { - throw new UserInputException('objectIDs'); - } - } - - foreach ($this->getObjects() as $article) { - if (!$article->canPublish()) { - throw new PermissionDeniedException(); - } - - if ($article->publicationStatus != Article::PUBLISHED) { - throw new UserInputException('objectIDs'); - } - } - } - - /** - * Unpublishes articles. - */ - public function unpublish() { - $usersToArticles = $articleIDs = []; - foreach ($this->getObjects() as $articleEditor) { - $articleEditor->update(['publicationStatus' => Article::UNPUBLISHED]); - - if (!isset($usersToArticles[$articleEditor->userID])) { - $usersToArticles[$articleEditor->userID] = 0; - } - - $usersToArticles[$articleEditor->userID]--; - - $articleIDs[] = $articleEditor->articleID; - } - - // delete user notifications - UserNotificationHandler::getInstance()->removeNotifications('com.woltlab.wcf.article.notification', $articleIDs); - - // delete recent activity events - UserActivityEventHandler::getInstance()->removeEvents('com.woltlab.wcf.article.recentActivityEvent', $articleIDs); - - ArticleEditor::updateArticleCounter($usersToArticles); - - $this->unmarkItems(); - } - - /** - * Validates parameters to search for an article by its localized title. - */ - public function validateSearch() { - $this->readString('searchString'); - } - - /** - * Searches for an article by its localized title. - * - * @return array list of matching articles - */ - public function search() { - $sql = "SELECT articleID - FROM wcf".WCF_N."_article_content - WHERE title LIKE ? - AND ( - languageID = ? - OR languageID IS NULL - ) - ORDER BY title"; - $statement = WCF::getDB()->prepareStatement($sql, 5); - $statement->execute([ - '%' . $this->parameters['searchString'] . '%', - WCF::getLanguage()->languageID, - ]); - - $articleIDs = []; - while ($articleID = $statement->fetchColumn()) { - $articleIDs[] = $articleID; - } - - $articleList = new ArticleList(); - $articleList->setObjectIDs($articleIDs); - $articleList->readObjects(); - - $articles = []; - foreach ($articleList as $article) { - $articles[] = [ - 'displayLink' => $article->getLink(), - 'name' => $article->getTitle(), - 'articleID' => $article->articleID, - ]; - } - - return $articles; - } - - /** - * Unmarks articles. - * - * @param integer[] $articleIDs - */ - protected function unmarkItems(array $articleIDs = []) { - if (empty($articleIDs)) { - foreach ($this->getObjects() as $article) { - $articleIDs[] = $article->articleID; - } - } - - if (!empty($articleIDs)) { - ClipboardHandler::getInstance()->unmark($articleIDs, ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article')); - } - } +class ArticleAction extends AbstractDatabaseObjectAction +{ + /** + * article editor instance + * @var ArticleEditor + */ + public $articleEditor; + + /** + * language object + * @var Language + */ + public $language; + + /** + * @inheritDoc + */ + protected $className = ArticleEditor::class; + + /** + * @inheritDoc + */ + protected $permissionsCreate = ['admin.content.article.canManageArticle']; + + /** + * @inheritDoc + */ + protected $permissionsDelete = ['admin.content.article.canManageArticle']; + + /** + * @inheritDoc + */ + protected $permissionsUpdate = ['admin.content.article.canManageArticle']; + + /** + * @inheritDoc + */ + protected $requireACP = ['create', 'update']; + + /** + * @inheritDoc + */ + protected $allowGuestAccess = ['markAllAsRead']; + + /** + * @inheritDoc + * @return Article + */ + public function create() + { + /** @var Article $article */ + $article = parent::create(); + + // save article content + if (!empty($this->parameters['content'])) { + foreach ($this->parameters['content'] as $languageID => $content) { + if (!empty($content['htmlInputProcessor'])) { + /** @noinspection PhpUndefinedMethodInspection */ + $content['content'] = $content['htmlInputProcessor']->getHtml(); + } + + /** @var ArticleContent $articleContent */ + $articleContent = ArticleContentEditor::create([ + 'articleID' => $article->articleID, + 'languageID' => $languageID ?: null, + 'title' => $content['title'], + 'teaser' => $content['teaser'], + 'content' => $content['content'], + 'imageID' => $content['imageID'], + 'teaserImageID' => $content['teaserImageID'], + 'metaTitle' => $content['metaTitle'] ?? '', + 'metaDescription' => $content['metaDescription'] ?? '', + ]); + $articleContentEditor = new ArticleContentEditor($articleContent); + + // save tags + if (!empty($content['tags'])) { + TagEngine::getInstance()->addObjectTags( + 'com.woltlab.wcf.article', + $articleContent->articleContentID, + $content['tags'], + ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID()) + ); + } + + // update search index + SearchIndexManager::getInstance()->set( + 'com.woltlab.wcf.article', + $articleContent->articleContentID, + $articleContent->content, + $articleContent->title, + $article->time, + $article->userID, + $article->username, + $languageID ?: null, + $articleContent->teaser + ); + + // save embedded objects + if (!empty($content['htmlInputProcessor'])) { + /** @noinspection PhpUndefinedMethodInspection */ + $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID); + if (MessageEmbeddedObjectManager::getInstance()->registerObjects($content['htmlInputProcessor'])) { + $articleContentEditor->update(['hasEmbeddedObjects' => 1]); + } + } + } + } + + // reset storage + if (ARTICLE_ENABLE_VISIT_TRACKING) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } + + if ($article->publicationStatus == Article::PUBLISHED) { + ArticleEditor::updateArticleCounter([$article->userID => 1]); + + UserObjectWatchHandler::getInstance()->updateObject( + 'com.woltlab.wcf.article.category', + $article->getCategory()->categoryID, + 'article', + 'com.woltlab.wcf.article.notification', + new ArticleUserNotificationObject($article) + ); + + UserActivityEventHandler::getInstance()->fireEvent( + 'com.woltlab.wcf.article.recentActivityEvent', + $article->articleID, + null, + $article->userID, + $article->time + ); + } + + return $article; + } + + /** + * @inheritDoc + */ + public function update() + { + parent::update(); + + $isRevert = (!empty($this->parameters['isRevert'])); + + // update article content + if (!empty($this->parameters['content'])) { + foreach ($this->getObjects() as $article) { + $versionData = []; + $hasChanges = false; + + foreach ($this->parameters['content'] as $languageID => $content) { + if (!empty($content['htmlInputProcessor'])) { + /** @noinspection PhpUndefinedMethodInspection */ + $content['content'] = $content['htmlInputProcessor']->getHtml(); + } + + $articleContent = ArticleContent::getArticleContent($article->articleID, ($languageID ?: null)); + $articleContentEditor = null; + if ($articleContent !== null) { + // update + $articleContentEditor = new ArticleContentEditor($articleContent); + $articleContentEditor->update([ + 'title' => $content['title'], + 'teaser' => $content['teaser'], + 'content' => $content['content'], + 'imageID' => ($isRevert) ? $articleContent->imageID : $content['imageID'], + 'teaserImageID' => ($isRevert) ? $articleContent->teaserImageID : $content['teaserImageID'], + 'metaTitle' => $content['metaTitle'] ?? '', + 'metaDescription' => $content['metaDescription'] ?? '', + ]); + + $versionData[] = $articleContent; + if ($articleContent->content != $content['content'] || $articleContent->teaser != $content['teaser'] || $articleContent->title != $content['title']) { + $hasChanges = true; + } + + // delete tags + if (!$isRevert && empty($content['tags'])) { + TagEngine::getInstance()->deleteObjectTags( + 'com.woltlab.wcf.article', + $articleContent->articleContentID, + ($languageID ?: null) + ); + } + } else { + /** @var ArticleContent $articleContent */ + $articleContent = ArticleContentEditor::create([ + 'articleID' => $article->articleID, + 'languageID' => $languageID ?: null, + 'title' => $content['title'], + 'teaser' => $content['teaser'], + 'content' => $content['content'], + 'imageID' => ($isRevert) ? null : $content['imageID'], + 'teaserImageID' => ($isRevert) ? null : $content['teaserImageID'], + 'metaTitle' => $content['metaTitle'] ?? '', + 'metaDescription' => $content['metaDescription'] ?? '', + ]); + $articleContentEditor = new ArticleContentEditor($articleContent); + + $versionData[] = $articleContent; + $hasChanges = true; + } + + // save tags + if (!$isRevert && !empty($content['tags'])) { + TagEngine::getInstance()->addObjectTags( + 'com.woltlab.wcf.article', + $articleContent->articleContentID, + $content['tags'], + ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID()) + ); + } + + // update search index + SearchIndexManager::getInstance()->set( + 'com.woltlab.wcf.article', + $articleContent->articleContentID, + $content['content'] ?? $articleContent->content, + $content['title'] ?? $articleContent->title, - $article->time, - $article->userID, - $article->username, ++ $this->parameters['data']['time'] ?? $article->time, ++ $this->parameters['data']['userID'] ?? $article->userID, ++ $this->parameters['data']['username'] ?? $article->username, + $languageID ?: null, + $content['teaser'] ?? $articleContent->teaser + ); + + // save embedded objects + if (!empty($content['htmlInputProcessor'])) { + /** @noinspection PhpUndefinedMethodInspection */ + $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID); + if ($articleContent->hasEmbeddedObjects != MessageEmbeddedObjectManager::getInstance()->registerObjects($content['htmlInputProcessor'])) { + $articleContentEditor->update(['hasEmbeddedObjects' => $articleContent->hasEmbeddedObjects ? 0 : 1]); + } + } + } + + if ($hasChanges) { + $articleObj = new ArticleVersionTracker($article->getDecoratedObject()); + $articleObj->setContent($versionData); + VersionTracker::getInstance()->add('com.woltlab.wcf.article', $articleObj); + } + } + } + + // reset storage + if (ARTICLE_ENABLE_VISIT_TRACKING) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } + + $publicationStatus = (isset($this->parameters['data']['publicationStatus'])) ? $this->parameters['data']['publicationStatus'] : null; + if ($publicationStatus !== null) { + $usersToArticles = $resetArticleIDs = []; + /** @var ArticleEditor $articleEditor */ + foreach ($this->objects as $articleEditor) { + if ($publicationStatus != $articleEditor->publicationStatus) { + // The article was published before or was now published. + if ($publicationStatus == Article::PUBLISHED || $articleEditor->publicationStatus == Article::PUBLISHED) { + if (!isset($usersToArticles[$articleEditor->userID])) { + $usersToArticles[$articleEditor->userID] = 0; + } + + $usersToArticles[$articleEditor->userID] += ($publicationStatus == Article::PUBLISHED) ? 1 : -1; + } + + if ($publicationStatus == Article::PUBLISHED) { + UserObjectWatchHandler::getInstance()->updateObject( + 'com.woltlab.wcf.article.category', + $articleEditor->getCategory()->categoryID, + 'article', + 'com.woltlab.wcf.article.notification', + new ArticleUserNotificationObject($articleEditor->getDecoratedObject()) + ); + + UserActivityEventHandler::getInstance()->fireEvent( + 'com.woltlab.wcf.article.recentActivityEvent', + $articleEditor->articleID, + null, - $articleEditor->userID, - $articleEditor->time ++ $this->parameters['data']['userID'] ?? $articleEditor->userID, ++ $this->parameters['data']['time'] ?? $articleEditor->time + ); + } else { + $resetArticleIDs[] = $articleEditor->articleID; + } + } + } + + if (!empty($resetArticleIDs)) { + // delete user notifications + UserNotificationHandler::getInstance()->removeNotifications( + 'com.woltlab.wcf.article.notification', + $resetArticleIDs + ); + // delete recent activity events + UserActivityEventHandler::getInstance()->removeEvents( + 'com.woltlab.wcf.article.recentActivityEvent', + $resetArticleIDs + ); + } + + if (!empty($usersToArticles)) { + ArticleEditor::updateArticleCounter($usersToArticles); + } + } + + // update author in recent activities + if (isset($this->parameters['data']['userID'])) { + $sql = "UPDATE wcf" . WCF_N . "_user_activity_event + SET userID = ? + WHERE objectTypeID = ? + AND objectID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + + foreach ($this->objects as $articleEditor) { + if ($articleEditor->userID != $this->parameters['data']['userID']) { + $statement->execute([ + $this->parameters['data']['userID'], + UserActivityEventHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article.recentActivityEvent'), + $articleEditor->articleID, + ]); + } + } + } + } + + /** + * Validates parameters to delete articles. + * + * @throws PermissionDeniedException + * @throws UserInputException + */ + public function validateDelete() + { + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + foreach ($this->getObjects() as $article) { + if (!$article->canDelete()) { + throw new PermissionDeniedException(); + } + + if (!$article->isDeleted) { + throw new UserInputException('objectIDs'); + } + } + } + + /** + * @inheritDoc + */ + public function delete() + { + $usersToArticles = $articleIDs = $articleContentIDs = []; + foreach ($this->getObjects() as $article) { + $articleIDs[] = $article->articleID; + foreach ($article->getArticleContents() as $articleContent) { + $articleContentIDs[] = $articleContent->articleContentID; + } + + if ($article->publicationStatus == Article::PUBLISHED) { + if (!isset($usersToArticles[$article->userID])) { + $usersToArticles[$article->userID] = 0; + } + $usersToArticles[$article->userID]--; + } + } + + // delete articles + parent::delete(); + + if (!empty($articleIDs)) { + // delete like data + ReactionHandler::getInstance()->removeReactions('com.woltlab.wcf.likeableArticle', $articleIDs); + // delete comments + CommentHandler::getInstance()->deleteObjects('com.woltlab.wcf.articleComment', $articleContentIDs); + // delete tag to object entries + TagEngine::getInstance()->deleteObjects('com.woltlab.wcf.article', $articleContentIDs); + // delete entry from search index + SearchIndexManager::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs); + // delete user notifications + UserNotificationHandler::getInstance()->removeNotifications( + 'com.woltlab.wcf.article.notification', + $articleIDs + ); + // delete recent activity events + UserActivityEventHandler::getInstance()->removeEvents( + 'com.woltlab.wcf.article.recentActivityEvent', + $articleIDs + ); + // delete embedded object references + MessageEmbeddedObjectManager::getInstance()->removeObjects( + 'com.woltlab.wcf.article.content', + $articleContentIDs + ); + // update wcf1_user.articles + ArticleEditor::updateArticleCounter($usersToArticles); + } + + $this->unmarkItems(); + + return [ + 'objectIDs' => $this->objectIDs, + 'redirectURL' => LinkHandler::getInstance()->getLink('ArticleList', ['isACP' => true]), + ]; + } + + /** + * Validates parameters to move articles to the trash bin. + * + * @throws PermissionDeniedException + * @throws UserInputException + */ + public function validateTrash() + { + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + foreach ($this->getObjects() as $article) { + if (!$article->canDelete()) { + throw new PermissionDeniedException(); + } + + if ($article->isDeleted) { + throw new UserInputException('objectIDs'); + } + } + } + + /** + * Moves articles to the trash bin. + */ + public function trash() + { + foreach ($this->getObjects() as $articleEditor) { + $articleEditor->update(['isDeleted' => 1]); + } + + $this->unmarkItems(); + + // reset storage + if (ARTICLE_ENABLE_VISIT_TRACKING) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } + + return ['objectIDs' => $this->objectIDs]; + } + + /** + * Validates parameters to restore articles. + * + * @throws UserInputException + */ + public function validateRestore() + { + $this->validateDelete(); + } + + /** + * Restores articles. + */ + public function restore() + { + foreach ($this->getObjects() as $articleEditor) { + $articleEditor->update(['isDeleted' => 0]); + } + + $this->unmarkItems(); + + // reset storage + if (ARTICLE_ENABLE_VISIT_TRACKING) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } + + return ['objectIDs' => $this->objectIDs]; + } + + /** + * Validates parameters to toggle between i18n and monolingual mode. + * + * @throws UserInputException + */ + public function validateToggleI18n() + { + WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']); + + $this->articleEditor = $this->getSingleObject(); + if ($this->articleEditor->getDecoratedObject()->isMultilingual) { + $this->readInteger('languageID'); + $this->language = LanguageFactory::getInstance()->getLanguage($this->parameters['languageID']); + if ($this->language === null) { + throw new UserInputException('languageID'); + } + + $contents = $this->articleEditor->getArticleContents(); + if (!isset($contents[$this->language->languageID])) { + // there is no content + throw new UserInputException('languageID'); + } + } + } + + /** + * Toggles between i18n and monolingual mode. + */ + public function toggleI18n() + { + $removeContent = []; + + // i18n -> monolingual + if ($this->articleEditor->getDecoratedObject()->isMultilingual) { + foreach ($this->articleEditor->getArticleContents() as $articleContent) { + if ($articleContent->languageID == $this->language->languageID) { + $articleContentEditor = new ArticleContentEditor($articleContent); + $articleContentEditor->update(['languageID' => null]); + } else { + $removeContent[] = $articleContent; + } + } + } else { + // monolingual -> i18n + $articleContent = $this->articleEditor->getArticleContent(); + $data = []; + foreach (LanguageFactory::getInstance()->getLanguages() as $language) { + $data[$language->languageID] = [ + 'title' => $articleContent->title, + 'teaser' => $articleContent->teaser, + 'content' => $articleContent->content, + 'imageID' => $articleContent->imageID ?: null, + 'teaserImageID' => $articleContent->teaserImageID ?: null, + ]; + } + + $action = new self([$this->articleEditor], 'update', ['content' => $data]); + $action->executeAction(); + + $removeContent[] = $articleContent; + } + + if (!empty($removeContent)) { + $action = new ArticleContentAction($removeContent, 'delete'); + $action->executeAction(); + } + + // flush edit history + VersionTracker::getInstance()->reset( + 'com.woltlab.wcf.article', + $this->articleEditor->getDecoratedObject()->articleID + ); + + // update article's i18n state + $this->articleEditor->update([ + 'isMultilingual' => ($this->articleEditor->getDecoratedObject()->isMultilingual) ? 0 : 1, + ]); + } + + /** + * Marks articles as read. + */ + public function markAsRead() + { + if (empty($this->parameters['visitTime'])) { + $this->parameters['visitTime'] = TIME_NOW; + } + + if (empty($this->objects)) { + $this->readObjects(); + } + + foreach ($this->getObjects() as $article) { + VisitTracker::getInstance()->trackObjectVisit( + 'com.woltlab.wcf.article', + $article->articleID, + $this->parameters['visitTime'] + ); + } + + // reset storage + if (WCF::getUser()->userID) { + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); + } + } + + /** + * Marks all articles as read. + */ + public function markAllAsRead() + { + VisitTracker::getInstance()->trackTypeVisit('com.woltlab.wcf.article'); + + // reset storage + if (WCF::getUser()->userID) { + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles'); + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadWatchedArticles'); + UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticlesByCategory'); + } + } + + /** + * Validates the mark all as read action. + */ + public function validateMarkAllAsRead() + { + // does nothing + } + + /** + * Validates the `setCategory` action. + * + * @throws UserInputException + */ + public function validateSetCategory() + { + WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']); + + $this->readBoolean('useMarkedArticles', true); + + // if no object ids are given, use clipboard handler + if (empty($this->objectIDs) && $this->parameters['useMarkedArticles']) { + $this->objectIDs = \array_keys(ClipboardHandler::getInstance()->getMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article'))); + } + + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + $this->readInteger('categoryID'); + if (ArticleCategory::getCategory($this->parameters['categoryID']) === null) { + throw new UserInputException('categoryID'); + } + } + + /** + * Sets the category of articles. + */ + public function setCategory() + { + foreach ($this->getObjects() as $articleEditor) { + $articleEditor->update(['categoryID' => $this->parameters['categoryID']]); + } + + $this->unmarkItems(); + } + + /** + * Validates the `publish` action. + * + * @throws PermissionDeniedException + * @throws UserInputException + */ + public function validatePublish() + { + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + foreach ($this->getObjects() as $article) { + if (!$article->canPublish()) { + throw new PermissionDeniedException(); + } + + if ($article->publicationStatus == Article::PUBLISHED) { + throw new UserInputException('objectIDs'); + } + } + } + + /** + * Publishes articles. + */ + public function publish() + { + $usersToArticles = []; + foreach ($this->getObjects() as $articleEditor) { + $articleEditor->update([ + 'time' => TIME_NOW, + 'publicationStatus' => Article::PUBLISHED, + 'publicationDate' => 0, + ]); + + if (!isset($usersToArticles[$articleEditor->userID])) { + $usersToArticles[$articleEditor->userID] = 0; + } + + $usersToArticles[$articleEditor->userID]++; + + UserObjectWatchHandler::getInstance()->updateObject( + 'com.woltlab.wcf.article.category', + $articleEditor->getCategory()->categoryID, + 'article', + 'com.woltlab.wcf.article.notification', + new ArticleUserNotificationObject($articleEditor->getDecoratedObject()) + ); + + UserActivityEventHandler::getInstance()->fireEvent( + 'com.woltlab.wcf.article.recentActivityEvent', + $articleEditor->articleID, + null, + $articleEditor->userID, + TIME_NOW + ); + } + + ArticleEditor::updateArticleCounter($usersToArticles); + + // reset storage + if (ARTICLE_ENABLE_VISIT_TRACKING) { + UserStorageHandler::getInstance()->resetAll('unreadArticles'); + UserStorageHandler::getInstance()->resetAll('unreadWatchedArticles'); + UserStorageHandler::getInstance()->resetAll('unreadArticlesByCategory'); + } + + $this->unmarkItems(); + } + + /** + * Validates the `unpublish` action. + * + * @throws PermissionDeniedException + * @throws UserInputException + */ + public function validateUnpublish() + { + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + foreach ($this->getObjects() as $article) { + if (!$article->canPublish()) { + throw new PermissionDeniedException(); + } + + if ($article->publicationStatus != Article::PUBLISHED) { + throw new UserInputException('objectIDs'); + } + } + } + + /** + * Unpublishes articles. + */ + public function unpublish() + { + $usersToArticles = $articleIDs = []; + foreach ($this->getObjects() as $articleEditor) { + $articleEditor->update(['publicationStatus' => Article::UNPUBLISHED]); + + if (!isset($usersToArticles[$articleEditor->userID])) { + $usersToArticles[$articleEditor->userID] = 0; + } + + $usersToArticles[$articleEditor->userID]--; + + $articleIDs[] = $articleEditor->articleID; + } + + // delete user notifications + UserNotificationHandler::getInstance()->removeNotifications( + 'com.woltlab.wcf.article.notification', + $articleIDs + ); + + // delete recent activity events + UserActivityEventHandler::getInstance()->removeEvents( + 'com.woltlab.wcf.article.recentActivityEvent', + $articleIDs + ); + + ArticleEditor::updateArticleCounter($usersToArticles); + + $this->unmarkItems(); + } + + /** + * Validates parameters to search for an article by its localized title. + */ + public function validateSearch() + { + $this->readString('searchString'); + } + + /** + * Searches for an article by its localized title. + * + * @return array list of matching articles + */ + public function search() + { + $sql = "SELECT articleID + FROM wcf" . WCF_N . "_article_content + WHERE title LIKE ? + AND ( + languageID = ? + OR languageID IS NULL + ) + ORDER BY title"; + $statement = WCF::getDB()->prepareStatement($sql, 5); + $statement->execute([ + '%' . $this->parameters['searchString'] . '%', + WCF::getLanguage()->languageID, + ]); + + $articleIDs = []; + while ($articleID = $statement->fetchColumn()) { + $articleIDs[] = $articleID; + } + + $articleList = new ArticleList(); + $articleList->setObjectIDs($articleIDs); + $articleList->readObjects(); + + $articles = []; + foreach ($articleList as $article) { + $articles[] = [ + 'displayLink' => $article->getLink(), + 'name' => $article->getTitle(), + 'articleID' => $article->articleID, + ]; + } + + return $articles; + } + + /** + * Unmarks articles. + * + * @param int[] $articleIDs + */ + protected function unmarkItems(array $articleIDs = []) + { + if (empty($articleIDs)) { + foreach ($this->getObjects() as $article) { + $articleIDs[] = $article->articleID; + } + } + + if (!empty($articleIDs)) { + ClipboardHandler::getInstance()->unmark( + $articleIDs, + ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.article') + ); + } + } } diff --cc wcfsetup/install/files/lib/system/form/builder/field/TextFormField.class.php index 5cb6d94c3f,dd5708573f..5985f451ba --- a/wcfsetup/install/files/lib/system/form/builder/field/TextFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/TextFormField.class.php @@@ -8,112 -6,72 +8,112 @@@ use wcf\system\language\LanguageFactory /** * Implementation of a form field for single-line text values. - * - * @author Matthias Schmidt - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @package WoltLabSuite\Core\System\Form\Builder\Field - * @since 5.2 + * + * @author Matthias Schmidt + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @package WoltLabSuite\Core\System\Form\Builder\Field + * @since 5.2 */ -class TextFormField extends AbstractFormField implements IAutoFocusFormField, II18nFormField, IImmutableFormField, IMaximumLengthFormField, IMinimumLengthFormField, IPlaceholderFormField { - use TAutoFocusFormField; - use TImmutableFormField; - use TI18nFormField { - validate as protected i18nValidate; - } - use TMaximumLengthFormField; - use TMinimumLengthFormField; - use TPlaceholderFormField; - - /** - * @inheritDoc - */ - protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Value'; - - /** - * @inheritDoc - */ - protected $templateName = '__textFormField'; - - /** - * @inheritDoc - */ - public function validate() { - if ($this->isI18n()) { - $this->i18nValidate(); - - if (empty($this->getValidationErrors())) { - $value = $this->getValue(); - if ($this->hasPlainValue()) { - $this->validateText($value); - } - else { - foreach ($value as $languageID => $languageValue) { - $this->validateText($languageValue, LanguageFactory::getInstance()->getLanguage($languageID)); - } - } - } - } - else { - if ($this->isRequired() && ($this->getValue() === null || $this->getValue() === '')) { - $this->addValidationError(new FormFieldValidationError('empty')); - } - else if ($this->getValue() !== null && $this->getValue() !== '') { - $this->validateText($this->getValue()); - } - } - - parent::validate(); - } - - /** - * Checks the length of the given text with the given language. - * - * @param string $text validated text - * @param null|Language $language language of validated text or `null` for monolingual text - */ - protected function validateText($text, Language $language = null) { - $this->validateMinimumLength($text, $language); - $this->validateMaximumLength($text, $language); - } +class TextFormField extends AbstractFormField implements + IAttributeFormField, + IAutoCompleteFormField, + IAutoFocusFormField, + ICssClassFormField, + II18nFormField, + IImmutableFormField, + IInputModeFormField, + IMaximumLengthFormField, + IMinimumLengthFormField, + IPatternFormField, + IPlaceholderFormField +{ + use TInputAttributeFormField; + use TTextAutoCompleteFormField; + use TAutoFocusFormField; + use TCssClassFormField; + use TImmutableFormField; + use TInputModeFormField; + use TI18nFormField { + validate as protected i18nValidate; + } + use TMaximumLengthFormField; + use TMinimumLengthFormField; + use TPatternFormField; + use TPlaceholderFormField; + + /** + * @inheritDoc + */ + protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Value'; + + /** + * @inheritDoc + */ + protected $templateName = '__textFormField'; + + /** + * Creates a new instance of `TextFormField`. + */ + public function __construct() + { + $this->addFieldClass('long'); + } + + /** + * @inheritDoc + */ + protected function getValidInputModes(): array + { + return [ + 'text', + 'tel', + 'url', + 'email', + 'numeric', + 'decimal', + 'search', + ]; + } + + /** + * @inheritDoc + */ + public function validate() + { + if ($this->isI18n()) { + $this->i18nValidate(); + + if (empty($this->getValidationErrors())) { + $value = $this->getValue(); + if ($this->hasPlainValue()) { + $this->validateText($value); + } else { + foreach ($value as $languageID => $languageValue) { + $this->validateText($languageValue, LanguageFactory::getInstance()->getLanguage($languageID)); + } + } + } + } else { + if ($this->isRequired() && ($this->getValue() === null || $this->getValue() === '')) { + $this->addValidationError(new FormFieldValidationError('empty')); - } else { ++ } elseif ($this->getValue() !== null && $this->getValue() !== '') { + $this->validateText($this->getValue()); + } + } + + parent::validate(); + } + + /** + * Checks the length of the given text with the given language. + * + * @param string $text validated text + * @param null|Language $language language of validated text or `null` for monolingual text + */ + protected function validateText($text, ?Language $language = null) + { + $this->validateMinimumLength($text, $language); + $this->validateMaximumLength($text, $language); + } } diff --cc wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php index dd130ac90c,d9fb610971..c1a3bf5a57 --- a/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/wysiwyg/WysiwygFormField.class.php @@@ -25,443 -21,394 +25,445 @@@ use wcf\util\StringUtil /** * Implementation of a form field for wysiwyg editors. - * - * @author Matthias Schmidt - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @package WoltLabSuite\Core\System\Form\Builder\Field - * @since 5.2 + * + * @author Matthias Schmidt + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @package WoltLabSuite\Core\System\Form\Builder\Field + * @since 5.2 */ -class WysiwygFormField extends AbstractFormField implements IMaximumLengthFormField, IMinimumLengthFormField, IObjectTypeFormNode { - use TMaximumLengthFormField; - use TMinimumLengthFormField; - use TObjectTypeFormNode; - - /** - * identifier used to autosave the field value; if empty, autosave is disabled - * @var string - */ - protected $autosaveId = ''; - - /** - * input processor containing the wysiwyg text - * @var HtmlInputProcessor - */ - protected $htmlInputProcessor; - - /** - * last time the field has been edited; if `0`, the last edit time is unknown - * @var int - */ - protected $lastEditTime = 0; - - /** - * quote-related data used to create the JavaScript quote manager - * @var null|array - */ - protected $quoteData; - - /** - * is `true` if this form field supports attachments, otherwise `false` - * @var boolean - */ - protected $supportAttachments = false; - - /** - * is `true` if this form field supports mentions, otherwise `false` - * @var boolean - */ - protected $supportMentions = false; - - /** - * is `true` if this form field supports quotes, otherwise `false` - * @var boolean - */ - protected $supportQuotes = false; - - /** - * @inheritDoc - */ - protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Value'; - - /** - * @inheritDoc - */ - protected $templateName = '__wysiwygFormField'; - - /** - * Sets the identifier used to autosave the field value and returns this field. - * - * @param string $autosaveId identifier used to autosave field value - * @return WysiwygFormField this field - */ - public function autosaveId($autosaveId) { - $this->autosaveId = $autosaveId; - - return $this; - } - - /** - * @inheritDoc - */ - public function cleanup() { - MessageQuoteManager::getInstance()->saved(); - } - - /** - * Returns the identifier used to autosave the field value. If autosave is disabled, - * an empty string is returned. - * - * @return string - */ - public function getAutosaveId() { - return $this->autosaveId; - } - - /** - * @inheritDoc - */ - public function getFieldHtml() { - if ($this->supportsQuotes()) { - MessageQuoteManager::getInstance()->assignVariables(); - } - - /** @noinspection PhpUndefinedFieldInspection */ - $disallowedBBCodesPermission = $this->getObjectType()->disallowedBBCodesPermission; - if ($disallowedBBCodesPermission === null) { - $disallowedBBCodesPermission = 'user.message.disallowedBBCodes'; - } - - BBCodeHandler::getInstance()->setDisallowedBBCodes(explode( - ',', - WCF::getSession()->getPermission($disallowedBBCodesPermission) - )); - - return parent::getFieldHtml(); - } - - /** - * @inheritDoc - */ - public function getObjectTypeDefinition() { - return 'com.woltlab.wcf.message'; - } - - /** - * Returns the last time the field has been edited. If no last edit time has - * been set, `0` is returned. - * - * @return int - */ - public function getLastEditTime() { - return $this->lastEditTime; - } - - /** - * Returns all quote data or specific quote data if an argument is given. - * - * @param null|string $index quote data index - * @return string[]|string - * - * @throws \BadMethodCallException if quotes are not supported for this field - * @throws \InvalidArgumentException if unknown quote data is requested - */ - public function getQuoteData($index = null) { - if (!$this->supportQuotes()) { - throw new \BadMethodCallException("Quotes are not supported."); - } - - if ($index === null) { - return $this->quoteData; - } - - if (!isset($this->quoteData[$index])) { - throw new \InvalidArgumentException("Unknown quote data '{$index}'."); - } - - return $this->quoteData[$index]; - } - - /** - * @inheritDoc - */ - public function hasSaveValue() { - return false; - } - - /** - * Sets the last time this field has been edited and returns this field. - * - * @param int $lastEditTime last time field has been edited - * @return WysiwygFormField this field - */ - public function lastEditTime($lastEditTime) { - $this->lastEditTime = $lastEditTime; - - return $this; - } - - /** - * @inheritDoc - */ - public function populate() { - parent::populate(); - - $this->getDocument()->getDataHandler()->addProcessor(new CustomFormDataProcessor('wysiwyg', function(IFormDocument $document, array $parameters) { - if ($this->checkDependencies()) { - $parameters[$this->getObjectProperty() . '_htmlInputProcessor'] = $this->htmlInputProcessor; - } - - return $parameters; - })); - - return $this; - } - - /** - * Sets the data required for advanced quote support for when quotable content is present - * on the active page and returns this field. - * - * Calling this method automatically enables quote support for this field. - * - * @param string $objectType name of the relevant `com.woltlab.wcf.message.quote` object type - * @param string $actionClass action class implementing `wcf\data\IMessageQuoteAction` - * @param string[] $selectors selectors for the quotable content (required keys: `container`, `messageBody`, and `messageContent`) - * @return static - * - * @throws \InvalidArgumentException if any of the given arguments is invalid - */ - public function quoteData($objectType, $actionClass, array $selectors = []) { - if (ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.message.quote', $objectType) === null) { - throw new \InvalidArgumentException("Unknown message quote object type '{$objectType}'."); - } - - if (!class_exists($actionClass)) { - throw new \InvalidArgumentException("Unknown class '{$actionClass}'"); - } - if (!is_subclass_of($actionClass, IMessageQuoteAction::class)) { - throw new \InvalidArgumentException("'{$actionClass}' does not implement '" . IMessageQuoteAction::class . "'."); - } - - if (!empty($selectors)) { - foreach (['container', 'messageBody', 'messageContent'] as $selector) { - if (!isset($selectors[$selector])) { - throw new \InvalidArgumentException("Missing selector '{$selector}'."); - } - } - } - - $this->supportQuotes(); - - $this->quoteData = [ - 'actionClass' => $actionClass, - 'objectType' => $objectType, - 'selectors' => $selectors - ]; - - return $this; - } - - /** - * @inheritDoc - */ - public function readValue() { - if ($this->getDocument()->hasRequestData($this->getPrefixedId())) { - $value = $this->getDocument()->getRequestData($this->getPrefixedId()); - - if (is_string($value)) { - $this->value = StringUtil::trim($value); - } - } - - if ($this->supportsQuotes()) { - MessageQuoteManager::getInstance()->readFormParameters(); - } - - return $this; - } - - /** - * Sets if the form field supports attachments and returns this field. - * - * @param boolean $supportAttachments - * @return WysiwygFormField this field - */ - public function supportAttachments($supportAttachments = true) { - $this->supportAttachments = $supportAttachments; - - return $this; - } - - /** - * Sets if the form field supports mentions and returns this field. - * - * @param boolean $supportMentions - * @return WysiwygFormField this field - */ - public function supportMentions($supportMentions = true) { - $this->supportMentions = $supportMentions; - - return $this; - } - - /** - * Sets if the form field supports quotes and returns this field. - * - * @param boolean $supportQuotes - * @return WysiwygFormField this field - */ - public function supportQuotes($supportQuotes = true) { - $this->supportQuotes = $supportQuotes; - - if (!$this->supportsQuotes()) { - // unset previously set quote data - $this->quoteData = null; - } - else { - MessageQuoteManager::getInstance()->readParameters(); - } - - return $this; - } - - /** - * Returns `true` if the form field supports attachments and returns `false` otherwise. - * - * Important: If this method returns `true`, it does not necessarily mean that attachment - * support will also work as that is the task of `WysiwygAttachmentFormField`. This method - * is primarily relevant to inform the JavaScript API that the field supports attachments - * so that the relevant editor plugin is loaded. - * - * By default, attachments are not supported. - * - * @return boolean - */ - public function supportsAttachments() { - return $this->supportAttachments; - } - - /** - * Returns `true` if the form field supports mentions and returns `false` otherwise. - * - * By default, mentions are not supported. - * - * @return boolean - */ - public function supportsMentions() { - return $this->supportMentions; - } - - /** - * Returns `true` if the form field supports quotes and returns `false` otherwise. - * - * By default, quotes are not supported. - * - * @return boolean - */ - public function supportsQuotes() { - return $this->supportQuotes; - } - - /** - * @inheritDoc - */ - public function validate() { - /** @noinspection PhpUndefinedFieldInspection */ - $disallowedBBCodesPermission = $this->getObjectType()->disallowedBBCodesPermission; - if ($disallowedBBCodesPermission === null) { - $disallowedBBCodesPermission = 'user.message.disallowedBBCodes'; - } - - BBCodeHandler::getInstance()->setDisallowedBBCodes(explode( - ',', - WCF::getSession()->getPermission($disallowedBBCodesPermission) - )); - - $this->htmlInputProcessor = new HtmlInputProcessor(); - $this->htmlInputProcessor->process($this->getValue(), $this->getObjectType()->objectType); - - if ($this->isRequired() && $this->htmlInputProcessor->appearsToBeEmpty()) { - $this->addValidationError(new FormFieldValidationError('empty')); - } - else { - $disallowedBBCodes = $this->htmlInputProcessor->validate(); - if (!empty($disallowedBBCodes)) { - $this->addValidationError(new FormFieldValidationError( - 'disallowedBBCodes', - 'wcf.message.error.disallowedBBCodes', - ['disallowedBBCodes' => $disallowedBBCodes] - )); - } - else { - $message = $this->htmlInputProcessor->getTextContent(); - if ($message !== '') { - $this->validateMinimumLength($message); - $this->validateMaximumLength($message); - - if (empty($this->getValidationErrors()) && ENABLE_CENSORSHIP) { - $result = Censorship::getInstance()->test($message); - if ($result) { - $this->addValidationError(new FormFieldValidationError( - 'censoredWords', - 'wcf.message.error.censoredWordsFound', - ['censoredWords' => $result] - )); - } - } - } - } - } - - parent::validate(); - } +class WysiwygFormField extends AbstractFormField implements + IAttributeFormField, + IMaximumLengthFormField, + IMinimumLengthFormField, + IObjectTypeFormNode +{ + use TInputAttributeFormField { + getReservedFieldAttributes as private inputGetReservedFieldAttributes; + } + use TMaximumLengthFormField; + use TMinimumLengthFormField; + use TObjectTypeFormNode; + + /** + * identifier used to autosave the field value; if empty, autosave is disabled + * @var string + */ + protected $autosaveId = ''; + + /** + * input processor containing the wysiwyg text + * @var HtmlInputProcessor + */ + protected $htmlInputProcessor; + + /** + * last time the field has been edited; if `0`, the last edit time is unknown + * @var int + */ + protected $lastEditTime = 0; + + /** + * quote-related data used to create the JavaScript quote manager + * @var null|array + */ + protected $quoteData; + + /** + * is `true` if this form field supports attachments, otherwise `false` + * @var bool + */ + protected $supportAttachments = false; + + /** + * is `true` if this form field supports mentions, otherwise `false` + * @var bool + */ + protected $supportMentions = false; + + /** + * is `true` if this form field supports quotes, otherwise `false` + * @var bool + */ + protected $supportQuotes = false; + + /** + * @inheritDoc + */ + protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Value'; + + /** + * @inheritDoc + */ + protected $templateName = '__wysiwygFormField'; + + /** + * Sets the identifier used to autosave the field value and returns this field. + * + * @param string $autosaveId identifier used to autosave field value + * @return WysiwygFormField this field + */ + public function autosaveId($autosaveId) + { + $this->autosaveId = $autosaveId; + + return $this; + } + + /** + * @inheritDoc + */ + public function cleanup() + { + MessageQuoteManager::getInstance()->saved(); + } + + /** + * Returns the identifier used to autosave the field value. If autosave is disabled, + * an empty string is returned. + * + * @return string + */ + public function getAutosaveId() + { + return $this->autosaveId; + } + + /** + * @inheritDoc + */ + public function getFieldHtml() + { + if ($this->supportsQuotes()) { + MessageQuoteManager::getInstance()->assignVariables(); + } + + /** @noinspection PhpUndefinedFieldInspection */ + $disallowedBBCodesPermission = $this->getObjectType()->disallowedBBCodesPermission; + if ($disallowedBBCodesPermission === null) { + $disallowedBBCodesPermission = 'user.message.disallowedBBCodes'; + } + + BBCodeHandler::getInstance()->setDisallowedBBCodes(\explode( + ',', + WCF::getSession()->getPermission($disallowedBBCodesPermission) + )); + + return parent::getFieldHtml(); + } + + /** + * @inheritDoc + */ + public function getObjectTypeDefinition() + { + return 'com.woltlab.wcf.message'; + } + + /** + * Returns the last time the field has been edited. If no last edit time has + * been set, `0` is returned. + * + * @return int + */ + public function getLastEditTime() + { + return $this->lastEditTime; + } + + /** + * Returns all quote data or specific quote data if an argument is given. + * + * @param null|string $index quote data index + * @return string[]|string + * + * @throws \BadMethodCallException if quotes are not supported for this field + * @throws \InvalidArgumentException if unknown quote data is requested + */ + public function getQuoteData($index = null) + { + if (!$this->supportQuotes()) { + throw new \BadMethodCallException("Quotes are not supported."); + } + + if ($index === null) { + return $this->quoteData; + } + + if (!isset($this->quoteData[$index])) { + throw new \InvalidArgumentException("Unknown quote data '{$index}'."); + } + + return $this->quoteData[$index]; + } + + /** + * @inheritDoc + */ + public function hasSaveValue() + { + return false; + } + + /** + * Sets the last time this field has been edited and returns this field. + * + * @param int $lastEditTime last time field has been edited + * @return WysiwygFormField this field + */ + public function lastEditTime($lastEditTime) + { + $this->lastEditTime = $lastEditTime; + + return $this; + } + + /** + * @inheritDoc + */ + public function populate() + { + parent::populate(); + + $this->getDocument()->getDataHandler()->addProcessor(new CustomFormDataProcessor( + 'wysiwyg', + function (IFormDocument $document, array $parameters) { + if ($this->checkDependencies()) { + $parameters[$this->getObjectProperty() . '_htmlInputProcessor'] = $this->htmlInputProcessor; + } + + return $parameters; + } + )); + + return $this; + } + + /** + * Sets the data required for advanced quote support for when quotable content is present + * on the active page and returns this field. + * + * Calling this method automatically enables quote support for this field. + * + * @param string $objectType name of the relevant `com.woltlab.wcf.message.quote` object type + * @param string $actionClass action class implementing `wcf\data\IMessageQuoteAction` + * @param string[] $selectors selectors for the quotable content (required keys: `container`, `messageBody`, and `messageContent`) + * @return static + * + * @throws \InvalidArgumentException if any of the given arguments is invalid + */ + public function quoteData($objectType, $actionClass, array $selectors = []) + { + if ( + ObjectTypeCache::getInstance()->getObjectTypeByName( + 'com.woltlab.wcf.message.quote', + $objectType + ) === null + ) { + throw new \InvalidArgumentException("Unknown message quote object type '{$objectType}'."); + } + + if (!\class_exists($actionClass)) { + throw new \InvalidArgumentException("Unknown class '{$actionClass}'"); + } + if (!\is_subclass_of($actionClass, IMessageQuoteAction::class)) { + throw new \InvalidArgumentException( + "'{$actionClass}' does not implement '" . IMessageQuoteAction::class . "'." + ); + } + + if (!empty($selectors)) { + foreach (['container', 'messageBody', 'messageContent'] as $selector) { + if (!isset($selectors[$selector])) { + throw new \InvalidArgumentException("Missing selector '{$selector}'."); + } + } + } + + $this->supportQuotes(); + + $this->quoteData = [ + 'actionClass' => $actionClass, + 'objectType' => $objectType, + 'selectors' => $selectors, + ]; + + return $this; + } + + /** + * @inheritDoc + */ + public function readValue() + { + if ($this->getDocument()->hasRequestData($this->getPrefixedId())) { + $value = $this->getDocument()->getRequestData($this->getPrefixedId()); + + if (\is_string($value)) { + $this->value = StringUtil::trim($value); + } + } + + if ($this->supportsQuotes()) { + MessageQuoteManager::getInstance()->readFormParameters(); + } + + return $this; + } + + /** + * Sets if the form field supports attachments and returns this field. + * + * @param bool $supportAttachments + * @return WysiwygFormField this field + */ + public function supportAttachments($supportAttachments = true) + { + $this->supportAttachments = $supportAttachments; + + return $this; + } + + /** + * Sets if the form field supports mentions and returns this field. + * + * @param bool $supportMentions + * @return WysiwygFormField this field + */ + public function supportMentions($supportMentions = true) + { + $this->supportMentions = $supportMentions; + + return $this; + } + + /** + * Sets if the form field supports quotes and returns this field. + * + * @param bool $supportQuotes + * @return WysiwygFormField this field + */ + public function supportQuotes($supportQuotes = true) + { + $this->supportQuotes = $supportQuotes; + + if (!$this->supportsQuotes()) { + // unset previously set quote data + $this->quoteData = null; + } else { + MessageQuoteManager::getInstance()->readParameters(); + } + + return $this; + } + + /** + * Returns `true` if the form field supports attachments and returns `false` otherwise. + * + * Important: If this method returns `true`, it does not necessarily mean that attachment + * support will also work as that is the task of `WysiwygAttachmentFormField`. This method + * is primarily relevant to inform the JavaScript API that the field supports attachments + * so that the relevant editor plugin is loaded. + * + * By default, attachments are not supported. + * + * @return bool + */ + public function supportsAttachments() + { + return $this->supportAttachments; + } + + /** + * Returns `true` if the form field supports mentions and returns `false` otherwise. + * + * By default, mentions are not supported. + * + * @return bool + */ + public function supportsMentions() + { + return $this->supportMentions; + } + + /** + * Returns `true` if the form field supports quotes and returns `false` otherwise. + * + * By default, quotes are not supported. + * + * @return bool + */ + public function supportsQuotes() + { + return $this->supportQuotes; + } + + /** + * @inheritDoc + */ + public function validate() + { + /** @noinspection PhpUndefinedFieldInspection */ + $disallowedBBCodesPermission = $this->getObjectType()->disallowedBBCodesPermission; + if ($disallowedBBCodesPermission === null) { + $disallowedBBCodesPermission = 'user.message.disallowedBBCodes'; + } + + BBCodeHandler::getInstance()->setDisallowedBBCodes(\explode( + ',', + WCF::getSession()->getPermission($disallowedBBCodesPermission) + )); + + $this->htmlInputProcessor = new HtmlInputProcessor(); + $this->htmlInputProcessor->process($this->getValue(), $this->getObjectType()->objectType); + + if ($this->isRequired() && $this->htmlInputProcessor->appearsToBeEmpty()) { + $this->addValidationError(new FormFieldValidationError('empty')); + } else { + $disallowedBBCodes = $this->htmlInputProcessor->validate(); + if (!empty($disallowedBBCodes)) { + $this->addValidationError(new FormFieldValidationError( + 'disallowedBBCodes', + 'wcf.message.error.disallowedBBCodes', + ['disallowedBBCodes' => $disallowedBBCodes] + )); + } else { + $message = $this->htmlInputProcessor->getTextContent(); - $this->validateMinimumLength($message); - $this->validateMaximumLength($message); - - if (empty($this->getValidationErrors()) && ENABLE_CENSORSHIP) { - $result = Censorship::getInstance()->test($message); - if ($result) { - $this->addValidationError(new FormFieldValidationError( - 'censoredWords', - 'wcf.message.error.censoredWordsFound', - ['censoredWords' => $result] - )); ++ if ($message !== '') { ++ $this->validateMinimumLength($message); ++ $this->validateMaximumLength($message); ++ ++ if (empty($this->getValidationErrors()) && ENABLE_CENSORSHIP) { ++ $result = Censorship::getInstance()->test($message); ++ if ($result) { ++ $this->addValidationError(new FormFieldValidationError( ++ 'censoredWords', ++ 'wcf.message.error.censoredWordsFound', ++ ['censoredWords' => $result] ++ )); ++ } + } + } + } + } + + parent::validate(); + } + + /** + * @inheritDoc + * @since 5.4 + */ + protected static function getReservedFieldAttributes(): array + { + return \array_merge( + static::inputGetReservedFieldAttributes(), + [ + 'data-autosave', + 'data-autosave-last-edit-time', + 'data-disable-attachments', + 'data-support-mention', + ] + ); + } }