2 declare(strict_types
=1);
3 namespace wcf\data\article
;
4 use wcf\data\article\category\ArticleCategory
;
5 use wcf\data\article\content\ArticleContent
;
6 use wcf\data\article\content\ArticleContentAction
;
7 use wcf\data\article\content\ArticleContentEditor
;
8 use wcf\data\language\Language
;
9 use wcf\data\AbstractDatabaseObjectAction
;
10 use wcf\system\clipboard\ClipboardHandler
;
11 use wcf\system\comment\CommentHandler
;
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');
147 public function update() {
150 $isRevert = (!empty($this->parameters
['isRevert']));
152 // update article content
153 if (!empty($this->parameters
['content'])) {
154 foreach ($this->getObjects() as $article) {
158 foreach ($this->parameters
['content'] as $languageID => $content) {
159 if (!empty($content['htmlInputProcessor'])) {
160 /** @noinspection PhpUndefinedMethodInspection */
161 $content['content'] = $content['htmlInputProcessor']->getHtml();
164 $articleContent = ArticleContent
::getArticleContent($article->articleID
, ($languageID ?
: null));
165 $articleContentEditor = null;
166 if ($articleContent !== null) {
168 $articleContentEditor = new ArticleContentEditor($articleContent);
169 $articleContentEditor->update([
170 'title' => $content['title'],
171 'teaser' => $content['teaser'],
172 'content' => $content['content'],
173 'imageID' => ($isRevert) ?
$articleContent->imageID
: $content['imageID'],
174 'teaserImageID' => ($isRevert) ?
$articleContent->teaserImageID
: $content['teaserImageID']
177 $versionData[] = $articleContent;
178 if ($articleContent->content
!= $content['content'] ||
$articleContent->teaser
!= $content['teaser'] ||
$articleContent->title
!= $content['title']) {
183 if (!$isRevert && empty($content['tags'])) {
184 TagEngine
::getInstance()->deleteObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID
, ($languageID ?
: null));
188 /** @var ArticleContent $articleContent */
189 $articleContent = ArticleContentEditor
::create([
190 'articleID' => $article->articleID
,
191 'languageID' => $languageID ?
: null,
192 'title' => $content['title'],
193 'teaser' => $content['teaser'],
194 'content' => $content['content'],
195 'imageID' => ($isRevert) ?
null : $content['imageID'],
196 'teaserImageID' => ($isRevert) ?
null : $content['teaserImageID']
198 $articleContentEditor = new ArticleContentEditor($articleContent);
200 $versionData[] = $articleContent;
205 if (!$isRevert && !empty($content['tags'])) {
206 TagEngine
::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID
, $content['tags'], ($languageID ?
: LanguageFactory
::getInstance()->getDefaultLanguageID()));
209 // update search index
210 SearchIndexManager
::getInstance()->set(
211 'com.woltlab.wcf.article',
212 $articleContent->articleContentID
,
213 $articleContent->content
,
214 $articleContent->title
,
219 $articleContent->teaser
222 // save embedded objects
223 if (!empty($content['htmlInputProcessor'])) {
224 /** @noinspection PhpUndefinedMethodInspection */
225 $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID
);
226 if ($articleContent->hasEmbeddedObjects
!= MessageEmbeddedObjectManager
::getInstance()->registerObjects($content['htmlInputProcessor'])) {
227 $articleContentEditor->update(['hasEmbeddedObjects' => $articleContent->hasEmbeddedObjects ?
0 : 1]);
233 $articleObj = new ArticleVersionTracker($article->getDecoratedObject());
234 $articleObj->setContent($versionData);
235 VersionTracker
::getInstance()->add('com.woltlab.wcf.article', $articleObj);
241 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
242 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
247 * Validates parameters to delete articles.
249 * @throws UserInputException
251 public function validateDelete() {
252 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
254 if (empty($this->objects
)) {
255 $this->readObjects();
257 if (empty($this->objects
)) {
258 throw new UserInputException('objectIDs');
262 foreach ($this->getObjects() as $article) {
263 if (!$article->isDeleted
) {
264 throw new UserInputException('objectIDs');
272 public function delete() {
273 $articleIDs = $articleContentIDs = [];
274 foreach ($this->getObjects() as $article) {
275 $articleIDs[] = $article->articleID
;
276 foreach ($article->getArticleContents() as $articleContent) {
277 $articleContentIDs[] = $articleContent->articleContentID
;
284 if (!empty($articleIDs)) {
286 LikeHandler
::getInstance()->removeLikes('com.woltlab.wcf.likeableArticle', $articleIDs);
288 CommentHandler
::getInstance()->deleteObjects('com.woltlab.wcf.articleComment', $articleContentIDs);
289 // delete tag to object entries
290 TagEngine
::getInstance()->deleteObjects('com.woltlab.wcf.article', $articleContentIDs);
291 // delete entry from search index
292 SearchIndexManager
::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs);
295 $this->unmarkItems();
298 'objectIDs' => $this->objectIDs
,
299 'redirectURL' => LinkHandler
::getInstance()->getLink('ArticleList', ['isACP' => true])
304 * Validates parameters to move articles to the trash bin.
306 * @throws UserInputException
308 public function validateTrash() {
309 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
311 if (empty($this->objects
)) {
312 $this->readObjects();
314 if (empty($this->objects
)) {
315 throw new UserInputException('objectIDs');
319 foreach ($this->getObjects() as $article) {
320 if ($article->isDeleted
) {
321 throw new UserInputException('objectIDs');
327 * Moves articles to the trash bin.
329 public function trash() {
330 foreach ($this->getObjects() as $articleEditor) {
331 $articleEditor->update(['isDeleted' => 1]);
334 $this->unmarkItems();
337 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
338 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
341 return ['objectIDs' => $this->objectIDs
];
345 * Validates parameters to restore articles.
347 * @throws UserInputException
349 public function validateRestore() {
350 $this->validateDelete();
356 public function restore() {
357 foreach ($this->getObjects() as $articleEditor) {
358 $articleEditor->update(['isDeleted' => 0]);
361 $this->unmarkItems();
364 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
365 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
368 return ['objectIDs' => $this->objectIDs
];
372 * Validates parameters to toggle between i18n and monolingual mode.
374 * @throws UserInputException
376 public function validateToggleI18n() {
377 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
379 $this->articleEditor
= $this->getSingleObject();
380 if ($this->articleEditor
->getDecoratedObject()->isMultilingual
) {
381 $this->readInteger('languageID');
382 $this->language
= LanguageFactory
::getInstance()->getLanguage($this->parameters
['languageID']);
383 if ($this->language
=== null) {
384 throw new UserInputException('languageID');
387 $contents = $this->articleEditor
->getArticleContents();
388 if (!isset($contents[$this->language
->languageID
])) {
389 // there is no content
390 throw new UserInputException('languageID');
396 * Toggles between i18n and monolingual mode.
398 public function toggleI18n() {
401 // i18n -> monolingual
402 if ($this->articleEditor
->getDecoratedObject()->isMultilingual
) {
403 foreach ($this->articleEditor
->getArticleContents() as $articleContent) {
404 if ($articleContent->languageID
== $this->language
->languageID
) {
405 $articleContentEditor = new ArticleContentEditor($articleContent);
406 $articleContentEditor->update(['languageID' => null]);
409 $removeContent[] = $articleContent;
414 // monolingual -> i18n
415 $articleContent = $this->articleEditor
->getArticleContent();
417 foreach (LanguageFactory
::getInstance()->getLanguages() as $language) {
418 $data[$language->languageID
] = [
419 'title' => $articleContent->title
,
420 'teaser' => $articleContent->teaser
,
421 'content' => $articleContent->content
,
422 'imageID' => $articleContent->imageID ?
: null,
423 'teaserImageID' => $articleContent->teaserImageID ?
: null
427 $action = new ArticleAction([$this->articleEditor
], 'update', ['content' => $data]);
428 $action->executeAction();
430 $removeContent[] = $articleContent;
433 if (!empty($removeContent)) {
434 $action = new ArticleContentAction($removeContent, 'delete');
435 $action->executeAction();
438 // flush edit history
439 VersionTracker
::getInstance()->reset('com.woltlab.wcf.article', $this->articleEditor
->getDecoratedObject()->articleID
);
441 // update article's i18n state
442 $this->articleEditor
->update([
443 'isMultilingual' => ($this->articleEditor
->getDecoratedObject()->isMultilingual
) ?
0 : 1
448 * Marks articles as read.
450 public function markAsRead() {
451 if (empty($this->parameters
['visitTime'])) {
452 $this->parameters
['visitTime'] = TIME_NOW
;
455 if (empty($this->objects
)) {
456 $this->readObjects();
459 foreach ($this->getObjects() as $article) {
460 VisitTracker
::getInstance()->trackObjectVisit('com.woltlab.wcf.article', $article->articleID
, $this->parameters
['visitTime']);
464 if (WCF
::getUser()->userID
) {
465 UserStorageHandler
::getInstance()->reset([WCF
::getUser()->userID
], 'unreadArticles');
470 * Marks all articles as read.
472 public function markAllAsRead() {
473 VisitTracker
::getInstance()->trackTypeVisit('com.woltlab.wcf.article');
476 if (WCF
::getUser()->userID
) {
477 UserStorageHandler
::getInstance()->reset([WCF
::getUser()->userID
], 'unreadArticles');
482 * Validates the mark all as read action.
484 public function validateMarkAllAsRead() {
489 * Validates the `setCategory` action.
491 * @throws UserInputException
493 public function validateSetCategory() {
494 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
496 $this->readBoolean('useMarkedArticles', true);
498 // if no object ids are given, use clipboard handler
499 if (empty($this->objectIDs
) && $this->parameters
['useMarkedArticles']) {
500 $this->objectIDs
= array_keys(ClipboardHandler
::getInstance()->getMarkedItems(ClipboardHandler
::getInstance()->getObjectTypeID('com.woltlab.wcf.article')));
503 if (empty($this->objects
)) {
504 $this->readObjects();
506 if (empty($this->objects
)) {
507 throw new UserInputException('objectIDs');
511 $this->readInteger('categoryID');
512 if (ArticleCategory
::getCategory($this->parameters
['categoryID']) === null) {
513 throw new UserInputException('categoryID');
518 * Sets the category of articles.
520 public function setCategory() {
521 foreach ($this->getObjects() as $articleEditor) {
522 $articleEditor->update(['categoryID' => $this->parameters
['categoryID']]);
525 $this->unmarkItems();
529 * Validates the `publish` action.
531 * @throws UserInputException
533 public function validatePublish() {
534 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
536 if (empty($this->objects
)) {
537 $this->readObjects();
539 if (empty($this->objects
)) {
540 throw new UserInputException('objectIDs');
544 foreach ($this->getObjects() as $article) {
545 if ($article->publicationStatus
== Article
::PUBLISHED
) {
546 throw new UserInputException('objectIDs');
552 * Publishes articles.
554 public function publish() {
555 foreach ($this->getObjects() as $articleEditor) {
556 $articleEditor->update([
558 'publicationStatus' => Article
::PUBLISHED
,
559 'publicationDate' => 0
563 $this->unmarkItems();
567 * Validates the `unpublish` action.
569 * @throws UserInputException
571 public function validateUnpublish() {
572 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
574 if (empty($this->objects
)) {
575 $this->readObjects();
577 if (empty($this->objects
)) {
578 throw new UserInputException('objectIDs');
582 foreach ($this->getObjects() as $article) {
583 if ($article->publicationStatus
!= Article
::PUBLISHED
) {
584 throw new UserInputException('objectIDs');
590 * Unpublishes articles.
592 public function unpublish() {
593 foreach ($this->getObjects() as $articleEditor) {
594 $articleEditor->update(['publicationStatus' => Article
::UNPUBLISHED
]);
597 $this->unmarkItems();
603 * @param integer[] $articleIDs
605 protected function unmarkItems(array $articleIDs = []) {
606 if (empty($articleIDs)) {
607 foreach ($this->getObjects() as $article) {
608 $articleIDs[] = $article->articleID
;
612 if (!empty($articleIDs)) {
613 ClipboardHandler
::getInstance()->unmark($articleIDs, ClipboardHandler
::getInstance()->getObjectTypeID('com.woltlab.wcf.article'));