<?php
/**
 * Notifications
 * This file contains classes and functions which allow plugins to register and send notifications.
 *
 * There are notification methods which are provided out of the box
 * (see notification_init() ). Each method is identified by a string, e.g. "email".
 *
 * To register an event use register_notification_handler() and pass the method name and a
 * handler function.
 *
 * To send a notification call notify() passing it the method you wish to use combined with a
 * number of method specific addressing parameters.
 *
 * Catch NotificationException to trap errors.
 *
 * @package Elgg.Core
 * @subpackage Notifications
 */

/** Notification handlers */
global $NOTIFICATION_HANDLERS;
$NOTIFICATION_HANDLERS = array();

/**
 * This function registers a handler for a given notification type (eg "email")
 *
 * @param string $method  The method
 * @param string $handler The handler function, in the format
 *                        "handler(ElggEntity $from, ElggUser $to, $subject,
 *                        $message, array $params = NULL)". This function should
 *                        return false on failure, and true/a tracking message ID on success.
 * @param array  $params  An associated array of other parameters for this handler
 *                        defining some properties eg. supported msg length or rich text support.
 *
 * @return bool
 */
function register_notification_handler($method, $handler, $params = NULL) {
	global $NOTIFICATION_HANDLERS;

	if (is_callable($handler, true)) {
		$NOTIFICATION_HANDLERS[$method] = new stdClass;

		$NOTIFICATION_HANDLERS[$method]->handler = $handler;
		if ($params) {
			foreach ($params as $k => $v) {
				$NOTIFICATION_HANDLERS[$method]->$k = $v;
			}
		}

		return true;
	}

	return false;
}

/**
 * This function unregisters a handler for a given notification type (eg "email")
 *
 * @param string $method The method
 *
 * @return void
 * @since 1.7.1
 */
function unregister_notification_handler($method) {
	global $NOTIFICATION_HANDLERS;

	if (isset($NOTIFICATION_HANDLERS[$method])) {
		unset($NOTIFICATION_HANDLERS[$method]);
	}
}

/**
 * Notify a user via their preferences.
 *
 * @param mixed  $to               Either a guid or an array of guid's to notify.
 * @param int    $from             GUID of the sender, which may be a user, site or object.
 * @param string $subject          Message subject.
 * @param string $message          Message body.
 * @param array  $params           Misc additional parameters specific to various methods.
 * @param mixed  $methods_override A string, or an array of strings specifying the delivery
 *                                 methods to use - or leave blank for delivery using the
 *                                 user's chosen delivery methods.
 *
 * @return array Compound array of each delivery user/delivery method's success or failure.
 * @throws NotificationException
 */
function notify_user($to, $from, $subject, $message, array $params = NULL, $methods_override = "") {
	global $NOTIFICATION_HANDLERS;

	// Sanitise
	if (!is_array($to)) {
		$to = array((int)$to);
	}
	$from = (int)$from;
	//$subject = sanitise_string($subject);

	// Get notification methods
	if (($methods_override) && (!is_array($methods_override))) {
		$methods_override = array($methods_override);
	}

	$result = array();

	foreach ($to as $guid) {
		// Results for a user are...
		$result[$guid] = array();

		if ($guid) { // Is the guid > 0?
			// Are we overriding delivery?
			$methods = $methods_override;
			if (!$methods) {
				$tmp = get_user_notification_settings($guid);
				$methods = array();
				// $tmp may be false. don't cast
				if (is_object($tmp)) {
					foreach ($tmp as $k => $v) {
						// Add method if method is turned on for user!
						if ($v) {
							$methods[] = $k;
						}
					}
				}
			}

			if ($methods) {
				// Deliver
				foreach ($methods as $method) {

					if (!isset($NOTIFICATION_HANDLERS[$method])) {
						continue;
					}

					// Extract method details from list
					$details = $NOTIFICATION_HANDLERS[$method];
					$handler = $details->handler;
					/* @var callable $handler */

					if ((!$NOTIFICATION_HANDLERS[$method]) || (!$handler) || (!is_callable($handler))) {
						error_log(elgg_echo('NotificationException:NoHandlerFound', array($method)));
					}

					elgg_log("Sending message to $guid using $method");

					// Trigger handler and retrieve result.
					try {
						$result[$guid][$method] = call_user_func($handler,
							$from ? get_entity($from) : NULL, 	// From entity
							get_entity($guid), 					// To entity
							$subject,							// The subject
							$message, 			// Message
							$params								// Params
						);
					} catch (Exception $e) {
						error_log($e->getMessage());
					}

				}
			}
		}
	}

	return $result;
}

