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\PermissionDeniedException
;
12 use wcf\system\exception\UserInputException
;
13 use wcf\system\language\LanguageFactory
;
14 use wcf\system\like\LikeHandler
;
15 use wcf\system\message\embedded\
object\MessageEmbeddedObjectManager
;
16 use wcf\system\request\LinkHandler
;
17 use wcf\system\search\SearchIndexManager
;
18 use wcf\system\tagging\TagEngine
;
19 use wcf\system\user\storage\UserStorageHandler
;
20 use wcf\system\version\VersionTracker
;
21 use wcf\system\visitTracker\VisitTracker
;
25 * Executes article related actions.
28 * @copyright 2001-2018 WoltLab GmbH
29 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
30 * @package WoltLabSuite\Core\Data\Article
33 * @method ArticleEditor[] getObjects()
34 * @method ArticleEditor getSingleObject()
36 class ArticleAction
extends AbstractDatabaseObjectAction
{
38 * article editor instance
41 public $articleEditor;
52 protected $className = ArticleEditor
::class;
57 protected $permissionsCreate = ['admin.content.article.canManageArticle'];
62 protected $permissionsDelete = ['admin.content.article.canManageArticle'];
67 protected $permissionsUpdate = ['admin.content.article.canManageArticle'];
72 protected $requireACP = ['create', 'delete', 'restore', 'toggleI18n', 'trash', 'update'];
77 protected $allowGuestAccess = ['markAllAsRead'];
83 public function create() {
84 /** @var Article $article */
85 $article = parent
::create();
87 // save article content
88 if (!empty($this->parameters
['content'])) {
89 foreach ($this->parameters
['content'] as $languageID => $content) {
90 if (!empty($content['htmlInputProcessor'])) {
91 /** @noinspection PhpUndefinedMethodInspection */
92 $content['content'] = $content['htmlInputProcessor']->getHtml();
95 /** @var ArticleContent $articleContent */
96 $articleContent = ArticleContentEditor
::create([
97 'articleID' => $article->articleID
,
98 'languageID' => $languageID ?
: null,
99 'title' => $content['title'],
100 'teaser' => $content['teaser'],
101 'content' => $content['content'],
102 'imageID' => $content['imageID'],
103 'teaserImageID' => $content['teaserImageID']
105 $articleContentEditor = new ArticleContentEditor($articleContent);
108 if (!empty($content['tags'])) {
109 TagEngine
::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID
, $content['tags'], ($languageID ?
: LanguageFactory
::getInstance()->getDefaultLanguageID()));
112 // update search index
113 SearchIndexManager
::getInstance()->set(
114 'com.woltlab.wcf.article',
115 $articleContent->articleContentID
,
116 $articleContent->content
,
117 $articleContent->title
,
122 $articleContent->teaser
125 // save embedded objects
126 if (!empty($content['htmlInputProcessor'])) {
127 /** @noinspection PhpUndefinedMethodInspection */
128 $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID
);
129 if (MessageEmbeddedObjectManager
::getInstance()->registerObjects($content['htmlInputProcessor'])) {
130 $articleContentEditor->update(['hasEmbeddedObjects' => 1]);
137 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
138 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
141 if ($article->publicationStatus
== Article
::PUBLISHED
) {
142 ArticleEditor
::updateArticleCounter([$article->userID
=> 1]);
151 public function update() {
154 $isRevert = (!empty($this->parameters
['isRevert']));
156 // update article content
157 if (!empty($this->parameters
['content'])) {
158 foreach ($this->getObjects() as $article) {
162 foreach ($this->parameters
['content'] as $languageID => $content) {
163 if (!empty($content['htmlInputProcessor'])) {
164 /** @noinspection PhpUndefinedMethodInspection */
165 $content['content'] = $content['htmlInputProcessor']->getHtml();
168 $articleContent = ArticleContent
::getArticleContent($article->articleID
, ($languageID ?
: null));
169 $articleContentEditor = null;
170 if ($articleContent !== null) {
172 $articleContentEditor = new ArticleContentEditor($articleContent);
173 $articleContentEditor->update([
174 'title' => $content['title'],
175 'teaser' => $content['teaser'],
176 'content' => $content['content'],
177 'imageID' => ($isRevert) ?
$articleContent->imageID
: $content['imageID'],
178 'teaserImageID' => ($isRevert) ?
$articleContent->teaserImageID
: $content['teaserImageID']
181 $versionData[] = $articleContent;
182 if ($articleContent->content
!= $content['content'] ||
$articleContent->teaser
!= $content['teaser'] ||
$articleContent->title
!= $content['title']) {
187 if (!$isRevert && empty($content['tags'])) {
188 TagEngine
::getInstance()->deleteObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID
, ($languageID ?
: null));
192 /** @var ArticleContent $articleContent */
193 $articleContent = ArticleContentEditor
::create([
194 'articleID' => $article->articleID
,
195 'languageID' => $languageID ?
: null,
196 'title' => $content['title'],
197 'teaser' => $content['teaser'],
198 'content' => $content['content'],
199 'imageID' => ($isRevert) ?
null : $content['imageID'],
200 'teaserImageID' => ($isRevert) ?
null : $content['teaserImageID']
202 $articleContentEditor = new ArticleContentEditor($articleContent);
204 $versionData[] = $articleContent;
209 if (!$isRevert && !empty($content['tags'])) {
210 TagEngine
::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID
, $content['tags'], ($languageID ?
: LanguageFactory
::getInstance()->getDefaultLanguageID()));
213 // update search index
214 SearchIndexManager
::getInstance()->set(
215 'com.woltlab.wcf.article',
216 $articleContent->articleContentID
,
217 $articleContent->content
,
218 $articleContent->title
,
223 $articleContent->teaser
226 // save embedded objects
227 if (!empty($content['htmlInputProcessor'])) {
228 /** @noinspection PhpUndefinedMethodInspection */
229 $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID
);
230 if ($articleContent->hasEmbeddedObjects
!= MessageEmbeddedObjectManager
::getInstance()->registerObjects($content['htmlInputProcessor'])) {
231 $articleContentEditor->update(['hasEmbeddedObjects' => $articleContent->hasEmbeddedObjects ?
0 : 1]);
237 $articleObj = new ArticleVersionTracker($article->getDecoratedObject());
238 $articleObj->setContent($versionData);
239 VersionTracker
::getInstance()->add('com.woltlab.wcf.article', $articleObj);
245 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
246 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
249 $publicationStatus = (isset($this->parameters
['data']['publicationStatus'])) ?
$this->parameters
['data']['publicationStatus'] : null;
250 if ($publicationStatus !== null) {
251 $usersToArticles = [];
252 /** @var ArticleEditor $articleEditor */
253 foreach ($this->objects
as $articleEditor) {
254 if ($publicationStatus != $articleEditor->publicationStatus
) {
255 // The article was published before or was now published.
256 if ($publicationStatus == Article
::PUBLISHED ||
$articleEditor->publicationStatus
== Article
::PUBLISHED
) {
257 if (!isset($usersToArticles[$articleEditor->userID
])) {
258 $usersToArticles[$articleEditor->userID
] = 0;
261 $usersToArticles[$articleEditor->userID
] +
= ($publicationStatus == Article
::PUBLISHED
) ?
1 : -1;
266 if (!empty($usersToArticles)) {
267 ArticleEditor
::updateArticleCounter($usersToArticles);
273 * Validates parameters to delete articles.
275 * @throws UserInputException
277 public function validateDelete() {
278 if (empty($this->objects
)) {
279 $this->readObjects();
281 if (empty($this->objects
)) {
282 throw new UserInputException('objectIDs');
286 foreach ($this->getObjects() as $article) {
287 if (!$article->canDelete()) {
288 throw new PermissionDeniedException();
291 if (!$article->isDeleted
) {
292 throw new UserInputException('objectIDs');
300 public function delete() {
301 $articleIDs = $articleContentIDs = [];
302 foreach ($this->getObjects() as $article) {
303 $articleIDs[] = $article->articleID
;
304 foreach ($article->getArticleContents() as $articleContent) {
305 $articleContentIDs[] = $articleContent->articleContentID
;
312 if (!empty($articleIDs)) {
314 LikeHandler
::getInstance()->removeLikes('com.woltlab.wcf.likeableArticle', $articleIDs);
316 CommentHandler
::getInstance()->deleteObjects('com.woltlab.wcf.articleComment', $articleContentIDs);
317 // delete tag to object entries
318 TagEngine
::getInstance()->deleteObjects('com.woltlab.wcf.article', $articleContentIDs);
319 // delete entry from search index
320 SearchIndexManager
::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs);
323 $this->unmarkItems();
326 'objectIDs' => $this->objectIDs
,
327 'redirectURL' => LinkHandler
::getInstance()->getLink('ArticleList', ['isACP' => true])
332 * Validates parameters to move articles to the trash bin.
334 * @throws UserInputException
336 public function validateTrash() {
337 if (empty($this->objects
)) {
338 $this->readObjects();
340 if (empty($this->objects
)) {
341 throw new UserInputException('objectIDs');
345 foreach ($this->getObjects() as $article) {
346 if (!$article->canDelete()) {
347 throw new PermissionDeniedException();
350 if ($article->isDeleted
) {
351 throw new UserInputException('objectIDs');
357 * Moves articles to the trash bin.
359 public function trash() {
360 foreach ($this->getObjects() as $articleEditor) {
361 $articleEditor->update(['isDeleted' => 1]);
364 $this->unmarkItems();
367 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
368 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
371 return ['objectIDs' => $this->objectIDs
];
375 * Validates parameters to restore articles.
377 * @throws UserInputException
379 public function validateRestore() {
380 $this->validateDelete();
386 public function restore() {
387 foreach ($this->getObjects() as $articleEditor) {
388 $articleEditor->update(['isDeleted' => 0]);
391 $this->unmarkItems();
394 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
395 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
398 return ['objectIDs' => $this->objectIDs
];
402 * Validates parameters to toggle between i18n and monolingual mode.
404 * @throws UserInputException
406 public function validateToggleI18n() {
407 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
409 $this->articleEditor
= $this->getSingleObject();
410 if ($this->articleEditor
->getDecoratedObject()->isMultilingual
) {
411 $this->readInteger('languageID');
412 $this->language
= LanguageFactory
::getInstance()->getLanguage($this->parameters
['languageID']);
413 if ($this->language
=== null) {
414 throw new UserInputException('languageID');
417 $contents = $this->articleEditor
->getArticleContents();
418 if (!isset($contents[$this->language
->languageID
])) {
419 // there is no content
420 throw new UserInputException('languageID');
426 * Toggles between i18n and monolingual mode.
428 public function toggleI18n() {
431 // i18n -> monolingual
432 if ($this->articleEditor
->getDecoratedObject()->isMultilingual
) {
433 foreach ($this->articleEditor
->getArticleContents() as $articleContent) {
434 if ($articleContent->languageID
== $this->language
->languageID
) {
435 $articleContentEditor = new ArticleContentEditor($articleContent);
436 $articleContentEditor->update(['languageID' => null]);
439 $removeContent[] = $articleContent;
444 // monolingual -> i18n
445 $articleContent = $this->articleEditor
->getArticleContent();
447 foreach (LanguageFactory
::getInstance()->getLanguages() as $language) {
448 $data[$language->languageID
] = [
449 'title' => $articleContent->title
,
450 'teaser' => $articleContent->teaser
,
451 'content' => $articleContent->content
,
452 'imageID' => $articleContent->imageID ?
: null,
453 'teaserImageID' => $articleContent->teaserImageID ?
: null
457 $action = new ArticleAction([$this->articleEditor
], 'update', ['content' => $data]);
458 $action->executeAction();
460 $removeContent[] = $articleContent;
463 if (!empty($removeContent)) {
464 $action = new ArticleContentAction($removeContent, 'delete');
465 $action->executeAction();
468 // flush edit history
469 VersionTracker
::getInstance()->reset('com.woltlab.wcf.article', $this->articleEditor
->getDecoratedObject()->articleID
);
471 // update article's i18n state
472 $this->articleEditor
->update([
473 'isMultilingual' => ($this->articleEditor
->getDecoratedObject()->isMultilingual
) ?
0 : 1
478 * Marks articles as read.
480 public function markAsRead() {
481 if (empty($this->parameters
['visitTime'])) {
482 $this->parameters
['visitTime'] = TIME_NOW
;
485 if (empty($this->objects
)) {
486 $this->readObjects();
489 foreach ($this->getObjects() as $article) {
490 VisitTracker
::getInstance()->trackObjectVisit('com.woltlab.wcf.article', $article->articleID
, $this->parameters
['visitTime']);
494 if (WCF
::getUser()->userID
) {
495 UserStorageHandler
::getInstance()->reset([WCF
::getUser()->userID
], 'unreadArticles');
500 * Marks all articles as read.
502 public function markAllAsRead() {
503 VisitTracker
::getInstance()->trackTypeVisit('com.woltlab.wcf.article');
506 if (WCF
::getUser()->userID
) {
507 UserStorageHandler
::getInstance()->reset([WCF
::getUser()->userID
], 'unreadArticles');
512 * Validates the mark all as read action.
514 public function validateMarkAllAsRead() {
519 * Validates the `setCategory` action.
521 * @throws UserInputException
523 public function validateSetCategory() {
524 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
526 $this->readBoolean('useMarkedArticles', true);
528 // if no object ids are given, use clipboard handler
529 if (empty($this->objectIDs
) && $this->parameters
['useMarkedArticles']) {
530 $this->objectIDs
= array_keys(ClipboardHandler
::getInstance()->getMarkedItems(ClipboardHandler
::getInstance()->getObjectTypeID('com.woltlab.wcf.article')));
533 if (empty($this->objects
)) {
534 $this->readObjects();
536 if (empty($this->objects
)) {
537 throw new UserInputException('objectIDs');
541 $this->readInteger('categoryID');
542 if (ArticleCategory
::getCategory($this->parameters
['categoryID']) === null) {
543 throw new UserInputException('categoryID');
548 * Sets the category of articles.
550 public function setCategory() {
551 foreach ($this->getObjects() as $articleEditor) {
552 $articleEditor->update(['categoryID' => $this->parameters
['categoryID']]);
555 $this->unmarkItems();
559 * Validates the `publish` action.
561 * @throws UserInputException
563 public function validatePublish() {
564 if (empty($this->objects
)) {
565 $this->readObjects();
567 if (empty($this->objects
)) {
568 throw new UserInputException('objectIDs');
572 foreach ($this->getObjects() as $article) {
573 if (!$article->canPublish()) {
574 throw new PermissionDeniedException();
577 if ($article->publicationStatus
== Article
::PUBLISHED
) {
578 throw new UserInputException('objectIDs');
584 * Publishes articles.
586 public function publish() {
587 $usersToArticles = [];
588 foreach ($this->getObjects() as $articleEditor) {
589 $articleEditor->update([
591 'publicationStatus' => Article
::PUBLISHED
,
592 'publicationDate' => 0
595 if (!isset($usersToArticles[$articleEditor->userID
])) {
596 $usersToArticles[$articleEditor->userID
] = 0;
599 $usersToArticles[$articleEditor->userID
]++
;
602 ArticleEditor
::updateArticleCounter($usersToArticles);
604 $this->unmarkItems();
608 * Validates the `unpublish` action.
610 * @throws UserInputException
612 public function validateUnpublish() {
613 if (empty($this->objects
)) {
614 $this->readObjects();
616 if (empty($this->objects
)) {
617 throw new UserInputException('objectIDs');
621 foreach ($this->getObjects() as $article) {
622 if (!$article->canPublish()) {
623 throw new PermissionDeniedException();
626 if ($article->publicationStatus
!= Article
::PUBLISHED
) {
627 throw new UserInputException('objectIDs');
633 * Unpublishes articles.
635 public function unpublish() {
636 $usersToArticles = [];
637 foreach ($this->getObjects() as $articleEditor) {
638 $articleEditor->update(['publicationStatus' => Article
::UNPUBLISHED
]);
640 if (!isset($usersToArticles[$articleEditor->userID
])) {
641 $usersToArticles[$articleEditor->userID
] = 0;
644 $usersToArticles[$articleEditor->userID
]--;
647 ArticleEditor
::updateArticleCounter($usersToArticles);
649 $this->unmarkItems();
655 * @param integer[] $articleIDs
657 protected function unmarkItems(array $articleIDs = []) {
658 if (empty($articleIDs)) {
659 foreach ($this->getObjects() as $article) {
660 $articleIDs[] = $article->articleID
;
664 if (!empty($articleIDs)) {
665 ClipboardHandler
::getInstance()->unmark($articleIDs, ClipboardHandler
::getInstance()->getObjectTypeID('com.woltlab.wcf.article'));