1<?php 2/** 3 * Notifications 4 * This file contains classes and functions which allow plugins to register and send notifications. 5 * 6 * There are notification methods which are provided out of the box 7 * (see notification_init() ). Each method is identified by a string, e.g. "email". 8 * 9 * To register an event use register_notification_handler() and pass the method name and a 10 * handler function. 11 * 12 * To send a notification call notify() passing it the method you wish to use combined with a 13 * number of method specific addressing parameters. 14 * 15 * Catch NotificationException to trap errors. 16 * 17 * Adding a New Notification Event 18 * =============================== 19 * 1. Register the event with elgg_register_notification_event() 20 * 21 * 2. Register for the notification message plugin hook: 22 * 'prepare', 'notification:[event name]'. The event name is of the form 23 * [action]:[type]:[subtype]. For example, the publish event for a blog 24 * would be named 'publish:object:blog'. 25 * 26 * The parameter array for the plugin hook has the keys 'event', 'method', 27 * 'recipient', and 'language'. The event is an \Elgg\Notifications\Event 28 * object and can provide access to the original object of the event through 29 * the method getObject() and the original actor through getActor(). 30 * 31 * The plugin hook callback modifies and returns a 32 * \Elgg\Notifications\Notification object that holds the message content. 33 * 34 * 35 * Adding a Delivery Method 36 * ========================= 37 * 1. Register the delivery method name with elgg_register_notification_method() 38 * 39 * 2. Register for the plugin hook for sending notifications: 40 * 'send', 'notification:[method name]'. It receives the notification object 41 * of the namespace Elgg\Notifications; 42 * 43 * class Notification in the params array with the 44 * key 'notification'. The callback should return a boolean to indicate whether 45 * the message was sent. 46 * 47 * 48 * Subscribing a User for Notifications 49 * ==================================== 50 * Users subscribe to receive notifications based on container and delivery method. 51 */ 52 53/** 54 * Register a notification event 55 * 56 * Elgg sends notifications for the items that have been registered with this 57 * function. For example, if you want notifications to be sent when a bookmark 58 * has been created or updated, call the function like this: 59 * 60 * elgg_register_notification_event('object', 'bookmarks', array('create', 'update')); 61 * 62 * @param string $object_type 'object', 'user', 'group', 'site' 63 * @param string $object_subtype The subtype or name of the entity 64 * @param array $actions Array of actions or empty array for the action event. 65 * An event is usually described by the first string passed 66 * to elgg_trigger_event(). Examples include 67 * 'create', 'update', and 'publish'. The default is 'create'. 68 * @return void 69 * @since 1.9 70 */ 71function elgg_register_notification_event($object_type, $object_subtype, array $actions = []) { 72 _elgg_services()->notifications->registerEvent($object_type, $object_subtype, $actions); 73} 74 75/** 76 * Unregister a notification event 77 * 78 * @param string $object_type 'object', 'user', 'group', 'site' 79 * @param string $object_subtype The type of the entity 80 * @param array $actions The notification action to unregister, leave empty for all actions 81 * Example ('create', 'delete', 'publish') 82 * 83 * @return bool 84 * @since 1.9 85 * @see elgg_register_notification_event() 86 */ 87function elgg_unregister_notification_event($object_type, $object_subtype, array $actions = []) { 88 return _elgg_services()->notifications->unregisterEvent($object_type, $object_subtype, $actions); 89} 90 91/** 92 * Register a delivery method for notifications 93 * 94 * Register for the 'send', 'notification:[method name]' plugin hook to handle 95 * sending a notification. A notification object is in the params array for the 96 * hook with the key 'notification'. See \Elgg\Notifications\Notification. 97 * 98 * @param string $name The notification method name 99 * @return void 100 * @see elgg_unregister_notification_method() 101 * @since 1.9 102 */ 103function elgg_register_notification_method($name) { 104 _elgg_services()->notifications->registerMethod($name); 105} 106 107/** 108 * Returns registered delivery methods for notifications 109 * <code> 110 * [ 111 * 'email' => 'email', 112 * 'sms' => 'sms', 113 * ] 114 * </code> 115 * 116 * @return array 117 * @since 2.3 118 */ 119function elgg_get_notification_methods() { 120 return _elgg_services()->notifications->getMethods(); 121} 122 123/** 124 * Unregister a delivery method for notifications 125 * 126 * @param string $name The notification method name 127 * @return bool 128 * @see elgg_register_notification_method() 129 * @since 1.9 130 */ 131function elgg_unregister_notification_method($name) { 132 return _elgg_services()->notifications->unregisterMethod($name); 133} 134 135/** 136 * Subscribe a user to notifications about a target entity 137 * 138 * @param int $user_guid The GUID of the user to subscribe to notifications 139 * @param string $method The delivery method of the notifications 140 * @param int $target_guid The entity to receive notifications about 141 * @return bool 142 * @since 1.9 143 */ 144function elgg_add_subscription($user_guid, $method, $target_guid) { 145 $methods = _elgg_services()->notifications->getMethods(); 146 $db = _elgg_services()->db; 147 $subs = new \Elgg\Notifications\SubscriptionsService($db, $methods); 148 return $subs->addSubscription($user_guid, $method, $target_guid); 149} 150 151/** 152 * Unsubscribe a user to notifications about a target entity 153 * 154 * @param int $user_guid The GUID of the user to unsubscribe to notifications 155 * @param string $method The delivery method of the notifications to stop 156 * @param int $target_guid The entity to stop receiving notifications about 157 * @return bool 158 * @since 1.9 159 */ 160function elgg_remove_subscription($user_guid, $method, $target_guid) { 161 $methods = _elgg_services()->notifications->getMethods(); 162 $db = _elgg_services()->db; 163 $subs = new \Elgg\Notifications\SubscriptionsService($db, $methods); 164 return $subs->removeSubscription($user_guid, $method, $target_guid); 165} 166 167/** 168 * Get the subscriptions for the content created inside this container. 169 * 170 * The return array is of the form: 171 * 172 * array( 173 * <user guid> => array('email', 'sms', 'ajax'), 174 * ); 175 * 176 * @param int $container_guid GUID of the entity acting as a container 177 * @return array User GUIDs (keys) and their subscription types (values). 178 * @since 1.9 179 * @todo deprecate once new subscriptions system has been added 180 */ 181function elgg_get_subscriptions_for_container($container_guid) { 182 $methods = _elgg_services()->notifications->getMethods(); 183 $db = _elgg_services()->db; 184 $subs = new \Elgg\Notifications\SubscriptionsService($db, $methods); 185 return $subs->getSubscriptionsForContainer($container_guid); 186} 187 188/** 189 * Queue a notification event for later handling 190 * 191 * Checks to see if this event has been registered for notifications. 192 * If so, it adds the event to a notification queue. 193 * 194 * This function triggers the 'enqueue', 'notification' hook. 195 * 196 * @param \Elgg\Event $event 'all', 'all' 197 * 198 * @return void 199 * @internal 200 * @since 1.9 201 */ 202function _elgg_enqueue_notification_event(\Elgg\Event $event) { 203 _elgg_services()->notifications->enqueueEvent($event->getName(), $event->getType(), $event->getObject()); 204} 205 206/** 207 * Process notification queue 208 * 209 * @return void 210 * 211 * @internal 212 */ 213function _elgg_notifications_cron() { 214 // calculate when we should stop 215 // @todo make configurable? 216 $stop_time = time() + 45; 217 _elgg_services()->notifications->processQueue($stop_time); 218} 219 220/** 221 * Send an email notification 222 * 223 * @param \Elgg\Hook $hook 'send', 'notification:email' 224 * 225 * @return bool 226 * @internal 227 */ 228function _elgg_send_email_notification(\Elgg\Hook $hook) { 229 230 if ($hook->getValue() === true) { 231 // assume someone else already sent the message 232 return; 233 } 234 235 $message = $hook->getParam('notification'); 236 if (!$message instanceof \Elgg\Notifications\Notification) { 237 return false; 238 } 239 240 $sender = $message->getSender(); 241 $recipient = $message->getRecipient(); 242 243 if (!$sender) { 244 return false; 245 } 246 247 if (!$recipient || !$recipient->email) { 248 return false; 249 } 250 251 $email = \Elgg\Email::factory([ 252 'from' => $sender, 253 'to' => $recipient, 254 'subject' => $message->subject, 255 'body' => $message->body, 256 'params' => $message->params, 257 ]); 258 259 return _elgg_services()->emails->send($email); 260} 261 262/** 263 * Adds default Message-ID header to all e-mails 264 * 265 * @param \Elgg\Hook $hook "prepare", "system:email" 266 * 267 * @see https://tools.ietf.org/html/rfc5322#section-3.6.4 268 * 269 * @return void|\Elgg\Email 270 * @internal 271 */ 272function _elgg_notifications_smtp_default_message_id_header(\Elgg\Hook $hook) { 273 $email = $hook->getValue(); 274 275 if (!$email instanceof \Elgg\Email) { 276 return; 277 } 278 279 $hostname = parse_url(elgg_get_site_url(), PHP_URL_HOST); 280 $url_path = parse_url(elgg_get_site_url(), PHP_URL_PATH); 281 282 $mt = microtime(true); 283 284 $email->addHeader('Message-ID', "{$url_path}.default.{$mt}@{$hostname}"); 285 286 return $email; 287} 288 289/** 290 * Adds default thread SMTP headers to group messages correctly. 291 * Note that it won't be sufficient for some email clients. Ie. Gmail is looking at message subject anyway. 292 * 293 * @param \Elgg\Hook $hook "prepare", "system:email" 294 * 295 * @return void|\Elgg\Email 296 * @internal 297 */ 298function _elgg_notifications_smtp_thread_headers(\Elgg\Hook $hook) { 299 $email = $hook->getValue(); 300 if (!$email instanceof \Elgg\Email) { 301 return; 302 } 303 304 $notificationParams = $email->getParams(); 305 306 $notification = elgg_extract('notification', $notificationParams); 307 if (!$notification instanceof \Elgg\Notifications\Notification) { 308 return; 309 } 310 311 $object = elgg_extract('object', $notification->params); 312 if (!$object instanceof \ElggEntity) { 313 return; 314 } 315 316 $event = elgg_extract('event', $notification->params); 317 if (!$event instanceof \Elgg\Notifications\NotificationEvent) { 318 return; 319 } 320 321 $hostname = parse_url(elgg_get_site_url(), PHP_URL_HOST); 322 $urlPath = parse_url(elgg_get_site_url(), PHP_URL_PATH); 323 324 if ($event->getAction() === 'create') { 325 // create event happens once per entity and we need to guarantee message id uniqueness 326 // and at the same time have thread message id that we don't need to store 327 $messageId = "{$urlPath}.entity.{$object->guid}@{$hostname}"; 328 } else { 329 $mt = microtime(true); 330 $messageId = "{$urlPath}.entity.{$object->guid}.$mt@{$hostname}"; 331 } 332 333 $email->addHeader("Message-ID", $messageId); 334 335 // let's just thread comments by default 336 $container = $object->getContainerEntity(); 337 if ($container instanceof \ElggEntity && $object instanceof \ElggComment) { 338 $threadMessageId = "<{$urlPath}.entity.{$container->guid}@{$hostname}>"; 339 $email->addHeader('In-Reply-To', $threadMessageId); 340 $email->addHeader('References', $threadMessageId); 341 } 342 343 return $email; 344} 345 346/** 347 * Notification init 348 * 349 * @return void 350 * 351 * @internal 352 */ 353function _elgg_notifications_init() { 354 elgg_register_plugin_hook_handler('cron', 'minute', '_elgg_notifications_cron', 100); 355 elgg_register_event_handler('all', 'all', '_elgg_enqueue_notification_event', 700); 356 357 // add email notifications 358 elgg_register_notification_method('email'); 359 elgg_register_plugin_hook_handler('send', 'notification:email', '_elgg_send_email_notification'); 360 elgg_register_plugin_hook_handler('prepare', 'system:email', '_elgg_notifications_smtp_default_message_id_header', 1); 361 elgg_register_plugin_hook_handler('prepare', 'system:email', '_elgg_notifications_smtp_thread_headers'); 362 363 // add ability to set personal notification method 364 elgg_extend_view('forms/usersettings/save', 'core/settings/account/notifications'); 365 elgg_register_plugin_hook_handler('usersettings:save', 'user', '_elgg_save_notification_user_settings'); 366} 367 368/** 369 * Notify a user via their preferences. 370 * 371 * @param mixed $to Either a guid or an array of guid's to notify. 372 * @param int $from GUID of the sender, which may be a user, site or object. 373 * @param string $subject Message subject. 374 * @param string $message Message body. 375 * @param array $params Misc additional parameters specific to various methods. 376 * @param mixed $methods_override A string, or an array of strings specifying the delivery 377 * methods to use - or leave blank for delivery using the 378 * user's chosen delivery methods. 379 * 380 * @return array Compound array of each delivery user/delivery method's success or failure. 381 * @internal 382 */ 383function _elgg_notify_user($to, $from, $subject, $message, array $params = null, $methods_override = "") { 384 385 $notify_service = _elgg_services()->notifications; 386 387 // Sanitise 388 if (!is_array($to)) { 389 $to = [(int) $to]; 390 } 391 $from = (int) $from; 392 //$subject = sanitise_string($subject); 393 // Get notification methods 394 if (($methods_override) && (!is_array($methods_override))) { 395 $methods_override = [$methods_override]; 396 } 397 398 $result = []; 399 400 foreach ($to as $guid) { 401 // Results for a user are... 402 $result[$guid] = []; 403 404 $recipient = get_entity($guid); 405 if (empty($recipient)) { 406 continue; 407 } 408 409 // Are we overriding delivery? 410 $methods = $methods_override; 411 if (empty($methods)) { 412 $methods = []; 413 414 if (!($recipient instanceof ElggUser)) { 415 // not sending to a user so can't get user notification settings 416 continue; 417 } 418 419 $tmp = $recipient->getNotificationSettings(); 420 if (empty($tmp)) { 421 // user has no notification settings 422 continue; 423 } 424 425 foreach ($tmp as $k => $v) { 426 // Add method if method is turned on for user! 427 if ($v) { 428 $methods[] = $k; 429 } 430 } 431 } 432 433 if (empty($methods)) { 434 continue; 435 } 436 437 // Deliver 438 foreach ($methods as $method) { 439 $handler = $notify_service->getDeprecatedHandler($method); 440 /* @var callable $handler */ 441 if (!$handler || !is_callable($handler)) { 442 elgg_log("No handler registered for the method $method", 'INFO'); 443 continue; 444 } 445 446 elgg_log("Sending message to $guid using $method"); 447 448 // Trigger handler and retrieve result. 449 try { 450 $result[$guid][$method] = call_user_func( 451 $handler, 452 $from ? get_entity($from) : null, 453 get_entity($guid), 454 $subject, 455 $message, 456 $params 457 ); 458 } catch (Exception $e) { 459 elgg_log($e, 'ERROR'); 460 } 461 } 462 } 463 464 return $result; 465} 466 467/** 468 * Notify a user via their preferences. 469 * 470 * @param mixed $to Either a guid or an array of guid's to notify. 471 * @param int $from GUID of the sender, which may be a user, site or object. 472 * @param string $subject Message subject. 473 * @param string $message Message body. 474 * @param array $params Misc additional parameters specific to various methods. 475 * 476 * By default Elgg core supports three parameters, which give 477 * notification plugins more control over the notifications: 478 * 479 * object => null|\ElggEntity|\ElggAnnotation The object that 480 * is triggering the notification. 481 * 482 * action => null|string Word that describes the action that 483 * is triggering the notification (e.g. "create" 484 * or "update"). 485 * 486 * summary => null|string Summary that notification plugins 487 * can use alongside the notification title and body. 488 * 489 * @param mixed $methods_override A string, or an array of strings specifying the delivery 490 * methods to use - or leave blank for delivery using the 491 * user's chosen delivery methods. 492 * 493 * @return array Compound array of each delivery user/delivery method's success or failure. 494 * @throws NotificationException 495 */ 496function notify_user($to, $from = 0, $subject = '', $message = '', array $params = [], $methods_override = null) { 497 498 $params['subject'] = $subject; 499 $params['body'] = $message; 500 $params['methods_override'] = $methods_override; 501 502 if ($from) { 503 $sender = get_entity($from); 504 } else { 505 $sender = elgg_get_site_entity(); 506 } 507 if (!$sender) { 508 return []; 509 } 510 511 $recipients = []; 512 $to = (array) $to; 513 foreach ($to as $guid) { 514 $recipient = get_entity($guid); 515 if (!$recipient) { 516 continue; 517 } 518 $recipients[] = $recipient; 519 } 520 521 return _elgg_services()->notifications->sendInstantNotifications($sender, $recipients, $params); 522} 523 524/** 525 * Send an email to any email address 526 * 527 * @param \Elgg\Email $email Email 528 * @return bool 529 * @since 1.7.2 530 */ 531function elgg_send_email($email) { 532 533 if (!$email instanceof \Elgg\Email) { 534 elgg_deprecated_notice(__FUNCTION__ . ' 535 should be given a single instance of \Elgg\Email 536 ', '3.0'); 537 538 $args = func_get_args(); 539 $email = \Elgg\Email::factory([ 540 'from' => array_shift($args), 541 'to' => array_shift($args), 542 'subject' => array_shift($args), 543 'body' => array_shift($args), 544 'params' => array_shift($args) ? : [], 545 ]); 546 } 547 548 return _elgg_services()->emails->send($email); 549} 550 551/** 552 * Replace default email transport 553 * 554 * @note If you are replacing the transport persistently, e.g. on each page request via 555 * a plugin, avoid using plugin settings to store transport configuration, as it 556 * may be expensive to fetch these settings. Instead, configure the transport 557 * via elgg-config/settings.php or use site config DB storage. 558 * 559 * @param \Zend\Mail\Transport\TransportInterface $mailer Transport 560 * @return void 561 */ 562function elgg_set_email_transport(\Zend\Mail\Transport\TransportInterface $mailer) { 563 _elgg_services()->setValue('mailer', $mailer); 564} 565 566/** 567 * Save personal notification settings - input comes from request 568 * 569 * @param \Elgg\Hook $hook 'usersettings:save', 'user' 570 * 571 * @return void 572 * @internal 573 */ 574function _elgg_save_notification_user_settings(\Elgg\Hook $hook) { 575 576 $user = $hook->getUserParam(); 577 $request = $hook->getParam('request'); 578 579 if (!$user instanceof ElggUser || !$request instanceof \Elgg\Request) { 580 return; 581 } 582 583 $method = $request->getParam('method'); 584 585 $current_settings = $user->getNotificationSettings(); 586 587 $result = false; 588 foreach ($method as $key => $value) { 589 // check if setting has changed and skip if not 590 if ($current_settings[$key] === ($value === 'yes')) { 591 continue; 592 } 593 594 $result = $user->setNotificationSetting($key, ($value === 'yes')); 595 if (!$result) { 596 $request->validation()->fail('notification_method', '', elgg_echo('notifications:usersettings:save:fail')); 597 } 598 } 599 600 if ($result) { 601 $request->validation()->pass('notification_method', '', elgg_echo('notifications:usersettings:save:ok')); 602 } 603} 604 605/** 606 * @see \Elgg\Application::loadCore Do not do work here. Just register for events. 607 */ 608return function(\Elgg\EventsService $events) { 609 $events->registerHandler('init', 'system', '_elgg_notifications_init'); 610}; 611