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