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