<?php
// $Id: notifications.module,v 1.6.2.9.2.50.2.18.2.27 2010/04/09 18:48:34 jareyero Exp $

/**
 * @file
 * Notifications module
 *
 * This is the base module of the notifications framework. It handles event processing, queueing,
 * message composition and sending.
 * 
 * Different subscriptions types are provided by plug-in modules implementing hook_notifications()
 * Most of the UI is implemented in notifications_ui module
 * The messaging framework is used for message delivery
 * Token module is used for token replacement in messages 
 * 
 * This is based on the previous subscriptions module
 * 
 * Development Seed, http://www.developmentseed.org, 2007 
 *
 */

// Define some values for subscription status
// Blocked subscriptions, for blocked users
define('NOTIFICATIONS_SUBSCRIPTION_BLOCKED', 0);
// Enabled ones, will produce notifications
define('NOTIFICATIONS_SUBSCRIPTION_ACTIVE', 1);
// Temporarily disabled ones, maybe user on holidays
define('NOTIFICATIONS_SUBSCRIPTION_INACTIVE', 2);

/**
 * Implementation of hook_autoload_info().
 */
function notifications_autoload_info() {
  return array(
    'Notifications_Event' => array('file' => 'includes/notifications_event.class.inc'),
    'Notifications_Subscription' => array('file' => 'includes/notifications_subscription.class.inc'),
  ); 
}
/**
 * Implementation of hook_menu().
 */
function notifications_menu() {
  // Administration. This one will override messaging menu item
  $items['admin/messaging'] = array(
    'title' => 'Messaging & Notifications',
    'access arguments' => array('administer notifications'),
    'description' => 'Administer and configure messaging and notifications',
    'page callback' => 'system_admin_menu_block_page',
    'file' => 'system.admin.inc',
    'file path' => drupal_get_path('module', 'system'),
  );
  // Site settings
  $items['admin/messaging/notifications'] = array(
    'title' => 'Notifications Settings',
    'description' => 'Site settings for user notifications.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notifications_settings_form'),
    'access arguments' => array('administer site configuration'),
    'file' => 'notifications.admin.inc',
  );
  $items['admin/messaging/notifications/settings'] = array(
    'title' => 'General',
    'weight' => -10,
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'file' => 'notifications.admin.inc',
  );
  $items['admin/messaging/notifications/intervals'] = array(
    'title' => 'Intervals',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notifications_send_intervals_form'),
    'type' => MENU_LOCAL_TASK,
    'access arguments' => array('administer site configuration'),
    'file' => 'notifications.admin.inc',
  );
  $items['admin/messaging/notifications/events'] = array(
    'title' => 'Events',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notifications_admin_events_form'),
    'type' => MENU_LOCAL_TASK,
    'access arguments' => array('administer site configuration'),
    'file' => 'notifications.admin.inc',
  );
  // Subscribe links. For this items access will be checked later in the page
  $items['notifications/subscribe/%user'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => 'notifications_page_subscribe',
    'page arguments' => array(2),
    'access callback' => 'notifications_access_subscribe',
    'access arguments' => array(2),
    'file' => 'notifications.pages.inc',
  );
  // Unsubscribe links This page will need to work with anonymous users
  $items['notifications/unsubscribe'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => 'notifications_page_unsubscribe',
    'page arguments' => array(2, 3),
    'access callback' => TRUE,
    'file' => 'notifications.pages.inc',
  );
  // Edit subscription, stand alone page
  $items['notifications/subscription/%notifications_subscription'] = array(
    'type' => MENU_CALLBACK,
    'title' => 'Edit subscription',
    'page callback' => 'notifications_subscription_edit_page',
    'page arguments' => array(2),
    'access callback' => 'notifications_subscription_access',
    'access arguments' => array('edit', 2),
    'file' => 'notifications.pages.inc',
  );
  // Manage destinations
  $items['notifications/destination/%messaging_destination/edit'] = array(
    'type' => MENU_CALLBACK,
    'title' => 'Edit destination',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notifications_edit_destination_form', 2),
    'access callback' => 'notifications_destination_access',
    'access arguments' => array('edit', 2),
    'file' => 'notifications.manage.inc',
  );
  $items['notifications/destination/%messaging_destination/manage'] = array(
    'type' => MENU_CALLBACK,
    'title' => 'Manage destination',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notifications_manage_destination_form', 2),
    'access callback' => 'notifications_destination_access',
    'access arguments' => array('manage', 2),
    'file' => 'notifications.manage.inc',
  );
  // User account tabs
  $items['user/%user/notifications'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Notifications',
    //'page callback' => 'notifications_page_user_overview',
    //'page arguments' => array(1),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notifications_user_overview', 1),
    'access callback' => 'notifications_access_user',
    'access arguments' => array(1),
    'file' => 'notifications.pages.inc',
  );
  $items['user/%user/notifications/overview'] = array(
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'title' => 'Overview',
    'weight' => -10,
  );  
  $items['user/%user/notifications/subscriptions'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Subscriptions',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notifications_manage_user_subscriptions', 1),
    'access callback' => 'notifications_access_user',
    'access arguments' => array(1, 'manage'),
    'file' => 'notifications.manage.inc',
  );
  // Edit subscription under subscriptions tab
  $items['user/%user/notifications/subscriptions/edit/%notifications_subscription'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Edit subscription',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notifications_subscription_form', 5),
    'access callback' => 'notifications_subscription_access',
    'access arguments' => array('edit', 5),
    'file' => 'notifications.pages.inc',
  );
  // Delete subscription under subscriptions tab
  $items['user/%user/notifications/subscriptions/delete/%notifications_subscription'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Delete subscription',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notifications_form_unsubscribe_confirm', 5),
    'access callback' => 'notifications_subscription_access',
    'access arguments' => array('unsubscribe', 5),
    'file' => 'notifications.pages.inc',
  );
  $items['user/%user/notifications/update/%'] = array(
    'type' => MENU_CALLBACK,
    'title' => 'Update subscriptions',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notifications_update_user_subscriptions', 1, 4),
    'access callback' => 'notifications_access_user',
    'access arguments' => array(1, 'maintain'),
    'file' => 'notifications.pages.inc',
  );  
  // Some autocomplete callbacks
  $items['notifications/autocomplete/node/title'] = array(
    'title' => 'Node title autocomplete',
    'page callback' => 'notifications_node_autocomplete_title',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
    'file' => 'includes/node.inc',
  );
  // Some autocomplete callbacks
  $items['notifications/autocomplete/node/type'] = array(
    'title' => 'Node title autocomplete',
    'page callback' => 'notifications_node_autocomplete_type',
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
    'file' => 'includes/node.inc',
  );
  return $items;
}

/**
 * Menu access callback for user subscriptions
 * 
 * @param $account
 *   User account to which these subscriptions below
 * @param $op
 *   - maintain = create / delete
 *   - manage = use the per account administration page
 */
function notifications_access_user($account, $op = 'maintain') {
  global $user;
  
  if (user_access('administer notifications') || user_access('manage all subscriptions')) {
    return TRUE;
  }
  else {
    return $account->uid && $user->uid == $account->uid &&
      (($op == 'maintain' && user_access('maintain own subscriptions')) || ($op == 'manage' && user_access('manage own subscriptions')));
  }
}

/**
 * Menu access callback, add a given subscription type
 */
function notifications_access_user_add($account = NULL, $type = NULL) {
  global $user;
  
  $account = $account ? $account : $user;
  
  if (notifications_access_user($account)) {
    if ($type && ($access = notifications_subscription_types($type, 'access'))) {
      return user_access($access, $account);
    }
    else {
      return TRUE;
    }
  }
}

