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