Index: og.module
===================================================================
--- og.module	(revision 3274)
+++ og.module	(working copy)
@@ -136,7 +136,9 @@
   $function = array_shift($args);
   $gid = $args[0];
   $node = node_load((int)$gid);
-  if (node_access('view', $node)) {
+  // Ignore node view access check if they're subscribing to a group.  Needed
+  // for private group invites.
+  if ($function == 'og_subscribe' || node_access('view', $node)) {
     return call_user_func_array($function, $args);
   }
   else {
@@ -609,6 +611,12 @@
 
   $form['mails'] = array('#type' => 'textarea', '#title' => t('Email addresses or usernames'),  '#description' => t('Enter up to %max email addresses or usernames. Separate multiple addresses by commas or new lines. Each person will receive an invitation message from you.', array('%max' => $max)));
   $form['pmessage'] = array('#type' => 'textarea', '#title' => t('Personal message'), '#description' => t('Optional. Enter a message which will become part of the invitation email.'));
+  if (($node = node_load($gid)) && $node->og_selective == OG_MODERATED) {
+    $form['jointoken'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Create unique subscription urls to automatically approve invitees'),
+    );
+  }
   $form['op'] = array('#type' => 'submit', '#value' => t('Send invitation'));
   $form['gid'] = array ('#type' => 'value', '#value' => $gid);
   $form['valid_emails'] = array('#type' => 'value', '#value' => array());
@@ -668,33 +676,64 @@
     '@group' => $node->title,
     '@description' => $node->og_description,
     '@site' => variable_get('site_name', 'drupal'),
-    '!group_url' => url("og/subscribe/$node->nid", NULL, NULL, TRUE),
     '@body' => $form_values['pmessage']
   );
   
   global $user;
   $from = $user->mail;
+  $add_token = $form_values['jointoken'] == 1;
   foreach ($emails as $mail) {
+    $variables['!group_url'] = _og_invite_url($mail, $node->nid, $add_token);
     drupal_mail('og_invite_form', $mail, _og_user_mail_text('og_invite_user_subject', $variables), _og_user_mail_text('og_invite_user_body', $variables), $from);
   }
   drupal_set_message(format_plural(count($emails), '1 invitation sent.', '@count invitations sent.'));
 }
 
