<?php
// $Id: notify.module,v 2.68.2.26 2010/08/05 19:42:21 matt2000 Exp $

/**
 * @file
 * Notify module sends email digests of new content and comments.
 *
 * The notification module allows users to subscribe to periodic e-mails which
 * include all new or revised content and/or comments much like the daily news
 * letters sent by some websites.  Even if this feature is not configured for
 * normal site users, it can be a useful feature for an administrator of a site
 * to monitor content submissions and comment posts.
 */

define('NOTIFY_NODE_TYPE', 'notify_node_type_');

/**
 * Implementation of hook_help().
 */
function notify_help($section) {
  switch ($section) {
    case 'admin/help#notify':
      $output = '<p>'. t('The notification module allows users to subscribe to periodic e-mails which include all new or revised content and/or comments much like the daily news letters sent by some websites.  Even if this feature is not configured for normal site users, it can be a useful feature for an administrator of a site to monitor content submissions and comment posts.') .'</p>';
      $output .= '<p>'. t('The administrator sets the frequency of the e-mails in the notify administration interface. They can also set how many e-mail failures should occur before notify stops sending notifications.  Note that cron must be enabled for notifications to be sent out.') .'</p>';
      $output .= t('<p>You can</p><ul><li>set up your site to run tasks automatically at required intervals. For more information, see <a href="@admin-help-system">cron</a>.</li><li>administer notify <a href="@admin-settings-notify">administer &gt;&gt; settings &gt;&gt; notify</a>.</li></ul>', array('@admin-help-system' => url('admin/help/system'), '@admin-settings-notify' => url('admin/settings/notify')));
      $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="@notify">Notify page</a>.', array('@notify' => 'http://www.drupal.org/handbook/modules/notify/')) .'</p>';
      return $output;
  }
}

/**
 * Menu callback; display notify settings page.
 */
function notify_admin_settings() {
  $period = array(
    900         => format_interval(900),
    1800        => format_interval(1800),
    3600        => format_interval(3600),
    10800       => format_interval(10800),
    21600       => format_interval(21600),
    32400       => format_interval(32400),
    43200       => format_interval(43200),
    86400       => format_interval(86400),
    172800      => format_interval(172800),
    259200      => format_interval(259200),
    604800      => format_interval(604800),
    1209600     => format_interval(1209600),
    2419200     => format_interval(2419200),
    -1          => t('Never'),
  );

  $form = array();

  $form['notify_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('E-mail notification settings'),
    '#collapsible' => TRUE,
  );

  $form['notify_settings']['notify_send'] = array(
    '#type' => 'select',
    '#title' => 'Send notifications every',
    '#default_value' => variable_get('notify_send', array(86400)),
    '#options' => $period,
    '#description' => 'How often should new content notifications be sent? Requires cron to be running at least this often.',
  );

  $form['notify_settings']['notify_send_hour'] = array(
    '#type' => 'select',
    '#title' => 'Hour to Send Notifications',
    '#description' => 'Specify the hour (24-hour clock) in which notifications should be sent, if the frequency is one day or greater.',
    '#default_value' => variable_get('notify_send_hour', 9),
    '#options' => array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23),
  );

  $form['notify_settings']['notify_attempts'] = array(
    '#type' => 'select',
    '#title' => 'Number of failed sends after which notifications are disabled',
    '#default_value' => variable_get('notify_attempts', array(5)),
    '#options' => array(t('Disabled'), 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20),
  );

  $form['notify_settings']['notify_reg_default'] = array(
    '#type' => 'checkbox',
    '#title' => 'Notification checkbox default on new user registration form',
    '#return_value' => 1,
    '#default_value' => variable_get('notify_reg_default', 1),
  );

  $form['notify_settings']['notify_include_updates'] = array(
    '#type' => 'checkbox',
    '#title' => 'Include updated posts in notifications',
    '#return_value' => 1,
    '#default_value' => variable_get('notify_include_updates', 1),
  );

  $set = 'ntype';
  $form[$set] = array(
    '#type' => 'fieldset',
    '#title' => 'Notification by node type',
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#description' => 'Having nothing checked defaults to sending notifications about all node types.'
  );

  foreach (node_get_types('types', array()) as $type => $object) {
    $form[$set][NOTIFY_NODE_TYPE . $type] = array(
      '#type' => 'checkbox',
      '#title' => $object->name,
      '#return_value' => 1,
      '#default_value' => variable_get(NOTIFY_NODE_TYPE . $type, 0),
    );
  }

  return system_settings_form($form);
}

