You are here

push_notifications.module in Push Notifications 7

Same filename and directory in other branches
  1. 8 push_notifications.module

Push Notifications functionality.

File

push_notifications.module
View source
<?php

/**
 * @file
 * Push Notifications functionality.
 */

/**
 * Constants Definition.
 */

//
// Device Types.
//
define('PUSH_NOTIFICATIONS_TYPE_ID_IOS', variable_get('push_notifications_type_id_ios', 0));
define('PUSH_NOTIFICATIONS_TYPE_ID_ANDROID', variable_get('push_notifications_type_id_anroid', 1));

//
// Apple Variables
//
// Apple Server
define('PUSH_NOTIFICATIONS_APNS_HOST', variable_get('push_notifications_apns_host', 'gateway.push.apple.com'));

// Apple Server port.
define('PUSH_NOTIFICATIONS_APNS_PORT', 2195);

// Apple Feedback Server, initially set to development server.
define('PUSH_NOTIFICATIONS_APNS_FEEDBACK_HOST', variable_get('push_notifications_apns_feedback_host', 'feedback.push.apple.com'));

// Apple Feedback Server port.
define('PUSH_NOTIFICATIONS_APNS_FEEDBACK_PORT', 2196);

// Random suffix for certificate string.
define('PUSH_NOTIFICATIONS_APNS_CERTIFICATE_RANDOM', variable_get('push_notifications_apns_certificate_random', ''));

// Name of certificate, initially set to development certificate.
define('PUSH_NOTIFICATIONS_APNS_CERTIFICATE', variable_get('push_notifications_apns_certificate', 'apns-production' . PUSH_NOTIFICATIONS_APNS_CERTIFICATE_RANDOM . '.pem'));

// Size limit for individual payload, in bytes.
define('PUSH_NOTIFICATIONS_APNS_PAYLOAD_SIZE_LIMIT', 2048);

// Payload sound
define('PUSH_NOTIFICATIONS_APNS_NOTIFICATION_SOUND', variable_get('push_notifications_apns_notification_sound', 'default'));

// Boolean value to indicate wether Apple's feedback service should be called
// on cron to remove unused tokens from our database.
define('PUSH_NOTIFICATIONS_APNS_QUERY_FEEDBACK_SERVICE', variable_get('push_notifications_apns_query_feedback_service', 1));

// Maximum of messages to send per stream context.
define('PUSH_NOTIFICATIONS_APNS_STREAM_CONTEXT_LIMIT', variable_get('push_notifications_apns_stream_context_limit', 1));

//
// Google Variables
//
// Google Push Notification Types
// 0 => Cloud 2 Device Messaging
// 1 => Google Cloud Messaging
define('PUSH_NOTIFICATIONS_GOOGLE_TYPE_C2DM', 0);
define('PUSH_NOTIFICATIONS_GOOGLE_TYPE_GCM', 1);
define('PUSH_NOTIFICATIONS_GOOGLE_TYPE_FCM', 2);
define('PUSH_NOTIFICATIONS_GOOGLE_TYPE', variable_get('push_notifications_google_type', PUSH_NOTIFICATIONS_GOOGLE_TYPE_GCM));

//
// C2DM Variables
//
// C2DM Credentials.
define('PUSH_NOTIFICATIONS_C2DM_USERNAME', variable_get('push_notifications_c2dm_username', ''));
define('PUSH_NOTIFICATIONS_C2DM_PASSWORD', variable_get('push_notifications_c2dm_password', ''));
define('PUSH_NOTIFICATIONS_C2DM_CLIENT_LOGIN_ACTION_URL', variable_get('push_notifications_c2dm_client_login_action_url', 'https://www.google.com/accounts/ClientLogin'));

// C2DM Server Post URL
define('PUSH_NOTIFICATIONS_C2DM_SERVER_POST_URL', variable_get('push_notifications_c2dm_server_post_url', 'https://android.apis.google.com/c2dm/send'));

//
// GCM Variables
//
// GCM API KEY Credentials.
define('PUSH_NOTIFICATIONS_GCM_API_KEY', variable_get('push_notifications_gcm_api_key', ''));

// GCM Server Post URL
define('PUSH_NOTIFICATIONS_GCM_SERVER_POST_URL', variable_get('push_notifications_gcm_server_post_url', 'https://android.googleapis.com/gcm/send'));

//
// FCM Variables
//
// FCM Cloud Messaging TokenSender ID
define('PUSH_NOTIFICATIONS_FCM_CLOUD_MESSAGING_TOKEN', variable_get('push_notifications_fcm_cloud_messaging_token', ''));

// FCM Cloud Messaging Server Key
define('PUSH_NOTIFICATIONS_FCM_SERVER_KEY', variable_get('push_notifications_fcm_server_key', ''));

// FCM Server Post URL
define('PUSH_NOTIFICATIONS_FCM_SERVER_POST_URL', variable_get('push_notifications_fcm_server_post_url', 'https://fcm.googleapis.com/fcm/send'));

/**
 * Implements of hook_menu().
 */
function push_notifications_menu() {
  $items = array();
  $items['admin/config/services/push_notifications'] = array(
    'type' => MENU_NORMAL_ITEM,
    'title' => 'Push Notifications',
    'access arguments' => array(
      'send push notifications',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'push_notifications_admin_overview_form',
    ),
    'description' => 'Push Notifications Settings.',
    'file' => 'push_notifications.admin.inc',
    'file path' => drupal_get_path('module', 'push_notifications') . '/includes',
  );
  $items['admin/config/services/push_notifications/overview'] = array(
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'title' => 'Overview',
    'weight' => -50,
    'access arguments' => array(
      'send push notifications',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'push_notifications_admin_overview_form',
    ),
    'description' => 'Push Notifications Settings.',
    'file' => 'push_notifications.admin.inc',
    'file path' => drupal_get_path('module', 'push_notifications') . '/includes',
  );
  $items['admin/config/services/push_notifications/configure'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Configuration',
    'access arguments' => array(
      'administer site configuration',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'push_notifications_admin_form',
    ),
    'description' => 'Push Notifications Settings.',
    'file' => 'push_notifications.admin.inc',
    'file path' => drupal_get_path('module', 'push_notifications') . '/includes',
  );
  $items['admin/config/services/push_notifications/message'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Send Push',
    'access arguments' => array(
      'send push notifications',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'push_notifications_mass_push_form',
    ),
    'description' => 'Send a Push Notification',
    'file' => 'push_notifications.admin.inc',
    'file path' => drupal_get_path('module', 'push_notifications') . '/includes',
  );
  return $items;
}

