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
;
14 use wcf\util\ArrayUtil
;
17 * Represents a conversation.
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
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
44 class Conversation
extends DatabaseObject
implements IRouteController
, ITitledLinkObject
{
48 protected static $databaseTableName = 'conversation';
53 protected static $databaseTableIndexName = 'conversationID';
56 * default participation state
59 const STATE_DEFAULT
= 0;
62 * conversation is hidden but returns visible upon new message
65 const STATE_HIDDEN
= 1;
68 * conversation was left permanently
71 const STATE_LEFT
/*4DEAD*/ = 2;
74 * first message object
75 * @var ConversationMessage
77 protected $firstMessage = null;
82 public function getTitle() {
83 return $this->subject
;
89 public function getLink() {
90 return LinkHandler
::getInstance()->getLink('Conversation', ['object' => $this]);
94 * Returns true if this conversation is new for the active user.
98 public function isNew() {
99 if (!$this->isDraft
&& $this->lastPostTime
> $this->lastVisitTime
) {
107 * Returns true if the active user doesn't have read the given message.
109 * @param ConversationMessage $message
112 public function isNewMessage(ConversationMessage
$message) {
113 if (!$this->isDraft
&& $message->time
> $this->lastVisitTime
) {
121 * Loads participation data for given user id (default: current user) on runtime.
122 * You should use Conversation::getUserConversation() instead if possible.
124 * @param integer $userID
126 public function loadUserParticipation($userID = null) {
127 if ($userID === null) {
128 $userID = WCF
::getUser()->userID
;
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);
144 * Returns a specific user conversation.
146 * @param integer $conversationID
147 * @param integer $userID
148 * @return Conversation
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);
167 * Returns a list of user conversations.
169 * @param integer[] $conversationIDs
170 * @param integer $userID
171 * @return Conversation[]
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)
181 $statement = WCF
::getDB()->prepareStatement($sql);
182 $statement->execute($conditionBuilder->getParameters());
184 while (($row = $statement->fetchArray())) {
185 $conversations[$row['conversationID']] = new Conversation(null, $row);
188 return $conversations;
192 * Returns true if the active user has the permission to read this conversation.
196 public function canRead() {
197 if (!WCF
::getUser()->userID
) return false;
199 if ($this->isDraft
&& $this->userID
== WCF
::getUser()->userID
) return true;
201 if ($this->participantID
== WCF
::getUser()->userID
&& $this->hideConversation
!= self
::STATE_LEFT
) return true;
207 * Returns true if current user can add new participants to this conversation.
211 public function canAddParticipants() {
212 if ($this->isDraft
) {
217 if (WCF
::getUser()->userID
!= $this->userID
&& !$this->participantCanInvite
) {
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')) {
231 * Returns the first message in this conversation.
233 * @return ConversationMessage
235 public function getFirstMessage() {
236 if ($this->firstMessage
=== null) {
237 $this->firstMessage
= new ConversationMessage($this->firstMessageID
);
240 return $this->firstMessage
;
244 * Sets the first message.
246 * @param ConversationMessage $message
248 public function setFirstMessage(ConversationMessage
$message) {
249 $this->firstMessage
= $message;
253 * Returns a list of the ids of all participants.
255 * @param boolean $excludeLeftParticipants
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
]);
263 $sql = "SELECT participantID
264 FROM wcf".WCF_N
."_conversation_to_user
266 $statement = WCF
::getDB()->prepareStatement($sql);
267 $statement->execute($conditions->getParameters());
269 return $statement->fetchAll(\PDO
::FETCH_COLUMN
);
273 * Returns a list of the usernames of all participants.
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
]);
286 return $statement->fetchAll(\PDO
::FETCH_COLUMN
);
290 * Returns false if the active user is the last participant of this conversation.
294 public function hasOtherParticipants() {
295 if ($this->userID
== WCF
::getUser()->userID
) {
297 if ($this->participants
== 0) return false;
301 if ($this->participants
> 1) return true;
302 if ($this->isInvisible
&& $this->participants
> 0) return true;
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;
323 * Returns true if given user id (default: current user) is participant
324 * of all given conversation ids.
326 * @param integer[] $conversationIDs
327 * @param integer $userID
330 public static function isParticipant(array $conversationIDs, $userID = null) {
331 if ($userID === null) $userID = WCF
::getUser()->userID
;
333 // check if user is the initial author
334 $conditions = new PreparedStatementConditionBuilder();
335 $conditions->add("conversationID IN (?)", [$conversationIDs]);
336 $conditions->add("userID = ?", [$userID]);
338 $sql = "SELECT conversationID
339 FROM wcf".WCF_N
."_conversation
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]);
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
]);
355 $sql = "SELECT conversationID
356 FROM wcf".WCF_N
."_conversation_to_user
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]);
366 if (!empty($conversationIDs)) {
374 * Validates the participants.
376 * @param mixed $participants
377 * @param string $field
378 * @param integer[] $existingParticipants
379 * @return array $result
380 * @throws UserInputException
382 public static function validateParticipants($participants, $field = 'participants', array $existingParticipants = []) {
386 // loop through participants and check their settings
387 $participantList = UserProfile
::getUserProfilesByUsername((is_array($participants) ?
$participants : ArrayUtil
::trim(explode(',', $participants))));
389 // load user storage at once to avoid multiple queries
391 foreach ($participantList as $user) {
393 $userIDs[] = $user->userID
;
396 UserStorageHandler
::getInstance()->loadStorage($userIDs);
398 foreach ($participantList as $participant => $user) {
400 if ($user === null) {
401 throw new UserInputException($field, 'notFound');
405 if ($user->userID
== WCF
::getUser()->userID
) {
406 throw new UserInputException($field, 'isAuthor');
408 else if (in_array($user->userID
, $existingParticipants)) {
409 throw new UserInputException($field, 'duplicate');
413 self
::validateParticipant($user, $field);
416 $existingParticipants[] = $result[] = $user->userID
;
418 catch (UserInputException
$e) {
419 $error[] = ['type' => $e->getType(), 'username' => $participant];
423 if (!empty($error)) {
424 throw new UserInputException($field, $error);
431 * Validates the given participant.
433 * @param UserProfile $user
434 * @param string $field
435 * @throws UserInputException
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');
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');
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');
456 // active user is ignored by participant
457 if ($user->isIgnoredUser(WCF
::getUser()->userID
)) {
458 throw new UserInputException($field, 'ignoresYou');
461 // check participant's mailbox quota
462 if (ConversationHandler
::getInstance()->getConversationCount($user->userID
) >= $user->getPermission('user.conversation.maxConversations')) {
463 throw new UserInputException($field, 'mailboxIsFull');