/**
 * Implementation of hook_cron().
 */
function notify_cron() {
  $send_last = variable_get('notify_send_last', 0);
  $send_interval = variable_get('notify_send', 86400);
  $send_hour = variable_get('notify_send_hour', 9);
  $send_start = time();
  if ( ($send_start - $send_last > $send_interval)
    && (date('G', $send_start) >= $send_hour || $send_interval < 86400)
    && ($send_interval != -1) //special case of settings to send 'never'
      ) {
    _notify_send($send_start);
    variable_set('notify_send_last', $send_start);
  }
}

/**
 * Implementation of hook_user().
 */
function notify_user($type, &$edit, &$user, $category = NULL) {
  switch ($type) {
    case 'delete':
      db_query('DELETE FROM {notify} WHERE uid = %d', $user->uid);
      break;

    case 'register':
      return _notify_user_reg_fields();
      break;

    case 'insert':
      if (isset($edit['notify_decision']) && $edit['notify_decision'] == 1) {
        db_query('INSERT INTO {notify} (uid, status, node, teasers, comment) VALUES (%d, %d, %d, %d, %d)', $user->uid, 1, 1, 0, 0);
        $edit['notify_decision'] = NULL;
      }
      break;

  }
}


/**
 * Returns form fields to be added to User Regsitration form
 */
function _notify_user_reg_fields() {
  if (!user_access('access notify')) return;

  // Get the variable for how often the notifications are sent out
  $period = variable_get("notify_send", 86400);

  // Add a fieldset containing a checkbox for users to accept
  // getting updates on the registration form.
  $fields['notify_agree'] = array(
    '#type' => 'fieldset',
    '#title' => t('Email Notifications')
  );

  // Add the checkbox to the fieldset
  $fields['notify_agree']['notify_decision'] = array(
    '#type' => 'checkbox',
    '#title' => t('Receive email notifications of new content posted to this site. Notifications are sent every ' . format_interval($period) . '.'),
    '#return_value' => 1,
    '#default_value' => variable_get('notify_reg_default', 1),
  );

  return $fields;
}


/**
 * Implementation of hook_perm().
 */
function notify_perm() {
  return array('access notify', 'administer notify');
}

/**
 * Implementation of hook_menu().
 *
 * @return array
 */
function notify_menu() {
  $items = array();

  $items['admin/settings/notify'] = array(
    'title' => 'Notification settings',
    'description' => 'Adjust settings for new content notifications sent by e-mail.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notify_admin_settings'),
    'access callback' => 'user_access',
    'access arguments' => array('administer notify'),
    'type' => MENU_NORMAL_ITEM,
  );
  $items['user/%user/notify'] = array(
    'title' => 'Notification settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notify_user_settings_form', 1),
    'access callback' => 'notify_user_access',
    'access arguments' => array(1),
    'type' => MENU_LOCAL_TASK
  );
  $items['admin/user/user/notify'] = array(
    'title' => 'Notifications',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('notify_admin_users'),
    'access callback' => 'user_access',
    'access arguments' => array('administer notify'),
    'type' => MENU_LOCAL_TASK,
  );

  return $items;
}

/**
 * Checks access to notifications settings tab
 */
function notify_user_access($account = NULL) {
  return $account->uid &&
    (
      // Always let users view their own profile.
      ($GLOBALS['user']->uid == $account->uid && user_access('access notify'))  ||
      // Administrators can view all accounts.
      user_access('administer notify')
    );
}


/**
 * Register the themeing the form data into a table at
 * admin/user/user/notify
 *
 * @return array
 */
function notify_theme() {
  return array(
    'notify_admin_users' => array(
    'arguments' => array('form' => NULL)
    )
  );
}


