Index: mollom.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.admin.inc,v
retrieving revision 1.42
diff -u -p -r1.42 mollom.admin.inc
--- mollom.admin.inc	29 Sep 2010 02:34:30 -0000	1.42
+++ mollom.admin.inc	9 Oct 2010 13:47:15 -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 (@discard)', array(
+        '!protection-mode' => $modes[$mollom_form['mode']],
+        '@discard' => $mollom_form['discard'] ? t('block') : t('moderate'),
+      )),
       l(t('Configure'), 'admin/config/content/mollom/manage/' . $form_id),
       l(t('Unprotect'), 'admin/config/content/mollom/unprotect/' . $form_id),
     );
@@ -149,7 +151,7 @@ function mollom_admin_configure_form($fo
         '#type' => 'radios',
         '#title' => t('Protection mode'),
         '#options' => $modes,
-        '#default_value' => $mollom_form['mode'],
+        '#default_value' => isset($mollom_form['mode']) ? $mollom_form['mode'] : key($modes),
       );
 
       if (!empty($mollom_form['elements'])) {
@@ -174,6 +176,24 @@ function mollom_admin_configure_form($fo
           ),
         );
 
+        $form['mollom']['discard'] = array(
+          '#type' => 'radios',
+          '#title' => t('When a post is blocked'),
+          '#default_value' => $mollom_form['discard'],
+          '#options' => array(
+            1 => t('Automatically discard'),
+            0 => t('Retain for manual moderation'),
+          ),
+          // Only possible for forms supporting moderation of unpublished posts.
+          '#access' => !empty($mollom_form['moderation callback']),
+          // Only possible for forms protected via text analysis.
+          '#states' => array(
+            'visible' => array(
+              ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
+            ),
+          ),
+        );
+
         // Form elements defined by hook_mollom_form_info() use the
         // 'parent][child' syntax, which Form API also uses internally for
         // form_set_error(), and which allows us to recurse into nested fields
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	9 Oct 2010 03:12:44 -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 put a bad form submission into a
+ *         // moderation queue instead of discarding it.
+ *         'moderation callback' => 'im_mollom_form_moderation',
  *         // 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 'moderation 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_form_moderation(&$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.
+ *   - moderation callback: (optional) A function name to invoke when a form
+ *     submission would normally be discarded. This allows modules to put such
+ *     posts into a moderation queue (i.e., to accept but not publish them) by
+ *     altering the $form or $form_state that are 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.29
diff -u -p -r1.29 mollom.install
--- mollom.install	5 Oct 2010 01:05:13 -0000	1.29
+++ mollom.install	9 Oct 2010 03:13:19 -0000
@@ -109,6 +109,13 @@ function mollom_schema() {
         'not null' => TRUE,
         'default' => 0,
       ),
