OSDN Git Service

Temporary removed about marketing to wiki users by ads for pressblog.jp
[pukiwiki/pukiwiki_sandbox.git] / spam / spam.php
1 <?php
2 // $Id: spam.php,v 1.90 2007/01/03 07:05:59 henoheno Exp $
3 // Copyright (C) 2006-2007 PukiWiki Developers Team
4 // License: GPL v2 or (at your option) any later version
5
6 // Functions for Concept-work of spam-uri metrics
7 // (PHP 4 >= 4.3.0): preg_match_all(PREG_OFFSET_CAPTURE): $method['uri_XXX'] related feature
8
9 if (! defined('SPAM_INI_FILE')) define('SPAM_INI_FILE', 'spam.ini.php');
10
11 // ---------------------
12 // Compat
13
14 // (PHP 4 >= 4.2.0): var_export(): mail-reporting and dump related
15 if (! function_exists('var_export')) {
16         function var_export() {
17                 return 'var_export() is not found' . "\n";
18         }
19 }
20
21 // ---------------------
22 // URI pickup
23
24 // Return an array of URIs in the $string
25 // [OK] http://nasty.example.org#nasty_string
26 // [OK] http://nasty.example.org:80/foo/xxx#nasty_string/bar
27 // [OK] ftp://nasty.example.org:80/dfsdfs
28 // [OK] ftp://cnn.example.com&story=breaking_news@10.0.0.1/top_story.htm (from RFC3986)
29 function uri_pickup($string = '', $normalize = TRUE,
30         $preserve_rawuri = FALSE, $preserve_chunk = TRUE)
31 {
32         // Not available for: IDN(ignored)
33         $array = array();
34         preg_match_all(
35                 // scheme://userinfo@host:port/path/or/pathinfo/maybefile.and?query=string#fragment
36                 // Refer RFC3986 (Regex below is not strict)
37                 '#(\b[a-z][a-z0-9.+-]{1,8})://' .       // 1: Scheme
38                 '(?:' .
39                         '([^\s<>"\'\[\]/\#?@]*)' .              // 2: Userinfo (Username)
40                 '@)?' .
41                 '(' .
42                         // 3: Host
43                         '\[[0-9a-f:.]+\]' . '|' .                               // IPv6([colon-hex and dot]): RFC2732
44                         '(?:[0-9]{1-3}\.){3}[0-9]{1-3}' . '|' . // IPv4(dot-decimal): 001.22.3.44
45                         '[^\s<>"\'\[\]:/\#?&\\\]+' .                    // FQDN: foo.example.org
46                 ')' .
47                 '(?::([0-9]*))?' .                                      // 4: Port
48                 '((?:/+[^\s<>"\'\[\]/\#]+)*/+)?' .      // 5: Directory path or path-info
49                 '([^\s<>"\'\[\]\#?]+)?' .                       // 6: File?
50                 '(?:\?([^\s<>"\'\[\]\#]+))?' .          // 7: Query string
51                 '(?:\#([a-z0-9._~%!$&\'()*+,;=:@-]*))?' .       // 8: Fragment
52                 '#i',
53                  $string, $array, PREG_SET_ORDER | PREG_OFFSET_CAPTURE
54         );
55
56         // Shrink $array
57         static $parts = array(
58                 1 => 'scheme', 2 => 'userinfo', 3 => 'host', 4 => 'port',
59                 5 => 'path', 6 => 'file', 7 => 'query', 8 => 'fragment'
60         );
61         $default = array('');
62         foreach(array_keys($array) as $uri) {
63                 $_uri = & $array[$uri];
64                 array_rename_keys($_uri, $parts, TRUE, $default);
65
66                 $offset = $_uri['scheme'][1]; // Scheme's offset
67                 foreach(array_keys($_uri) as $part) {
68                         // Remove offsets for each part
69                         $_uri[$part] = & $_uri[$part][0];
70                 }
71
72                 if ($normalize) {
73                         $_uri['scheme'] = scheme_normalize($_uri['scheme']);
74                         if ($_uri['scheme'] === '') {
75                                 unset($array[$uri]);
76                                 continue;
77                         }
78                         $_uri['host']  = strtolower($_uri['host']);
79                         $_uri['port']  = port_normalize($_uri['port'], $_uri['scheme'], FALSE);
80                         $_uri['path']  = path_normalize($_uri['path']);
81                         if ($preserve_rawuri) $_uri['rawuri'] = & $_uri[0];
82
83                         // DEBUG
84                         //$_uri['uri'] = uri_array_implode($_uri);
85                 } else {
86                         $_uri['uri'] = & $_uri[0]; // Raw
87                 }
88                 unset($_uri[0]); // Matched string itself
89                 if (! $preserve_chunk) {
90                         unset(
91                                 $_uri['scheme'],
92                                 $_uri['userinfo'],
93                                 $_uri['host'],
94                                 $_uri['port'],
95                                 $_uri['path'],
96                                 $_uri['file'],
97                                 $_uri['query'],
98                                 $_uri['fragment']
99                         );
100                 }
101
102                 // Area offset for area_measure()
103                 $_uri['area']['offset'] = $offset;
104         }
105
106         return $array;
107 }
108
109 // Destructive normalize of URI array
110 // NOTE: Give me the uri_pickup() result with chunks
111 function uri_array_normalize(& $pickups, $preserve = TRUE)
112 {
113         if (! is_array($pickups)) return $pickups;
114
115         foreach (array_keys($pickups) as $key) {
116                 $_key = & $pickups[$key];
117                 $_key['path']     = isset($_key['path']) ? strtolower($_key['path']) : '';
118                 $_key['file']     = isset($_key['file']) ? file_normalize($_key['file']) : '';
119                 $_key['query']    = isset($_key['query']) ? query_normalize(strtolower($_key['query']), TRUE) : '';
120                 $_key['fragment'] = (isset($_key['fragment']) && $preserve) ?
121                         strtolower($_key['fragment']) : ''; // Just ignore
122         }
123
124         return $pickups;
125 }
126
127 // An URI array => An URI (See uri_pickup())
128 function uri_array_implode($uri = array())
129 {
130         if (empty($uri) || ! is_array($uri)) return NULL;
131
132         $tmp = array();
133         if (isset($uri['scheme']) && $uri['scheme'] !== '') {
134                 $tmp[] = & $uri['scheme'];
135                 $tmp[] = '://';
136         }
137         if (isset($uri['userinfo']) && $uri['userinfo'] !== '') {
138                 $tmp[] = & $uri['userinfo'];
139                 $tmp[] = '@';
140         }
141         if (isset($uri['host']) && $uri['host'] !== '') {
142                 $tmp[] = & $uri['host'];
143         }
144         if (isset($uri['port']) && $uri['port'] !== '') {
145                 $tmp[] = ':';
146                 $tmp[] = & $uri['port'];
147         }
148         if (isset($uri['path']) && $uri['path'] !== '') {
149                 $tmp[] = & $uri['path'];
150         }
151         if (isset($uri['file']) && $uri['file'] !== '') {
152                 $tmp[] = & $uri['file'];
153         }
154         if (isset($uri['query']) && $uri['query'] !== '') {
155                 $tmp[] = '?';
156                 $tmp[] = & $uri['query'];
157         }
158         if (isset($uri['fragment']) && $uri['fragment'] !== '') {
159                 $tmp[] = '#';
160                 $tmp[] = & $uri['fragment'];
161         }
162
163         return implode('', $tmp);
164 }
165
166 // $array['something'] => $array['wanted']
167 function array_rename_keys(& $array, $keys = array('from' => 'to'), $force = FALSE, $default = '')
168 {
169         if (! is_array($array) || ! is_array($keys)) return FALSE;
170
171         // Nondestructive test
172         if (! $force)
173                 foreach(array_keys($keys) as $from)
174                         if (! isset($array[$from]))
175                                 return FALSE;
176
177         foreach($keys as $from => $to) {
178                 if ($from === $to) continue;
179                 if (! $force || isset($array[$from])) {
180                         $array[$to] = & $array[$from];
181                         unset($array[$from]);
182                 } else  {
183                         $array[$to] = $default;
184                 }
185         }
186
187         return TRUE;
188 }
189
190 // ---------------------
191 // Area pickup
192
193 // Pickup all of markup areas
194 function area_pickup($string = '', $method = array())
195 {
196         $area = array();
197         if (empty($method)) return $area;
198
199         // Anchor tag pair by preg_match and preg_match_all()
200         // [OK] <a href></a>
201         // [OK] <a href=  >Good site!</a>
202         // [OK] <a href= "#" >test</a>
203         // [OK] <a href="http://nasty.example.com">visit http://nasty.example.com/</a>
204         // [OK] <a href=\'http://nasty.example.com/\' >discount foobar</a> 
205         // [NG] <a href="http://ng.example.com">visit http://ng.example.com _not_ended_
206         $regex = '#<a\b[^>]*\bhref\b[^>]*>.*?</a\b[^>]*(>)#i';
207         if (isset($method['area_anchor'])) {
208                 $areas = array();
209                 $count = isset($method['asap']) ?
210                         preg_match($regex, $string) :
211                         preg_match_all($regex, $string, $areas);
212                 if (! empty($count)) $area['area_anchor'] = $count;
213         }
214         if (isset($method['uri_anchor'])) {
215                 $areas = array();
216                 preg_match_all($regex, $string, $areas, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
217                 foreach(array_keys($areas) as $_area) {
218                         $areas[$_area] =  array(
219                                 $areas[$_area][0][1], // Area start (<a href>)
220                                 $areas[$_area][1][1], // Area end   (</a>)
221                         );
222                 }
223                 if (! empty($areas)) $area['uri_anchor'] = $areas;
224         }
225
226         // phpBB's "BBCode" pair by preg_match and preg_match_all()
227         // [OK] [url][/url]
228         // [OK] [url]http://nasty.example.com/[/url]
229         // [OK] [link]http://nasty.example.com/[/link]
230         // [OK] [url=http://nasty.example.com]visit http://nasty.example.com/[/url]
231         // [OK] [link http://nasty.example.com/]buy something[/link]
232         $regex = '#\[(url|link)\b[^\]]*\].*?\[/\1\b[^\]]*(\])#i';
233         if (isset($method['area_bbcode'])) {
234                 $areas = array();
235                 $count = isset($method['asap']) ?
236                         preg_match($regex, $string) :
237                         preg_match_all($regex, $string, $areas, PREG_SET_ORDER);
238                 if (! empty($count)) $area['area_bbcode'] = $count;
239         }
240         if (isset($method['uri_bbcode'])) {
241                 $areas = array();
242                 preg_match_all($regex, $string, $areas, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
243                 foreach(array_keys($areas) as $_area) {
244                         $areas[$_area] = array(
245                                 $areas[$_area][0][1], // Area start ([url])
246                                 $areas[$_area][2][1], // Area end   ([/url])
247                         );
248                 }
249                 if (! empty($areas)) $area['uri_bbcode'] = $areas;
250         }
251
252         // Various Wiki syntax
253         // [text_or_uri>text_or_uri]
254         // [text_or_uri:text_or_uri]
255         // [text_or_uri|text_or_uri]
256         // [text_or_uri->text_or_uri]
257         // [text_or_uri text_or_uri] // MediaWiki
258         // MediaWiki: [http://nasty.example.com/ visit http://nasty.example.com/]
259
260         return $area;
261 }
262
263 // If in doubt, it's a little doubtful
264 // if (Area => inside <= Area) $brief += -1
265 function area_measure($areas, & $array, $belief = -1, $a_key = 'area', $o_key = 'offset')
266 {
267         if (! is_array($areas) || ! is_array($array)) return;
268
269         $areas_keys = array_keys($areas);
270         foreach(array_keys($array) as $u_index) {
271                 $offset = isset($array[$u_index][$o_key]) ?
272                         intval($array[$u_index][$o_key]) : 0;
273                 foreach($areas_keys as $a_index) {
274                         if (isset($array[$u_index][$a_key])) {
275                                 $offset_s = intval($areas[$a_index][0]);
276                                 $offset_e = intval($areas[$a_index][1]);
277                                 // [Area => inside <= Area]
278                                 if ($offset_s < $offset && $offset < $offset_e) {
279                                         $array[$u_index][$a_key] += $belief;
280                                 }
281                         }
282                 }
283         }
284 }
285
286 // ---------------------
287 // Spam-uri pickup
288
289 // Domain exposure callback (See spam_uri_pickup_preprocess())
290 // http://victim.example.org/?foo+site:nasty.example.com+bar
291 // => http://nasty.example.com/?refer=victim.example.org
292 // NOTE: 'refer=' is not so good for (at this time).
293 // Consider about using IP address of the victim, try to avoid that.
294 function _preg_replace_callback_domain_exposure($matches = array())
295 {
296         $result = '';
297
298         // Preserve the victim URI as a complicity or ...
299         if (isset($matches[5])) {
300                 $result =
301                         $matches[1] . '://' .   // scheme
302                         $matches[2] . '/' .             // victim.example.org
303                         $matches[3];                    // The rest of all (before victim)
304         }
305
306         // Flipped URI
307         if (isset($matches[4])) {
308                 $result = 
309                         $matches[1] . '://' .   // scheme
310                         $matches[4] .                   // nasty.example.com
311                         '/?refer=' . strtolower($matches[2]) .  // victim.example.org
312                         ' ' . $result;
313         }
314
315         return $result;
316 }
317
318 // Preprocess: rawurldecode() and adding space(s) and something
319 // to detect/count some URIs _if possible_
320 // NOTE: It's maybe danger to var_dump(result). [e.g. 'javascript:']
321 // [OK] http://victim.example.org/go?http%3A%2F%2Fnasty.example.org
322 // [OK] http://victim.example.org/http://nasty.example.org
323 function spam_uri_pickup_preprocess($string = '')
324 {
325         if (! is_string($string)) return '';
326
327         $string = rawurldecode($string);
328
329         // Domain exposure (See _preg_replace_callback_domain_exposure())
330         $string = preg_replace_callback(
331                 array(
332                         // Something Google: http://www.google.com/supported_domains
333                         '#(http)://([a-z0-9.]+\.google\.[a-z]{2,3}(?:\.[a-z]{2})?)/' .
334                         '([a-z0-9?=&.%_+-]+)' .         // ?query=foo+
335                         '\bsite:([a-z0-9.%_-]+)' .      // site:nasty.example.com
336                         //'()' .        // Preserve or remove?
337                         '#i',
338                 ),
339                 '_preg_replace_callback_domain_exposure',
340                 $string
341         );
342
343         // URI exposure (uriuri => uri uri)
344         $string = preg_replace(
345                 array(
346                         '#(?<! )(?:https?|ftp):/#i',
347                 //      '#[a-z][a-z0-9.+-]{1,8}://#i',
348                 //      '#[a-z][a-z0-9.+-]{1,8}://#i'
349                 ),
350                 ' $0',
351                 $string
352         );
353
354         return $string;
355 }
356
357 // Main function of spam-uri pickup
358 function spam_uri_pickup($string = '', $method = array())
359 {
360         if (! is_array($method) || empty($method)) {
361                 $method = check_uri_spam_method();
362         }
363
364         $string = spam_uri_pickup_preprocess($string);
365
366         $array  = uri_pickup($string);
367
368         // Area elevation of URIs, for '(especially external)link' intension
369         if (! empty($array)) {
370                 $_method = array();
371                 if (isset($method['uri_anchor'])) $_method['uri_anchor'] = & $method['uri_anchor'];
372                 if (isset($method['uri_bbcode'])) $_method['uri_bbcode'] = & $method['uri_bbcode'];
373                 $areas = area_pickup($string, $_method, TRUE);
374                 if (! empty($areas)) {
375                         $area_shadow = array();
376                         foreach (array_keys($array) as $key) {
377                                 $area_shadow[$key] = & $array[$key]['area'];
378                                 foreach (array_keys($_method) as $_key) {
379                                         $area_shadow[$key][$_key] = 0;
380                                 }
381                         }
382                         foreach (array_keys($_method) as $_key) {
383                                 if (isset($areas[$_key])) {
384                                         area_measure($areas[$_key], $area_shadow, 1, $_key);
385                                 }
386                         }
387                 }
388         }
389
390         // Remove 'offset's for area_measure()
391         foreach(array_keys($array) as $key)
392                 unset($array[$key]['area']['offset']);
393
394         return $array;
395 }
396
397
398 // ---------------------
399 // Normalization
400
401 // Scheme normalization: Renaming the schemes
402 // snntp://example.org =>  nntps://example.org
403 // NOTE: Keep the static lists simple. See also port_normalize().
404 function scheme_normalize($scheme = '', $considerd_harmfull = TRUE)
405 {
406         // Abbreviations considerable they don't have link intension
407         static $abbrevs = array(
408                 'ttp'   => 'http',
409                 'ttps'  => 'https',
410         );
411
412         // Alias => normalized
413         static $aliases = array(
414                 'pop'   => 'pop3',
415                 'news'  => 'nntp',
416                 'imap4' => 'imap',
417                 'snntp' => 'nntps',
418                 'snews' => 'nntps',
419                 'spop3' => 'pop3s',
420                 'pops'  => 'pop3s',
421         );
422
423         $scheme = strtolower(trim($scheme));
424         if (isset($abbrevs[$scheme])) {
425                 if ($considerd_harmfull) {
426                         $scheme = $abbrevs[$scheme];
427                 } else {
428                         $scheme = '';
429                 }
430         }
431         if (isset($aliases[$scheme])) $scheme = $aliases[$scheme];
432
433         return $scheme;
434 }
435
436 // Port normalization: Suppress the (redundant) default port
437 // HTTP://example.org:80/ => http://example.org/
438 // HTTP://example.org:8080/ => http://example.org:8080/
439 // HTTPS://example.org:443/ => https://example.org/
440 function port_normalize($port, $scheme, $scheme_normalize = TRUE)
441 {
442         // Schemes that users _maybe_ want to add protocol-handlers
443         // to their web browsers. (and attackers _maybe_ want to use ...)
444         // Reference: http://www.iana.org/assignments/port-numbers
445         static $array = array(
446                 // scheme => default port
447                 'ftp'     =>    21,
448                 'ssh'     =>    22,
449                 'telnet'  =>    23,
450                 'smtp'    =>    25,
451                 'tftp'    =>    69,
452                 'gopher'  =>    70,
453                 'finger'  =>    79,
454                 'http'    =>    80,
455                 'pop3'    =>   110,
456                 'sftp'    =>   115,
457                 'nntp'    =>   119,
458                 'imap'    =>   143,
459                 'irc'     =>   194,
460                 'wais'    =>   210,
461                 'https'   =>   443,
462                 'nntps'   =>   563,
463                 'rsync'   =>   873,
464                 'ftps'    =>   990,
465                 'telnets' =>   992,
466                 'imaps'   =>   993,
467                 'ircs'    =>   994,
468                 'pop3s'   =>   995,
469                 'mysql'   =>  3306,
470         );
471
472         $port = trim($port);
473         if ($port === '') return $port;
474
475         if ($scheme_normalize) $scheme = scheme_normalize($scheme);
476         if (isset($array[$scheme]) && $port == $array[$scheme])
477                 $port = ''; // Ignore the defaults
478
479         return $port;
480 }
481
482 // Path normalization
483 // http://example.org => http://example.org/
484 // http://example.org#hoge => http://example.org/#hoge
485 // http://example.org/path/a/b/./c////./d => http://example.org/path/a/b/c/d
486 // http://example.org/path/../../a/../back => http://example.org/back
487 function path_normalize($path = '', $divider = '/', $addroot = TRUE)
488 {
489         if (! is_string($path) || $path == '')
490                 return $addroot ? $divider : '';
491
492         $path = trim($path);
493         $last = ($path[strlen($path) - 1] == $divider) ? $divider : '';
494         $array = explode($divider, $path);
495
496         // Remove paddings
497         foreach(array_keys($array) as $key) {
498                 if ($array[$key] == '' || $array[$key] == '.')
499                          unset($array[$key]);
500         }
501         // Back-track
502         $tmp = array();
503         foreach($array as $value) {
504                 if ($value == '..') {
505                         array_pop($tmp);
506                 } else {
507                         array_push($tmp, $value);
508                 }
509         }
510         $array = & $tmp;
511
512         $path = $addroot ? $divider : '';
513         if (! empty($array)) $path .= implode($divider, $array) . $last;
514
515         return $path;
516 }
517
518 // DirectoryIndex normalize (Destructive and rough)
519 function file_normalize($string = 'index.html.en')
520 {
521         static $array = array(
522                 'index'                 => TRUE,        // Some system can omit the suffix
523                 'index.htm'             => TRUE,
524                 'index.html'    => TRUE,
525                 'index.shtml'   => TRUE,
526                 'index.jsp'             => TRUE,
527                 'index.php'             => TRUE,
528                 'index.php3'    => TRUE,
529                 'index.php4'    => TRUE,
530                 //'index.pl'    => TRUE,
531                 //'index.py'    => TRUE,
532                 //'index.rb'    => TRUE,
533                 'index.cgi'             => TRUE,
534                 'default.htm'   => TRUE,
535                 'default.html'  => TRUE,
536                 'default.asp'   => TRUE,
537                 'default.aspx'  => TRUE,
538         );
539
540         // Content-negothiation filter:
541         // Roughly removing ISO 639 -like
542         // 2-letter suffixes (See RFC3066)
543         $matches = array();
544         if (preg_match('/(.*)\.[a-z][a-z](?:-[a-z][a-z])?$/i', $string, $matches)) {
545                 $_string = $matches[1];
546         } else {
547                 $_string = & $string;
548         }
549
550         if (isset($array[strtolower($_string)])) {
551                 return '';
552         } else {
553                 return $string;
554         }
555 }
556
557 // Sort query-strings if possible (Destructive and rough)
558 // [OK] &&&&f=d&b&d&c&a=0dd  =>  a=0dd&b&c&d&f=d
559 // [OK] nothing==&eg=dummy&eg=padding&eg=foobar  =>  eg=foobar
560 function query_normalize($string = '', $equal = FALSE, $equal_cutempty = TRUE)
561 {
562         $array = explode('&', $string);
563
564         // Remove '&' paddings
565         foreach(array_keys($array) as $key) {
566                 if ($array[$key] == '') {
567                          unset($array[$key]);
568                 }
569         }
570
571         // Consider '='-sepalated input and paddings
572         if ($equal) {
573                 $equals = $not_equals = array();
574                 foreach ($array as $part) {
575                         if (strpos($part, '=') === FALSE) {
576                                  $not_equals[] = $part;
577                         } else {
578                                 list($key, $value) = explode('=', $part, 2);
579                                 $value = ltrim($value, '=');
580                                 if (! $equal_cutempty || $value != '') {
581                                         $equals[$key] = $value;
582                                 }
583                         }
584                 }
585
586                 $array = & $not_equals;
587                 foreach ($equals as $key => $value) {
588                         $array[] = $key . '=' . $value;
589                 }
590                 unset($equals);
591         }
592
593         natsort($array);
594         return implode('&', $array);
595 }
596
597 // ---------------------
598 // Part One : Checker
599
600 function generate_glob_regex($string = '', $divider = '/')
601 {
602         static $from = array(
603                          1 => '*',
604                         11 => '?',
605         //              22 => '[',      // Maybe cause regex compilation error (e.g. '[]')
606         //              23 => ']',      //
607                 );
608         static $mid = array(
609                          1 => '_AST_',
610                         11 => '_QUE_',
611         //              22 => '_RBR_',
612         //              23 => '_LBR_',
613                 );
614         static $to = array(
615                          1 => '.*',
616                         11 => '.',
617         //              22 => '[',
618         //              23 => ']',
619                 );
620
621         if (is_array($string)) {
622                 // Recurse
623                 return '(?:' .
624                         implode('|',    // OR
625                                 array_map('generate_glob_regex',
626                                         $string,
627                                         array_pad(array(), count($string), $divider)
628                                 )
629                         ) .
630                 ')';
631         } else {
632                 $string = str_replace($from, $mid, $string); // Hide
633                 $string = preg_quote($string, $divider);
634                 $string = str_replace($mid, $to, $string);   // Unhide
635                 return $string;
636         }
637 }
638
639 // TODO: Ignore list
640 // TODO: preg_grep() ?
641 // TODO: Multi list
642 function is_badhost($hosts = '', $asap = TRUE)
643 {
644         static $regex;
645
646         if (! isset($regex)) {
647                 $regex = array();
648                 $regex['badhost'] = array();
649
650                 // Sample
651                 if (FALSE) {
652                         $blocklist['badhost'] = array(
653                                 //'*',                  // Deny all uri
654                                 //'10.20.*.*',  // 10.20.example.com also matches
655                                 //'*.blogspot.com',     // Blog services subdomains
656                                 //array('blogspot.com', '*.blogspot.com')
657                         );
658                         foreach ($blocklist['badhost'] as $part) {
659                                 $_part = is_array($part) ? implode('/', $part) : $part;
660                                 $regex['badhost'][$_part] = '/^' . generate_glob_regex($part) . '$/i';
661                         }
662                 }
663
664                 // Load
665                 if (file_exists(SPAM_INI_FILE)) {
666                         $blocklist = array();
667                         require(SPAM_INI_FILE);
668                         foreach ($blocklist['badhost'] as $part) {
669                                 $_part = is_array($part) ? implode('/', $part) : $part;
670                                 $regex['badhost'][$_part] = '/^' . generate_glob_regex($part) . '$/i';
671                         }
672                 }
673         }
674
675         $result = array();
676         if (! is_array($hosts)) $hosts = array($hosts);
677
678         foreach($hosts as $host) {
679                 if (! is_string($host)) $host = '';
680                 foreach ($regex['badhost'] as $part => $_regex) {
681                         if (preg_match($_regex, $host)) {
682                                 if (! isset($result[$part]))  $result[$part] = array();
683                                 $result[$part][] = $host;
684                                 if ($asap) {
685                                         return $result;
686                                 } else {
687                                         break;
688                                 }
689                         }
690                 }
691         }
692
693         return $result;
694 }
695
696 // Default (enabled) methods and thresholds (for content insertion)
697 function check_uri_spam_method($times = 1, $t_area = 0, $rule = TRUE)
698 {
699         $times  = intval($times);
700         $t_area = intval($t_area);
701
702         $positive = array(
703                 // Thresholds
704                 'quantity'     =>  8 * $times,  // Allow N URIs
705                 'non_uniqhost' =>  3 * $times,  // Allow N duped (and normalized) Hosts
706                 'non_uniquri'  =>  0 * $times,  // Allow N duped (and normalized) URIs
707
708                 // Areas
709                 'area_anchor'  => $t_area,      // Using <a href> HTML tag
710                 'area_bbcode'  => $t_area,      // Using [url] or [link] BBCode
711                 //'uri_anchor' => $t_area,      // URI inside <a href> HTML tag
712                 //'uri_bbcode' => $t_area,      // URI inside [url] or [link] BBCode
713         );
714         if ($rule) {
715                 $bool = array(
716                         // Rules
717                         //'asap'   => TRUE,     // Quit or return As Soon As Possible
718                         'uniqhost' => TRUE,     // Show uniq host (at block notification mail)
719                         'badhost'  => TRUE,     // Check badhost
720                 );
721         } else {
722                 $bool = array();
723         }
724
725         // Remove non-$positive values
726         foreach (array_keys($positive) as $key) {
727                 if ($positive[$key] < 0) unset($positive[$key]);
728         }
729
730         return $positive + $bool;
731 }
732
733 // Simple/fast spam check
734 function check_uri_spam($target = '', $method = array())
735 {
736         if (! is_array($method) || empty($method)) {
737                 $method = check_uri_spam_method();
738         }
739         $progress = array(
740                 'sum' => array(
741                         'quantity'    => 0,
742                         'uniqhost'    => 0,
743                         'non_uniqhost'=> 0,
744                         'non_uniquri' => 0,
745                         'badhost'     => 0,
746                         'area_anchor' => 0,
747                         'area_bbcode' => 0,
748                         'uri_anchor'  => 0,
749                         'uri_bbcode'  => 0,
750                 ),
751                 'is_spam' => array(),
752                 'method'  => & $method,
753         );
754         $sum     = & $progress['sum'];
755         $is_spam = & $progress['is_spam'];
756         $asap    = isset($method['asap']);
757
758         // Return if ...
759         if (is_array($target)) {
760                 foreach($target as $str) {
761                         // Recurse
762                         $_progress = check_uri_spam($str, $method);
763                         $_sum      = & $_progress['sum'];
764                         $_is_spam  = & $_progress['is_spam'];
765                         foreach (array_keys($_sum) as $key) {
766                                 $sum[$key] += $_sum[$key];
767                         }
768                         foreach(array_keys($_is_spam) as $key) {
769                                 if (is_array($_is_spam[$key])) {
770                                         // Marge keys (badhost)
771                                         foreach(array_keys($_is_spam[$key]) as $_key) {
772                                                 if (! isset($is_spam[$key][$_key])) {
773                                                         $is_spam[$key][$_key] =  $_is_spam[$key][$_key];
774                                                 } else {
775                                                         $is_spam[$key][$_key] += $_is_spam[$key][$_key];
776                                                 }
777                                         }
778                                 } else {
779                                         $is_spam[$key] = TRUE;
780                                 }
781                         }
782                         if ($asap && $is_spam) break;
783                 }
784                 return $progress;
785         }
786
787         // Area: There's HTML anchor tag
788         if ((! $asap || ! $is_spam) && isset($method['area_anchor'])) {
789                 $key = 'area_anchor';
790                 $_asap = isset($method['asap']) ? array('asap' => TRUE) : array();
791                 $result = area_pickup($target, array($key => TRUE) + $_asap);
792                 if ($result) {
793                         $sum[$key] = $result[$key];
794                         if (isset($method[$key]) && $sum[$key] > $method[$key]) {
795                                 $is_spam[$key] = TRUE;
796                         }
797                 }
798         }
799
800         // Area: There's 'BBCode' linking tag
801         if ((! $asap || ! $is_spam) && isset($method['area_bbcode'])) {
802                 $key = 'area_bbcode';
803                 $_asap = isset($method['asap']) ? array('asap' => TRUE) : array();
804                 $result = area_pickup($target, array($key => TRUE) + $_asap);
805                 if ($result) {
806                         $sum[$key] = $result[$key];
807                         if (isset($method[$key]) && $sum[$key] > $method[$key]) {
808                                 $is_spam[$key] = TRUE;
809                         }
810                 }
811         }
812
813         // Return if ...
814         if ($asap && $is_spam) {
815                 return $progress;
816         }
817         // URI Init
818         $pickups = spam_uri_pickup($target, $method);
819         if (empty($pickups)) {
820                 return $progress;
821         }
822
823         // URI: Check quantity
824         $sum['quantity'] += count($pickups);
825                 // URI quantity
826         if ((! $asap || ! $is_spam) && isset($method['quantity']) &&
827                 $sum['quantity'] > $method['quantity']) {
828                 $is_spam['quantity'] = TRUE;
829         }
830
831         // URI: used inside HTML anchor tag pair
832         if ((! $asap || ! $is_spam) && isset($method['uri_anchor'])) {
833                 $key = 'uri_anchor';
834                 foreach($pickups as $pickup) {
835                         if (isset($pickup['area'][$key])) {
836                                 $sum[$key] += $pickup['area'][$key];
837                                 if(isset($method[$key]) &&
838                                         $sum[$key] > $method[$key]) {
839                                         $is_spam[$key] = TRUE;
840                                         if ($asap && $is_spam) break;
841                                 }
842                                 if ($asap && $is_spam) break;
843                         }
844                 }
845         }
846
847         // URI: used inside 'BBCode' pair
848         if ((! $asap || ! $is_spam) && isset($method['uri_bbcode'])) {
849                 $key = 'uri_bbcode';
850                 foreach($pickups as $pickup) {
851                         if (isset($pickup['area'][$key])) {
852                                 $sum[$key] += $pickup['area'][$key];
853                                 if(isset($method[$key]) &&
854                                         $sum[$key] > $method[$key]) {
855                                         $is_spam[$key] = TRUE;
856                                         if ($asap && $is_spam) break;
857                                 }
858                                 if ($asap && $is_spam) break;
859                         }
860                 }
861         }
862
863         // URI: Uniqueness (and removing non-uniques)
864         if ((! $asap || ! $is_spam) && isset($method['non_uniquri'])) {
865
866                 // Destructive normalize of URIs
867                 uri_array_normalize($pickups);
868
869                 $uris = array();
870                 foreach (array_keys($pickups) as $key) {
871                         $uris[$key] = uri_array_implode($pickups[$key]);
872                 }
873                 $count = count($uris);
874                 $uris  = array_unique($uris);
875                 $sum['non_uniquri'] += $count - count($uris);
876                 if ($sum['non_uniquri'] > $method['non_uniquri']) {
877                         $is_spam['non_uniquri'] = TRUE;
878                 }
879                 if (! $asap || ! $is_spam) {
880                         foreach (array_diff(array_keys($pickups),
881                                 array_keys($uris)) as $remove) {
882                                 unset($pickups[$remove]);
883                         }
884                 }
885                 unset($uris);
886         }
887
888         // Return if ...
889         if ($asap && $is_spam) {
890                 return $progress;
891         }
892
893         // Host: Uniqueness (uniq / non-uniq)
894         $hosts = array();
895         foreach ($pickups as $pickup) $hosts[] = & $pickup['host'];
896         $hosts = array_unique($hosts);
897         $sum['uniqhost'] += count($hosts);
898         if ((! $asap || ! $is_spam) && isset($method['non_uniqhost'])) {
899                 $sum['non_uniqhost'] = $sum['quantity'] - $sum['uniqhost'];
900                 if ($sum['non_uniqhost'] > $method['non_uniqhost']) {
901                         $is_spam['non_uniqhost'] = TRUE;
902                 }
903         }
904
905         // Return if ...
906         if ($asap && $is_spam) {
907                 return $progress;
908         }
909
910         // URI: Bad host
911         if ((! $asap || ! $is_spam) && isset($method['badhost'])) {
912                 $badhost = is_badhost($hosts, $asap);
913                 if (! empty($badhost)) {
914                         $sum['badhost'] += array_count_leaves($badhost);
915                         foreach(array_keys($badhost) as $keys) {
916                                 $is_spam['badhost'][$keys] =
917                                         array_count_leaves($badhost[$keys]);
918                         }
919                         unset($badhost);
920                 }
921         }
922
923         return $progress;
924 }
925
926 // Count leaves
927 function array_count_leaves($array = array(), $count_empty_array = FALSE)
928 {
929         if (! is_array($array) || (empty($array) && $count_empty_array))
930                 return 1;
931
932         // Recurse
933         $result = 0;
934         foreach ($array as $part) {
935                 $result += array_count_leaves($part, $count_empty_array);
936         }
937         return $result;
938 }
939
940 // ---------------------
941 // Reporting
942
943 // TODO: Don't show unused $method!
944 // Summarize $progress (blocked only)
945 function summarize_spam_progress($progress = array(), $blockedonly = FALSE)
946 {
947         if ($blockedonly) {
948                 $tmp = array_keys($progress['is_spam']);
949         } else {
950                 $tmp = array();
951                 $method = & $progress['method'];
952                 if (isset($progress['sum'])) {
953                         foreach ($progress['sum'] as $key => $value) {
954                                 if (isset($method[$key])) {
955                                         $tmp[] = $key . '(' . $value . ')';
956                                 }
957                         }
958                 }
959         }
960
961         return implode(', ', $tmp);
962 }
963
964 // ---------------------
965 // Exit
966
967 // Common bahavior for blocking
968 // NOTE: Call this function from various blocking feature, to disgueise the reason 'why blocked'
969 function spam_exit($mode = '', $data = array())
970 {
971         switch ($mode) {
972                 case '':        echo("\n");     break;
973                 case 'dump':
974                         echo('<pre>' . "\n");
975                         echo htmlspecialchars(var_export($data, TRUE));
976                         echo('</pre>' . "\n");
977                         break;
978         };
979
980         // Force exit
981         exit;
982 }
983
984
985 // ---------------------
986 // Simple filtering
987
988 // TODO: Record them
989 // Simple/fast spam filter ($target: 'a string' or an array())
990 function pkwk_spamfilter($action, $page, $target = array('title' => ''), $method = array(), $exitmode = '')
991 {
992         $progress = check_uri_spam($target, $method);
993
994         if (! empty($progress['is_spam'])) {
995                 // Mail to administrator(s)
996                 pkwk_spamnotify($action, $page, $target, $progress, $method);
997
998                 // Exit
999                 spam_exit($exitmode, $progress);
1000         }
1001 }
1002
1003 // ---------------------
1004 // PukiWiki original
1005
1006 // Mail to administrator(s)
1007 function pkwk_spamnotify($action, $page, $target = array('title' => ''), $progress = array(), $method = array())
1008 {
1009         global $notify, $notify_subject;
1010
1011         if (! $notify) return;
1012
1013         $asap = isset($method['asap']);
1014
1015         $summary['ACTION']  = 'Blocked by: ' . summarize_spam_progress($progress, TRUE);
1016         if (! $asap) {
1017                 $summary['METRICS'] = summarize_spam_progress($progress);
1018         }
1019         if (isset($progress['is_spam']['badhost'])) {
1020                 $badhost = array();
1021                 foreach($progress['is_spam']['badhost'] as $glob=>$number) {
1022                         $badhost[] = $glob . '(' . $number . ')';
1023                 }
1024                 $summary['DETAIL_BADHOST'] = implode(', ', $badhost);
1025         }
1026         $summary['COMMENT'] = $action;
1027         $summary['PAGE']    = '[blocked] ' . (is_pagename($page) ? $page : '');
1028         $summary['URI']     = get_script_uri() . '?' . rawurlencode($page);
1029         $summary['USER_AGENT']  = TRUE;
1030         $summary['REMOTE_ADDR'] = TRUE;
1031         pkwk_mail_notify($notify_subject,  var_export($target, TRUE), $summary);
1032 }
1033
1034 ?>