Merge branch '2.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / comment / CommentAction.class.php
1 <?php
2 namespace wcf\data\comment;
3 use wcf\data\comment\response\CommentResponse;
4 use wcf\data\comment\response\CommentResponseAction;
5 use wcf\data\comment\response\CommentResponseEditor;
6 use wcf\data\comment\response\CommentResponseList;
7 use wcf\data\comment\response\StructuredCommentResponse;
8 use wcf\data\object\type\ObjectTypeCache;
9 use wcf\data\user\UserProfile;
10 use wcf\data\AbstractDatabaseObjectAction;
11 use wcf\system\comment\CommentHandler;
12 use wcf\system\exception\PermissionDeniedException;
13 use wcf\system\exception\UserInputException;
14 use wcf\system\like\LikeHandler;
15 use wcf\system\recaptcha\RecaptchaHandler;
16 use wcf\system\user\activity\event\UserActivityEventHandler;
17 use wcf\system\user\notification\object\CommentResponseUserNotificationObject;
18 use wcf\system\user\notification\object\CommentUserNotificationObject;
19 use wcf\system\user\notification\UserNotificationHandler;
20 use wcf\system\WCF;
21 use wcf\util\MessageUtil;
22 use wcf\util\UserUtil;
23
24 /**
25 * Executes comment-related actions.
26 *
27 * @author Alexander Ebert
28 * @copyright 2001-2014 WoltLab GmbH
29 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
30 * @package com.woltlab.wcf
31 * @subpackage data.comment
32 * @category Community Framework
33 */
34 class CommentAction extends AbstractDatabaseObjectAction {
35 /**
36 * @see \wcf\data\AbstractDatabaseObjectAction::$allowGuestAccess
37 */
38 protected $allowGuestAccess = array('addComment', 'addResponse', 'loadComments', 'getGuestDialog');
39
40 /**
41 * @see \wcf\data\AbstractDatabaseObjectAction::$className
42 */
43 protected $className = 'wcf\data\comment\CommentEditor';
44
45 /**
46 * comment object
47 * @var \wcf\data\comment\Comment
48 */
49 protected $comment = null;
50
51 /**
52 * comment processor
53 * @var \wcf\system\comment\manager\ICommentManager
54 */
55 protected $commentProcessor = null;
56
57 /**
58 * response object
59 * @var \wcf\data\comment\response\CommentResponse
60 */
61 protected $response = null;
62
63 /**
64 * comment object created by addComment()
65 * @var \wcf\data\comment\Comment
66 */
67 public $createdComment = null;
68
69 /**
70 * response object created by addResponse()
71 * @var \wcf\data\comment\response\CommentResponse
72 */
73 public $createdResponse = null;
74
75 /**
76 * errors occuring durch the validation of addComment or addResponse
77 * @var array
78 */
79 public $validationErrors = array();
80
81 /**
82 * @see \wcf\data\AbstractDatabaseObjectAction::delete()
83 */
84 public function delete() {
85 if (empty($this->objects)) {
86 $this->readObjects();
87 }
88
89 // update counters
90 $processors = array();
91 $groupCommentIDs = $commentIDs = array();
92 foreach ($this->objects as $comment) {
93 if (!isset($processors[$comment->objectTypeID])) {
94 $objectType = ObjectTypeCache::getInstance()->getObjectType($comment->objectTypeID);
95 $processors[$comment->objectTypeID] = $objectType->getProcessor();
96
97 $groupCommentIDs[$comment->objectTypeID] = array();
98 }
99
100 $processors[$comment->objectTypeID]->updateCounter($comment->objectID, -1 * ($comment->responses + 1));
101 $groupCommentIDs[$comment->objectTypeID][] = $comment->commentID;
102 $commentIDs[] = $comment->commentID;
103 }
104
105 if (!empty($groupCommentIDs)) {
106 $likeObjectIDs = array();
107 foreach ($groupCommentIDs as $objectTypeID => $objectIDs) {
108 // remove activity events
109 $objectType = ObjectTypeCache::getInstance()->getObjectType($objectTypeID);
110 if (UserActivityEventHandler::getInstance()->getObjectTypeID($objectType->objectType.'.recentActivityEvent')) {
111 UserActivityEventHandler::getInstance()->removeEvents($objectType->objectType.'.recentActivityEvent', $objectIDs);
112 }
113
114 $likeObjectIDs = array_merge($likeObjectIDs, $objectIDs);
115
116 // delete notifications
117 $objectType = ObjectTypeCache::getInstance()->getObjectType($comment->objectTypeID);
118 if (UserNotificationHandler::getInstance()->getObjectTypeID($objectType->objectType.'.notification')) {
119 UserNotificationHandler::getInstance()->deleteNotifications('comment', $objectType->objectType.'.notification', array(), $objectIDs);
120 }
121 }
122
123 // remove likes
124 LikeHandler::getInstance()->removeLikes('com.woltlab.wcf.comment', $likeObjectIDs);
125 }
126
127 // delete responses
128 if (!empty($commentIDs)) {
129 $commentResponseList = new CommentResponseList();
130 $commentResponseList->getConditionBuilder()->add('comment_response.commentID IN (?)', array($commentIDs));
131 $commentResponseList->readObjectIDs();
132 if (count($commentResponseList->getObjectIDs())) {
133 $action = new CommentResponseAction($commentResponseList->getObjectIDs(), 'delete', array(
134 'ignoreCounters' => true
135 ));
136 $action->executeAction();
137 }
138 }
139
140 return parent::delete();
141 }
142
143 /**
144 * Validates parameters to load comments.
145 */
146 public function validateLoadComments() {
147 $this->readInteger('lastCommentTime', false, 'data');
148 $this->readInteger('objectID', false, 'data');
149
150 $objectType = $this->validateObjectType();
151 $this->commentProcessor = $objectType->getProcessor();
152 if (!$this->commentProcessor->isAccessible($this->parameters['data']['objectID'])) {
153 throw new PermissionDeniedException();
154 }
155 }
156
157 /**
158 * Returns parsed comments.
159 *
160 * @return array
161 */
162 public function loadComments() {
163 $commentList = CommentHandler::getInstance()->getCommentList($this->commentProcessor, $this->parameters['data']['objectTypeID'], $this->parameters['data']['objectID'], false);
164 $commentList->getConditionBuilder()->add("comment.time < ?", array($this->parameters['data']['lastCommentTime']));
165 $commentList->readObjects();
166
167 WCF::getTPL()->assign(array(
168 'commentList' => $commentList,
169 'likeData' => (MODULE_LIKE ? $commentList->getLikeData() : array())
170 ));
171
172 return array(
173 'lastCommentTime' => $commentList->getMinCommentTime(),
174 'template' => WCF::getTPL()->fetch('commentList')
175 );
176 }
177
178 /**
179 * Validates parameters to add a comment.
180 */
181 public function validateAddComment() {
182 CommentHandler::enforceFloodControl();
183
184 $this->readInteger('objectID', false, 'data');
185
186 $this->validateUsername();
187 $this->validateRecaptcha();
188
189 $this->validateMessage();
190 $objectType = $this->validateObjectType();
191
192 // validate object id and permissions
193 $this->commentProcessor = $objectType->getProcessor();
194 if (!$this->commentProcessor->canAdd($this->parameters['data']['objectID'])) {
195 throw new PermissionDeniedException();
196 }
197 }
198
199 /**
200 * Adds a comment.
201 *
202 * @return array
203 */
204 public function addComment() {
205 if (!empty($this->validationErrors)) {
206 return array(
207 'errors' => $this->validationErrors
208 );
209 }
210
211 // create comment
212 $this->createdComment = CommentEditor::create(array(
213 'objectTypeID' => $this->parameters['data']['objectTypeID'],
214 'objectID' => $this->parameters['data']['objectID'],
215 'time' => TIME_NOW,
216 'userID' => WCF::getUser()->userID ?: null,
217 'username' => WCF::getUser()->userID ? WCF::getUser()->username : $this->parameters['data']['username'],
218 'message' => $this->parameters['data']['message'],
219 'responses' => 0,
220 'responseIDs' => serialize(array())
221 ));
222
223 // update counter
224 $this->commentProcessor->updateCounter($this->parameters['data']['objectID'], 1);
225
226 // fire activity event
227 $objectType = ObjectTypeCache::getInstance()->getObjectType($this->parameters['data']['objectTypeID']);
228 if ($this->createdComment->userID && UserActivityEventHandler::getInstance()->getObjectTypeID($objectType->objectType.'.recentActivityEvent')) {
229 UserActivityEventHandler::getInstance()->fireEvent($objectType->objectType.'.recentActivityEvent', $this->createdComment->commentID);
230 }
231
232 // fire notification event
233 if (UserNotificationHandler::getInstance()->getObjectTypeID($objectType->objectType.'.notification')) {
234 $notificationObjectType = UserNotificationHandler::getInstance()->getObjectTypeProcessor($objectType->objectType.'.notification');
235 $userID = $notificationObjectType->getOwnerID($this->createdComment->commentID);
236 if ($userID != WCF::getUser()->userID) {
237 $notificationObject = new CommentUserNotificationObject($this->createdComment);
238
239 UserNotificationHandler::getInstance()->fireEvent('comment', $objectType->objectType.'.notification', $notificationObject, array($userID));
240 }
241 }
242
243 if (!$this->createdComment->userID) {
244 // save user name is session
245 WCF::getSession()->register('username', $this->createdComment->username);
246
247 // save last comment time for flood control
248 WCF::getSession()->register('lastCommentTime', $this->createdComment->time);
249
250 // unmark recaptcha as done for furture requests
251 WCF::getSession()->unregister('recaptchaDone');
252 }
253
254 return array(
255 'template' => $this->renderComment($this->createdComment)
256 );
257 }
258
259 /**
260 * Validates parameters to add a response.
261 */
262 public function validateAddResponse() {
263 CommentHandler::enforceFloodControl();
264
265 $this->readInteger('objectID', false, 'data');
266 $this->validateMessage();
267
268 $this->validateUsername();
269 $this->validateRecaptcha();
270
271 // validate comment id
272 $this->validateCommentID();
273
274 $objectType = $this->validateObjectType();
275
276 // validate object id and permissions
277 $this->commentProcessor = $objectType->getProcessor();
278 if (!$this->commentProcessor->canAdd($this->parameters['data']['objectID'])) {
279 throw new PermissionDeniedException();
280 }
281 }
282
283 /**
284 * Adds a response.
285 *
286 * @return array
287 */
288 public function addResponse() {
289 if (!empty($this->validationErrors)) {
290 return array(
291 'errors' => $this->validationErrors
292 );
293 }
294
295 // create response
296 $this->createdResponse = CommentResponseEditor::create(array(
297 'commentID' => $this->comment->commentID,
298 'time' => TIME_NOW,
299 'userID' => WCF::getUser()->userID ?: null,
300 'username' => WCF::getUser()->userID ? WCF::getUser()->username : $this->parameters['data']['username'],
301 'message' => $this->parameters['data']['message']
302 ));
303
304 // update response data
305 $responseIDs = $this->comment->getResponseIDs();
306 if (count($responseIDs) < 3) {
307 $responseIDs[] = $this->createdResponse->responseID;
308 }
309 $responses = $this->comment->responses + 1;
310
311 // update comment
312 $commentEditor = new CommentEditor($this->comment);
313 $commentEditor->update(array(
314 'responseIDs' => serialize($responseIDs),
315 'responses' => $responses
316 ));
317
318 // update counter
319 $this->commentProcessor->updateCounter($this->parameters['data']['objectID'], 1);
320
321 // fire activity event
322 $objectType = ObjectTypeCache::getInstance()->getObjectType($this->comment->objectTypeID);
323 if ($this->createdResponse->userID && UserActivityEventHandler::getInstance()->getObjectTypeID($objectType->objectType.'.response.recentActivityEvent')) {
324 UserActivityEventHandler::getInstance()->fireEvent($objectType->objectType.'.response.recentActivityEvent', $this->createdResponse->responseID);
325 }
326
327 // fire notification event
328 if (UserNotificationHandler::getInstance()->getObjectTypeID($objectType->objectType.'.response.notification')) {
329 $notificationObject = new CommentResponseUserNotificationObject($this->createdResponse);
330 if ($this->comment->userID != WCF::getUser()->userID) {
331 UserNotificationHandler::getInstance()->fireEvent('commentResponse', $objectType->objectType.'.response.notification', $notificationObject, array($this->comment->userID));
332 }
333
334 // notify the container owner
335 if (UserNotificationHandler::getInstance()->getObjectTypeID($objectType->objectType.'.notification')) {
336 $notificationObjectType = UserNotificationHandler::getInstance()->getObjectTypeProcessor($objectType->objectType.'.notification');
337 $userID = $notificationObjectType->getOwnerID($this->comment->commentID);
338
339 if ($userID != $this->comment->userID && $userID != WCF::getUser()->userID) {
340 UserNotificationHandler::getInstance()->fireEvent('commentResponseOwner', $objectType->objectType.'.response.notification', $notificationObject, array($userID));
341 }
342 }
343 }
344
345 if (!$this->createdResponse->userID) {
346 // save user name is session
347 WCF::getSession()->register('username', $this->createdResponse->username);
348
349 // save last comment time for flood control
350 WCF::getSession()->register('lastCommentTime', $this->createdResponse->time);
351
352 // unmark recaptcha as done for furture requests
353 WCF::getSession()->unregister('recaptchaDone');
354 }
355
356 return array(
357 'commentID' => $this->comment->commentID,
358 'template' => $this->renderResponse($this->createdResponse),
359 'responses' => $responses
360 );
361 }
362
363 /**
364 * Validates parameters to edit a comment or a response.
365 */
366 public function validatePrepareEdit() {
367 // validate comment id or response id
368 try {
369 $this->validateCommentID();
370 }
371 catch (UserInputException $e) {
372 try {
373 $this->validateResponseID();
374 }
375 catch (UserInputException $e) {
376 throw new UserInputException('objectIDs');
377 }
378 }
379
380 // validate object type id
381 $objectType = $this->validateObjectType();
382
383 // validate object id and permissions
384 $this->commentProcessor = $objectType->getProcessor();
385 if ($this->comment !== null) {
386 if (!$this->commentProcessor->canEditComment($this->comment)) {
387 throw new PermissionDeniedException();
388 }
389 }
390 else {
391 if (!$this->commentProcessor->canEditResponse($this->response)) {
392 throw new PermissionDeniedException();
393 }
394 }
395 }
396
397 /**
398 * Prepares editing of a comment or a response.
399 *
400 * @return array
401 */
402 public function prepareEdit() {
403 $message = '';
404 if ($this->comment !== null) {
405 $message = $this->comment->message;
406 }
407 else {
408 $message = $this->response->message;
409 }
410
411 $returnValues = array(
412 'action' => 'prepare',
413 'message' => $message
414 );
415 if ($this->comment !== null) {
416 $returnValues['commentID'] = $this->comment->commentID;
417 }
418 else {
419 $returnValues['responseID'] = $this->response->responseID;
420 }
421
422 return $returnValues;
423 }
424
425 /**
426 * @see \wcf\data\comment\CommentAction::validatePrepareEdit()
427 */
428 public function validateEdit() {
429 $this->validatePrepareEdit();
430
431 $this->validateMessage();
432 }
433
434 /**
435 * Edits a comment or response.
436 *
437 * @return array
438 */
439 public function edit() {
440 $returnValues = array(
441 'action' => 'saved',
442 );
443
444 if ($this->response === null) {
445 $editor = new CommentEditor($this->comment);
446 $editor->update(array(
447 'message' => $this->parameters['data']['message']
448 ));
449 $comment = new Comment($this->comment->commentID);
450 $returnValues['commentID'] = $this->comment->commentID;
451 $returnValues['message'] = $comment->getFormattedMessage();
452 }
453 else {
454 $editor = new CommentResponseEditor($this->response);
455 $editor->update(array(
456 'message' => $this->parameters['data']['message']
457 ));
458 $response = new CommentResponse($this->response->responseID);
459 $returnValues['responseID'] = $this->response->responseID;
460 $returnValues['message'] = $response->getFormattedMessage();
461 }
462
463 return $returnValues;
464 }
465
466 /**
467 * Validates parameters to remove a comment or response.
468 */
469 public function validateRemove() {
470 // validate comment id or response id
471 try {
472 $this->validateCommentID();
473 }
474 catch (UserInputException $e) {
475 try {
476 $this->validateResponseID();
477 }
478 catch (UserInputException $e) {
479 throw new UserInputException('objectIDs');
480 }
481 }
482
483 // validate object type id
484 $objectType = $this->validateObjectType();
485
486 // validate object id and permissions
487 $this->commentProcessor = $objectType->getProcessor();
488 if ($this->comment !== null) {
489 if (!$this->commentProcessor->canDeleteComment($this->comment)) {
490 throw new PermissionDeniedException();
491 }
492 }
493 else {
494 if (!$this->commentProcessor->canDeleteResponse($this->response)) {
495 throw new PermissionDeniedException();
496 }
497 }
498 }
499
500 /**
501 * Removes a comment or response.
502 *
503 * @return array
504 */
505 public function remove() {
506 if ($this->comment !== null) {
507 $objectAction = new CommentAction(array($this->comment), 'delete');
508 $objectAction->executeAction();
509
510 return array(
511 'commentID' => $this->comment->commentID
512 );
513 }
514 else {
515 $objectAction = new CommentResponseAction(array($this->response), 'delete');
516 $objectAction->executeAction();
517
518 return array(
519 'responseID' => $this->response->responseID
520 );
521 }
522 }
523
524 /**
525 * Validates the 'getGuestDialog' action.
526 */
527 public function validateGetGuestDialog() {
528 if (WCF::getUser()->userID) {
529 throw new PermissionDeniedException();
530 }
531
532 CommentHandler::enforceFloodControl();
533
534 $this->readInteger('objectID', false, 'data');
535 $objectType = $this->validateObjectType();
536
537 // validate object id and permissions
538 $this->commentProcessor = $objectType->getProcessor();
539 if (!$this->commentProcessor->canAdd($this->parameters['data']['objectID'])) {
540 throw new PermissionDeniedException();
541 }
542
543 // validate message already at this point to make sure that the
544 // message is valid when submitting the dialog to avoid having to
545 // go back to the message to fix it
546 $this->validateMessage();
547 }
548
549 /**
550 * Returns the dialog for guests when they try to write a comment letting
551 * them enter a username and solving a captcha.
552 *
553 * @return array
554 */
555 public function getGuestDialog() {
556 RecaptchaHandler::getInstance()->assignVariables();
557
558 return array(
559 'template' => WCF::getTPL()->fetch('commentAddGuestDialog', 'wcf', array(
560 'ajaxRecaptcha' => true,
561 'username' => WCF::getSession()->getVar('username')
562 ))
563 );
564 }
565
566 /**
567 * Renders a comment.
568 *
569 * @param \wcf\data\comment\Comment $comment
570 * @return string
571 */
572 protected function renderComment(Comment $comment) {
573 $comment = new StructuredComment($comment);
574 $comment->setIsDeletable($this->commentProcessor->canDeleteComment($comment->getDecoratedObject()));
575 $comment->setIsEditable($this->commentProcessor->canEditComment($comment->getDecoratedObject()));
576
577 // set user profile
578 if ($comment->userID) {
579 $userProfile = UserProfile::getUserProfile($comment->userID);
580 $comment->setUserProfile($userProfile);
581 }
582
583 WCF::getTPL()->assign(array(
584 'commentList' => array($comment)
585 ));
586 return WCF::getTPL()->fetch('commentList');
587 }
588
589 /**
590 * Renders a response.
591 *
592 * @param \wcf\data\comment\response\CommentResponse $response
593 * @return string
594 */
595 protected function renderResponse(CommentResponse $response) {
596 $response = new StructuredCommentResponse($response);
597 $response->setIsDeletable($this->commentProcessor->canDeleteResponse($response->getDecoratedObject()));
598 $response->setIsEditable($this->commentProcessor->canEditResponse($response->getDecoratedObject()));
599
600 // set user profile
601 if ($response->userID) {
602 $userProfile = UserProfile::getUserProfile($response->userID);
603 $response->setUserProfile($userProfile);
604 }
605
606 // render response
607 WCF::getTPL()->assign(array(
608 'responseList' => array($response)
609 ));
610 return WCF::getTPL()->fetch('commentResponseList');
611 }
612
613 /**
614 * Validates message parameter.
615 */
616 protected function validateMessage() {
617 $this->readString('message', false, 'data');
618 $this->parameters['data']['message'] = MessageUtil::stripCrap($this->parameters['data']['message']);
619
620 if (empty($this->parameters['data']['message'])) {
621 throw new UserInputException('message');
622 }
623 }
624
625 /**
626 * Validates object type id parameter.
627 *
628 * @return \wcf\data\object\type\ObjectType
629 */
630 protected function validateObjectType() {
631 $this->readInteger('objectTypeID', false, 'data');
632
633 $objectType = ObjectTypeCache::getInstance()->getObjectType($this->parameters['data']['objectTypeID']);
634 if ($objectType === null) {
635 throw new UserInputException('objectTypeID');
636 }
637
638 return $objectType;
639 }
640
641 /**
642 * Validates comment id parameter.
643 */
644 protected function validateCommentID() {
645 $this->readInteger('commentID', false, 'data');
646
647 $this->comment = new Comment($this->parameters['data']['commentID']);
648 if ($this->comment === null || !$this->comment->commentID) {
649 throw new UserInputException('commentID');
650 }
651 }
652
653 /**
654 * Validates response id parameter.
655 */
656 protected function validateResponseID() {
657 if (isset($this->parameters['data']['responseID'])) {
658 $this->response = new CommentResponse($this->parameters['data']['responseID']);
659 }
660 if ($this->response === null || !$this->response->responseID) {
661 throw new UserInputException('responseID');
662 }
663 }
664
665 /**
666 * Validates the username parameter.
667 */
668 protected function validateUsername() {
669 if (WCF::getUser()->userID) return;
670
671 try {
672 $this->readString('username', false, 'data');
673
674 if (!UserUtil::isValidUsername($this->parameters['data']['username'])) {
675 throw new UserInputException('username', 'notValid');
676 }
677 if (!UserUtil::isAvailableUsername($this->parameters['data']['username'])) {
678 throw new UserInputException('username', 'notUnique');
679 }
680 }
681 catch (UserInputException $e) {
682 if ($e->getType() == 'empty') {
683 $this->validationErrors['username'] = WCF::getLanguage()->get('wcf.global.form.error.empty');
684 }
685 else {
686 $this->validationErrors['username'] = WCF::getLanguage()->get('wcf.user.username.error.'.$e->getType());
687 }
688 }
689 }
690
691 /**
692 * Validates the recaptcha challenge.
693 */
694 protected function validateRecaptcha() {
695 if (WCF::getUser()->userID || !MODULE_SYSTEM_RECAPTCHA || WCF::getSession()->getVar('recaptchaDone')) return;
696
697 $this->readString('recaptchaChallenge');
698 $this->readString('recaptchaResponse');
699
700 try {
701 RecaptchaHandler::getInstance()->validate($this->parameters['recaptchaChallenge'], $this->parameters['recaptchaResponse']);
702 }
703 catch (UserInputException $e) {
704 $this->validationErrors['recaptcha'] = WCF::getLanguage()->get('wcf.recaptcha.error.recaptchaString.false');
705 }
706 }
707
708 /**
709 * Returns the comment object.
710 *
711 * @return \wcf\data\comment\Comment
712 */
713 public function getComment() {
714 return $this->comment;
715 }
716
717 /**
718 * Returns the comment response object.
719 *
720 * @return \wcf\data\comment\response\CommentResponse
721 */
722 public function getResponse() {
723 return $this->response;
724 }
725
726 /**
727 * Returns the comment manager.
728 *
729 * @return \wcf\system\comment\manager\ICommentManager
730 */
731 public function getCommentManager() {
732 return $this->commentProcessor;
733 }
734 }