'; $output .= t('All listed forms below are protected by Mollom, unless users are able to bypass Mollom\'s protection.', array( '@permissions-url' => url('admin/user/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/settings/mollom/add'), )); $output .= '

'; return $output; } if ($path == 'admin/settings/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 .= '

' . t('Mollom blacklist') . '

'; $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 .= '

'; $output .= ''; $output .= '

'; $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/settings/mollom') === 0 && user_access('administer mollom')) { // Re-check the status on the settings form only. $status = _mollom_status($_GET['q'] == 'admin/settings/mollom/settings'); if ($status !== TRUE) { // Fetch and display requirements error message, without re-checking. module_load_install('mollom'); $requirements = mollom_requirements('runtime', FALSE); drupal_set_message($requirements['mollom']['description'], 'error'); } } $path = drupal_get_path('module', 'mollom'); drupal_add_js($path . '/mollom.js'); drupal_add_css($path . '/mollom.css'); } /** * Implements hook_link(). */ function mollom_link($type, $object, $teaser = FALSE) { $links = array(); // Only show the links if the module is configured. if (_mollom_status() === TRUE) { if ($type == 'comment' && user_access('administer comments') && mollom_get_mode('comment_form')) { $links['mollom_report'] = array( 'title' => t('report to Mollom'), 'href' => 'mollom/report/comment/' . $object->cid, 'query' => drupal_get_destination(), ); } elseif ($type == 'node' && user_access('administer nodes') && mollom_get_mode($object->type . '_node_form')) { $links['mollom_report'] = array( 'title' => t('report to Mollom'), 'href' => 'mollom/report/node/' . $object->nid, 'query' => drupal_get_destination(), ); } } return $links; } /** * 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/settings/mollom'] = array( 'title' => 'Mollom', 'description' => 'Mollom is a web service that helps you manage your community.', 'page callback' => 'mollom_admin_form_list', 'access arguments' => array('administer mollom'), 'file' => 'mollom.admin.inc', ); $items['admin/settings/mollom/forms'] = array( 'title' => 'Forms', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['admin/settings/mollom/add'] = array( 'title' => 'Add form', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_configure_form'), 'access arguments' => array('administer mollom'), 'type' => MENU_LOCAL_TASK, 'file' => 'mollom.admin.inc', ); $items['admin/settings/mollom/manage/%mollom_form'] = array( 'title' => 'Configure', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_configure_form', 4), 'access arguments' => array('administer mollom'), 'file' => 'mollom.admin.inc', ); $items['admin/settings/mollom/unprotect/%mollom_form'] = array( 'title' => 'Unprotect form', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_unprotect_form', 4), 'access arguments' => array('administer mollom'), 'type' => MENU_CALLBACK, 'file' => 'mollom.admin.inc', ); $items['admin/settings/mollom/blacklist'] = array( 'title' => 'Blacklist', '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/settings/mollom/blacklist/spam'] = array( 'title' => 'Spam', 'description' => 'Configure spam blacklist entries.', 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items['admin/settings/mollom/blacklist/profanity'] = array( 'title' => 'Profanity', 'description' => 'Configure profanity blacklist entries.', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_blacklist_form', 4), 'access arguments' => array('administer mollom'), 'type' => MENU_LOCAL_TASK, 'file' => 'mollom.admin.inc', ); $items['admin/settings/mollom/blacklist/unwanted'] = array( 'title' => 'Unwanted', 'description' => 'Configure unwanted blacklist entries.', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_blacklist_form', 4), 'access arguments' => array('administer mollom'), 'type' => MENU_LOCAL_TASK, 'file' => 'mollom.admin.inc', ); $items['admin/settings/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/settings/mollom/settings'] = array( 'title' => 'Settings', 'description' => 'Configure Mollom keys and global settings.', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_settings'), 'access arguments' => array('administer mollom'), 'type' => MENU_LOCAL_TASK, 'file' => 'mollom.admin.inc', ); $items['admin/reports/mollom'] = array( 'title' => 'Mollom 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('administer mollom'), 'file' => 'mollom.admin.inc', ); // AJAX callback to request new CAPTCHA. $items['mollom/captcha/%/%'] = array( 'page callback' => 'mollom_captcha_js', 'page arguments' => array(2, 3), '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. */ 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_perm(). */ function mollom_perm() { return array( 'administer mollom', 'bypass mollom protection', ); } /** * Implements hook_flush_caches(). */ function mollom_flush_caches() { return array('cache_mollom'); } /** * Implements hook_cron(). */ function mollom_cron() { // Mollom session data auto-expires after 6 months. $expired = time() - 86400 * 30 * 6; db_query("DELETE FROM {mollom} WHERE changed < %d", array($expired)); } /** * 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_fetch_object(db_query_range("SELECT * FROM {mollom} WHERE entity = '%s' AND did = '%s'", array($entity, $id), 0, 1)); } /** * 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 stored data * is an associative array containing Mollom session information for the posted * content: * - session: The session ID returned by the Mollom server. * - quality: A quality rating assigned to the content to tell whether or not * it's spam. * - languages: An array containing language codes the content might be * written in. * * 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 save. * @param $id * The entity id the data belongs to. * * @todo Remove usage of global $mollom variable. */ function mollom_data_save($entity, $id) { // Nothing to do, if we do not have a valid Mollom response. if (empty($GLOBALS['mollom']['response']['session_id'])) { return FALSE; } $data = $GLOBALS['mollom']['response']; $data['session'] = $data['session_id']; $data['entity'] = $entity; $data['did'] = $id; $data['changed'] = time(); // Convert languages into a string. if (!empty($data['languages'])) { $data['languages'] = implode(' ', $data['languages']); } // Merge in default values that may not exist in the response. $data += array( 'languages' => '', 'quality' => '', 'reputation' => '', ); $update = db_result(db_query_range("SELECT 'did' FROM {mollom} WHERE entity = '%s' AND did = '%s'", $entity, $id, 0, 1)); drupal_write_record('mollom', $data, $update ? $update : array()); return $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, $ids) { $placeholders = db_placeholders($ids, 'varchar'); return db_query("DELETE FROM {mollom} WHERE entity = '%s' AND did IN ($placeholders)", array_merge(array($entity), $ids)); } /** * Helper function to add Mollom feedback options to confirmation forms. */ function mollom_data_delete_form_alter(&$form, &$form_state) { if (!isset($form['actions']['#weight'])) { $form['actions']['#weight'] = 100; } 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 no 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, $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)) { $result = _mollom_send_feedback($data->session, $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) { static $forms; // 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. if (!isset($forms)) { $forms = mollom_form_cache(); } // Remind of enabled testing mode on all protected forms. if (isset($forms['protected'][$form_id]) || strpos($_GET['q'], 'admin/settings/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; } } // Compute the weight of the CAPTCHA so we can position it in the form. // #type 'actions' is new in D7, but contributed modules use the concept // in D6 already. We therefore expect the key without #type. if (isset($form['actions']) && !isset($form['actions']['#type'])) { // D6 code should set a #weight. If none is set, we ensure a default of // 100, like #type 'actions' in D7. if (!isset($form['actions']['#weight'])) { $form['actions']['#weight'] = 100; } $weight = $form['actions']['#weight'] - 1; } else { $weight = 99999; foreach (element_children($form) as $key) { // Scan the top-level form elements for buttons. if (isset($form[$key]['#type']) && in_array($form[$key]['#type'], array('submit', 'button', 'image_button'))) { // For each button, slightly increase the weight to allocate room for // the CAPTCHA. if (isset($form[$key]['#weight'])) { $form[$key]['#weight'] += 0.0002; } else { $form[$key]['#weight'] = 1.0002; } // We want to position the CAPTCHA just before the first button, so // we make the CAPTCHA's weight slightly lighter than the lightest // button's weight. $weight = min($weight, $form[$key]['#weight'] - 0.0001); } } } // Add Mollom form widget. $form['mollom'] = array( '#type' => 'mollom', '#mollom_form' => $mollom_form, '#weight' => $weight, '#tree' => TRUE, ); // Add Mollom form validation handlers. $form['#validate'][] = 'mollom_validate_analysis'; $form['#validate'][] = 'mollom_validate_captcha'; // Add a submit handler to remove form state storage. $form['#submit'][] = 'mollom_form_submit'; // 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' => '
', '#suffix' => '
', '#value' => t('By submitting this form, you accept the Mollom privacy policy.', array( '@privacy-policy-url' => 'http://mollom.com/web-service-privacy-policy', )), '#weight' => 10, ); } } } // Integrate with delete confirmation forms to send feedback to Mollom. if (isset($forms['delete'][$form_id])) { mollom_data_delete_form_alter($form, $form_state); // Report before deleting. This needs to be handled here, since // mollom_data_delete_form_alter() is re-used for mass-operation forms. array_unshift($form['#submit'], 'mollom_data_delete_form_submit'); } } /** * Returns a cached mapping of protected and delete confirmation form ids. * * @param $reset * (optional) Boolean whether to reset the static cache, flush the database * cache, and return nothing (TRUE). Defaults to FALSE. * * @return * An associative array containing: * - protected: An associative array whose keys are protected form IDs and * whose values are the corresponding module names the form belongs to. * - delete: An associative array whose keys are 'delete form' ids and whose * values are protected form ids; e.g. * @code * array( * 'node_delete_confirm' => 'article_node_form', * ) * @endcode * A single delete confirmation form id can map to multiple registered * $form_ids, but only the first is taken into account. As in above example, * we assume that all 'TYPE_node_form' definitions belong to the same entity * and therefore have an identical 'post_id' mapping. */ function mollom_form_cache($reset = FALSE) { if ($reset) { // This catches both 'mollom_form_cache' as well as mollom_form_load()'s // 'mollom:form:*' entries. cache_clear_all('mollom', 'cache', TRUE); return; } if ($cache = cache_get('mollom_form_cache')) { return $cache->data; } $result = db_query("SELECT form_id, module FROM {mollom_form}"); $forms['protected'] = array(); while ($row = db_fetch_array($result)) { $forms['protected'][$row['form_id']] = $row['module']; } // Build a list of delete confirmation forms of entities integrating with // Mollom, so we are able to alter the delete confirmation form to display // our feedback options. $forms['delete'] = array(); foreach (mollom_form_list() as $form_id => $info) { if (!isset($info['delete form']) || !isset($info['entity'])) { continue; } // We expect that the same delete confirmation form uses the same form // element mapping, so multiple 'delete form' definitions are only processed // once. Additionally, we only care for protected forms. if (!isset($forms['delete'][$info['delete form']]) && isset($forms['protected'][$form_id])) { // A delete confirmation form integration requires a 'post_id' mapping. $form_info = mollom_form_info($form_id, $info['module']); if (isset($form_info['mapping']['post_id'])) { $forms['delete'][$info['delete form']] = $form_id; } } } cache_set('mollom_form_cache', $forms); return $forms; } /** * Return the protection mode for a given form id. * * @return * The protection mode for the given form id, one of: * - MOLLOM_MODE_DISABLED: None. * - MOLLOM_MODE_CAPTCHA: CAPTCHA only. * - MOLLOM_MODE_ANALYSIS: Text analysis with CAPTCHA fallback. */ function mollom_get_mode($form_id) { static $modes; if (!isset($modes[$form_id])) { $mollom_form = mollom_form_load($form_id); $modes[$form_id] = isset($mollom_form['mode']) ? $mollom_form['mode'] : MOLLOM_MODE_DISABLED; } return $modes[$form_id]; } /** * Returns a list of protectable forms registered via hook_mollom_form_info(). */ function mollom_form_list() { $form_list = array(); foreach (module_implements('mollom_form_list') as $module) { $function = $module . '_mollom_form_list'; $module_forms = $function(); foreach ($module_forms as $form_id => $info) { $form_list[$form_id] = $info; $form_list[$form_id] += array( 'form_id' => $form_id, 'module' => $module, ); } } return $form_list; } /** * Returns information about a form registered via hook_mollom_form_info(). * * @param $form_id * The form id to return information for. * @param $module * The module name $form_id belongs to. */ function mollom_form_info($form_id, $module) { $form_info = module_invoke($module, 'mollom_form_info', $form_id); if (empty($form_info)) { $form_info = array(); } // Ensure default properties. $form_info += array( 'form_id' => $form_id, 'title' => $form_id, 'module' => $module, 'entity' => NULL, 'mode' => NULL, 'bypass access' => array(), 'elements' => array(), 'mapping' => array(), 'mail ids' => array(), ); // Allow modules to alter the default form information. drupal_alter('mollom_form_info', $form_info, $form_id); return $form_info; } /** * Creates a bare Mollom form configuration. * * @param $form_id * (optional) The form id to create the Mollom form configuration for. */ function mollom_form_new($form_id = NULL) { $mollom_form = array(); if (isset($form_id)) { $form_list = mollom_form_list(); if (isset($form_list[$form_id])) { $mollom_form += $form_list[$form_id]; } $mollom_form += mollom_form_info($form_id, $form_list[$form_id]['module']); } // Ensure default properties. $mollom_form += array( 'form_id' => $form_id, 'title' => $form_id, 'mode' => NULL, 'checks' => array(), 'elements' => array(), 'enabled_fields' => array(), ); // Enable all fields for textual analysis by default. if (!empty($mollom_form['elements'])) { $mollom_form['checks'] = array('spam'); $mollom_form['enabled_fields'] = array_keys($mollom_form['elements']); } return $mollom_form; } /** * Menu argument loader; Loads Mollom configuration and form information for a given form id. */ function mollom_form_load($form_id) { $cid = 'mollom:form:' . $form_id; if ($cache = cache_get($cid)) { return $cache->data; } else { $mollom_form = db_fetch_array(db_query("SELECT * FROM {mollom_form} WHERE form_id = '%s'", $form_id)); if ($mollom_form) { $mollom_form['checks'] = unserialize($mollom_form['checks']); $mollom_form['enabled_fields'] = unserialize($mollom_form['enabled_fields']); // Attach form registry information. $form_list = mollom_form_list(); if (isset($form_list[$form_id])) { $mollom_form += $form_list[$form_id]; } $mollom_form += mollom_form_info($form_id, $mollom_form['module']); cache_set($cid, $mollom_form); } } return $mollom_form; } /** * Saves a Mollom form configuration. */ function mollom_form_save(&$mollom_form) { $exists = db_result(db_query_range("SELECT 1 FROM {mollom_form} WHERE form_id = '%s'", $mollom_form['form_id'], 0, 1)); $status = drupal_write_record('mollom_form', $mollom_form, ($exists ? 'form_id' : array())); // Allow modules to react on saved form configurations. if ($status === SAVED_NEW) { module_invoke_all('mollom_form_insert', $mollom_form); } else { module_invoke_all('mollom_form_update', $mollom_form); } // Flush cached Mollom forms and the Mollom form mapping cache. mollom_form_cache(TRUE); return $status; } /** * Given an array of values and an array of fields, extract data for use. * * This function generates the data to send for validation to Mollom by walking * through the submitted form values and * - copying element values as specified via 'mapping' in hook_mollom_form_info() * into the dedicated data properties * - collecting and concatenating all fields that have been selected for textual * analysis into the 'post_body' property * * The processing accounts for the following possibilities: * - A field was selected for textual analysis, but there is no submitted form * value. The value should have been appended to the 'post_body' property, but * will be skipped. * - A field is contained in the 'mapping' and there is a submitted form value. * The value will not be appended to the 'post_body', but instead be assigned * to the specified data property. * - All fields specified in 'mapping', for which there is a submitted value, * but which were NOT selected for textual analysis, are assigned to the * specified data property. This is usually the case for form elements that * hold system user information. * * @param $values * An array containing submitted form values, usually $form_state['values']. * @param $fields * A list of strings representing form elements to extract. Nested fields are * in the form of 'parent][child'. * @param $mapping * An associative array of form elements to map to Mollom's dedicated data * properties. See hook_mollom_form_info() for details. * * @see hook_mollom_form_info() */ function mollom_form_get_values($form_values, $fields, $mapping) { global $user; // All elements specified in $mapping must be excluded from $fields, as they // are used for dedicated $data properties instead. To reduce the parsing code // size, we are turning a given $mapping of f.e. // array('post_title' => 'title_form_element') // into // array('title_form_element' => 'post_title') // and we reset $mapping afterwards. // When iterating over the $fields, this allows us to quickly test whether the // current field should be excluded, and if it should, we directly get the // mapped property name to rebuild $mapping with the field values. $exclude_fields = array(); if (!empty($mapping)) { $exclude_fields = array_flip($mapping); } $mapping = array(); // Process all fields that have been selected for text analysis. $post_body = array(); foreach ($fields as $field) { // Nested elements use a key of 'parent][child', so we need to recurse. $parents = explode('][', $field); $value = $form_values; foreach ($parents as $key) { $value = isset($value[$key]) ? $value[$key] : NULL; } // If this field was contained in $mapping and should be excluded, add it to // $mapping with the actual form element value, and continue to the next // field. Also unset this field from $exclude_fields, so we can process the // remaining mappings below. if (isset($exclude_fields[$field]) && drupal_validate_utf8($value)) { $mapping[$exclude_fields[$field]] = $value; unset($exclude_fields[$field]); continue; } // Only add form element values that are not empty. if (isset($value)) { if (is_string($value) && drupal_validate_utf8($value) && drupal_strlen($value)) { $post_body[$field] = $value; } // Recurse into nested values (e.g. multiple value fields). elseif (!empty($value)) { // Ensure we have a flat array to implode(); form values of // field_attach_form() use several subkeys. _mollom_flatten_form_values($value); if (($value = implode("\n", $value)) && drupal_validate_utf8($value)) { $post_body[$field] = $value; } } } } $post_body = implode("\n", $post_body); // Try to assign any further form values by processing the remaining mappings, // which have been turned into $exclude_fields above. All fields that were // already used for 'post_body' no longer exist in $exclude_fields. foreach ($exclude_fields as $field => $property) { // Nested elements use a key of 'parent][child', so we need to recurse. $parents = explode('][', $field); $value = $form_values; foreach ($parents as $key) { $value = isset($value[$key]) ? $value[$key] : NULL; } if (isset($value) && drupal_validate_utf8($value)) { $mapping[$property] = $value; } } // Mollom's XML-RPC methods only accept data properties that are defined. We // also do not want to send more than we have to, so we need to build an // exact data structure. $data = array(); // Post id; not sent to Mollom. // @see mollom_form_submit() if (!empty($mapping['post_id'])) { $data['post_id'] = $mapping['post_id']; } // Post title. if (!empty($mapping['post_title'])) { $data['post_title'] = $mapping['post_title']; } // Post body. if (!empty($post_body)) { $data['post_body'] = $post_body; } // User name. if (!empty($mapping['author_name'])) { $data['author_name'] = $mapping['author_name']; // Try to inherit user from author name. $account = user_load(array('name' => $data['author_name'])); } elseif (!empty($user->name)) { $data['author_name'] = $user->name; } // User e-mail. if (!empty($mapping['author_mail'])) { $data['author_mail'] = $mapping['author_mail']; } elseif (!empty($data['author_name'])) { if (!empty($account->mail)) { $data['author_mail'] = $account->mail; } } elseif (!empty($user->mail)) { $data['author_mail'] = $user->mail; } // User homepage. if (!empty($mapping['author_url'])) { $data['author_url'] = $mapping['author_url']; } // User ID. if (!empty($mapping['author_id'])) { $data['author_id'] = $mapping['author_id']; } elseif (!empty($data['author_name'])) { if (!empty($account->uid)) { $data['author_id'] = $account->uid; } } elseif (!empty($user->uid)) { $data['author_id'] = $user->uid; } // User OpenID. if (!empty($mapping['author_openid'])) { $data['author_openid'] = $mapping['author_openid']; } elseif (!empty($data['author_id'])) { if (!empty($account->uid) && ($openid = _mollom_get_openid($account))) { $data['author_openid'] = $openid; } } elseif (!empty($user->uid) && ($openid = _mollom_get_openid($user))) { $data['author_openid'] = $openid; } // User IP. $data['author_ip'] = ip_address(); 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)) { $result = db_query("SELECT * FROM {authmap} WHERE module = 'openid' AND uid = %d", $account->uid); $ids = array(); while ($identity = db_fetch_object($result)) { $ids[] = $identity->authname; } 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() { static $warned; 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/settings/mollom/settings') { $admin_message = t('Visit the Mollom settings page to disable it.', array( '@settings-url' => url('admin/settings/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. * * Normally, Mollom's integration with Form API would be quite simple: * - If a form is protected by Mollom, we setup initial information * about the session and the form in $form_state['storage'], bound to the * 'form_build_id'. * - We enable form caching via $form_state['cache'], so our information in the * form storage is cached. Form API then automatically ensures a proper * 'form_build_id' for every form and every user. * - We mainly work in and after form validation. Textual analysis validates all * values in the form as a form validation handler. If this validation fails, * we alter the form (during validation) to add a CAPTCHA. If the CAPTCHA * response is invalid, we still alter the form during validation to display a * new CAPTCHA, but without the previously entered value. * - In short, roughly: * - Form construction: Nothing. * - Form processing: Nothing. * - Form validation: Perform validation and alterations based on validation. * * This, however, is not possible due to various bugs in Drupal core. * - Form caching cannot be enabled for certain forms, because they contain * processing and validation logic. * http://drupal.org/node/644222 * - $form_state['storage'] is not updated after form processing and validation. * http://drupal.org/node/644150 * - Form validation handlers cannot alter the form structure. * http://drupal.org/node/642702 * * Hence, something that could be done in one simple function becomes quite a * nightmare: * - We need our own {cache_mollom} table as replacement for native form * caching, as well as our own logic to validate a submitted 'session_id' * ('form_build_id') against forms and users. * - We need to perform form alterations during form rendering, where * $form_state is no longer available. To make this possible, we leverage the * fact that an element property that is a reference to a key in $form_state * (which in itself is passed by reference) persists on to the rendering * layer. The essential part is: * @code * $element['#mollom'] = &$form_state['mollom']; * @endcode * - Since we cannot alter elements in the form structure during form * validation, this reference already needs to be set up during form * processing (in a #process callback), while everything else lives in form * validation handlers (unless it needs to add or alter the form structure). * * @see mollom_form_alter() */ /** * Implements hook_elements(). */ function mollom_elements() { return array( 'mollom' => array( '#input' => TRUE, '#process' => array( // Try to fetch a Mollom session from cache during form processing/validation. 'mollom_process_mollom_session_id', // Setup a new Mollom session. 'mollom_process_mollom', ), '#pre_render' => array('mollom_pre_render_mollom'), ), ); } /** * Implements hook_theme(). */ function mollom_theme() { return array( 'mollom' => array( 'arguments' => array('element' => NULL), ), 'mollom_admin_blacklist_form' => array( 'arguments' => array('form' => NULL), 'file' => 'mollom.admin.inc', ), ); } /** * Format the Mollom form element. * * This works like #type 'markup' and is only required, because D6 only supports * #process callbacks on elements with #input = TRUE. * * @see form_builder() * @see _form_builder_handle_input_element() */ function theme_mollom($element) { return isset($element['#children']) ? $element['#children'] : ''; } /** * Form element #process callback for the 'mollom' element. * * The 'mollom' form element is stateful. The Mollom session ID that is exchanged * between Drupal, the Mollom back-end, and the user allows us to keep track of * the form validation state. * * The session ID is valid for a given $form_id only. We expire it as soon as * the form is submitted, to avoid it being replayed. */ function mollom_process_mollom($element, $input, &$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, 'response' => array( 'session_id' => '', ), ); } $form_state['mollom'] += $element['#mollom_form']; // 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."), ); // Make Mollom form and session information available to #pre_render callback. // This must be assigned by reference. It is the essential "communication // layer" between form API and the rendering system. Any modifications to // $form_state['mollom'] will be carried over to the element for rendering. $element['#mollom'] = &$form_state['mollom']; // Make Mollom form and session information available to entirely different // functions. $GLOBALS['mollom'] = &$form_state['mollom']; return $element; } /** * Form element #process callback for Mollom's form storage handling. * * Albeit this *should* be an #element_validate handler that is only executed * during form validation, we must use a #process callback, because * mollom_process_mollom() needs to copy over $form_state['mollom'] into * $element['#mollom'], and as of now, Form API does not allow form validation * handlers to alter any elements in the form structure by reference. * @see http://drupal.org/node/642702 */ function mollom_process_mollom_session_id($element, $input, &$form_state) { // The current state can come either from the $form_state, if the form // was just rebuilt in the same request, or from data posted by the user. In // the latter case the state is fetched from the temporary data store. It is // verified that the session was created for the current form and that it has // not expired or already been used. if (empty($form_state['mollom']) && !empty($input['session_id'])) { @list($timestamp, $mollom_session_id) = explode('-', $input['session_id'], 2); if (empty($mollom_session_id)) { watchdog('mollom', 'Bogus session id %session.', array('%session' => $form_state['input']['mollom']['session_id']), WATCHDOG_WARNING); } elseif (!$cache = cache_get($mollom_session_id, 'cache_mollom')) { if (time() - $timestamp > 30 * 60) { watchdog('mollom', 'Expired session id %session.', array('%session' => $mollom_session_id)); } else { 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); } } elseif ($cache->data['form_id'] !== $form_state['values']['form_id']) { 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); } else { $form_state['mollom'] = $cache->data; } } 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']); $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(); } // If a new session ID was generated by Mollom, flush the old session from the // database cache. if (!empty($form_state['mollom']['response']['session_id']) && $result['session_id'] != $form_state['mollom']['response']['session_id']) { cache_clear_all($form_state['mollom']['response']['session_id'], 'cache_mollom'); } // Store the response returned by Mollom. $form_state['mollom']['response'] = $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). $teaser = truncate_utf8(strip_tags(isset($data['post_title']) ? $data['post_title'] : isset($data['post_body']) ? $data['post_body'] : '--'), 40); 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; form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.')); _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.')); } 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']) { 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'])) { form_set_error('mollom][captcha', t('The word verification field is required.')); 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']); $data = array( 'session_id' => $form_state['mollom']['response']['session_id'], 'captcha_result' => $form_state['values']['mollom']['captcha'], 'author_ip' => $all_data['author_ip'], 'author_id' => isset($all_data['author_id']) ? $all_data['author_id'] : NULL, ); $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; if ($result === TRUE) { $form_state['mollom']['passed_captcha'] = TRUE; _mollom_watchdog(array( 'Correct CAPTCHA' => array(), 'Data:
@data
' => array('@data' => $data), 'Result:
@result
' => array('@result' => $result), ), WATCHDOG_INFO); } else { 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 element #pre_render callback for CAPTCHA element. * * Conditionally alters the #type of the CAPTCHA form element into a 'hidden' * element if the response was correct. If it was not, then we empty the value * of the textfield to allow the user to re-enter a new one. * * This #pre_render trick is required, because form API validation does not * allow form validation handlers to alter the actual form structure. Both the * form constructor function and the #process callback for the 'mollom' element * are therefore executed too early (before form validation), so the CAPTCHA * element still contains not yet validated (default) values. * We also cannot invoke a form validation handler during form construction or * processing, because mollom_form_get_values() would be invoked too early * and therefore $form_state['values'] would not contain any additions from * form validation functions like mollom_comment_form_validate(). * @see http://drupal.org/node/642702 */ function mollom_pre_render_mollom($element) { $form_state['mollom'] = &$element['#mollom']; // 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; // Empty the CAPTCHA field value, since the user has to re-enter a new one. $element['captcha']['#value'] = ''; // Prevent the page cache from storing a form containing a CAPTCHA element. $GLOBALS['conf']['cache'] = CACHE_DISABLED; $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']; // If a new session ID was generated by Mollom, flush the old session from // the database cache. if (!empty($form_state['mollom']['response']['session_id']) && $captcha['response']['session_id'] != $form_state['mollom']['response']['session_id']) { cache_clear_all($form_state['mollom']['response']['session_id'], 'cache_mollom'); } // Assign the session ID returned by Mollom. $form_state['mollom']['response']['session_id'] = $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; } // If we received a Mollom session id via textual analysis or a CAPTCHA // request, inject it to the form. $timestamp = time(); if (!empty($form_state['mollom']['response']['session_id'])) { $element['session_id']['#value'] = $timestamp . '-' . $form_state['mollom']['response']['session_id']; cache_set($form_state['mollom']['response']['session_id'], $form_state['mollom'], 'cache_mollom', $timestamp + 21600); } return $element; } /** * 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. $data = 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($data['post_id'])) { mollom_data_save($form_state['mollom']['entity'], $data['post_id']); } } // Flush Mollom session information from database cache. if (!empty($form_state['mollom']['response']['session_id'])) { cache_clear_all($form_state['mollom']['response']['session_id'], 'cache_mollom'); } // Remove Mollom session information from form state to account for unforeseen // new builds of the form. unset($form_state['mollom']); } /** * @} End of "defgroup mollom_form_api". */ /** * Call a remote procedure at the Mollom server. * * This function automatically adds the information required to authenticate * 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, $method, $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 .= '
'; } // Each item in $parts is a part of the log message. foreach ($parts as $string => $string_arguments) { $message .= $string . "\n"; $arguments += $string_arguments; } // Prettify replacement token values, if possible. foreach ($arguments as $token => $array) { $flat_value = FALSE; if (is_array($array)) { $flat_value = ''; foreach ($array as $key => $value) { if (is_array($value)) { $flat_value = FALSE; break; } $value = var_export($value, TRUE); // Indent the new value, so there is a visual separation from the last. $flat_value .= " {$key} = {$value}\n"; } } // Only convert one-dimensional arrays, or we would lose debugging data. if ($flat_value !== FALSE) { $arguments[$token] = $flat_value; } else { $arguments[$token] = var_export($array, TRUE); } } watchdog('mollom', $message, $arguments, $severity); } /** * Helper function for mollom() to invoke watchdog() with cumulative messages. * * We do not want false errors to clutter the log, for example, when the server * list failed, but we were able to retrieve new servers. We therefore collect * all messages and invoke this function in mollom() right before returning any * XML-RPC response with the entire stack of collected messages. * This is also required for tests to pass. */ function _mollom_watchdog_multiple($messages, $severity) { foreach ($messages as $message) { _mollom_watchdog($message, $severity); } } /** * Returns version information to send with mollom.verifyKey. * * Retrieves platform and module version information for mollom.verifyKey, which * is normally invoked on Mollom's administration pages only. * * This information is solely used to speed up support requests and technical * inquiries. The data may also be aggregated to help the Mollom staff to make * decisions on new features or the necessity of back-porting improved * functionality to older versions. * * @return * An array containing: * - platform_name: The name of the Drupal distribution; i.e., "Drupal". * - platform_version: The version of Drupal; e.g., "7.0". * - client_name: The module name; i.e., "Mollom". * - client_version: The version of the module; e.g., "7.x-1.0". * * @see _mollom_status() */ function _mollom_get_version() { if ($cache = cache_get('mollom_version')) { return $cache->data; } // Retrieve Drupal distribution and installation profile information. include_once './includes/install.inc'; // Prior to D7, there is no distribution name recorded anywhere, so we have to // use "Drupal". $profile_info['distribution_name'] = 'Drupal'; $profile_info['version'] = VERSION; // Retrieve Mollom module information. $mollom_info = db_result(db_query("SELECT info FROM {system} WHERE type = 'module' AND name = 'mollom'")); $mollom_info = unserialize($mollom_info); if (empty($mollom_info['version'])) { // Manually build a module version string for repository checkouts. $mollom_info['version'] = DRUPAL_CORE_COMPATIBILITY . '-1.x-dev'; } $data = array( 'platform_name' => $profile_info['distribution_name'], 'platform_version' => $profile_info['version'], 'client_name' => $mollom_info['name'], 'client_version' => $mollom_info['version'], ); cache_set('mollom_version', $data); return $data; } /** * Send feedback to Mollom. */ function _mollom_send_feedback($session_id, $feedback = 'spam') { $result = mollom('mollom.sendFeedback', array( 'session_id' => $session_id, 'feedback' => $feedback, )); _mollom_watchdog(array( 'Reported %feedback for session id %session.' => array('%session' => $session_id, '%feedback' => $feedback), )); return $result; } /** * Fetch the site's Mollom statistics from the API. * * @param $refresh * A boolean if TRUE, will force the statistics to be re-fetched and stored * in the cache. * * @return * An array of statistics. */ function mollom_get_statistics($refresh = FALSE) { $statistics = FALSE; $cache = cache_get('mollom:statistics'); // Only fetch if $refresh is TRUE, the cache is empty, or the cache is expired. if ($refresh || !$cache || time() >= $cache->expire) { if (_mollom_status() === TRUE) { $statistics = drupal_map_assoc(array( 'total_days', 'total_accepted', 'total_rejected', 'yesterday_accepted', 'yesterday_rejected', 'today_accepted', 'today_rejected', )); foreach ($statistics as $statistic) { $result = mollom('mollom.getStatistics', array('type' => $statistic)); if ($result === NETWORK_ERROR || $result === MOLLOM_ERROR) { // If there was an error, stop fetching statistics and store FALSE // in the cache. This will help prevent from making unnecessary // requests to Mollom if the service is down or the server cannot // connect to the Mollom service. $statistics = FALSE; break; } else { $statistics[$statistic] = $result; } } } // Cache the statistics and set them to expire in one hour. cache_set('mollom:statistics', $statistics, 'cache', time() + 3600); } else { $statistics = $cache->data; } return $statistics; } /** * Implements hook_content_extra_fields(). * * Allow users of CCK to re-order the CAPTCHA field on node forms through the * CCK UI. */ function mollom_content_extra_fields($type_name) { if ($mollom_form = mollom_form_load($type_name . '_node_form')) { $extras['mollom'] = array( 'label' => t('Mollom'), 'description' => t('Mollom CAPTCHA or privacy policy link'), 'weight' => 99, ); return $extras; } } /** * Get the HTML markup for a Mollom CAPTCHA. * * @param $type * The CAPTCHA type to retrieve, e.g. 'image' or 'audio'. * @param $data * An optional array of parameters to send to Mollom when requesting the * CAPTCHA. * * @return * An array with the following key/value pairs: * - 'data': An array of parameters sent to Mollom when requesting the * CAPTCHA. * - 'response': An array with the response from Mollom. * - 'markup': The markup of the CAPTCHA HTML. */ function mollom_get_captcha($type, array $data = array()) { $data += array( 'author_ip' => ip_address(), 'ssl' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on', ); // @todo Convert these to actual theme functions? $output = ''; switch ($type) { case 'audio': $response = mollom('mollom.getAudioCaptcha', $data); if ($response) { $source = url(base_path() . drupal_get_path('module', 'mollom') . '/mollom-captcha-player.swf', array( 'query' => array('url' => $response['url']), 'external' => TRUE, )); $output = ''; $output .= ''; $output .= ''; $output .= ''; $output .= ''; $output .= ''; $output .= ''; $output .= ''; $output .= ''; $output .= ''; $output = '' . $output . ''; $output .= ' (' . t('verify using image') . ')'; } break; case 'image': $response = mollom('mollom.getImageCaptcha', $data); if ($response) { $captcha = theme('image', $response['url'], t('Type the characters you see in this picture.'), '', NULL, FALSE); $output = '' . $captcha . ''; $output .= ' (' . t('verify using audio') . ')'; } break; } return array( 'data' => $data, 'response' => $response, 'markup' => $output, ); } /** * Implements hook_mail_alter(). * * Adds a "report as inappropriate" link to e-mails sent after Mollom-protected * form submissions. */ function mollom_mail_alter(&$message) { // Attached the Mollom report link to any mails with IDs specified from the // submitted form's hook_mollom_form_info(). This should ensure that the // report link is added to mails sent by actual users and not any mails sent // by Drupal since they should never be reported as spam. if (!empty($GLOBALS['mollom']['mail ids']) && in_array($message['id'], $GLOBALS['mollom']['mail ids'])) { mollom_mail_add_report_link($message); } } /** * Add the 'Report as innapropriate' link to an e-mail message. */ function mollom_mail_add_report_link(&$message) { if (!empty($GLOBALS['mollom']['response']['session_id'])) { mollom_data_save('session', $GLOBALS['mollom']['response']['session_id']); $report_link = t('Report as inappropriate: @link', array( '@link' => url('mollom/report/session/' . $GLOBALS['mollom']['response']['session_id'], array('absolute' => TRUE)), )); // Until D7, hook_mail_alter() accepts both arrays and strings. if (is_array($message['body'])) { $message['body'][] = $report_link; } else { $message['body'] .= "\n\n" . $report_link; } } } /** * @name mollom_node Node module integration for Mollom. * @{ */ /** * Implements hook_mollom_form_list(). */ function node_mollom_form_list() { $forms = array(); foreach (node_get_types('types') as $type) { $form_id = $type->type . '_node_form'; $forms[$form_id] = array( 'title' => t('@name form', array('@name' => $type->name)), 'entity' => 'node', 'report access callback' => 'node_mollom_report_access', 'report delete callback' => 'node_mollom_report_delete', ); } return $forms; } /** * Implements hook_mollom_form_info(). */ function node_mollom_form_info($form_id) { // Retrieve internal type from $form_id. $nodetype = drupal_substr($form_id, 0, -10); $type = node_get_types('type', $nodetype); $form_info = array( // @todo This is incompatible with node access. 'bypass access' => array('administer nodes', 'edit any ' . $type->type . ' content'), 'bundle' => $type->type, 'elements' => array(), 'mapping' => array( 'post_id' => 'nid', 'author_name' => 'name', ), ); // @see node_content_form() if ($type->has_title) { $form_info['elements']['title'] = check_plain($type->title_label); $form_info['mapping']['post_title'] = 'title'; } if ($type->has_body) { $form_info['elements']['body'] = check_plain($type->body_label); } // Add text fields. if (module_exists('content')) { $content_info = content_types($type->type); foreach ($content_info['fields'] as $field_name => $field) { if ($field['type'] == 'text') { $form_info['elements'][$field_name] = check_plain(t($field['widget']['label'])); } } } return $form_info; } /** * Implements hook_nodeapi(). */ function mollom_nodeapi($node, $op) { if ($op == 'insert') { mollom_data_save('node', $node->nid); } elseif ($op == 'delete') { mollom_data_delete('node', $node->nid); } } /** * Implements hook_form_FORMID_alter(). * * Hook into the mass comment administration page and add some operations to * communicate ham/spam to the XML-RPC server. * * @see mollom_node_admin_overview_submit() */ function mollom_form_node_admin_content_alter(&$form, $form_state) { module_load_include('inc', 'mollom', 'mollom.admin'); $form['admin']['options']['operation']['#options']['mollom-unpublish'] = t('Report to Mollom and unpublish'); $form['admin']['options']['operation']['#options']['mollom-delete'] = t('Report to Mollom and delete'); $form['#validate'][] = 'mollom_node_admin_overview_submit'; } /** * Mollom report access callback; Determine access to report and delete a node. */ function node_mollom_report_access($entity, $id) { $node = node_load($id); return $node && node_access('delete', $node); } /** * Mollom report delete callback; Deletes a node. */ function node_mollom_report_delete($entity, $id) { node_delete($id); } /** * @} End of "name mollom_node". */ /** * @name mollom_comment Comment module integration for Mollom. * @{ */ /** * Implements hook_mollom_form_list(). */ function comment_mollom_form_list() { $forms['comment_form'] = array( 'title' => t('Comment form'), 'entity' => 'comment', 'report access' => array('administer comments'), 'report delete callback' => 'comment_mollom_report_delete', ); return $forms; } /** * Implements hook_mollom_form_info(). */ function comment_mollom_form_info($form_id) { $form_info = array( 'mode' => MOLLOM_MODE_ANALYSIS, 'bypass access' => array('administer comments'), 'elements' => array( 'subject' => t('Subject'), 'comment' => t('Comment'), ), 'mapping' => array( 'post_id' => 'cid', 'post_title' => 'subject', // In D6, comment_form() dynamically uses different form elements for // anonymous users, authenticated users, and comment administrators. 'author_name' => 'name', 'author_mail' => 'mail', 'author_url' => 'homepage', ), ); return $form_info; } /** * Implements hook_form_FORMID_alter(). * * When a registered user posts a comment or when a comment administrator edits * an existing comment, comment_form() does not define 'name' and 'mail' form * elements, so our form element mapping will fail. * * @see comment_mollom_form_info() * @see mollom_comment_form_validate() * * @todo Remove in D7. */ function mollom_form_comment_form_alter(&$form, &$form_state) { if (isset($form['author']) || isset($form['admin']['author'])) { $form['#validate'][] = 'mollom_comment_form_validate'; } } /** * Form validation handler for comment_form(). * * @todo Remove in D7. */ function mollom_comment_form_validate($form, &$form_state) { // If there were no validation errors, prepare submitted form values for // validation via Mollom. if (!form_get_errors()) { // Author is a registered user or comment is edited by administrator. if (isset($form_state['values']['author'])) { // Populate 'name' with value of 'author'. if (!isset($form_state['values']['name'])) { form_set_value(array('#parents' => array('name')), $form_state['values']['author'], $form_state); } // Populate 'mail' based on corresponding user account. if (!isset($form_state['values']['mail'])) { // This should already be validated by comment_validate(), but we still // double-check that we have a valid account before trying to access it. $account = user_load(array('name' => $form_state['values']['author'])); if ($account) { form_set_value(array('#parents' => array('mail')), $account->mail, $form_state); } } } } } /** * Implements hook_comment(). */ function mollom_comment($comment, $op) { if ($op == 'insert') { mollom_data_save('comment', $comment['cid']); } elseif ($op == 'delete') { mollom_data_delete('comment', $comment->cid); } } /** * Implements hook_form_FORMID_alter(). * * Hook into the mass comment administration page and add some operations to * communicate ham/spam to the XML-RPC server. * * @see mollom_comment_admin_overview_submit() */ function mollom_form_comment_admin_overview_alter(&$form, $form_state) { module_load_include('inc', 'mollom', 'mollom.admin'); $form['options']['operation']['#options']['mollom-unpublish'] = t('Report to Mollom and unpublish'); $form['options']['operation']['#options']['mollom-delete'] = t('Report to Mollom and delete'); $form['#submit'][] = 'mollom_comment_admin_overview_submit'; } /** * Mollom report delete callback; Deletes a comment and its replies. * * @see comment_confirm_delete_submit() */ function comment_mollom_report_delete($entity, $id) { module_load_include('inc', 'comment', 'comment.admin'); $comment = _comment_load($id); // Delete the comment and its replies. _comment_delete_thread($comment); _comment_update_node_statistics($comment->nid); // Clear the cache so an anonymous user sees that his comment was deleted. cache_clear_all(); drupal_set_message(t('The comment has been deleted.')); } /** * @} End of "name mollom_comment". */ /** * @name mollom_user User module integration for Mollom. * @{ */ /** * Implements hook_mollom_form_list(). */ function user_mollom_form_list() { $forms['user_register'] = array( 'title' => t('User registration form'), 'entity' => 'user', ); $forms['user_pass'] = array( 'title' => t('User password request form'), 'entity' => 'user', ); return $forms; } /** * Implements hook_mollom_form_info(). */ function user_mollom_form_info($form_id) { switch ($form_id) { case 'user_register': $form_info = array( 'mode' => MOLLOM_MODE_CAPTCHA, 'bypass access' => array('administer users'), 'mapping' => array( 'post_id' => 'uid', 'author_name' => 'name', 'author_mail' => 'mail', ), ); return $form_info; case 'user_pass': $form_info = array( 'mode' => MOLLOM_MODE_CAPTCHA, 'bypass access' => array('administer users'), 'mapping' => array( 'post_id' => 'uid', 'author_name' => 'name', // The 'name' form element accepts either a username or mail address. 'author_mail' => 'name', ), ); return $form_info; } } /** * @} End of "name mollom_user". */ /** * @name mollom_contact Contact module integration for Mollom. * @{ */ /** * Implements hook_mollom_form_list(). */ function contact_mollom_form_list() { $forms['contact_mail_page'] = array( 'title' => t('Site-wide contact form'), ); $forms['contact_mail_user'] = array( 'title' => t('User contact form'), ); return $forms; } /** * Implements hook_mollom_form_info(). */ function contact_mollom_form_info($form_id) { switch ($form_id) { case 'contact_mail_page': $form_info = array( 'mode' => MOLLOM_MODE_ANALYSIS, 'bypass access' => array('administer site-wide contact form'), 'mail ids' => array('contact_page_mail'), 'elements' => array( 'subject' => t('Subject'), 'message' => t('Message'), ), 'mapping' => array( 'post_title' => 'subject', 'author_name' => 'name', 'author_mail' => 'mail', ), ); return $form_info; case 'contact_mail_user': $form_info = array( 'mode' => MOLLOM_MODE_ANALYSIS, 'bypass access' => array('administer users'), 'mail ids' => array('contact_user_mail'), 'elements' => array( 'subject' => t('Subject'), 'message' => t('Message'), ), 'mapping' => array( 'post_title' => 'subject', ), ); return $form_info; } } /** * @} End of "name mollom_contact". */