/**
 * Implements of hook_cron().
 */
function push_notifications_cron() {
  if (PUSH_NOTIFICATIONS_APNS_QUERY_FEEDBACK_SERVICE) {
    push_notifications_apns_feedback_service();
  }
}

/**
 * Implements hook_permission().
 */
function push_notifications_permission() {
  return array(
    'register device token' => array(
      'title' => t('Register device token'),
      'description' => t('Allows users to register a device token.'),
    ),
    'remove device token' => array(
      'title' => t('Remove device token'),
      'description' => t('Allows users to remove a device token.'),
    ),
    'send push notifications' => array(
      'title' => t('Send push notifications'),
      'description' => t('Allow users to send push notifications to devices.'),
    ),
    'view device tokens' => array(
      'title' => t('View device tokens'),
      'description' => t('Allow users to view device tokens.'),
    ),
  );
}

/**
 * Implements hook_services_resources().
 */
function push_notifications_services_resources() {
  return array(
    'push_notifications' => array(
      'create' => array(
        'help' => 'Registers a device token. For type, pass \'ios\' for iOS devices and \'android\' for Android devices.',
        'callback' => '_push_notifications_service_create_device_token',
        'file' => array(
          'type' => 'inc',
          'module' => 'push_notifications',
          'name' => 'includes/push_notifications.service',
        ),
        'access arguments' => array(
          'register device token',
        ),
        'access arguments append' => FALSE,
        'args' => array(
          array(
            'name' => 'token',
            'type' => 'string',
            'description' => 'Device Token',
            'optional' => FALSE,
            'source' => 'data',
          ),
          array(
            'name' => 'type',
            'type' => 'string',
            'description' => 'Device Type',
            'optional' => FALSE,
            'source' => 'data',
          ),
          array(
            'name' => 'language',
            'type' => 'string',
            'description' => 'Language',
            'optional' => TRUE,
            'source' => 'data',
          ),
        ),
      ),
      'delete' => array(
        'help' => 'Removes a registered a device token. Only needs the token.',
        'callback' => '_push_notifications_service_delete_device_token',
        'file' => array(
          'type' => 'inc',
          'module' => 'push_notifications',
          'name' => 'includes/push_notifications.service',
        ),
        'access arguments' => array(
          'remove device token',
        ),
        'access arguments append' => FALSE,
        'args' => array(
          array(
            'name' => 'token',
            'type' => 'string',
            'description' => 'Device Token',
            'optional' => FALSE,
            'source' => array(
              'path' => '0',
            ),
          ),
        ),
      ),
    ),
  );
}

/**
 * Implements of hook_views_api().
 */
function push_notifications_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'push_notifications') . '/views',
  );
}

/**
 * Implements hook_privatemsg_message_insert.
 */
function push_notifications_privatemsg_message_insert($message) {
  if (variable_get('push_notifications_privatemsg_integration', 0)) {

    // Compose the payload. If the body is empty, just use the subject line.
    // Otherwise, combine subject and body.
    $payload = empty($message->body) ? $message->subject : $message->subject . ' ' . $message->body;
    $payload = 'From ' . $message->author->name . ': ' . $payload;

    // Compose an array of recipients.
    $recipients = array();
    foreach ($message->recipients as $recipient) {
      if ($recipient->type == "role" && $recipient->name != 'authenticated user') {
        $results = db_select('users_roles', 'ur')
          ->fields('ur', array(
          'uid',
        ))
          ->condition('ur.rid', $recipient->rid, '=')
          ->execute()
          ->fetchCol();
      }
      elseif ($recipient->type == "role" && $recipient->name == 'authenticated user') {
        $results = db_select('users', 'u')
          ->fields('u')
          ->execute()
          ->fetchCol();
      }
      if ($recipient->type == "role") {
        foreach ($results as $result) {
          $recipients[] = $result;
        }
      }
      else {
        $recipients[] = $recipient->uid;
      }
    }
    push_notifications_send_message($recipients, $payload);
  }
}

/**
 * Determine if this user has already stored a token
 * in the database. The same device token can be
 * registered for multiple users, because multiple
 * users can login from the same device.
 *
 * @param $token
 *   Device Token.
 * @param $uid
 *   User ID.
 * @param $exclude
 *   Set this to true to find (at least one) other user(s) who have this
 *   token stored. Optional, defaults to false.
 *
 * @return
 *   User ID of token, if found.
 */
function push_notifications_find_token($token = '', $uid = '', $exclude = FALSE) {
  if ($token == '') {
    return FALSE;
  }
  $query = db_select('push_notifications_tokens', 'pnt');
  $query
    ->fields('pnt', array(
    'token',
  ));
  $query
    ->condition('pnt.token', $token);
  if ($exclude) {
    $query
      ->condition('pnt.uid', $uid, '!=');
    $query
      ->range(0, 1);
  }
  else {
    $query
      ->condition('pnt.uid', $uid);
  }
  $result = $query
    ->execute();
  return $result
    ->fetchField();
}

