'. 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 'user/%/invites/pending': $output = '
'. t("The invitations shown on this page haven't been accepted yet."); break; case 'user/%/invites/expired': $output = '
'. 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 deleted means the user account has been terminated.') .'
'; if (!user_access('withdraw accepted invitations')) { $output .= ''. 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.") .'
'; } return $output; } /** * Display module help. */ function _invite_module_help() { $file = drupal_get_path('module', 'invite') .'/README.txt'; if (file_exists($file)) { return _filter_autop(check_plain(file_get_contents($file))); } } /** * Implementation of hook_theme(). */ function invite_theme() { return array( 'invite_form' => array( 'arguments' => array('form' => NULL), ), 'invite_user_overview' => array( 'arguments' => array('items' => NULL), 'file' => 'invite_admin.inc', ), 'invite_token_help' => array( 'arguments' => array('type' => NULL, 'prefix' => NULL, 'suffix' => NULL), 'file' => 'invite_token.inc', ), ); } /** * Implementation of hook_perm(). */ function invite_perm() { return array( 'send invitations', 'send mass invitations', 'track invitations', 'withdraw accepted invitations' ); } /** * Implements hook_init(). */ function invite_init() { global $user; // Notify current user about newly joined invitees. if (!empty($user->invite_sent) && !module_invoke('throttle', 'status')) { invite_notify($user->uid); } } /** * Implementation of hook_menu(). */ function invite_menu() { // Admin menu items $items['admin/user/invite'] = array( 'title' => 'Invites', 'page callback' => 'invite_admin_overview', 'access arguments' => array('administer site configuration'), 'type' => MENU_NORMAL_ITEM, 'file' => 'invite_admin.inc', ); $items['admin/user/invite/list'] = array( 'title' => 'Inviters', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['admin/user/invite/settings'] = array( 'title' => 'Settings', 'page callback' => 'drupal_get_form', 'page arguments' => array('invite_settings'), 'access arguments' => array('administer site configuration'), 'type' => MENU_LOCAL_TASK, 'weight' => 10, 'file' => 'invite_admin.inc', ); $items['admin/user/invite/details/%user'] = array( 'title callback' => 'invite_admin_details_page_title', 'title arguments' => array(4), 'page callback' => 'invite_admin_details', 'page arguments' => array(4), 'access arguments' => array('administer site configuration'), 'type' => MENU_LOCAL_TASK, 'file' => 'invite_admin.inc', ); // Frontend menu items $items['invite'] = array( 'title' => 'Invite a friend', 'title callback' => 'invite_page_title', 'page callback' => 'drupal_get_form', 'page arguments' => array('invite_form', 'page', array()), 'access arguments' => array('send invitations'), 'type' => MENU_NORMAL_ITEM, ); $items['invite/accept/%invite'] = array( 'page callback' => 'invite_accept', 'page arguments' => array(2), 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); $items['invite/withdraw'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('invite_cancel'), 'access arguments' => array('track invitations'), 'type' => MENU_CALLBACK, ); $items['invite/resend/%invite'] = array( 'title' => 'Resend invitation', 'page callback' => 'invite_resend', 'page arguments' => array(2), 'access arguments' => array('send invitations'), 'type' => MENU_CALLBACK, ); // User profile tabs $items['user/%user/invites'] = array( 'title' => 'Invitations', 'page callback' => 'invite_user_overview', 'access callback' => 'invite_access_callback', 'access arguments' => array('track invitations', 1), 'type' => MENU_LOCAL_TASK, 'file' => 'invite_admin.inc', ); $items['user/%user/invites/accepted'] = array( 'title' => 'Accepted', 'page callback' => 'invite_user_overview', 'page arguments' => array('accepted'), 'access callback' => 'invite_access_callback', 'access arguments' => array('track invitations', 1), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -5, 'file' => 'invite_admin.inc', ); $items['user/%user/invites/pending'] = array( 'title' => 'Pending', 'page callback' => 'invite_user_overview', 'page arguments' => array('pending'), 'access callback' => 'invite_access_callback', 'access arguments' => array('track invitations', 1), 'type' => MENU_LOCAL_TASK, 'file' => 'invite_admin.inc', ); $items['user/%user/invites/expired'] = array( 'title' => 'Expired', 'page callback' => 'invite_user_overview', 'page arguments' => array('expired'), 'access callback' => 'invite_access_callback', 'access arguments' => array('track invitations', 1), 'type' => MENU_LOCAL_TASK, 'weight' => 5, 'file' => 'invite_admin.inc', ); $items['user/%user/invites/new'] = array( 'title' => 'New invitation', 'page callback' => 'drupal_get_form', 'page arguments' => array('invite_form', 'page', array()), 'access callback' => 'invite_access_callback', 'access arguments' => array('send invitations', 1), 'type' => MENU_LOCAL_TASK, 'weight' => 10, ); return $items; } /** * Title callback allowing for customization of the invite page title. * * @param $title * The default page title, ie. non-overridden. */ function invite_page_title($title) { return variable_get('invite_page_title', $title); } /** * Title callback for the user details administration page. * * @param $account */ function invite_admin_details_page_title($account) { return t('Invitees of @name', array('@name' => $account->name)); } /** * Access callback ensuring the user profile tabs are visible only to their * owner. * * @param $permission * Required permission to view the item. * @param $account * A user object. */ function invite_access_callback($permission, $account) { return ($account->uid == $GLOBALS['user']->uid && user_access($permission)); } /** * Displays a notification message when an invited user has registered. * * @param $uid * The user id to check accepted invitations for. */ function invite_notify($uid) { $result = db_query('SELECT invitee FROM {invite_notifications} WHERE uid = %d', $uid); while ($row = db_fetch_object($result)) { $account = user_load(array('uid' => $row->invitee, 'status' => 1)); if ($account) { drupal_set_message(t('!user (@email) has joined @site-name!', array('!user' => theme('username', $account), '@email' => $account->mail, '@site-name' => variable_get('site_name', t('Drupal'))))); db_query("DELETE FROM {invite_notifications} WHERE uid = %d AND invitee = %d", $uid, $row->invitee); } } } /** * Menu callback; handle incoming requests for accepting an invite. * * @param $invite * A (unvalidated) invite object. */ function invite_accept($invite) { global $user; if (!$user->uid && invite_validate($invite)) { $_SESSION[INVITE_SESSION] = $invite->reg_code; drupal_goto('user/register'); } drupal_goto(); } /** * Implementation of hook_form_alter(). */ function invite_form_alter(&$form, $form_state, $form_id) { switch ($form_id) { case 'user_admin_settings': // Add new registration mode. // We prepend the option value with a numeric value to make 3rd party // modules like LoginToboggan act like expected. This works because // checking for ('1-inviteonly' == 1) returns TRUE. To reliably determine // the variable value later, we need to use the strict equality operator // (===). $form['registration']['user_register']['#options']['1-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/regcode). if (!$invite && $code = arg(2)) { if ($invite = invite_load($code)) { if (invite_validate($invite)) { $_SESSION[INVITE_SESSION] = $invite->reg_code; } } } if ($invite) { // Preset the 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) === '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) === '1-inviteonly') { $new_items = array(); $new_items[] = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.')))); $form['links']['#value'] = theme('item_list', $new_items); } break; } } /** * 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' AND canceled = 0", $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])) { return invite_load($_SESSION[INVITE_SESSION]); } return FALSE; } /** * 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->joined != 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; } } /** * 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'", $account->mail)); if ($code) { $invite = invite_load($code); } } if ($invite) { _invite_accept($invite, $account); // Flag the inviting user, this triggers status notifications and // saves us some queries otherwise. if ($invite->inviter->uid) { user_save($invite->inviter, array('invite_sent' => TRUE)); } unset($_SESSION[INVITE_SESSION]); } break; case 'delete': invite_delete($account->uid); break; } } /** * Set an invitation's status to accepted. * * @param $invite * An invite object. * @param $account * The user object of the invitee. */ function _invite_accept($invite, $account) { // Update the invitation record. db_query("UPDATE {invite} SET email = '%s', invitee = %d, joined = %d WHERE reg_code = '%s'", $account->mail, $account->uid, time(), $invite->reg_code); // Delete all invites to these e-mail addresses, except this one. db_query("DELETE FROM {invite} WHERE (email = '%s' OR email = '%s') AND reg_code <> '%s'", $invite->email, $account->mail, $invite->reg_code); // Add all users who invited this particular e-mail address to the // notification queue. db_query("INSERT INTO {invite_notifications} (uid, invitee) SELECT uid, %d from {invite} WHERE (email = '%s' OR email = '%s') AND canceled = 0", $account->uid, $invite->email, $account->mail); // Escalate the invitee's role. _invite_escalate_role($account); // Unblock user account. db_query("UPDATE {users} SET status = 1 WHERE uid = %d", $account->uid); } /** * Escalates an invited user's role, based on the role(s) of the inviter. * * @param $account * The user object of the invitee. */ function _invite_escalate_role($account) { // Default target role. $roles = array('default'); // Add roles of inviter. $inviter_uid = db_result(db_query("SELECT uid FROM {invite} WHERE invitee = %d", $account->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, $account); // Save new user role(s). foreach ($targets as $target) { db_query("DELETE FROM {users_roles} WHERE uid = %d AND rid = %d", $account->uid, $target); db_query("INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)", $account->uid, $target); } // Notify other modules of role escalation. $args = array('invitee' => $account, 'inviter' => $inviter, 'roles' => $targets); module_invoke_all('invite', 'escalate', $args); } /** * Physically delete all invites from and to a user. * * @param $uid * The user id to delete invites for. */ function invite_delete($uid) { // Delete invite for this user if the originating user has the permission. $origin = db_result(db_query("SELECT uid FROM {invite} WHERE invitee = %d", $uid)); if ($origin && $inviter = user_load(array('uid' => $origin))) { if (user_access('withdraw accepted invitations', $inviter)) { db_query("DELETE FROM {invite} WHERE invitee = %d", $uid); } } // Delete any invites originating from this user. db_query("DELETE FROM {invite} WHERE uid = %d", $uid); // Clean up the notification queue. db_query("DELETE FROM {invite_notifications} WHERE uid = %d OR invitee = %d", $uid, $uid); } /** * Implementation of hook_block(). */ function invite_block($op = 'list', $delta = 0, $edit = array()) { if ($op == 'list') { $blocks[0] = array('info' => t('Invite a friend'), 'cache' => BLOCK_CACHE_PER_ROLE); return $blocks; } else if ($op == 'view') { $block = array(); 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; } } /** * Generate the invite forms. * * @param $form_satate * A keyed array containing the current state of the form. * @param $op * The type of form to generate, 'page' or 'block'. * @param $edit * Previous values when resending an invite. * @return * A form definition. */ function invite_form(&$form_state, $op = 'page', $edit = array()) { global $user; if (!is_array($edit)) { $edit = (array)$edit; } $remaining_invites = invite_get_remaining_invites($user); if ($remaining_invites == 0) { if ($op == 'block') { // Hide block. $form['#access'] = FALSE; return $form; } else if (!$edit) { // Deny access when NOT resending an invite. drupal_set_message(t("Sorry, you've reached the maximum number of invitations."), 'error'); drupal_goto(referer_uri()); } } $form['resent'] = array( '#type' => 'value', '#value' => $edit ? $edit['resent'] + 1 : 0, ); $form['reg_code'] = array( '#type' => 'value', '#value' => $edit ? $edit['reg_code'] : NULL, ); if ($remaining_invites != INVITE_UNLIMITED) { $form['remaining_invites'] = array( '#type' => 'value', '#value' => $remaining_invites, ); } switch ($op) { case 'page': default: $form += invite_page_form($remaining_invites, $edit); break; case 'block': $form += invite_block_form($remaining_invites); break; } return $form; } /** * Calculate the remaining invites of a user. * * @param $account * A user object. * @return * The number of remaining invites. */ function invite_get_remaining_invites($account) { if ($account->uid == 1) { return INVITE_UNLIMITED; } // Check user property for remaining invites. $data = unserialize($account->data); if (isset($data['invites'])) { $remaining = $data['invites']; } else { $remaining = invite_get_role_limit($account); if ($remaining > 0) { // Legacy support. $sent = db_result(db_query("SELECT COUNT(*) FROM {invite} WHERE uid = %d", $account->uid)); $remaining = max($remaining - $sent, 0); if ($sent > 0) { // Update user property for faster lookup next time. user_save($account, array('invites' => $remaining)); } } } return $remaining; } /** * Calculate the max. number of invites based on a user's role. * * @param $account * A user object. * @return * The configured maximum of invites. */ function invite_get_role_limit($account) { if (!isset($account->roles)) { $account = user_load(array('uid' => $account->uid)); } $role_limit = 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); if ($role_max == INVITE_UNLIMITED) { return INVITE_UNLIMITED; } $role_limit = max($role_max, $role_limit); } } return $role_limit; } /** * Generate the invite page form. * * @param $remaining_invite * Number of remaining invites. * @param $edit * Previous values when resending an invite. * @return * A form definition. */ function invite_page_form($remaining_invites, $edit = array()) { global $user; // Remaining invites. if ($remaining_invites != INVITE_UNLIMITED) { $form['remaining_invites_markup']['#value'] = format_plural($remaining_invites, 'You have 1 invite remaining.', 'You have @count invites remaining.'); } // Sender e-mail 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 e-mail address. // @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. if (!$edit) { $failed_emails = ''; $allow_multiple = user_access('send mass invitations'); 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($allow_multiple ? 99 : 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'; } } else { // The email is not editable when resending an invite. $allow_multiple = FALSE; $form['email_markup'] = array( '#type' => 'item', '#title' => t('To'), '#value' => check_plain($edit['email']), ); $form['email'] = array( '#type' => 'value', '#value' => $edit['email'], ); } // Message subject. if ($edit && !empty($edit['data']['subject'])) { $subject = $edit['data']['subject']; } else { $subject = invite_get_subject(); } // Add prefix. $prefix = t('Re:'); if ($edit && drupal_substr($subject, 0, strlen($prefix)) != $prefix) { $subject = $prefix .' '. $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', '#default_value' => ($edit && !empty($edit['data']['message'])) ? $edit['data']['message'] : '', '#description' => format_plural($allow_multiple ? 1 : 99, '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('Send invite'), ); return $form; } /** * Generate the invite block form. * * @param $remaining_invite * Number of remaining invites. * @return * A form definition. */ function invite_block_form($remaining_invites) { global $user; $form['#action'] = url('invite'); $form['invite'] = array( '#value' => t('Recommend @site-name to:', array('@site-name' => variable_get('site_name', t('Drupal')))), ); $description = ''; if ($remaining_invites != INVITE_UNLIMITED) { $description = format_plural($remaining_invites, '1 invite remaining', '@count invites remaining'); } $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' => '