/**
 * Get the notification settings for a given user.
 *
 * @param int $user_guid The user id
 *
 * @return stdClass|false
 */
function get_user_notification_settings($user_guid = 0) {
	$user_guid = (int)$user_guid;

	if ($user_guid == 0) {
		$user_guid = elgg_get_logged_in_user_guid();
	}

	// @todo: there should be a better way now that metadata is cached. E.g. just query for MD names, then
	// query user object directly
	$all_metadata = elgg_get_metadata(array(
		'guid' => $user_guid,
		'limit' => 0
	));
	if ($all_metadata) {
		$prefix = "notification:method:";
		$return = new stdClass;

		foreach ($all_metadata as $meta) {
			$name = substr($meta->name, strlen($prefix));
			$value = $meta->value;

			if (strpos($meta->name, $prefix) === 0) {
				$return->$name = $value;
			}
		}

		return $return;
	}

	return false;
}

/**
 * Set a user notification pref.
 *
 * @param int    $user_guid The user id.
 * @param string $method    The delivery method (eg. email)
 * @param bool   $value     On(true) or off(false).
 *
 * @return bool
 */
function set_user_notification_setting($user_guid, $method, $value) {
	$user_guid = (int)$user_guid;
	$method = sanitise_string($method);

	$user = get_entity($user_guid);
	if (!$user) {
		$user = elgg_get_logged_in_user_entity();
	}

	if (($user) && ($user instanceof ElggUser)) {
		$prefix = "notification:method:$method";
		$user->$prefix = $value;
		$user->save();

		return true;
	}

	return false;
}

/**
 * Send a notification via email.
 *
 * @param ElggEntity $from    The from user/site/object
 * @param ElggUser   $to      To which user?
 * @param string     $subject The subject of the message.
 * @param string     $message The message body
 * @param array      $params  Optional parameters (none taken in this instance)
 *
 * @return bool
 * @throws NotificationException
 * @access private
 */
function email_notify_handler(ElggEntity $from, ElggUser $to, $subject, $message,
array $params = NULL) {

	global $CONFIG;

	if (!$from) {
		$msg = elgg_echo('NotificationException:MissingParameter', array('from'));
		throw new NotificationException($msg);
	}

	if (!$to) {
		$msg = elgg_echo('NotificationException:MissingParameter', array('to'));
		throw new NotificationException($msg);
	}

	if ($to->email == "") {
		$msg = elgg_echo('NotificationException:NoEmailAddress', array($to->guid));
		throw new NotificationException($msg);
	}

	// To
	$to = $to->email;

	// From
	$site = elgg_get_site_entity();
	// If there's an email address, use it - but only if its not from a user.
	if (!($from instanceof ElggUser) && $from->email) {
		$from = $from->email;
	} else if ($site && $site->email) {
		// Use email address of current site if we cannot use sender's email
		$from = $site->email;
	} else {
		// If all else fails, use the domain of the site.
		$from = 'noreply@' . get_site_domain($CONFIG->site_guid);
	}

	return elgg_send_email($from, $to, $subject, $message);
}

/**
 * Send an email to any email address
 *
 * @param string $from    Email address or string: "name <email>"
 * @param string $to      Email address or string: "name <email>"
 * @param string $subject The subject of the message
 * @param string $body    The message body
 * @param array  $params  Optional parameters (none used in this function)
 *
 * @return bool
 * @throws NotificationException
 * @since 1.7.2
 */