/**
 * Store a token in the database.
 * Removes all spaces in the token.
 *
 * @param $token
 *   Device token.
 * @param $type_id
 *   Device type id.
 * @param $uid
 *   User ID.
 * @param $language
 *   Language that this token is registered for, optional.
 *
 * @return
 *   Failure to write a record will return FALSE, Otherwise SAVED_NEW.
 */
function push_notifications_store_token($token = '', $type_id = '', $uid = '', $language = '') {

  // Let modules modify the token before it is saved to the database.
  foreach (module_implements('push_notifications_store_token') as $module) {
    $function = $module . '_push_notifications_store_token';
    $function($token, $type_id, $uid);
  }

  // Invoke rules.
  if (module_exists('rules')) {
    rules_invoke_event_by_args('push_notifications_before_token_insert', array(
      'token' => $token,
      'type_id' => $type_id,
      'uid' => $uid,
      'language' => $language,
    ));
  }
  if (!is_string($token) || !is_numeric($type_id) || !is_numeric($uid)) {
    return FALSE;
  }

  // Default language to site default.
  if ($language == '') {
    $default_language = language_default();
    $language = $default_language->language;
  }

  // Write record.
  $table = 'push_notifications_tokens';
  $record = new stdClass();
  $record->token = $token;
  $record->uid = $uid;
  $record->type = $type_id;
  $record->language = $language;
  $record->timestamp = time();
  $result = drupal_write_record($table, $record);

  // Allow modules to react to the newly created record after creation.
  foreach (module_implements('push_notifications_post_store_token') as $module) {
    $function = $module . '_push_notifications_post_store_token';
    $function($record);
  }

  // Invoke rules.
  if (module_exists('rules')) {
    rules_invoke_event_by_args('push_notifications_after_token_insert', array(
      'token' => $token,
      'type_id' => $type_id,
      'uid' => $uid,
      'language' => $language,
      'result' => $result,
    ));
  }
  return $result;
}

/**
 * Open an APNS connection.
 * Should be closed by calling fclose($connection) after usage.
 */
function push_notifications_open_apns() {

  // Determine the absolute path of the certificate.
  // @see http://stackoverflow.com/questions/809682
  $apns_cert = _push_notifications_get_apns_certificate();

  // Create a stream context.
  $stream_context = stream_context_create();

  // Set options on the stream context.
  stream_context_set_option($stream_context, 'ssl', 'local_cert', $apns_cert);

  // If the user has a passphrase stored, we use it.
  $passphrase = variable_get('push_notifications_apns_passphrase', '');
  if (strlen($passphrase)) {
    stream_context_set_option($stream_context, 'ssl', 'passphrase', $passphrase);
  }
  if (variable_get('push_notifications_set_entrust_certificate', FALSE)) {
    stream_context_set_option($stream_context, 'ssl', 'CAfile', drupal_get_path('module', 'push_notifications') . '/certificates/entrust_2048_ca.cer');
  }

  // Open an Internet socket connection.
  $apns = stream_socket_client('ssl://' . PUSH_NOTIFICATIONS_APNS_HOST . ':' . PUSH_NOTIFICATIONS_APNS_PORT, $error, $error_string, 2, STREAM_CLIENT_CONNECT, $stream_context);
  if (!$apns) {
    watchdog('push_notifications', 'Connection to Apple Notification Server failed.', NULL, WATCHDOG_ERROR);
    return FALSE;
  }
  else {
    return $apns;
  }
}

/**
 * Check size of a push notification payload.
 * Payload can't exceed PUSH_NOTIFICATIONS_APNS_PAYLOAD_SIZE_LIMIT.
 *
 * @param $payload
 *   Message.
 *
 * @return
 *   Returns true if message is below the limit, false otherwise.
 */
function push_notifications_check_payload_size($payload = '') {
  if ($payload == '') {
    return FALSE;
  }

  // JSON-encode the payload.
  $payload = json_encode($payload);

  // Verify that the payload doesn't exceed limit.
  $payload_size = mb_strlen($payload, '8bit');
  $size_valid = $payload_size > PUSH_NOTIFICATIONS_APNS_PAYLOAD_SIZE_LIMIT ? FALSE : TRUE;
  return $size_valid;
}

/**
 * Send a simple message alert to an array of recipients.
 *
 * @param array $recipients Array of user ids.
 * @param string $message Message to be included in payload.
 * @return mixed Flag to indicate if delivery was successful.
 */
function push_notifications_send_message($recipients, $message) {
  if (!is_array($recipients) || !is_string($message)) {
    return FALSE;
  }

  // Let modules modify the message before it is sent.
  foreach (module_implements('push_notifications_send_message') as $module) {
    $function = $module . '_push_notifications_send_message';
    $function($message, $type = 'simple');
  }

  // Shorten the message characters / 8 bit.
  $message = truncate_utf8($message, PUSH_NOTIFICATIONS_APNS_PAYLOAD_SIZE_LIMIT, TRUE, TRUE);

  // Convert the payload into the correct format for delivery.
  $payload = array(
    'alert' => $message,
  );

  // Determine if any of the recipients have one or multiple tokens stored.
  $tokens = array();
  foreach ($recipients as $uid) {
    $user_tokens = push_notification_get_user_tokens($uid);
    if (!empty($user_tokens)) {
      $tokens = array_merge($tokens, $user_tokens);
    }
  }

  // Stop right here if none of these users have any tokens.
  if (empty($tokens)) {
    return FALSE;
  }

  // Send a simple alert message.
  push_notifications_send_alert($tokens, $payload);
}

/**
 * Send a simple message alert to a single account.
 *
 * @param object $account User account.
 * @param string $message Message to be included in payload.
 * @return mixed
 */
function push_notifications_send_message_account($account, $message) {
  if (!is_object($account) || !is_numeric($account->uid)) {
    watchdog('push_notifications', t('Not a valid account object.'));
    return false;
  }
  push_notifications_send_message(array(
    $account->uid,
  ), $message);
}

