TRUE, 'type' => MENU_CALLBACK, 'delivery callback' => 'mollom_test_server_rest_deliver', ); $items[$path . '/site'] = $base + array( 'page callback' => 'mollom_test_server_rest_site', ); $items[$path . '/content'] = $base + array( 'page callback' => 'mollom_test_server_rest_content', ); $items[$path . '/captcha'] = $base + array( 'page callback' => 'mollom_test_server_rest_captcha', ); $items[$path . '/feedback'] = $base + array( 'page callback' => 'mollom_test_server_rest_send_feedback', ); $items[$path . '/blacklist/%'] = $base + array( 'page callback' => 'mollom_test_server_rest_blacklist', 'page arguments' => array($base_args + 2), ); // @todo Whitelist endpoints. return $items; } /** * Returns HTTP request query parameters for the current request. * * @see Mollom::httpBuildQuery() * @see http://php.net/manual/en/wrappers.php.php */ function mollom_test_server_rest_get_parameters() { $data = &drupal_static(__FUNCTION__); if (isset($data)) { return $data; } // @todo Replace with Mollom::getServerParameters() if ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD') { $data = Mollom::httpParseQuery($_SERVER['QUERY_STRING']); // Remove $_GET['q']. // @see .htaccess unset($data['q']); } elseif ($_SERVER['REQUEST_METHOD'] == 'POST' || $_SERVER['REQUEST_METHOD'] == 'PUT') { $data = Mollom::httpParseQuery(file_get_contents('php://input')); } return $data; } /** * Returns the parsed HTTP Authorization request header as an array. * * @todo Replace with Mollom::getServerAuthentication() */ function mollom_test_server_rest_get_auth_header() { $header = &drupal_static(__FUNCTION__); if (isset($header)) { return $header; } $header = array(); if (function_exists('apache_request_headers')) { $headers = apache_request_headers(); if (isset($headers['Authorization'])) { $input = $headers['Authorization']; } } elseif (isset($_SERVER['HTTP_AUTHORIZATION'])) { $input = $_SERVER['HTTP_AUTHORIZATION']; } if (isset($input)) { preg_match_all('@([^, =]+)="([^"]*)"@', $input, $header); $header = array_combine($header[1], $header[2]); } return $header; } /** * Delivery callback for REST API endpoints. */ function mollom_test_server_rest_deliver($page_callback_result) { // All fake-server responses are not cached. drupal_page_is_cacheable(FALSE); drupal_add_http_header('Content-Type', 'application/xml; charset=utf-8'); $xml = new DOMDocument('1.0', 'utf-8'); $element = $xml->createElement('response'); // Append status response parameters. // @todo Add support for custom codes (redirect/refresh) + error messages. $code = 200; if (!is_array($page_callback_result) && $page_callback_result !== TRUE) { switch ($page_callback_result) { case MENU_NOT_FOUND: $code = 404; $message = 'Not found'; break; case Mollom::AUTH_ERROR: $code = 401; $message = 'Unauthorized'; break; default: $code = 400; $message = 'Bad request'; break; } } $status = array( 'code' => $code, ); if (isset($message)) { $status['message'] = $message; } mollom_test_server_rest_add_xml($xml, $element, $status); // Append other response parameters. if (is_array($page_callback_result)) { mollom_test_server_rest_add_xml($xml, $element, $page_callback_result); } $xml->appendChild($element); print $xml->saveXML(); // Perform end-of-request tasks. drupal_page_footer(); } function mollom_test_server_rest_add_xml(DOMDocument $doc, DOMNode $parent, $data, $key = NULL) { if (is_scalar($data)) { // Mollom REST API always uses integers instead of Booleans due to varying // implementations of JSON protocol across client platforms/frameworks. if (is_bool($data)) { $data = (int) $data; } $element = $doc->createTextNode($data); $parent->appendChild($element); } else { foreach ($data as $property => $value) { $key = (is_numeric($property) ? 'item' : $property); $element = $doc->createElement($key); $parent->appendChild($element); mollom_test_server_rest_add_xml($doc, $element, $value, $key); } } } /** * Returns whether the OAuth request signature is valid. */ function mollom_test_server_rest_validate_auth() { $data = mollom_test_server_rest_get_parameters(); $header = mollom_test_server_rest_get_auth_header(); $sites = variable_get('mollom_test_server_site', array()); // Validate the timestamp. $client_time = $header['oauth_timestamp']; $time = REQUEST_TIME; $offset = abs($time - $client_time); if ($offset > Mollom::TIME_OFFSET_MAX) { return FALSE; } $sent_signature = $header['oauth_signature']; unset($header['oauth_signature']); $base_string = implode('&', array( $_SERVER['REQUEST_METHOD'], Mollom::rawurlencode($GLOBALS['base_url'] . '/' . $_GET['q']), Mollom::rawurlencode(Mollom::httpBuildQuery($data + $header)), )); $nonce = $header['oauth_nonce']; if (!isset($sites[$header['oauth_consumer_key']]['privateKey'])) { return FALSE; } $privateKey = $sites[$header['oauth_consumer_key']]['privateKey']; $key = Mollom::rawurlencode($privateKey) . '&' . ''; $signature = rawurlencode(base64_encode(hash_hmac('sha1', $base_string, $key, TRUE))); return $signature === $sent_signature; } /** * REST callback for CRUD site operations. * * @param $publicKey * (optional) The public key of a site. * @param $delete * (optional) Whether to delete the site with $publicKey. */ function mollom_test_server_rest_site($publicKey = NULL, $delete = FALSE) { $data = mollom_test_server_rest_get_parameters(); $bin = 'mollom_test_server_site'; $sites = variable_get($bin, array()); if (isset($publicKey)) { // Validate authentication. if (!mollom_test_server_rest_validate_auth()) { return Mollom::AUTH_ERROR; } // Check whether publicKey exists. if (!isset($sites[$publicKey])) { return MENU_NOT_FOUND; } } if ($_SERVER['REQUEST_METHOD'] == 'GET') { // Return existing site. if (isset($publicKey)) { $response = $sites[$publicKey]; } // Return list of existing sites. else { $response = array( 'list' => array_values($sites), 'listCount' => count($sites), 'listOffset' => 0, 'listTotal' => count($sites), ); return $response; } } else { // Update site. if (isset($publicKey) && !$delete) { $sites[$publicKey] = $data + $sites[$publicKey]; variable_set($bin, $sites); $response = $sites[$publicKey]; } // Create new site. // Authentication is ignored in this case. elseif (!$delete) { $data['publicKey'] = $publicKey = md5(rand() . REQUEST_TIME); $data['privateKey'] = $privateKey = md5(rand() . REQUEST_TIME); // Apply default values. $data += array( 'url' => '', 'email' => '', 'expectedLanguages' => array(), 'subscriptionType' => '', // Client version info is not defined by default. ); $sites[$publicKey] = $data; variable_set($bin, $sites); $response = $data; } // Delete site. else { unset($sites[$publicKey]); variable_set($bin, $sites); return TRUE; } } return array('site' => $response); } /** * REST callback for mollom.checkContent to perform textual analysis. */ function mollom_test_server_rest_content($contentId = NULL) { $data = mollom_test_server_rest_get_parameters(); if ($_SERVER['REQUEST_METHOD'] == 'GET') { // @todo List/read content. if (empty($contentId)) { return FALSE; } return FALSE; } else { // Content ID in request parameters must match the one in path. if (isset($data['id']) && $data['id'] != $contentId) { return FALSE; } if (isset($contentId)) { $data['id'] = $contentId; } } // Default POST: Create or update content and check it. return array('content' => mollom_test_server_check_content($data)); } /** * REST callback to for CAPTCHAs. */ function mollom_test_server_rest_captcha($captchaId = NULL) { $data = mollom_test_server_rest_get_parameters(); if ($_SERVER['REQUEST_METHOD'] == 'GET') { // There is no GET /captcha[/{captchaId}]. return FALSE; } else { // CAPTCHA ID in request parameters must match the one in path. if (isset($data['id']) && $data['id'] != $captchaId) { return FALSE; } // Verify CAPTCHA. if (isset($captchaId)) { $data['id'] = $captchaId; $response = mollom_test_server_check_captcha($data); if (!is_array($response)) { return $response; } return array('captcha' => $response); } } // Create a new CAPTCHA resource. return array('captcha' => mollom_test_server_get_captcha($data)); } /** * REST callback for Blacklist API. * * @param $public_key * The public key of a site. * * @todo Abstract actual functionality like other REST handlers. */ function mollom_test_server_rest_blacklist($public_key, $entryId = NULL, $delete = FALSE) { if (empty($public_key)) { return FALSE; } $data = mollom_test_server_rest_get_parameters(); // Prepare text value. if (isset($data['value'])) { $data['value'] = drupal_strtolower(trim($data['value'])); } $bin = 'mollom_test_server_blacklist_' . $public_key; $entries = variable_get($bin, array()); if ($_SERVER['REQUEST_METHOD'] == 'GET') { // List blacklist entries. if (empty($entryId)) { $response = array(); // Remove deleted entries (== FALSE). $entries = array_filter($entries); $response['list'] = $entries; // @todo Not required yet. $response['listCount'] = count($entries); $response['listOffset'] = 0; $response['listTotal'] = count($entries); return $response; } // Read a single entry. else { // Check whether the entry exists and was not deleted. if (!empty($entries[$entryId])) { return array('entry' => $entries[$entryId]); } else { return MENU_NOT_FOUND; } } } else { // Update an existing entry. if (isset($entryId)) { // Entry ID must match. if (isset($data['id']) && $data['id'] != $entryId) { return FALSE; } // Check that the entry was not deleted. if (empty($entries[$entryId])) { return MENU_NOT_FOUND; } // Entry ID cannot be updated. unset($data['id']); $entries[$entryId] = $data; variable_set($bin, $entries); $response = $data; $response['id'] = $entryId; return array('entry' => $response); } // Create a new entry. elseif (!$delete) { $entryId = max(array_keys($entries)) + 1; $data['id'] = $entryId; $entries[$entryId] = $data; variable_set($bin, $entries); $response = $data; return array('entry' => $response); } // Delete an existing entry. else { // Check that the entry was not deleted already. if (!empty($entries[$entryId])) { $entries[$entryId] = FALSE; variable_set($bin, $entries); return TRUE; } else { return MENU_NOT_FOUND; } } } } /** * REST callback for mollom.sendFeedback to send feedback for a moderated post. */ function mollom_test_server_rest_send_feedback() { $data = mollom_test_server_rest_get_parameters(); // A resource ID is required. if (empty($data['contentId']) && empty($data['captchaId'])) { return 400; } // The feedback is valid if the supplied reason is one of the supported // strings. Otherwise, it's a bad request. $storage = variable_get('mollom_test_server_feedback', array()); $storage[] = $data; variable_set('mollom_test_server_feedback', $storage); // Default value assumed in the API for feedback type is "moderate". if (empty($data['type'])) { $data['type'] = 'moderate'; } $reason_result = in_array($data['reason'], array('spam', 'profanity', 'quality', 'unwanted', 'approve', 'delete')); $feedback_result = in_array($data['type'], array('flag', 'moderate')); return $reason_result && $feedback_result ? TRUE : 400; } /** * API callback for mollom.checkContent to perform textual analysis. * * @todo Add support for 'redirect' and 'refresh' values. */ function mollom_test_server_check_content($data) { $response = array(); // If only a single value for checks is passed, it is a string. if (isset($data['checks']) && is_string($data['checks'])) { $data['checks'] = array($data['checks']); } $header = mollom_test_server_rest_get_auth_header(); $publicKey = $header['oauth_consumer_key']; // Fetch blacklist. $blacklist = variable_get('mollom_test_server_blacklist_' . $publicKey, array()); // Determine content keys to analyze. $post_keys = array('postTitle' => 1, 'postBody' => 1); $type = FALSE; if (isset($data['type']) && in_array($data['type'], array('user'))) { $type = $data['type']; if ($type == 'user') { $post_keys += array('authorName' => 1, 'authorMail' => 1); } } $post = implode('\n', array_intersect_key($data, $post_keys)); $update = isset($data['stored']); // Spam filter: Check post_title and post_body for ham, spam, or unsure. if (!$update && (!isset($data['checks']) || in_array('spam', $data['checks']))) { $spam = FALSE; $ham = FALSE; // 'spam' always has precedence. if (strpos($post, 'spam') !== FALSE) { $spam = TRUE; } // Otherwise, check for 'ham'. elseif (strpos($post, 'ham') !== FALSE) { $ham = TRUE; } // Lastly, take a forced 'unsure' into account. elseif (strpos($post, 'unsure') !== FALSE) { // Enabled unsure mode. if (!isset($data['unsure']) || $data['unsure']) { $spam = TRUE; $ham = TRUE; } // Binary mode. else { $spam = FALSE; $ham = TRUE; } } // Check blacklist. if ($matches = mollom_test_server_check_content_blacklist($post, $blacklist, 'spam')) { $spam = TRUE; $ham = FALSE; $response['reason'] = 'blacklist'; $response['blacklistSpam'] = $matches; } if ($spam && $ham) { $response['spamScore'] = 0.5; $response['spamClassification'] = 'unsure'; $qualityScore = 0.5; } elseif ($spam) { $response['spamScore'] = 1.0; $response['spamClassification'] = 'spam'; $qualityScore = 0.0; } elseif ($ham) { $response['spamScore'] = 0.0; $response['spamClassification'] = 'ham'; $qualityScore = 1.0; } else { $response['spamScore'] = 0.5; $response['spamClassification'] = 'unsure'; $qualityScore = NULL; } // In case a previous spam check was unsure and a CAPTCHA was solved, the // result is supposed to be ham - unless the new content is spam. if (!empty($data['id']) && $response['spamClassification'] == 'unsure') { $content_captchas = variable_get('mollom_test_server_content_captcha', array()); if (!empty($content_captchas[$data['id']])) { $response['spamScore'] = 0.0; $response['spamClassification'] = 'ham'; } } } // Quality filter. if (isset($data['checks']) && in_array('quality', $data['checks'])) { if (isset($qualityScore)) { $response['qualityScore'] = $qualityScore; } else { $response['qualityScore'] = 0; } } // Profanity filter. if (isset($data['checks']) && in_array('profanity', $data['checks'])) { $profanityScore = 0.0; if (strpos($post, 'profanity') !== FALSE) { $profanityScore = 1.0; } // Check blacklist. if ($matches = mollom_test_server_check_content_blacklist($post, $blacklist, 'profanity')) { $profanityScore = 1.0; $response['blacklistProfanity'] = $matches; } $response['profanityScore'] = $profanityScore; } // Language detection. if (isset($data['checks']) && in_array('language', $data['checks'])) { $languages = array(); if (stripos($post, 'ist seit der Mitte')) { $languages[] = array( 'languageCode' => 'de', ); } if (stripos($post, 'it is the most populous city')) { $languages[] = array( 'languageCode' => 'en', ); } if (count($languages) == 0) { $languages[] = array( 'languageCode' => 'zxx', ); } $score = 1/count($languages); foreach($languages as $id => $langObj) { $languages[$id]['languageScore'] = $score; } $response['languages'] = $languages; } $storage = variable_get('mollom_test_server_content', array()); $contentId = (!empty($data['id']) ? $data['id'] : md5(mt_rand())); if (isset($storage[$contentId])) { $storage[$contentId] = array_merge($storage[$contentId], $data); } else { $storage[$contentId] = $data; } if ($update) { $response = array_merge($storage[$contentId], $response); } $response['id'] = $contentId; variable_set('mollom_test_server_content', $storage); return $response; } /** * Checks a string against blacklisted terms. */ function mollom_test_server_check_content_blacklist($string, $blacklist, $reason) { $terms = array(); foreach ($blacklist as $entry) { if ($entry['reason'] == $reason) { $term = preg_quote($entry['value']); if ($entry['match'] == 'exact') { $term = '\b' . $term . '\b'; } $terms[] = $term; } } if (!empty($terms)) { $terms = '/(' . implode('|', $terms) . ')/'; preg_match_all($terms, strtolower($string), $matches); return $matches[1]; } return array(); } /** * API callback for mollom.getImageCaptcha to fetch a CAPTCHA image. */ function mollom_test_server_get_captcha($data) { $captchaId = (!empty($data['id']) ? $data['id'] : md5(mt_rand())); $response = array( 'id' => $captchaId, ); // Return a HTTPS URL if 'ssl' parameter was passed. $base_url = $GLOBALS['base_url']; if (!empty($data['ssl'])) { $base_url = str_replace('http', 'https', $base_url); } $response['url'] = $base_url . '/' . drupal_get_path('module', 'mollom') . '/images/powered-by-mollom-2.gif?captchaId=' . $captchaId; $storage = variable_get('mollom_test_server_captcha', array()); $storage[$captchaId] = $data; variable_set('mollom_test_server_captcha', $storage); return $response; } /** * API callback for mollom.checkCaptcha to validate a CAPTCHA response. */ function mollom_test_server_check_captcha($data) { $response = array(); if (isset($data['solution']) && $data['solution'] == 'correct') { $response['solved'] = TRUE; } else { $response['solved'] = FALSE; $response['reason'] = ''; } $storage = variable_get('mollom_test_server_captcha', array()); $captchaId = $data['id']; if (!isset($storage[$captchaId])) { return MENU_NOT_FOUND; } $storage[$captchaId] = array_merge($storage[$captchaId], $data); $response['id'] = $captchaId; variable_set('mollom_test_server_captcha', $storage); if (isset($storage[$captchaId]['contentId'])) { $contentId = $storage[$captchaId]['contentId']; $content_captchas = variable_get('mollom_test_server_content_captcha', array()); $content_captchas[$contentId] = $response['solved']; variable_set('mollom_test_server_content_captcha', $content_captchas); } return $response; }