3 namespace wcf\data\conversation\message
;
5 use wcf\data\AbstractDatabaseObjectAction
;
6 use wcf\data\conversation\Conversation
;
7 use wcf\data\conversation\ConversationAction
;
8 use wcf\data\conversation\ConversationEditor
;
9 use wcf\data\DatabaseObject
;
10 use wcf\data\IAttachmentMessageQuickReplyAction
;
11 use wcf\data\IMessageInlineEditorAction
;
12 use wcf\data\IMessageQuoteAction
;
13 use wcf\data\smiley\SmileyCache
;
14 use wcf\system\attachment\AttachmentHandler
;
15 use wcf\system\bbcode\BBCodeHandler
;
16 use wcf\system\conversation\ConversationHandler
;
17 use wcf\system\event\EventHandler
;
18 use wcf\system\exception\NamedUserException
;
19 use wcf\system\exception\PermissionDeniedException
;
20 use wcf\system\exception\UserInputException
;
21 use wcf\system\flood\FloodControl
;
22 use wcf\system\html\input\HtmlInputProcessor
;
23 use wcf\system\html\upcast\HtmlUpcastProcessor
;
24 use wcf\system\message\censorship\Censorship
;
25 use wcf\system\message\embedded\
object\MessageEmbeddedObjectManager
;
26 use wcf\system\message\QuickReplyManager
;
27 use wcf\system\message\quote\MessageQuoteManager
;
28 use wcf\system\moderation\queue\ModerationQueueManager
;
29 use wcf\system\search\SearchIndexManager
;
30 use wcf\system\user\notification\
object\ConversationMessageUserNotificationObject
;
31 use wcf\system\user\notification\UserNotificationHandler
;
32 use wcf\system\user\storage\UserStorageHandler
;
34 use wcf\util\StringUtil
;
35 use wcf\util\UserUtil
;
38 * Executes conversation message-related actions.
41 * @copyright 2001-2019 WoltLab GmbH
42 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
44 * @method ConversationMessageEditor[] getObjects()
45 * @method ConversationMessageEditor getSingleObject()
47 class ConversationMessageAction
extends AbstractDatabaseObjectAction
implements
48 IAttachmentMessageQuickReplyAction
,
49 IMessageInlineEditorAction
,
55 protected $className = ConversationMessageEditor
::class;
64 * @var HtmlInputProcessor
66 public $htmlInputProcessor;
69 * conversation message object
70 * @var ConversationMessage
76 * @return ConversationMessage
78 public function create()
80 if (!isset($this->parameters
['data']['enableHtml'])) {
81 $this->parameters
['data']['enableHtml'] = 1;
85 if (isset($this->parameters
['attachmentHandler']) && $this->parameters
['attachmentHandler'] !== null) {
86 $this->parameters
['data']['attachments'] = \
count($this->parameters
['attachmentHandler']);
91 if (!isset($this->parameters
['data']['ipAddress'])) {
92 $this->parameters
['data']['ipAddress'] = UserUtil
::getIpAddress();
95 // do not track ip address
96 if (isset($this->parameters
['data']['ipAddress'])) {
97 unset($this->parameters
['data']['ipAddress']);
101 if (!empty($this->parameters
['htmlInputProcessor'])) {
102 /** @noinspection PhpUndefinedMethodInspection */
103 $this->parameters
['data']['message'] = $this->parameters
['htmlInputProcessor']->getHtml();
107 /** @var ConversationMessage $message */
108 $message = parent
::create();
109 $messageEditor = new ConversationMessageEditor($message);
112 $conversation = ($this->parameters
['conversation'] ??
new Conversation($message->conversationID
));
113 $conversationEditor = new ConversationEditor($conversation);
115 if (empty($this->parameters
['isFirstPost'])) {
116 // update last message
117 $conversationEditor->addMessage($message);
119 // fire notification event
120 if (!$conversation->isDraft
) {
121 // don't notify message author
122 $notificationRecipients = \array_diff
($conversation->getParticipantIDs(true), [$message->userID
]);
123 if (!empty($notificationRecipients)) {
124 UserNotificationHandler
::getInstance()->fireEvent(
125 'conversationMessage',
126 'com.woltlab.wcf.conversation.message.notification',
127 new ConversationMessageUserNotificationObject($message),
128 $notificationRecipients
133 $userConversation = Conversation
::getUserConversation($conversation->conversationID
, $message->userID
);
134 if ($userConversation !== null && $userConversation->isInvisible
) {
135 // make invisible participant visible
136 $sql = "UPDATE wcf" . WCF_N
. "_conversation_to_user
138 WHERE participantID = ?
139 AND conversationID = ?";
140 $statement = WCF
::getDB()->prepareStatement($sql);
141 $statement->execute([$message->userID
, $conversation->conversationID
]);
143 $conversationEditor->updateParticipantSummary();
144 $conversationEditor->updateParticipantCount();
147 // reset visibility if it was hidden but not left
148 $sql = "UPDATE wcf" . WCF_N
. "_conversation_to_user
149 SET hideConversation = ?
150 WHERE conversationID = ?
151 AND hideConversation = ?";
152 $statement = WCF
::getDB()->prepareStatement($sql);
153 $statement->execute([
154 Conversation
::STATE_DEFAULT
,
155 $conversation->conversationID
,
156 Conversation
::STATE_HIDDEN
,
161 UserStorageHandler
::getInstance()->reset($conversation->getParticipantIDs(), 'unreadConversationCount');
163 // update search index
164 SearchIndexManager
::getInstance()->set(
165 'com.woltlab.wcf.conversation.message',
168 !empty($this->parameters
['isFirstPost']) ?
$conversation->subject
: '',
174 // update attachments
175 if (isset($this->parameters
['attachmentHandler']) && $this->parameters
['attachmentHandler'] !== null) {
176 /** @noinspection PhpUndefinedMethodInspection */
177 $this->parameters
['attachmentHandler']->updateObjectID($message->messageID
);
180 // save embedded objects
181 if (!empty($this->parameters
['htmlInputProcessor'])) {
182 /** @noinspection PhpUndefinedMethodInspection */
183 $this->parameters
['htmlInputProcessor']->setObjectID($message->messageID
);
185 if (MessageEmbeddedObjectManager
::getInstance()->registerObjects($this->parameters
['htmlInputProcessor'])) {
186 $messageEditor->update(['hasEmbeddedObjects' => 1]);
191 if (isset($this->parameters
['removeQuoteIDs']) && !empty($this->parameters
['removeQuoteIDs'])) {
192 MessageQuoteManager
::getInstance()->markQuotesForRemoval($this->parameters
['removeQuoteIDs']);
194 MessageQuoteManager
::getInstance()->removeMarkedQuotes();
196 // return new message
203 public function update()
206 if (isset($this->parameters
['attachmentHandler']) && $this->parameters
['attachmentHandler'] !== null) {
207 $this->parameters
['data']['attachments'] = \
count($this->parameters
['attachmentHandler']);
210 if (!empty($this->parameters
['htmlInputProcessor'])) {
211 /** @noinspection PhpUndefinedMethodInspection */
212 $this->parameters
['data']['message'] = $this->parameters
['htmlInputProcessor']->getHtml();
217 // update search index / embedded objects
218 if (isset($this->parameters
['data']) && isset($this->parameters
['data']['message'])) {
219 foreach ($this->getObjects() as $message) {
220 $conversation = $message->getConversation();
221 SearchIndexManager
::getInstance()->set(
222 'com.woltlab.wcf.conversation.message',
224 $this->parameters
['data']['message'],
225 $conversation->firstMessageID
== $message->messageID ?
$conversation->subject
: '',
231 if (!empty($this->parameters
['htmlInputProcessor'])) {
232 /** @noinspection PhpUndefinedMethodInspection */
233 $this->parameters
['htmlInputProcessor']->setObjectID($message->messageID
);
235 if ($message->hasEmbeddedObjects
!= MessageEmbeddedObjectManager
::getInstance()->registerObjects($this->parameters
['htmlInputProcessor'])) {
236 $message->update(['hasEmbeddedObjects' => $message->hasEmbeddedObjects ?
0 : 1]);
246 public function delete()
248 $count = parent
::delete();
250 $attachmentMessageIDs = $conversationIDs = [];
251 foreach ($this->getObjects() as $message) {
252 if (!\
in_array($message->conversationID
, $conversationIDs)) {
253 $conversationIDs[] = $message->conversationID
;
256 if ($message->attachments
) {
257 $attachmentMessageIDs[] = $message->messageID
;
261 // rebuild conversations
262 if (!empty($conversationIDs)) {
263 $conversationAction = new ConversationAction($conversationIDs, 'rebuild');
264 $conversationAction->executeAction();
267 if (!empty($this->objectIDs
)) {
268 // delete notifications
269 UserNotificationHandler
::getInstance()
270 ->removeNotifications('com.woltlab.wcf.conversation.message.notification', $this->objectIDs
);
272 // update search index
273 SearchIndexManager
::getInstance()->delete('com.woltlab.wcf.conversation.message', $this->objectIDs
);
275 // update embedded objects
276 MessageEmbeddedObjectManager
::getInstance()
277 ->removeObjects('com.woltlab.wcf.conversation.message', $this->objectIDs
);
279 // remove moderation queues
280 ModerationQueueManager
::getInstance()
281 ->removeQueues('com.woltlab.wcf.conversation.message', $this->objectIDs
);
284 // remove attachments
285 if (!empty($attachmentMessageIDs)) {
286 AttachmentHandler
::removeAttachments('com.woltlab.wcf.conversation.message', $attachmentMessageIDs);
295 public function validateQuickReply()
298 ConversationHandler
::getInstance()->enforceFloodControl(true);
299 } catch (NamedUserException
$e) {
300 throw new UserInputException('message', $e->getMessage());
303 QuickReplyManager
::getInstance()->setDisallowedBBCodes(\
explode(
305 WCF
::getSession()->getPermission('user.message.disallowedBBCodes')
307 QuickReplyManager
::getInstance()->validateParameters($this, $this->parameters
, Conversation
::class);
313 public function quickReply()
315 $returnValues = QuickReplyManager
::getInstance()->createMessage(
318 ConversationAction
::class,
319 CONVERSATION_LIST_DEFAULT_SORT_ORDER
,
320 'conversationMessageList'
323 EventHandler
::getInstance()->fireAction($this, 'afterQuickReply', $returnValues);
325 FloodControl
::getInstance()->registerContent('com.woltlab.wcf.conversation.message');
327 return $returnValues;
333 public function validateBeginEdit()
335 $this->readInteger('containerID');
336 $this->readInteger('objectID');
338 $this->conversation
= new Conversation($this->parameters
['containerID']);
339 if (!$this->conversation
->conversationID
) {
340 throw new UserInputException('containerID');
343 if ($this->conversation
->isClosed ||
!Conversation
::isParticipant([$this->conversation
->conversationID
])) {
344 throw new PermissionDeniedException();
347 $this->message
= new ConversationMessage($this->parameters
['objectID']);
348 if (!$this->message
->messageID
) {
349 throw new UserInputException('objectID');
352 if (!$this->message
->canEdit()) {
353 throw new PermissionDeniedException();
356 BBCodeHandler
::getInstance()->setDisallowedBBCodes(\
explode(
358 WCF
::getSession()->getPermission('user.message.disallowedBBCodes')
365 public function beginEdit()
367 $upcastProcessor = new HtmlUpcastProcessor();
368 $upcastProcessor->process(
369 $this->message
->message
,
370 'com.woltlab.wcf.conversation.message',
371 $this->message
->messageID
373 WCF
::getTPL()->assign([
374 'defaultSmilies' => SmileyCache
::getInstance()->getCategorySmilies(),
375 'message' => $this->message
,
376 'text' => $upcastProcessor->getHtml(),
377 'permissionCanUseSmilies' => 'user.message.canUseSmilies',
378 'wysiwygSelector' => 'messageEditor' . $this->message
->messageID
,
381 $tmpHash = StringUtil
::getRandomID();
382 $attachmentHandler = new AttachmentHandler(
383 'com.woltlab.wcf.conversation.message',
384 $this->message
->messageID
,
387 $attachmentList = $attachmentHandler->getAttachmentList();
389 WCF
::getTPL()->assign([
390 'attachmentHandler' => $attachmentHandler,
391 'attachmentList' => $attachmentList->getObjects(),
392 'attachmentObjectID' => $this->message
->messageID
,
393 'attachmentObjectType' => 'com.woltlab.wcf.conversation.message',
394 'attachmentParentObjectID' => 0,
395 'tmpHash' => $tmpHash,
399 'actionName' => 'beginEdit',
400 'template' => WCF
::getTPL()->fetch('conversationMessageInlineEditor'),
407 public function validateSave()
409 $this->readString('message', true, 'data');
411 if (empty($this->parameters
['data']['message'])) {
412 throw new UserInputException(
414 WCF
::getLanguage()->getDynamicVariable('wcf.global.form.error.empty')
418 $this->validateBeginEdit();
420 $this->validateMessage(
422 $this->getHtmlInputProcessor($this->parameters
['data']['message'], $this->message
->messageID
)
429 public function save()
433 if (!$this->message
->getConversation()->isDraft
) {
434 $data['lastEditTime'] = TIME_NOW
;
435 $data['editCount'] = $this->message
->editCount +
1;
437 // execute update action
438 $action = new self([$this->message
], 'update', [
440 'htmlInputProcessor' => $this->getHtmlInputProcessor(),
442 $action->executeAction();
445 $this->message
= new ConversationMessage($this->message
->messageID
);
446 $this->message
->getAttachments();
448 $attachmentList = $this->message
->getAttachments(true);
450 if ($attachmentList !== null) {
452 $attachmentList->setPermissions([
453 'canDownload' => true,
454 'canViewPreview' => true,
457 $count = \
count($attachmentList);
460 // update count to reflect number of attachments after edit
461 if ($count != $this->message
->attachments
) {
462 $messageEditor = new ConversationMessageEditor($this->message
);
463 $messageEditor->update(['attachments' => $count]);
466 // load embedded objects
467 MessageEmbeddedObjectManager
::getInstance()
468 ->loadObjects('com.woltlab.wcf.conversation.message', [$this->message
->messageID
]);
471 'actionName' => 'save',
472 'message' => $this->message
->getFormattedMessage(),
475 WCF
::getTPL()->assign([
476 'attachmentList' => $attachmentList,
477 'objectID' => $this->message
->messageID
,
479 $data['attachmentList'] = WCF
::getTPL()->fetch('attachments');
487 public function validateContainer(DatabaseObject
$container)
489 /** @var Conversation $container */
491 if (!$container->conversationID
) {
492 throw new UserInputException('objectID');
494 if ($container->isClosed
) {
495 throw new PermissionDeniedException();
497 $container->loadUserParticipation();
498 if (!$container->canReply()) {
499 throw new PermissionDeniedException();
506 public function validateMessage(DatabaseObject
$container, HtmlInputProcessor
$htmlInputProcessor)
508 $message = $htmlInputProcessor->getTextContent();
509 if (\
mb_strlen($message) > WCF
::getSession()->getPermission('user.conversation.maxLength')) {
510 throw new UserInputException(
512 WCF
::getLanguage()->getDynamicVariable(
513 'wcf.message.error.tooLong',
514 ['maxTextLength' => WCF
::getSession()->getPermission('user.conversation.maxLength')]
519 // search for disallowed bbcodes
520 $disallowedBBCodes = $htmlInputProcessor->validate();
521 if (!empty($disallowedBBCodes)) {
522 throw new UserInputException(
524 WCF
::getLanguage()->getDynamicVariable(
525 'wcf.message.error.disallowedBBCodes',
526 ['disallowedBBCodes' => $disallowedBBCodes]
531 $censoredWords = Censorship
::getInstance()->test($message);
532 if ($censoredWords) {
533 throw new UserInputException(
535 WCF
::getLanguage()->getDynamicVariable(
536 'wcf.message.error.censoredWordsFound',
537 ['censoredWords' => $censoredWords]
546 public function getMessageList(DatabaseObject
$container, $lastMessageTime)
548 /** @var Conversation $container */
550 $messageList = new ViewableConversationMessageList();
551 $messageList->setConversation($container);
552 $messageList->getConditionBuilder()
553 ->add("conversation_message.conversationID = ?", [$container->conversationID
]);
554 $messageList->getConditionBuilder()
555 ->add("conversation_message.time > ?", [$lastMessageTime]);
556 $messageList->sqlOrderBy
= "conversation_message.time " . CONVERSATION_LIST_DEFAULT_SORT_ORDER
;
557 $messageList->readObjects();
565 public function getPageNo(DatabaseObject
$container)
567 /** @var Conversation $container */
569 $sql = "SELECT COUNT(*) AS count
570 FROM wcf" . WCF_N
. "_conversation_message
571 WHERE conversationID = ?";
572 $statement = WCF
::getDB()->prepareStatement($sql);
573 $statement->execute([$container->conversationID
]);
574 $count = $statement->fetchArray();
576 return [\
intval(\
ceil($count['count'] / CONVERSATION_MESSAGES_PER_PAGE
)), $count['count']];
582 public function getRedirectUrl(DatabaseObject
$container, DatabaseObject
$message)
584 /** @var ConversationMessage $message */
585 return $message->getLink();
591 public function validateSaveFullQuote()
593 $this->message
= $this->getSingleObject();
595 if (!Conversation
::isParticipant([$this->message
->conversationID
])) {
596 throw new PermissionDeniedException();
603 public function saveFullQuote()
605 $quoteID = MessageQuoteManager
::getInstance()->addQuote(
606 'com.woltlab.wcf.conversation.message',
607 $this->message
->conversationID
,
608 $this->message
->messageID
,
609 $this->message
->getExcerpt(),
610 $this->message
->getMessage()
613 if ($quoteID === false) {
614 $removeQuoteID = MessageQuoteManager
::getInstance()->getQuoteID(
615 'com.woltlab.wcf.conversation.message',
616 $this->message
->messageID
,
617 $this->message
->getExcerpt(),
618 $this->message
->getMessage()
620 MessageQuoteManager
::getInstance()->removeQuote($removeQuoteID);
624 'count' => MessageQuoteManager
::getInstance()->countQuotes(),
625 'fullQuoteMessageIDs' => MessageQuoteManager
::getInstance()->getFullQuoteObjectIDs(
626 ['com.woltlab.wcf.conversation.message']
631 $returnValues['renderedQuote'] = MessageQuoteManager
::getInstance()->getQuoteComponents($quoteID);
634 return $returnValues;
640 public function validateSaveQuote()
642 $this->readString('message');
643 $this->readBoolean('renderQuote', true);
644 $this->message
= $this->getSingleObject();
646 if (!Conversation
::isParticipant([$this->message
->conversationID
])) {
647 throw new PermissionDeniedException();
654 public function saveQuote()
656 $quoteID = MessageQuoteManager
::getInstance()->addQuote(
657 'com.woltlab.wcf.conversation.message',
658 $this->message
->conversationID
,
659 $this->message
->messageID
,
660 $this->parameters
['message'],
665 'count' => MessageQuoteManager
::getInstance()->countQuotes(),
666 'fullQuoteMessageIDs' => MessageQuoteManager
::getInstance()->getFullQuoteObjectIDs(
667 ['com.woltlab.wcf.conversation.message']
671 if ($this->parameters
['renderQuote']) {
672 $returnValues['renderedQuote'] = MessageQuoteManager
::getInstance()->getQuoteComponents($quoteID);
675 return $returnValues;
681 public function validateGetRenderedQuotes()
683 $this->readInteger('parentObjectID');
685 $this->conversation
= new Conversation($this->parameters
['parentObjectID']);
686 if (!$this->conversation
->conversationID
) {
687 throw new UserInputException('parentObjectID');
694 public function getRenderedQuotes()
696 $quotes = MessageQuoteManager
::getInstance()
697 ->getQuotesByParentObjectID('com.woltlab.wcf.conversation.message', $this->conversation
->conversationID
);
700 'template' => \
implode("\n\n", $quotes),
707 public function getAttachmentHandler(DatabaseObject
$container)
709 return new AttachmentHandler('com.woltlab.wcf.conversation.message', 0, $this->parameters
['tmpHash']);
715 public function getHtmlInputProcessor($message = null, $objectID = 0)
717 if ($message === null) {
718 return $this->htmlInputProcessor
;
721 $this->htmlInputProcessor
= new HtmlInputProcessor();
722 $this->htmlInputProcessor
->process($message, 'com.woltlab.wcf.conversation.message', $objectID);
724 return $this->htmlInputProcessor
;