Index: mollom.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/Attic/mollom.admin.inc,v
retrieving revision 1.1.2.9
diff -u -p -r1.1.2.9 mollom.admin.inc
--- mollom.admin.inc	19 Nov 2009 16:48:59 -0000	1.1.2.9
+++ mollom.admin.inc	21 Nov 2009 15:14:44 -0000
@@ -7,10 +7,227 @@
  */
 
 /**
- * Form builder; Administration settings form.
+ * Menu callback; Displays a list of forms configured for Mollom.
+ */
+function mollom_admin_form_list() {
+  $modes = array(
+    MOLLOM_MODE_DISABLED => t('No protection'),
+    MOLLOM_MODE_CAPTCHA => t('CAPTCHA only'),
+    MOLLOM_MODE_ANALYSIS => t('Text analysis and CAPTCHA backup'),
+  );
+
+  $header = array(
+    t('Form'),
+    t('Protection mode'),
+    array('data' => t('Operations'), 'colspan' => 2),
+  );
+  $rows = array();
+  $result = db_query("SELECT form_id FROM {mollom_form}");
+  while ($form_id = db_result($result)) {
+    $mollom_form = mollom_form_load($form_id);
+    $rows[] = array(
+      $mollom_form['title'],
+      $modes[$mollom_form['mode']],
+      l(t('Configure'), 'admin/settings/mollom/manage/' . $form_id),
+      l(t('Unprotect'), 'admin/settings/mollom/unprotect/' . $form_id),
+    );
+  }
+
+  // Add a row to add a form.
+  if (empty($rows)) {
+    $rows[] = array(array('data' => l(t('Add form'), 'admin/settings/mollom/add'), 'colspan' => 4));
+  }
+
+  return theme('table', $header, $rows);
+}
+
+/**
+ * Return registered forms as an array suitable for a 'checkboxes' form element #options property.
+ */
+function mollom_admin_form_options() {
+  // Retrieve all registered forms.
+  $form_info = mollom_get_form_info();
+
+  // Remove already configured form ids.
+  $result = db_query("SELECT form_id FROM {mollom_form}");
+  while ($form_id = db_result($result)) {
+    unset($form_info[$form_id]);
+  }
+
+  // Load module information.
+  $modules = module_implements('mollom_form_info');
+  $placeholders = db_placeholders($modules, 'varchar');
+  $result = db_query("SELECT name, info FROM {system} WHERE type = 'module' AND name IN ($placeholders)", $modules);
+  $modules = array();
+  while ($row = db_fetch_object($result)) {
+    $module_info = unserialize($row->info);
+    $modules[$row->name] = t($module_info['name']);
+  }
+
+  // Transform form information into an associative array suitable for #options.
+  foreach ($form_info as $form_id => $info) {
+    $form_info[$form_id] = $modules[$info['module']] . ': ' . $info['title'];
+  }
+  // Sort form options by title.
+  asort($form_info);
+
+  return $form_info;
+}
+
+/**
+ * Form builder; Configure Mollom protection for a form.
+ */
+function mollom_admin_configure_form(&$form_state, $mollom_form = NULL) {
+  // If no $mollom_form was passed, then we are adding a new form configuration.
+  if (!isset($mollom_form)) {
+    if (!isset($form_state['storage']['mollom_form'])) {
+      $form_state['storage']['step'] = 'select';
+    }
+    else {
+      $form_state['storage']['step'] = 'configure';
+      $mollom_form = $form_state['storage']['mollom_form'];
+    }
+  }
+  // Otherwise, we are editing an existing form configuration.
+  else {
+    $form_state['storage']['step'] = 'configure';
+    $form_state['storage']['mollom_form'] = $mollom_form;
+  }
+
+  $form['#tree'] = TRUE;
+
+  switch ($form_state['storage']['step']) {
+    case 'select':
+      drupal_add_js(drupal_get_path('module', 'mollom') . '/mollom.js');
+
+      $form['mollom']['form_id'] = array(
+        '#type' => 'select',
+        '#title' => t('Form'),
+        '#options' => mollom_admin_form_options(),
+        '#required' => TRUE,
+      );
+      $form['actions']['next'] = array(
+        '#type' => 'submit',
+        '#value' => t('Next'),
+        '#submit' => array('mollom_admin_configure_form_next_submit'),
+      );
+      break;
+
+    case 'configure':
+      // Display a list of fields for textual analysis (last step).
+      $form['mollom']['form_id'] = array(
+        '#type' => 'value',
+        '#value' => $mollom_form['form_id'],
+      );
+      $form['mollom']['form_title'] = array(
+        '#type' => 'item',
+        '#title' => t('Form'),
+        '#value' => $mollom_form['title'],
+      );
+      $form['mollom']['fields'] = array(
+        '#type' => 'checkboxes',
+        '#title' => t('Fields to analyze'),
+        '#options' => $mollom_form['elements'],
+        '#default_value' => $mollom_form['fields'],
+        '#description' => t('If no fields are selected, the form will be protected by a CAPTCHA.'),
+      );
+      if (empty($form['mollom']['fields']['#options'])) {
+        $form['mollom']['fields']['#description'] = t('No fields are available.');
+      }
+      $form['actions']['submit'] = array(
+        '#type' => 'submit',
+        '#value' => t('Save'),
+      );
+      break;
+  }
+
+  $form['actions']['cancel'] = array(
+    '#value' => l(t('Cancel'), 'admin/settings/mollom'),
+  );
+
+  return $form;
+}
+
+/**
+ * Form submit handler for 'Next' button on Mollom form configuration form.
+ */
+function mollom_admin_configure_form_next_submit($form, &$form_state) {
+  $mollom_form = $form_state['values']['mollom'];
+
+  // Load form information into $form_state for configuration.
+  $form_state['storage']['mollom_form'] = mollom_form_load($mollom_form['form_id']);
+
+  $form_state['storage']['step'] = 'configure';
+}
+
+/**
+ * Form submit handler for Mollom form configuration form.
+ */
+function mollom_admin_configure_form_submit($form, &$form_state) {
+  $mollom_form = $form_state['values']['mollom'];
+  // Merge in form information from $form_state.
+  $mollom_form += $form_state['storage']['mollom_form'];
+  // Update form information in $form_state for potential rebuilds.
+  $form_state['storage']['mollom_form'] = $mollom_form;
+
+  // Prepare selected fields for storage.
+  $mollom_form['fields'] = array_keys(array_filter($mollom_form['fields']));
+  // Determine form protection to use; use CAPTCHA-only protection if no fields
+  // were selected, otherwise text analysis.
+  $mollom_form['mode'] = (!empty($mollom_form['fields']) ? MOLLOM_MODE_ANALYSIS : MOLLOM_MODE_CAPTCHA);
+
+  $status = mollom_form_save($mollom_form);
+  if ($status === SAVED_NEW) {
+    drupal_set_message('The form protection has been added.');
+  }
+  else {
+    drupal_set_message('The form protection has been updated.');
+  }
+
+  unset($form_state['storage']);
+  $form_state['redirect'] = 'admin/settings/mollom';
+}
+
+/**
+ * Form builder; Remove Mollom protection from a form.
+ */
+function mollom_admin_unprotect_form(&$form_state, $mollom_form) {
+  $form['#tree'] = TRUE;
+  $form['form'] = array(
+    '#type' => 'item',
+    '#title' => t('Form'),
+    '#value' => $mollom_form['title'],
+  );
+  $form['mollom']['form_id'] = array(
+    '#type' => 'value',
+    '#value' => $mollom_form['form_id'],
+  );
+
+  return confirm_form($form,
+    t('Are you sure you want to unprotect this form?'),
+    'admin/settings/mollom',
+    t('Mollom will no longer protect this form from spam.')
+  );
+}
+
+/**
+ * Form submit handler for mollom_admin_unprotect_form().
+ */
+function mollom_admin_unprotect_form_submit($form, &$form_state) {
+  db_query("DELETE FROM {mollom_form} WHERE form_id = '%s'", $form_state['values']['mollom']['form_id']);
+
+  $form_state['redirect'] = 'admin/settings/mollom';
+}
+
+/**
+ * Form builder; Global Mollom settings form.
  */
 function mollom_admin_settings() {
   $keys = _mollom_access();
+  // Since keys are not verified in HTTP POST requests, we need to default to
+  // a successful key status. Otherwise, the fallback form elements would not
+  // be accessible and therefore no value would be stored.
+  $status = $keys;
 
   if ($keys) {
     if (!$_POST) {
@@ -24,72 +241,30 @@ function mollom_admin_settings() {
       // Verify the key and output a status message.
       // @todo This must be a form #validate handler; the form must not be
       //   submitted if validation fails.
-      _mollom_verify_key();
+      $status = _mollom_verify_key();
     }
-
-    $description = '<p>' . t("Mollom can be used to block all types of spam received on your website's protected forms. Each form can be set to one of the following options:") . '</p>';
-    $description .= '<ul><li>' . t("<strong>Text analysis and CAPTCHA backup</strong>: Mollom analyzes the data submitted on the form and presents a CAPTCHA challenge if necessary. This option is strongly recommended, as it takes full advantage of the Mollom anti-spam service to categorize your posts into ham (not spam) and spam.") . '</li>';
-    $description .= '<li>' . t("<strong>CAPTCHA only</strong>: the form's data is not sent to Mollom for analysis, and a remotely-hosted CAPTCHA challenge is always presented. This option is useful when you wish to always display a CAPTCHA or want to send less data to the Mollom network. Note, however, that forms displayed with a CAPTCHA are never cached, so always displaying a CAPTCHA challenge may reduce performance.") . '</li>';
-    $description .= '<li>' . t('<strong>No protection</strong>: Mollom is not used with this form.') . '</li></ul>';
-    $description .= '<p>';
-    $description .= t("Data is processsed and stored as explained in our <a href=\"@mollom-privacy\">Web Service Privacy Policy</a>. It is your responsibility to provide any necessary notices and obtain the appropriate consent regarding Mollom's use of your data. For more information, see <a href=\"@mollom-works\">How Mollom Works</a> and the <a href=\"@mollom-faq\">Mollom FAQ</a>.", array(
-      '@mollom-privacy' => 'http://mollom.com/service-agreement-free-subscriptions',
-      '@mollom-works' => 'http://mollom.com/how-mollom-works',
-      '@mollom-faq' => 'http://mollom.com/faq'));
-    $description .= '</p>';
-
-    $form['spam'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Spam protection settings'),
-      '#description' => $description,
-      '#collapsible' => TRUE,
-    );
-
-    $modes = array(
-      MOLLOM_MODE_DISABLED => t('No protection'),
-      MOLLOM_MODE_CAPTCHA => t('CAPTCHA only'),
-      MOLLOM_MODE_ANALYSIS => t('Text analysis and CAPTCHA backup'),
-    );
-
-    $forms = _mollom_protectable_forms();
-    foreach ($forms as $form_id => $details) {
-      $mode = _mollom_get_mode($form_id);
-      // @todo Use #tree instead.
-      $name = 'mollom_' . $form_id;
-
-      // @todo What's happening here?
-      $options = array_slice($modes, 0, $details['mode'] + 1);
-
-      $form['spam'][$name] = array(
-        '#type' => 'select',
-        '#title' => t('Protect @name', array('@name' => $details['name'])),
-        '#options' => $options,
-        '#default_value' => $mode,
-      );
-    }
-
-    $form['server'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Server settings'),
-      '#collapsible' => TRUE,
-      '#collapsed' => $keys,
-    );
-    $form['server']['mollom_fallback'] = array(
-      '#type' => 'radios',
-      '#title' => t('Fallback strategy'),
-      // Default to treating everything as inappropriate.
-      '#default_value' => variable_get('mollom_fallback', MOLLOM_FALLBACK_BLOCK),
-      '#options' => array(
-        MOLLOM_FALLBACK_BLOCK => t('Block all submissions of protected forms until the server problems are resolved'),
-        MOLLOM_FALLBACK_ACCEPT => t('Leave all forms unprotected and accept all submissions'),
-      ),
-      '#description' => t('When the Mollom servers are down or otherwise unreachable, no text analysis is performed and no CAPTCHAs are generated. If this occurs, your Drupal site will use the configured fallback strategy, and will either accept all submissions without spam checking, or block all submissions until the server or connection problems are resolved. Subscribers to <a href="@pricing">Mollom Plus</a> receive access to <a href="@sla">Mollom\'s high-availability backend infrastructure</a>, not available to free users, reducing potential downtime.', array(
-        '@pricing' => 'http://mollom.com/pricing',
-        '@sla' => 'http://mollom.com/standard-service-level-agreement',
-      )),
-    );
   }
 
+  $form['server'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Server settings'),
+    '#access' => $status,
+  );
+  $form['server']['mollom_fallback'] = array(
+    '#type' => 'radios',
+    '#title' => t('Fallback strategy'),
+    // Default to treating everything as inappropriate.
+    '#default_value' => variable_get('mollom_fallback', MOLLOM_FALLBACK_BLOCK),
+    '#options' => array(
+      MOLLOM_FALLBACK_BLOCK => t('Block all submissions of protected forms until the server problems are resolved'),
+      MOLLOM_FALLBACK_ACCEPT => t('Leave all forms unprotected and accept all submissions'),
+    ),
+    '#description' => t('When the Mollom servers are down or otherwise unreachable, no text analysis is performed and no CAPTCHAs are generated. If this occurs, your Drupal site will use the configured fallback strategy, and will either accept all submissions without spam checking, or block all submissions until the server or connection problems are resolved. Subscribers to <a href="@pricing">Mollom Plus</a> receive access to <a href="@sla">Mollom\'s high-availability backend infrastructure</a>, not available to free users, reducing potential downtime.', array(
+      '@pricing' => 'http://mollom.com/pricing',
+      '@sla' => 'http://mollom.com/standard-service-level-agreement',
+    )),
+  );
+
   $form['access-keys'] = array(
     '#type' => 'fieldset',
     '#title' => t('Mollom access keys'),
@@ -99,7 +274,7 @@ function mollom_admin_settings() {
       '@mollom-manager-add' => 'http://mollom.com/site-manager/add',
       '@mollom-manager' => 'http://mollom.com/site-manager',
     )),
-    '#collapsible' => TRUE,
+    '#collapsible' => $keys,
     '#collapsed' => $keys,
   );
   $form['access-keys']['mollom_public_key'] = array(
@@ -128,12 +303,15 @@ function _mollom_verify_key() {
 
   if ($status === NETWORK_ERROR) {
     drupal_set_message(t('We tried to contact the Mollom servers but we encountered a network error. Please make sure that your web server can make outgoing HTTP requests.'), 'error');
+    return FALSE;
   }
   elseif ($status === MOLLOM_ERROR) {
     drupal_set_message(t('We contacted the Mollom servers to verify your keys: your keys do not exist or are no longer valid. Please visit the <em>Manage sites</em> page on the Mollom website again: <a href="@mollom-user">@mollom-user</a>.', array('@mollom-user' => 'http://mollom.com/user')), 'error');
+    return FALSE;
   }
   else {
     drupal_set_message(t('We contacted the Mollom servers to verify your keys: the Mollom services are operating correctly. We are now blocking spam.'));
+    return TRUE;
   }
 }
 
Index: mollom.api.php
===================================================================
RCS file: mollom.api.php
diff -N mollom.api.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ mollom.api.php	3 Dec 2009 21:41:21 -0000
@@ -0,0 +1,108 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * API documentation for Mollom module.
+ */
+
+/**
+ * Return information about forms that can be protected by Mollom.
+ *
+ * Mollom invokes this hook for all modules to gather information about forms
+ * that can be protected. Only forms that have been registered via this hook are
+ * configurable in Mollom's administration interface.
+ *
+ * @return
+ *   An associative array containing information about forms that can be
+ *   protected. Each key is a $form_id whose value is an associative array:
+ *   - title: The human-readable name of the form.
+ *   - mode: (optional) The default protection mode for the form, which can be
+ *     one of:
+ *     - MOLLOM_MODE_DISABLED: No protection.
+ *     - MOLLOM_MODE_CAPTCHA: CAPTCHA-only protection.
+ *     - MOLLOM_MODE_ANALYSIS: Text analysis of submitted form values with
+ *       fallback to CAPTCHA.
+ *     If omitted, the form will not be configured upon installation of Mollom
+ *     module.
+ *   - bypass access: (optional) A list of user permissions to check for the
+ *     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.
+ *   - elements: (optional) An associative array of elements in the form that
+ *     can be configured for Mollom's text analysis. The site administrator can
+ *     only select the form elements to process (and exclude certain elements)
+ *     when a form registers elements. Each key is a form API element #parents
+ *     string representation of the location of an element in the form. For
+ *     example, a key of "myelement" denotes a form element value on the
+ *     top-level of submitted form values. For nested elements, a key of
+ *     "parent][child" denotes that the value of 'child' is found below 'parent'
+ *     in the submitted form values. Each value contains the form element label.
+ *     If omitted, Mollom can only provide a CAPTCHA protection for the form.
+ *   - mapping: (optional) An associative array to explicitly map form elements
+ *     (that have been specified in 'elements') to the data structure that is
+ *     sent to Mollom for validation. The submitted form values of all mapped
+ *     elements are not used for the post's body, so Mollom can validate certain
+ *     values individually (such as the author's e-mail address). None of the
+ *     mappings are required, but most implementations most likely want to at
+ *     least denote the form element that contains the title of a post.
+ *     The following mappings are possible:
+ *     - post_title: The form element value that should be used as title.
+ *     - post_body: Mollom automatically assigns this property based on all
+ *       elements that have been selected for textual analysis in Mollom's
+ *       administrative form configuration.
+ *     - author_name: The form element value that should be used as author name.
+ *     - author_mail: The form element value that should be used as the author's
+ *       e-mail address.
+ *     - author_url: The form element value that should be used as the author's
+ *       homepage.
+ *     - author_id: The form element value that should be used as the author's
+ *       user uid.
+ *     - author_openid: Mollom automatically assigns this property based on
+ *       'author_id', if no explicit form element value mapping was specified.
+ *     - author_ip: Mollom automatically assigns the user's IP address if no
+ *       explicit form element value mapping was specified.
+ */
+function hook_mollom_form_info() {
+  // Mymodule's comment form.
+  $forms['mymodule_comment_form'] = array(
+    'title' => t('Comment form'),
+    'mode' => MOLLOM_MODE_ANALYSIS,
+    'bypass access' => array('administer comments'),
+    'elements' => array(
+      'subject' => t('Subject'),
+      'body' => t('Body'),
+    ),
+    'mapping' => array(
+      'post_title' => 'subject',
+      'author_name' => 'name',
+      'author_mail' => 'mail',
+      'author_url' => 'homepage',
+    ),
+  );
+  // Mymodule's user registration form.
+  $forms['mymodule_user_register'] = array(
+    'title' => t('User registration form'),
+    'mode' => MOLLOM_MODE_CAPTCHA,
+    'mapping' => array(
+      'author_name' => 'name',
+      'author_mail' => 'mail',
+    ),
+  );
+
+  return $forms;
+}
+
+/**
+ * Alter registered information about forms that can be protected by Mollom.
+ *
+ * @param &$form_info
+ *   An associative array containing protectable forms. See
+ *   hook_mollom_form_info() for details.
+ */
+function hook_mollom_form_info_alter(&$form_info) {
+  if (isset($form_info['comment_form'])) {
+    $form_info['comment_form']['elements']['mymodule_field'] = t('My additional field');
+  }
+}
+
Index: mollom.form.inc
===================================================================
RCS file: mollom.form.inc
diff -N mollom.form.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ mollom.form.inc	3 Dec 2009 21:42:53 -0000
@@ -0,0 +1,157 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Implements hook_mollom_form_info() on behalf of other modules.
+ */
+
+/**
+ * Implements hook_mollom_form_info().
+ */
+function comment_mollom_form_info() {
+  $forms['comment_form'] = array(
+    'title' => t('Comment form'),
+    'mode' => MOLLOM_MODE_ANALYSIS,
+    'bypass access' => array('administer comments'),
+    'elements' => array(
+      'subject' => t('Subject'),
+      'comment' => t('Comment'),
+    ),
+    'mapping' => array(
+      'post_title' => 'subject',
+      // @todo comment_form() dynamically uses different form elements for
+      //   anonymous and authenticated users. This makes it impossible for
+      //   Mollom to define a valid mapping. Drupal core sucks again?
+      'author_name' => 'name',
+      'author_mail' => 'mail',
+      'author_url' => 'homepage',
+    ),
+  );
+
+  return $forms;
+}
+
+/**
+ * Implements hook_mollom_form_info().
+ */
+function contact_mollom_form_info() {
+  $forms['contact_mail_page'] = array(
+    'title' => t('Site-wide contact form'),
+    'mode' => MOLLOM_MODE_ANALYSIS,
+    'bypass access' => array('administer site-wide contact form'),
+    'elements' => array(
+      'subject' => t('Subject'),
+      'message' => t('Message'),
+    ),
+    'mapping' => array(
+      'post_title' => 'subject',
+      'author_name' => 'name',
+      'author_mail' => 'mail',
+    ),
+  );
+  $forms['contact_mail_user'] = array(
+    'title' => t('User contact form'),
+    'mode' => MOLLOM_MODE_ANALYSIS,
+    'bypass access' => array('administer users'),
+    'elements' => array(
+      'subject' => t('Subject'),
+      'message' => t('Message'),
+    ),
+    'mapping' => array(
+      'post_title' => 'subject',
+      'author_name' => 'name',
+      'author_mail' => 'mail',
+    ),
+  );
+
+  return $forms;
+}
+
+/**
+ * Implements hook_mollom_form_info().
+ */
+function node_mollom_form_info() {
+  $types = node_get_types('types');
+  $forms = array();
+  foreach ($types as $type) {
+    $form_id = $type->type . '_node_form';
+    $forms[$form_id] = array(
+      'title' => t('@name form', array('@name' => $type->name)),
+      'mode' => MOLLOM_MODE_ANALYSIS,
+      'bypass access' => array('administer nodes', 'edit any ' . $type->type . ' content'),
+      'elements' => array(),
+      'mapping' => array(
+        'author_name' => 'name',
+      ),
+    );
+    // @see node_content_form()
+    if ($type->has_title) {
+      $forms[$form_id]['elements']['title'] = check_plain($type->title_label);
+      $forms[$form_id]['mapping']['title'] = 'title';
+    }
+    if ($type->has_body) {
+      $forms[$form_id]['elements']['body'] = check_plain($type->body_label);
+      // @todo Consider optional, dependent 'format' support in D7.
+      // $forms[$form_id]['elements']['format'] = t('Input format'),
+    }
+    // Add CCK fields by default.
+    if (module_exists('content')) {
+      $content_info = content_types($type->type);
+      foreach ($content_info['fields'] as $field_name => $field) {
+        // We only consider text fields for text analysis.
+        if ($field['type'] == 'text') {
+          $forms[$form_id]['elements'][$field_name] = check_plain(t($field['widget']['label']));
+        }
+      }
+    }
+  }
+
+  return $forms;
+}
+
+/**
+ * Implements hook_mollom_form_info().
+ */
+function user_mollom_form_info() {
+  $forms['user_register'] = array(
+    'title' => t('User registration form'),
+    'mode' => MOLLOM_MODE_CAPTCHA,
+    'mapping' => array(
+      'author_name' => 'name',
+      'author_mail' => 'mail',
+    ),
+  );
+  $forms['user_pass'] = array(
+    'title' => t('User password request form'),
+    'mode' => MOLLOM_MODE_CAPTCHA,
+    'mapping' => array(
+      'author_name' => 'name',
+      'author_mail' => 'name',
+    ),
+  );
+
+  return $forms;
+}
+
+/**
+ * Implements hook_mollom_form_info().
+ *
+ * @todo Move this into Webform module.
+ */
+function webform_mollom_form_info() {
+  // @todo Install Webform and figure out data structures.
+  $forms = array();
+  return $forms;
+  $webforms = db_query("SELECT n.nid, n.title FROM {node} n WHERE n.type = 'webform' AND n.status = 1");
+  while ($webform = db_fetch_object($webforms)) {
+    $forms['webform_client_form_'. $webform->nid] = array(
+      'title' => $webform->title,
+      'mode' => MOLLOM_MODE_ANALYSIS,
+      'elements' => array(),
+    );
+  }
+
+  return $forms;
+}
+
Index: mollom.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.install,v
retrieving revision 1.2.2.11
diff -u -p -r1.2.2.11 mollom.install
--- mollom.install	17 Nov 2009 17:01:49 -0000	1.2.2.11
+++ mollom.install	25 Nov 2009 13:13:37 -0000
@@ -53,6 +53,38 @@ function mollom_schema() {
     'primary key' => array('did'),
   );
 
+  $schema['mollom_form'] = array(
+    'description' => 'Stores configuration for forms protected by Mollom.',
+    'fields' => array(
+      'form_id' => array(
+        'description' => 'The $form_id of the form being protected.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'mode' => array(
+        'description' => 'The configured protection mode to use for the form.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'fields' => array(
+        'type' => 'text',
+        'serialize' => TRUE,
+      ),
+      'module' => array(
+        'description' => 'The module name the $form_id belongs to.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+    ),
+    'primary key' => array('form_id'),
+  );
+
   $schema['cache_mollom'] = drupal_get_schema_unprocessed('system', 'cache');
   $schema['cache_mollom']['description'] = 'Cache table for the Mollom module to store information for forms it protects.';
 
@@ -64,6 +96,26 @@ function mollom_schema() {
  */
 function mollom_install() {
   drupal_install_schema('mollom');
+
+  // Install default form configuration for enabled, supported modules.
+  foreach (module_list(FALSE, FALSE) as $module) {
+    drupal_load('module', $module);
+  }
+  drupal_load('module', 'mollom');
+
+  $form_info = mollom_get_form_info();
+  foreach ($form_info as $form_id => $info) {
+    if (!empty($info['mode'])) {
+      $info['fields'] = array_keys($info['elements']);
+      // Upon installation, our own schema is not available yet, so we cannot
+      // use mollom_form_save(), resp. drupal_write_record().
+      db_query("INSERT INTO {mollom_form} (form_id, mode, fields) VALUES ('%s', %d, '%s')", array(
+        $info['form_id'],
+        $info['mode'],
+        serialize($info['fields']),
+      ));
+    }
+  }
 }
 
 /**
@@ -97,14 +149,18 @@ function mollom_update_2() {
  * Upgrade form protection storage.
  */
 function mollom_update_3() {
-  // Load the Drupal module so that _mollom_protectable_forms() is available.
+  // Load mollom_get_form_info() and hook_mollom_form_info() implementations.
+  foreach (module_list(FALSE, FALSE) as $module) {
+    drupal_load('module', $module);
+  }
   drupal_load('module', 'mollom');
 
-  foreach (_mollom_protectable_forms() as $form_id => $details) {
+  foreach (mollom_get_form_info() as $form_id => $info) {
     $name = 'mollom_' . $form_id;
     $mode = variable_get($name, NULL);
-    if ($mode && $details['mode'] == MOLLOM_MODE_ANALYSIS) {
-      // $mode was stored as 1, default to MOLLOM_MODE_ANALYSIS if the form supports it.
+    // $mode was stored as 1; default to MOLLOM_MODE_ANALYSIS if the form
+    // supports it.
+    if (isset($mode) && $info['mode'] == MOLLOM_MODE_ANALYSIS) {
       variable_set($name, MOLLOM_MODE_ANALYSIS);
     }
   }
@@ -120,3 +176,61 @@ function mollom_update_4() {
   return $ret;
 }
 
+/**
+ * Add the {mollom_form} table.
+ */
+function mollom_update_6105() {
+  $ret = array();
+  $schema = array(
+    'fields' => array(
+      'form_id' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'mode' => array(
+        'description' => 'The configured protection mode to use for the form.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'fields' => array(
+        'type' => 'text',
+        'serialize' => TRUE,
+      ),
+      'module' => array(
+        'description' => 'The module name the $form_id belongs to.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+    ),
+    'primary key' => array('form_id'),
+  );
+  db_create_table($ret, 'mollom_form', $schema);
+
+  // Migrate form configuration for enabled, supported modules.
+  foreach (module_list(FALSE, FALSE) as $module) {
+    drupal_load('module', $module);
+  }
+  drupal_load('module', 'mollom');
+
+  $form_info = mollom_get_form_info();
+  $result = db_query("SELECT name, value FROM {variable} WHERE name LIKE 'mollom_%%' AND name NOT IN ('mollom_servers', 'mollom_fallback', 'mollom_public_key', 'mollom_private_key')");
+  while ($row = db_fetch_object($result)) {
+    $form_id = substr($row->name, 7);
+    $mode = unserialize($row->value);
+    if (!empty($mode) && isset($form_info[$form_id])) {
+      $info = $form_info[$form_id];
+      $info['mode'] = $mode;
+      $info['fields'] = array_keys($info['elements']);
+      mollom_form_save($info);
+    }
+    variable_del($row->name);
+  }
+  return $ret;
+}
+
Index: mollom.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/mollom.module,v
retrieving revision 1.2.2.101
diff -u -p -r1.2.2.101 mollom.module
--- mollom.module	17 Nov 2009 23:39:49 -0000	1.2.2.101
+++ mollom.module	3 Dec 2009 21:50:36 -0000
@@ -5,7 +5,10 @@
  * @file
  * Main Mollom integration module functions.
  *
+ * @todo Increase module weight to catch form alterations by other modules.
  * @todo Remove usage of global $mollom_response variable.
+ * @todo D7: Implement hook_modules_uninstalled() to remove obsolete records in
+ *   {mollom_form}.
  */
 
 /**
@@ -91,12 +94,34 @@ define('MOLLOM_REDIRECT', 1200);
  */
 function mollom_help($path, $arg) {
   if ($path == 'admin/settings/mollom') {
-    return t("Allowing users to react, participate and contribute while still keeping your site's content under control can be a huge challenge. Mollom is a web service that helps you identify content quality and, more importantly, helps you stop spam. When content moderation becomes easier, you have more time and energy to interact with your web community. More information about Mollom is available on the <a href=\"@mollom-website\">Mollom website</a> or in the <a href=\"@mollom-faq\">Mollom FAQ</a>.",
+    return t('All listed forms below are protected by Mollom. You can <a href="@add-form-url">add a form</a> to protect, configure already protected forms, or remove the protection.', array(
+      '@add-form-url' => url('admin/settings/mollom/add'),
+    ));
+  }
+  if ($path == 'admin/help#mollom') {
+    $output = '<p>';
+    $output = t("Allowing users to react, participate and contribute while still keeping your site's content under control can be a huge challenge. Mollom is a web service that helps you identify content quality and, more importantly, helps you stop spam. When content moderation becomes easier, you have more time and energy to interact with your web community. More information about Mollom is available on the <a href=\"@mollom-website\">Mollom website</a> or in the <a href=\"@mollom-faq\">Mollom FAQ</a>.",
       array(
         '@mollom-website' => 'http://mollom.com',
         '@mollom-faq' => 'http://mollom.com/faq',
       )
     );
+    $output .= '</p><p>';
+    $output .= t("Mollom can be used to block all types of spam received on your website's protected forms. Each form can be set to one of the following options:");
+    $output .= '</p><ul><li>';
+    $output .= t("<strong>Text analysis and CAPTCHA backup</strong>: Mollom analyzes the data submitted on the form and presents a CAPTCHA challenge if necessary. This option is strongly recommended, as it takes full advantage of the Mollom anti-spam service to categorize your posts into ham (not spam) and spam.");
+    $output .= '</li><li>';
+    $output .= t("<strong>CAPTCHA only</strong>: the form's data is not sent to Mollom for analysis, and a remotely-hosted CAPTCHA challenge is always presented. This option is useful when you wish to always display a CAPTCHA or want to send less data to the Mollom network. Note, however, that forms displayed with a CAPTCHA are never cached, so always displaying a CAPTCHA challenge may reduce performance.");
+    $output .= '</li><li>';
+    $output .= t('<strong>No protection</strong>: Mollom is not used with this form.');
+    $output .= '</li></ul><p>';
+    $output .= t("Data is processsed and stored as explained in our <a href=\"@mollom-privacy\">Web Service Privacy Policy</a>. It is your responsibility to provide any necessary notices and obtain the appropriate consent regarding Mollom's use of your data. For more information, see <a href=\"@mollom-works\">How Mollom Works</a> and the <a href=\"@mollom-faq\">Mollom FAQ</a>.", array(
+      '@mollom-privacy' => 'http://mollom.com/service-agreement-free-subscriptions',
+      '@mollom-works' => 'http://mollom.com/how-mollom-works',
+      '@mollom-faq' => 'http://mollom.com/faq')
+    );
+    $output .= '</p>';
+    return $output;
   }
 }
 
@@ -155,16 +180,54 @@ function mollom_menu() {
     'file' => 'mollom.pages.inc',
     'type' => MENU_CALLBACK,
   );
+
   $items['admin/settings/mollom'] = array(
     'title' => 'Mollom',
     'description' => 'Mollom is a web service that helps you manage your community.',
+    'page callback' => 'mollom_admin_form_list',
+    'access arguments' => array('administer mollom'),
+    'file' => 'mollom.admin.inc',
+  );
+  $items['admin/settings/mollom/list'] = array(
+    'title' => 'List',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items['admin/settings/mollom/add'] = array(
+    'title' => 'Add form',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('mollom_admin_configure_form'),
+    'access arguments' => array('administer mollom'),
+    'type' => MENU_LOCAL_TASK,
+    'file' => 'mollom.admin.inc',
+  );
+  $items['admin/settings/mollom/manage/%mollom_form'] = array(
+    'title' => 'Configure',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('mollom_admin_configure_form', 4),
+    'access arguments' => array('administer mollom'),
+    'file' => 'mollom.admin.inc',
+  );
+  $items['admin/settings/mollom/unprotect/%mollom_form'] = array(
+    'title' => 'Unprotect form',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('mollom_admin_unprotect_form', 4),
+    'access arguments' => array('administer mollom'),
+    'type' => MENU_CALLBACK,
+    'file' => 'mollom.admin.inc',
+  );
+  $items['admin/settings/mollom/settings'] = array(
+    'title' => 'Settings',
+    'description' => 'Configure Mollom keys and global settings.',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('mollom_admin_settings'),
     'access arguments' => array('administer mollom'),
+    'type' => MENU_LOCAL_TASK,
     'file' => 'mollom.admin.inc',
   );
+
   $items['admin/reports/mollom'] = array(
-    'title' => 'Mollom',
+    'title' => 'Mollom statistics',
     'description' => 'Reports and usage statistics for the Mollom module.',
     'page callback' => 'mollom_reports_page',
     'access callback' => '_mollom_access',
@@ -247,7 +310,11 @@ function mollom_set_data($did, $data) {
     $data['languages'] = implode(' ', $data['languages']);
   }
   // Merge in some default values that may have not been in the response.
-  $data += array('reputation' => '', 'languages' => '');
+  $data += array(
+    'languages' => '',
+    'quality' => '',
+    'reputation' => '',
+  );
 
   // @todo Convert to drupal_write_record().
   if (db_result(db_query_range("SELECT 1 FROM {mollom} WHERE did = '%s'", $did, 0, 1))) {
@@ -305,11 +372,17 @@ function mollom_comment($comment, $op) {
  * This function intercepts all forms in Drupal and Mollom-enables them if
  * necessary.
  */
-function mollom_form_alter(&$form, $form_state, $form_id) {
+function mollom_form_alter(&$form, &$form_state, $form_id) {
   // Site administrators don't have their content checked with Mollom.
   if (!user_access('post with no checking')) {
-    // Retrieve the mode of protection required for this form.
-    if ($mode = _mollom_get_mode($form_id)) {
+    // Retrieve configuration for this form.
+    if ($mollom_form = mollom_form_load($form_id)) {
+      // Determine whether to bypass validation for the current user.
+      foreach ($mollom_form['bypass access'] as $permission) {
+        if (user_access($permission)) {
+          return;
+        }
+      }
       // Compute the weight of the CAPTCHA so we can position it in the form.
       $weight = 99999;
       foreach (element_children($form) as $key) {
@@ -329,15 +402,46 @@ function mollom_form_alter(&$form, $form
           $weight = min($weight, $form[$key]['#weight'] - 0.0001);
         }
       }
-      // Add Mollom form protection widget.
+      // Add Mollom form widget.
       $form['mollom'] = array(
         '#type' => 'mollom',
-        '#mode' => $mode,
+        '#mollom_form' => $mollom_form,
         '#weight' => $weight,
       );
-      // Add a submit handler that will clean the Mollom state as soon as the
-      // form is successfully submitted.
-      $form['#submit'][] = 'mollom_clean_state';
+      // Add Mollom form validation handlers.
+      $form['#validate'][] = 'mollom_validate_analysis';
+      $form['#validate'][] = 'mollom_validate_captcha';
+
+      // Add a submit handler to remove form state storage.
+      $form['#submit'][] = 'mollom_form_submit';
+    }
+  }
+}
+
+/**
+ * Implements hook_form_FORMID_alter().
+ */
+function mollom_form_comment_form_alter(&$form, &$form_state) {
+  // When editing existing comments, comment_form() defines different form
+  // elements, so our form element mapping will fail.
+  // @see comment_mollom_form_info()
+  if (!empty($form['cid']['#value'])) {
+    $form['#validate'][] = 'mollom_comment_form_validate';
+  }
+}
+
+/**
+ * Form validation handler for comment_form().
+ */
+function mollom_comment_form_validate($form, &$form_state) {
+  if (!form_get_errors()) {
+    // Author is a registered user; editing as comment moderator.
+    if (isset($form_state['values']['author']) && !isset($form_state['values']['name'])) {
+      // Populate 'name'.
+      form_set_value(array('#parents' => array('name')), $form_state['values']['author'], $form_state);
+      // Populate 'mail'.
+      $account = user_load(array('name' => $form_state['values']['author']));
+      form_set_value(array('#parents' => array('mail')), $account->mail, $form_state);
     }
   }
 }
@@ -373,156 +477,221 @@ function mollom_form_node_admin_content_
 }
 
 /**
- * Helper function to prepare XML-RPC data from a contact form submission.
- *
- * @see mollom_validate()
- */
-function _mollom_data_contact_mail($form_state) {
-  global $user;
-
-  $data = array(
-    'post_title'    => isset($form_state['subject']) ? $form_state['subject'] : NULL,
-    'post_body'     => isset($form_state['message']) ? $form_state['message'] : NULL,
-    'author_name'   => isset($form_state['name']) ? $form_state['name'] : (isset($user->name) ? $user->name : NULL),
-    'author_mail'   => isset($form_state['mail']) ? $form_state['mail'] : (isset($user->mail) ? $user->mail : NULL),
-    'author_openid' => $user->uid ? _mollom_get_openid($user) : NULL,
-    'author_id'     => $user->uid ? $user->uid : NULL,
-    'author_ip'     => ip_address(),
-  );
-
-  return $data;
-}
-
-/**
- * Prepare XML-RPC data from a contact form submission.
- *
- * @see mollom_validate()
- */
-function mollom_data_contact_mail_page($form_state) {
-  return _mollom_data_contact_mail($form_state);
-}
-
-/**
- * Prepare XML-RPC data from a contact form submission.
+ * Return the protection mode for a given form id.
  *
- * @see mollom_validate()
+ * @return
+ *   The protection mode for the given form id, one of:
+ *   - MOLLOM_MODE_DISABLED: None.
+ *   - MOLLOM_MODE_CAPTCHA: CAPTCHA only.
+ *   - MOLLOM_MODE_ANALYSIS: Text analysis with CAPTCHA fallback.
  */
-function mollom_data_contact_mail_user($form_state) {
-  return _mollom_data_contact_mail($form_state);
+function _mollom_get_mode($form_id) {
+  $mollom_form = mollom_form_load($form_id);
+  return isset($mollom_form['mode']) ? $mollom_form['mode'] : MOLLOM_MODE_DISABLED;
 }
 
 /**
- * Prepare XML-RPC data from a comment form submission.
+ * Returns information about protectable forms registered via hook_mollom_form_info().
  *
- * @see mollom_validate()
- */
-function mollom_data_comment_form($form_state) {
-  global $user;
+ * @param $form_id
+ *   (optional) The form id to return information for. If omitted, information
+ *   for all registered forms is returned.
+ */
+function mollom_get_form_info($form_id = NULL) {
+  static $form_info;
+
+  if (!isset($form_info)) {
+    module_load_include('inc', 'mollom', 'mollom.form');
+    $form_info = array();
+    foreach (module_implements('mollom_form_info') as $module) {
+      $function = $module . '_mollom_form_info';
+      $module_forms = $function();
+      if (isset($module_forms) && is_array($module_forms)) {
+        foreach ($module_forms as $id => $info) {
+          // Ensure basic properties for all forms.
+          $module_forms[$id] += array(
+            'form_id' => $id,
+            'module' => $module,
+            'title' => $id,
+            'mode' => NULL,
+            'bypass access' => array(),
+            'elements' => array(),
+            'mapping' => array(),
+          );
+        }
+        $form_info = array_merge_recursive($form_info, $module_forms);
+      }
+    }
 
-  $data = array(
-    'post_title'    => isset($form_state['subject']) ? $form_state['subject'] : NULL,
-    'post_body'     => isset($form_state['comment']) ? $form_state['comment'] : NULL,
-    'author_name'   => isset($form_state['name']) ? $form_state['name'] : (isset($user->name) ? $user->name : NULL),
-    'author_mail'   => isset($form_state['mail']) ? $form_state['mail'] : (isset($user->mail) ? $user->mail : NULL),
-    'author_url'    => isset($form_state['homepage']) ? $form_state['homepage'] : NULL,
-    'author_openid' => $user->uid ? _mollom_get_openid($user) : NULL,
-    'author_id'     => $user->uid ? $user->uid : NULL,
-    'author_ip'     => isset($form_state['cid']) ? NULL : ip_address(),
-  );
+    // Allow modules to alter the default form information.
+    drupal_alter('mollom_form_info', $form_info);
+  }
 
-  return $data;
+  if (isset($form_id)) {
+    return isset($form_info[$form_id]) ? $form_info[$form_id] : array();
+  }
+  return $form_info;
 }
 
 /**
- * Prepare XML-RPC data from a node form submission.
- *
- * @see mollom_validate()
+ * Menu argument loader; Loads Mollom configuration and form information for a given form id.
  */
-function mollom_data_node_form($form_state) {
-  global $user;
+function mollom_form_load($form_id) {
+  $mollom_form = db_fetch_array(db_query_range("SELECT * FROM {mollom_form} WHERE form_id = '%s'", $form_id, 0, 1));
+  if ($mollom_form) {
+    $mollom_form['fields'] = unserialize($mollom_form['fields']);
 
-  // Render the node so that all visible fields are prepared and
-  // concatenated:
-  $data = node_build_content((object)$form_state, FALSE, FALSE);
-  $content = drupal_render($data->content);
-
-  $data = array(
-    'post_title'    => isset($form_state['title']) ? $form_state['title'] : NULL,
-    'post_body'     => $content,
-    'author_name'   => isset($form_state['name']) ? $form_state['name'] : (isset($user->name) ? $user->name : NULL),
-    'author_mail'   => isset($form_state['mail']) ? $form_state['mail'] : (isset($user->mail) ? $user->mail : NULL),
-    'author_url'    => isset($form_state['homepage']) ? $form_state['homepage'] : NULL,
-    'author_openid' => $user->uid ? _mollom_get_openid($user) : NULL,
-    'author_id'     => $user->uid ? $user->uid : NULL,
-    'author_ip'     => isset($form_state['nid']) ? NULL : ip_address(),
-  );
+    // Attach form registry information.
+    $mollom_form += mollom_get_form_info($form_id);
 
-  return $data;
+    // Ensure default values (partially for administrative configuration).
+    $mollom_form += array(
+      'form_id' => $form_id,
+      'title' => $form_id,
+      'elements' => array(),
+    );
+  }
+  return $mollom_form;
 }
 
 /**
- * Return the protection mode for a given form id.
- *
- * @return
- *   The protection mode for the given form id, one of:
- *   - MOLLOM_MODE_DISABLED: None.
- *   - MOLLOM_MODE_CAPTCHA: CAPTCHA only.
- *   - MOLLOM_MODE_ANALYSIS: Text analysis with CAPTCHA fallback.
- *
- * @todo Store form ids in a single variable.
+ * Saves a Mollom form configuration.
  */
-function _mollom_get_mode($form_id) {
-  $mode = variable_get('mollom_' . $form_id, NULL);
-  if (!isset($mode)) {
-    $forms = _mollom_protectable_forms();
-    return isset($forms[$form_id]['mode']) ? $forms[$form_id]['mode'] : MOLLOM_MODE_DISABLED;
+function mollom_form_save(&$mollom_form) {
+  $exists = db_result(db_query_range("SELECT 1 FROM {mollom_form} WHERE form_id = '%s'", $mollom_form['form_id'], 0, 1));
+  if ($exists) {
+    $status = drupal_write_record('mollom_form', $mollom_form, 'form_id');
   }
-  return $mode;
+  else {
+    $status = drupal_write_record('mollom_form', $mollom_form);
+  }
+  // Allow modules to react on saved form configurations.
+  if (isset($status) && $status) {
+    if ($status === SAVED_NEW) {
+      module_invoke_all('mollom_form_insert', $mollom_form);
+    }
+    elseif ($status === SAVED_UPDATED) {
+      module_invoke_all('mollom_form_update', $mollom_form);
+    }
+  }
+  return $status;
 }
 
 /**
- * Returns a list of forms that can be protected with Mollom.
+ * Given an array of values and an array of fields, extract data for use.
+ *
+ * This function generates the data to send for validation to Mollom by walking
+ * through the submitted form values and
+ * - copying element values as specified via 'mapping' in hook_mollom_form_info()
+ *   into the dedicated data properties
+ * - collecting and concatenating all fields that have been selected for textual
+ *   analysis into the 'post_body' property
+ *
+ * The processing accounts for the following possibilities:
+ * - A field was selected for textual analysis, but there is no submitted form
+ *   value. The value should have been appended to the 'post_body' property, but
+ *   will be skipped.
+ * - A field is contained in the 'mapping' and there is a submitted form value.
+ *   The value will not be appended to the 'post_body', but instead be assigned
+ *   to the specified data property.
+ * - All fields specified in 'mapping', for which there is a submitted value,
+ *   but which were NOT selected for textual analysis, are assigned to the
+ *   specified data property. This is usually the case for form elements that
+ *   hold system user information.
+ *
+ * @param $values
+ *   An array containing submitted form values, usually $form_state['values'].
+ * @param $fields
+ *   A list of strings representing form elements to extract. Nested fields are
+ *   in the form of 'parent][child'.
+ * @param $mapping
+ *   An associative array of form elements to map to Mollom's dedicated data
+ *   properties. See hook_mollom_form_info() for details.
+ *
+ * @see hook_mollom_form_info()
  */
-function _mollom_protectable_forms() {
-  static $forms = NULL;
-
-  if (!isset($forms)) {
-    if (module_exists('comment')) {
-      $forms['comment_form'] = array(
-        'name' => 'comment form',
-        'mode' => MOLLOM_MODE_ANALYSIS,
-      );
-    }
-    if (module_exists('contact')) {
-      $forms['contact_mail_page'] = array(
-        'name' => 'site-wide contact form',
-        'mode' => MOLLOM_MODE_ANALYSIS,
-      );
+function mollom_form_get_values($form_values, $fields, $mapping) {
+  global $user;
 
-      $forms['contact_mail_user'] = array(
-        'name' => 'per-user contact forms',
-        'mode' => MOLLOM_MODE_ANALYSIS,
-      );
+  // All elements specified in $mapping must be excluded from $fields, as they
+  // are used for dedicated $data properties instead. To reduce the parsing code
+  // size, we are turning a given $mapping of f.e.
+  //   array('post_title' => 'title_form_element')
+  // into
+  //   array('title_form_element' => 'post_title')
+  // and we reset $mapping afterwards.
+  // When iterating over the $fields, this allows us to quickly test whether the
+  // current field should be excluded, and if it should, we directly get the
+  // mapped property name to rebuild $mapping with the field values.
+  $exclude_fields = array();
+  if (!empty($mapping)) {
+    $exclude_fields = array_flip($mapping);
+  }
+  $mapping = array();
+
+  // Process all fields that have been selected for text analysis.
+  $post_body = array();
+  foreach ($fields as $field) {
+    // Nested elements use a key of 'parent][child', so we need to recurse.
+    $parents = explode('][', $field);
+    $value = $form_values;
+    foreach ($parents as $key) {
+      $value = isset($value[$key]) ? $value[$key] : NULL;
+    }
+    // If this field was contained in $mapping and should be excluded, add it to
+    // $mapping with the actual form element value, and continue to the next
+    // field. Also unset this field from $exclude_fields, so we can process the
+    // remaining mappings below.
+    if (isset($exclude_fields[$field])) {
+      $mapping[$exclude_fields[$field]] = $value;
+      unset($exclude_fields[$field]);
+      continue;
+    }
+    // Only add form element values that are not empty.
+    if (isset($value) && strlen($value)) {
+      $post_body[$field] = $value;
+    }
+  }
+  $post_body = implode("\n", $post_body);
+
+  // Try to assign any further form values by processing the remaining mappings,
+  // which have been turned into $exclude_fields above. All fields that were
+  // already used for 'post_body' no longer exist in $exclude_fields.
+  foreach ($exclude_fields as $field => $property) {
+    // Nested elements use a key of 'parent][child', so we need to recurse.
+    $parents = explode('][', $field);
+    $value = $form_values;
+    foreach ($parents as $key) {
+      $value = isset($value[$key]) ? $value[$key] : NULL;
     }
-    $forms['user_register'] = array(
-      'name' => 'user registration form',
-      'mode' => MOLLOM_MODE_CAPTCHA,
-    );
-    $forms['user_pass'] = array(
-      'name' => 'user password request form',
-      'mode' => MOLLOM_MODE_CAPTCHA,
-    );
-    // Add all node forms.
-    $types = node_get_types('names');
-    foreach ($types as $type => $name) {
-      $forms[$type . '_node_form'] = array(
-        'name' => drupal_strtolower($name) . ' form',
-        'mode' => MOLLOM_MODE_ANALYSIS,
-      );
+    if (isset($value)) {
+      $mapping[$property] = $value;
     }
   }
 
-  return $forms;
+  $data = array();
+  // Add the post body.
+  $data += array(
+    'post_body' => !empty($post_body) ? $post_body : NULL,
+  );
+  // Add data of mapped fields.
+  $data += $mapping;
+
+  // Add user data.
+  $data += array(
+    'author_name' => isset($user->name) ? $user->name : NULL,
+    'author_mail' => isset($user->mail) ? $user->mail : NULL,
+    'author_url' => NULL,
+    'author_id' => $user->uid ? $user->uid : NULL,
+    'author_ip' => ip_address(),
+  );
+  if (isset($data['author_id'])) {
+    // Automatically inherit OpenID identifiers if we have a user id.
+    $account = user_load(array('uid' => $data['author_id']));
+    $data['author_openid'] = _mollom_get_openid($account);
+  }
+
+  return $data;
 }
 
 /**
@@ -561,6 +730,58 @@ function _mollom_fallback() {
 }
 
 /**
+ * @defgroup mollom_form_api Mollom Form API workarounds
+ * @{
+ * Various helper functions to work around bugs in Form API.
+ *
+ * Normally, Mollom's integration with Form API would be quite simple:
+ * - If a form is protected by Mollom, we setup initial information
+ *   about the session and the form in $form_state['storage'], bound to the
+ *   'form_build_id'.
+ * - We enable form caching via $form_state['cache'], so our information in the
+ *   form storage is cached. Form API then automatically ensures a proper
+ *   'form_build_id' for every form and every user.
+ * - We mainly work in and after form validation. Textual analysis validates all
+ *   values in the form as a form validation handler. If this validation fails,
+ *   we alter the form (during validation) to add a CAPTCHA. If the CAPTCHA
+ *   response is invalid, we still alter the form during validation to display a
+ *   new CAPTCHA, but without the previously entered value.
+ * - In short, roughly:
+ *   - Form construction: Nothing.
+ *   - Form processing: Nothing.
+ *   - Form validation: Perform validation and alterations based on validation.
+ *
+ * This, however, is not possible due to various bugs in Drupal core.
+ * - Form caching cannot be enabled for certain forms, because they contain
+ *   processing and validation logic.
+ *   http://drupal.org/node/644222
+ * - $form_state['storage'] is not updated after form processing and validation.
+ *   http://drupal.org/node/644150
+ * - Form validation handlers cannot alter the form structure.
+ *   http://drupal.org/node/642702
+ *
+ * Hence, something that could be done in one simple function becomes quite a
+ * nightmare:
+ * - We need our own {cache_mollom} table as replacement for native form
+ *   caching, as well as our own logic to validate a submitted 'session_id'
+ *   ('form_build_id') against forms and users.
+ * - We need to perform form alterations during form rendering, where
+ *   $form_state is no longer available. To make this possible, we leverage the
+ *   fact that an element property that is a reference to a key in $form_state
+ *   (which in itself is passed by reference) persists on to the rendering
+ *   layer. The essential part is:
+ *   @code
+ *     $element['#mollom'] = &$form_state['mollom'];
+ *   @endcode
+ * - Since we cannot alter elements in the form structure during form
+ *   validation, this reference already needs to be set up during form
+ *   processing (in a #process callback), while everything else lives in form
+ *   validation handlers (unless it needs to add or alter the form structure).
+ *
+ * @see mollom_form_alter()
+ */
+
+/**
  * Implements hook_elements().
  */
 function mollom_elements() {
@@ -568,46 +789,122 @@ function mollom_elements() {
     'mollom' => array(
       '#input' => TRUE,
       '#process' => array(
-        'mollom_expand_element',
+        // Try to fetch a Mollom session from cache during form processing/validation.
+        'mollom_process_mollom_session_id',
+        // Setup a new Mollom session.
+        'mollom_process_mollom',
       ),
+      '#pre_render' => array('mollom_pre_render_mollom'),
+    ),
+  );
+}
+
+/**
+ * Implements hook_theme().
+ */
+function mollom_theme() {
+  return array(
+    'mollom' => array(
+      'arguments' => array('element' => NULL),
     ),
   );
 }
 
 /**
- * Form element process handler to expand the 'mollom' element.
+ * Format the Mollom form element.
  *
- * The mollom form element is stateful. The Mollom session ID that is exchanged
+ * This works like #type 'markup' and is only required, because D6 only supports
+ * #process callbacks on elements with #input = TRUE.
+ * @see form_builder()
+ * @see _form_builder_handle_input_element()
+ *
+ * @todo Remove in D7; also #input from mollom_elements().
+ */
+function theme_mollom($element) {
+  return isset($element['#children']) ? $element['#children'] : '';
+}
+
+/**
+ * Form element #process callback for the 'mollom' element.
+ *
+ * The 'mollom' form element is stateful. The Mollom session ID that is exchanged
  * between Drupal, the Mollom back-end, and the user allows us to keep track of
  * the form validation state.
  * The session ID is valid for a specific user session and a given $form_id
  * only. We expire it as soon as the form is submitted, to avoid it being
  * replayed.
- *
- * @return
- *   The mollom element expanded with a captcha if necessary.
- *
- * @todo Needs major overhaul. This function invokes form validation callbacks
- *   (that are no real form validation handlers) at the wrong time (during form
- *   building). Additionally, it tries to cache values in $form_state, but
- *   $form_state itself is not cached. A proper re-implementation could perhaps
- *   even eliminate {cache_mollom}.
  */
-function mollom_expand_element($element, $edit, &$form_state, $form) {
+function mollom_process_mollom($element, $input, &$form_state, $complete_form) {
   $element['#tree'] = TRUE;
 
+  // Setup initial Mollom session and form information.
+  if (empty($form_state['mollom'])) {
+    $form_state['mollom'] = array(
+      'form_build_id' => $complete_form['#build_id'],
+      'session_id' => NULL,
+      'form_id' => $element['#mollom_form']['form_id'],
+      'require_analysis' => $element['#mollom_form']['mode'] == MOLLOM_MODE_ANALYSIS,
+      'require_captcha' => $element['#mollom_form']['mode'] == MOLLOM_MODE_CAPTCHA,
+      'passed_captcha' => FALSE,
+      'user_session_id' => session_id(),
+      'fields' => $element['#mollom_form']['fields'],
+      'mapping' => $element['#mollom_form']['mapping'],
+    );
+  }
+
+  // Add the Mollom session element.
+  $element['session_id'] = array(
+    '#type' => 'hidden',
+    '#default_value' => '',
+    '#attributes' => array('class' => 'mollom-session-id'),
+  );
+
+  // Add the CAPTCHA element.
+  $element['captcha'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Word verification'),
+    // @todo Or simply move into #pre_render, 'cause this is cosmetical only.
+    '#required' => $form_state['mollom']['require_captcha'],
+    '#size' => 10,
+    '#default_value' => '',
+    '#description' => t("Type the characters you see in the picture above; if you can't read them, submit the form and a new image will be generated."),
+  );
+
+  // Add the JavaScript to switch to an audio CAPTCHA.
+  drupal_add_js(drupal_get_path('module', 'mollom') . '/mollom.js');
+  drupal_add_css(drupal_get_path('module', 'mollom') . '/mollom.css');
+
+  // Make Mollom form and session information available to #pre_render callback.
+  // This must be assigned by reference. It is the essential "communication
+  // layer" between form API and the rendering system. Any modifications to
+  // $form_state['mollom'] will be carried over to the element for rendering.
+  $element['#mollom'] = &$form_state['mollom'];
+
+  return $element;
+}
+
+/**
+ * Form element #process callback for Mollom's form storage handling.
+ *
+ * Albeit this *should* be an #element_validate handler that is only executed
+ * during form validation, we must use a #process callback, because
+ * mollom_process_mollom() needs to copy over $form_state['mollom'] into
+ * $element['#mollom'], and as of now, Form API does not allow form validation
+ * handlers to alter any elements in the form structure by reference.
+ * @see http://drupal.org/node/642702
+ *
+ * @todo Investigate usage of $form_state['storage']['mollom'], but watch out
+ *   for broken form state caching.
+ */
+function mollom_process_mollom_session_id($element, $input, &$form_state, $complete_form) {
   // The current state can come either from the $form_state, if the form
-  // was just rebuilt in the same request...
-  if (!empty($form_state['mollom'])) {
-    $mollom_state = $form_state['mollom'];
-  }
-  // ... or from data posted by the user. In that case we validate that the correct
-  // form_id and user session ID is used...
-  elseif (!empty($edit['session_id'])) {
-    list($timestamp, $mollom_session_id) = explode('-', $edit['session_id'], 2);
+  // was just rebuilt in the same request or from data posted by the user. In
+  // that case we validate that the correct form_id and user session ID is used.
+  if (empty($form_state['mollom']) && !empty($input['session_id'])) {
+    list($timestamp, $mollom_session_id) = explode('-', $input['session_id'], 2);
 
     if (!$mollom_session_id) {
-      watchdog('mollom', "Mollom ID '%session' is badly formatted.", array('%session' => $edit['session_id']));
+      watchdog('mollom', "Mollom ID '%session' is badly formatted.", array('%session' => $input['session_id']));
     }
     elseif (!$cache = cache_get($mollom_session_id, 'cache_mollom')) {
       if (time() - $timestamp > 30 * 60) {
@@ -617,255 +914,200 @@ function mollom_expand_element($element,
         watchdog('mollom', "Mollom ID '%session' is invalid, probably reused.", array('%session' => $mollom_session_id));
       }
     }
-    elseif ($cache->data['#form_id'] !== $form_state['values']['form_id']) {
-      watchdog('mollom', "Mollom ID '%session' has been tampered with: it was generated for form %form1, but is used on form %form2.", array('%session' => $mollom_session_id, '%form1' => $cache->data['#form_id'], '%form2' => $form_state['values']['form_id']));
+    elseif ($cache->data['form_id'] !== $form_state['values']['form_id']) {
+      watchdog('mollom', "Mollom ID '%session' has been tampered with: it was generated for form %form1, but is used on form %form2.", array('%session' => $mollom_session_id, '%form1' => $cache->data['form_id'], '%form2' => $form_state['values']['form_id']));
     }
-    elseif ($cache->data['#user_session_id'] !== session_id()) {
-      watchdog('mollom', "Mollom ID '%session' has been tampered with: it was generated for a user with Drupal session ID %sid1, but is used by a user with Drupal session ID %sid2.", array('%session' => $mollom_session_id, '%sid1' => $cache->data['#user_session_id'], '%sid2' => session_id()));
+    elseif ($cache->data['user_session_id'] !== session_id()) {
+      watchdog('mollom', "Mollom ID '%session' has been tampered with: it was generated for a user with Drupal session ID %sid1, but is used by a user with Drupal session ID %sid2.", array('%session' => $mollom_session_id, '%sid1' => $cache->data['user_session_id'], '%sid2' => session_id()));
     }
     else {
-      $mollom_state = $cache->data;
+      $form_state['mollom'] = $cache->data;
     }
   }
+  return $element;
+}
 
-  // ... finally, if no valid state has been found, we generate an empty one.
-  if (empty($mollom_state)) {
-    $mollom_state = array(
-      '#session_id' => NULL,
-      '#form_id' => $form_state['values']['form_id'],
-      '#require_analysis' => $element['#mode'] == MOLLOM_MODE_ANALYSIS,
-      '#require_captcha' => $element['#mode'] == MOLLOM_MODE_CAPTCHA,
-      '#passed_captcha' => FALSE,
-      '#user_session_id' => session_id(),
-    );
+/**
+ * Form validation handler to perform textual analysis of submitted form values.
+ *
+ * Validation needs to re-run in case of a form validation error (elsewhere in
+ * the form). In case Mollom's textual analysis returns no definite result, we
+ * must fall back to a CAPTCHA.
+ */
+function mollom_validate_analysis($form, &$form_state) {
+  if (!$form_state['mollom']['require_analysis'] || $form_state['mollom']['require_captcha']) {
+    return;
   }
 
-  // If this form can be submitted by drupal_process_form(), we process it
-  // through our validation handlers.
-  if (!empty($form['#programmed']) || (!empty($form['#post']) && (isset($form['#post']['form_id']) && ($form['#post']['form_id'] == $form['form_id']['#value'])))) {
-    _mollom_debug("mollom_expand_element: submitted handler");
-
-    // First, perform captcha validation if required.
-    if (!empty($mollom_state['#require_captcha']) && empty($mollom_state['#passed_captcha'])) {
-      mollom_validate_captcha($mollom_state, $form_state, $edit);
-    }
-
-    // Then, perform text analysis if required.
-    if (!empty($mollom_state['#require_analysis'])) {
-      mollom_validate_analysis($mollom_state, $form_state, $edit);
-    }
+  $data = mollom_form_get_values($form_state['values'], $form_state['mollom']['fields'], $form_state['mollom']['mapping']);
+  if (!empty($form_state['mollom']['session_id'])) {
+    $data += array('session_id' => $form_state['mollom']['session_id']);
   }
+  $result = mollom('mollom.checkContent', $data);
 
-  if (!empty($mollom_state['#require_captcha']) && empty($mollom_state['#passed_captcha'])) {
-    _mollom_debug('mollom_form_alter registered mollom_validate_captcha handler');
-    _mollom_insert_captcha($mollom_state, $element);
-
-    // This prevents the Drupal page cache from storing the page when we
-    // generated a captcha or when the user already passed the captcha.
-    // This is not required for text analysis, because the above code will
-    // simply generate a new session if the cached one doesn't match the user.
-    // @todo Find a better way to do this in D7.
-    // @todo This potentially breaks other forms and modules that run after this
-    //   function.
-    $_SERVER['REQUEST_METHOD'] = 'POST';
-  }
-
-  if (!empty($mollom_state['#session_id'])) {
-    // We store the Mollom session only if something useful was done.
-    // We save it in two places: as an hidden form field and in the cache
-    // so that it persists form submission, and in $form_state so that it
-    // persists form rebuilds.
-    $element['session_id'] = array(
-      '#type' => 'hidden',
-      '#value' => time() . '-' . $mollom_state['#session_id'],
-      '#attributes' => array('class' => 'mollom-session-id'),
-    );
-    cache_set($mollom_state['#session_id'], $mollom_state, 'cache_mollom', time() + 60 * 30);
-
-    $form_state['mollom'] = $mollom_state;
+  // Trigger global fallback behavior if there is no result.
+  if (!isset($result['session_id']) || !isset($result['spam'])) {
+    return _mollom_fallback();
   }
 
-  return $element;
-}
+  // Assign the session ID returned by Mollom.
+  $form_state['mollom']['session_id'] = $result['session_id'];
+  // Store the response for #submit handlers.
+  $GLOBALS['mollom_response'] = $result;
 
-/**
- * Clean the Mollom state as soon as the form has been submitted.
- */
-function mollom_clean_state($form_id, $form_state) {
-  if (!empty($form_state['values']['mollom']['session_id'])) {
-    cache_clear_all($form_state['values']['mollom']['session_id'], 'cache_mollom');
-  }
-}
+  switch ($result['spam']) {
+    case MOLLOM_ANALYSIS_HAM:
+      watchdog('mollom', 'Ham: %message<br />Session: %session', array('%message' => $data['post_body'], '%session' => $result['session_id']));
+      break;
 
-/**
- * Implements hook_theme().
- */
-function mollom_theme() {
-  return array(
-    'mollom' => array(
-      'arguments' => array('element' => NULL),
-    ),
-  );
-}
+    case MOLLOM_ANALYSIS_SPAM:
+      form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.'));
+      watchdog('mollom', 'Spam: %message<br />Session: %session', array('%message' => $data['post_body'], '%session' => $result['session_id']));
+      break;
 
-/**
- * Format the Mollom form element.
- */
-function theme_mollom($element) {
-  return isset($element['#children']) ? $element['#children'] : '';
+    default:
+      // Fall back to a CAPTCHA.
+      $form_state['mollom']['require_captcha'] = TRUE;
+
+      form_set_error('mollom', t("We're sorry, but the spam filter thinks your submission could be spam. Please complete the CAPTCHA."));
+      watchdog('mollom', 'Unsure: %message<br />Session: %session', array('%message' => $data['post_body'], '%session' => $result['session_id']));
+      break;
+  }
 }
 
 /**
- * Performs text analysis of form values and optionally sets a form validation error.
- *
- * @see mollom_expand_element()
+ * Form validation handler for CAPTCHA form element.
  */
-function mollom_validate_analysis(&$mollom_state, $form_state) {
-  _mollom_debug("mollom_validate_analysis for '" . $form_state['values']['form_id'] . "'");
-
-  $data = array();
-
-  $form_id = $form_state['values']['form_id'];
-
-  $pos = strpos($form_id, '_node_form');
-  if ($pos !== FALSE) {
-    // The node forms use dynamic form IDs so must use a special
-    // case for these.
-    $data = mollom_data_node_form($form_state['values']);
-  }
-  else {
-    $function = 'mollom_data_' . $form_id;
-    if (function_exists($function)) {
-      $data = $function($form_state['values']);
-    }
+function mollom_validate_captcha($element, &$form_state) {
+  if (!$form_state['mollom']['require_captcha'] || $form_state['mollom']['passed_captcha']) {
+    return;
   }
 
-  $mollom = !empty($mollom_state['#session_id']) ? array('session_id' => $mollom_state['#session_id']) : array();
-
-  $result = mollom('mollom.checkContent', $data + $mollom);
-
-  if (isset($result['session_id']) && isset($result['spam'])) {
-    _mollom_debug('mollom_validate_analysis retrieved spam status ' . $result['spam'] . " and session ID '" . $result['session_id'] . "'");
-
-    // Store the session ID that Mollom returned and make sure that it persists
-    // across page requests.
-    $mollom_state['#session_id'] = $result['session_id'];
+  // Bail out if no value was provided.
+  if (empty($form_state['values']['mollom']['captcha'])) {
+    form_set_error('mollom][captcha', t('The CAPTCHA field is required.'));
+    return;
+  }
 
-    // Check the spam results and act accordingly.
-    if ($result['spam'] == MOLLOM_ANALYSIS_HAM) {
-      // Keep track of the response so we can use it later to save the data in
-      // the database.
-      $GLOBALS['mollom_response'] = $result;
+  // Check the CAPTCHA result.
+  $result = mollom('mollom.checkCaptcha', array(
+    'session_id' => $form_state['mollom']['session_id'],
+    'captcha_result' => $form_state['values']['mollom']['captcha'],
+    'author_ip' => ip_address(),
+  ));
+
+  // Explictly check for TRUE, since mollom.checkCaptcha() can also return an
+  // error message (e.g. expired or invalid session_id).
+  if ($result === TRUE) {
+    $form_state['mollom']['passed_captcha'] = TRUE;
 
-      watchdog('mollom', 'Ham: %message<br />Session: %session', array('%message' => $data['post_body'], '%session' => $result['session_id']));
-    }
-    elseif ($result['spam'] == MOLLOM_ANALYSIS_SPAM) {
-      form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.'));
-      watchdog('mollom', 'Spam: %message<br />Session: %session', array('%message' => $data['post_body'], '%session' => $result['session_id']));
-    }
-    else {
-      if (empty($mollom_state['#passed_captcha'])) {
-        // A captcha will be generated.
-        form_set_error('mollom', t("We're sorry, but the spam filter thinks your submission could be spam. Please complete the CAPTCHA."));
-        watchdog('mollom', 'Unsure: %message<br />Session: %session', array('%message' => $data['post_body'], '%session' => $result['session_id']));
-        $mollom_state['#require_captcha'] = TRUE;
-      }
-    }
+    watchdog('mollom', 'The CAPTCHA response was correct. Form data: %data', array('%data' => print_r($form_state['values'], TRUE)));
   }
   else {
-    return _mollom_fallback();
+    form_set_error('mollom][captcha', t('The CAPTCHA was not completed correctly. Please complete this new CAPTCHA and try again.'));
+    watchdog('mollom', 'The CAPTCHA response was incorrect. Form data: %data', array('%data' => print_r($form_state['values'], TRUE)));
   }
 }
 
 /**
- * Validates the CAPTCHA form element value and optionally sets a form validation error.
+ * Form element #pre_render callback for CAPTCHA element.
+ *
+ * Conditionally alters the #type of the CAPTCHA form element into a 'hidden'
+ * element if the response was correct. If it was not, then we empty the value
+ * of the textfield to allow the user to re-enter a new one.
  *
- * @see mollom_expand_element()
+ * This #pre_render trick is required, because form API validation does not
+ * allow form validation handlers to alter the actual form structure. Both the
+ * form constructor function and the #process callback for the 'mollom' element
+ * are therefore executed too early (before form validation), so the CAPTCHA
+ * element still contains not yet validated (default) values.
+ * We also cannot invoke a form validation handler during form construction or
+ * processing, because mollom_form_get_values() would be invoked too early
+ * and therefore $form_state['values'] would not contain any additions from
+ * form validation functions like mollom_comment_form_validate().
+ * @see http://drupal.org/node/642702
  */
-function mollom_validate_captcha(&$mollom_state, $form_state, $edit) {
-  _mollom_debug("mollom_validate_captcha");
-
-  // Mollom cannot possibly verify the CAPTCHA if we don't have a decent
-  // session ID to start with.
-  /* if (empty($mollom_state['#session_id'])) {
-    watchdog('mollom', 'The user submitted a form protected by a CAPTCHA without a Mollom ID.');
-    form_set_error('mollom][captcha', t('The CAPTCHA was not completed correctly. Please complete this new CAPTCHA and try again.'));
+function mollom_pre_render_mollom($element) {
+  // Prevent the page cache from storing a form containing a CAPTCHA element.
+  if ($element['#mollom']['require_captcha']) {
+    $GLOBALS['conf']['cache'] = CACHE_DISABLED;
   }
-  else */
-  if (!empty($edit['captcha'])) {
-    // Check the CAPTCHA result.
-    $result = mollom('mollom.checkCaptcha', array(
-      'session_id' => $mollom_state['#session_id'],
-      'captcha_result' => $edit['captcha'],
-      'author_ip' => ip_address(),
-    ));
 
-    _mollom_debug('mollom_validate_captcha the captcha result was ' . (int)$result);
+  // Request and inject a CAPTCHA when required; but also in case validation
+  // through textual analysis failed.
+  if ($element['#mollom']['require_captcha'] && !$element['#mollom']['passed_captcha']) {
+    // Request a CAPTCHA; always default to an image CAPTCHA.
+    $data['author_ip'] = ip_address();
+    if (!empty($element['#mollom']['session_id'])) {
+      $data['session_id'] = $element['#mollom']['session_id'];
+    }
+    $result = mollom('mollom.getImageCaptcha', $data);
+
+    // If we get a response, add the image CAPTCHA to the form element.
+    if (isset($result['session_id']) && isset($result['url'])) {
+      $captcha = '<a href="http://mollom.com" class="mollom-captcha">';
+      $captcha .= '<img src="' . url($result['url']) . '" alt="Mollom CAPTCHA" />';
+      // @todo This suffix needs to be injected via JavaScript.
+      $captcha .= '</a> (<a href="#" class="mollom-audio-captcha">' . t('play audio CAPTCHA') . '</a>)';
+      $element['captcha']['#field_prefix'] = $captcha;
 
-    // Important: we explictly check for TRUE because mollom.checkCaptcha() can
-    // return FALSE as well as an error message (e.g. when the Mollom ID expired
-    // or when the Mollom ID is invalid).
-    if ($result === TRUE) {
-      watchdog('mollom', 'The CAPTCHA resonse was correct. Form data: %data', array('%data' => print_r($form_state['values'], TRUE)));
-      $mollom_state['#passed_captcha'] = TRUE;
+      // Assign the session ID returned by Mollom.
+      $element['#mollom']['session_id'] = $result['session_id'];
     }
-    else {
-      watchdog('mollom', 'The CAPTCHA response was incorrect. Form data: %data', array('%data' => print_r($form_state['values'], TRUE)));
-      form_set_error('mollom][captcha', t('The CAPTCHA was not completed correctly. Please complete this new CAPTCHA and try again.'));
+    // Otherwise, hide the CAPTCHA if there was a communication error.
+    elseif ($result === NETWORK_ERROR) {
+      $element['captcha']['#type'] = 'hidden';
     }
   }
+
+  // If we received a Mollom session id via textual analysis or a CAPTCHA
+  // request, inject it to the form.
+  if (!empty($element['#mollom']['session_id'])) {
+    $element['session_id']['#value'] = time() . '-' . $element['#mollom']['session_id'];
+  }
+
+  // If no CAPTCHA is required or the response was correct, hide the CAPTCHA.
+  if (!$element['#mollom']['require_captcha'] || $element['#mollom']['passed_captcha']) {
+    $element['captcha']['#type'] = 'hidden';
+  }
+  // Otherwise, empty the value of the CAPTCHA, since the user has to re-enter
+  // a new one.
   else {
-    form_set_error('mollom][captcha', t('The CAPTCHA field is required.'));
+    $element['captcha']['#value'] = '';
+  }
+
+  // Due to a bug in Drupal core, we need to manually update Mollom session
+  // information in $form_state.
+  // @see http://drupal.org/node/644150
+  // And due to yet even more bugs in Drupal core form constructor functions,
+  // some forms (such as comment_form()) cannot be cached, since they contain
+  // processing/validation logic. We therefore cannot use $form_state
+  // along with form caching. >:-/
+  // @see http://drupal.org/node/644222
+  if (!empty($element['#mollom']['session_id'])) {
+    cache_set($element['#mollom']['session_id'], $element['#mollom'], 'cache_mollom', time() + 21600);
   }
+
+  return $element;
 }
 
 /**
- * Form element process handler to expand an element into a CAPTCHA form element.
- *
- * @see mollom_expand_element()
+ * Form submit handler to flush Mollom session and form information from cache.
  */
-function _mollom_insert_captcha(&$mollom_state, &$element) {
-  _mollom_debug('_mollom_insert_captcha');
-
-  // Prepare the author's IP.
-  $data['author_ip'] = ip_address();
-
-  if (!empty($mollom_state['#session_id'])) {
-    $data['session_id'] = $mollom_state['#session_id'];
-  }
-
-  // Request a CAPTCHA; we always default to an image CAPTCHA.
-  $response = mollom('mollom.getImageCaptcha', $data);
-
-  if (isset($response['session_id']) && isset($response['url'])) {
-    _mollom_debug("_mollom_insert_captcha retrieved URL '" . $response['url'] . "' and session ID '" . $response['session_id'] . "'");
-
-    // Include the JavaScript allowing the user to switch to an
-    // AUDIO captcha instead.
-    drupal_add_js(drupal_get_path('module', 'mollom') . '/mollom.js');
-    drupal_add_css(drupal_get_path('module', 'mollom') . '/mollom.css');
-
-    // Add the CAPTCHA to the form.
-    $element['captcha'] = array(
-      '#type' => 'textfield',
-      '#processed' => TRUE,
-      '#title' => t('Word verification'),
-      '#field_prefix' => '<a href="http://mollom.com" class="mollom-captcha"><img src="' . url($response['url']) . '" alt="Mollom CAPTCHA" /></a> (<a href="#" class="mollom-audio-captcha">' . t('play audio CAPTCHA') . '</a>)',
-      '#required' => TRUE,
-      '#size' => 10,
-      // The previously entered value is useless because the captcha is
-      // regenerated at each form rebuild.
-      '#value' => '',
-      '#description' => t("Type the characters you see in the picture above; if you can't read them, submit the form and a new image will be generated."),
-    );
-
-    // Store the session ID that Mollom returned so that it persists across page
-    // requests.
-    $mollom_state['#session_id'] = $response['session_id'];
+function mollom_form_submit($form_id, &$form_state) {
+  // Flush Mollom session information from form state cache.
+  if (!empty($form_state['values']['mollom']['session_id'])) {
+    cache_clear_all($form_state['values']['mollom']['session_id'], 'cache_mollom');
   }
+  // Remove Mollom session information from form state to account for unforeseen rebuilds.
+  unset($form_state['mollom']);
 }
 
 /**
+ * @} End of "defgroup mollom_form_api".
+ */
+
+/**
  * Call a remote procedure at the Mollom server.
  *
  * This function automatically adds the information required to authenticate
@@ -970,11 +1212,20 @@ function mollom($method, $data = array()
 /**
  * Helper function to debug the form API workflow.
  *
- * Uncomment the function body to activate.
+ * Comment the return to enable.
  */
 function _mollom_debug($message) {
-  // print $message .'<br />';
-  // error_log($message);
+  return;
+  if (function_exists('debug')) {
+    debug($message);
+  }
+  else {
+    print $message .'<br />';
+    error_log($message);
+  }
+  // This is how it should work. 27/11/2009 sun
+//  echo "<pre>"; var_dump(__FUNCTION__); echo "</pre>\n";
+//  !isset($form_state['mollom']) || krumo($form_state['mollom']);
 }
 
 /**
Index: tests/mollom.test
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/mollom/tests/Attic/mollom.test,v
retrieving revision 1.1.2.20
diff -u -p -r1.1.2.20 mollom.test
--- tests/mollom.test	19 Nov 2009 16:48:59 -0000	1.1.2.20
+++ tests/mollom.test	3 Dec 2009 23:08:49 -0000
@@ -84,7 +84,12 @@ class MollomWebTestCase extends DrupalWe
     $modules[] = 'mollom';
     call_user_func_array(array('parent', 'setUp'), $modules);
 
-    $this->admin_user = $this->drupalCreateUser(array('administer mollom'));
+    $this->admin_user = $this->drupalCreateUser(array(
+      'administer mollom',
+      'access administration pages',
+      'administer content types',
+      'administer comments',
+    ));
     $this->setKeys();
     $this->assertValidKeys();
   }
@@ -127,17 +132,89 @@ class MollomWebTestCase extends DrupalWe
   }
 
   /**
+   * Configure Mollom protection for a given form.
+   *
+   * @param $form_id
+   *   The form id to configure.
+   * @param $fields
+   *   (optional) A list of form elements to enable for text analysis.
+   */
+  protected function setProtection($form_id, $fields = NULL) {
+    // Determine whether the form is already protected.
+    $exists = db_result(db_query_range("SELECT 1 FROM {mollom_form} WHERE form_id = '%s'", $form_id, 0, 1));
+    // Add a new form.
+    if (!$exists) {
+      $this->drupalGet('admin/settings/mollom');
+      $this->clickLink(t('Add form'));
+      $edit = array(
+        'mollom[form_id]' => $form_id,
+      );
+      $this->drupalPost(NULL, $edit, t('Next'));
+    }
+    // Edit an existing form.
+    else {
+      $this->drupalGet('admin/settings/mollom/manage/' . $form_id);
+    }
+
+    $edit = array();
+    // Explicitly enable the passed fields, if $fields were passed.
+    if (isset($fields)) {
+      foreach ($fields as $field) {
+        $edit['mollom[fields][' . $field . ']'] = TRUE;
+      }
+    }
+    $form_info = mollom_get_form_info($form_id);
+    foreach (array_keys($form_info['elements']) as $field) {
+      // Due to SimpleTest's form handling of checkboxes, we need to disable all
+      // remaining checkboxes manually.
+      if (isset($fields)) {
+        if (!isset($edit[$field])) {
+          $edit['mollom[fields][' . $field . ']'] = FALSE;
+        }
+      }
+      // If no $fields were passed, enable all elements exposed by the
+      // implementation.
+      else {
+        $edit['mollom[fields][' . $field . ']'] = TRUE;
+      }
+    }
+    $this->drupalPost(NULL, $edit, t('Save'));
+    if (!$exists) {
+      $this->assertText(t('The form protection has been added.'));
+    }
+    else {
+      $this->assertText(t('The form protection has been updated.'));
+    }
+  }
+
+  /**
+   * Remove Mollom protection for a given form.
+   *
+   * @param $form_id
+   *   The form id to configure.
+   */
+  protected function delProtection($form_id) {
+    // Determine whether the form is protected.
+    $exists = db_result(db_query_range("SELECT 1 FROM {mollom_form} WHERE form_id = '%s'", $form_id, 0, 1));
+    if ($exists) {
+      $this->drupalGet('admin/settings/mollom/unprotect/' . $form_id);
+      $this->assertText(t('Mollom will no longer protect this form from spam.'), t('Unprotect confirmation form found.'));
+      $this->drupalPost(NULL, array(), t('Confirm'));
+    }
+  }
+
+  /**
    * Test that the CAPTCHA field is found on the current page.
    */
   protected function assertCaptchaField() {
-    $this->assertFieldByID('edit-mollom-captcha', '', 'CAPTCHA field found on ' . $this->getUrl());
+    $this->assertFieldByXPath('//input[@type="text"][@name="mollom[captcha]"]', '', 'CAPTCHA field found.');
   }
 
   /**
    * Test that the CAPTCHA field is not found on the current page.
    */
   protected function assertNoCaptchaField() {
-    $this->assertNoFieldByID('edit-mollom-captcha', '', 'CAPTCHA field not found on ' . $this->getUrl());
+    $this->assertNoFieldByXPath('//input[@type="text"][@name="mollom[captcha]"]', '', 'CAPTCHA field not found.');
   }
 
   /**
@@ -279,6 +356,41 @@ class MollomWebTestCase extends DrupalWe
     }
     return $value;
   }
+
+  /**
+   * Retrieve submitted XML-RPC values from testing server implementation.
+   *
+   * @see mollom_test.module
+   */
+  protected function getServerRecord() {
+    $storage = variable_get('mollom_test_check_content', array());
+    $return = array_shift($storage);
+    variable_set('mollom_test_check_content', $storage);
+    return $return;
+  }
+
+  /**
+   * Helper function for assertEqual().
+   *
+   * Check to see if two values are equal. And provide a meaningful response.
+   *
+   * @param $name
+   *   A name or identifier to use in the assertion message.
+   * @param $first
+   *   The first value to check.
+   * @param $second
+   *   The second value to check.
+   * @return
+   *   TRUE if the assertion succeeded, FALSE otherwise.
+   */
+  function _assertEqual($name, $first, $second) {
+    $message = strtr("@name: '@first' is equal to '@second'.", array(
+      '@name' => $name,
+      '@first' => $first,
+      '@second' => $second,
+    ));
+    $this->assertEqual($first, $second, $message);
+  }
 }
 
 class MollomAccessTestCase extends MollomWebTestCase {
@@ -297,7 +409,7 @@ class MollomAccessTestCase extends Mollo
   function testKeyPairs() {
     // Check that a success message is shown.
     $this->drupalLogin($this->admin_user);
-    $this->drupalGet('admin/settings/mollom');
+    $this->drupalGet('admin/settings/mollom/settings');
     $this->assertText(t('We contacted the Mollom servers to verify your keys: the Mollom services are operating correctly. We are now blocking spam.'));
 
     // Set up invalid test keys and check that an error message is shown.
@@ -305,10 +417,10 @@ class MollomAccessTestCase extends Mollo
       'mollom_public_key' => 'invalid-public-key',
       'mollom_private_key' => 'invalid-private-key',
     );
-    $this->drupalPost('admin/settings/mollom', $edit, t('Save configuration'));
+    $this->drupalPost(NULL, $edit, t('Save configuration'));
     $this->assertText(t('The configuration options have been saved.'));
 
-    $this->drupalGet('admin/settings/mollom');
+    $this->drupalGet('admin/settings/mollom/settings');
     $this->assertRaw(t('"messages error"'), t('The Mollom settings page reports that the Mollom keys are invalid.'));
   }
 
@@ -326,13 +438,52 @@ class MollomAccessTestCase extends Mollo
     $this->assertResponse(200);
 
     // Check access for a user that has everything except the 'administer
-    // site configuration' permission. This user should not have access
-    // to the Mollom settings page.
+    // mollom' permission. This user should not have access to the Mollom
+    // settings page.
     $this->web_user = $this->drupalCreateUser(array_diff(module_invoke_all('perm'), array('administer mollom')));
     $this->drupalLogin($this->web_user);
     $this->drupalGet('admin/settings/mollom');
     $this->assertResponse(403);
   }
+
+  /**
+   * Tests 'bypass access' property of registered forms.
+   */
+  function testBypassAccess() {
+    $node = $this->drupalCreateNode(array('body' => 'node body', 'type' => 'story'));
+
+    // Create a regular user and post a comment.
+    $this->web_user = $this->drupalCreateUser(array('post comments', 'post comments without approval'));
+    $this->drupalLogin($this->web_user);
+    $edit = array(
+      'comment' => 'ham',
+    );
+    $this->drupalPost('comment/reply/' . $node->nid, $edit, t('Preview'));
+    $this->drupalPost(NULL, array(), t('Save'));
+    $this->assertText('node body');
+    $this->assertText($edit['comment']);
+
+    // Ensure a user having one of the permissions to bypass access can post
+    // spam without triggering the spam protection.
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet('node/' . $node->nid);
+    $this->clickLink('edit');
+    $this->drupalPost(NULL, array('subject' => '', 'comment' => 'spam'), t('Preview'));
+    $this->assertNoText($this->spam_message);
+    $this->drupalPost(NULL, array(), t('Save'));
+    $this->assertNoText($this->spam_message);
+    $this->assertText('node body');
+
+    // Log in back the regular user and try to edit the comment containing spam.
+    $this->drupalLogin($this->web_user);
+    $this->drupalGet('node/' . $node->nid);
+    $this->clickLink('edit');
+    $this->drupalPost(NULL, array(), t('Preview'));
+    $this->assertText($this->spam_message);
+    $this->drupalPost(NULL, array(), t('Save'));
+    $this->assertText($this->spam_message);
+    $this->assertNoText('node body');
+  }
 }
 
 class MollomFallbackTestCase extends MollomWebTestCase {
@@ -345,9 +496,8 @@ class MollomFallbackTestCase extends Mol
   }
 
   function setUp() {
-    parent::setUp();
-    // Enable Mollom for the request password form.
-    variable_set('mollom_user_pass', MOLLOM_MODE_CAPTCHA);
+    // Enable testing server implementation.
+    parent::setUp('mollom_test');
   }
 
   /**
@@ -355,9 +505,11 @@ class MollomFallbackTestCase extends Mol
    * the Mollom servers are unreachable.
    */
   function testFallbackMechanismBlock() {
-    // Set the fallback strategy to 'blocking mode'.
+    // Enable Mollom for the request password form.
     $this->drupalLogin($this->admin_user);
-    $this->drupalPost('admin/settings/mollom', array('mollom_fallback' => MOLLOM_FALLBACK_BLOCK), t('Save configuration'));
+    $this->setProtection('user_pass');
+    // Set the fallback strategy to 'blocking mode'.
+    $this->drupalPost('admin/settings/mollom/settings', array('mollom_fallback' => MOLLOM_FALLBACK_BLOCK), t('Save configuration'));
     $this->assertText('The configuration options have been saved.');
     $this->drupalLogout();
 
@@ -377,9 +529,11 @@ class MollomFallbackTestCase extends Mol
    * the Mollom servers are unreachable.
    */
   function testFallbackMechanismAccept() {
-    // Set the fallback strategy to 'accept mode'.
+    // Enable Mollom for the request password form.
     $this->drupalLogin($this->admin_user);
-    $this->drupalPost('admin/settings/mollom', array('mollom_fallback' => MOLLOM_FALLBACK_ACCEPT), t('Save configuration'));
+    $this->setProtection('user_pass');
+    // Set the fallback strategy to 'accept mode'.
+    $this->drupalPost('admin/settings/mollom/settings', array('mollom_fallback' => MOLLOM_FALLBACK_ACCEPT), t('Save configuration'));
     $this->assertText('The configuration options have been saved.');
     $this->drupalLogout();
 
@@ -397,7 +551,7 @@ class MollomFallbackTestCase extends Mol
    * Make sure that spam protection is still active even when some of the
    * Mollom servers are unavailable.
    *
-   * @todo @todo Test mail sending with assertMail() now that it is available.
+   * @todo Test mail sending with assertMail() now that it is available.
    */
   function testFailoverMechanism() {
     // Set the fallback strategy to 'blocking mode', so that if the failover
@@ -409,10 +563,10 @@ class MollomFallbackTestCase extends Mol
     variable_set('mollom_servers', array(
       'http://fake-host-1',
       'http://fake-host-2',
+      $GLOBALS['base_url'] . '/xmlrpc.php?version=',
       'http://xmlrpc1.mollom.com', // The real server.
       'http://fake-host-3',
-      )
-    );
+    ));
 
     // Validate that the request password form has a CAPTCHA text field and
     // that a user is not blocked from submitting it.
@@ -477,11 +631,13 @@ class MollomUserFormsTestCase extends Mo
   /**
    * Make sure that the request password form is protected correctly.
    *
-   * @todo @todo Test mail sending with assertMail() now that it is available.
+   * @todo Test mail sending with assertMail() now that it is available.
    */
   function testProtectRequestPassword() {
     // We first enable Mollom for the request password form.
-    variable_set('mollom_user_pass', MOLLOM_MODE_CAPTCHA);
+    $this->drupalLogin($this->admin_user);
+    $this->setProtection('user_pass');
+    $this->drupalLogout();
 
     // Create a new user.
     $this->web_user = $this->drupalCreateUser();
@@ -502,7 +658,9 @@ class MollomUserFormsTestCase extends Mo
    */
   function testProtectRegisterUser() {
     // We first enable Mollom for the user registration form.
-    variable_set('mollom_user_register', MOLLOM_MODE_CAPTCHA);
+    $this->drupalLogin($this->admin_user);
+    $this->setProtection('user_register');
+    $this->drupalLogout();
 
     // Validate that the user registration form has a CAPTCHA text field.
     $this->drupalGet('user/register');
@@ -543,7 +701,6 @@ class MollomCommentFormTestCase extends 
     $this->web_user = $this->drupalCreateUser(array('access comments', 'post comments', 'post comments without approval', 'create story content'));
     $this->node = $this->drupalCreateNode(array('type' => 'story', 'uid' => $this->web_user->uid));
     variable_set('comment_preview_story', COMMENT_PREVIEW_OPTIONAL);
-    $this->drupalLogin($this->web_user);
   }
 
   /**
@@ -551,9 +708,12 @@ class MollomCommentFormTestCase extends 
    */
   function testUnprotectedCommentForm() {
     // Disable Mollom for comments.
-    variable_set('mollom_comment_form', MOLLOM_MODE_DISABLED);
+    $this->drupalLogin($this->admin_user);
+    $this->delProtection('comment_form');
+    $this->drupalLogout();
 
     // Request the comment reply form. There should be no CAPTCHA.
+    $this->drupalLogin($this->web_user);
     $this->drupalGet('comment/reply/'. $this->node->nid);
     $this->assertNoCaptchaField();
 
@@ -563,7 +723,7 @@ class MollomCommentFormTestCase extends 
 
     // Save the comment and make sure it appears.
     $this->drupalPost(NULL, array(), t('Save'));
-    $this->assertRaw('<p>spam</p>', t('A comment that is known to be ham appears on the screen after it is submitted.'));
+    $this->assertRaw('<p>spam</p>', t('A comment that is known to be spam appears on the screen after it is submitted.'));
   }
 
   /**
@@ -571,9 +731,12 @@ class MollomCommentFormTestCase extends 
    */
   function testCaptchaProtectedCommentForm() {
     // Enable Mollom CAPTCHA protection for comments.
-    variable_set('mollom_comment_form', MOLLOM_MODE_CAPTCHA);
+    $this->drupalLogin($this->admin_user);
+    $this->setProtection('comment_form', array());
+    $this->drupalLogout();
 
     // Request the comment reply form. There should be a CAPTCHA form.
+    $this->drupalLogin($this->web_user);
     $this->drupalGet('comment/reply/'. $this->node->nid);
     $this->assertCaptchaField();
 
@@ -599,9 +762,12 @@ class MollomCommentFormTestCase extends 
    */
   function testTextAnalysisProtectedCommentForm() {
     // Enable Mollom text-classification for comments.
-    variable_set('mollom_comment_form', MOLLOM_MODE_ANALYSIS);
+    $this->drupalLogin($this->admin_user);
+    $this->setProtection('comment_form');
+    $this->drupalLogout();
 
     // Request the comment reply form.  Initially, there should be no CAPTCHA.
+    $this->drupalLogin($this->web_user);
     $this->drupalGet('comment/reply/'. $this->node->nid);
     $this->assertNoCaptchaField();
 
@@ -688,18 +854,20 @@ class MollomContactFormTestCase extends 
     parent::setUp('contact');
 
     $this->web_user = $this->drupalCreateUser(array('access site-wide contact form', 'access user profiles'));
-    $this->drupalLogin($this->web_user);
   }
 
   /**
    * Make sure that the user contact form is protected correctly.
    *
-   * @todo @todo Test mail sending with assertMail() now that it is available.
+   * @todo Test mail sending with assertMail() now that it is available.
    */
   function testProtectContactUserForm() {
     // Enable Mollom for the contact form.
-    variable_set('mollom_contact_mail_user', MOLLOM_MODE_ANALYSIS);
+    $this->drupalLogin($this->admin_user);
+    $this->setProtection('contact_mail_user');
+    $this->drupalLogout();
 
+    $this->drupalLogin($this->web_user);
     $url = 'user/' . $this->admin_user->uid . '/contact';
     $button = t('Send e-mail');
     $success = t('The message has been sent.');
@@ -720,13 +888,16 @@ class MollomContactFormTestCase extends 
   /**
    * Make sure that the site-wide contact form is protected correctly.
    *
-   * @todo @todo Test mail sending with assertMail() now that it is available.
+   * @todo Test mail sending with assertMail() now that it is available.
    */
   function testProtectContactSiteForm() {
     // Enable Mollom for the contact form.
-    variable_set('mollom_contact_mail_page', MOLLOM_MODE_ANALYSIS);
+    $this->drupalLogin($this->admin_user);
+    $this->setProtection('contact_mail_page');
+    $this->drupalLogout();
 
     // Add some fields to the contact form so that it is active.
+    $this->drupalLogin($this->web_user);
     db_query("INSERT INTO {contact} (category, recipients, reply) VALUES ('%s', '%s', '%s')", 'test category', $this->web_user->mail, 'test auto-reply');
 
     $url = 'contact';
@@ -810,3 +981,128 @@ class MollomResellerTestCase extends Mol
     $this->assertEqual(count($sites), 0, t('All Mollom sites have been deleted.'));
   }
 }
+
+/**
+ * Test form value processing.
+ */
+class MollomDataTestCase extends MollomWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Data processing',
+      'description' => 'Verify that form registry information is properly transformed into data that is sent to Mollom servers.',
+      'group' => 'Mollom',
+    );
+  }
+
+  function setUp() {
+    // Enable testing server implementation.
+    parent::setUp('mollom_test');
+    // Re-route Mollom communication to this testing site.
+    variable_set('mollom_servers', array($GLOBALS['base_url'] . '/xmlrpc.php?version='));
+  }
+
+  /**
+   * Test mollom_form_get_values().
+   */
+  function testFormGetValues() {
+    global $user;
+
+    // Form registry information.
+    $form_info = array(
+      'elements' => array(
+        'subject' => 'Subject',
+        'message' => 'Message',
+        'parent][child' => 'Some nested element',
+      ),
+      'mapping' => array(
+        'post_title' => 'subject',
+        'author_name' => 'name',
+        'author_mail' => 'mail',
+      ),
+    );
+    // Fields configured via Mollom admin UI based on $form_info['elements'].
+    $fields = array(
+      'subject',
+      'message',
+      'parent][child',
+    );
+    // Submitted form values.
+    $values = array(
+      'subject' => 'Foo',
+      'message' => 'Bar',
+      'parent' => array(
+        'child' => 'Beer',
+      ),
+      'name' => 'Drupaler',
+    );
+    $data = mollom_form_get_values($values, $fields, $form_info['mapping']);
+
+    $this->_assertEqual('post_title', $data['post_title'], $values['subject']);
+    $this->_assertEqual('post_body', $data['post_body'], $values['message'] . "\n" . $values['parent']['child']);
+    $this->_assertEqual('author_name', $data['author_name'], $values['name']);
+    $this->_assertEqual('author_mail', $data['author_mail'], $user->mail);
+    $this->_assertEqual('author_url', $data['author_url'], NULL);
+    $this->_assertEqual('author_openid', $data['author_openid'], _mollom_get_openid($user));
+    $this->_assertEqual('author_id', $data['author_id'], $user->uid);
+    $this->_assertEqual('author_ip', $data['author_ip'], ip_address());
+  }
+
+  /**
+   * Test submitted post and author information.
+   */
+  function testAnalysis() {
+    // Verify that comment form is protected.
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet('admin/settings/mollom');
+    $this->assertText(t('Comment form'));
+    $this->drupalGet('admin/settings/mollom/manage/comment_form');
+
+    // Make comment preview optional.
+    $edit = array(
+      'comment_preview' => 0,
+    );
+    $this->drupalPost('admin/content/node-type/story', $edit, t('Save content type'));
+
+    // Create a node we can comment on.
+    $node = $this->drupalCreateNode(array('type' => 'story', 'promote' => 1));
+    $this->drupalGet('');
+    $this->assertText($node->title);
+
+    // Log in regular user and post a comment.
+    $this->drupalLogout();
+    $this->web_user = $this->drupalCreateUser(array('post comments without approval'));
+    $this->drupalLogin($this->web_user);
+    $this->drupalGet('');
+    $this->clickLink(t('Add new comment'));
+    $edit = array(
+      'subject' => $this->randomString(),
+      'comment' => $this->randomString(),
+    );
+    $this->drupalPost(NULL, $edit, t('Save'));
+    $comment = db_fetch_object(db_query("SELECT * FROM {comments} WHERE subject = '%s'", $edit['subject']));
+
+    // Verify that submitted data equals post data.
+    $data = $this->getServerRecord();
+    $this->_assertEqual('post_title', $data['post_title'], $edit['subject']);
+    $this->_assertEqual('post_body', $data['post_body'], $edit['comment']);
+    $this->_assertEqual('author_name', $data['author_name'], $this->web_user->name);
+    $this->_assertEqual('author_mail', $data['author_mail'], $this->web_user->mail);
+    $this->_assertEqual('author_id', $data['author_id'], $this->web_user->uid);
+
+    // Log in admin user and edit comment.
+    $this->drupalLogout();
+    $this->drupalLogin($this->admin_user);
+    $this->drupalGet('comment/edit/' . $comment->cid);
+    // Post without modification.
+    $this->drupalPost(NULL, array(), t('Save'));
+
+    // Verify that submitted data equals post data.
+    $data = $this->getServerRecord();
+    $this->_assertEqual('post_title', $data['post_title'], $edit['subject']);
+    $this->_assertEqual('post_body', $data['post_body'], $edit['comment']);
+    $this->_assertEqual('author_name', $data['author_name'], $this->web_user->name);
+    $this->_assertEqual('author_mail', $data['author_mail'], $this->web_user->mail);
+    #$this->_assertEqual('author_id', $data['author_id'], $this->web_user->uid);
+  }
+}
+
Index: tests/mollom_test.info
===================================================================
RCS file: tests/mollom_test.info
diff -N tests/mollom_test.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ tests/mollom_test.info	25 Nov 2009 11:16:09 -0000
@@ -0,0 +1,5 @@
+; $Id$
+name = Mollom Test
+description = Testing module for Mollom functionality.
+core = 6.x
+hidden = TRUE
Index: tests/mollom_test.module
===================================================================
RCS file: tests/mollom_test.module
diff -N tests/mollom_test.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ tests/mollom_test.module	27 Nov 2009 15:26:26 -0000
@@ -0,0 +1,60 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Testing functionality for Mollom module.
+ */
+
+/**
+ * Implements hook_xmlrpc().
+ */
+function mollom_test_xmlrpc() {
+  return array(
+    // $data contains a variable amount of properties, so we cannot specify a
+    // signature.
+    'mollom.checkContent' => 'mollom_test_check_content',
+    'mollom.getImageCaptcha' => 'mollom_test_get_captcha',
+    'mollom.checkCaptcha' => 'mollom_test_check_captcha',
+ );
+}
+
+/**
+ * XML-RPC callback for mollom.checkContent to perform textual analysis.
+ */
+function mollom_test_check_content($data) {
+  $storage = variable_get(__FUNCTION__, array());
+  $storage[] = $data;
+  variable_set(__FUNCTION__, $storage);
+
+  return array(
+    'session_id' => isset($data['session_id']) ? $data['session_id'] : session_id(),
+    'spam' => MOLLOM_ANALYSIS_HAM,
+  );
+}
+
+/**
+ * XML-RPC callback for mollom.getImageCaptcha to fetch a CATPCHA image.
+ */
+function mollom_test_get_captcha($data) {
+  $storage = variable_get(__FUNCTION__, array());
+  $storage[] = $data;
+  variable_set(__FUNCTION__, $storage);
+
+  return array(
+    'session_id' => isset($data['session_id']) ? $data['session_id'] : session_id(),
+    'url' => $GLOBALS['base_url'] . '/' . drupal_get_path('module', 'mollom') . '/images/powered-by-mollom-2.gif',
+  );
+}
+
+/**
+ * XML-RPC callback for mollom.checkCaptcha to valie a CAPTCHA response.
+ */
+function mollom_test_check_captcha($data) {
+  $storage = variable_get(__FUNCTION__, array());
+  $storage[] = $data;
+  variable_set(__FUNCTION__, $storage);
+
+  return TRUE;
+}
+
