? og_privacy.admin.inc
Index: og_privacy.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/og_privacy/og_privacy.module,v
retrieving revision 1.1.2.1
diff -u -p -r1.1.2.1 og_privacy.module
--- og_privacy.module	2 Sep 2010 22:37:39 -0000	1.1.2.1
+++ og_privacy.module	16 Nov 2010 22:23:38 -0000
@@ -1,6 +1,8 @@
 <?php
 // $Id: og_privacy.module,v 1.1.2.1 2010/09/02 22:37:39 grayside Exp $
 
+define('OG_PRIVACY_BATCH_SIZE', 10);
+
 /**
  * Implementation of hook_enable().
  */
@@ -24,6 +26,66 @@ function og_privacy_nodeapi(&$node, $op,
 }
 
 /**
+ * Implementation of hook_menu().
+ */
+function og_privacy_menu() {
+  $items = array();
+  $items['admin/og/og_privacy'] = array(
+    'title' => 'Organic groups privacy',
+    'description' => 'Rebuild the privacy of group posts in modified groups.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('og_privacy_rebuild_confirm'),
+    'access callback' => 'og_privacy_rebuild_access',
+    // Any user than can potentially trigger a og_privacy_needs_rebuild()
+    // has to be allowed access to the 'node access rebuild' confirm form.
+    'access arguments' => array('administer site configuration'),
+    'file' => 'og_privacy.admin.inc',
+    'type' => MENU_LOCAL_TASK,
+  );
+  return $items;
+}
+
+/**
+ * Identify whether the current user can access the privacy rebuild page.
+ *
+ * If no module implements a policy, og_privacy does nothing.
+ *
+ * @param $perm
+ *  Access permission defined in og_privacy_menu().
+ *
+ * @return
+ *  Boolean value, TRUE to grant access.
+ *
+ * @todo Decide whether to replace this with a message on a non-functioning form.
+ *   This is more realistic, but the other might have usability gains.
+ *
+ */
+function og_privacy_rebuild_access($perm) {
+  $modules = module_implements('og_privacy_policy_info');
+  return user_access($perm) && !empty($modules);
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function og_privacy_help($path, $arg) {
+  // Remind site administrators about groups being flagged for privacy 
+  // status rebuild. We don't need to issue the message on the confirm form, or
+  // while the rebuild is being processed.
+  if ($path != 'batch' && strpos($path, '#') === FALSE
+       && user_access('access administration pages') && og_privacy_needs_rebuild()) {
+    if ($path == 'admin/og/og_privacy') {
+      $message = t('The group privacy settings need to be recalculated.');
+    }
+    else {
+      $message = t('Group post privacy needs to be recalculated. Please visit <a href="@og_privacy_rebuild">this page</a>.',
+        array('@og_privacy_rebuild' => url('admin/og/og_privacy')));
+    }
+    drupal_set_message($message . t(' You will need to rebuild content permissions afterward.'), 'error');
+  }
+}
+
+/**
  * Identify whether the current node is covered by a privacy policy.
  */
 function og_privacy_node_denied($node) {
@@ -112,3 +174,189 @@ function og_privacy_set_privacy_reason($
 function og_privacy_get_privacy_reason($node) {
   return og_privacy_set_privacy_reason($node);
 }
+
+/**
+ * Implementation of hook_spaces_og_preset_privacy_change().
+ *
+ * When a Group's preset is changed between public and private under Spaces OG,
+ * spaces updates all nodes to match the group's settings, and asks for content
+ * access to be rebuilt. This is a critical security flaw if you do not want
+ * that kind of access uniformity.
+ *
+ * This hook implementation is used to fire off an og_privacy-based recalculation
+ * of the nodes affected by Spaces OG's update.
+ * 
+ * This hook implementation depends on patch #2 at http://drupal.org/node/901670.
+ */
+function og_privacy_spaces_og_preset_privacy_change($privacy, $group_node, $space) {
+  if ($privacy) {
+    // Do nothing if the group is becoming "more" private.
+    // Increased privacy should always win.
+    return;
+  }
+  og_privacy_needs_rebuild($group_node->nid);
+}
+
+/**
+ * Calculate privacy status for a set of nodes.
+ *
+ * @param $groups
+ *  Array of group nids whose posts should be bulk reprocessed.
+ */
+function og_privacy_rebuild($groups = NULL) {
+  if (empty($groups)) {
+    $groups = og_privacy_needs_rebuild();
+    if (empty($groups)) {
+      drupal_set_message(t('No groups specified for processing.'), 'notice');
+      return;
+    }
+  }
+  if (!is_array($groups)) {
+    $groups = array($groups);
+  }
+  $nids = array();
+  foreach ($groups as $group) {
+    $nids = array_merge($nids, _og_privacy_nodes_in_group($group));
+  }
+  og_privacy_rebuild_by_batch($nids);
+}
+
+/**
+ * Dispatch node updates to batch process.
+ *
+ * @param $nid
+ *  NIDs of all nodes to be processed.
+ */
+function og_privacy_rebuild_by_batch($nids) {
+  $batch = array(
+    // Title to display while running.
+    'title' => t('Updating Group Post Privacy'),
+     // Operations to complete, in order. Defined below.
+    'operations' => array(),
+    'finished' => '_og_privacy_batch_process_finished',
+    'init_message' => t('Initializing...'),
+    'progress_message' => t('Updated @current out of @total posts.'),
+    'error_message' => t('Encountered an error while updating group post privacy settings.'),
+  );
+  foreach ($nids as $nid) {
+    $batch['operations'][] = array('_og_privacy_batch_process', array($nid));
+  }
+  batch_set($batch);
+}
+
+/**
+ * Run through a set of nodes and update them.
+ * Callback process for batch operation.
+ */
+function _og_privacy_batch_process($nid, &$context) {
+  if (!isset($context['sandbox']['progress'])) {
+    $context['sandbox']['progress'] = 0;
+    $context['sandbox']['max'] = count($nid);
+  }
+
+    _og_privacy_rebuild_node_policy($nid);
+    $context['results'][] = $nid;
+    $context['message'] = t('Now processing node #@nid', array('@nid' => $nid));
+    $context['sandbox']['progress']++;
+
+  // Inform the batch engine that we are not finished,
+  // and provide an estimation of the completion level we reached.
+  if ($context['sandbox']['progress'] != $context['sandbox']['max'] || $context['sandbox']['progress'] >= $context['sandbox']['max'] ) {
+    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
+  }
+}
+
+/**
+ * Batch 'finished' callback
+ */
+function _og_privacy_batch_process_finished($success, $results, $operations) {
+  if ($success) {
+    $message = t('!count posts processed.', array('!count' => count($results)));
+    // No longer need to worry about privacy rebuild.
+    og_privacy_needs_rebuild(FALSE);
+    // However, node access now needs to be recalculated for privacy status.
+    node_access_needs_rebuild(TRUE);
+  }
+  else {
+    $error_operation = reset($operations);
+    $message = t('An error occurred while processing %error_operation with arguments: @arguments',
+      array('%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE))
+    );
+  }
+  drupal_set_message($message);
+}
+
+/**
+ * Check and apply privacy status for a specified node.
+ *
+ * @param $nid
+ *  Node ID of the node to update.
+ *
+ * @return
+ *  Boolean. TRUE for Public, FALSE for Private.
+ */
+function _og_privacy_rebuild_node_policy($nid) {
+  $node = node_load($nid);
+  if ($private = og_privacy_node_denied($node)) {
+    db_query("UPDATE {og_access_post} SET og_public=%d WHERE nid=%d",
+      array(OG_VISIBLE_GROUPONLY, $nid)
+    );
+  }
+  else {
+    db_query("UPDATE {og_access_post} SET og_public=%d WHERE nid=%d",
+      array(OG_VISIBLE_BOTH, $nid)
+    );
+  }
+  return (bool)!$private;
+}
+
+/**
+ * Identify organic group's whose posts need to be reprocessed by OG Privacy.
+ *
+ * If a group status changes and requires reconsideration of group post privacy,
+ * that group should be flagged for reprocessing here. This status will be used
+ * to inform the site administrators that there are group posts which need 
+ * reprocessing to avoid potential security breaches.
+ *
+ * @param $gid
+ *  (optional) A group node's id to be flagged for privacy rebuild.
+ *
+ * @return
+ *  (If no value was provided for $rebuild) The current value of groups
+ *  triggered for rebuild. If it is set to "FALSE" will erase the current value.
+ */
+function og_privacy_needs_rebuild($gid = NULL) {
+  if (!isset($gid)) {
+    return variable_get('og_privacy_needs_rebuild', array());
+  }
+  elseif ($gid) {
+    $groups = variable_get('og_privacy_needs_rebuild', array());
+    if (!array_key_exists($gid, $groups)) {
+      $groups[$gid] = $gid;
+      variable_set('og_privacy_needs_rebuild', $groups);
+    }
+  }
+  else {
+    variable_del('og_privacy_needs_rebuild');
+  }
+}
+
+/**
+ * Provide an array of all node nid's in a given group.
+ *
+ * @param $gid
+ *  The nid of the group to check.
+ * 
+ * @return
+ *  Array of nids for posts claiming that group as a container.
+ */
+function _og_privacy_nodes_in_group($gid) {
+  $results = db_query("SELECT nid FROM {og_ancestry} WHERE group_nid = %d",
+            array($gid)
+  );
+  $nodes = array();
+  while ($result = db_fetch_object($results)) {
+    $nodes[] = $result->nid;
+  }
+  return $nodes;
+}