/**
 * Check signature
 * 
 * @param $params
 *   Optional array of path elements. If not specified the current URL will be used
 * @param $prefix
 *   When using the path this must match the first part of it, which is not used in the signature
 */
function notifications_check_signature($params = NULL, $prefix = 'notifications') {
  // If not parameters passed, get them from path, discard the first one that must match $prefix
  if (!$args && arg(0) == $prefix) {
    $params = arg();
    array_shift($params);
  }
  return $params && !empty($_GET['signature']) && $_GET['signature'] == _notifications_signature($params, !empty($_GET['confirm']));  
}

/**
 * Menu access callback for subscribe links
 * 
 * More access checking depending on subscription type will be done at the destination page
 */
function notifications_access_subscribe($account) {
  global $user;
  
  if (user_access('administer notifications') || user_access('manage all subscriptions')) return TRUE;
  
  return $account && $account->uid && ($user->uid == $account->uid) && user_access('maintain own subscriptions');
}

/**
 * Menu loading, subscription
 */
function notifications_subscription_load($sid) {
  return notifications_load_subscription($sid);
}

/**
 * Menu access callback
 */
function notifications_subscription_access($op, $subscription, $account = NULL) {
  global $user;

  $account = $account ? $account : $user;  
  if (user_access('administer notifications') || user_access('manage all subscriptions')) {
    return TRUE;
  } 
  switch ($op) {
    case 'edit':
    case 'unsubscribe':
      return $subscription->uid && ($subscription->uid == $account->uid) && user_access('maintain own subscriptions');
  }
  return FALSE;
}

/**
 * Menu access callback for destinations
 */
function notifications_destination_access($op, $destination) {  
  // Access will be granted only to administrator for now
  return user_access('administer notifications');
}

/**
 * Implementation of hook_perms()
 * 
 * This module defines the following permissions
 * - administer notifications = Full access to all administration for the module
 * - maintain own subscriptions = Create / delete own subscriptions
 * - manage own subscriptions = Access the subscriptions management tab
 * - manage all subscriptions = Administer other users subscriptions
 */
function notifications_perm() {
  return array('administer notifications', 'maintain own subscriptions', 'manage own subscriptions', 'manage all subscriptions');
}

/**
 * Implementation of hook_user().
 */
function notifications_user($type, $edit, &$user, $category = NULL) {
  switch ($type) {
    case 'delete';
      // Delete related data on tables
      notifications_delete_subscriptions(array('uid' => $user->uid));
      break;
    case 'update':
      if (isset($edit['status'])) {
        if ($edit['status'] == 0) { // user is being blocked now
          // Delete pending notifications and block existing active subscriptions
          db_query('UPDATE {notifications} SET status = %d WHERE status = %d AND uid = %d', NOTIFICATIONS_SUBSCRIPTION_BLOCKED, NOTIFICATIONS_SUBSCRIPTION_ACTIVE, $user->uid);
          notitications_include('process.inc');
          notifications_queue_clean(array('uid' => $user->uid));
        }
        else {
          // User may be being unblocked, unblock subscriptions if any
          db_query('UPDATE {notifications} SET status = %d WHERE status = %d AND uid = %d', NOTIFICATIONS_SUBSCRIPTION_ACTIVE, NOTIFICATIONS_SUBSCRIPTION_BLOCKED, $user->uid);
        }
      }
      break;
  }
}

/**
 * Implementation of hook_form_alter()
 */
function notifications_form_alter(&$form, $form_state, $form_id) {
  switch ($form_id) {
    // Default send interval for user form
    case 'user_profile_form':
      if ($form['_category']['#value'] == 'account' && (user_access('maintain own subscriptions') || user_access('administer notifications'))) {
        $form['messaging']['#title'] = t('Messaging and Notifications settings');
        $send_intervals = notifications_send_intervals();
        $form['messaging']['notifications_send_interval'] = array(
          '#type' => 'select',
          '#title' => t('Default send interval'),
          '#options' => $send_intervals,
          '#default_value' => notifications_user_setting('send_interval', $form['_account']['#value']),
          '#disabled' => count($send_intervals) == 1,
          '#description' => t('Default send interval for subscriptions.'),
        );    
      }
  }
}

/**
 * Gets a user setting, defaults to default system setting for each
 * 
 * @param $name
 *   Setting name
 * @param $account
 *   Optional user account, will default to current user
 * @param $default
 *   Optional default to return if this is not set
 */
function notifications_user_setting($name, $account = NULL, $default = NULL) {
  global $user;

  $account = $account ? $account : $user;
  // Default send method is taken from messaging module
  if ($name == 'send_method') {
    return messaging_method_default($account);
  } 
  $field = 'notifications_'. $name;
  if (isset($account->$field)) {
    return $account->$field;
  }
  else {
    return variable_get('notifications_default_'. $name, $default);
  }
}

/**
 * Pass on event to queue processing and run it to the end
 * 
 * @param $event
 *   Array with event parameters
 */
function notifications_event($event) {
  if (is_array($event)) {
    $event = notifications_event_build($event);
  }
  if ($event) {
    notifications_include('event.inc');
    notifications_event_trigger($event);
    return $event;
  }
  
}

/**
 * Build event object from array
 */
function notifications_event_build($params) {
  notifications_include('event.inc');
  global $user, $language;
  
  // Fill in event with default values
  $params += array(
    'uid' => $user->uid,
    'language' => $language->language,
    'type' => 'default', // Object/event type
    'action' => 'default', // Action that happened to the object
    'save' => TRUE,
    'queue' => TRUE,
  );
  $params += array('typekey' => $params['type'] . '-' . $params['action']);
  // Check whether we have to save and queue this event, defaults to yes if not set
  // If not enabled, do not store nor queue this event, can be changed by plug-in modules
  if (notifications_event_enabled($params['typekey'])) {
    return new Notifications_Event($params);
  }
  else {
    // Event type not enabled, return nothing
    return NULL;
  }
}

/**
 * Check whether we have enabled events of this type
 */
function notifications_event_enabled($key, $default = TRUE) {
  $info = variable_get('notifications_event_enabled', array());
  $status = isset($info[$key]) ? $info[$key] : $default;
  // If this has a parent type, will be enabled just if parent is
  if ($status && ($parent = notifications_event_types($key, 'parent'))) {
    return notifications_event_enabled($parent, FALSE);
  }
  else {
    return $status;
  }
}

/**
 * Implementation of hook_exit()
 * 
 * This is where the immediate sending is done if enabled, so we are sure all other modules
 * have finished node processing when node update.
 */
function notifications_exit() {
  // We don't load the function if not loaded, which means no events were triggered
  if (function_exists('notifications_event_process_immediate')) {
    notifications_event_process_immediate();
  }
}

/**
 * Load multiple subscriptions that match some condition
 */
/**
 * Build subscription object properly
 */
function notifications_build_subscription($subscription) {
  // Build object if not built previously
  if (!is_object($subscription)) {
    $subscription = (object)$subscription;
  }
  if (is_a($subscription, 'Notifications_Subscription')) {
    return $subscription;
  }
  else {
    return Notifications_Subscription::build($subscription);
  }
}

/**
 * Update or create subscription
 * 
 * This function checks for duplicated subscriptions before saving.
 * If a similar subscription is found it will be updated.
 * If no subscription is found and it is new, the sid will be added into the object.
 * 
 * @param $subscription
 *   Subscription object or array
 * @return integer
 *   Failure to write a record will return FALSE. Otherwise SAVED_NEW or SAVED_UPDATED is returned depending on the operation performed.
 */