/**
 * Menu callback; show user notification options.
 */
function notify_user_settings_form(&$form_state, $arg) {
  global $user;

  if ($user->uid != $arg->uid && !user_access('administer notify') ) {
    drupal_access_denied();
    return;
  }

  $account = user_load(array('uid' => $arg->uid));
  if (!is_object($account)) {
    drupal_not_found();
    return;
  }

  $result = db_query('SELECT u.uid, u.name, u.mail, n.status, n.node, n.teasers, n.comment FROM {users} u LEFT JOIN {notify} n ON u.uid = n.uid WHERE u.uid = %d AND u.status = 1', $account->uid);
  $notify = db_fetch_object($result);
  $form = array();
  if (!$notify->mail) {
        drupal_set_message(t('Your e-mail address must be specified on your <a href="@url">my account</a> page.', array('@url' => url('user/'. $account->uid .'/edit'))), 'error');
  }

  $form['notify_page_master'] = array('#type' => 'fieldset', '#title' => t('Master switch'));
  $form['notify_page_master']['status'] = array('#type' => 'radios',
    '#title' => t('Notify status'),
    '#default_value' => $notify->status,
    '#options' =>  array(t('Disabled'), t('Enabled')),
    '#description' => t('Do you wish to receive periodic e-mails when new content is posted?'),
  );

  $form['notify_page_detailed'] = array('#type' => 'fieldset', '#title' => t('Detailed settings'));
  $form['notify_page_detailed']['node'] = array('#type' => 'radios',
    '#title' => t('Notify new content'),
    '#default_value' => $notify->node,
    '#options' => array(t('Disabled'), t('Enabled')),
    '#description' => t('Include new posts in the notification mail.'),
  );
  $form['notify_page_detailed']['teasers'] = array('#type' => 'radios',
    '#title' => t('Content'),
    '#default_value' => $notify->teasers,
    '#options' => array(t('Title only'), t('Title + Teaser'), t('Title + Body')),
    '#description' => t('Select the amount of each post that you would like to see in your notification e-mails.'),
  );
  $form['notify_page_detailed']['comment'] = array('#type' => 'radios',
    '#title' => t('Notify new comments'),
    '#default_value' => $notify->comment,
    '#options' => array(t('Disabled'), t('Enabled')),
    '#description' => t('Include new comments in the notification mail.'),
  );
  $form['uid'] = array('#type' => 'value', '#value' => $account->uid);
  $form['submit'] = array('#type' => 'submit', '#value' => t('Save settings'));

  return $form;
}

function notify_user_settings_form_submit($form, &$form_state) {
  unset($form);
  db_query('DELETE FROM {notify} WHERE uid = %d', $form_state['values']['uid']);
  db_query('INSERT INTO {notify} (uid, status, node, teasers, comment) VALUES (%d, %d, %d, %d, %d)', $form_state['values']['uid'], $form_state['values']['status'], $form_state['values']['node'], $form_state['values']['teasers'], $form_state['values']['comment']);
  drupal_set_message(t('Notify settings saved.'));
}

/**
 * Menu callback; show admininster user notification settings form.
 */
function notify_admin_users() {
  $form = array();
  $form['#tree'] = TRUE;
  $form['info'] = array('#value' => t('The following table shows all users that have notifications enabled.'));

  $form['users'] = array();

  $result = db_query('SELECT u.uid, u.name, u.mail, n.* FROM {users} u LEFT JOIN {notify} n ON u.uid = n.uid WHERE n.status = 1 AND u.status = 1 ORDER BY u.name');
  while ($notify = db_fetch_object($result)) {
    $form['users'][$notify->uid] = array();
    $form['users'][$notify->uid]['name'] = array('#type' => 'markup', '#value' => theme('username', $notify));
    $form['users'][$notify->uid]['mail'] = array('#type' => 'markup', '#value' => $notify->mail);
    $form['users'][$notify->uid]['node'] = array('#type' => 'checkbox', '#default_value' => $notify->node);
    $form['users'][$notify->uid]['teasers'] = array('#type' => 'select', '#default_value' => $notify->teasers, '#options' => array(t('Title only'), t('Title + Teaser'), t('Title + Body')));
    $form['users'][$notify->uid]['comment'] = array('#type' => 'checkbox', '#default_value' => $notify->comment);
    $form['users'][$notify->uid]['attempts'] = array('#type' => 'textfield', '#size' => 2,  '#default_value' => $notify->attempts ? intval($notify->attempts) : 0);
  }

  $form['flush'] = array(
    '#title' => t('Flush e-mail queue'),
    '#type' => 'checkbox',
    '#default_value' => FALSE,
    '#description' => t('Send out any pending notification e-mails currently in queue.'),
  );

  $form['submit'] = array('#type' => 'submit', '#value' => t('Save settings'));

  return $form;
}

