Fixed time zone calculation issue
[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\ObjectTypeCache;
4 use wcf\data\user\notification\event\recipient\UserNotificationEventRecipientList;
5 use wcf\data\user\notification\event\UserNotificationEventList;
6 use wcf\data\user\notification\UserNotification;
7 use wcf\data\user\notification\UserNotificationAction;
8 use wcf\data\user\notification\UserNotificationList;
9 use wcf\data\user\User;
10 use wcf\data\user\UserEditor;
11 use wcf\data\user\UserProfile;
12 use wcf\system\cache\builder\UserNotificationEventCacheBuilder;
13 use wcf\system\database\util\PreparedStatementConditionBuilder;
14 use wcf\system\exception\SystemException;
15 use wcf\system\mail\Mail;
16 use wcf\system\user\notification\event\IUserNotificationEvent;
17 use wcf\system\user\notification\object\IUserNotificationObject;
18 use wcf\system\user\storage\UserStorageHandler;
19 use wcf\system\SingletonFactory;
20 use wcf\system\WCF;
21 use wcf\util\StringUtil;
22
23 /**
24 * Handles user notifications.
25 *
26 * @author Marcel Werk, Oliver Kliebisch
27 * @copyright 2001-2014 WoltLab GmbH, Oliver Kliebisch
28 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
29 * @package com.woltlab.wcf
30 * @subpackage system.user.notification
31 * @category Community Framework
32 */
33 class UserNotificationHandler extends SingletonFactory {
34 /**
35 * list of available object types
36 * @var array
37 */
38 protected $availableObjectTypes = array();
39
40 /**
41 * list of available events
42 * @var array
43 */
44 protected $availableEvents = array();
45
46 /**
47 * number of outstanding notifications
48 * @var integer
49 */
50 protected $notificationCount = null;
51
52 /**
53 * list of object types
54 * @var array<\wcf\data\object\type\ObjectType>
55 */
56 protected $objectTypes = array();
57
58 /**
59 * @see \wcf\system\SingletonFactory::init()
60 */
61 protected function init() {
62 // get available object types
63 $this->objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.notification.objectType');
64 foreach ($this->objectTypes as $typeName => $object) {
65 $this->availableObjectTypes[$typeName] = $object->getProcessor();
66 }
67
68 // get available events
69 $this->availableEvents = UserNotificationEventCacheBuilder::getInstance()->getData();
70 }
71
72 /**
73 * Triggers a notification event.
74 *
75 * @param string $eventName
76 * @param string $objectType
77 * @param \wcf\system\user\notification\object\IUserNotificationObject $notificationObject
78 * @param array<integer> $recipientIDs
79 * @param array<mixed> $additionalData
80 */
81 public function fireEvent($eventName, $objectType, IUserNotificationObject $notificationObject, array $recipientIDs, array $additionalData = array()) {
82 // check given object type and event name
83 if (!isset($this->availableEvents[$objectType][$eventName])) {
84 throw new SystemException("Unknown event ".$objectType."-".$eventName." given");
85 }
86
87 // get objects
88 $objectTypeObject = $this->availableObjectTypes[$objectType];
89 $event = $this->availableEvents[$objectType][$eventName];
90
91 // get author's profile
92 $userProfile = null;
93 if ($notificationObject->getAuthorID()) {
94 if ($notificationObject->getAuthorID() == WCF::getUser()->userID) {
95 $userProfile = new UserProfile(WCF::getUser());
96 }
97 else {
98 $userProfile = UserProfile::getUserProfile($notificationObject->getAuthorID());
99 }
100 }
101 if ($userProfile === null) {
102 $userProfile = new UserProfile(new User(null, array()));
103 }
104
105 // set object data
106 $event->setObject(new UserNotification(null, array()), $notificationObject, $userProfile, $additionalData);
107
108 // find existing events
109 $userIDs = array();
110 $conditionBuilder = new PreparedStatementConditionBuilder();
111 $conditionBuilder->add('notification_to_user.notificationID = notification.notificationID');
112 $conditionBuilder->add('notification_to_user.userID IN (?)', array($recipientIDs));
113 $conditionBuilder->add('notification.eventHash = ?', array($event->getEventHash()));
114 $sql = "SELECT notification_to_user.userID
115 FROM wcf".WCF_N."_user_notification notification,
116 wcf".WCF_N."_user_notification_to_user notification_to_user
117 ".$conditionBuilder;
118 $statement = WCF::getDB()->prepareStatement($sql);
119 $statement->execute($conditionBuilder->getParameters());
120 while ($row = $statement->fetchArray()) $userIDs[] = $row['userID'];
121
122 // skip recipients with outstanding notifications
123 if (!empty($userIDs)) {
124 $recipientIDs = array_diff($recipientIDs, $userIDs);
125 if (empty($recipientIDs)) return;
126 }
127
128 // get recipients
129 $recipientList = new UserNotificationEventRecipientList();
130 $recipientList->getConditionBuilder()->add('event_to_user.eventID = ?', array($event->eventID));
131 $recipientList->getConditionBuilder()->add('event_to_user.userID IN (?)', array($recipientIDs));
132 $recipientList->readObjects();
133 if (count($recipientList)) {
134 // find existing notification
135 $notification = UserNotification::getNotification($objectTypeObject->packageID, $event->eventID, $notificationObject->getObjectID());
136 if ($notification !== null) {
137 // only update recipients
138 $action = new UserNotificationAction(array($notification), 'addRecipients', array(
139 'recipients' => $recipientList->getObjects()
140 ));
141 $action->executeAction();
142 }
143 else {
144 // create new notification
145 $action = new UserNotificationAction(array(), 'create', array(
146 'data' => array(
147 'packageID' => $objectTypeObject->packageID,
148 'eventID' => $event->eventID,
149 'objectID' => $notificationObject->getObjectID(),
150 'authorID' => ($event->getAuthorID() ?: null),
151 'time' => TIME_NOW,
152 'eventHash' => $event->getEventHash(),
153 'additionalData' => serialize($additionalData)
154 ),
155 'recipients' => $recipientList->getObjects()
156 ));
157 $result = $action->executeAction();
158 $notification = $result['returnValues'];
159 }
160
161 // sends notifications
162 foreach ($recipientList->getObjects() as $recipient) {
163 if ($recipient->mailNotificationType == 'instant') {
164 $this->sendInstantMailNotification($notification, $recipient, $event);
165 }
166 }
167
168 // reset notification count
169 UserStorageHandler::getInstance()->reset($recipientList->getObjectIDs(), 'userNotificationCount');
170 }
171 }
172
173 /**
174 * Returns the number of outstanding notifications for the active user.
175 *
176 * @return integer
177 */
178 public function getNotificationCount() {
179 if ($this->notificationCount === null) {
180 $this->notificationCount = 0;
181
182 if (WCF::getUser()->userID) {
183 // load storage data
184 UserStorageHandler::getInstance()->loadStorage(array(WCF::getUser()->userID));
185
186 // get ids
187 $data = UserStorageHandler::getInstance()->getStorage(array(WCF::getUser()->userID), 'userNotificationCount');
188
189 // cache does not exist or is outdated
190 if ($data[WCF::getUser()->userID] === null) {
191 $conditionBuilder = new PreparedStatementConditionBuilder();
192 $conditionBuilder->add('notification.notificationID = notification_to_user.notificationID');
193 $conditionBuilder->add('notification_to_user.userID = ?', array(WCF::getUser()->userID));
194
195 $sql = "SELECT COUNT(*) AS count
196 FROM wcf".WCF_N."_user_notification_to_user notification_to_user,
197 wcf".WCF_N."_user_notification notification
198 ".$conditionBuilder->__toString();
199 $statement = WCF::getDB()->prepareStatement($sql);
200 $statement->execute($conditionBuilder->getParameters());
201 $row = $statement->fetchArray();
202 $this->notificationCount = $row['count'];
203
204 // update storage data
205 UserStorageHandler::getInstance()->update(WCF::getUser()->userID, 'userNotificationCount', serialize($this->notificationCount));
206 }
207 else {
208 $this->notificationCount = unserialize($data[WCF::getUser()->userID]);
209 }
210 }
211 }
212
213 return $this->notificationCount;
214 }
215
216 /**
217 * Returns a limited list of outstanding notifications.
218 *
219 * @param integer $limit
220 * @param integer $offset
221 * @return array<array>
222 */
223 public function getNotifications($limit = 5, $offset = 0) {
224 // build enormous query
225 $conditions = new PreparedStatementConditionBuilder();
226 $conditions->add("notification_to_user.userID = ?", array(WCF::getUser()->userID));
227 $conditions->add("notification.notificationID = notification_to_user.notificationID");
228
229 $sql = "SELECT notification_to_user.notificationID, notification_event.eventID,
230 object_type.objectType, notification.objectID,
231 notification.additionalData, notification.authorID,
232 notification.time
233 FROM wcf".WCF_N."_user_notification_to_user notification_to_user,
234 wcf".WCF_N."_user_notification notification
235 LEFT JOIN wcf".WCF_N."_user_notification_event notification_event
236 ON (notification_event.eventID = notification.eventID)
237 LEFT JOIN wcf".WCF_N."_object_type object_type
238 ON (object_type.objectTypeID = notification_event.objectTypeID)
239 ".$conditions."
240 ORDER BY notification.time DESC";
241 $statement = WCF::getDB()->prepareStatement($sql, $limit, $offset);
242 $statement->execute($conditions->getParameters());
243
244 $authorIDs = $events = $objectTypes = $eventIDs = $notificationIDs = array();
245 while ($row = $statement->fetchArray()) {
246 $events[] = $row;
247
248 // cache object types
249 if (!isset($objectTypes[$row['objectType']])) {
250 $objectTypes[$row['objectType']] = array(
251 'objectType' => $this->availableObjectTypes[$row['objectType']],
252 'objectIDs' => array(),
253 'objects' => array()
254 );
255 }
256
257 $objectTypes[$row['objectType']]['objectIDs'][] = $row['objectID'];
258 $eventIDs[] = $row['eventID'];
259 $notificationIDs[] = $row['notificationID'];
260 $authorIDs[] = $row['authorID'];
261 }
262
263 // return an empty set if no notifications exist
264 if (empty($events)) {
265 return array(
266 'count' => 0,
267 'notifications' => array()
268 );
269 }
270
271 // load authors
272 $authors = UserProfile::getUserProfiles($authorIDs);
273 $unknownAuthor = new UserProfile(new User(null, array('userID' => null, 'username' => WCF::getLanguage()->get('wcf.user.guest'))));
274
275 // load objects associated with each object type
276 foreach ($objectTypes as $objectType => $objectData) {
277 $objectTypes[$objectType]['objects'] = $objectData['objectType']->getObjectsByIDs($objectData['objectIDs']);
278 }
279
280 // load required events
281 $eventList = new UserNotificationEventList();
282 $eventList->getConditionBuilder()->add("user_notification_event.eventID IN (?)", array($eventIDs));
283 $eventList->readObjects();
284 $eventObjects = $eventList->getObjects();
285
286 // load notification objects
287 $notificationList = new UserNotificationList();
288 $notificationList->getConditionBuilder()->add("user_notification.notificationID IN (?)", array($notificationIDs));
289 $notificationList->readObjects();
290 $notificationObjects = $notificationList->getObjects();
291
292 // build notification data
293 $notifications = array();
294 foreach ($events as $event) {
295 $className = $eventObjects[$event['eventID']]->className;
296 $class = new $className($eventObjects[$event['eventID']]);
297
298 $class->setObject(
299 $notificationObjects[$event['notificationID']],
300 $objectTypes[$event['objectType']]['objects'][$event['objectID']],
301 (isset($authors[$event['authorID']]) ? $authors[$event['authorID']] : $unknownAuthor),
302 unserialize($event['additionalData'])
303 );
304
305 $data = array(
306 'event' => $class,
307 'notificationID' => $event['notificationID'],
308 'time' => $event['time']
309 );
310
311 $notifications[] = $data;
312 }
313
314 return array(
315 'count' => count($notifications),
316 'notifications' => $notifications
317 );
318 }
319
320 /**
321 * Returns event object for given object type and event, returns NULL on failure.
322 *
323 * @param string $objectType
324 * @param string $eventName
325 * @return \wcf\system\user\notification\event\IUserNotificationEvent
326 */
327 public function getEvent($objectType, $eventName) {
328 if (!isset($this->availableEvents[$objectType][$eventName])) return null;
329
330 return $this->availableEvents[$objectType][$eventName];
331 }
332
333 /**
334 * Retrieves a notification id.
335 *
336 * @param integer $eventID
337 * @param integer $objectID
338 * @param integer $authorID
339 * @param integer $time
340 * @return integer
341 */
342 public function getNotificationID($eventID, $objectID, $authorID = null, $time = null) {
343 if ($authorID === null && $time === null) {
344 throw new SystemException("authorID and time cannot be omitted at once.");
345 }
346
347 $conditions = new PreparedStatementConditionBuilder();
348 $conditions->add("eventID = ?", array($eventID));
349 $conditions->add("objectID = ?", array($objectID));
350 if ($authorID !== null) $conditions->add("authorID = ?", array($authorID));
351 if ($time !== null) $conditions->add("time = ?", array($time));
352
353 $sql = "SELECT notificationID
354 FROM wcf".WCF_N."_user_notification
355 ".$conditions;
356 $statement = WCF::getDB()->prepareStatement($sql);
357 $statement->execute($conditions->getParameters());
358
359 $row = $statement->fetchArray();
360
361 return ($row === false) ? null : $row['notificationID'];
362 }
363
364 /**
365 * Returns a list of available object types.
366 *
367 * @return array<\wcf\system\user\notification\object\type\IUserNotificationObjectType>
368 */
369 public function getAvailableObjectTypes() {
370 return $this->availableObjectTypes;
371 }
372
373 /**
374 * Returns a list of available events.
375 *
376 * @return array<\wcf\system\user\notification\event\IUserNotificationEvent>
377 */
378 public function getAvailableEvents() {
379 return $this->availableEvents;
380 }
381
382 /**
383 * Returns object type id by name.
384 *
385 * @param string $objectType
386 * @return integer
387 */
388 public function getObjectTypeID($objectType) {
389 if (isset($this->objectTypes[$objectType])) {
390 return $this->objectTypes[$objectType]->objectTypeID;
391 }
392
393 return 0;
394 }
395
396 /**
397 * Returns object type by name.
398 *
399 * @param string $objectType
400 * @return object
401 */
402 public function getObjectTypeProcessor($objectType) {
403 if (isset($this->availableObjectTypes[$objectType])) {
404 return $this->availableObjectTypes[$objectType];
405 }
406
407 return null;
408 }
409
410 /**
411 * Sends the mail notification.
412 *
413 * @param \wcf\data\user\notification\UserNotification $notification
414 * @param \wcf\data\user\User $user
415 * @param \wcf\system\user\notification\event\IUserNotificationEvent $event
416 */
417 public function sendInstantMailNotification(UserNotification $notification, User $user, IUserNotificationEvent $event) {
418 // recipient's language
419 $event->setLanguage($user->getLanguage());
420
421 // add mail header
422 $message = $user->getLanguage()->getDynamicVariable('wcf.user.notification.mail.header', array(
423 'user' => $user
424 ))."\n\n";
425
426 // get message
427 $message .= $event->getEmailMessage();
428
429 // append notification mail footer
430 $token = $user->notificationMailToken;
431 if (!$token) {
432 // generate token if not present
433 $token = mb_substr(StringUtil::getHash(serialize(array($user->userID, StringUtil::getRandomID()))), 0, 20);
434 $editor = new UserEditor($user);
435 $editor->update(array('notificationMailToken' => $token));
436 }
437 $message .= "\n\n".$user->getLanguage()->getDynamicVariable('wcf.user.notification.mail.footer', array(
438 'user' => $user,
439 'token' => $token,
440 'notification' => $notification
441 ));
442
443 // build mail
444 $mail = new Mail(array($user->username => $user->email), $user->getLanguage()->getDynamicVariable('wcf.user.notification.mail.subject', array('title' => $event->getEmailTitle())), $message);
445 $mail->setLanguage($user->getLanguage());
446 $mail->send();
447 }
448
449 /**
450 * Deletes notifications.
451 *
452 * @param string $eventName
453 * @param string $objectType
454 * @param array<integer> $recipientIDs
455 * @param array<integer> $objectIDs
456 */
457 public function deleteNotifications($eventName, $objectType, array $recipientIDs, array $objectIDs = array()) {
458 // check given object type and event name
459 if (!isset($this->availableEvents[$objectType][$eventName])) {
460 throw new SystemException("Unknown event ".$objectType."-".$eventName." given");
461 }
462
463 // get objects
464 $objectTypeObject = $this->availableObjectTypes[$objectType];
465 $event = $this->availableEvents[$objectType][$eventName];
466
467 // delete notifications
468 $sql = "DELETE FROM wcf".WCF_N."_user_notification_to_user
469 WHERE notificationID IN (
470 SELECT notificationID
471 FROM wcf".WCF_N."_user_notification
472 WHERE packageID = ?
473 AND eventID = ?
474 ".(!empty($objectIDs) ? "AND objectID IN (?".(count($objectIDs) > 1 ? str_repeat(',?', count($objectIDs) - 1) : '').")" : '')."
475 )
476 ".(!empty($recipientIDs) ? ("AND userID IN (?".(count($recipientIDs) > 1 ? str_repeat(',?', count($recipientIDs) - 1) : '').")") : '');
477 $parameters = array($objectTypeObject->packageID, $event->eventID);
478 if (!empty($objectIDs)) $parameters = array_merge($parameters, $objectIDs);
479 if (!empty($recipientIDs)) $parameters = array_merge($parameters, $recipientIDs);
480 $statement = WCF::getDB()->prepareStatement($sql);
481 $statement->execute($parameters);
482
483 // reset storage
484 if (!empty($recipientIDs)) {
485 UserStorageHandler::getInstance()->reset($recipientIDs, 'userNotificationCount');
486 }
487 else {
488 UserStorageHandler::getInstance()->resetAll('userNotificationCount');
489 }
490 }
491
492 /**
493 * Returns the user's notification setting for the given event.
494 *
495 * @param string $objectType
496 * @param string $eventName
497 * @return mixed
498 */
499 public function getEventSetting($objectType, $eventName) {
500 // get event
501 $event = $this->getEvent($objectType, $eventName);
502
503 // get setting
504 $sql = "SELECT mailNotificationType
505 FROM wcf".WCF_N."_user_notification_event_to_user
506 WHERE eventID = ?
507 AND userID = ?";
508 $statement = WCF::getDB()->prepareStatement($sql);
509 $statement->execute(array($event->eventID, WCF::getUser()->userID));
510 $row = $statement->fetchArray();
511 if ($row === false) return false;
512 return $row['mailNotificationType'];
513 }
514 }