function notifications_save_subscription(&$subscription) {
  global $user;  
  // Build object if not built previously
  $subscription = notifications_build_subscription($subscription);
  $subscription->conditions = count($subscription->get_fields());
  // Set account that will fill in account parameters
  $account = $subscription->uid ? messaging_load_user($subscription->uid) : $user;
  $subscription->set_account($account);
  // Update existing one or save new checking for duplicates
  $result = FALSE;
  if (!empty($subscription->sid)) {
    $op = 'update';
    $result = $subscription->save();
  }
  elseif ($subscription->check_destination()) {
    if ($duplicate = notifications_get_subscriptions(array('mdid' => $subscription->mdid, 'type' => $subscription->type, 'event_type' => $subscription->event_type, 'module' => $subscription->module, 'send_interval' => $subscription->send_interval), $subscription->get_conditions(), TRUE)) {
      // We've found duplicates, resolve conflict updating first, deleting the rest
      // It is possible that we had a disabled one, this updating will fix it
      $update = array_shift($duplicate);
      unset($subscription->sid); // It may be 0
      foreach ($subscription as $key => $value) {
        if (isset($value)) {
          $update->$key = $value;
        }
      }
      $subscription->sid = $update->sid;
      // If there are more, delete, keep the table clean
      while ($dupe = array_shift($duplicate)) {
        notifications_delete_subscription($dupe->sid);
      }
      return notifications_save_subscription($subscription);  
    }
    else {
      $op = 'insert';    
      $result = $subscription->save();
    }
  }
  
  // If the operation has worked so far, update fields and inform other modules
  if ($result !== FALSE) {
    module_invoke_all('notifications_subscription', $op, $subscription);
  }

  return $result;
}

/**
 * Get an individual subscription.
 *
 * @param $subs
 *   Either a subscription object or a subscription id (sid).
 * @param $refresh
 *   Force cache refresh
 * @return
 *   Subscriptions object.
 */
function notifications_load_subscription($subs, $refresh = FALSE) {
  $subscriptions = &messaging_static(__FUNCTION__);
  if (is_object($subs)) {
    if(is_a($subs, 'Notifications_Subscription')) {
      $subscriptions[$subs->sid] = $subs;
    }
    else {
      $subscriptions[$subs->sid] = Notifications_Subscription::build($subs);
    }
    return $subscriptions[$subs->sid];
  }
  else {
    if ($refresh || !$subscriptions || !array_key_exists($subs, $subscriptions)) {
      $subscriptions[$subs] = Notifications_Subscription::load($subs);
    }
    return $subscriptions[$subs];
  }
}

/**
 * Get users with static caching for existing users
 * 
 * If not uid passed it will return an anonymous fake user (with destination, send_method)
 * We need to pass the send method to produce the right tokens later
 * 
 * This provides some API support for user-less subscriptions, i.e. when we've got just
 * an email address but no user associated. The idea is that these fake users will be properly 
 * handled by messaging module
 * 
 * @todo Possibly all this should be handled by messaging layer
 * 
 * @param $uid
 *   Uid of the user account to load, none to use anonymous user
 * @param $destination
 *   Messaging destination (mail, sms number, etc..), just for anonymous users
 * @param $send_method
 *   Messaging send method key (mail, sms, xmpp, etc..), just for anonymous users
 */
function notifications_load_user($uid, $destination = NULL, $send_method = NULL) {
  if ($uid) {
    return messaging_load_user($uid);
  }
  else {
    $account = drupal_anonymous_user();
    $account->destination = $destination;
    $account->send_method = $send_method;
    return $account;
  }
}

/**
 * Delete subscription and clean up related data.
 * 
 * It also removes pending notifications related to that subscription 
 * 
 * @param $sid
 *   Subscription object or id of subscriptin to delete
 */
function notifications_delete_subscription($sid) {
  $sid = is_object($sid) ? $sid->sid : $sid;
  $subscriptions = &messaging_static('notifications_load_subscription');
  $subscriptions[$sid] = NULL;
  foreach (array('notifications', 'notifications_fields', 'notifications_queue') as $table) {
    db_query("DELETE FROM {". $table ."} WHERE sid = %d", $sid);
  }
}

/**
 * Delete multiple subscriptions and clean up related data (pending notifications, fields).
 * 
 * Warning: If !$limit, it will delete also subscriptions with more conditions than the fields passed.
 * 
 * @param array $params
 *   Array of multiple conditions in the notifications table to delete subscriptions
 * @param array $field_conditions
 *   Array of multiple conditions in the notifications_fields table to delete subscriptions
 * @param $limit
 *   Whether to limit the result to subscriptions with exactly that condition fields
 */
function notifications_delete_subscriptions($params, $field_conditions = array(), $limit = FALSE) {
  notifications_include('query.inc');
  
  // For exact condition fields we add one more main condition.
  if ($limit) {
    $params += array('conditions' => count($field_conditions));
  }
   // This will get partially loaded objects, only with sid field
  $query['select'][] = 's.sid';
  $subscriptions = notifications_query_subscriptions($params, $field_conditions, $query, FALSE);

  // This is the actual deletion. We've fetched the values from the db so this needs no escaping.
  if ($subscriptions) {
    $delete = array_keys($subscriptions);
    $placeholders = db_placeholders($delete);
    foreach (array('notifications_fields', 'notifications_queue', 'notifications') as $table) {
      db_query('DELETE FROM {' . $table . '} WHERE sid IN (' . $placeholders . ')', $delete);
    }
    // Now remove them from the cache too
    if ($cache = &messaging_static('notifications_load_subscription')) {
      $cache = array_diff_key($cache, $subscriptions);
    }
  }
}

/**
 * Shorthand function for deleting everything related to a destination
 */
function notifications_delete_destination($mdid) {
  notifications_delete_subscriptions(array('mdid' => $mdid));
  Messaging_Destination::delete_multiple(array('mdid' => $mdid));
}

/**
 * Get subscriptions that match a set of conditions.
 *
 * @param $main_conditions
 *   Conditions for the main notifications table
 * @param $field_conditions
 *   Optional array of condition fields. The elements may be
 *   - single field => value pairs 
 *   - or numeric key => array('type' => field, 'value' => value)
 * @param $exact_fields
 *   Whether to limit the result to subscriptions with exactly that condition fields. Otherwise we
 *   look for subscriptions that have and match that fields but may have more than that.
 * @param $key
 *   Optional key field to use as the array index. Will default to sid 
 *   For notifications with one field, it may be 'value' or 'intval'
 * @param $pager
 *   Whether to throw a pager query 
 * @return
 *   Array of subscriptions indexed by uid, module, field, value, author
 * 
 * @todo Check field types for building the query
 */
function notifications_get_subscriptions($main_conditions, $field_conditions = NULL, $exact_fields = TRUE, $key = 'sid', $pager = NULL) {
  notifications_include('query.inc');
  // Build query conditions using the query builder.  
  $query['select'][] = 's.*';
  // If we have the exact fields, make sure we match only the rows with this number of conditions
  if ($exact_fields && isset($field_conditions)) {
    $main_conditions += array('conditions' => count($field_conditions));
  }
  // Query for the notifications table
  $result = notifications_query_subscriptions($main_conditions, $field_conditions, $query);

  // Build list with results, we may need to index by a different field
  if ($key == 'sid') {
    $subscriptions = $result;
  }
  else {
    $subscriptions = array();
    foreach ($result as $subs) {
      if ($key == 'value' || $key == 'intval') {
        $field = array_shift($subs->get_fields());
        $subscriptions[$field->value] = $subs;
      }
      else {
        $subscriptions[$subs->$key] = $subs;
      }      
    }
  }

  return $subscriptions;
}

