[ Index ]

PHP Cross Reference of Drupal 6 (gatewave)

title

Body

[close]

/sites/all/modules/mollom/ -> mollom.module (source)

   1  <?php
   2  // $Id: mollom.module,v 1.2.2.167 2010/09/15 15:10:11 sun Exp $
   3  
   4  /**
   5   * @file
   6   * Main Mollom integration module functions.
   7   */
   8  
   9  /**
  10   * Mollom API version; used for XML-RPC communication with Mollom servers.
  11   */
  12  define('MOLLOM_API_VERSION', '1.0');
  13  
  14  /**
  15   * Text analysis result flag: No result.
  16   */
  17  define('MOLLOM_ANALYSIS_UNKNOWN', 0);
  18  
  19  /**
  20   * Text analysis result flag: Content is no spam.
  21   */
  22  define('MOLLOM_ANALYSIS_HAM', 1);
  23  
  24  /**
  25   * Text analysis result flag: Content is spam.
  26   */
  27  define('MOLLOM_ANALYSIS_SPAM', 2);
  28  
  29  /**
  30   * Text analysis result flag: Ambiguous result.
  31   */
  32  define('MOLLOM_ANALYSIS_UNSURE', 3);
  33  
  34  /**
  35   * Form protection mode: No protection.
  36   */
  37  define('MOLLOM_MODE_DISABLED', 0);
  38  
  39  /**
  40   * Form protection mode: CAPTCHA-only protection.
  41   */
  42  define('MOLLOM_MODE_CAPTCHA', 1);
  43  
  44  /**
  45   * Form protection mode: Text analysis with fallback to CAPTCHA.
  46   */
  47  define('MOLLOM_MODE_ANALYSIS', 2);
  48  
  49  /**
  50   * XML-RPC communication failure fallback mode: Block all submissions of protected forms.
  51   */
  52  define('MOLLOM_FALLBACK_BLOCK', 0);
  53  
  54  /**
  55   * XML-RPC communication failure fallback mode: Accept all submissions of protected forms.
  56   */
  57  define('MOLLOM_FALLBACK_ACCEPT', 1);
  58  
  59  /**
  60   * XML-RPC communication failure: No servers could be reached.
  61   *
  62   * @todo Prefix with MOLLOM_.
  63   */
  64  define('NETWORK_ERROR', 900);
  65  
  66  /**
  67   * XML-RPC communication failure: Error on Mollom server.
  68   *
  69   * @todo Prefix with SERVER_.
  70   */
  71  define('MOLLOM_ERROR', 1000);
  72  
  73  /**
  74   * XML-RPC communication failure: Mollom server requests client to update its server list.
  75   *
  76   * @todo Prefix with SERVER_.
  77   */
  78  define('MOLLOM_REFRESH', 1100);
  79  
  80  /**
  81   * XML-RPC communication failure: Mollom server defers communication to next server in server list.
  82   *
  83   * @todo Prefix with SERVER_.
  84   */
  85  define('MOLLOM_REDIRECT', 1200);
  86  
  87  /**
  88   * Implements hook_help().
  89   */
  90  function mollom_help($path, $arg) {
  91    $output = '';
  92  
  93    if ($path == 'admin/settings/mollom') {
  94      $output .= '<p>';
  95      $output .= t('All listed forms below are protected by Mollom, unless users are able to <a href="@permissions-url">bypass Mollom\'s protection</a>.', array(
  96        '@permissions-url' => url('admin/user/permissions', array('fragment' => 'module-mollom')),
  97      ));
  98      $output .= '</p>';
  99      $output .= '<p>';
 100      $output .= t('You can <a href="@add-form-url">add a form</a> to protect, configure already protected forms, or remove the protection.', array(
 101        '@add-form-url' => url('admin/settings/mollom/add'),
 102      ));
 103      $output .= '</p>';
 104      return $output;
 105    }
 106  
 107    if ($path == 'admin/settings/mollom/blacklist') {
 108      return t('Mollom automatically blocks unwanted content and learns from all participating sites to improve its filters. On top of automatic filtering, you can define a custom blacklist.');
 109    }
 110  
 111    if ($path == 'admin/help#mollom') {
 112      $output = '<p>';
 113      $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>.",
 114        array(
 115          '@mollom-website' => 'http://mollom.com',
 116          '@mollom-faq' => 'http://mollom.com/faq',
 117        )
 118      );
 119      $output .= '</p><p>';
 120      $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:");
 121      $output .= '</p><ul><li>';
 122      $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.");
 123      $output .= '</li><li>';
 124      $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.");
 125      $output .= '</li></ul><p>';
 126      $output .= t("Data is processed 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(
 127        '@mollom-privacy' => 'http://mollom.com/service-agreement-free-subscriptions',
 128        '@mollom-works' => 'http://mollom.com/how-mollom-works',
 129        '@mollom-faq' => 'http://mollom.com/faq')
 130      );
 131      $output .= '</p>';
 132      $output .= '<h3>' . t('Mollom blacklist') . '</h3>';
 133      $output .= '<p>';
 134      $output .= t("Mollom's filters are shared and trained globally over all participating sites. Due to this, unwanted content might still be accepted on your site, even after sending feedback to Mollom. By using the site-specific blacklist, the filters can be customized to your specific needs. Each entry specifies a reason for why it has been blacklisted, which further helps in improving Mollom's automated filtering.");
 135      $output .= '</p>';
 136      $output .= '<p>';
 137      $output .= t("All blacklist entries are applied to a context: the entire submitted post, or only links in the post. When limiting the context to links, both the link URL and the link text is taken into account.");
 138      $output .= '</p>';
 139      $output .= '<p>';
 140      $output .= t("If a blacklist entry contains multiple words, various combinations will be matched. For example, when adding \"<code>replica&nbsp;watches</code>\" limited to links, the following links will be blocked:");
 141      $output .= '</p>';
 142      $output .= '<ul>
 143  <li><code>http://replica-watches.com</code></li>
 144  <li><code>http://replica-watches.com/some/path</code></li>
 145  <li><code>http://replicawatches.net</code></li>
 146  <li><code>http://example.com/replica/watches</code></li>
 147  <li><code>&lt;a href="http://example.com"&gt;replica watches&lt;/a&gt;</code></li>
 148  </ul>';
 149      $output .= '<p>';
 150      $output .= t("The blacklist is optional. There is no whitelist, i.e., if a blacklist entry is matched in a post, it overrides any other filter result and the post will not be accepted. Blacklisting potentially ambiguous words should be avoided.");
 151      $output .= '</p>';
 152      return $output;
 153    }
 154  }
 155  
 156  /**
 157   * Implements hook_init().
 158   */
 159  function mollom_init() {
 160    // On all Mollom administration pages, check the module configuration and
 161    // display the corresponding requirements error, if invalid.
 162    if (empty($_POST) && strpos($_GET['q'], 'admin/settings/mollom') === 0 && user_access('administer mollom')) {
 163      // Re-check the status on the settings form only.
 164      $status = _mollom_status($_GET['q'] == 'admin/settings/mollom/settings');
 165      if ($status !== TRUE) {
 166        // Fetch and display requirements error message, without re-checking.
 167        module_load_install('mollom');
 168        $requirements = mollom_requirements('runtime', FALSE);
 169        drupal_set_message($requirements['mollom']['description'], 'error');
 170      }
 171    }
 172    $path = drupal_get_path('module', 'mollom');
 173    drupal_add_js($path . '/mollom.js');
 174    drupal_add_css($path . '/mollom.css');
 175  }
 176  
 177  /**
 178   * Implements hook_link().
 179   */
 180  function mollom_link($type, $object, $teaser = FALSE) {
 181    $links = array();
 182  
 183    // Only show the links if the module is configured.
 184    if (_mollom_status() === TRUE) {
 185      if ($type == 'comment' && user_access('administer comments') && mollom_get_mode('comment_form')) {
 186        $links['mollom_report'] = array(
 187          'title' => t('report to Mollom'),
 188          'href' => 'mollom/report/comment/' . $object->cid,
 189          'query' => drupal_get_destination(),
 190        );
 191      }
 192      elseif ($type == 'node' && user_access('administer nodes') && mollom_get_mode($object->type . '_node_form')) {
 193        $links['mollom_report'] = array(
 194          'title' => t('report to Mollom'),
 195          'href' => 'mollom/report/node/' . $object->nid,
 196          'query' => drupal_get_destination(),
 197        );
 198      }
 199    }
 200  
 201    return $links;
 202  }
 203  
 204  /**
 205   * Implements hook_menu().
 206   */
 207  function mollom_menu() {
 208    $items['mollom/report/%/%'] = array(
 209      'title' => 'Report to Mollom',
 210      'page callback' => 'drupal_get_form',
 211      'page arguments' => array('mollom_report_form', 2, 3),
 212      'access callback' => 'mollom_report_access',
 213      'access arguments' => array(2, 3),
 214      'file' => 'mollom.pages.inc',
 215      'type' => MENU_CALLBACK,
 216    );
 217  
 218    $items['admin/settings/mollom'] = array(
 219      'title' => 'Mollom',
 220      'description' => 'Mollom is a web service that helps you manage your community.',
 221      'page callback' => 'mollom_admin_form_list',
 222      'access arguments' => array('administer mollom'),
 223      'file' => 'mollom.admin.inc',
 224    );
 225    $items['admin/settings/mollom/forms'] = array(
 226      'title' => 'Forms',
 227      'type' => MENU_DEFAULT_LOCAL_TASK,
 228      'weight' => -10,
 229    );
 230    $items['admin/settings/mollom/add'] = array(
 231      'title' => 'Add form',
 232      'page callback' => 'drupal_get_form',
 233      'page arguments' => array('mollom_admin_configure_form'),
 234      'access arguments' => array('administer mollom'),
 235      'type' => MENU_LOCAL_TASK,
 236      'file' => 'mollom.admin.inc',
 237    );
 238    $items['admin/settings/mollom/manage/%mollom_form'] = array(
 239      'title' => 'Configure',
 240      'page callback' => 'drupal_get_form',
 241      'page arguments' => array('mollom_admin_configure_form', 4),
 242      'access arguments' => array('administer mollom'),
 243      'file' => 'mollom.admin.inc',
 244    );
 245    $items['admin/settings/mollom/unprotect/%mollom_form'] = array(
 246      'title' => 'Unprotect form',
 247      'page callback' => 'drupal_get_form',
 248      'page arguments' => array('mollom_admin_unprotect_form', 4),
 249      'access arguments' => array('administer mollom'),
 250      'type' => MENU_CALLBACK,
 251      'file' => 'mollom.admin.inc',
 252    );
 253    $items['admin/settings/mollom/blacklist'] = array(
 254      'title' => 'Blacklist',
 255      'description' => 'Configure blacklists.',
 256      'page callback' => 'drupal_get_form',
 257      'page arguments' => array('mollom_admin_blacklist_form'),
 258      'access arguments' => array('administer mollom'),
 259      'type' => MENU_LOCAL_TASK,
 260      'file' => 'mollom.admin.inc',
 261    );
 262    $items['admin/settings/mollom/blacklist/spam'] = array(
 263      'title' => 'Spam',
 264      'description' => 'Configure spam blacklist entries.',
 265      'type' => MENU_DEFAULT_LOCAL_TASK,
 266      'weight' => -10,
 267    );
 268    $items['admin/settings/mollom/blacklist/profanity'] = array(
 269      'title' => 'Profanity',
 270      'description' => 'Configure profanity blacklist entries.',
 271      'page callback' => 'drupal_get_form',
 272      'page arguments' => array('mollom_admin_blacklist_form', 4),
 273      'access arguments' => array('administer mollom'),
 274      'type' => MENU_LOCAL_TASK,
 275      'file' => 'mollom.admin.inc',
 276    );
 277    $items['admin/settings/mollom/blacklist/unwanted'] = array(
 278      'title' => 'Unwanted',
 279      'description' => 'Configure unwanted blacklist entries.',
 280      'page callback' => 'drupal_get_form',
 281      'page arguments' => array('mollom_admin_blacklist_form', 4),
 282      'access arguments' => array('administer mollom'),
 283      'type' => MENU_LOCAL_TASK,
 284      'file' => 'mollom.admin.inc',
 285    );
 286    $items['admin/settings/mollom/blacklist/delete'] = array(
 287      'title' => 'Delete',
 288      'page callback' => 'drupal_get_form',
 289      'page arguments' => array('mollom_admin_blacklist_delete'),
 290      'access arguments' => array('administer mollom'),
 291      'type' => MENU_CALLBACK,
 292      'file' => 'mollom.admin.inc',
 293    );
 294    $items['admin/settings/mollom/settings'] = array(
 295      'title' => 'Settings',
 296      'description' => 'Configure Mollom keys and global settings.',
 297      'page callback' => 'drupal_get_form',
 298      'page arguments' => array('mollom_admin_settings'),
 299      'access arguments' => array('administer mollom'),
 300      'type' => MENU_LOCAL_TASK,
 301      'file' => 'mollom.admin.inc',
 302    );
 303  
 304    $items['admin/reports/mollom'] = array(
 305      'title' => 'Mollom statistics',
 306      'description' => 'Reports and usage statistics for the Mollom module.',
 307      'page callback' => 'drupal_get_form',
 308      'page arguments' => array('mollom_reports_page'),
 309      'access callback' => '_mollom_access',
 310      'access arguments' => array('administer mollom'),
 311      'file' => 'mollom.admin.inc',
 312    );
 313  
 314    // AJAX callback to request new CAPTCHA.
 315    $items['mollom/captcha/%/%'] = array(
 316      'page callback' => 'mollom_captcha_js',
 317      'page arguments' => array(2, 3),
 318      'access callback' => '_mollom_access',
 319      'file' => 'mollom.pages.inc',
 320      'type' => MENU_CALLBACK,
 321    );
 322  
 323    return $items;
 324  }
 325  
 326  /**
 327   * Access callback; check if the module is configured.
 328   *
 329   * This function does not actually check whether Mollom keys are valid for the
 330   * site, but just if the keys have been entered.
 331   *
 332   * @param $permission
 333   *   An optional permission string to check with user_access().
 334   *
 335   * @return
 336   *   TRUE if the module has been configured and user_access() has been checked,
 337   *   FALSE otherwise.
 338   */
 339  function _mollom_access($permission = FALSE) {
 340    return variable_get('mollom_public_key', '') && variable_get('mollom_private_key', '') && (!$permission || user_access($permission));
 341  }
 342  
 343  /**
 344   * Menu access callback; Determine access to report to Mollom.
 345   *
 346   * The special $entity type "session" may be used for mails and messages, which
 347   * originate from form submissions protected by Mollom, and can be reported by
 348   * anyone; $id is expected to be a Mollom session id instead of an entity id
 349   * then.
 350   *
 351   * @param $entity
 352   *   The entity type of the data to report.
 353   * @param $id
 354   *   The entity id of the data to report.
 355   */
 356  function mollom_report_access($entity, $id) {
 357    // The special entity 'session' means that $id is a Mollom session_id, which
 358    // can always be reported by everyone.
 359    if ($entity == 'session') {
 360      return !empty($id) ? TRUE : FALSE;
 361    }
 362    // Retrieve information about all protectable forms. We use the first valid
 363    // definition, because we assume that multiple form definitions just denote
 364    // variations of the same entity (e.g. node content types).
 365    foreach (mollom_form_list() as $form_id => $info) {
 366      if (!isset($info['entity']) || $info['entity'] != $entity) {
 367        continue;
 368      }
 369      // If there is a 'report access callback', invoke it.
 370      if (isset($info['report access callback']) && function_exists($info['report access callback'])) {
 371        $function = $info['report access callback'];
 372        return $function($entity, $id);
 373      }
 374      // Otherwise, if there is a 'report access' list of permissions, iterate
 375      // over them.
 376      if (isset($info['report access'])) {
 377        foreach ($info['report access'] as $permission) {
 378          if (user_access($permission)) {
 379            return TRUE;
 380          }
 381        }
 382      }
 383    }
 384    // If we end up here, then the current user is not permitted to report this
 385    // content.
 386    return FALSE;
 387  }
 388  
 389  /**
 390   * Implements hook_perm().
 391   */
 392  function mollom_perm() {
 393    return array(
 394      'administer mollom',
 395      'bypass mollom protection',
 396    );
 397  }
 398  
 399  /**
 400   * Implements hook_flush_caches().
 401   */
 402  function mollom_flush_caches() {
 403    return array('cache_mollom');
 404  }
 405  
 406  /**
 407   * Implements hook_cron().
 408   */
 409  function mollom_cron() {
 410    // Mollom session data auto-expires after 6 months.
 411    $expired = time() - 86400 * 30 * 6;
 412    db_query("DELETE FROM {mollom} WHERE changed < %d", array($expired));
 413  }
 414  
 415  /**
 416   * Load a Mollom data record from the database.
 417   *
 418   * @param $entity
 419   *   The entity type to retrieve data for.
 420   * @param $id
 421   *   The entity id to retrieve data for.
 422   */
 423  function mollom_data_load($entity, $id) {
 424    return db_fetch_object(db_query_range("SELECT * FROM {mollom} WHERE entity = '%s' AND did = '%s'", array($entity, $id), 0, 1));
 425  }
 426  
 427  /**
 428   * Save Mollom validation data to the database.
 429   *
 430   * Based on the specified entity type and id, this function stores the
 431   * validation results returned by Mollom in the database. The stored data
 432   * is an associative array containing Mollom session information for the posted
 433   * content:
 434   * - session: The session ID returned by the Mollom server.
 435   * - quality: A quality rating assigned to the content to tell whether or not
 436   *   it's spam.
 437   * - languages: An array containing language codes the content might be
 438   *   written in.
 439   *
 440   * The special $entity type "session" may be used for mails and messages, which
 441   * originate from form submissions protected by Mollom, and can be reported by
 442   * anyone; $id is expected to be a Mollom session id instead of an entity id
 443   * then.
 444   *
 445   * @param $entity
 446   *   The entity type of the data to save.
 447   * @param $id
 448   *   The entity id the data belongs to.
 449   *
 450   * @todo Remove usage of global $mollom variable.
 451   */
 452  function mollom_data_save($entity, $id) {
 453    // Nothing to do, if we do not have a valid Mollom response.
 454    if (empty($GLOBALS['mollom']['response']['session_id'])) {
 455      return FALSE;
 456    }
 457    $data = $GLOBALS['mollom']['response'];
 458    $data['session'] = $data['session_id'];
 459    $data['entity'] = $entity;
 460    $data['did'] = $id;
 461    $data['changed'] = time();
 462  
 463    // Convert languages into a string.
 464    if (!empty($data['languages'])) {
 465      $data['languages'] = implode(' ', $data['languages']);
 466    }
 467    // Merge in default values that may not exist in the response.
 468    $data += array(
 469      'languages' => '',
 470      'quality' => '',
 471      'reputation' => '',
 472    );
 473  
 474    $update = db_result(db_query_range("SELECT 'did' FROM {mollom} WHERE entity = '%s' AND did = '%s'", $entity, $id, 0, 1));
 475    drupal_write_record('mollom', $data, $update ? $update : array());
 476    return $data;
 477  }
 478  
 479  /**
 480   * Deletes a Mollom session data record from the database.
 481   *
 482   * @param $entity
 483   *   The entity type to delete data for.
 484   * @param $id
 485   *   The entity id to delete data for.
 486   */
 487  function mollom_data_delete($entity, $id) {
 488    return mollom_data_delete_multiple($entity, array($id));
 489  }
 490  
 491  /**
 492   * Deletes multiple Mollom session data records from the database.
 493   *
 494   * @param $entity
 495   *   The entity type to delete data for.
 496   * @param $ids
 497   *   An array of entity ids to delete data for.
 498   */
 499  function mollom_data_delete_multiple($entity, $ids) {
 500    $placeholders = db_placeholders($ids, 'varchar');
 501    return db_query("DELETE FROM {mollom} WHERE entity = '%s' AND did IN ($placeholders)", array_merge(array($entity), $ids));
 502  }
 503  
 504  /**
 505   * Helper function to add Mollom feedback options to confirmation forms.
 506   */
 507  function mollom_data_delete_form_alter(&$form, &$form_state) {
 508    if (!isset($form['actions']['#weight'])) {
 509      $form['actions']['#weight'] = 100;
 510    }
 511    if (!isset($form['description']['#weight'])) {
 512      $form['description']['#weight'] = 90;
 513    }
 514    $form['mollom'] = array(
 515      '#tree' => TRUE,
 516      '#weight' => 80,
 517    );
 518    $form['mollom']['feedback'] = array(
 519      '#type' => 'radios',
 520      '#title' => t('Report as inappropriate'),
 521      '#options' => array(
 522        0 => t('Do no report'),
 523        'spam' => t('Spam, unsolicited advertising'),
 524        'profanity' => t('Obscene, violent, profane'),
 525        'low-quality' => t('Low-quality'),
 526        'unwanted' => t('Unwanted, taunting, off-topic'),
 527      ),
 528      '#default_value' => 0,
 529      '#description' => t('Sending feedback to <a href="@mollom-url">Mollom</a> improves the automated moderation of new submissions.', array('@mollom-url' => 'http://mollom.com')),
 530    );
 531  }
 532  
 533  /**
 534   * Send feedback to Mollom and delete Mollom data.
 535   *
 536   * @see mollom_form_alter()
 537   */
 538  function mollom_data_delete_form_submit($form, &$form_state) {
 539    $forms = mollom_form_cache();
 540    $mollom_form = mollom_form_load($forms['delete'][$form_state['values']['form_id']]);
 541    $data = mollom_form_get_values($form_state['values'], $mollom_form['enabled_fields'], $mollom_form['mapping']);
 542  
 543    $entity = $mollom_form['entity'];
 544    $id = $data['post_id'];
 545  
 546    if (!empty($form_state['values']['mollom']['feedback'])) {
 547      if (mollom_data_report($entity, $id, $form_state['values']['mollom']['feedback']) === TRUE) {
 548        drupal_set_message(t('The content was successfully reported as inappropriate.'));
 549      }
 550    }
 551  
 552    // Remove Mollom session data.
 553    mollom_data_delete($entity, $id);
 554  }
 555  
 556  /**
 557   * Sends feedback for a Mollom session data record.
 558   *
 559   * @param $entity
 560   *   The entity type to send feedback for.
 561   * @param $id
 562   *   The entity id to send feedback for.
 563   */
 564  function mollom_data_report($entity, $id, $feedback) {
 565    return mollom_data_report_multiple($entity, array($id), $feedback);
 566  }
 567  
 568  /**
 569   * Sends feedback for multiple Mollom session data records.
 570   *
 571   * @param $entity
 572   *   The entity type to send feedback for.
 573   * @param $ids
 574   *   An array of entity ids to send feedback for.
 575   */
 576  function mollom_data_report_multiple($entity, $ids, $feedback) {
 577    $return = TRUE;
 578    foreach ($ids as $id) {
 579      // Load the Mollom session data.
 580      $data = mollom_data_load($entity, $id);
 581      // Send feedback, if we have session data.
 582      if (isset($data->session)) {
 583        $result = _mollom_send_feedback($data->session, $feedback);
 584        $return = $return && $result;
 585      }
 586    }
 587    return $return;
 588  }
 589  
 590  /**
 591   * Implements hook_form_alter().
 592   *
 593   * This function intercepts all forms in Drupal and Mollom-enables them if
 594   * necessary.
 595   */
 596  function mollom_form_alter(&$form, &$form_state, $form_id) {
 597    static $forms;
 598  
 599    // Skip installation and update forms.
 600    if (defined('MAINTENANCE_MODE')) {
 601      return;
 602    }
 603    // Verify global Mollom configuration status.
 604    $status = _mollom_status();
 605    if ($status !== TRUE) {
 606      return;
 607    }
 608  
 609    // Retrieve a list of all protected forms once.
 610    if (!isset($forms)) {
 611      $forms = mollom_form_cache();
 612    }
 613  
 614    // Remind of enabled testing mode on all protected forms.
 615    if (isset($forms['protected'][$form_id]) || strpos($_GET['q'], 'admin/settings/mollom') === 0) {
 616      _mollom_testing_mode_warning();
 617    }
 618  
 619    // Site administrators don't have their content checked with Mollom.
 620    if (!user_access('bypass mollom protection')) {
 621      // Retrieve configuration for this form.
 622      if (isset($forms['protected'][$form_id]) && ($mollom_form = mollom_form_load($form_id))) {
 623        // Determine whether to bypass validation for the current user.
 624        foreach ($mollom_form['bypass access'] as $permission) {
 625          if (user_access($permission)) {
 626            return;
 627          }
 628        }
 629        // Compute the weight of the CAPTCHA so we can position it in the form.
 630        // #type 'actions' is new in D7, but contributed modules use the concept
 631        // in D6 already. We therefore expect the key without #type.
 632        if (isset($form['actions']) && !isset($form['actions']['#type'])) {
 633          // D6 code should set a #weight. If none is set, we ensure a default of
 634          // 100, like #type 'actions' in D7.
 635          if (!isset($form['actions']['#weight'])) {
 636            $form['actions']['#weight'] = 100;
 637          }
 638          $weight = $form['actions']['#weight'] - 1;
 639        }
 640        else {
 641          $weight = 99999;
 642          foreach (element_children($form) as $key) {
 643            // Scan the top-level form elements for buttons.
 644            if (isset($form[$key]['#type']) && in_array($form[$key]['#type'], array('submit', 'button', 'image_button'))) {
 645              // For each button, slightly increase the weight to allocate room for
 646              // the CAPTCHA.
 647              if (isset($form[$key]['#weight'])) {
 648                $form[$key]['#weight'] += 0.0002;
 649              }
 650              else {
 651                $form[$key]['#weight'] = 1.0002;
 652              }
 653              // We want to position the CAPTCHA just before the first button, so
 654              // we make the CAPTCHA's weight slightly lighter than the lightest
 655              // button's weight.
 656              $weight = min($weight, $form[$key]['#weight'] - 0.0001);
 657            }
 658          }
 659        }
 660        // Add Mollom form widget.
 661        $form['mollom'] = array(
 662          '#type' => 'mollom',
 663          '#mollom_form' => $mollom_form,
 664          '#weight' => $weight,
 665          '#tree' => TRUE,
 666        );
 667        // Add Mollom form validation handlers.
 668        $form['#validate'][] = 'mollom_validate_analysis';
 669        $form['#validate'][] = 'mollom_validate_captcha';
 670  
 671        // Add a submit handler to remove form state storage.
 672        $form['#submit'][] = 'mollom_form_submit';
 673  
 674        // Add link to privacy policy on forms protected via textual analysis,
 675        // if enabled.
 676        if ($mollom_form['mode'] == MOLLOM_MODE_ANALYSIS && variable_get('mollom_privacy_link', 1)) {
 677          $form['mollom']['privacy'] = array(
 678            '#prefix' => '<div class="description mollom-privacy">',
 679            '#suffix' => '</div>',
 680            '#value' => t('By submitting this form, you accept the <a href="@privacy-policy-url">Mollom privacy policy</a>.', array(
 681              '@privacy-policy-url' => 'http://mollom.com/web-service-privacy-policy',
 682            )),
 683            '#weight' => 10,
 684          );
 685        }
 686      }
 687    }
 688    // Integrate with delete confirmation forms to send feedback to Mollom.
 689    if (isset($forms['delete'][$form_id])) {
 690      mollom_data_delete_form_alter($form, $form_state);
 691      // Report before deleting. This needs to be handled here, since
 692      // mollom_data_delete_form_alter() is re-used for mass-operation forms.
 693      array_unshift($form['#submit'], 'mollom_data_delete_form_submit');
 694    }
 695  }
 696  
 697  /**
 698   * Returns a cached mapping of protected and delete confirmation form ids.
 699   *
 700   * @param $reset
 701   *   (optional) Boolean whether to reset the static cache, flush the database
 702   *   cache, and return nothing (TRUE). Defaults to FALSE.
 703   *
 704   * @return
 705   *   An associative array containing:
 706   *   - protected: An associative array whose keys are protected form IDs and
 707   *     whose values are the corresponding module names the form belongs to.
 708   *   - delete: An associative array whose keys are 'delete form' ids and whose
 709   *     values are protected form ids; e.g.
 710   *     @code
 711   *     array(
 712   *       'node_delete_confirm' => 'article_node_form',
 713   *     )
 714   *     @endcode
 715   *     A single delete confirmation form id can map to multiple registered
 716   *     $form_ids, but only the first is taken into account. As in above example,
 717   *     we assume that all 'TYPE_node_form' definitions belong to the same entity
 718   *     and therefore have an identical 'post_id' mapping.
 719   */
 720  function mollom_form_cache($reset = FALSE) {
 721    if ($reset) {
 722      // This catches both 'mollom_form_cache' as well as mollom_form_load()'s
 723      // 'mollom:form:*' entries.
 724      cache_clear_all('mollom', 'cache', TRUE);
 725      return;
 726    }
 727  
 728    if ($cache = cache_get('mollom_form_cache')) {
 729      return $cache->data;
 730    }
 731  
 732    $result = db_query("SELECT form_id, module FROM {mollom_form}");
 733    $forms['protected'] = array();
 734    while ($row = db_fetch_array($result)) {
 735      $forms['protected'][$row['form_id']] = $row['module'];
 736    }
 737  
 738    // Build a list of delete confirmation forms of entities integrating with
 739    // Mollom, so we are able to alter the delete confirmation form to display
 740    // our feedback options.
 741    $forms['delete'] = array();
 742    foreach (mollom_form_list() as $form_id => $info) {
 743      if (!isset($info['delete form']) || !isset($info['entity'])) {
 744        continue;
 745      }
 746      // We expect that the same delete confirmation form uses the same form
 747      // element mapping, so multiple 'delete form' definitions are only processed
 748      // once. Additionally, we only care for protected forms.
 749      if (!isset($forms['delete'][$info['delete form']]) && isset($forms['protected'][$form_id])) {
 750        // A delete confirmation form integration requires a 'post_id' mapping.
 751        $form_info = mollom_form_info($form_id, $info['module']);
 752        if (isset($form_info['mapping']['post_id'])) {
 753          $forms['delete'][$info['delete form']] = $form_id;
 754        }
 755      }
 756    }
 757    cache_set('mollom_form_cache', $forms);
 758  
 759    return $forms;
 760  }
 761  
 762  /**
 763   * Return the protection mode for a given form id.
 764   *
 765   * @return
 766   *   The protection mode for the given form id, one of:
 767   *   - MOLLOM_MODE_DISABLED: None.
 768   *   - MOLLOM_MODE_CAPTCHA: CAPTCHA only.
 769   *   - MOLLOM_MODE_ANALYSIS: Text analysis with CAPTCHA fallback.
 770   */
 771  function mollom_get_mode($form_id) {
 772    static $modes;
 773  
 774    if (!isset($modes[$form_id])) {
 775      $mollom_form = mollom_form_load($form_id);
 776      $modes[$form_id] = isset($mollom_form['mode']) ? $mollom_form['mode'] : MOLLOM_MODE_DISABLED;
 777    }
 778  
 779    return $modes[$form_id];
 780  }
 781  
 782  /**
 783   * Returns a list of protectable forms registered via hook_mollom_form_info().
 784   */
 785  function mollom_form_list() {
 786    $form_list = array();
 787    foreach (module_implements('mollom_form_list') as $module) {
 788      $function = $module . '_mollom_form_list';
 789      $module_forms = $function();
 790      foreach ($module_forms as $form_id => $info) {
 791        $form_list[$form_id] = $info;
 792        $form_list[$form_id] += array(
 793          'form_id' => $form_id,
 794          'module' => $module,
 795        );
 796      }
 797    }
 798    return $form_list;
 799  }
 800  
 801  /**
 802   * Returns information about a form registered via hook_mollom_form_info().
 803   *
 804   * @param $form_id
 805   *   The form id to return information for.
 806   * @param $module
 807   *   The module name $form_id belongs to.
 808   */
 809  function mollom_form_info($form_id, $module) {
 810    $form_info = module_invoke($module, 'mollom_form_info', $form_id);
 811    if (empty($form_info)) {
 812      $form_info = array();
 813    }
 814  
 815    // Ensure default properties.
 816    $form_info += array(
 817      'form_id' => $form_id,
 818      'title' => $form_id,
 819      'module' => $module,
 820      'entity' => NULL,
 821      'mode' => NULL,
 822      'bypass access' => array(),
 823      'elements' => array(),
 824      'mapping' => array(),
 825      'mail ids' => array(),
 826    );
 827  
 828    // Allow modules to alter the default form information.
 829    drupal_alter('mollom_form_info', $form_info, $form_id);
 830  
 831    return $form_info;
 832  }
 833  
 834  /**
 835   * Creates a bare Mollom form configuration.
 836   *
 837   * @param $form_id
 838   *   (optional) The form id to create the Mollom form configuration for.
 839   */
 840  function mollom_form_new($form_id = NULL) {
 841    $mollom_form = array();
 842    if (isset($form_id)) {
 843      $form_list = mollom_form_list();
 844      if (isset($form_list[$form_id])) {
 845        $mollom_form += $form_list[$form_id];
 846      }
 847      $mollom_form += mollom_form_info($form_id, $form_list[$form_id]['module']);
 848    }
 849    // Ensure default properties.
 850    $mollom_form += array(
 851      'form_id' => $form_id,
 852      'title' => $form_id,
 853      'mode' => NULL,
 854      'checks' => array(),
 855      'elements' => array(),
 856      'enabled_fields' => array(),
 857    );
 858    // Enable all fields for textual analysis by default.
 859    if (!empty($mollom_form['elements'])) {
 860      $mollom_form['checks'] = array('spam');
 861      $mollom_form['enabled_fields'] = array_keys($mollom_form['elements']);
 862    }
 863  
 864    return $mollom_form;
 865  }
 866  
 867  /**
 868   * Menu argument loader; Loads Mollom configuration and form information for a given form id.
 869   */
 870  function mollom_form_load($form_id) {
 871    $cid = 'mollom:form:' . $form_id;
 872    if ($cache = cache_get($cid)) {
 873      return $cache->data;
 874    }
 875    else {
 876      $mollom_form = db_fetch_array(db_query("SELECT * FROM {mollom_form} WHERE form_id = '%s'", $form_id));
 877      if ($mollom_form) {
 878        $mollom_form['checks'] = unserialize($mollom_form['checks']);
 879        $mollom_form['enabled_fields'] = unserialize($mollom_form['enabled_fields']);
 880  
 881        // Attach form registry information.
 882        $form_list = mollom_form_list();
 883        if (isset($form_list[$form_id])) {
 884          $mollom_form += $form_list[$form_id];
 885        }
 886        $mollom_form += mollom_form_info($form_id, $mollom_form['module']);
 887  
 888        cache_set($cid, $mollom_form);
 889      }
 890    }
 891    return $mollom_form;
 892  }
 893  
 894  /**
 895   * Saves a Mollom form configuration.
 896   */
 897  function mollom_form_save(&$mollom_form) {
 898    $exists = db_result(db_query_range("SELECT 1 FROM {mollom_form} WHERE form_id = '%s'", $mollom_form['form_id'], 0, 1));
 899    $status = drupal_write_record('mollom_form', $mollom_form, ($exists ? 'form_id' : array()));
 900  
 901    // Allow modules to react on saved form configurations.
 902    if ($status === SAVED_NEW) {
 903      module_invoke_all('mollom_form_insert', $mollom_form);
 904    }
 905    else {
 906      module_invoke_all('mollom_form_update', $mollom_form);
 907    }
 908  
 909    // Flush cached Mollom forms and the Mollom form mapping cache.
 910    mollom_form_cache(TRUE);
 911  
 912    return $status;
 913  }
 914  
 915  /**
 916   * Given an array of values and an array of fields, extract data for use.
 917   *
 918   * This function generates the data to send for validation to Mollom by walking
 919   * through the submitted form values and
 920   * - copying element values as specified via 'mapping' in hook_mollom_form_info()
 921   *   into the dedicated data properties
 922   * - collecting and concatenating all fields that have been selected for textual
 923   *   analysis into the 'post_body' property
 924   *
 925   * The processing accounts for the following possibilities:
 926   * - A field was selected for textual analysis, but there is no submitted form
 927   *   value. The value should have been appended to the 'post_body' property, but
 928   *   will be skipped.
 929   * - A field is contained in the 'mapping' and there is a submitted form value.
 930   *   The value will not be appended to the 'post_body', but instead be assigned
 931   *   to the specified data property.
 932   * - All fields specified in 'mapping', for which there is a submitted value,
 933   *   but which were NOT selected for textual analysis, are assigned to the
 934   *   specified data property. This is usually the case for form elements that
 935   *   hold system user information.
 936   *
 937   * @param $values
 938   *   An array containing submitted form values, usually $form_state['values'].
 939   * @param $fields
 940   *   A list of strings representing form elements to extract. Nested fields are
 941   *   in the form of 'parent][child'.
 942   * @param $mapping
 943   *   An associative array of form elements to map to Mollom's dedicated data
 944   *   properties. See hook_mollom_form_info() for details.
 945   *
 946   * @see hook_mollom_form_info()
 947   */
 948  function mollom_form_get_values($form_values, $fields, $mapping) {
 949    global $user;
 950  
 951    // All elements specified in $mapping must be excluded from $fields, as they
 952    // are used for dedicated $data properties instead. To reduce the parsing code
 953    // size, we are turning a given $mapping of f.e.
 954    //   array('post_title' => 'title_form_element')
 955    // into
 956    //   array('title_form_element' => 'post_title')
 957    // and we reset $mapping afterwards.
 958    // When iterating over the $fields, this allows us to quickly test whether the
 959    // current field should be excluded, and if it should, we directly get the
 960    // mapped property name to rebuild $mapping with the field values.
 961    $exclude_fields = array();
 962    if (!empty($mapping)) {
 963      $exclude_fields = array_flip($mapping);
 964    }
 965    $mapping = array();
 966  
 967    // Process all fields that have been selected for text analysis.
 968    $post_body = array();
 969    foreach ($fields as $field) {
 970      // Nested elements use a key of 'parent][child', so we need to recurse.
 971      $parents = explode('][', $field);
 972      $value = $form_values;
 973      foreach ($parents as $key) {
 974        $value = isset($value[$key]) ? $value[$key] : NULL;
 975      }
 976      // If this field was contained in $mapping and should be excluded, add it to
 977      // $mapping with the actual form element value, and continue to the next
 978      // field. Also unset this field from $exclude_fields, so we can process the
 979      // remaining mappings below.
 980      if (isset($exclude_fields[$field]) && drupal_validate_utf8($value)) {
 981        $mapping[$exclude_fields[$field]] = $value;
 982        unset($exclude_fields[$field]);
 983        continue;
 984      }
 985      // Only add form element values that are not empty.
 986      if (isset($value)) {
 987        if (is_string($value) && drupal_validate_utf8($value) && drupal_strlen($value)) {
 988          $post_body[$field] = $value;
 989        }
 990        // Recurse into nested values (e.g. multiple value fields).
 991        elseif (!empty($value)) {
 992          // Ensure we have a flat array to implode(); form values of
 993          // field_attach_form() use several subkeys.
 994          _mollom_flatten_form_values($value);
 995          if (($value = implode("\n", $value)) && drupal_validate_utf8($value)) {
 996            $post_body[$field] = $value;
 997          }
 998        }
 999      }
1000    }
1001    $post_body = implode("\n", $post_body);
1002  
1003    // Try to assign any further form values by processing the remaining mappings,
1004    // which have been turned into $exclude_fields above. All fields that were
1005    // already used for 'post_body' no longer exist in $exclude_fields.
1006    foreach ($exclude_fields as $field => $property) {
1007      // Nested elements use a key of 'parent][child', so we need to recurse.
1008      $parents = explode('][', $field);
1009      $value = $form_values;
1010      foreach ($parents as $key) {
1011        $value = isset($value[$key]) ? $value[$key] : NULL;
1012      }
1013      if (isset($value) && drupal_validate_utf8($value)) {
1014        $mapping[$property] = $value;
1015      }
1016    }
1017  
1018    // Mollom's XML-RPC methods only accept data properties that are defined. We
1019    // also do not want to send more than we have to, so we need to build an
1020    // exact data structure.
1021    $data = array();
1022    // Post id; not sent to Mollom.
1023    // @see mollom_form_submit()
1024    if (!empty($mapping['post_id'])) {
1025      $data['post_id'] = $mapping['post_id'];
1026    }
1027    // Post title.
1028    if (!empty($mapping['post_title'])) {
1029      $data['post_title'] = $mapping['post_title'];
1030    }
1031    // Post body.
1032    if (!empty($post_body)) {
1033      $data['post_body'] = $post_body;
1034    }
1035  
1036    // User name.
1037    if (!empty($mapping['author_name'])) {
1038      $data['author_name'] = $mapping['author_name'];
1039      // Try to inherit user from author name.
1040      $account = user_load(array('name' => $data['author_name']));
1041    }
1042    elseif (!empty($user->name)) {
1043      $data['author_name'] = $user->name;
1044    }
1045  
1046    // User e-mail.
1047    if (!empty($mapping['author_mail'])) {
1048      $data['author_mail'] = $mapping['author_mail'];
1049    }
1050    elseif (!empty($data['author_name'])) {
1051      if (!empty($account->mail)) {
1052        $data['author_mail'] = $account->mail;
1053      }
1054    }
1055    elseif (!empty($user->mail)) {
1056      $data['author_mail'] = $user->mail;
1057    }
1058  
1059    // User homepage.
1060    if (!empty($mapping['author_url'])) {
1061      $data['author_url'] = $mapping['author_url'];
1062    }
1063  
1064    // User ID.
1065    if (!empty($mapping['author_id'])) {
1066      $data['author_id'] = $mapping['author_id'];
1067    }
1068    elseif (!empty($data['author_name'])) {
1069      if (!empty($account->uid)) {
1070        $data['author_id'] = $account->uid;
1071      }
1072    }
1073    elseif (!empty($user->uid)) {
1074      $data['author_id'] = $user->uid;
1075    }
1076  
1077    // User OpenID.
1078    if (!empty($mapping['author_openid'])) {
1079      $data['author_openid'] = $mapping['author_openid'];
1080    }
1081    elseif (!empty($data['author_id'])) {
1082      if (!empty($account->uid) && ($openid = _mollom_get_openid($account))) {
1083        $data['author_openid'] = $openid;
1084      }
1085    }
1086    elseif (!empty($user->uid) && ($openid = _mollom_get_openid($user))) {
1087      $data['author_openid'] = $openid;
1088    }
1089  
1090    // User IP.
1091    $data['author_ip'] = ip_address();
1092  
1093    return $data;
1094  }
1095  
1096  /**
1097   * Recursive helper function to flatten nested form values.
1098   *
1099   * Takes a potentially nested array and moves all nested keys to the top-level.
1100   */
1101  function _mollom_flatten_form_values(&$values) {
1102    foreach ($values as $key => $value) {
1103      if (is_array($value)) {
1104        $values += _mollom_flatten_form_values($value);
1105        unset($values[$key]);
1106      }
1107    }
1108    return $values;
1109  }
1110  
1111  /**
1112   * Helper function to return OpenID identifiers associated with a given user account.
1113   */
1114  function _mollom_get_openid($account) {
1115    if (isset($account->uid)) {
1116      $result = db_query("SELECT * FROM {authmap} WHERE module = 'openid' AND uid = %d", $account->uid);
1117  
1118      $ids = array();
1119      while ($identity = db_fetch_object($result)) {
1120        $ids[] = $identity->authname;
1121      }
1122  
1123      if (!empty($ids)) {
1124        return implode($ids, ' ');
1125      }
1126    }
1127  }
1128  
1129  /**
1130   * Returns the (last known) status of the configured Mollom API keys.
1131   *
1132   * @param $reset
1133   *   (optional) Boolean whether to reset the stored state and re-check.
1134   *   Defaults to FALSE.
1135   *
1136   * @return
1137   *   TRUE if the module is considered operable, or an associative array
1138   *   describing the current status of the module:
1139   *   - keys: Boolean whether Mollom API keys have been configured.
1140   *   - keys valid: TRUE if Mollom API keys are valid, or the error code as
1141   *     returned by Mollom servers.
1142   *   - servers: Boolean whether there is a non-empty list of Mollom servers.
1143   *
1144   * @see mollom_init()
1145   * @see mollom_admin_settings()
1146   * @see mollom_requirements()
1147   */
1148  function _mollom_status($reset = FALSE) {
1149    // Load stored status.
1150    $status = variable_get('mollom_status', array(
1151      'keys' => FALSE,
1152      'keys valid' => FALSE,
1153    ));
1154  
1155    // Both API keys are required.
1156    $public_key = variable_get('mollom_public_key', '');
1157    $private_key = variable_get('mollom_private_key', '');
1158    $status['keys'] = (!empty($public_key) && !empty($private_key));
1159  
1160    // If we have keys and are asked to reset, check whether keys are valid.
1161    if ($status['keys'] && $reset) {
1162      $status['keys valid'] = mollom('mollom.verifyKey', _mollom_get_version());
1163    }
1164  
1165    // In case of an error, indicate whether we have a non-empty server list.
1166    if ($status['keys valid'] !== TRUE) {
1167      $servers = variable_get('mollom_servers', array());
1168      $status['servers'] = !empty($servers);
1169    }
1170  
1171    // Update stored status upon reset.
1172    if ($reset) {
1173      variable_set('mollom_status', $status);
1174    }
1175  
1176    return ($status['keys valid'] === TRUE ? TRUE : $status);
1177  }
1178  
1179  /**
1180   * Outputs a warning message about enabled testing mode (once).
1181   */
1182  function _mollom_testing_mode_warning() {
1183    static $warned;
1184    if (isset($warned)) {
1185      return;
1186    }
1187    $warned = TRUE;
1188  
1189    if (variable_get('mollom_testing_mode', 0) && empty($_POST)) {
1190      $admin_message = '';
1191      if (user_access('administer mollom') && $_GET['q'] != 'admin/settings/mollom/settings') {
1192        $admin_message = t('Visit the <a href="@settings-url">Mollom settings page</a> to disable it.', array(
1193          '@settings-url' => url('admin/settings/mollom/settings'),
1194        ));
1195      }
1196      $message = t('Mollom testing mode is still enabled. !admin-message', array(
1197        '!admin-message' => $admin_message,
1198      ));
1199      drupal_set_message($message, 'warning');
1200    }
1201  }
1202  
1203  /**
1204   * Helper function to log and optionally output an error message when Mollom servers are unavailable.
1205   */
1206  function _mollom_fallback() {
1207    $fallback = variable_get('mollom_fallback', MOLLOM_FALLBACK_BLOCK);
1208    // @todo Prevents mollom_admin_settings() from implementing a proper form
1209    //   validation. Add !empty($_POST) to this condition + manually invoke from
1210    //   mollom_process_form() on GET requests? Or don't call it from mollom()?
1211    //   Anything, but just don't mix FAPI logic into XML-RPC logic.
1212    if ($fallback == MOLLOM_FALLBACK_BLOCK) {
1213      form_set_error('mollom', t("The spam filter installed on this site is currently unavailable. Per site policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple of minutes."));
1214    }
1215  
1216    $servers = variable_get('mollom_servers', array());
1217    _mollom_watchdog(array(
1218      'All servers unavailable: %servers' => array('%servers' => $servers ? implode(', ', $servers) : '--'),
1219      'Last error: @code %message' => array('@code' => xmlrpc_errno(), '%message' => xmlrpc_error_msg()),
1220    ), WATCHDOG_ERROR);
1221  }
1222  
1223  /**
1224   * @defgroup mollom_form_api Mollom Form API workarounds
1225   * @{
1226   * Various helper functions to work around bugs in Form API.
1227   *
1228   * Normally, Mollom's integration with Form API would be quite simple:
1229   * - If a form is protected by Mollom, we setup initial information
1230   *   about the session and the form in $form_state['storage'], bound to the
1231   *   'form_build_id'.
1232   * - We enable form caching via $form_state['cache'], so our information in the
1233   *   form storage is cached. Form API then automatically ensures a proper
1234   *   'form_build_id' for every form and every user.
1235   * - We mainly work in and after form validation. Textual analysis validates all
1236   *   values in the form as a form validation handler. If this validation fails,
1237   *   we alter the form (during validation) to add a CAPTCHA. If the CAPTCHA
1238   *   response is invalid, we still alter the form during validation to display a
1239   *   new CAPTCHA, but without the previously entered value.
1240   * - In short, roughly:
1241   *   - Form construction: Nothing.
1242   *   - Form processing: Nothing.
1243   *   - Form validation: Perform validation and alterations based on validation.
1244   *
1245   * This, however, is not possible due to various bugs in Drupal core.
1246   * - Form caching cannot be enabled for certain forms, because they contain
1247   *   processing and validation logic.
1248   *   http://drupal.org/node/644222
1249   * - $form_state['storage'] is not updated after form processing and validation.
1250   *   http://drupal.org/node/644150
1251   * - Form validation handlers cannot alter the form structure.
1252   *   http://drupal.org/node/642702
1253   *
1254   * Hence, something that could be done in one simple function becomes quite a
1255   * nightmare:
1256   * - We need our own {cache_mollom} table as replacement for native form
1257   *   caching, as well as our own logic to validate a submitted 'session_id'
1258   *   ('form_build_id') against forms and users.
1259   * - We need to perform form alterations during form rendering, where
1260   *   $form_state is no longer available. To make this possible, we leverage the
1261   *   fact that an element property that is a reference to a key in $form_state
1262   *   (which in itself is passed by reference) persists on to the rendering
1263   *   layer. The essential part is:
1264   *   @code
1265   *     $element['#mollom'] = &$form_state['mollom'];
1266   *   @endcode
1267   * - Since we cannot alter elements in the form structure during form
1268   *   validation, this reference already needs to be set up during form
1269   *   processing (in a #process callback), while everything else lives in form
1270   *   validation handlers (unless it needs to add or alter the form structure).
1271   *
1272   * @see mollom_form_alter()
1273   */
1274  
1275  /**
1276   * Implements hook_elements().
1277   */
1278  function mollom_elements() {
1279    return array(
1280      'mollom' => array(
1281        '#input' => TRUE,
1282        '#process' => array(
1283          // Try to fetch a Mollom session from cache during form processing/validation.
1284          'mollom_process_mollom_session_id',
1285          // Setup a new Mollom session.
1286          'mollom_process_mollom',
1287        ),
1288        '#pre_render' => array('mollom_pre_render_mollom'),
1289      ),
1290    );
1291  }
1292  
1293  /**
1294   * Implements hook_theme().
1295   */
1296  function mollom_theme() {
1297    return array(
1298      'mollom' => array(
1299        'arguments' => array('element' => NULL),
1300      ),
1301      'mollom_admin_blacklist_form' => array(
1302        'arguments' => array('form' => NULL),
1303        'file' => 'mollom.admin.inc',
1304      ),
1305    );
1306  }
1307  
1308  /**
1309   * Format the Mollom form element.
1310   *
1311   * This works like #type 'markup' and is only required, because D6 only supports
1312   * #process callbacks on elements with #input = TRUE.
1313   *
1314   * @see form_builder()
1315   * @see _form_builder_handle_input_element()
1316   */
1317  function theme_mollom($element) {
1318    return isset($element['#children']) ? $element['#children'] : '';
1319  }
1320  
1321  /**
1322   * Form element #process callback for the 'mollom' element.
1323   *
1324   * The 'mollom' form element is stateful. The Mollom session ID that is exchanged
1325   * between Drupal, the Mollom back-end, and the user allows us to keep track of
1326   * the form validation state.
1327   *
1328   * The session ID is valid for a given $form_id only. We expire it as soon as
1329   * the form is submitted, to avoid it being replayed.
1330   */
1331  function mollom_process_mollom($element, $input, &$form_state, $complete_form) {
1332    // Setup initial Mollom session and form information.
1333    if (empty($form_state['mollom'])) {
1334      $form_state['mollom'] = array(
1335        'require_analysis' => $element['#mollom_form']['mode'] == MOLLOM_MODE_ANALYSIS,
1336        'require_captcha' => $element['#mollom_form']['mode'] == MOLLOM_MODE_CAPTCHA,
1337        'passed_captcha' => FALSE,
1338        'response' => array(
1339          'session_id' => '',
1340        ),
1341      );
1342    }
1343    $form_state['mollom'] += $element['#mollom_form'];
1344  
1345    // Add the Mollom session element.
1346    $element['session_id'] = array(
1347      '#type' => 'hidden',
1348      '#default_value' => isset($form_state['mollom']['response']['session_id']) ? $form_state['mollom']['response']['session_id'] : '',
1349      '#attributes' => array('class' => 'mollom-session-id'),
1350    );
1351  
1352    // Add the CAPTCHA element.
1353    $element['captcha'] = array(
1354      '#type' => 'textfield',
1355      '#title' => t('Word verification'),
1356      '#size' => 10,
1357      '#default_value' => '',
1358      '#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. Not case sensitive."),
1359    );
1360  
1361    // Make Mollom form and session information available to #pre_render callback.
1362    // This must be assigned by reference. It is the essential "communication
1363    // layer" between form API and the rendering system. Any modifications to
1364    // $form_state['mollom'] will be carried over to the element for rendering.
1365    $element['#mollom'] = &$form_state['mollom'];
1366  
1367    // Make Mollom form and session information available to entirely different
1368    // functions.
1369    $GLOBALS['mollom'] = &$form_state['mollom'];
1370  
1371    return $element;
1372  }
1373  
1374  /**
1375   * Form element #process callback for Mollom's form storage handling.
1376   *
1377   * Albeit this *should* be an #element_validate handler that is only executed
1378   * during form validation, we must use a #process callback, because
1379   * mollom_process_mollom() needs to copy over $form_state['mollom'] into
1380   * $element['#mollom'], and as of now, Form API does not allow form validation
1381   * handlers to alter any elements in the form structure by reference.
1382   * @see http://drupal.org/node/642702
1383   */
1384  function mollom_process_mollom_session_id($element, $input, &$form_state) {
1385    // The current state can come either from the $form_state, if the form
1386    // was just rebuilt in the same request, or from data posted by the user. In
1387    // the latter case the state is fetched from the temporary data store. It is
1388    // verified that the session was created for the current form and that it has
1389    // not expired or already been used.
1390    if (empty($form_state['mollom']) && !empty($input['session_id'])) {
1391      @list($timestamp, $mollom_session_id) = explode('-', $input['session_id'], 2);
1392  
1393      if (empty($mollom_session_id)) {
1394        watchdog('mollom', 'Bogus session id %session.', array('%session' => $form_state['input']['mollom']['session_id']), WATCHDOG_WARNING);
1395      }
1396      elseif (!$cache = cache_get($mollom_session_id, 'cache_mollom')) {
1397        if (time() - $timestamp > 30 * 60) {
1398          watchdog('mollom', 'Expired session id %session.', array('%session' => $mollom_session_id));
1399        }
1400        else {
1401          watchdog('mollom', 'Unknown session id %session. This is not a bug in Mollom. If this happens too often, check your site for attacks.', array('%session' => $mollom_session_id), WATCHDOG_WARNING);
1402        }
1403      }
1404      elseif ($cache->data['form_id'] !== $form_state['values']['form_id']) {
1405        watchdog('mollom', 'Invalid form id %form_id for session id %session (generated for %form_id_session).  This is not a bug in Mollom. If this happens too often, check your site for attacks.', array('%session' => $mollom_session_id, '%form_id_session' => $cache->data['form_id'], '%form_id' => $form_state['values']['form_id']), WATCHDOG_WARNING);
1406      }
1407      else {
1408        $form_state['mollom'] = $cache->data;
1409      }
1410    }
1411    return $element;
1412  }
1413  
1414  /**
1415   * Form validation handler to perform textual analysis of submitted form values.
1416   *
1417   * Validation needs to re-run in case of a form validation error (elsewhere in
1418   * the form). In case Mollom's textual analysis returns no definite result, we
1419   * must trigger a CAPTCHA, but text analysis is always performed, even if the
1420   * CAPTCHA was solved correctly.
1421   */
1422  function mollom_validate_analysis(&$form, &$form_state) {
1423    // Text analysis may only ever be skipped, if we do not require it in the
1424    // first place. With regard to that, $form_state['mollom']['require_analysis']
1425    // is only set once during initialization of $form_state['mollom'] in
1426    // mollom_process_form() and must not be updated elsewhere.
1427    if (!$form_state['mollom']['require_analysis']) {
1428      return;
1429    }
1430  
1431    // Perform textual analysis.
1432    $all_data = mollom_form_get_values($form_state['values'], $form_state['mollom']['enabled_fields'], $form_state['mollom']['mapping']);
1433    $data = $all_data;
1434    // Remove 'post_id' property; only used by mollom_form_submit().
1435    if (isset($data['post_id'])) {
1436      unset($data['post_id']);
1437    }
1438    $data['session_id'] = $form_state['mollom']['response']['session_id'];
1439    $data['checks'] = implode(',', $form_state['mollom']['checks']);
1440    $result = mollom('mollom.checkContent', $data);
1441    // Use all available data properties for log messages below.
1442    $data += $all_data;
1443  
1444    // Trigger global fallback behavior if there is no result.
1445    if (!isset($result['session_id'])) {
1446      return _mollom_fallback();
1447    }
1448  
1449    // If a new session ID was generated by Mollom, flush the old session from the
1450    // database cache.
1451    if (!empty($form_state['mollom']['response']['session_id']) && $result['session_id'] != $form_state['mollom']['response']['session_id']) {
1452      cache_clear_all($form_state['mollom']['response']['session_id'], 'cache_mollom');
1453    }
1454    // Store the response returned by Mollom.
1455    $form_state['mollom']['response'] = $result;
1456  
1457    // Handle the spam check result.
1458    // The Mollom backend is remembering results of previous mollom.checkContent
1459    // invocations for a single user/post session. When content is re-checked
1460    // during form validation, the result may change according to the values that
1461    // have been submitted (which e.g. can change during previews). Only in case
1462    // the spam check led to a MOLLOM_ANALYSIS_UNSURE result, and the user solved
1463    // the CAPTCHA correctly, subsequent spam check results will likely be
1464    // MOLLOM_ANALYSIS_HAM (though not guaranteed).
1465    $teaser = truncate_utf8(strip_tags(isset($data['post_title']) ? $data['post_title'] : isset($data['post_body']) ? $data['post_body'] : '--'), 40);
1466    if (isset($result['spam'])) {
1467      switch ($result['spam']) {
1468        case MOLLOM_ANALYSIS_HAM:
1469          $form_state['mollom']['require_captcha'] = FALSE;
1470          _mollom_watchdog(array(
1471            'Ham: %teaser' => array('%teaser' => $teaser),
1472            'Data:<pre>@data</pre>' => array('@data' => $data),
1473            'Result:<pre>@result</pre>' => array('@result' => $result),
1474          ), WATCHDOG_INFO);
1475          break;
1476  
1477        case MOLLOM_ANALYSIS_SPAM:
1478          $form_state['mollom']['require_captcha'] = FALSE;
1479          form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.'));
1480          _mollom_watchdog(array(
1481            'Spam: %teaser' => array('%teaser' => $teaser),
1482            'Data:<pre>@data</pre>' => array('@data' => $data),
1483            'Result:<pre>@result</pre>' => array('@result' => $result),
1484          ));
1485          break;
1486  
1487        case MOLLOM_ANALYSIS_UNSURE:
1488          _mollom_watchdog(array(
1489            'Unsure: %teaser' => array('%teaser' => $teaser),
1490            'Data:<pre>@data</pre>' => array('@data' => $data),
1491            'Result:<pre>@result</pre>' => array('@result' => $result),
1492          ), WATCHDOG_INFO);
1493  
1494          // Only throw a validation error and retrieve a CAPTCHA, if we check
1495          // this post for the first time. Otherwise, mollom_validate_captcha()
1496          // issued the CAPTCHA and needs to validate it prior to throwing any
1497          // errors.
1498          if (!$form_state['mollom']['require_captcha']) {
1499            $form_state['mollom']['require_captcha'] = TRUE;
1500            form_set_error('mollom][captcha', t('To complete this form, please complete the word verification below.'));
1501          }
1502          break;
1503  
1504        case MOLLOM_ANALYSIS_UNKNOWN:
1505        default:
1506          // If we end up here, something went totally wrong.
1507          _mollom_fallback();
1508          break;
1509      }
1510    }
1511  }
1512  
1513  /**
1514   * Form validation handler for Mollom's CAPTCHA form element.
1515   *
1516   * Validates whether a CAPTCHA was solved correctly. A form may contain a
1517   * CAPTCHA, if it was configured to be protected by a CAPTCHA only, or when the
1518   * text analysis result is "unsure".
1519   */
1520  function mollom_validate_captcha(&$form, &$form_state) {
1521    // CAPTCHA validation may only be skipped, if we do not require it in the
1522    // first place, or if the user already solved a CAPTCHA correctly. We need to
1523    // validate, if $form_state['mollom']['require_captcha'] is TRUE, which is
1524    // either set during initialization of $form_state['mollom'] in
1525    // mollom_process_form(), or after performing text analysis. The second
1526    // return condition, $form_state['mollom']['passed_captcha'], may only ever be
1527    // set by this validation handler and must not be changed elsewhere.
1528   if (!$form_state['mollom']['require_captcha'] || $form_state['mollom']['passed_captcha']) {
1529      return;
1530    }
1531  
1532    // Nothing to validate if there is no value.
1533    // @todo The field is #required, so Form API should already handle this. Add a
1534    //   test to be sure and remove this code.
1535    if (empty($form_state['values']['mollom']['captcha'])) {
1536      form_set_error('mollom][captcha', t('The word verification field is required.'));
1537      return;
1538    }
1539  
1540    // Check the CAPTCHA result.
1541    // Next to the Mollom session id and captcha result, the Mollom back-end also
1542    // takes into account the author's IP and local user id (if registered). Any
1543    // other values are ignored.
1544    $all_data = mollom_form_get_values($form_state['values'], $form_state['mollom']['enabled_fields'], $form_state['mollom']['mapping']);
1545    $data = array(
1546      'session_id' => $form_state['mollom']['response']['session_id'],
1547      'captcha_result' => $form_state['values']['mollom']['captcha'],
1548      'author_ip' => $all_data['author_ip'],
1549      'author_id' => isset($all_data['author_id']) ? $all_data['author_id'] : NULL,
1550    );
1551    $result = mollom('mollom.checkCaptcha', $data);
1552    // Use all available data properties for log messages below.
1553    $data += $all_data;
1554  
1555    // Invoke fallback behavior upon a server error; communication errors are
1556    // handled by mollom() already. A server error may happen in case of an
1557    // expired or invalid session_id.
1558    if ($result === MOLLOM_ERROR) {
1559      return _mollom_fallback();
1560    }
1561  
1562    // Store the response for #submit handlers.
1563    $form_state['mollom']['response']['captcha'] = $result;
1564  
1565    if ($result === TRUE) {
1566      $form_state['mollom']['passed_captcha'] = TRUE;
1567  
1568      _mollom_watchdog(array(
1569        'Correct CAPTCHA' => array(),
1570        'Data:<pre>@data</pre>' => array('@data' => $data),
1571        'Result:<pre>@result</pre>' => array('@result' => $result),
1572      ), WATCHDOG_INFO);
1573    }
1574    else {
1575      form_set_error('mollom][captcha', t('The word verification was not completed correctly. Please complete this new word verification and try again.'));
1576      _mollom_watchdog(array(
1577        'Incorrect CAPTCHA' => array(),
1578        'Data:<pre>@data</pre>' => array('@data' => $data),
1579        'Result:<pre>@result</pre>' => array('@result' => $result),
1580      ), WATCHDOG_INFO);
1581    }
1582  }
1583  
1584  /**
1585   * Form element #pre_render callback for CAPTCHA element.
1586   *
1587   * Conditionally alters the #type of the CAPTCHA form element into a 'hidden'
1588   * element if the response was correct. If it was not, then we empty the value
1589   * of the textfield to allow the user to re-enter a new one.
1590   *
1591   * This #pre_render trick is required, because form API validation does not
1592   * allow form validation handlers to alter the actual form structure. Both the
1593   * form constructor function and the #process callback for the 'mollom' element
1594   * are therefore executed too early (before form validation), so the CAPTCHA
1595   * element still contains not yet validated (default) values.
1596   * We also cannot invoke a form validation handler during form construction or
1597   * processing, because mollom_form_get_values() would be invoked too early
1598   * and therefore $form_state['values'] would not contain any additions from
1599   * form validation functions like mollom_comment_form_validate().
1600   * @see http://drupal.org/node/642702
1601   */
1602  function mollom_pre_render_mollom($element) {
1603    $form_state['mollom'] = &$element['#mollom'];
1604    // Request and inject a CAPTCHA when required; but also in case validation
1605    // through textual analysis failed.
1606    if ($form_state['mollom']['require_captcha'] && !$form_state['mollom']['passed_captcha']) {
1607      $element['captcha']['#required'] = TRUE;
1608      // Empty the CAPTCHA field value, since the user has to re-enter a new one.
1609      $element['captcha']['#value'] = '';
1610  
1611      // Prevent the page cache from storing a form containing a CAPTCHA element.
1612      $GLOBALS['conf']['cache'] = CACHE_DISABLED;
1613  
1614      $data = array();
1615      if (!empty($form_state['mollom']['response']['session_id'])) {
1616        $data['session_id'] = $form_state['mollom']['response']['session_id'];
1617      }
1618      $captcha = mollom_get_captcha('image', $data);
1619  
1620      // If we get a response, add the image CAPTCHA to the form element.
1621      if (isset($captcha['response']['session_id']) && !empty($captcha['markup'])) {
1622        $element['captcha']['#field_prefix'] = $captcha['markup'];
1623  
1624        // If a new session ID was generated by Mollom, flush the old session from
1625        // the database cache.
1626        if (!empty($form_state['mollom']['response']['session_id']) && $captcha['response']['session_id'] != $form_state['mollom']['response']['session_id']) {
1627          cache_clear_all($form_state['mollom']['response']['session_id'], 'cache_mollom');
1628        }
1629        // Assign the session ID returned by Mollom.
1630        $form_state['mollom']['response']['session_id'] = $captcha['response']['session_id'];
1631      }
1632      // Otherwise, we have a communication or configuration error.
1633      // @todo Short-cut form processing entirely in this case; see also
1634      //   mollom_validate_captcha().
1635      else {
1636        $form_state['mollom']['require_analysis'] = FALSE;
1637        $form_state['mollom']['require_captcha'] = FALSE;
1638        return array();
1639      }
1640    }
1641    // If no CAPTCHA is required or the response was correct, hide the CAPTCHA.
1642    elseif (!$form_state['mollom']['require_captcha'] || $form_state['mollom']['passed_captcha']) {
1643      $element['captcha']['#access'] = FALSE;
1644    }
1645  
1646    // If we received a Mollom session id via textual analysis or a CAPTCHA
1647    // request, inject it to the form.
1648    $timestamp = time();
1649    if (!empty($form_state['mollom']['response']['session_id'])) {
1650      $element['session_id']['#value'] = $timestamp . '-' . $form_state['mollom']['response']['session_id'];
1651      cache_set($form_state['mollom']['response']['session_id'], $form_state['mollom'], 'cache_mollom', $timestamp + 21600);
1652    }
1653  
1654    return $element;
1655  }
1656  
1657  /**
1658   * Form submit handler to flush Mollom session and form information from cache.
1659   */
1660  function mollom_form_submit($form, &$form_state) {
1661    // Some modules are implementing multi-step forms without separate form
1662    // submit handlers. In case we reach here and the form will be rebuilt, we
1663    // need to defer our submit handling until final submission.
1664    if (!empty($form_state['rebuild'])) {
1665      return;
1666    }
1667    // If an 'entity' and a 'post_id' mapping was provided via
1668    // hook_mollom_form_info(), try to automatically store Mollom session data.
1669    if (!empty($form_state['mollom']['entity']) && isset($form_state['mollom']['mapping']['post_id'])) {
1670      // For new entities, the entity's form submit handler will have added the
1671      // new entity id value into $form_state['values'], so we need to rebuild the
1672      // data mapping. We do not care for the actual fields, only for the value of
1673      // the mapped post_id.
1674      $data = mollom_form_get_values($form_state['values'], array(), $form_state['mollom']['mapping']);
1675      // We only consider non-empty and non-zero values as valid entity ids.
1676      if (!empty($data['post_id'])) {
1677        mollom_data_save($form_state['mollom']['entity'], $data['post_id']);
1678      }
1679    }
1680    // Flush Mollom session information from database cache.
1681    if (!empty($form_state['mollom']['response']['session_id'])) {
1682      cache_clear_all($form_state['mollom']['response']['session_id'], 'cache_mollom');
1683    }
1684    // Remove Mollom session information from form state to account for unforeseen
1685    // new builds of the form.
1686    unset($form_state['mollom']);
1687  }
1688  
1689  /**
1690   * @} End of "defgroup mollom_form_api".
1691   */
1692  
1693  /**
1694   * Call a remote procedure at the Mollom server.
1695   *
1696   * This function automatically adds the information required to authenticate
1697   * against Mollom.
1698   *
1699   * @todo Currently, this function's return value mixes actual values and
1700   *   error values. We should rewrite the error handling so that calling
1701   *   functions can properly handle error situations.
1702   */
1703  function mollom($method, $data = array()) {
1704    module_load_include('inc', 'mollom');
1705    $messages = array();
1706  
1707    // Initialize refresh variable.
1708    $refresh = FALSE;
1709  
1710    // Enable testing mode.
1711    if (variable_get('mollom_testing_mode', 0)) {
1712      $data['testing'] = TRUE;
1713    }
1714  
1715    // Retrieve the list of Mollom servers from the database.
1716    $servers = variable_get('mollom_servers', array());
1717  
1718    if (empty($servers)) {
1719      // Retrieve a new list of servers.
1720      $servers = _mollom_retrieve_server_list();
1721      // If API keys are invalid, a XML-RPC error code is returned.
1722      if (!is_array($servers)) {
1723        return $servers;
1724      }
1725  
1726      $messages[] = array(
1727        'Refreshed servers: %servers' => array('%servers' => implode(', ', $servers)),
1728      );
1729  
1730      // Store the list of servers in the database.
1731      variable_set('mollom_servers', $servers);
1732    }
1733  
1734    if (is_array($servers)) {
1735      // Send the request to the first server; if that fails, try the other
1736      // servers in the list.
1737      reset($servers);
1738      while ($server = current($servers)) {
1739        $result = xmlrpc($server . '/' . MOLLOM_API_VERSION, $method, $data + _mollom_authentication());
1740  
1741        if ($result === FALSE && ($error = xmlrpc_error())) {
1742          if ($error->code === MOLLOM_REFRESH) {
1743            // Avoid endless loops.
1744            if (!$refresh) {
1745              $refresh = TRUE;
1746  
1747              // Retrieve a new list of valid Mollom servers.
1748              $servers = _mollom_retrieve_server_list();
1749              // If API keys are invalid, the XML-RPC error code is returned.
1750              // To reach this, we must have had a server list (and therefore
1751              // valid keys) before, so we do not immediately return (like above),
1752              // but instead trigger the fallback mode.
1753              if (!is_array($servers)) {
1754                break;
1755              }
1756  
1757              // Reset the list of servers to restart from the first server.
1758              reset($servers);
1759  
1760              // Update the server list.
1761              variable_set('mollom_servers', $servers);
1762  
1763              $messages[] = array(
1764                'Refreshed servers: %servers' => array('%servers' => implode(', ', $servers)),
1765              );
1766            }
1767          }
1768          elseif ($error->code === MOLLOM_REDIRECT) {
1769            // Try the next server in the list.
1770            $next = next($servers);
1771  
1772            $messages[] = array(
1773              'Server %server redirected to: %next.' => array('%server' => $server, '%next' => $next),
1774            );
1775          }
1776          else {
1777            $messages[] = array(
1778              'Error @errno from %server for %method: %message' => array(
1779                '@errno' => $error->code,
1780                '%server' => $server,
1781                '%method' => $method,
1782                '%message' => $error->message,
1783              ),
1784              'Data:<pre>@data</pre>' => array('@data' => $data),
1785            );
1786  
1787            // Instantly return upon a 'real' error.
1788            if ($error->code === MOLLOM_ERROR) {
1789              _mollom_watchdog_multiple($messages, WATCHDOG_ERROR);
1790              return MOLLOM_ERROR;
1791            }
1792            // Otherwise, try the next server.
1793            next($servers);
1794          }
1795        }
1796        else {
1797          _mollom_watchdog_multiple($messages, WATCHDOG_DEBUG);
1798          return $result;
1799        }
1800      }
1801    }
1802  
1803    // If none of the servers worked, activate the fallback mechanism.
1804    // @todo mollom() can be invoked outside of form processing. _mollom_fallback()
1805    //   unconditionally invokes form_set_error(), which always displays the
1806    //   fallback error message. Ideally, we would pass a $verbose argument to
1807    //   _mollom_fallback(), but for that, we'd have to know here already.
1808    //   Consequently, mollom() would need that $verbose argument. In the end, we
1809    //   likely want to either embed the fallback handling into form processing,
1810    //   or introduce a new helper function that is invoked instead of mollom()
1811    //   during form processing.
1812    if ($method != 'mollom.verifyKey') {
1813      _mollom_fallback();
1814    }
1815  
1816    // If everything failed, we reset the server list to force Mollom to request
1817    // a new list.
1818    variable_del('mollom_servers');
1819  
1820    // Report this error.
1821    $messages[] = array(
1822      'All servers unreachable or returning errors. The server list was emptied.' => array(),
1823    );
1824    _mollom_watchdog_multiple($messages, WATCHDOG_ERROR);
1825  
1826    return NETWORK_ERROR;
1827  }
1828  
1829  /**
1830   * Log a Mollom system message.
1831   *
1832   * @param $parts
1833   *   A list of message parts. Each item is an associative array whose keys are
1834   *   log message strings and whose corresponding values are t()-style
1835   *   replacement token arguments. At least one part is required.
1836   * @param $severity
1837   *   The severity of the message, as per RFC 3164. Possible values are
1838   *   WATCHDOG_ERROR, WATCHDOG_WARNING, etc.
1839   *
1840   * @see watchdog()
1841   */
1842  function _mollom_watchdog(array $parts, $severity = WATCHDOG_NOTICE) {
1843    // First message part is required.
1844    $message = key($parts);
1845    $arguments = $parts[$message];
1846    unset($parts[$message]);
1847  
1848    // Hide further message details in the log overview table, if any.
1849    // @see theme_dblog_message()
1850    if ($parts) {
1851      $message = str_pad($message, 56, ' ', STR_PAD_RIGHT);
1852      $message .= '<br />';
1853    }
1854  
1855    // Each item in $parts is a part of the log message.
1856    foreach ($parts as $string => $string_arguments) {
1857      $message .= $string . "\n";
1858      $arguments += $string_arguments;
1859    }
1860  
1861    // Prettify replacement token values, if possible.
1862    foreach ($arguments as $token => $array) {
1863      $flat_value = FALSE;
1864      if (is_array($array)) {
1865        $flat_value = '';
1866        foreach ($array as $key => $value) {
1867          if (is_array($value)) {
1868            $flat_value = FALSE;
1869            break;
1870          }
1871          $value = var_export($value, TRUE);
1872          // Indent the new value, so there is a visual separation from the last.
1873          $flat_value .= "  {$key} = {$value}\n";
1874        }
1875      }
1876      // Only convert one-dimensional arrays, or we would lose debugging data.
1877      if ($flat_value !== FALSE) {
1878        $arguments[$token] = $flat_value;
1879      }
1880      else {
1881        $arguments[$token] = var_export($array, TRUE);
1882      }
1883    }
1884  
1885    watchdog('mollom', $message, $arguments, $severity);
1886  }
1887  
1888  /**
1889   * Helper function for mollom() to invoke watchdog() with cumulative messages.
1890   *
1891   * We do not want false errors to clutter the log, for example, when the server
1892   * list failed, but we were able to retrieve new servers. We therefore collect
1893   * all messages and invoke this function in mollom() right before returning any
1894   * XML-RPC response with the entire stack of collected messages.
1895   * This is also required for tests to pass.
1896   */
1897  function _mollom_watchdog_multiple($messages, $severity) {
1898    foreach ($messages as $message) {
1899      _mollom_watchdog($message, $severity);
1900    }
1901  }
1902  
1903  /**
1904   * Returns version information to send with mollom.verifyKey.
1905   *
1906   * Retrieves platform and module version information for mollom.verifyKey, which
1907   * is normally invoked on Mollom's administration pages only.
1908   *
1909   * This information is solely used to speed up support requests and technical
1910   * inquiries. The data may also be aggregated to help the Mollom staff to make
1911   * decisions on new features or the necessity of back-porting improved
1912   * functionality to older versions.
1913   *
1914   * @return
1915   *   An array containing:
1916   *   - platform_name: The name of the Drupal distribution; i.e., "Drupal".
1917   *   - platform_version: The version of Drupal; e.g., "7.0".
1918   *   - client_name: The module name; i.e., "Mollom".
1919   *   - client_version: The version of the module; e.g., "7.x-1.0".
1920   *
1921   * @see _mollom_status()
1922   */
1923  function _mollom_get_version() {
1924    if ($cache = cache_get('mollom_version')) {
1925      return $cache->data;
1926    }
1927  
1928    // Retrieve Drupal distribution and installation profile information.
1929    include_once  './includes/install.inc';
1930    // Prior to D7, there is no distribution name recorded anywhere, so we have to
1931    // use "Drupal".
1932    $profile_info['distribution_name'] = 'Drupal';
1933    $profile_info['version'] = VERSION;
1934  
1935    // Retrieve Mollom module information.
1936    $mollom_info = db_result(db_query("SELECT info FROM {system} WHERE type = 'module' AND name = 'mollom'"));
1937    $mollom_info = unserialize($mollom_info);
1938    if (empty($mollom_info['version'])) {
1939      // Manually build a module version string for repository checkouts.
1940      $mollom_info['version'] = DRUPAL_CORE_COMPATIBILITY . '-1.x-dev';
1941    }
1942  
1943    $data = array(
1944      'platform_name' => $profile_info['distribution_name'],
1945      'platform_version' => $profile_info['version'],
1946      'client_name' => $mollom_info['name'],
1947      'client_version' => $mollom_info['version'],
1948    );
1949    cache_set('mollom_version', $data);
1950  
1951    return $data;
1952  }
1953  
1954  /**
1955   * Send feedback to Mollom.
1956   */
1957  function _mollom_send_feedback($session_id, $feedback = 'spam') {
1958    $result = mollom('mollom.sendFeedback', array(
1959      'session_id' => $session_id,
1960      'feedback' => $feedback,
1961    ));
1962    _mollom_watchdog(array(
1963      'Reported %feedback for session id %session.' => array('%session' => $session_id, '%feedback' => $feedback),
1964    ));
1965    return $result;
1966  }
1967  
1968  /**
1969   * Fetch the site's Mollom statistics from the API.
1970   *
1971   * @param $refresh
1972   *   A boolean if TRUE, will force the statistics to be re-fetched and stored
1973   *   in the cache.
1974   *
1975   * @return
1976   *   An array of statistics.
1977   */
1978  function mollom_get_statistics($refresh = FALSE) {
1979    $statistics = FALSE;
1980    $cache = cache_get('mollom:statistics');
1981  
1982    // Only fetch if $refresh is TRUE, the cache is empty, or the cache is expired.
1983    if ($refresh || !$cache || time() >= $cache->expire) {
1984      if (_mollom_status() === TRUE) {
1985        $statistics = drupal_map_assoc(array(
1986          'total_days',
1987          'total_accepted',
1988          'total_rejected',
1989          'yesterday_accepted',
1990          'yesterday_rejected',
1991          'today_accepted',
1992          'today_rejected',
1993        ));
1994  
1995        foreach ($statistics as $statistic) {
1996          $result = mollom('mollom.getStatistics', array('type' => $statistic));
1997          if ($result === NETWORK_ERROR || $result === MOLLOM_ERROR) {
1998            // If there was an error, stop fetching statistics and store FALSE
1999            // in the cache. This will help prevent from making unnecessary
2000            // requests to Mollom if the service is down or the server cannot
2001            // connect to the Mollom service.
2002            $statistics = FALSE;
2003            break;
2004          }
2005          else {
2006            $statistics[$statistic] = $result;
2007          }
2008        }
2009      }
2010  
2011      // Cache the statistics and set them to expire in one hour.
2012      cache_set('mollom:statistics', $statistics, 'cache', time() + 3600);
2013    }
2014    else {
2015      $statistics = $cache->data;
2016    }
2017  
2018    return $statistics;
2019  }
2020  
2021  /**
2022   * Implements hook_content_extra_fields().
2023   *
2024   * Allow users of CCK to re-order the CAPTCHA field on node forms through the
2025   * CCK UI.
2026   */
2027  function mollom_content_extra_fields($type_name) {
2028    if ($mollom_form = mollom_form_load($type_name . '_node_form')) {
2029      $extras['mollom'] = array(
2030        'label' => t('Mollom'),
2031        'description' => t('Mollom CAPTCHA or privacy policy link'),
2032        'weight' => 99,
2033      );
2034      return $extras;
2035    }
2036  }
2037  
2038  /**
2039   * Get the HTML markup for a Mollom CAPTCHA.
2040   *
2041   * @param $type
2042   *   The CAPTCHA type to retrieve, e.g. 'image' or 'audio'.
2043   * @param $data
2044   *   An optional array of parameters to send to Mollom when requesting the
2045   *   CAPTCHA.
2046   *
2047   * @return
2048   *   An array with the following key/value pairs:
2049   *     - 'data': An array of parameters sent to Mollom when requesting the
2050   *       CAPTCHA.
2051   *     - 'response': An array with the response from Mollom.
2052   *     - 'markup': The markup of the CAPTCHA HTML.
2053   */
2054  function mollom_get_captcha($type, array $data = array()) {
2055    $data += array(
2056      'author_ip' => ip_address(),
2057      'ssl' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on',
2058    );
2059  
2060    // @todo Convert these to actual theme functions?
2061    $output = '';
2062    switch ($type) {
2063      case 'audio':
2064        $response = mollom('mollom.getAudioCaptcha', $data);
2065        if ($response) {
2066          $source = url(base_path() . drupal_get_path('module', 'mollom') . '/mollom-captcha-player.swf', array(
2067            'query' => array('url' => $response['url']),
2068            'external' => TRUE,
2069          ));
2070          $output = '<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0" width="110" height="50">';
2071          $output .= '<param name="allowFullScreen" value="false" />';
2072          $output .= '<param name="movie" value="' . $source . '" />';
2073          $output .= '<param name="loop" value="false" />';
2074          $output .= '<param name="menu" value="false" />';
2075          $output .= '<param name="quality" value="high" />';
2076          $output .= '<param name="wmode" value="transparent" />';
2077          $output .= '<param name="bgcolor" value="#ffffff" />';
2078          $output .= '<embed src="' . $source . '" loop="false" menu="false" quality="high" wmode="transparent" bgcolor="#ffffff" width="110" height="50" align="baseline" allowScriptAccess="sameDomain" allowFullScreen="false" type="application/x-shockwave-flash" pluginspage="http://www.adobe.com/go/getflashplayer_de" />';
2079          $output .= '</object>';
2080  
2081          $output = '<span class="mollom-captcha-content mollom-audio-captcha">' . $output . '</span>';
2082          $output .= ' (<a href="#" class="mollom-switch-captcha mollom-image-captcha">' . t('verify using image') . '</a>)';
2083        }
2084        break;
2085  
2086      case 'image':
2087        $response = mollom('mollom.getImageCaptcha', $data);
2088        if ($response) {
2089          $captcha = theme('image', $response['url'], t('Type the characters you see in this picture.'), '', NULL, FALSE);
2090          $output = '<span class="mollom-captcha-content mollom-image-captcha">' . $captcha . '</span>';
2091          $output .= ' (<a href="#" class="mollom-switch-captcha mollom-audio-captcha">' . t('verify using audio') . '</a>)';
2092        }
2093        break;
2094    }
2095  
2096    return array(
2097      'data' => $data,
2098      'response' => $response,
2099      'markup' => $output,
2100    );
2101  }
2102  
2103  /**
2104   * Implements hook_mail_alter().
2105   *
2106   * Adds a "report as inappropriate" link to e-mails sent after Mollom-protected
2107   * form submissions.
2108   */
2109  function mollom_mail_alter(&$message) {
2110    // Attached the Mollom report link to any mails with IDs specified from the
2111    // submitted form's hook_mollom_form_info(). This should ensure that the
2112    // report link is added to mails sent by actual users and not any mails sent
2113    // by Drupal since they should never be reported as spam.
2114    if (!empty($GLOBALS['mollom']['mail ids']) && in_array($message['id'], $GLOBALS['mollom']['mail ids'])) {
2115      mollom_mail_add_report_link($message);
2116    }
2117  }
2118  
2119  /**
2120   * Add the 'Report as innapropriate' link to an e-mail message.
2121   */
2122  function mollom_mail_add_report_link(&$message) {
2123    if (!empty($GLOBALS['mollom']['response']['session_id'])) {
2124      mollom_data_save('session', $GLOBALS['mollom']['response']['session_id']);
2125      $report_link = t('Report as inappropriate: @link', array(
2126        '@link' => url('mollom/report/session/' . $GLOBALS['mollom']['response']['session_id'], array('absolute' => TRUE)),
2127      ));
2128      // Until D7, hook_mail_alter() accepts both arrays and strings.
2129      if (is_array($message['body'])) {
2130        $message['body'][] = $report_link;
2131      }
2132      else {
2133        $message['body'] .= "\n\n" . $report_link;
2134      }
2135    }
2136  }
2137  
2138  /**
2139   * @name mollom_node Node module integration for Mollom.
2140   * @{
2141   */
2142  
2143  /**
2144   * Implements hook_mollom_form_list().
2145   */
2146  function node_mollom_form_list() {
2147    $forms = array();
2148    foreach (node_get_types('types') as $type) {
2149      $form_id = $type->type . '_node_form';
2150      $forms[$form_id] = array(
2151        'title' => t('@name form', array('@name' => $type->name)),
2152        'entity' => 'node',
2153        'report access callback' => 'node_mollom_report_access',
2154        'report delete callback' => 'node_mollom_report_delete',
2155      );
2156    }
2157    return $forms;
2158  }
2159  
2160  /**
2161   * Implements hook_mollom_form_info().
2162   */
2163  function node_mollom_form_info($form_id) {
2164    // Retrieve internal type from $form_id.
2165    $nodetype = drupal_substr($form_id, 0, -10);
2166  
2167    $type = node_get_types('type', $nodetype);
2168    $form_info = array(
2169      // @todo This is incompatible with node access.
2170      'bypass access' => array('administer nodes', 'edit any ' . $type->type . ' content'),
2171      'bundle' => $type->type,
2172      'elements' => array(),
2173      'mapping' => array(
2174        'post_id' => 'nid',
2175        'author_name' => 'name',
2176      ),
2177    );
2178    // @see node_content_form()
2179    if ($type->has_title) {
2180      $form_info['elements']['title'] = check_plain($type->title_label);
2181      $form_info['mapping']['post_title'] = 'title';
2182    }
2183    if ($type->has_body) {
2184      $form_info['elements']['body'] = check_plain($type->body_label);
2185    }
2186  
2187    // Add text fields.
2188    if (module_exists('content')) {
2189      $content_info = content_types($type->type);
2190      foreach ($content_info['fields'] as $field_name => $field) {
2191        if ($field['type'] == 'text') {
2192          $form_info['elements'][$field_name] = check_plain(t($field['widget']['label']));
2193        }
2194      }
2195    }
2196  
2197    return $form_info;
2198  }
2199  
2200  /**
2201   * Implements hook_nodeapi().
2202   */
2203  function mollom_nodeapi($node, $op) {
2204    if ($op == 'insert') {
2205      mollom_data_save('node', $node->nid);
2206    }
2207    elseif ($op == 'delete') {
2208      mollom_data_delete('node', $node->nid);
2209    }
2210  }
2211  
2212  /**
2213   * Implements hook_form_FORMID_alter().
2214   *
2215   * Hook into the mass comment administration page and add some operations to
2216   * communicate ham/spam to the XML-RPC server.
2217   *
2218   * @see mollom_node_admin_overview_submit()
2219   */
2220  function mollom_form_node_admin_content_alter(&$form, $form_state) {
2221    module_load_include('inc', 'mollom', 'mollom.admin');
2222    $form['admin']['options']['operation']['#options']['mollom-unpublish'] = t('Report to Mollom and unpublish');
2223    $form['admin']['options']['operation']['#options']['mollom-delete'] = t('Report to Mollom and delete');
2224    $form['#validate'][] = 'mollom_node_admin_overview_submit';
2225  }
2226  
2227  /**
2228   * Mollom report access callback; Determine access to report and delete a node.
2229   */
2230  function node_mollom_report_access($entity, $id) {
2231    $node = node_load($id);
2232    return $node && node_access('delete', $node);
2233  }
2234  
2235  /**
2236   * Mollom report delete callback; Deletes a node.
2237   */
2238  function node_mollom_report_delete($entity, $id) {
2239    node_delete($id);
2240  }
2241  
2242  /**
2243   * @} End of "name mollom_node".
2244   */
2245  
2246  /**
2247   * @name mollom_comment Comment module integration for Mollom.
2248   * @{
2249   */
2250  
2251  /**
2252   * Implements hook_mollom_form_list().
2253   */
2254  function comment_mollom_form_list() {
2255    $forms['comment_form'] = array(
2256      'title' => t('Comment form'),
2257      'entity' => 'comment',
2258      'report access' => array('administer comments'),
2259      'report delete callback' => 'comment_mollom_report_delete',
2260    );
2261    return $forms;
2262  }
2263  
2264  /**
2265   * Implements hook_mollom_form_info().
2266   */
2267  function comment_mollom_form_info($form_id) {
2268    $form_info = array(
2269      'mode' => MOLLOM_MODE_ANALYSIS,
2270      'bypass access' => array('administer comments'),
2271      'elements' => array(
2272        'subject' => t('Subject'),
2273        'comment' => t('Comment'),
2274      ),
2275      'mapping' => array(
2276        'post_id' => 'cid',
2277        'post_title' => 'subject',
2278        // In D6, comment_form() dynamically uses different form elements for
2279        // anonymous users, authenticated users, and comment administrators.
2280        'author_name' => 'name',
2281        'author_mail' => 'mail',
2282        'author_url' => 'homepage',
2283      ),
2284    );
2285  
2286    return $form_info;
2287  }
2288  
2289  /**
2290   * Implements hook_form_FORMID_alter().
2291   *
2292   * When a registered user posts a comment or when a comment administrator edits
2293   * an existing comment, comment_form() does not define 'name' and 'mail' form
2294   * elements, so our form element mapping will fail.
2295   *
2296   * @see comment_mollom_form_info()
2297   * @see mollom_comment_form_validate()
2298   *
2299   * @todo Remove in D7.
2300   */
2301  function mollom_form_comment_form_alter(&$form, &$form_state) {
2302    if (isset($form['author']) || isset($form['admin']['author'])) {
2303      $form['#validate'][] = 'mollom_comment_form_validate';
2304    }
2305  }
2306  
2307  /**
2308   * Form validation handler for comment_form().
2309   *
2310   * @todo Remove in D7.
2311   */
2312  function mollom_comment_form_validate($form, &$form_state) {
2313    // If there were no validation errors, prepare submitted form values for
2314    // validation via Mollom.
2315    if (!form_get_errors()) {
2316      // Author is a registered user or comment is edited by administrator.
2317      if (isset($form_state['values']['author'])) {
2318        // Populate 'name' with value of 'author'.
2319        if (!isset($form_state['values']['name'])) {
2320          form_set_value(array('#parents' => array('name')), $form_state['values']['author'], $form_state);
2321        }
2322        // Populate 'mail' based on corresponding user account.
2323        if (!isset($form_state['values']['mail'])) {
2324          // This should already be validated by comment_validate(), but we still
2325          // double-check that we have a valid account before trying to access it.
2326          $account = user_load(array('name' => $form_state['values']['author']));
2327          if ($account) {
2328            form_set_value(array('#parents' => array('mail')), $account->mail, $form_state);
2329          }
2330        }
2331      }
2332    }
2333  }
2334  
2335  /**
2336   * Implements hook_comment().
2337   */
2338  function mollom_comment($comment, $op) {
2339    if ($op == 'insert') {
2340      mollom_data_save('comment', $comment['cid']);
2341    }
2342    elseif ($op == 'delete') {
2343      mollom_data_delete('comment', $comment->cid);
2344    }
2345  }
2346  
2347  /**
2348   * Implements hook_form_FORMID_alter().
2349   *
2350   * Hook into the mass comment administration page and add some operations to
2351   * communicate ham/spam to the XML-RPC server.
2352   *
2353   * @see mollom_comment_admin_overview_submit()
2354   */
2355  function mollom_form_comment_admin_overview_alter(&$form, $form_state) {
2356    module_load_include('inc', 'mollom', 'mollom.admin');
2357    $form['options']['operation']['#options']['mollom-unpublish'] = t('Report to Mollom and unpublish');
2358    $form['options']['operation']['#options']['mollom-delete'] = t('Report to Mollom and delete');
2359    $form['#submit'][] = 'mollom_comment_admin_overview_submit';
2360  }
2361  
2362  /**
2363   * Mollom report delete callback; Deletes a comment and its replies.
2364   *
2365   * @see comment_confirm_delete_submit()
2366   */
2367  function comment_mollom_report_delete($entity, $id) {
2368    module_load_include('inc', 'comment', 'comment.admin');
2369    $comment = _comment_load($id);
2370    // Delete the comment and its replies.
2371    _comment_delete_thread($comment);
2372    _comment_update_node_statistics($comment->nid);
2373    // Clear the cache so an anonymous user sees that his comment was deleted.
2374    cache_clear_all();
2375  
2376    drupal_set_message(t('The comment has been deleted.'));
2377  }
2378  
2379  /**
2380   * @} End of "name mollom_comment".
2381   */
2382  
2383  /**
2384   * @name mollom_user User module integration for Mollom.
2385   * @{
2386   */
2387  
2388  /**
2389   * Implements hook_mollom_form_list().
2390   */
2391  function user_mollom_form_list() {
2392    $forms['user_register'] = array(
2393      'title' => t('User registration form'),
2394      'entity' => 'user',
2395    );
2396    $forms['user_pass'] = array(
2397      'title' => t('User password request form'),
2398      'entity' => 'user',
2399    );
2400    return $forms;
2401  }
2402  
2403  /**
2404   * Implements hook_mollom_form_info().
2405   */
2406  function user_mollom_form_info($form_id) {
2407    switch ($form_id) {
2408      case 'user_register':
2409        $form_info = array(
2410          'mode' => MOLLOM_MODE_CAPTCHA,
2411          'bypass access' => array('administer users'),
2412          'mapping' => array(
2413            'post_id' => 'uid',
2414            'author_name' => 'name',
2415            'author_mail' => 'mail',
2416          ),
2417        );
2418        return $form_info;
2419  
2420      case 'user_pass':
2421        $form_info = array(
2422          'mode' => MOLLOM_MODE_CAPTCHA,
2423          'bypass access' => array('administer users'),
2424          'mapping' => array(
2425            'post_id' => 'uid',
2426            'author_name' => 'name',
2427            // The 'name' form element accepts either a username or mail address.
2428            'author_mail' => 'name',
2429          ),
2430        );
2431        return $form_info;
2432    }
2433  }
2434  
2435  /**
2436   * @} End of "name mollom_user".
2437   */
2438  
2439  /**
2440   * @name mollom_contact Contact module integration for Mollom.
2441   * @{
2442   */
2443  
2444  /**
2445   * Implements hook_mollom_form_list().
2446   */
2447  function contact_mollom_form_list() {
2448    $forms['contact_mail_page'] = array(
2449      'title' => t('Site-wide contact form'),
2450    );
2451    $forms['contact_mail_user'] = array(
2452      'title' => t('User contact form'),
2453    );
2454    return $forms;
2455  }
2456  
2457  /**
2458   * Implements hook_mollom_form_info().
2459   */
2460  function contact_mollom_form_info($form_id) {
2461    switch ($form_id) {
2462      case 'contact_mail_page':
2463        $form_info = array(
2464          'mode' => MOLLOM_MODE_ANALYSIS,
2465          'bypass access' => array('administer site-wide contact form'),
2466          'mail ids' => array('contact_page_mail'),
2467          'elements' => array(
2468            'subject' => t('Subject'),
2469            'message' => t('Message'),
2470          ),
2471          'mapping' => array(
2472            'post_title' => 'subject',
2473            'author_name' => 'name',
2474            'author_mail' => 'mail',
2475          ),
2476        );
2477        return $form_info;
2478  
2479      case 'contact_mail_user':
2480        $form_info = array(
2481          'mode' => MOLLOM_MODE_ANALYSIS,
2482          'bypass access' => array('administer users'),
2483          'mail ids' => array('contact_user_mail'),
2484          'elements' => array(
2485            'subject' => t('Subject'),
2486            'message' => t('Message'),
2487          ),
2488          'mapping' => array(
2489            'post_title' => 'subject',
2490          ),
2491        );
2492        return $form_info;
2493    }
2494  }
2495  
2496  /**
2497   * @} End of "name mollom_contact".
2498   */


Generated: Thu Mar 24 11:18:33 2011 Cross-referenced by PHPXref 0.7