Mollom website or in the Mollom FAQ.", array( '@mollom-website' => 'http://mollom.com', '@mollom-faq' => 'http://mollom.com/faq', )); } } /** * Implementation of hook_link(). */ function mollom_link($type, $object = NULL) { // We extend the comment links so people can send feedback: if ($type == 'comment' && user_access('administer comments') && _mollom_get_mode('comment_form')) { $links['comment_spam'] = array( 'title' => t('mark as abuse'), 'href' => "mollom/comment/$object->cid" ); return $links; } if ($type == 'node' && user_access('administer nodes') && _mollom_get_mode($object->type .'_node_form')) { $links['node_spam'] = array( 'title' => t('Delete post'), 'href' => "mollom/node/$object->nid" ); return $links; } } /** * Implementation of hook_menu(). */ function mollom_menu() { $items['mollom/comment'] = array( 'title' => 'Report and delete', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_report_comment'), 'access arguments' => array('administer comments'), 'type' => MENU_CALLBACK, ); $items['mollom/node'] = array( 'title' => 'Report and delete', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_report_node'), 'access arguments' => array('administer nodes'), 'type' => MENU_CALLBACK, ); $items['mollom/contact'] = array( 'title' => 'Report and delete', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_report_contact'), 'access arguments' => array(TRUE), // Everyone can report contact form feedback 'type' => MENU_CALLBACK, ); $items['admin/settings/mollom'] = array( 'description' => 'Mollom is a web service that helps you manage your community.', 'title' => 'Mollom', 'page callback' => 'drupal_get_form', 'page arguments' => array('mollom_admin_settings'), 'access arguments' => array('administer site configuration'), ); // A menu callback that is used for AJAX purposes: $items['mollom/captcha/%/%'] = array( 'title' => 'Request CAPTCHA', 'page callback' => 'mollom_captcha', 'page arguments' => array(2, 3), 'access callback' => TRUE, 'type' => MENU_CALLBACK, ); return $items; } /** * Implementation of hook_perm(). */ function mollom_perm() { return array('post with no checking'); } /** * An AJAX callback to retrieve a CAPTCHA. */ function mollom_captcha($type, $session_id) { // TODO: add error handling $output = ''; if ($type == 'audio') { $response = mollom('mollom.getAudioCaptcha', array('author_ip' => ip_address(), 'session_id' => $session_id)); if ($response) { $output = ''; $output .= ' ('. t('use image CAPTCHA') .')'; } } if ($type == 'image') { $response = mollom('mollom.getImageCaptcha', array('author_ip' => ip_address(), 'session_id' => $session_id)); if ($response) { $output = 'Mollom CAPTCHA'; $output .= ' ('. t('play audio CAPTCHA') .')'; } } print $output; exit(); } /** * Helper function used to load a Mollom session ID from the database. */ function mollom_get_data($did) { return db_fetch_object(db_query("SELECT * FROM {mollom} WHERE did = '%s'", $did)); } /** * Helper function used to store a Mollom session ID into the database. */ function mollom_set_data($session, $quality, $languages, $did) { if (db_result(db_query("SELECT session FROM {mollom} WHERE did = '%s'", $did))) { db_query("UPDATE {mollom} SET session = '%s', quality = '%s', languages = '%s' WHERE did = '%s'", $session, $quality, $languages, $did); } else { db_query("INSERT INTO {mollom} (session, quality, languages, did) VALUES ('%s', '%s', '%s', '%s')", $session, $quality, $languages, $did); } } /** * Return a list of the possible feedback options for content. */ function _mollom_feedback_options() { return array( '#type' => 'radios', '#title' => t('Optionally report this to Mollom'), '#options' => array( 'none' => t("Don't send feedback to Mollom"), 'spam' => t('Report as spam or unsolicited advertising'), 'profanity' => t('Report as obscene, violent or profane content'), 'low-quality' => t('Report as low-quality content or writing'), 'unwanted' => t('Report as unwanted, taunting or off-topic content'), ), '#default_value' => 'none', '#description' => t("Mollom is a web service that helps you moderate your site's content: see http://mollom.com for more information. By sending feedback to Mollom, you teach Mollom what content you like and what content you dislike. Like that, Mollom can do a better job helping you to moderate your site's content. If you want to report multiple posts at once, you can use Mollom's bulk operations on the content and comment administration pages."), ); } /** * This function is used to report a comment as feedback and to delete it. */ function mollom_report_comment($form_state, $cid) { if ($comment = _comment_load($cid)) { $form['cid'] = array('#type' => 'value', '#value' => $cid); $form['feedback'] = _mollom_feedback_options(); return confirm_form($form, t('Are you sure you want to delete the comment and report it?'), isset($_GET['destination']) ? $_GET['destination'] : 'node/'. $comment->nid, t('This action cannot be undone.'), t('Delete'), t('Cancel')); } } /** * This function is used to delete a comment and to optionally send feedback to Mollom. */ function mollom_report_comment_submit($form, &$form_state) { if ($form_state['values']['confirm']) { if ($comment = _comment_load($form_state['values']['cid'])) { // Load the Mollom session data: $data = mollom_get_data('comment-'. $comment->cid); // Provide feedback to Mollom if available: if (isset($data) && isset($data->session) && isset($form_state['values']['feedback'])) { mollom('mollom.sendFeedback', array('session_id' => $data->session, 'feedback' => $form_state['values']['feedback'])); } // Delete comment and its replies: include_once drupal_get_path('module', 'comment') .'/comment.admin.inc'; _comment_delete_thread($comment); _comment_update_node_statistics($comment->nid); cache_clear_all(); drupal_set_message(t('The comment has been deleted.')); } } $form_state['redirect'] = "node/$comment->nid"; } /** * This function is used to delete a node and to optionally send feedback to Mollom. */ function mollom_report_node($form_state, $nid) { if ($node = node_load($nid)) { $form['nid'] = array('#type' => 'value', '#value' => $node->nid); $form['feedback'] = _mollom_feedback_options(); return confirm_form($form, t('Are you sure you want to delete %title and report it?', array('%title' => $node->title)), isset($_GET['destination']) ? $_GET['destination'] : 'node/'. $node->nid, t('This action cannot be undone.'), t('Delete'), t('Cancel')); } } /** * This function is used to delete a node and to optionally send feedback to Mollom. */ function mollom_report_node_submit($form, &$form_state) { if ($form_state['values']['confirm']) { if ($node = node_load($form_state['values']['nid'])) { // Load the Mollom session data: $data = mollom_get_data('node-'. $node->nid); // Provide feedback to Mollom if available: if ($data->session && $form_state['values']['feedback']) { mollom('mollom.sendFeedback', array('session_id' => $data->session, 'feedback' => $form_state['values']['feedback'])); } // Delete the node. Calling this function will delete any comments, // clear the cache and print a status message. node_delete($node->nid); } } $form_state['redirect'] = ''; } /** * This function adds a 'report as inappropriate' link to the e-mails that are sent * by the contact module. */ function mollom_mail_alter(&$message) { if (isset($GLOBALS['mollom_response']) && isset($GLOBALS['mollom_response']['session_id'])) { $message['body'][] = t('Report as inappropriate: @link', array('@link' => url('mollom/contact/'. $GLOBALS['mollom_response']['session_id'], array('absolute' => TRUE)))); } } /** * This function is used to report a contact form message as inappropriate. */ function mollom_report_contact($form_state, $session) { $form['session'] = array('#type' => 'value', '#value' => $session); $form['feedback'] = _mollom_feedback_options(); return confirm_form($form, t('Are you sure you want to report the e-mail message as inappropriate?'), isset($_GET['destination']) ? $_GET['destination'] : '', t('This action cannot be undone.'), t('Report as inappropriate'), t('Cancel')); } /** * This function is used to report a contact form message as inappropriate. */ function mollom_report_contact_submit($form, &$form_state) { if ($form_state['values']['feedback']) { mollom('mollom.sendFeedback', array('session_id' => $form_state['values']['session'], 'feedback' => $form_state['values']['feedback'])); drupal_set_message(t('The e-mail has been reported as inappropriate.')); } $form_state['redirect'] = ''; } /** * This function implements the _nodeapi hook and is called when a node is inserted. */ function mollom_nodeapi($node, $op) { if ($op == 'insert' && isset($GLOBALS['mollom_response']) && isset($GLOBALS['mollom_response']['session_id'])) { $languages = isset($GLOBALS['mollom_response']['languages']) ? implode(' ', $GLOBALS['mollom_response']['languages']) : ''; mollom_set_data($GLOBALS['mollom_response']['session_id'], $GLOBALS['mollom_response']['quality'], $languages, 'node-'. $node->nid); } } /** * This function implements the _comment hook and is called when a comment is inserted. */ function mollom_comment($comment, $op) { if ($op == 'insert' && isset($GLOBALS['mollom_response']) && isset($GLOBALS['mollom_response']['session_id'])) { $languages = isset($GLOBALS['mollom_response']['languages']) ? implode(' ', $GLOBALS['mollom_response']['languages']) : ''; mollom_set_data($GLOBALS['mollom_response']['session_id'], $GLOBALS['mollom_response']['quality'], $languages, 'comment-'. $comment['cid']); } } /** * This is a helper function to help insert the CAPTCHA into the form. * It's quite an ugly hack due to Drupal 6's inability to handle * dynamic forms. Let's try to fix this in Drupal 7 and beyond. */ function mollom_form_value() { return ''; } /** * This function intercepts all forms in Drupal and Mollom-enables them if * necessary. */ function mollom_form_alter(&$form, $form_state, $form_id) { // Catch all handlers -- this makes it easy to protect all forms // with Mollom. Site administrators don't have their content // checked with Mollom. if (!user_access('post with no checking')) { // Retrieve the mode of protection that is required for this form: $mode = _mollom_get_mode($form_id); // Some special code that enables to /hack/ the session ID and // the CAPTCHA into the form: if (isset($form_state['mollom'])) { if (isset($form_state['mollom']['captcha']) && $form_state['mollom']['captcha'] == TRUE) { $mode = MOLLOM_MODE_CAPTCHA; } $form['#input'] = TRUE; $form['#process'][] = 'mollom_form_process'; } if ($mode == MOLLOM_MODE_ANALYSIS) { _mollom_debug('mollom_form_alter registered mollom_validate_analysis handler'); $form['#validate'][] = 'mollom_validate_analysis'; _mollom_insert_session_id($form, $form_state); } elseif ($mode == MOLLOM_MODE_CAPTCHA) { _mollom_debug('mollom_form_alter registered mollom_validate_captcha handler'); $form['#validate'][] = 'mollom_validate_captcha'; _mollom_insert_captcha($form, $form_state); } } // Hook into the mass comment administration page and add some // operations to communicate ham/spam to the XML-RPC server: if ($form_id == 'comment_admin_overview') { $form['options']['operation']['#options']['mollom-unpublish'] = t('Report to Mollom as spam and unpublish'); $form['options']['operation']['#options']['mollom-delete'] = t('Report to Mollom as spam and delete'); $form['#validate'][] = 'mollom_comment_admin_overview_submit'; } // Hook into the mass comment administration page and add some // operations to communicate ham/spam to the XML-RPC server: if ($form_id == 'node_admin_content') { $form['options']['operation']['#options']['mollom-unpublish'] = t('Report to Mollom as spam and unpublish'); $form['options']['operation']['#options']['mollom-delete'] = t('Report to Mollom as spam and delete'); $form['#validate'][] = 'mollom_node_admin_overview_submit'; } } function mollom_comment_admin_overview_submit($form, &$form_state) { // The operation has the following format: , // where '' can be 'unpublish' or 'delete'. list($id, $operation) = explode('-', $form_state['values']['operation']); if ($id == 'mollom') { foreach ($form_state['values']['comments'] as $cid => $value) { if ($value) { // First, send the proper information to the XML-RPC server: if ($data = mollom_get_data('comment-'. $cid)) { mollom('mollom.sendFeedback', array('session_id' => $data->session, 'feedback' => 'spam')); } // Second, perform the proper operation on the comments: if ($comment = _comment_load($cid)) { if ($operation == 'unpublish') { db_query("UPDATE {comments} SET status = %d WHERE cid = %d", COMMENT_NOT_PUBLISHED, $cid); _comment_update_node_statistics($comment->nid); } elseif ($operation == 'delete') { _comment_delete_thread($comment); _comment_update_node_statistics($comment->nid); } } } } // Clear the cache: cache_clear_all(); if ($operation == 'delete') { drupal_set_message(t('The selected comments have been reported as inappropriate and are deleted.')); } else { drupal_set_message(t('The selected comments have been reported as inappropriate and are unpublished.')); } } } function mollom_node_admin_overview_submit($form, &$form_state) { // The operation has the following format: , // where '' can be 'unpublish' or 'delete'. list($id, $operation) = explode('-', $form_state['values']['operation']); if ($id == 'mollom') { foreach ($form_state['values']['nodes'] as $nid => $value) { if ($value) { if ($data = mollom_get_data('node-'. $nid)) { mollom('mollom.sendFeedback', array('session_id' => $data->session, 'feedback' => 'spam')); } if ($node = node_load($nid)) { if ($operation == 'unpublish') { db_query('UPDATE {node} SET status = 0 WHERE nid = %d', $nid); } elseif ($operation == 'delete') { node_delete($nid); } } } } // Clear the cache: cache_clear_all(); if ($operation == 'delete') { drupal_set_message(t('The selected posts have been reported as inappropriate and are deleted.')); } else { drupal_set_message(t('The selected posts have been reported as inappropriate and are unpublished.')); } } } /** * This function will be called by mollom_validate to prepare the * XML-RPC data from the comment submission form's $form_state['values'] ... */ function _mollom_data_contact_mail($form_state) { global $user; $data = array( 'post_title' => isset($form_state['subject']) ? $form_state['subject'] : NULL, 'post_body' => isset($form_state['message']) ? $form_state['message'] : NULL, 'author_name' => isset($form_state['name']) ? $form_state['name'] : (isset($user->name) ? $user->name : NULL), 'author_mail' => isset($form_state['mail']) ? $form_state['mail'] : (isset($user->mail) ? $user->mail : NULL), 'author_openid' => isset($user->uid) ? _mollom_get_openid($user) : NULL, 'author_id' => $user->uid > 0 ? $user->uid : NULL, 'author_ip' => ip_address(), ); return $data; } /** * This function will be called by mollom_validate to prepare the * XML-RPC data from the comment submission form's $form_state['values'] ... */ function mollom_data_contact_mail_page($form_state) { return _mollom_data_contact_mail($form_state); } /** * This function will be called by mollom_validate to prepare the * XML-RPC data from the comment submission form's $form_state['values'] ... */ function mollom_data_contact_mail_user($form_state) { return _mollom_data_contact_mail($form_state); } /** * This function will be called by mollom_validate to prepare the * XML-RPC data from the comment submission form's $form_state['values'] ... */ function mollom_data_comment_form($form_state) { global $user; $data = array( 'post_title' => isset($form_state['subject']) ? $form_state['subject'] : NULL, 'post_body' => isset($form_state['comment']) ? $form_state['comment'] : NULL, 'author_name' => isset($form_state['name']) ? $form_state['name'] : (isset($user->name) ? $user->name : NULL), 'author_mail' => isset($form_state['mail']) ? $form_state['mail'] : (isset($user->mail) ? $user->mail : NULL), 'author_url' => isset($form_state['homepage']) ? $form_state['homepage'] : NULL, 'author_openid' => isset($user->uid) ? _mollom_get_openid($user) : NULL, 'author_id' => $user->uid > 0 ? $user->uid : NULL, 'author_ip' => isset($form_state['cid']) ? NULL : ip_address(), ); return $data; } /** * This function will be called by mollom_validate to prepare the * XML-RPC data from the comment submission form's $form_state['values'] ... */ function mollom_data_node_form($form_state) { global $user; // Render the node so that all visible fields are prepared and // concatenated: $data = node_build_content((object)$form_state, FALSE, FALSE); $content = drupal_render($data->content); $data = array( 'post_title' => isset($form_state['title']) ? $form_state['title'] : NULL, 'post_body' => $content, 'author_name' => isset($form_state['name']) ? $form_state['name'] : (isset($user->name) ? $user->name : NULL), 'author_mail' => isset($form_state['mail']) ? $form_state['mail'] : (isset($user->mail) ? $user->mail : NULL), 'author_url' => isset($form_state['homepage']) ? $form_state['homepage'] : NULL, 'author_openid' => isset($user->uid) ? _mollom_get_openid($user) : NULL, 'author_id' => $user->uid > 0 ? $user->uid : NULL, 'author_ip' => isset($form_state['nid']) ? '' : ip_address(), ); return $data; } /** * Given a form ID, this function will return the strategy that is used * to protect this form. Could be MOLLOM_MODE_DISABLED (none), * MOLLOM_MODE_CAPTCHA (CAPTCHAs only) or MOLLOM_MODE_ANALYSIS (text * analysis with smart CAPTCHA support). */ function _mollom_get_mode($form_id) { if (variable_get('mollom_'. $form_id, MOLLOM_MODE_DISABLED)) { $forms = _mollom_protectable_forms(); return $forms[$form_id]['mode']; } return MOLLOM_MODE_DISABLED; } /** * This function lists all the forms that you can protect with Mollom. * If you want to protect additional forms with Mollom add the form ID * to this list. */ function _mollom_protectable_forms() { static $forms = NULL; if (!$forms) { if (module_exists('comment')) { $forms['comment_form'] = array( 'name' => 'comment form', 'mode' => MOLLOM_MODE_ANALYSIS, ); } if (module_exists('contact')) { $forms['contact_mail_page'] = array( 'name' => 'site-wide contact form', 'mode' => MOLLOM_MODE_ANALYSIS, ); $forms['contact_mail_user'] = array( 'name' => 'per-user contact forms', 'mode' => MOLLOM_MODE_ANALYSIS, ); } $forms['user_register'] = array( 'name' => 'user registration form', 'mode' => MOLLOM_MODE_CAPTCHA, ); $forms['user_pass'] = array( 'name' => 'user password request form', 'mode' => MOLLOM_MODE_CAPTCHA, ); // Add all the node types: $types = node_get_types('names'); foreach ($types as $type => $name) { $forms[$type .'_node_form'] = array( 'name' => drupal_strtolower($name) .' form', 'mode' => MOLLOM_MODE_ANALYSIS, ); } } return $forms; } // Temporary test code function _mollom_update_comments() { $result = db_query_range('SELECT * FROM {comments}', 0, 1000); while ($comment = db_fetch_object($result)) { $data = array( 'post_title' => $comment->subject, 'post_body' => $comment->comment, ); $response = mollom('mollom.checkContent', $data); if ($response['spam'] == MOLLOM_ANALYSIS_SPAM) { print "$comment->subject
$comment->comment
"; } else { print $response['spam']; } } } function mollom_admin_settings() { // _mollom_update_comments(); $keys = variable_get('mollom_public_key', '') && variable_get('mollom_private_key', ''); if ($keys) { // Print a status message about the key: if (!$_POST) { // When a user visits the Mollom administration page, we automatically // clear the server list. This will cause the client to fetch a fresh // server list from the server. variable_del('mollom_servers'); // Verify the key: _mollom_verify_key(); } $form['statistics'] = array( '#type' => 'fieldset', '#title' => t('Site usage statistics'), '#collapsible' => TRUE, ); $form['statistics']['message'] = array( '#value' => '
', ); $form['spam'] = array( '#type' => 'fieldset', '#title' => t('Spam protection settings'), '#description' => '

