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