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\exception\PermissionDeniedException
;
15 use wcf\system\exception\UserInputException
;
16 use wcf\system\html\input\HtmlInputProcessor
;
17 use wcf\system\message\censorship\Censorship
;
18 use wcf\system\message\embedded\
object\MessageEmbeddedObjectManager
;
19 use wcf\system\message\quote\MessageQuoteManager
;
20 use wcf\system\message\QuickReplyManager
;
21 use wcf\system\moderation\queue\ModerationQueueManager
;
22 use wcf\system\request\LinkHandler
;
23 use wcf\system\search\SearchIndexManager
;
24 use wcf\system\user\notification\
object\ConversationMessageUserNotificationObject
;
25 use wcf\system\user\notification\UserNotificationHandler
;
26 use wcf\system\user\storage\UserStorageHandler
;
28 use wcf\util\StringUtil
;
31 * Executes conversation message-related actions.
34 * @copyright 2001-2016 WoltLab GmbH
35 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
36 * @package WoltLabSuite\Core\Data\Conversation\Message
38 * @method ConversationMessageEditor[] getObjects()
39 * @method ConversationMessageEditor getSingleObject()
41 class ConversationMessageAction
extends AbstractDatabaseObjectAction
implements IAttachmentMessageQuickReplyAction
, IMessageInlineEditorAction
, IMessageQuoteAction
{
45 protected $className = ConversationMessageEditor
::class;
54 * @var HtmlInputProcessor
56 public $htmlInputProcessor;
59 * conversation message object
60 * @var ConversationMessage
66 * @return ConversationMessage
68 public function create() {
69 if (!isset($this->parameters
['data']['enableHtml'])) $this->parameters
['data']['enableHtml'] = 1;
72 if (isset($this->parameters
['attachmentHandler']) && $this->parameters
['attachmentHandler'] !== null) {
73 $this->parameters
['data']['attachments'] = count($this->parameters
['attachmentHandler']);
78 if (!isset($this->parameters
['data']['ipAddress'])) {
79 $this->parameters
['data']['ipAddress'] = WCF
::getSession()->ipAddress
;
83 // do not track ip address
84 if (isset($this->parameters
['data']['ipAddress'])) {
85 unset($this->parameters
['data']['ipAddress']);
89 if (!empty($this->parameters
['htmlInputProcessor'])) {
90 /** @noinspection PhpUndefinedMethodInspection */
91 $this->parameters
['data']['message'] = $this->parameters
['htmlInputProcessor']->getHtml();
95 /** @var ConversationMessage $message */
96 $message = parent
::create();
97 $messageEditor = new ConversationMessageEditor($message);
100 $conversation = (isset($this->parameters
['conversation']) ?
$this->parameters
['conversation'] : new Conversation($message->conversationID
));
101 $conversationEditor = new ConversationEditor($conversation);
103 if (empty($this->parameters
['isFirstPost'])) {
104 // update last message
105 $conversationEditor->addMessage($message);
107 // fire notification event
108 if (!$conversation->isDraft
) {
109 $notificationRecipients = array_diff($conversation->getParticipantIDs(true), [$message->userID
]); // don't notify message author
110 if (!empty($notificationRecipients)) {
111 UserNotificationHandler
::getInstance()->fireEvent(
112 'conversationMessage',
113 'com.woltlab.wcf.conversation.message.notification',
114 new ConversationMessageUserNotificationObject($message),
115 $notificationRecipients
120 $userConversation = Conversation
::getUserConversation($conversation->conversationID
, $message->userID
);
121 if ($userConversation !== null && $userConversation->isInvisible
) {
122 // make invisible participant visible
123 $sql = "UPDATE wcf".WCF_N
."_conversation_to_user
125 WHERE participantID = ?
126 AND conversationID = ?";
127 $statement = WCF
::getDB()->prepareStatement($sql);
128 $statement->execute([$message->userID
, $conversation->conversationID
]);
130 $conversationEditor->updateParticipantSummary();
131 $conversationEditor->updateParticipantCount();
134 // reset visibility if it was hidden but not left
135 $sql = "UPDATE wcf".WCF_N
."_conversation_to_user
136 SET hideConversation = ?
137 WHERE conversationID = ?
138 AND hideConversation = ?";
139 $statement = WCF
::getDB()->prepareStatement($sql);
140 $statement->execute([
141 Conversation
::STATE_DEFAULT
,
142 $conversation->conversationID
,
143 Conversation
::STATE_HIDDEN
148 UserStorageHandler
::getInstance()->reset($conversation->getParticipantIDs(), 'unreadConversationCount');
150 // update search index
151 SearchIndexManager
::getInstance()->set(
152 'com.woltlab.wcf.conversation.message',
155 !empty($this->parameters
['isFirstPost']) ?
$conversation->subject
: '',
161 // update attachments
162 if (isset($this->parameters
['attachmentHandler']) && $this->parameters
['attachmentHandler'] !== null) {
163 /** @noinspection PhpUndefinedMethodInspection */
164 $this->parameters
['attachmentHandler']->updateObjectID($message->messageID
);
167 // save embedded objects
168 if (!empty($this->parameters
['htmlInputProcessor'])) {
169 /** @noinspection PhpUndefinedMethodInspection */
170 $this->parameters
['htmlInputProcessor']->setObjectID($message->messageID
);
172 if (MessageEmbeddedObjectManager
::getInstance()->registerObjects($this->parameters
['htmlInputProcessor'])) {
173 $messageEditor->update(['hasEmbeddedObjects' => 1]);
178 if (isset($this->parameters
['removeQuoteIDs']) && !empty($this->parameters
['removeQuoteIDs'])) {
179 MessageQuoteManager
::getInstance()->markQuotesForRemoval($this->parameters
['removeQuoteIDs']);
181 MessageQuoteManager
::getInstance()->removeMarkedQuotes();
183 // return new message
190 public function update() {
192 if (isset($this->parameters
['attachmentHandler']) && $this->parameters
['attachmentHandler'] !== null) {
193 $this->parameters
['data']['attachments'] = count($this->parameters
['attachmentHandler']);
196 if (!empty($this->parameters
['htmlInputProcessor'])) {
197 /** @noinspection PhpUndefinedMethodInspection */
198 $this->parameters
['data']['message'] = $this->parameters
['htmlInputProcessor']->getHtml();
203 // update search index / embedded objects
204 if (isset($this->parameters
['data']) && isset($this->parameters
['data']['message'])) {
205 foreach ($this->getObjects() as $message) {
206 $conversation = $message->getConversation();
207 SearchIndexManager
::getInstance()->set(
208 'com.woltlab.wcf.conversation.message',
210 $this->parameters
['data']['message'],
211 $conversation->firstMessageID
== $message->messageID ?
$conversation->subject
: '',
217 if (!empty($this->parameters
['htmlInputProcessor'])) {
218 /** @noinspection PhpUndefinedMethodInspection */
219 $this->parameters
['htmlInputProcessor']->setObjectID($message->messageID
);
221 if ($message->hasEmbeddedObjects
!= MessageEmbeddedObjectManager
::getInstance()->registerObjects($this->parameters
['htmlInputProcessor'])) {
222 $message->update(['hasEmbeddedObjects' => $message->hasEmbeddedObjects ?
0 : 1]);
232 public function delete() {
233 $count = parent
::delete();
235 $attachmentMessageIDs = $conversationIDs = [];
236 foreach ($this->getObjects() as $message) {
237 if (!in_array($message->conversationID
, $conversationIDs)) {
238 $conversationIDs[] = $message->conversationID
;
241 if ($message->attachments
) {
242 $attachmentMessageIDs[] = $message->messageID
;
246 // rebuild conversations
247 if (!empty($conversationIDs)) {
248 $conversationAction = new ConversationAction($conversationIDs, 'rebuild');
249 $conversationAction->executeAction();
252 if (!empty($this->objectIDs
)) {
253 // delete notifications
254 UserNotificationHandler
::getInstance()->removeNotifications('com.woltlab.wcf.conversation.message.notification', $this->objectIDs
);
256 // update search index
257 SearchIndexManager
::getInstance()->delete('com.woltlab.wcf.conversation.message', $this->objectIDs
);
259 // update embedded objects
260 MessageEmbeddedObjectManager
::getInstance()->removeObjects('com.woltlab.wcf.conversation.message', $this->objectIDs
);
262 // remove moderation queues
263 ModerationQueueManager
::getInstance()->removeQueues('com.woltlab.wcf.conversation.message', $this->objectIDs
);
266 // remove attachments
267 if (!empty($attachmentMessageIDs)) {
268 AttachmentHandler
::removeAttachments('com.woltlab.wcf.conversation.message', $attachmentMessageIDs);
277 public function validateQuickReply() {
278 QuickReplyManager
::getInstance()->setDisallowedBBCodes(explode(',', WCF
::getSession()->getPermission('user.message.disallowedBBCodes')));
279 QuickReplyManager
::getInstance()->validateParameters($this, $this->parameters
, Conversation
::class);
285 public function quickReply() {
286 return QuickReplyManager
::getInstance()->createMessage(
289 ConversationAction
::class,
290 CONVERSATION_LIST_DEFAULT_SORT_ORDER
,
291 'conversationMessageList'
298 public function validateJumpToExtended() {
299 $this->readInteger('containerID');
300 $this->readString('message', true);
301 $this->readString('tmpHash', true);
303 $this->conversation
= new Conversation($this->parameters
['containerID']);
304 if (!$this->conversation
->conversationID
) {
305 throw new UserInputException('containerID');
307 else if ($this->conversation
->isClosed ||
!Conversation
::isParticipant([$this->conversation
->conversationID
])) {
308 throw new PermissionDeniedException();
311 // editing existing message
312 if (isset($this->parameters
['messageID'])) {
313 $this->message
= new ConversationMessage(intval($this->parameters
['messageID']));
314 if (!$this->message
->messageID ||
($this->message
->conversationID
!= $this->conversation
->conversationID
)) {
315 throw new UserInputException('messageID');
318 if (!$this->message
->canEdit()) {
319 throw new PermissionDeniedException();
327 public function jumpToExtended() {
329 if ($this->message
=== null) {
330 QuickReplyManager
::getInstance()->setMessage('conversation', $this->conversation
->conversationID
, $this->parameters
['message']);
331 $url = LinkHandler
::getInstance()->getLink('ConversationMessageAdd', ['id' => $this->conversation
->conversationID
]);
335 QuickReplyManager
::getInstance()->setMessage('conversationMessage', $this->message
->messageID
, $this->parameters
['message']);
336 $url = LinkHandler
::getInstance()->getLink('ConversationMessageEdit', ['id' => $this->message
->messageID
]);
339 if (!empty($this->parameters
['tmpHash'])) {
340 QuickReplyManager
::getInstance()->setTmpHash($this->parameters
['tmpHash']);
352 public function validateBeginEdit() {
353 $this->readInteger('containerID');
354 $this->readInteger('objectID');
356 $this->conversation
= new Conversation($this->parameters
['containerID']);
357 if (!$this->conversation
->conversationID
) {
358 throw new UserInputException('containerID');
361 if ($this->conversation
->isClosed ||
!Conversation
::isParticipant([$this->conversation
->conversationID
])) {
362 throw new PermissionDeniedException();
365 $this->message
= new ConversationMessage($this->parameters
['objectID']);
366 if (!$this->message
->messageID
) {
367 throw new UserInputException('objectID');
370 if (!$this->message
->canEdit()) {
371 throw new PermissionDeniedException();
374 BBCodeHandler
::getInstance()->setDisallowedBBCodes(explode(',', WCF
::getSession()->getPermission('user.message.disallowedBBCodes')));
380 public function beginEdit() {
381 WCF
::getTPL()->assign([
382 'defaultSmilies' => SmileyCache
::getInstance()->getCategorySmilies(),
383 'message' => $this->message
,
384 'permissionCanUseSmilies' => 'user.message.canUseSmilies',
385 'wysiwygSelector' => 'messageEditor'.$this->message
->messageID
388 if (MODULE_ATTACHMENT
) {
389 $tmpHash = StringUtil
::getRandomID();
390 $attachmentHandler = new AttachmentHandler('com.woltlab.wcf.conversation.message', $this->message
->messageID
, $tmpHash);
391 $attachmentList = $attachmentHandler->getAttachmentList();
393 WCF
::getTPL()->assign([
394 'attachmentHandler' => $attachmentHandler,
395 'attachmentList' => $attachmentList->getObjects(),
396 'attachmentObjectID' => $this->message
->messageID
,
397 'attachmentObjectType' => 'com.woltlab.wcf.conversation.message',
398 'attachmentParentObjectID' => 0,
399 'tmpHash' => $tmpHash
404 'actionName' => 'beginEdit',
405 'template' => WCF
::getTPL()->fetch('conversationMessageInlineEditor')
412 public function validateSave() {
413 $this->readString('message', true, 'data');
415 if (empty($this->parameters
['data']['message'])) {
416 throw new UserInputException('message', WCF
::getLanguage()->getDynamicVariable('wcf.global.form.error.empty'));
419 $this->validateBeginEdit();
421 $this->validateMessage($this->conversation
, $this->getHtmlInputProcessor($this->parameters
['data']['message'], $this->message
->messageID
));
427 public function save() {
430 if (!$this->message
->getConversation()->isDraft
) {
431 $data['lastEditTime'] = TIME_NOW
;
432 $data['editCount'] = $this->message
->editCount +
1;
434 // execute update action
435 $action = new ConversationMessageAction([$this->message
], 'update', [
437 'htmlInputProcessor' => $this->getHtmlInputProcessor()
439 $action->executeAction();
442 $this->message
= new ConversationMessage($this->message
->messageID
);
443 $this->message
->getAttachments();
445 $attachmentList = null;
446 if (MODULE_ATTACHMENT
) {
447 $attachmentList = $this->message
->getAttachments(true);
449 if ($attachmentList !== null) {
451 $attachmentList->setPermissions([
452 'canDownload' => true,
453 'canViewPreview' => true
456 $count = count($attachmentList);
459 // update count to reflect number of attachments after edit
460 if ($count != $this->message
->attachments
) {
461 $messageEditor = new ConversationMessageEditor($this->message
);
462 $messageEditor->update(['attachments' => $count]);
466 // load embedded objects
467 MessageEmbeddedObjectManager
::getInstance()->loadObjects('com.woltlab.wcf.conversation.message', [$this->message
->messageID
]);
470 'actionName' => 'save',
471 'message' => $this->message
->getFormattedMessage()
474 if (MODULE_ATTACHMENT
) {
475 WCF
::getTPL()->assign([
476 'attachmentList' => $attachmentList,
477 'objectID' => $this->message
->messageID
479 $data['attachmentList'] = WCF
::getTPL()->fetch('attachments');
488 public function validateContainer(DatabaseObject
$conversation) {
489 /** @var Conversation $conversation */
491 if (!$conversation->conversationID
) {
492 throw new UserInputException('objectID');
494 if ($conversation->isClosed
) {
495 throw new PermissionDeniedException();
497 $conversation->loadUserParticipation();
498 if (!$conversation->canRead()) {
499 throw new PermissionDeniedException();
506 public function validateMessage(DatabaseObject
$container, HtmlInputProcessor
$htmlInputProcessor) {
507 $message = $htmlInputProcessor->getTextContent();
508 if (mb_strlen($message) > WCF
::getSession()->getPermission('user.conversation.maxLength')) {
509 throw new UserInputException('message', WCF
::getLanguage()->getDynamicVariable('wcf.message.error.tooLong', ['maxTextLength' => WCF
::getSession()->getPermission('user.conversation.maxLength')]));
512 // search for disallowed bbcodes
513 $disallowedBBCodes = $htmlInputProcessor->validate();
514 if (!empty($disallowedBBCodes)) {
515 throw new UserInputException('text', WCF
::getLanguage()->getDynamicVariable('wcf.message.error.disallowedBBCodes', ['disallowedBBCodes' => $disallowedBBCodes]));
518 // search for censored words
519 if (ENABLE_CENSORSHIP
) {
520 $result = Censorship
::getInstance()->test($message);
522 throw new UserInputException('message', WCF
::getLanguage()->getDynamicVariable('wcf.message.error.censoredWordsFound', ['censoredWords' => $result]));
530 public function getMessageList(DatabaseObject
$conversation, $lastMessageTime) {
531 /** @var Conversation $conversation */
533 $messageList = new ViewableConversationMessageList();
534 $messageList->setConversation($conversation);
535 $messageList->getConditionBuilder()->add("conversation_message.conversationID = ?", [$conversation->conversationID
]);
536 $messageList->getConditionBuilder()->add("conversation_message.time > ?", [$lastMessageTime]);
537 $messageList->sqlOrderBy
= "conversation_message.time ".CONVERSATION_LIST_DEFAULT_SORT_ORDER
;
538 $messageList->readObjects();
546 public function getPageNo(DatabaseObject
$conversation) {
547 /** @var Conversation $conversation */
549 $sql = "SELECT COUNT(*) AS count
550 FROM wcf".WCF_N
."_conversation_message
551 WHERE conversationID = ?";
552 $statement = WCF
::getDB()->prepareStatement($sql);
553 $statement->execute([$conversation->conversationID
]);
554 $count = $statement->fetchArray();
556 return [intval(ceil($count['count'] / CONVERSATION_MESSAGES_PER_PAGE
)), $count['count']];
562 public function getRedirectUrl(DatabaseObject
$conversation, DatabaseObject
$message) {
563 /** @var ConversationMessage $message */
564 return LinkHandler
::getInstance()->getLink('Conversation', [
565 'object' => $conversation,
566 'messageID' => $message->messageID
567 ]).'#message'.$message->messageID
;
573 public function validateSaveFullQuote() {
574 $this->message
= $this->getSingleObject();
576 if (!Conversation
::isParticipant([$this->message
->conversationID
])) {
577 throw new PermissionDeniedException();
584 public function saveFullQuote() {
585 $quoteID = MessageQuoteManager
::getInstance()->addQuote(
586 'com.woltlab.wcf.conversation.message',
587 $this->message
->conversationID
,
588 $this->message
->messageID
,
589 $this->message
->getExcerpt(),
590 $this->message
->getMessage()
593 if ($quoteID === false) {
594 $removeQuoteID = MessageQuoteManager
::getInstance()->getQuoteID(
595 'com.woltlab.wcf.conversation.message',
596 $this->message
->messageID
,
597 $this->message
->getExcerpt(),
598 $this->message
->getMessage()
600 MessageQuoteManager
::getInstance()->removeQuote($removeQuoteID);
604 'count' => MessageQuoteManager
::getInstance()->countQuotes(),
605 'fullQuoteMessageIDs' => MessageQuoteManager
::getInstance()->getFullQuoteObjectIDs(['com.woltlab.wcf.conversation.message'])
609 $returnValues['renderedQuote'] = MessageQuoteManager
::getInstance()->getQuoteComponents($quoteID);
612 return $returnValues;
618 public function validateSaveQuote() {
619 $this->readString('message');
620 $this->readBoolean('renderQuote', true);
621 $this->message
= $this->getSingleObject();
623 if (!Conversation
::isParticipant([$this->message
->conversationID
])) {
624 throw new PermissionDeniedException();
631 public function saveQuote() {
632 $quoteID = MessageQuoteManager
::getInstance()->addQuote(
633 'com.woltlab.wcf.conversation.message',
634 $this->message
->conversationID
,
635 $this->message
->messageID
,
636 $this->parameters
['message'],
641 'count' => MessageQuoteManager
::getInstance()->countQuotes(),
642 'fullQuoteMessageIDs' => MessageQuoteManager
::getInstance()->getFullQuoteObjectIDs(['com.woltlab.wcf.conversation.message'])
645 if ($this->parameters
['renderQuote']) {
646 $returnValues['renderedQuote'] = MessageQuoteManager
::getInstance()->getQuoteComponents($quoteID);
649 return $returnValues;
655 public function validateGetRenderedQuotes() {
656 $this->readInteger('parentObjectID');
658 $this->conversation
= new Conversation($this->parameters
['parentObjectID']);
659 if (!$this->conversation
->conversationID
) {
660 throw new UserInputException('parentObjectID');
667 public function getRenderedQuotes() {
668 $quotes = MessageQuoteManager
::getInstance()->getQuotesByParentObjectID('com.woltlab.wcf.conversation.message', $this->conversation
->conversationID
);
671 'template' => implode("\n\n", $quotes)
678 public function getAttachmentHandler(DatabaseObject
$conversation) {
679 return new AttachmentHandler('com.woltlab.wcf.conversation.message', 0, $this->parameters
['tmpHash']);
685 public function getHtmlInputProcessor($message = null, $objectID = 0) {
686 if ($message === null) {
687 return $this->htmlInputProcessor
;
690 $this->htmlInputProcessor
= new HtmlInputProcessor();
691 $this->htmlInputProcessor
->process($message, 'com.woltlab.wcf.conversation.message', $objectID);
693 return $this->htmlInputProcessor
;