+      'moderate' => array(
+        'description' => 'Whether the content needs to be moderated.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
       // Server response columns are NULL by default, because any default value
       // would have an unintended meaning. Also, values are stored in individual
       // columns, so as to be able to join and filter/sort on these values for
@@ -174,6 +181,13 @@ function mollom_schema() {
         'not null' => FALSE,
         'serialize' => TRUE,
       ),
+      'discard' => array(
+        'description' => 'Whether to discard (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',
@@ -751,3 +765,27 @@ function mollom_update_7008() {
   db_drop_index('mollom', 'session');
   db_add_index('mollom', 'session_id', array('session_id'));
 }
+
+/**
+ * Add {mollom_form}.discard and {mollom}.moderate columns.
+ */
+function mollom_update_7009() {
+  if (!db_field_exists('mollom_form', 'discard')) {
+    db_add_field('mollom_form', 'discard', array(
+      'description' => 'Whether to discard (1) or moderate (0) bad posts.',
+      'type' => 'int',
+      'size' => 'tiny',
+      'not null' => TRUE,
+      'default' => 1,
+    ));
+  }
+  if (!db_field_exists('mollom', 'moderate')) {
+    db_add_field('mollom', 'moderate', array(
+      'description' => 'Whether the content needs to be moderated.',
+      'type' => 'int',
+      'size' => 'tiny',
+      'not null' => TRUE,
+      'default' => 0,
+    ));
+  }
+}
Index: mollom.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.module,v
retrieving revision 1.87
diff -u -p -r1.87 mollom.module
--- mollom.module	5 Oct 2010 01:05:13 -0000	1.87
+++ mollom.module	9 Oct 2010 03:14:59 -0000
@@ -458,6 +458,29 @@ function mollom_data_save($data) {
 }
 
 /**
+ * Updates stored Mollom session data to mark a bad post as moderated.
+ *
+ * @param $entity
+ *   The entity type of the moderated post.
+ * @param $id
+ *   The entity id of the moderated post.
+ */
+function mollom_data_moderate($entity, $id) {
+  $data = mollom_data_load($entity, $id);
+  // Nothing to do, if no data exists.
+  if (!$data) {
+    return;
+  }
+
+  // Report the session to Mollom.
+  _mollom_send_feedback($data->session_id, 'ham');
+
+  // Mark the session data as moderated.
+  $data->moderate = 0;
+  mollom_data_save($data);
+}
+
+/**
  * Deletes a Mollom session data record from the database.
  *
  * @param $entity
@@ -618,6 +641,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';
@@ -747,6 +771,7 @@ function mollom_form_info($form_id, $mod
     'module' => $module,
     'entity' => NULL,
     'mode' => NULL,
+    'discard' => TRUE,
     'bypass access' => array(),
     'elements' => array(),
     'mapping' => array(),
@@ -1225,6 +1250,7 @@ function mollom_process_mollom($element,
       'require_analysis' => $element['#mollom_form']['mode'] == MOLLOM_MODE_ANALYSIS,
       'require_captcha' => $element['#mollom_form']['mode'] == MOLLOM_MODE_CAPTCHA,
       'passed_captcha' => FALSE,
+      'require_moderation' => FALSE,
       'response' => array(
         'session_id' => '',
       ),
@@ -1232,6 +1258,13 @@ function mollom_process_mollom($element,
   }
   $form_state['mollom'] += $element['#mollom_form'];
 
+  // By default, bad form submissions are discarded, unless the form was
+  // configured to moderate bad posts. 'discard' may only be FALSE, if there is
+  // a valid 'moderation callback'. Otherwise, it must be TRUE.
+  if (empty($form_state['mollom']['moderation callback']) || !function_exists($form_state['mollom']['moderation callback'])) {
+    $form_state['mollom']['discard'] = TRUE;
+  }
+
   // Add the Mollom session element.
   $element['session_id'] = array(
     '#type' => 'hidden',
@@ -1330,10 +1363,22 @@ 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']['discard']) {
+      form_set_error('mollom', t('Your submission has triggered the profanity filter and will not be accepted until the inappropriate language is removed.'));
+    }
+    else {
+      $form_state['mollom']['require_moderation'] = TRUE;
+    }
+    _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.
@@ -1344,7 +1389,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:
@@ -1358,7 +1402,12 @@ 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']['discard']) {
+          form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.'));
+        }
+        else {
+          $form_state['mollom']['require_moderation'] = TRUE;
+        }
         _mollom_watchdog(array(
           'Spam: %teaser' => array('%teaser' => $teaser),
           'Data:<pre>@data</pre>' => array('@data' => $data),
@@ -1484,6 +1533,20 @@ function mollom_validate_captcha(&$form,
 }
 
 /**
+ * Form validation handler to perform post-validation tasks.
+ */
+function mollom_validate_post(&$form, &$form_state) {
+  // Unpublish a post instead of discarding it. If 'discard' is FALSE, then the
+  // 'moderation callback' is responsible for altering $form_state in a way that
+  // the post ends up in a moderation queue. Most callbacks will only want to
+  // set or change a value in $form_state.
+  if ($form_state['mollom']['require_moderation']) {
+    $function = $form_state['mollom']['moderation callback'];
+    $function($form, $form_state);
+  }
+}
+
+/**
  * Form submit handler to flush Mollom session and form information from cache.
  */
 function mollom_form_submit($form, &$form_state) {
@@ -1503,10 +1566,13 @@ function mollom_form_submit($form, &$for
     $values = mollom_form_get_values($form_state['values'], array(), $form_state['mollom']['mapping']);
     // We only consider non-empty and non-zero values as valid entity ids.
     if (!empty($values['post_id'])) {
+      // Save the Mollom session data.
       $data = (object) $form_state['mollom']['response'];
       $data->entity = $form_state['mollom']['entity'];
       $data->id = $values['post_id'];
       $data->form_id = $form_state['mollom']['form_id'];
+      // Set the moderation flag for forms accepting bad posts.
+      $data->moderate = $form_state['mollom']['require_moderation'];
       mollom_data_save($data);
     }
   }
@@ -1996,19 +2062,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) {
@@ -2020,6 +2080,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,
+    'moderation callback' => 'node_mollom_form_moderation',
     'elements' => array(),
     'mapping' => array(
       'post_id' => 'nid',
@@ -2044,6 +2105,31 @@ function node_mollom_form_info($form_id)
 }
 
 /**
+ * Mollom form moderation callback for nodes.
+ */
+function node_mollom_form_moderation(&$form, &$form_state) {
+  $form_state['values']['status'] = 0;
+}
+
+/**
+ * Implements hook_node_presave().
+ */
+function mollom_node_presave($comment) {
+  // If an existing node is published and we have session data stored for it,
+  // mark the data as moderated.
+  if (!empty($node->nid) && $node->status) {
+    mollom_data_moderate('node', $node->nid);
+  }
+}
+
+/**
+ * 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) {
@@ -2086,6 +2172,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;
@@ -2098,6 +2185,7 @@ function comment_mollom_form_info($form_
   $form_info = array(
     'mode' => MOLLOM_MODE_ANALYSIS,
     'bypass access' => array('administer comments'),
+    'moderation callback' => 'comment_mollom_form_moderation',
     'elements' => array(
       'subject' => t('Subject'),
       // @todo Update for Field API.
@@ -2116,6 +2204,24 @@ function comment_mollom_form_info($form_
 }
 
 /**
+ * Mollom form moderation callback for comments.
+ */
+function comment_mollom_form_moderation(&$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,
+  // mark the data as moderated.
+  if (!empty($comment->cid) && $comment->status == COMMENT_PUBLISHED) {
+    mollom_data_moderate('comment', $comment->cid);
+  }
+}
+
+/**
  * Implements hook_comment_delete().
  */
 function mollom_comment_delete($comment) {
@@ -2161,11 +2267,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;
 }
@@ -2179,6 +2284,7 @@ function user_mollom_form_info($form_id)
       $form_info = array(
         'mode' => MOLLOM_MODE_CAPTCHA,
         'bypass access' => array('administer users'),
+        'moderation callback' => 'user_mollom_form_moderation',
         'mapping' => array(
           'post_id' => 'uid',
           'author_name' => 'name',
@@ -2203,6 +2309,13 @@ function user_mollom_form_info($form_id)
 }
 
 /**
+ * Mollom form moderation callback for user accounts.
+ */
+function user_mollom_form_moderation(&$form, &$form_state) {
+  $form_state['values']['status'] = 0;
+}
+
+/**
  * Implements hook_form_FORMID_alter().
  */
 function mollom_form_user_multiple_cancel_confirm_alter(&$form, &$form_state) {
Index: tests/mollom.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/tests/mollom.test,v
retrieving revision 1.67
diff -u -p -r1.67 mollom.test
--- tests/mollom.test	5 Oct 2010 12:01:02 -0000	1.67
+++ tests/mollom.test	9 Oct 2010 03:15:33 -0000
@@ -89,11 +89,16 @@ class MollomWebTestCase extends DrupalWe
     $this->resetSessionID();
     $this->messages = array();
 
+    // @see DrupalWebTestCase::setUp()
+    $modules = func_get_args();
+    if (isset($modules[0]) && is_array($modules[0])) {
+      $modules = $modules[0];
+    }
+
     // If not explicitly disabled by a test, setup with Mollom and default admin
     // user.
     if (empty($this->disableDefaultSetup)) {
       // Call parent::setUp() allowing Mollom test cases to pass further modules.
-      $modules = func_get_args();
       $modules[] = 'mollom';
       $modules[] = 'dblog';
       parent::setUp($modules);
@@ -108,7 +113,6 @@ class MollomWebTestCase extends DrupalWe
       ));
     }
     else {
-      $modules = func_get_args();
       $modules[] = 'dblog';
       parent::setUp($modules);
     }
@@ -637,8 +641,8 @@ class MollomWebTestCase extends DrupalWe
    * @see MollomWebTestCase->assertSessionID()
    * @see DrupalWebTestCase->drupalPost()
    */
-  protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL) {
-    $output = parent::drupalPost($path, $edit, $submit, $options, $headers, $form_html_id);
+  protected function drupalPost($path, $edit, $submit, array $options = array(), array $headers = array(), $form_html_id = NULL, $extra_post = NULL) {
+    $output = parent::drupalPost($path, $edit, $submit, $options, $headers, $form_html_id, $extra_post);
     $options += array('watchdog' => TRUE);
     $this->assertMollomWatchdogMessages($options['watchdog']);
     return $output;
@@ -1103,7 +1107,10 @@ class MollomAccessTestCase extends Mollo
     $this->drupalGet('node/' . $node->nid);
     $this->clickLink('edit');
 
-    $this->drupalPost(NULL, array('subject' => '', 'comment_body[und][0][value]' => 'spam'), t('Preview'));
+    $edit = array(
+      'comment_body[und][0][value]' => 'spam',
+    );
+    $this->drupalPost(NULL, $edit, t('Preview'));
     $this->assertNoText($this->spam_message);
 
     $this->drupalPost(NULL, array(), t('Save'));
@@ -1618,7 +1625,7 @@ class MollomProfanityTestCase extends Mo
     $this->assertMollomData('comment', $cid, $session_id);
 
     // Sequence: Post unsure spam (not profanity), post profanity along with
-    // correct CAPTCHA, and expect that to be rejected.
+    // correct CAPTCHA, and expect that to be discarded.
     $this->web_user = $this->drupalCreateUser();
     $this->drupalLogin($this->web_user);
     $edit = array(
@@ -2087,7 +2094,7 @@ class MollomCommentFormTestCase extends 
     $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => $edit['comment_body[und][0][value]']))->fetchField();
     $this->assertMollomData('comment', $cid, $session_id);
 
-    // Try to save a new 'spam' comment; it should be rejected, with no CAPTCHA
+    // Try to save a new 'spam' comment; it should be discarded, with no CAPTCHA
     // appearing on the page.
     $this->resetSessionID();
     $this->drupalGet('comment/reply/' . $this->node->nid);
@@ -2098,7 +2105,7 @@ class MollomCommentFormTestCase extends 
     $this->assertCommentCount($this->node->nid, $original_number_of_comments);
     $this->assertPrivacyLink();
 
-    // Try to save again; it should be rejected, with no CAPTCHA.
+    // Try to save again; it should be discarded, with no CAPTCHA.
     $this->assertSpamSubmit(NULL, array('comment_body[und][0][value]'), array(), t('Save'));
     $session_id = $this->assertSessionIDInForm();
     $this->assertCommentCount($this->node->nid, $original_number_of_comments);
@@ -2145,6 +2152,8 @@ class MollomCommentFormTestCase extends 
 }
 
 class MollomContactFormTestCase extends MollomWebTestCase {
+  protected $profile = 'testing';
+
   public static function getInfo() {
     return array(
       'name' => 'Contact form protection',
@@ -2154,7 +2163,15 @@ class MollomContactFormTestCase extends 
   }
 
   function setUp() {
-    parent::setUp('contact');
+    $this->disableDefaultSetup = TRUE;
+    parent::setUp(array('contact', 'mollom'));
+    $this->setKeys();
+    $this->assertValidKeys();
+
+    $this->admin_user = $this->drupalCreateUser(array(
+      'access administration pages',
+      'administer mollom',
+    ));
 
     $this->web_user = $this->drupalCreateUser(array('access site-wide contact form', 'access user profiles', 'access user contact forms'));
   }
@@ -2608,6 +2625,144 @@ class MollomDataTestCase extends MollomW
 }
 
 /**
+ * Tests text analysis functionality.
+ *
+ * @todo Verify that no button captions appear in the data that is sent for
+ *   analyis; i.e., no "Add" string for mollom_test_form.
+ */
+class MollomAnalysisTestCase extends MollomWebTestCase {
+  protected $profile = 'testing';
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Text analysis',
+      'description' => 'Tests text analysis functionality.',
+      'group' => 'Mollom',
+    );
+  }
+
+  function setUp() {
+    // @todo This is the new default setUp() procedure for all new tests, which
+    //   should be moved into MollomWebTestCase::setUp() after cleaning up the
+    //   tests.
+    $this->disableDefaultSetup = TRUE;
+    parent::setUp(array('mollom', 'mollom_test'));
+    $this->setKeys();
+    $this->assertValidKeys();
+
+    $this->admin_user = $this->drupalCreateUser(array(
+      'access administration pages',
+      'administer mollom',
+    ));
+    $this->web_user = $this->drupalCreateUser(array());
+  }
+
+  /**
+   * Tests accepting bad posts and moderating them.
+   */
+  function testDiscardModeration() {
+    $this->drupalLogin($this->admin_user);
+    // Verify that mollom_basic_test_form cannot be configured to put posts into
+    // moderation queue.
+    $this->drupalGet('admin/config/content/mollom/manage/mollom_basic_elements_test_form');
+    $this->assertNoFieldByName('mollom[discard]');
+    // Configure mollom_test_form to accept bad posts.
+    $this->setProtection('mollom_test_form', MOLLOM_MODE_ANALYSIS, NULL, array(
+      'mollom[checks][profanity]' => TRUE,
+      'mollom[discard]' => 0,
+    ));
+    $this->drupalLogout();
+
+    // Verify that we are able to post spam and the post is unpublished.
+    $edit = array(
+      'title' => $this->randomString(),
+      'body' => 'spam profanity',
+    );
+    $this->drupalPost('mollom-test/form', $edit, 'Submit');
+    $mid = $this->assertTestSubmitData();
+    $data = $this->assertMollomData('mollom_test', $mid);
+    $record = mollom_test_load($mid);
+    $this->assertEqual($record['status'], 0, t('Unpublished test post found.'));
+    $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM);
+    $this->assertSame('profanity', $data->profanity, 1);
+    $this->assertSame('moderate', $data->moderate, 1);
+
+    // Verify that editing the post does neither change the session data, nor
+    // the publishing status.
+    $edit = array(
+      'title' => $this->randomString(),
+      'body' => 'spam profanity spam profanity',
+    );
+    $this->drupalPost(NULL, $edit, 'Submit');
+    $mid = $this->assertTestSubmitData($mid);
+    $data = $this->assertMollomData('mollom_test', $mid);
+    $record = mollom_test_load($mid);
+    $this->assertEqual($record['status'], 0, t('Unpublished test post found.'));
+    $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM);
+    $this->assertSame('profanity', $data->profanity, 1);
+    $this->assertSame('moderate', $data->moderate, 1);
+
+    // Verify that publishing the post changes the session data accordingly.
+    $this->drupalLogin($this->admin_user);
+    $edit = array(
+      'status' => TRUE,
+    );
+    $this->drupalPost('mollom-test/form/' . $mid, $edit, 'Submit');
+    $mid = $this->assertTestSubmitData($mid);
+    $data = $this->assertMollomData('mollom_test', $mid);
+    $record = mollom_test_load($mid);
+    $this->assertEqual($record['status'], 1, t('Published test post found.'));
+    $this->assertSame('spam', $data->spam, MOLLOM_ANALYSIS_SPAM);
+    $this->assertSame('profanity', $data->profanity, 1);
+    $this->assertSame('moderate', $data->moderate, 0);
+
+    // Verify that neither ham or unsure spam posts, nor non-profane posts are
+    // marked for moderation.
+    $this->drupalLogout();
+    $expectations = array(
+      'ham' => array('spam' => MOLLOM_ANALYSIS_HAM, 'profanity' => 0),
+      'unsure' => array('spam' => MOLLOM_ANALYSIS_UNSURE, 'profanity' => 0),
+      $this->randomString() => array('spam' => MOLLOM_ANALYSIS_UNSURE, 'profanity' => 0),
+    );
+    foreach ($expectations as $body => $expected) {
+      $edit = array(
+        'title' => $this->randomString(),
+        'body' => $body,
+      );
+      $this->drupalPost('mollom-test/form', $edit, 'Submit');
+      if ($expected['spam'] == MOLLOM_ANALYSIS_UNSURE) {
+        $this->postCorrectCaptcha(NULL, array(), 'Submit');
+      }
+      $mid = $this->assertTestSubmitData();
+      $data = $this->assertMollomData('mollom_test', $mid);
+      $record = mollom_test_load($mid);
+      $this->assertEqual($record['status'], 1, t('Published test post %body found.', array('%body' => $body)));
+      $this->assertSame('spam', $data->spam, $expected['spam']);
+      $this->assertSame('profanity', $data->profanity, $expected['profanity']);
+      $this->assertSame('moderate', $data->moderate, 0);
+    }
+  }
+
+  /**
+   * Asserts a successful mollom_test_form submission.
+   *
+   * @param $old_mid
+   *   (optional) The existing test record id to assert.
+   */
+  protected function assertTestSubmitData($old_mid = NULL) {
+    $this->assertText('Successful form submission.');
+    $mid = $this->getFieldValueByName('mid');
+    if (isset($old_mid)) {
+      $this->assertSame('Test record id', $mid, $old_mid);
+    }
+    else {
+      $this->assertTrue($mid > 0, t('Test record id @id found.', array('@id' => $mid)));
+    }
+    return $mid;
+  }
+}
+
+/**
  * Tests report to Mollom functionality.
  */
 class MollomReportTestCase extends MollomWebTestCase {
Index: tests/mollom_test.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/tests/mollom_test.install,v
retrieving revision 1.2
diff -u -p -r1.2 mollom_test.install
--- tests/mollom_test.install	17 Feb 2010 09:05:13 -0000	1.2
+++ tests/mollom_test.install	8 Oct 2010 22:08:31 -0000
@@ -13,11 +13,29 @@ function mollom_test_schema() {
   $schema['mollom_test'] = array(
     'description' => 'Stores testing data for Mollom test form.',
     'fields' => array(
-      'mid' => array('type' => 'serial', 'not null' => TRUE,
+      'mid' => array(
         'description' => 'Primary key: Unique mollom_test entity ID.',
+        'type' => 'serial',
+        'not null' => TRUE,
       ),
-      'body' => array('type' => 'text', 'not null' => TRUE,
-        'description' => 'The body field of mollom_test_form().',
+      'title' => array(
+        'description' => 'Title of the post.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'body' => array(
+        'description' => 'Body of the post.',
+        'type' => 'text',
+        'not null' => TRUE,
+      ),
+      'status' => array(
+        'description' => 'Publishing status.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 1,
       ),
     ),
     'primary key' => array('mid'),
Index: tests/mollom_test.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/tests/mollom_test.module,v
retrieving revision 1.14
diff -u -p -r1.14 mollom_test.module
--- tests/mollom_test.module	11 Sep 2010 02:22:27 -0000	1.14
+++ tests/mollom_test.module	9 Oct 2010 00:56:54 -0000
@@ -18,6 +18,7 @@ function mollom_test_xmlrpc() {
     'mollom.checkContent' => 'mollom_test_check_content',
     'mollom.getImageCaptcha' => 'mollom_test_get_captcha',
     'mollom.checkCaptcha' => 'mollom_test_check_captcha',
+    'mollom.sendFeedback' => 'mollom_test_send_feedback',
  );
 }
 
@@ -170,6 +171,20 @@ function mollom_test_check_captcha($data
 }
 
 /**
+ * XML-RPC callback for mollom.sendFeedback to send feedback for a moderated post.
+ */
+function mollom_test_send_feedback($data) {
+  $storage = variable_get(__FUNCTION__, array());
+  $storage[] = $data;
+  variable_set(__FUNCTION__, $storage);
+
+  if (in_array($data['feedback'], array('spam', 'profanity', 'low-quality', 'unwanted', 'ham'))) {
+    return TRUE;
+  }
+  xmlrpc_error(MOLLOM_ERROR);
+}
+
+/**
  * Implements hook_menu().
  */
 function mollom_test_menu() {
@@ -183,12 +198,34 @@ function mollom_test_menu() {
 }
 
 /**
+ * Implements hook_forms().
+ */
+function mollom_forms() {
+  $forms['mollom_basic_test_form'] = array(
+    'callback' => 'mollom_test_form',
+  );
+  return $forms;
+}
+
+/**
  * Implements hook_mollom_form_list().
  */
 function mollom_test_mollom_form_list() {
   $forms['mollom_test_form'] = array(
     'title' => 'Mollom test form',
     'entity' => 'mollom_test',
+    'entity delete multiple callback' => 'mollom_test_delete_multiple',
+    'moderation callback' => 'mollom_test_mollom_form_moderation',
+  );
+  // The basic test form is identical to the mollom_test_form, but only
+  // registers minimal information (e.g., no entity or moderation callback) to
+  // integrate with Mollom.
+  $forms['mollom_basic_test_form'] = array(
+    'title' => 'Mollom basic test form',
+  );
+  // Same as above, but supports elements for text analysis.
+  $forms['mollom_basic_elements_test_form'] = array(
+    'title' => 'Mollom basic elements test form',
   );
   return $forms;
 }
@@ -197,10 +234,11 @@ function mollom_test_mollom_form_list() 
  * Implements hook_mollom_form_info().
  */
 function mollom_test_mollom_form_info($form_id) {
+  if ($form_id == 'mollom_basic_test_form') {
+    return array();
+  }
   $form_info = array(
-    'title' => 'Mollom test form',
     'bypass access' => array('administer mollom'),
-    'entity' => 'mollom_test',
     'elements' => array(
       'title' => 'Title',
       'body' => 'Body',
@@ -221,20 +259,22 @@ function mollom_test_mollom_form_info($f
  * Form builder for Mollom test form.
  */
 function mollom_test_form($form, &$form_state, $mid = NULL) {
-  if (empty($form_state['storage'])) {
-    $body = '';
-    if (isset($mid)) {
-      $body = db_query("SELECT body FROM {mollom_test} WHERE mid = :mid", array(':mid' => $mid))->fetchField();
-    }
-    $form_state['storage'] = array(
-      'mid' => $mid,
-      'title' => '',
-      'body' => $body,
-      'exclude' => '',
-      'parent' => array('child' => ''),
-      'field' => array(),
-    );
-  }
+  // Due to #limit_validation_errors, submitting the form with the "Add" button
+  // will only expose validated values in the submit handler, so our storage may
+  // be incomplete. Therefore, the default values always have to be overloaded.
+  $form_state += array('storage' => array());
+  if (isset($mid) && ($record = mollom_test_load($mid))) {
+    $form_state['storage'] = $record;
+  }
+  $form_state['storage'] += array(
+    'mid' => $mid,
+    'title' => '',
+    'body' => '',
+    'exclude' => '',
+    'parent' => array('child' => ''),
+    'field' => array(),
+    'status' => 1,
+  );
   // Always add an empty field the user can submit.
   $form_state['storage']['field']['new'] = '';
 
@@ -282,11 +322,19 @@ function mollom_test_form($form, &$form_
   $form['field']['submit'] = array(
     '#type' => 'submit',
     '#value' => 'Add',
-    '#validate_parents' => array('field'),
+    '#limit_validation_errors' => array(array('field')),
     '#submit' => array('mollom_test_form_field_submit'),
     '#weight' => 1000,
   );
 
+  $form['status'] = array(
+    '#type' => 'checkbox',
+    '#title' => 'Published',
+    '#default_value' => $form_state['storage']['status'],
+    // For simplicity, re-use Mollom module's administration permission.
+    '#access' => user_access('administer mollom'),
+  );
+
   $form['actions'] = array('#type' => 'actions');
   $form['actions']['submit'] = array('#type' => 'submit', '#value' => 'Submit');
 
@@ -297,10 +345,18 @@ function mollom_test_form($form, &$form_
  * Form element submit handler for mollom_test_form().
  */
 function mollom_test_form_field_submit($form, &$form_state) {
-  // Store the new value and clear out the 'new' field.
+  // Remove internal Form API values.
+  form_state_values_clean($form_state);
+  // Remove all empty values of the multiple value field.
+  $form_state['values']['field'] = array_filter($form_state['values']['field']);
+  // Update the storage with submitted values.
   $form_state['storage'] = $form_state['values'];
-  $form_state['storage']['field'][] = $form_state['values']['field']['new'];
+  // Store the new value and clear out the 'new' field.
+  if (isset($form_state['values']['field']['new'])) {
+    $form_state['storage']['field'][] = $form_state['values']['field']['new'];
+  }
   unset($form_state['input']['field']['new']);
+
   $form_state['rebuild'] = TRUE;
 }
 
@@ -311,6 +367,12 @@ function mollom_test_form_submit($form, 
   $form_state['values']['field'][] = $form_state['values']['field']['new'];
   unset($form_state['values']['field']['new']);
 
+  // Allow modules to alter the record before saving. This is done for code
+  // consistency only.
+  // @see mollom_mollom_test_presave()
+  form_state_values_clean($form_state);
+  module_invoke_all('mollom_test_presave', $form_state['values']);
+
   // Store submission.
   $update = !empty($form_state['values']['mid']) ? 'mid' : array();
   drupal_write_record('mollom_test', $form_state['values'], $update);
@@ -331,3 +393,40 @@ function mollom_test_form_alter(&$form, 
     $form_state['mollom']['require_captcha'] = FALSE;
   }
 }
+
+/**
+ * Mollom form moderation callback for a mollom_test record.
+ */
+function mollom_test_mollom_form_moderation(&$form, &$form_state) {
+  $form_state['values']['status'] = 0;
+}
+
+/**
+ * Implements hook_mollom_test_presave() on behalf of mollom.module.
+ */
+function mollom_mollom_test_presave($record) {
+  // If an existing record is published and we have session data stored for it,
+  // mark the data as moderated.
+  if (!empty($record['mid']) && $record['status']) {
+    mollom_data_moderate('mollom_test', $record['mid']);
+  }
+}
+
+/**
+ * Loads a {mollom_test} data record by id.
+ */
+function mollom_test_load($mid) {
+  return db_query('SELECT * FROM {mollom_test} WHERE mid = :mid', array(':mid' => $mid))->fetchAssoc();
+}
+
+/**
+ * Deletes multiple stored {mollom_test} data records.
+ *
+ * @param $mids
+ *   The mids to delete.
+ */
+function mollom_test_delete_multiple(array $mids) {
+  db_delete('mollom_test')
+    ->condition('mid', $mids)
+    ->execute();
+}