/**
 * Send a simple message alert to all tokens in the database.
 *
 * @param string $message Message to be included in payload.
 * @param string $target_group Defines the target group for this message.
 *   Valid options are:
 *   - any
 *   - authenticated
 *   - anonymous
 * @return mixed Flag indicating if message delivery was successful.
 */
function push_notifications_send_message_bulk($message = '', $target_group = '') {
  if (empty($message)) {
    watchdog('push_notifications', t('Message cannot be empty'));
    return false;
  }

  // Get all tokens in the system.
  $tokens = push_notifications_get_tokens(array(
    'account_type' => $target_group,
  ), true);

  // Let modules modify the message before it is sent.
  foreach (module_implements('push_notifications_send_message') as $module) {
    $function = $module . '_push_notifications_send_message';
    $function($message, $type = 'bulk');
  }

  // Shorten the message characters / 8 bit.
  $message = truncate_utf8($message, PUSH_NOTIFICATIONS_APNS_PAYLOAD_SIZE_LIMIT, TRUE, TRUE);

  // Convert the payload into the correct format for delivery.
  $payload = array(
    'alert' => $message,
  );

  // Stop right here if none of these users have any tokens.
  if (empty($tokens)) {
    return FALSE;
  }

  // Send out alert.
  push_notifications_send_alert($tokens, $payload);
}

/**
 * Handle delivery of simple alert message.
 *
 * @param array $tokens Array of token record objects.
 * @param array $payload Payload.
 *
 */
function push_notifications_send_alert($tokens = array(), $payload = array()) {

  // Group tokens into types.
  $tokens_ios = array();
  $tokens_android = array();
  foreach ($tokens as $token) {
    switch ($token->type) {
      case PUSH_NOTIFICATIONS_TYPE_ID_IOS:
        $tokens_ios[] = $token->token;
        break;
      case PUSH_NOTIFICATIONS_TYPE_ID_ANDROID:
        $tokens_android[] = $token->token;
        break;
    }
  }

  // Send payload to iOS recipients.
  if (!empty($tokens_ios)) {

    // Convert the payload into the correct format for APNS.
    $payload_apns = array(
      'aps' => $payload,
    );
    push_notifications_apns_send_message($tokens_ios, $payload_apns);
  }

  // Send payload to Android recipients if configured correctly.
  if (!empty($tokens_android) && (PUSH_NOTIFICATIONS_C2DM_USERNAME && PUSH_NOTIFICATIONS_C2DM_PASSWORD || PUSH_NOTIFICATIONS_GCM_API_KEY || PUSH_NOTIFICATIONS_FCM_SERVER_KEY)) {

    // Determine which method to use for Google push notifications.
    switch (PUSH_NOTIFICATIONS_GOOGLE_TYPE) {
      case PUSH_NOTIFICATIONS_GOOGLE_TYPE_C2DM:
        push_notifications_c2dm_send_message($tokens_android, $payload);
        break;
      case PUSH_NOTIFICATIONS_GOOGLE_TYPE_GCM:
        push_notifications_gcm_send_message($tokens_android, $payload);
        break;
      case PUSH_NOTIFICATIONS_GOOGLE_TYPE_FCM:
        push_notifications_fcm_send_message($tokens_android, $payload);
        break;
    }
  }
}

/**
 * Send out push notifications through APNS.
 *
 * @param $tokens
 *   Array of iOS tokens
 * @param $payload
 *   Payload to send. Minimum requirement
 *   is a nested array in this format:
 *   $payload = array(
 *     'aps' => array(
 *       'alert' => 'Push Notification Test',
 *     );
 *   );
 *
 * @return
 *   Array with the following keys:
 *   - count_attempted (# of attempted messages sent)
 *   - count_success   (# of successful sends)
 *   - success         (# boolean)
 *   - message         (Prepared result message)
 */
function push_notifications_apns_send_message($tokens, $payload) {
  if (!is_array($tokens) || empty($payload) || is_array($tokens) && empty($tokens)) {
    return FALSE;
  }
  $payload_apns = array();

  // Allow for inclusion of custom payloads.
  foreach ($payload as $key => $value) {
    if ($key != 'aps') {
      $payload_apns[$key] = $value;
    }
  }

  // Add the default 'aps' key for the payload.
  $payload_apns['aps'] = $payload['aps'];

  // Set the default sound if no sound was set.
  if (!isset($payload_apns['aps']['sound'])) {
    $payload_apns['aps']['sound'] = PUSH_NOTIFICATIONS_APNS_NOTIFICATION_SOUND;
  }

  // JSON-encode the payload.
  $payload_apns = json_encode($payload_apns);
  $result = array(
    'count_attempted' => 0,
    'count_success' => 0,
    'success' => 0,
    'message' => '',
  );

  // Send a push notification to every recipient.
  $stream_counter = 0;
  foreach ($tokens as $token) {

    // Open an apns connection, if necessary.
    if ($stream_counter == 0) {
      $apns = push_notifications_open_apns();
      if (!$apns) {
        $result['message'] = t('APNS connection could not be established. Check to make sure you are using a valid certificate file.');
        return $result;
      }
    }
    $stream_counter++;
    $result['count_attempted']++;
    $apns_message = chr(0) . chr(0) . chr(32) . pack('H*', $token) . pack('n', strlen($payload_apns)) . $payload_apns;

    // Write the payload to the currently active streaming connection.
    $success = fwrite($apns, $apns_message);
    if ($success) {
      $result['count_success']++;
    }
    elseif ($success == 0 || $success == FALSE || $success < strlen($apns_message)) {
      $error_message = array(
        'message' => t('APNS message could not be sent.'),
        'token' => 'Token: ' . $token,
        'data' => 'fwrite returned: ' . $success,
      );
      watchdog('push_notifications', implode($error_message, '<br />'));
    }

    // Reset the stream counter if no more messages should
    // be sent with the current stream context.
    // This results in the generation of a new stream context
    // at the beginning of this loop.
    if ($stream_counter >= PUSH_NOTIFICATIONS_APNS_STREAM_CONTEXT_LIMIT) {
      $stream_counter = 0;
      if (is_resource($apns)) {
        fclose($apns);
      }
    }
  }

  // Close the apns connection if it hasn't already been closed.
  // Need to check if $apns is a resource, as pointer will not
  // be closed by fclose.
  if (is_resource($apns)) {
    fclose($apns);
  }
  $result['message'] = t('Successfully sent !count_success iOS push messages (attempted to send !count messages).', array(
    '!count_success' => $result['count_success'],
    '!count' => $result['count_attempted'],
  ));
  $result['success'] = TRUE;

  // Invoke rules.
  if (module_exists('rules')) {
    rules_invoke_event_by_args('push_notifications_after_apns_send', array(
      'type_id' => PUSH_NOTIFICATIONS_TYPE_ID_IOS,
      'payload' => $payload,
      'count_attempted' => $result['count_attempted'],
      'count_success' => $result['count_success'],
      'success' => $result['success'],
      'result_message' => $result['message'],
    ));
  }
  return $result;
}