/**
 * Get subscription for a given user to some object
 * 
 * This is a shorthand function to quickly check some types of subscriptions.
 * 
 * @param $account
 *   User id or user account object to check subscriptions for
 * @param $type
 *   Object type
 * @param $field
 *   Field type for the subscription. I.e. for a node it will be nid
 * @param $value
 *   Field value to check subscriptions to. I.e. $node
 * 
 * @return
 *   Array of subscriptions for this user and object indexed by sid 
 */
function notifications_user_get_subscriptions($account, $type, $object, $refresh = FALSE) {
  $subscriptions = &messaging_static(__FUNCTION__);
  $account = is_numeric($account) ? messaging_load_user($account) : $account;
  // No subscriptions for anonymous users
  if (!$account || !$account->uid) return array();
  
  // Check the object exists, is loaded and we've got a key field
  $object = notifications_object_load($type, $object);
  $key = notifications_object_type($type, 'key_field');
  if (!$object || !$key || empty($object->$key)) return array();
  
  // Now we've got the key fields for caching, try the cache or find subscriptions
  if ($refresh || !isset($subscriptions[$account->uid][$type][$object->$key])) {
    // Collect subscription types for this object and this account
    // Get allowed subscription options for this account to this object
    $user_subscriptions = array();
    $subscribe_options = notifications_object_subscribe_options($type, $object, $account);
    foreach ($subscribe_options as $template) {
      $type_subscriptions = notifications_get_subscriptions(
        array('uid' => $account->uid, 'type' => $template['type']), // Main conditions
        $template['fields'], // Field conditions
        TRUE // Match exactly these condition fields, no less, no more
      );
      if ($type_subscriptions) {
        $user_subscriptions += $type_subscriptions; //array_merge($user_subscriptions, $type_subscriptions);
      }
    }
    $subscriptions[$account->uid][$type][$object->$key] = $user_subscriptions;
  }
  return $subscriptions[$account->uid][$type][$object->$key];
}

/**
 * Entry point for the object API
 * 
 * @param
 *   Variable number of arguments, the first one is the notifications_object_* function to invoke
 */
function notifications_object() {
  $args = func_get_args();
  $function = 'notifications_object_' . array_shift($args);
  if (!function_exists($function)) notifications_include('object.inc');
  return call_user_func_array($function, $args);
}

/**
 * Get info about object types
 *
 * @param $type
 *   String, the subscriptions type OPTIONAL
 * @param $field
 *   String, a specific field to retrieve info from OPTIONAL
 *   
 *   Information for a given field and type
 *   or information for a given field for all types
 */
function notifications_object_type($type = NULL, $field = NULL) {
  $types = notifications_info('object types');
  return notifications_array_info($types, $type, $field);
}

/**
 * Load an object of type defined by notifications 'object types'
 * 
 * @param $type
 *   Object type
 * @param $value
 *   Object key value, or the object itself
 */
function notifications_object_load($type, $value) {
  if (is_object($value)) {
    return $value;
  }
  else {
    return _notifications_object_callback($type, 'load callback', $value);
  }
}


/**
 * Invoke a callback for an object type
 * 
 * @param $type
 *   Object type
 * @param $name
 *   Callback name
 * @param $arg1, $arg2...
 *   Variable arguments
 */
function _notifications_object_callback() {
  $args = func_get_args();
  $type = array_shift($args);
  $name = array_shift($args);
  return _notifications_info_callback(notifications_object_type($type), $name, $args);
}

/**
 * Invoke a callback for a subscription type
 * 
 * @param $type
 *   Subscription type
 * @param $name
 *   Callback name
 * @param $arg1, $arg2...
 *   Variable arguments
 */
function _notifications_subscription_callback() {
  $args = func_get_args();
  $type = array_shift($args);
  $name = array_shift($args);
  return _notifications_info_callback(notifications_subscription_types($type), $name, $args);
}

/**
 * Invoke a callback for a field type. 
 * 
 * If the field has no callback it will default to the field object type callback
 * 
 * @param $type
 *   Subscription type
 * @param $name
 *   Callback name
 * @param $arg1, $arg2...
 *   Variable arguments
 */
function _notifications_field_callback() {
  $args = func_get_args();
  $type = array_shift($args);
  $name = array_shift($args);
  // We try first the field callback, then the field object type callback
  $info = notifications_subscription_fields($type);
  if (isset($info[$name])) {
    return _notifications_info_callback($info, $name, $args);
  }
  elseif (!empty($info['object_type']) && ($object_info = notifications_object_type($info['object_type'])) && isset($object_info[$name])) {
    return _notifications_info_callback($object_info, $name, $args);
  }
  else {
    return NULL;
  }
}
/**
 * Get a callback for an objec type
 */
function _notifications_info_callback($info, $name, $args) {
  // We may need to include the file before
  if (isset($info['file'])) {
    notifications_include($info['file'], $info['module']);
  }
  if (isset($info[$name]) && function_exists($info[$name])) {
    // The callback may have arguments, a property like 'format callback args'
    if (isset($info[$name . ' args'])) {
      $args[] = $info[$name . ' args'];
    }
    return call_user_func_array($info[$name], $args);
  }

}

/**
 * Get info about subscription types
 *
 * @param $type
 *   String, the subscriptions type OPTIONAL
 * @param $field
 *   String, a specific field to retrieve info from OPTIONAL
 * @param $check_access
 *   Whether to check user access and filter out disabled types
 *   
 *   Information for a given field and type
 *   or information for a given field for all types
 */
function notifications_subscription_types($type = NULL, $field = NULL, $check_access = FALSE) {
  $types = notifications_info('subscription types');

  $result = $types;
  if ($check_access) {
    foreach ($types as $key => $info) {
      if (!empty($info['disabled']) || (!empty($info['access']) && !user_access($info['access']))) {
        unset($result[$key]);
      }
    }
  }

  return notifications_array_info($result, $type, $field);
}

/**
 * Get information about subscriptions fields
 */
function notifications_subscription_fields($type = NULL, $property = NULL) {
  $fields = notifications_info('subscription fields');
  return notifications_array_info($fields, $type, $property);
}


/**
 * Subscription information field for several forms
 */
function notifications_subscription_info_field($subscription) {
  return $subscription->form_info();
}

/**
 * Get information from an array of data
 * 
 * @param $data
 *   Array of data indexed by type
 * @param $type
 *   Type to return out of the array, none to return all types
 * @param $property
 *   Property to return, none to return all properties
 */
function notifications_array_info($data, $type = NULL, $field = NULL) {
  if ($field && $type) {
    return isset($data[$type][$field]) ? $data[$type][$field] : NULL;
  }
  elseif ($field) {
    $return = array();
    foreach ($data as $id => $info) {
      $return[$id] = isset($info[$field]) ? $info[$field] : NULL;
    }
    return $return;
  }
  elseif ($type) {
    return isset($data[$type]) ? $data[$type] : array();
  }
  else {
    return $data;
  }  
}

/**
 * Serialize an array ordering the keys before. This is useful for cache indexes
 */
function notifications_array_serialize($array) {
  if (!$array) {
    return '';
  }
  // First we sort and serialize multiple conditions
  foreach ($array as $key => $value) {
    if (is_array($value)) {
      sort($value);
      $array[$key] = implode(':', $value);
    }
  }
  // Now we sort the whole condtions array maintaining index association
  asort($array);
  return implode(',', array_keys($array)) . '/' . implode(',', $array);  
}

/**
 * Invokes hook_notifications() with a single parameter or more but not needing
 * an object to be passed as reference.
 */
