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