/**
 * Submit for the notify_admin form.
 */
function notify_admin_users_submit($form, &$form_state) {
  unset($form);

  if ($form_state['values']['users']) {
    foreach ($form_state['values']['users'] as $uid => $settings) {
      db_query('UPDATE {notify} SET node = %d, teasers = %d, comment = %d, attempts = %d WHERE uid = %d', $settings['node'], $settings['teasers'], $settings['comment'], $settings['attempts'], $uid);
    }
  }
  drupal_set_message(t('Notify settings saved.'));

  if ($form_state['values']['flush']) {
    $send_start = time();
    list($num_sent, $num_failed) = _notify_send($send_start);
    variable_set('notify_send_last', $send_start);

    if ($num_sent > 0) {
      drupal_set_message(t('!count pending notification e-mails have been sent.', array('!count' => $num_sent)));
    }
    elseif ($num_failed > 0) {
      drupal_set_message(t('!count notification e-mails could not be sent.', array('!count' => $num_failed)), 'error');
    }
    else {
      drupal_set_message(t('No notification e-mails needed to be sent.'));
    }
  }
}

/**
 * Theme function to theme the admin user settings form in a table format.
 */
function theme_notify_admin_users($form) {
  $output = drupal_render($form['info']);
  $header = array(t('Username'), t('E-mail address'), t('Content'), t('Teasers'), t('Comment'), t('Failed attempts'));

  $rows = array();
  foreach (element_children($form['users']) as $uid) {
    $row = array();
    foreach (element_children($form['users'][$uid]) as $entry_key) {
      unset($form['users'][$uid][$entry_key]['#title']);

      $row[] = drupal_render($form['users'][$uid][$entry_key]);
    }
    $rows[] = $row;
  }

  if (!$rows) {
    $rows[] = array(array('data' => t('No users have notifications enabled.'), 'colspan' => 6));
  }
  $output .= theme('table', $header, $rows);

  $output .= drupal_render($form);
  return $output;
}

/**
 * Formatting of outgoing mail, taken from mail.inc, part of project.module
 */
function _notify_content($node, $notify) {
  static $i = 0;

  switch ($notify->teasers) {
    case 0:
      return;
    case 1:
      $txt = check_markup($node->teaser, $node->format, FALSE);
      break;
    case 2:
      $txt = check_markup($node->body, $node->format, FALSE);
  }

  return drupal_html_to_text($txt);
}

/**
 * Helper function to send the notification email.
 *
 * TODO: Needs some cleanup and themability.
 */