/**
 * Determine the auth string from C2DM server.
 */
function push_notifications_c2dm_token() {
  $data = array(
    'Email' => PUSH_NOTIFICATIONS_C2DM_USERNAME,
    'Passwd' => PUSH_NOTIFICATIONS_C2DM_PASSWORD,
    'accountType' => 'HOSTED_OR_GOOGLE',
    'source' => 'Company-AppName-Version',
    'service' => 'ac2dm',
  );
  $curl = curl_init();
  curl_setopt($curl, CURLOPT_URL, PUSH_NOTIFICATIONS_C2DM_CLIENT_LOGIN_ACTION_URL);
  curl_setopt($curl, CURLOPT_FOLLOWLOCATION, TRUE);
  curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
  curl_setopt($curl, CURLOPT_POST, TRUE);
  curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
  curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
  $response = curl_exec($curl);
  curl_close($curl);

  // Get the auth token.
  preg_match("/Auth=([a-z0-9_\\-]+)/i", $response, $matches);
  $auth_token = $matches[1];
  if (!$auth_token) {
    watchdog('push_notifications', 'Google C2DM Server did not provide an authentication token.', NULL, WATCHDOG_ERROR);
  }
  else {
    return $auth_token;
  }
}

/**
 * Send out push notifications through C2DM.
 *
 * @param $tokens
 *   Array of iOS tokens
 * @param $payload
 *   Payload to send.
 *
 * @return
 *   Array with the following keys:
 *   - count_attempted (# of attempted messages sent)
 *   - count_success   (# of successful sends)
 *   - success         (# boolean)
 *   - message         (Prepared result message)
 */
function push_notifications_c2dm_send_message($tokens, $payload) {
  if (!is_array($tokens) || empty($payload) || is_array($tokens) && empty($tokens)) {
    return FALSE;
  }

  // Determine an updated authentication token.
  // Google is very vague about how often this token changes,
  // so we'll just get a new token every time.
  $auth_token = push_notifications_c2dm_token();
  if (!$auth_token) {
    $result['message'] = t('Google C2DM Server did not provide an authentication token. Check your C2DM credentials.');
    return $result;
  }

  // Define an array of result values.
  $result = array(
    'count_attempted' => 0,
    'count_success' => 0,
    'success' => 0,
    'message' => '',
  );

  // Define the header.
  $headers = array();
  $headers[] = 'Authorization: GoogleLogin auth=' . $auth_token;

  // Send a push notification to every recipient.
  foreach ($tokens as $token) {
    $result['count_attempted']++;

    // Convert the payload into the correct format for C2DM payloads.
    // Prefill an array with values from other modules first.
    $data = array();
    foreach ($payload as $key => $value) {
      if ($key != 'alert') {
        $data['data.' . $key] = $value;
      }
    }

    // Fill the default values required for each payload.
    $data['registration_id'] = $token;
    $data['collapse_key'] = time();
    $data['data.message'] = $payload['alert'];
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, PUSH_NOTIFICATIONS_C2DM_SERVER_POST_URL);
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($curl, CURLOPT_POST, TRUE);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    $response = curl_exec($curl);
    $info = curl_getinfo($curl);
    curl_close($curl);

    // If Google's server returns a reply, but that reply includes an error, log the error message.
    if ($info['http_code'] == 200 && (isset($response) && preg_match('/Error/', $response))) {
      watchdog('push_notifications', "Google's Server returned an error: " . $response, NULL, WATCHDOG_ERROR);

      // If the device token is invalid or not registered (anymore because the user
      // has uninstalled the application), remove this device token.
      if (preg_match('/InvalidRegistration/', $response) || preg_match('/NotRegistered/', $response)) {
        push_notifications_purge_token($token, PUSH_NOTIFICATIONS_TYPE_ID_ANDROID);
        watchdog('daddyhunt_apns', 'C2DM token not valid anymore. Removing token ' . $token);
      }
    }

    // Success if the http response status is 200 and the response
    // data does not containt the word "Error".
    if ($info['http_code'] == 200 && (isset($response) && !preg_match('/Error/', $response))) {
      $result['count_success']++;
    }
  }
  $result['message'] = t('Successfully sent !count_success Android push messages (attempted to send !count messages).', array(
    '!count_success' => $result['count_success'],
    '!count' => $result['count_attempted'],
  ));
  $result['success'] = TRUE;

  // Invoke rules.
  if (module_exists('rules')) {
    rules_invoke_event_by_args('push_notifications_after_c2dm_send', array(
      'type_id' => PUSH_NOTIFICATIONS_TYPE_ID_ANDROID,
      'payload' => $payload,
      'count_attempted' => $result['count_attempted'],
      'count_success' => $result['count_success'],
      'success' => $result['success'],
      'result_message' => $result['message'],
    ));
  }
  return $result;
}