'. t("Mollom can be used to block all sorts of spam received by your website. Your Drupal site will send data you want checked for spam to the Mollom servers, which will reply with either 'spam' or 'ham' (not spam). If Mollom is not fully confident in its decision, it will ask the user to fill out a CAPTCHA. On the rare occasion that Mollom asks the poster to fill out a CAPTCHA, Mollom assumes that all legitimate posters will take the extra time to fill out this CAPTCHA. Using the CAPTCHA, Mollom avoids legitimate messages being incorrectly classified as spam and it eliminates the need to moderate messages that Mollom decided to block.") .'

'. '

'. t("Administrators can inspect the logs to see what Mollom has blocked. If Mollom made a mistake and some spam slipped through, you can report it to Mollom when you delete the post or comment. By doing so, Mollom learns how it can do a better job protecting your site. If you want to report multiple posts at once, you can use Mollom's bulk operations on the content administration page or the comment administration page.", array('@logs' => url('admin/reports/dblog'), '@content-admin' => url('admin/content/node'), '@comment-admin' => url('admin/content/comment'))) .'

'. '

'. t("To perform its service, Mollom processes, stores and compares the data submitted by your site's visitors as explained in our Web Service Privacy Policy. As the controller of the data being processed, it is your responsibility to inform your website's visitors, and to obtain appropriate consent from them to allow Mollom to process their data.") .'

