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