/**
 * Send out push notifications through GCM.
 *
 * @link http://developer.android.com/guide/google/gcm/index.html
 *
 * @param $tokens
 *   Array of gcm tokens
 * @param $payload
 *   Payload to send.
 *
 * @return
 *   Array with the following keys:
 *   - count_attempted (# of attempted messages sent)
 *   - count_success   (# of successful sends)
 *   - success         (# boolean)
 *   - message         (Prepared result message)
 */
function push_notifications_gcm_send_message($tokens, $payload) {
  if (!is_array($tokens) || empty($payload) || is_array($tokens) && empty($tokens)) {
    return FALSE;
  }

  // Define an array of result values.
  $result = array(
    'count_attempted' => 0,
    'count_success' => 0,
    'success' => 0,
    'message' => '',
  );

  // Define the header.
  $headers = array();
  $headers[] = 'Content-Type:application/json';
  $headers[] = 'Authorization:key=' . PUSH_NOTIFICATIONS_GCM_API_KEY;

  // Check of many token bundles can be build.
  $token_bundles = ceil(count($tokens) / 1000);
  $result['count_attempted'] = count($tokens);

  // Send a push notification to every recipient.
  for ($i = 0; $i < $token_bundles; $i++) {

    // Create a token bundle.
    $bundle_tokens = array_slice($tokens, $i * 1000, 1000, FALSE);

    // Convert the payload into the correct format for C2DM payloads.
    // Prefill an array with values from other modules first.
    $data = array();
    foreach ($payload as $key => $value) {
      if ($key != 'alert') {
        $data['data'][$key] = $value;
      }
    }

    // Fill the default values required for each payload.
    $data['registration_ids'] = $bundle_tokens;
    $data['collapse_key'] = (string) time();
    $data['data']['message'] = $payload['alert'];
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, PUSH_NOTIFICATIONS_GCM_SERVER_POST_URL);
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($curl, CURLOPT_POST, TRUE);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
    $response_raw = curl_exec($curl);
    $info = curl_getinfo($curl);
    curl_close($curl);
    $response = FALSE;
    if (isset($response_raw)) {
      $response = json_decode($response_raw);
    }

    // If Google's server returns a reply, but that reply includes an error,
    // log the error message.
    if ($info['http_code'] == 200 && !empty($response->failure)) {
      watchdog('push_notifications', "Google's Server returned an error: " . $response_raw, NULL, WATCHDOG_ERROR);

      // Analyze the failure.
      foreach ($response->results as $token_index => $message_result) {
        if (!empty($message_result->error)) {

          // If the device token is invalid or not registered (anymore because the user
          // has uninstalled the application), remove this device token.
          if ($message_result->error == 'NotRegistered' || $message_result->error == 'InvalidRegistration') {
            push_notifications_purge_token($bundle_tokens[$token_index], PUSH_NOTIFICATIONS_TYPE_ID_ANDROID);
            watchdog('push_notifications', 'GCM token not valid anymore. Removing token ' . $bundle_tokens[$token_index]);
          }
        }
      }
    }

    // Count the successful sent push notifications if there are any.
    if ($info['http_code'] == 200 && !empty($response->success)) {
      $result['count_success'] += $response->success;
    }
  }
  $result['message'] = t('Successfully sent !count_success Android push messages (attempted to send !count messages).', array(
    '!count_success' => $result['count_success'],
    '!count' => $result['count_attempted'],
  ));
  $result['success'] = TRUE;

  // Invoke rules.
  if (module_exists('rules')) {
    rules_invoke_event_by_args('push_notifications_after_gcm_send', array(
      'type_id' => PUSH_NOTIFICATIONS_TYPE_ID_ANDROID,
      'payload' => $payload,
      'count_attempted' => $result['count_attempted'],
      'count_success' => $result['count_success'],
      'success' => $result['success'],
      'result_message' => $result['message'],
    ));
  }
  return $result;
}

/**
 * Send out push notifications through FCM.
 *
 * @link https://firebase.google.com/docs/cloud-messaging/
 *
 * @param $tokens
 *   Array of fcm tokens
 * @param $payload
 *   Payload to send.
 *
 * @return
 *   Array with the following keys:
 *   - count_attempted (# of attempted messages sent)
 *   - count_success   (# of successful sends)
 *   - success         (# boolean)
 *   - message         (Prepared result message)
 */
