Commit | Line | Data |
---|---|---|
320f4a6d | 1 | <?php |
a9229942 | 2 | |
320f4a6d | 3 | namespace wcf\system\user\notification; |
a9229942 | 4 | |
c5fc8e59 | 5 | use ParagonIE\ConstantTime\Hex; |
7a23a706 | 6 | use wcf\data\object\type\ObjectType; |
320f4a6d MW |
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; | |
320f4a6d MW |
12 | use wcf\data\user\User; |
13 | use wcf\data\user\UserEditor; | |
14 | use wcf\data\user\UserProfile; | |
9d375181 | 15 | use wcf\form\NotificationUnsubscribeForm; |
4a7ceda1 | 16 | use wcf\system\background\BackgroundQueueHandler; |
a9229942 | 17 | use wcf\system\background\job\NotificationEmailDeliveryBackgroundJob; |
320f4a6d | 18 | use wcf\system\cache\builder\UserNotificationEventCacheBuilder; |
a8c5936e | 19 | use wcf\system\cache\runtime\UserProfileRuntimeCache; |
320f4a6d | 20 | use wcf\system\database\util\PreparedStatementConditionBuilder; |
a9229942 | 21 | use wcf\system\email\Email; |
3c5d2b85 | 22 | use wcf\system\email\mime\MimePartFacade; |
69fa057b | 23 | use wcf\system\email\mime\RecipientAwareTextMimePart; |
69fa057b | 24 | use wcf\system\email\UserMailbox; |
dd900ec4 AE |
25 | use wcf\system\event\EventHandler; |
26 | use wcf\system\exception\SystemException; | |
f9076d74 | 27 | use wcf\system\request\LinkHandler; |
a9229942 | 28 | use wcf\system\SingletonFactory; |
320f4a6d MW |
29 | use wcf\system\user\notification\event\IUserNotificationEvent; |
30 | use wcf\system\user\notification\object\IUserNotificationObject; | |
a9229942 | 31 | use wcf\system\user\notification\object\type\IUserNotificationObjectType; |
320f4a6d | 32 | use wcf\system\user\storage\UserStorageHandler; |
320f4a6d | 33 | use wcf\system\WCF; |
320f4a6d MW |
34 | |
35 | /** | |
36 | * Handles user notifications. | |
a9229942 TD |
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 | |
320f4a6d | 42 | */ |
a9229942 TD |
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 | |
c240c98a | 436 | ON notification_event.eventID = notification.eventID |
a9229942 | 437 | LEFT JOIN wcf" . WCF_N . "_object_type object_type |
c240c98a | 438 | ON object_type.objectTypeID = notification_event.objectTypeID |
a9229942 TD |
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 | |
1a34a55e | 502 | $authors = UserProfileRuntimeCache::getInstance()->getObjects($authorIDs); |
a9229942 TD |
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 | |
c0b28aa2 | 599 | * @return IUserNotificationEvent|null |
a9229942 TD |
600 | */ |
601 | public function getEvent($objectType, $eventName) | |
602 | { | |
603 | if (!isset($this->availableEvents[$objectType][$eventName])) { | |
c0b28aa2 | 604 | return null; |
a9229942 TD |
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 | { | |
813c41ce | 706 | return $this->availableObjectTypes[$objectType] ?? null; |
a9229942 TD |
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']); | |
cde15eae TD |
775 | } else { |
776 | $email->setMessageID(\sprintf( | |
777 | 'com.woltlab.wcf.genericNotification/%d/%d', | |
778 | $notification->notificationID, | |
779 | TIME_NOW | |
780 | )); | |
a9229942 TD |
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(); | |
1f46bd16 | 910 | $conditions->add("confirmTime = ?", [0]); |
a9229942 TD |
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); | |
1f46bd16 | 926 | $confirmedCount = $statement->getAffectedRows(); |
a9229942 TD |
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 | ||
1f46bd16 TD |
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]); | |
a9229942 | 948 | |
1f46bd16 TD |
949 | if (!empty($recipientIDs)) { |
950 | UserStorageHandler::getInstance()->reset($recipientIDs, 'userNotificationCount'); | |
951 | } else { | |
952 | UserStorageHandler::getInstance()->resetAll('userNotificationCount'); | |
953 | } | |
a9229942 TD |
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]); | |
1f46bd16 | 981 | $conditions->add("confirmTime = ?", [0]); |
a9229942 TD |
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); | |
1f46bd16 | 991 | $confirmedCount = $statement->getAffectedRows(); |
a9229942 TD |
992 | |
993 | $parameters = ['notificationIDs' => $notificationIDs]; | |
994 | EventHandler::getInstance()->fireAction($this, 'markAsConfirmedByIDs', $parameters); | |
995 | ||
1f46bd16 TD |
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]); | |
a9229942 | 1000 | |
1f46bd16 TD |
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 | } | |
a9229942 TD |
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 | } | |
320f4a6d | 1069 | } |