Merge branch '3.1' into 5.2
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / user / notification / UserNotificationHandler.class.php
1 <?php
2 namespace wcf\system\user\notification;
3 use wcf\data\object\type\ObjectType;
4 use wcf\data\object\type\ObjectTypeCache;
5 use wcf\data\user\notification\event\recipient\UserNotificationEventRecipientList;
6 use wcf\data\user\notification\event\UserNotificationEventList;
7 use wcf\data\user\notification\UserNotification;
8 use wcf\data\user\notification\UserNotificationAction;
9 use wcf\data\user\User;
10 use wcf\data\user\UserEditor;
11 use wcf\data\user\UserProfile;
12 use wcf\system\cache\builder\UserNotificationEventCacheBuilder;
13 use wcf\system\cache\runtime\UserProfileRuntimeCache;
14 use wcf\system\database\util\PreparedStatementConditionBuilder;
15 use wcf\system\email\mime\MimePartFacade;
16 use wcf\system\email\mime\RecipientAwareTextMimePart;
17 use wcf\system\email\Email;
18 use wcf\system\email\UserMailbox;
19 use wcf\system\event\EventHandler;
20 use wcf\system\exception\SystemException;
21 use wcf\system\request\LinkHandler;
22 use wcf\system\user\notification\event\IUserNotificationEvent;
23 use wcf\system\user\notification\object\type\IUserNotificationObjectType;
24 use wcf\system\user\notification\object\IUserNotificationObject;
25 use wcf\system\user\storage\UserStorageHandler;
26 use wcf\system\SingletonFactory;
27 use wcf\system\WCF;
28
29 /**
30 * Handles user notifications.
31 *
32 * @author Marcel Werk, Oliver Kliebisch
33 * @copyright 2001-2019 WoltLab GmbH, Oliver Kliebisch
34 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
35 * @package WoltLabSuite\Core\System\User\Notification
36 */
37 class UserNotificationHandler extends SingletonFactory {
38 /**
39 * list of available object types
40 * @var IUserNotificationObjectType[]
41 */
42 protected $availableObjectTypes = [];
43
44 /**
45 * list of available events
46 * @var IUserNotificationEvent[][]
47 */
48 protected $availableEvents = [];
49
50 /**
51 * number of outstanding notifications
52 * @var integer
53 */
54 protected $notificationCount = null;
55
56 /**
57 * list of object types
58 * @var ObjectType[]
59 */
60 protected $objectTypes = [];
61
62 /**
63 * @inheritDoc
64 */
65 protected function init() {
66 // get available object types
67 $this->objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.notification.objectType');
68 foreach ($this->objectTypes as $typeName => $object) {
69 $this->availableObjectTypes[$typeName] = $object->getProcessor();
70 }
71
72 // get available events
73 $this->availableEvents = UserNotificationEventCacheBuilder::getInstance()->getData();
74 }
75
76 /**
77 * Triggers a notification event.
78 *
79 * @param string $eventName
80 * @param string $objectType
81 * @param IUserNotificationObject $notificationObject
82 * @param integer[] $recipientIDs
83 * @param mixed[] $additionalData
84 * @param integer $baseObjectID
85 * @throws SystemException
86 */
87 public function fireEvent($eventName, $objectType, IUserNotificationObject $notificationObject, array $recipientIDs, array $additionalData = [], $baseObjectID = 0) {
88 // check given object type and event name
89 if (!isset($this->availableEvents[$objectType][$eventName])) {
90 throw new SystemException("Unknown event ".$objectType."-".$eventName." given");
91 }
92
93 // get objects
94 $objectTypeObject = $this->availableObjectTypes[$objectType];
95 $event = $this->availableEvents[$objectType][$eventName];
96
97 // get author's profile
98 $userProfile = null;
99 if ($notificationObject->getAuthorID()) {
100 if ($notificationObject->getAuthorID() == WCF::getUser()->userID) {
101 $userProfile = new UserProfile(WCF::getUser());
102 }
103 else {
104 $userProfile = UserProfileRuntimeCache::getInstance()->getObject($notificationObject->getAuthorID());
105 }
106 }
107 if ($userProfile === null) {
108 $userProfile = new UserProfile(new User(null, []));
109 }
110
111 // set object data
112 $event->setObject(new UserNotification(null, []), $notificationObject, $userProfile, $additionalData);
113 $event->setAuthors([$event->getAuthorID() => $event->getAuthor()]);
114
115 $parameters = [
116 'eventName' => $eventName,
117 'objectType' => $objectType,
118 'notificationObject' => $notificationObject,
119 'recipientIDs' => $recipientIDs,
120 'additionalData' => $additionalData,
121 'baseObjectID' => $baseObjectID,
122 'objectTypeObject' => $objectTypeObject,
123 'userProfile' => $userProfile,
124 'event' => $event
125 ];
126 // @deprecated 5.2 This event exposes incomplete data and should not be used, please use the following events instead.
127 EventHandler::getInstance()->fireAction($this, 'fireEvent', $parameters);
128
129 // find existing notifications
130 $conditions = new PreparedStatementConditionBuilder();
131 $conditions->add("userID IN (?)", [$recipientIDs]);
132 $conditions->add("eventID = ?", [$event->eventID]);
133 $conditions->add("eventHash = ?", [$event->getEventHash()]);
134 $conditions->add("confirmTime = ?", [0]);
135
136 $sql = "SELECT notificationID, userID
137 FROM wcf".WCF_N."_user_notification
138 ".$conditions;
139 $statement = WCF::getDB()->prepareStatement($sql);
140 $statement->execute($conditions->getParameters());
141 $notifications = $statement->fetchMap('userID', 'notificationID');
142
143 // check if event supports stacking and author should be added
144 if (!empty($notifications) && $event->isStackable()) {
145 $conditions = new PreparedStatementConditionBuilder();
146 $conditions->add("notificationID IN (?)", [array_values($notifications)]);
147 if ($notificationObject->getAuthorID()) {
148 $conditions->add("authorID = ?", [$notificationObject->getAuthorID()]);
149 }
150 else {
151 $conditions->add("authorID IS NULL");
152 }
153
154 $sql = "SELECT notificationID
155 FROM wcf".WCF_N."_user_notification_author
156 ".$conditions;
157 $statement = WCF::getDB()->prepareStatement($sql);
158 $statement->execute($conditions->getParameters());
159 $notificationIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
160
161 // filter array of existing notifications and remove values which
162 // do not have a notification from this author yet (inverse logic!)
163 foreach ($notifications as $userID => $notificationID) {
164 if (!in_array($notificationID, $notificationIDs)) {
165 unset($notifications[$userID]);
166 }
167 }
168
169 if (!empty($notificationIDs)) {
170 // update trigger count
171 $sql = "UPDATE wcf".WCF_N."_user_notification
172 SET timesTriggered = timesTriggered + ?,
173 guestTimesTriggered = guestTimesTriggered + ?
174 WHERE notificationID = ?";
175 $statement = WCF::getDB()->prepareStatement($sql);
176
177 WCF::getDB()->beginTransaction();
178 foreach ($notificationIDs as $notificationID) {
179 $statement->execute([
180 1,
181 $notificationObject->getAuthorID() ? 0 : 1,
182 $notificationID
183 ]);
184 }
185 WCF::getDB()->commitTransaction();
186
187 $triggerCountParameters = $parameters;
188 $triggerCountParameters['updateTriggerCount'] = $notificationIDs;
189 EventHandler::getInstance()->fireAction($this, 'updateTriggerCount', $triggerCountParameters);
190 unset($triggerCountParameters);
191 }
192 }
193
194 $recipientIDs = array_diff($recipientIDs, array_keys($notifications));
195 if (empty($recipientIDs)) {
196 return;
197 }
198
199 // remove recipients that are blocking the current user
200 if ($userProfile->getUserID()) {
201 // we use a live query here to avoid offloading this to the UserProfile
202 // class, as we're potentially dealing with a lot of users and loading
203 // their user storage data can get really expensive
204 $conditions = new PreparedStatementConditionBuilder();
205 $conditions->add("userID IN (?)", [$recipientIDs]);
206 $conditions->add("ignoreUserID = ?", [$userProfile->getUserID()]);
207
208 $sql = "SELECT userID
209 FROM wcf" . WCF_N . "_user_ignore
210 ".$conditions;
211 $statement = WCF::getDB()->prepareStatement($sql);
212 $statement->execute($conditions->getParameters());
213 $userIDs = [];
214 while ($userID = $statement->fetchColumn()) {
215 $userIDs[] = $userID;
216 }
217
218 if (!empty($userIDs)) {
219 $recipientIDs = array_diff($recipientIDs, $userIDs);
220 }
221
222 if (empty($recipientIDs)) {
223 return;
224 }
225 }
226
227 // get recipients
228 $recipientList = new UserNotificationEventRecipientList();
229 $recipientList->getConditionBuilder()->add('event_to_user.eventID = ?', [$event->eventID]);
230 $recipientList->getConditionBuilder()->add('event_to_user.userID IN (?)', [$recipientIDs]);
231 $recipientList->readObjects();
232 $recipients = $recipientList->getObjects();
233 if (!empty($recipients)) {
234 $data = [
235 'authorID' => $event->getAuthorID() ?: null,
236 'data' => [
237 'eventID' => $event->eventID,
238 'authorID' => $event->getAuthorID() ?: null,
239 'objectID' => $notificationObject->getObjectID(),
240 'baseObjectID' => $baseObjectID,
241 'eventHash' => $event->getEventHash(),
242 'packageID' => $objectTypeObject->packageID,
243 'mailNotified' => $event->supportsEmailNotification() ? 0 : 1,
244 'time' => TIME_NOW,
245 'additionalData' => serialize($additionalData)
246 ],
247 'recipients' => $recipients
248 ];
249
250 if ($event->isStackable()) {
251 $data['notifications'] = $notifications;
252
253 $action = new UserNotificationAction([], 'createStackable', $data);
254 }
255 else {
256 $data['data']['timesTriggered'] = 1;
257 $action = new UserNotificationAction([], 'createDefault', $data);
258 }
259
260 $result = $action->executeAction();
261 $notifications = $result['returnValues'];
262
263 // send notifications
264 if ($event->supportsEmailNotification()) {
265 foreach ($recipients as $recipient) {
266 if ($recipient->mailNotificationType == 'instant') {
267 if (isset($notifications[$recipient->userID]) && $notifications[$recipient->userID]['isNew']) {
268 $event->setObject($notifications[$recipient->userID]['object'], $notificationObject, $userProfile, $additionalData);
269 $event->setAuthors([$userProfile->userID => $userProfile]);
270 $this->sendInstantMailNotification($notifications[$recipient->userID]['object'], $recipient, $event);
271 }
272 }
273 }
274 }
275
276 // reset notification count
277 UserStorageHandler::getInstance()->reset(array_keys($recipients), 'userNotificationCount');
278
279 $parameters['notifications'] = $notifications;
280 $parameters['recipients'] = $recipients;
281 EventHandler::getInstance()->fireAction($this, 'createdNotification', $parameters);
282 }
283 }
284
285 /**
286 * Returns the number of outstanding notifications for the active user.
287 *
288 * @param boolean $skipCache
289 * @return integer
290 */
291 public function getNotificationCount($skipCache = false) {
292 if ($this->notificationCount === null || $skipCache) {
293 $this->notificationCount = 0;
294
295 if (WCF::getUser()->userID) {
296 $data = UserStorageHandler::getInstance()->getField('userNotificationCount');
297
298 // cache does not exist or is outdated
299 if ($data === null || $skipCache) {
300 $sql = "SELECT COUNT(*)
301 FROM wcf".WCF_N."_user_notification
302 WHERE userID = ?
303 AND confirmTime = ?";
304 $statement = WCF::getDB()->prepareStatement($sql);
305 $statement->execute([
306 WCF::getUser()->userID,
307 0
308 ]);
309
310 $this->notificationCount = $statement->fetchSingleColumn();
311
312 // update storage data
313 UserStorageHandler::getInstance()->update(WCF::getUser()->userID, 'userNotificationCount', serialize($this->notificationCount));
314 }
315 else {
316 $this->notificationCount = unserialize($data);
317 }
318 }
319 }
320
321 return $this->notificationCount;
322 }
323
324 /**
325 * Counts all existing notifications for current user and returns it.
326 *
327 * @return integer
328 */
329 public function countAllNotifications() {
330 $sql = "SELECT COUNT(*)
331 FROM wcf".WCF_N."_user_notification
332 WHERE userID = ?";
333 $statement = WCF::getDB()->prepareStatement($sql);
334 $statement->execute([WCF::getUser()->userID]);
335
336 return $statement->fetchSingleColumn();
337 }
338
339 /**
340 * Returns a list of notifications.
341 *
342 * @param integer $limit
343 * @param integer $offset
344 * @param boolean $showConfirmedNotifications DEPRECATED
345 * @return mixed[]
346 */
347 public function getNotifications($limit = 5, $offset = 0, $showConfirmedNotifications = false) {
348 $notifications = $this->fetchNotifications($limit, $offset);
349
350 return $this->processNotifications($notifications);
351 }
352
353 /**
354 * Returns a mixed list of notifications, containing leading unconfirmed notifications in their chronological
355 * order regardless of the overall order of already confirmed items.
356 *
357 * @return array
358 */
359 public function getMixedNotifications() {
360 $notificationCount = $this->getNotificationCount(true);
361
362 $notifications = [];
363 if ($notificationCount > 0) {
364 $notifications = $this->fetchNotifications(10, 0, 0);
365 }
366
367 $count = count($notifications);
368 $limit = 10 - $count;
369
370 if ($limit) {
371 $notifications = array_merge($notifications, $this->fetchNotifications($limit, 0, 1));
372 }
373
374 $returnValues = $this->processNotifications($notifications);
375 $returnValues['notificationCount'] = $notificationCount;
376
377 return $returnValues;
378 }
379
380 /**
381 * Fetches a list of notifications based upon given conditions.
382 *
383 * @param integer $limit
384 * @param integer $offset
385 * @param mixed $filterByConfirmed
386 * @return UserNotification[]
387 */
388 protected function fetchNotifications($limit, $offset, $filterByConfirmed = null) {
389 // build enormous query
390 $conditions = new PreparedStatementConditionBuilder();
391 $conditions->add("notification.userID = ?", [WCF::getUser()->userID]);
392
393 if ($filterByConfirmed !== null) {
394 // consider only unconfirmed notifications
395 if ($filterByConfirmed == 0) {
396 $conditions->add("notification.confirmTime = ?", [0]);
397 }
398 else {
399 // consider only notifications marked as confirmed in the past 48 hours (86400 = 1 day)
400 $conditions->add("notification.confirmTime >= ?", [TIME_NOW - (2 * 86400)]);
401 }
402 }
403
404 $sql = "SELECT notification.*, notification_event.eventID, object_type.objectType
405 FROM wcf".WCF_N."_user_notification notification
406 LEFT JOIN wcf".WCF_N."_user_notification_event notification_event
407 ON (notification_event.eventID = notification.eventID)
408 LEFT JOIN wcf".WCF_N."_object_type object_type
409 ON (object_type.objectTypeID = notification_event.objectTypeID)
410 ".$conditions."
411 ORDER BY notification.time DESC";
412 $statement = WCF::getDB()->prepareStatement($sql, $limit, $offset);
413 $statement->execute($conditions->getParameters());
414
415 return $statement->fetchObjects(UserNotification::class, 'notificationID');
416 }
417
418 /**
419 * Processes a list of notification objects.
420 *
421 * @param UserNotification[] $notificationObjects
422 * @return mixed[]
423 */
424 public function processNotifications(array $notificationObjects) {
425 // return an empty set if no notifications exist
426 if (empty($notificationObjects)) {
427 return [
428 'count' => 0,
429 'notifications' => []
430 ];
431 }
432
433 $eventIDs = $notificationIDs = $objectTypes = [];
434 foreach ($notificationObjects as $notification) {
435 // cache object types
436 if (!isset($objectTypes[$notification->objectType])) {
437 $objectTypes[$notification->objectType] = [
438 'objectType' => $this->availableObjectTypes[$notification->objectType],
439 'objectIDs' => [],
440 'objects' => []
441 ];
442 }
443
444 $objectTypes[$notification->objectType]['objectIDs'][] = $notification->objectID;
445 $eventIDs[] = $notification->eventID;
446 $notificationIDs[] = $notification->notificationID;
447 }
448
449 // load authors
450 $conditions = new PreparedStatementConditionBuilder();
451 $conditions->add("notificationID IN (?)", [$notificationIDs]);
452 $sql = "SELECT notificationID, authorID
453 FROM wcf".WCF_N."_user_notification_author
454 ".$conditions."
455 ORDER BY time ASC";
456 $statement = WCF::getDB()->prepareStatement($sql);
457 $statement->execute($conditions->getParameters());
458 $authorIDs = $authorToNotification = [];
459 while ($row = $statement->fetchArray()) {
460 if ($row['authorID']) {
461 $authorIDs[] = $row['authorID'];
462 }
463
464 if (!isset($authorToNotification[$row['notificationID']])) {
465 $authorToNotification[$row['notificationID']] = [];
466 }
467
468 $authorToNotification[$row['notificationID']][] = $row['authorID'];
469 }
470
471 // load authors
472 $authors = UserProfile::getUserProfiles($authorIDs);
473 $unknownAuthor = new UserProfile(new User(null, ['userID' => null, 'username' => WCF::getLanguage()->get('wcf.user.guest')]));
474
475 // load objects associated with each object type
476 foreach ($objectTypes as $objectType => $objectData) {
477 /** @noinspection PhpUndefinedMethodInspection */
478 $objectTypes[$objectType]['objects'] = $objectData['objectType']->getObjectsByIDs($objectData['objectIDs']);
479 }
480
481 // load required events
482 $eventList = new UserNotificationEventList();
483 $eventList->getConditionBuilder()->add("user_notification_event.eventID IN (?)", [$eventIDs]);
484 $eventList->readObjects();
485 $eventObjects = $eventList->getObjects();
486
487 // build notification data
488 $notifications = [];
489 $deleteNotifications = [];
490 foreach ($notificationObjects as $notification) {
491 $object = $objectTypes[$notification->objectType]['objects'][$notification->objectID];
492 if ($object->__unknownNotificationObject) {
493 $deleteNotifications[] = $notification;
494 continue;
495 }
496
497 $className = $eventObjects[$notification->eventID]->className;
498
499 /** @var IUserNotificationEvent $class */
500 $class = new $className($eventObjects[$notification->eventID]);
501 $class->setObject(
502 $notification,
503 $object,
504 (isset($authors[$notification->authorID]) ? $authors[$notification->authorID] : $unknownAuthor),
505 $notification->additionalData
506 );
507
508 if (isset($authorToNotification[$notification->notificationID])) {
509 $eventAuthors = [];
510 foreach ($authorToNotification[$notification->notificationID] as $userID) {
511 if (!$userID) {
512 $eventAuthors[0] = $unknownAuthor;
513 }
514 else if (isset($authors[$userID])) {
515 $eventAuthors[$userID] = $authors[$userID];
516 }
517 }
518 if (!empty($eventAuthors)) {
519 $class->setAuthors($eventAuthors);
520 }
521 }
522
523 $data = [
524 'authors' => count($class->getAuthors()),
525 'event' => $class,
526 'notificationID' => $notification->notificationID,
527 'time' => $notification->time
528 ];
529
530 $data['confirmed'] = ($notification->confirmTime > 0);
531
532 $notifications[] = $data;
533 }
534
535 // check access
536 foreach ($notifications as $index => $notificationData) {
537 /** @var IUserNotificationEvent $event */
538 $event = $notificationData['event'];
539 if (!$event->checkAccess()) {
540 if ($event->deleteNoAccessNotification()) {
541 $deleteNotifications[] = $event->getNotification();
542 }
543
544 unset($notifications[$index]);
545 }
546 }
547
548 if (!empty($deleteNotifications)) {
549 $notificationAction = new UserNotificationAction($deleteNotifications, 'delete');
550 $notificationAction->executeAction();
551
552 // reset notification counter
553 UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'userNotificationCount');
554 }
555
556 return [
557 'count' => count($notifications),
558 'notifications' => $notifications
559 ];
560 }
561
562 /**
563 * Returns event object for given object type and event, returns NULL on failure.
564 *
565 * @param string $objectType
566 * @param string $eventName
567 * @return IUserNotificationEvent
568 */
569 public function getEvent($objectType, $eventName) {
570 if (!isset($this->availableEvents[$objectType][$eventName])) return null;
571
572 return $this->availableEvents[$objectType][$eventName];
573 }
574
575 /**
576 * Returns all events for given object type.
577 *
578 * @param string $objectType
579 * @return IUserNotificationEvent[]
580 */
581 public function getEvents($objectType) {
582 if (!isset($this->availableEvents[$objectType])) return [];
583
584 return $this->availableEvents[$objectType];
585 }
586
587 /**
588 * Retrieves a notification id.
589 *
590 * @param integer $eventID
591 * @param integer $objectID
592 * @param integer $authorID
593 * @param integer $time
594 * @return integer
595 * @throws SystemException
596 */
597 public function getNotificationID($eventID, $objectID, $authorID = null, $time = null) {
598 if ($authorID === null && $time === null) {
599 throw new SystemException("authorID and time cannot be omitted at once.");
600 }
601
602 $conditions = new PreparedStatementConditionBuilder();
603 $conditions->add("eventID = ?", [$eventID]);
604 $conditions->add("objectID = ?", [$objectID]);
605 if ($authorID !== null) $conditions->add("authorID = ?", [$authorID]);
606 if ($time !== null) $conditions->add("time = ?", [$time]);
607
608 $sql = "SELECT notificationID
609 FROM wcf".WCF_N."_user_notification
610 ".$conditions;
611 $statement = WCF::getDB()->prepareStatement($sql);
612 $statement->execute($conditions->getParameters());
613
614 $row = $statement->fetchArray();
615
616 return ($row === false) ? null : $row['notificationID'];
617 }
618
619 /**
620 * Returns a list of available object types.
621 *
622 * @return IUserNotificationObjectType[]
623 */
624 public function getAvailableObjectTypes() {
625 return $this->availableObjectTypes;
626 }
627
628 /**
629 * Returns a list of available events.
630 *
631 * @return IUserNotificationEvent[][]
632 */
633 public function getAvailableEvents() {
634 return $this->availableEvents;
635 }
636
637 /**
638 * Returns object type id by name.
639 *
640 * @param string $objectType
641 * @return integer
642 */
643 public function getObjectTypeID($objectType) {
644 if (isset($this->objectTypes[$objectType])) {
645 return $this->objectTypes[$objectType]->objectTypeID;
646 }
647
648 return 0;
649 }
650
651 /**
652 * Returns the processor of the object type with the given name or `null`
653 * if no such processor exists
654 *
655 * @param string $objectType
656 * @return IUserNotificationObjectType|null
657 */
658 public function getObjectTypeProcessor($objectType) {
659 if (isset($this->availableObjectTypes[$objectType])) {
660 return $this->availableObjectTypes[$objectType];
661 }
662
663 return null;
664 }
665
666 /**
667 * Sends the mail notification.
668 *
669 * @param UserNotification $notification
670 * @param User $user
671 * @param IUserNotificationEvent $event
672 */
673 public function sendInstantMailNotification(UserNotification $notification, User $user, IUserNotificationEvent $event) {
674 // no notifications for disabled or banned users
675 if ($user->activationCode) return;
676 if ($user->banned) return;
677
678 // recipient's language
679 $event->setLanguage($user->getLanguage());
680
681 // generate token if not present
682 if (!$user->notificationMailToken) {
683 $token = bin2hex(\random_bytes(10));
684 $editor = new UserEditor($user);
685 $editor->update(['notificationMailToken' => $token]);
686
687 // reload user
688 $user = new User($user->userID);
689 }
690
691 $email = new Email();
692 $email->setSubject($user->getLanguage()->getDynamicVariable('wcf.user.notification.mail.subject', [
693 'title' => $event->getEmailTitle()
694 ]));
695 $email->addRecipient(new UserMailbox($user));
696
697 $message = $event->getEmailMessage('instant');
698 if (is_array($message)) {
699 if (!isset($message['variables'])) $message['variables'] = [];
700 $variables = array_merge($message['variables'], [
701 'notificationContent' => $message,
702 'event' => $event,
703 'notificationType' => 'instant',
704 'variables' => $message['variables'] // deprecated, but is kept for backwards compatibility
705 ]);
706
707 if (isset($message['message-id'])) {
708 $email->setMessageID($message['message-id']);
709 }
710 if (isset($message['in-reply-to'])) {
711 foreach ($message['in-reply-to'] as $inReplyTo) $email->addInReplyTo($inReplyTo);
712 }
713 if (isset($message['references'])) {
714 foreach ($message['references'] as $references) $email->addReferences($references);
715 }
716
717 $html = new RecipientAwareTextMimePart('text/html', 'email_notification', 'wcf', $variables);
718 $plainText = new RecipientAwareTextMimePart('text/plain', 'email_notification', 'wcf', $variables);
719 $email->setBody(new MimePartFacade([$html, $plainText]));
720 }
721 else {
722 $email->setBody(new RecipientAwareTextMimePart('text/plain', 'email_notification', 'wcf', [
723 'notificationContent' => $message,
724 'event' => $event,
725 'notificationType' => 'instant'
726 ]));
727 }
728
729 $email->send();
730 }
731
732 /**
733 * This method does not delete notifications, instead it marks them as confirmed. The system
734 * does not allow to delete them, but since it was intended in WCF 2.0, this method only
735 * exists for compatibility reasons.
736 *
737 * Please consider replacing your calls with markAsConfirmed().
738 *
739 * @deprecated
740 *
741 * @param string $eventName
742 * @param string $objectType
743 * @param integer[] $recipientIDs
744 * @param integer[] $objectIDs
745 */
746 public function deleteNotifications($eventName, $objectType, array $recipientIDs, array $objectIDs = []) {
747 $this->markAsConfirmed($eventName, $objectType, $recipientIDs, $objectIDs);
748 }
749
750 /**
751 * Removes notifications, this method should only be invoked for delete objects.
752 *
753 * @param string $objectType
754 * @param integer[] $objectIDs
755 * @throws SystemException
756 */
757 public function removeNotifications($objectType, array $objectIDs) {
758 // check given object type
759 $objectTypeObj = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.notification.objectType', $objectType);
760 if ($objectTypeObj === null) {
761 throw new SystemException("Unknown object type ".$objectType." given");
762 }
763
764 // get event ids
765 $sql = "SELECT eventID
766 FROM wcf".WCF_N."_user_notification_event
767 WHERE objectTypeID = ?";
768 $statement = WCF::getDB()->prepareStatement($sql);
769 $statement->execute([
770 $objectTypeObj->objectTypeID
771 ]);
772 $eventIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
773
774 if (!empty($eventIDs)) {
775 $conditions = new PreparedStatementConditionBuilder();
776 $conditions->add("eventID IN (?)", [$eventIDs]);
777 $conditions->add("objectID IN (?)", [$objectIDs]);
778
779 $sql = "SELECT userID
780 FROM wcf".WCF_N."_user_notification
781 ".$conditions;
782 $statement = WCF::getDB()->prepareStatement($sql);
783 $statement->execute($conditions->getParameters());
784 $userIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
785
786 // reset number of notifications
787 if (!empty($userIDs)) {
788 UserStorageHandler::getInstance()->reset(array_unique($userIDs), 'userNotificationCount');
789 }
790
791 $parameters = [
792 'eventIDs' => $eventIDs,
793 'objectIDs' => $objectIDs,
794 'objectType' => $objectType,
795 'userIDs' => $userIDs,
796 ];
797 EventHandler::getInstance()->fireAction($this, 'removeNotifications', $parameters);
798
799 // delete notifications
800 $sql = "DELETE FROM wcf".WCF_N."_user_notification
801 ".$conditions;
802 $statement = WCF::getDB()->prepareStatement($sql);
803 $statement->execute($conditions->getParameters());
804 }
805 }
806
807 /**
808 * Marks notifications as confirmed
809 *
810 * @param string $eventName
811 * @param string $objectType
812 * @param integer[] $recipientIDs
813 * @param integer[] $objectIDs
814 * @throws SystemException
815 */
816 public function markAsConfirmed($eventName, $objectType, array $recipientIDs, array $objectIDs = []) {
817 // check given object type and event name
818 if (!isset($this->availableEvents[$objectType][$eventName])) {
819 throw new SystemException("Unknown event ".$objectType."-".$eventName." given");
820 }
821
822 // get objects
823 $event = $this->availableEvents[$objectType][$eventName];
824
825 // mark as confirmed
826 $conditions = new PreparedStatementConditionBuilder();
827 $conditions->add("eventID = ?", [$event->eventID]);
828 if (!empty($recipientIDs)) $conditions->add("userID IN (?)", [$recipientIDs]);
829 if (!empty($objectIDs)) $conditions->add("objectID IN (?)", [$objectIDs]);
830
831 $sql = "UPDATE wcf".WCF_N."_user_notification
832 SET confirmTime = ?
833 ".$conditions;
834 $statement = WCF::getDB()->prepareStatement($sql);
835 $parameters = $conditions->getParameters();
836 array_unshift($parameters, TIME_NOW);
837 $statement->execute($parameters);
838
839 $parameters = [
840 'event' => $event,
841 'eventName' => $eventName,
842 'objectIDs' => $objectIDs,
843 'objectType' => $objectType,
844 'recipientIDs' => $recipientIDs,
845 ];
846 EventHandler::getInstance()->fireAction($this, 'markAsConfirmed', $parameters);
847
848 // delete notification_to_user assignments (mimic legacy notification system)
849 $sql = "DELETE FROM wcf".WCF_N."_user_notification_to_user
850 WHERE notificationID NOT IN (
851 SELECT notificationID
852 FROM wcf".WCF_N."_user_notification
853 WHERE confirmTime = ?
854 )";
855 $statement = WCF::getDB()->prepareStatement($sql);
856 $statement->execute([0]);
857
858 // reset storage
859 if (!empty($recipientIDs)) {
860 UserStorageHandler::getInstance()->reset($recipientIDs, 'userNotificationCount');
861 }
862 else {
863 UserStorageHandler::getInstance()->resetAll('userNotificationCount');
864 }
865 }
866
867 /**
868 * Marks a single notification id as confirmed.
869 *
870 * @param integer $notificationID
871 * @deprecated 5.2 Please use `UserNotificationHandler::markAsConfirmedByIDs()` instead.
872 */
873 public function markAsConfirmedByID($notificationID) {
874 $this->markAsConfirmedByIDs([$notificationID]);
875 }
876
877 /**
878 * Marks a list of notification ids as confirmed.
879 *
880 * @param integer[] $notificationIDs
881 */
882 public function markAsConfirmedByIDs(array $notificationIDs) {
883 if (empty($notificationIDs)) {
884 return;
885 }
886
887 $conditions = new PreparedStatementConditionBuilder();
888 $conditions->add("notificationID IN (?)", [$notificationIDs]);
889
890 // mark notifications as confirmed
891 $sql = "UPDATE wcf".WCF_N."_user_notification
892 SET confirmTime = ?
893 ".$conditions;
894 $statement = WCF::getDB()->prepareStatement($sql);
895 $parameters = $conditions->getParameters();
896 array_unshift($parameters, TIME_NOW);
897 $statement->execute($parameters);
898
899 $parameters = ['notificationIDs' => $notificationIDs];
900 EventHandler::getInstance()->fireAction($this, 'markAsConfirmedByIDs', $parameters);
901
902 // delete notification_to_user assignments (mimic legacy notification system)
903 $sql = "DELETE FROM wcf".WCF_N."_user_notification_to_user
904 ".$conditions;
905 $statement = WCF::getDB()->prepareStatement($sql);
906 $statement->execute($conditions->getParameters());
907
908 // reset user storage
909 UserStorageHandler::getInstance()->reset([WCF::getUser()->userID], 'userNotificationCount');
910 }
911
912 /**
913 * Returns the user's notification setting for the given event.
914 *
915 * @param string $objectType
916 * @param string $eventName
917 * @return mixed
918 */
919 public function getEventSetting($objectType, $eventName) {
920 // get event
921 $event = $this->getEvent($objectType, $eventName);
922
923 // get setting
924 $sql = "SELECT mailNotificationType
925 FROM wcf".WCF_N."_user_notification_event_to_user
926 WHERE eventID = ?
927 AND userID = ?";
928 $statement = WCF::getDB()->prepareStatement($sql);
929 $statement->execute([$event->eventID, WCF::getUser()->userID]);
930 $row = $statement->fetchArray();
931 if ($row === false) return false;
932 return $row['mailNotificationType'];
933 }
934
935 /**
936 * Returns the title and text-only message body for the latest notification,
937 * that is both unread and newer than `$lastRequestTimestamp`. May return an
938 * empty array if there is no new notification.
939 *
940 * @param integer $lastRequestTimestamp
941 * @return string[]
942 */
943 public function getLatestNotification($lastRequestTimestamp) {
944 $notifications = $this->fetchNotifications(1, 0, 0);
945 if (!empty($notifications) && reset($notifications)->time > $lastRequestTimestamp) {
946 $notifications = $this->processNotifications($notifications);
947
948 if (isset($notifications['notifications'][0])) {
949 /** @var IUserNotificationEvent $event */
950 $event = $notifications['notifications'][0]['event'];
951
952 return [
953 'title' => strip_tags($event->getTitle()),
954 'message' => strip_tags($event->getMessage()),
955 'link' => LinkHandler::getInstance()->getLink('NotificationConfirm', ['id' => $event->getNotification()->notificationID])
956 ];
957 }
958 }
959
960 return [];
961 }
962 }