function push_notifications_fcm_send_message($tokens, $payload) {
  if (!is_array($tokens) || empty($payload) || is_array($tokens) && empty($tokens)) {
    return FALSE;
  }

  // Define an array of result values.
  $result = array(
    'count_attempted' => 0,
    'count_success' => 0,
    'success' => 0,
    'message' => '',
  );

  // Define the header.
  $headers = array();
  $headers[] = 'Content-Type:application/json';
  $headers[] = 'Authorization: key=' . PUSH_NOTIFICATIONS_FCM_SERVER_KEY;

  // Check of many token bundles can be build.
  $token_bundles = ceil(count($tokens) / 1000);
  $result['count_attempted'] = count($tokens);

  // Send a push notification to every recipient.
  for ($i = 0; $i < $token_bundles; $i++) {

    // Create a token bundle.
    $bundle_tokens = array_slice($tokens, $i * 1000, 1000, FALSE);

    // Convert the payload into the correct format for C2DM payloads.
    // Prefill an array with values from other modules first.
    $data = array();
    foreach ($payload as $key => $value) {
      if ($key != 'alert') {
        $data['data'][$key] = $value;
      }
    }

    // Fill the default values required for each payload.
    $data['registration_ids'] = $bundle_tokens;
    $data['collapse_key'] = (string) time();
    $data['data']['message'] = $payload['alert'];
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, PUSH_NOTIFICATIONS_FCM_SERVER_POST_URL);
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($curl, CURLOPT_POST, TRUE);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
    $response_raw = curl_exec($curl);
    $info = curl_getinfo($curl);
    curl_close($curl);
    $response = FALSE;
    if (isset($response_raw)) {
      $response = json_decode($response_raw);
    }

    // If Google's server returns a reply, but that reply includes an error,
    // log the error message.
    if ($info['http_code'] == 200 && !empty($response->failure)) {
      watchdog('push_notifications', "Google's Server returned an error: " . $response_raw, NULL, WATCHDOG_ERROR);

      // Analyze the failure.
      foreach ($response->results as $token_index => $message_result) {
        if (!empty($message_result->error)) {

          // If the device token is invalid or not registered (anymore because the user
          // has uninstalled the application), remove this device token.
          if ($message_result->error == 'NotRegistered' || $message_result->error == 'InvalidRegistration') {
            push_notifications_purge_token($bundle_tokens[$token_index], PUSH_NOTIFICATIONS_TYPE_ID_ANDROID);
            watchdog('push_notifications', 'FCM token not valid anymore. Removing token ' . $bundle_tokens[$token_index]);
          }
        }
      }
    }

    // Count the successful sent push notifications if there are any.
    if ($info['http_code'] == 200 && !empty($response->success)) {
      $result['count_success'] += $response->success;
    }
  }
  $result['message'] = t('Successfully sent !count_success Firebase Cloud Messages (attempted to send !count messages).', array(
    '!count_success' => $result['count_success'],
    '!count' => $result['count_attempted'],
  ));
  $result['success'] = TRUE;

  // Invoke rules.
  if (module_exists('rules')) {
    rules_invoke_event_by_args('push_notifications_after_fcm_send', array(
      'type_id' => PUSH_NOTIFICATIONS_TYPE_ID_ANDROID,
      'payload' => $payload,
      'count_attempted' => $result['count_attempted'],
      'count_success' => $result['count_success'],
      'success' => $result['success'],
      'result_message' => $result['message'],
    ));
  }
  return $result;
}

/**
 * Determine all recipients from a specific device type.
 *
 * @param array $filters Filter option for query. Allows filtering by:
 *   - Language (key = language)
 *   - Device type (key = type_id)
 *   - Account type (key = account_type)
 * @param $raw
 *   Boolean, set true to retrieve the raw query results.
 *
 * @return mixed Array of results, null if no entries.
 */
function push_notifications_get_tokens($filters = array(), $raw = FALSE) {

  // Validate format of filters argument.
  if (!is_array($filters)) {
    return FALSE;
  }

  // Select all tokens for this type id.
  $query = db_select('push_notifications_tokens', 'pnt');
  $query
    ->fields('pnt');

  // Filter by device type, if required.
  if (array_key_exists('type_id', $filters)) {
    $query
      ->condition('pnt.type', $filters['type_id']);
  }

  // Filter by language, if required.
  if (array_key_exists('language', $filters) && is_string($filters['language'])) {
    $query
      ->condition('pnt.language', $filters['language']);
  }

  // Filter by anonymous vs. authenticated users.
  if (array_key_exists('account_type', $filters) && in_array($filters['account_type'], array(
    'anonymous',
    'authenticated',
  ))) {
    switch ($filters['account_type']) {
      case 'anonymous':
        $query
          ->condition('pnt.uid', 0);
        break;
      case 'authenticated':
        $query
          ->condition('pnt.uid', 0, '!=');
        break;
    }
  }
  $result = $query
    ->execute();

  // Return raw result, if needed.
  if ($raw) {
    return $result;
  }
  else {
    $tokens = array();
    foreach ($result as $record) {
      $tokens[] = $record->token;
    }
    return $tokens;
  }
}

/**
 * Determine all tokens for a specfic user.
 *
 * @param int $uid User Id.
 * @return array Array of token database records.
 *
 */
function push_notification_get_user_tokens($uid) {
  if (!is_numeric($uid)) {
    return FALSE;
  }

  // Select all tokens for this user.
  $query = db_select('push_notifications_tokens', 'pnt');
  $query
    ->fields('pnt');
  $query
    ->condition('pnt.uid', $uid);
  $result = $query
    ->execute();
  $tokens = array();
  foreach ($result as $record) {
    $tokens[$record->token] = $record;
  }
  return $tokens;
}

/**
 * Determine any languages used in the push
 * notifications table.
 */
function push_notifications_used_languages() {
  $query = db_select('push_notifications_tokens', 'pnt');
  $query
    ->fields('pnt', array(
    'language',
  ));
  $query
    ->distinct();
  $result = $query
    ->execute();

  // Convert the records into an array with
  // full language code available.
  include_once DRUPAL_ROOT . '/includes/iso.inc';
  $languages = _locale_get_predefined_list();
  $used_languages = array();
  foreach ($result as $record) {
    $used_languages[$record->language] = $languages[$record->language][0];
  }
  if (!empty($used_languages)) {

    // Sort the languages alphabetically.
    $used_langauges = asort($used_languages);

    // Add an "All" option.
    array_unshift($used_languages, 'All Recipients');
  }
  return $used_languages;
}

/**
 * Delete a token.
 *
 * @param $token
 *   Device token.
 *
 * @param $type_id
 *   Device Type ID.
 *
 */
function push_notifications_purge_token($token = '', $type_id = '') {
  if ($token == '' || !is_string($token)) {
    return FALSE;
  }

  // Allows other modules to respond to a token being purged.
  module_invoke_all('push_notifications_purge_token', $token, $type_id);

  // Invoke rules.
  if (module_exists('rules')) {
    rules_invoke_event_by_args('push_notifications_before_token_delete', array(
      'token' => $token,
      'type_id' => $type_id,
    ));
  }
  $query = db_delete('push_notifications_tokens');
  $query
    ->condition('token', $token);
  $result = $query
    ->execute();

  // Invoke rules.
  if (module_exists('rules')) {
    rules_invoke_event_by_args('push_notifications_after_token_delete', array(
      'token' => $token,
      'type_id' => $type_id,
    ));
  }
  return $result;
}

