Commit | Line | Data |
---|---|---|
9544b6b4 | 1 | <?php |
fea86294 | 2 | |
9544b6b4 | 3 | namespace wcf\data\conversation; |
fea86294 | 4 | |
f34884b9 | 5 | use wcf\data\conversation\message\ConversationMessage; |
fea86294 | 6 | use wcf\data\DatabaseObject; |
ce8c322f | 7 | use wcf\data\IPopoverObject; |
83545ad0 | 8 | use wcf\data\user\group\UserGroup; |
3da2f11a | 9 | use wcf\data\user\ignore\UserIgnore; |
65b37bf6 | 10 | use wcf\data\user\UserProfile; |
83545ad0 | 11 | use wcf\system\cache\runtime\UserProfileRuntimeCache; |
65b37bf6 | 12 | use wcf\system\conversation\ConversationHandler; |
98b69027 | 13 | use wcf\system\database\util\PreparedStatementConditionBuilder; |
65b37bf6 | 14 | use wcf\system\exception\UserInputException; |
9544b6b4 MW |
15 | use wcf\system\request\IRouteController; |
16 | use wcf\system\request\LinkHandler; | |
65b37bf6 | 17 | use wcf\system\user\storage\UserStorageHandler; |
9544b6b4 | 18 | use wcf\system\WCF; |
65b37bf6 | 19 | use wcf\util\ArrayUtil; |
9544b6b4 MW |
20 | |
21 | /** | |
22 | * Represents a conversation. | |
fea86294 TD |
23 | * |
24 | * @author Marcel Werk | |
25 | * @copyright 2001-2019 WoltLab GmbH | |
26 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> | |
fea86294 | 27 | * |
c85e9df8 | 28 | * @property-read int $conversationID unique id of the conversation |
fea86294 | 29 | * @property-read string $subject subject of the conversation |
c85e9df8 MS |
30 | * @property-read int $time timestamp at which the conversation has been started |
31 | * @property-read int $firstMessageID id of the first conversation message | |
32 | * @property-read int|null $userID id of the user who started the conversation or `null` if the user does not exist anymore | |
fea86294 | 33 | * @property-read string $username name of the user who started the conversation |
c85e9df8 MS |
34 | * @property-read int $lastPostTime timestamp at which the conversation's last message has been written |
35 | * @property-read int|null $lastPosterID id of the user who wrote the conversation's last message or `null` if the user does not exist anymore | |
fea86294 | 36 | * @property-read string $lastPoster name of the user who wrote the conversation's last message |
c85e9df8 MS |
37 | * @property-read int $replies number of replies on the conversation |
38 | * @property-read int $attachments total number of attachments in all messages of the conversation | |
39 | * @property-read int $participants number of participants of the conversations | |
fea86294 | 40 | * @property-read string $participantSummary serialized data of five of the conversation participants (sorted by username) |
c85e9df8 MS |
41 | * @property-read int $participantCanInvite is `1` if participants can invite other users to join the conversation, otherwise `0` |
42 | * @property-read int $isClosed is `1` if the conversation is closed for new messages, otherwise `0` | |
43 | * @property-read int $isDraft is `1` if the conversation is a draft only, thus not sent to any participant, otherwise `0` | |
fea86294 | 44 | * @property-read string $draftData serialized ids of the participants and invisible participants if conversation is a draft, otherwise `0` |
c85e9df8 MS |
45 | * @property-read int|null $participantID id of the user whose conversations are fetched via `UserConversationList`, otherwise `null` |
46 | * @property-read int|null $hideConversation is `1` if the user has hidden conversation, otherwise `0`; is `null` if the conversation has not been fetched via `UserConversationList` | |
47 | * @property-read int|null $isInvisible is `1` if the user is invisible in conversation, otherwise `0`; is `null` if the conversation has not been fetched via `UserConversationList` | |
48 | * @property-read int|null $lastVisitTime timestamp at which the user last visited the conversation after a new messsage had been written or `0` if they have not visited it at all; is `null` if the conversation has not been fetched via `UserConversationList` | |
49 | * @property-read int|null $joinedAt timestamp at which the user joined the conversation; is `null` if the conversation has not been fetched via `UserConversationList` | |
50 | * @property-read int|null $leftAt timestamp at which the user left the conversation or `0` if they did not leave the conversation; is `null` if the conversation has not been fetched via `UserConversationList` | |
51 | * @property-read int|null $lastMessageID id of the last message written before the user left the conversation or `0` if they did not leave the conversation; is `null` if the conversation has not been fetched via `UserConversationList` | |
9544b6b4 | 52 | */ |
fea86294 TD |
53 | class Conversation extends DatabaseObject implements IPopoverObject, IRouteController |
54 | { | |
55 | /** | |
56 | * default participation state | |
c85e9df8 | 57 | * @var int |
fea86294 | 58 | */ |
8470b74e | 59 | public const STATE_DEFAULT = 0; |
fea86294 TD |
60 | |
61 | /** | |
62 | * conversation is hidden but returns visible upon new message | |
c85e9df8 | 63 | * @var int |
fea86294 | 64 | */ |
8470b74e | 65 | public const STATE_HIDDEN = 1; |
fea86294 TD |
66 | |
67 | /** | |
68 | * conversation was left permanently | |
c85e9df8 | 69 | * @var int |
fea86294 | 70 | */ |
8470b74e | 71 | public const STATE_LEFT/*4DEAD*/ = 2; |
fea86294 TD |
72 | |
73 | /** | |
74 | * true if the current user can add users without limitations | |
bbdcc6c5 | 75 | * @var bool |
fea86294 TD |
76 | */ |
77 | protected $canAddUnrestricted; | |
78 | ||
79 | /** | |
80 | * first message object | |
81 | * @var ConversationMessage | |
82 | */ | |
83 | protected $firstMessage; | |
84 | ||
85 | /** | |
86 | * true if the current user is an active participant of this conversation | |
bbdcc6c5 | 87 | * @var bool |
fea86294 TD |
88 | */ |
89 | protected $isActiveParticipant; | |
90 | ||
91 | /** | |
92 | * @inheritDoc | |
93 | */ | |
beb426fe | 94 | public function getTitle(): string |
fea86294 TD |
95 | { |
96 | return $this->subject; | |
97 | } | |
98 | ||
99 | /** | |
100 | * @inheritDoc | |
101 | */ | |
beb426fe | 102 | public function getLink(): string |
fea86294 TD |
103 | { |
104 | return LinkHandler::getInstance()->getLink('Conversation', [ | |
105 | 'object' => $this, | |
106 | 'forceFrontend' => true, | |
107 | ]); | |
108 | } | |
109 | ||
110 | /** | |
111 | * Returns true if this conversation is new for the active user. | |
fea86294 | 112 | */ |
beb426fe | 113 | public function isNew(): bool |
fea86294 TD |
114 | { |
115 | if (!$this->isDraft && $this->lastPostTime > $this->lastVisitTime) { | |
116 | return true; | |
117 | } | |
118 | ||
119 | return false; | |
120 | } | |
121 | ||
122 | /** | |
123 | * Returns true if the active user doesn't have read the given message. | |
fea86294 | 124 | */ |
beb426fe | 125 | public function isNewMessage(ConversationMessage $message): bool |
fea86294 TD |
126 | { |
127 | if (!$this->isDraft && $message->time > $this->lastVisitTime) { | |
128 | return true; | |
129 | } | |
130 | ||
131 | return false; | |
132 | } | |
133 | ||
134 | /** | |
135 | * Returns true if the conversation is not closed or the user was not removed. | |
fea86294 | 136 | */ |
beb426fe | 137 | public function canReply(): bool |
fea86294 | 138 | { |
852b30f0 TD |
139 | if (!$this->canRead()) { |
140 | return false; | |
141 | } | |
142 | ||
fea86294 TD |
143 | return !$this->isClosed && !$this->leftAt && WCF::getSession()->getPermission('user.conversation.canReplyToConversation'); |
144 | } | |
145 | ||
146 | /** | |
147 | * Overrides the last message data, used when `leftAt < lastPostTime`. | |
148 | * | |
c85e9df8 | 149 | * @param int $userID |
fea86294 | 150 | * @param string $username |
c85e9df8 | 151 | * @param int $time |
fea86294 TD |
152 | */ |
153 | public function setLastMessage($userID, $username, $time) | |
154 | { | |
155 | $this->data['lastPostTime'] = $time; | |
156 | $this->data['lastPosterID'] = $userID; | |
157 | $this->data['lastPoster'] = $username; | |
158 | } | |
159 | ||
160 | /** | |
161 | * Loads participation data for given user id (default: current user) on runtime. | |
162 | * You should use Conversation::getUserConversation() instead if possible. | |
163 | * | |
c85e9df8 | 164 | * @param int $userID |
fea86294 TD |
165 | */ |
166 | public function loadUserParticipation($userID = null) | |
167 | { | |
168 | if ($userID === null) { | |
169 | $userID = WCF::getUser()->userID; | |
170 | } | |
171 | ||
8fbd8b01 MS |
172 | $sql = "SELECT * |
173 | FROM wcf" . WCF_N . "_conversation_to_user | |
174 | WHERE participantID = ? | |
175 | AND conversationID = ?"; | |
fea86294 TD |
176 | $statement = WCF::getDB()->prepareStatement($sql); |
177 | $statement->execute([$userID, $this->conversationID]); | |
178 | $row = $statement->fetchArray(); | |
179 | if ($row !== false) { | |
180 | $this->data = \array_merge($this->data, $row); | |
181 | } | |
182 | } | |
183 | ||
184 | /** | |
185 | * Returns a specific user conversation. | |
186 | * | |
c85e9df8 MS |
187 | * @param int $conversationID |
188 | * @param int $userID | |
6b63c755 | 189 | * @return null|Conversation |
fea86294 TD |
190 | */ |
191 | public static function getUserConversation($conversationID, $userID) | |
192 | { | |
8fbd8b01 MS |
193 | $sql = "SELECT conversation_to_user.*, conversation.* |
194 | FROM wcf" . WCF_N . "_conversation conversation | |
195 | LEFT JOIN wcf" . WCF_N . "_conversation_to_user conversation_to_user | |
6124508f MS |
196 | ON conversation_to_user.participantID = ? |
197 | AND conversation_to_user.conversationID = conversation.conversationID | |
8fbd8b01 | 198 | WHERE conversation.conversationID = ?"; |
fea86294 TD |
199 | $statement = WCF::getDB()->prepareStatement($sql); |
200 | $statement->execute([$userID, $conversationID]); | |
201 | $row = $statement->fetchArray(); | |
202 | if ($row !== false) { | |
203 | return new self(null, $row); | |
204 | } | |
6b63c755 MS |
205 | |
206 | return null; | |
fea86294 TD |
207 | } |
208 | ||
209 | /** | |
210 | * Returns a list of user conversations. | |
211 | * | |
c85e9df8 MS |
212 | * @param int[] $conversationIDs |
213 | * @param int $userID | |
fea86294 TD |
214 | * @return Conversation[] |
215 | */ | |
216 | public static function getUserConversations(array $conversationIDs, $userID) | |
217 | { | |
218 | $conditionBuilder = new PreparedStatementConditionBuilder(); | |
219 | $conditionBuilder->add('conversation.conversationID IN (?)', [$conversationIDs]); | |
8fbd8b01 MS |
220 | $sql = "SELECT conversation_to_user.*, conversation.* |
221 | FROM wcf" . WCF_N . "_conversation conversation | |
222 | LEFT JOIN wcf" . WCF_N . "_conversation_to_user conversation_to_user | |
6124508f MS |
223 | ON conversation_to_user.participantID = " . $userID . " |
224 | AND conversation_to_user.conversationID = conversation.conversationID | |
8fbd8b01 | 225 | " . $conditionBuilder; |
fea86294 TD |
226 | $statement = WCF::getDB()->prepareStatement($sql); |
227 | $statement->execute($conditionBuilder->getParameters()); | |
228 | $conversations = []; | |
229 | while ($row = $statement->fetchArray()) { | |
230 | $conversations[$row['conversationID']] = new self(null, $row); | |
231 | } | |
232 | ||
233 | return $conversations; | |
234 | } | |
235 | ||
236 | /** | |
237 | * Returns true if the active user has the permission to read this conversation. | |
fea86294 | 238 | */ |
beb426fe | 239 | public function canRead(): bool |
fea86294 TD |
240 | { |
241 | if (!WCF::getUser()->userID) { | |
242 | return false; | |
243 | } | |
244 | ||
245 | if ($this->isDraft && $this->userID == WCF::getUser()->userID) { | |
246 | return true; | |
247 | } | |
248 | ||
249 | if ($this->participantID == WCF::getUser()->userID && $this->hideConversation != self::STATE_LEFT) { | |
250 | return true; | |
251 | } | |
252 | ||
253 | return false; | |
254 | } | |
255 | ||
256 | /** | |
257 | * Returns true if the current user can add new participants to this conversation. | |
fea86294 | 258 | */ |
beb426fe | 259 | public function canAddParticipants(): bool |
fea86294 TD |
260 | { |
261 | if ($this->isDraft) { | |
262 | return false; | |
263 | } | |
264 | ||
265 | // check permissions | |
266 | if (WCF::getUser()->userID != $this->userID) { | |
267 | if ( | |
268 | !$this->participantCanInvite | |
269 | && !WCF::getSession()->getPermission('mod.conversation.canAlwaysInviteUsers') | |
270 | ) { | |
271 | return false; | |
272 | } | |
273 | } | |
274 | ||
275 | // check for maximum number of participants | |
276 | // note: 'participants' does not track invisible participants, this will be checked on the fly! | |
277 | if ($this->participants >= WCF::getSession()->getPermission('user.conversation.maxParticipants')) { | |
278 | return false; | |
279 | } | |
280 | ||
281 | if (!$this->isActiveParticipant()) { | |
282 | return false; | |
283 | } | |
284 | ||
285 | return true; | |
286 | } | |
287 | ||
288 | /** | |
289 | * Returns true if the current user can add participants without limitations. | |
fea86294 | 290 | */ |
beb426fe | 291 | public function canAddParticipantsUnrestricted(): bool |
fea86294 TD |
292 | { |
293 | if ($this->canAddUnrestricted === null) { | |
294 | $this->canAddUnrestricted = false; | |
295 | if ($this->isActiveParticipant()) { | |
296 | $sql = "SELECT joinedAt | |
8fbd8b01 MS |
297 | FROM wcf" . WCF_N . "_conversation_to_user |
298 | WHERE conversationID = ? | |
299 | AND participantID = ?"; | |
fea86294 TD |
300 | $statement = WCF::getDB()->prepareStatement($sql); |
301 | $statement->execute([ | |
302 | $this->conversationID, | |
303 | WCF::getUser()->userID, | |
304 | ]); | |
305 | $joinedAt = $statement->fetchSingleColumn(); | |
306 | ||
307 | if ($joinedAt !== false && $joinedAt == 0) { | |
308 | $this->canAddUnrestricted = true; | |
309 | } | |
310 | } | |
311 | } | |
312 | ||
313 | return $this->canAddUnrestricted; | |
314 | } | |
315 | ||
316 | /** | |
317 | * Returns the first message in this conversation. | |
318 | * | |
319 | * @return ConversationMessage | |
320 | */ | |
321 | public function getFirstMessage() | |
322 | { | |
323 | if ($this->firstMessage === null) { | |
324 | $this->firstMessage = new ConversationMessage($this->firstMessageID); | |
325 | } | |
326 | ||
327 | return $this->firstMessage; | |
328 | } | |
329 | ||
330 | /** | |
331 | * Sets the first message. | |
332 | * | |
333 | * @param ConversationMessage $message | |
334 | */ | |
335 | public function setFirstMessage(ConversationMessage $message) | |
336 | { | |
337 | $this->firstMessage = $message; | |
338 | } | |
339 | ||
340 | /** | |
341 | * Returns a list of the ids of all participants. | |
342 | * | |
bbdcc6c5 | 343 | * @param bool $excludeLeftParticipants |
c85e9df8 | 344 | * @return int[] |
fea86294 TD |
345 | */ |
346 | public function getParticipantIDs($excludeLeftParticipants = false) | |
347 | { | |
348 | $conditions = new PreparedStatementConditionBuilder(); | |
349 | $conditions->add("conversationID = ?", [$this->conversationID]); | |
350 | $conditions->add("participantID IS NOT NULL"); | |
351 | if ($excludeLeftParticipants) { | |
352 | $conditions->add("(hideConversation <> ? AND leftAt = ?)", [self::STATE_LEFT, 0]); | |
353 | } | |
354 | ||
8fbd8b01 MS |
355 | $sql = "SELECT participantID |
356 | FROM wcf" . WCF_N . "_conversation_to_user | |
357 | " . $conditions; | |
fea86294 TD |
358 | $statement = WCF::getDB()->prepareStatement($sql); |
359 | $statement->execute($conditions->getParameters()); | |
360 | ||
361 | return $statement->fetchAll(\PDO::FETCH_COLUMN); | |
362 | } | |
363 | ||
364 | /** | |
365 | * Returns a list of the usernames of all participants. | |
366 | * | |
bbdcc6c5 MS |
367 | * @param bool $excludeSelf |
368 | * @param bool $leftByOwnChoice | |
fea86294 TD |
369 | * @return string[] |
370 | */ | |
371 | public function getParticipantNames($excludeSelf = false, $leftByOwnChoice = false) | |
372 | { | |
373 | $conditions = new PreparedStatementConditionBuilder(); | |
374 | $conditions->add("conversationID = ?", [$this->conversationID]); | |
375 | if ($excludeSelf) { | |
376 | $conditions->add("conversation_to_user.participantID <> ?", [WCF::getUser()->userID]); | |
377 | } | |
378 | if ($leftByOwnChoice) { | |
379 | $conditions->add("conversation_to_user.leftByOwnChoice = ?", [1]); | |
380 | } | |
381 | ||
8fbd8b01 MS |
382 | $sql = "SELECT user_table.username |
383 | FROM wcf" . WCF_N . "_conversation_to_user conversation_to_user | |
384 | LEFT JOIN wcf" . WCF_N . "_user user_table | |
6124508f | 385 | ON user_table.userID = conversation_to_user.participantID |
8fbd8b01 | 386 | " . $conditions; |
fea86294 TD |
387 | $statement = WCF::getDB()->prepareStatement($sql); |
388 | $statement->execute($conditions->getParameters()); | |
389 | ||
390 | return $statement->fetchAll(\PDO::FETCH_COLUMN); | |
391 | } | |
392 | ||
393 | /** | |
394 | * Returns false if the active user is the last participant of this conversation. | |
fea86294 | 395 | */ |
beb426fe | 396 | public function hasOtherParticipants(): bool |
fea86294 TD |
397 | { |
398 | if ($this->userID == WCF::getUser()->userID) { | |
399 | // author | |
400 | if ($this->participants == 0) { | |
401 | return false; | |
402 | } | |
403 | ||
404 | return true; | |
405 | } else { | |
406 | if ($this->participants > 1) { | |
407 | return true; | |
408 | } | |
409 | if ($this->isInvisible && $this->participants > 0) { | |
410 | return true; | |
411 | } | |
412 | ||
413 | if ($this->userID) { | |
414 | // check if author has left the conversation | |
8fbd8b01 MS |
415 | $sql = "SELECT hideConversation |
416 | FROM wcf" . WCF_N . "_conversation_to_user | |
417 | WHERE conversationID = ? | |
418 | AND participantID = ?"; | |
fea86294 TD |
419 | $statement = WCF::getDB()->prepareStatement($sql); |
420 | $statement->execute([$this->conversationID, $this->userID]); | |
421 | $row = $statement->fetchArray(); | |
422 | if ($row !== false) { | |
423 | if ($row['hideConversation'] != self::STATE_LEFT) { | |
424 | return true; | |
425 | } | |
426 | } | |
427 | } | |
428 | ||
429 | return false; | |
430 | } | |
431 | } | |
432 | ||
433 | /** | |
434 | * Returns true if the current user is an active participant of this conversation. | |
fea86294 | 435 | */ |
beb426fe | 436 | public function isActiveParticipant(): bool |
fea86294 TD |
437 | { |
438 | if ($this->isActiveParticipant === null) { | |
439 | $sql = "SELECT leftAt | |
8fbd8b01 MS |
440 | FROM wcf" . WCF_N . "_conversation_to_user |
441 | WHERE conversationID = ? | |
442 | AND participantID = ?"; | |
fea86294 TD |
443 | $statement = WCF::getDB()->prepareStatement($sql); |
444 | $statement->execute([ | |
445 | $this->conversationID, | |
446 | WCF::getUser()->userID, | |
447 | ]); | |
448 | $leftAt = $statement->fetchSingleColumn(); | |
449 | ||
450 | $this->isActiveParticipant = ($leftAt !== false && $leftAt == 0); | |
451 | } | |
452 | ||
453 | return $this->isActiveParticipant; | |
454 | } | |
455 | ||
456 | /** | |
457 | * @inheritDoc | |
458 | */ | |
beb426fe | 459 | public function getPopoverLinkClass(): string |
fea86294 TD |
460 | { |
461 | return 'conversationLink'; | |
462 | } | |
463 | ||
464 | /** | |
465 | * Returns true if the given user id (default: current user) is participant | |
466 | * of all given conversation ids. | |
467 | * | |
c85e9df8 MS |
468 | * @param int[] $conversationIDs |
469 | * @param int $userID | |
fea86294 | 470 | */ |
beb426fe | 471 | public static function isParticipant(array $conversationIDs, $userID = null): bool |
fea86294 TD |
472 | { |
473 | if ($userID === null) { | |
474 | $userID = WCF::getUser()->userID; | |
475 | } | |
476 | ||
477 | // check if user is the initial author | |
478 | $conditions = new PreparedStatementConditionBuilder(); | |
479 | $conditions->add("conversationID IN (?)", [$conversationIDs]); | |
480 | $conditions->add("userID = ?", [$userID]); | |
481 | ||
8fbd8b01 MS |
482 | $sql = "SELECT conversationID |
483 | FROM wcf" . WCF_N . "_conversation | |
484 | " . $conditions; | |
fea86294 TD |
485 | $statement = WCF::getDB()->prepareStatement($sql); |
486 | $statement->execute($conditions->getParameters()); | |
487 | while ($row = $statement->fetchArray()) { | |
488 | $index = \array_search($row['conversationID'], $conversationIDs); | |
489 | unset($conversationIDs[$index]); | |
490 | } | |
491 | ||
492 | // check for participation | |
493 | if (!empty($conversationIDs)) { | |
494 | $conditions = new PreparedStatementConditionBuilder(); | |
495 | $conditions->add("conversationID IN (?)", [$conversationIDs]); | |
496 | $conditions->add("participantID = ?", [$userID]); | |
497 | $conditions->add("hideConversation <> ?", [self::STATE_LEFT]); | |
498 | ||
8fbd8b01 MS |
499 | $sql = "SELECT conversationID |
500 | FROM wcf" . WCF_N . "_conversation_to_user | |
501 | " . $conditions; | |
fea86294 TD |
502 | $statement = WCF::getDB()->prepareStatement($sql); |
503 | $statement->execute($conditions->getParameters()); | |
504 | while ($row = $statement->fetchArray()) { | |
505 | $index = \array_search($row['conversationID'], $conversationIDs); | |
506 | unset($conversationIDs[$index]); | |
507 | } | |
508 | } | |
509 | ||
510 | if (!empty($conversationIDs)) { | |
511 | return false; | |
512 | } | |
513 | ||
514 | return true; | |
515 | } | |
516 | ||
517 | /** | |
518 | * Validates the participants. | |
519 | * | |
520 | * @param mixed $participants | |
521 | * @param string $field | |
c85e9df8 | 522 | * @param int[] $existingParticipants |
fea86294 TD |
523 | * @return array $result |
524 | * @throws UserInputException | |
525 | */ | |
526 | public static function validateParticipants( | |
527 | $participants, | |
528 | $field = 'participants', | |
529 | array $existingParticipants = [] | |
530 | ) { | |
531 | $result = []; | |
532 | $error = []; | |
533 | ||
534 | // loop through participants and check their settings | |
535 | $participantList = UserProfile::getUserProfilesByUsername( | |
536 | (\is_array($participants) ? $participants : ArrayUtil::trim(\explode(',', $participants))) | |
537 | ); | |
538 | ||
539 | // load user storage at once to avoid multiple queries | |
540 | $userIDs = []; | |
541 | foreach ($participantList as $user) { | |
542 | if ($user) { | |
543 | $userIDs[] = $user->userID; | |
544 | } | |
545 | } | |
546 | UserStorageHandler::getInstance()->loadStorage($userIDs); | |
547 | ||
548 | foreach ($participantList as $participant => $user) { | |
549 | try { | |
550 | if ($user === null) { | |
551 | throw new UserInputException($field, 'notFound'); | |
552 | } | |
553 | ||
554 | // user is author | |
555 | if ($user->userID == WCF::getUser()->userID) { | |
556 | throw new UserInputException($field, 'isAuthor'); | |
557 | } elseif (\in_array($user->userID, $existingParticipants)) { | |
558 | throw new UserInputException($field, 'duplicate'); | |
559 | } | |
560 | ||
561 | // validate user | |
562 | self::validateParticipant($user, $field); | |
563 | ||
564 | // no error | |
565 | $existingParticipants[] = $result[] = $user->userID; | |
566 | } catch (UserInputException $e) { | |
567 | $error[] = ['type' => $e->getType(), 'username' => $participant]; | |
568 | } | |
569 | } | |
570 | ||
571 | if (!empty($error)) { | |
572 | throw new UserInputException($field, $error); | |
573 | } | |
574 | ||
575 | return $result; | |
576 | } | |
577 | ||
578 | /** | |
579 | * Validates the group participants. | |
580 | * | |
581 | * @param mixed $participants | |
582 | * @param string $field | |
c85e9df8 | 583 | * @param int[] $existingParticipants |
fea86294 TD |
584 | * @return array $result |
585 | */ | |
586 | public static function validateGroupParticipants( | |
587 | $participants, | |
588 | $field = 'participants', | |
589 | array $existingParticipants = [] | |
590 | ) { | |
591 | $groupIDs = \is_array($participants) ? $participants : ArrayUtil::toIntegerArray(\explode(',', $participants)); | |
592 | $validGroupIDs = []; | |
593 | $result = []; | |
594 | ||
595 | foreach ($groupIDs as $groupID) { | |
596 | $group = UserGroup::getGroupByID($groupID); | |
597 | /** @noinspection PhpUndefinedFieldInspection */ | |
598 | if ($group !== null && $group->canBeAddedAsConversationParticipant) { | |
599 | $validGroupIDs[] = $groupID; | |
600 | } | |
601 | } | |
602 | ||
603 | if (!empty($validGroupIDs)) { | |
604 | $userIDs = []; | |
605 | $conditionBuilder = new PreparedStatementConditionBuilder(); | |
606 | $conditionBuilder->add('groupID IN (?)', [$validGroupIDs]); | |
607 | $sql = "SELECT DISTINCT userID | |
8fbd8b01 MS |
608 | FROM wcf" . WCF_N . "_user_to_group |
609 | " . $conditionBuilder; | |
fea86294 TD |
610 | $statement = WCF::getDB()->prepareStatement($sql); |
611 | $statement->execute($conditionBuilder->getParameters()); | |
612 | while ($userID = $statement->fetchColumn()) { | |
613 | $userIDs[] = $userID; | |
614 | } | |
615 | ||
616 | if (!empty($userIDs)) { | |
617 | $users = UserProfileRuntimeCache::getInstance()->getObjects($userIDs); | |
618 | UserStorageHandler::getInstance()->loadStorage($userIDs); | |
619 | ||
620 | foreach ($users as $user) { | |
621 | // user is author | |
622 | if ($user->userID == WCF::getUser()->userID) { | |
623 | continue; | |
624 | } elseif (\in_array($user->userID, $existingParticipants)) { | |
625 | continue; | |
626 | } | |
627 | ||
628 | try { | |
629 | // validate user | |
630 | self::validateParticipant($user, $field); | |
631 | ||
632 | // no error | |
633 | $result[] = $user->userID; | |
634 | } catch (UserInputException $e) { | |
635 | } | |
636 | } | |
637 | } | |
638 | } | |
639 | ||
640 | return $result; | |
641 | } | |
642 | ||
643 | /** | |
644 | * Validates the given participant. | |
645 | * | |
646 | * @param UserProfile $user | |
647 | * @param string $field | |
648 | * @throws UserInputException | |
649 | */ | |
650 | public static function validateParticipant(UserProfile $user, $field = 'participants') | |
651 | { | |
652 | // check participant's settings and permissions | |
653 | if (!$user->getPermission('user.conversation.canUseConversation')) { | |
654 | throw new UserInputException($field, 'canNotUseConversation'); | |
655 | } | |
656 | ||
657 | if (!WCF::getSession()->getPermission('user.profile.cannotBeIgnored')) { | |
658 | // check if user wants to receive any conversations | |
659 | /** @noinspection PhpUndefinedFieldInspection */ | |
660 | if ($user->canSendConversation == 2) { | |
661 | throw new UserInputException($field, 'doesNotAcceptConversation'); | |
662 | } | |
663 | ||
664 | // check if user only wants to receive conversations by | |
665 | // users they are following and if the active user is followed | |
666 | // by the relevant user | |
667 | /** @noinspection PhpUndefinedFieldInspection */ | |
668 | if ($user->canSendConversation == 1 && !$user->isFollowing(WCF::getUser()->userID)) { | |
669 | throw new UserInputException($field, 'doesNotAcceptConversation'); | |
670 | } | |
671 | ||
672 | // active user is ignored by participant | |
3da2f11a | 673 | if ($user->isIgnoredUser(WCF::getUser()->userID, UserIgnore::TYPE_BLOCK_DIRECT_CONTACT)) { |
fea86294 TD |
674 | throw new UserInputException($field, 'ignoresYou'); |
675 | } | |
676 | ||
677 | // check participant's mailbox quota | |
678 | if (ConversationHandler::getInstance()->getConversationCount($user->userID) >= $user->getPermission('user.conversation.maxConversations')) { | |
679 | throw new UserInputException($field, 'mailboxIsFull'); | |
680 | } | |
681 | } | |
682 | } | |
9544b6b4 | 683 | } |