Index: mollom.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.module,v
retrieving revision 1.71
diff -u -p -r1.71 mollom.module
--- mollom.module	11 Sep 2010 02:22:27 -0000	1.71
+++ mollom.module	11 Sep 2010 14:59:14 -0000
@@ -1244,7 +1244,7 @@ function mollom_process_mollom($element,
  * must fall back to a CAPTCHA.
  */
 function mollom_validate_analysis(&$form, &$form_state) {
-  if (!$form_state['mollom']['require_analysis'] || $form_state['mollom']['require_captcha']) {
+  if (!$form_state['mollom']['require_analysis']) {
     return;
   }
 
@@ -1259,7 +1259,6 @@ function mollom_validate_analysis(&$form
   $result = mollom('mollom.checkContent', $data);
 
   // Trigger global fallback behavior if there is no result.
-  // @todo Isn't mollom() invoking _mollom_fallback() already?
   if (!isset($result['session_id'])) {
     return _mollom_fallback();
   }
@@ -1268,15 +1267,18 @@ function mollom_validate_analysis(&$form
   $form_state['mollom']['response'] = $result;
   $form['mollom']['session_id']['#value'] = $result['session_id'];
 
-  // Check the profanity threshold of the content.
+  // 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)));
   }
 
+  // Handle the spam check result.
+  // The Mollom backend is remembering previous mollom.checkContent invocations within a single session. When a content is re-checked during form validation, 
   if (isset($result['spam'])) {
     switch ($result['spam']) {
       case MOLLOM_ANALYSIS_HAM:
+        // 
         $form_state['mollom']['require_captcha'] = FALSE;
         watchdog('mollom', 'Ham: <pre>@message</pre>Result: <pre>@result</pre>', array('@message' => print_r($data, TRUE), '@result' => print_r($result, TRUE)));
         break;
@@ -1288,27 +1290,34 @@ function mollom_validate_analysis(&$form
         break;
 
       case MOLLOM_ANALYSIS_UNSURE:
-        $form_state['mollom']['require_captcha'] = TRUE;
-        form_set_error('mollom][captcha', t('To complete this form, please complete the word verification below.'));
         watchdog('mollom', 'Unsure: <pre>@message</pre>Result: <pre>@result</pre>', array('@message' => print_r($data, TRUE), '@result' => print_r($result, TRUE)));
 
-        $form['mollom']['captcha']['#access'] = TRUE;
-        $form['mollom']['captcha']['#required'] = TRUE;
+        // Only throw an error and retrieve a CAPTCHA, if we are check this post
+        // for the first time. Otherwise, mollom_validate_captcha() takes over
+        // CAPTCHA validation.
+        if (!$form_state['mollom']['require_captcha']) {
+          $form_state['mollom']['require_captcha'] = TRUE;
+          form_set_error('mollom][captcha', t('To complete this form, please complete the word verification below.'));
+
+          $form['mollom']['captcha']['#access'] = TRUE;
+          $form['mollom']['captcha']['#required'] = TRUE;
+
+          $captcha_data = array(
+            'author_ip' => $data['author_ip'],
+            'session_id' => $result['session_id'],
+          );
+          $captcha = mollom_get_captcha('image', $captcha_data);
 
-        $captcha_data = array(
-          'author_ip' => $data['author_ip'],
-          'session_id' => $result['session_id'],
-        );
-        $captcha = mollom_get_captcha('image', $captcha_data);
-
-        // If we get a response, add the image CAPTCHA to the form element.
-        if (isset($captcha['response']['session_id']) && !empty($captcha['markup'])) {
-          $form_state['mollom']['response']['session_id'] = $captcha['response']['session_id'];
-          $form['mollom']['session_id']['#value'] = $captcha['response']['session_id'];
-          $form['mollom']['captcha']['#field_prefix'] = $captcha['markup'];
+          // If we get a response, add the image CAPTCHA to the form element.
+          if (isset($captcha['response']['session_id']) && !empty($captcha['markup'])) {
+            $form_state['mollom']['response']['session_id'] = $captcha['response']['session_id'];
+            $form['mollom']['session_id']['#value'] = $captcha['response']['session_id'];
+            $form['mollom']['captcha']['#field_prefix'] = $captcha['markup'];
+          }
         }
         break;
 
+      case MOLLOM_ANALYSIS_UNKNOWN:
       default:
         // If we end up here, something went totally wrong.
         _mollom_fallback();
@@ -1321,19 +1330,15 @@ function mollom_validate_analysis(&$form
  * Form validation handler for CAPTCHA form element.
  */
 function mollom_validate_captcha(&$form, &$form_state) {
-  if (!$form_state['mollom']['require_captcha']) {
-    $form['mollom']['captcha']['#access'] = FALSE;
-    return;
-  }
-
-  // When re-validating a form that already passed a CAPTCHA in a previous
-  // request, we need to re-populate our global variable for mollom_data_save().
-  if ($form_state['mollom']['passed_captcha']) {
+  // Skip this validation, if we do not require a CAPTCHA or already passed one.
+  if (!$form_state['mollom']['require_captcha'] || $form_state['mollom']['passed_captcha']) {
     $form['mollom']['captcha']['#access'] = FALSE;
     return;
   }
 
   // Nothing to validate if there is no value.
+  // @todo The field is #required, so Form API should already handle this. Add a
+  //   test to be sure and remove this code.
   if (empty($form_state['values']['mollom']['captcha'])) {
     return;
   }
Index: tests/mollom.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/tests/mollom.test,v
retrieving revision 1.55
diff -u -p -r1.55 mollom.test
--- tests/mollom.test	11 Sep 2010 02:22:27 -0000	1.55
+++ tests/mollom.test	11 Sep 2010 15:22:10 -0000
@@ -558,6 +558,7 @@ class MollomWebTestCase extends DrupalWe
    *   (optional) The XML-RPC method name to retrieve submitted values from.
    *   Defaults to 'mollom.checkContent'.
    *
+   * @see MollomWebTestCase::resetServerRecords()
    * @see mollom_test_xmlrpc()
    */
   protected function getServerRecord($method = 'mollom.checkContent') {
@@ -575,6 +576,26 @@ class MollomWebTestCase extends DrupalWe
   }
 
   /**
+   * Resets recorded XML-RPC values.
+   *
+   * @param $method
+   *   (optional) The XML-RPC method name to reset records of. Defaults to
+   *   'mollom.checkContent'.
+   *
+   * @see MollomWebTestCase::getServerRecord()
+   * @see mollom_test_xmlrpc()
+   */
+  protected function resetServerRecords($method = 'mollom.checkContent') {
+    // Map the XML-RPC method name to the corresponding function callback name.
+    drupal_load('module', 'mollom_test');
+    $method_function_map = mollom_test_xmlrpc();
+    $function = $method_function_map[$method];
+
+    // Delete the variable.
+    variable_del($function);
+  }
+
+  /**
    * Wraps drupalGet() for additional watchdog message assertion.
    *
    * @param $options
@@ -819,14 +840,22 @@ class MollomResponseTestCase extends Mol
     );
   }
 
+  function setUp() {
+    $this->disableDefaultSetup = TRUE;
+    parent::setUp();
+    $this->setKeys();
+    $this->assertValidKeys();
+  }
+
   /**
    * Tests mollom.checkContent().
    */
   function testCheckContent() {
+    $this->web_user = $this->drupalCreateUser(array());
     $data = array(
-      'author_name' => $this->admin_user->name,
-      'author_mail' => $this->admin_user->mail,
-      'author_id' => $this->admin_user->uid,
+      'author_name' => $this->web_user->name,
+      'author_mail' => $this->web_user->mail,
+      'author_id' => $this->web_user->uid,
       'author_ip' => ip_address(),
     );
 
@@ -872,7 +901,19 @@ class MollomResponseTestCase extends Mol
     $this->assertSame('profanity', $result['profanity'], 1);
     $session_id = $this->assertSessionID($result['session_id']);
 
+    // Change the string to contain profanity only.
+    $data['post_body'] = 'profanity';
+    $data['checks'] = 'spam,quality,profanity';
+    $data['session_id'] = $session_id;
+    $result = mollom('mollom.checkContent', $data);
+    $this->assertMollomWatchdogMessages();
+    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE);
+    $this->assertSame('quality', $result['quality'], 0);
+    $this->assertSame('profanity', $result['profanity'], 1);
+    $session_id = $this->assertSessionID($result['session_id']);
+
     // Disable spam checking, only do profanity checking.
+    $data['post_body'] = 'spam profanity';
     $data['checks'] = 'profanity';
     $data['session_id'] = $session_id;
     $result = mollom('mollom.checkContent', $data);
@@ -894,6 +935,53 @@ class MollomResponseTestCase extends Mol
   }
 
   /**
+   * Tests results of mollom.checkContent() across requests for a single session.
+   *
+   * @todo Not sure whether testing this sequence actually makes sense anymore.
+   */
+  function testCheckContentSession() {
+    $this->web_user = $this->drupalCreateUser(array());
+    $data = array(
+      'author_name' => $this->web_user->name,
+      'author_mail' => $this->web_user->mail,
+      'author_id' => $this->web_user->uid,
+      'author_ip' => ip_address(),
+    );
+
+    // Sequence: Post unsure spam, correct CAPTCHA, change post into spam,
+    // expect it to be ham (due to correct CAPTCHA).
+    $data['post_body'] = 'unsure';
+    $result = mollom('mollom.checkContent', $data);
+    $this->assertMollomWatchdogMessages();
+    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_UNSURE);
+    $data['session_id'] = $this->assertSessionID($result['session_id']);
+
+    $captcha_data = array(
+      'session_id' => $data['session_id'],
+      'author_ip' => $data['author_ip'],
+    );
+    $result = mollom('mollom.getImageCaptcha', $captcha_data);
+    $this->assertMollomWatchdogMessages();
+    $data['session_id'] = $this->assertSessionID($result['session_id']);
+
+    $captcha_data = array(
+      'session_id' => $data['session_id'],
+      'author_ip' => $data['author_ip'],
+      'author_id' => $data['author_id'],
+      'captcha_result' => 'correct',
+    );
+    $result = mollom('mollom.checkCaptcha', $captcha_data);
+    $this->assertMollomWatchdogMessages();
+    $this->assertIdentical($result, TRUE, t('CAPTCHA response was correct.'));
+
+    $data['post_body'] = 'spam';
+    $result = mollom('mollom.checkContent', $data);
+    $this->assertMollomWatchdogMessages();
+    $this->assertSame('spam', $result['spam'], MOLLOM_ANALYSIS_HAM);
+    $data['session_id'] = $this->assertSessionID($result['session_id']);
+  }
+
+  /**
    * Tests mollom.getImageCaptcha().
    */
   function testGetImageCaptcha() {
@@ -1379,26 +1467,24 @@ class MollomBlacklistTestCase extends Mo
 class MollomProfanityTestCase extends MollomWebTestCase {
   public static function getInfo() {
     return array(
-      'name' => 'Profanity filtering',
-      'description' => 'Verify that forms can be properly protected and unprotected.',
+      'name' => 'Profanity checking',
+      'description' => 'Tests form protection with text analysis checking for profanity.',
       'group' => 'Mollom',
     );
   }
 
   function setUp() {
     parent::setUp('mollom_test');
-    // Re-route Mollom communication to this testing site.
-    variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version='));
 
-    $this->drupalLogin($this->admin_user);
+    user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access comments', 'post comments', 'post comments without approval'));
   }
 
   /**
-   * Test the different levels of profanity filtering. With our test Mollom
-   * server, the profanity keyword is 'Joomla'.
+   * Tests text analysis profanity checking.
    */
   function testProfanityFiltering() {
     // Protect Mollom test form but do not enable the profanity filter.
+    $this->drupalLogin($this->admin_user);
     $edit_config = array(
       'mollom[checks][profanity]' => FALSE,
     );
@@ -1437,6 +1523,68 @@ class MollomProfanityTestCase extends Mo
     $this->assertText('Successful form submission.');
     $this->assertNoText($this->profanity_message);
   }
+
+  function testExaminer() {
+    variable_set('comment_preview_article', DRUPAL_OPTIONAL);
+    $node = $this->drupalCreateNode(array('type' => 'article'));
+    $langcode = LANGUAGE_NONE;
+
+    // Enable spam and profanity checking for the article node comment form.
+    $this->drupalLogin($this->admin_user);
+    $edit_config = array(
+      'mollom[checks][profanity]' => TRUE,
+      'mollom[checks][spam]' => TRUE,
+    );
+    $this->setProtection('comment_node_article_form', MOLLOM_MODE_ANALYSIS, NULL, $edit_config);
+    $this->drupalLogout();
+
+    // Test plan 1: Post profanity (ham), remove profanity, and expect that to
+    // be accepted.
+    $edit = array(
+      'subject' => $this->randomName(),
+    );
+    $this->drupalGet("node/{$node->nid}");
+    $this->assertNoCaptchaField();
+    $this->assertPrivacyLink();
+
+    $edit["comment_body[$langcode][0][value]"] = 'profanity ham';
+    $this->drupalPost(NULL, $edit, t('Save'));
+    $this->assertText($this->profanity_message);
+    $this->assertNoText(t('Your comment has been posted.'));
+    $session_id = $this->assertSessionIDInForm();
+
+    $edit["comment_body[$langcode][0][value]"] = 'not profane ham';
+    $this->drupalPost(NULL, $edit, t('Save'));
+    $this->assertNoText($this->profanity_message);
+    $this->assertText(t('Your comment has been posted.'));
+    $this->assertRaw('<p>' . $edit["comment_body[$langcode][0][value]"] . '</p>', t('Comment previously containing profanity was found.'));
+    $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => $edit['subject']))->fetchField();
+    $this->assertMollomData('comment', $cid, $session_id);
+
+    // Test plan 2: Post unsure spam, post profanity along with correct CAPTCHA,
+    // and expect that to be rejected.
+    $this->web_user = $this->drupalCreateUser();
+    $this->drupalLogin($this->web_user);
+    $edit = array(
+      'subject' => $this->randomName(),
+    );
+    $this->drupalGet("node/{$node->nid}");
+    $this->assertNoCaptchaField();
+    $this->assertPrivacyLink();
+
+    $edit["comment_body[$langcode][0][value]"] = 'unsure';
+    $this->drupalPost(NULL, $edit, t('Save'));
+    $this->assertCaptchaField();
+    $this->assertNoText($this->profanity_message);
+    $this->assertNoText(t('Your comment has been posted.'));
+    $session_id = $this->assertSessionIDInForm();
+
+    $edit["comment_body[$langcode][0][value]"] = 'unsure profanity';
+    $this->postCorrectCaptcha(NULL, $edit, t('Save'));
+    $this->assertNoCaptchaField();
+    $this->assertText($this->profanity_message);
+    $this->assertNoText(t('Your comment has been posted.'));
+  }
 }
 
 /**
@@ -1844,18 +1992,18 @@ class MollomCommentFormTestCase extends 
     $session_id = $this->assertSessionIDInForm();
     $this->assertPrivacyLink();
 
-    // Try to submit the form by using an invalid CAPTCHA. At this point,
-    // the submission should be rejected and a new CAPTCHA generated (even
-    // if the text of the comment is changed to ham).
-    $this->postIncorrectCaptcha(NULL, array('comment_body[und][0][value]' => 'ham'), t('Save'));
+    // Try to submit the form by solving the CAPTCHA incorrectly. At this point,
+    // the submission should be blocked and a new CAPTCHA generated, but only if
+    // the comment is still neither ham or spam.
+    $this->postIncorrectCaptcha(NULL, array(), t('Save'));
+    $this->assertCaptchaField();
     $session_id = $this->assertSessionIDInForm();
     $this->assertPrivacyLink();
 
-    // Now try using a valid CAPTCHA. The CAPTCHA form should no longer
-    // be present.
+    // Correctly solving the CAPTCHA should accept the form submission.
     $this->postCorrectCaptcha(NULL, array(), t('Save'));
-    $this->assertRaw('<p>ham</p>', t('A comment that is known to be ham appears on the screen after it is submitted.'));
-    $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => 'ham'))->fetchField();
+    $this->assertRaw('<p>unsure</p>', t('A comment that may contain spam was found.'));
+    $cid = db_query('SELECT cid FROM {comment} WHERE subject = :subject ORDER BY created DESC', array(':subject' => 'unsure'))->fetchField();
     $this->assertMollomData('comment', $cid, $session_id);
 
     // Try to save a new 'spam' comment; it should be rejected, with no CAPTCHA
@@ -2263,6 +2411,7 @@ class MollomDataTestCase extends MollomW
     $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
 
     // Log out and post a comment as anonymous user.
+    $this->resetServerRecords();
     $this->drupalLogout();
     $this->drupalGet('node/' . $node->nid);
     $this->clickLink(t('Add new comment'));
@@ -2295,6 +2444,7 @@ class MollomDataTestCase extends MollomW
     $this->assertSame('author_id', $data['author_id'], NULL);
 
     // Log in admin user and edit comment containing spam.
+    $this->resetServerRecords();
     $this->drupalLogin($this->admin_user);
     $this->drupalGet('comment/' . $comment->cid . '/edit');
     // Post without modification.
