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