function notifications_module_information($op, $arg0 = NULL, $arg1 = NULL, $arg2 = NULL) {
  $result = array();
  foreach (module_implements('notifications') as $module) {
    $function = $module .'_notifications';
    if ($return = $function($op, $arg0, $arg1, $arg2)) {
      // Add module name to each result if it is an info array
      foreach ($return as $key => &$value) {
        if (is_array($value)) {
          $value += array('module' => $module);
        }
      }
      // Note we use array_merge instead of array_merge_recursive()
      $result = array_merge($result, $return);
    }
  }
  return $result;
}

/**
 * Implementation of hook_messaging()
 * 
 * This hook provides information about the mensaje templates this module uses and related tokens.
 * 
 * Depending on $op, this hook takes different parameters and returns different pieces of information:
 * 
 * - 'message groups'
 *   Get array of message groups, each of which will have one or more keys for different templates
 *   Each group should have a unique key, so it should start with the module name
 * - 'message keys'
 *   Get message template parts for a given group ($arg1)
 *   Return array of key => name for each part
 * - 'messages'
 *   Get default message templates for a given group ($arg1).
 *   It should return default texts, indexed by message key that will be the default templates
 *   These templates may be edited on the 'Messaging templates' page
 * - 'tokens'
 *   Get available tokens for a given message group and key ($arg1).
 *   Return array of token keys that will be available for this message templates
 *   The tokens themselves may be default tokens (provided by token module) or we can add new
 *   tokens implementing hook_token_list() and hook_token_value()
 * 
 * @param $op
 *   Operation, type of information to retrieve
 * @param $arg1, $arg2...
 *   Different parameters depending on $op
 */
function notifications_messaging($op, $arg1 = NULL, $arg2 = NULL, $arg3 = NULL, $arg4 = NULL) {
  switch ($op) {
    case 'message types':
      $info['notifications'] = array(
        'name' => t('Notifications'),
        'description' => t('Messages coming from user subscriptions and system events')
      );
      return $info;
    case 'message groups':
      return notifications_get_templates('info', 'enabled');
    case 'message keys':
      return notifications_get_templates('parts', $arg1);
    case 'messages':
      return notifications_get_templates('defaults', $arg1);
    case 'tokens':
      return notifications_get_templates('tokens', $arg1);
    case 'method update':
      // A messaging method has been disabled ($arg1) and replaced by the new one ($arg2)
      // Update subscriptions
      db_query("UPDATE {notifications} SET send_method = '%s' WHERE send_method = '%s'", $arg2, $arg1);
      // Purge notifications queue, we may lost some notifications but it's the safest option.
      db_query("DELETE FROM {notifications_queue} WHERE send_method = '%s'", $arg1);
      break;
  }
}

/**
 * Implementation of hook_notifications_templates() 
 */
function notifications_notifications_templates($op, $type = NULL, $language = NULL) {
  switch ($op) {
    case 'info':
      $info = array();
      // Generic notifications event
      if ($type == 'all' || $type == 'notifications-event') {
        $info['notifications-event'] = array(
          'module' => 'notifications',
          'name' => t('Notifications event'),
          'description' => t('Common parts for all Notifications messages for a single event. This is useful for defining a common header and/or footer for all these messages.'),
          'fallback' => FALSE, // This template has no fallback
        );
      }
      if ($type == 'digest' || $type == 'notifications-digest') {
        $info['notifications-digest'] = array(
          'module' => 'notifications',
          'name' => t('Notifications digest'),
          'description' => t('Depending on your settings for each Send interval, Notifications may be digested, this is grouped and summarized in a single message. These are the common parts for Notifications digests.'),
          'fallback' => FALSE, // This template has no fallback
        );
      }
      return $info;
    case 'parts':
      switch ($type) {
        case 'notifications-event':
          // Event notifications
          return array(
            'subject' => t('Subject'),
            'header' => t('Header'),
            'main' => t('Content'),
            'footer' => t('Footer'),
          );
        case 'notifications-digest':
          return array(
            'subject' => t('Subject'),
            'header' =>  t('Header'),
            'main' => t('Line for digested events'),
            'closing' => t('Group closing'),
            'footer' => t('Footer'),
          );
      }
      break;
    case 'defaults':
      // Event notifications
      if ($type == 'notifications-event') {
        return array(
          'subject' => t('Event notification for [user] from [site-name]'),
          'header' => t("Greetings [user],"),
          'main' => t("A item to which you are subscribed has been updated"),
          'footer' => array(
              t('This is an automatic message from [site-name]'),
              t('To manage your subscriptions, browse to [subscriptions-manage]'),
              t('You can unsubscribe at [unsubscribe-url]'),
          ),
        );
      }
      // Digested messages
      if ($type == 'notifications-digest') {
        return array(
          'subject' => t('[site-name] subscription update for [user]'),
          'header' => t("Greetings, [user].\n\nThese are your messages"),
          'main' => t("A [type] has been updated: [title]\n\n[event_list]"),
          'closing' => '...',
          'footer' => array(
            t('This is an automatic message from [site-name]'),
            t('To manage your subscriptions, browse to [subscriptions-manage]'),
          ),        
        );
      }
      break;
    case 'tokens':
      $args = explode('-', $type);
      $tokens = array();
      // These are the token groups that will be used for this module's messages
      if ($args[0] == 'notifications') {
        $tokens = array('subscription', 'user');
        if ($args[1] == 'event') {
          // If the template is for a single event, it can use event tokens
          $tokens[] = 'event';
        }
        elseif ($args[1] == 'digest') {
          // Template for multiple events, can use events (note the plural) tokens
          $tokens[] = 'events';
        }
      }
      return $tokens;    
  }
}
/**
 * Get notifications templates
 * 
 * This will filter out templates for disabled events
 * 
 * $op = 'info', 'parts', 'tokens', 'defaults'
 */
function notifications_get_templates($op = 'info', $type = 'all', $language = NULL) {
  static $all_templates;

  switch ($op) {
    case 'info':
      if ($type == 'all' || $type == 'enabled') {
        if (!isset($all_templates)) {
          $all_templates = module_invoke_all('notifications_templates', 'info', 'all');
        }
        if ($type == 'all') {
          return $all_templates;
        }
        elseif ($type == 'enabled') {
          // Get a list of templates for enabled event types
          $enabled = array_filter(notifications_event_types(NULL, 'template', FALSE));
          return messaging_template_extract_group($enabled, $all_templates);
        }
      }
      else {
        return module_invoke_all('notifications_templates', 'info', $type, $language);
      }
      break;
    default:
      return module_invoke_all('notifications_templates', $op, $type, $language);
  }
}
/**
 * Implementation of hook_token_values()
 * 
 * @ TODO: Work out event tokens
 */
function notifications_token_values($type, $object = NULL, $options = array()) {
  switch ($type) {
    case 'subscription':
      $values = array();
      // Tokens only for registered users
      if (($subscription = $object)) {
        $link = notifications_get_link('unsubscribe', array('sid' => $subscription->sid, 'signed' => TRUE, 'absolute' => TRUE));
        $values['unsubscribe-url'] = url($link['href'], $link['options']);
        $values['subscription-type'] = $subscription->get_type('name');
        $values['subscription-name'] = $subscription->get_name();
        $values['subscription-description-short'] = $subscription->format_short(FALSE);
        $values['subscription-description-long'] = $subscription->format_long(FALSE);
      }
      return $values;
    case 'user':
      $values = array();
      if (($account = $object) && !empty($object->uid)) {
        // We have a real user, so we produce full links
        $values['subscriptions-manage'] = url("user/$account->uid/notifications", array('absolute' => TRUE));
        $link = notifications_get_link('unsubscribe', array('uid' => $account->uid, 'signed' => TRUE, 'absolute' => TRUE));
        $values['unsubscribe-url-global'] = url($link['href'], $link['options']);
      }
      return $values;
    case 'destination':
      // @todo Destination tokens for registered users
      break;
    case 'event':
      if ($event = $object) {
        $values['event-type-description'] = $event->get_type('description');
        $values['event-date-small'] = format_date($event->created, 'small');
        $values['event-date-medium'] = format_date($event->created, 'medium');
        $values['event-date-large'] = format_date($event->created, 'large');
        return $values;
      }
      break;
  }
}

