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

' . 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/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', '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/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()); 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) { 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 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, 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' => '
', '#suffix' => '
', '#markup' => 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])) { // Check whether the user is allowed to report to Mollom. Limiting report // access is optional for forms integrating via 'delete form' and allowed by // default, since we assume that users being able to delete entities are // sufficiently trusted to also report to Mollom. $access = TRUE; // Retrieve information about the protected form; the form cache maps delete // confirmation forms to protected form_ids, and protected form_ids to their // originating modules. $mollom_form_id = $forms['delete'][$form_id]; $module = $forms['protected'][$mollom_form_id]; $form_info = mollom_form_info($mollom_form_id, $module); // Check access, if there is a 'report access' permission list. if (isset($form_info['report access'])) { $access = FALSE; foreach ($form_info['report access'] as $permission) { if (user_access($permission)) { $access = TRUE; break; } } } if ($access) { 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) { $forms = &drupal_static(__FUNCTION__); if ($reset) { // This catches both 'mollom_form_cache' as well as mollom_form_load()'s // 'mollom:form:*' entries. cache_clear_all('mollom', 'cache', TRUE); unset($forms); return; } if (isset($forms)) { return $forms; } if ($cache = cache_get('mollom_form_cache')) { $forms = $cache->data; return $forms; } $forms['protected'] = db_query("SELECT form_id, module FROM {mollom_form}")->fetchAllKeyed(); // 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; } /** * 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, 'discard' => TRUE, '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(), '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_query('SELECT * FROM {mollom_form} WHERE form_id = :form_id', array(':form_id' => $form_id))->fetchAssoc(); 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 = module_invoke($mollom_form['module'], '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_query_range('SELECT 1 FROM {mollom_form} WHERE form_id = :form_id', 0, 1, array(':form_id' => $mollom_form['form_id']))->fetchField(); $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; } /** * Deletes a Mollom form configuration. */ function mollom_form_delete($form_id) { $mollom_form = mollom_form_load($form_id); db_delete('mollom_form') ->condition('form_id', $form_id) ->execute(); // Allow modules to react on saved form configurations. module_invoke_all('mollom_form_delete', $mollom_form); // Flush cached Mollom forms and the Mollom form mapping cache. mollom_form_cache(TRUE); } /** * 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])) { $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_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))) { $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)) { $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_by_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(); // Honeypot. // For the Mollom backend, it only matters whether 'honeypot' is non-empty. // The submitted value is only taken over to allow site administrators to // see the actual honeypot value in watchdog log entries. if (isset($form_values['mollom']['homepage']) && $form_values['mollom']['homepage'] !== '') { $data['honeypot'] = $form_values['mollom']['homepage']; } // Ensure that all $data values contain valid UTF-8. Invalid UTF-8 would be // sanitized into an empty string, so the Mollom backend would not receive // any value. $valid_utf8 = TRUE; foreach ($data as $key => $value) { if (!drupal_validate_utf8($value)) { $valid_utf8 = FALSE; } } if (!$valid_utf8) { form_set_error('mollom', t('Invalid form values. Your submission will not be accepted.')); _mollom_watchdog(array( 'Invalid UTF-8 in form values' => array(), 'Data:
@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' => '
', '#suffix' => '
', ); // 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']; mollom_data_save($data); } } // 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, 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 .= '
'; } // 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. $profile = drupal_get_profile(); $profile_info = system_get_info('module', $profile) + array( 'distribution_name' => 'Drupal', 'version' => VERSION, ); // Retrieve Mollom module information. $mollom_info = system_get_info('module', 'mollom'); 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 || REQUEST_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', REQUEST_TIME + 3600); } else { $statistics = $cache->data; } return $statistics; } /** * Implements hook_field_extra_fields(). * * Allow users to re-order Mollom form additions through Field UI. */ function mollom_field_extra_fields() { $extras = array(); $forms = array_flip(db_query('SELECT form_id FROM {mollom_form}')->fetchCol()); foreach (mollom_form_list() as $form_id => $info) { // @todo Technically, an 'entity' does not need to be a Entity/Field API // kind of entity. Ideally of course, developers should use fieldable // entities, but contributed/custom code may not. It is not clear whether // registering extra fields for non-existing entities/bundles can break // anything, so leaving it this way for now. if (isset($info['entity']) && isset($forms[$form_id])) { // If the entity type does not implement bundles, then entity_get_info() // assumes a single bundle named after the entity. $entity_type = $info['entity']; $bundle = (isset($info['bundle']) ? $info['bundle'] : $entity_type); $extras[$entity_type][$bundle]['form']['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', array('path' => url($response['url']), 'alt' => t('Type the characters you see in this picture.'), 'getsize' => 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) { // Attaches 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 = $GLOBALS['mollom']; $data = (object) $mollom['response']; $data->entity = 'session'; $data->id = $mollom['response']['session_id']; $data->form_id = $mollom['form_id']; mollom_data_save($data); $report_link = t('Report as inappropriate: @link', array( '@link' => url("mollom/report/{$data->entity}/{$data->id}", array('absolute' => TRUE)), )); $message['body'][] = $report_link; } } /** * Implements hook_entity_update(). */ function mollom_entity_update($entity, $type) { // If an existing entity is published and we have session data stored for it, // mark the data as moderated. if (!empty($entity->status)) { list($id) = entity_extract_ids($type, $entity); mollom_data_moderate($type, $id); } } /** * Implements hook_entity_delete(). */ function mollom_entity_delete($entity, $type) { list($id) = entity_extract_ids($type, $entity); mollom_data_delete($type, $id); } /** * @name mollom_node Node module integration for Mollom. * @{ */ /** * Implements hook_mollom_form_list(). */ function node_mollom_form_list() { $forms = array(); foreach (node_type_get_types() as $type) { $form_id = $type->type . '_node_form'; $forms[$form_id] = array( 'title' => t('@name form', array('@name' => $type->name)), 'entity' => 'node', 'bundle' => $type->type, 'delete form' => 'node_delete_confirm', 'entity delete multiple callback' => 'node_delete_multiple', ); } 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_type_get_type($nodetype); $form_info = array( // @todo This is incompatible with node access. 'bypass access' => array('bypass node access'), 'bundle' => $type->type, 'moderation callback' => 'node_mollom_form_moderation', 'elements' => array(), 'mapping' => array( 'post_id' => 'nid', 'author_name' => 'name', ), ); // @see node_permission() if (in_array($type->type, node_permissions_get_configured_types())) { $form_info['bypass access'][] = 'edit any ' . $type->type . ' content'; $form_info['bypass access'][] = 'delete any ' . $type->type . ' content'; } // @see node_content_form() if ($type->has_title) { $form_info['elements']['title'] = check_plain($type->title_label); $form_info['mapping']['post_title'] = 'title'; } // Add text fields. $fields = field_info_fields(); foreach (field_info_instances('node', $type->type) as $field_name => $field) { if (in_array($fields[$field_name]['type'], array('text', 'text_long', 'text_with_summary'))) { $form_info['elements'][$field_name . '][' . LANGUAGE_NONE . '][0][value'] = check_plain(t($field['label'])); } } return $form_info; } /** * Mollom form moderation callback for nodes. */ function node_mollom_form_moderation(&$form, &$form_state) { $form_state['values']['status'] = 0; } /** * Implements hook_form_FORMID_alter(). */ function mollom_form_node_multiple_delete_confirm_alter(&$form, &$form_state) { mollom_data_delete_form_alter($form, $form_state); // Report before deletion. array_unshift($form['#submit'], 'mollom_form_node_multiple_delete_confirm_submit'); } /** * Form submit handler for node_multiple_delete_confirm(). */ function mollom_form_node_multiple_delete_confirm_submit($form, &$form_state) { $nids = array_keys($form_state['values']['nodes']); if (!empty($form_state['values']['mollom']['feedback'])) { if (mollom_data_report_multiple('node', $nids, $form_state['values']['mollom']['feedback'])) { drupal_set_message(t('The posts were successfully reported as inappropriate.')); } } mollom_data_delete_multiple('node', $nids); } /** * @} End of "name mollom_node". */ /** * @name mollom_comment Comment module integration for Mollom. * @{ */ /** * Implements hook_mollom_form_list(). */ function comment_mollom_form_list() { $forms = array(); foreach (node_type_get_types() as $type) { $form_id = "comment_node_{$type->type}_form"; $forms[$form_id] = array( 'title' => t('@name comment form', array('@name' => $type->name)), 'entity' => 'comment', 'bundle' => 'comment_node_' . $type->type, 'delete form' => 'comment_confirm_delete', 'entity delete multiple callback' => 'comment_delete_multiple', ); } 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'), 'moderation callback' => 'comment_mollom_form_moderation', 'elements' => array( 'subject' => t('Subject'), // @todo Update for Field API. 'comment_body][und][0][value' => t('Comment'), ), 'mapping' => array( 'post_id' => 'cid', 'post_title' => 'subject', 'author_name' => 'name', 'author_mail' => 'mail', 'author_url' => 'homepage', ), ); return $form_info; } /** * Mollom form moderation callback for comments. */ function comment_mollom_form_moderation(&$form, &$form_state) { $form_state['values']['status'] = COMMENT_NOT_PUBLISHED; } /** * Implements hook_form_FORMID_alter(). */ function mollom_form_comment_multiple_delete_confirm_alter(&$form, &$form_state) { mollom_data_delete_form_alter($form, $form_state); // Report before deletion. array_unshift($form['#submit'], 'mollom_form_comment_multiple_delete_confirm_submit'); } /** * Form submit handler for node_multiple_delete_confirm(). */ function mollom_form_comment_multiple_delete_confirm_submit($form, &$form_state) { $cids = array_keys($form_state['values']['comments']); if (!empty($form_state['values']['mollom']['feedback'])) { if (mollom_data_report_multiple('comment', $cids, $form_state['values']['mollom']['feedback'])) { drupal_set_message(t('The posts were successfully reported as inappropriate.')); } } mollom_data_delete_multiple('comment', $cids); } /** * @} 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_form'] = array( 'title' => t('User registration form'), 'entity' => 'user', 'delete form' => 'user_cancel_confirm_form', 'entity delete multiple callback' => 'user_delete_multiple', ); $forms['user_pass'] = array( 'title' => t('User password request form'), ); return $forms; } /** * Implements hook_mollom_form_info(). */ function user_mollom_form_info($form_id) { switch ($form_id) { case 'user_register_form': $form_info = array( 'bypass access' => array('administer users'), 'moderation callback' => 'user_mollom_form_moderation', '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; } } /** * Mollom form moderation callback for user accounts. */ function user_mollom_form_moderation(&$form, &$form_state) { $form_state['values']['status'] = 0; } /** * Implements hook_form_FORMID_alter(). */ function mollom_form_user_multiple_cancel_confirm_alter(&$form, &$form_state) { mollom_data_delete_form_alter($form, $form_state); // Report before deletion. array_unshift($form['#submit'], 'mollom_form_user_multiple_cancel_confirm_submit'); } /** * Form submit handler for node_multiple_delete_confirm(). */ function mollom_form_user_multiple_cancel_confirm_submit($form, &$form_state) { $uids = array_keys($form_state['values']['accounts']); if (!empty($form_state['values']['mollom']['feedback'])) { if (mollom_data_report_multiple('user', $uids, $form_state['values']['mollom']['feedback'])) { drupal_set_message(t('The users were successfully reported.')); } } mollom_data_delete_multiple('user', $uids); } /** * @} 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_site_form'] = array( 'title' => t('Site-wide contact form'), ); $forms['contact_personal_form'] = 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_site_form': $form_info = array( 'mode' => MOLLOM_MODE_ANALYSIS, 'bypass access' => array('administer contact forms'), '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_personal_form': $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', 'author_name' => 'name', 'author_mail' => 'mail', ), ); return $form_info; } } /** * @} End of "name mollom_contact". */ /** * @name mollom_profile Profile module integration for Mollom. * @{ */ /** * Implements hook_mollom_form_info_alter(). * * Adds profile fields exposed on the user registration form. */ function profile_mollom_form_info_alter(&$form_info, $form_id) { if ($form_id != 'user_register_form') { return; } // @see profile_form_profile() $result = db_query("SELECT name, title FROM {profile_field} WHERE register = 1 AND type IN (:types)", array( ':types' => array('textfield', 'textarea', 'url', 'list'), )); foreach ($result as $field) { $form_info['elements'][$field->name] = check_plain($field->title); } } /** * @} End of "name mollom_profile". */