Commit | Line | Data |
---|---|---|
a5a4f02d MW |
1 | <?php |
2 | namespace wcf\data\article; | |
a5a4f02d | 3 | use wcf\data\article\content\ArticleContent; |
70ecac8c | 4 | use wcf\data\article\content\ArticleContentAction; |
a5a4f02d | 5 | use wcf\data\article\content\ArticleContentEditor; |
7a08797b | 6 | use wcf\data\AbstractDatabaseObjectAction; |
70ecac8c | 7 | use wcf\data\language\Language; |
2fd812d7 | 8 | use wcf\system\comment\CommentHandler; |
a81168d4 | 9 | use wcf\system\exception\UserInputException; |
a5a4f02d | 10 | use wcf\system\language\LanguageFactory; |
2fd812d7 | 11 | use wcf\system\like\LikeHandler; |
ef17c746 | 12 | use wcf\system\message\embedded\object\MessageEmbeddedObjectManager; |
402a1169 | 13 | use wcf\system\request\LinkHandler; |
2fd812d7 | 14 | use wcf\system\search\SearchIndexManager; |
a5a4f02d | 15 | use wcf\system\tagging\TagEngine; |
ce6f758b | 16 | use wcf\system\user\storage\UserStorageHandler; |
3bd00cc5 | 17 | use wcf\system\version\VersionTracker; |
ce6f758b | 18 | use wcf\system\visitTracker\VisitTracker; |
a81168d4 | 19 | use 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 | */ | |
33 | class 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 | } |