function _notify_send($send_start = NULL) {
  $send_start = $send_start ? $send_start : time();
  $period = variable_get('notify_send_last', $send_start - variable_get('notify_send', 86400));
  $separator = '------------------------------------------------------------------------------';
  $mini_separator = '---';

  $num_sent = 0;
  $num_failed = 0;

  _notify_switch_user(); // Store current user

  // Fetch all node type authorized by notify settings
  //Note that queries use 'nn' alias to avoid conflicts when query is rewritten by access control modules
  $ntype = array();
  foreach (node_get_types() as $type => $name) {
    if (variable_get(NOTIFY_NODE_TYPE . $type, 0)) {
      $ntype[] = $type;
    }
    if (count($ntype) >= 1) {
      $nodetypes_query = "AND (nn.type = '". implode("' OR nn.type = '", $ntype) ."') ";
    }
    else {
      $nodetypes_query = '';
    }
  }

  // build query to fetch desired nodes.
  $q = 'SELECT nn.nid FROM {node} nn WHERE (nn.status = 1 OR nn.moderate = 1) ' . $nodetypes_query . ' AND ((nn.created > %d AND nn.created <= %d)';
  //include updated nodes?
  $q .= variable_get('notify_include_updates', 1) ? ' OR (nn.changed > %d AND nn.changed <= %d))' : ')';
  $q .= ' ORDER BY nn.created ASC';

  //Run the query, and load the nodes
  _notify_switch_user(1); //Query as if user 1, we check for individual user access later.
  $nresult = db_query(db_rewrite_sql($q, 'nn'), $period, $send_start, $period, $send_start);
  $nodes = array();
  while ($node = db_fetch_object($nresult)) {
    $nodes[$node->nid] = node_load($node->nid);
  }

  // Fetch new comments.
  $comments = array();
  if (module_exists('comment')) {
      $cresult = db_query(db_rewrite_sql('SELECT c.nid, c.cid, c.subject, c.name, c.status FROM {comments} c INNER JOIN {node} nn ON c.nid = nn.nid WHERE c.timestamp > %d AND c.timestamp <= %d ' . $nodetypes_query . ' ORDER BY c.nid, c.timestamp', 'c'), $period, $send_start);
      while ($comment = db_fetch_object($cresult)) {
        $comments[$comment->nid][] = $comment;
      }
  }

  if (count($nodes) || count($comments)) {

    // Fetch users with notify enabled
    $uresult = db_query('SELECT u.uid, u.name, u.mail, u.language, n.status, n.node, n.teasers, n.comment FROM {notify} n INNER JOIN {users} u ON n.uid = u.uid WHERE n.status = 1 AND u.status = 1 AND n.attempts <= %d', variable_get('notify_attempts', 5));

    while ($user = db_fetch_object($uresult)) {
      // Switch current user to this account to use node_access functions, etc.
      _notify_switch_user($user->uid);

      $node_body = '';
      $comment_body = '';

      // Write new node content to e-mail if user has permissions and nodes are
      // ready to be sent. @TODO - cache by role
      if ($user->node && user_access('access content') && count($nodes)) {

        $node_count = 0;
        foreach ($nodes as $node) {
          // Skip to next if this user is NOT allowed to view this node.
          if (!node_access('view', $node)) {
            continue;
          }

          // @TODO: Add functionality to hook into moderation modules?
          if ($node->moderate == 1) {
            $status = t('Queued');
          }
          elseif ($node->status == 1) {
            $status = t('Published');
          }
          elseif ($node->status == 0) {
            $status = t('Unpublished');
          }

          if ($node_count > 0) {
            $node_body .= $mini_separator ."\n\n";
          }
          $node_body .= ++$node_count .'. '. $node->title ."\n";
          $node_body .= t('!status !type by !author', array('!status' => $status, '!type' => node_get_types('name', $node), '!author' => ($node->name ? $node->name : variable_get('anonymous', 'Anonymous')))) ."\n";
          $node_body .= '[ '. url('node/'. $node->nid, array('absolute' => TRUE)) ." ]\n\n";
          $node_body .= _notify_content($node, $user) ."\n";
        }

        // Prepend node e-mail header as long as user could access at least one node.
        if ($node_count > 0) {
          $node_body = $separator ."\n"
            . t('Recent content - !count', array('!count' => format_plural(count($nodes), '1 new post', '@count new posts'))) ."\n"
            . $separator ."\n\n". $node_body;
        }
      }

      // Write new comments to e-mail if user has permissions and there are
      // comments to be sent.
      if ($user->comment && user_access('access comments') && count($comments)) {
        $total_comment_count = 0;
        foreach ($comments as $nid => $comment) {
          // If we don't already have the node, fetch it.
          if (!isset($nodes[$nid])) {
            $nodes[$nid] = node_load($nid);
          }

          // Don't show comments if we're not allowed to view this node.
          if (!node_access('view', $nodes[$nid])) {
            continue;
          }

          if ($comment_body) {
            $comment_body .= $mini_separator ."\n\n";
          }
          $comment_body .= t('!count attached to !type posted by !author: !title', array('!count' => format_plural(count($comment), '1 new comment', '@count new comments'), '!title' => $nodes[$nid]->title, '!type' => node_get_types('name', $nodes[$nid]), '!author' => $nodes[$nid]->name ? $nodes[$nid]->name : variable_get('anonymous', 'Anonymous'))) ."\n";

          $comment_count = 0;
            foreach ($comment as $c) {
            //Determine whether to show comment status
            if (user_access('administer comments')) {
              $status = $c->status == COMMENT_PUBLISHED ? t('Published') : t('Unpublished');
              $status = " [$status]";
            }
            $comment_body .= '   '. ++$comment_count .'.'. t('!status !title by !author',
                                                              array('!status' => $status,
                                                                    '!title' => $c->subject,
                                                                    '!author' => ($c->name ? $c->name : variable_get('anonymous', 'Anonymous')))) ."\n"
              .'     '. url('node/'. $nid, array('fragment' => 'comment-'. $c->cid, 'absolute' => TRUE)) ."\n\n";
            $total_comment_count++;
          }
        }

        if ($total_comment_count > 0) {
          $comment_body = $separator ."\n"
            . t('Recent comments - !count', array('!count' => format_plural($total_comment_count, '1 new comment', '@count new comments'))) ."\n"
            . $separator ."\n\n". $comment_body;
        }
      }

      $body = $node_body . $comment_body;

      // If there was anything new, send mail.
      if ($body) {
        // Set up initial values for e-mail.

        $headers = array();//'From' => "$from_name <$from>");
        if (!drupal_mail('notify', 'send', $user->mail, user_preferred_language($user), array('content' => $body))) {
          $num_failed++;
          db_query('UPDATE {notify} SET attempts = attempts + 1 WHERE uid = %d', $user->uid);
          watchdog('notify', 'User %name (%mail) could not be notified. Mail error.', array('%name' => $user->name, '%mail' => $user->mail), WATCHDOG_ERROR);
        }
        else {
          $num_sent++;
          watchdog('notify', 'User %name (%mail) notified successfully.', array('%name' => $user->name, '%mail' => $user->mail), WATCHDOG_INFO);
        }
      }
    }
  }
  // Restore user.
  _notify_switch_user();
  return array($num_sent, $num_failed);
}

