/**
* Executes article related actions.
- *
- * @author Marcel Werk
- * @copyright 2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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')
+ );
+ }
+ }
}
/**
* Implementation of a form field for wysiwyg editors.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package WoltLabSuite\Core\System\Form\Builder\Field
- * @since 5.2
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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',
+ ]
+ );
+ }
}