2f7d797d447e58c6ba08c83ef79ad8fe8fe783f9
[GitHub/WoltLab/com.woltlab.wcf.conversation.git] / files / lib / data / conversation / Conversation.class.php
1 <?php
2 namespace wcf\data\conversation;
3 use wcf\data\conversation\message\ConversationMessage;
4 use wcf\data\user\UserProfile;
5 use wcf\data\DatabaseObject;
6 use wcf\data\ITitledLinkObject;
7 use wcf\system\conversation\ConversationHandler;
8 use wcf\system\database\util\PreparedStatementConditionBuilder;
9 use wcf\system\exception\UserInputException;
10 use wcf\system\request\IRouteController;
11 use wcf\system\request\LinkHandler;
12 use wcf\system\user\storage\UserStorageHandler;
13 use wcf\system\WCF;
14 use wcf\util\ArrayUtil;
15
16 /**
17 * Represents a conversation.
18 *
19 * @author Marcel Werk
20 * @copyright 2001-2016 WoltLab GmbH
21 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
22 * @package com.woltlab.wcf.conversation
23 * @subpackage data.conversation
24 * @category Community Framework
25 *
26 * @property-read integer $conversationID
27 * @property-read string $subject
28 * @property-read integer $time
29 * @property-read integer $firstMessageID
30 * @property-read integer|null $userID
31 * @property-read string $username
32 * @property-read integer $lastPostTime
33 * @property-read integer|null $lastPosterID
34 * @property-read string $lastPoster
35 * @property-read integer $replies
36 * @property-read integer $attachments
37 * @property-read integer $participants
38 * @property-read string $participantSummary
39 * @property-read integer $participantCanInvite
40 * @property-read integer $isClosed
41 * @property-read integer $isDraft
42 * @property-read string $draftData
43 */
44 class Conversation extends DatabaseObject implements IRouteController, ITitledLinkObject {
45 /**
46 * @inheritDoc
47 */
48 protected static $databaseTableName = 'conversation';
49
50 /**
51 * @inheritDoc
52 */
53 protected static $databaseTableIndexName = 'conversationID';
54
55 /**
56 * default participation state
57 * @var integer
58 */
59 const STATE_DEFAULT = 0;
60
61 /**
62 * conversation is hidden but returns visible upon new message
63 * @var integer
64 */
65 const STATE_HIDDEN = 1;
66
67 /**
68 * conversation was left permanently
69 * @var integer
70 */
71 const STATE_LEFT/*4DEAD*/ = 2;
72
73 /**
74 * first message object
75 * @var ConversationMessage
76 */
77 protected $firstMessage = null;
78
79 /**
80 * @inheritDoc
81 */
82 public function getTitle() {
83 return $this->subject;
84 }
85
86 /**
87 * @inheritDoc
88 */
89 public function getLink() {
90 return LinkHandler::getInstance()->getLink('Conversation', ['object' => $this]);
91 }
92
93 /**
94 * Returns true if this conversation is new for the active user.
95 *
96 * @return boolean
97 */
98 public function isNew() {
99 if (!$this->isDraft && $this->lastPostTime > $this->lastVisitTime) {
100 return true;
101 }
102
103 return false;
104 }
105
106 /**
107 * Returns true if the active user doesn't have read the given message.
108 *
109 * @param ConversationMessage $message
110 * @return boolean
111 */
112 public function isNewMessage(ConversationMessage $message) {
113 if (!$this->isDraft && $message->time > $this->lastVisitTime) {
114 return true;
115 }
116
117 return false;
118 }
119
120 /**
121 * Loads participation data for given user id (default: current user) on runtime.
122 * You should use Conversation::getUserConversation() instead if possible.
123 *
124 * @param integer $userID
125 */
126 public function loadUserParticipation($userID = null) {
127 if ($userID === null) {
128 $userID = WCF::getUser()->userID;
129 }
130
131 $sql = "SELECT *
132 FROM wcf".WCF_N."_conversation_to_user
133 WHERE participantID = ?
134 AND conversationID = ?";
135 $statement = WCF::getDB()->prepareStatement($sql);
136 $statement->execute([$userID, $this->conversationID]);
137 $row = $statement->fetchArray();
138 if ($row !== false) {
139 $this->data = array_merge($this->data, $row);
140 }
141 }
142
143 /**
144 * Returns a specific user conversation.
145 *
146 * @param integer $conversationID
147 * @param integer $userID
148 * @return Conversation
149 */
150 public static function getUserConversation($conversationID, $userID) {
151 $sql = "SELECT conversation_to_user.*, conversation.*
152 FROM wcf".WCF_N."_conversation conversation
153 LEFT JOIN wcf".WCF_N."_conversation_to_user conversation_to_user
154 ON (conversation_to_user.participantID = ? AND conversation_to_user.conversationID = conversation.conversationID)
155 WHERE conversation.conversationID = ?";
156 $statement = WCF::getDB()->prepareStatement($sql);
157 $statement->execute([$userID, $conversationID]);
158 $row = $statement->fetchArray();
159 if ($row !== false) {
160 return new Conversation(null, $row);
161 }
162
163 return null;
164 }
165
166 /**
167 * Returns a list of user conversations.
168 *
169 * @param integer[] $conversationIDs
170 * @param integer $userID
171 * @return Conversation[]
172 */
173 public static function getUserConversations(array $conversationIDs, $userID) {
174 $conditionBuilder = new PreparedStatementConditionBuilder();
175 $conditionBuilder->add('conversation.conversationID IN (?)', [$conversationIDs]);
176 $sql = "SELECT conversation_to_user.*, conversation.*
177 FROM wcf".WCF_N."_conversation conversation
178 LEFT JOIN wcf".WCF_N."_conversation_to_user conversation_to_user
179 ON (conversation_to_user.participantID = ".$userID." AND conversation_to_user.conversationID = conversation.conversationID)
180 ".$conditionBuilder;
181 $statement = WCF::getDB()->prepareStatement($sql);
182 $statement->execute($conditionBuilder->getParameters());
183 $conversations = [];
184 while (($row = $statement->fetchArray())) {
185 $conversations[$row['conversationID']] = new Conversation(null, $row);
186 }
187
188 return $conversations;
189 }
190
191 /**
192 * Returns true if the active user has the permission to read this conversation.
193 *
194 * @return boolean
195 */
196 public function canRead() {
197 if (!WCF::getUser()->userID) return false;
198
199 if ($this->isDraft && $this->userID == WCF::getUser()->userID) return true;
200
201 if ($this->participantID == WCF::getUser()->userID && $this->hideConversation != self::STATE_LEFT) return true;
202
203 return false;
204 }
205
206 /**
207 * Returns true if current user can add new participants to this conversation.
208 *
209 * @return boolean
210 */
211 public function canAddParticipants() {
212 if ($this->isDraft) {
213 return false;
214 }
215
216 // check permissions
217 if (WCF::getUser()->userID != $this->userID && !$this->participantCanInvite) {
218 return false;
219 }
220
221 // check for maximum number of participants
222 // note: 'participants' does not track invisible participants, this will be checked on the fly!
223 if ($this->participants >= WCF::getSession()->getPermission('user.conversation.maxParticipants')) {
224 return false;
225 }
226
227 return true;
228 }
229
230 /**
231 * Returns the first message in this conversation.
232 *
233 * @return ConversationMessage
234 */
235 public function getFirstMessage() {
236 if ($this->firstMessage === null) {
237 $this->firstMessage = new ConversationMessage($this->firstMessageID);
238 }
239
240 return $this->firstMessage;
241 }
242
243 /**
244 * Sets the first message.
245 *
246 * @param ConversationMessage $message
247 */
248 public function setFirstMessage(ConversationMessage $message) {
249 $this->firstMessage = $message;
250 }
251
252 /**
253 * Returns a list of the ids of all participants.
254 *
255 * @param boolean $excludeLeftParticipants
256 * @return integer[]
257 */
258 public function getParticipantIDs($excludeLeftParticipants = false) {
259 $conditions = new PreparedStatementConditionBuilder();
260 $conditions->add("conversationID = ?", [$this->conversationID]);
261 if ($excludeLeftParticipants) $conditions->add("hideConversation <> ?", [self::STATE_LEFT]);
262
263 $sql = "SELECT participantID
264 FROM wcf".WCF_N."_conversation_to_user
265 ".$conditions;
266 $statement = WCF::getDB()->prepareStatement($sql);
267 $statement->execute($conditions->getParameters());
268
269 return $statement->fetchAll(\PDO::FETCH_COLUMN);
270 }
271
272 /**
273 * Returns a list of the usernames of all participants.
274 *
275 * @return string[]
276 */
277 public function getParticipantNames() {
278 $sql = "SELECT user_table.username
279 FROM wcf".WCF_N."_conversation_to_user conversation_to_user
280 LEFT JOIN wcf".WCF_N."_user user_table
281 ON (user_table.userID = conversation_to_user.participantID)
282 WHERE conversationID = ?";
283 $statement = WCF::getDB()->prepareStatement($sql);
284 $statement->execute([$this->conversationID]);
285
286 return $statement->fetchAll(\PDO::FETCH_COLUMN);
287 }
288
289 /**
290 * Returns false if the active user is the last participant of this conversation.
291 *
292 * @return boolean
293 */
294 public function hasOtherParticipants() {
295 if ($this->userID == WCF::getUser()->userID) {
296 // author
297 if ($this->participants == 0) return false;
298 return true;
299 }
300 else {
301 if ($this->participants > 1) return true;
302 if ($this->isInvisible && $this->participants > 0) return true;
303
304 if ($this->userID) {
305 // check if author has left the conversation
306 $sql = "SELECT hideConversation
307 FROM wcf".WCF_N."_conversation_to_user
308 WHERE conversationID = ?
309 AND participantID = ?";
310 $statement = WCF::getDB()->prepareStatement($sql);
311 $statement->execute([$this->conversationID, $this->userID]);
312 $row = $statement->fetchArray();
313 if ($row !== false) {
314 if ($row['hideConversation'] != self::STATE_LEFT) return true;
315 }
316 }
317
318 return false;
319 }
320 }
321
322 /**
323 * Returns true if given user id (default: current user) is participant
324 * of all given conversation ids.
325 *
326 * @param integer[] $conversationIDs
327 * @param integer $userID
328 * @return boolean
329 */
330 public static function isParticipant(array $conversationIDs, $userID = null) {
331 if ($userID === null) $userID = WCF::getUser()->userID;
332
333 // check if user is the initial author
334 $conditions = new PreparedStatementConditionBuilder();
335 $conditions->add("conversationID IN (?)", [$conversationIDs]);
336 $conditions->add("userID = ?", [$userID]);
337
338 $sql = "SELECT conversationID
339 FROM wcf".WCF_N."_conversation
340 ".$conditions;
341 $statement = WCF::getDB()->prepareStatement($sql);
342 $statement->execute($conditions->getParameters());
343 while (($row = $statement->fetchArray())) {
344 $index = array_search($row['conversationID'], $conversationIDs);
345 unset($conversationIDs[$index]);
346 }
347
348 // check for participation
349 if (!empty($conversationIDs)) {
350 $conditions = new PreparedStatementConditionBuilder();
351 $conditions->add("conversationID IN (?)", [$conversationIDs]);
352 $conditions->add("participantID = ?", [$userID]);
353 $conditions->add("hideConversation <> ?", [self::STATE_LEFT]);
354
355 $sql = "SELECT conversationID
356 FROM wcf".WCF_N."_conversation_to_user
357 ".$conditions;
358 $statement = WCF::getDB()->prepareStatement($sql);
359 $statement->execute($conditions->getParameters());
360 while (($row = $statement->fetchArray())) {
361 $index = array_search($row['conversationID'], $conversationIDs);
362 unset($conversationIDs[$index]);
363 }
364 }
365
366 if (!empty($conversationIDs)) {
367 return false;
368 }
369
370 return true;
371 }
372
373 /**
374 * Validates the participants.
375 *
376 * @param mixed $participants
377 * @param string $field
378 * @param integer[] $existingParticipants
379 * @return array $result
380 * @throws UserInputException
381 */
382 public static function validateParticipants($participants, $field = 'participants', array $existingParticipants = []) {
383 $result = [];
384 $error = [];
385
386 // loop through participants and check their settings
387 $participantList = UserProfile::getUserProfilesByUsername((is_array($participants) ? $participants : ArrayUtil::trim(explode(',', $participants))));
388
389 // load user storage at once to avoid multiple queries
390 $userIDs = [];
391 foreach ($participantList as $user) {
392 if ($user) {
393 $userIDs[] = $user->userID;
394 }
395 }
396 UserStorageHandler::getInstance()->loadStorage($userIDs);
397
398 foreach ($participantList as $participant => $user) {
399 try {
400 if ($user === null) {
401 throw new UserInputException($field, 'notFound');
402 }
403
404 // user is author
405 if ($user->userID == WCF::getUser()->userID) {
406 throw new UserInputException($field, 'isAuthor');
407 }
408 else if (in_array($user->userID, $existingParticipants)) {
409 throw new UserInputException($field, 'duplicate');
410 }
411
412 // validate user
413 self::validateParticipant($user, $field);
414
415 // no error
416 $existingParticipants[] = $result[] = $user->userID;
417 }
418 catch (UserInputException $e) {
419 $error[] = ['type' => $e->getType(), 'username' => $participant];
420 }
421 }
422
423 if (!empty($error)) {
424 throw new UserInputException($field, $error);
425 }
426
427 return $result;
428 }
429
430 /**
431 * Validates the given participant.
432 *
433 * @param UserProfile $user
434 * @param string $field
435 * @throws UserInputException
436 */
437 public static function validateParticipant(UserProfile $user, $field = 'participants') {
438 // check participant's settings and permissions
439 if (!$user->getPermission('user.conversation.canUseConversation')) {
440 throw new UserInputException($field, 'canNotUseConversation');
441 }
442
443 if (!WCF::getSession()->getPermission('user.profile.cannotBeIgnored')) {
444 // check if user wants to receive any conversations
445 if ($user->canSendConversation == 2) {
446 throw new UserInputException($field, 'doesNotAcceptConversation');
447 }
448
449 // check if user only wants to receive conversations by
450 // users they are following and if the active user is followed
451 // by the relevant user
452 if ($user->canSendConversation == 1 && !$user->isFollowing(WCF::getUser()->userID)) {
453 throw new UserInputException($field, 'doesNotAcceptConversation');
454 }
455
456 // active user is ignored by participant
457 if ($user->isIgnoredUser(WCF::getUser()->userID)) {
458 throw new UserInputException($field, 'ignoresYou');
459 }
460
461 // check participant's mailbox quota
462 if (ConversationHandler::getInstance()->getConversationCount($user->userID) >= $user->getPermission('user.conversation.maxConversations')) {
463 throw new UserInputException($field, 'mailboxIsFull');
464 }
465 }
466 }
467 }