Index: mollom.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.admin.inc,v
retrieving revision 1.41
diff -u -p -r1.41 mollom.admin.inc
--- mollom.admin.inc	26 Sep 2010 19:42:55 -0000	1.41
+++ mollom.admin.inc	27 Sep 2010 16:12:14 -0000
@@ -13,9 +13,8 @@ function mollom_admin_form_list() {
   _mollom_testing_mode_warning();
 
   $modes = array(
-    MOLLOM_MODE_DISABLED => t('None'),
-    MOLLOM_MODE_CAPTCHA => t('CAPTCHA'),
     MOLLOM_MODE_ANALYSIS => t('Text analysis'),
+    MOLLOM_MODE_CAPTCHA => t('CAPTCHA'),
   );
 
   $header = array(
@@ -29,7 +28,10 @@ function mollom_admin_form_list() {
     $mollom_form = mollom_form_load($form_id);
     $rows[] = array(
       $mollom_form['title'],
-      $modes[$mollom_form['mode']],
+      t('!protection-mode (@reject)', array(
+        '!protection-mode' => $modes[$mollom_form['mode']],
+        '@reject' => $mollom_form['reject'] ? t('reject') : t('unpublish'),
+      )),
       l(t('Configure'), 'admin/config/content/mollom/manage/' . $form_id),
       l(t('Unprotect'), 'admin/config/content/mollom/unprotect/' . $form_id),
     );
@@ -153,6 +155,27 @@ function mollom_admin_configure_form($fo
       );
 
       if (!empty($mollom_form['elements'])) {
+        // By default, Mollom module rejects all posts that did not successfully
+        // pass mollom.checkContent. Instead of rejecting posts, site admins
+        // can optionally configure that posts shall be unpublished instead, so
+        // they can go through manual moderation. The negated form widget may
+        // look odd, but users can rather relate to more common terms like
+        // "unpublish" or "moderation". Furthermore, this setting should stay
+        // optional and disabled by default, as Mollom should reject bad posts.
+        $form['mollom']['reject'] = array(
+          '#type' => 'checkbox',
+          '#title' => t('Reject bad posts'),
+          '#default_value' => $mollom_form['reject'],
+          '#description' => t('Disable to manually moderate all posts.'),
+          // Only possible for forms supporting moderation of unpublished posts.
+          '#access' => !empty($mollom_form['reject callback']),
+          '#states' => array(
+            'visible' => array(
+              ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
+            ),
+          ),
+        );
+
         // If not re-configuring an existing protection, make it the default.
         if (!isset($mollom_form['mode'])) {
           $form['mollom']['mode']['#default_value'] = MOLLOM_MODE_ANALYSIS;
Index: mollom.api.php
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.api.php,v
retrieving revision 1.6
diff -u -p -r1.6 mollom.api.php
--- mollom.api.php	12 Sep 2010 22:05:55 -0000	1.6
+++ mollom.api.php	27 Sep 2010 16:12:14 -0000
@@ -150,6 +150,9 @@
  *       $form_info = array(
  *         // Optional: User permission list to skip Mollom's protection for.
  *         'bypass access' => array('administer instant messages'),
+ *         // Optional: Function to invoke to unpublish a bad form submission
+ *         // instead of rejecting it.
+ *         'reject callback' => 'im_mollom_reject',
  *         // Optional: To allow textual analysis of the form values, the form
  *         // elements needs to be registered individually. The keys are the
  *         // field keys in $form_state['values']. Sub-keys are noted using "]["
@@ -205,6 +208,16 @@
  * Additionally, the "post_id" data property always needs to be mapped to a form
  * element that holds the entity id.
  *
+ * When registering a 'reject callback', then the registered function needs to
+ * be available when the form is validated, and it is responsible for changing
+ * the submitted form values in a way that results in an unpublished post ending
+ * up in a moderation queue:
+ * @code
+ * function im_mollom_reject(&$form, &$form_state) {
+ *   $form_state['values']['status'] = 0;
+ * }
+ * @endcode
+ *
  * @see mollom_node
  * @see mollom_comment
  * @see mollom_user
@@ -276,6 +289,10 @@ function hook_mollom_form_list() {
  *     current user to determine whether to protect the form with Mollom or do
  *     not validate submitted form values. If the current user has at least one
  *     of the listed permissions, the form will not be protected.
+ *   - reject callback: (optional) A function name to invoke when a form
+ *     submission would normally be rejected. This allows modules to put such
+ *     posts into a moderation queue (i.e., accept but not publish them) by
+ *     altering the $form or $form_state information being passed by reference.
  *   - mail ids: (optional) An array of mail IDs that will be sent as a result
  *     of this form being submitted. When these mails are sent, a 'report to
  *     Mollom' link will be included at the bottom of the mail body. Be sure to
Index: mollom.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.install,v
retrieving revision 1.28
diff -u -p -r1.28 mollom.install
--- mollom.install	25 Sep 2010 01:05:41 -0000	1.28
+++ mollom.install	27 Sep 2010 16:17:55 -0000
@@ -174,6 +174,13 @@ function mollom_schema() {
         'not null' => FALSE,
         'serialize' => TRUE,
       ),
+      'reject' => array(
+        'description' => 'Whether to reject (1) or unpublish (0) bad form submissions.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 1,
+      ),
       'enabled_fields' => array(
         'description' => 'Form elements to analyze.',
         'type' => 'text',
@@ -723,3 +730,17 @@ function mollom_update_7007() {
     }
   }
 }
+
+/**
+ * Add {mollom_form}.reject column to form configuration.
+ */
+function mollom_update_7008() {
+  if (!db_field_exists('mollom_form', 'reject')) {
+    db_add_field('mollom_form', 'reject', array(
+      'type' => 'int',
+      'size' => 'tiny',
+      'not null' => TRUE,
+      'default' => 1,
+    ));
+  }
+}
Index: mollom.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.module,v
retrieving revision 1.85
diff -u -p -r1.85 mollom.module
--- mollom.module	25 Sep 2010 01:05:41 -0000	1.85
+++ mollom.module	27 Sep 2010 16:43:09 -0000
@@ -399,6 +399,26 @@ function mollom_cron() {
   db_delete('mollom')
     ->condition('changed', $expired, '<')
     ->execute();
+
+  // Delete all bad posts older than two weeks, which have not been published.
+  $form_list = mollom_form_list();
+  $delete = array();
+  $query = db_select('mollom', 'm')
+    ->fields('m')
+    ->condition('changed', REQUEST_TIME - 86400 * 14, '<')
+    ->condition(db_or()
+      ->condition('spam', MOLLOM_ANALYSIS_SPAM)
+      ->condition('profanity', 0.5, '>=')
+    );
+  foreach ($query->execute() as $data) {
+    if (isset($form_list[$data->form_id]['entity delete multiple callback'])) {
+      $function = $form_list[$data->form_id]['entity delete multiple callback'];
+      $delete[$function][] = $data->did;
+    }
+  }
+  foreach ($delete as $function => $ids) {
+    $function($ids);
+  }
 }
 
 /**
@@ -443,10 +463,6 @@ function mollom_data_load($entity, $id) 
  * @todo Remove usage of global $mollom variable.
  */
 function mollom_data_save($entity, $id, $form_id, $response) {
-  // Nothing to do, if we do not have a valid Mollom response.
-  if (empty($GLOBALS['mollom']['response']['session_id'])) {
-    return FALSE;
-  }
   $data = array(
     'entity' => $entity,
     'did' => $id,
@@ -627,6 +643,7 @@ function mollom_form_alter(&$form, &$for
       // Add Mollom form validation handlers.
       $form['#validate'][] = 'mollom_validate_analysis';
       $form['#validate'][] = 'mollom_validate_captcha';
+      $form['#validate'][] = 'mollom_validate_post';
 
       // Add a submit handler to remove form state storage.
       $form['#submit'][] = 'mollom_form_submit';
@@ -755,6 +772,7 @@ function mollom_form_info($form_id, $mod
     'module' => $module,
     'entity' => NULL,
     'mode' => NULL,
+    'reject' => TRUE,
     'bypass access' => array(),
     'elements' => array(),
     'mapping' => array(),
@@ -1223,6 +1241,13 @@ function mollom_process_mollom($element,
   }
   $form_state['mollom'] += $element['#mollom_form'];
 
+  // By default, bad form submissions are rejected, unless the form was
+  // configured to unpublish bad posts. 'reject' may only be FALSE, if there is
+  // a valid 'reject callback'. Otherwise, it must be TRUE.
+  if (empty($form_state['mollom']['reject callback']) || !function_exists($form_state['mollom']['reject callback'])) {
+    $form_state['mollom']['reject'] = TRUE;
+  }
+
   // Add the Mollom session element.
   $element['session_id'] = array(
     '#type' => 'hidden',
@@ -1321,10 +1346,19 @@ function mollom_validate_analysis(&$form
   $form_state['mollom']['response'] = $result;
   $form['mollom']['session_id']['#value'] = $result['session_id'];
 
+  // Prepare watchdog message teaser text.
+  $teaser = truncate_utf8(strip_tags(isset($data['post_title']) ? $data['post_title'] : isset($data['post_body']) ? $data['post_body'] : '--'), 40);
+
   // Handle the profanity check result.
   if (isset($result['profanity']) && $result['profanity'] >= 0.5) {
-    form_set_error('mollom', t('Your submission has triggered the profanity filter and will not be accepted until the inappropriate language is removed.'));
-    watchdog('mollom', 'Profanity: <pre>@message</pre>Result: <pre>@result</pre>', array('@message' => print_r($data, TRUE), '@result' => print_r($result, TRUE)));
+    if ($form_state['mollom']['reject']) {
+      form_set_error('mollom', t('Your submission has triggered the profanity filter and will not be accepted until the inappropriate language is removed.'));
+    }
+    _mollom_watchdog(array(
+      'Profanity: %teaser' => array('%teaser' => $teaser),
+      'Data:<pre>@data</pre>' => array('@data' => $data),
+      'Result:<pre>@result</pre>' => array('@result' => $result),
+    ));
   }
 
   // Handle the spam check result.
@@ -1335,7 +1369,6 @@ function mollom_validate_analysis(&$form
   // the spam check led to a MOLLOM_ANALYSIS_UNSURE result, and the user solved
   // the CAPTCHA correctly, subsequent spam check results will likely be
   // MOLLOM_ANALYSIS_HAM (though not guaranteed).
-  $teaser = truncate_utf8(strip_tags(isset($data['post_title']) ? $data['post_title'] : isset($data['post_body']) ? $data['post_body'] : '--'), 40);
   if (isset($result['spam'])) {
     switch ($result['spam']) {
       case MOLLOM_ANALYSIS_HAM:
@@ -1349,7 +1382,9 @@ function mollom_validate_analysis(&$form
 
       case MOLLOM_ANALYSIS_SPAM:
         $form_state['mollom']['require_captcha'] = FALSE;
-        form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.'));
+        if ($form_state['mollom']['reject']) {
+          form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.'));
+        }
         _mollom_watchdog(array(
           'Spam: %teaser' => array('%teaser' => $teaser),
           'Data:<pre>@data</pre>' => array('@data' => $data),
@@ -1475,6 +1510,26 @@ function mollom_validate_captcha(&$form,
 }
 
 /**
+ * Form validation handler to perform post-validation tasks.
+ *
+ * Since our individual form validation handlers are not re-run after positive
+ * validation, any changes applied to form values will not persist across
+ * multiple form submission attempts and rebuilds.
+ */
+function mollom_validate_post(&$form, &$form_state) {
+  // Unpublish a post instead of rejecting it. If 'reject' is not TRUE, then
+  // the 'reject callback' is responsible for altering $form_state in a way that
+  // the post ends up unpublished in a moderation queue. Most callbacks will
+  // only want to set a value in $form_state. Technically, modules do not need
+  // to implement a 'reject callback' to achieve this, they may simply add a
+  // custom form validation handler (or use an existing one).
+  if (!$form_state['mollom']['reject']) {
+    $function = $form_state['mollom']['reject callback'];
+    $function($form, $form_state);
+  }
+}
+
+/**
  * Form submit handler to flush Mollom session and form information from cache.
  */
 function mollom_form_submit($form, &$form_state) {
@@ -1979,19 +2034,13 @@ function node_mollom_form_list() {
       'entity' => 'node',
       'bundle' => $type->type,
       'delete form' => 'node_delete_confirm',
+      'entity delete multiple callback' => 'node_delete_multiple',
     );
   }
   return $forms;
 }
 
 /**
- * Implements hook_node_delete().
- */
-function mollom_node_delete($node) {
-  mollom_data_delete('node', $node->nid);
-}
-
-/**
  * Implements hook_mollom_form_info().
  */
 function node_mollom_form_info($form_id) {
@@ -2003,6 +2052,7 @@ function node_mollom_form_info($form_id)
     // @todo This is incompatible with node access.
     'bypass access' => array('bypass node access', 'edit any ' . $type->type . ' content'),
     'bundle' => $type->type,
+    'reject callback' => 'node_mollom_reject',
     'elements' => array(),
     'mapping' => array(
       'post_id' => 'nid',
@@ -2027,6 +2077,20 @@ function node_mollom_form_info($form_id)
 }
 
 /**
+ * Mollom reject callback.
+ */
+function node_mollom_reject(&$form, &$form_state) {
+  $form_state['values']['status'] = 0;
+}
+
+/**
+ * Implements hook_node_delete().
+ */
+function mollom_node_delete($node) {
+  mollom_data_delete('node', $node->nid);
+}
+
+/**
  * Implements hook_form_FORMID_alter().
  */
 function mollom_form_node_multiple_delete_confirm_alter(&$form, &$form_state) {
@@ -2069,6 +2133,7 @@ function comment_mollom_form_list() {
       'entity' => 'comment',
       'bundle' => 'comment_node_' . $type->type,
       'delete form' => 'comment_confirm_delete',
+      'entity delete multiple callback' => 'comment_delete_multiple',
     );
   }
   return $forms;
@@ -2081,6 +2146,7 @@ function comment_mollom_form_info($form_
   $form_info = array(
     'mode' => MOLLOM_MODE_ANALYSIS,
     'bypass access' => array('administer comments'),
+    'reject callback' => 'comment_mollom_reject',
     'elements' => array(
       'subject' => t('Subject'),
       // @todo Update for Field API.
@@ -2099,6 +2165,40 @@ function comment_mollom_form_info($form_
 }
 
 /**
+ * Mollom reject callback.
+ */
+function comment_mollom_reject(&$form, &$form_state) {
+  $form_state['values']['status'] = COMMENT_NOT_PUBLISHED;
+}
+
+/**
+ * Implements hook_comment_presave().
+ */
+function mollom_comment_presave($comment) {
+  // If an existing comment is published and we have session data stored for it,
+  // send 'ham' feedback to Mollom.
+  if (!empty($comment->cid) && $comment->status == COMMENT_PUBLISHED) {
+    if ($data = mollom_data_load('comment', $comment->cid)) {
+      _mollom_send_feedback($data->session, 'ham');
+      // Update the stored session data, so this comment is no longer deleted
+      // in upcoming cron runs.
+      // @todo Actually, we should update the stored values for 'spam' and/or
+      //   'profanity', so they no longer appear in content overviews that join
+      //   on {mollom}. However, as of now, the stored values are 1:1 the return
+      //   values of Mollom servers, and just updating them would mean that when
+      //   editing a post, we'd potentially overwrite the "good" values again.
+      //   Hence, we likely need to do both: Update the analysis values AND use
+      //   a new column to prevent existing published content from being deleted.
+      db_update('mollom')
+        ->fields(array('delete' => 0))
+        ->condition('entity', 'comment')
+        ->condition('did', $comment->cid)
+        ->execute();
+    }
+  }
+}
+
+/**
  * Implements hook_comment_delete().
  */
 function mollom_comment_delete($comment) {
@@ -2144,11 +2244,10 @@ function user_mollom_form_list() {
     'title' => t('User registration form'),
     'entity' => 'user',
     'delete form' => 'user_cancel_confirm_form',
+    'entity delete multiple callback' => 'user_delete_multiple',
   );
   $forms['user_pass'] = array(
     'title' => t('User password request form'),
-    'entity' => 'user',
-    'delete form' => 'user_cancel_confirm_form',
   );
   return $forms;
 }
@@ -2162,6 +2261,7 @@ function user_mollom_form_info($form_id)
       $form_info = array(
         'mode' => MOLLOM_MODE_CAPTCHA,
         'bypass access' => array('administer users'),
+        'reject callback' => 'user_mollom_reject',
         'mapping' => array(
           'post_id' => 'uid',
           'author_name' => 'name',
@@ -2208,6 +2308,13 @@ function mollom_form_user_multiple_cance
 }
 
 /**
+ * Mollom reject callback.
+ */
+function user_mollom_reject(&$form, &$form_state) {
+  $form_state['values']['status'] = 0;
+}
+
+/**
  * @} End of "name mollom_user".
  */
 