function elgg_send_email($from, $to, $subject, $body, array $params = NULL) {
	global $CONFIG;

	if (!$from) {
		$msg = elgg_echo('NotificationException:MissingParameter', array('from'));
		throw new NotificationException($msg);
	}

	if (!$to) {
		$msg = elgg_echo('NotificationException:MissingParameter', array('to'));
		throw new NotificationException($msg);
	}

	// return TRUE/FALSE to stop elgg_send_email() from sending
	$mail_params = array(
							'to' => $to,
							'from' => $from,
							'subject' => $subject,
							'body' => $body,
							'params' => $params
					);

	$result = elgg_trigger_plugin_hook('email', 'system', $mail_params, NULL);
	if ($result !== NULL) {
		return $result;
	}

	$header_eol = "\r\n";
	if (isset($CONFIG->broken_mta) && $CONFIG->broken_mta) {
		// Allow non-RFC 2822 mail headers to support some broken MTAs
		$header_eol = "\n";
	}

	// Windows is somewhat broken, so we use just address for to and from
	if (strtolower(substr(PHP_OS, 0, 3)) == 'win') {
		// strip name from to and from
		if (strpos($to, '<')) {
			preg_match('/<(.*)>/', $to, $matches);
			$to = $matches[1];
		}
		if (strpos($from, '<')) {
			preg_match('/<(.*)>/', $from, $matches);
			$from = $matches[1];
		}
	}

	$headers = "From: $from{$header_eol}"
		. "Content-Type: text/plain; charset=UTF-8; format=flowed{$header_eol}"
		. "MIME-Version: 1.0{$header_eol}"
		. "Content-Transfer-Encoding: 8bit{$header_eol}";


	// Sanitise subject by stripping line endings
	$subject = preg_replace("/(\r\n|\r|\n)/", " ", $subject);
	// this is because Elgg encodes everything and matches what is done with body
	$subject = html_entity_decode($subject, ENT_COMPAT, 'UTF-8'); // Decode any html entities
	if (is_callable('mb_encode_mimeheader')) {
		$subject = mb_encode_mimeheader($subject, "UTF-8", "B");
	}

	// Format message
	$body = html_entity_decode($body, ENT_COMPAT, 'UTF-8'); // Decode any html entities
	$body = elgg_strip_tags($body); // Strip tags from message
	$body = preg_replace("/(\r\n|\r)/", "\n", $body); // Convert to unix line endings in body
	$body = preg_replace("/^From/", ">From", $body); // Change lines starting with From to >From

	return mail($to, $subject, wordwrap($body), $headers);
}

/**
 * Correctly initialise notifications and register the email handler.
 *
 * @return void
 * @access private
 */
function notification_init() {
	// Register a notification handler for the default email method
	register_notification_handler("email", "email_notify_handler");

	// Add settings view to user settings & register action
	elgg_extend_view('forms/account/settings', 'core/settings/account/notifications');

	elgg_register_plugin_hook_handler('usersettings:save', 'user', 'notification_user_settings_save');
}

/**
 * Includes the action to save user notifications
 *
 * @return void
 * @todo why can't this call action(...)?
 * @access private
 */
function notification_user_settings_save() {
	global $CONFIG;
	//@todo Wha??
	include($CONFIG->path . "actions/notifications/settings/usersettings/save.php");
}

/**
 * Register an entity type and subtype to be eligible for notifications
 *
 * @param string $entity_type    The type of entity
 * @param string $object_subtype Its subtype
 * @param string $language_name  Its localized notification string (eg "New blog post")
 *
 * @return void
 */
