Merge branch 'master' into next
[GitHub/WoltLab/com.woltlab.wcf.conversation.git] / files / lib / data / conversation / message / ConversationMessageAction.class.php
1 <?php
2 namespace wcf\data\conversation\message;
3 use wcf\data\conversation\Conversation;
4 use wcf\data\conversation\ConversationAction;
5 use wcf\data\conversation\ConversationEditor;
6 use wcf\data\smiley\SmileyCache;
7 use wcf\data\AbstractDatabaseObjectAction;
8 use wcf\data\DatabaseObject;
9 use wcf\data\IAttachmentMessageQuickReplyAction;
10 use wcf\data\IMessageInlineEditorAction;
11 use wcf\data\IMessageQuoteAction;
12 use wcf\system\attachment\AttachmentHandler;
13 use wcf\system\bbcode\BBCodeHandler;
14 use wcf\system\bbcode\BBCodeParser;
15 use wcf\system\bbcode\PreParser;
16 use wcf\system\exception\PermissionDeniedException;
17 use wcf\system\exception\UserInputException;
18 use wcf\system\message\censorship\Censorship;
19 use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
20 use wcf\system\message\quote\MessageQuoteManager;
21 use wcf\system\message\QuickReplyManager;
22 use wcf\system\moderation\queue\ModerationQueueManager;
23 use wcf\system\request\LinkHandler;
24 use wcf\system\search\SearchIndexManager;
25 use wcf\system\user\notification\object\ConversationMessageUserNotificationObject;
26 use wcf\system\user\notification\UserNotificationHandler;
27 use wcf\system\user\storage\UserStorageHandler;
28 use wcf\system\WCF;
29 use wcf\util\MessageUtil;
30 use wcf\util\StringUtil;
31
32 /**
33 * Executes conversation message-related actions.
34 *
35 * @author Marcel Werk
36 * @copyright 2001-2015 WoltLab GmbH
37 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
38 * @package com.woltlab.wcf.conversation
39 * @subpackage data.conversation.message
40 * @category Community Framework
41 */
42 class ConversationMessageAction extends AbstractDatabaseObjectAction implements IAttachmentMessageQuickReplyAction, IMessageInlineEditorAction, IMessageQuoteAction {
43 /**
44 * @see \wcf\data\AbstractDatabaseObjectAction::$className
45 */
46 protected $className = 'wcf\data\conversation\message\ConversationMessageEditor';
47
48 /**
49 * conversation object
50 * @var \wcf\data\conversation\Conversation
51 */
52 public $conversation = null;
53
54 /**
55 * conversation message object
56 * @var \wcf\data\conversation\message\ConversationMessage
57 */
58 public $message = null;
59
60 /**
61 * @see \wcf\data\AbstractDatabaseObjectAction::create()
62 */
63 public function create() {
64 // count attachments
65 if (isset($this->parameters['attachmentHandler']) && $this->parameters['attachmentHandler'] !== null) {
66 $this->parameters['data']['attachments'] = count($this->parameters['attachmentHandler']);
67 }
68
69 if (LOG_IP_ADDRESS) {
70 // add ip address
71 if (!isset($this->parameters['data']['ipAddress'])) {
72 $this->parameters['data']['ipAddress'] = WCF::getSession()->ipAddress;
73 }
74 }
75 else {
76 // do not track ip address
77 if (isset($this->parameters['data']['ipAddress'])) {
78 unset($this->parameters['data']['ipAddress']);
79 }
80 }
81
82 // create message
83 $message = parent::create();
84 $messageEditor = new ConversationMessageEditor($message);
85
86 // get conversation
87 $conversation = (isset($this->parameters['converation']) ? $this->parameters['converation'] : new Conversation($message->conversationID));
88 $conversationEditor = new ConversationEditor($conversation);
89
90 if (empty($this->parameters['isFirstPost'])) {
91 // update last message
92 $conversationEditor->addMessage($message);
93
94 // fire notification event
95 if (!$conversation->isDraft) {
96 $notificationRecipients = array_diff($conversation->getParticipantIDs(true), array($message->userID)); // don't notify message author
97 if (!empty($notificationRecipients)) {
98 UserNotificationHandler::getInstance()->fireEvent('conversationMessage', 'com.woltlab.wcf.conversation.message.notification', new ConversationMessageUserNotificationObject($message), $notificationRecipients);
99 }
100 }
101
102 $userConversation = Conversation::getUserConversation($conversation->conversationID, $message->userID);
103 if ($userConversation !== null && $userConversation->isInvisible) {
104 // make invisible participant visible
105 $sql = "UPDATE wcf".WCF_N."_conversation_to_user
106 SET isInvisible = 0
107 WHERE participantID = ?
108 AND conversationID = ?";
109 $statement = WCF::getDB()->prepareStatement($sql);
110 $statement->execute(array($message->userID, $conversation->conversationID));
111
112 $conversationEditor->updateParticipantSummary();
113 $conversationEditor->updateParticipantCount();
114 }
115
116 // reset visibility if it was hidden but not left
117 $sql = "UPDATE wcf".WCF_N."_conversation_to_user
118 SET hideConversation = ?
119 WHERE conversationID = ?
120 AND hideConversation = ?";
121 $statement = WCF::getDB()->prepareStatement($sql);
122 $statement->execute(array(
123 Conversation::STATE_DEFAULT,
124 $conversation->conversationID,
125 Conversation::STATE_HIDDEN
126 ));
127 }
128
129 // reset storage
130 UserStorageHandler::getInstance()->reset($conversation->getParticipantIDs(), 'unreadConversationCount');
131
132 // update search index
133 SearchIndexManager::getInstance()->add('com.woltlab.wcf.conversation.message', $message->messageID, $message->message, (!empty($this->parameters['isFirstPost']) ? $conversation->subject : ''), $message->time, $message->userID, $message->username);
134
135 // update attachments
136 if (isset($this->parameters['attachmentHandler']) && $this->parameters['attachmentHandler'] !== null) {
137 $this->parameters['attachmentHandler']->updateObjectID($message->messageID);
138 }
139
140 // save embedded objects
141 if (MessageEmbeddedObjectManager::getInstance()->registerObjects('com.woltlab.wcf.conversation.message', $message->messageID, $message->message)) {
142 $messageEditor->update(array(
143 'hasEmbeddedObjects' => 1
144 ));
145 }
146
147 // clear quotes
148 if (isset($this->parameters['removeQuoteIDs']) && !empty($this->parameters['removeQuoteIDs'])) {
149 MessageQuoteManager::getInstance()->markQuotesForRemoval($this->parameters['removeQuoteIDs']);
150 }
151 MessageQuoteManager::getInstance()->removeMarkedQuotes();
152
153 // return new message
154 return $message;
155 }
156
157 /**
158 * @see \wcf\data\AbstractDatabaseObjectAction::update()
159 */
160 public function update() {
161 // count attachments
162 if (isset($this->parameters['attachmentHandler']) && $this->parameters['attachmentHandler'] !== null) {
163 $this->parameters['data']['attachments'] = count($this->parameters['attachmentHandler']);
164 }
165
166 parent::update();
167
168 // update search index / embedded objects
169 if (isset($this->parameters['data']) && isset($this->parameters['data']['message'])) {
170 foreach ($this->objects as $message) {
171 $conversation = $message->getConversation();
172 SearchIndexManager::getInstance()->update('com.woltlab.wcf.conversation.message', $message->messageID, $this->parameters['data']['message'], ($conversation->firstMessageID == $message->messageID ? $conversation->subject : ''), $message->time, $message->userID, $message->username);
173
174 if ($message->hasEmbeddedObjects != MessageEmbeddedObjectManager::getInstance()->registerObjects('com.woltlab.wcf.conversation.message', $message->messageID, $this->parameters['data']['message'])) {
175 $message->update(array(
176 'hasEmbeddedObjects' => ($message->hasEmbeddedObjects ? 0 : 1)
177 ));
178 }
179 }
180 }
181 }
182
183 /**
184 * @see \wcf\data\AbstractDatabaseObjectAction::delete()
185 */
186 public function delete() {
187 $count = parent::delete();
188
189 $attachmentMessageIDs = $conversationIDs = array();
190 foreach ($this->objects as $message) {
191 if (!in_array($message->conversationID, $conversationIDs)) {
192 $conversationIDs[] = $message->conversationID;
193 }
194
195 if ($message->attachments) {
196 $attachmentMessageIDs[] = $message->messageID;
197 }
198 }
199
200 // rebuild conversations
201 if (!empty($conversationIDs)) {
202 $conversationAction = new ConversationAction($conversationIDs, 'rebuild');
203 $conversationAction->executeAction();
204 }
205
206 if (!empty($this->objectIDs)) {
207 // delete notifications
208 UserNotificationHandler::getInstance()->deleteNotifications('conversationMessage', 'com.woltlab.wcf.conversation.message.notification', array(), $this->objectIDs);
209
210 // update search index
211 SearchIndexManager::getInstance()->delete('com.woltlab.wcf.conversation.message', $this->objectIDs);
212
213 // update embedded objects
214 MessageEmbeddedObjectManager::getInstance()->removeObjects('com.woltlab.wcf.conversation.message', $this->objectIDs);
215
216 // remove moderation queues
217 ModerationQueueManager::getInstance()->removeQueues('com.woltlab.wcf.conversation.message', $this->objectIDs);
218 }
219
220 // remove attachments
221 if (!empty($attachmentMessageIDs)) {
222 AttachmentHandler::removeAttachments('com.woltlab.wcf.conversation.message', $attachmentMessageIDs);
223 }
224
225 return $count;
226 }
227
228 /**
229 * @see \wcf\data\IMessageQuickReply::validateQuickReply()
230 */
231 public function validateQuickReply() {
232 QuickReplyManager::getInstance()->setAllowedBBCodes(explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')));
233 QuickReplyManager::getInstance()->validateParameters($this, $this->parameters, 'wcf\data\conversation\Conversation');
234 }
235
236 /**
237 * @see \wcf\data\IMessageQuickReply::quickReply()
238 */
239 public function quickReply() {
240 return QuickReplyManager::getInstance()->createMessage(
241 $this,
242 $this->parameters,
243 'wcf\data\conversation\ConversationAction',
244 CONVERSATION_LIST_DEFAULT_SORT_ORDER,
245 'conversationMessageList'
246 );
247 }
248
249 /**
250 * @see \wcf\data\IExtendedMessageQuickReplyAction::validateJumpToExtended()
251 */
252 public function validateJumpToExtended() {
253 $this->readInteger('containerID');
254 $this->readString('message', true);
255 $this->readString('tmpHash', true);
256
257 $this->conversation = new Conversation($this->parameters['containerID']);
258 if (!$this->conversation->conversationID) {
259 throw new UserInputException('containerID');
260 }
261 else if ($this->conversation->isClosed || !Conversation::isParticipant(array($this->conversation->conversationID))) {
262 throw new PermissionDeniedException();
263 }
264
265 // editing existing message
266 if (isset($this->parameters['messageID'])) {
267 $this->message = new ConversationMessage(intval($this->parameters['messageID']));
268 if (!$this->message->messageID || ($this->message->conversationID != $this->conversation->conversationID)) {
269 throw new UserInputException('messageID');
270 }
271
272 if (!$this->message->canEdit()) {
273 throw new PermissionDeniedException();
274 }
275 }
276 }
277
278 /**
279 * @see \wcf\data\IExtendedMessageQuickReplyAction::jumpToExtended()
280 */
281 public function jumpToExtended() {
282 // quick reply
283 if ($this->message === null) {
284 QuickReplyManager::getInstance()->setMessage('conversation', $this->conversation->conversationID, $this->parameters['message']);
285 $url = LinkHandler::getInstance()->getLink('ConversationMessageAdd', array('id' => $this->conversation->conversationID));
286 }
287 else {
288 // editing message
289 QuickReplyManager::getInstance()->setMessage('conversationMessage', $this->message->messageID, $this->parameters['message']);
290 $url = LinkHandler::getInstance()->getLink('ConversationMessageEdit', array('id' => $this->message->messageID));
291 }
292
293 if (!empty($this->parameters['tmpHash'])) {
294 QuickReplyManager::getInstance()->setTmpHash($this->parameters['tmpHash']);
295 }
296
297 // redirect
298 return array(
299 'url' => $url
300 );
301 }
302
303 /**
304 * @see \wcf\data\IMessageInlineEditorAction::validateBeginEdit()
305 */
306 public function validateBeginEdit() {
307 $this->readInteger('containerID');
308 $this->readInteger('objectID');
309
310 $this->conversation = new Conversation($this->parameters['containerID']);
311 if (!$this->conversation->conversationID) {
312 throw new UserInputException('containerID');
313 }
314
315 if ($this->conversation->isClosed || !Conversation::isParticipant(array($this->conversation->conversationID))) {
316 throw new PermissionDeniedException();
317 }
318
319 $this->message = new ConversationMessage($this->parameters['objectID']);
320 if (!$this->message->messageID) {
321 throw new UserInputException('objectID');
322 }
323
324 if (!$this->message->canEdit()) {
325 throw new PermissionDeniedException();
326 }
327 }
328
329 /**
330 * @see \wcf\data\IMessageInlineEditorAction::beginEdit()
331 */
332 public function beginEdit() {
333 BBCodeHandler::getInstance()->setAllowedBBCodes(explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')));
334
335 WCF::getTPL()->assign(array(
336 'defaultSmilies' => SmileyCache::getInstance()->getCategorySmilies(),
337 'message' => $this->message,
338 'permissionCanUseSmilies' => 'user.message.canUseSmilies',
339 'wysiwygSelector' => 'messageEditor'.$this->message->messageID
340 ));
341
342 if (MODULE_ATTACHMENT) {
343 $tmpHash = StringUtil::getRandomID();
344 $attachmentHandler = new AttachmentHandler('com.woltlab.wcf.conversation.message', $this->message->messageID, $tmpHash);
345 $attachmentList = $attachmentHandler->getAttachmentList();
346
347 WCF::getTPL()->assign(array(
348 'attachmentHandler' => $attachmentHandler,
349 'attachmentList' => $attachmentList->getObjects(),
350 'attachmentObjectID' => $this->message->messageID,
351 'attachmentObjectType' => 'com.woltlab.wcf.conversation.message',
352 'attachmentParentObjectID' => 0,
353 'tmpHash' => $tmpHash
354 ));
355 }
356
357 return array(
358 'actionName' => 'beginEdit',
359 'template' => WCF::getTPL()->fetch('conversationMessageInlineEditor')
360 );
361 }
362
363 /**
364 * @see \wcf\data\IMessageInlineEditorAction::validateSave()
365 */
366 public function validateSave() {
367 $this->readString('message', true, 'data');
368
369 if (empty($this->parameters['data']['message'])) {
370 throw new UserInputException('message', WCF::getLanguage()->get('wcf.global.form.error.empty'));
371 }
372
373 $this->validateBeginEdit();
374 $this->validateMessage($this->conversation, $this->parameters['data']['message']);
375 }
376
377 /**
378 * @see \wcf\data\IMessageInlineEditorAction::save()
379 */
380 public function save() {
381 $data = array(
382 'message' => PreParser::getInstance()->parse(MessageUtil::stripCrap($this->parameters['data']['message']), explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')))
383 );
384 if (!$this->message->getConversation()->isDraft) {
385 $data['lastEditTime'] = TIME_NOW;
386 $data['editCount'] = $this->message->editCount + 1;
387 }
388 // execute update action
389 $action = new ConversationMessageAction(array($this->message), 'update', array('data' => $data));
390 $action->executeAction();
391
392 // load new message
393 $this->message = new ConversationMessage($this->message->messageID);
394 $this->message->getAttachments();
395
396 if (MODULE_ATTACHMENT) {
397 $attachmentList = $this->message->getAttachments(true);
398 $count = 0;
399 if ($attachmentList !== null) {
400 // set permisions
401 $attachmentList->setPermissions(array(
402 'canDownload' => true,
403 'canViewPreview' => true
404 ));
405
406 $count = count($attachmentList);
407 }
408
409 // update count to reflect number of attachments after edit
410 if ($count != $this->message->attachments) {
411 $messageEditor = new ConversationMessageEditor($this->message);
412 $messageEditor->update(array('attachments' => $count));
413 }
414 }
415
416 // load embedded objects
417 MessageEmbeddedObjectManager::getInstance()->loadObjects('com.woltlab.wcf.conversation.message', array($this->message->messageID));
418
419 $data = array(
420 'actionName' => 'save',
421 'message' => $this->message->getFormattedMessage()
422 );
423
424 if (MODULE_ATTACHMENT) {
425 WCF::getTPL()->assign(array(
426 'attachmentList' => $attachmentList,
427 'objectID' => $this->message->messageID
428 ));
429 $data['attachmentList'] = WCF::getTPL()->fetch('attachments');
430 }
431
432 return $data;
433 }
434
435 /**
436 * @see \wcf\data\IMessageQuickReply::validateContainer()
437 */
438 public function validateContainer(DatabaseObject $conversation) {
439 if (!$conversation->conversationID) {
440 throw new UserInputException('objectID');
441 }
442 if ($conversation->isClosed) {
443 throw new PermissionDeniedException();
444 }
445 $conversation->loadUserParticipation();
446 if (!$conversation->canRead()) {
447 throw new PermissionDeniedException();
448 }
449 }
450
451 /**
452 * @see \wcf\data\IMessageQuickReplyAction::validateMessage()
453 */
454 public function validateMessage(DatabaseObject $container, $message) {
455 if (mb_strlen($message) > WCF::getSession()->getPermission('user.conversation.maxLength')) {
456 throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.tooLong', array('maxTextLength' => WCF::getSession()->getPermission('user.conversation.maxLength'))));
457 }
458
459 // search for disallowed bbcodes
460 $disallowedBBCodes = BBCodeParser::getInstance()->validateBBCodes($message, explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')));
461 if (!empty($disallowedBBCodes)) {
462 throw new UserInputException('text', WCF::getLanguage()->getDynamicVariable('wcf.message.error.disallowedBBCodes', array('disallowedBBCodes' => $disallowedBBCodes)));
463 }
464
465 // search for censored words
466 if (ENABLE_CENSORSHIP) {
467 $result = Censorship::getInstance()->test($message);
468 if ($result) {
469 throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.censoredWordsFound', array('censoredWords' => $result)));
470 }
471 }
472 }
473
474 /**
475 * @see \wcf\data\IMessageQuickReplyAction::getMessageList()
476 */
477 public function getMessageList(DatabaseObject $conversation, $lastMessageTime) {
478 $messageList = new ViewableConversationMessageList();
479 $messageList->setConversation($conversation);
480 $messageList->getConditionBuilder()->add("conversation_message.conversationID = ?", array($conversation->conversationID));
481 $messageList->getConditionBuilder()->add("conversation_message.time > ?", array($lastMessageTime));
482 $messageList->sqlOrderBy = "conversation_message.time ".CONVERSATION_LIST_DEFAULT_SORT_ORDER;
483 $messageList->readObjects();
484
485 return $messageList;
486 }
487
488 /**
489 * @see \wcf\data\IMessageQuickReply::getPageNo()
490 */
491 public function getPageNo(DatabaseObject $conversation) {
492 $sql = "SELECT COUNT(*) AS count
493 FROM wcf".WCF_N."_conversation_message
494 WHERE conversationID = ?";
495 $statement = WCF::getDB()->prepareStatement($sql);
496 $statement->execute(array($conversation->conversationID));
497 $count = $statement->fetchArray();
498
499 return array(intval(ceil($count['count'] / CONVERSATION_MESSAGES_PER_PAGE)), $count['count']);
500 }
501
502 /**
503 * @see \wcf\data\IMessageQuickReply::getRedirectUrl()
504 */
505 public function getRedirectUrl(DatabaseObject $conversation, DatabaseObject $message) {
506 return LinkHandler::getInstance()->getLink('Conversation', array(
507 'object' => $conversation,
508 'messageID' => $message->messageID
509 )).'#message'.$message->messageID;
510 }
511
512 /**
513 * @see \wcf\data\IMessageQuoteAction::validateSaveFullQuote()
514 */
515 public function validateSaveFullQuote() {
516 $this->message = $this->getSingleObject();
517
518 if (!Conversation::isParticipant(array($this->message->conversationID))) {
519 throw new PermissionDeniedException();
520 }
521 }
522
523 /**
524 * @see \wcf\data\IMessageQuoteAction::saveFullQuote()
525 */
526 public function saveFullQuote() {
527 $quoteID = MessageQuoteManager::getInstance()->addQuote(
528 'com.woltlab.wcf.conversation.message',
529 $this->message->conversationID,
530 $this->message->messageID,
531 $this->message->getExcerpt(),
532 $this->message->getMessage()
533 );
534
535 if ($quoteID === false) {
536 $removeQuoteID = MessageQuoteManager::getInstance()->getQuoteID('com.woltlab.wcf.conversation.message', $this->message->messageID, $this->message->getExcerpt(), $this->message->getMessage());
537 MessageQuoteManager::getInstance()->removeQuote($removeQuoteID);
538 }
539
540 $returnValues = array(
541 'count' => MessageQuoteManager::getInstance()->countQuotes(),
542 'fullQuoteMessageIDs' => MessageQuoteManager::getInstance()->getFullQuoteObjectIDs(array('com.woltlab.wcf.conversation.message'))
543 );
544
545 if ($quoteID) {
546 $returnValues['renderedQuote'] = MessageQuoteManager::getInstance()->getQuoteComponents($quoteID);
547 }
548
549 return $returnValues;
550 }
551
552 /**
553 * @see \wcf\data\IMessageQuoteAction::validateSaveQuote()
554 */
555 public function validateSaveQuote() {
556 $this->readString('message');
557 $this->readBoolean('renderQuote', true);
558 $this->message = $this->getSingleObject();
559
560 if (!Conversation::isParticipant(array($this->message->conversationID))) {
561 throw new PermissionDeniedException();
562 }
563 }
564
565 /**
566 * @see \wcf\data\IMessageQuoteAction::saveQuote()
567 */
568 public function saveQuote() {
569 $quoteID = MessageQuoteManager::getInstance()->addQuote('com.woltlab.wcf.conversation.message', $this->message->conversationID, $this->message->messageID, $this->parameters['message'], false);
570
571 $returnValues = array(
572 'count' => MessageQuoteManager::getInstance()->countQuotes(),
573 'fullQuoteMessageIDs' => MessageQuoteManager::getInstance()->getFullQuoteObjectIDs(array('com.woltlab.wcf.conversation.message'))
574 );
575
576 if ($this->parameters['renderQuote']) {
577 $returnValues['renderedQuote'] = MessageQuoteManager::getInstance()->getQuoteComponents($quoteID);
578 }
579
580 return $returnValues;
581 }
582
583 /**
584 * @see \wcf\data\IMessageQuoteAction::validateGetRenderedQuotes()
585 */
586 public function validateGetRenderedQuotes() {
587 $this->readInteger('parentObjectID');
588
589 $this->conversation = new Conversation($this->parameters['parentObjectID']);
590 if (!$this->conversation->conversationID) {
591 throw new UserInputException('parentObjectID');
592 }
593 }
594
595 /**
596 * @see \wcf\data\IMessageQuoteAction::getRenderedQuotes()
597 */
598 public function getRenderedQuotes() {
599 $quotes = MessageQuoteManager::getInstance()->getQuotesByParentObjectID('com.woltlab.wcf.conversation.message', $this->conversation->conversationID);
600
601 return array(
602 'template' => implode("\n\n", $quotes)
603 );
604 }
605
606 /**
607 * @see \wcf\data\IAttachmentMessageQuickReplyAction::getAttachmentHandler()
608 */
609 public function getAttachmentHandler(DatabaseObject $conversation) {
610 return new AttachmentHandler('com.woltlab.wcf.conversation.message', 0, $this->parameters['tmpHash']);
611 }
612 }