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