From 814c10a088c1e82041c66cc977284fdfe10dc313 Mon Sep 17 00:00:00 2001
From: sun <sun@unleashedmind.com>
Date: Wed, 31 Oct 2012 01:15:44 +0100
Subject: [PATCH] - #1250614 by sun: Allow to protect forms without text
 fields with text analysis without resorting to CAPTCHA
 (e.g., user registration form).

---
 mollom.admin.inc  | 228 +++++++++++++++++++++++++-----------------------------
 mollom.api.php    |   8 ++
 mollom.module     | 205 ++++++++++++++++++++++++++++++++++++------------
 tests/mollom.test | 185 +++++++++++++++++++++++++++++++++++++++-----
 4 files changed, 434 insertions(+), 192 deletions(-)

diff --git a/mollom.admin.inc b/mollom.admin.inc
index 0a461a9..e44f740 100644
--- a/mollom.admin.inc
+++ b/mollom.admin.inc
@@ -188,11 +188,7 @@ function mollom_admin_configure_form(&$form_state, $mollom_form = NULL) {
       );
 
       $modes = array();
-      // Textual analysis, if any elements are available.
-      if (!empty($mollom_form['elements'])) {
-        $modes[MOLLOM_MODE_ANALYSIS] = t('Text analysis');
-      }
-      // CAPTCHA-only, always available.
+      $modes[MOLLOM_MODE_ANALYSIS] = t('Text analysis');
       $modes[MOLLOM_MODE_CAPTCHA] = t('CAPTCHA');
 
       $form['mollom']['mode'] = array(
@@ -228,121 +224,111 @@ function mollom_admin_configure_form(&$form_state, $mollom_form = NULL) {
         '!permission-list' => theme('item_list', $permissions),
       ));
 
