Implemented toggle for articles between i18n/monolingual
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / article / ArticleAction.class.php
CommitLineData
a5a4f02d
MW
1<?php
2namespace wcf\data\article;
a5a4f02d 3use wcf\data\article\content\ArticleContent;
70ecac8c 4use wcf\data\article\content\ArticleContentAction;
a5a4f02d 5use wcf\data\article\content\ArticleContentEditor;
7a08797b 6use wcf\data\AbstractDatabaseObjectAction;
70ecac8c 7use wcf\data\language\Language;
2fd812d7 8use wcf\system\comment\CommentHandler;
a81168d4 9use wcf\system\exception\UserInputException;
a5a4f02d 10use wcf\system\language\LanguageFactory;
2fd812d7 11use wcf\system\like\LikeHandler;
ef17c746 12use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
402a1169 13use wcf\system\request\LinkHandler;
2fd812d7 14use wcf\system\search\SearchIndexManager;
a5a4f02d 15use wcf\system\tagging\TagEngine;
ce6f758b 16use wcf\system\user\storage\UserStorageHandler;
3bd00cc5 17use wcf\system\version\VersionTracker;
ce6f758b 18use wcf\system\visitTracker\VisitTracker;
a81168d4 19use wcf\system\WCF;
a5a4f02d
MW
20
21/**
22 * Executes article related actions.
23 *
24 * @author Marcel Werk
cea1798f 25 * @copyright 2001-2017 WoltLab GmbH
a5a4f02d 26 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
e71525e4
MW
27 * @package WoltLabSuite\Core\Data\Article
28 * @since 3.0
a5a4f02d
MW
29 *
30 * @method ArticleEditor[] getObjects()
31 * @method ArticleEditor getSingleObject()
32 */
33class ArticleAction extends AbstractDatabaseObjectAction {
a81168d4
AE
34 /**
35 * article editor instance
36 * @var ArticleEditor
37 */
38 public $articleEditor;
39
70ecac8c
AE
40 /**
41 * language object
42 * @var Language
43 */
44 public $language;
45
a5a4f02d
MW
46 /**
47 * @inheritDoc
48 */
49 protected $className = ArticleEditor::class;
50
51 /**
52 * @inheritDoc
53 */
54 protected $permissionsCreate = ['admin.content.article.canManageArticle'];
55
56 /**
57 * @inheritDoc
58 */
59 protected $permissionsDelete = ['admin.content.article.canManageArticle'];
60
61 /**
62 * @inheritDoc
63 */
64 protected $permissionsUpdate = ['admin.content.article.canManageArticle'];
65
66 /**
67 * @inheritDoc
68 */
70ecac8c 69 protected $requireACP = ['create', 'delete', 'restore', 'toggleI18n', 'trash', 'update'];
a5a4f02d 70
ce6f758b
MW
71 /**
72 * @inheritDoc
73 */
74 protected $allowGuestAccess = ['markAllAsRead'];
75
a5a4f02d
MW
76 /**
77 * @inheritDoc
78 * @return Article
79 */
80 public function create() {
81 /** @var Article $article */
82 $article = parent::create();
83
84 // save article content
85 if (!empty($this->parameters['content'])) {
86 foreach ($this->parameters['content'] as $languageID => $content) {
ef17c746
MW
87 if (!empty($content['htmlInputProcessor'])) {
88 /** @noinspection PhpUndefinedMethodInspection */
89 $content['content'] = $content['htmlInputProcessor']->getHtml();
90 }
91
2fd812d7 92 /** @var ArticleContent $articleContent */
a5a4f02d
MW
93 $articleContent = ArticleContentEditor::create([
94 'articleID' => $article->articleID,
63b9817b 95 'languageID' => $languageID ?: null,
a5a4f02d
MW
96 'title' => $content['title'],
97 'teaser' => $content['teaser'],
98 'content' => $content['content'],
79305986
MW
99 'imageID' => $content['imageID'],
100 'teaserImageID' => $content['teaserImageID']
a5a4f02d 101 ]);
ef17c746 102 $articleContentEditor = new ArticleContentEditor($articleContent);
a5a4f02d
MW
103
104 // save tags
105 if (!empty($content['tags'])) {
106 TagEngine::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, $content['tags'], ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID()));
107 }
2fd812d7
MW
108
109 // update search index
e5adfbe2
MS
110 SearchIndexManager::getInstance()->set(
111 'com.woltlab.wcf.article',
112 $articleContent->articleContentID,
113 $articleContent->content,
114 $articleContent->title,
115 $article->time,
116 $article->userID,
117 $article->username,
118 $languageID ?: null,
119 $articleContent->teaser
120 );
ef17c746
MW
121
122 // save embedded objects
123 if (!empty($content['htmlInputProcessor'])) {
2f273839
MW
124 /** @noinspection PhpUndefinedMethodInspection */
125 $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID);
bfb52dd9 126 if (MessageEmbeddedObjectManager::getInstance()->registerObjects($content['htmlInputProcessor'])) {
ef17c746
MW
127 $articleContentEditor->update(['hasEmbeddedObjects' => 1]);
128 }
129 }
a5a4f02d
MW
130 }
131 }
132
ce6f758b
MW
133 // reset storage
134 if (ARTICLE_ENABLE_VISIT_TRACKING) {
135 UserStorageHandler::getInstance()->resetAll('unreadArticles');
136 }
137
a5a4f02d
MW
138 return $article;
139 }
140
141 /**
142 * @inheritDoc
143 */
144 public function update() {
145 parent::update();
146
1bee099c
AE
147 $isRevert = (!empty($this->parameters['isRevert']));
148
a5a4f02d
MW
149 // update article content
150 if (!empty($this->parameters['content'])) {
151 foreach ($this->getObjects() as $article) {
3bd00cc5
AE
152 $versionData = [];
153 $hasChanges = false;
154
a5a4f02d 155 foreach ($this->parameters['content'] as $languageID => $content) {
ef17c746
MW
156 if (!empty($content['htmlInputProcessor'])) {
157 /** @noinspection PhpUndefinedMethodInspection */
158 $content['content'] = $content['htmlInputProcessor']->getHtml();
159 }
160
a5a4f02d 161 $articleContent = ArticleContent::getArticleContent($article->articleID, ($languageID ?: null));
ef17c746 162 $articleContentEditor = null;
a5a4f02d
MW
163 if ($articleContent !== null) {
164 // update
ef17c746
MW
165 $articleContentEditor = new ArticleContentEditor($articleContent);
166 $articleContentEditor->update([
a5a4f02d
MW
167 'title' => $content['title'],
168 'teaser' => $content['teaser'],
169 'content' => $content['content'],
1bee099c
AE
170 'imageID' => ($isRevert) ? $articleContent->imageID : $content['imageID'],
171 'teaserImageID' => ($isRevert) ? $articleContent->teaserImageID : $content['teaserImageID']
a5a4f02d
MW
172 ]);
173
3bd00cc5
AE
174 $versionData[] = $articleContent;
175 if ($articleContent->content != $content['content'] || $articleContent->teaser != $content['teaser'] || $articleContent->title != $content['title']) {
176 $hasChanges = true;
177 }
178
a5a4f02d 179 // delete tags
1bee099c 180 if (!$isRevert && empty($content['tags'])) {
a5a4f02d
MW
181 TagEngine::getInstance()->deleteObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, ($languageID ?: null));
182 }
183 }
184 else {
ef17c746 185 /** @var ArticleContent $articleContent */
a5a4f02d
MW
186 $articleContent = ArticleContentEditor::create([
187 'articleID' => $article->articleID,
63b9817b 188 'languageID' => $languageID ?: null,
a5a4f02d
MW
189 'title' => $content['title'],
190 'teaser' => $content['teaser'],
191 'content' => $content['content'],
1bee099c
AE
192 'imageID' => ($isRevert) ? null : $content['imageID'],
193 'teaserImageID' => ($isRevert) ? null : $content['teaserImageID']
a5a4f02d 194 ]);
ef17c746 195 $articleContentEditor = new ArticleContentEditor($articleContent);
3bd00cc5
AE
196
197 $versionData[] = $articleContent;
198 $hasChanges = true;
a5a4f02d
MW
199 }
200
201 // save tags
1bee099c 202 if (!$isRevert && !empty($content['tags'])) {
a5a4f02d
MW
203 TagEngine::getInstance()->addObjectTags('com.woltlab.wcf.article', $articleContent->articleContentID, $content['tags'], ($languageID ?: LanguageFactory::getInstance()->getDefaultLanguageID()));
204 }
2fd812d7
MW
205
206 // update search index
e5adfbe2
MS
207 SearchIndexManager::getInstance()->set(
208 'com.woltlab.wcf.article',
209 $articleContent->articleContentID,
210 $articleContent->content,
211 $articleContent->title,
212 $article->time,
213 $article->userID,
214 $article->username,
215 $languageID ?: null,
216 $articleContent->teaser
217 );
ef17c746
MW
218
219 // save embedded objects
220 if (!empty($content['htmlInputProcessor'])) {
2f273839
MW
221 /** @noinspection PhpUndefinedMethodInspection */
222 $content['htmlInputProcessor']->setObjectID($articleContent->articleContentID);
bfb52dd9 223 if ($articleContent->hasEmbeddedObjects != MessageEmbeddedObjectManager::getInstance()->registerObjects($content['htmlInputProcessor'])) {
63b9817b 224 $articleContentEditor->update(['hasEmbeddedObjects' => $articleContent->hasEmbeddedObjects ? 0 : 1]);
ef17c746
MW
225 }
226 }
a5a4f02d 227 }
3bd00cc5
AE
228
229 if ($hasChanges) {
230 $articleObj = new ArticleVersionTracker($article->getDecoratedObject());
231 $articleObj->setContent($versionData);
232 VersionTracker::getInstance()->add('com.woltlab.wcf.article', $articleObj);
233 }
a5a4f02d
MW
234 }
235 }
ce6f758b
MW
236
237 // reset storage
238 if (ARTICLE_ENABLE_VISIT_TRACKING) {
239 UserStorageHandler::getInstance()->resetAll('unreadArticles');
240 }
a5a4f02d 241 }
2fd812d7
MW
242
243 /**
244 * @inheritDoc
245 */
246 public function delete() {
247 $articleIDs = $articleContentIDs = [];
248 foreach ($this->getObjects() as $article) {
249 $articleIDs[] = $article->articleID;
0c968ac8 250 foreach ($article->getArticleContents() as $articleContent) {
2fd812d7
MW
251 $articleContentIDs[] = $articleContent->articleContentID;
252 }
253 }
254
255 // delete articles
256 parent::delete();
257
258 if (!empty($articleIDs)) {
259 // delete like data
260 LikeHandler::getInstance()->removeLikes('com.woltlab.wcf.likeableArticle', $articleIDs);
261 // delete comments
62e2632a 262 CommentHandler::getInstance()->deleteObjects('com.woltlab.wcf.articleComment', $articleContentIDs);
2fd812d7
MW
263 // delete tag to object entries
264 TagEngine::getInstance()->deleteObjects('com.woltlab.wcf.article', $articleContentIDs);
265 // delete entry from search index
266 SearchIndexManager::getInstance()->delete('com.woltlab.wcf.article', $articleContentIDs);
267 }
402a1169
AE
268
269 return [
270 'redirectURL' => LinkHandler::getInstance()->getLink('ArticleList', ['isACP' => true])
271 ];
2fd812d7 272 }
a81168d4
AE
273
274 /**
275 * Validates parameters to move an article to the trash bin.
276 *
277 * @throws UserInputException
278 */
279 public function validateTrash() {
280 WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
281
282 $this->articleEditor = $this->getSingleObject();
283 if ($this->articleEditor->isDeleted) {
284 throw new UserInputException('objectIDs');
285 }
286 }
287
288 /**
289 * Moves an article to the trash bin.
290 */
291 public function trash() {
292 $this->articleEditor->update(['isDeleted' => 1]);
293 }
294
295 /**
296 * Validates parameters o restore an article.
297 *
298 * @throws UserInputException
299 */
300 public function validateRestore() {
301 WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
302
303 $this->articleEditor = $this->getSingleObject();
304 if (!$this->articleEditor->isDeleted) {
305 throw new UserInputException('objectIDs');
306 }
307 }
308
309 /**
310 * Restores an article.
311 */
312 public function restore() {
313 $this->articleEditor->update(['isDeleted' => 0]);
314 }
ce6f758b 315
70ecac8c
AE
316 /**
317 * Validates parameters to toggle between i18n and monolingual mode.
318 *
319 * @throws UserInputException
320 */
321 public function validateToggleI18n() {
322 WCF::getSession()->checkPermissions(['admin.content.article.canManageArticle']);
323
324 $this->articleEditor = $this->getSingleObject();
325 if ($this->articleEditor->getDecoratedObject()->isMultilingual) {
326 $this->readInteger('languageID');
327 $this->language = LanguageFactory::getInstance()->getLanguage($this->parameters['languageID']);
328 if ($this->language === null) {
329 throw new UserInputException('languageID');
330 }
331
332 $contents = $this->articleEditor->getArticleContents();
333 if (!isset($contents[$this->language->languageID])) {
334 // there is no content
335 throw new UserInputException('languageID');
336 }
337 }
338 }
339
340 /**
341 * Toggles between i18n and monolingual mode.
342 */
343 public function toggleI18n() {
344 $removeContent = [];
345
346 // i18n -> monolingual
347 if ($this->articleEditor->getDecoratedObject()->isMultilingual) {
348 foreach ($this->articleEditor->getArticleContents() as $articleContent) {
349 if ($articleContent->languageID == $this->language->languageID) {
350 $articleContentEditor = new ArticleContentEditor($articleContent);
351 $articleContentEditor->update(['languageID' => null]);
352 }
353 else {
354 $removeContent[] = $articleContent;
355 }
356 }
357 }
358 else {
359 // monolingual -> i18n
360 $articleContent = $this->articleEditor->getArticleContent();
361 $data = [];
362 foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
363 $data[$language->languageID] = [
364 'title' => $articleContent->title,
365 'teaser' => $articleContent->teaser,
366 'content' => $articleContent->content,
367 'imageID' => $articleContent->imageID ?: null,
368 'teaserImageID' => $articleContent->teaserImageID ?: null
369 ];
370 }
371
372 $action = new ArticleAction([$this->articleEditor], 'update', ['content' => $data]);
373 $action->executeAction();
374
375 $removeContent[] = $articleContent;
376 }
377
378 if (!empty($removeContent)) {
379 $action = new ArticleContentAction($removeContent, 'delete');
380 $action->executeAction();
381 }
382
383 // flush edit history
384 VersionTracker::getInstance()->reset('com.woltlab.wcf.article', $this->articleEditor->getDecoratedObject()->articleID);
385
386 // update article's i18n state
387 $this->articleEditor->update([
388 'isMultilingual' => ($this->articleEditor->getDecoratedObject()->isMultilingual) ? 0 : 1
389 ]);
390 }
391
ce6f758b
MW
392 /**
393 * Marks articles as read.
394 */
395 public function markAsRead() {
396 if (empty($this->parameters['visitTime'])) {
397 $this->parameters['visitTime'] = TIME_NOW;
398 }
399
400 if (empty($this->objects)) {
401 $this->readObjects();
402 }
403
404 $articleIDs = [];
405 foreach ($this->getObjects() as $article) {
406 $articleIDs[] = $article->articleID;
407 VisitTracker::getInstance()->trackObjectVisit('com.woltlab.wcf.article', $article->articleID, $this->parameters['visitTime']);
408 }
409
410 // reset storage
411 if (WCF::getUser()->userID) {
412 UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles');
413 }
414 }
415
416 /**
417 * Marks all articles as read.
418 */
419 public function markAllAsRead() {
420 VisitTracker::getInstance()->trackTypeVisit('com.woltlab.wcf.article');
421
422 // reset storage
423 if (WCF::getUser()->userID) {
424 UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadArticles');
425 }
426 }
427
428 /**
429 * Validates the mark all as read action.
430 */
431 public function validateMarkAllAsRead() {
432 // does nothing
433 }
a5a4f02d 434}