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');
146 public function update() {
149 $isRevert = (!empty($this->parameters
['isRevert']));
151 // update article content
152 if (!empty($this->parameters
['content'])) {
153 foreach ($this->getObjects() as $article) {
157 foreach ($this->parameters
['content'] as $languageID => $content) {
158 if (!empty($content['htmlInputProcessor'])) {
159 /** @noinspection PhpUndefinedMethodInspection */
160 $content['content'] = $content['htmlInputProcessor']->getHtml();
163 $articleContent = ArticleContent
::getArticleContent($article->articleID
, ($languageID ?
: null));
164 $articleContentEditor = null;
165 if ($articleContent !== null) {
167 $articleContentEditor = new ArticleContentEditor($articleContent);
168 $articleContentEditor->update([
169 'title' => $content['title'],
170 'teaser' => $content['teaser'],
171 'content' => $content['content'],
172 'imageID' => ($isRevert) ?
$articleContent->imageID
: $content['imageID'],
173 'teaserImageID' => ($isRevert) ?
$articleContent->teaserImageID
: $content['teaserImageID']
176 $versionData[] = $articleContent;
177 if ($articleContent->content
!= $content['content'] ||
$articleContent->teaser
!= $content['teaser'] ||
$articleContent->title
!= $content['title']) {
182 if (!$isRevert && empty($content['tags'])) {
183 TagEngine
::getInstance()->deleteObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID
, ($languageID ?
: null));
187 /** @var ArticleContent $articleContent */
188 $articleContent = ArticleContentEditor
::create([
189 'articleID' => $article->articleID
,
190 'languageID' => $languageID ?
: null,
191 'title' => $content['title'],
192 'teaser' => $content['teaser'],
193 'content' => $content['content'],
194 'imageID' => ($isRevert) ?
null : $content['imageID'],
195 'teaserImageID' => ($isRevert) ?
null : $content['teaserImageID']
197 $articleContentEditor = new ArticleContentEditor($articleContent);
199 $versionData[] = $articleContent;
204 if (!$isRevert && !empty($content['tags'])) {
205 TagEngine
::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID
, $content['tags'], ($languageID ?
: LanguageFactory
::getInstance()->getDefaultLanguageID()));
208 // update search index
209 SearchIndexManager
::getInstance()->set(
210 'com.woltlab.wcf.article',
211 $articleContent->articleContentID
,
212 $articleContent->content
,
213 $articleContent->title
,
218 $articleContent->teaser
221 // save embedded objects
222 if (!empty($content['htmlInputProcessor'])) {
223 /** @noinspection PhpUndefinedMethodInspection */
224 $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID
);
225 if ($articleContent->hasEmbeddedObjects
!= MessageEmbeddedObjectManager
::getInstance()->registerObjects($content['htmlInputProcessor'])) {
226 $articleContentEditor->update(['hasEmbeddedObjects' => $articleContent->hasEmbeddedObjects ?
0 : 1]);
232 $articleObj = new ArticleVersionTracker($article->getDecoratedObject());
233 $articleObj->setContent($versionData);
234 VersionTracker
::getInstance()->add('com.woltlab.wcf.article', $articleObj);
240 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
241 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
246 * Validates parameters to delete articles.
248 * @throws UserInputException
250 public function validateDelete() {
251 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
253 if (empty($this->objects
)) {
254 $this->readObjects();
256 if (empty($this->objects
)) {
257 throw new UserInputException('objectIDs');
261 foreach ($this->getObjects() as $article) {
262 if (!$article->isDeleted
) {
263 throw new UserInputException('objectIDs');
271 public function delete() {
272 $articleIDs = $articleContentIDs = [];
273 foreach ($this->getObjects() as $article) {
274 $articleIDs[] = $article->articleID
;
275 foreach ($article->getArticleContents() as $articleContent) {
276 $articleContentIDs[] = $articleContent->articleContentID
;
283 if (!empty($articleIDs)) {
285 LikeHandler
::getInstance()->removeLikes('com.woltlab.wcf.likeableArticle', $articleIDs);
287 CommentHandler
::getInstance()->deleteObjects('com.woltlab.wcf.articleComment', $articleContentIDs);
288 // delete tag to object entries
289 TagEngine
::getInstance()->deleteObjects('com.woltlab.wcf.article', $articleContentIDs);
290 // delete entry from search index
291 SearchIndexManager
::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs);
294 $this->unmarkItems();
297 'objectIDs' => $this->objectIDs
,
298 'redirectURL' => LinkHandler
::getInstance()->getLink('ArticleList', ['isACP' => true])
303 * Validates parameters to move articles to the trash bin.
305 * @throws UserInputException
307 public function validateTrash() {
308 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
310 if (empty($this->objects
)) {
311 $this->readObjects();
313 if (empty($this->objects
)) {
314 throw new UserInputException('objectIDs');
318 foreach ($this->getObjects() as $article) {
319 if ($article->isDeleted
) {
320 throw new UserInputException('objectIDs');
326 * Moves articles to the trash bin.
328 public function trash() {
329 foreach ($this->getObjects() as $articleEditor) {
330 $articleEditor->update(['isDeleted' => 1]);
333 $this->unmarkItems();
336 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
337 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
340 return ['objectIDs' => $this->objectIDs
];
344 * Validates parameters to restore articles.
346 * @throws UserInputException
348 public function validateRestore() {
349 $this->validateDelete();
355 public function restore() {
356 foreach ($this->getObjects() as $articleEditor) {
357 $articleEditor->update(['isDeleted' => 0]);
360 $this->unmarkItems();
363 if (ARTICLE_ENABLE_VISIT_TRACKING
) {
364 UserStorageHandler
::getInstance()->resetAll('unreadArticles');
367 return ['objectIDs' => $this->objectIDs
];
371 * Validates parameters to toggle between i18n and monolingual mode.
373 * @throws UserInputException
375 public function validateToggleI18n() {
376 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
378 $this->articleEditor
= $this->getSingleObject();
379 if ($this->articleEditor
->getDecoratedObject()->isMultilingual
) {
380 $this->readInteger('languageID');
381 $this->language
= LanguageFactory
::getInstance()->getLanguage($this->parameters
['languageID']);
382 if ($this->language
=== null) {
383 throw new UserInputException('languageID');
386 $contents = $this->articleEditor
->getArticleContents();
387 if (!isset($contents[$this->language
->languageID
])) {
388 // there is no content
389 throw new UserInputException('languageID');
395 * Toggles between i18n and monolingual mode.
397 public function toggleI18n() {
400 // i18n -> monolingual
401 if ($this->articleEditor
->getDecoratedObject()->isMultilingual
) {
402 foreach ($this->articleEditor
->getArticleContents() as $articleContent) {
403 if ($articleContent->languageID
== $this->language
->languageID
) {
404 $articleContentEditor = new ArticleContentEditor($articleContent);
405 $articleContentEditor->update(['languageID' => null]);
408 $removeContent[] = $articleContent;
413 // monolingual -> i18n
414 $articleContent = $this->articleEditor
->getArticleContent();
416 foreach (LanguageFactory
::getInstance()->getLanguages() as $language) {
417 $data[$language->languageID
] = [
418 'title' => $articleContent->title
,
419 'teaser' => $articleContent->teaser
,
420 'content' => $articleContent->content
,
421 'imageID' => $articleContent->imageID ?
: null,
422 'teaserImageID' => $articleContent->teaserImageID ?
: null
426 $action = new ArticleAction([$this->articleEditor
], 'update', ['content' => $data]);
427 $action->executeAction();
429 $removeContent[] = $articleContent;
432 if (!empty($removeContent)) {
433 $action = new ArticleContentAction($removeContent, 'delete');
434 $action->executeAction();
437 // flush edit history
438 VersionTracker
::getInstance()->reset('com.woltlab.wcf.article', $this->articleEditor
->getDecoratedObject()->articleID
);
440 // update article's i18n state
441 $this->articleEditor
->update([
442 'isMultilingual' => ($this->articleEditor
->getDecoratedObject()->isMultilingual
) ?
0 : 1
447 * Marks articles as read.
449 public function markAsRead() {
450 if (empty($this->parameters
['visitTime'])) {
451 $this->parameters
['visitTime'] = TIME_NOW
;
454 if (empty($this->objects
)) {
455 $this->readObjects();
458 foreach ($this->getObjects() as $article) {
459 VisitTracker
::getInstance()->trackObjectVisit('com.woltlab.wcf.article', $article->articleID
, $this->parameters
['visitTime']);
463 if (WCF
::getUser()->userID
) {
464 UserStorageHandler
::getInstance()->reset([WCF
::getUser()->userID
], 'unreadArticles');
469 * Marks all articles as read.
471 public function markAllAsRead() {
472 VisitTracker
::getInstance()->trackTypeVisit('com.woltlab.wcf.article');
475 if (WCF
::getUser()->userID
) {
476 UserStorageHandler
::getInstance()->reset([WCF
::getUser()->userID
], 'unreadArticles');
481 * Validates the mark all as read action.
483 public function validateMarkAllAsRead() {
488 * Validates the `setCategory` action.
490 * @throws UserInputException
492 public function validateSetCategory() {
493 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
495 $this->readBoolean('useMarkedArticles', true);
497 // if no object ids are given, use clipboard handler
498 if (empty($this->objectIDs
) && $this->parameters
['useMarkedArticles']) {
499 $this->objectIDs
= array_keys(ClipboardHandler
::getInstance()->getMarkedItems(ClipboardHandler
::getInstance()->getObjectTypeID('com.woltlab.wcf.article')));
502 if (empty($this->objects
)) {
503 $this->readObjects();
505 if (empty($this->objects
)) {
506 throw new UserInputException('objectIDs');
510 $this->readInteger('categoryID');
511 if (ArticleCategory
::getCategory($this->parameters
['categoryID']) === null) {
512 throw new UserInputException('categoryID');
517 * Sets the category of articles.
519 public function setCategory() {
520 foreach ($this->getObjects() as $articleEditor) {
521 $articleEditor->update(['categoryID' => $this->parameters
['categoryID']]);
524 $this->unmarkItems();
528 * Validates the `publish` action.
530 * @throws UserInputException
532 public function validatePublish() {
533 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
535 if (empty($this->objects
)) {
536 $this->readObjects();
538 if (empty($this->objects
)) {
539 throw new UserInputException('objectIDs');
543 foreach ($this->getObjects() as $article) {
544 if ($article->publicationStatus
== Article
::PUBLISHED
) {
545 throw new UserInputException('objectIDs');
551 * Publishes articles.
553 public function publish() {
554 foreach ($this->getObjects() as $articleEditor) {
555 $articleEditor->update([
557 'publicationStatus' => Article
::PUBLISHED
,
558 'publicationDate' => 0
562 $this->unmarkItems();
566 * Validates the `unpublish` action.
568 * @throws UserInputException
570 public function validateUnpublish() {
571 WCF
::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
573 if (empty($this->objects
)) {
574 $this->readObjects();
576 if (empty($this->objects
)) {
577 throw new UserInputException('objectIDs');
581 foreach ($this->getObjects() as $article) {
582 if ($article->publicationStatus
!= Article
::PUBLISHED
) {
583 throw new UserInputException('objectIDs');
589 * Unpublishes articles.
591 public function unpublish() {
592 foreach ($this->getObjects() as $articleEditor) {
593 $articleEditor->update(['publicationStatus' => Article
::UNPUBLISHED
]);
596 $this->unmarkItems();
602 * @param integer[] $articleIDs
604 protected function unmarkItems(array $articleIDs = []) {
605 if (empty($articleIDs)) {
606 foreach ($this->getObjects() as $article) {
607 $articleIDs[] = $article->articleID
;
611 if (!empty($articleIDs)) {
612 ClipboardHandler
::getInstance()->unmark($articleIDs, ClipboardHandler
::getInstance()->getObjectTypeID('com.woltlab.wcf.article'));