/**
 * Implementation of hook_token_list(). Documents the individual
 * tokens handled by the module.
 */
function notifications_token_list($type = 'all') {
  $tokens = array();
  if ($type == 'user' || $type == 'all') {
    $tokens['user']['subscriptions-manage']    = t('The url for the current user to manage subscriptions.');
    $tokens['user']['unsubscribe-url-global'] = t('The url to allow a user to delete all their subscriptions.');
  }
  if ($type == 'subscription' || $type == 'all') {
    $tokens['subscription']['unsubscribe-url']  = t('The url for disabling a specific subscription.');
    $tokens['subscription']['subscription-type'] = t('The subscription type.');
    $tokens['subscription']['subscription-name'] = t('The subscription name.');
    $tokens['subscription']['subscription-description-short'] = t('The subscription short description.');
    $tokens['subscription']['subscription-description-long'] = t('The subscription long description.');
  }
  if ($type == 'event' || $type == 'all') {
    $tokens['event']['event-type-description'] = t('Description of event type.');
    $tokens['event']['event-date-small'] = t('Date of the event, short format.');
    $tokens['event']['event-date-medium'] = t('Date of the event, medium format.');
    $tokens['event']['event-date-large'] = t('Date of the event, large format.');
  }
  return $tokens;
}

/**
 * Get event types. Invoking this function will also load the event API
 * 
 * @param $typekey
 *   Event type key
 * @param $property
 *   Property to return
 * @param $enabled
 *   Whether to return also disabled events
 */
function notifications_event_types($typekey = NULL, $property = NULL, $disabled = TRUE) {
  $info = &notifications_info('event types');
  $enabled = &messaging_static(__FUNCTION__ . '_enabled');
  if (!isset($enabled)) {
    notifications_include('event.inc');
    $enabled = array();
    foreach ($info as $key => &$event) {
      if (empty($event['disabled']) && notifications_event_enabled($key)) {
        $enabled[$key] = &$event;
      }
    }
  }
  return $disabled ? notifications_array_info($info, $typekey, $property) : notifications_array_info($enabled, $typekey, $property);
}

/**
 * Implementation of hook_cron()
 */
function notifications_cron() {
  if (variable_get('notifications_process_on_cron', TRUE)) {
    notifications_include('process.inc');
    notifications_process_run();
  }
}

/**
 * Return link array for subscriptions
 * 
 * @param $type
 *   Link type: 'subscribe' | 'unsubscribe'
 * @param $params
 *   Aditional parameters for the subscription, may be
 *   - uid, the user for which the link is generated
 *   - confirm, whether to show confirmation page or not
 *   - destination, form destination or TRUE to use current path
 *   - signed, to produce a signed link that can be used by anonymous users (Example: unsubscribe link in emails)
 *   - Other subscription parameters: type, fields...
 * @param $format
 *   Whether to return a formatted link instead of a raw one (array)
 */
function notifications_get_link($type, $params, $format = FALSE) {
  // Add some default values
  $params += array('uid' => 0);
  $elements = array();

  switch ($type) {
    case 'subscribe':
      $elements = array(
        'subscribe',
        $params['uid'],
        $params['type'],
        implode(',', array_keys($params['fields'])),
        implode(',', $params['fields'])
      );
      $title = t('subscribe');
      break;
    case 'unsubscribe':
      $elements[] = 'unsubscribe';
      // The unsubscribe link can be for a single subscription or all subscriptions for a user
      if (!empty($params['sid'])) {      
        $elements[] = 'sid';
        $elements[] = $params['sid'];   
      }
      elseif (!empty($params['uid'])) {
        $elements[] = 'uid';
        $elements[] = $params['uid'];
      }
      $title = t('unsubscribe');
      break;
    case 'edit': // Edit subscription
      $elements[] = 'subscription';
      $elements[] = $params['sid'];
      $elements[] = 'edit';
      $title = t('edit');
      break;  
  }
  $params += array('title' => $title);
  $link =  _notifications_get_link($elements, $params);
  return $format ? l($params['title'], $link['href'], $link['options']) : $link;
}

/**
 * Get notifications link from elements and params
 */
function _notifications_get_link($elements, $params = array()) {
  // Add some default values
  $params += array(
    'confirm' => TRUE,
    'signed' => FALSE,
    'destination' => FALSE,
    'query' => array(),
  );
  if ($params['destination'] === TRUE) {
    $params['destination'] = $_GET['q'];
  }
  // Build query string using named parameters
  $query = $params['query'];
  if ($params['destination']) {
    $query['destination'] = $params['destination'];
  }
  // To skip the confirmation form, the link must be signed
  // Note tat the query string will be 'confirm=1' to skip confirmation form
  if (!$params['confirm']) {
    $query['confirm'] = 1;
    $params['signed'] = 1;
  }
  if ($params['signed']) {
    $query['signature'] =  _notifications_signature($elements, !$params['confirm']);
  }
  // Build final link parameters
  $options['query'] = $query;
  foreach (array('absolute', 'html') as $name) {
    if (isset($params[$name])) {
      $options[$name] = $params[$name];
    }
  }
  return array(
    'href' => 'notifications/'. implode('/', $elements),
    'options' => $options,
  );  
}

/**
 * Check access to objects
 * 
 * This will check permissions for subscriptions and events before subscribing
 * and before getting updates.
 * 
 * @param $type
 *   Type of object to check for access. Possible values:
 *   - 'event', will check access to event objects
 *   - 'subscription', will check access to subscribed objects
 */
function notifications_user_allowed($type, $account, $object = NULL) {
  notifications_include('object.inc');
  // First invoke the hook for 'event'  or 'subscription'. If we get any false return value, that's it
  $hook = 'notifications_' . $type;
  foreach (module_implements($hook) as $module) {   
    $permission = module_invoke($module, $hook, 'access', $object, $account);
    if (isset($permission) && ($permission === FALSE || is_array($permission) && in_array(FALSE, $permission, TRUE))) {
      return FALSE;
    }
  }
  if (is_object($object)) {
    // For events and subscriptions check first all objects are loaded
    if (!$object->load_objects()) {
      return FALSE;
    }
    // Now check all the loaded objects
    foreach ($object->get_objects() as $check_type => $values) {
      // Subscriptions can have several objects of one type
      $type_list = is_array($values) ? $values : array($values);
      foreach ($type_list as $check_object) {
        if (notifications_object_access($check_type, $check_object, $account) === FALSE) {
          return FALSE;
        }
      }
    }
  }
  // This means we've done all the checking and found nothing, so we allow access
  return TRUE;
}

/**
 * Implementation of hook_notifications()
 * 
 * Check access permissions to subscriptions and take care of some backwards compatibility
 */
