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  */
54 class 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