? 267814.unblockable7.patch
? pm_block_user.unblockable6.patch
Index: pm_block_user/add-rule-button.png
===================================================================
RCS file: pm_block_user/add-rule-button.png
diff -N pm_block_user/add-rule-button.png
Binary files /dev/null and add-rule-button.png differ
Index: pm_block_user/pm_block_user.css
===================================================================
RCS file: pm_block_user/pm_block_user.css
diff -N pm_block_user/pm_block_user.css
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ pm_block_user/pm_block_user.css	25 Apr 2009 07:39:19 -0000
@@ -0,0 +1,48 @@
+#pm-block-user-settings .sticky-header {
+  /* Stop the remove button overlapping the sticky header */
+  z-index: 10;
+}
+#pm-block-user-settings .throbber {
+  margin: 4px 0;
+}
+#add-rule-button, #remove-rule-button {
+  height: 45px;
+  /* Stop the remove button overlapping the sticky header */
+  z-index: 1;
+}
+#add-rule-button label {
+  line-height: 26px;
+  margin-left: 2px;
+}
+#remove-rule-button {
+  width: 100px;
+  position: relative;
+}
+#remove-rule-button label {
+  display: block;
+  clear: both;
+  text-align: center;
+}
+#add-rule-button input,
+#remove-rule-button input {
+  display: inline-block;
+  overflow: hidden;
+  width: 24px;
+  height: 24px;
+  padding: 0;
+  margin: 0;
+  border: 0;
+  text-indent: -9999px;
+  cursor: pointer; /* Hand shaped cursor */
+  cursor: hand; /* for old versions of IE */
+}
+#add-rule-button input {
+  background: transparent url('add-rule-button.png') left top no-repeat;
+}
+#remove-rule-button input {
+  background: transparent url('remove-rule-button.png') left top no-repeat;
+  /* Only way to centre-align remove button without having AHAH throbber
+   * interfere with it */
+  margin-left: 36px;
+}
+
Index: pm_block_user/pm_block_user.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/privatemsg/pm_block_user/pm_block_user.module,v
retrieving revision 1.1.2.4
diff -u -p -r1.1.2.4 pm_block_user.module
--- pm_block_user/pm_block_user.module	16 Apr 2009 19:03:44 -0000	1.1.2.4
+++ pm_block_user/pm_block_user.module	25 Apr 2009 07:39:20 -0000
@@ -7,6 +7,26 @@
  */
 
 /**
+ * Disallow blocking private messages from a user.
+ */
+define('PM_BLOCK_USER_DISALLOW_BLOCKING', 0);
+
+/**
+ * Disallow sending private messages to a user.
+ */
+define('PM_BLOCK_USER_DISALLOW_SENDING', 1);
+
+/**
+ * Implementation of hook_help().
+ */
+function pm_block_user_help($path) {
+  switch ($path) {
+    case 'admin/settings/messages/block':
+      return '<p>' . t('This area is used to define the rules which govern whether a user may blocked from sending private messages. Rules allow control of who may block messages from whom. By default all users are allowed to block messages from anyone else. However, a site may have groups of users that need to contact or get information to others, for example: the site may have administrative staff or be a forum with moderators. Groups of users are defined by roles, which can be managed on the <a href="@roles">roles configuration page</a>.', array('@roles' => url('admin/user/roles'))) . '</p>';
+  }
+}
+
+/**
  * Implementation of hook_menu().
  */
 function pm_block_user_menu() {
@@ -14,13 +34,395 @@ function pm_block_user_menu() {
     'title'            => 'Block user messages',
     'page callback'    => 'drupal_get_form',
     'page arguments'   => array('pm_block_user_form', 2),
-    'access callback'  => 'privatemsg_user_access',
+    'access callback'  => '_pm_block_user_access',
+    'access arguments' => array(2),
     'type'             => MENU_CALLBACK,
     'weight'           => -10,
   );
+
+  $items['admin/settings/messages/block'] = array(
+    'title'            => 'User blocking rules',
+    'description'      => 'Configure rules for which users may block each other.',
+    'page callback'    => 'drupal_get_form',
+    'page arguments'   => array('pm_block_user_settings'),
+    'access arguments' => array('administer privatemsg settings'),
+    'type'             => MENU_LOCAL_TASK,
+  );
+
+  $items['messages/block/js'] = array(
+    'title' => 'Javascript Block Actions Form',
+    'page callback' => 'pm_block_user_js',
+    'access arguments' => array('administer privatemsg settings'),
+    'type' => MENU_CALLBACK,
+  );
+
   return $items;
 }
 
