OSDN Git Service

Very roughly added 'goodhost' to $method['badhost']
[pukiwiki/pukiwiki_sandbox.git] / spam.php
1 <?php
2 // $Id: spam.php,v 1.91 2007/01/03 07:35:54 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: preg_grep() ?
640 // TODO: Multi list
641 function is_badhost($hosts = '', $asap = TRUE)
642 {
643         static $regex;
644
645         if (! isset($regex)) {
646                 $regex = array();
647
648                 // Sample
649                 if (FALSE) {
650                         $blocklist['badhost'] = array(
651                                 //'*',                  // Deny all uri
652                                 //'10.20.*.*',  // 10.20.example.com also matches
653                                 //'*.blogspot.com',     // Blog services subdomains
654                                 //array('blogspot.com', '*.blogspot.com')
655                         );
656                         foreach ($blocklist['badhost'] as $part) {
657                                 $_part = is_array($part) ? implode('/', $part) : $part;
658                                 $regex['badhost'][$_part] = '/^' . generate_glob_regex($part) . '$/i';
659                         }
660                 }
661
662                 // Load
663                 if (file_exists(SPAM_INI_FILE)) {
664                         $blocklist = array();
665                         require(SPAM_INI_FILE);
666                         foreach(array('goodhost', 'badhost') as $key) {
667                                 if (! isset($blocklist[$key])) continue;
668                                 foreach ($blocklist[$key] as $part) {
669                                         $_part = is_array($part) ? implode('/', $part) : $part;
670                                         $regex[$key][$_part] = '/^' . generate_glob_regex($part) . '$/i';
671                                 }
672                         }
673                 }
674         }
675
676         $result = array();
677         if (! is_array($hosts)) $hosts = array($hosts);
678
679         foreach($hosts as $host) {
680                 if (! is_string($host)) continue;
681
682                 $is_good = FALSE;
683                 foreach ($regex['goodhost'] as $_regex) {
684                         if (preg_match($_regex, $host)) {
685                                 $is_good = TRUE;
686                                 break;
687                         }
688                 }
689                 if ($is_good) continue;
690
691                 foreach ($regex['badhost'] as $part => $_regex) {
692                         if (preg_match($_regex, $host)) {
693                                 if (! isset($result[$part]))  $result[$part] = array();
694                                 $result[$part][] = $host;
695                                 if ($asap) {
696                                         return $result;
697                                 } else {
698                                         break;
699                                 }
700                         }
701                 }
702         }
703         return $result;
704 }
705
706 // Default (enabled) methods and thresholds (for content insertion)
707 function check_uri_spam_method($times = 1, $t_area = 0, $rule = TRUE)
708 {
709         $times  = intval($times);
710         $t_area = intval($t_area);
711
712         $positive = array(
713                 // Thresholds
714                 'quantity'     =>  8 * $times,  // Allow N URIs
715                 'non_uniqhost' =>  3 * $times,  // Allow N duped (and normalized) Hosts
716                 'non_uniquri'  =>  0 * $times,  // Allow N duped (and normalized) URIs
717
718                 // Areas
719                 'area_anchor'  => $t_area,      // Using <a href> HTML tag
720                 'area_bbcode'  => $t_area,      // Using [url] or [link] BBCode
721                 //'uri_anchor' => $t_area,      // URI inside <a href> HTML tag
722                 //'uri_bbcode' => $t_area,      // URI inside [url] or [link] BBCode
723         );
724         if ($rule) {
725                 $bool = array(
726                         // Rules
727                         //'asap'   => TRUE,     // Quit or return As Soon As Possible
728                         'uniqhost' => TRUE,     // Show uniq host (at block notification mail)
729                         'badhost'  => TRUE,     // Check badhost
730                 );
731         } else {
732                 $bool = array();
733         }
734
735         // Remove non-$positive values
736         foreach (array_keys($positive) as $key) {
737                 if ($positive[$key] < 0) unset($positive[$key]);
738         }
739
740         return $positive + $bool;
741 }
742
743 // Simple/fast spam check
744 function check_uri_spam($target = '', $method = array())
745 {
746         if (! is_array($method) || empty($method)) {
747                 $method = check_uri_spam_method();
748         }
749         $progress = array(
750                 'sum' => array(
751                         'quantity'    => 0,
752                         'uniqhost'    => 0,
753                         'non_uniqhost'=> 0,
754                         'non_uniquri' => 0,
755                         'badhost'     => 0,
756                         'area_anchor' => 0,
757                         'area_bbcode' => 0,
758                         'uri_anchor'  => 0,
759                         'uri_bbcode'  => 0,
760                 ),
761                 'is_spam' => array(),
762                 'method'  => & $method,
763         );
764         $sum     = & $progress['sum'];
765         $is_spam = & $progress['is_spam'];
766         $asap    = isset($method['asap']);
767
768         // Return if ...
769         if (is_array($target)) {
770                 foreach($target as $str) {
771                         // Recurse
772                         $_progress = check_uri_spam($str, $method);
773                         $_sum      = & $_progress['sum'];
774                         $_is_spam  = & $_progress['is_spam'];
775                         foreach (array_keys($_sum) as $key) {
776                                 $sum[$key] += $_sum[$key];
777                         }
778                         foreach(array_keys($_is_spam) as $key) {
779                                 if (is_array($_is_spam[$key])) {
780                                         // Marge keys (badhost)
781                                         foreach(array_keys($_is_spam[$key]) as $_key) {
782                                                 if (! isset($is_spam[$key][$_key])) {
783                                                         $is_spam[$key][$_key] =  $_is_spam[$key][$_key];
784                                                 } else {
785                                                         $is_spam[$key][$_key] += $_is_spam[$key][$_key];
786                                                 }
787                                         }
788                                 } else {
789                                         $is_spam[$key] = TRUE;
790                                 }
791                         }
792                         if ($asap && $is_spam) break;
793                 }
794                 return $progress;
795         }
796
797         // Area: There's HTML anchor tag
798         if ((! $asap || ! $is_spam) && isset($method['area_anchor'])) {
799                 $key = 'area_anchor';
800                 $_asap = isset($method['asap']) ? array('asap' => TRUE) : array();
801                 $result = area_pickup($target, array($key => TRUE) + $_asap);
802                 if ($result) {
803                         $sum[$key] = $result[$key];
804                         if (isset($method[$key]) && $sum[$key] > $method[$key]) {
805                                 $is_spam[$key] = TRUE;
806                         }
807                 }
808         }
809
810         // Area: There's 'BBCode' linking tag
811         if ((! $asap || ! $is_spam) && isset($method['area_bbcode'])) {
812                 $key = 'area_bbcode';
813                 $_asap = isset($method['asap']) ? array('asap' => TRUE) : array();
814                 $result = area_pickup($target, array($key => TRUE) + $_asap);
815                 if ($result) {
816                         $sum[$key] = $result[$key];
817                         if (isset($method[$key]) && $sum[$key] > $method[$key]) {
818                                 $is_spam[$key] = TRUE;
819                         }
820                 }
821         }
822
823         // Return if ...
824         if ($asap && $is_spam) {
825                 return $progress;
826         }
827         // URI Init
828         $pickups = spam_uri_pickup($target, $method);
829         if (empty($pickups)) {
830                 return $progress;
831         }
832
833         // URI: Check quantity
834         $sum['quantity'] += count($pickups);
835                 // URI quantity
836         if ((! $asap || ! $is_spam) && isset($method['quantity']) &&
837                 $sum['quantity'] > $method['quantity']) {
838                 $is_spam['quantity'] = TRUE;
839         }
840
841         // URI: used inside HTML anchor tag pair
842         if ((! $asap || ! $is_spam) && isset($method['uri_anchor'])) {
843                 $key = 'uri_anchor';
844                 foreach($pickups as $pickup) {
845                         if (isset($pickup['area'][$key])) {
846                                 $sum[$key] += $pickup['area'][$key];
847                                 if(isset($method[$key]) &&
848                                         $sum[$key] > $method[$key]) {
849                                         $is_spam[$key] = TRUE;
850                                         if ($asap && $is_spam) break;
851                                 }
852                                 if ($asap && $is_spam) break;
853                         }
854                 }
855         }
856
857         // URI: used inside 'BBCode' pair
858         if ((! $asap || ! $is_spam) && isset($method['uri_bbcode'])) {
859                 $key = 'uri_bbcode';
860                 foreach($pickups as $pickup) {
861                         if (isset($pickup['area'][$key])) {
862                                 $sum[$key] += $pickup['area'][$key];
863                                 if(isset($method[$key]) &&
864                                         $sum[$key] > $method[$key]) {
865                                         $is_spam[$key] = TRUE;
866                                         if ($asap && $is_spam) break;
867                                 }
868                                 if ($asap && $is_spam) break;
869                         }
870                 }
871         }
872
873         // URI: Uniqueness (and removing non-uniques)
874         if ((! $asap || ! $is_spam) && isset($method['non_uniquri'])) {
875
876                 // Destructive normalize of URIs
877                 uri_array_normalize($pickups);
878
879                 $uris = array();
880                 foreach (array_keys($pickups) as $key) {
881                         $uris[$key] = uri_array_implode($pickups[$key]);
882                 }
883                 $count = count($uris);
884                 $uris  = array_unique($uris);
885                 $sum['non_uniquri'] += $count - count($uris);
886                 if ($sum['non_uniquri'] > $method['non_uniquri']) {
887                         $is_spam['non_uniquri'] = TRUE;
888                 }
889                 if (! $asap || ! $is_spam) {
890                         foreach (array_diff(array_keys($pickups),
891                                 array_keys($uris)) as $remove) {
892                                 unset($pickups[$remove]);
893                         }
894                 }
895                 unset($uris);
896         }
897
898         // Return if ...
899         if ($asap && $is_spam) {
900                 return $progress;
901         }
902
903         // Host: Uniqueness (uniq / non-uniq)
904         $hosts = array();
905         foreach ($pickups as $pickup) $hosts[] = & $pickup['host'];
906         $hosts = array_unique($hosts);
907         $sum['uniqhost'] += count($hosts);
908         if ((! $asap || ! $is_spam) && isset($method['non_uniqhost'])) {
909                 $sum['non_uniqhost'] = $sum['quantity'] - $sum['uniqhost'];
910                 if ($sum['non_uniqhost'] > $method['non_uniqhost']) {
911                         $is_spam['non_uniqhost'] = TRUE;
912                 }
913         }
914
915         // Return if ...
916         if ($asap && $is_spam) {
917                 return $progress;
918         }
919
920         // URI: Bad host
921         if ((! $asap || ! $is_spam) && isset($method['badhost'])) {
922                 $badhost = is_badhost($hosts, $asap);
923                 if (! empty($badhost)) {
924                         $sum['badhost'] += array_count_leaves($badhost);
925                         foreach(array_keys($badhost) as $keys) {
926                                 $is_spam['badhost'][$keys] =
927                                         array_count_leaves($badhost[$keys]);
928                         }
929                         unset($badhost);
930                 }
931         }
932
933         return $progress;
934 }
935
936 // Count leaves
937 function array_count_leaves($array = array(), $count_empty_array = FALSE)
938 {
939         if (! is_array($array) || (empty($array) && $count_empty_array))
940                 return 1;
941
942         // Recurse
943         $result = 0;
944         foreach ($array as $part) {
945                 $result += array_count_leaves($part, $count_empty_array);
946         }
947         return $result;
948 }
949
950 // ---------------------
951 // Reporting
952
953 // TODO: Don't show unused $method!
954 // Summarize $progress (blocked only)
955 function summarize_spam_progress($progress = array(), $blockedonly = FALSE)
956 {
957         if ($blockedonly) {
958                 $tmp = array_keys($progress['is_spam']);
959         } else {
960                 $tmp = array();
961                 $method = & $progress['method'];
962                 if (isset($progress['sum'])) {
963                         foreach ($progress['sum'] as $key => $value) {
964                                 if (isset($method[$key])) {
965                                         $tmp[] = $key . '(' . $value . ')';
966                                 }
967                         }
968                 }
969         }
970
971         return implode(', ', $tmp);
972 }
973
974 // ---------------------
975 // Exit
976
977 // Common bahavior for blocking
978 // NOTE: Call this function from various blocking feature, to disgueise the reason 'why blocked'
979 function spam_exit($mode = '', $data = array())
980 {
981         switch ($mode) {
982                 case '':        echo("\n");     break;
983                 case 'dump':
984                         echo('<pre>' . "\n");
985                         echo htmlspecialchars(var_export($data, TRUE));
986                         echo('</pre>' . "\n");
987                         break;
988         };
989
990         // Force exit
991         exit;
992 }
993
994
995 // ---------------------
996 // Simple filtering
997
998 // TODO: Record them
999 // Simple/fast spam filter ($target: 'a string' or an array())
1000 function pkwk_spamfilter($action, $page, $target = array('title' => ''), $method = array(), $exitmode = '')
1001 {
1002         $progress = check_uri_spam($target, $method);
1003
1004         if (! empty($progress['is_spam'])) {
1005                 // Mail to administrator(s)
1006                 pkwk_spamnotify($action, $page, $target, $progress, $method);
1007
1008                 // Exit
1009                 spam_exit($exitmode, $progress);
1010         }
1011 }
1012
1013 // ---------------------
1014 // PukiWiki original
1015
1016 // Mail to administrator(s)
1017 function pkwk_spamnotify($action, $page, $target = array('title' => ''), $progress = array(), $method = array())
1018 {
1019         global $notify, $notify_subject;
1020
1021         if (! $notify) return;
1022
1023         $asap = isset($method['asap']);
1024
1025         $summary['ACTION']  = 'Blocked by: ' . summarize_spam_progress($progress, TRUE);
1026         if (! $asap) {
1027                 $summary['METRICS'] = summarize_spam_progress($progress);
1028         }
1029         if (isset($progress['is_spam']['badhost'])) {
1030                 $badhost = array();
1031                 foreach($progress['is_spam']['badhost'] as $glob=>$number) {
1032                         $badhost[] = $glob . '(' . $number . ')';
1033                 }
1034                 $summary['DETAIL_BADHOST'] = implode(', ', $badhost);
1035         }
1036         $summary['COMMENT'] = $action;
1037         $summary['PAGE']    = '[blocked] ' . (is_pagename($page) ? $page : '');
1038         $summary['URI']     = get_script_uri() . '?' . rawurlencode($page);
1039         $summary['USER_AGENT']  = TRUE;
1040         $summary['REMOTE_ADDR'] = TRUE;
1041         pkwk_mail_notify($notify_subject,  var_export($target, TRUE), $summary);
1042 }
1043
1044 ?>