+/**
+ * Construct the invitation url - adding timestamp and hash query params if requested.
+ */
+function _og_invite_url($email, $gid, $add_token = FALSE) {
+  if ($add_token) {
+    $timestamp = time();
+    $invite_hash = _og_invite_rehash($email, $timestamp, $gid);
+    return url("og/subscribe/$gid", "t=$timestamp&h=$invite_hash", NULL, TRUE);
+  }
+  return url("og/subscribe/$gid");
+}
+
+/**
+ * make a hash of email, timestamp, and gid
+ */
+function _og_invite_rehash($email, $timestamp, $gid) {
+  return md5($timestamp . $email . $gid);
+}
+
 function og_subscribe($gid, $uid = NULL) {
   global $user;
+  if(isset($_GET['h'])) {
+    $_SESSION['og_join_params'] = array('timestamp' => $_GET['t'], 'hash' => $_GET['h'], 'path' => $_GET['q']);    
+  }
   if (is_null($uid)) {
     if ($user->uid) {
       $account = $user;
     }
     else {
-      drupal_set_message(t('In order to join this group, you must login or register a new account. After you have successfully done so, you will need to request membership again.'));
-      drupal_goto('user');
+      drupal_set_message(t('In order to join this group, you must login or register a new account.'));
+      drupal_goto('user', drupal_get_destination());
     }
   }
   else {
     $account = user_load(array('uid' => $uid));
   }
   $node = node_load($gid);
+  // If user doesn't have view access and also no join group hash token
+  // provided, return access denied.  If join group hash token present,
+  // continue.  The token will be validated by og_confirm_subscribe().
+  if (!isset($_GET['h']) && !node_access('view', $node)) {
+    drupal_access_denied();
+    exit();
+  }
+
   if ($node->og_selective >= OG_INVITE_ONLY || $node->status == 0 || !og_is_group_type($node->type)) {
     drupal_access_denied();
     exit();
@@ -716,13 +755,69 @@
 }
 
 /**
+ * Gets the group join information from the session if available and passes basic validity tests.
+ */
+function _og_get_join_token() {
+  static $join_token;
+  
+  if (!isset($join_token)) {
+    $join_token = FALSE;
+    if (isset($_SESSION['og_join_params'])) {
+      $timestamp = $_SESSION['og_join_params']['timestamp'];
+      $hash = $_SESSION['og_join_params']['hash'];
+      $path = $_SESSION['og_join_params']['path'];
+      
+      if (strpos($path, 'og/subscribe/') !== 0) {
+        return $join_token;
+      }
+
+      $path_args = explode('/', $path);
+      
+      // is session state for this group
+      if (!isset($path_args[2]) || !is_numeric($path_args[2])) {
+        return $join_token;
+      }
+      
+      $join_token = array('hash' => $hash, 'timestamp' => $timestamp, 'gid' => $path_args[2]);
+    }
+  }
+  
+  return $join_token;
+}
+
+/**
+ * Returns TRUE if the hash matches the group and account email.
+ */
+function _og_check_join_token($join_token, $gid, $account) {
+  return $join_token['gid'] == $gid && $join_token['hash'] == _og_invite_rehash($account->mail, $join_token['timestamp'], $gid);
+}
+
+/**
+ * Remove the join token from the session.
+ */
+function _og_use_join_token($join_token) {
+  unset($_SESSION['og_join_params']);
+}
+
+/**
  * Confirm og membership form
  */
 function og_confirm_subscribe($gid, $node, $account) {
  $form['gid'] = array('#type' => 'value', '#value' => $gid);
  $form['account'] = array('#type' => 'value', '#value' => $account);
- if ($node->og_selective == OG_MODERATED) {
-   $form['request'] = array('#type' => 'textarea', '#title' => t('Additional details'), '#description' => t('Add any detail which will help an administrator decide whether to approve or deny your membership request.'));
+ $join_token = _og_get_join_token();
+ // If it's a moderated group, and the join hash is incorrect, present a form to
+ // enter in details on why group membership should be approved.
+ if ($node->og_selective == OG_MODERATED && !($join_token !== FALSE && _og_check_join_token($join_token, $gid, $account))) {
+   // If the group is private, don't display the form, and return access denied
+   // immediately.
+   if (!node_access('view', $node)) {
+     drupal_access_denied();
+     exit();
+   }
+   else {
+     $form['request'] = array('#type' => 'textarea', '#title' => t('Additional details'), '#description' => t('Add any detail which will help an administrator decide whether to approve or deny your membership request.'));
+   }
  }
  return confirm_form($form, 
                 t('Are you sure you want to join the group %title?', array('%title' => $node->title)),
@@ -753,6 +848,17 @@
   $node = node_load($gid);
   switch ($node->og_selective) {
     case OG_MODERATED:
+      $join_token = _og_get_join_token();
+
+      // If join token in the session matches this subscription then bypass approval. Invalid token silently defaults to usual workflow.
+      if ($join_token !== FALSE && _og_check_join_token($join_token, $gid, $account)) {
+        og_save_subscription($gid, $account->uid, array('is_active' => 1)); // as per http://drupal.org/node/156224
+        $return_value = array('type' => 'subscribed',
+                              'message' => t('You are now a member of the %group.', array('%group' => $node->title)));
+        // Remove the session variable.
+        _og_use_join_token($join_token);
+        break;
+      }
       og_save_subscription($gid, $account->uid, array('is_active' => 0));
 
       $sql = og_list_users_sql(1, 1);
@@ -1924,7 +2030,7 @@
       case 'og_invite_user_subject':
         return t("Invitation to join the group '@group' at @site.", $variables);
       case 'og_invite_user_body':
-        return t("Hi. I'm a member of '@group' and I welcome you to join this group as well. Please see the link and message below.\n\n@group\n@description\nJoin: !group_url\n@body", $variables);
+        return t("Hi. I'm a member of '@group' and I welcome you to join this group as well. Please see the link and message below.\n\n@group\n@description\nJoin: !group_url\n\n@body", $variables);
       case 'og_request_user_subject':
         return t("Membership request for '@group' from '@username'.", $variables);        
       case 'og_request_user_body':
@@ -1962,6 +2068,9 @@
 
   switch ($op) {
     case 'register':
+      // set gids from session if user has followed og invite url
+      _og_join_set_gids();
+      
       $options = array();
       list($types, $in) = og_get_sql_args();
       
@@ -2022,9 +2131,34 @@
         }
       }
       break;
+    case 'login':
+      _og_join_set_destination();
+      break;
   }
 }
 
+/**
+ * Streamlined invitation workflow - populate request destination paramater from url stored in session variable.
+ */
+function _og_join_set_destination() {
+  // Ensure that subscribe page is the destination while join session variable present.
+  if (!isset($_REQUEST['destination']) && isset($_SESSION['og_join_params'])) {
+    $_REQUEST['destination'] = $_SESSION['og_join_params']['path'];
+  }  
+}
+
+/**
+ * Streamlined invitation workflow - populate request gids paramater from gid derived from session variable.
+ */
+function _og_join_set_gids() {
+  // Get gids from session.
+  if (!isset($_REQUEST['gids']) && isset($_SESSION['og_join_params'])) {
+    $join_token = _og_get_join_token();
+    
+    $_REQUEST['gids'] = array($join_token['gid']);
+  }  
+}
+
 function og_save_ancestry($node) {
   if (og_is_group_post_type($node->type)) {
     $sql = "DELETE FROM {og_ancestry} WHERE nid = %d";
