'; $output .= t('All listed forms below are protected by Mollom, unless users are able to bypass Mollom\'s protection.', array( '@permissions-url' => url('admin/people/permissions', array('fragment' => 'module-mollom')), )); $output .= '
'; $output .= ''; $output .= t('You can add a form to protect, configure already protected forms, or remove the protection.', array( '@add-form-url' => url('admin/config/content/mollom/add'), )); $output .= '
'; return $output; } if ($path == 'admin/config/content/mollom/blacklist') { 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.'); } if ($path == 'admin/help#mollom') { $output = ''; $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 Mollom website or in the Mollom FAQ.", array( '@mollom-website' => 'http://mollom.com', '@mollom-faq' => 'http://mollom.com/faq', ) ); $output .= '
'; $output .= t("Mollom can be used to block all types of spam received on your website's protected forms. Each form can be set to one of the following options:"); $output .= '
'; $output .= t("Data is processed and stored as explained in our Web Service Privacy Policy. 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 How Mollom Works and the Mollom FAQ.", array( '@mollom-privacy' => 'http://mollom.com/service-agreement-free-subscriptions', '@mollom-works' => 'http://mollom.com/how-mollom-works', '@mollom-faq' => 'http://mollom.com/faq') ); $output .= '
'; $output .= ''; $output .= t("If Mollom may not block a spam post for any reason, you can help to train and improve its filters. To do so, choose the appropriate feedback option to report to Mollom when deleting the post on your site."); $output .= '
'; $output .= ''; $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."); $output .= '
'; $output .= ''; $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."); $output .= '
'; $output .= '';
$output .= t("If a blacklist entry contains multiple words, various combinations will be matched. For example, when adding \"replica watches
\" limited to links, the following links will be blocked:");
$output .= '
http://replica-watches.com
http://replica-watches.com/some/path
http://replicawatches.net
http://example.com/replica/watches
<a href="http://example.com">replica watches</a>
'; $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."); $output .= '
'; return $output; } } /** * Implements hook_init(). */ function mollom_init() { // On all Mollom administration pages, check the module configuration and // display the corresponding requirements error, if invalid. if (empty($_POST) && strpos($_GET['q'], 'admin/config/content/mollom') === 0 && user_access('administer mollom')) { // Re-check the status on the settings form only. $status = _mollom_status($_GET['q'] == 'admin/config/content/mollom/settings'); if ($status !== TRUE) { // Fetch and display requirements error message, without re-checking. module_load_install('mollom'); $requirements = mollom_requirements('runtime', FALSE); if (isset($requirements['mollom']['description'])) { drupal_set_message($requirements['mollom']['description'], 'error'); } } } } /** * Implements hook_menu(). */ function mollom_menu() { $items['mollom/report/%/%'] = array( 'title' => 'Report to Mollom', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_report_form', 2, 3), 'access callback' => 'mollom_report_access', 'access arguments' => array(2, 3), 'file' => 'mollom.pages.inc', 'type' => MENU_CALLBACK, ); $items['admin/config/content/mollom'] = array( 'title' => 'Mollom spam blocking and content moderation', 'description' => 'Configure how the Mollom service moderates user-submitted content such as spam and profanity.', 'page callback' => 'mollom_admin_form_list', 'access arguments' => array('administer mollom'), 'file' => 'mollom.admin.inc', ); $items['admin/config/content/mollom/forms'] = array( 'title' => 'Forms', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['admin/config/content/mollom/add'] = array( 'title' => 'Add form', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_configure_form'), 'access arguments' => array('administer mollom'), 'type' => MENU_LOCAL_ACTION, 'file' => 'mollom.admin.inc', ); $items['admin/config/content/mollom/manage/%mollom_form'] = array( 'title' => 'Configure', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_configure_form', 5), 'access arguments' => array('administer mollom'), 'file' => 'mollom.admin.inc', ); $items['admin/config/content/mollom/unprotect/%mollom_form'] = array( 'title' => 'Unprotect form', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_unprotect_form', 5), 'access arguments' => array('administer mollom'), 'file' => 'mollom.admin.inc', ); $items['admin/config/content/mollom/blacklist'] = array( 'title' => 'Blacklists', 'description' => 'Configure blacklists.', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_blacklist_form'), 'access arguments' => array('administer mollom'), 'type' => MENU_LOCAL_TASK, 'file' => 'mollom.admin.inc', ); $items['admin/config/content/mollom/blacklist/spam'] = array( 'title' => 'Spam', 'description' => 'Configure spam blacklist entries.', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['admin/config/content/mollom/blacklist/profanity'] = array( 'title' => 'Profanity', 'description' => 'Configure profanity blacklist entries.', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_blacklist_form', 5), 'access arguments' => array('administer mollom'), 'type' => MENU_LOCAL_TASK, 'file' => 'mollom.admin.inc', ); $items['admin/config/content/mollom/blacklist/unwanted'] = array( 'title' => 'Unwanted', 'description' => 'Configure unwanted blacklist entries.', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_blacklist_form', 5), 'access arguments' => array('administer mollom'), 'type' => MENU_LOCAL_TASK, 'file' => 'mollom.admin.inc', ); $items['admin/config/content/mollom/blacklist/delete'] = array( 'title' => 'Delete', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_blacklist_delete'), 'access arguments' => array('administer mollom'), 'type' => MENU_CALLBACK, 'file' => 'mollom.admin.inc', ); $items['admin/config/content/mollom/settings'] = array( 'title' => 'Settings', 'description' => 'Configure Mollom keys and global settings.', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_settings'), 'access arguments' => array('administer mollom'), 'type' => MENU_LOCAL_TASK, 'file' => 'mollom.admin.inc', ); $items['admin/reports/mollom'] = array( 'title' => 'Mollom statistics', 'description' => 'Reports and usage statistics for the Mollom module.', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_reports_page'), 'access callback' => '_mollom_access', 'access arguments' => array('access mollom statistics'), 'file' => 'mollom.admin.inc', ); // AJAX callback to request new CAPTCHA. $items['mollom/captcha/%/%/%'] = array( 'page callback' => 'mollom_captcha_js', 'page arguments' => array(2, 3, 4), 'access callback' => '_mollom_access', 'file' => 'mollom.pages.inc', 'type' => MENU_CALLBACK, ); return $items; } /** * Access callback; check if the module is configured. * * This function does not actually check whether Mollom keys are valid for the * site, but just if the keys have been entered. * * @param $permission * An optional permission string to check with user_access(). * * @return * TRUE if the module has been configured and user_access() has been checked, * FALSE otherwise. */ function _mollom_access($permission = FALSE) { return variable_get('mollom_public_key', '') && variable_get('mollom_private_key', '') && (!$permission || user_access($permission)); } /** * Menu access callback; Determine access to report to Mollom. * * The special $entity type "session" may be used for mails and messages, which * originate from form submissions protected by Mollom, and can be reported by * anyone; $id is expected to be a Mollom session id instead of an entity id * then. * * @param $entity * The entity type of the data to report. * @param $id * The entity id of the data to report. * * @todo Revamp this based on new {mollom}.form_id info. */ function mollom_report_access($entity, $id) { // The special entity 'session' means that $id is a Mollom session_id, which // can always be reported by everyone. if ($entity == 'session') { return !empty($id) ? TRUE : FALSE; } // Retrieve information about all protectable forms. We use the first valid // definition, because we assume that multiple form definitions just denote // variations of the same entity (e.g. node content types). foreach (mollom_form_list() as $form_id => $info) { if (!isset($info['entity']) || $info['entity'] != $entity) { continue; } // If there is a 'report access callback', invoke it. if (isset($info['report access callback']) && function_exists($info['report access callback'])) { $function = $info['report access callback']; return $function($entity, $id); } // Otherwise, if there is a 'report access' list of permissions, iterate // over them. if (isset($info['report access'])) { foreach ($info['report access'] as $permission) { if (user_access($permission)) { return TRUE; } } } } // If we end up here, then the current user is not permitted to report this // content. return FALSE; } /** * Implements hook_permission(). */ function mollom_permission() { return array( 'administer mollom' => array( 'title' => t('Administer Mollom-protected forms and Mollom settings'), ), 'bypass mollom protection' => array( 'title' => t('Bypass Mollom protection on forms'), ), 'access mollom statistics' => array( 'title' => t('View Mollom statistics'), ), ); } /** * Implements hook_modules_installed(). */ function mollom_modules_installed($modules) { drupal_static_reset('mollom_get_form_info'); } /** * Implements hook_modules_uninstalled(). */ function mollom_modules_uninstalled($modules) { db_delete('mollom_form')->condition('module', $modules)->execute(); } /** * Implements hook_cron(). */ function mollom_cron() { // Mollom session data auto-expires after 6 months. $expired = REQUEST_TIME - 86400 * 30 * 6; db_delete('mollom') ->condition('changed', $expired, '<') ->execute(); } /** * Load a Mollom data record from the database. * * @param $entity * The entity type to retrieve data for. * @param $id * The entity id to retrieve data for. */ function mollom_data_load($entity, $id) { return db_query_range('SELECT * FROM {mollom} WHERE entity = :entity AND id = :id', 0, 1, array(':entity' => $entity, ':id' => $id))->fetchObject(); } /** * Save Mollom validation data to the database. * * Based on the specified entity type and id, this function stores the * validation results returned by Mollom in the database. * * The special $entity type "session" may be used for mails and messages, which * originate from form submissions protected by Mollom, and can be reported by * anyone; $id is expected to be a Mollom session id instead of an entity id * then. * * @param $data * An object containing Mollom session data for the entity, containing at * least the following properties: * - entity: The entity type of the data to save. * - id: The entity ID the data belongs to. * - form_id: The form ID the session data belongs to. * - session_id: The session ID returned by Mollom. * And optionally: * - spam: A spam check result integer returned by Mollom, which can be * MOLLOM_ANALYSIS_SPAM, MOLLOM_ANALYSIS_UNSURE, or MOLLOM_ANALYSIS_HAM. * - quality: A rating of the content's quality, in the range of 0 and 1.0. * - profanity: A profanity check rating returned by Mollom, in the range of * 0 and 1.0. * - languages: An array containing language codes the content might be * written in. */ function mollom_data_save($data) { $data->changed = REQUEST_TIME; // Convert languages array into a string. // @todo This conversion and data handling is not correct; needs work. if (isset($data->languages) && is_array($data->languages)) { $data->languages = implode(' ', $data->languages); } $update = db_query_range("SELECT 'id' FROM {mollom} WHERE entity = :entity AND id = :id", 0, 1, array( ':entity' => $data->entity, ':id' => $data->id, ))->fetchField(); drupal_write_record('mollom', $data, $update ? $update : array()); if (!$update) { module_invoke_all('mollom_data_insert', $data); } else { module_invoke_all('mollom_data_update', $data); } return $data; } /** * Updates stored Mollom session data to mark a bad post as moderated. * * @param $entity * The entity type of the moderated post. * @param $id * The entity id of the moderated post. */ function mollom_data_moderate($entity, $id) { $data = mollom_data_load($entity, $id); // Nothing to do, if no data exists. if (!$data) { return; } // Report the session to Mollom. _mollom_send_feedback($data->session_id, 'ham'); // Mark the session data as moderated. $data->moderate = 0; mollom_data_save($data); } /** * Deletes a Mollom session data record from the database. * * @param $entity * The entity type to delete data for. * @param $id * The entity id to delete data for. */ function mollom_data_delete($entity, $id) { return mollom_data_delete_multiple($entity, array($id)); } /** * Deletes multiple Mollom session data records from the database. * * @param $entity * The entity type to delete data for. * @param $ids * An array of entity ids to delete data for. */ function mollom_data_delete_multiple($entity, array $ids) { foreach ($ids as $id) { $data = mollom_data_load($entity, $id); module_invoke_all('mollom_data_delete', $data); } return db_delete('mollom')->condition('entity', $entity)->condition('id', $ids)->execute(); } /** * Helper function to add Mollom feedback options to confirmation forms. */ function mollom_data_delete_form_alter(&$form, &$form_state) { if (!isset($form['description']['#weight'])) { $form['description']['#weight'] = 90; } $form['mollom'] = array( '#tree' => TRUE, '#weight' => 80, ); $form['mollom']['feedback'] = array( '#type' => 'radios', '#title' => t('Report as inappropriate'), '#options' => array( 0 => t('Do not report'), 'spam' => t('Spam, unsolicited advertising'), 'profanity' => t('Obscene, violent, profane'), 'low-quality' => t('Low-quality'), 'unwanted' => t('Unwanted, taunting, off-topic'), ), '#default_value' => 0, '#description' => t('Sending feedback to Mollom improves the automated moderation of new submissions.', array('@mollom-url' => 'http://mollom.com')), ); } /** * Send feedback to Mollom and delete Mollom data. * * @see mollom_form_alter() */ function mollom_data_delete_form_submit($form, &$form_state) { $forms = mollom_form_cache(); $mollom_form = mollom_form_load($forms['delete'][$form_state['values']['form_id']]); $data = mollom_form_get_values($form_state['values'], $mollom_form['enabled_fields'], $mollom_form['mapping']); $entity = $mollom_form['entity']; $id = $data['post_id']; if (!empty($form_state['values']['mollom']['feedback'])) { if (mollom_data_report($entity, $id, $form_state['values']['mollom']['feedback']) === TRUE) { drupal_set_message(t('The content was successfully reported as inappropriate.')); } } // Remove Mollom session data. mollom_data_delete($entity, $id); } /** * Sends feedback for a Mollom session data record. * * @param $entity * The entity type to send feedback for. * @param $id * The entity id to send feedback for. */ function mollom_data_report($entity, $id, $feedback) { return mollom_data_report_multiple($entity, array($id), $feedback); } /** * Sends feedback for multiple Mollom session data records. * * @param $entity * The entity type to send feedback for. * @param $ids * An array of entity ids to send feedback for. */ function mollom_data_report_multiple($entity, array $ids, $feedback) { $return = TRUE; foreach ($ids as $id) { // Load the Mollom session data. $data = mollom_data_load($entity, $id); // Send feedback, if we have session data. if (isset($data->session_id)) { $result = _mollom_send_feedback($data->session_id, $feedback); $return = $return && $result; } } return $return; } /** * Implements hook_form_alter(). * * This function intercepts all forms in Drupal and Mollom-enables them if * necessary. */ function mollom_form_alter(&$form, &$form_state, $form_id) { // Skip installation and update forms. if (defined('MAINTENANCE_MODE')) { return; } // Verify global Mollom configuration status. $status = _mollom_status(); if ($status !== TRUE) { return; } // Retrieve a list of all protected forms once. $forms = mollom_form_cache(); // Remind of enabled testing mode on all protected forms. if (isset($forms['protected'][$form_id]) || strpos($_GET['q'], 'admin/config/content/mollom') === 0) { _mollom_testing_mode_warning(); } // Site administrators don't have their content checked with Mollom. if (!user_access('bypass mollom protection')) { // Retrieve configuration for this form. if (isset($forms['protected'][$form_id]) && ($mollom_form = mollom_form_load($form_id))) { // Determine whether to bypass validation for the current user. foreach ($mollom_form['bypass access'] as $permission) { if (user_access($permission)) { return; } } // Add Mollom form widget. $form['mollom'] = array( '#type' => 'mollom', '#mollom_form' => $mollom_form, // #type 'actions' defaults to 100. '#weight' => (isset($form['actions']['#weight']) ? $form['actions']['#weight'] - 1 : 99), '#tree' => TRUE, ); // Enable caching of this form; required for our form validation and // submit handlers. $form_state['cache'] = TRUE; // Add Mollom form validation handlers. $form['#validate'][] = 'mollom_validate_analysis'; $form['#validate'][] = 'mollom_validate_captcha'; $form['#validate'][] = 'mollom_validate_post'; // Prepend a submit handler to clean up internal Mollom values from // $form_state['values']. array_unshift($form['#submit'], 'mollom_form_pre_submit'); // Append a submit handler to store Mollom session data. Requires that // the primary submit handler has run already, so a potential 'post_id' // mapping can be retrieved from $form_state['values']. $form['#submit'][] = 'mollom_form_submit'; // Add link to privacy policy on forms protected via textual analysis, // if enabled. if ($mollom_form['mode'] == MOLLOM_MODE_ANALYSIS && variable_get('mollom_privacy_link', 1)) { $form['mollom']['privacy'] = array( '#prefix' => '@data' => array('@data' => $data), )); $data = FALSE; } return $data; } /** * Recursive helper function to flatten nested form values. * * Takes a potentially nested array and moves all nested keys to the top-level. */ function _mollom_flatten_form_values(&$values) { foreach ($values as $key => $value) { if (is_array($value)) { $values += _mollom_flatten_form_values($value); unset($values[$key]); } } return $values; } /** * Helper function to return OpenID identifiers associated with a given user account. */ function _mollom_get_openid($account) { if (isset($account->uid)) { $ids = db_query('SELECT authname FROM {authmap} WHERE module = :module AND uid = :uid', array(':module' => 'openid', ':uid' => $account->uid))->fetchCol(); if (!empty($ids)) { return implode($ids, ' '); } } } /** * Returns the (last known) status of the configured Mollom API keys. * * @param $reset * (optional) Boolean whether to reset the stored state and re-check. * Defaults to FALSE. * * @return * TRUE if the module is considered operable, or an associative array * describing the current status of the module: * - keys: Boolean whether Mollom API keys have been configured. * - keys valid: TRUE if Mollom API keys are valid, or the error code as * returned by Mollom servers. * - servers: Boolean whether there is a non-empty list of Mollom servers. * * @see mollom_init() * @see mollom_admin_settings() * @see mollom_requirements() */ function _mollom_status($reset = FALSE) { // Load stored status. $status = variable_get('mollom_status', array( 'keys' => FALSE, 'keys valid' => FALSE, )); // Both API keys are required. $public_key = variable_get('mollom_public_key', ''); $private_key = variable_get('mollom_private_key', ''); $status['keys'] = (!empty($public_key) && !empty($private_key)); // If we have keys and are asked to reset, check whether keys are valid. if ($status['keys'] && $reset) { $status['keys valid'] = mollom('mollom.verifyKey', _mollom_get_version()); } // In case of an error, indicate whether we have a non-empty server list. if ($status['keys valid'] !== TRUE) { $servers = variable_get('mollom_servers', array()); $status['servers'] = !empty($servers); } // Update stored status upon reset. if ($reset) { variable_set('mollom_status', $status); } return ($status['keys valid'] === TRUE ? TRUE : $status); } /** * Outputs a warning message about enabled testing mode (once). */ function _mollom_testing_mode_warning() { $warned = &drupal_static(__FUNCTION__); if (isset($warned)) { return; } $warned = TRUE; if (variable_get('mollom_testing_mode', 0) && empty($_POST)) { $admin_message = ''; if (user_access('administer mollom') && $_GET['q'] != 'admin/config/content/mollom/settings') { $admin_message = t('Visit the Mollom settings page to disable it.', array( '@settings-url' => url('admin/config/content/mollom/settings'), )); } $message = t('Mollom testing mode is still enabled. !admin-message', array( '!admin-message' => $admin_message, )); drupal_set_message($message, 'warning'); } } /** * Helper function to log and optionally output an error message when Mollom servers are unavailable. */ function _mollom_fallback() { $fallback = variable_get('mollom_fallback', MOLLOM_FALLBACK_BLOCK); // @todo Prevents mollom_admin_settings() from implementing a proper form // validation. Add !empty($_POST) to this condition + manually invoke from // mollom_process_form() on GET requests? Or don't call it from mollom()? // Anything, but just don't mix FAPI logic into XML-RPC logic. if ($fallback == MOLLOM_FALLBACK_BLOCK) { 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.")); } $servers = variable_get('mollom_servers', array()); _mollom_watchdog(array( 'All servers unavailable: %servers' => array('%servers' => $servers ? implode(', ', $servers) : '--'), 'Last error: @code %message' => array('@code' => xmlrpc_errno(), '%message' => xmlrpc_error_msg()), ), WATCHDOG_ERROR); } /** * @defgroup mollom_form_api Mollom Form API workarounds * @{ * Various helper functions to work around bugs in Form API. * * Mollom's integration with Form API is quite simple: * - If a form is protected by Mollom, we setup initial information * about the session and the form in $form_state['mollom']. * - We mainly work in and after form validation. Textual analysis validates all * values in the form as a form validation handler. If this validation fails, * we alter the form (during validation) to add a CAPTCHA. If the CAPTCHA * response is invalid, we still alter the form during validation to display a * new CAPTCHA, but without the previously entered value. * - Form API keeps our $form_state information, because we short-cut form * rebuilding by issueing a form validation error until the submitted form * values are valid. * - In short, very roughly: * - Form construction: Nothing. * - Form processing: Nothing. * - Form validation: Perform validation and alterations based on validation. * * @see mollom_form_alter() */ /** * Implements hook_element_info(). */ function mollom_element_info() { return array( 'mollom' => array( '#process' => array( 'mollom_process_mollom', ), ), ); } /** * Implements hook_theme(). */ function mollom_theme() { return array( 'mollom_admin_blacklist_form' => array( 'render element' => 'form', 'file' => 'mollom.admin.inc', ), ); } /** * Form element #process callback for the 'mollom' element. * * The 'mollom' form element is stateful. The Mollom session ID that is exchanged * between Drupal, the Mollom back-end, and the user allows us to keep track of * the form validation state. * * The session ID is valid for a given $form_id only. We expire it as soon as * the form is submitted, to avoid it being replayed. */ function mollom_process_mollom($element, &$form_state, $complete_form) { // Setup initial Mollom session and form information. if (empty($form_state['mollom'])) { $form_state['mollom'] = array( 'require_analysis' => $element['#mollom_form']['mode'] == MOLLOM_MODE_ANALYSIS, 'require_captcha' => $element['#mollom_form']['mode'] == MOLLOM_MODE_CAPTCHA, 'passed_captcha' => FALSE, 'require_moderation' => FALSE, 'response' => array( 'session_id' => '', ), ); } $form_state['mollom'] += $element['#mollom_form']; // By default, bad form submissions are discarded, unless the form was // configured to moderate bad posts. 'discard' may only be FALSE, if there is // a valid 'moderation callback'. Otherwise, it must be TRUE. if (empty($form_state['mollom']['moderation callback']) || !function_exists($form_state['mollom']['moderation callback'])) { $form_state['mollom']['discard'] = TRUE; } // Add the Mollom session element. $element['session_id'] = array( '#type' => 'hidden', '#default_value' => isset($form_state['mollom']['response']['session_id']) ? $form_state['mollom']['response']['session_id'] : '', '#attributes' => array('class' => 'mollom-session-id'), ); // Add the CAPTCHA element. $element['captcha'] = array( '#type' => 'textfield', '#title' => t('Word verification'), '#size' => 10, '#default_value' => '', '#description' => t("Type the characters you see in the picture above; if you can't read them, submit the form and a new image will be generated. Not case sensitive."), ); // Request and inject a CAPTCHA when required; but also in case validation // through textual analysis failed. if ($form_state['mollom']['require_captcha'] && !$form_state['mollom']['passed_captcha']) { $element['captcha']['#required'] = TRUE; // Prevent the page cache from storing a form containing a CAPTCHA element. $GLOBALS['conf']['cache'] = 0; $data = array(); if (!empty($form_state['mollom']['response']['session_id'])) { $data['session_id'] = $form_state['mollom']['response']['session_id']; } $captcha = mollom_get_captcha('image', $data); // If we get a response, add the image CAPTCHA to the form element. if (isset($captcha['response']['session_id']) && !empty($captcha['markup'])) { $element['captcha']['#field_prefix'] = $captcha['markup']; // Assign the session ID returned by Mollom. $form_state['mollom']['response']['session_id'] = $captcha['response']['session_id']; $element['session_id']['#value'] = $captcha['response']['session_id']; } // Otherwise, we have a communication or configuration error. // @todo Short-cut form processing entirely in this case; see also // mollom_validate_captcha(). else { $form_state['mollom']['require_analysis'] = FALSE; $form_state['mollom']['require_captcha'] = FALSE; return array(); } } // If no CAPTCHA is required or the response was correct, hide the CAPTCHA. elseif (!$form_state['mollom']['require_captcha'] || $form_state['mollom']['passed_captcha']) { $element['captcha']['#access'] = FALSE; } // Add a spambot trap. Purposively use 'homepage' as field name. // @todo Use a random field name (from the usual names of 'name', 'email', // 'url', etc.) to make it harder to identify this trap. $element['homepage'] = array( '#type' => 'textfield', // Wrap the entire honeypot form element markup into a hidden container, so // robots cannot simply check for a style attribute, but instead have to // implement advanced DOM processing to figure out whether they are dealing // with a honeypot field. '#prefix' => ' ', ); // Make Mollom form and session information available to entirely different // functions. $GLOBALS['mollom'] = &$form_state['mollom']; return $element; } /** * Form validation handler to perform textual analysis of submitted form values. * * Validation needs to re-run in case of a form validation error (elsewhere in * the form). In case Mollom's textual analysis returns no definite result, we * must trigger a CAPTCHA, but text analysis is always performed, even if the * CAPTCHA was solved correctly. */ function mollom_validate_analysis(&$form, &$form_state) { // Text analysis may only ever be skipped, if we do not require it in the // first place. With regard to that, $form_state['mollom']['require_analysis'] // is only set once during initialization of $form_state['mollom'] in // mollom_process_form() and must not be updated elsewhere. if (!$form_state['mollom']['require_analysis']) { return; } // Perform textual analysis. $all_data = mollom_form_get_values($form_state['values'], $form_state['mollom']['enabled_fields'], $form_state['mollom']['mapping']); // Cancel processing upon invalid UTF-8 data. if ($all_data === FALSE) { return; } $data = $all_data; // Remove 'post_id' property; only used by mollom_form_submit(). if (isset($data['post_id'])) { unset($data['post_id']); } $data['session_id'] = $form_state['mollom']['response']['session_id']; $data['checks'] = implode(',', $form_state['mollom']['checks']); $result = mollom('mollom.checkContent', $data); // Use all available data properties for log messages below. $data += $all_data; // Trigger global fallback behavior if there is no result. if (!isset($result['session_id'])) { return _mollom_fallback(); } // Store the response returned by Mollom. $form_state['mollom']['response'] = $result; $form['mollom']['session_id']['#value'] = $result['session_id']; // Prepare watchdog message teaser text. $teaser = truncate_utf8(strip_tags(isset($data['post_title']) ? $data['post_title'] : isset($data['post_body']) ? $data['post_body'] : '--'), 40); // Handle the profanity check result. if (isset($result['profanity']) && $result['profanity'] >= 0.5) { if ($form_state['mollom']['discard']) { form_set_error('mollom', t('Your submission has triggered the profanity filter and will not be accepted until the inappropriate language is removed.')); } else { $form_state['mollom']['require_moderation'] = TRUE; } _mollom_watchdog(array( 'Profanity: %teaser' => array('%teaser' => $teaser), 'Data:
@data' => array('@data' => $data), 'Result:
@result' => array('@result' => $result), )); } // Handle the spam check result. // The Mollom backend is remembering results of previous mollom.checkContent // invocations for a single user/post session. When content is re-checked // during form validation, the result may change according to the values that // have been submitted (which e.g. can change during previews). Only in case // the spam check led to a MOLLOM_ANALYSIS_UNSURE result, and the user solved // the CAPTCHA correctly, subsequent spam check results will likely be // MOLLOM_ANALYSIS_HAM (though not guaranteed). if (isset($result['spam'])) { switch ($result['spam']) { case MOLLOM_ANALYSIS_HAM: $form_state['mollom']['require_captcha'] = FALSE; _mollom_watchdog(array( 'Ham: %teaser' => array('%teaser' => $teaser), 'Data:
@data' => array('@data' => $data), 'Result:
@result' => array('@result' => $result), ), WATCHDOG_INFO); break; case MOLLOM_ANALYSIS_SPAM: $form_state['mollom']['require_captcha'] = FALSE; if ($form_state['mollom']['discard']) { form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.')); } else { $form_state['mollom']['require_moderation'] = TRUE; } _mollom_watchdog(array( 'Spam: %teaser' => array('%teaser' => $teaser), 'Data:
@data' => array('@data' => $data), 'Result:
@result' => array('@result' => $result), )); break; case MOLLOM_ANALYSIS_UNSURE: _mollom_watchdog(array( 'Unsure: %teaser' => array('%teaser' => $teaser), 'Data:
@data' => array('@data' => $data), 'Result:
@result' => array('@result' => $result), ), WATCHDOG_INFO); // Only throw a validation error and retrieve a CAPTCHA, if we check // this post for the first time. Otherwise, mollom_validate_captcha() // issued the CAPTCHA and needs to validate it prior to throwing any // errors. if (!$form_state['mollom']['require_captcha']) { $form_state['mollom']['require_captcha'] = TRUE; form_set_error('mollom][captcha', t('To complete this form, please complete the word verification below.')); $form['mollom']['captcha']['#access'] = TRUE; $form['mollom']['captcha']['#required'] = TRUE; $captcha_data = array( 'session_id' => $result['session_id'], ); $captcha = mollom_get_captcha('image', $captcha_data); // If we get a response, add the image CAPTCHA to the form element. if (isset($captcha['response']['session_id']) && !empty($captcha['markup'])) { $form_state['mollom']['response']['session_id'] = $captcha['response']['session_id']; $form['mollom']['session_id']['#value'] = $captcha['response']['session_id']; $form['mollom']['captcha']['#field_prefix'] = $captcha['markup']; } } break; case MOLLOM_ANALYSIS_UNKNOWN: default: // If we end up here, something went totally wrong. _mollom_fallback(); break; } } } /** * Form validation handler for Mollom's CAPTCHA form element. * * Validates whether a CAPTCHA was solved correctly. A form may contain a * CAPTCHA, if it was configured to be protected by a CAPTCHA only, or when the * text analysis result is "unsure". */ function mollom_validate_captcha(&$form, &$form_state) { // CAPTCHA validation may only be skipped, if we do not require it in the // first place, or if the user already solved a CAPTCHA correctly. We need to // validate, if $form_state['mollom']['require_captcha'] is TRUE, which is // either set during initialization of $form_state['mollom'] in // mollom_process_form(), or after performing text analysis. The second // return condition, $form_state['mollom']['passed_captcha'], may only ever be // set by this validation handler and must not be changed elsewhere. if (!$form_state['mollom']['require_captcha'] || $form_state['mollom']['passed_captcha']) { $form['mollom']['captcha']['#access'] = FALSE; return; } // Nothing to validate if there is no value. // @todo The field is #required, so Form API should already handle this. Add a // test to be sure and remove this code. if (empty($form_state['values']['mollom']['captcha'])) { return; } // Check the CAPTCHA result. // Next to the Mollom session id and captcha result, the Mollom back-end also // takes into account the author's IP and local user id (if registered). Any // other values are ignored. $all_data = mollom_form_get_values($form_state['values'], $form_state['mollom']['enabled_fields'], $form_state['mollom']['mapping']); // Cancel processing upon invalid UTF-8 data. if ($all_data === FALSE) { return; } $data = array( 'session_id' => $form_state['mollom']['response']['session_id'], 'captcha_result' => $form_state['values']['mollom']['captcha'], 'author_ip' => $all_data['author_ip'], ); if (isset($all_data['author_id'])) { $data['author_id'] = $all_data['author_id']; } if (isset($all_data['honeypot'])) { $data['honeypot'] = $all_data['honeypot']; } $result = mollom('mollom.checkCaptcha', $data); // Use all available data properties for log messages below. $data += $all_data; // Invoke fallback behavior upon a server error; communication errors are // handled by mollom() already. A server error may happen in case of an // expired or invalid session_id. if ($result === MOLLOM_ERROR) { return _mollom_fallback(); } // Store the response for #submit handlers. $form_state['mollom']['response']['captcha'] = $result; $form['mollom']['session_id']['#value'] = $form_state['mollom']['response']['session_id']; if ($result === TRUE) { $form_state['mollom']['passed_captcha'] = TRUE; $form['mollom']['captcha']['#access'] = FALSE; _mollom_watchdog(array( 'Correct CAPTCHA' => array(), 'Data:
@data' => array('@data' => $data), 'Result:
@result' => array('@result' => $result), ), WATCHDOG_INFO); } else { // UX: Empty the CAPTCHA field value, as the user has to re-enter a new one. $form['mollom']['captcha']['#value'] = ''; form_set_error('mollom][captcha', t('The word verification was not completed correctly. Please complete this new word verification and try again.')); _mollom_watchdog(array( 'Incorrect CAPTCHA' => array(), 'Data:
@data' => array('@data' => $data), 'Result:
@result' => array('@result' => $result), ), WATCHDOG_INFO); } } /** * Form validation handler to perform post-validation tasks. */ function mollom_validate_post(&$form, &$form_state) { // Retain a post instead of discarding it. If 'discard' is FALSE, then the // 'moderation callback' is responsible for altering $form_state in a way that // the post ends up in a moderation queue. Most callbacks will only want to // set or change a value in $form_state. if ($form_state['mollom']['require_moderation']) { $function = $form_state['mollom']['moderation callback']; $function($form, $form_state); } } /** * Form submit handler to clean up internal Mollom values from $form_state['values']. * * Some form submit handlers blindly take over and save all submitted form * values in $form_state['values'] into the database. To prevent Mollom's * internal values from being mistakenly stored somewhere else, this submit * handler is prepended to the stack of $form['#submit'] handlers of protected * forms. * * @todo Fix Drupal core to remove the need for separately prepended submit * handlers like this one by making form_state_values_clean() invoke a hook. * @see http://drupal.org/node/939510 */ function mollom_form_pre_submit($form, &$form_state) { // Some modules are implementing multi-step forms without separate form // submit handlers. In case we reach here and the form will be rebuilt, we // need to defer our submit handling until final submission. if (!empty($form_state['rebuild'])) { return; } // When having passed the form validation stage and reaching the form // submission stage, all submitted form values have been processed into // $form_state['mollom'] already, so the entire top-level 'mollom' key can be // safely removed. unset($form_state['values']['mollom']); } /** * Form submit handler to flush Mollom session and form information from cache. */ function mollom_form_submit($form, &$form_state) { // Some modules are implementing multi-step forms without separate form // submit handlers. In case we reach here and the form will be rebuilt, we // need to defer our submit handling until final submission. if (!empty($form_state['rebuild'])) { return; } // If an 'entity' and a 'post_id' mapping was provided via // hook_mollom_form_info(), try to automatically store Mollom session data. if (!empty($form_state['mollom']['entity']) && isset($form_state['mollom']['mapping']['post_id'])) { // For new entities, the entity's form submit handler will have added the // new entity id value into $form_state['values'], so we need to rebuild the // data mapping. We do not care for the actual fields, only for the value of // the mapped post_id. $values = mollom_form_get_values($form_state['values'], array(), $form_state['mollom']['mapping']); // We only consider non-empty and non-zero values as valid entity ids. if (!empty($values['post_id'])) { // Save the Mollom session data. $data = (object) $form_state['mollom']['response']; $data->entity = $form_state['mollom']['entity']; $data->id = $values['post_id']; $data->form_id = $form_state['mollom']['form_id']; // Set the moderation flag for forms accepting bad posts. $data->moderate = $form_state['mollom']['require_moderation']; $form_state['mollom']['data'] = mollom_data_save($data); } } } /** * @} End of "defgroup mollom_form_api". */ /** * Call a remote procedure at the Mollom server. * * This function automatically adds the information required to authenticate * against Mollom. * * @todo Currently, this function's return value mixes actual values and * error values. We should rewrite the error handling so that calling * functions can properly handle error situations. */ function mollom($method, $data = array()) { module_load_include('inc', 'mollom'); $messages = array(); // Initialize refresh variable. $refresh = FALSE; // Enable testing mode. if (variable_get('mollom_testing_mode', 0)) { $data['testing'] = TRUE; } // Retrieve the list of Mollom servers from the database. $servers = variable_get('mollom_servers', array()); if (empty($servers)) { // Retrieve a new list of servers. $servers = _mollom_retrieve_server_list(); // If API keys are invalid, a XML-RPC error code is returned. if (!is_array($servers)) { return $servers; } $messages[] = array( 'Refreshed servers: %servers' => array('%servers' => implode(', ', $servers)), ); // Store the list of servers in the database. variable_set('mollom_servers', $servers); } if (is_array($servers)) { // Send the request to the first server; if that fails, try the other // servers in the list. reset($servers); while ($server = current($servers)) { $result = xmlrpc($server . '/' . MOLLOM_API_VERSION, array( $method => array($data + _mollom_authentication()), )); if ($result === FALSE && ($error = xmlrpc_error())) { if ($error->code === MOLLOM_REFRESH) { // Avoid endless loops. if (!$refresh) { $refresh = TRUE; // Retrieve a new list of valid Mollom servers. $servers = _mollom_retrieve_server_list(); // If API keys are invalid, the XML-RPC error code is returned. // To reach this, we must have had a server list (and therefore // valid keys) before, so we do not immediately return (like above), // but instead trigger the fallback mode. if (!is_array($servers)) { break; } // Reset the list of servers to restart from the first server. reset($servers); // Update the server list. variable_set('mollom_servers', $servers); $messages[] = array( 'Refreshed servers: %servers' => array('%servers' => implode(', ', $servers)), ); } } elseif ($error->code === MOLLOM_REDIRECT) { // Try the next server in the list. $next = next($servers); $messages[] = array( 'Server %server redirected to: %next.' => array('%server' => $server, '%next' => $next), ); } else { $messages[] = array( 'Error @errno from %server for %method: %message' => array( '@errno' => $error->code, '%server' => $server, '%method' => $method, '%message' => $error->message, ), 'Data:
@data' => array('@data' => $data), ); // Instantly return upon a 'real' error. if ($error->code === MOLLOM_ERROR) { _mollom_watchdog_multiple($messages, WATCHDOG_ERROR); return MOLLOM_ERROR; } // Otherwise, try the next server. next($servers); } } else { _mollom_watchdog_multiple($messages, WATCHDOG_DEBUG); return $result; } } } // If none of the servers worked, activate the fallback mechanism. // @todo mollom() can be invoked outside of form processing. _mollom_fallback() // unconditionally invokes form_set_error(), which always displays the // fallback error message. Ideally, we would pass a $verbose argument to // _mollom_fallback(), but for that, we'd have to know here already. // Consequently, mollom() would need that $verbose argument. In the end, we // likely want to either embed the fallback handling into form processing, // or introduce a new helper function that is invoked instead of mollom() // during form processing. if ($method != 'mollom.verifyKey') { _mollom_fallback(); } // If everything failed, we reset the server list to force Mollom to request // a new list. variable_del('mollom_servers'); // Report this error. $messages[] = array( 'All servers unreachable or returning errors. The server list was emptied.' => array(), ); _mollom_watchdog_multiple($messages, WATCHDOG_ERROR); return NETWORK_ERROR; } /** * Log a Mollom system message. * * @param $parts * A list of message parts. Each item is an associative array whose keys are * log message strings and whose corresponding values are t()-style * replacement token arguments. At least one part is required. * @param $severity * The severity of the message, as per RFC 3164. Possible values are * WATCHDOG_ERROR, WATCHDOG_WARNING, etc. * * @see watchdog() */ function _mollom_watchdog(array $parts, $severity = WATCHDOG_NOTICE) { // First message part is required. $message = key($parts); $arguments = $parts[$message]; unset($parts[$message]); // Hide further message details in the log overview table, if any. // @see theme_dblog_message() if ($parts) { $message = str_pad($message, 56, ' ', STR_PAD_RIGHT); $message .= '