function notifications_notifications($op) {
  switch ($op) {
    case 'build methods':
      // Return array of building engines
      $info['simple'] = array(
        'type' => 'simple',
        'name' => t('Simple'),
        'description' => t('Produces one message per event, without digesting.'),
        'build callback' => 'notifications_process_build_simple',
        'digest' => FALSE,
      );
      return $info;

    case 'object types':
      notifications_include('object.inc');
      $types['node'] = array(
        'name' => t('Node'),
        'key_field' => 'nid',
        'load callback' => 'node_load',
        'autocomplete path' => 'notifications/autocomplete/node/title',
        'autocomplete callback' => 'notifications_node_nid2autocomplete',
        'format callback' => 'notifications_node_nid2title',
        'value callback' => 'notifications_node_title2nid',
        'access callback' => 'notifications_node_user_access',
        'file' => 'node.inc',
      );      
      $types['user'] = array(
        'name' => t('User'),
        'key_field' => 'uid',
        'load callback' => 'notifications_load_user',
        'autocomplete path' => 'user/autocomplete',
        'autocomplete callback' => 'notifications_user_name_callback',
        'format callback' => 'notifications_user_uid2name',
        'value callback' => 'notifications_user_name2uid',
        'access callback' => 'notifications_user_user_access',
        'file' => 'user.inc',
      ); 
      return $types;
      
  }
}

/**
 * Implementation of hook notifications_subscription()
 */
function notifications_notifications_subscription($op, $subscription = NULL, $account = NULL) {
  switch ($op) {
    case 'access':
      // First we check valid subscription type
      $access = FALSE;
      if ($subscription->type && ($info = notifications_subscription_types($subscription->type))) {
        // To allow mixed subscription types to work we dont have a fixed field list
        // Then check specific access to this type. Each type must have a permission
        if (!empty($info['access callback'])) {
          $access = call_user_func($info['access callback'], $account, $subscription);
        }
        elseif (!empty($info['access']) && (user_access($info['access'], $account) || user_access('administer notifications', $account))) {
          // Check matching fields
          if (!empty($info['variable_fields']) || !array_diff($info['fields'], array_keys($subscription->get_conditions()))) {
            $access = TRUE;
          }
        }
      } 
      if ($access) {
        // If permissions are ok, we invoke the old callback, which returns an array
        $old_access = notifications_module_information('access', 'subscription', $subscription, $account);
        return !$old_access || !in_array(FALSE, $old_access, TRUE); 
      }
      else {
        // If this is access denied, no need for further checking
        return FALSE;
      }

  }  
}

/**
 * Implementation of hook notifications_event()
 * 
 * Invoke hook notifications with old parameters
 */
function notifications_notifications_event($op, $event, $account = NULL) {
  switch ($op) {
    case 'query':
      $query = array();
      // First get all the condition fields for the event object type
      // I.e. for a node event, we get all condition fields for the event's node
      if ($object = $event->get_object($event->type)) {
        if ($fields = notifications_object_condition_fields($event->type, $object)) {
          // We get a merged array with field => array(value1, value2...);
          $query[]['fields'] = $fields;
        }        
      }
      // Add in other conditions invoking the old hook
      if ($more = notifications_module_information('query', 'event', $event->type, $event)) {
        $query = array_merge($query, $more);
      }
      return $query;
    case 'access':
      return notifications_module_information('access', 'event', $event, $account);
    case 'load':
      // Invoke old hook for backwards compatibility
      return module_invoke_all('notifications', 'event load', $event);
  }
}

/**
 * List of send intervals. These may be overriden in a variable.
 */
function _notifications_send_intervals() {
  return variable_get('notifications_send_intervals', array(
      // -1 => t('Never'),
      0 => t('Immediately'),
      3600 => t('Every hour'),
      43200 => t('Twice a day'),
      86400 => t('Daily'),
      604800 => t('Weekly'),  
    )
  );
}

/**
 * List of send intervals, translated.
 */
function notifications_send_intervals($account = NULL) {
  $list = &messaging_static(__FUNCTION__);
  if ($account && !$account->uid && function_exists('notifications_anonymous_send_intervals')) {
    return notifications_anonymous_send_intervals();
  }
  if (!isset($intervals)) {
    if ($intervals = variable_get('notifications_send_intervals', FALSE)) {
      foreach ($intervals as $key => $name) {
        $list[$key] = notifications_translate("send_interval:$key:name", $name);
      }
    }
    else {
      $list = _notifications_send_intervals();
    }
  }
  return $list;
}

/**
 * List of send methods
 * 
 * @param $account
 *   Optional user account, for checking permissions against this account
 */
function _notifications_send_methods($account = NULL) {
  return variable_get('notifications_send_methods', messaging_method_list($account));
}

/**
 * Get list of send methods for user or anonymous account
 */
function notifications_send_methods($account) {
  // We restrict send methods for anonymous accounts when edited by regular users
  if (empty($account->uid) && !user_access('administer notifications') && function_exists('notifications_anonymous_send_methods')) {
    return notifications_anonymous_send_methods();
  }
  else {
    return _notifications_send_methods($account);
  }
}

/**
 * Signature for url parameters
 * 
 * @param $params
 *   Subscription parameters
 * @param $skip_confirm
 *   TRUE to skip confirmation form
 */
function _notifications_signature($params, $skip_confirm = FALSE) {
  return md5('notifications:'. drupal_get_private_key() .':'. ($skip_confirm ? 1 : 0) .':'. implode(':', $params));
}

/**
 * Default values for subscription
 */
function _notifications_subscription_defaults($account = NULL) {
  return array(
    'send_interval' => notifications_user_setting('send_interval', $account, 0),
    'send_method' => notifications_user_setting('send_method', $account, ''),
    'module' => 'notifications',
    'status' => NOTIFICATIONS_SUBSCRIPTION_ACTIVE,
    'destination' => '',
    'cron' => 1,
  ); 
}

/**
 * Status list
 */
function _notifications_subscription_status() {
  return array(
    NOTIFICATIONS_SUBSCRIPTION_ACTIVE => t('active'),
    NOTIFICATIONS_SUBSCRIPTION_BLOCKED => t('blocked'),
    NOTIFICATIONS_SUBSCRIPTION_INACTIVE => t('inactive'),
 );
}

/**
 * Build list of subscription types
 * 
 * Note: some custom types may have user defined strings, that's why the check_plain() everywhere
 */
function _notifications_subscription_types($format = 'short', $filter = NULL) {
  $options = array();  
  foreach (notifications_subscription_types() as $type => $info) {
    if (!$filter || count(array_intersect_assoc($filter, $info)) == count($filter)) {
      switch ($format) {
        case 'short':
          $options[$type] = check_plain($info['title']);
          break;
        case 'long':
          $options[$type] = '<strong>'. check_plain($info['title']) .'</strong>.';
          if (!empty($info['description'])) { 
            $options[$type] .= ' '. check_plain($info['description']);
          }
          break;
      }
    }
  }
  return $options;
}

/**
 * Callback for module dependent data
 * 
 * Some data stored in the notifications system is meant to be processed by other modules and
 * this is indicated by a module column in the data.
 * 
 * This function calls the module function if available, defaulting to the notifications provided
 * function when not. The arguments are passed as is
 * 
 * @param $module
 *   Module name
 * @param $function
 *   Function name in module
 */
 function notifications_callback() {
   $args = func_get_args();
   $module = array_shift($args);
   $function = array_shift($args);
   if ($module && function_exists($module .'_notifications_'. $function)) {
     $callback = $module .'_notifications_'. $function;
   }
   else {
     $callback = 'notifications_'. $function;
   }
   return call_user_func_array($callback, $args);
 }

/**
 * Generic subscriptions content form
 * 
 * Builds a form for a user to manage its own subscriptions with
 * some generic parameters
 * 
 * Currently it only manages simple condition fields
 * @param $account 
 *   User account
 * @param $type
 *   Subscription type
 * @param $subscriptions
 *   Current subscriptions of this type. If null they'll be loaded
 * @param $list
 *   Array with available subscriptions indexed by field value
 * @param $defaults
 *   Default value for subscriptions
 * @param $options
 *   Optional, array of aditional options for the form
 */
