2 namespace wcf\data\article
;
3 use wcf\data\article\category\ArticleCategory
;
4 use wcf\data\article\content\ArticleContent
;
5 use wcf\data\article\content\ArticleContentAction
;
6 use wcf\data\article\content\ArticleContentEditor
;
7 use wcf\data\language\Language
;
8 use wcf\data\AbstractDatabaseObjectAction
;
9 use wcf\system\clipboard\ClipboardHandler
;
10 use wcf\system\comment\CommentHandler
;
11 use wcf\system\exception\UserInputException
;
12 use wcf\system\language\LanguageFactory
;
13 use wcf\system\like\LikeHandler
;
14 use wcf\system\message\embedded\
object\MessageEmbeddedObjectManager
;
15 use wcf\system\request\LinkHandler
;
16 use wcf\system\search\SearchIndexManager
;
17 use wcf\system\tagging\TagEngine
;
18 use wcf\system\user\storage\UserStorageHandler
;
19 use wcf\system\version\VersionTracker
;
20 use wcf\system\visitTracker\VisitTracker
;
24 * Executes article related actions.
27 * @copyright 2001-2018 WoltLab GmbH
28 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
29 * @package WoltLabSuite\Core\Data\Article
32 * @method ArticleEditor[] getObjects()
33 * @method ArticleEditor getSingleObject()
35 class ArticleAction
extends AbstractDatabaseObjectAction
{
37 * article editor instance
40 public $articleEditor;
51 protected $className = ArticleEditor
::class;
56 protected $permissionsCreate = ['admin.content.article.canManageArticle'];
61 protected $permissionsDelete = ['admin.content.article.canManageArticle'];
66 protected $permissionsUpdate = ['admin.content.article.canManageArticle'];
71 protected $requireACP = ['create', 'delete', 'restore', 'toggleI18n', 'trash', 'update'];
76 protected $allowGuestAccess = ['markAllAsRead'];
82 public function create() {
83 /** @var Article $article */
84 $article = parent
::create();
86 // save article content
87 if (!empty($this->parameters
['content'])) {
88 foreach ($this->parameters
['content'] as $languageID => $content) {
89 if (!empty($content['htmlInputProcessor'])) {
90 /** @noinspection PhpUndefinedMethodInspection */
91 $content['content'] = $content['htmlInputProcessor']->getHtml();
94 /** @var ArticleContent $articleContent */
95 $articleContent = ArticleContentEditor
::create([
96 'articleID' => $article->articleID
,
97 'languageID' => $languageID ?
: null,
98 'title' => $content['title'],
99 'teaser' => $content['teaser'],
100 'content' => $content['content'],
101 'imageID' => $content['imageID'],
102 'teaserImageID' => $content['teaserImageID']
104 $articleContentEditor = new ArticleContentEditor($articleContent);
107 if (!empty($content['tags'])) {
108 TagEngine
::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID
, $content['tags'], ($languageID ?
: LanguageFactory
::getInstance()->getDefaultLanguageID()));
111 // update search index
112 SearchIndexManager
::getInstance()->set(
113 'com.woltlab.wcf.article',
114 $articleContent->articleContentID
,
115 $articleContent->content
,
116 $articleContent->title
,
121 $articleContent->teaser
124 // save embedded objects
125 if (!empty($content['htmlInputProcessor'])) {
126 /** @noinspection PhpUndefinedMethodInspection */
127 $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID
);
128 if (MessageEmbeddedObjectManager
::getInstance()->registerObjects($content['htmlInputProcessor'])) {
129 $articleContentEditor->update(['hasEmbeddedObjects' => 1]);
136 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
137 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
140 if ($article->publicationStatus
== Article
::PUBLISHED
) {
141 ArticleEditor
::updateArticleCounter([$article->userID
=> 1]);
150 public function update() {
153 $isRevert = (!empty($this->parameters
['isRevert']));
155 // update article content
156 if (!empty($this->parameters
['content'])) {
157 foreach ($this->getObjects() as $article) {
161 foreach ($this->parameters
['content'] as $languageID => $content) {
162 if (!empty($content['htmlInputProcessor'])) {
163 /** @noinspection PhpUndefinedMethodInspection */
164 $content['content'] = $content['htmlInputProcessor']->getHtml();
167 $articleContent = ArticleContent
::getArticleContent($article->articleID
, ($languageID ?
: null));
168 $articleContentEditor = null;
169 if ($articleContent !== null) {
171 $articleContentEditor = new ArticleContentEditor($articleContent);
172 $articleContentEditor->update([
173 'title' => $content['title'],
174 'teaser' => $content['teaser'],
175 'content' => $content['content'],
176 'imageID' => ($isRevert) ?
$articleContent->imageID
: $content['imageID'],
177 'teaserImageID' => ($isRevert) ?
$articleContent->teaserImageID
: $content['teaserImageID']
180 $versionData[] = $articleContent;
181 if ($articleContent->content
!= $content['content'] ||
$articleContent->teaser
!= $content['teaser'] ||
$articleContent->title
!= $content['title']) {
186 if (!$isRevert && empty($content['tags'])) {
187 TagEngine
::getInstance()->deleteObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID
, ($languageID ?
: null));
191 /** @var ArticleContent $articleContent */
192 $articleContent = ArticleContentEditor
::create([
193 'articleID' => $article->articleID
,
194 'languageID' => $languageID ?
: null,
195 'title' => $content['title'],
196 'teaser' => $content['teaser'],
197 'content' => $content['content'],
198 'imageID' => ($isRevert) ?
null : $content['imageID'],
199 'teaserImageID' => ($isRevert) ?
null : $content['teaserImageID']
201 $articleContentEditor = new ArticleContentEditor($articleContent);
203 $versionData[] = $articleContent;
208 if (!$isRevert && !empty($content['tags'])) {
209 TagEngine
::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID
, $content['tags'], ($languageID ?
: LanguageFactory
::getInstance()->getDefaultLanguageID()));
212 // update search index
213 SearchIndexManager
::getInstance()->set(
214 'com.woltlab.wcf.article',
215 $articleContent->articleContentID
,
216 $articleContent->content
,
217 $articleContent->title
,
222 $articleContent->teaser
225 // save embedded objects
226 if (!empty($content['htmlInputProcessor'])) {
227 /** @noinspection PhpUndefinedMethodInspection */
228 $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID
);
229 if ($articleContent->hasEmbeddedObjects
!= MessageEmbeddedObjectManager
::getInstance()->registerObjects($content['htmlInputProcessor'])) {
230 $articleContentEditor->update(['hasEmbeddedObjects' => $articleContent->hasEmbeddedObjects ?
0 : 1]);
236 $articleObj = new ArticleVersionTracker($article->getDecoratedObject());
237 $articleObj->setContent($versionData);
238 VersionTracker
::getInstance()->add('com.woltlab.wcf.article', $articleObj);
244 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
245 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
248 $publicationStatus = (isset($this->parameters
['data']['publicationStatus'])) ?
$this->parameters
['data']['publicationStatus'] : null;
249 if ($publicationStatus !== null) {
250 $usersToArticles = [];
251 /** @var ArticleEditor $articleEditor */
252 foreach ($this->objects
as $articleEditor) {
253 if ($publicationStatus != $articleEditor->publicationStatus
) {
254 // The article was published before or was now published.
255 if ($publicationStatus == Article
::PUBLISHED ||
$articleEditor->publicationStatus
== Article
::PUBLISHED
) {
256 if (!isset($usersToArticles[$articleEditor->userID
])) {
257 $usersToArticles[$articleEditor->userID
] = 0;
260 $usersToArticles[$articleEditor->userID
] +
= ($publicationStatus == Article
::PUBLISHED
) ?
1 : -1;
265 if (!empty($usersToArticles)) {
266 ArticleEditor
::updateArticleCounter($usersToArticles);
272 * Validates parameters to delete articles.
274 * @throws UserInputException
276 public function validateDelete() {
277 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
279 if (empty($this->objects
)) {
280 $this->readObjects();
282 if (empty($this->objects
)) {
283 throw new UserInputException('objectIDs');
287 foreach ($this->getObjects() as $article) {
288 if (!$article->isDeleted
) {
289 throw new UserInputException('objectIDs');
297 public function delete() {
298 $articleIDs = $articleContentIDs = [];
299 foreach ($this->getObjects() as $article) {
300 $articleIDs[] = $article->articleID
;
301 foreach ($article->getArticleContents() as $articleContent) {
302 $articleContentIDs[] = $articleContent->articleContentID
;
309 if (!empty($articleIDs)) {
311 LikeHandler
::getInstance()->removeLikes('com.woltlab.wcf.likeableArticle', $articleIDs);
313 CommentHandler
::getInstance()->deleteObjects('com.woltlab.wcf.articleComment', $articleContentIDs);
314 // delete tag to object entries
315 TagEngine
::getInstance()->deleteObjects('com.woltlab.wcf.article', $articleContentIDs);
316 // delete entry from search index
317 SearchIndexManager
::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs);
320 $this->unmarkItems();
323 'objectIDs' => $this->objectIDs
,
324 'redirectURL' => LinkHandler
::getInstance()->getLink('ArticleList', ['isACP' => true])
329 * Validates parameters to move articles to the trash bin.
331 * @throws UserInputException
333 public function validateTrash() {
334 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
336 if (empty($this->objects
)) {
337 $this->readObjects();
339 if (empty($this->objects
)) {
340 throw new UserInputException('objectIDs');
344 foreach ($this->getObjects() as $article) {
345 if ($article->isDeleted
) {
346 throw new UserInputException('objectIDs');
352 * Moves articles to the trash bin.
354 public function trash() {
355 foreach ($this->getObjects() as $articleEditor) {
356 $articleEditor->update(['isDeleted' => 1]);
359 $this->unmarkItems();
362 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
363 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
366 return ['objectIDs' => $this->objectIDs
];
370 * Validates parameters to restore articles.
372 * @throws UserInputException
374 public function validateRestore() {
375 $this->validateDelete();
381 public function restore() {
382 foreach ($this->getObjects() as $articleEditor) {
383 $articleEditor->update(['isDeleted' => 0]);
386 $this->unmarkItems();
389 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
390 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
393 return ['objectIDs' => $this->objectIDs
];
397 * Validates parameters to toggle between i18n and monolingual mode.
399 * @throws UserInputException
401 public function validateToggleI18n() {
402 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
404 $this->articleEditor
= $this->getSingleObject();
405 if ($this->articleEditor
->getDecoratedObject()->isMultilingual
) {
406 $this->readInteger('languageID');
407 $this->language
= LanguageFactory
::getInstance()->getLanguage($this->parameters
['languageID']);
408 if ($this->language
=== null) {
409 throw new UserInputException('languageID');
412 $contents = $this->articleEditor
->getArticleContents();
413 if (!isset($contents[$this->language
->languageID
])) {
414 // there is no content
415 throw new UserInputException('languageID');
421 * Toggles between i18n and monolingual mode.
423 public function toggleI18n() {
426 // i18n -> monolingual
427 if ($this->articleEditor
->getDecoratedObject()->isMultilingual
) {
428 foreach ($this->articleEditor
->getArticleContents() as $articleContent) {
429 if ($articleContent->languageID
== $this->language
->languageID
) {
430 $articleContentEditor = new ArticleContentEditor($articleContent);
431 $articleContentEditor->update(['languageID' => null]);
434 $removeContent[] = $articleContent;
439 // monolingual -> i18n
440 $articleContent = $this->articleEditor
->getArticleContent();
442 foreach (LanguageFactory
::getInstance()->getLanguages() as $language) {
443 $data[$language->languageID
] = [
444 'title' => $articleContent->title
,
445 'teaser' => $articleContent->teaser
,
446 'content' => $articleContent->content
,
447 'imageID' => $articleContent->imageID ?
: null,
448 'teaserImageID' => $articleContent->teaserImageID ?
: null
452 $action = new ArticleAction([$this->articleEditor
], 'update', ['content' => $data]);
453 $action->executeAction();
455 $removeContent[] = $articleContent;
458 if (!empty($removeContent)) {
459 $action = new ArticleContentAction($removeContent, 'delete');
460 $action->executeAction();
463 // flush edit history
464 VersionTracker
::getInstance()->reset('com.woltlab.wcf.article', $this->articleEditor
->getDecoratedObject()->articleID
);
466 // update article's i18n state
467 $this->articleEditor
->update([
468 'isMultilingual' => ($this->articleEditor
->getDecoratedObject()->isMultilingual
) ?
0 : 1
473 * Marks articles as read.
475 public function markAsRead() {
476 if (empty($this->parameters
['visitTime'])) {
477 $this->parameters
['visitTime'] = TIME_NOW
;
480 if (empty($this->objects
)) {
481 $this->readObjects();
484 foreach ($this->getObjects() as $article) {
485 VisitTracker
::getInstance()->trackObjectVisit('com.woltlab.wcf.article', $article->articleID
, $this->parameters
['visitTime']);
489 if (WCF
::getUser()->userID
) {
490 UserStorageHandler
::getInstance()->reset([WCF
::getUser()->userID
], 'unreadArticles');
495 * Marks all articles as read.
497 public function markAllAsRead() {
498 VisitTracker
::getInstance()->trackTypeVisit('com.woltlab.wcf.article');
501 if (WCF
::getUser()->userID
) {
502 UserStorageHandler
::getInstance()->reset([WCF
::getUser()->userID
], 'unreadArticles');
507 * Validates the mark all as read action.
509 public function validateMarkAllAsRead() {
514 * Validates the `setCategory` action.
516 * @throws UserInputException
518 public function validateSetCategory() {
519 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
521 $this->readBoolean('useMarkedArticles', true);
523 // if no object ids are given, use clipboard handler
524 if (empty($this->objectIDs
) && $this->parameters
['useMarkedArticles']) {
525 $this->objectIDs
= array_keys(ClipboardHandler
::getInstance()->getMarkedItems(ClipboardHandler
::getInstance()->getObjectTypeID('com.woltlab.wcf.article')));
528 if (empty($this->objects
)) {
529 $this->readObjects();
531 if (empty($this->objects
)) {
532 throw new UserInputException('objectIDs');
536 $this->readInteger('categoryID');
537 if (ArticleCategory
::getCategory($this->parameters
['categoryID']) === null) {
538 throw new UserInputException('categoryID');
543 * Sets the category of articles.
545 public function setCategory() {
546 foreach ($this->getObjects() as $articleEditor) {
547 $articleEditor->update(['categoryID' => $this->parameters
['categoryID']]);
550 $this->unmarkItems();
554 * Validates the `publish` action.
556 * @throws UserInputException
558 public function validatePublish() {
559 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
561 if (empty($this->objects
)) {
562 $this->readObjects();
564 if (empty($this->objects
)) {
565 throw new UserInputException('objectIDs');
569 foreach ($this->getObjects() as $article) {
570 if ($article->publicationStatus
== Article
::PUBLISHED
) {
571 throw new UserInputException('objectIDs');
577 * Publishes articles.
579 public function publish() {
580 $usersToArticles = [];
581 foreach ($this->getObjects() as $articleEditor) {
582 $articleEditor->update([
584 'publicationStatus' => Article
::PUBLISHED
,
585 'publicationDate' => 0
588 if (!isset($usersToArticles[$articleEditor->userID
])) {
589 $usersToArticles[$articleEditor->userID
] = 0;
592 $usersToArticles[$articleEditor->userID
]++
;
595 ArticleEditor
::updateArticleCounter($usersToArticles);
597 $this->unmarkItems();
601 * Validates the `unpublish` action.
603 * @throws UserInputException
605 public function validateUnpublish() {
606 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
608 if (empty($this->objects
)) {
609 $this->readObjects();
611 if (empty($this->objects
)) {
612 throw new UserInputException('objectIDs');
616 foreach ($this->getObjects() as $article) {
617 if ($article->publicationStatus
!= Article
::PUBLISHED
) {
618 throw new UserInputException('objectIDs');
624 * Unpublishes articles.
626 public function unpublish() {
627 $usersToArticles = [];
628 foreach ($this->getObjects() as $articleEditor) {
629 $articleEditor->update(['publicationStatus' => Article
::UNPUBLISHED
]);
631 if (!isset($usersToArticles[$articleEditor->userID
])) {
632 $usersToArticles[$articleEditor->userID
] = 0;
635 $usersToArticles[$articleEditor->userID
]--;
638 ArticleEditor
::updateArticleCounter($usersToArticles);
640 $this->unmarkItems();
646 * @param integer[] $articleIDs
648 protected function unmarkItems(array $articleIDs = []) {
649 if (empty($articleIDs)) {
650 foreach ($this->getObjects() as $article) {
651 $articleIDs[] = $article->articleID
;
655 if (!empty($articleIDs)) {
656 ClipboardHandler
::getInstance()->unmark($articleIDs, ClipboardHandler
::getInstance()->getObjectTypeID('com.woltlab.wcf.article'));