You are here

invite.module in Invite 5

Allows your users to send and track invitations to join your site.

File

invite.module
View source
<?php

/**
 * @file
 * Allows your users to send and track invitations to join your site.
 */

/**
 * Session variable name.
 */
define('INVITE_SESSION_NAME', 'invite_code');

/**
 * Special unlimited value.
 */
define('INVITE_UNLIMITED_INVITES', -1);

/**
 * Implementation of hook_help().
 *
 * Returns appropriate help text for all three invite status pages.
 */
function invite_help($section) {

  // Show module help
  if ($section == 'admin/help#invite') {
    $file = drupal_get_path('module', 'invite') . '/README.txt';
    if (file_exists($file)) {
      return _filter_autop(file_get_contents($file));
    }
    return;
  }

  // Show introductory text on overview pages
  switch ($section) {
    case 'invite/list':
    case 'invite/list/accepted':
      $output = '<p>' . t("The invitations shown on this page have been used to join the site. Clicking on an e-mail address takes you to the user's profile page.");
      break;
    case 'invite/list/pending':
      $output = '<p>' . t("The invitations shown on this page haven't been accepted yet.");
      break;
    case 'invite/list/expired':
      $output = '<p>' . t('The invitations shown on this page have not been used to register on the site within the expiration period of @count days.', array(
        '@count' => variable_get('invite_expiry', 30),
      ));
      break;
    default:
      return;
  }
  $output .= ' ' . t('The status <em>deleted</em> means the user account has been terminated.') . '</p>';
  if (!user_access('withdraw accepted invitations')) {
    $output .= '<p>' . t("At any time, you may withdraw either pending or expired invitations. Accepted invitations can't be withdrawn and count permanently toward your invitation allotment.") . '</p>';
  }
  return $output;
}

/**
 * Implementation of hook_perm().
 */
function invite_perm() {
  return array(
    'send invitations',
    'send mass invitations',
    'track invitations',
    'withdraw accepted invitations',
  );
}

/**
 * Implementation of hook_menu().
 */
function invite_menu($may_cache) {
  global $user;
  $items = array();
  $send_access = user_access('send invitations');
  $track_access = user_access('track invitations');
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/user/invite',
      'title' => 'Invite settings',
      'description' => t('Manage Invite settings'),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'invite_settings',
      'access' => user_access('administer site configuration'),
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'invite/accept',
      'callback' => 'invite_action',
      'access' => TRUE,
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'invite',
      'title' => variable_get('invite_page_title', t('Invite your friends and colleagues')),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'invite_form',
        'page',
      ),
      'access' => $send_access,
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'invite/add',
      'title' => t('New invitation'),
      'access' => $send_access,
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'weight' => -1,
    );
    $items[] = array(
      'path' => 'invite/list',
      'title' => t('Your invitations'),
      'callback' => 'invite_overview',
      'access' => $track_access,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'invite/list/accepted',
      'title' => t('Accepted'),
      'callback' => 'invite_overview',
      'callback arguments' => array(
        'accepted',
      ),
      'access' => $track_access,
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'weight' => -5,
    );
    $items[] = array(
      'path' => 'invite/list/pending',
      'title' => t('Pending'),
      'callback' => 'invite_overview',
      'callback arguments' => array(
        'pending',
      ),
      'access' => $track_access,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'invite/list/expired',
      'title' => t('Expired'),
      'callback' => 'invite_overview',
      'callback arguments' => array(
        'expired',
      ),
      'access' => $track_access,
      'type' => MENU_LOCAL_TASK,
      'weight' => 5,
    );
    $items[] = array(
      'path' => 'invite/withdraw',
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'invite_delete',
      ),
      'access' => $track_access,
      'type' => MENU_CALLBACK,
    );
  }
  else {
    if ($user->uid && $send_access) {

      // Check for newly registered users
      _invite_check_messages($user->uid);
    }
  }
  return $items;
}

/**
 * Implementation of hook_form_alter().
 */
function invite_form_alter($form_id, &$form) {
  switch ($form_id) {
    case 'user_admin_settings':

      // Add new registration mode
      $form['registration']['user_register']['#options']['inviteonly'] = t('New user registration by invitation only.');
      break;
    case 'user_register':

      // In order to prevent caching of the preset e-mail address, we have to
      // disable caching for user/register
      $GLOBALS['conf']['cache'] = CACHE_DISABLED;
      $invite = invite_load_from_session();

      // Legacy url support (user/register/123)
      if (!$invite && ($code = arg(2))) {
        if ($invite = invite_load($code)) {
          if (_invite_validate($invite)) {
            $_SESSION[INVITE_SESSION_NAME] = $invite->reg_code;
          }
        }
      }
      if ($invite) {

        // Preset e-mail field
        if (isset($form['account'])) {
          $field =& $form['account'];
        }
        else {
          $field =& $form;
        }
        if (isset($field['mail'])) {
          $field['mail']['#default_value'] = $invite->email;
        }
      }
      else {
        if (variable_get('user_register', 1) == 'inviteonly' && !user_access('administer users')) {
          drupal_set_message(t('Sorry, new user registration by invitation only.'));
          drupal_goto();
        }
      }
      break;
    case 'user_login_block':

      // Remove temptation for non members to try and register
      if (variable_get('user_register', 1) == 'inviteonly') {
        $new_items = array();
        $new_items[] = l(t('Request new password'), 'user/password', array(
          'title' => t('Request new password via e-mail.'),
        ));
        $form['links']['#value'] = theme('item_list', $new_items);
      }
      break;
  }
}

/**
 * Implementation of hook_user().
 */