', '#collapsible' => TRUE, ); $forms = _mollom_protectable_forms(); foreach ($forms as $formid => $details) { $name = 'mollom_'. $formid; $form['spam'][$name] = array( '#type' => 'checkbox', '#title' => t('Protect @name', array('@name' => $details['name'])), '#default_value' => variable_get($name, MOLLOM_MODE_DISABLED), ); } $form['server'] = array( '#type' => 'fieldset', '#title' => t('Server settings'), '#collapsible' => TRUE, '#collapsed' => $keys, ); $form['server']['mollom_fallback'] = array( '#type' => 'radios', '#title' => t('Fallback strategy'), '#default_value' => variable_get('mollom_fallback', MOLLOM_FALLBACK_BLOCK), // we default to treating everything as inappropriate '#options' => array( MOLLOM_FALLBACK_BLOCK => t('Block all submissions on the protected forms until the server problems are resolved'), MOLLOM_FALLBACK_ACCEPT => t('Leave all forms unprotected and accept all submissions'), ), '#description' => t('In the very unlikely event that the Mollom servers are down or otherwise unreachable, no text analysis can be performed and no CAPTCHAs can be generated. At that point, your Drupal site will use the configured fallback strategy. You can choose to blindly accept all submissions without spam checking, or you can choose to block all submissions until the server or connection problems are resolved.'), ); } $form['access-keys'] = array( '#type' => 'fieldset', '#title' => t('Mollom access keys'), '#description' => t('In order to use Mollom, you need a public and a private key. Visit http://mollom.com/user and create a user account to obtain a private and a public access key.'), '#collapsible' => TRUE, '#collapsed' => $keys, ); $form['access-keys']['mollom_public_key'] = array( '#type' => 'textfield', '#title' => t('Public key'), '#default_value' => variable_get('mollom_public_key', ''), '#description' => t('The public key is used to uniquely identify you.'), '#required' => TRUE, ); $form['access-keys']['mollom_private_key'] = array( '#type' => 'textfield', '#title' => t('Private key'), '#default_value' => variable_get('mollom_private_key', ''), '#description' => t('The private key is used to prevent someone from hijacking your requests. It is like a password and should never be shared with anyone.'), '#required' => TRUE, ); return system_settings_form($form); } /** * A helper function that returns the OpenID identifiers associated with the specified 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 (count($ids)) { return implode($ids, ' '); } } } function _mollom_fallback() { $fallback = variable_get('mollom_fallback', MOLLOM_FALLBACK_BLOCK); if ($fallback == MOLLOM_FALLBACK_BLOCK) { form_set_error('mollom', t("The spam filter that is installed on this site is currently not available. Per the site's policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple minutes.")); } watchdog('mollom', 'All Mollom servers were unavailable: %servers, last error: @errno - %error_msg', array('%servers' => print_r(variable_get('mollom_servers', array()), TRUE), '@errno' => xmlrpc_errno(), '%error_msg' => xmlrpc_error_msg()), WATCHDOG_ERROR); } /** * This the a form API validator function that will be called to perform text analysis on a form. */ function mollom_validate_analysis($form, &$form_state) { _mollom_debug("mollom_validate_analysis for '". $form_state['values']['form_id'] ."'"); $data = array(); $form_id = $form_state['values']['form_id']; $pos = strpos($form_id, '_node_form'); if ($pos !== FALSE) { // The node forms use dynamic form IDs so we had to create a special // case for these. $data = mollom_data_node_form($form_state['values']); } else { $function = 'mollom_data_'. $form_id; if (function_exists($function)) { $data = $function($form_state['values']); } } if (isset($form_state['values']['op'])) { $mollom = isset($form_state['values']['session_id']) ? array('session_id' => $form_state['values']['session_id']) : array(); $result = mollom('mollom.checkContent', $data + $mollom); if (isset($result['session_id']) && isset($result['spam'])) { _mollom_debug('mollom_validate_analysis retrieved spam status '. $result['spam'] ." and session ID '". $result['session_id'] ."'"); // Store the session ID that Mollom returned and make sure that it persists across page requests: _mollom_register_session_id($form_state, $result['session_id']); // Check the spam results and act accordingly: if ($result['spam'] == MOLLOM_ANALYSIS_HAM) { // Keep track of the response so we can use it later on to save the data in the database: $GLOBALS['mollom_response'] = $result; watchdog('mollom', 'Ham: %message', array('%message' => $data['post_body'])); } elseif ($result['spam'] == MOLLOM_ANALYSIS_SPAM) { form_set_error('mollom', t('Your submission has triggered the installed spam filter and will not be accepted.')); watchdog('mollom', 'Spam: %message', array('%message' => $data['post_body'])); } else { form_set_error('mollom', t('We are sorry, but the spam filter on this site decided that your submission could be spam. Please fill in the CAPTCHA first.')); watchdog('mollom', 'Unsure: %message', array('%message' => $data['post_body'])); _mollom_register_captcha($form_state); } } else { _mollom_fallback(); } } } /** * This the a form API validator function that will be called to check a CAPTCHA on a form. */ function mollom_validate_captcha(&$form, &$form_state) { _mollom_debug("mollom_validate_captcha for '". $form_state['values']['form_id'] ."'"); if (isset($form_state['values']['session_id'])) { if (isset($form_state['values']['captcha'])) { // Check the CAPTCHA result: $result = mollom('mollom.checkCaptcha', array( 'session_id' => $form_state['values']['session_id'], 'captcha_result' => $form_state['values']['captcha'], 'author_ip' => ip_address(), )); _mollom_debug('mollom_validate_captcha the captcha result was '. (int)$result); if ($result == FALSE) { watchdog('mollom', 'Incorrect CAPTCHA'); form_set_error('captcha', t('The entered CAPTCHA solution is not correct. We generated a new CAPTCHA so please try again.')); _mollom_register_captcha($form_state); } else { // Keep track of the response so we can use it later on to save the data in the database: $GLOBALS['mollom_response']['session_id'] = $form_state['values']['session_id']; // Pass FALSE so we actually remove the CAPTCHA from the form: _mollom_register_captcha($form_state, FALSE); // TODO: this break the 'request password form' -- it shouldn't rebuild the form. Removing this breaks the comment form. watchdog('mollom', 'Correct CAPTCHA'); } } else { form_set_error('captcha', t('The CAPTCHA field is required.')); _mollom_register_captcha($form_state); } } else { // Mark the form so that we generate a new CAPTCHA. _mollom_register_captcha($form_state); } } /** * This is a helper function to insert the CAPTCHA into the form. We restore * the context at the right time (which happens to be when hook_form_process * is called). */ function mollom_form_process($form, $edit, &$form_state, $complete_form) { if (isset($form_state['mollom'])) { if (isset($form_state['mollom']['post'])) { _mollom_debug("_mollom_form_process found CAPTCHA request"); $form['#post'] = $form_state['mollom']['post']; } } return $form; } /** * This is a help function that registers a Mollom session ID so that * on the next page, we can insert the session ID in the form. */ // TODO: is this function required? function _mollom_register_session_id(&$form_state, $session_id) { _mollom_debug("_mollom_register_session_id with session ID '$session_id'"); $form_state['values']['session_id'] = $session_id; // Required to make things flow ... } /** * This function inserts a Mollom session ID into the form. It is * called during the construction of the form, just before the form * is rendered. */ function _mollom_insert_session_id(&$form, $form_state) { // The function '_mollom_insert_session_id' can be called as the // result of a POST operation or as the result of an internal form // API operation. Depending on the caller, the session ID can be // found in a different location. // TODO: is this something we can make more consistent in Drupal 7? if (isset($form_state['values']) && isset($form_state['values']['session_id'])) { _mollom_debug("_mollom_insert_session_id with session ID '". $form_state['values']['session_id'] ."'"); $form['session_id'] = array( '#type' => 'hidden', '#value' => $form_state['values']['session_id'], ); } if (isset($form_state['post']) && isset($form_state['post']['session_id'])) { _mollom_debug("_mollom_insert_session_id with session ID '". $form_state['post']['session_id'] ."'"); $form['session_id'] = array( '#type' => 'hidden', '#value' => $form_state['post']['session_id'], ); } } /** * This is a helper function that registers the form data so that on * the next page, we can insert the CAPTCHA on the form. We store * the form's context in $form_state['mollom'] and ask the form to * rebuild itself. The rebuilding code will take the saved context * and use it in the rebuild form: see mollom_form_process(). This * is a bit of a hack but the form API code is a little braindead * when it comes to dynamic forms. Let's try to make this better in * Drupal 7 and beyond. */ function _mollom_register_captcha(&$form_state, $captcha = TRUE) { $form_id = $form_state['values']['form_id']; _mollom_debug("_mollom_register_captcha for $form_id with captcha ". (int)$captcha); $form_state['mollom']['post'] = $_POST; // This is quite a hack ... $form_state['mollom']['captcha'] = $captcha; // Whether to add or remove the CAPTCHA ... $mode = _mollom_get_mode($form_id); if ($mode == MOLLOM_MODE_ANALYSIS) { $form_state['rebuild'] = TRUE; } } /** * This function inserts a CAPTCHA into the form. It is called * during the construction of the form, just before the form * is rendered. */ function _mollom_insert_captcha(&$form, $form_state) { _mollom_debug('_mollom_insert_captcha'); // Prepare the author's IP: $data['author_ip'] = ip_address(); // The function '_mollom_insert_captcha' can be called as the result // of a POST operation or as the result of an internal form API // operation. Depending on the caller, the session ID can be found // in a different location. // TODO: is this something we can make more consistent in Drupal 7? if (isset($form_state['values']) && isset($form_state['values']['session_id'])) { $data['session_id'] = $form_state['values']['session_id']; } if (isset($form_state['post']) && isset($form_state['post']['session_id'])) { $data['session_id'] = $form_state['post']['session_id']; } // Request a CAPTCHA -- we always default to an image CAPTCHA: $response = mollom('mollom.getImageCaptcha', $data); if (isset($response['session_id']) && isset($response['url'])) { _mollom_debug("_mollom_insert_captcha retrieved URL '". $response['url'] ."' and session ID '". $response['session_id'] ."'"); // Include the JavaScript that allows the user to switch to an // AUDIO captcha instead: drupal_add_js(drupal_get_path('module', 'mollom') .'/mollom.js'); // Compute the weight of the CAPTCHA so we can position it in the form. $weight = 99999; foreach (element_children($form) as $key) { // Iterate over the form elements looking for buttons: if (isset($form[$key]['#type']) && ($form[$key]['#type'] == 'submit' || $form[$key]['#type'] == 'button')) { // For each button, slightly increase their weight to allocate // room for the CAPTCHA: if (isset($form[$key]['#weight'])) { $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 the session ID to the form: $form['session_id'] = array( '#type' => 'hidden', '#name' => 'session_id', '#id' => 'edit-session-id', '#value' => $response['session_id'], ); // Add the CAPTCHA to the form: $form['captcha'] = array( '#type' => 'textfield', '#name' => 'captcha', '#id' => 'edit-captcha', '#title' => t('Word verification'), '#field_prefix' => '', '#required' => TRUE, '#size' => 10, '#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."), '#weight' => $weight, ); // Sort the form before rendering: unset($form['#sorted']); } } /** * This function will contact Mollom to verify the configured key pair. */ function _mollom_verify_key() { $status = mollom('mollom.verifyKey'); $message = t('We contacted the Mollom servers to verify your keys'); if ($status) { drupal_set_message(t('@message: the Mollom services are operating correctly. We are now blocking spam.', array('@message' => $message))); } else { drupal_set_message(t('@message: your keys do not exist or are no longer valid. Please visit the user settings page on the Mollom website again: @mollom-user.', array('@message' => $message, '@mollom-user' => 'http://mollom.com/user')), 'error'); } } /** * This function is used to refresh the list of servers that can be used to contact Mollom. */ function _mollom_retrieve_server_list() { // Start from a hard coded list of servers: $servers = array('http://xmlrpc1.mollom.com', 'http://xmlrpc2.mollom.com', 'http://xmlrpc3.mollom.com'); // Use the list of servers to retrieve a list of servers from mollom.com: foreach ($servers as $server) { $result = xmlrpc($server .'/'. MOLLOM_API_VERSION, 'mollom.getServerList', _mollom_authentication()); if (!xmlrpc_errno()) { return $result; } else { watchdog('mollom', 'Error @errno: %server - %message - mollom.getServerList', array('@errno' => xmlrpc_errno(), '%server' => $server, '%message' => xmlrpc_error_msg()), WATCHDOG_ERROR); // Reset the XMLRPC error: xmlrpc_error(0); // FIXME: this is crazy. } } return array(); } /** * Call a remote procedure at the Mollom server. This function will * automatically add the information required to authenticate against * Mollom. */ function mollom($method, $data = array()) { // Initialize refresh variable: $refresh = FALSE; // Retrieve the list of Mollom servers from the database: $servers = variable_get('mollom_servers', NULL); if ($servers == NULL) { // Retrieve a new list of servers: $servers = _mollom_retrieve_server_list(); // 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()); // Debug output: if (isset($data['session_id'])) { _mollom_debug("called $method at server $server with session ID '". $data['session_id'] ."'"); } else { _mollom_debug("called $method at server $server with no session ID"); } if ($errno = xmlrpc_errno()) { if ($errno == MOLLOM_REFRESH) { if (!$refresh) { // Safety pal to avoid endless loops // Retrieve a list of valid Mollom servers from mollom.com: $servers = _mollom_retrieve_server_list(); // Reset the list of servers so we start from the first server in the list: reset($servers); // Store the updated list of servers in the database: variable_set('mollom_servers', $servers); // Log this for debuging purposes: watchdog('mollom', 'The list of available Mollom servers was refreshed: @servers.', array('@servers' => print_r($servers, TRUE))); // Mark that we have refreshed the list: $refresh = TRUE; } } elseif ($errno == MOLLOM_REDIRECT) { // If this is a network error, we go to the next server in the list. $next = next($servers); // Do nothing, we automatically select the next server. watchdog('mollom', 'The Mollom server %server asked to use the next Mollom server in the list: %next.', array('%server' => $server, '%next' => $next)); } else { watchdog('mollom', 'Error @errno from %server: %message - %method -
@data
', array('@errno' => xmlrpc_errno(), '%server' => $server, '%message' => xmlrpc_error_msg(), '%method' => $method, '@data' => print_r($data, TRUE)), WATCHDOG_ERROR); // If it is a 'clean' Mollom error we return instantly. if ($errno == MOLLOM_ERROR) { return $result; } // If this is a network error, we go to the next server in the list. next($servers); } // Reset the XMLRPC error: xmlrpc_error(0); // FIXME: this is crazy. } else { return $result; } } } // If none of the servers worked, activate the fallback mechanism: _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: watchdog('mollom', 'No Mollom servers could be reached or all servers returned an error -- the server list was emptied.', WATCHDOG_ERROR); } /** * This function generate an array with all the information required to * authenticate against Mollom. To prevent that requests are forged and * that you are impersonated, each request is signed with a hash computed * based on a private key and a timestamp. * * Both the client and the server share the secret key that is used to * create the authentication hash based on a timestamp. They both hash * the timestamp with the secret key, and if the hashes match, the * authenticity of the message has been validated. * * To avoid that someone can intercept a (hash, timestamp)-pair and * use that to impersonate a client, Mollom will reject the request * when the timestamp is more than 15 minutes off. * * Make sure your server's time is synchronized with the world clocks, * and that you don't share your private key with anyone else. */ function _mollom_authentication() { $public_key = variable_get('mollom_public_key', ''); $private_key = variable_get('mollom_private_key', ''); // Generate a timestamp according to the dateTime format (http://www.w3.org/TR/xmlschema-2/#dateTime): $time = gmdate("Y-m-d\TH:i:s.\\0\\0\\0O", time()); // Generate a random number: $nonce = md5(mt_rand()); // Calculate a HMAC-SHA1 according to RFC2104 (http://www.ietf.org/rfc/rfc2104.txt): $hash = base64_encode( pack('H*', sha1((str_pad($private_key, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . pack('H*', sha1((str_pad($private_key, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . $time .':'. $nonce .':'. $private_key)))) ); // Store everything in an array. Elsewhere in the code, we'll add the // actual data before we pass it onto the XML-RPC library: $data['public_key'] = $public_key; $data['time'] = $time; $data['hash'] = $hash; $data['nonce'] = $nonce; return $data; } /** * This is a helper function for developers to help debug the form API workflow in this module. * Uncomment the function body to activate. */ function _mollom_debug($message) { // print $message .'
'; // watchdog('mollom', $message); }