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