function invite_user($op, &$edit, &$account, $category = NULL) {
  switch ($op) {
    case 'insert':
      $invite = invite_load_from_session();
      if (!$invite) {

        // Try to look up an invitation in case a user has been invited to join
        // the site, but did go straight to the site and signed up without
        // using the invite link.
        $code = db_result(db_query("SELECT reg_code FROM {invite} WHERE email = '%s'", $edit['mail']));
        if ($code) {
          $invite = invite_load($code);
        }
      }
      if ($invite) {

        // Update invite status
        _invite_set_accepted($edit['mail'], $account->uid, $invite->reg_code);

        // Escalate user role
        _invite_role_escalate($account);

        // Unblock user account
        _invite_unblock($account->uid);
        unset($_SESSION[INVITE_SESSION_NAME]);
      }
      break;
    case 'delete':

      // Only delete invites of existing users if the configuration allows it to
      $delete_joined = user_access('withdraw accepted invitations');

      // Delete invite for this user
      if ($delete_joined) {
        db_query("DELETE FROM {invite} WHERE mid = %d", $account->uid);
      }

      // Delete any invites originating from this user
      $sql = "DELETE FROM {invite} WHERE uid = %d";
      if (!$delete_joined) {
        $sql .= " AND timestamp != 0";
      }
      db_query($sql, $account->uid);
      break;
  }
}

/**
 * Implementation of hook_block().
 */
function invite_block($op = 'list', $delta = 0, $edit = array()) {
  if ($op == 'list') {
    $blocks[0] = array(
      'info' => t('Invite a friend'),
    );
    return $blocks;
  }
  else {
    if ($op == 'view') {
      switch ($delta) {
        case 0:
          if (user_access('send invitations')) {
            $block = array(
              'subject' => t('Invite a friend'),
              'content' => drupal_get_form('invite_form', 'block'),
            );
          }
          break;
      }
      return $block;
    }
  }
}

/**
 * Implementation of hook_cron().
 */
function invite_cron() {

  // TODO: cron should check which invitations are expired and possibly send a follow up mail
  // This should also trigger a module_invoke_all('invite', 'expire', $args) call so that other modules can
  // react to the expired invitation.
}

/**
 * Implementation of hook_disable().
 */
function invite_disable() {
  if (variable_get('user_register', 1) == 'inviteonly') {
    variable_set('user_register', 1);
    drupal_set_message(t('User registration option reset to %no_approval.', array(
      '%no_approval' => t('Visitors can create accounts and no administrator approval is required.'),
    )));
  }
}

/**
 * Menu callback; display invite settings form.
 *
 * @return
 *   A form definition array.
 */
