Reformat SQL queries using spaces
[GitHub/WoltLab/com.woltlab.wcf.conversation.git] / files / lib / data / conversation / ConversationAction.class.php
CommitLineData
9544b6b4 1<?php
fea86294 2
9544b6b4 3namespace wcf\data\conversation;
fea86294
TD
4
5use wcf\data\AbstractDatabaseObjectAction;
5e279c42 6use wcf\data\conversation\label\ConversationLabel;
9544b6b4 7use wcf\data\conversation\message\ConversationMessageAction;
5d7f0df0 8use wcf\data\conversation\message\ConversationMessageList;
a0c1a541 9use wcf\data\conversation\message\SimplifiedViewableConversationMessageList;
232cdc4b 10use wcf\data\IClipboardAction;
ce8c322f 11use wcf\data\IPopoverAction;
d8963ec2 12use wcf\data\IVisitableObjectAction;
265e4e9f 13use wcf\data\user\group\UserGroup;
e8fe47c2 14use wcf\system\clipboard\ClipboardHandler;
7f07124d 15use wcf\system\conversation\ConversationHandler;
5d7f0df0 16use wcf\system\database\util\PreparedStatementConditionBuilder;
c5a889cc 17use wcf\system\event\EventHandler;
5e279c42 18use wcf\system\exception\PermissionDeniedException;
e8fe47c2 19use wcf\system\exception\UserInputException;
65a160b7 20use wcf\system\log\modification\ConversationModificationLogHandler;
b2e0a2ad 21use wcf\system\request\LinkHandler;
87de5988 22use wcf\system\search\SearchIndexManager;
8b467fcd
MW
23use wcf\system\user\notification\object\ConversationUserNotificationObject;
24use wcf\system\user\notification\UserNotificationHandler;
9544b6b4
MW
25use wcf\system\user\storage\UserStorageHandler;
26use wcf\system\WCF;
27
28/**
29 * Executes conversation-related actions.
fea86294
TD
30 *
31 * @author Marcel Werk
32 * @copyright 2001-2019 WoltLab GmbH
33 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
34 * @package WoltLabSuite\Core\Data\Conversation
35 *
36 * @method ConversationEditor[] getObjects()
37 * @method ConversationEditor getSingleObject()
9544b6b4 38 */
fea86294
TD
39class ConversationAction extends AbstractDatabaseObjectAction implements
40 IClipboardAction,
41 IPopoverAction,
42 IVisitableObjectAction
43{
44 /**
45 * @inheritDoc
46 */
47 protected $className = ConversationEditor::class;
48
49 /**
50 * conversation object
51 * @var ConversationEditor
52 */
53 public $conversation;
54
55 /**
56 * list of conversation data modifications
57 * @var mixed[][]
58 */
59 protected $conversationData = [];
60
fea86294
TD
61 /**
62 * @inheritDoc
63 * @return Conversation
64 */
65 public function create()
66 {
67 // create conversation
68 $data = $this->parameters['data'];
69 $data['lastPosterID'] = $data['userID'];
70 $data['lastPoster'] = $data['username'];
71 $data['lastPostTime'] = $data['time'];
72 // count participants
73 if (!empty($this->parameters['participants'])) {
74 $data['participants'] = \count($this->parameters['participants']);
75 }
76 // count attachments
77 if (isset($this->parameters['attachmentHandler']) && $this->parameters['attachmentHandler'] !== null) {
78 $data['attachments'] = \count($this->parameters['attachmentHandler']);
79 }
80 $conversation = \call_user_func([$this->className, 'create'], $data);
81 $conversationEditor = new ConversationEditor($conversation);
82
83 if (!$conversation->isDraft) {
84 // save participants
85 $conversationEditor->updateParticipants(
86 (!empty($this->parameters['participants']) ? $this->parameters['participants'] : []),
87 (!empty($this->parameters['invisibleParticipants']) ? $this->parameters['invisibleParticipants'] : []),
88 'all'
89 );
90
91 // add author
92 if ($data['userID'] !== null) {
93 $conversationEditor->updateParticipants([$data['userID']], [], 'all');
94 }
95
96 // update conversation count
97 UserStorageHandler::getInstance()->reset($conversation->getParticipantIDs(), 'conversationCount');
98
99 // mark conversation as read for the author
8fbd8b01
MS
100 $sql = "UPDATE wcf" . WCF_N . "_conversation_to_user
101 SET lastVisitTime = ?
102 WHERE participantID = ?
103 AND conversationID = ?";
fea86294
TD
104 $statement = WCF::getDB()->prepareStatement($sql);
105 $statement->execute([$data['time'], $data['userID'], $conversation->conversationID]);
106 } else {
107 // update conversation count
108 UserStorageHandler::getInstance()->reset([$data['userID']], 'conversationCount');
109 }
110
111 // update participant summary
112 $conversationEditor->updateParticipantSummary();
113
114 // create message
115 $messageData = $this->parameters['messageData'];
116 $messageData['conversationID'] = $conversation->conversationID;
117 $messageData['time'] = $this->parameters['data']['time'];
118 $messageData['userID'] = $this->parameters['data']['userID'];
119 $messageData['username'] = $this->parameters['data']['username'];
120
121 $messageAction = new ConversationMessageAction([], 'create', [
122 'data' => $messageData,
123 'conversation' => $conversation,
124 'isFirstPost' => true,
125 'attachmentHandler' => $this->parameters['attachmentHandler'] ?? null,
126 'htmlInputProcessor' => $this->parameters['htmlInputProcessor'] ?? null,
127 ]);
128 $resultValues = $messageAction->executeAction();
129
130 // update first message id
131 $conversationEditor->update([
132 'firstMessageID' => $resultValues['returnValues']->messageID,
133 ]);
134
135 $conversation->setFirstMessage($resultValues['returnValues']);
136 if (!$conversation->isDraft) {
137 // fire notification event
138 $notificationRecipients = \array_merge(
139 (!empty($this->parameters['participants']) ? $this->parameters['participants'] : [])(!empty($this->parameters['invisibleParticipants']) ? $this->parameters['invisibleParticipants'] : [])
140 );
141 UserNotificationHandler::getInstance()->fireEvent(
142 'conversation',
143 'com.woltlab.wcf.conversation.notification',
144 new ConversationUserNotificationObject($conversation),
145 $notificationRecipients
146 );
147 }
148
149 return $conversation;
150 }
151
152 /**
153 * @inheritDoc
154 */
155 public function delete()
156 {
157 // deletes messages
158 $messageList = new ConversationMessageList();
159 $messageList->getConditionBuilder()->add('conversation_message.conversationID IN (?)', [$this->objectIDs]);
160 $messageList->readObjectIDs();
161 $action = new ConversationMessageAction($messageList->getObjectIDs(), 'delete');
162 $action->executeAction();
163
164 // get the list of participants in order to reset the 'unread conversation'-counter
165 $participantIDs = [];
166 if (!empty($this->objectIDs)) {
167 $conditions = new PreparedStatementConditionBuilder();
168 $conditions->add("conversationID IN (?)", [$this->objectIDs]);
169 $sql = "SELECT DISTINCT participantID
8fbd8b01
MS
170 FROM wcf" . WCF_N . "_conversation_to_user
171 " . $conditions;
fea86294
TD
172 $statement = WCF::getDB()->prepareStatement($sql);
173 $statement->execute($conditions->getParameters());
174
175 while ($participantID = $statement->fetchColumn()) {
176 $participantIDs[] = $participantID;
177 }
178 }
179
180 // delete conversations
181 parent::delete();
182
183 if (!empty($this->objectIDs)) {
184 // delete notifications
185 UserNotificationHandler::getInstance()
186 ->removeNotifications('com.woltlab.wcf.conversation.notification', $this->objectIDs);
187
188 // remove modification logs
189 ConversationModificationLogHandler::getInstance()->deleteLogs($this->objectIDs);
190
191 // reset the number of unread conversations
192 if (!empty($participantIDs)) {
193 UserStorageHandler::getInstance()->reset($participantIDs, 'unreadConversationCount');
194 }
195 }
196 }
197
198 /**
199 * @inheritDoc
200 */
201 public function update()
202 {
203 if (!isset($this->parameters['participants'])) {
204 $this->parameters['participants'] = [];
205 }
206 if (!isset($this->parameters['invisibleParticipants'])) {
207 $this->parameters['invisibleParticipants'] = [];
208 }
209
210 // count participants
211 if (!empty($this->parameters['participants'])) {
212 $this->parameters['data']['participants'] = \count($this->parameters['participants']);
213 }
214
215 parent::update();
216
217 foreach ($this->getObjects() as $conversation) {
218 // participants
219 if (!empty($this->parameters['participants']) || !empty($this->parameters['invisibleParticipants'])) {
220 // get current participants
221 $participantIDs = $conversation->getParticipantIDs();
222
223 $conversation->updateParticipants(
224 (!empty($this->parameters['participants']) ? $this->parameters['participants'] : []),
225 (!empty($this->parameters['invisibleParticipants']) ? $this->parameters['invisibleParticipants'] : []),
226 (!empty($this->parameters['visibility']) ? $this->parameters['visibility'] : 'all')
227 );
228 $conversation->updateParticipantSummary();
229
230 // check if new participants have been added
231 $newParticipantIDs = \array_diff(\array_merge(
232 $this->parameters['participants'],
233 $this->parameters['invisibleParticipants']
234 ), $participantIDs);
235 if (!empty($newParticipantIDs)) {
236 // update conversation count
237 UserStorageHandler::getInstance()->reset($newParticipantIDs, 'unreadConversationCount');
238 UserStorageHandler::getInstance()->reset($newParticipantIDs, 'conversationCount');
239
240 // fire notification event
241 UserNotificationHandler::getInstance()->fireEvent(
242 'conversation',
243 'com.woltlab.wcf.conversation.notification',
244 new ConversationUserNotificationObject($conversation->getDecoratedObject()),
245 $newParticipantIDs
246 );
247 }
248 }
249
250 // draft status
251 if (isset($this->parameters['data']['isDraft'])) {
252 if ($conversation->isDraft && !$this->parameters['data']['isDraft']) {
253 // add author
254 $conversation->updateParticipants([$conversation->userID], [], 'all');
255
256 // update conversation count
257 UserStorageHandler::getInstance()
258 ->reset($conversation->getParticipantIDs(), 'unreadConversationCount');
259 UserStorageHandler::getInstance()
260 ->reset($conversation->getParticipantIDs(), 'conversationCount');
261 }
262 }
263 }
264 }
265
266 /**
267 * @inheritDoc
268 */
269 public function markAsRead()
270 {
271 if (empty($this->parameters['visitTime'])) {
272 $this->parameters['visitTime'] = TIME_NOW;
273 }
274
275 // in case this is a call via PHP and the userID parameter is missing, set it to the userID of the current user
276 if (!isset($this->parameters['userID'])) {
277 $this->parameters['userID'] = WCF::getUser()->userID;
278 }
279
280 if (empty($this->objects)) {
281 $this->readObjects();
282 }
283
284 $conversationIDs = [];
8fbd8b01
MS
285 $sql = "UPDATE wcf" . WCF_N . "_conversation_to_user
286 SET lastVisitTime = ?
287 WHERE participantID = ?
288 AND conversationID = ?";
fea86294
TD
289 $statement = WCF::getDB()->prepareStatement($sql);
290 WCF::getDB()->beginTransaction();
291 foreach ($this->getObjects() as $conversation) {
292 $statement->execute([
293 $this->parameters['visitTime'],
294 $this->parameters['userID'],
295 $conversation->conversationID,
296 ]);
297 $conversationIDs[] = $conversation->conversationID;
298 }
299 WCF::getDB()->commitTransaction();
300
301 // reset storage
302 UserStorageHandler::getInstance()->reset([$this->parameters['userID']], 'unreadConversationCount');
303
304 // mark notifications as confirmed
305 if (!empty($conversationIDs)) {
306 // conversation start notification
307 $conditionBuilder = new PreparedStatementConditionBuilder();
308 $conditionBuilder->add('notification.eventID = ?', [
309 UserNotificationHandler::getInstance()
310 ->getEvent('com.woltlab.wcf.conversation.notification', 'conversation')
311 ->eventID,
312 ]);
313 $conditionBuilder->add('notification.objectID = conversation.conversationID');
314 $conditionBuilder->add('notification.userID = ?', [$this->parameters['userID']]);
315 $conditionBuilder->add('conversation.conversationID IN (?)', [$conversationIDs]);
316 $conditionBuilder->add('conversation.time <= ?', [$this->parameters['visitTime']]);
317
8fbd8b01
MS
318 $sql = "SELECT conversation.conversationID
319 FROM wcf" . WCF_N . "_conversation conversation,
320 wcf" . WCF_N . "_user_notification notification
321 " . $conditionBuilder;
fea86294
TD
322 $statement = WCF::getDB()->prepareStatement($sql);
323 $statement->execute($conditionBuilder->getParameters());
324 $notificationObjectIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
325
326 if (!empty($notificationObjectIDs)) {
327 UserNotificationHandler::getInstance()->markAsConfirmed(
328 'conversation',
329 'com.woltlab.wcf.conversation.notification',
330 [$this->parameters['userID']],
331 $notificationObjectIDs
332 );
333 }
334
335 // conversation reply notification
336 $conditionBuilder = new PreparedStatementConditionBuilder();
337 $conditionBuilder->add('notification.eventID = ?', [
338 UserNotificationHandler::getInstance()
339 ->getEvent('com.woltlab.wcf.conversation.message.notification', 'conversationMessage')
340 ->eventID,
341 ]);
342 $conditionBuilder->add('notification.objectID = conversation_message.messageID');
343 $conditionBuilder->add('notification.userID = ?', [$this->parameters['userID']]);
344 $conditionBuilder->add('conversation_message.conversationID IN (?)', [$conversationIDs]);
345 $conditionBuilder->add('conversation_message.time <= ?', [$this->parameters['visitTime']]);
346
8fbd8b01
MS
347 $sql = "SELECT conversation_message.messageID
348 FROM wcf" . WCF_N . "_conversation_message conversation_message,
349 wcf" . WCF_N . "_user_notification notification
350 " . $conditionBuilder;
fea86294
TD
351 $statement = WCF::getDB()->prepareStatement($sql);
352 $statement->execute($conditionBuilder->getParameters());
353 $notificationObjectIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
354
355 if (!empty($notificationObjectIDs)) {
356 UserNotificationHandler::getInstance()->markAsConfirmed(
357 'conversationMessage',
358 'com.woltlab.wcf.conversation.message.notification',
359 [$this->parameters['userID']],
360 $notificationObjectIDs
361 );
362 }
363 }
364
365 if (!empty($conversationIDs)) {
366 $this->unmarkItems($conversationIDs);
367 }
368
369 $returnValues = [
370 'totalCount' => ConversationHandler::getInstance()
371 ->getUnreadConversationCount($this->parameters['userID'], true),
372 ];
373
374 if (\count($conversationIDs) == 1) {
375 $returnValues['markAsRead'] = \reset($conversationIDs);
376 }
377
378 return $returnValues;
379 }
380
381 /**
382 * @inheritDoc
383 */
384 public function validateMarkAsRead()
385 {
386 // visitTime might not be in the future
387 if (isset($this->parameters['visitTime'])) {
388 $this->parameters['visitTime'] = \intval($this->parameters['visitTime']);
389 if ($this->parameters['visitTime'] > TIME_NOW) {
390 $this->parameters['visitTime'] = TIME_NOW;
391 }
392 }
393
394 // userID should always be equal to the userID of the current user when called via AJAX
395 $this->parameters['userID'] = WCF::getUser()->userID;
396
397 if (empty($this->objects)) {
398 $this->readObjects();
399 }
400
401 // check participation
402 $conversationIDs = [];
403 foreach ($this->getObjects() as $conversation) {
404 $conversationIDs[] = $conversation->conversationID;
405 }
406
407 if (empty($conversationIDs)) {
408 throw new UserInputException('objectIDs');
409 }
410
411 if (!Conversation::isParticipant($conversationIDs)) {
412 throw new PermissionDeniedException();
413 }
414 }
415
416 /**
417 * Marks all conversations as read.
418 */
419 public function markAllAsRead()
420 {
8fbd8b01
MS
421 $sql = "UPDATE wcf" . WCF_N . "_conversation_to_user
422 SET lastVisitTime = ?
423 WHERE participantID = ?";
fea86294
TD
424 $statement = WCF::getDB()->prepareStatement($sql);
425 $statement->execute([
426 TIME_NOW,
427 WCF::getUser()->userID,
428 ]);
429
430 // reset storage
431 UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadConversationCount');
432
433 // confirm obsolete notifications
434 UserNotificationHandler::getInstance()->markAsConfirmed(
435 'conversation',
436 'com.woltlab.wcf.conversation.notification',
437 [WCF::getUser()->userID]
438 );
439 UserNotificationHandler::getInstance()->markAsConfirmed(
440 'conversationMessage',
441 'com.woltlab.wcf.conversation.message.notification',
442 [WCF::getUser()->userID]
443 );
444
445 return [
446 'markAllAsRead' => true,
447 ];
448 }
449
450 /**
451 * Validates the markAllAsRead action.
452 */
453 public function validateMarkAllAsRead()
454 {
455 // does nothing
456 }
457
458 /**
459 * Validates user access for label management.
460 *
461 * @throws PermissionDeniedException
462 */
463 public function validateGetLabelManagement()
464 {
465 if (!WCF::getSession()->getPermission('user.conversation.canUseConversation')) {
466 throw new PermissionDeniedException();
467 }
468 }
469
470 /**
471 * Returns the conversation label management.
472 *
473 * @return array
474 */
475 public function getLabelManagement()
476 {
477 WCF::getTPL()->assign([
478 'cssClassNames' => ConversationLabel::getLabelCssClassNames(),
479 'labelList' => ConversationLabel::getLabelsByUser(),
480 ]);
481
482 return [
483 'actionName' => 'getLabelManagement',
484 'template' => WCF::getTPL()->fetch('conversationLabelManagement'),
485 'maxLabels' => WCF::getSession()->getPermission('user.conversation.maxLabels'),
486 'labelCount' => \count(ConversationLabel::getLabelsByUser()),
487 ];
488 }
489
490 /**
491 * @inheritDoc
492 */
493 public function validateGetPopover()
494 {
495 $this->conversation = $this->getSingleObject();
496 if (!Conversation::isParticipant([$this->conversation->conversationID])) {
497 throw new PermissionDeniedException();
498 }
499 }
500
501 /**
502 * @inheritDoc
503 */
504 public function getPopover()
505 {
506 $messageList = new SimplifiedViewableConversationMessageList();
507 $messageList->getConditionBuilder()
508 ->add("conversation_message.messageID = ?", [$this->conversation->firstMessageID]);
509 $messageList->readObjects();
510
511 return [
512 'template' => WCF::getTPL()->fetch('conversationMessagePreview', 'wcf', [
513 'message' => $messageList->getSingleObject(),
514 ]),
515 ];
516 }
517
518 /**
519 * Validates the get message preview action.
520 *
521 * @throws PermissionDeniedException
522 * @deprecated 5.3 Use `validateGetPopover()` instead.
523 */
524 public function validateGetMessagePreview()
525 {
526 $this->validateGetPopover();
527 }
528
529 /**
530 * Returns a preview of a message in a specific conversation.
531 *
532 * @return string[]
533 * @deprecated 5.3 Use `getPopover()` instead.
534 */
535 public function getMessagePreview()
536 {
537 return $this->getPopover();
538 }
539
540 /**
541 * Validates parameters to close conversations.
542 *
543 * @throws PermissionDeniedException
544 * @throws UserInputException
545 */
546 public function validateClose()
547 {
548 // read objects
549 if (empty($this->objects)) {
550 $this->readObjects();
551
552 if (empty($this->objects)) {
553 throw new UserInputException('objectIDs');
554 }
555 }
556
557 // validate ownership
558 foreach ($this->getObjects() as $conversation) {
559 if ($conversation->isClosed || ($conversation->userID != WCF::getUser()->userID)) {
560 throw new PermissionDeniedException();
561 }
562 }
563 }
564
565 /**
566 * Closes conversations.
567 *
568 * @return mixed[][]
569 */
570 public function close()
571 {
572 foreach ($this->getObjects() as $conversation) {
573 $conversation->update(['isClosed' => 1]);
574 $this->addConversationData($conversation->getDecoratedObject(), 'isClosed', 1);
575
576 ConversationModificationLogHandler::getInstance()->close($conversation->getDecoratedObject());
577 }
578
579 $this->unmarkItems();
580
581 return $this->getConversationData();
582 }
583
584 /**
585 * Validates parameters to open conversations.
586 *
587 * @throws PermissionDeniedException
588 * @throws UserInputException
589 */
590 public function validateOpen()
591 {
592 // read objects
593 if (empty($this->objects)) {
594 $this->readObjects();
595
596 if (empty($this->objects)) {
597 throw new UserInputException('objectIDs');
598 }
599 }
600
601 // validate ownership
602 foreach ($this->getObjects() as $conversation) {
603 if (!$conversation->isClosed || ($conversation->userID != WCF::getUser()->userID)) {
604 throw new PermissionDeniedException();
605 }
606 }
607 }
608
609 /**
610 * Opens conversations.
611 *
612 * @return mixed[][]
613 */
614 public function open()
615 {
616 foreach ($this->getObjects() as $conversation) {
617 $conversation->update(['isClosed' => 0]);
618 $this->addConversationData($conversation->getDecoratedObject(), 'isClosed', 0);
619
620 ConversationModificationLogHandler::getInstance()->open($conversation->getDecoratedObject());
621 }
622
623 $this->unmarkItems();
624
625 return $this->getConversationData();
626 }
627
628 /**
629 * Validates conversations for leave form.
630 *
631 * @throws PermissionDeniedException
632 * @throws UserInputException
633 */
634 public function validateGetLeaveForm()
635 {
636 if (empty($this->objectIDs)) {
637 throw new UserInputException('objectIDs');
638 }
639
640 // validate participation
641 if (!Conversation::isParticipant($this->objectIDs)) {
642 throw new PermissionDeniedException();
643 }
644 }
645
646 /**
647 * Returns dialog form to leave conversations.
648 *
649 * @return array
650 */
651 public function getLeaveForm()
652 {
653 // get hidden state from first conversation (all others have the same state)
8fbd8b01
MS
654 $sql = "SELECT hideConversation
655 FROM wcf" . WCF_N . "_conversation_to_user
656 WHERE conversationID = ?
657 AND participantID = ?";
fea86294
TD
658 $statement = WCF::getDB()->prepareStatement($sql);
659 $statement->execute([
660 \current($this->objectIDs),
661 WCF::getUser()->userID,
662 ]);
663 $row = $statement->fetchArray();
664
665 WCF::getTPL()->assign('hideConversation', ($row !== false ? $row['hideConversation'] : 0));
666
667 return [
668 'actionName' => 'getLeaveForm',
669 'template' => WCF::getTPL()->fetch('conversationLeave'),
670 ];
671 }
672
673 /**
674 * Validates parameters to hide conversations.
675 *
676 * @throws PermissionDeniedException
677 * @throws UserInputException
678 */
679 public function validateHideConversation()
680 {
681 $this->parameters['hideConversation'] = isset($this->parameters['hideConversation']) ? \intval($this->parameters['hideConversation']) : null;
682 if (
683 $this->parameters['hideConversation'] === null
684 || !\in_array(
685 $this->parameters['hideConversation'],
686 [Conversation::STATE_DEFAULT, Conversation::STATE_HIDDEN, Conversation::STATE_LEFT]
687 )
688 ) {
689 throw new UserInputException('hideConversation');
690 }
691
692 if (empty($this->objectIDs)) {
693 throw new UserInputException('objectIDs');
694 }
695
696 // validate participation
697 if (!Conversation::isParticipant($this->objectIDs)) {
698 throw new PermissionDeniedException();
699 }
700 }
701
702 /**
703 * Hides or restores conversations.
704 *
705 * @return string[]
706 */
707 public function hideConversation()
708 {
8fbd8b01
MS
709 $sql = "UPDATE wcf" . WCF_N . "_conversation_to_user
710 SET hideConversation = ?
711 WHERE conversationID = ?
712 AND participantID = ?";
fea86294
TD
713 $statement = WCF::getDB()->prepareStatement($sql);
714
715 WCF::getDB()->beginTransaction();
716 foreach ($this->objectIDs as $conversationID) {
717 $statement->execute([
718 $this->parameters['hideConversation'],
719 $conversationID,
720 WCF::getUser()->userID,
721 ]);
722 }
723 WCF::getDB()->commitTransaction();
724
725 // reset user's conversation counters if user leaves conversation
726 // permanently
727 if ($this->parameters['hideConversation'] == Conversation::STATE_LEFT) {
728 UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'conversationCount');
729 UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadConversationCount');
730 }
731
732 // add modification log entry
733 if ($this->parameters['hideConversation'] == Conversation::STATE_LEFT) {
734 if (empty($this->objects)) {
735 $this->readObjects();
736 }
737
738 foreach ($this->getObjects() as $conversation) {
739 ConversationModificationLogHandler::getInstance()->leave($conversation->getDecoratedObject());
740 }
741 }
742
743 // unmark items
744 $this->unmarkItems();
745
746 if ($this->parameters['hideConversation'] == Conversation::STATE_LEFT) {
747 // update participants count and participant summary
748 ConversationEditor::updateParticipantCounts($this->objectIDs);
749 ConversationEditor::updateParticipantSummaries($this->objectIDs);
750
751 // delete conversation if all users have left it
752 $conditionBuilder = new PreparedStatementConditionBuilder();
753 $conditionBuilder->add('conversation.conversationID IN (?)', [$this->objectIDs]);
754 $conditionBuilder->add('conversation_to_user.conversationID IS NULL');
8fbd8b01
MS
755 $sql = "SELECT DISTINCT conversation.conversationID
756 FROM wcf" . WCF_N . "_conversation conversation
757 LEFT JOIN wcf" . WCF_N . "_conversation_to_user conversation_to_user
758 ON (
759 conversation_to_user.conversationID = conversation.conversationID
760 AND conversation_to_user.hideConversation <> " . Conversation::STATE_LEFT . "
761 AND conversation_to_user.participantID IS NOT NULL
762 )
763 " . $conditionBuilder;
fea86294
TD
764 $statement = WCF::getDB()->prepareStatement($sql);
765 $statement->execute($conditionBuilder->getParameters());
766 $conversationIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
767
768 if (!empty($conversationIDs)) {
769 $action = new self($conversationIDs, 'delete');
770 $action->executeAction();
771 }
772 }
773
774 return [
775 'actionName' => 'hideConversation',
776 'redirectURL' => LinkHandler::getInstance()->getLink('ConversationList'),
777 ];
778 }
779
780 /**
781 * Validates parameters to return the mixed conversation list.
782 */
783 public function validateGetMixedConversationList()
784 {
785 // does nothing
786 }
787
788 /**
789 * Returns a mixed conversation list with up to 10 unread conversations.
790 *
791 * @return mixed[][]
792 */
793 public function getMixedConversationList()
794 {
8fbd8b01
MS
795 $sqlSelect = ' , (
796 SELECT participantID
797 FROM wcf' . WCF_N . '_conversation_to_user
798 WHERE conversationID = conversation.conversationID
799 AND participantID <> conversation.userID
800 AND isInvisible = 0
801 ORDER BY username, participantID
802 LIMIT 1
803 ) AS otherParticipantID
804 , (
805 SELECT username
806 FROM wcf' . WCF_N . '_conversation_to_user
807 WHERE conversationID = conversation.conversationID
808 AND participantID <> conversation.userID
809 AND isInvisible = 0
810 ORDER BY username, participantID
811 LIMIT 1
812 ) AS otherParticipant';
fea86294
TD
813
814 $unreadConversationList = new UserConversationList(WCF::getUser()->userID);
815 $unreadConversationList->sqlSelects .= $sqlSelect;
816 $unreadConversationList->getConditionBuilder()->add('conversation_to_user.lastVisitTime < lastPostTime');
817 $unreadConversationList->sqlLimit = 10;
818 $unreadConversationList->sqlOrderBy = 'lastPostTime DESC';
819 $unreadConversationList->readObjects();
820
821 $conversations = [];
822 $count = 0;
823 foreach ($unreadConversationList as $conversation) {
824 $conversations[] = $conversation;
825 $count++;
826 }
827
828 if ($count < 10) {
829 $conversationList = new UserConversationList(WCF::getUser()->userID);
830 $conversationList->sqlSelects .= $sqlSelect;
831 $conversationList->getConditionBuilder()->add('conversation_to_user.lastVisitTime >= lastPostTime');
832 $conversationList->sqlLimit = (10 - $count);
833 $conversationList->sqlOrderBy = 'lastPostTime DESC';
834 $conversationList->readObjects();
835
836 foreach ($conversationList as $conversation) {
837 $conversations[] = $conversation;
838 }
839 }
840
841 WCF::getTPL()->assign([
842 'conversations' => $conversations,
843 ]);
844
845 $totalCount = ConversationHandler::getInstance()->getUnreadConversationCount();
846 if ($count < 10 && $count < $totalCount) {
847 UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'unreadConversationCount');
848 }
849
850 return [
851 'template' => WCF::getTPL()->fetch('conversationListUserPanel'),
852 'totalCount' => $totalCount,
853 ];
854 }
855
856 /**
857 * Validates the 'unmarkAll' action.
858 */
859 public function validateUnmarkAll()
860 {
861 // does nothing
862 }
863
864 /**
865 * Unmarks all conversations.
866 */
867 public function unmarkAll()
868 {
869 ClipboardHandler::getInstance()->removeItems(
870 ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.conversation.conversation')
871 );
872 }
873
874 /**
875 * Validates parameters to display the 'add participants' form.
876 *
877 * @throws PermissionDeniedException
878 */
879 public function validateGetAddParticipantsForm()
880 {
881 $this->conversation = $this->getSingleObject();
882 if (
883 !Conversation::isParticipant([$this->conversation->conversationID])
884 || !$this->conversation->canAddParticipants()
885 ) {
886 throw new PermissionDeniedException();
887 }
888 }
889
890 /**
891 * Shows the 'add participants' form.
892 *
893 * @return array
894 */
895 public function getAddParticipantsForm()
896 {
897 $restrictUserGroupIDs = [];
898 foreach (UserGroup::getAllGroups() as $group) {
899 if ($group->canBeAddedAsConversationParticipant) {
900 $restrictUserGroupIDs[] = $group->groupID;
901 }
902 }
903
904 return [
905 'excludedSearchValues' => $this->conversation->getParticipantNames(false, true),
906 'maxItems' => WCF::getSession()->getPermission('user.conversation.maxParticipants') - $this->conversation->participants,
907 'canAddGroupParticipants' => WCF::getSession()->getPermission('user.conversation.canAddGroupParticipants'),
908 'template' => WCF::getTPL()->fetch(
909 'conversationAddParticipants',
910 'wcf',
911 ['conversation' => $this->conversation]
912 ),
913 'restrictUserGroupIDs' => $restrictUserGroupIDs,
914 ];
915 }
916
917 /**
918 * Validates parameters to add new participants.
919 */
920 public function validateAddParticipants()
921 {
922 $this->validateGetAddParticipantsForm();
923
924 // validate participants
925 $this->readStringArray('participants', true);
926 $this->readIntegerArray('participantsGroupIDs', true);
927
928 if (!$this->conversation->getDecoratedObject()->isDraft) {
929 $this->readString('visibility');
930 if (!\in_array($this->parameters['visibility'], ['all', 'new'])) {
931 throw new UserInputException('visibility');
932 }
933
934 if ($this->parameters['visibility'] === 'all' && !$this->conversation->canAddParticipantsUnrestricted()) {
935 throw new UserInputException('visibility');
936 }
937 }
938 }
939
940 /**
941 * Adds new participants.
942 *
943 * @return array
944 */
945 public function addParticipants()
946 {
947 try {
948 $existingParticipants = $this->conversation->getParticipantIDs(true);
949 $participantIDs = Conversation::validateParticipants(
950 $this->parameters['participants'],
951 'participants',
952 $existingParticipants
953 );
954 if (
955 !empty($this->parameters['participantsGroupIDs'])
956 && WCF::getSession()->getPermission('user.conversation.canAddGroupParticipants')
957 ) {
958 $validGroupParticipants = Conversation::validateGroupParticipants(
959 $this->parameters['participantsGroupIDs'],
960 'participants',
961 $existingParticipants
962 );
963 $validGroupParticipants = \array_diff($validGroupParticipants, $participantIDs);
964 if (empty($validGroupParticipants)) {
965 throw new UserInputException('participants', 'emptyGroup');
966 }
967 $participantIDs = \array_merge($participantIDs, $validGroupParticipants);
968 }
969
970 $parameters = [
971 'participantIDs' => $participantIDs,
972 ];
973 EventHandler::getInstance()->fireAction($this, 'addParticipants_validateParticipants', $parameters);
974 $participantIDs = $parameters['participantIDs'];
975 } catch (UserInputException $e) {
976 $errorMessage = '';
977 $errors = \is_array($e->getType()) ? $e->getType() : [['type' => $e->getType()]];
978 foreach ($errors as $type) {
979 if (!empty($errorMessage)) {
980 $errorMessage .= ' ';
981 }
982 $errorMessage .= WCF::getLanguage()->getDynamicVariable(
983 'wcf.conversation.participants.error.' . $type['type'],
984 ['errorData' => $type]
985 );
986 }
987
988 return [
989 'actionName' => 'addParticipants',
990 'errorMessage' => $errorMessage,
991 ];
992 }
993
994 // validate limit
995 $newCount = $this->conversation->participants + \count($participantIDs);
996 if ($newCount > WCF::getSession()->getPermission('user.conversation.maxParticipants')) {
997 return [
998 'actionName' => 'addParticipants',
999 'errorMessage' => WCF::getLanguage()->getDynamicVariable('wcf.conversation.participants.error.tooManyParticipants'),
1000 ];
1001 }
1002
1003 $count = 0;
1004 $successMessage = '';
1005 if (!empty($participantIDs)) {
1006 // check for already added participants
1007 if ($this->conversation->isDraft) {
1008 $draftData = \unserialize($this->conversation->draftData);
1009 $draftData['participants'] = \array_merge($draftData['participants'], $participantIDs);
1010 $data = ['data' => ['draftData' => \serialize($draftData)]];
1011 } else {
1012 $data = [
1013 'participants' => $participantIDs,
1014 'visibility' => (isset($this->parameters['visibility'])) ? $this->parameters['visibility'] : 'all',
1015 ];
1016 }
1017
1018 $conversationAction = new self([$this->conversation], 'update', $data);
1019 $conversationAction->executeAction();
1020
1021 $count = \count($participantIDs);
1022 $successMessage = WCF::getLanguage()->getDynamicVariable(
1023 'wcf.conversation.edit.addParticipants.success',
1024 ['count' => $count]
1025 );
1026
1027 ConversationModificationLogHandler::getInstance()
1028 ->addParticipants($this->conversation->getDecoratedObject(), $participantIDs);
1029
1030 if (!$this->conversation->isDraft) {
1031 // update participant summary
1032 $this->conversation->updateParticipantSummary();
1033 }
1034 }
1035
1036 return [
1037 'count' => $count,
1038 'successMessage' => $successMessage,
1039 ];
1040 }
1041
1042 /**
1043 * Validates parameters to remove a participant from a conversation.
1044 *
1045 * @throws PermissionDeniedException
1046 * @throws UserInputException
1047 */
1048 public function validateRemoveParticipant()
1049 {
1050 $this->readInteger('userID');
1051
1052 // validate conversation
1053 $this->conversation = $this->getSingleObject();
1054 if (!$this->conversation->conversationID) {
1055 throw new UserInputException('objectIDs');
1056 }
1057
1058 // check ownership
1059 if ($this->conversation->userID != WCF::getUser()->userID) {
1060 throw new PermissionDeniedException();
1061 }
1062
1063 // validate participants
1064 if (
1065 $this->parameters['userID'] == WCF::getUser()->userID
1066 || !Conversation::isParticipant([$this->conversation->conversationID])
1067 || !Conversation::isParticipant([$this->conversation->conversationID], $this->parameters['userID'])
1068 ) {
1069 throw new PermissionDeniedException();
1070 }
1071 }
1072
1073 /**
1074 * Removes a participant from a conversation.
1075 */
1076 public function removeParticipant()
1077 {
1078 $this->conversation->removeParticipant($this->parameters['userID']);
1079 $this->conversation->updateParticipantSummary();
1080
1081 ConversationModificationLogHandler::getInstance()
1082 ->removeParticipant($this->conversation->getDecoratedObject(), $this->parameters['userID']);
1083
1084 // reset storage
1085 UserStorageHandler::getInstance()->reset([$this->parameters['userID']], 'unreadConversationCount');
1086
1087 return [
1088 'userID' => $this->parameters['userID'],
1089 ];
1090 }
1091
1092 /**
1093 * Rebuilds the conversation data of the relevant conversations.
1094 */
1095 public function rebuild()
1096 {
1097 if (empty($this->objects)) {
1098 $this->readObjects();
1099 }
1100
1101 // collect number of messages for each conversation
1102 $conditionBuilder = new PreparedStatementConditionBuilder();
1103 $conditionBuilder->add('conversation_message.conversationID IN (?)', [$this->objectIDs]);
8fbd8b01
MS
1104 $sql = "SELECT conversationID, COUNT(messageID) AS messages, SUM(attachments) AS attachments
1105 FROM wcf" . WCF_N . "_conversation_message conversation_message
1106 " . $conditionBuilder . "
1107 GROUP BY conversationID";
fea86294
TD
1108 $statement = WCF::getDB()->prepareStatement($sql);
1109 $statement->execute($conditionBuilder->getParameters());
1110
1111 $objectIDs = [];
1112 while ($row = $statement->fetchArray()) {
1113 if (!$row['messages']) {
1114 continue;
1115 }
1116 $objectIDs[] = $row['conversationID'];
1117
1118 $conversationEditor = new ConversationEditor(new Conversation(null, [
1119 'conversationID' => $row['conversationID'],
1120 ]));
1121 $conversationEditor->update([
1122 'attachments' => $row['attachments'],
1123 'replies' => $row['messages'] - 1,
1124 ]);
1125 $conversationEditor->updateFirstMessage();
1126 $conversationEditor->updateLastMessage();
1127 }
1128
1129 // delete conversations without messages
1130 $deleteConversationIDs = \array_diff($this->objectIDs, $objectIDs);
1131 if (!empty($deleteConversationIDs)) {
1132 $conversationAction = new self($deleteConversationIDs, 'delete');
1133 $conversationAction->executeAction();
1134 }
1135 }
1136
1137 /**
1138 * Validates the parameters to edit a conversation's subject.
1139 *
1140 * @throws PermissionDeniedException
1141 */
1142 public function validateEditSubject()
1143 {
1144 $this->readString('subject');
1145
1146 $this->conversation = $this->getSingleObject();
1147 if ($this->conversation->userID != WCF::getUser()->userID) {
1148 throw new PermissionDeniedException();
1149 }
1150 }
1151
1152 /**
1153 * Edits a conversation's subject.
1154 *
1155 * @return string[]
1156 */
1157 public function editSubject()
1158 {
1159 $subject = \mb_substr($this->parameters['subject'], 0, 255);
1160
1161 $this->conversation->update([
1162 'subject' => $subject,
1163 ]);
1164
1165 $message = $this->conversation->getFirstMessage();
1166
1167 SearchIndexManager::getInstance()->set(
1168 'com.woltlab.wcf.conversation.message',
1169 $message->messageID,
1170 $message->message,
1171 $subject,
1172 $message->time,
1173 $message->userID,
1174 $message->username
1175 );
1176
1177 return [
1178 'subject' => $subject,
1179 ];
1180 }
1181
1182 /**
1183 * Adds conversation modification data.
1184 *
1185 * @param Conversation $conversation
1186 * @param string $key
1187 * @param mixed $value
1188 */
1189 protected function addConversationData(Conversation $conversation, $key, $value)
1190 {
1191 if (!isset($this->conversationData[$conversation->conversationID])) {
1192 $this->conversationData[$conversation->conversationID] = [];
1193 }
1194
1195 $this->conversationData[$conversation->conversationID][$key] = $value;
1196 }
1197
1198 /**
1199 * Returns conversation data.
1200 *
1201 * @return mixed[][]
1202 */
1203 protected function getConversationData()
1204 {
1205 return [
1206 'conversationData' => $this->conversationData,
1207 ];
1208 }
1209
1210 /**
1211 * Unmarks conversations.
1212 *
1213 * @param integer[] $conversationIDs
1214 */
1215 protected function unmarkItems(array $conversationIDs = [])
1216 {
1217 if (empty($conversationIDs)) {
1218 $conversationIDs = $this->objectIDs;
1219 }
1220
1221 ClipboardHandler::getInstance()->unmark(
1222 $conversationIDs,
1223 ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.conversation.conversation')
1224 );
1225 }
9544b6b4 1226}