+/**
+ * Menu callback for blocked user settings.
+ */
+function pm_block_user_settings(&$form_state) {
+  drupal_add_css(drupal_get_path('module', 'pm_block_user') .'/pm_block_user.css');
+  // Need to cache form for AHAH, so it can be rebuilt from cache later.
+  $form = array(
+    '#cache' => TRUE,
+  );
+
+  // Container for just the actions, used for AHAH.
+  $form['block_actions'] = array(
+    '#tree' => TRUE,
+    '#prefix' => '<div id="block-actions">',
+    '#suffix' => '</div>',
+    '#theme' => 'pm_block_user_actions',
+  );
+
+  // Should we populate the form with data from $form_state or the database?
+  if (!isset($form_state['pm_block_user']['block_actions'])) {
+    $block_actions = variable_get('pm_block_user_actions', array());
+  }
+  else {
+    $block_actions = $form_state['pm_block_user']['block_actions'];
+  }
+  foreach ($block_actions as $delta => $details) {
+    // $delta may be zero, if $block_actions is an empty array.
+    $delta = ($delta > 0 ? $delta : 1);
+    $details['delta'] = $delta;
+    $form['block_actions'][$delta] = _pm_block_user_actions_form($details);
+  }
+
+  // The magic AHAH callback that adds more rows.
+  $form['pm_block_actions_more'] = array(
+    '#type' => 'submit',
+    '#value' => t('More'),
+    '#weight' => 1,
+    '#prefix' => '<div id="add-rule-button">',
+    '#suffix' => '<label for="edit-pm-block-actions-more">' . t('Add new rule') . '</label></div>',
+    '#submit' => array('pm_block_user_more_submit'),
+    '#ahah' => array(
+      'path' => 'messages/block/js',
+      'wrapper' => 'block-actions',
+      'method' => 'replace',
+      'effect' => 'fade',
+    ),
+  );
+
+  $form['submit_form'] = array(
+    '#type' => 'submit',
+    '#weight' => 10,
+    '#value' => t('Save configuration'),
+  );
+
+  return $form;
+}
+
+/**
+ * Builds row of sending, receiving roles and actions that go with them.
+ * 
+ * @param $details
+ *   Details of the row: default values and the unique row number (delta).
+ * @param $blacklist
+ *   When the functionality has been added, this will allow building actions
+ *   based on a whitelist or blacklist. The current code only covers the use
+ *   case of a blacklist, where blocking everyone is allowed by default and
+ *   rules are exceptions to that. Conversely, a whitelist will disallow
+ *   blocking by default and rules will configure roles that are allowed to
+ *   block.
+ *
+ * @return
+ *   Part of a form with controls for sending, receiving and actions.
+ */
+function _pm_block_user_actions_form($details, $blacklist = TRUE) {
+  $form = array(
+    '#tree' => TRUE,
+  );
+  $delta = $details['delta'];
+  // FALSE by default, or if the user has checked the 'Enabled' check box for
+  // this row.
+  $row_disabled = (isset($details['enabled']) ? !$details['enabled'] : FALSE);
+
+  $form['author'] = array(
+    '#type' => 'select',
+    '#options' => user_roles(TRUE),
+    '#default_value' => (isset($details['author']) ? $details['author'] : DRUPAL_AUTHENTICATED_RID),
+    '#disabled' => $row_disabled,
+  );
+  $form['recipient'] = array(
+    '#type' => 'select',
+    '#options' => user_roles(TRUE),
+    '#default_value' => (isset($details['recipient']) ? $details['recipient'] : DRUPAL_AUTHENTICATED_RID),
+    '#disabled' => $row_disabled,
+  );
+  // Provide different action radios if we're using a whitelist or a blacklist.
+  if ($blacklist) {
+    $form['action']  = array(
+      '#type' => 'radios',
+      '#options' => array(
+        PM_BLOCK_USER_DISALLOW_BLOCKING => t('Disallow blocking author'),
+        PM_BLOCK_USER_DISALLOW_SENDING => t('Disallow sending message'),
+      ),
+      '#default_value' => (isset($details['action']) ? $details['action'] : PM_BLOCK_USER_DISALLOW_BLOCKING),
+      '#disabled' => $row_disabled,
+    );
+  }
+  else {
+    // TODO: add whitelist action here.
+  }
+  $form['enabled'] = array(
+    '#type' => 'checkbox',
+    '#default_value' => (isset($details['enabled']) ? $details['enabled'] : TRUE),
+  );
+  $form['remove'] = array(
+    '#type' => 'submit',
+    '#submit' => array('pm_block_user_remove_submit'),
+    '#value' => t('Remove_' . $delta),
+    '#attributes' => array('class' => 'remove-action'),
+    '#prefix' => '<div id="remove-rule-button">',
+    '#suffix' => '<label for="edit-remove">' . t('Remove rule') . '</label></div>',
+    '#ahah' => array(
+      'path' => 'messages/block/js',
+      'wrapper' => 'block-actions',
+      'method' => 'replace',
+      'effect' => 'fade',
+    ),
+  );
+
+  return $form;
+}
+
+/**
+ * Submit handler for 'More' button, adds a new action.
+ *
+ * @see pm_block_user_remove_submit()
+ */
+function pm_block_user_more_submit($form, &$form_state) {
+  unset($form_state['submit_handlers']);
+  form_execute_handlers('submit', $form, $form_state);
+  // Get the submitted actions, then put them into a special area of
+  // the $form_state.
+  $submitted_values = $form_state['values'];
+  // Add an empty action.
+  $submitted_values['block_actions'][] = array();
+  $form_state['pm_block_user'] = $submitted_values;
+  // Rebuild the form by passing our $form_state through the
+  // pm_block_user_settings() builder function.
+  $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit handler for 'Remove' button, removes an action.
+ *
+ * @see pm_block_user_more_submit()
+ */
+function pm_block_user_remove_submit($form, &$form_state) {
+  unset($form_state['submit_handlers']);
+  form_execute_handlers('submit', $form, $form_state);
+  $submitted_values = $form_state['values'];
+  // Remove the requested action.
+  $delta = $form_state['clicked_button']['#parents'][1];
+  unset($submitted_values['block_actions'][$delta]);
+  $form_state['pm_block_user'] = $submitted_values;
+  $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Submit handler for admin form.
+ */
+function pm_block_user_settings_submit($form, &$form_state) {
+  // We don't want it to submit when we're adding/removing actions.
+  if ($form_state['clicked_button']['#id'] == 'edit-submit-form') {
+    // If the form's 'block_actions' aren't set, the user has deleted all the
+    // rows in the table, so we save an empty array to stop errors in the form
+    // builder.
+    if (isset($form_state['values']['block_actions'])) {
+      variable_set('pm_block_user_actions', _pm_block_user_settings_filter($form_state['values']['block_actions']));
+    }
+    else {
+      variable_set('pm_block_user_actions', array());
+    }
+    drupal_set_message(t('The configuration options have been saved.'));
+  }
+}
+
+/**
+ * Takes an array of settings and filters out anything that's un-needed.
+ * Leaving only settings to be saved.
+ *
+ * @param $settings
+ *   The array of settings to filter.
+ *
+ * @return
+ *   Array of settings, ready to be stored in the database.
+ *
+ * @see pm_block_user_settings_submit()
+ */
+function _pm_block_user_settings_filter($settings) {
+  // Add-in the names of any settings to be saved into the array below.
+  $save_keys = array('author', 'recipient', 'action', 'enabled');
+  $matching = array();
+  // Run through each of the keys we want to save, creating a new array.
+  // It's not possible to simply check for unwanted values and unset() them as
+  // the array is multi-dimensional.
+  foreach ($save_keys as $save_key) {
+    if (isset($settings[$save_key])) {
+      $matching[$save_key] = $settings[$save_key];
+    }
+  }
+  if (count($matching) > 0) {
+    return $matching;
+  }
+  else {
+    return array_map('_pm_block_user_settings_filter', $settings);
+  }
+}
+
+/**
+ * Menu callback for AHAH handling.
+ */
+function pm_block_user_js() {
+  // See: http://drupal.org/node/331941 for the philosophy of Drupal AHAH.
+  $form_state = array('storage' => NULL, 'submitted' => FALSE);
+  $form_build_id = $_POST['form_build_id'];
+  $form = form_get_cache($form_build_id, $form_state);
+  $args = $form['#parameters'];
+  $form_id = array_shift($args);
+  $form['#post'] = $_POST;
+  $form['#redirect'] = FALSE;
+  $form['#programmed'] = FALSE;
+  $form_state['post'] = $_POST;
+  drupal_process_form($form_id, $form, $form_state);
+  $form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);
+  $output_form = $form['block_actions'];
+  unset($output_form['#prefix'], $output_form['#suffix']); // Prevent duplicate wrappers.
+  $output = theme('status_messages') . drupal_render($output_form);
+
+  // Hack to make behaviours attach to new HTML controls (delete buttons in
+  // this case).
+  $javascript = drupal_add_js(NULL, NULL, 'header');
+  drupal_json(array('status' => TRUE, 'data' => $output));
+}
+
+/**
+ * Implementation of hook_theme().
+ */
+function pm_block_user_theme() {
+  return array(
+    'pm_block_user_actions' => array(
+      'arguments' => array('form' => NULL),
+    ),
+  );
+}
+
+/**
+ * Theme the user actions form.
+ *
+ * @ingroup themeable
+ */
+function theme_pm_block_user_actions($form) {
+  $rows = array();
+  $headers = array(
+    t('If the author has the role'),
+    t('And the recipient has the role'),
+    t('Action'),
+    t('Enabled'),
+    '',
+  );
+  $form_data = element_children($form);
+
+  foreach ($form_data as $key) {
+    // Build the table row.
+    $row = array(
+      'data' => array(
+        array('data' => drupal_render($form[$key]['author'])),
+        array('data' => drupal_render($form[$key]['recipient'])),
+        array('data' => drupal_render($form[$key]['action'])),
+        array('data' => drupal_render($form[$key]['enabled'])),
+        array('data' => drupal_render($form[$key]['remove'])),
+      ),
+    );
+
+    // Add additional attributes to the row, such as a class for this row.
+    if (isset($form[$key]['#attributes'])) {
+      $row = array_merge($row, $form[$key]['#attributes']);
+    }
+    $rows[] = $row;
+  }
+
+  // If there are no rows, output some instructions for the user.
+  if (empty($form_data)) {
+    $rows[] = array(
+      array(
+        'data' => t("No rules have been added. All users may block private messages from each other. To limit which users may be blocked, click 'Add new rule'."),
+        'colspan' => '5',
+      ),
+    );
+  }
+
+  $output = theme('table', $headers, $rows);
+  $output .= drupal_render($form);
+
+  return $output;
+}
+
+/**
+ * Provides access argument for blocking user menu item.
+ *
+ * @param $account
+ *   User object representing the account the menu item will block private
+ *   messages from.
+ *
+ * @return
+ *   TRUE if the user is allowed to block $account, or FALSE if not.
+ */
+function _pm_block_user_access($account) {
+  global $user;
+  if (!user_access('read privatemsg', $user)) {
+    return FALSE;
+  }
+  if (_pm_block_user_rule_exists($account, $user, PM_BLOCK_USER_DISALLOW_BLOCKING)) {
+    return FALSE;
+  }
+  return TRUE;
+}
+
+/**
+ * Checks in saved settings whether a rule exists for a given author, recipient
+ * and action. For example: if this is passed User A (who has the admin role),
+ * User B (who has the authenticated user role) and
+ * PM_BLOCK_USER_DISALLOW_BLOCKING parameters, and a rule is configured that
+ * disallows authenticated users blocking admins then this function will return
+ * TRUE.
+ *
+ * @param $author
+ *   Author user account to check.
+ * @param $recipient
+ *   Receiver user account to check.
+ * @param $action
+ *   The action to be taken, defaults to PM_BLOCK_USER_DISALLOW_BLOCKING.
+ *
+ * @see PM_BLOCK_USER_DISALLOW_BLOCKING
+ * @see PM_BLOCK_USER_DISALLOW_SENDING
+ *
+ * @return
+ *   Boolean value which is TRUE if a rule exists for the combination of
+ *   author recipient and action.
+ */
+function _pm_block_user_rule_exists($author, $recipient, $action = PM_BLOCK_USER_DISALLOW_BLOCKING) {
+  // TODO: should we do anything special for user 1 here?
+  $block_actions = variable_get('pm_block_user_actions', array());
+  foreach ($block_actions as $delta => $details) {
+    // If this rule doesn't relate to $action, or it's disabled
+    // ignore it and go to next loop iteration.
+    if ($details['action'] != $action || !$details['enabled']) {
+      continue;
+    }
+    if (isset($author->roles[$details['author']]) && isset($recipient->roles[$details['recipient']])) {
+      return TRUE;
+    }
+  }
+  return FALSE;
+}
+
 function pm_block_user_form($form_state, $author) {
   global $user;
 
@@ -69,8 +471,19 @@ function pm_block_user_form($form_state,
   }
 }
 
+/**
+ * Implementation of hook_form_submit().
+ */
 function pm_block_user_form_submit($form, &$form_state) {
-
+  $author = user_load($form_state['values']['author']);
+  $recipient = user_load($form_state['values']['recipient']);
+  if (_pm_block_user_rule_exists($author, $recipient, PM_BLOCK_USER_DISALLOW_BLOCKING)) {
+    // It's highly unlikely a user will ever get this far, but we'll set a
+    // message explaining that a rule forbad sending their message.
+    drupal_set_message(t('Sorry, rules setup by the site administrator forbid blocking !name.', array('!name' => $recipient->name)), 'error');
+    drupal_access_denied();
+    return;
+  }
   if ($form_state['values']['confirm']) {
     switch ($form_state['values']['block_action']) {
       case 'block_user':
@@ -89,12 +502,31 @@ function pm_block_user_form_submit($form
 /**
  * Implementation of hook_privatemsg_block_message.
  */
- function pm_block_user_privatemsg_block_message($author, $recipients) {
-  global $user;
+function pm_block_user_privatemsg_block_message($author, $recipients) {
+  $blocked = array();
+  // Loop through each recipient and ensure there is no rule blocking this
+  // author from sending them private messages.
+  foreach ($recipients as $uid => &$recipient) {
+    // Ensure we have a recipient user object which includes roles.
+    if (!isset($recipient->roles)) {
+      $recipient = user_load($uid);
+    }
+    if (_pm_block_user_rule_exists($author, $recipient, PM_BLOCK_USER_DISALLOW_SENDING)) {
+      $blocked[] = array(
+        'uid' => $recipient->uid,
+        'message' => t('Sorry, rules setup by the site administrator forbid sending messages to !name.', array('!name' => $recipient->name)),
+      );
+    }
+  }
+  unset($recipient);
   $args = array_merge(array($author->uid), array_keys($recipients));
   $result = db_query('SELECT recipient FROM {pm_block_user} WHERE author = %d AND recipient IN ('. db_placeholders($recipients) .') GROUP BY recipient', $args);
-  $blocked = array();
   while ($row = db_fetch_array($result)) {
+    $recipient = $recipients[$row['recipient']];
+    // If there's a rule disallowing blocking of this message, send it anyway.
+    if (_pm_block_user_rule_exists($author, $recipient, PM_BLOCK_USER_DISALLOW_BLOCKING)) {
+      continue;
+    }
     $blocked[] = array(
       'uid' => $row['recipient'],
       'message' => t('!name has chosen to not recieve any more messages from you.', array('!name' => $recipients[$row['recipient']]->name))
@@ -113,22 +545,25 @@ function pm_block_user_privatemsg_sql_lo
 /**
  * Implementation of hook_privatemsg_message_view_alter.
  */
- function pm_block_user_privatemsg_message_view_alter(&$vars) {
+function pm_block_user_privatemsg_message_view_alter(&$vars) {
   global $user;
 
-  $author_id = $vars['message']['author']->uid;
+  $author = $vars['message']['author'];
+  if (_pm_block_user_rule_exists($author, $user, PM_BLOCK_USER_DISALLOW_BLOCKING)) {
+    return;
+  }
   if (!isset($vars['message']['thread_id'])) {
     // No thread id, this is probably only a preview
     return;
   }
   $thread_id = $vars['message']['thread_id'];
 
-  if ($user->uid <> $author_id) {
+  if ($user->uid <> $author->uid) {
     if ($vars['message']['is_blocked']) {
-      $vars['message_actions'][] = array('title' => t('Unblock author'), 'href' => 'messages/block/'. $author_id, array('query' => 'destination=messages/view/' . $thread_id));
+      $vars['message_actions'][] = array('title' => t('Unblock author'), 'href' => 'messages/block/'. $author->uid, array('query' => 'destination=messages/view/' . $thread_id));
     }
     else {
-      $vars['message_actions'][] = array('title' => t('Block author'), 'href' => 'messages/block/'. $author_id, array('query' => 'destination=messages/view/' . $thread_id));
+      $vars['message_actions'][] = array('title' => t('Block author'), 'href' => 'messages/block/'. $author->uid, array('query' => 'destination=messages/view/' . $thread_id));
     }
   }
-}
\ No newline at end of file
+}
Index: pm_block_user/remove-rule-button.png
===================================================================
RCS file: pm_block_user/remove-rule-button.png
diff -N pm_block_user/remove-rule-button.png
Binary files /dev/null and remove-rule-button.png differ