function notifications_user_form($form_state, $account, $type, $subscriptions, $list, $defaults, $options = array()) {
  // Complete defaults
  $info = notifications_subscription_types($type);
  $field = $info['fields'][0];
  $field_title = !empty($options['title']) ? $options['title'] : '';
  if (is_null($subscriptions)) {
    // Fetch subscriptions with given parameters
    $subscriptions = notifications_get_subscriptions(array('type' => $type, 'event_type' => $info['event_type'], 'uid' => $account->uid), array(), FALSE, 'value');
  }
  $defaults += array(
    'sid' => 0,
    'type' => $type,
    'event_type' => $info['event_type'],
  );
  $defaults += _notifications_subscription_defaults($account);
  // Hide Send method column if only one
  $send_methods = _notifications_send_methods();
  $header = array(theme('table_select_header_cell'), $field_title, t('Send interval'));
  if (count($send_methods) > 1) {
    $header[] = t('Send method');
  }  
  $form['defaults'] = array('#type' => 'value', '#value' => $defaults);  
  $form['account'] = array('#type' => 'value', '#value' => $account);
  $form['current'] = array('#type' => 'value', '#value' => $subscriptions);
  $form['subscription_fields'] = array('#type' => 'value', '#value' => array());
  $form['subscriptions'] = array(
    '#tree' => TRUE,
    '#theme' => 'notifications_form_table',
    '#header' => $header,
  );
  $send_intervals = notifications_send_intervals();
  foreach ($list as $key => $title) {
    $rowdefaults = isset($subscriptions[$key]) ? (array)($subscriptions[$key]) : $defaults;
    $rowdefaults += $rowdefaults;
    $form['subscriptions']['checkbox'][$key] = array(
      '#type' => 'checkbox',
      '#default_value' => $rowdefaults['sid'],
    );
    $form['subscriptions']['title'][$key] = array(
      '#value' => $title,
    );
    $form['subscriptions']['send_interval'][$key] = array(
      '#type' => 'select',
      '#options' => $send_intervals,
      '#default_value' => $rowdefaults['send_interval'],
    );
    // Hide send methods if only one available
    if (count($send_methods) > 1) {
      $form['subscriptions']['send_method'][$key] = array(
        '#type' => 'select',
        '#options' => _notifications_send_methods(),
        '#default_value' => $rowdefaults['send_method'],
      );
    }
    else {
      $form['subscriptions']['send_method'][$key] = array('#type' => 'value', '#value' => $rowdefaults['send_method']);
    }
    // Pass on the fields for processing
    $form['subscription_fields']['#value'][$key] = array($field => $key);
  
  }
  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));

  return $form; 
}

/**
 * Process generic form submission
 */
function notifications_user_form_submit($form, &$form_state) {
  $form_values = $form_state['values'];
  $account = $form_values['account'];
  $current = $form_values['current'];
  $defaults = $form_values['defaults'];
  $defaults += array('uid' => $account->uid);
  $fields = $form_values['subscription_fields'];
  $values = $form_values['subscriptions'];
  $check = 'checkbox';

  foreach ($values[$check] as $index => $value) {
    $subscription = NULL;
    if ($value) {
      // Checked, save only if new or changed
      if (!isset($current[$index])) {
        $subscription = $defaults;
      }
      elseif ($current[$index]->send_interval != $values['send_interval'][$index] || $current[$index]->send_method != $values['send_method'][$index]) {
        $subscription = (array)($current[$index]);        
      }
      // Complete and save
      if ($subscription) {
        $subscription['send_interval'] = $values['send_interval'][$index];
        $subscription['send_method'] = $values['send_method'][$index];
        $subscription['fields'] = $fields[$index];
        notifications_save_subscription($subscription);
      }
    }
    elseif (isset($current[$index])) {
      notifications_delete_subscription($current[$index]->sid);
    }
  }
}

/**
 * Display a form field for a notifications_field
 */
function notifications_subscription_form_field($type, $value = NULL, $subscription_type = NULL) {
  return Notifications_Subscription::form_field($type, $value, $subscription_type);
}

/**
 * Format subscription for display
 * 
 * Just for backwards compatiblity, we use Notifications_Subscription formatting functions now
 */
function notifications_format_subscription($subscription, $format = 'short', $html = TRUE) {
  // Build object if not built previously
  $subscription = notifications_build_subscription($subscription);
  $method = 'format_' . $format;
  return $subscription->$method($html);
}

/**
 * Implementation of hook_theme()
 */
function notifications_theme() {
  return array(
    'notifications_form_table' => array(
      'arguments' => array('element' => NULL),
      'file' => 'notifications.admin.inc',
    ),
    'notifications_send_intervals_form' => array(
      'arguments' => array('element' => NULL),
      'file' => 'notifications.admin.inc',
    ),
    'notifications_manage_subscriptions' => array(
      'arguments' => array('form' => NULL),
      'file' => 'notifications.manage.inc',
    ),
    'notifications_subscriptions_filter_form' => array(
      'arguments' => array('form' => NULL),
      'file' => 'notifications.manage.inc',
    ),
    'notifications_table_form' => array(
      'arguments' => array('form' => NULL),
      'file' => 'notifications.admin.inc',
    ),
    'notifications_subscription_fields' => array(
      'arguments' => array('form' => NULL),
      'file' => 'notifications.pages.inc',
    ),
  );
}

/**
 * Short hand for info logs
 */
function notifications_log($message = NULL, $variables = NULL) {
  return messaging_log($message, $variables);
}

/**
 * Short hand for debug logs
 */
function notifications_debug($message = NULL, $variables = NULL) {
  return messaging_debug($message, $variables);
}

/**   
 * Wrapper function for 1i8nstrings() if i18nstrings enabled.   
 */   
function notifications_translate($name, $string, $langcode = NULL, $textgroup = 'notifications') {
  return function_exists('i18nstrings') ? i18nstrings($textgroup . ':' . $name, $string, $langcode) : $string;  
}

/**
 * Implementation of hook_locale().
 */
function notifications_locale($op = 'groups') {
  switch ($op) {
    case 'groups':
      return array('notifications' => t('Notifications'));
    case 'info':
      $info['notifications']['refresh callback'] = 'notifications_locale_refresh';
      $info['notifications']['format'] = FALSE; // Strings have no format
      return $info;
  }
}

/**
 * Refresh notifications strings
 */
function notifications_locale_refresh() {
  if ($intervals = variable_get('notifications_send_intervals', FALSE)) {
    foreach ($intervals as $key => $name) {
      i18nstrings_update("notifications:send_interval:$key:name", $name);
    }
  }
  return TRUE;
}

/**
 * Include module files as necessary.
 * 
 * The files must be in an 'includes' subfolder inside the module folder. 
 */
function notifications_include($file, $module = 'notifications') {
  return messaging_include($file, $module);
}

/**
 * Invoke hook_notifications($name) on all modules
 * 
 * This is like module_invoke all with some differences:
 * - The results are just merged (not recursively)
 * - The module name is added to each resulting array
 * - We cache all the results 
 */
function &notifications_info($name, $param = NULL, $refresh = FALSE) {
  $info = &messaging_static('notifications_info_' . $name);
  if (!isset($info) || $refresh) {
    $info = array();
    foreach (module_implements('notifications') as $module) {
      $function = $module .'_notifications';
      if ($return = $function($name, $param)) {
        // Add module name to each result if it is an info array
        foreach ($return as $key => &$value) {
          if (is_array($value)) {
            $value += array('module' => $module);
          }
        }
        // Note we use array_merge instead of array_merge_recursive()
        $info = array_merge($info, $return);
      }
    }
    // Provide alter hook
    drupal_alter('notifications_' . strtr(' ', '_', $name), $info);
  }      
  return $info;
}