1<?php 2/** 3 * FormSpamCheck class 4 * 5 * The purpose of this class is to have a single interface to multiple anti-form-spam services on the internet. 6 * That way, you can write your application code to check for spam and decide which of the services to use 7 * to actually identify the spam. 8 * 9 * 10 * Currently supported: 11 * 12 * StopForumSpam: http://www.stopforumspam.com 13 * 14 * Project Honeypot: http://www.projecthoneypot.org 15 * 16 * Akismet: http://www.akismet.com 17 * 18 * Mollom: http://www.mollom.com 19 * 20 * @author Michiel Dethmers, phpList Ltd, http://www.phplist.com 21 * @version 0.2 - Sept 8th 2011 - added Mollom support 22 * 23 * version 0.1 - 24 August 2011 24 * @license LGPL (Lesser Gnu Public License) http://www.gnu.org/licenses/lgpl-3.0.html 25 * @package FormSpamCheck 26 * Free to use, distribute and modify in Open as well as Closed Source software 27 * NO WARRANTY WHATSOEVER! 28 * --------------- 29 * 30 * For more information and how to set up and configure, http://www.phplist.com/formspamclass 31 * 32 * 33 * It currently uses three services, stopforumspam.com, project honeypot and akismet 34 * If you know of any other services that can be integrated, let me know. 35 * 36 * Credits: Very loosely based on the original phpBB mod from "microUgly" 37 * http://www.phpbb.com/community/viewtopic.php?f=70&t=1349145 38 * 39 * 40 */ 41 42 43/** 44 * FormspamCheck class, centralised spam protection 45 * 46 * Check form submission against multipe spam protection sources 47 * 48 * @example example.php 49 * 50 * @package FormSpamCheck 51 * @subpackage classes 52 * 53 */ 54class botBouncer { 55 56 /** var LE - line ending */ 57 private $LE = "\n"; 58 private $honeyPotApiKey = ''; 59 private $akismetApiKey = ''; 60 private $akismetBlogURL = ''; 61 private $memCached = false; 62 private $doHpCheck = false; 63 private $akismetEnabled = false; 64 private $logRoot = '/var/log/formspam'; 65 private $logActivity = true; 66 private $debug = false; 67 private $debugToLog = true; 68 private $UA = 'FormSpamCheck class (v.0.0.1)'; 69 // The StopFormSpam API URL 70 private $stopSpamAPIUrl = 'http://www.stopforumspam.com/api'; 71 private $startTime = 0; 72 private $mollomCheck = ''; 73 private $mollomEnabled = false; 74 75 /** 76 * (array) matchDetails - further details on a match provided by SFS 77 */ 78 79 public $matchDetails = ''; 80 81 /** 82 * (string) matchedBy - which service returned the match when isSpam returns true 83 */ 84 85 public $matchedBy = ''; 86 87 /** 88 * (string) matchedOn - what field was matched when isSpam returns true 89 */ 90 91 public $matchedOn = ''; 92 93 /** 94 * (bool) isSpam - flag indicating spam (true) or ham (false) after running any spamcheck 95 */ 96 public $isSpam = false; 97 98 private $services = array( 99 'SFS' => 'Stop Forum Spam', 100 'HP' => 'Honeypot Project', 101 'AKI' => 'Akismet', 102 'MOL' => 'Mollom', 103 ); 104 105 private $sfsSpamTriggers = array ( ## set a default, in case it's not in config 106 'username' => array ( 107 'ban_end' => FALSE, 108 'freq_tolerance' => 2, 109 'ban_reason' => 'You have been identified as a spammer.', 110 ), 111 'email' => array ( 112 'ban_end' => FALSE, 113 'freq_tolerance' => 0, 114 'ban_reason' => 'You have been identified as a spammer.', 115 ), 116 'ip' => array ( 117 'ban_end' => 604800,// 7 days 118 'freq_tolerance' => 1, 119 'ban_reason' => 'You have been identified as a spammer.', 120 ) 121 ); 122 123 private $akismetFields = array( 124 'blog', 125 'user_ip', 126 'user_agent', 127 'referrer', 128 'permalink', 129 'comment_type', 130 'comment_author', 131 'comment_author_email', 132 'comment_author_url', 133 'comment_content' 134 ); 135 136 public function setDebug($setting) { 137 $this->debug = (bool)$setting; 138 $this->debugToLog = (bool) $setting; 139 } 140 141 /** 142 * constructor 143 * 144 * initialise class with services available. There's no need to use all, if any service is not 145 * configured, the check for it will be disabled automatically 146 * 147 * @param string $hpKey - API key for Honeypot Project 148 * @param string $akismetKey - API key for Akismet service 149 * @param string $akismetUrl - BlogURL for Akismet service 150 * 151 */ 152 153 public function __construct($hpKey = '',$akismetKey = '',$akismetUrl = '', $mollomPrivateKey = '',$mollomPublicKey = '') { 154 if (!function_exists('curl_init')) { 155 print 'curl dependency error'; 156 return; 157 } 158 $this->dbg('FSC Init'); 159 if (!empty($hpKey)) { 160 $this->honeyPotApiKey = $hpKey; 161 $this->doHpCheck = true; 162 } elseif (!empty($GLOBALS['honeyPotApiKey'])) { 163 $this->honeyPotApiKey = $GLOBALS['honeyPotApiKey']; 164 $this->doHpCheck = true; 165 } 166 if (!empty($akismetKey)) { 167 $this->akismetApiKey = $akismetKey; 168 $this->akismetEnabled = true; 169 } elseif (!empty($GLOBALS['akismetApiKey'])) { 170 $this->akismetApiKey = $GLOBALS['akismetApiKey']; 171 $this->akismetEnabled = true; 172 } 173 if (!empty($akismetUrl)) { 174 $this->akismetBlogURL = $akismetUrl; 175 } elseif (!empty($GLOBALS['akismetBlogURL'])) { 176 $this->akismetBlogURL = $GLOBALS['akismetBlogURL']; 177 ## @todo verify validity 178 } elseif (!empty($_SERVER['HTTP_HOST'])) { 179 $this->akismetBlogURL = $_SERVER['HTTP_HOST']; 180 } 181 182 if (!empty($GLOBALS['logRoot']) && is_writable($GLOBALS['logRoot'])) { 183 $this->logRoot = $GLOBALS['logRoot']; 184 } 185 if (isset($GLOBALS['ForumSpamBanTriggers'])) { 186 $this->spamTriggers = $GLOBALS['ForumSpamBanTriggers']; 187 } 188 189 if (isset($GLOBALS['memCachedServer']) && class_exists('Memcached', false)) { 190 $this->setMemcached($GLOBALS['memCachedServer']); 191 } else { 192 if (!class_exists('Memcached',false)) { 193 $this->dbg('memcache not available, class "Memcached" not found'); 194 } else { 195 $this->dbg('memcache not available, config "memCachedServer" not set'); 196 } 197 } 198 199 if (is_file(dirname(__FILE__).'/mollom.php') && !empty($mollomPrivateKey) && !empty($mollomPublicKey)) { 200 $this->dbg('loading mollom'); 201 @include dirname(__FILE__).'/mollom.php'; 202 if (class_exists('Mollom',false)) { 203 $this->mollomCheck = new Mollom(); 204 $this->dbg('mollom instantiated'); 205 try { 206 $this->mollomCheck->setPrivateKey($mollomPrivateKey); 207 $this->mollomCheck->setPublicKey($mollomPublicKey); 208 $serverList = $this->getCache('mollomServerList'); 209 if (empty($serverList)) { 210 $serverList = $this->mollomCheck->getServerList(); 211 $this->setCache('mollomServerList',$serverList); 212 } else { 213 $this->mollomCheck->setServerList($serverList); 214 } 215 $validKey = $this->getCache('mollomKeyValid'); 216 if ($validKey == 'YES') { 217 $this->mollomEnabled = true; 218 } else { 219 if ($this->mollomCheck->verifyKey()) { 220 $this->mollomEnabled = true; 221 $this->setCache('mollomKeyValid','YES'); 222 } else { 223 $this->setCache('mollomKeyValid','NO'); 224 } 225 } 226 } catch (Exception $e) { 227 $this->dbg('Mollon exception: '.$e->getMessage()); 228 $this->mollomEnabled = false; 229 } 230 } else { 231 $this->dbg('mollom class not found'); 232 } 233 } else { 234 $this->dbg('mollom not enabled'); 235 } 236 237 $now = gettimeofday(); 238 $this->startTime = $now['sec'] * 1000000 + $now['usec']; 239 } 240 241 /** 242 * setLogRoot - specify where to write logfiles 243 * 244 * @param string $dir - directory where to write to, defaults to /var/log/formspam 245 * @return bool - true is successful 246 */ 247 248 public function setLogRoot ($dir) { 249 if (!empty($dir) && is_writable($dir)) { 250 $this->logRoot = $dir; 251 $this->dbg('Logging to '.$dir); 252 return true; 253 } else { 254 $this->dbg('Unable to write logs to '.$dir); 255 return false; 256 } 257 } 258 259 /** setMemcached 260 * 261 * use memCached server for caching 262 * 263 * @param string memCachedServer = server for memcache (use servername:port if port differs from default) 264 * @return bool success 265 */ 266 267 public function setMemcached($memCachedServer = '') { 268 if (class_exists('Memcached') && !empty($memCachedServer)) { 269 $this->memCached = new Memcached(); 270 if (strpos($memCachedServer,':') !== FALSE) { 271 list($server,$port) = explode(':',$memCachedServer); 272 } else { 273 $server = $memCachedServer; 274 $port = 11211; 275 } 276 $this->dbg('memcache: '.$server); 277 return $this->memCached->addServer($server,$port); 278 } 279 return false; 280 } 281 282 private function dbg($msg) { 283 if ($this->debugToLog) { 284 $this->addLogEntry('fsc-debug.log',$msg); 285 } 286 287 if (!$this->debug) return; 288 print $msg."\n"; 289 } 290 291 /** 292 * elapsed, a simple timer to monitor speed 293 * 294 * @return the number of microseconds used since instantiation 295 */ 296 297 public function elapsed() { 298 $now = gettimeofday(); 299 $end = $now['sec'] * 1000000 + $now['usec']; 300 $elapsed = $end - $this->startTime; 301 return $elapsed; 302 } 303 304 private function addLogEntry($logFile,$entry) { 305 if (empty($this->logRoot)) return; 306 if (!$this->logActivity) return; 307 $logFile = basename($logFile,'.log'); 308 if (!is_writable($this->logRoot)) { 309 # $this->dbg('cannot write logfile '.$this->logRoot.'/'.$logFile.date('Y-m-d').'.log'); 310 return; 311 } 312 $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ' - '; 313 if (isset($_SERVER['REQUEST_URI'])) { 314 $logEntry = date('Y-m-d H:i:s').' '.$ip.' '.$_SERVER['REQUEST_URI'].' '.$entry; 315 } else { 316 $logEntry = date('Y-m-d H:i:s').' '.$ip.' - '.$entry; 317 } 318 file_put_contents($this->logRoot.'/'.$logFile.date('Y-m-d').'.log',$logEntry."\n",FILE_APPEND); 319 } 320 321 private function getCache($key) { 322 if (!$this->memCached) return false; 323 $val = $this->memCached->get($key); 324 $this->dbg('CACHE: '.$key .' = '.$val); 325 return $val; 326 } 327 328 private function setCache($key,$val,$expiry = 0) { 329 if (!$this->memCached) return false; 330 if (!$expiry) $expiry = 86400; 331 return $this->memCached->set($key,$val,$expiry); 332 } 333 334 private function defaults($item) { 335 switch ($item) { 336 case 'ip': return $_SERVER['REMOTE_ADDR']; 337 case 'email': return ''; 338 case 'username': return 'Anonymous'; 339 default: return ''; 340 } 341 } 342 343 private function setDefaults($data) { 344 if (!isset($data['url'])) $data['url'] = ''; 345 if (!isset($data['content'])) $data['content'] = ''; 346 if (!isset($data['ips']) || !is_array($data['ips'])) $data['ips'] = array($this->defaults('ip')); 347 return $data; 348 } 349 350 /** 351 * honeypotCheck - verify IP using Honeypot project 352 * 353 * @param string $ip - IP address to check 354 * @return bool - true is spam, false is ham 355 * 356 */ 357 358 public function honeypotCheck($ip) { 359 if (!$this->doHpCheck) return; 360 361 ## honeypot requests will be cached in DNS anyway 362 $rev = array_reverse(explode('.', $ip)); 363 $lookup = $this->honeyPotApiKey.'.'.implode('.', $rev) . '.dnsbl.httpbl.org'; 364 365 $rev = gethostbyname($lookup); 366 if ($lookup != $rev) { 367 $this->matchedOn = 'ip'; 368 $this->addLogEntry('honeypot.log','SPAM '.$lookup.' '.$rev); 369 $this->isSpam = true; 370 return true; 371 } else { 372 $this->addLogEntry('honeypot.log','HAM '.$lookup.' '.$rev); 373 return false; 374 } 375 } 376 377 // Authenticates your Akismet API key 378 private function akismet_verify_key() { 379# $this->dbg('akismet key check'); 380 381 if (empty($this->akismetApiKey)) { 382 $this->dbg('No Akismet API Key'); 383 return false; 384 } 385 $cached = $this->getCache('akismetKeyValid'); 386 if (empty($cached)) { 387 $request = array( 388 'key'=> $this->akismetApiKey, 389 'blog' => $this->akismetBlogURL 390 ); 391 392 $keyValid = $this->doPOST('http://rest.akismet.com/1.1/verify-key',$request); 393 $this->addLogEntry('akismet.log','KEY CHECK: '.$keyValid.' http://rest.akismet.com/1.1/verify-key'.serialize($request)); 394 $this->setCache('akismetKeyValid',$keyValid); 395 } else { 396 $this->addLogEntry('akismet.log','KEY CHECK (cached) '.$cached); 397 $this->dbg('akismet key (cached) '.$cached); 398 $keyValid = $cached; 399 } 400 401 if ( 'valid' == $keyValid ) { 402 $this->dbg('akismet key valid'); 403 return true; 404 } else { 405 $this->dbg('akismet key not valid'); 406 return false; 407 } 408 } 409 410 411 /** 412 * mollomCheck - check data against mollom 413 * 414 * @param array $data - associative array with data to use for checking 415 * 416 * @return bool: true is spam, false is ham 417 */ 418 419 public function mollomCheck($data) { 420 if (!$this->mollomEnabled) return false; 421 $this->dbg('mollom check'); 422 $data = $this->setDefaults($data); 423 $cached = $this->getCache('mollom'.md5(serialize($data))); 424 if (!empty($cached)) { 425 $isSpam = $cached; 426 $data['fromcache'] = '(cached)'; // for logging 427 } else { 428 try { 429 $isSpam = $this->mollomCheck->checkContent( 430 '', # sessionID 431 '', # $postTitle 432 $data['content'], # $postBody 433 $data['username'], # $authorName 434 $data['url'], # $authorUrl 435 $data['email'], # authorEmail 436 '', # $authorOpenId 437 '', # $authorId 438 $data['ips'] ## added to mollom.php class for commandline processing 439 ); 440 $this->setCache('mollom'.md5(serialize($data)),$isSpam); 441 $data['fromcache'] = ''; 442 } catch (Exception $e) { 443 $this->dbg('Exception thrown '.$e->getMessage()); 444 $isSpam = array('spam'=> 'exception'); 445 } 446 } 447 448 if ($isSpam['spam'] == 'spam') { 449 $this->dbg('mollom check SPAM'); 450 $this->matchedOn = 'unknown'; 451 $this->addLogEntry('mollom.log',$data['fromcache'].' SPAM '.$data['username'].' '.$data['email'].' '.join(',',$data['ips'])); 452 $this->isSpam = true; 453 return true; 454 } else { 455 ## mollom has state "unsure" as well, but let's just take that as HAM for now 456 $this->dbg('mollom check HAM'); 457 $this->addLogEntry('mollom.log',$data['fromcache'].' HAM '.$data['username'].' '.$data['email'].' '.join(',',$data['ips'])); 458 return false; 459 } 460 } 461 462 /** 463 * akismetCheck - check data against akismet 464 * 465 * @param array $data - associative array with data to use for checking 466 * 467 * possible keys for data (all optional): blog, user_ip, user_agent, referrer, permalink, comment_type, comment_author, comment_author_email, comment_author_url, comment_content 468 * 469 * @return bool: true is spam, false is ham 470 */ 471 472 public function akismetCheck($data) { 473 if (!$this->akismetEnabled) return false; 474 if (!$this->akismet_verify_key()) return false; 475 $this->dbg('akismet check'); 476 if (!is_array($data['ips'])) $data['ips'] = array(); 477 478 ## set some values the way akismet expects them 479 $data['user_ip'] = !empty($data['ips'][0]) ? $data['ips'][0]: $this->defaults('ip'); ## akismet only handles one IP, so take the first 480 $data['comment_author'] = !empty($data['username']) ? $data['username'] : $this->defaults('username'); 481 $data['comment_author_email'] = !empty($data['email']) ? $data['email'] : $this->defaults('email'); 482 $data['comment_content'] = !empty($data['content']) ? $data['content'] : $this->defaults('content'); 483 484 foreach ($this->akismetFields as $field) { 485 if (!isset($data[$field])) { 486 switch ($field) { 487 ## set some defaults that will probably return Ham 488 case 'blog': $data['blog'] = $this->akismetBlogURL;break; 489 case 'user_ip': $data['user_ip'] = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR']:'';break; 490 case 'user_agent': $data['user_agent'] = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT']:'';break; 491 case 'referrer': $data['referrer'] = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER']:'http://www.wordpress.com';break; 492 case 'permalink': $data['permalink'] = '';break; 493 case 'comment_type': $data['comment_type'] = 'comment';break; 494 case 'comment_author': $data['comment_author'] = 'Admin';break; 495 case 'comment_author_email': $data['comment_author_email'] = 'formspamcheck@gmail.com';break; 496 case 'comment_author_url': $data['comment_author_url'] = '';break; 497 case 'comment_content': $data['comment_content'] = '';break; 498 } 499 } 500 } 501 502 $cached = $this->getCache('akismet'.md5(serialize($data))); 503 if (!empty($cached)) { 504 $isSpam = $cached; 505 $data['fromcache'] = '(cached)'; // for logging 506 } else { 507 $isSpam = $this->doPOST('http://'.$this->akismetApiKey.'.rest.akismet.com/1.1/comment-check',$data); 508 $this->setCache('akismet'.md5(serialize($data)),$isSpam); 509 $data['fromcache'] = ''; 510 } 511 512 if ( 'true' == $isSpam ) { 513 $this->dbg('akismet check SPAM'); 514 $this->matchedOn = 'unknown'; 515 $this->addLogEntry('akismet.log',$data['fromcache'].' SPAM '.$data['username'].' '.$data['email'].' '.join(',',$data['ips'])); 516 $this->isSpam = true; 517 return true; 518 } else { 519 $this->dbg('akismet check HAM'); 520 $this->addLogEntry('akismet.log',$data['fromcache'].' HAM '.$data['username'].' '.$data['email'].' '.join(',',$data['ips'])); 521 return false; 522 } 523 } 524 525 /** 526 * doPOST - run a POST request to some URL and return the result 527 */ 528 private function doPOST($url,$requestdata = array()) { 529 $date = date('r'); 530 531 $requestheader = array( 532 'Host: '.parse_url($url,PHP_URL_HOST), 533 'Content-Type: application/x-www-form-urlencoded', 534 'Date: '. $date, 535 ); 536 $data = ''; 537 foreach ($requestdata as $param => $value) { 538 if (!is_array($value)) { 539 $data .= $param.'='.urlencode($value).'&'; 540 } // else -> forget about arrays for now 541 } 542 $data = substr($data,0,-1); 543 $requestheader[] = 'Content-Length: '.strlen($data); 544 545 $header = ''; 546 foreach ($requestheader as $param) { 547 $header .= $param.$this->LE; 548 } 549 550 $curl = curl_init(); 551 curl_setopt($curl, CURLOPT_URL, $url); 552 curl_setopt($curl, CURLOPT_TIMEOUT, 30); 553 curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 554 curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE); 555 curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE); 556 curl_setopt($curl, CURLOPT_HTTPHEADER,$requestheader); 557 curl_setopt($curl, CURLOPT_DNS_USE_GLOBAL_CACHE, TRUE); 558 curl_setopt($curl, CURLOPT_USERAGENT,$this->UA); 559 curl_setopt($curl, CURLOPT_POST, 1); 560 561 curl_setopt($curl, CURLOPT_POSTFIELDS, $data); 562 563 $result = curl_exec($curl); 564 $status = curl_getinfo($curl,CURLINFO_HTTP_CODE); 565 if ($status != 200) { 566 $error = curl_error($curl); 567 $this->dbg('Curl Error '.$status.' '.$error); 568 } 569 curl_close($curl); 570 return $result; 571 } 572 573 /** 574 * doGET - run a GET request to some URL and return the result 575 */ 576 577 private function doGET($cUrl) { 578 $ch = curl_init(); 579 curl_setopt($ch, CURLOPT_URL, $cUrl); 580 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 581 $result = curl_exec($ch); 582 return $result; 583 } 584 585 586 /** setSFSSpamTriggers - set StopForumSpam triggers, if you want to be more specific on the triggers 587 * 588 * @param array $triggers array with details for SFS triggers 589 * 590 * defaults to: 591 * 592 * array ( 593 * 594 * 'username' => array ( // ban on username 595 * 596 * 'ban_end' => FALSE, // Permanent ban 597 * 598 * 'freq_tolerance' => 2, // allow when 2 or less in the frequency API field 599 * 600 * 'ban_reason' => 'Error processing data, please try again', ## let's not make them any wiser 601 * 602 * ), 603 * 604 * 'email' => array ( // ban on email 605 * 606 * 'ban_end' => FALSE, // Permanent ban 607 * 608 * 'freq_tolerance' => 0, 609 * 610 * 'ban_reason' => 'Error processing data, please try again', ## let's not make them any wiser 611 * 612 * ), 613 * 614 * 'ip' => array ( // ban on ip address 615 * 616 * 'ban_end' => 630000, // 60*60*24*7 ban for 7 days 617 * 618 * 'freq_tolerance' => 1, 619 * 620 * 'ban_reason' => 'Error processing data, please try again', ## let's not make them any wiser 621 * 622 * ) 623 * 624 *); 625 * 626 * 627 * @returns null 628 * 629 */ 630 631 632 public function setSFSSpamTriggers($triggers = array()) { 633 if (sizeof($triggers)) { 634 $this->spamTriggers = $triggers; 635 } 636 } 637 638 /** 639 * stopForumSpamCheck - check using the SFS API 640 * 641 * @param array $data - array containing data to check 642 * 643 * needs to contain at least one of 644 * 645 * $data['username'] - (string) username to check 646 * 647 * $data['ips'] - (array) list of IPs to check 648 * 649 * $data['email'] - (string) email to check 650 * 651 * @return integer - number of times something was matched 652 * 653 */ 654 655 function stopForumSpamCheck($data = array()) { 656 if (!sizeof($data['ips']) && isset($_SERVER['REMOTE_ADDR'])) { 657 $data['ips'][] = $_SERVER['REMOTE_ADDR']; 658 if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { 659 $data['ips'][] = $_SERVER['HTTP_X_FORWARDED_FOR']; 660 } 661 } 662 663 $isSfsSpam = 0; 664 $this->dbg('SFS check'); 665 666 $spamTriggers = $this->sfsSpamTriggers; 667 if (empty($data['username'])) { 668 unset($spamTriggers['username']); 669 } else { 670 $spamTriggers['username']['value'] = $data['username']; 671 } 672 if (empty($data['ips'])) { 673 unset($spamTriggers['ip']); 674 } else { 675 $spamTriggers['ip']['value'] = $data['ips']; 676 } 677 if (empty($data['email'])) { 678 unset($spamTriggers['email']); 679 } else { 680 $spamTriggers['email']['value'] = $data['email']; 681 } 682 683 $apiRequest = ''; 684 foreach ($spamTriggers as $trigger => $banDetails) { 685 if (!empty($banDetails['value'])) { 686 if (is_array($banDetails['value'])) { 687 foreach ($banDetails['value'] as $v) { 688 $apiRequest .= $trigger.'[]='.urlencode($v).'&'; 689 } 690 } else { 691 $apiRequest .= $trigger.'[]='.urlencode($banDetails['value']).'&'; 692 } 693 } 694 } 695 696 $cached = $this->getCache('SFS'.$apiRequest); 697 if (!$cached) { 698 $cUrl = $this->stopSpamAPIUrl.'?'.$apiRequest.'&unix'; 699 $this->addLogEntry('sfs-apicall.log',$cUrl); 700 $xml = $this->doGET($cUrl); 701 702 if (!$xml) { 703 $this->addLogEntry('sfs-apicall.log','FAIL ON XML'); 704 return false; 705 } 706 $this->setCache('SFS'.$apiRequest,$xml); 707 $cached = ''; // for logging 708 } else { 709 $xml = $cached; 710 $cached = '(cached)'; // for logging 711 } 712 ## the resulting XML is an 713 $response = simplexml_load_string($xml); 714 715 # var_dump($response);exit; 716 $spamMatched = array(); 717 if ($response->success) { 718 $muninEntry = ''; 719 foreach ($spamTriggers as $trigger => $banDetails) { 720 ## iterate over the results found, eg email, ip and username 721 foreach ($response->$trigger as $resultEntry) { 722 if ($resultEntry->appears) { 723 # var_dump($resultEntry); 724 if ( 725 ( 726 ## there's a ban end check if it's still in range 727 (!empty($banDetails['ban_end']) && $resultEntry->lastseen+$banDetails['ban_end'] > time()) 728 ## or the ban is permanent 729 || empty($banDetails['ban_end'])) && 730 ## check if the frequency is in range 731 ((int)$resultEntry->frequency > $banDetails['freq_tolerance']) 732 ) { 733 $isSfsSpam++; 734 $banDetails['matchedon'] = $trigger; 735 $this->matchedOn .= $trigger .';'; 736 $muninEntry .= ' SFSMATCH '.$trigger; 737 $banDetails['matchedvalue'] = (string)$resultEntry->value; 738 $banDetails['frequency'] = (string)$resultEntry->frequency; 739 $spamMatched[] = $banDetails; 740 } 741 } 742 } 743 } 744 } 745 # var_dump($spamMatched); 746 $this->matchDetails = $spamMatched; 747 if ($isSfsSpam) { 748 $this->dbg('SFS check SPAM'); 749 $this->addLogEntry('munin-graph.log',$muninEntry); 750 $this->addLogEntry('sfs.log',$cached.' SPAM '.$data['username'].' '.$data['email'].' '.join(',',$data['ips'])); 751 } else { 752 $this->dbg('SFS check HAM'); 753 $this->addLogEntry('sfs.log',$cached.' HAM '.$data['username'].' '.$data['email'].' '.join(',',$data['ips'])); 754 } 755 $this->isSpam = $this->isSpam || $isSfsSpam > 0; 756 return $isSfsSpam; 757 } 758 759 760 /** 761 * isSpam - match submission against spam protection sources 762 * @param array $data - array containing information 763 * structure: 764 * 765 * $data['email'] = (string) email address 766 * 767 * $data['username'] = (string) username 768 * 769 * $data['ips'] = array ('ip1','ip2') 770 * 771 * $data['user_agent'] = (string) Browser Agent 772 * 773 * $data['referrer'] = (string) referring URL 774 * 775 * $data['content'] = (string) Other content 776 * 777 * @param bool $checkAll - continue checking other services 778 * 779 * true - check against all services 780 * 781 * false - only check next service if previous one returned ham 782 * 783 * @return integer - number of services that returned "spam" status. If checkAll is false will be 0 or 1 784 */ 785 786 function isSpam($data,$checkAll = false) { 787 $this->dbg('isSpam call'); 788 ## for external functionality testing, allow "test=ham" or "test=spam" 789 if (isset($data['test'])) { 790 if ($data['test'] == 'ham') { 791 $this->matchedBy = 'HAM test'; 792 return false; 793 } elseif ($data['test'] == 'spam') { 794 $this->matchedBy = 'SPAM test'; 795 return true; 796 } 797 } 798 $isSpam = 0; 799 $servicesMatched = array(); 800 801 ## honeypot will be fastest 802 if ($this->doHpCheck && !empty($data['ips'])) { 803 $this->dbg('hpCheck'); 804 $isHP = false; 805 foreach ($data['ips'] as $ip) { 806 $this->dbg('hpCheck IP '.$ip); 807 if ($this->honeypotCheck($ip)) { 808 $this->dbg('hpCheck SPAM'); 809 $isHP = true; 810 $this->matchedBy = 'Honeypot Project'; 811 $servicesMatched[] = 'HP'; 812 $isSpam++; 813 } 814 } 815 if ($isHP) { ## make sure to only log once, if multiple IPs are checked 816 $this->addLogEntry('munin-graph.log','HPSPAM'); 817 } else { 818 $this->addLogEntry('munin-graph.log','HPHAM'); 819 } 820 } 821 if ((!$isSpam || $checkAll)) { 822 $num = $this->stopForumSpamCheck($data); 823 if ($num) { 824 $this->matchedBy = 'Stop Forum Spam'; 825 $this->dbg('SFS SPAM'); 826 $this->addLogEntry('munin-graph.log','SFSSPAM'); 827 $isSpam += $num; 828 $servicesMatched[] = 'SFS'; 829 } else { 830 $this->addLogEntry('munin-graph.log','SFSHAM'); 831 } 832 } 833 if ((!$isSpam || $checkAll) && $this->akismetEnabled) { 834 if ($this->akismetCheck($data)) { 835 $this->dbg('Akismet SPAM'); 836 $this->matchedBy = 'Akismet'; 837 $servicesMatched[] = 'AKI'; 838 $isSpam++; 839 $this->addLogEntry('munin-graph.log','AKISPAM'); 840 } else { 841 $this->addLogEntry('munin-graph.log','AKIHAM'); 842 } 843 } 844 845 if ((!$isSpam || $checkAll) && $this->mollomEnabled) { 846 if ($this->mollomCheck($data)) { 847 $this->dbg('Mollom SPAM'); 848 $this->matchedBy = 'Mollom'; 849 $servicesMatched[] = 'MOL'; 850 $isSpam++; 851 $this->addLogEntry('munin-graph.log','MOLSPAM'); 852 } else { 853 $this->addLogEntry('munin-graph.log','MOLHAM'); 854 } 855 } 856 857 ## to test the comparison code below 858/* 859 $isSpam = 1; 860 $servicesMatched = array_keys($this->services); 861*/ 862 863 if ($isSpam) { 864 ## Add a log to graph a comparison: a hit on SVC1 -> hit or miss in SVC2? 865 foreach (array_keys($this->services) as $svcMain) { 866 if (in_array($svcMain,$servicesMatched)) { ## hit on svcMain 867 foreach (array_keys($this->services) as $svcCompare) { 868 if ($svcCompare != $svcMain) { ## no need to compare with ourselves 869 if (in_array($svcCompare,$servicesMatched)) { ## also a hit on svcCompare 870 $this->addLogEntry('munin-graph-compare.log',$svcMain.' - '.$svcCompare.' HIT '); 871 } else { 872 $this->addLogEntry('munin-graph-compare.log',$svcMain.' - '.$svcCompare.' MISS '); 873 } 874 } 875 } 876 } 877 } 878 } 879 880 $this->dbg('overall SpamScore '.sprintf('%d',$isSpam)); 881 $this->isSpam = (bool) $isSpam > 0; 882 if ($this->isSpam) { 883 $this->addLogEntry('munin-graph.log','TOTAL LEVEL '.$isSpam); 884 } 885 $this->addLogEntry('munin-graph-timing.log',$this->elapsed()); 886 return $isSpam; 887 } 888 889} // eo class 890 891