function register_notification_object($entity_type, $object_subtype, $language_name) {
	global $CONFIG;

	if ($entity_type == '') {
		$entity_type = '__BLANK__';
	}
	if ($object_subtype == '') {
		$object_subtype = '__BLANK__';
	}

	if (!isset($CONFIG->register_objects)) {
		$CONFIG->register_objects = array();
	}

	if (!isset($CONFIG->register_objects[$entity_type])) {
		$CONFIG->register_objects[$entity_type] = array();
	}

	$CONFIG->register_objects[$entity_type][$object_subtype] = $language_name;
}

/**
 * Establish a 'notify' relationship between the user and a content author
 *
 * @param int $user_guid   The GUID of the user who wants to follow a user's content
 * @param int $author_guid The GUID of the user whose content the user wants to follow
 *
 * @return bool Depending on success
 */
function register_notification_interest($user_guid, $author_guid) {
	return add_entity_relationship($user_guid, 'notify', $author_guid);
}

/**
 * Remove a 'notify' relationship between the user and a content author
 *
 * @param int $user_guid   The GUID of the user who is following a user's content
 * @param int $author_guid The GUID of the user whose content the user wants to unfollow
 *
 * @return bool Depending on success
 */
function remove_notification_interest($user_guid, $author_guid) {
	return remove_entity_relationship($user_guid, 'notify', $author_guid);
}

/**
 * Automatically triggered notification on 'create' events that looks at registered
 * objects and attempts to send notifications to anybody who's interested
 *
 * @see register_notification_object
 *
 * @param string $event       create
 * @param string $object_type mixed
 * @param mixed  $object      The object created
 *
 * @return bool
 * @access private
 */
function object_notifications($event, $object_type, $object) {
	// We only want to trigger notification events for ElggEntities
	if ($object instanceof ElggEntity) {
		/* @var ElggEntity $object */

		// Get config data
		global $CONFIG, $SESSION, $NOTIFICATION_HANDLERS;

		$hookresult = elgg_trigger_plugin_hook('object:notifications', $object_type, array(
			'event' => $event,
			'object_type' => $object_type,
			'object' => $object,
		), false);
		if ($hookresult === true) {
			return true;
		}

		// Have we registered notifications for this type of entity?
		$object_type = $object->getType();
		if (empty($object_type)) {
			$object_type = '__BLANK__';
		}

		$object_subtype = $object->getSubtype();
		if (empty($object_subtype)) {
			$object_subtype = '__BLANK__';
		}

		if (isset($CONFIG->register_objects[$object_type][$object_subtype])) {
			$subject = $CONFIG->register_objects[$object_type][$object_subtype];
			$string = $subject . ": " . $object->getURL();

			// Get users interested in content from this person and notify them
			// (Person defined by container_guid so we can also subscribe to groups if we want)
			foreach ($NOTIFICATION_HANDLERS as $method => $foo) {
				$interested_users = elgg_get_entities_from_relationship(array(
					'site_guids' => ELGG_ENTITIES_ANY_VALUE,
					'relationship' => 'notify' . $method,
					'relationship_guid' => $object->container_guid,
					'inverse_relationship' => TRUE,
					'type' => 'user',
					'limit' => false
				));
				/* @var ElggUser[] $interested_users */

				if ($interested_users && is_array($interested_users)) {
					foreach ($interested_users as $user) {
						if ($user instanceof ElggUser && !$user->isBanned()) {
							if (($user->guid != $SESSION['user']->guid) && has_access_to_entity($object, $user)
							&& $object->access_id != ACCESS_PRIVATE) {
								$body = elgg_trigger_plugin_hook('notify:entity:message', $object->getType(), array(
									'entity' => $object,
									'to_entity' => $user,
									'method' => $method), $string);
								if (empty($body) && $body !== false) {
									$body = $string;
								}
								if ($body !== false) {
									notify_user($user->guid, $object->container_guid, $subject, $body,
										null, array($method));
								}
							}
						}
					}
				}
			}
		}
	}
}

// Register a startup event
elgg_register_event_handler('init', 'system', 'notification_init', 0);
elgg_register_event_handler('create', 'object', 'object_notifications');
