'. 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. Administrators can still inspect the logs to see what Mollom has blocked.", array('@logs' => url('admin/logs/watchdog'))) .'
'. ''. 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.") .'
'. ''. t("More information about how Mollom works, is available on the \"How Mollom works\" page and the Mollom FAQ.", array('@mollom-workings' => 'http://mollom.com/how-mollom-works', '@mollom-faq' => 'http://mollom.com/faq')) .'
', '#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, ); $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 event that Mollom stopped servicing your requests, i.e. because a certain usage quota was reached, 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 this problem is resolved. You can also upgrade to Mollom Plus: paying customers automatically get access to Mollom\'s high-availability backend infrastructure not available to free users.', array('@pricing' => 'http://mollom.com/pricing', '@sla' => 'http://mollom.com/standard-service-level-agreement')), ); } $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); } 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', t('Mollom was unavailable (error: @errno - %error_msg)', array('@errno' => xmlrpc_errno(), '%error_msg' => xmlrpc_error_msg())), WATCHDOG_ERROR); } function mollom_protect_form_analysis(&$form, $data) { $mollom = $_POST['session-id'] ? array('session_id' => $_POST['session-id']) : array(); $result = mollom('mollom.checkContent', $data + $mollom); if (isset($result['spam'])) { // We make the response available to other form API handlers: $GLOBALS['mollom_response'] = $result; if ($result['spam'] == MOLLOM_ANALYSIS_HAM) { watchdog('mollom', t('Ham: %message@dataResult:
@result', array('%message' => $data['post_body'], '@data' => print_r($data, TRUE), '@result' => print_r($result, TRUE)))); } else if ($result['spam'] == MOLLOM_ANALYSIS_SPAM) { form_set_error('analysis', t('Your submission has triggered the installed spam filter and will not be accepted.')); watchdog('mollom', t('Spam: %message
@dataResult:
@result', array('%message' => $data['post_body'], '@data' => print_r($data, TRUE), '@result' => print_r($result, TRUE)))); } else { if ($_POST['captcha-solution']) { // Check the CAPTCHA result: $result = mollom('mollom.checkCaptcha', $data + array('session_id' => $_POST['session-id'], 'solution' => $_POST['captcha-solution'], 'author_ip' => _mollom_ip_address())); if (is_bool($result)) { if ($result) { $GLOBALS['mollom_response']['spam'] = MOLLOM_ANALYSIS_HAM; $GLOBALS['mollom_response']['session_id'] = $_POST['session-id']; watchdog('mollom', t('Correct CAPTCHA: %message
@dataResult:
@result', array('%message' => $data['post_body'], '@data' => print_r($data, TRUE), '@result' => print_r($result, TRUE)))); } else { form_set_error('captcha', t('The entered CAPTCHA solution is not correct. We generated a new CAPTCHA so please try again.')); watchdog('mollom', t('Incorrect CAPTCHA: %message
@dataResult:
@result', array('%message' => $data['post_body'], '@data' => print_r($data, TRUE), '@result' => print_r($result, TRUE)))); } } else { _mollom_fallback(); } } else { form_set_error('analysis', t('We are sorry, but the spam filter on this site decided that your submission could be spam. Please fill in the CAPTCHA below to get your submission accepted.')); watchdog('mollom', t('Unsure: %message
@dataResult:
@result', array('%message' => $data['post_body'], '@data' => print_r($data, TRUE), '@result' => print_r($result, TRUE)))); } } } else { _mollom_fallback(); } } function mollom_protect_form_captcha(&$form, $data) { if ($_POST['session-id']) { if ($_POST['captcha-solution']) { // Check the CAPTCHA result: $data += array( 'session_id' => $_POST['session-id'], 'captcha_result' => $_POST['captcha-solution'], 'author_ip' => _mollom_ip_address(), ); $result = mollom('mollom.checkCaptcha', $data); if (is_bool($result)) { // We make the response available to other form API handlers: $GLOBALS['mollom_response']['session_id'] = $_POST['session-id']; if (!$result) { form_set_error('captcha', t('The entered CAPTCHA solution is not correct. We generated a new CAPTCHA so please try again.')); watchdog('mollom', t('Incorrect CAPTCHA
@data', array('@data' => print_r($data, TRUE)))); // Generate a new CAPTCHA: $GLOBALS['mollom_response'] = mollom('mollom.getImageCaptcha', array('author_ip' => _mollom_ip_address(), 'session_id' => $_POST['session-id'])); } else { watchdog('mollom', t('Correct CAPTCHA
@data', array('@data' => print_r($data, TRUE)))); } } else { _mollom_fallback(); } } else { form_set_error('captcha', t('The CAPTCHA field is required.')); // Generate a new CAPTCHA: $GLOBALS['mollom_response'] = mollom('mollom.getImageCaptcha', array('author_ip' => _mollom_ip_address())); } } else { form_set_error('captcha', t('The form data has been altered, the session-id field is required.')); // Generate a new CAPTCHA: $GLOBALS['mollom_response'] = mollom('mollom.getImageCaptcha', array('author_ip' => _mollom_ip_address())); } } function mollom_captcha_prerender($form_id, &$form) { $response = $GLOBALS['mollom_response']; $mode = _mollom_get_mode($form_id); if ($mode == MOLLOM_MODE_ANALYSIS && $response['spam'] == MOLLOM_ANALYSIS_UNSURE) { $response = mollom('mollom.getImageCaptcha', array('author_ip' => _mollom_ip_address(), 'session_id' => $response['session_id'])); $captcha = TRUE; } else if ($mode == MOLLOM_MODE_CAPTCHA) { $response = mollom('mollom.getImageCaptcha', array('author_ip' => _mollom_ip_address(), 'session_id' => $response['session_id'])); $captcha = TRUE; } if (isset($response['session_id'])) { $form['session-id'] = array( '#type' => 'hidden', '#name' => 'session-id', '#id' => 'edit-session-id', '#value' => $response['session_id'], ); if ($captcha) { // Include the Javascript that allows the user to switch to an // AUDIO captcha instead: $url = base_path() . (variable_get('clean_url', 0) ? '' : 'index.php?q='); drupal_add_js(array('mollom_base_url' => $url), 'setting'); drupal_add_js(drupal_get_path('module', 'mollom') .'/mollom.js'); // We add an image captcha to the form. We can't do this from the // validate hook, hence the need for the #pre_render trick. In // Drupal 6, we can simply use $form_state['rebuild'] = TRUE in // the validate-hook, and rebuild the form from there but in // Drupal 5, we have to use this prerender-hook hack. // Because we're already past the prepare state of the form API, // we have a little bit more work to do ...: $form['captcha-solution'] = array( '#type' => 'textfield', '#name' => 'captcha-solution', '#id' => 'edit-captcha-solution', '#parents' => array(), '#title' => t('Word verification'), '#field_prefix' => '', '#required' => TRUE, '#size' => 10, '#value' => $form['#post']['captcha'], '#description' => t("Type the characters shown in the picture above; if you can't read them, submit the form and a new image will be generated."), '#weight' => min($form['submit']['#weight'], $form['preview']['#weight']) + 100, ); // Move the preview and/or submit button below the captcha: $form['preview']['#weight'] += 101; $form['submit']['#weight'] += 101; // The weights changed so we re-sort the array: uasort($form, "_element_sort"); } } } function _mollom_verify_key() { $status = mollom('mollom.verifyKey'); $message = t('We contacted the Mollom servers to verify your keys'); if (xmlrpc_errno()) { drupal_set_message(t('@message: %error (ERROR)', array('@message' => $message, '%error' => xmlrpc_error_msg())), 'error'); } else { 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'); } } } function _mollom_ip_address() { static $ip_address = NULL; if (!isset($ip_address)) { $ip_address = $_SERVER['REMOTE_ADDR']; if (variable_get('reverse_proxy', 0) && array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) { // If an array of known reverse proxy IPs is provided, then trust // the XFF header if request really comes from one of them. $reverse_proxy_addresses = variable_get('reverse_proxy_addresses', array()); if (!empty($reverse_proxy_addresses) && in_array($ip_address, $reverse_proxy_addresses, TRUE)) { // If there are several arguments, we need to check the most // recently added one, i.e. the last one. $ip_address = array_pop(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])); } } } return $ip_address; } /** * 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', t('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 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)) { reset($servers); while ($server = current($servers)) { $result = xmlrpc($server .'/'. MOLLOM_API_VERSION, $method, $data + _mollom_authentication()); 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', t('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', t('The Mollom server %server asked to use the next Mollom server in the list: %next.', array('%server' => $server, '%next' => $next))); } else { watchdog('mollom', t('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', t('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; }