Merge branch '2.0'
[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 * Returns all events for given object type.
335 *
336 * @param string $objectType
337 * @return array<\wcf\system\user\notification\event\IUserNotificationEvent>
338 */
339 public function getEvents($objectType) {
340 if (!isset($this->availableEvents[$objectType])) return array();
341
342 return $this->availableEvents[$objectType];
343 }
344
345 /**
346 * Retrieves a notification id.
347 *
348 * @param integer $eventID
349 * @param integer $objectID
350 * @param integer $authorID
351 * @param integer $time
352 * @return integer
353 */
354 public function getNotificationID($eventID, $objectID, $authorID = null, $time = null) {
355 if ($authorID === null && $time === null) {
356 throw new SystemException("authorID and time cannot be omitted at once.");
357 }
358
359 $conditions = new PreparedStatementConditionBuilder();
360 $conditions->add("eventID = ?", array($eventID));
361 $conditions->add("objectID = ?", array($objectID));
362 if ($authorID !== null) $conditions->add("authorID = ?", array($authorID));
363 if ($time !== null) $conditions->add("time = ?", array($time));
364
365 $sql = "SELECT notificationID
366 FROM wcf".WCF_N."_user_notification
367 ".$conditions;
368 $statement = WCF::getDB()->prepareStatement($sql);
369 $statement->execute($conditions->getParameters());
370
371 $row = $statement->fetchArray();
372
373 return ($row === false) ? null : $row['notificationID'];
374 }
375
376 /**
377 * Returns a list of available object types.
378 *
379 * @return array<\wcf\system\user\notification\object\type\IUserNotificationObjectType>
380 */
381 public function getAvailableObjectTypes() {
382 return $this->availableObjectTypes;
383 }
384
385 /**
386 * Returns a list of available events.
387 *
388 * @return array<\wcf\system\user\notification\event\IUserNotificationEvent>
389 */
390 public function getAvailableEvents() {
391 return $this->availableEvents;
392 }
393
394 /**
395 * Returns object type id by name.
396 *
397 * @param string $objectType
398 * @return integer
399 */
400 public function getObjectTypeID($objectType) {
401 if (isset($this->objectTypes[$objectType])) {
402 return $this->objectTypes[$objectType]->objectTypeID;
403 }
404
405 return 0;
406 }
407
408 /**
409 * Returns object type by name.
410 *
411 * @param string $objectType
412 * @return object
413 */
414 public function getObjectTypeProcessor($objectType) {
415 if (isset($this->availableObjectTypes[$objectType])) {
416 return $this->availableObjectTypes[$objectType];
417 }
418
419 return null;
420 }
421
422 /**
423 * Sends the mail notification.
424 *
425 * @param \wcf\data\user\notification\UserNotification $notification
426 * @param \wcf\data\user\User $user
427 * @param \wcf\system\user\notification\event\IUserNotificationEvent $event
428 */
429 public function sendInstantMailNotification(UserNotification $notification, User $user, IUserNotificationEvent $event) {
430 // recipient's language
431 $event->setLanguage($user->getLanguage());
432
433 // add mail header
434 $message = $user->getLanguage()->getDynamicVariable('wcf.user.notification.mail.header', array(
435 'user' => $user
436 ))."\n\n";
437
438 // get message
439 $message .= $event->getEmailMessage();
440
441 // append notification mail footer
442 $token = $user->notificationMailToken;
443 if (!$token) {
444 // generate token if not present
445 $token = mb_substr(StringUtil::getHash(serialize(array($user->userID, StringUtil::getRandomID()))), 0, 20);
446 $editor = new UserEditor($user);
447 $editor->update(array('notificationMailToken' => $token));
448 }
449 $message .= "\n\n".$user->getLanguage()->getDynamicVariable('wcf.user.notification.mail.footer', array(
450 'user' => $user,
451 'token' => $token,
452 'notification' => $notification
453 ));
454
455 // build mail
456 $mail = new Mail(array($user->username => $user->email), $user->getLanguage()->getDynamicVariable('wcf.user.notification.mail.subject', array('title' => $event->getEmailTitle())), $message);
457 $mail->setLanguage($user->getLanguage());
458 $mail->send();
459 }
460
461 /**
462 * Deletes notifications.
463 *
464 * @param string $eventName
465 * @param string $objectType
466 * @param array<integer> $recipientIDs
467 * @param array<integer> $objectIDs
468 */
469 public function deleteNotifications($eventName, $objectType, array $recipientIDs, array $objectIDs = array()) {
470 // check given object type and event name
471 if (!isset($this->availableEvents[$objectType][$eventName])) {
472 throw new SystemException("Unknown event ".$objectType."-".$eventName." given");
473 }
474
475 // get objects
476 $objectTypeObject = $this->availableObjectTypes[$objectType];
477 $event = $this->availableEvents[$objectType][$eventName];
478
479 // delete notifications
480 $sql = "DELETE FROM wcf".WCF_N."_user_notification_to_user
481 WHERE notificationID IN (
482 SELECT notificationID
483 FROM wcf".WCF_N."_user_notification
484 WHERE packageID = ?
485 AND eventID = ?
486 ".(!empty($objectIDs) ? "AND objectID IN (?".(count($objectIDs) > 1 ? str_repeat(',?', count($objectIDs) - 1) : '').")" : '')."
487 )
488 ".(!empty($recipientIDs) ? ("AND userID IN (?".(count($recipientIDs) > 1 ? str_repeat(',?', count($recipientIDs) - 1) : '').")") : '');
489 $parameters = array($objectTypeObject->packageID, $event->eventID);
490 if (!empty($objectIDs)) $parameters = array_merge($parameters, $objectIDs);
491 if (!empty($recipientIDs)) $parameters = array_merge($parameters, $recipientIDs);
492 $statement = WCF::getDB()->prepareStatement($sql);
493 $statement->execute($parameters);
494
495 // reset storage
496 if (!empty($recipientIDs)) {
497 UserStorageHandler::getInstance()->reset($recipientIDs, 'userNotificationCount');
498 }
499 else {
500 UserStorageHandler::getInstance()->resetAll('userNotificationCount');
501 }
502 }
503
504 /**
505 * Returns the user's notification setting for the given event.
506 *
507 * @param string $objectType
508 * @param string $eventName
509 * @return mixed
510 */
511 public function getEventSetting($objectType, $eventName) {
512 // get event
513 $event = $this->getEvent($objectType, $eventName);
514
515 // get setting
516 $sql = "SELECT mailNotificationType
517 FROM wcf".WCF_N."_user_notification_event_to_user
518 WHERE eventID = ?
519 AND userID = ?";
520 $statement = WCF::getDB()->prepareStatement($sql);
521 $statement->execute(array($event->eventID, WCF::getUser()->userID));
522 $row = $statement->fetchArray();
523 if ($row === false) return false;
524 return $row['mailNotificationType'];
525 }
526 }