OSDN Git Service

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