function invite_settings() {
  include drupal_get_path('module', 'invite') . '/invite_token.inc';
  $roles = user_roles(0, 'send invitations');
  if (count($roles) == 0) {
    drupal_set_message(t('Please enable the <em>send invitations</em> permission for at least one role. This can be done on the <a href="!admin-user-access">Access control page</a>.', array(
      '!admin-user-access' => url('admin/user/access'),
    )));
  }
  $target_roles = user_roles(1);

  // General settings
  $form['general'] = array(
    '#type' => 'fieldset',
    '#title' => t('General settings'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['general']['invite_target_role_default'] = array(
    '#type' => 'select',
    '#title' => t('Default target role'),
    '#default_value' => variable_get('invite_target_role_default', DRUPAL_AUTHENTICATED_RID),
    '#options' => $target_roles,
    '#description' => t('Choose the default role that invited users will be added to when they register. For example, <em>authenticated user</em>.'),
    '#required' => TRUE,
  );
  $form['general']['invite_expiry'] = array(
    '#type' => 'select',
    '#title' => t('Invitation expiry'),
    '#default_value' => variable_get('invite_expiry', 30),
    '#options' => drupal_map_assoc(array(
      1,
      3,
      7,
      14,
      30,
      60,
    )),
    '#description' => t('Set the expiry period for user invitations, in days.'),
    '#multiple' => FALSE,
    '#required' => TRUE,
  );

  // Role settings
  $form['role'] = array(
    '#type' => 'fieldset',
    '#title' => t('Role settings'),
    '#description' => t('Note: Permission related settings can be found at the <a href="!admin-user-access">Access control page</a>.', array(
      '!admin-user-access' => url('admin/user/access'),
    )),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  foreach ($roles as $role) {
    $role_no_space = str_replace(' ', '_', $role);
    $form['role'][$role_no_space] = array(
      '#type' => 'fieldset',
      '#title' => t('@role settings', array(
        '@role' => ucfirst($role),
      )),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
    );
    $form['role'][$role_no_space]['invite_target_role_' . $role_no_space] = array(
      '#type' => 'select',
      '#title' => t('Target role'),
      '#default_value' => variable_get('invite_target_role_' . $role_no_space, DRUPAL_AUTHENTICATED_RID),
      '#options' => $target_roles,
      '#description' => t('You may choose to add invited users to another role (in addition to the default role set in the general section) when they have been invited by a member of %role.', array(
        '%role' => $role,
      )),
      '#required' => TRUE,
    );
    $form['role'][$role_no_space]['invite_maxnum_' . $role_no_space] = array(
      '#type' => 'select',
      '#title' => t('Invitation limit'),
      '#default_value' => variable_get('invite_maxnum_' . $role_no_space, INVITE_UNLIMITED_INVITES),
      '#options' => array(
        5 => 5,
        10 => 10,
        20 => 20,
        50 => 50,
        100 => 100,
        500 => 500,
        1000 => 1000,
        INVITE_UNLIMITED_INVITES => t('unlimited'),
      ),
      '#description' => t('Allows to limit the total number of invitations members of %role can send.', array(
        '%role' => $role,
      )),
      '#multiple' => FALSE,
      '#required' => TRUE,
    );
  }

  // E-mail settings
  $form['email'] = array(
    '#type' => 'fieldset',
    '#title' => t('E-mail settings'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['email']['invite_subject'] = array(
    '#type' => 'textfield',
    '#title' => t('Subject'),
    '#default_value' => variable_get('invite_subject', t('[inviter-raw] has sent you an invite!')),
    '#size' => 30,
    '#maxlength' => 64,
    '#description' => t('Type the default subject of the invitation e-mail.') . ' ' . t('Use the syntax [token] if you want to insert a replacement pattern.'),
    '#required' => TRUE,
  );
  $form['email']['invite_subject_editable'] = array(
    '#type' => 'checkbox',
    '#title' => t('Editable subject'),
    '#description' => t('Choose whether users should be able to customize the subject.'),
    '#default_value' => variable_get('invite_subject_editable', FALSE),
  );
  $form['email']['invite_default_mail_template'] = array(
    '#type' => 'textarea',
    '#title' => t('Mail template'),
    '#default_value' => _invite_get_mail_template(),
    '#required' => TRUE,
    '#description' => t('Use the syntax [token] if you want to insert a replacement pattern.'),
  );
  $form['email']['token_help'] = array(
    '#title' => t('Replacement patterns'),
    '#type' => 'fieldset',
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['email']['token_help']['help'] = array(
    '#value' => theme('invite_token_help', array(
      'user',
      'profile',
      'invite',
    )),
  );
  $form['email']['invite_use_users_email'] = array(
    '#type' => 'radios',
    '#title' => t('<em>From</em> e-mail address'),
    '#description' => t('Choose which e-mail address will be in the From: header for the invitation mails sent; <em>site</em> or <em>inviter</em>. <em>Site</em> will use the default e-mail address of the site, whereas <em>inviter</em> will use the e-mail address of the user who is sending the invitation. Alternatively, you can set this value manually by clicking on <em>advanced settings</em> below.'),
    '#options' => array(
      t('site'),
      t('inviter'),
    ),
    '#default_value' => variable_get('invite_use_users_email', 0),
  );
  $form['email']['invite_use_users_email_replyto'] = array(
    '#type' => 'radios',
    '#title' => t('<em>Reply-To</em> e-mail address'),
    '#description' => t('Choose which e-mail address will be in the Reply-To: header for the invitation mails sent; <em>site</em> or <em>inviter</em>. <em>Site</em> will use the default e-mail address of the site, whereas <em>inviter</em> will use the e-mail address of the user who is sending the invitation. Alternatively, you can set this value manually by clicking on <em>advanced settings</em> below.'),
    '#options' => array(
      t('site'),
      t('inviter'),
    ),
    '#default_value' => variable_get('invite_use_users_email_replyto', 0),
  );
  $form['email']['advanced'] = array(
    '#type' => 'fieldset',
    '#title' => t('Advanced settings'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#description' => t('<strong>Note:</strong> unless these fields are blank, they will override the radio button choices above.'),
  );
  $form['email']['advanced']['invite_manual_from'] = array(
    '#type' => 'textfield',
    '#title' => t('Manually override <em>From</em> e-mail address'),
    '#default_value' => variable_get('invite_manual_from', NULL),
    '#description' => t('The e-mail address the invitation e-mail is sent from.'),
  );
  $form['email']['advanced']['invite_manual_reply_to'] = array(
    '#type' => 'textfield',
    '#title' => t('Manually override <em>Reply-To</em> e-mail address'),
    '#default_value' => variable_get('invite_manual_reply_to', NULL),
    '#description' => t('The e-mail address you want recipients to reply to.'),
  );

  // Invite page customization
  $form['custom'] = array(
    '#type' => 'fieldset',
    '#title' => t('Invite page customization'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
  );
  $form['custom']['invite_page_title'] = array(
    '#type' => 'textfield',
    '#title' => t('Invite page title'),
    '#default_value' => variable_get('invite_page_title', t('Invite your friends and colleagues')),
    '#description' => t('The title of the page where users invite friends.'),
    '#required' => TRUE,
  );
  return system_settings_form($form);
}

/**
 * Menu callback; handle incoming requests for accepting an invite.
 */
function invite_action() {
  global $user;
  if (!$user->uid) {

    // User not logged in
    $invite = invite_load(arg(2));

    // Check for a valid invite code
    if (_invite_validate($invite)) {
      $_SESSION[INVITE_SESSION_NAME] = $invite->reg_code;
      drupal_goto('user/register');
    }
  }
  drupal_goto();
}

/**
 * Load an invite record for a tracking code.
 *
 * @param $code
 *   A registration code to load the invite record for.
 * @return
 *   An invite record.
 */
function invite_load($code) {
  $result = db_query("SELECT * FROM {invite} WHERE reg_code = '%s'", $code);
  if ($invite = db_fetch_object($result)) {
    $invite->inviter = user_load(array(
      'uid' => $invite->uid,
    ));
    $invite->data = (array) unserialize($invite->data);
  }
  return $invite;
}

/**
 * Returns an invite record from an invite code stored in the user's session.
 *
 * @return
 *   An invite record, or FALSE if there is no invite code stored in the
 *   user's session.
 */
function invite_load_from_session() {
  if (isset($_SESSION[INVITE_SESSION_NAME])) {
    return invite_load($_SESSION[INVITE_SESSION_NAME]);
  }
  return FALSE;
}

/**
 * Menu callback; display an overview of sent invitations.
 *
 * @param $page
 *   Which invites to list: accepted, pending, or expired.
 */
function invite_overview($page = 'accepted') {
  global $user;
  $rows = array();
  $time = time();
  $profile_access = user_access('access user profiles');
  $allow_delete = user_access('withdraw accepted invitations');
  switch ($page) {
    case 'accepted':
    default:
      $sql = "SELECT i.*, u.uid FROM {invite} i LEFT JOIN {users} u ON u.uid = i.mid AND u.uid != 0 WHERE i.uid = %d AND i.timestamp > 0 ORDER BY u.uid DESC, i.expiry DESC";
      break;
    case 'pending':
      $sql = "SELECT * FROM {invite} WHERE uid = %d AND timestamp = 0 AND expiry >= %d ORDER BY expiry DESC";
      break;
    case 'expired':
      $sql = "SELECT * FROM {invite} WHERE uid = %d AND timestamp = 0 AND expiry < %d ORDER BY expiry DESC";
      break;
  }
  $result = pager_query($sql, 50, 0, NULL, $user->uid, $time);
  while ($invite = db_fetch_object($result)) {
    $row = array();
    switch ($page) {
      case 'accepted':
      default:
        $account_exists = !is_null($invite->uid);
        if ($profile_access) {
          $row[] = $account_exists ? l($invite->email, 'user/' . $invite->mid, array(
            'title' => t('View user profile.'),
          )) : check_plain($invite->email);
        }
        else {
          $row[] = check_plain($invite->email);
        }
        $row[] = $account_exists ? t('Accepted') : t('Deleted');
        $row[] = $allow_delete ? l(t('Withdraw'), "invite/withdraw/{$page}/{$invite->reg_code}") : '';
        break;
      case 'pending':
      case 'expired':
        $expired = $invite->expiry < $time;
        $row[] = check_plain($invite->email);
        $row[] = $expired ? t('Expired') : t('Pending');
        $row[] = l(t('Withdraw'), "invite/withdraw/{$page}/{$invite->reg_code}");
        break;
    }
    $rows[] = $row;
  }
  return theme('invite_overview', $rows);
}

/**
 * Theme function; display the invite overview table.
 *
 * @param $items
 *   An array of table rows.
 *
 * @ingroup themeable
 */
function theme_invite_overview($items) {
  if (count($items) > 0) {
    $headers = array(
      t('E-mail'),
      t('Status'),
      '',
    );
    $output = theme('table', $headers, $items, array(
      'id' => 'invites',
    ));
    $output .= theme('pager', NULL, 50, 0);
  }
  else {
    $output = t('No invitations available.');
  }
  return $output;
}

/**
 * Generate the invite form.
 *
 * @param $op
 *   The type of form to generate, 'page' or 'block'.
 * @return
 *   A form definition array.
 */
function invite_form($op = 'page') {
  global $user;
  $max_invites = invite_max_invites();
  $allow_multiple = user_access('send mass invitations');

  // Check that the user is within limits
  if ($max_invites != INVITE_UNLIMITED_INVITES) {
    $invites_sent = db_result(db_query("SELECT COUNT(*) FROM {invite} WHERE uid = %d", $user->uid));
    $invites_left = max($max_invites - $invites_sent, 0);
    if ($invites_left == 0) {
      if ($op == 'block') {

        // Hide the block
        $form['#access'] = FALSE;
        return $form;
      }
      else {
        drupal_set_message(t('Sorry, you reached the maximum number (@max) of invitations.', array(
          '@max' => $max_invites,
        )), 'error');
        drupal_goto(user_access('track invitations') ? 'invite/list' : '<front>');
      }
    }
  }
  else {
    $invites_left = INVITE_UNLIMITED_INVITES;
  }
  $form['remaining_invites'] = array(
    '#type' => 'value',
    '#value' => $invites_left,
  );
  switch ($op) {
    case 'block':
      $form['#action'] = url('invite');
      $form['invite'] = array(
        '#value' => t('Recommend @site-name to:', array(
          '@site-name' => variable_get('site_name', t('Drupal')),
        )),
      );
      $description = '';
      if ($max_invites != INVITE_UNLIMITED_INVITES) {
        $description = format_plural($invites_left, '1 invite left', '@count invites left');
      }
      $form['email'] = array(
        '#type' => 'textfield',
        '#size' => 20,
        '#maxlength' => 64,
        '#description' => $description,
        '#required' => TRUE,
      );
      $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Send invite'),
      );
      $form['link'] = array(
        '#prefix' => '<div><small>',
        '#value' => l(t('View your invites'), 'invite/list'),
        '#suffix' => '</small></div>',
        '#access' => user_access('track invitations'),
      );
      break;
    case 'page':
    default:

      // Remaining invites
      if ($max_invites != INVITE_UNLIMITED_INVITES) {
        $form['remaining_invites_markup'] = array(
          '#value' => format_plural($invites_left, 'You have 1 invite left.', 'You have @count invites left.'),
        );
      }

      // Sender email address
      if ($user->uid && variable_get('invite_use_users_email', 0)) {
        $from = $user->mail;
      }
      else {
        $from = variable_get('site_mail', ini_get('sendmail_from'));
      }

      // Personalize displayed email
      // see http://drupal.org/project/pmail
      if (module_exists('pmail')) {
        $from = personalize_email($from);
      }
      $form['from'] = array(
        '#type' => 'item',
        '#title' => t('From'),
        '#value' => check_plain($from),
      );

      // Recipient email address
      $failed_emails = '';
      if (isset($_SESSION['invite_failed_emails'])) {
        $failed_emails = implode("\n", (array) unserialize($_SESSION['invite_failed_emails']));
        unset($_SESSION['invite_failed_emails']);
      }
      $form['email'] = array(
        '#title' => t('To'),
        '#default_value' => $failed_emails,
        '#description' => format_plural((int) $allow_multiple + 1, 'Type the e-mail address of the person you would like to invite.', 'Type the e-mail addresses of the persons you would like to invite. Addresses should be separated by newlines or commas.'),
        '#required' => TRUE,
      );
      if ($allow_multiple) {
        $form['email']['#type'] = 'textarea';
        $form['email']['#rows'] = 3;
      }
      else {
        $form['email']['#type'] = 'textfield';
        $form['email']['#maxlength'] = 64;
      }
      if ($failed_emails) {
        $form['email']['#attributes']['class'] = 'error';
      }

      // Message subject
      $subject = invite_get_subject();
      if (variable_get('invite_subject_editable', FALSE)) {
        $form['subject'] = array(
          '#type' => 'textfield',
          '#title' => t('Subject'),
          '#default_value' => $subject,
          '#maxlength' => 64,
          '#description' => t('Type the subject of the invitation e-mail.'),
          '#required' => TRUE,
        );
      }
      else {
        $form['subject'] = array(
          '#type' => 'item',
          '#title' => t('Subject'),
          '#value' => check_plain($subject),
        );
      }

      // Message body
      $form['body'] = array(
        '#type' => 'item',
        '#title' => t('Message'),
      );
      $form['message'] = array(
        '#type' => 'textarea',
        '#description' => format_plural((int) $allow_multiple + 1, 'This message will be added to the mail sent to the person you are inviting.', 'This message will be added to the mail sent to the persons you are inviting.'),
      );
      $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Submit'),
      );
      break;
  }
  return $form;
}

/**
 * Theme function for the invite form.
 *
 * @ingroup themeable
 */
function theme_invite_form($form) {
  $output = '';
  $op = $form['#parameters'][1];
  if ($op == 'page') {

    // Show form elements
    $output .= drupal_render($form['remaining_invites_markup']);
    $output .= drupal_render($form['remaining_invites']);
    $output .= drupal_render($form['from']);
    $output .= drupal_render($form['email']);
    $output .= drupal_render($form['subject']);

    // Show complete invitation message
    $output .= drupal_render($form['body']);
    $output .= '<div class="invite-message"><div class="opening">';

    // Prepare invitation message
    $message_form = "</p></div>\n" . drupal_render($form['message']) . "\n" . '<div class="closing"><p>';
    $body = _filter_autop(t(_invite_get_mail_template()));

    // Perform token replacement on message body
    $types = _invite_token_types(array(
      'data' => array(
        'message' => $message_form,
      ),
    ));
    $output .= token_replace_multiple($body, $types);
    $output .= "</div></div>\n";
  }

  // Render all missing form elements
  $output .= drupal_render($form);
  return $output;
}

/**
 * Forms API callback; validates the incoming form data.
 *
 * Filters out e-mails that are already registered or have been invited before.
 * Checks the invite limit of the user and the max. number of invites per turn.
 */
function invite_form_validate($form_id, &$edit) {
  global $user;
  $emails = _invite_get_emails($edit['email']);
  if (count($emails) > 0) {

    // Filter out already registered users, but pass validation
    $failed_emails = _invite_validate_emails("SELECT mail AS email FROM {users} WHERE mail IN (%s)", $emails);
    if (count($failed_emails)) {
      $error = format_plural(count($failed_emails), 'The following recipient is already a member:', 'The following recipients are already members:') . '<br />';
      foreach ($failed_emails as $key => $email) {
        $account = user_load(array(
          'mail' => $email,
        ));
        $failed_emails[$key] = theme('username', $account) . ' (' . check_plain($email) . ')';
      }
      $error .= implode(', ', $failed_emails);
      drupal_set_message($error, 'error');
    }

    // Filter out already invited users, but pass validation
    $failed_emails = _invite_validate_emails("SELECT email FROM {invite} WHERE email IN (%s)", $emails);
    if (count($failed_emails)) {
      $error = format_plural(count($failed_emails), 'The following recipient has already been invited:', 'The following recipients have already been invited:') . '<br />';
      $error .= implode(', ', array_map('check_plain', $failed_emails));
      drupal_set_message($error, 'error');
    }

    // Check that there is at least one valid e-mail remaining after filtering
    // out dupes
    if (count($emails) == 0) {
      form_set_error('email');
      return;
    }

    // Check invite limit, fail to let the user choose which ones to send
    if ($edit['remaining_invites'] != INVITE_UNLIMITED_INVITES && count($emails) > $edit['remaining_invites']) {
      form_set_error('email', format_plural($edit['remaining_invites'], 'You have only 1 invite left.', 'You have only @count invites left.'));
      return;
    }

    // Check number of e-mails
    if (!user_access('send mass invitations') && count($emails) > 1) {
      form_set_error('email', t('You cannot send more than one invitation.'));
      return;
    }
  }

  // Save valid emails
  form_set_value(array(
    '#parents' => array(
      'valid_emails',
    ),
  ), $emails);
}

/**
 * Forms API callback; processes the incoming form data.
 *
 * Sends out invitation e-mails.
 */
function invite_form_submit($form_id, $edit) {
  $failed_emails = array();
  $count_failed = $count_success = 0;
  if (isset($_SESSION['invite_failed_emails'])) {
    $failed_emails = (array) unserialize($_SESSION['invite_failed_emails']);
    $count_failed = count($failed_emails);
  }
  $subject = isset($edit['subject']) ? trim($edit['subject']) : invite_get_subject();
  $message = isset($edit['message']) ? trim($edit['message']) : NULL;
  foreach ($edit['valid_emails'] as $email) {

    // Create the invite object
    $invite = _invite_substitutions(array(
      'email' => $email,
      'code' => invite_regcode(),
      'data' => array(
        'subject' => $subject,
        'message' => $message,
      ),
    ));

    // Perform token replacement on mail body
    $body = token_replace_multiple(t(_invite_get_mail_template()), _invite_token_types($invite));

    // Send e-mail
    if (invite_send_invite($email, $subject, $body)) {

      // Save invite
      invite_save($invite);

      // Notify other modules
      $args = array(
        'inviter' => $invite->inviter,
        'code' => $invite->code,
      );
      module_invoke_all('invite', 'invite', $args);
      $count_success++;
    }
    else {
      $failed_emails[] = $email;
    }
  }
  if ($failed_emails) {
    $_SESSION['invite_failed_emails'] = serialize($failed_emails);
  }
  if ($count_success) {
    $message = format_plural($count_success, 'Your invitation has been successfully sent. You will be notified when the invitee joins the site.', '@count invitations have been successfully sent. You will be notified when any invitee joins the site.');
    drupal_set_message($message);
  }
  if ($count_failed) {
    $message = format_plural($count_failed, 'The entered e-mail address is invalid. Please correct it.', '@count entered e-mail addresses are invalid. Please correct them.');
    drupal_set_message($message, 'error');
  }
  else {
    if (user_access('track invitations')) {
      return 'invite/list';
    }
  }
  return 'invite';
}

/**
 * Return max number of invites a user may send.
 *
 * @param $uid
 *   A user id.
 * @return
 *   The number of invites the user may send.
 */
function invite_max_invites($uid = NULL) {
  global $user;
  $account = is_null($uid) ? $user : user_load(array(
    'uid' => $uid,
  ));
  if ($account->uid == 1) {
    return INVITE_UNLIMITED_INVITES;
  }
  $max = 0;
  foreach (user_roles(0, 'send invitations') as $role) {
    $role_no_space = str_replace(' ', '_', $role);
    if (in_array($role, $account->roles)) {
      $role_max = variable_get('invite_maxnum_' . $role_no_space, INVITE_UNLIMITED_INVITES);
      if ($role_max == INVITE_UNLIMITED_INVITES) {
        return INVITE_UNLIMITED_INVITES;
      }
      if ($role_max > $max) {
        $max = $role_max;
      }
    }
  }
  return $max;
}

/**
 * Return the invite e-mail subject.
 *
 * @param $substitutions
 *   Associative array of substitutions for token replacement.
 * @return
 *   The e-mail subject.
 */
function invite_get_subject($substitutions = array()) {
  $subject = t(variable_get('invite_subject', t('[inviter-raw] has sent you an invite!')));
  return token_replace_multiple($subject, _invite_token_types($substitutions));
}

/**
 * Creates a unique tracking code.
 *
 * @return
 *   An 8-digit long, unique tracking code.
 */
function invite_regcode() {
  do {
    $reg_code = user_password(8);
    $result = db_query("SELECT COUNT(*) FROM {invite} WHERE reg_code = '%s'", $reg_code);
  } while (db_result($result));
  return $reg_code;
}

/**
 * Sends an invite to the specified e-mail address.
 *
 * @param $recipient
 *   The recipient's e-mail address.
 * @param $subject
 *   The e-mail subject.
 * @param $body
 *   The e-mail body.
 */
function invite_send_invite($recipient, $subject, $body) {
  global $user;
  $headers = array();

  // Prevent e-mail looking like spam to SPF-enabled MTAs
  // see http://drupal.org/node/133789
  $from_site = variable_get('site_mail', ini_get('sendmail_from'));
  if ($from_site) {
    $headers['Sender'] = $headers['Return-Path'] = $headers['Errors-To'] = $from_site;
  }

  // Manual settings override custom settings below
  // Note: default value must be NULL to comply with legacy Drupal versions
  $from = variable_get('invite_manual_from', NULL);
  $reply_to = variable_get('invite_manual_reply_to', NULL);

  // Set custom From and Reply-To headers
  if (!$from) {
    if ($user->uid && variable_get('invite_use_users_email', 0)) {
      $from = $user->mail;
    }
    else {
      if ($from_site) {
        $from = $from_site;
      }
    }
  }
  if (!$reply_to) {
    if ($user->uid && variable_get('invite_use_users_email_replyto', 0)) {
      $reply_to = $user->mail;
    }
    else {
      if ($from_site) {
        $reply_to = $from_site;
      }
    }
  }
  if ($reply_to) {
    $headers['Reply-To'] = $reply_to;
  }
  if (!($success = drupal_mail('invite-mail', $recipient, $subject, wordwrap($body, 72), $from, $headers))) {
    static $error_shown = FALSE;
    if (!$error_shown) {
      drupal_set_message(t('Problems occurred while sending the invitation(s). Please contact the site administrator.'), 'error');
      $error_shown = TRUE;
    }
    watchdog('invite', t('Failed sending invitation. To: @email From: @from', array(
      '@email' => '<' . $recipient . '>',
      '@from' => '<' . $from . '>',
    )));
  }
  return $success;
}

/**
 * Save an invite to the database.
 *
 * @param $edit
 *   Associative array of data to store.
 * @return
 *   The result of the database operation.
 */
function invite_save($edit) {
  $edit = (array) $edit;
  $expiry = time() + variable_get('invite_expiry', 30) * 60 * 60 * 24;
  $data = serialize($edit['data']);
  return db_query("INSERT INTO {invite} (email, reg_code, uid, expiry, data) VALUES ('%s', '%s', %d, %d, '%s')", $edit['email'], $edit['code'], $edit['inviter']->uid, $expiry, $data);
}

/**
 * Menu callback; display confirm form to delete an invitation.
 *
 * @param $origin
 *   String denoting the orginating invite status page.
 * @param $code
 *   A registration code to remove the invitation for.
 */
function invite_delete($origin, $code) {
  global $user;
  $invite = invite_load($code);

  // Inviter must be the current user
  if ($invite->inviter->uid == $user->uid) {

    // Verify the invitation may be deleted
    if (!$invite->timestamp || user_access('withdraw accepted invitations')) {
      $form['invite'] = array(
        '#type' => 'value',
        '#value' => $invite,
      );
      $description = !$invite->timestamp && $invite->expiry > time() ? t("The invitee won't be able to register any more using this invitation.") : '';
      return confirm_form($form, t('Are you sure you want to withdraw the invitation to %email?', array(
        '%email' => $invite->email,
      )), "invite/list/{$origin}", $description . ' ' . t('This action cannot be undone.'), t('Withdraw'), t('Cancel'));
    }
    else {
      drupal_set_message(t('Invitations to registered users cannot be withdrawn.'), 'error');
    }
  }
  else {
    watchdog('invite', t('Detected malicious attempt to delete an invitation.'), WATCHDOG_WARNING, l(t('view'), 'user/' . $user->uid));
    drupal_access_denied();
  }
  drupal_goto('invite/list');
}

/**
 * Submit handler to delete an invitation.
 */
function invite_delete_submit($form_id, $form_values) {
  $invite = $form_values['invite'];
  db_query("DELETE FROM {invite} WHERE reg_code = '%s'", $invite->reg_code);
  drupal_set_message(t('Invitation to %email has been withdrawn.', array(
    '%email' => $invite->email,
  )));

  // Notify other modules
  $args = array(
    'inviter' => $invite->inviter,
    'email' => $invite->email,
  );
  module_invoke_all('invite', 'cancel', $args);
  drupal_goto('invite/list');
}

/**
 * @{
 * Token functions.
 */

/**
 * Implementation of hook_token_values().
 */
function invite_token_values($type = 'all', $object = NULL) {
  $values = array();
  if ($type == 'invite' && is_object($object)) {

    // NOTE: Invite is currently only capable of sending plain text e-mails.
    // If you intend to change that, you need to properly escape all user input
    // by adding check_plain() around below values!
    $values['inviter-raw'] = $object->inviter->name;
    $values['invite-mail'] = $object->email;
    $values['invite-message-raw'] = $object->data['message'];
    $values['join-link'] = url('invite/accept/' . $object->code, NULL, NULL, TRUE);
  }
  return $values;
}

/**
 * Implementation of hook_token_list().
 */
function invite_token_list($type = 'all') {
  if ($type == 'invite' || $type == 'all') {
    $tokens['invite']['inviter-raw'] = t("Inviter's name. WARNING - raw user input.");
    $tokens['invite']['invite-mail'] = t('The e-mail address of the invited user.');
    $tokens['invite']['invite-message-raw'] = t('The personal message for the invitee as unfiltered text. WARNING - raw user input.');
    $tokens['invite']['join-link'] = t('The link to the registration page of the site.');
    return $tokens;
  }
}

/**
 * @}
 */

/**
 * @{
 * Helper functions.
 */

/**
 * Extract valid e-mail addresses from a string.
 *
 * E-mails must be separated by newlines or commas. E-mails are allowed to
 * include a display name (eg. Some Name <foo@example.com>). Invalid addresses
 * are filtered out and stored in a session variable for re-display.
 *
 * @param $string
 *   The string to process. Recognized delimiters are comma, NL and CR.
 * @return
 *   Array of valid e-mail addresses.
 */
function _invite_get_emails($string) {
  $valid_emails = $failed_emails = array();
  $user = '[a-zA-Z0-9_\\-\\.\\+\\^!#\\$%&*+\\/\\=\\?\\`\\|\\{\\}~\']+';
  $domain = '(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.?)+';
  $ipv4 = '[0-9]{1,3}(\\.[0-9]{1,3}){3}';
  $ipv6 = '[0-9a-fA-F]{1,4}(\\:[0-9a-fA-F]{1,4}){7}';
  $rx = "/({$user}@({$domain}|(\\[({$ipv4}|{$ipv6})\\])))>?\$/";
  $emails = array_unique(split("[,\n\r]", $string));
  foreach ($emails as $email) {
    $email = trim($email);
    if ($email) {
      if (preg_match($rx, $email, $match)) {
        $valid_emails[] = $match[1];
      }
      else {
        $failed_emails[] = $email;
      }
    }
  }
  if (count($failed_emails)) {
    $_SESSION['invite_failed_emails'] = serialize($failed_emails);
  }
  return $valid_emails;
}

/**
 * Filter out e-mails based on a database query.
 *
 * @param $sql
 *   The database query to execute. The query is expected to conatain one
 *   replacement variable, namely a <code>IN(%s)</code> construct, where
 *   the given list of e-mail addresses will be inserted.
 * @param &$emails
 *   The list of e-mail addresses to validate. When this function returns, all
 *   invalid e-mails have already been removed.
 * @return
 *   The list of failed e-mail addresses.
 */
function _invite_validate_emails($sql, &$emails) {
  $failed_emails = array();
  $emails_sql = "'" . implode("','", array_map('db_escape_string', $emails)) . "'";
  $result = db_query(sprintf($sql, $emails_sql));
  while ($row = db_fetch_object($result)) {
    $failed_emails[] = $row->email;
  }

  // Leave only valid emails
  $emails = array_diff($emails, $failed_emails);
  return $failed_emails;
}

/**
 * Validates an invite record.
 *
 * @param $invite
 *   An invite record as returned by invite_load().
 * @return
 *   TRUE if the invite is valid, otherwise this function won't return.
 */
function _invite_validate($invite) {
  if (!$invite || !$invite->inviter) {
    drupal_set_message(t('This invitation has been withdrawn.'));
    drupal_goto();
  }
  else {
    if ($invite->timestamp != 0) {
      drupal_set_message(t('This invitation has already been used. Please login now with your username and password.'));
      drupal_goto('user');
    }
    else {
      if ($invite->expiry < time()) {
        drupal_set_message(t('Sorry, this invitation has expired.'));
        drupal_goto();
      }
      else {
        return TRUE;
      }
    }
  }
}

/**
 * Escalates an invited user's role(s), based on the role(s) of the inviter.
 *
 * @param $invitee
 *   A user object.
 */
function _invite_role_escalate($invitee) {

  // Default target role
  $roles = array(
    'default',
  );

  // Add roles of inviter
  $inviter_uid = db_result(db_query("SELECT uid FROM {invite} WHERE mid = %d", $invitee->uid));
  if ($inviter_uid && ($inviter = user_load(array(
    'uid' => $inviter_uid,
  )))) {
    $roles = array_merge($roles, array_intersect($inviter->roles, user_roles(0, 'send invitations')));
  }

  // Map to configured target roles
  $targets = array();
  foreach ($roles as $role) {
    $role_no_space = str_replace(' ', '_', $role);
    $target = variable_get('invite_target_role_' . $role_no_space, DRUPAL_AUTHENTICATED_RID);
    if ($target != DRUPAL_AUTHENTICATED_RID) {
      $targets[$target] = $target;
    }
  }

  // Notify other modules of changed user
  $edit = array(
    'roles' => $targets,
  );
  user_module_invoke('update', $edit, $invitee);

  // Save new user role(s)
  foreach ($targets as $target) {
    db_query("DELETE FROM {users_roles} WHERE uid = %d AND rid = %d", $invitee->uid, $target);
    db_query("INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)", $invitee->uid, $target);
  }

  // Notify other modules of role escalation
  $args = array(
    'invitee' => $invitee,
    'inviter' => $inviter,
    'roles' => $targets,
  );
  module_invoke_all('invite', 'escalate', $args);
}

/**
 * Sets an invitation's status to accepted.
 *
 * @param $email
 *   The e-mail address the invitee actually used in the registration.
 * @param $uid
 *   The user id of the newly registered user.
 * @param $code
 *   The registration code of the originating invite.
 */
function _invite_set_accepted($email, $uid, $code) {
  db_query("UPDATE {invite} SET timestamp = %d, mid = %d, email = '%s' WHERE reg_code = '%s'", time(), $uid, $email, $code);
}

/**
 * Displays a notification message when an invited user has registered.
 *
 * @param $uid
 *   The user id to check accepted invitations for.
 */
function _invite_check_messages($uid) {
  $result = db_query('SELECT i.mid, i.email FROM {invite} i INNER JOIN {users} u ON u.uid = i.mid AND u.status = 1 WHERE i.uid = %d AND i.timestamp != 0 AND i.received = 0', $uid);
  while ($invite = db_fetch_object($result)) {
    $account = user_load(array(
      'uid' => $invite->mid,
    ));
    drupal_set_message(t('!user (@email) has joined @site-name!', array(
      '!user' => theme('username', $account),
      '@email' => $invite->email,
      '@site-name' => variable_get('site_name', t('Drupal')),
    )));
    db_query("UPDATE {invite} SET received = 1 WHERE email = '%s'", $invite->email);
  }
}

/**
 * Returns the configured or default e-mail template.
 *
 * @return
 *   The localized e-mail body.
 */
function _invite_get_mail_template() {
  $template = t("Your friend, [inviter-raw], has invited you to join [site-name] at [site-url].\n\nTo become a member of [site-name], click the link below or paste it into the address bar of your browser.\n\n[join-link]\n\n----------\n[invite-message-raw]");
  return variable_get('invite_default_mail_template', $template);
}

/**
 * Provide token types for use in invite message replacements.
 *
 * @param $args
 *   Associative array of additional arguments to merge in the invite object.
 * @return
 *   Array of token types suitable as input for token_replace().
 */
function _invite_token_types($args = array()) {
  global $user;
  if (!is_array($args)) {
    $args = (array) $args;
  }
  $invite = _invite_substitutions($args);
  return array(
    'user' => $user,
    'profile' => $user,
    'invite' => $invite,
  );
}

/**
 * Create an invite object with reasonable default values for use in
 * token replacements.
 *
 * @param $args
 *   Associative array of additional arguments to merge into the invite object.
 * @return
 *   The invite object.
 */
function _invite_substitutions($args = array()) {
  global $user;
  $defaults = array(
    'inviter' => $user,
    'email' => '--recipient-email--',
    'code' => '--invite-code--',
    'data' => array(
      'subject' => NULL,
      'message' => NULL,
    ),
  );
  return (object) array_merge($defaults, $args);
}

/**
 * Approves the account of an invited user.
 *
 * Invited users are always automatically approved (ie. unblocked).
 *
 * @param $uid
 *   The user id to unblock.
 */
function _invite_unblock($uid) {
  db_query("UPDATE {users} SET status = 1 WHERE uid = %d", $uid);
}

Functions

Namesort descending Description
invite_action Menu callback; handle incoming requests for accepting an invite.
invite_block Implementation of hook_block().
invite_cron Implementation of hook_cron().
invite_delete Menu callback; display confirm form to delete an invitation.
invite_delete_submit Submit handler to delete an invitation.
invite_disable Implementation of hook_disable().
invite_form Generate the invite form.
invite_form_alter Implementation of hook_form_alter().
invite_form_submit Forms API callback; processes the incoming form data.
invite_form_validate Forms API callback; validates the incoming form data.
invite_get_subject Return the invite e-mail subject.
invite_help Implementation of hook_help().
invite_load Load an invite record for a tracking code.
invite_load_from_session Returns an invite record from an invite code stored in the user's session.
invite_max_invites Return max number of invites a user may send.
invite_menu Implementation of hook_menu().
invite_overview Menu callback; display an overview of sent invitations.
invite_perm Implementation of hook_perm().
invite_regcode Creates a unique tracking code.
invite_save Save an invite to the database.
invite_send_invite Sends an invite to the specified e-mail address.
invite_settings Menu callback; display invite settings form.
invite_token_list Implementation of hook_token_list().
invite_token_values Implementation of hook_token_values().
invite_user Implementation of hook_user().
theme_invite_form Theme function for the invite form.
theme_invite_overview Theme function; display the invite overview table.
_invite_check_messages Displays a notification message when an invited user has registered.
_invite_get_emails Extract valid e-mail addresses from a string.
_invite_get_mail_template Returns the configured or default e-mail template.
_invite_role_escalate Escalates an invited user's role(s), based on the role(s) of the inviter.
_invite_set_accepted Sets an invitation's status to accepted.
_invite_substitutions Create an invite object with reasonable default values for use in token replacements.
_invite_token_types Provide token types for use in invite message replacements.
_invite_unblock Approves the account of an invited user.
_invite_validate Validates an invite record.
_invite_validate_emails Filter out e-mails based on a database query.

Constants

Namesort descending Description
INVITE_SESSION_NAME Session variable name.
INVITE_UNLIMITED_INVITES Special unlimited value.