-      if (!empty($mollom_form['elements'])) {
-        // If not re-configuring an existing protection, make it the default.
-        if (!isset($mollom_form['mode'])) {
-          $form['mollom']['mode']['#default_value'] = 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;
+      }
 
-        // Textual analysis filters.
-        $form['mollom']['checks'] = array(
-          '#type' => 'checkboxes',
-          '#title' => t('Text analysis checks'),
-          '#options' => array(
-            'spam' => t('Spam'),
-            'profanity' => t('Profanity'),
-          ),
-          '#default_value' => $mollom_form['checks'],
-        );
-
-        // 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
-        // during processing of submitted form values. However, since we are using
-        // those keys also as internal values to configure the fields to use for
-        // textual analysis, we need to encode them. Otherwise, a nested field key
-        // would result in the following checkbox attribute:
-        //   '#name' => 'mollom[enabled_fields][parent][child]'
-        // This would lead to a form validation error, because it is a valid key.
-        // By encoding them, we prevent this from happening:
-        //   '#name' => 'mollom[enabled_fields][parent%5D%5Bchild]'
-        $elements = array();
-        foreach ($mollom_form['elements'] as $key => $value) {
-          $elements[rawurlencode($key)] = $value;
-        }
-        $enabled_fields = array();
-        foreach ($mollom_form['enabled_fields'] as $value) {
-          $enabled_fields[] = rawurlencode($value);
-        }
-        $form['mollom']['enabled_fields'] = array(
-          '#type' => 'checkboxes',
-          '#title' => t('Text fields to analyze'),
-          '#options' => $elements,
-          '#default_value' => $enabled_fields,
-          '#required' => $mollom_form['mode'] == MOLLOM_MODE_ANALYSIS,
-          '#description' => t('Only enable fields that accept text (not numbers). Omit fields that contain sensible data (e.g., credit card numbers) or computed/auto-generated values, as well as author information fields (e.g., name, e-mail).'),
-        );
-
-        $form['mollom']['strictness'] = array(
-          '#type' => 'radios',
-          '#title' => t('Text analysis accuracy'),
-          '#options' => array(
-            'strict' => t('Strict'),
-            'normal' => t('Normal'),
-            'relaxed' => t('Relaxed'),
-          ),
-          '#default_value' => $mollom_form['strictness'],
-          // Only possible for forms protected via text analysis.
-          '#access' => $modes[MOLLOM_MODE_ANALYSIS],
-        );
-
-        $form['mollom']['unsure'] = array(
-          '#type' => 'radios',
-          '#title' => t('When text analysis is unsure'),
-          '#default_value' => $mollom_form['unsure'],
-          // @todo Add 'accept' and 'discard' actions for unsure posts.
-          '#options' => array(
-            'captcha' => t('Show a CAPTCHA'),
-            'moderate' => t('Retain the post for manual moderation'),
-          ),
-          '#required' => $mollom_form['mode'] == MOLLOM_MODE_ANALYSIS,
-          // Only possible for forms protected via text analysis.
-          '#states' => array(
-            'visible' => array(
-              ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
-              ':input[name="mollom[checks][spam]"]' => array('checked' => TRUE),
-            ),
-          ),
-        );
-        // Only possible for forms supporting moderation of unpublished posts.
-        if (empty($mollom_form['moderation callback'])) {
-          unset($form['mollom']['unsure']['#options']['moderate']);
-        }
-        // Until more options are supported, the entire field/widget only makes
-        // sense for forms that support moderation of unpublished posts.
-        $form['mollom']['unsure']['#access'] = !empty($mollom_form['moderation callback']);
-
-        $form['mollom']['discard'] = array(
-          '#type' => 'radios',
-          '#title' => t('When text analysis identifies spam'),
-          '#default_value' => $mollom_form['discard'],
-          '#options' => array(
-            0 => t('Retain the post for manual moderation'),
-            1 => t('Discard the post'),
-          ),
-          '#required' => $mollom_form['mode'] == MOLLOM_MODE_ANALYSIS,
-          // Only possible for forms supporting moderation of unpublished posts.
-          '#access' => !empty($mollom_form['moderation callback']),
-        );
-
-        $form['mollom']['moderation'] = array(
-          '#type' => 'checkbox',
-          '#title' => t('Allow content to be moderated from the hosted <a href="@moderation-url">@moderation-product</a>', array(
-            '@moderation-url' => 'http://mollom.com/moderation',
-            '@moderation-product' => 'Mollom moderation system',
-          )),
-          '#default_value' => $mollom_form['moderation'],
-          // Only possible for forms which result in a locally stored entity.
-          '#access' => !empty($mollom_form['entity']),
-          // Only possible for forms protected via text analysis.
-          '#states' => array(
-            'visible' => array(
-              ':input[name="mollom[mode]"]' => array('value' => (string) MOLLOM_MODE_ANALYSIS),
-            ),
-          ),
-          '#description' => t('Provides a unified moderation interface, supporting multiple sites, moderation teams, and detailed analytics.'),
-        );
+      // Textual analysis filters.
+      $form['mollom']['checks'] = array(
+        '#type' => 'checkboxes',
+        '#title' => t('Text analysis checks'),
+        '#options' => array(
+          'spam' => t('Spam'),
+          'profanity' => t('Profanity'),
+        ),
+        '#default_value' => $mollom_form['checks'],
+      );
+      // Profanity check requires text to analyze; unlike the spam check, there
+      // is no fallback in case there is no text.
+      if (empty($$mollom_form['elements'])) {
+        unset($form['mollom']['checks']['#options']['profanity']);
+      }
+
+      // 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
+      // during processing of submitted form values. However, since we are using
+      // those keys also as internal values to configure the fields to use for
+      // textual analysis, we need to encode them. Otherwise, a nested field key
+      // would result in the following checkbox attribute:
+      //   '#name' => 'mollom[enabled_fields][parent][child]'
+      // This would lead to a form validation error, because it is a valid key.
+      // By encoding them, we prevent this from happening:
+      //   '#name' => 'mollom[enabled_fields][parent%5D%5Bchild]'
+      $elements = array();
+      foreach ($mollom_form['elements'] as $key => $value) {
+        $elements[rawurlencode($key)] = $value;
+      }
+      $enabled_fields = array();
+      foreach ($mollom_form['enabled_fields'] as $value) {
+        $enabled_fields[] = rawurlencode($value);
       }
+      $form['mollom']['enabled_fields'] = array(
+        '#type' => 'checkboxes',
+        '#title' => t('Text fields to analyze'),
+        '#options' => $elements,
+        '#default_value' => $enabled_fields,
+        '#required' => $mollom_form['mode'] == MOLLOM_MODE_ANALYSIS,
+        '#description' => t('Only enable fields that accept text (not numbers). Omit fields that contain sensible data (e.g., credit card numbers) or computed/auto-generated values, as well as author information fields (e.g., name, e-mail).'),
+      );
+
+      $form['mollom']['strictness'] = array(
+        '#type' => 'radios',
+        '#title' => t('Text analysis accuracy'),
+        '#options' => array(
+          'strict' => t('Strict'),
+          'normal' => t('Normal'),
+          'relaxed' => t('Relaxed'),
+        ),
+        '#default_value' => $mollom_form['strictness'],
+        // Only possible for forms protected via text analysis.
+        '#access' => $modes[MOLLOM_MODE_ANALYSIS],
+      );
+
+      $form['mollom']['unsure'] = array(
+        '#type' => 'radios',
+        '#title' => t('When text analysis is unsure'),
+        '#default_value' => $mollom_form['unsure'],
+        // @todo Add 'accept' and 'discard' actions for unsure posts.
+        '#options' => array(
+          'captcha' => t('Show a CAPTCHA'),
+          'moderate' => t('Retain the post for manual moderation'),
+        ),
+        '#required' => $mollom_form['mode'] == MOLLOM_MODE_ANALYSIS,
+      );
+      // Only possible for forms supporting moderation of unpublished posts.
+      if (empty($mollom_form['moderation callback'])) {
+        unset($form['mollom']['unsure']['#options']['moderate']);
+      }
+      // Until more options are supported, the entire field/widget only makes
+      // sense for forms that support moderation of unpublished posts.
+      $form['mollom']['unsure']['#access'] = !empty($mollom_form['moderation callback']);
+
+      $form['mollom']['discard'] = array(
+        '#type' => 'radios',
+        '#title' => t('When text analysis identifies spam'),
+        '#default_value' => $mollom_form['discard'],
+        '#options' => array(
+          0 => t('Retain the post for manual moderation'),
+          1 => t('Discard the post'),
+        ),
+        '#required' => $mollom_form['mode'] == MOLLOM_MODE_ANALYSIS,
+        // Only possible for forms supporting moderation of unpublished posts.
+        '#access' => !empty($mollom_form['moderation callback']),
+      );
+
+      $form['mollom']['moderation'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Allow content to be moderated from the hosted <a href="@moderation-url">@moderation-product</a>', array(
+          '@moderation-url' => 'http://mollom.com/moderation',
+          '@moderation-product' => 'Mollom moderation system',
+        )),
+        '#default_value' => $mollom_form['moderation'],
+        // Only possible for forms which result in a locally stored entity.
+        '#access' => !empty($mollom_form['entity']),
+        '#description' => t('Provides a unified moderation interface, supporting multiple sites, moderation teams, and detailed analytics.'),
+      );
 
       $form['actions']['submit'] = array(
         '#type' => 'submit',
@@ -390,18 +376,14 @@ function mollom_admin_configure_form_validate(&$form, &$form_state) {
   // 'checks' and 'enabled_fields', as their labels do not work with the default
   // #required form error message.
   if ($form_state['storage']['step'] == 'configure') {
-    // Make field checkboxes required, if protection mode is textual analysis.
+    // Make field checkboxes required, if protection mode is text analysis.
     $required = ($form_state['values']['mollom']['mode'] == MOLLOM_MODE_ANALYSIS);
     $form['mollom']['checks']['#required'] = $required;
-    $form['mollom']['enabled_fields']['#required'] = $required;
     $form['mollom']['discard']['#required'] = $required;
 
     if ($required && !array_filter($form_state['values']['mollom']['checks'])) {
       form_error($form['mollom']['checks'], t('At least one text analysis check is required.'));
     }
-    if ($required && !array_filter($form_state['values']['mollom']['enabled_fields'])) {
-      form_error($form['mollom']['enabled_fields'], t('At least one field is required for text analysis.'));
-    }
   }
 }
 
diff --git a/mollom.api.php b/mollom.api.php
index 3598be9..e8f2a2b 100644
--- a/mollom.api.php
+++ b/mollom.api.php
@@ -248,6 +248,12 @@
  *     confirmation form constructor to assign the mapped post_id key in $form
  *     as a #value. See http://drupal.org/node/645374 for examples. Optionally
  *     limit access to report options by defining 'report access' permissions.
+ *   - report path: (optional) A Drupal system path pattern to be used for
+ *     reporting an entity to Mollom via a "Report to Mollom" link in e-mail
+ *     notifications. This typically points to the menu router path that allows
+ *     to delete an entity. The placeholder '%id' is dynamically replaced with
+ *     the entity ID. For example, user_mollom_form_list() specifies
+ *     'user/%id/cancel'.
  *   - report access: (optional) A list containing user permission strings, from
  *     which the current user needs to have at least one. Should only be used if
  *     no "report access callback" was defined.
@@ -290,6 +296,8 @@ function hook_mollom_form_list() {
     'delete form file' => array(
       'name' => 'mymodule.pages',
     ),
+    // Specify where to find the delete confirmation form for e-mails.
+    'report path' => 'user/%id/cancel',
     // Optionally limit access to report options on the delete confirmation form.
     'report access' => array('administer users', 'bypass node access'),
   );
diff --git a/mollom.module b/mollom.module
index 7735d5e..0852d25 100644
--- a/mollom.module
+++ b/mollom.module
@@ -664,6 +664,9 @@ function mollom_form_alter(&$form, &$form_state, $form_id) {
         '#tree' => TRUE,
       );
       // Add Mollom form validation handlers.
+      // Form-level validation handlers are required, since we need access to
+      // all validated and submitted form values. _form_validate() invokes
+      // #element_validate handlers while it is recursing into the form.
       $form['#validate'][] = 'mollom_validate_analysis';
       $form['#validate'][] = 'mollom_validate_captcha';
       $form['#validate'][] = 'mollom_validate_post';
@@ -672,10 +675,6 @@ function mollom_form_alter(&$form, &$form_state, $form_id) {
       // @see _mollom_form_state_values_clean()
       $form['#after_build'][] = 'mollom_form_buttons_after_build';
 
-      // Prepend a submit handler to clean up internal Mollom values from
-      // $form_state['values'].
-      $form += array('#submit' => array());
-      array_unshift($form['#submit'], 'mollom_form_pre_submit');
       // Append a submit handler to store Mollom session data. Requires that
       // the primary submit handler has run already, so a potential 'post_id'
       // mapping can be retrieved from $form_state['values'].
@@ -916,10 +915,8 @@ function mollom_form_new($form_id) {
   $mollom_form += mollom_form_info($form_id, $form_list[$form_id]['module']);
 
   // Enable all fields for textual analysis by default.
-  if (!empty($mollom_form['elements'])) {
-    $mollom_form['checks'] = array('spam');
-    $mollom_form['enabled_fields'] = array_keys($mollom_form['elements']);
-  }
+  $mollom_form['checks'] = array('spam');
+  $mollom_form['enabled_fields'] = array_keys($mollom_form['elements']);
 
   return $mollom_form;
 }
@@ -1615,7 +1612,19 @@ function mollom_process_mollom($element, $input, &$form_state, $complete_form) {
     '#value' => $form_state['mollom']['mollom_build_id'],
   );
 
-  // Add the Mollom session elements.
+  // Add the Mollom session data elements.
+  // These elements resemble the {mollom} database schema. The form validation
+  // handlers will pollute them with values returned by Mollom. For entity
+  // forms, the submitted values will appear in a $entity->mollom property,
+  // which in turn represents the Mollom session data record to be stored.
+  $element['entity'] = array(
+    '#type' => 'value',
+    '#value' => isset($form_state['mollom']['entity']) ? $form_state['mollom']['entity'] : 'mollom_content',
+  );
+  $element['id'] = array(
+    '#type' => 'value',
+    '#value' => NULL,
+  );
   $element['contentId'] = array(
     '#type' => 'hidden',
     '#default_value' => isset($form_state['mollom']['response']['content']['id']) ? $form_state['mollom']['response']['content']['id'] : '',
@@ -1626,6 +1635,25 @@ function mollom_process_mollom($element, $input, &$form_state, $complete_form) {
     '#default_value' => isset($form_state['mollom']['response']['captcha']['id']) ? $form_state['mollom']['response']['captcha']['id'] : '',
     '#attributes' => array('class' => 'mollom-captcha-id'),
   );
+  $element['form_id'] = array(
+    '#type' => 'value',
+    '#value' => $form_state['mollom']['form_id'],
+  );
+  $element['moderate'] = array(
+    '#type' => 'value',
+    '#value' => 0,
+  );
+  $data_spec = array(
+    '#type' => 'value',
+    '#value' => NULL,
+  );
+  $element['spamScore'] = $data_spec;
+  $element['spamClassification'] = $data_spec;
+  $element['solved'] = $data_spec;
+  $element['qualityScore'] = $data_spec;
+  $element['profanityScore'] = $data_spec;
+  $element['reason'] = $data_spec;
+  $element['languages'] = $data_spec;
 
   // Add the CAPTCHA element.
   $element['captcha'] = array(
@@ -1751,6 +1779,7 @@ function mollom_form_add_captcha(&$element, &$form_state) {
     $element['captcha']['#field_prefix'] = $captcha;
     // Assign the session ID returned by Mollom.
     $element['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['id'];
+    $form_state['values']['mollom']['captchaId'] = $form_state['mollom']['response']['captcha']['id'];
   }
   // Otherwise, we have a communication or configuration error.
   else {
@@ -1805,7 +1834,14 @@ function mollom_validate_analysis(&$form, &$form_state) {
 
   // Store the response returned by Mollom.
   $form_state['mollom']['response']['content'] = $result;
-  $form['mollom']['contentId']['#value'] = $result['id'];
+  // Set form element values accordingly. Do not overwrite the entity ID with
+  // the contentId, nor a possibly existing captchaId.
+  $result['contentId'] = $result['id'];
+  unset($result['id']);
+  $form_state['values']['mollom'] = array_merge($form_state['values']['mollom'], $result);
+  // #value has to be set manually to output it in case of a validation error,
+  // since this is not a element-level but a form-level validation handler.
+  $form['mollom']['contentId']['#value'] = $result['contentId'];
 
   // Prepare watchdog message teaser text.
   $teaser = '--';
@@ -1964,7 +2000,14 @@ function mollom_validate_captcha(&$form, &$form_state) {
 
   // Store the response for #submit handlers.
   $form_state['mollom']['response']['captcha'] = $result;
-  $form['mollom']['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['id'];
+  // Set form element values accordingly. Do not overwrite the entity ID with
+  // the contentId, nor a possibly existing captchaId.
+  $result['captchaId'] = $result['id'];
+  unset($result['id']);
+  $form_state['values']['mollom'] = array_merge($form_state['values']['mollom'], $result);
+  // #value has to be set manually to output it in case of a validation error,
+  // since this is not a element-level but a form-level validation handler.
+  $form['mollom']['captchaId']['#value'] = $result['captchaId'];
 
   if (!empty($result['solved'])) {
     $form_state['mollom']['passed_captcha'] = TRUE;
@@ -2050,40 +2093,29 @@ function mollom_validate_post(&$form, &$form_state) {
   // 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']) {
+    $form_state['values']['mollom']['moderate'] = 1;
+
     $function = $form_state['mollom']['moderation callback'];
     $function($form, $form_state);
   }
 }
 
 /**
- * Form submit handler to clean up internal Mollom values from $form_state['values'].
- *
- * Some form submit handlers blindly take over and save all submitted form
- * values in $form_state['values'] into the database. To prevent Mollom's
- * internal values from being mistakenly stored somewhere else, this submit
- * handler is prepended to the stack of $form['#submit'] handlers of protected
- * forms.
- *
- * @todo Fix Drupal core to remove the need for separately prepended submit
- *   handlers like this one by making form_state_values_clean() invoke a hook.
- * @see http://drupal.org/node/939510
- */
-function mollom_form_pre_submit($form, &$form_state) {
-  // Some modules are implementing multi-step forms without separate form
-  // submit handlers. In case we reach here and the form will be rebuilt, we
-  // need to defer our submit handling until final submission.
-  if (!empty($form_state['rebuild'])) {
-    return;
-  }
-  // When having passed the form validation stage and reaching the form
-  // submission stage, all submitted form values have been processed into
-  // $form_state['mollom'] already, so the entire top-level 'mollom' key can be
-  // safely removed.
-  unset($form_state['values']['mollom']);
-}
-
-/**
  * Form submit handler to flush Mollom session and form information from cache.
+ *
+ * @todo Check whether this is still needed with mollom_entity_insert(). For
+ *   entity forms, this approach never really worked, since:
+ *   - The primary submit handler fails to set the new ID of a newly stored
+ *     entity in the submitted form values (which has been standardized in core,
+ *     but is not enforced anywhere), so the postId cannot be extracted from
+ *     submitted form values.
+ *   - This submit handler is invoked too early, before the primary submit
+ *     handler processed and saved the entity, so the postId cannot be extracted
+ *     from submitted form values.
+ *   - This submit handler is invoked too late; the primary submit handler might
+ *     send out e-mails directly after saving the entity (e.g.,
+ *     user_register_form_submit()), so mollom_mail_alter() is invoked before
+ *     Mollom session data has been saved.
  */
 function mollom_form_submit($form, &$form_state) {
   // Some modules are implementing multi-step forms without separate form
@@ -2616,6 +2648,10 @@ function mollom_get_captcha(&$form_state) {
  * form submissions.
  *
  * @see mollom_mail_add_report_link()
+ *
+ * @todo With mollom_entity_insert(), $message['params'] might contain an array
+ *   key that has a ::$mollom property holding the Mollom session data,
+ *   potentially eliminating the need for $GLOBALS['mollom'].
  */
 function mollom_mail_alter(&$message) {
   // Attaches the Mollom report link to any mails with IDs specified from the
@@ -2628,7 +2664,7 @@ function mollom_mail_alter(&$message) {
 }
 
 /**
- * Add the 'Report as innapropriate' link to an e-mail message.
+ * Add the 'Report as inappropriate' link to an e-mail message.
  *
  * @param array $message
  *   The message to alter.
@@ -2640,21 +2676,43 @@ function mollom_mail_alter(&$message) {
  */
 function mollom_mail_add_report_link(array &$message, array $mollom) {
   if (!empty($mollom['response']['content']['id']) || !empty($mollom['response']['captcha']['id'])) {
-    $data = (object) $mollom['response'];
+    // Check whether an entity was stored with the submission.
+    $data = FALSE;
     if (!empty($mollom['response']['content']['id'])) {
-      $data->entity = 'mollom_content';
-      $data->id = $data->content['id'];
-      $data->contentId = $data->content['id'];
+      $data = mollom_content_load($mollom['response']['content']['id']);
+    }
+    elseif (!empty($mollom['response']['captcha']['id'])) {
+      $data = mollom_db_query_range('SELECT * FROM {mollom} WHERE captchaId = :captchaId', 0, 1, array(':captchaId' => $mollom['response']['captcha']['id']))->fetchObject();
+    }
+    if (!$data) {
+      // @todo Mollom session data should have been saved earlier already;
+      //   eliminate this.
+      $data = (object) $mollom['response'];
+      if (!empty($mollom['response']['content']['id'])) {
+        $data->entity = 'mollom_content';
+        $data->id = $data->content['id'];
+        $data->contentId = $data->content['id'];
+      }
+      else {
+        $data->entity = 'mollom_captcha';
+        $data->id = $data->captcha['id'];
+        $data->captchaId = $data->captcha['id'];
+      }
+      $data->form_id = $mollom['form_id'];
+      mollom_data_save($data);
+    }
+    // Determine report URI.
+    $mollom_form = mollom_form_load($data->form_id);
+    if (isset($mollom_form['report path'])) {
+      $path = strtr($mollom_form['report path'], array(
+        '%id' => $data->id,
+      ));
     }
     else {
-      $data->entity = 'mollom_captcha';
-      $data->id = $data->captcha['id'];
-      $data->captchaId = $data->captcha['id'];
+      $path = "mollom/report/{$data->entity}/{$data->id}";
     }
-    $data->form_id = $mollom['form_id'];
-    mollom_data_save($data);
     $report_link = t('Report as inappropriate: @link', array(
-      '@link' => url("mollom/report/{$data->entity}/{$data->id}", array('absolute' => TRUE)),
+      '@link' => url($path, array('absolute' => TRUE)),
     ));
     // Until D7, hook_mail_alter() accepts both arrays and strings.
     if (is_array($message['body'])) {
@@ -2667,6 +2725,51 @@ function mollom_mail_add_report_link(array &$message, array $mollom) {
 }
 
 /**
+ * Implements hook_entity_insert().
+ */
+function mollom_entity_insert($entity, $type) {
+  list($id) = entity_extract_ids($type, $entity);
+  if (!empty($entity->mollom) && !empty($id)) {
+    $entity->mollom['id'] = $id;
+    $data = (object) $entity->mollom;
+    mollom_data_save($data);
+  }
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function mollom_entity_update($entity, $type) {
+  // If an existing entity is published and we have session data stored for it,
+  // mark the data as moderated.
+  $update = FALSE;
+  // If the entity update function provides the original entity, only mark the
+  // data as moderated when the entity's status transitioned to published.
+  if (isset($entity->original->status)) {
+    if (empty($entity->original->status) && !empty($entity->status)) {
+      $update = TRUE;
+    }
+  }
+  // If there is no original entity to compare against, check for the current
+  // status only.
+  elseif (!empty($entity->status)) {
+    $update = TRUE;
+  }
+  if ($update) {
+    list($id) = entity_extract_ids($type, $entity);
+    mollom_data_moderate($type, $id);
+  }
+}
+
+/**
+ * Implements hook_entity_delete().
+ */
+function mollom_entity_delete($entity, $type) {
+  list($id) = entity_extract_ids($type, $entity);
+  mollom_data_delete($type, $id);
+}
+
+/**
  * @name mollom_moderation Mollom Moderation integration.
  * @{
  *
@@ -3372,15 +3475,18 @@ function mollom_form_comment_admin_overview_alter(&$form, $form_state) {
  */
 function user_mollom_form_list() {
   $forms['user_register'] = array(
+    'mode' => MOLLOM_MODE_CAPTCHA,
     'title' => t('User registration form'),
     'entity' => 'user',
     'delete form' => 'user_confirm_delete',
     'delete form file' => array(
       'name' => 'user.pages',
     ),
+    'report path' => 'user/%id/delete',
     'report access' => array('administer users'),
   );
   $forms['user_pass'] = array(
+    'mode' => MOLLOM_MODE_CAPTCHA,
     'title' => t('User password request form'),
   );
   return $forms;
@@ -3406,7 +3512,6 @@ function user_mollom_form_info($form_id) {
 
     case 'user_pass':
       $form_info = array(
-        'mode' => MOLLOM_MODE_CAPTCHA,
         'bypass access' => array('administer users'),
         'mapping' => array(
           'post_id' => 'uid',
diff --git a/tests/mollom.test b/tests/mollom.test
index 49552b9..a0d0966 100644
--- a/tests/mollom.test
+++ b/tests/mollom.test
@@ -672,6 +672,8 @@ class MollomWebTestCase extends DrupalWebTestCase {
    *   - entity: The entity type contained in the report link URL.
    *   - id: The entity ID contained in the report link URL.
    *   - mail: The full mail message array, as recorded by TestingMailSystem.
+   *   - external: TRUE.
+   *   The array can be passed directly as $options to drupalGet().
    */
   protected function assertMailMollomReportLink($entity_type = 'mollom_content') {
     // Grab the last sent mail.
@@ -681,12 +683,33 @@ class MollomWebTestCase extends DrupalWebTestCase {
     variable_set('drupal_test_email_collector', $captured_emails);
 
     $found = FALSE;
-    if (preg_match('@http.+?mollom/report/([^/]+)/([^\s]+)@', $message['body'], $matches)) {
+    // Determine the report URI pattern for the passed entity type.
+    $path = FALSE;
+    foreach (mollom_form_list() as $form_id => $info) {
+      if (isset($info['entity']) && $info['entity'] == $entity_type && isset($info['report path'])) {
+        $path = $info['report path'];
+        break;
+      }
+    }
+    if ($path) {
+      $path = strtr($path, array('%id' => '([^\s]+)'));
+      if (preg_match('@http.+?' . $path . '@', $message['body'], $matches)) {
+        $found = array(
+          'url' => $matches[0],
+          'entity' => $entity_type,
+          'id' => $matches[1],
+          'mail' => $message,
+          'external' => TRUE,
+        );
+      }
+    }
+    elseif (preg_match('@http.+?mollom/report/([^/]+)/([^\s]+)@', $message['body'], $matches)) {
       $found = array(
         'url' => $matches[0],
         'entity' => $matches[1],
         'id' => $matches[2],
         'mail' => $message,
+        'external' => TRUE,
       );
     }
     $this->assertTrue($found, t('Report to Mollom link found in e-mail: %url', array('%url' => $found['url'])));
@@ -2103,16 +2126,6 @@ class MollomFormConfigurationTestCase extends MollomWebTestCase {
     $this->drupalPost(NULL, $edit, t('Next'));
     $this->assertText('Mollom test form');
 
-    // Verify that text analysis cannot be enabled without enabling fields.
-    $edit = array(
-      'mollom[mode]' => MOLLOM_MODE_ANALYSIS,
-    );
-    foreach ($form_info['elements'] as $key => $label) {
-      $edit['mollom[enabled_fields][' . rawurlencode($key) . ']'] = FALSE;
-    }
-    $this->drupalPost(NULL, $edit, t('Save'));
-    $this->assertText(t('!name field is required.', array('!name' => t('Text fields to analyze'))));
-
     $edit = array(
       'mollom[mode]' => MOLLOM_MODE_ANALYSIS,
       'mollom[checks][spam]' => TRUE,
@@ -2386,9 +2399,11 @@ class MollomUserFormsTestCase extends MollomWebTestCase {
   /**
    * Make sure that the request password form is protected correctly.
    */
-  function testProtectRequestPassword() {
-    // We first enable Mollom for the request password form.
+  function testUserPasswordCaptcha() {
     $this->drupalLogin($this->admin_user);
+    // Verify that the protection mode defaults to CAPTCHA.
+    $this->drupalGet('admin/settings/mollom/add/user_pass');
+    $this->assertFieldByName('mollom[mode]', MOLLOM_MODE_CAPTCHA);
     $this->setProtection('user_pass', MOLLOM_MODE_CAPTCHA);
     $this->drupalLogout();
 
@@ -2412,9 +2427,11 @@ class MollomUserFormsTestCase extends MollomWebTestCase {
   /**
    * Make sure that the user registration form is protected correctly.
    */
-  function testProtectRegisterUser() {
-    // We first enable Mollom for the user registration form.
+  function testUserRegisterCaptcha() {
     $this->drupalLogin($this->admin_user);
+    // Verify that the protection mode defaults to CAPTCHA.
+    $this->drupalGet('admin/settings/mollom/add/user_register');
+    $this->assertFieldByName('mollom[mode]', MOLLOM_MODE_CAPTCHA);
     $this->setProtection('user_register', MOLLOM_MODE_CAPTCHA);
     $this->drupalLogout();
 
@@ -2442,14 +2459,144 @@ class MollomUserFormsTestCase extends MollomWebTestCase {
     // Try to register with a valid CAPTCHA. Make sure the user was able
     // to successfully register.
     $this->postCorrectCaptcha('user/register', $edit, t('Create new account'));
-    //$this->assertText(t('Your password and further instructions have been sent to your e-mail address.'));
-    $this->assertRaw(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.<br />In the meantime, a welcome message with further instructions has been sent to your e-mail address.'));
-    $this->assertTrue(user_load(array('name' => $name)), t('The user who attempted to register appears in the database when the CAPTCHA is valid.'));
+    $this->assertText(t('Your account is currently pending approval by the site administrator.'));
+    $account = user_load_by_name($edit['name']);
+    $this->assertTrue($account, 'New user found after solving CAPTCHA.');
+    $this->assertEqual($account->status, 0, 'New user account is pending approval.');
+    $data = $this->assertMollomData('user', $account->uid);
+    $this->assertSame('$data->moderate', $data->moderate, 0);
+
+    // First mail sent is the site administrator approval.
+    $link = $this->assertMailMollomReportLink('user');
+    // Second mail goes to the user who registered.
+    $this->assertNoMailMollomReportLink();
+
+    // Verify that the user account is deleted after reporting it as spam.
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet($link['url'], $link);
+    $edit = array(
+      'mollom[feedback]' => 'spam',
+    );
+    $this->drupalPost(NULL, $edit, t('Cancel account'));
+    $account = user_load($account->uid, TRUE);
+    $this->assertFalse($account, 'Reported user account not found.');
+  }
+
+  /**
+   * Tests text analysis protection with CAPTCHA for user registration form.
+   */
+  function testUserRegisterAnalysisCaptcha() {
+    // Allow registration by site visitors without administrator approval.
+    variable_set('user_register', USER_REGISTER_VISITORS);
+
+    $this->drupalLogin($this->admin_user);
+    $this->setProtection('user_register', MOLLOM_MODE_ANALYSIS);
+    $this->drupalLogout();
+
+    // Retrieve initial count of registered users.
+    $count_initial = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+
+    // Verify that a spam user registration is blocked.
+    /*
+    $this->drupalGet('user/register');
+    $this->assertNoCaptchaField();
+    $edit = array(
+      'name' => 'spam',
+      'mail' => 'spam@example.com',
+    );
+    $this->drupalPost(NULL, $edit, t('Create new account'));
+    $this->assertNoCaptchaField();
+    $this->assertText($this->spam_message);
+    $count_new = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+    $this->assertEqual($count_initial, $count_new, 'Existing user count found.');
+    $this->assertFalse(user_load_by_name($edit['name']), 'New user not found.');
+    */
+
+    // Verify that a unsure registration triggers a CAPTCHA.
+    $this->drupalGet('user/register');
+    $this->assertNoCaptchaField();
+    $edit = array(
+      'name' => 'unsure',
+      'mail' => 'unsure@example.com',
+    );
+    $this->drupalPost(NULL, $edit, t('Create new account'));
+    $this->assertCaptchaField();
+
+    // Verify that user count is still the same.
+    $count_new = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+    $this->assertEqual($count_initial, $count_new, 'Existing user count found.');
+
+    // Verify that solving the CAPTCHA registers the user.
+    $this->postCorrectCaptcha(NULL, array(), t('Create new account'));
+    $account = user_load_by_name($edit['name']);
+    $this->assertTrue($account, 'New user found after solving CAPTCHA.');
+    $this->assertEqual($account->status, 1, 'New user account is active.');
+    $data = $this->assertMollomData('user', $account->uid);
+    $this->assertSame('$data->moderate', $data->moderate, 0);
+  }
+
+  /**
+   * Tests text analysis protection without CAPTCHA for user registration form.
+   */
+  function testUserRegisterAnalysisModerate() {
+    // Allow registration by site visitors without administrator approval.
+    variable_set('user_register', USER_REGISTER_VISITORS);
+
+    $this->drupalLogin($this->admin_user);
+    $this->setProtection('user_register', MOLLOM_MODE_ANALYSIS, array(), array(
+      'mollom[unsure]' => 'moderate',
+    ));
+    $this->drupalLogout();
+
+    // Retrieve initial count of registered users.
+    $count_initial = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+
+    // Verify that a spam user registration is blocked.
+    /*
+    $this->drupalGet('user/register');
+    $this->assertNoCaptchaField();
+    $edit = array(
+      'name' => 'spam',
+      'mail' => 'spam@example.com',
+    );
+    $this->drupalPost(NULL, $edit, t('Create new account'));
+    $this->assertNoCaptchaField();
+    $this->assertText($this->spam_message);
+    $count_new = db_query("SELECT COUNT(uid) FROM {users}")->fetchField();
+    $this->assertEqual($count_initial, $count_new, 'Existing user count found.');
+    $this->assertFalse(user_load_by_name($edit['name']), 'New user not found.');
+    */
+
+    // Verify that a unsure registration triggers no CAPTCHA and requires approval.
+    $this->drupalGet('user/register');
+    $this->assertNoCaptchaField();
+    $edit = array(
+      'name' => 'unsure',
+      'mail' => 'unsure@example.com',
+    );
+    $this->drupalPost(NULL, $edit, t('Create new account'));
+    $this->assertNoCaptchaField();
+    $this->assertText(t('Your account is currently pending approval by the site administrator.'));
+    $account = user_load_by_name($edit['name']);
+    $this->assertTrue($account, 'New user found after solving CAPTCHA.');
+    $this->assertEqual($account->status, 0, 'New user account is pending approval.');
+    $data = $this->assertMollomData('user', $account->uid);
+    $this->assertSame('$data->moderate', $data->moderate, 1);
 
     // First mail sent is the site administrator approval.
-    $this->assertMailMollomReportLink('mollom_captcha');
+    $link = $this->assertMailMollomReportLink('user');
     // Second mail goes to the user who registered.
     $this->assertNoMailMollomReportLink();
+
+    // Verify that the user account is deleted after reporting it as spam.
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet($link['url'], $link);
+    $edit = array(
+      'mollom[feedback]' => 'spam',
+    );
+    $this->drupalPost(NULL, $edit, t('Cancel account'));
+    $account = user_load($account->uid, TRUE);
+    $this->assertFalse($account, 'Reported user account not found.');
   }
 }
 
-- 
1.7.11.msysgit.1

