* @version 1.1.3 * * @copyright Copyright (c) 2008, Tijs Verkoyen. All rights reserved. * @license http://mollom.local/license BSD License */ class Mollom { /** * The allowed reverse proxy addresses * * @var array */ private static $allowedReverseProxyAddresses = array(); /** * Your private key * * Set it by calling Mollom::setPrivateKey(''); * * @var string */ private static $privateKey; /** * Your public key * * Set it by calling Mollom::setPublicKey(''); * * @var string */ private static $publicKey; /** * Reverse proxy allowed? * * @var bool */ private static $reverseProxy = false; /** * The default server * * No need to change * * @var string */ private static $serverHost = 'xmlrpc.mollom.com'; /** * The cache for the serverlist * * No need to change * * @var array */ private static $serverList = array(); /** * Default timeout * * @var int */ private static $timeout = 10; /** * The default user-agent * * Change it by calling Mollom::setUserAgent(''); * * @var string */ private static $userAgent = 'MollomPHP/1.1.3'; /** * The current Mollom-version * * No need to change * * @var string */ private static $version = '1.0'; /** * Build the value so we can use it in XML-RPC requests * * @return string * @param mixed $value */ private function buildValue($value) { // get type $type = gettype($value); // build value switch ($type) { case 'string': // escape it, cause Mollom can't handle CDATA (no pun intended) $value = htmlspecialchars($value, ENT_QUOTES, 'ISO-8859-15'); return ''. $value .''."\n"; case 'array': // init struct $struct = ''."\n"; $struct .= ' '."\n"; // loop array foreach ($value as $key => $value) $struct .= str_replace("\n", '', ''. "\n" .''. $key .''. mollom::buildValue($value) .'') . "\n"; $struct .= ' '."\n"; $struct .= ''."\n"; // return return $struct; default: return ''. $value .''."\n"; } } /** * Validates the answer for a CAPTCHA * * When the answer is false, you should request a new image- or audio-CAPTCHA, make sure your provide the current Mollom-sessionid. * The sessionid will be used to track spambots that try to solve CAPTHCA's by brute force. * * @return bool * @param string $sessionId * @param string $solution */ public static function checkCaptcha($sessionId, $solution) { // redefine $sessionId = (string) $sessionId; $solution = (string) $solution; // set autor ip $authorIp = self::getIpAddress(); // set parameters $parameters['session_id'] = $sessionId; $parameters['solution'] = $solution; if($authorIp != null) $parameters['author_ip'] = (string) $authorIp; // do the call $responseString = self::doCall('checkCaptcha', $parameters); // validate if(!isset($responseString->params->param->value->boolean)) throw new Exception('Invalid response in checkCapthca.'); // return if((string) $responseString->params->param->value->boolean == '1') return true; // fallback return false; } /** * Check if the data is spam or not, and gets an assessment of the datas quality * * This function will be used most. The more data you submit the more accurate the claasification will be. * If the spamstatus is 'unsure', you could send the user an extra check (eg. a captcha). * * REMARK: the Mollom-sessionid is NOT related to HTTP-session, so don't send 'session_id'. * * The function will return an array with 3 elements: * - spam the spam-status (unknown/ham/spam/unsure) * - quality an assessment of the content's quality (between 0 and 1) * - session_id Molloms session_id * * @return array * @param string[optional] $sessionId * @param string[optional] $postTitle * @param string[optional] $postBody * @param string[optional] $authorName * @param string[optional] $authorUrl * @param string[optional] $authorEmail * @param string[optional] $authorOpenId * @param string[optional] $authorId */ public static function checkContent($sessionId = null, $postTitle = null, $postBody = null, $authorName = null, $authorUrl = null, $authorEmail = null, $authorOpenId = null, $authorId = null, $ips = array()) { // validate if($sessionId === null && $postTitle === null && $postBody === null && $authorName === null && $authorUrl === null && $authorEmail === null && $authorOpenId === null && $authorId === null) throw new Exception('Specify at least on argument'); // init var $parameters = array(); $aReturn = array(); // add parameters if($sessionId !== null) $parameters['session_id'] = (string) $sessionId; if($postTitle !== null) $parameters['post_title'] = (string) $postTitle; if($postBody !== null) $parameters['post_body'] = (string) $postBody; if($authorName !== null) $parameters['author_name'] = (string) $authorName; if($authorUrl !== null) $parameters['author_url'] = (string) $authorUrl; if($authorEmail !== null) $parameters['author_mail'] = (string) $authorEmail; if($authorOpenId != null) $parameters['author_openid'] = (string) $authorOpenId; if($authorId != null) $parameters['author_id'] = (string) $authorId; // set autor ip if (!sizeof($ips)) { $authorIp = self::getIpAddress(); } else { $authorIp = $ips[0]; } if($authorIp != null) $parameters['author_ip'] = (string) $authorIp; // do the call $responseString = self::doCall('checkContent', $parameters); // validate if(!isset($responseString->params->param->value->struct->member)) throw new Exception('Invalid response in checkContent.'); // loop parts foreach ($responseString->params->param->value->struct->member as $part) { // get key $key = $part->name; // get value switch ($part->name) { case 'spam': $value = (string) $part->value->int; switch($value) { case '0': $aReturn['spam'] = 'unknown'; break; case '1': $aReturn['spam'] = 'ham'; break; case '2': $aReturn['spam'] = 'spam'; break; case '3': $aReturn['spam'] = 'unsure'; break; } break; case 'quality': $aReturn['quality'] = (float) $part->value->double; break; case 'session_id': $aReturn['session_id'] = (string) $part->value->string; break; } } // return return $aReturn; } /** * Make the call * * @return SimpleXMLElement * @param string $method * @param array[optional] $parameters */ private static function doCall($method, $parameters = array(), $server = null, $counter = 0) { // count available servers $countServerList = count(self::$serverList); if($server === null && $countServerList == 0) throw new Exception('No servers found, populate the serverlist. See setServerList().'); // redefine var $method = (string) $method; $parameters = (array) $parameters; // possible methods $aPossibleMethods = array('checkCaptcha', 'checkContent', 'getAudioCaptcha', 'getImageCaptcha', 'getServerList', 'getStatistics', 'sendFeedback', 'verifyKey'); // check if method is valid if(!in_array($method, $aPossibleMethods)) throw new Exception('Invalid method. Only '. implode(', ', $aPossibleMethods) .' are possible methods.'); // check if public key is set if(self::$publicKey === null) throw new Exception('Public key wasn\'t set.'); // check if private key is set if(self::$privateKey === null) throw new Exception('Private key wasn\'t set.'); // still null if($server === null) $server = self::$serverList[$counter]; // cleanup server string $server = str_replace(array('http://', 'https://'), '', $server); // create timestamp $time = gmdate("Y-m-d\TH:i:s.\\0\\0\\0O", time()); // create nonce $nonce = md5(time()); // create has $hash = base64_encode( pack("H*", sha1((str_pad(self::$privateKey, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . pack("H*", sha1((str_pad(self::$privateKey, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . $time . ':'. $nonce .':'. self::$privateKey)))) ); // add parameters $parameters['public_key'] = self::$publicKey; $parameters['time'] = $time; $parameters['hash'] = $hash; $parameters['nonce'] = $nonce; // build request $requestBody = '' ."\n"; $requestBody .= '' ."\n"; $requestBody .= ' mollom.'. $method .'' ."\n"; $requestBody .= ' ' ."\n"; $requestBody .= ' '."\n"; $requestBody .= ' '. self::buildValue($parameters) ."\n"; $requestBody .= ' '."\n"; $requestBody .= ' ' ."\n"; $requestBody .= '' ."\n"; // create curl $curl = @curl_init(); // set useragent @curl_setopt($curl, CURLOPT_USERAGENT, self::$userAgent); // set options @curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); @curl_setopt($curl, CURLOPT_POST, true); @curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); @curl_setopt($curl, CURLOPT_HEADER, true); @curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); @curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, self::$timeout); @curl_setopt($curl, CURLOPT_TIMEOUT, self::$timeout); // set url @curl_setopt($curl, CURLOPT_URL, $server .'/'. self::$version); // set body @curl_setopt($curl, CURLOPT_POSTFIELDS, $requestBody); // get response $response = @curl_exec($curl); // get errors $errorNumber = (int) @curl_errno($curl); $errorString = @curl_error($curl); // close @curl_close($curl); // validate response if($response === false || $errorNumber != 0) { // increment counter $counter++; // no servers left if($errorNumber == 28 && !isset(self::$serverList[$counter]) && $countServerList != 0) throw new Exception('No more servers available, try to increase the timeout.'); // timeout elseif($errorNumber == 28 && isset(self::$serverList[$counter])) return self::doCall($method, $parameters, self::$serverList[$counter], $counter); // other error else throw new Exception('Something went wrong. Maybe the following message can be handy.
'. $errorString, $errorNumber); } // process response $parts = explode("\r\n\r\n", $response); // validate if(!isset($parts[0]) || !isset($parts[1])) throw new Exception('Invalid response in doCall.'); // get headers $headers = $parts[0]; // rebuild body array_shift($parts); $body = implode('', $parts); // validate header $aValidHeaders = array('HTTP/1.0 200', 'HTTP/1.1 200'); if(!in_array(substr($headers, 0, 12), $aValidHeaders)) throw new Exception('Invalid headers.'); // do some validation $responseXML = @simplexml_load_string($body); if($responseXML === false) throw new Exception('Invalid body.'); if(isset($responseXML->fault)) { $code = (isset($responseXML->fault->value->struct->member[0]->value->int)) ? (int) $responseXML->fault->value->struct->member[0]->value->int : 'unknown'; $message = (isset($responseXML->fault->value->struct->member[1]->value->string)) ? (string) $responseXML->fault->value->struct->member[1]->value->string : 'unknown'; // handle errors switch ($code) { // code 1000 (Parse error or internal problem) case 1000: throw new Exception('[error '.$code .'] '. $message, $code); // code 1100 (Serverlist outdated) case 1100: throw new Exception('[error '.$code .'] '. $message, $code); // code 1200 (Server too busy) case 1200: if(self::$serverList === null) self::getServerList(); // do call again return self::doCall($method, $parameters, self::$serverList[$counter], $counter++); break; default: throw new Exception('[error '.$code .'] '. $message, $code); } } // return return $responseXML; } /** * Get a CAPTCHA-mp3 generated by Mollom * * If your already called getAudioCaptcha make sure you provide at least the $sessionId. It will be used * to identify visitors that are trying to break a CAPTCHA with brute force. * * REMARK: the Mollom-sessionid is NOT related to HTTP-session, so don't send 'session_id'. * * The function will return an array with 3 elements: * - session_id the session_id from Mollom * - url the url to the mp3-file * - html html that can be used on your website to display the CAPTCHA * * @return array * @param string[optional] $sessionId */ public static function getAudioCaptcha($sessionId = null) { // init vars $aReturn = array(); $parameters = array(); // set autor ip $authorIp = self::getIpAddress(); // set parameters if($sessionId != null) $parameters['session_id'] = (string) $sessionId; if($authorIp != null) $parameters['author_ip'] = (string) $authorIp; // do the call $responseString = self::doCall('getAudioCaptcha', $parameters); // validate if(!isset($responseString->params->param->value->struct->member)) throw new Exception('Invalid response in getAudioCaptcha.'); // loop elements foreach ($responseString->params->param->value->struct->member as $part) $aReturn[(string) $part->name] = (string) $part->value->string; // add image html $aReturn['html'] = ''."\n" ."\t".''."\n" ."\t".''."\n" .''; // return return $aReturn; } /** * Get a CAPTCHA-image generated by Mollom * * If your already called getImageCaptcha make sure you provide at least the $sessionId. It will be used * to identify visitors that are trying to break a CAPTCHA with brute force. * * REMARK: the Mollom-sessionid is NOT related to HTTP-session, so don't send 'session_id'. * * The function will return an array with 3 elements: * - session_id the session_id from Mollom * - url the url to the image * - html html that can be used on your website to display the CAPTCHA * * @return array * @param string[optional] $sessionId */ public static function getImageCaptcha($sessionId = null) { // init vars $aReturn = array(); $parameters = array(); // set autor ip $authorIp = self::getIpAddress(); // set parameters if($sessionId !== null) $parameters['session_id'] = (string) $sessionId; if($authorIp !== null) $parameters['author_ip'] = (string) $authorIp; // do the call $responseString = self::doCall('getImageCaptcha', $parameters); // validate if(!isset($responseString->params->param->value->struct->member)) throw new Exception('Invalid response in getImageCaptcha.'); // loop elements foreach ($responseString->params->param->value->struct->member as $part) $aReturn[(string) $part->name] = (string) $part->value->string; // add image html $aReturn['html'] = 'Mollom CAPTCHA'; // return return $aReturn; } /** * Get the real IP-address * * @return string */ public static function getIpAddress() { // pre check if(!isset($_SERVER['REMOTE_ADDR'])) return null; // get ip $ipAddress = $_SERVER['REMOTE_ADDR']; if(self::$reverseProxy) { if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { if(!empty(self::$allowedReverseProxyAddresses) && in_array($ipAddress, self::$allowedProxyAddresses, true)) { return array_pop(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])); } } // running in a cluster environment if(isset($_SERVER['HTTP_X_CLUSTER_CLIENT_IP'])) return $_SERVER['HTTP_X_CLUSTER_CLIENT_IP']; } // fallback print 'IP:'.$ipAddress; return $ipAddress; } /** * Obtains a list of valid servers * * @return array */ public static function getServerList($counter = 0) { // do the call $responseString = self::doCall('getServerList', array(), self::$serverHost, $counter); // validate if(!isset($responseString->params->param->value->array->data->value)) throw new Exception('Invalid response in getServerList.'); // loop servers and add them foreach ($responseString->params->param->value->array->data->value as $server) self::$serverList[] = (string) $server->string; if(count(self::$serverList) == 0) self::$serverList = array('http://xmlrpc3.mollom.com', 'http://xmlrpc2.mollom.com', 'http://xmlrpc1.mollom.com'); // return return self::$serverList; } /** * Retrieve statistics from Mollom * * Allowed types are listed below: * - total_days Number of days Mollom has been used * - total_accepted Total of blocked spam * - total_rejected Total accepted posts (not working?) * - yesterday_accepted Amount of spam blocked yesterday * - yesterday_rejected Number of posts accepted yesterday (not working?) * - today_accepted Amount of spam blocked today * - today_rejected Number of posts accepted today (not working?) * * @return int * @param string $type */ public static function getStatistics($type) { // possible types $aPossibleTypes = array('total_days', 'total_accepted', 'total_rejected', 'yesterday_accepted', 'yesterday_rejected', 'today_accepted', 'today_rejected'); // redefine $type = (string) $type; // validate if(!in_array($type, $aPossibleTypes)) throw new Exception('Invalid type. Only '. implode(', ', $aPossibleTypes) .' are possible types.'); // do the call $responseString = self::doCall('getStatistics', array('type' => $type)); // validate if(!isset($responseString->params->param->value->int)) throw new Exception('Invalid response in getStatistics.'); // return return (int) $responseString->params->param->value->int; } /** * Send feedback to Mollom. * * With this method you can help train Mollom. Implement this method if possible. The more feedback is provided the more accurate * Mollom will be. * * Allowed feedback-strings are listed below: * - spam Spam or unsolicited advertising * - profanity Obscene, violent or profane content * - low-quality Low-quality content or writing * - unwanted Unwanted, taunting or off-topic content * * @return bool * @param string $sessionId * @param string $feedback */ public static function sendFeedback($sessionId, $feedback) { // possible feedback $aPossibleFeedback = array('spam', 'profanity', 'low-quality', 'unwanted'); // redefine $sessionId = (string) $sessionId; $feedback = (string) $feedback; // validate if(!in_array($feedback, $aPossibleFeedback)) throw new Exception('Invalid feedback. Only '. implode(', ', $aPossibleFeedback) .' are possible feedback-strings.'); // build parameters $parameters['session_id'] = $sessionId; $parameters['feedback'] = $feedback; // do the call $responseString = self::doCall('sendFeedback', $parameters); // validate if(!isset($responseString->params->param->value->boolean)) throw new Exception('Invalid response in sendFeedback.'); // return if((string) $responseString->params->param->value->boolean == 1) return true; // fallback return false; } /** * Set the allowed reverse proxy Addresses * * @return void * @param array $addresses */ public static function setAllowedReverseProxyAddresses($addresses) { // store allowed ip-addresses self::$allowedReverseProxyAddresses = (array) $addresses; // set reverse proxy self::$reverseProxy = (!empty($addresses)) ? true : false; } /** * Set the private key * * @return void * @param string $key */ public static function setPrivateKey($key) { self::$privateKey = (string) $key; } /** * Set the public key * * @return void * @param string $key */ public static function setPublicKey($key) { self::$publicKey = (string) $key; } /** * Set the server list * * @return void * @param array $servers */ public static function setServerList($servers) { // redefine $server = (array) $servers; // loop servers foreach ($servers as $server) self::$serverList[] = $server; } /** * Set timeout * * @return void * @param int $timeout */ public static function setTimeOut($timeout) { // redefine $timeout = (int) $timeout; // validate if($timeout == 0) throw new Exception('Invalid timeout. Timeout shouldn\'t be 0.'); // set property self::$timeout = $timeout; } /** * Set the user agent * * @return void * @param string $newUserAgent */ public static function setUserAgent($newUserAgent) { self::$userAgent .= ' '. (string) $newUserAgent; } /** * Verifies your key * * Returns information about the status of your key. Mollom will respond with a boolean value (true/false). * False means that your keys is disabled or doesn't exists. True means the key is enabled and working properly. * * @return bool */ public static function verifyKey() { // do the call $responseString = self::doCall('verifyKey'); // validate if(!isset($responseString->params->param->value->boolean)) throw new Exception('Invalid response in verifyKey.'); // return if((string) $responseString->params->param->value->boolean == '1') return true; // fallback return false; } } ?>