function notify_mail($key, &$message, $params) {
  global $user;

  $message['subject'] = t('!sitename new content notification for !username', array('!username' => $user->name, '!sitename' => variable_get('site_name', 'Drupal')));
  $message['body'] = t('Greetings !user,', array('!user' => $user->name)) ."\n\n";
  $message['body'] .= $params['content'];
  $message['body'] .= "\n-- \n";
  $message['body'] .= t('This is an automatic e-mail from !sitename.', array('!sitename' => variable_get('site_name', 'Drupal'))) ."\n";
  $message['body'] .= t('To stop receiving these e-mails, change your notification preferences at !notify-url', array('!notify-url' => url("user/$user->uid/notify" , array('absolute' => TRUE)))) ."\n";
}

/**
 * Switch from original user to mail submission user and back.
 *
 * NOTE: Copied from mailhandler
 *
 * Note: You first need to run _notify_switch_user without
 * argument to store the current user. Call _notify_switch_user
 * without argument to set the user back to the original user.
 *
 * @param $uid The user ID to switch to
 */
function _notify_switch_user($uid = NULL) {
  global $user;
  static $orig_user = array();

  if (isset($uid)) {
    // Should a user visit cron.php, or should this module be invoked via poormanscron and _notify_send() does not complete,
    // the visitor will end up logged in as the "switched to user" for subsequent requests unless we disable saving the session
    // until we are sure we're the invoking user again.
    session_save_session(FALSE);
    $user = user_load(array('uid' => $uid));
  }
  // Retrieve the initial user, can be called multiple times.
  elseif (count($orig_user)) {
    $user = array_shift($orig_user);
    array_unshift($orig_user, $user);
    session_save_session(TRUE);
  }
  // Store the initial user.
  else {
    $orig_user[] = $user;
  }
}