/**
 * Connect to Apple's feedback server to remove unused device tokens.
 * Connection modeled after daddyhunt_apns_send_notifications function.
 *
 * @see http://stackoverflow.com/questions/4774681/php-script-for-apple-push-notification-feedback-service-gets-timeout-every-time
 * @see http://stackoverflow.com/questions/1278834/php-technique-to-query-the-apns-feedback-server/2298882#2298882
 */
function push_notifications_apns_feedback_service() {

  // Create a Stream context and open an Internet socket connection.
  $stream_context = stream_context_create();
  $apns_cert = _push_notifications_get_apns_certificate();
  stream_context_set_option($stream_context, 'ssl', 'local_cert', $apns_cert);

  // If the user has a passphrase stored, we use it.
  $passphrase = variable_get('push_notifications_apns_passphrase', '');
  if (strlen($passphrase)) {
    stream_context_set_option($stream_context, 'ssl', 'passphrase', $passphrase);
  }
  if (variable_get('push_notifications_set_entrust_certificate', FALSE)) {
    stream_context_set_option($stream_context, 'ssl', 'CAfile', drupal_get_path('module', 'push_notifications') . '/certificates/entrust_2048_ca.cer');
  }
  $apns = stream_socket_client('ssl://' . PUSH_NOTIFICATIONS_APNS_FEEDBACK_HOST . ':' . PUSH_NOTIFICATIONS_APNS_FEEDBACK_PORT, $error, $error_string, 2, STREAM_CLIENT_CONNECT, $stream_context);
  if (!$apns) {
    return;
  }

  // Gather expired tokens in an array
  $tokens = array();
  while (!feof($apns)) {
    $data = fread($apns, 38);
    if (strlen($data)) {
      $tokens[] = unpack("N1timestamp/n1length/H*devtoken", $data);
    }
  }

  // Close connection.
  fclose($apns);
  if (empty($tokens)) {
    watchdog('push_notifications', 'Apple\'s feedback service returned no tokens to be removed.');
    return;
  }

  // Remove all tokens that are not valid anymore.
  $counter = 0;
  foreach ($tokens as $token) {
    push_notifications_purge_token($token['devtoken'], PUSH_NOTIFICATIONS_TYPE_ID_IOS);
    $counter++;
  }

  // Invoke rules.
  if (module_exists('rules')) {
    rules_invoke_event_by_args('push_notifications_after_apns_feedback', array(
      'counter' => $counter,
    ));
  }

  // Give some feedback after the process finished.
  watchdog('push_notifications', '!count were removed after pulling the Apple feedback service.', array(
    '!count' => $counter,
  ));
}

/**
 * Get the full path to the APNS certificate.
 *
 * @return string
 *   The path to the certificate file on the server.
 */
function _push_notifications_get_apns_certificate() {
  $path = variable_get('push_notifications_apns_certificate_folder', '');
  if (empty($path)) {
    $path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'certificates' . DIRECTORY_SEPARATOR;
  }
  $path .= PUSH_NOTIFICATIONS_APNS_CERTIFICATE;
  if (!file_exists($path)) {
    watchdog('push_notifications', 'Cannot find apns certificate file at @path', array(
      '@path' => $path,
    ), WATCHDOG_WARNING, l(t('settings'), 'admin/config/services/push_notifications/configure'));
  }
  return $path;
}

/**
* Generate and set the random file ending for APNS certificates.
*/
function _push_notifications_set_random_certificate_string() {

  // Generate a random 10-digit string.
  $random_string = substr(md5(microtime()), 0, 10);
  variable_set('push_notifications_apns_certificate_random', '-' . $random_string);
}

Functions

Namesort descending Description
push_notifications_apns_feedback_service Connect to Apple's feedback server to remove unused device tokens. Connection modeled after daddyhunt_apns_send_notifications function.
push_notifications_apns_send_message Send out push notifications through APNS.
push_notifications_c2dm_send_message Send out push notifications through C2DM.
push_notifications_c2dm_token Determine the auth string from C2DM server.
push_notifications_check_payload_size Check size of a push notification payload. Payload can't exceed PUSH_NOTIFICATIONS_APNS_PAYLOAD_SIZE_LIMIT.
push_notifications_cron Implements of hook_cron().
push_notifications_fcm_send_message Send out push notifications through FCM.
push_notifications_find_token Determine if this user has already stored a token in the database. The same device token can be registered for multiple users, because multiple users can login from the same device.
push_notifications_gcm_send_message Send out push notifications through GCM.
push_notifications_get_tokens Determine all recipients from a specific device type.
push_notifications_menu Implements of hook_menu().
push_notifications_open_apns Open an APNS connection. Should be closed by calling fclose($connection) after usage.
push_notifications_permission Implements hook_permission().
push_notifications_privatemsg_message_insert Implements hook_privatemsg_message_insert.
push_notifications_purge_token Delete a token.
push_notifications_send_alert Handle delivery of simple alert message.
push_notifications_send_message Send a simple message alert to an array of recipients.
push_notifications_send_message_account Send a simple message alert to a single account.
push_notifications_send_message_bulk Send a simple message alert to all tokens in the database.
push_notifications_services_resources Implements hook_services_resources().
push_notifications_store_token Store a token in the database. Removes all spaces in the token.
push_notifications_used_languages Determine any languages used in the push notifications table.
push_notifications_views_api Implements of hook_views_api().
push_notification_get_user_tokens Determine all tokens for a specfic user.
_push_notifications_get_apns_certificate Get the full path to the APNS certificate.
_push_notifications_set_random_certificate_string Generate and set the random file ending for APNS certificates.

Constants