postfilter-test.php
1 <?php 2 include_once("rpc.php"); 3 include_once("util.php"); 4 5 function out_of_order_keys($post, $f1, $f2) 6 { 7 $pkeys = array_keys($_POST); 8 9 return array_search($f1, $pkeys) > array_search($f2, $pkeys); 10 } 11 12 // style 1 - no spaces, just letters+numbers in fields 13 function is_random_text($fields) 14 { 15 foreach($fields as $field) { 16 $num = preg_match('/[0-9]/', $field); 17 $lc = preg_match('/[a-z]/', $field); 18 $uc = preg_match('/[A-Z]/', $field); 19 $spc = preg_match('/ /', $field); 20 if ($spc || ($num + $lc + $uc)<2) return false; 21 } 22 23 return true; 24 } 25 26 // style 2 - uc+lc+spaces in fields 27 function is_random_word_text($fields) 28 { 29 foreach($fields as $field) { 30 $other = preg_match('/[^a-zA-Z \n]/', $field); 31 $lc = preg_match('/[a-z]/', $field); 32 $uc = preg_match('/[A-Z]/', $field); 33 $spc = preg_match('/ /', $field); 34 if ($other || !$spc || !$lc || !$uc) return false; 35 } 36 37 return true; 38 } 39 40 function has_cyrillic($txt) 41 { 42 return preg_match('/[\xD0-\xD3][\x80-\xBF]/', $txt) > 0; 43 } 44 45 function expand_short_urls($com) 46 { 47 $urls = array(); 48 $urltext = ""; 49 50 $com = preg_replace("/\(dot\)|DOT/", ".", $com); 51 preg_replace("@(([A-Za-z0-9_-]+\.)?((tinyurl|go2cut|doiop|x2t|snipr|gorkk|veeox|gurlx|goshrink|shrink[a-z]+|shortmaker|notlong|2gohere|urlmr|adjix|icanhaz|linkbee|oeeq|url9|urlzen|cladpal|redirx|yuarel|llfk|as2h|at|url-go|shrten|eyodude|urlxp|myurlz|allno1|nlz2|ye-s|way|lymme|unrelo|qfwr|urluda|golinkgo|cnekt|12n3|peqno|pasukan|vktw|snipie|4gk|82au|[a-z]+url|tinyden)\\.com|(lix|urlm|t2w|trigg|flib)\\.in|j\\.mp|hub\\.tm|(smarturl|fogz|urlink)\\.eu|sn\\.vc|atu\\.ca|(a|is)\\.gd|(xrl|nuurl|kore|p3n|txtn|hepy|w95|freepl|0x3|2tu)\\.us|dduri\\.info|cc\\.st|mzan\\.si|(\xe2\x9d\xbd|cctv)\\.ws|(metamark|sogood|idek|2tr|ln-s|urlaxe|littleurl)\\.net|(fyad|linkmenow|linkplug)\\.org|ad\\.vu|(bit|xa|ow|3|smal|to)\\.ly|safe\\.mn|goo\\.gl|is\\.gd|zi\\.ma|(tr|sn|jar|gow)\\.im|twurl\\.(cc|nl)|(fon|cli|rod|mug)\\.gs|(urlenco|dboost)\\.de|(tiny|bizz|blu|juu)\\.cc|2big\\.at|(crum|sk9)\\.pl|(bloat|pnt|lnq)\\.me|kl\\.am|(showip|2so)\\.be|minilink\\.me|lurl\\.no|(jai|cvm)\\.biz|xs\\.md|short\\.to|(yep|trunc)\\.it|a\\.nf|shortlinks\\.co\\.uk|redir\\.ec|tim\\.pe)(/[?A-Za-z0-9_-]*)?)@ei", '$urls[] = "http://$1";', $com); 52 53 foreach($urls as $url) { 54 $com .= strtolower(rpc_find_real_url(str_replace("preview.tinyurl","tinyurl",$url))); 55 } 56 57 return $com; 58 } 59 60 function normalize_ascii($text, $preserve_case = 0) 61 { 62 $text = preg_replace( '#[(\[={]dot[)\]=}]#i', '.', $text ); 63 64 $t = Transliterator::create("Any-Latin; nfd; [:nonspacing mark:] remove; nfkc; Latin-ASCII"); 65 if (!$t) return $text; 66 $text = $t->transliterate($text); 67 if (!$preserve_case) $text = strtolower($text); 68 69 return $text; 70 } 71 72 function strip_zerowidth($text) { 73 $text = preg_replace('/ 74 [\x17\x8\x1f]|\x{0702}|\x{1D176}|\x{008D}|\x{00A0}|\x{205F}|\x{FEFF}|\x{11A6}|\x{00AD}|\x{3164}|\x{2800}|\x{180B}|\x{180C}|\x{180D}| 75 \x{115F}|\x{1160}|\x{FFA0}|\x{034f}|\x{180e}|\x{17B4}|\x{17B5}| 76 [\x{0001}-\x{0008}\x{000E}\x{000F}\x{0010}-\x{001F}\x{007F}-\x{009F}]| 77 [\x{2000}-\x{200F}]| 78 [\x{2028}-\x{202F}]| 79 [\x{2060}-\x{206F}]| 80 [\x{fe00}-\x{fe0f}]| 81 [\x{FFF0}-\x{FFFB}]| 82 [\x{E0100}-\x{E01EF}]| 83 [\x{E0001}-\x{E007F}] 84 /ux', '', $text); 85 return $text; 86 } 87 88 /** 89 * Removes codepoints above 3134F: 90 * E0000..E007F; Tags 91 * E0100..E01EF; Variation Selectors Supplement 92 * F0000..FFFFF; Supplementary Private Use Area-A 93 * 100000..10FFFF; Supplementary Private Use Area-B 94 */ 95 function strip_private_unicode($str) { 96 if ($str === '') { 97 return $str; 98 } 99 100 return preg_replace('/[^\x{0000}-\x{3134F}]/u', '', $str); 101 } 102 103 function strip_emoticons($text, $has_sjis_art = false) { 104 $regex = '[\x{2300}-\x{2311}\x{2313}-\x{23FF}]|[\x{3200}-\x{32FF}\x{2190}-\x{21FF}\x{2580}-\x{259F}\x{2600}-\x{26FF}\x{2B00}-\x{2BFE}\x{1F700}-\x{1F77F}\x{1F780}-\x{1F7D8}\x{1F780}-\x{1F7D8}\x{1F800}-\x{1F8FF}\x{1F900}-\x{1F9FF}]|[\x{1F200}-\x{1F2FF}]|[\x{2460}-\x{24FF}]|[\x{1F100}-\x{1F1FF}]|[\x{1F600}-\x{1F64F}]|[\x{1F300}-\x{1F5FF}]|[\x{1F680}-\x{1F6FF}]|[\x{2600}-\x{26FF}]|[\x{2700}-\x{27BF}]|[\x{1F000}-\x{1F02F}]|[\x{1F0A0}-\x{1F0FF}]|[\x{2139}\x{23F2}]|[\x{1F910}-\x{1F9E6}]|[\x{0365}]|\x{FDFD}|[\x{0488}\x{0489}\x{1abe}\x{20dd}\x{20de}\x{20df}\x{20e0}\x{20e2}\x{20e3}\x{20e4}\x{a670}\x{a671}\x{a672}\x{061c}\x{070F}\x{0332}\x{0305}\x{2B55}]|[\x{202A}-\x{202E}\x{2060}-\x{206F}]|[\x{200E}\x{200F}\x{180e}\x{2b50}\x{23b3}\x{23F1}]|[\x{1F780}-\x{1F7FF}\x{1FA70}-\x{1FAFF}]|[\x{1D173}-\x{1D17A}\x{13000}-\x{1342F}\x{fe00}-\x{fe0f}]'; 105 106 if (!$has_sjis_art) { 107 $regex .= '|[\x{2502}-\x{257F}]'; 108 } 109 110 return preg_replace("/$regex/u", '', $text); 111 } 112 113 function strip_fake_capcodes($str) { 114 // double FULLWIDTH NUMBER SIGN or # 115 // PLACE OF INTEREST SIGN 116 return preg_replace('/[\x{FF03}#]{2,}|\x{2318}/u', '', $str); 117 } 118 119 function normalize_text($text, $filter = '') { 120 $text = normalize_ascii($text); 121 $text = strip_zerowidth($text); 122 123 //if ($filter) $text = $filter($text); 124 125 $text = preg_replace('@[^a-zA-Z0-9.,/&:;?=~_-]@', '', $text); 126 127 return $text; 128 } 129 130 function normalize_content( $name ) 131 { 132 // this needs some absolutely retarded shit to get this to not suck, however 133 // it is an almost fool proof way of translating to ascii letters 134 // without breaking kanji, cyrillic etc 135 136 $name = preg_replace( '#[\x{2600}-\x{26FF}]#u', '', $name ); 137 138 // set internal incoding to utf-8 139 //die($name); 140 $oldEncoding = mb_internal_encoding(); 141 mb_internal_encoding('UTF-8'); 142 $name = convert_to_utf8($name); 143 // Done, back to old encoding 144 145 $newname = ''; 146 147 $len = mb_strlen( $name ); 148 for( $i = 0; $i < $len; $i++ ) { 149 trans_similar_to_ascii( $newname, mb_substr( $name, $i, 1 ) ); 150 } 151 152 mb_internal_encoding($oldEncoding); 153 return $newname; 154 } 155 156 function convert_to_utf8( $content ) 157 { 158 if( !mb_check_encoding( $content, 'UTF-8' ) || !($content === mb_convert_encoding(mb_convert_encoding($content, 'UTF-32', 'UTF-8' ), 'UTF-8', 'UTF-32')) ) { 159 $content = mb_convert_encoding($content, 'UTF-8'); 160 } 161 162 return $content; 163 } 164 165 function mb_ord( $char ) 166 { 167 mb_detect_order( array( 'UTF-8', 'ISO-8859-15', 'ISO-8859-1', 'ASCII' ) ); 168 $result = unpack( 'N', mb_convert_encoding( $char, 'UCS-4BE', 'UTF-8' ) ); 169 170 if( is_array( $result ) === true ) return $result[1]; 171 172 return ord($char); 173 } 174 175 function normalize_check($com,$sub,$f) 176 { 177 $n = normalize_text($com.$sub, "expand_short_urls"); 178 $n2 = preg_replace("/[\x80-\xFF]/", "", html_entity_decode($com, ENT_QUOTES, "UTF-8")); 179 180 record_post_info($f, "from: $com$sub\nto: $n\ndeutf8: $n2"); 181 } 182 183 function match_banned_text($links, $text, $is_re) 184 { 185 $badlink = ""; 186 $should_ban = false; 187 188 foreach ($links as $l) { 189 if ($l == '#') continue; 190 191 $badlink = $l; 192 193 if ($is_re) { 194 $should_ban = preg_match($l, $text, $m) > 0; 195 $badlink = TEST_BOARD ? "'$l' ({$m[0]})" : "'{$m[0]}'"; 196 } else { 197 $should_ban = strpos($text, $l) !== FALSE; 198 } 199 200 if ($should_ban) 201 break; 202 } 203 204 return $should_ban ? $badlink : ""; 205 } 206 207 function check_banned_links($text, $links, $priv, $pub, $is_re, $name, $dest, $ban, $long, $perm = false) 208 { 209 //check_banned_links($normalized_com, $sex, "sex spam links", S_BANNEDLINK, false, $name, $dest, true, false); 210 $badlink = match_banned_text($links, $text, $is_re); 211 $should_ban = ($badlink != ""); 212 $len = $long ? 14 : 1; 213 $len = $perm ? -1 : $len; 214 215 if ($should_ban == true) { 216 $privres = sprintf("banned %s %s: %s", $is_re?"regex":"string",htmlspecialchars($badlink),$priv); 217 if ($ban) { 218 $pub = str_replace('Error: ', '', $pub); 219 auto_ban_poster($name, $len, 1, $privres, $pub, true); 220 } 221 if (TEST_BOARD) $pub .= "<br>".$privres; 222 error($pub, $dest); 223 } 224 } 225 226 // this used to check the autobans table but now it just permabans 227 function auto_ban($name, $reason) { 228 auto_ban_poster($name, 7, 1, $reason); 229 } 230 231 function get_jpeg_dimensions($contents) 232 { 233 // this is faster than getimagesize 234 235 $i = 0; 236 $len = strlen($contents); 237 238 if( ord($contents{0}) == 0xFF & ord($contents{1}) == 0xD8 && ord($contents{2}) == 0xFF & ord($contents{3}) == 0xE0 ) { 239 $i = 4; 240 241 if( $contents{$i+2} == 'J' && $contents{$i+3} == 'F' && $contents{$i+4} == 'I' && $contents{$i+5} == 'F' && ord($contents{$i+6}) == 0x00 ) { 242 // valid image. 243 $block_length = ord($contents{$i}) * 256 + ord($contents{$i+1}); 244 245 while( $i < $len ) { 246 $i += $block_length; 247 248 if( $i > $len ) { 249 return false; 250 } 251 252 if( ord($contents{$i}) != 0xFF ) { 253 return false; 254 } 255 256 257 if( ord($contents{$i+1}) == 0xC0 ) { 258 $width = ord($contents{$i+7})*256 + ord($contents{$i+8}); 259 $height = ord($contents{$i+5})*256 + ord($contents{$i+6}); 260 261 return array($width, $height); 262 } else { 263 $i+=2; 264 $block_length = ord($contents{$i}) * 256 + ord($contents{$i+1}); 265 } 266 } 267 } 268 } 269 270 return false; 271 } 272 273 function file_too_big_for_type( $ext, $w, $h, $fsize ) 274 { 275 if ($ext === ".gif" || $ext === ".pdf" || $ext === '.webm' || $ext === '.mp4') { 276 return NO; 277 } 278 279 $uncompressed_size = $w * $h * 4; 280 281 return ($fsize > (3*$uncompressed_size)) ? YES : NO; 282 } 283 284 function regex_ignoring_nulls($words) 285 { 286 $rwords = preg_replace("/./", "$0[^\\\\x01-\\\\xFF]*", $words); 287 $rwords = str_replace(".", "\\.", $rwords); 288 return "/".implode("|", $rwords)."/i"; 289 } 290 291 /** 292 * Strips exif from JPEG images 293 * $file needs to be safe to use as shell argument 294 */ 295 function strip_jpeg_exif($file) { 296 return system("/usr/local/bin/jpegtran -copy none -outfile '$file' '$file'") !== false; 297 } 298 299 /** 300 * Strips non-whitelisted PNG chunks. 301 * Returns an error if an animated PNG is detected. 302 * Overwrites input file if modifications have been made. 303 * $file needs to be safe to use as shell argument 304 * Returns the number of chunks skipped or an error code (negative value). 305 */ 306 function strip_png_chunks($file, $max_chunk_len = 16 * 1024 * 1024) { 307 $keep_chunks = [ 308 'ihdr', 309 'plte', 310 'idat', 311 'iend', 312 'trns', 313 'gama', 314 'sbit', 315 'phys', 316 'srgb', 317 'bkgd', 318 'time', 319 'chrm', 320 'iccp' 321 ]; 322 323 $img = fopen($file, 'rb'); 324 325 if (!$img) { 326 return -9; 327 } 328 329 $data = fread($img, 8); 330 331 if ($data !== "\x89PNG\r\n\x1a\n") { 332 fclose($img); 333 return -1; 334 } 335 336 $output = ''; 337 338 $skip_count = 0; 339 340 while (!feof($img)) { 341 $chunk_len_buf = fread($img, 4); 342 343 if (!$chunk_len_buf) { 344 break; 345 } 346 347 if (strlen($chunk_len_buf) !== 4) { 348 return -1; 349 } 350 351 $chunk_len = unpack('N', $chunk_len_buf)[1]; 352 353 if ($chunk_len > $max_chunk_len) { 354 return -1; 355 } 356 357 $chunk_type_buf = fread($img, 4); 358 359 if (strlen($chunk_type_buf) !== 4) { 360 return -1; 361 } 362 363 $chunk_type = strtolower($chunk_type_buf); 364 365 // aPNG is not supported 366 if ($chunk_type === 'actl' || $chink_type === 'fctl' || $chink_type === 'fdat') { 367 return -2; 368 } 369 370 if (in_array($chunk_type, $keep_chunks)) { 371 if ($chunk_len > 0) { 372 $data = fread($img, $chunk_len); 373 374 if (strlen($data) !== $chunk_len) { 375 return -1; 376 } 377 } 378 else { 379 $data = ''; 380 } 381 382 $crc = fread($img, 4); 383 384 if (strlen($crc) !== 4) { 385 return -1; 386 } 387 388 $output .= $chunk_len_buf . $chunk_type_buf . $data . $crc; 389 390 if ($chunk_type === 'iend') { 391 fread($img, 1); 392 if (!feof($img)) { 393 $skip_count++; 394 } 395 break; 396 } 397 } 398 else { 399 fseek($img, $chunk_len + 4, SEEK_CUR); 400 $skip_count++; 401 } 402 } 403 404 fclose($img); 405 406 if ($output === '') { 407 return -1; 408 } 409 410 if ($skip_count === 0) { 411 return 0; 412 } 413 414 $out_file = $file . '_pngtmp'; 415 416 $out = fopen($out_file, 'wb'); 417 418 if (!$out) { 419 return -9; 420 } 421 422 if (fwrite($out, "\x89PNG\r\n\x1a\n") === false) { 423 return -9; 424 } 425 426 if (fwrite($out, $output) === false) { 427 return -9; 428 } 429 430 fclose($out); 431 432 if (rename($out_file, $file) === false) { 433 return -9; 434 } 435 436 return $skip_count; 437 } 438 439 // Calculates the actual image data inside a JPEG file and errors out 440 // if it's smaller than the reported size. 441 function validate_jpeg_size($file, $reported_size) { 442 $eof = false; 443 444 $img = fopen($file, 'rb'); 445 446 $data = fread($img, 2); 447 448 if ($data !== "\xff\xd8") { 449 fclose($img); 450 return false; 451 } 452 453 while (!feof($img)) { 454 $data = fread($img, 1); 455 456 if ($data !== "\xff") { 457 continue; 458 } 459 460 while (!feof($img)) { 461 $data = fread($img, 1); 462 463 if ($data !== "\xff") { 464 break; 465 } 466 } 467 468 if (feof($img)) { 469 break; 470 } 471 472 $byte = unpack('C', $data)[1]; 473 474 if ($byte === 217) { 475 $eof = ftell($img); 476 break; 477 } 478 479 if ($byte === 0 || $byte === 1 || ($byte >= 208 && $byte <= 216)) { 480 continue; 481 } 482 483 $data = fread($img, 2); 484 485 $length = unpack('n', $data)[1]; 486 487 if ($length < 1) { 488 break; 489 } 490 491 fseek($img, $length - 2, SEEK_CUR); 492 } 493 494 fclose($img); 495 496 // 50 KB 497 if ($reported_size - $eof >= 51200) { 498 error(S_IMGCONTAINSFILE, $file); 499 } 500 501 return $eof; 502 } 503 504 /** 505 * Checks for extensions and comments 506 * and calculates actual GIF data. 507 * Strips extra data if it exists. 508 * $file needs to be safe to use as shell argument. 509 */ 510 function strip_gif_extra_data($file, $reported_size) { 511 $binary = '/usr/local/bin/gifsicle'; 512 513 $res = shell_exec("$binary --sinfo \"$file\" 2>&1"); 514 515 if ($res !== null) { 516 $size = 0; 517 518 $need_strip = false; 519 520 if (preg_match('/ extensions [0-9]+| comment /', $res)) { 521 $need_strip = true; 522 } 523 else if (preg_match_all('/compressed size ([0-9]+)/', $res, $m)) { 524 foreach ($m[1] as $frame_size) { 525 $size += (int)$frame_size; 526 } 527 528 // Strip if 50+ KB of extra data is found 529 if ($reported_size - $size >= 51200) { 530 $need_strip = true; 531 } 532 } 533 534 if ($need_strip) { 535 if (system("$binary --no-comments --no-extensions \"$file\" -o \"$file\" >/dev/null 2>&1") === false) { 536 // gifsicle error 537 return -1; 538 } 539 else { 540 // file was modified 541 return 1; 542 } 543 } 544 } 545 else { 546 // gifsicle error 547 return -1; 548 } 549 550 // nothing changed 551 return 0; 552 } 553 554 // No longer used 555 function spam_filter_post_image($name, $dest, $md5, $upfile_name, $ext, $w, $h, $fsize) 556 { 557 if( $upfile_name == '' ) error('Blank file names are not supported.'); 558 559 if (file_too_big_for_type($ext, $w, $h, $fsize) === YES) { 560 $lim = 3*4*$w*$h; 561 error(S_IMGCONTAINSFILE, $dest); 562 } 563 564 $img_bytes = file_get_contents($dest); 565 $img_beginning = strlen($img_bytes) > 0x50000 ? substr($img_bytes, 0, 0x40000).substr($img_bytes, -(0x10000)) : $img_bytes; 566 567 global $silent_reject; 568 $silent_reject = 0; 569 570 // protect against IE's retarded MIME-sniffing XSS vulnerability 571 // by doing our own sniffing and rejecting exploitable files 572 { 573 $negative_match = regex_ignoring_nulls(array("minitokyonet", "urchin.js")); 574 //except minitokyo from this, it causes false positives 575 if (preg_match($negative_match, $img_beginning)===0) 576 { 577 // '<body', '<head', '<html', '<plaintext', '<pre', '<table', '<title', '<channel', '<scriptlet', '<span', 578 // taken from URLMON.DLL in win2k... 579 $positive_match = regex_ignoring_nulls(array('<a href', '<script', '<iframe', 'unescape', 'base64', 'charAt', 'del %0', 'WScript.Shell')); 580 if (preg_match($positive_match, $img_beginning, $m)) 581 { 582 $foundxss = true; 583 $foundstr = htmlentities($m[0]); 584 // $xssban = true; 585 } 586 } 587 588 if ($foundxss) { 589 if ($xssban) auto_ban_poster($name, 7, 1, "script in image (from image, found $foundstr at $xsspos)", "Posting image with embedded virus."); 590 if (TEST_BOARD) error("Script in image: '$foundstr' at $xsspos"); 591 error('Detected possible malicious code in image file.', $dest); 592 } 593 594 /* if (BOARD_DIR == 'b' && strpos($img_beginning, "AppleMark")) { 595 record_post_info("/www/perhost/applemark.txt"); 596 $silent_reject = true; 597 return; 598 } */ 599 } 600 // don't allow embedded zips and rars 601 { 602 $rar2 = 'REs^'; 603 $rar1 = "Rar!\x1A\x07\x00"; 604 $zip = "PK\x03\x04"; 605 $sevenz = "7z\xBC\xAF'\x1C"; 606 $pfbind = "pFBind"; 607 $deny = array( 608 'REs^', 609 "Rar!\x1A\x07\x00", 610 "PK\x03\x04", 611 "7z\xBC\xAF'\x1C"/*, 612 "pFBind", 613 "RIFF", 614 "matroska", 615 616 // stupid ogg shit 617 "OggS\x00", 618 'libVorbis', 619 "moot\x00", 620 "Krni\x00"*/ 621 ); 622 623 624 foreach($deny as $arc_string) 625 { 626 if(strpos($img_bytes, $arc_string) !== false) 627 { 628 error(S_IMGCONTAINSFILE, $dest); 629 } 630 } 631 } 632 // reject APNG 633 if($ext == '.png') 634 { 635 if(strpos($img_bytes, 'acTL') !== false && strpos($img_bytes, 'fcTL') !== false && strpos($img_bytes, 'fdAT') !== false) 636 { 637 error('APNG format not supported.', $dest); 638 } 639 } 640 } 641 642 function register_postfilter_hit($filter_id) { 643 $long_ip = (int)ip2long($_SERVER['REMOTE_ADDR']); 644 645 $filter_id = (int)$filter_id; 646 647 $query = <<<SQL 648 SELECT id FROM postfilter_hits 649 WHERE filter_id = $filter_id AND long_ip = $long_ip AND created_on > DATE_SUB(NOW(), INTERVAL 1 HOUR) LIMIT 1 650 SQL; 651 652 $res = mysql_global_call($query); 653 654 if ($res && mysql_num_rows($res) === 1) { 655 return true; 656 } 657 658 $query = "INSERT INTO postfilter_hits (filter_id, board, long_ip) VALUES($filter_id, '%s', $long_ip)"; 659 660 return mysql_global_call($query, BOARD_DIR); 661 } 662 663 function log_postfilter_hit($filter, $board, $thread_id, $name, $sub, $com, $upfile_name) { 664 $ip = $_SERVER['REMOTE_ADDR']; 665 666 $country = $_SERVER['HTTP_X_GEO_COUNTRY']; 667 668 $threat_score = spam_filter_get_threat_score($country, !$thread_id, true); 669 670 $meta = spam_filter_format_http_headers("$name\n$sub\n$com", $country, $upfile_name, $threat_score); 671 672 $action = "filter_{$filter['id']}"; 673 674 $query = <<<SQL 675 INSERT INTO event_log(`type`, `board`, `thread_id`, `ip`, `meta`) 676 VALUES('%s', '%s', %d, '%s', '%s') 677 SQL; 678 679 mysql_global_call($query, $action, $board, $thread_id, $ip, $meta); 680 } 681 682 /** 683 * New postfilter. Uses the database. 684 * Errors-out if a Reject filter matches. 685 * Returns true if an Autosage filter matches. 686 * Otherwise returns false. 687 */ 688 function spam_filter_post_content_new($board, $resto, $com, $sub, $name, $upfile_name, $pwd = null, $pass_id = null) { 689 if (preg_match('/^php../', $upfile_name) === 1 && strpos($upfile_name, '.') === false) { 690 auto_ban_poster($name, 14, 1, 'PHP proxy (via filename check)', 'Proxy/Tor exit node.'); 691 error(S_GENERICERROR); 692 } 693 694 // Postfilter 695 $tbl = 'postfilter'; 696 697 $query = <<<SQL 698 SELECT id, pattern, autosage, log, regex, quiet, lenient, ops_only, min_count, 699 board, ban_days, created_on, updated_on FROM $tbl 700 WHERE active = 1 AND (board = '' OR board = '%s') 701 SQL; 702 703 $res = mysql_global_call($query, $board); 704 705 if (!$res) { 706 return false; 707 } 708 709 // Remove bbcode 710 if (strpos($com, '[') !== false) { 711 $com = preg_replace('/\[\/?(?:spoiler|code|sjis)\]/', '', $com); 712 } 713 714 // For string filters 715 $normalized_com = normalize_text($name.$sub.$upfile_name.$com); 716 // For regex filters 717 $expanded_com = "$name $sub $com"; 718 // For autosage filters 719 if (!$resto) { 720 $normalized_com_sage = preg_replace('/[.,!:>\/]+|>/', ' ', $sub . ' ' . $com . ' '. $name); 721 $normalized_com_sage = ucwords(strtolower($normalized_com_sage)); 722 $normalized_com_sage = normalize_ascii($normalized_com_sage, 1); 723 } 724 725 $userpwd = UserPwd::getSession(); 726 727 $matched_filter = false; 728 729 while ($filter = mysql_fetch_assoc($res)) { 730 // Counter mode: triggers when the number of matches is at least $min_count 731 $min_count = (int)$filter['min_count']; 732 733 if ($min_count < 1) { 734 $min_count = 1; 735 } 736 737 // Lenient filter 738 if ($filter['lenient']) { 739 if ($userpwd) { 740 if ($filter['updated_on']) { 741 $since_ts = (int)$filter['updated_on']; 742 } 743 else { 744 $since_ts = (int)$filter['created_on']; 745 } 746 747 if ($userpwd->isUserKnownOrVerified(60, $since_ts)) { // 1 hour 748 continue; 749 } 750 } 751 } 752 753 // OPs-only filter 754 if ($filter['ops_only'] && $resto) { 755 continue; 756 } 757 758 if ($filter['autosage']) { 759 // Autosage filter but the post is a reply 760 if ($resto) { 761 continue; 762 } 763 // Regex filter 764 if ($filter['regex']) { 765 if ($min_count > 1) { 766 if (preg_match_all($filter['pattern'], $expanded_com) >= $min_count) { 767 $matched_filter = $filter; 768 break; 769 } 770 } 771 else { 772 if (preg_match($filter['pattern'], $expanded_com) === 1) { 773 $matched_filter = $filter; 774 break; 775 } 776 } 777 } 778 // String filter for autosaging 779 else { 780 if ($min_count > 1) { 781 if (substr_count($normalized_com_sage, $filter['pattern']) >= $min_count) { 782 $matched_filter = $filter; 783 break; 784 } 785 } 786 else { 787 if (strpos($normalized_com_sage, $filter['pattern']) !== false) { 788 $matched_filter = $filter; 789 break; 790 } 791 } 792 } 793 } 794 // Regex filter 795 if ($filter['regex']) { 796 if ($min_count > 1) { 797 if (preg_match_all($filter['pattern'], $expanded_com) >= $min_count) { 798 $matched_filter = $filter; 799 break; 800 } 801 } 802 else { 803 if (preg_match($filter['pattern'], $expanded_com) === 1) { 804 $matched_filter = $filter; 805 break; 806 } 807 } 808 } 809 // String filter 810 else { 811 if ($min_count > 1) { 812 if (substr_count($normalized_com, $filter['pattern']) >= $min_count) { 813 $matched_filter = $filter; 814 break; 815 } 816 } 817 else { 818 if (strpos($normalized_com, $filter['pattern']) !== false) { 819 $matched_filter = $filter; 820 break; 821 } 822 } 823 } 824 } 825 826 if ($matched_filter !== false) { 827 // Update hit stats 828 register_postfilter_hit($matched_filter['id']); 829 830 // Autosage 831 if ($matched_filter['autosage']) { 832 return true; 833 } 834 // Log 835 else if ($matched_filter['log']) { 836 log_postfilter_hit($matched_filter, $board, $resto, $name, $sub, $com, $upfile_name); 837 } 838 // Reject 839 else { 840 if ($matched_filter['ban_days']) { 841 $err = S_BANNEDTEXT; 842 $ban_days = (int)$matched_filter['ban_days']; 843 $private_reason = 'banned string in comment (filter ID: ' . $matched_filter['id'] . ')'; 844 $public_reason = $err; 845 auto_ban_poster($name, $ban_days, 1, $private_reason, $public_reason, true, $pwd, $pass_id); 846 } 847 else { 848 $err = S_REJECTTEXT; 849 } 850 851 if ($matched_filter['quiet']) { 852 show_post_successful_fake($resto); 853 die(); 854 } 855 856 if (TEST_BOARD) { 857 $err .= ' (filter ID: ' . $matched_filter['id'] . ')'; 858 } 859 860 error($err); 861 } 862 } 863 864 // Other 865 if ($sub !== '') { 866 $normalized_sub = normalize_text($sub); 867 868 if (stripos($sub, 'moot') !== false) { 869 error("You can't post with that subject."); 870 } 871 872 if (stripos($normalized_com, '##') !== false || stripos($sub, 'admin') !== false) { 873 error("You can't post with that subject."); 874 } 875 } 876 877 return false; 878 } 879 880 function isIPRangeBannedReport($long_ip, $asn, $board, $userpwd = null) { 881 return isIPRangeBanned($long_ip, $asn, 882 [ 883 'board' => $board, 884 'is_report' => true, 885 'userpwd' => $userpwd, 886 ] 887 ); 888 } 889 890 // Checks if the IP is rangebanned 891 // options: 892 // board(string), is_sfw(bool, requires board) 893 // userpwd(UserPwd): instance of UserPwd or null, 894 // is_report(bool), is_op(bool), has_img(bool), 895 // browser_id(string), 896 // op_content(string): content of the thread OP for per-thread bans (unused) 897 // returns the rangeban database entry if the IP is banned, false otherwise 898 function isIPRangeBanned($long_ip, $asn, $options = []) { 899 $long_ip = (int)$long_ip; 900 901 $asn = (int)$asn; 902 903 $now = (int)$_SERVER['REQUEST_TIME']; 904 905 $cols = 'created_on, updated_on, expires_on, active, boards, ops_only, img_only, lenient, report_only, ua_ids'; 906 907 $query = <<<SQL 908 (SELECT SQL_NO_CACHE $cols FROM iprangebans 909 WHERE range_start <= $long_ip AND range_end >= $long_ip AND active = 1 910 AND (expires_on = 0 OR expires_on > $now)) 911 SQL; 912 913 if ($asn > 0) { 914 $query .= <<<SQL 915 UNION (SELECT $cols FROM iprangebans 916 WHERE asn = $asn AND active = 1 AND (expires_on = 0 OR expires_on > $now)) 917 SQL; 918 } 919 920 $query .= ' ORDER BY lenient ASC'; 921 922 $res = mysql_global_call($query); 923 924 if (!$res) { 925 return false; 926 } 927 928 // Parameters 929 if (isset($options['board'])) { 930 $board = $options['board']; 931 $is_sfw = isset($options['is_sfw']) && $options['is_sfw']; 932 } 933 else { 934 $board = null; 935 $is_sfw = false; 936 } 937 938 if (isset($options['browser_id'])) { 939 $browser_id = $options['browser_id']; 940 } 941 else { 942 $browser_id = null; 943 } 944 945 if (isset($options['req_sig'])) { 946 $req_sig = $options['req_sig']; 947 } 948 else { 949 $req_sig = null; 950 } 951 952 if (isset($options['op_content']) && $options['op_content'] !== '') { 953 $op_content = $options['op_content']; 954 } 955 else { 956 $op_content = null; 957 } 958 959 $is_op = isset($options['is_op']) && $options['is_op']; 960 $is_report = isset($options['is_report']) && $options['is_report']; 961 $has_img = isset($options['has_img']) && $options['has_img']; 962 963 if (isset($options['userpwd']) && $options['userpwd'] && $options['userpwd'] instanceof UserPwd) { 964 $userpwd = $options['userpwd']; 965 } 966 else { 967 $userpwd = null; 968 } 969 970 // OP-only and Image-only lenient rangebans also require a certain number of posts 971 $post_count_ok = $userpwd && $userpwd->postCount() >= 3 && ($userpwd->maskLifetime() > 900 || $userpwd->postCount() >= 15); 972 973 while ($range = mysql_fetch_assoc($res)) { 974 if ($range['boards']) { 975 if ($board === null) { 976 continue; 977 } 978 979 $board_matcher = ",{$range['boards']},"; 980 981 if (strpos($board_matcher, ",$board,") === false) { 982 // _ws_ scope affects all work safe boards 983 if ($is_sfw) { 984 if (strpos($board_matcher, ",_ws_,") === false) { 985 continue; 986 } 987 } 988 else { 989 continue; 990 } 991 } 992 } 993 994 $post_count_check = true; 995 996 if ($range['report_only'] && !$is_report) { 997 continue; 998 } 999 1000 if ($range['ops_only']) { 1001 if (!$is_op) { 1002 continue; 1003 } 1004 else { 1005 $post_count_check = $post_count_ok; 1006 } 1007 } 1008 1009 if ($range['img_only']) { 1010 if (!$has_img) { 1011 continue; 1012 } 1013 else { 1014 $post_count_check = $post_count_ok; 1015 } 1016 } 1017 1018 if ($range['ua_ids']) { 1019 $_skip = true; 1020 1021 if ($browser_id && strpos($range['ua_ids'], $browser_id) !== false) { 1022 $_skip = false; 1023 } 1024 1025 if ($_skip && $req_sig && strpos($range['ua_ids'], $req_sig) !== false) { 1026 $_skip = false; 1027 } 1028 1029 if ($_skip) { 1030 continue; 1031 } 1032 } 1033 1034 if ($userpwd && $range['lenient']) { 1035 $lenient = (int)$range['lenient']; 1036 1037 if ($range['updated_on']) { 1038 $since_ts = (int)$range['updated_on']; 1039 } 1040 else { 1041 $since_ts = (int)$range['created_on']; 1042 } 1043 1044 // Mode 1: Known 24h or Verified 1045 if ($lenient === 1 && ($userpwd->verifiedLevel() || ($userpwd->isUserKnown(1440, $since_ts) && $post_count_check))) { 1046 continue; 1047 } 1048 // Mode 2: Known 24h only 1049 else if ($lenient === 2 && $userpwd->isUserKnown(1440, $since_ts) && $post_count_check) { 1050 continue; 1051 } 1052 // Mode 3: Verified only 1053 else if ($lenient === 3 && $userpwd->verifiedLevel()) { 1054 continue; 1055 } 1056 } 1057 1058 return $range; 1059 } 1060 1061 return false; 1062 } 1063 1064 /** 1065 * Checks if the IP has enough posting history 1066 * $mode: 0 = check for replies, 1 = check for image replies, 2 = check of threads 1067 * Caches results. 1068 */ 1069 function spam_filter_is_ip_known($long_ip, $board = null, $mode = 0, $minutes_min = 0, $posts_min = 1) { 1070 static $cache = array(); 1071 1072 $long_ip = (int)$long_ip; 1073 1074 if (!$long_ip) { 1075 return false; 1076 } 1077 1078 $cache_key = "$long_ip.$board.$mode.$minutes_min.$posts_min"; 1079 1080 if (isset($cache[$cache_key])) { 1081 return $cache[$cache_key]; 1082 } 1083 1084 // Not after (3 days) 1085 $minutes_max = 4320; 1086 1087 // Not before 1088 $minutes_min = (int)$minutes_min; 1089 1090 // At least X replies 1091 $posts_min = (int)$posts_min; 1092 1093 // Board 1094 if ($board) { 1095 $board_clause = "AND board = '" . mysql_real_escape_string($board) . "'"; 1096 } 1097 else { 1098 $board_clause = ''; 1099 } 1100 1101 // Mode: 1 = image replies, 2 = threads, 0 = any reply 1102 if ($mode === 1) { 1103 $action_clause = "AND action = 'new_reply' AND had_image = 1"; 1104 } 1105 else if ($mode === 2) { 1106 $action_clause = "AND action = 'new_thread'"; 1107 } 1108 else { 1109 $action_clause = "AND action = 'new_reply'"; 1110 } 1111 1112 // Not before 1113 if (!$minutes_min) { 1114 $time_clause = "time >= DATE_SUB(NOW(), INTERVAL $minutes_max MINUTE)"; 1115 } 1116 else { 1117 $time_clause = "(time BETWEEN DATE_SUB(NOW(), INTERVAL $minutes_max MINUTE) AND DATE_SUB(NOW(), INTERVAL $minutes_min MINUTE))"; 1118 } 1119 1120 // Check posting history 1121 $query = <<<SQL 1122 SELECT SQL_NO_CACHE COUNT(*) FROM user_actions 1123 WHERE ip = $long_ip $action_clause $board_clause 1124 AND $time_clause 1125 SQL; 1126 1127 $res = mysql_global_call($query); 1128 1129 if (!$res) { 1130 return false; 1131 } 1132 1133 $count = (int)mysql_fetch_row($res)[0]; 1134 1135 if ($count < $posts_min) { 1136 $cache[$cache_key] = false; 1137 return false; 1138 } 1139 1140 // Check deletion history 1141 /* 1142 $query = <<<SQL 1143 SELECT SQL_NO_CACHE COUNT(*) FROM user_actions 1144 WHERE ip = $long_ip AND action = 'delete' 1145 AND time >= DATE_SUB(NOW(), INTERVAL 48 HOUR) 1146 SQL; 1147 1148 $res = mysql_global_call($query); 1149 1150 if (!$res) { 1151 return false; 1152 } 1153 1154 $count = (int)mysql_fetch_row($res)[0]; 1155 1156 if ($count > 0) { 1157 $cache[$cache_key] = false; 1158 return false; 1159 } 1160 */ 1161 1162 $cache[$cache_key] = true; 1163 1164 return true; 1165 } 1166 1167 /* 1168 * Checks if the user has a valid posting history to bypass a rangeban (FIXME: deprecated) 1169 */ 1170 function spam_filter_is_user_known($long_ip, $board = null, $pwd = null, $minutes = 15, $count = 1) { 1171 static $cache = array(); 1172 1173 $pwd = null; // FIXME 1174 1175 $interval = (int)$minutes; 1176 1177 $cache_key_ip = "{$long_ip}:{$interval}"; 1178 1179 if (isset($cache[$cache_key_ip])) { 1180 return $cache[$cache_key_ip]; 1181 } 1182 1183 if ($pwd && $board) { 1184 $cache_key_pwd = "{$board}:{$pwd}:{$interval}"; 1185 1186 if (isset($cache[$cache_key_pwd])) { 1187 return $cache[$cache_key_pwd]; 1188 } 1189 } 1190 1191 $count = (int)$count; 1192 1193 if ($count < 1) { 1194 $count = 1; 1195 } 1196 1197 // Check the IP 1198 $query = <<<SQL 1199 SELECT 1 FROM user_actions 1200 WHERE ip = %d AND action = 'new_reply' 1201 AND time < DATE_SUB(NOW(), INTERVAL $interval MINUTE) 1202 LIMIT $count 1203 SQL; 1204 1205 $res = mysql_global_call($query, $long_ip); 1206 1207 if (!$res) { 1208 return true; 1209 } 1210 1211 if (mysql_num_rows($res) === $count) { 1212 $cache[$cache_key_ip] = true; 1213 return true; 1214 } 1215 1216 $cache[$cache_key_ip] = false; 1217 1218 // Check the password 1219 if ($pwd && $board) { 1220 $time_lim = $_SERVER['REQUEST_TIME'] - ($interval * 60); 1221 1222 $query = <<<SQL 1223 SELECT 1 FROM `%s` WHERE pwd = '%s' AND time < $time_lim LIMIT 1 1224 SQL; 1225 1226 $res = mysql_board_call($query, $board, $pwd); 1227 1228 if (!$res) { 1229 return true; 1230 } 1231 1232 if (mysql_num_rows($res)) { 1233 $cache[$cache_key_pwd] = true; 1234 return true; 1235 } 1236 1237 $cache[$cache_key_pwd] = false; 1238 } 1239 1240 return false; 1241 } 1242 1243 function spam_filter_is_common_ua($ua) { 1244 $major = '[1-9]\d+'; 1245 $minor = '\.[.0-9]+'; 1246 $moz5 = 'Mozilla/5\.0'; 1247 $chrome = "Chrome/$major\.0\.0\.0"; 1248 $firefox = "Firefox/$major$minor$"; 1249 $gecko = 'Gecko/20100101'; 1250 $safari = "Safari/$major$minor"; 1251 $awk = "AppleWebKit/$major$minor"; 1252 $osxv = '[1-9][_0-9]+'; 1253 $iosv = 'Mobile/[1-9][E0-9]+'; 1254 $khtml = '\(KHTML, like Gecko\)'; 1255 1256 $patterns = [ 1257 "@^$moz5 \(Windows NT 10\.0; Win64; x64\) $awk $khtml $chrome $safari$@", 1258 "@^$moz5 \(Windows NT (?:10\.0|6\.[13]);(?: Win64; x64;)? rv:$major$minor\) $gecko $firefox@", 1259 "@^$moz5 \(Linux; Android 10; K\) $awk $khtml $chrome (?:Mobile )?$safari$@", 1260 "@^$moz5 \(iPhone; CPU iPhone OS $osxv like Mac OS X\) $awk $khtml Version/[1-9]+$minor $iosv $safari( OPX/[.0-9]+)?$@", 1261 "@^$moz5 \(Windows NT 10\.0; Win64; x64\) $awk $khtml $chrome $safari Edg/$major\.0\.0\.0$@", 1262 "@^$moz5 \(Windows NT 10\.0; Win64; x64\) $awk $khtml $chrome $safari OPR/$major\.0\.0\.0( \(Edition [^\)]{1,8}\))?$@", 1263 "@^$moz5 \(Macintosh; Intel Mac OS X 10_15_7\) $awk $khtml $chrome $safari$@", 1264 "@^$moz5 \(X11; Linux x86_64; rv:$major$minor\) $gecko $firefox$@", 1265 "@^$moz5 \(Android [1-9][0-9]{1,2}; Mobile; rv:$major$minor\) Gecko/$major$minor $firefox$@", 1266 "@^$moz5 \(Linux; Android 10; K\) $awk $khtml SamsungBrowser/[1-9][.0-9]+ $chrome (?:Mobile )?$safari$@", 1267 "@^$moz5 \(Macintosh; Intel Mac OS X 10\.15; rv:$major$minor\) $gecko $firefox$@", 1268 "@^$moz5 \(Macintosh; Intel Mac OS X 10_15_7\) $awk $khtml Version/$major$minor $safari$@", 1269 "@^$moz5 \(X11; Linux x86_64\) $awk $khtml $chrome $safari$@", 1270 "@^$moz5 \(Linux; Android $major; [^;]+; wv\) $awk $khtml Version/4\.0 Chrome/$major$minor Mobile $safari$@", 1271 "@^$moz5 \(Linux; Android 10; K\) $awk $khtml $chrome (?:Mobile )?$safari EdgA/$major$minor$@", 1272 "@^$moz5 \(iPhone; CPU iPhone OS $osxv like Mac OS X\) $awk $khtml CriOS/$major$minor $iosv $safari$@", 1273 "@^$moz5 \(Linux; Android 10; K\) $awk $khtml $chrome (?:Mobile )?$safari OPR/$major\.0\.0\.0$@", 1274 ]; 1275 1276 $val = false; 1277 1278 foreach ($patterns as $r) { 1279 if (preg_match($r, $ua)) { 1280 $val = true; 1281 break; 1282 } 1283 } 1284 1285 return $val; 1286 } 1287 1288 /** 1289 * Generates the ID of the device from browser's user agent 1290 */ 1291 function spam_filter_get_browser_id() { 1292 static $cache = null; 1293 1294 if ($cache !== null) { 1295 return $cache; 1296 } 1297 1298 $ua = $_SERVER['HTTP_USER_AGENT']; 1299 1300 if (!$ua) { 1301 return '0deadbeef'; 1302 } 1303 1304 if (preg_match('/Android|iPhone|iPad|Dalvik|Clover|Kuroba|ChanOS/', $ua)) { 1305 $is_mobile = 1; 1306 } 1307 else if (isset($_SERVER['HTTP_SEC_CH_UA_MOBILE']) && $_SERVER['HTTP_SEC_CH_UA_MOBILE'] === '?1') { 1308 $is_mobile = 1; 1309 } 1310 else { 1311 $is_mobile = 0; 1312 } 1313 1314 $clean_ua = preg_replace('/(\/|:)[.0-9]+/', '', $ua); 1315 1316 if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && $_SERVER['HTTP_SEC_CH_UA_MODEL'] && $_SERVER['HTTP_SEC_CH_UA_MODEL'] != '""') { 1317 $fmn = isset($_SERVER['HTTP_SEC_FETCH_MODE']) && $_SERVER['HTTP_SEC_FETCH_MODE'] === 'navigate'; 1318 1319 if (strpos($ua, '(Linux; Android 10; K)') !== false && !$fmn) { 1320 $clean_ua .= $_SERVER['HTTP_SEC_CH_UA_MODEL']; 1321 } 1322 } 1323 1324 $hmac_secret = 'd8d9616bce7b0a8fc83b422a7001b0492e17b4c16debeb43b77cf0060d9bdae3'; 1325 1326 $sig = hash_hmac('sha1', $clean_ua, $hmac_secret, true); 1327 1328 if (!$sig) { 1329 return ''; 1330 } 1331 1332 $cache = $is_mobile . bin2hex(substr($sig, 0, 4)); 1333 1334 return $cache; 1335 } 1336 1337 /** 1338 * Checks for rangebans when posting 1339 */ 1340 function spam_filter_post_ip($userpwd = null, $thread_id = null, $has_img = false) { 1341 global $captcha_bypass, $rangeban_bypass; 1342 1343 if (CAPTCHA) { 1344 if ($captcha_bypass === true) { 1345 return false; 1346 } 1347 } 1348 else { 1349 if ($rangeban_bypass) { 1350 return false; 1351 } 1352 } 1353 1354 $ip = $_SERVER['REMOTE_ADDR']; 1355 1356 if (isset($_SERVER['HTTP_X_GEO_ASN'])) { 1357 $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; 1358 } 1359 else { 1360 $_asninfo = GeoIP2::get_asn($ip); 1361 1362 if ($_asninfo) { 1363 $asn = (int)$_asninfo['asn']; 1364 } 1365 else { 1366 $asn = 0; 1367 } 1368 } 1369 1370 $long_ip = ip2long($ip); 1371 1372 if (!$long_ip) { 1373 return false; 1374 } 1375 1376 $browser_id = spam_filter_get_browser_id(); 1377 $req_sig = spam_filter_get_req_sig(); 1378 1379 $options = [ 1380 'board' => BOARD_DIR, 1381 'is_sfw' => DEFAULT_BURICHAN, 1382 'userpwd' => $userpwd, 1383 'is_op' => $thread_id == 0, 1384 'has_img' => $has_img, 1385 'browser_id' => $browser_id, 1386 'req_sig' => $req_sig 1387 ]; 1388 1389 if ($range = isIPRangeBanned($long_ip, $asn, $options)) { 1390 if ($range['lenient'] && $userpwd) { 1391 $userpwd->setCookie('.' . L::d(BOARD_DIR)); 1392 } 1393 1394 // Images only 1395 if ($range['img_only']) { 1396 $_err = S_IPRANGE_BLOCKED_IMG; 1397 } 1398 // Threads only 1399 else if ($range['ops_only']) { 1400 $_err = S_IPRANGE_BLOCKED_OP; 1401 } 1402 else { 1403 $_err = S_IPRANGE_BLOCKED; 1404 } 1405 1406 // Temporarily or Permanently 1407 if ($range['expires_on'] || $range['lenient']) { 1408 $_err .= ' ' . S_IPRANGE_BLOCKED_TEMP; 1409 } 1410 else { 1411 $_err .= ' ' . S_IPRANGE_BLOCKED_PERM; 1412 } 1413 1414 // Bypassed by verified or known users 1415 if ($range['lenient'] == 1) { 1416 $_err .= S_IPRANGE_BLOCKED_L1; 1417 } 1418 // Bypassed by known users only 1419 else if ($range['lenient'] == 2) { 1420 $_err .= S_IPRANGE_BLOCKED_L2; 1421 } 1422 // Bypassed by verified users only 1423 else if ($range['lenient'] == 3) { 1424 $_err .= S_IPRANGE_BLOCKED_L3; 1425 } 1426 1427 // 4chan pass mention 1428 $_err .= S_IPRANGE_BLOCKED_PASS; 1429 1430 error($_err); 1431 } 1432 1433 // Auto-rangebans, Mobile only 1434 // Bypassed by verified users or known users for at least 2h 1435 // or users who have made at least one post on the board 15 minutes ago 1436 if ($thread_id !== null && $browser_id[0] === '1') { 1437 $since_ts = 0; 1438 1439 if ($userpwd) { 1440 $user_known = $userpwd->isUserKnownOrVerified(120, 1); 1441 1442 $now = $_SERVER['REQUEST_TIME']; 1443 1444 if ($userpwd->postCount() > 0 && $userpwd->maskTs() <= $now - 900) { 1445 $since_ts = $userpwd->maskTs(); 1446 } 1447 } 1448 else { 1449 $user_known = false; 1450 } 1451 1452 if (!$user_known) { 1453 if (is_ip_auto_rangebanned($ip, BOARD_DIR, $thread_id, $browser_id, $since_ts)) { 1454 write_to_event_log('auto_range_hit', $ip, [ 1455 'board' => BOARD_DIR, 1456 'thread_id' => $thread_id, 1457 'ua_sig' => $browser_id 1458 ]); 1459 1460 if ($userpwd) { 1461 $userpwd->setCookie('.' . L::d(BOARD_DIR)); 1462 } 1463 1464 // Temporary, bypassablee by known or verified users 1465 error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1 . S_IPRANGE_BLOCKED_PASS); 1466 } 1467 } 1468 } 1469 1470 return false; 1471 } 1472 1473 function is_ip_auto_rangebanned($ip, $board, $thread_id, $browser_id, $since_ts = 0) { 1474 $range_sql = explode('.', $ip); 1475 1476 $range_sql = "{$range_sql[0]}.{$range_sql[1]}.%"; 1477 1478 $thread_id = (int)$thread_id; 1479 1480 if ($since_ts > 0) { 1481 $since_sql = ' AND created_on <= FROM_UNIXTIME(' . ((int)$since_ts) . ')'; 1482 } 1483 else { 1484 $since_sql = ''; 1485 } 1486 1487 $sql =<<<SQL 1488 SELECT id FROM event_log WHERE 1489 type = 'rangeban' AND board = '%s' AND thread_id = $thread_id AND ua_sig = '%s' 1490 AND ip LIKE '%s' 1491 AND created_on > DATE_SUB(NOW(), INTERVAL 120 MINUTE)$since_sql 1492 LIMIT 1 1493 SQL; 1494 1495 $res = mysql_global_call($sql, $board, $browser_id, $range_sql); 1496 1497 if (!$res) { 1498 return false; 1499 } 1500 1501 if (mysql_num_rows($res)) { 1502 return true; 1503 } 1504 1505 return false; 1506 } 1507 1508 /** 1509 * Dumps and formats HTTP headers and other request information for logging 1510 */ 1511 function spam_filter_format_http_headers($com = null, $country = null, $filename = null, $threat_score = null, $req_sig = null) { 1512 $bot_headers = ''; 1513 1514 foreach ($_SERVER as $_h_name => $_h_val) { 1515 if (substr($_h_name, 0, 5) == 'HTTP_') { 1516 if ($_h_name === 'HTTP_COOKIE') { 1517 $_cookies = array_keys($_COOKIE); 1518 $_cookies = array_intersect($_cookies, ['ws_style', 'nws_style', '4chan_pass', '_tcs', '_ga', 'cf_clearance' ]); 1519 $_cookie_count = count($_COOKIE); 1520 $bot_headers .= "HTTP_COOKIE: " . htmlspecialchars(implode(', ', $_cookies)) . " ($_cookie_count in total)\n"; 1521 } 1522 else if (strpos($_h_name, 'AUTH') !== false) { 1523 continue; 1524 } 1525 else { 1526 $bot_headers .= "$_h_name: " . htmlspecialchars($_h_val) . "\n"; 1527 } 1528 } 1529 } 1530 1531 $bot_headers .= "_POST: " . htmlspecialchars(implode(', ', array_keys($_POST))) . "\n"; 1532 1533 if ($country !== null) { 1534 $bot_headers .= "_Country: $country\n"; 1535 } 1536 1537 if ($threat_score !== null) { 1538 $bot_headers .= "_Score: " . $threat_score . "\n"; 1539 } 1540 1541 if ($req_sig !== null) { 1542 $bot_headers .= "_Sig: " . $req_sig . "\n"; 1543 } 1544 1545 if (isset($_COOKIE['_tcs'])) { 1546 $bot_headers .= "_TCS: " . htmlspecialchars($_COOKIE['_tcs']) . "\n"; 1547 } 1548 1549 if (isset($_COOKIE['4chan_pass'])) { 1550 $userpwd = UserPwd::getSession(); 1551 1552 if ($userpwd) { 1553 $bot_headers .= "_Pwd: " . htmlspecialchars($userpwd->getPwd()) . "\n"; 1554 } 1555 } 1556 1557 if ($filename !== null) { 1558 $bot_headers .= "_File: " . htmlspecialchars($filename) . "\n"; 1559 } 1560 1561 if ($com !== null) { 1562 $bot_headers .= "_Comment: $com"; 1563 } 1564 1565 return $bot_headers; 1566 } 1567 1568 function spam_filter_get_req_sig() { 1569 static $cache = null; 1570 1571 if ($cache !== null) { 1572 return $cache; 1573 } 1574 1575 $pick_headers = [ 1576 'HTTP_SEC_CH_UA_PLATFORM', 1577 'HTTP_SEC_CH_UA_MOBILE', 1578 'HTTP_SEC_CH_UA_MODEL', 1579 'HTTP_USER_AGENT', 1580 'HTTP_ACCEPT_LANGUAGE', 1581 'HTTP_SEC_FETCH_SITE', 1582 'HTTP_SEC_FETCH_MODE', 1583 'HTTP_SEC_FETCH_DEST', 1584 ]; 1585 1586 $need_headers = [ 1587 'HTTP_USER_AGENT', 1588 'HTTP_ACCEPT', 1589 'HTTP_REFERER', 1590 'HTTP_ACCEPT_LANGUAGE', 1591 ]; 1592 1593 $datapoints = []; 1594 1595 $keys = []; 1596 1597 foreach ($_SERVER as $k => $v) { 1598 if (in_array($k, $pick_headers)) { 1599 $keys[] = $k; 1600 } 1601 } 1602 1603 $keyline = implode('.', $keys); 1604 1605 $pointlines = [ 1606 'fetch_smd' => 'HTTP_SEC_FETCH_SITE.HTTP_SEC_FETCH_MODE.HTTP_SEC_FETCH_DEST', 1607 'fetch_dms' => 'HTTP_SEC_FETCH_DEST.HTTP_SEC_FETCH_MODE.HTTP_SEC_FETCH_SITE', 1608 'fetch_any' => 'HTTP_SEC_FETCH_' 1609 ]; 1610 1611 foreach ($pointlines as $key => $value) { 1612 if (strpos($keyline, $value) !== false) { 1613 $datapoints[] = $key; 1614 } 1615 } 1616 1617 if (isset($_SERVER['HTTP_SEC_GPC']) || isset($_SERVER['HTTP_DNT'])) { 1618 $datapoints[] = 'dnt_gpc'; 1619 } 1620 1621 if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { 1622 $datapoints[] = 'xrw'; 1623 } 1624 1625 if (preg_match('/HTTP_SEC_CH_UA_[^.]+\.HTTP_SEC_CH_UA_[^.]+\.HTTP_SEC_CH_UA_/', $keyline)) { 1626 $datapoints[] = 'ch_ua_block'; 1627 } 1628 1629 foreach ($need_headers as $k) { 1630 if (!isset($_SERVER[$k])) { 1631 $datapoints[] = 'missing'; 1632 break; 1633 } 1634 } 1635 1636 $sig = implode('+', $datapoints); 1637 1638 if (!$sig) { 1639 $sig = 'deadbeef'; 1640 } 1641 1642 $cache = substr(md5($sig), 0, 8); 1643 1644 return $cache; 1645 } 1646 1647 // Covers 251044 (79.14 %) unique IPs and 10454 (77.09 %) unique bans 1648 // IPs %: 0.1 | Bans %: 0 | GR1 all: 0 | Any EU: false 1649 function spam_filter_is_asn_whitelisted() { 1650 static $val = null; 1651 1652 static $whitelist = [ 1653 21928, 6167, 7922, 7018, 812, 701, 1221, 20115, 22773, 2856, 3320, 6805, 577, 1654 3209, 852, 20057, 20001, 5089, 4804, 10796, 8151, 5617, 7545, 11427, 209, 16086, 1655 719, 33363, 15557, 5650, 133612, 6327, 6128, 5607, 206067, 1267, 26599, 6830, 1656 8881, 2119, 14593, 28573, 3215, 26615, 3352, 1759, 7303, 11426, 35228, 55836, 1657 1136, 22085, 11351, 8708, 1257, 3269, 5410, 7418, 23693, 30722, 9299, 13285, 17676, 1658 3301, 7713, 5391, 33915, 44034, 8374, 4764, 51207, 29447, 27651, 7552, 12389, 1659 19108, 8359, 11315, 6057, 16591, 12479, 5769, 17072, 31615, 12271, 28403, 11664, 1660 6147, 15704, 10139, 39603, 12874, 25135, 5483, 5378, 4771, 4775, 12912, 6871, 1661 12322, 6614, 132199, 5432, 212238, 12929, 27699, 22927, 8473, 2860, 12430, 7029, 1662 6848, 5645, 8412, 62240, 8447, 15502, 174, 30036, 27747, 14638, 4230, 45727, 9009, 1663 17639, 4788, 10030, 11492, 45143, 18881, 2516, 21334, 15895, 4766, 16232, 6799, 1664 8400, 9443, 6079, 13999, 5610, 45899, 4761, 2586, 12353, 20845, 20365, 13280, 1665 22047, 3243, 4818, 27995, 20055, 24203, 9500, 25255, 45609, 29518, 7992, 39891, 1666 3303, 4773, 855, 8452, 136787, 18403, 3329, 52341, 1667 ]; 1668 1669 if ($val !== null) { 1670 return $val; 1671 } 1672 1673 if (isset($_SERVER['HTTP_X_GEO_ASN'])) { 1674 $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; 1675 } 1676 else { 1677 $asn = 0; 1678 } 1679 1680 if (!$asn) { 1681 return true; 1682 } 1683 1684 $val = in_array($asn, $whitelist); 1685 1686 return $val; 1687 } 1688 1689 function spam_filter_is_bad_actor() { 1690 static $cache = null; 1691 1692 if ($cache !== null) { 1693 return $cache; 1694 } 1695 /* 1696 if (isset($_SERVER['HTTP_X_HTTP_VERSION'])) { 1697 if (strpos($_SERVER['HTTP_X_HTTP_VERSION'], 'HTTP/1') === 0) { 1698 $cache = true; 1699 return true; 1700 } 1701 } 1702 */ 1703 $no_lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) === false; 1704 $no_accept = isset($_SERVER['HTTP_ACCEPT']) === false; 1705 1706 if ($no_lang && $no_accept) { 1707 $cache = true; 1708 return true; 1709 } 1710 1711 if ($no_lang && strpos($_SERVER['HTTP_USER_AGENT'], '; wv)') !== false) { 1712 $cache = true; 1713 return true; 1714 } 1715 1716 if ($no_accept && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') === false) { 1717 $cache = true; 1718 return true; 1719 } 1720 1721 if ($no_lang && isset($_SERVER['HTTP_REFERER'])) { 1722 $ref = $_SERVER['HTTP_REFERER']; 1723 1724 if (strpos($ref, 'sys.4chan.org') !== false || strpos($ref, '/thread/') !== false) { 1725 $cache = true; 1726 return true; 1727 } 1728 } 1729 1730 $cache = false; 1731 return false; 1732 } 1733 1734 function spam_filter_get_threat_score($country = null, $is_op = false, $multipart = true/*, &$log = []*/) { 1735 $increase = []; 1736 $more = []; 1737 1738 $domain = DEFAULT_BURICHAN ? '4channel' : '4chan'; 1739 1740 if (isset($_SERVER['HTTP_USER_AGENT'])) { 1741 $ua = $_SERVER['HTTP_USER_AGENT']; 1742 } 1743 else { 1744 $ua = ''; 1745 } 1746 1747 if (isset($_SERVER['HTTP_CONTENT_TYPE'])) { 1748 $content_type = $_SERVER['HTTP_CONTENT_TYPE']; 1749 } 1750 else { 1751 $content_type = ''; 1752 } 1753 1754 if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { 1755 $accept_lang = $_SERVER['HTTP_ACCEPT_LANGUAGE']; 1756 } 1757 else { 1758 $accept_lang = ''; 1759 } 1760 1761 if (isset($_SERVER['HTTP_ACCEPT'])) { 1762 $accept_header = $_SERVER['HTTP_ACCEPT']; 1763 } 1764 else { 1765 $accept_header = ''; 1766 } 1767 1768 if (isset($_SERVER['HTTP_REFERER'])) { 1769 $referer_header = $_SERVER['HTTP_REFERER']; 1770 } 1771 else { 1772 $referer_header = ''; 1773 } 1774 1775 $header_keys = array_keys($_SERVER); 1776 1777 $ua_is_webkit = false; 1778 1779 $check_for_sec_headers = false; 1780 1781 $is_mobile_ua = preg_match('/Android|Mobile/', $ua) || $_SERVER['HTTP_SEC_CH_UA_MOBILE'] === '?1'; 1782 1783 $is_brave = false; 1784 1785 if (isset($_SERVER['HTTP_SEC_CH_UA']) && strpos($_SERVER['HTTP_SEC_CH_UA'], 'Brave') !== false) { 1786 if (isset($_SERVER['HTTP_SEC_GPC'])) { 1787 $is_brave = true; 1788 } 1789 } 1790 1791 // Mobile app (webviews, etc) 1792 $is_webview = strpos($ua, '; wv') !== false; 1793 $is_mobile_app = !$accept_header && !$accept_lang && ($is_webview || strpos($referer_header, '/thread/') !== false); 1794 1795 if (!$is_mobile_app && !$accept_header && $accept_lang && strpos($referer_header, 'sys.4chan.org') !== false) { 1796 $is_mobile_app = true; 1797 } 1798 1799 if (!$is_mobile_app && strpos($ua, 'Mozilla/') === false && preg_match('/Android|Dalvik|iOS|iPhone/', $ua)) { 1800 $is_mobile_app = true; 1801 } 1802 1803 if (!$is_mobile_app && isset($_SERVER['HTTP_X_REQUESTED_WITH']) && preg_match('/floens|adamantcheese|clover/', $_SERVER['HTTP_X_REQUESTED_WITH'])) { 1804 $is_mobile_app = true; 1805 } 1806 1807 if (!$is_mobile_app && preg_match('/boundary=[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}$/', $content_type)) { 1808 if (strpos($ua, 'Android') !== false) { 1809 $is_mobile_app = true; 1810 } 1811 else if (strpos($ua, 'Firefox/') !== false && !$accept_lang) { 1812 $is_mobile_app = true; 1813 } 1814 } 1815 1816 // No UA 1817 if (!$ua) { 1818 $increase[] = 0.25; 1819 //$log[] = 'NO_UA'; 1820 } 1821 // Firefox 1822 else if ((strpos($ua, 'Firefox/') !== false || strpos($ua, 'FxiOS/') !== false) && strpos($ua, 'WebKit') === false) { 1823 // Suspicious Content-Type 1824 if ($multipart && !$is_mobile_app && !preg_match('/=-+([0-9]+|geckoformboundary[a-f0-9]+)$/i', $content_type)) { 1825 $increase[] = 0.25; 1826 //$log[] = 'BAD_CT_FF'; 1827 } 1828 1829 // Suspicious language 1830 if (!$accept_lang) { 1831 if (!$is_mobile_app) { 1832 $increase[] = 0.02; 1833 $more[] = 0.1; 1834 //$log[] = 'NO_LANG'; 1835 } 1836 } 1837 else if (preg_match('/[a-z]-[a-z]/', $accept_lang)) { 1838 $increase[] = 0.1; 1839 //$log[] = 'LC_LANG'; 1840 } 1841 1842 if (isset($_SERVER['HTTP_PRAGMA']) && !isset($_SERVER['HTTP_CACHE_CONTROL'])) { 1843 $increase[] = 0.35; 1844 $more[] = 0.1; 1845 //$log[] = 'FF_PRAGMA'; 1846 } 1847 1848 // Wrong Accept header 1849 if (strpos($accept_header, 'application/signed-exchange') !== false) { 1850 $increase[] = 0.15; 1851 //$log[] = 'FF_SIGEX'; 1852 } 1853 1854 // Old and spoofed versions 1855 if ($accept_header && preg_match('/(?:Firefox)\/([0-9]+)[^0-9]/', $ua, $m) && strpos($ua, 'PaleMoon') === false) { 1856 $v = (int)$m[1]; 1857 1858 if ($v < 52) { 1859 $increase[] = 0.2; 1860 //$log[] = 'OLD_FF'; 1861 } 1862 else if ($v < 60) { 1863 $increase[] = 0.1; 1864 //$log[] = 'OLD_FF'; 1865 } 1866 else if ($v < 78) { 1867 $increase[] = 0.01; 1868 //$log[] = 'OLD_FF'; 1869 } 1870 else if ($v > 500) { 1871 $increase[] = 0.5; 1872 //$log[] = 'FUTURE_FF'; 1873 } 1874 1875 if ($v > 110) { 1876 $check_for_sec_headers = true; 1877 } 1878 } 1879 } 1880 // Webkit 1881 else if (strpos($ua, 'WebKit') !== false) { 1882 $ua_is_webkit = true; 1883 $ua_is_chrome = strpos($ua, 'Chrome') !== false; 1884 1885 // Suspicious Content-Type 1886 if ($multipart && !$is_mobile_app) { 1887 if (!strpos($content_type, 'WebKit')) { 1888 $increase[] = 0.25; 1889 //$log[] = 'BAD_CT_WK'; 1890 } 1891 else if (strpos($content_type, '-') === false) { 1892 $increase[] = 0.50; 1893 //$log[] = 'BAD_CT_DASH'; 1894 } 1895 } 1896 1897 // Suspicious language 1898 if (!$accept_lang) { 1899 if (!$is_mobile_app) { 1900 $increase[] = 0.02; 1901 $more[] = 0.1; 1902 //$log[] = 'NO_LANG'; 1903 } 1904 } 1905 else if ($ua_is_chrome && strpos($ua, 'Android') === false && preg_match('/[a-z]-[a-z]/', $accept_lang)) { 1906 $increase[] = 0.1; 1907 //$log[] = 'LC_LANG'; 1908 } 1909 1910 // Old and spoofed versions 1911 if (preg_match('/(?:Chrome)\/([0-9]+)[^0-9]/', $ua, $m)) { 1912 $v = (int)$m[1]; 1913 1914 if ($v < 60) { 1915 $increase[] = 0.2; 1916 //$log[] = 'OLD_WK'; 1917 } 1918 else if ($v < 70) { 1919 $increase[] = 0.1; 1920 //$log[] = 'OLD_WK'; 1921 } 1922 else if ($v < 80) { 1923 $increase[] = 0.05; 1924 //$log[] = 'OLD_WK'; 1925 } 1926 else if ($v > 500) { 1927 $increase[] = 0.5; 1928 //$log[] = 'FUTURE_WK'; 1929 } 1930 1931 if ($v > 110) { 1932 $check_for_sec_headers = true; 1933 } 1934 } 1935 1936 if (preg_match('/(?:Safari)\/([0-9]+)/', $ua, $m)) { 1937 $v = (int)$m[1]; 1938 1939 if ($v < 530) { 1940 $increase[] = 0.5; 1941 //$log[] = 'OLD_SAFARI'; 1942 } 1943 } 1944 1945 // iPhone UA too short 1946 if (strpos($ua, 'iPhone') !== false && strpos($ua, 'Mobile') === false) { 1947 $increase[] = 0.06; 1948 //$log[] = 'SHORT_IPHONE'; 1949 } 1950 } 1951 // Other 1952 else { 1953 if (!$is_mobile_app && $multipart && preg_match('/boundary=[a-zA-Z0-9]+$/', $content_type)) { 1954 $increase[] = 0.5; 1955 //$log[] = 'STRANGE_CT'; 1956 } 1957 1958 if (preg_match('/Netscape\/|Opera\b|Camino\/|Trident\/|Presto\/|compatible; MSIE /', $ua)) { 1959 $increase[] = 0.75; 1960 //$log[] = 'OLD_UA'; 1961 } 1962 else if (!$is_mobile_app && strpos($ua, 'Mozilla/') === false) { 1963 $increase[] = 0.15; 1964 $more[] = 0.1; 1965 //$log[] = 'STRANGE_UA'; 1966 } 1967 1968 if (!$is_mobile_app && !$accept_lang) { 1969 $more[] = 0.25; 1970 //$log[] = 'NO_LANG'; 1971 } 1972 1973 // UA too short 1974 if (!$is_mobile_app && (strlen($ua) < 25 || strpos($ua, ' ') === false)) { 1975 $increase[] = 0.25; 1976 //$log[] = 'UA_SPOOF'; 1977 } 1978 } 1979 1980 // Suspicious Content-Type 1981 if ($multipart) { 1982 if (strpos($content_type, 'WebKit') && !$ua_is_webkit) { 1983 $increase[] = 0.25; 1984 //$log[] = 'BAD_UA_CT_WK'; 1985 } 1986 } 1987 1988 // Sec-Fetch headers should be together 1989 // Some iPhones have those separated 1990 if (!$is_brave && !$is_webview && strpos($ua, 'Chrome') !== false) { 1991 $_sf_start = false; 1992 $_sf_end = false; 1993 1994 foreach ($_SERVER as $_hdr => $_value) { 1995 if (strpos($_hdr, 'HTTP_SEC_FETCH_') === 0) { 1996 if ($_sf_start && $_sf_end) { 1997 $increase[] = 0.25; 1998 //$log[] = 'SPARSE_SEC_FETCH'; 1999 break; 2000 } 2001 2002 $_sf_start = true; 2003 } 2004 else if ($_sf_start) { 2005 $_sf_end = true; 2006 } 2007 } 2008 } 2009 2010 // HTTP_SEC_FETCH_USER should always be ?1 2011 if (isset($_SERVER['HTTP_SEC_FETCH_USER']) && $_SERVER['HTTP_SEC_FETCH_USER'] !== '?1') { 2012 $increase[] = 0.15; 2013 //$log[] = 'BAD_SEC_FU'; 2014 } 2015 2016 // Unusual Accept header 2017 if ($accept_header) { 2018 if (strpos($accept_header, 'text/plain') !== false) { 2019 $increase[] = 0.05; 2020 //$log[] = 'ACCEPT_TP'; 2021 } 2022 } 2023 2024 // Referer is set but is empty 2025 if (isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], '4chan.org') === false) { 2026 $increase[] = 0.1; 2027 //$log[] = 'BAD_REFERER'; 2028 } 2029 2030 // Platform mismatch: client hints vs user agent 2031 if (isset($_SERVER['HTTP_SEC_CH_UA_PLATFORM']) && $ua) { 2032 $_ch_platform = $_SERVER['HTTP_SEC_CH_UA_PLATFORM']; 2033 2034 if (strpos($ua, 'Windows') !== false) { 2035 if (strpos($_ch_platform, 'Windows') === false) { 2036 $increase[] = 0.5; 2037 //$log[] = 'CH_BAD_PLATFORM'; 2038 } 2039 } 2040 else if (strpos($ua, 'Mac OS') !== false) { 2041 if (strpos($_ch_platform, 'macOS') === false) { 2042 $increase[] = 0.5; 2043 //$log[] = 'CH_BAD_PLATFORM'; 2044 } 2045 } 2046 else if (strpos($ua, 'Linux') !== false) { 2047 if (preg_match('/Linux|Android|BSD/', $_ch_platform) === false) { 2048 $increase[] = 0.5; 2049 //$log[] = 'CH_BAD_PLATFORM'; 2050 } 2051 } 2052 } 2053 2054 if (isset($_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS']) && $accept_header === '*/*') { 2055 $increase[] = 0.2; 2056 $more[] = 0.1; 2057 //$log[] = 'ACCEPT_UIR'; 2058 } 2059 2060 if (!isset($_SERVER['HTTP_PRAGMA']) && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') === false) { 2061 $increase[] = 0.09; 2062 //$log[] = 'BAD_IPHONE_WV'; 2063 } 2064 2065 // Suspicious OS 2066 if (strpos($ua, 'Windows NT ') !== false) { 2067 if (preg_match('/Windows NT ([0-9]+)/', $ua, $m) && strpos($ua, 'Mypal/') === false) { 2068 $v = (int)$m[1]; 2069 2070 if ($v < 6) { 2071 if (strpos($ua, 'Goanna') === false) { 2072 $increase[] = 0.25; 2073 } 2074 else { 2075 $increase[] = 0.03; 2076 } 2077 //$log[] = 'OLD_WIN'; 2078 } 2079 else if ($v < 10) { 2080 $increase[] = 0.03; 2081 //$log[] = 'OLD_WIN'; 2082 } 2083 else if ($v > 10) { 2084 $increase[] = 0.5; 2085 //$log[] = 'FUTURE_WIN'; 2086 } 2087 } 2088 } 2089 else if (strpos($ua, 'Mac OS X ') !== false) { 2090 if (preg_match('/Mac OS X ([0-9]+)_([0-9]+)/', $ua, $m)) { 2091 $v_maj = (int)$m[1]; 2092 $v_min = (int)$m[2]; 2093 2094 if ($v_maj < 10) { 2095 $increase[] = 0.5; 2096 //$log[] = 'OLD_OSX'; 2097 } 2098 else if ($v_maj == 10) { 2099 if ($v_min < 7) { 2100 $increase[] = 0.25; 2101 //$log[] = 'OLD_OSX'; 2102 } 2103 else if ($v_min < 12) { 2104 $increase[] = 0.05; 2105 //$log[] = 'OLD_OSX'; 2106 } 2107 } 2108 else if ($v_maj > 10 && strpos($ua, 'Safari') !== false) { 2109 $increase[] = 0.30; 2110 //$log[] = 'FUTURE_OSX'; 2111 } 2112 } 2113 } 2114 else if (strpos($ua, 'Android') !== false) { 2115 if (preg_match('/Android ([0-9]+)/', $ua, $m)) { 2116 $v = (int)$m[1]; 2117 2118 if ($v < 4) { 2119 $increase[] = 0.25; 2120 //$log[] = 'OLD_DROID'; 2121 } 2122 else if ($v < 8) { 2123 $increase[] = 0.05; 2124 //$log[] = 'OLD_DROID'; 2125 } 2126 else if ($v > 20) { 2127 $increase[] = 0.5; 2128 //$log[] = 'FUTURE_DROID'; 2129 } 2130 } 2131 2132 if (strpos($ua, 'Win64;') !== false) { 2133 $increase[] = 0.25; 2134 //$log[] = 'OS_SOUP'; 2135 } 2136 } 2137 2138 // Spoofed OS 2139 if (preg_match('/Mozilla|Firefox|Chrome/', $ua) && !preg_match('/Windows NT|Android|Linux|Mac|iOS|X11;|BSD|Nintendo|PlayStation|Steam/', $ua)) { 2140 $increase[] = 0.20; 2141 //$log[] = 'NO_OS'; 2142 } 2143 2144 // Non-browser user agents 2145 if (preg_match('/headless|node-fetch|python-|java\/|jakarta|-perl|http-?client|-resty-|awesomium\//i', $ua)) { 2146 $increase[] = 1.0; 2147 //$log[] = 'NOT_BROWSER'; 2148 } 2149 2150 // Wrong content type 2151 if ($multipart) { 2152 // Posting 2153 if ($_SERVER['HTTP_CONTENT_TYPE'] === 'application/x-www-form-urlencoded') { 2154 $increase[] = 0.75; 2155 //$log[] = 'BAD_CT_MP'; 2156 } 2157 } 2158 else if (!$is_mobile_app) { 2159 // Reporting 2160 if ($_SERVER['HTTP_CONTENT_TYPE'] !== 'application/x-www-form-urlencoded') { 2161 $increase[] = 0.75; 2162 //$log[] = 'BAD_CT_NMP'; 2163 } 2164 } 2165 2166 // Unusual headers 2167 if (isset($_SERVER['HTTP_VARY'])) { 2168 $increase[] = 0.2; 2169 //$log[] = 'VARY_HDR'; 2170 } 2171 2172 if (isset($_SERVER['HTTP_PATH']) || isset($_SERVER['HTTP_SAME_ORIGIN']) || isset($_SERVER['HTTP_REFERRER_POLICY'])) { 2173 $increase[] = 0.8; 2174 //$log[] = 'USELESS_HDR'; 2175 } 2176 2177 if (!$is_mobile_app && !$is_webview) { 2178 if (isset($_SERVER['HTTP_SEC_FETCH_MODE']) && $_SERVER['HTTP_SEC_FETCH_MODE'] === 'navigate') { 2179 // Only threads should be posted using the default form 2180 if (!$is_op && !$is_mobile_ua) { 2181 $increase[] = 0.02; 2182 $more[] = 0.10; 2183 //$log[] = 'BAD_OP_SFM'; 2184 } 2185 2186 // Model hints are never sent when using the default form 2187 if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && !isset($_SERVER['HTTP_SEC_CH_UA_BITNESS'])) { 2188 $increase[] = 0.1; 2189 $more[] = 0.20; 2190 //$log[] = 'MODEL_NAV'; 2191 } 2192 } 2193 } 2194 2195 // iPhone fetch site none 2196 if (strpos($ua, 'like Mac OS') && isset($_SERVER['HTTP_SEC_FETCH_SITE']) && $_SERVER['HTTP_SEC_FETCH_SITE'] === 'none') { 2197 $increase[] = 0.08; 2198 $more[] = 0.10; 2199 //$log[] = 'IOS_FSN'; 2200 } 2201 2202 // No cookies 2203 if (!$is_mobile_app) { 2204 if (!isset($_SERVER['HTTP_COOKIE']) || !$_SERVER['HTTP_COOKIE']) { 2205 $increase[] = 0.05; 2206 $more[] = 0.25; 2207 //$log[] = 'NO_COOKIE'; 2208 } 2209 else if (count($_COOKIE) === 1 && isset($_COOKIE['cf_clearance'])) { 2210 $increase[] = 0.05; 2211 $more[] = 0.25; 2212 //$log[] = 'NO_COOKIE'; 2213 } 2214 } 2215 2216 // Timezones and Time 2217 if (isset($_COOKIE['_tcs'])) { 2218 list($_time, $_tz, $_time_s, $_tcs_v) = explode('.', $_COOKIE['_tcs']); 2219 2220 if (!$_tcs_v) { 2221 $increase[] = 0.09; 2222 //$log[] = 'BAD_TCS'; 2223 } 2224 else { 2225 if (!$is_webview && strpos($ua, 'Chrome/') !== false && $_tcs_v != 33) { 2226 $increase[] = 0.09; 2227 //$log[] = 'BAD_TCS_CR'; 2228 } 2229 } 2230 2231 if ($_time_s && isset($_POST['t-challenge']) && $_POST['t-challenge'] !== 'noop' && $_POST['t-response']) { 2232 $_d = $_SERVER['REQUEST_TIME'] - $_time_s; 2233 2234 if ($_d > 0 && $_d < 2) { 2235 $increase[] = 1.0; 2236 //$log[] = 'FAST_TCS'; 2237 } 2238 } 2239 2240 if (isset($_SERVER['HTTP_X_TIMEZONE'])) { 2241 $_tz0 = explode('/', $_tz, 2)[0]; 2242 if ($_tz0) { 2243 if ($_tz0 === 'UTC') { 2244 $increase[] = 0.02; 2245 $more[] = 0.02; 2246 //$log[] = 'UTC_TZ'; 2247 } 2248 else if ($_tz0 === 'Etc') { 2249 $increase[] = 0.01; 2250 $more[] = 0.01; 2251 //$log[] = 'ETC_TZ'; 2252 } 2253 else if (strpos($_SERVER['HTTP_X_TIMEZONE'], $_tz0) !== 0) { 2254 if (strpos($_tz0, 'Atlantic') === false || strpos($_SERVER['HTTP_X_TIMEZONE'], 'Europe') === false) { 2255 $increase[] = 0.03; 2256 $more[] = 0.03; 2257 //$log[] = 'BAD_TZ'; 2258 } 2259 } 2260 } 2261 } 2262 } 2263 2264 // No Accept 2265 if (!$accept_header && !$is_mobile_app) { 2266 $increase[] = 0.15; 2267 //$log[] = 'NO_ACCEPT'; 2268 } 2269 2270 // No SEC 2271 if ($check_for_sec_headers && !$is_mobile_app) { 2272 if (!isset($_SERVER['HTTP_SEC_FETCH_DEST']) || !isset($_SERVER['HTTP_SEC_FETCH_MODE']) || !isset($_SERVER['HTTP_SEC_FETCH_SITE'])) { 2273 $increase[] = 0.15; 2274 //$log[] = 'NO_SEC'; 2275 } 2276 } 2277 2278 // HTTP 1.1 2279 if (isset($_SERVER['HTTP_X_HTTP_VERSION']) && $_SERVER['HTTP_X_HTTP_VERSION'] === 'HTTP/1.1') { 2280 $more[] = 0.1; 2281 //$log[] = 'HTTP1'; 2282 } 2283 2284 // Spoofed device model 2285 if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && isset($_SERVER['HTTP_SEC_CH_UA_PLATFORM'])) { 2286 $_k1 = (int)array_search('HTTP_SEC_CH_UA_MODEL', $header_keys); 2287 $_k2 = (int)array_search('HTTP_SEC_CH_UA_PLATFORM', $header_keys); 2288 2289 if (abs($_k1 - $_k2) > 5) { 2290 $increase[] = 0.05; 2291 $more[] = 0.01; 2292 //$log[] = 'FAR_MODEL'; 2293 } 2294 } 2295 2296 if ($country) { 2297 // Language is set but doesn't match the country 2298 if ($accept_lang && strpos($accept_lang, 'en') !== 0) { 2299 $lang_regex = get_lang_regex_from_country($country); 2300 2301 if ($lang_regex && !preg_match($lang_regex, $accept_lang)) { 2302 $more[] = 0.025; 2303 $increase[] = 0.025; 2304 //$log[] = 'LANG_MISMATCH'; 2305 } 2306 2307 // No quality in lang 2308 if (!preg_match('/iPhone|iPad/', $ua)) { 2309 if ($accept_lang && strpos($accept_lang, ';') === false) { 2310 $more[] = 0.025; 2311 $increase[] = 0.025; 2312 //$log[] = 'LANG_NOQ'; 2313 } 2314 } 2315 } 2316 2317 // Highly suspicious countries 2318 $countries_0 = array( 2319 'AD','AE','AF','AG','AI','AL','AM','AN','AO','AQ','AS','AW','AX','AZ', 2320 'BB','BD','BF','BH','BI','BJ','BL','BM','BN','BO','BQ','BS','BT','BV','BW','BZ', 2321 'CC','CD','CF','CG','CI','CK','CM','CN','CR','CU','CV','CW','CX', 2322 'DJ','DM','DO','DZ', 2323 'EC','EG','EH','ER','ET', 2324 'FJ','FK','FM','FO', 2325 'GA','GD','GF','GG','GH','GI','GM','GN','GP','GQ','GS','GT','GU','GW','GY', 2326 'HK','HM','HN','HT', 2327 'IM','IO','IQ','IR','JE','JM','JO','KE','KG','KH','KI','KM','KN','KP','KW','KY','KZ', 2328 'LA','LB','LC','LK','LR','LS','LY', 2329 'MA','MD','MF','MG','MH','ML','MM','MN','MO','MP','MQ','MR','MS','MU','MV','MW','MZ', 2330 'NA','NC','NE','NF','NG','NI','NP','NR','NU', 2331 'OM','PA','PF','PG','PK','PM','PN','PS','PW','PY','QA','RE','RW', 2332 'SA','SB','SC','SD','SH','SJ','SL','SM','SN','SO','SR','SS','ST','SV','SX','SY','SZ', 2333 'TC','TD','TF','TG','TJ','TK','TM','TN','TO','TP','TR','TT','TV','TZ', 2334 'UG','UM','UZ','VA','VC','VG','VI','VU','WF','WS','YE','YT','YU','ZM','ZW','XX' 2335 ); 2336 2337 // Less suspicious countries 2338 $countries_1 = array( 2339 'BR','VE','AR','CL','UY','CO','PE','MX', 2340 'UA','BA','RU','MC','MK','CY','MT','ME','KR','JP','TH','VN','ID' 2341 ); 2342 2343 if (in_array($country, $countries_0)) { 2344 $more[] = 0.30; 2345 //$log[] = 'SUSP_COUNTRY_0'; 2346 } 2347 else if (in_array($country, $countries_1)) { 2348 $more[] = 0.10; 2349 //$log[] = 'SUSP_COUNTRY_1'; 2350 } 2351 } 2352 2353 $score = 0.0; 2354 2355 if (!empty($increase)) { 2356 $score += array_sum($increase); 2357 } 2358 2359 if ($score > 0.0 && !empty($more)) { 2360 foreach ($more as $r) { 2361 $score *= (1.0 + $r); 2362 } 2363 } 2364 2365 return round($score, 2); 2366 } 2367 2368 /** 2369 * Necrobumping prevention checks 2370 */ 2371 function spam_filter_can_bump_thread($thread_root) { 2372 $userpwd = UserPwd::getSession(); 2373 2374 if (!$userpwd || !$thread_root) { 2375 return true; 2376 } 2377 2378 if ($userpwd->maskLifetime() >= 21600) { // 6 hours 2379 return true; 2380 } 2381 2382 $thres = (int)(PAGE_MAX * DEF_PAGES * 0.85); 2383 2384 if ($thres <= 0) { 2385 return true; 2386 } 2387 2388 $sql = "SELECT COUNT(*) FROM `%s` WHERE resto = 0 AND archived = 0 AND root > '%s'"; 2389 2390 $res = mysql_board_call($sql, BOARD_DIR, $thread_root); 2391 2392 if (!$res) { 2393 return true; 2394 } 2395 2396 $pos = (int)mysql_fetch_row($res)[0]; 2397 2398 if ($pos < $thres) { 2399 return true; 2400 } 2401 2402 // Check the IP if cookies are blocked 2403 if (spam_filter_is_ip_known(ip2long($_SERVER['REMOTE_ADDR']), BOARD_DIR, 0, 720)) { 2404 return true; 2405 } 2406 2407 return false; 2408 } 2409 2410 /** 2411 * Returns true if the uploaded file had multiple previous bans for it 2412 * and should be blocked 2413 */ 2414 function check_for_banned_upload($md5) { 2415 if (!$md5 || BOARD_DIR === 'b') { 2416 return false; 2417 } 2418 2419 // 6 : Global 5 - NWS on Worksafe Board 2420 // 226 : Global 3 - Loli/shota pornography 2421 $template_clause = '226'; 2422 2423 if (DEFAULT_BURICHAN) { 2424 $template_clause .= ',6'; 2425 } 2426 2427 $sql = <<<SQL 2428 SELECT COUNT(*) as cnt FROM banned_users WHERE md5 = '%s' 2429 AND template_id IN($template_clause) LIMIT 1 2430 SQL; 2431 2432 $res = mysql_global_call($sql, $md5); 2433 2434 if (!$res) { 2435 return false; 2436 } 2437 2438 $count = (int)mysql_fetch_row($res)[0]; 2439 2440 if ($count >= 3) { 2441 return true; 2442 } 2443 2444 return false; 2445 } 2446 2447 function spam_filter_is_likely_automated($memcached = null, $threshold = 29) { 2448 if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { 2449 return false; 2450 } 2451 2452 $ua = $_SERVER['HTTP_USER_AGENT']; 2453 2454 // Skip Android Webviews 2455 if (strpos($ua, '; wv)') !== false) { 2456 return false; 2457 } 2458 2459 // Skip iPhone Webviews 2460 if (preg_match('/iPhone|iPad/', $ua) && !preg_match('/Mobile|Safari/', $ua)) { 2461 return false; 2462 } 2463 2464 $score = (int)$_SERVER['HTTP_X_BOT_SCORE']; 2465 2466 if ($score > 0 && $score <= $threshold) { 2467 return true; 2468 } 2469 2470 if ($memcached) { 2471 $key = 'bmbot' . $_SERVER['REMOTE_ADDR']; 2472 2473 if ($memcached->get($key)) { 2474 return true; 2475 } 2476 } 2477 2478 return false; 2479 } 2480 2481 function spam_filter_is_pwd_blocked($pwd, $type, $hours = 24) { 2482 if (!$pwd || !$type || $hours <= 0 || $hours > 720) { 2483 return false; 2484 } 2485 2486 $hours = (int)$hours; 2487 2488 $sql =<<<SQL 2489 SELECT 1 FROM event_log 2490 WHERE `type` = '%s' AND pwd = '%s' 2491 AND created_on > DATE_SUB(NOW(), INTERVAL $hours HOUR) 2492 LIMIT 1 2493 SQL; 2494 2495 $res = mysql_global_call($sql, $type, $pwd); 2496 2497 if (!$res) { 2498 return false; 2499 } 2500 2501 return mysql_num_rows($res) === 1; 2502 } 2503 2504 function spam_filter_has_country_changed($pwd) { 2505 if (!$pwd) { 2506 return false; 2507 } 2508 2509 $sql =<<<SQL 2510 SELECT 1 FROM event_log 2511 WHERE `type` = 'country_changed' AND pwd = '%s' 2512 AND created_on > DATE_SUB(NOW(), INTERVAL 24 HOUR) 2513 LIMIT 1 2514 SQL; 2515 2516 $res = mysql_global_call($sql, $pwd); 2517 2518 if (!$res) { 2519 return false; 2520 } 2521 2522 return mysql_num_rows($res) === 1; 2523 } 2524 2525 // Logs posts made by new users. 2526 // Returns 1 if the posting rate is above limits. 2527 function spam_filter_is_post_flood($ip, $userpwd, $board, $thread_id, $phash) { 2528 $user_is_known = $userpwd && $userpwd->isUserKnownOrVerified(60); 2529 2530 if ($user_is_known) { 2531 return 0; 2532 } 2533 2534 $thread_id = (int)$thread_id; 2535 2536 // Per thread reply flood 2537 if ($thread_id) { 2538 $interval_minutes = (int)ANTIFLOOD_INTERVAL_REPLY; 2539 $threshold = (int)ANTIFLOOD_THRES_REPLY; 2540 } 2541 // OP flood 2542 else { 2543 $interval_minutes = (int)ANTIFLOOD_INTERVAL_OP; 2544 $threshold = (int)ANTIFLOOD_THRES_OP; 2545 } 2546 2547 if (!$interval_minutes || !$threshold) { 2548 return 0; 2549 } 2550 2551 $long_ip = ip2long($ip); 2552 2553 if (!$long_ip) { 2554 return 0; 2555 } 2556 2557 $tbl = 'flood_log'; 2558 2559 // Prune old entries 2560 $sql = "DELETE FROM `$tbl` WHERE created_on < DATE_SUB(NOW(), INTERVAL 24 HOUR)"; 2561 mysql_global_call($sql); 2562 2563 // Count flood entries 2564 $ret_val = 0; 2565 2566 $sql = <<<SQL 2567 SELECT COUNT(*) FROM `$tbl` 2568 WHERE ip != '%s' 2569 AND created_on >= DATE_SUB(NOW(), INTERVAL $interval_minutes MINUTE) 2570 AND board = '%s' 2571 AND thread_id = $thread_id 2572 SQL; 2573 2574 $res = mysql_global_call($sql, $ip, $board); 2575 2576 if ($res) { 2577 $count = (int)mysql_fetch_row($res)[0]; 2578 2579 if ($count >= $threshold) { 2580 $ret_val = 1; 2581 } 2582 } 2583 2584 // Insert new entry 2585 $ua_sig = spam_filter_get_browser_id(); 2586 $req_sig = spam_filter_get_req_sig(); 2587 2588 $sql = <<<SQL 2589 INSERT INTO `$tbl` (board, thread_id, ip, phash, ua_sig, req_sig) 2590 VALUES ('%s', $thread_id, '%s', '%s', '%s', '%s') 2591 SQL; 2592 2593 $res = mysql_global_call($sql, $board, $ip, $phash, $ua_sig, $req_sig); 2594 2595 return $ret_val; 2596 } 2597 2598 function spam_filter_post_trip($name, $trip, $dest) { 2599 $normalized_name = normalize_text($name); 2600 if(stripos($normalized_name, 'moot') !== FALSE && ($trip == 'Ep8pui8Vw2' || stripos($normalized_name, 'ep8pui8vw2') !== FALSE) && !valid()) 2601 { 2602 error(S_FAILEDUPLOAD, $dest); 2603 } 2604 2605 if(preg_match("/[l|i|1][o|0][l|i|1]{2}c[o|0][n|m]/i", $trip)) 2606 { 2607 error(S_BANNEDTEXT, $dest); 2608 } 2609 } 2610 2611 // check a comment for fileshare content and put it in the rapidsearch queue 2612 function rapidsearch_check($board, $no, $body) { 2613 if( strlen( $body ) < 25 ) return; 2614 2615 $body = strtolower($body); 2616 $body = strip_tags($body); 2617 2618 $matches = array( 2619 'rapidshare.de/', 2620 'rapidshare.com/', 2621 'rapidshit.com/files/', 2622 'savefile.com/files', 2623 'megaupload.com/', 2624 'sharebigfile.com/download.php', 2625 'sendspace.com/file', 2626 'turboupload.com/d', 2627 'bigupload.com', 2628 'filefactory.com', 2629 'bandongo', 2630 'easy-sharing', 2631 'easy-share', 2632 'sexuploader', 2633 'rapidsharing.com', 2634 'uploadgalaxy', 2635 'up-file.com', 2636 'yousendit', 2637 'gigeshare', 2638 'gigasize', 2639 'depositfiles', 2640 'megarotic.com', 2641 'filewind.com', 2642 'mediafire.com', 2643 'xtremeuploader.com', 2644 'fileden.com', 2645 'zenixstudios.com', 2646 'megashare.com', 2647 'zshare.net', 2648 'megashare.com', 2649 'massmirror' 2650 ); 2651 2652 // oh my god the code before this 2653 foreach( $matches as $match ) { 2654 if( strpos( $body, $match ) !== false ) { 2655 rapidsearch_insert($board, $no, $body); 2656 return; 2657 } 2658 } 2659 } 2660 2661 function trans_similar_to_ascii(&$str, $char) { 2662 // UTF-16 2663 $ord = mb_ord($char); 2664 2665 switch ($ord) 2666 { 2667 case 0x2070: 2668 case 0x2080: 2669 case 0xFF10: 2670 $str .= chr(0x30); /* 0 */ 2671 break; 2672 case 0x00B9: 2673 case 0x2081: 2674 case 0xFF11: 2675 $str .= chr(0x31); /* 1 */ 2676 break; 2677 case 0x00B2: 2678 case 0x2082: 2679 case 0xFF12: 2680 $str .= chr(0x32); 2681 break; 2682 case 0x00B3: 2683 case 0x2083: 2684 case 0xFF13: 2685 $str .= chr(0x33); 2686 break; 2687 case 0x2074: 2688 case 0x2084: 2689 case 0xFF14: 2690 $str .= chr(0x34); 2691 break; 2692 case 0x2075: 2693 case 0x2085: 2694 case 0xFF15: 2695 $str .= chr(0x35); 2696 break; 2697 case 0x2076: 2698 case 0x2086: 2699 case 0xFF16: 2700 $str .= chr(0x36); 2701 break; 2702 case 0x2077: 2703 case 0x2087: 2704 case 0xFF17: 2705 $str .= chr(0x37); 2706 break; 2707 case 0x2078: 2708 case 0x2088: 2709 case 0xFF18: 2710 $str .= chr(0x38); 2711 break; 2712 case 0x2079: 2713 case 0x2089: 2714 case 0xFF19: 2715 $str .= chr(0x39); 2716 break; 2717 case 0xFE57: 2718 case 0xFF01: /* Exclamation mark */ 2719 $str .= chr(0x21); 2720 break; 2721 case 0xFF02: /* Quotation mark */ 2722 $str .= chr(0x22); 2723 break; 2724 case 0xFE5F: 2725 case 0xFF03: /* Number sign */ 2726 $str .= chr(0x23); 2727 break; 2728 case 0xFE69: 2729 case 0xFF04: /* Dollar sign */ 2730 $str .= chr(0x24); 2731 break; 2732 case 0xFE6A: 2733 case 0xFF05: /* Percent sign */ 2734 $str .= chr(0x25); 2735 break; 2736 case 0xFE60: 2737 case 0xFF06: /* Ampersand */ 2738 $str .= chr(0x26); 2739 break; 2740 case 0xFF07: /* Apostrophe */ 2741 $str .= chr(0x27); 2742 break; 2743 case 0x207D: 2744 case 0x208D: 2745 case 0xFE59: 2746 case 0xFF08: /* Left parenthesis */ 2747 $str .= chr(0x28); 2748 break; 2749 case 0x207E: 2750 case 0x208E: 2751 case 0xFE5A: 2752 case 0xFF09: /* Right parenthesis */ 2753 $str .= chr(0x29); 2754 break; 2755 case 0xFE61: 2756 case 0xFF0A: /* Asterisk */ 2757 $str .= chr(0x2A); 2758 break; 2759 case 0x207A: 2760 case 0x208A: 2761 case 0xFE62: 2762 case 0xFF0B: /* Plus sign */ 2763 $str .= chr(0x2B); 2764 break; 2765 case 0xFE50: 2766 case 0xFF0C: /* Comma */ 2767 $str .= chr(0x2C); 2768 break; 2769 case 0x207B: 2770 case 0x208B: 2771 case 0xFE63: 2772 case 0xFF0D: /* Hyphen */ 2773 $str .= chr(0x2D); 2774 break; 2775 case 0xFE52: 2776 case 0xFF0E: /* Full stop */ 2777 $str .= chr(0x2E); 2778 break; 2779 case 0xFF0F: /* Solidus */ 2780 $str .= chr(0x2F); 2781 break; 2782 case 0xFE55: 2783 case 0xFF1A: /* Colon */ 2784 $str .= chr(0x3A); 2785 break; 2786 case 0xFE54: 2787 case 0xFF1B: /* Semicolon */ 2788 $str .= chr(0x3B); 2789 break; 2790 case 0xFE64: 2791 case 0xFF1C: /* Less than sign */ 2792 $str .= chr(0x3C); 2793 break; 2794 case 0x207C: 2795 case 0x208C: 2796 case 0xFE66: 2797 case 0xFF1D: 2798 $str .= chr(0x3D); /* Equals sign */ 2799 break; 2800 case 0xFE65: 2801 case 0xFF1E: /* Greater than sign */ 2802 $str .= chr(0x3E); 2803 break; 2804 case 0xFE56: 2805 case 0xFF1F: /* Question mark */ 2806 $str .= chr(0x3F); 2807 break; 2808 case 0xFE6B: 2809 case 0xFF20: /* At sign */ 2810 $str .= chr(0x40); 2811 break; 2812 case 0x0410: 2813 case 0xFF21: 2814 case 0x1D400: 2815 case 0x1D434: 2816 case 0x1D468: 2817 case 0x1D49C: 2818 case 0x1D4D0: 2819 case 0x1D504: 2820 case 0x1D538: 2821 case 0x1D56C: 2822 case 0x1D5A0: 2823 case 0x1D5D4: 2824 case 0x1D63C: 2825 case 0x1D670: 2826 $str .= chr(0x41); /* A */ 2827 break; 2828 case 0x0042: /* B */ 2829 case 0x0412: 2830 case 0x212C: 2831 case 0xFF22: 2832 case 0x1D401: 2833 case 0x1D435: 2834 case 0x1D469: 2835 case 0x1D4D1: 2836 case 0x1D505: 2837 case 0x1D539: 2838 case 0x1D56D: 2839 case 0x1D5A1: 2840 case 0x1D5D5: 2841 case 0x1D609: 2842 case 0x1D63D: 2843 case 0x1D671: 2844 $str .= chr(0x42); 2845 break; 2846 case 0x0421: 2847 case 0x2102: 2848 case 0x212D: 2849 case 0xFF23: 2850 case 0x1D402: 2851 case 0x1D436: 2852 case 0x1D46A: 2853 case 0x1D49E: 2854 case 0x1D4D2: 2855 case 0x1D56E: 2856 case 0x1D5A2: 2857 case 0x1D5D6: 2858 case 0x1D60A: 2859 case 0x1D63E: 2860 case 0x1D672: 2861 $str .= chr(0x43); /* C */ 2862 break; 2863 case 0x2145: 2864 case 0xFF24: 2865 case 0x1D403: 2866 case 0x1D437: 2867 case 0x1D46B: 2868 case 0x1D49F: 2869 case 0x1D4D3: 2870 case 0x1D507: 2871 case 0x1D53B: 2872 case 0x1D56F: 2873 case 0x1D5A3: 2874 case 0x1D5D7: 2875 case 0x1D60B: 2876 case 0x1D63F: 2877 case 0x1D673: 2878 $str .= chr(0x44); /* D */ 2879 break; 2880 case 0x2130: /* E*/ 2881 case 0x0415: 2882 case 0xFF25: 2883 case 0x1D404: 2884 case 0x1D438: 2885 case 0x1D46C: 2886 case 0x1D4D4: 2887 case 0x1D508: 2888 case 0x1D53C: 2889 case 0x1D570: 2890 case 0x1D5A4: 2891 case 0x1D5D8: 2892 case 0x1D60C: 2893 case 0x1D640: 2894 case 0x1D674: 2895 $str .= chr(0x45); 2896 break; 2897 case 0x2131: 2898 case 0x213F: 2899 case 0xFF26: 2900 case 0x1D405: 2901 case 0x1D439: 2902 case 0x1D46D: 2903 case 0x1D4D5: 2904 case 0x1D509: 2905 case 0x1D53D: 2906 case 0x1D571: 2907 case 0x1D5A5: 2908 case 0x1D5D9: 2909 case 0x1D60D: 2910 case 0x1D641: 2911 case 0x1D675: 2912 $str .= chr(0x46); /* F */ 2913 break; 2914 case 0x0262: 2915 case 0xFF27: 2916 case 0x1D406: 2917 case 0x1D43A: 2918 case 0x1D46E: 2919 case 0x1D4A2: 2920 case 0x1D4D6: 2921 case 0x1D50A: 2922 case 0x1D53E: 2923 case 0x1D572: 2924 case 0x1D5A6: 2925 case 0x1D5DA: 2926 case 0x1D60E: 2927 case 0x1D642: 2928 case 0x1D676: 2929 $str .= chr(0x47); /* G */ 2930 break; 2931 case 0x029C: 2932 case 0x041D: 2933 case 0x210B: 2934 case 0x210C: 2935 case 0x210D: 2936 case 0xFF28: 2937 case 0x1D407: 2938 case 0x1D43B: 2939 case 0x1D46F: 2940 case 0x1D4D7: 2941 case 0x1D573: 2942 case 0x1D5A7: 2943 case 0x1D5DB: 2944 case 0x1D60F: 2945 case 0x1D643: 2946 case 0x1D677: 2947 $str .= chr(0x48); /* H */ 2948 break; 2949 case 0x0049: 2950 case 0x0406: 2951 case 0x2110: 2952 case 0x2111: 2953 case 0x2160: 2954 case 0xFF29: 2955 case 0x1D408: 2956 case 0x1D43C: 2957 case 0x1D470: 2958 case 0x1D4D8: 2959 case 0x1D540: 2960 case 0x1D574: 2961 case 0x1D5A8: 2962 case 0x1D5DC: 2963 case 0x1D610: 2964 case 0x1D644: 2965 case 0x1D678: 2966 $str .= chr(0x49); /* I */ 2967 break; 2968 case 0x026A: 2969 case 0x0408: 2970 case 0xFF2A: 2971 case 0x1D409: 2972 case 0x1D43D: 2973 case 0x1D471: 2974 case 0x1D4D9: 2975 case 0x1D541: 2976 case 0x1D575: 2977 case 0x1D5A9: 2978 case 0x1D5DD: 2979 case 0x1D611: 2980 case 0x1D645: 2981 case 0x1D679: 2982 $str .= chr(0x4A); /* J */ 2983 break; 2984 case 0xFF2B: 2985 case 0x1D40A: 2986 case 0x1D43E: 2987 case 0x1D472: 2988 case 0x1D4A5: 2989 case 0x1D4DA: 2990 case 0x1D50D: 2991 case 0x1D542: 2992 case 0x1D576: 2993 case 0x1D5AA: 2994 case 0x1D5DE: 2995 case 0x1D612: 2996 case 0x1D646: 2997 case 0x1D67A: 2998 $str .= chr(0x4B); /* K */ 2999 break; 3000 case 0xFF2C: 3001 case 0x1D40B: 3002 case 0x1D43F: 3003 case 0x1D473: 3004 case 0x1D4A6: 3005 case 0x1D4DB: 3006 case 0x1D50E: 3007 case 0x1D543: 3008 case 0x1D577: 3009 case 0x1D5AB: 3010 case 0x1D5DF: 3011 case 0x1D613: 3012 case 0x1D647: 3013 case 0x1D67B: 3014 $str .= chr(0x4C); /* L */ 3015 break; 3016 case 0x029F: 3017 case 0x041C: 3018 case 0xFF2D: 3019 case 0x2112: 3020 case 0x1D40C: 3021 case 0x1D440: 3022 case 0x1D474: 3023 case 0x1D4DC: 3024 case 0x1D50F: 3025 case 0x1D544: 3026 case 0x1D578: 3027 case 0x1D5AC: 3028 case 0x1D5E0: 3029 case 0x1D614: 3030 case 0x1D648: 3031 case 0x1D67C: 3032 $str .= chr(0x4D); /* M */ 3033 break; 3034 case 0x2133: 3035 case 0xFF2E: 3036 case 0x1D40D: 3037 case 0x1D441: 3038 case 0x1D475: 3039 case 0x1D4A9: 3040 case 0x1D4DD: 3041 case 0x1D511: 3042 case 0x1D579: 3043 case 0x1D5AD: 3044 case 0x1D5E1: 3045 case 0x1D615: 3046 case 0x1D649: 3047 case 0x1D67D: 3048 $str .= chr(0x4E); /* N */ 3049 break; 3050 case 0x004F: 3051 //case 0x00D8: 3052 case 0x041E: 3053 case 0x2205: 3054 case 0xFF2F: 3055 case 0x1D40E: 3056 case 0x1D442: 3057 case 0x1D476: 3058 case 0x1D4AA: 3059 case 0x1D4DE: 3060 case 0x1D512: 3061 case 0x1D546: 3062 case 0x1D57A: 3063 case 0x1D5AE: 3064 case 0x1D5E2: 3065 case 0x1D616: 3066 case 0x1D64A: 3067 case 0x1D67E: 3068 $str .= chr(0x4F); /* O */ 3069 break; 3070 case 0x0420: 3071 case 0x2118: 3072 case 0x2119: 3073 case 0xFF30: 3074 case 0x1D40F: 3075 case 0x1D443: 3076 case 0x1D477: 3077 case 0x1D4AB: 3078 case 0x1D4DF: 3079 case 0x1D513: 3080 case 0x1D57B: 3081 case 0x1D5AF: 3082 case 0x1D5E3: 3083 case 0x1D617: 3084 case 0x1D64B: 3085 case 0x1D67F: 3086 $str .= chr(0x50); /* P */ 3087 break; 3088 case 0x211A: 3089 case 0xFF31: 3090 case 0x1D410: 3091 case 0x1D444: 3092 case 0x1D478: 3093 case 0x1D4AC: 3094 case 0x1D4E0: 3095 case 0x1D514: 3096 case 0x1D57C: 3097 case 0x1D5B0: 3098 case 0x1D5E4: 3099 case 0x1D618: 3100 case 0x1D64C: 3101 case 0x1D680: 3102 $str .= chr(0x51); /* Q */ 3103 break; 3104 case 0x0280: 3105 case 0x211B: 3106 case 0x211C: 3107 case 0x211D: 3108 case 0xFF32: 3109 case 0x1D411: 3110 case 0x1D445: 3111 case 0x1D479: 3112 case 0x1D4E1: 3113 case 0x1D57D: 3114 case 0x1D5B1: 3115 case 0x1D5E5: 3116 case 0x1D619: 3117 case 0x1D64D: 3118 case 0x1D681: 3119 $str .= chr(0x52); /* R */ 3120 break; 3121 case 0x0405: 3122 case 0xFF33: 3123 case 0x1D412: 3124 case 0x1D446: 3125 case 0x1D47A: 3126 case 0x1D4AE: 3127 case 0x1D4E2: 3128 case 0x1D516: 3129 case 0x1D54A: 3130 case 0x1D57E: 3131 case 0x1D5B2: 3132 case 0x1D5E6: 3133 case 0x1D61A: 3134 case 0x1D64E: 3135 case 0x1D682: 3136 $str .= chr(0x53); /* S */ 3137 break; 3138 case 0x0422: 3139 case 0xFF34: 3140 case 0x1D413: 3141 case 0x1D447: 3142 case 0x1D47B: 3143 case 0x1D4AF: 3144 case 0x1D4E3: 3145 case 0x1D517: 3146 case 0x1D54B: 3147 case 0x1D57F: 3148 case 0x1D5B3: 3149 case 0x1D5E7: 3150 case 0x1D61B: 3151 case 0x1D64F: 3152 case 0x1D683: 3153 $str .= chr(0x54); /* T */ 3154 break; 3155 case 0x0055: 3156 case 0xFF35: 3157 case 0x1D414: 3158 case 0x1D448: 3159 case 0x1D47C: 3160 case 0x1D4B0: 3161 case 0x1D4E4: 3162 case 0x1D518: 3163 case 0x1D54C: 3164 case 0x1D580: 3165 case 0x1D5B4: 3166 case 0x1D5E8: 3167 case 0x1D61C: 3168 case 0x1D650: 3169 case 0x1D684: 3170 $str .= chr(0x55); /* U */ 3171 break; 3172 case 0xFF36: 3173 case 0x1D415: 3174 case 0x1D449: 3175 case 0x1D47D: 3176 case 0x1D4B1: 3177 case 0x1D4E5: 3178 case 0x1D519: 3179 case 0x1D54D: 3180 case 0x1D581: 3181 case 0x1D5B5: 3182 case 0x1D5E9: 3183 case 0x1D61D: 3184 case 0x1D651: 3185 case 0x1D685: 3186 $str .= chr(0x56); /* V */ 3187 break; 3188 case 0x051C: 3189 case 0xFF37: 3190 case 0x1D416: 3191 case 0x1D44A: 3192 case 0x1D47E: 3193 case 0x1D4B2: 3194 case 0x1D4E6: 3195 case 0x1D51A: 3196 case 0x1D54E: 3197 case 0x1D582: 3198 case 0x1D5B6: 3199 case 0x1D5EA: 3200 case 0x1D61E: 3201 case 0x1D652: 3202 case 0x1D686: 3203 $str .= chr(0x57); /* W */ 3204 break; 3205 case 0xFF38: 3206 case 0x1D417: 3207 case 0x1D44B: 3208 case 0x1D47F: 3209 case 0x1D4B3: 3210 case 0x1D4E7: 3211 case 0x1D51B: 3212 case 0x1D54F: 3213 case 0x1D583: 3214 case 0x1D5B7: 3215 case 0x1D5EB: 3216 case 0x1D61F: 3217 case 0x1D653: 3218 case 0x1D687: 3219 $str .= chr(0x58); /* X */ 3220 break; 3221 case 0x0059: 3222 case 0x2144: 3223 case 0xFF39: 3224 case 0x1D418: 3225 case 0x1D44C: 3226 case 0x1D480: 3227 case 0x1D4B4: 3228 case 0x1D4E8: 3229 case 0x1D51C: 3230 case 0x1D550: 3231 case 0x1D584: 3232 case 0x1D5B8: 3233 case 0x1D5EC: 3234 case 0x1D620: 3235 case 0x1D654: 3236 case 0x1D688: 3237 $str .= chr(0x59); /* Y */ 3238 break; 3239 case 0x29F5: 3240 case 0x29F9: 3241 case 0xFE68: 3242 $str .= chr(0x5C); /* Backslash */ 3243 break; 3244 case 0x0430; // CYRILLIC SMALL LETTER A 3245 case 0x2124: 3246 case 0x2128: 3247 case 0xFF3A: 3248 case 0xFF41: 3249 case 0x1D419: 3250 case 0x1D44D: 3251 case 0x1D482: 3252 case 0x1D656: 3253 case 0x1D68A: 3254 $str .= chr(0x61); /* a */ 3255 break; 3256 case 0xFF42: /* b */ 3257 case 0x1D41B: 3258 case 0x1D44F: 3259 case 0x1D483: 3260 case 0x1D4B7: 3261 case 0x1D4EB: 3262 case 0x1D51F: 3263 case 0x1D553: 3264 case 0x1D587: 3265 case 0x1D5BB: 3266 case 0x1D5EF: 3267 case 0x1D623: 3268 case 0x1D657: 3269 case 0x1D68B: 3270 $str .= chr(0x62); 3271 break; 3272 case 0x0063: /* c */ 3273 case 0x1D04: 3274 case 0x441: //CYRILLIC SMALL LETTER ES 3275 case 0xFF43: 3276 case 0x1D41C: 3277 case 0x1D450: 3278 case 0x1D484: 3279 case 0x1D4B8: 3280 case 0x1D4EC: 3281 case 0x1D520: 3282 case 0x1D554: 3283 case 0x1D588: 3284 case 0x1D5BC: 3285 case 0x1D5F0: 3286 case 0x1D624: 3287 case 0x1D658: 3288 case 0x1D68C: 3289 $str .= chr(0x63); 3290 break; 3291 case 0x0064: /* d */ 3292 case 0x1D05: 3293 case 0x2146: 3294 case 0xFF44: 3295 case 0x1D41D: 3296 case 0x1D451: 3297 case 0x1D485: 3298 case 0x1D4B9: 3299 case 0x1D4ED: 3300 case 0x1D521: 3301 case 0x1D555: 3302 case 0x1D589: 3303 case 0x1D5BD: 3304 case 0x1D5F1: 3305 case 0x1D625: 3306 case 0x1D659: 3307 case 0x1D68D: 3308 $str .= chr(0x64); 3309 break; 3310 case 0x0065: 3311 case 0x0435: // CYRILLIC SMALL LETTER IE 3312 case 0x1D07: 3313 case 0x212F: 3314 case 0x2147: 3315 case 0xFF45: 3316 case 0x1D41E: 3317 case 0x1D452: 3318 case 0x1D486: 3319 case 0x1D4EE: 3320 case 0x1D522: 3321 case 0x1D556: 3322 case 0x1D5BE: 3323 case 0x1D5F2: 3324 case 0x1D626: 3325 case 0x1D65A: 3326 case 0x1D68E: 3327 $str .= chr(0x65); /* e */ 3328 break; 3329 case 0x0066: /* f */ 3330 case 0xFF46: 3331 case 0x1D41F: 3332 case 0x1D453: 3333 case 0x1D487: 3334 case 0x1D4BB: 3335 case 0x1D4EF: 3336 case 0x1D523: 3337 case 0x1D557: 3338 case 0x1D58B: 3339 case 0x1D5BF: 3340 case 0x1D5F3: 3341 case 0x1D627: 3342 case 0x1D65B: 3343 case 0x1D68F: 3344 $str .= chr(0x66); 3345 break; 3346 case 0x0067: /* g */ 3347 case 0xFF47: 3348 case 0x210A: 3349 case 0x1D420: 3350 case 0x1D454: 3351 case 0x1D488: 3352 case 0x1D4F0: 3353 case 0x1D524: 3354 case 0x1D558: 3355 case 0x1D58C: 3356 case 0x1D5C0: 3357 case 0x1D5F4: 3358 case 0x1D628: 3359 case 0x1D65C: 3360 case 0x1D690: 3361 $str .= chr(0x67); 3362 break; 3363 case 0x0068: /* h */ 3364 case 0x04BB: 3365 case 0xFF48: 3366 case 0x1D421: 3367 case 0x1D489: 3368 case 0x1D4BD: 3369 case 0x1D4F1: 3370 case 0x1D525: 3371 case 0x1D559: 3372 case 0x1D58D: 3373 case 0x1D5C1: 3374 case 0x1D5F5: 3375 case 0x1D629: 3376 case 0x1D65D: 3377 case 0x1D691: 3378 $str .= chr(0x68); 3379 break; 3380 case 0x0456: 3381 case 0x1D09: 3382 case 0x2071: 3383 case 0xFF49: 3384 case 0x2148: 3385 case 0x1D422: 3386 case 0x1D456: 3387 case 0x1D48A: 3388 case 0x1D4BE: 3389 case 0x1D4F2: 3390 case 0x1D526: 3391 case 0x1D55A: 3392 case 0x1D58E: 3393 case 0x1D5C2: 3394 case 0x1D5F6: 3395 case 0x1D62A: 3396 case 0x1D65E: 3397 case 0x1D692: 3398 $str .= chr(0x69); /* i */ 3399 break; 3400 case 0x1D0A: 3401 case 0x2149: 3402 case 0xFF4A: 3403 case 0x1D423: 3404 case 0x1D457: 3405 case 0x1D48B: 3406 case 0x1D4BF: 3407 case 0x1D4F3: 3408 case 0x1D527: 3409 case 0x1D55B: 3410 case 0x1D58F: 3411 case 0x1D5C3: 3412 case 0x1D5F7: 3413 case 0x1D62B: 3414 case 0x1D65F: 3415 case 0x1D693: 3416 $str .= chr(0x6A); /* j */ 3417 break; 3418 case 0x006B: /* k */ 3419 case 0x1D0B: 3420 //case 0x03BA: 3421 case 0xFF4B: 3422 case 0x1D424: 3423 case 0x1D458: 3424 case 0x1D48C: 3425 case 0x1D4C0: 3426 case 0x1D4F4: 3427 case 0x1D528: 3428 case 0x1D55C: 3429 case 0x1D590: 3430 case 0x1D5C4: 3431 case 0x1D5F8: 3432 case 0x1D62C: 3433 case 0x1D660: 3434 case 0x1D694: 3435 $str .= chr(0x6B); 3436 break; 3437 case 0x006C: /* l */ 3438 case 0x2113: 3439 case 0xFF4C: 3440 case 0x1D425: 3441 case 0x1D459: 3442 case 0x1D48D: 3443 case 0x1D4C1: 3444 case 0x1D4F5: 3445 case 0x1D529: 3446 case 0x1D55D: 3447 case 0x1D591: 3448 case 0x1D5C5: 3449 case 0x1D5F9: 3450 case 0x1D62D: 3451 case 0x1D661: 3452 case 0x1D695: 3453 $str .= chr(0x6C); 3454 break; 3455 case 0x006D: /* m */ 3456 case 0x1D0D: 3457 case 0xFF4D: 3458 case 0x1D426: 3459 case 0x1D45A: 3460 case 0x1D48E: 3461 case 0x1D4C2: 3462 case 0x1D52A: 3463 case 0x1D55E: 3464 case 0x1D592: 3465 case 0x1D5C6: 3466 case 0x1D5FA: 3467 case 0x1D62E: 3468 case 0x1D662: 3469 case 0x1D696: 3470 case 0x1D4F6: 3471 $str .= chr(0x6D); 3472 break; 3473 case 0x207F: 3474 case 0xFF4E: 3475 case 0x1D427: 3476 case 0x1D45B: 3477 case 0x1D48F: 3478 case 0x1D4C3: 3479 case 0x1D4F7: 3480 case 0x1D52B: 3481 case 0x1D55F: 3482 case 0x1D593: 3483 case 0x1D5C7: 3484 case 0x1D5FB: 3485 case 0x1D62F: 3486 case 0x1D663: 3487 case 0x1D697: 3488 $str .= chr(0x6E); /* n */ 3489 break; 3490 case 0x006F: 3491 //case 0x00F8: 3492 case 0x043E: // CYRILLIC SMALL LETTER O 3493 case 0x1D0F: 3494 case 0x2134: 3495 case 0xFF4F: 3496 case 0x1D428: 3497 case 0x1D45C: 3498 case 0x1D490: 3499 case 0x1D4F8: 3500 case 0x1D52C: 3501 case 0x1D560: 3502 case 0x1D594: 3503 case 0x1D5C8: 3504 case 0x1D5FC: 3505 case 0x1D630: 3506 case 0x1D664: 3507 case 0x1D698: 3508 $str .= chr(0x6F); /* o */ 3509 break; 3510 case 0x0070: 3511 case 0x0440: // CYRILLIC SMALL LETTER ER 3512 case 0x1D18: 3513 case 0xFF50: 3514 case 0x213C: 3515 case 0x1D429: 3516 case 0x1D45D: 3517 case 0x1D491: 3518 case 0x1D4C5: 3519 case 0x1D4F9: 3520 case 0x1D52D: 3521 case 0x1D561: 3522 case 0x1D595: 3523 case 0x1D5C9: 3524 case 0x1D5FD: 3525 case 0x1D631: 3526 case 0x1D665: 3527 case 0x1D699: 3528 $str .= chr(0x70); /* p */ 3529 break; 3530 case 0x0071: /* q */ 3531 case 0x051B: 3532 case 0xFF51: 3533 case 0x1D42A: 3534 case 0x1D45E: 3535 case 0x1D492: 3536 case 0x1D4C6: 3537 case 0x1D4FA: 3538 case 0x1D52E: 3539 case 0x1D562: 3540 case 0x1D596: 3541 case 0x1D5CA: 3542 case 0x1D5FE: 3543 case 0x1D632: 3544 case 0x1D666: 3545 case 0x1D69A: 3546 $str .= chr(0x71); 3547 break; 3548 case 0x0072: /* r */ 3549 case 0xFF52: 3550 case 0x1D42B: 3551 case 0x1D45F: 3552 case 0x1D493: 3553 case 0x1D4C7: 3554 case 0x1D4FB: 3555 case 0x1D52F: 3556 case 0x1D563: 3557 case 0x1D597: 3558 case 0x1D5CB: 3559 case 0x1D5FF: 3560 case 0x1D633: 3561 case 0x1D667: 3562 case 0x1D69B: 3563 $str .= chr(0x72); 3564 break; 3565 case 0x0073: /* s */ 3566 case 0x0455: 3567 case 0xFF53: 3568 case 0x1D42C: 3569 case 0x1D460: 3570 case 0x1D494: 3571 case 0x1D4C8: 3572 case 0x1D4FC: 3573 case 0x1D530: 3574 case 0x1D564: 3575 case 0x1D598: 3576 case 0x1D5CC: 3577 case 0x1D600: 3578 case 0x1D634: 3579 case 0x1D668: 3580 case 0x1D69C: 3581 $str .= chr(0x73); 3582 break; 3583 case 0x0074: /* t */ 3584 case 0x1D1B: 3585 case 0xFF54: 3586 case 0x1D42D: 3587 case 0x1D461: 3588 case 0x1D495: 3589 case 0x1D4C9: 3590 case 0x1D4FD: 3591 case 0x1D531: 3592 case 0x1D565: 3593 case 0x1D599: 3594 case 0x1D5CD: 3595 case 0x1D601: 3596 case 0x1D635: 3597 case 0x1D669: 3598 case 0x1D69D: 3599 $str .= chr(0x74); 3600 break; 3601 case 0x0075: /* u */ 3602 case 0x1D1C: 3603 case 0xFF55: 3604 case 0x1D42E: 3605 case 0x1D462: 3606 case 0x1D496: 3607 case 0x1D4CA: 3608 case 0x1D4FE: 3609 case 0x1D532: 3610 case 0x1D566: 3611 case 0x1D59A: 3612 case 0x1D5CE: 3613 case 0x1D602: 3614 case 0x1D636: 3615 case 0x1D66A: 3616 case 0x1D69E: 3617 $str .= chr(0x75); 3618 break; 3619 case 0x0076: /* v */ 3620 case 0x1D20: 3621 case 0xFF56: 3622 case 0x1D42F: 3623 case 0x1D463: 3624 case 0x1D497: 3625 case 0x1D4CB: 3626 case 0x1D4FF: 3627 case 0x1D533: 3628 case 0x1D567: 3629 case 0x1D59B: 3630 case 0x1D5CF: 3631 case 0x1D603: 3632 case 0x1D637: 3633 case 0x1D66B: 3634 case 0x1D69F: 3635 $str .= chr(0x76); 3636 break; 3637 case 0x0077: /* w */ 3638 case 0x051D: 3639 case 0x1D21: 3640 case 0x24B2: 3641 case 0x24E6: 3642 case 0xFF57: 3643 case 0x1D430: 3644 case 0x1D464: 3645 case 0x1D498: 3646 case 0x1D4CC: 3647 case 0x1D500: 3648 case 0x1D534: 3649 case 0x1D568: 3650 case 0x1D59C: 3651 case 0x1D5D0: 3652 case 0x1D604: 3653 case 0x1D638: 3654 case 0x1D66C: 3655 case 0x1D6A0: 3656 $str .= chr(0x77); 3657 break; 3658 case 0x0078: /* x */ 3659 case 0xFF58: 3660 case 0x2718: 3661 case 0x1D431: 3662 case 0x1D465: 3663 case 0x1D499: 3664 case 0x1D4CD: 3665 case 0x1D501: 3666 case 0x1D535: 3667 case 0x1D569: 3668 case 0x1D59D: 3669 case 0x1D5D1: 3670 case 0x1D605: 3671 case 0x1D639: 3672 case 0x1D66D: 3673 case 0x1D6A1: 3674 $str .= chr(0x78); 3675 break; 3676 case 0x0443: // CYRILLIC SMALL LETTER U 3677 case 0xFF59: 3678 case 0x1D432: 3679 case 0x1D466: 3680 case 0x1D49A: 3681 case 0x1D4CE: 3682 case 0x1D502: 3683 case 0x1D536: 3684 case 0x1D56A: 3685 case 0x1D59E: 3686 case 0x1D5D2: 3687 case 0x1D606: 3688 case 0x1D63A: 3689 case 0x1D66E: 3690 case 0x1D6A2: 3691 $str .= chr(0x79); /* y */ 3692 break; 3693 case 0x007A: /* z */ 3694 case 0x1D22: 3695 case 0xFF5A: 3696 case 0x1D433: 3697 case 0x1D467: 3698 case 0x1D49B: 3699 case 0x1D4CF: 3700 case 0x1D503: 3701 case 0x1D537: 3702 case 0x1D56B: 3703 case 0x1D59F: 3704 case 0x1D5D3: 3705 case 0x1D607: 3706 case 0x1D63B: 3707 case 0x1D66F: 3708 case 0x1D6A3: 3709 $str .= chr(0x7A); 3710 break; 3711 case 0xFE5B: 3712 case 0xFF5B: /* left curly bracket */ 3713 $str .= chr(0x7B); 3714 break; 3715 case 0xFF5C: /* pipe */ 3716 $str .= chr(0x7C); 3717 break; 3718 case 0xFE5C: 3719 case 0xFF5D: /* right curly bracket */ 3720 $str .= chr(0x7D); 3721 break; 3722 case 0xFF5E: /* tilde */ 3723 $str .= chr(0x7E); 3724 break; 3725 3726 default: 3727 $str .= $char; 3728 break; 3729 } 3730 } 3731 3732 function get_lang_regex_from_country($country) { 3733 static $codes = [ 3734 'AD' => '/\b(?:ca)\b/', 3735 'AE' => '/\b(?:ar|fa|hi|ur)\b/', 3736 'AF' => '/\b(?:fa|ps|uz|tk)\b/', 3737 'AL' => '/\b(?:sq|el)\b/', 3738 'AM' => '/\b(?:hy)\b/', 3739 'AO' => '/\b(?:pt)\b/', 3740 'AR' => '/\b(?:es|it|de|fr|gn)\b/', 3741 'AS' => '/\b(?:sm|to)\b/', 3742 'AT' => '/\b(?:de|hr|hu|sl)\b/', 3743 'AW' => '/\b(?:nl|pap|es)\b/', 3744 'AX' => '/\b(?:sv)\b/', 3745 'AZ' => '/\b(?:az|ru|hy)\b/', 3746 'BA' => '/\b(?:bs|hr|sr)\b/', 3747 'BD' => '/\b(?:bn)\b/', 3748 'BE' => '/\b(?:nl|fr|de)\b/', 3749 'BF' => '/\b(?:fr|mos)\b/', 3750 'BG' => '/\b(?:bg|tr|rom)\b/', 3751 'BH' => '/\b(?:ar|fa|ur)\b/', 3752 'BI' => '/\b(?:fr|rn)\b/', 3753 'BJ' => '/\b(?:fr)\b/', 3754 'BL' => '/\b(?:fr)\b/', 3755 'BM' => '/\b(?:pt)\b/', 3756 'BN' => '/\b(?:ms)\b/', 3757 'BO' => '/\b(?:es|qu|ay)\b/', 3758 'BQ' => '/\b(?:nl|pap)\b/', 3759 'BR' => '/\b(?:pt|es|fr)\b/', 3760 'BT' => '/\b(?:dz)\b/', 3761 'BW' => '/\b(?:tn)\b/', 3762 'BY' => '/\b(?:be|ru)\b/', 3763 'BZ' => '/\b(?:es)\b/', 3764 'CA' => '/\b(?:fr|iu)\b/', 3765 'CC' => '/\b(?:ms)\b/', 3766 'CD' => '/\b(?:fr|ln|ktu|kg|sw|lua)\b/', 3767 'CF' => '/\b(?:fr|sg|ln|kg)\b/', 3768 'CG' => '/\b(?:fr|kg|ln)\b/', 3769 'CH' => '/\b(?:de|fr|it|rm)\b/', 3770 'CI' => '/\b(?:fr)\b/', 3771 'CK' => '/\b(?:mi)\b/', 3772 'CL' => '/\b(?:es)\b/', 3773 'CM' => '/\b(?:fr)\b/', 3774 'CN' => '/\b(?:zh|yue|wuu|dta|ug|za)\b/', 3775 'CO' => '/\b(?:es)\b/', 3776 'CR' => '/\b(?:es)\b/', 3777 'CU' => '/\b(?:es|pap)\b/', 3778 'CV' => '/\b(?:pt)\b/', 3779 'CW' => '/\b(?:nl|pap)\b/', 3780 'CX' => '/\b(?:zh|ms)\b/', 3781 'CY' => '/\b(?:el|tr)\b/', 3782 'CZ' => '/\b(?:cs|sk)\b/', 3783 'DE' => '/\b(?:de)\b/', 3784 'DJ' => '/\b(?:fr|ar|so|aa)\b/', 3785 'DK' => '/\b(?:da|fo|de)\b/', 3786 'DO' => '/\b(?:es)\b/', 3787 'DZ' => '/\b(?:ar|fr)\b/', 3788 'EC' => '/\b(?:es)\b/', 3789 'EE' => '/\b(?:et|ru)\b/', 3790 'EG' => '/\b(?:ar|fr)\b/', 3791 'EH' => '/\b(?:ar|mey)\b/', 3792 'ER' => '/\b(?:aa|ar|tig|kun|ti)\b/', 3793 'ES' => '/\b(?:es|ca|gl|eu|oc)\b/', 3794 'ET' => '/\b(?:am|om|ti|so|sid)\b/', 3795 'FI' => '/\b(?:fi|sv|smn)\b/', 3796 'FJ' => '/\b(?:fj)\b/', 3797 'FM' => '/\b(?:chk|pon|yap|kos|uli|woe|nkr|kpg)\b/', 3798 'FO' => '/\b(?:fo|da)\b/', 3799 'FR' => '/\b(?:fr|frp|br|co|ca|eu|oc)\b/', 3800 'GA' => '/\b(?:fr)\b/', 3801 'GB' => '/\b(?:en)\b/', 3802 'GE' => '/\b(?:ka|ru|hy|az)\b/', 3803 'GF' => '/\b(?:fr)\b/', 3804 'GG' => '/\b(?:nrf)\b/', 3805 'GH' => '/\b(?:ak|ee|tw)\b/', 3806 'GI' => '/\b(?:es|it|pt)\b/', 3807 'GL' => '/\b(?:kl|da)\b/', 3808 'GM' => '/\b(?:mnk|wof|wo|ff)\b/', 3809 'GN' => '/\b(?:fr)\b/', 3810 'GP' => '/\b(?:fr)\b/', 3811 'GQ' => '/\b(?:es|fr)\b/', 3812 'GR' => '/\b(?:el|fr)\b/', 3813 'GT' => '/\b(?:es)\b/', 3814 'GU' => '/\b(?:ch)\b/', 3815 'GW' => '/\b(?:pt|pov)\b/', 3816 'HK' => '/\b(?:zh|yue|zh)\b/', 3817 'HN' => '/\b(?:es|cab|miq)\b/', 3818 'HR' => '/\b(?:hr|sr)\b/', 3819 'HT' => '/\b(?:ht|fr)\b/', 3820 'HU' => '/\b(?:hu)\b/', 3821 'ID' => '/\b(?:id|nl|jv)\b/', 3822 'IE' => '/\b(?:ga)\b/', 3823 'IL' => '/\b(?:he|ar)\b/', 3824 'IM' => '/\b(?:gv)\b/', 3825 'IN' => '/\b(?:hi|bn|te|mr|ta|ur|gu|kn|ml|or|pa|as|bh|sat|ks|ne|sd|kok|doi|mni|sit|sa|fr|lus|inc)\b/', 3826 'IQ' => '/\b(?:ar|ku|hy)\b/', 3827 'IR' => '/\b(?:fa|ku)\b/', 3828 'IS' => '/\b(?:is|de|da|sv|no)\b/', 3829 'IT' => '/\b(?:it|de|fr|sc|ca|co|sl)\b/', 3830 'JE' => '/\b(?:fr|nrf)\b/', 3831 'JO' => '/\b(?:ar)\b/', 3832 'JP' => '/\b(?:ja)\b/', 3833 'KE' => '/\b(?:sw)\b/', 3834 'KG' => '/\b(?:ky|uz|ru)\b/', 3835 'KH' => '/\b(?:km|fr)\b/', 3836 'KI' => '/\b(?:gil)\b/', 3837 'KM' => '/\b(?:ar|fr)\b/', 3838 'KP' => '/\b(?:ko)\b/', 3839 'KR' => '/\b(?:ko)\b/', 3840 'XK' => '/\b(?:sq|sr)\b/', 3841 'KW' => '/\b(?:ar)\b/', 3842 'KZ' => '/\b(?:kk|ru)\b/', 3843 'LA' => '/\b(?:lo|fr)\b/', 3844 'LB' => '/\b(?:ar|fr|hy)\b/', 3845 'LI' => '/\b(?:de)\b/', 3846 'LK' => '/\b(?:si|ta)\b/', 3847 'LS' => '/\b(?:st|zu|xh)\b/', 3848 'LT' => '/\b(?:lt|ru|pl)\b/', 3849 'LU' => '/\b(?:lb|de|fr)\b/', 3850 'LV' => '/\b(?:lv|ru|lt)\b/', 3851 'LY' => '/\b(?:ar|it)\b/', 3852 'MA' => '/\b(?:ar|ber|fr)\b/', 3853 'MC' => '/\b(?:fr|it)\b/', 3854 'MD' => '/\b(?:ro|ru|gag|tr)\b/', 3855 'ME' => '/\b(?:sr|hu|bs|sq|hr|rom)\b/', 3856 'MF' => '/\b(?:fr)\b/', 3857 'MG' => '/\b(?:fr|mg)\b/', 3858 'MH' => '/\b(?:mh)\b/', 3859 'MK' => '/\b(?:mk|sq|tr|rmm|sr)\b/', 3860 'ML' => '/\b(?:fr|bm)\b/', 3861 'MM' => '/\b(?:my)\b/', 3862 'MN' => '/\b(?:mn|ru)\b/', 3863 'MO' => '/\b(?:zh|zh|pt)\b/', 3864 'MP' => '/\b(?:fil|tl|zh|ch)\b/', 3865 'MQ' => '/\b(?:fr)\b/', 3866 'MR' => '/\b(?:ar|fuc|snk|fr|mey|wo)\b/', 3867 'MT' => '/\b(?:mt)\b/', 3868 'MU' => '/\b(?:bho|fr)\b/', 3869 'MV' => '/\b(?:dv)\b/', 3870 'MW' => '/\b(?:ny|yao|tum|swk)\b/', 3871 'MX' => '/\b(?:es)\b/', 3872 'MY' => '/\b(?:ms|zh|ta|te|ml|pa|th)\b/', 3873 'MZ' => '/\b(?:pt|vmw)\b/', 3874 'NA' => '/\b(?:af|de|hz|naq)\b/', 3875 'NC' => '/\b(?:fr)\b/', 3876 'NE' => '/\b(?:fr|ha|kr|dje)\b/', 3877 'NG' => '/\b(?:ha|yo|ig|ff)\b/', 3878 'NI' => '/\b(?:es)\b/', 3879 'NL' => '/\b(?:nl|fy)\b/', 3880 'NO' => '/\b(?:no|nb|nn|se|fi)\b/', 3881 'NP' => '/\b(?:ne)\b/', 3882 'NR' => '/\b(?:na)\b/', 3883 'NU' => '/\b(?:niu)\b/', 3884 'NZ' => '/\b(?:mi)\b/', 3885 'OM' => '/\b(?:ar|bal|ur)\b/', 3886 'PA' => '/\b(?:es)\b/', 3887 'PE' => '/\b(?:es|qu|ay)\b/', 3888 'PF' => '/\b(?:fr|ty)\b/', 3889 'PG' => '/\b(?:ho|meu|tpi)\b/', 3890 'PH' => '/\b(?:tl|fil|ceb|tgl|ilo|hil|war|pam|bik|bcl|pag|mrw|tsg|mdh|cbk|krj|sgd|msb|akl|ibg|yka|mta|abx)\b/', 3891 'PK' => '/\b(?:ur|pa|sd|ps|brh)\b/', 3892 'PL' => '/\b(?:pl)\b/', 3893 'PM' => '/\b(?:fr)\b/', 3894 'PR' => '/\b(?:es)\b/', 3895 'PS' => '/\b(?:ar)\b/', 3896 'PT' => '/\b(?:pt|mwl)\b/', 3897 'PW' => '/\b(?:pau|sov|tox|ja|fil|zh)\b/', 3898 'PY' => '/\b(?:es|gn)\b/', 3899 'QA' => '/\b(?:ar|es)\b/', 3900 'RE' => '/\b(?:fr)\b/', 3901 'RO' => '/\b(?:ro|hu|rom)\b/', 3902 'RS' => '/\b(?:sr|hu|bs|rom)\b/', 3903 'RU' => '/\b(?:ru|tt|xal|cau|ady|kv|ce|tyv|cv|udm|tut|mns|bua|myv|mdf|chm|ba|inh|tut|kbd|krc|av|sah|nog)\b/', 3904 'RW' => '/\b(?:rw|fr|sw)\b/', 3905 'SA' => '/\b(?:ar)\b/', 3906 'SB' => '/\b(?:tpi)\b/', 3907 'SC' => '/\b(?:fr)\b/', 3908 'SD' => '/\b(?:ar|fia)\b/', 3909 'SE' => '/\b(?:sv|se|sma|fi)\b/', 3910 'SG' => '/\b(?:cmn|ms|ta|zh)\b/', 3911 'SI' => '/\b(?:sl|sh)\b/', 3912 'SJ' => '/\b(?:no|ru)\b/', 3913 'SK' => '/\b(?:sk|hu)\b/', 3914 'SL' => '/\b(?:mtem)\b/', 3915 'SM' => '/\b(?:it)\b/', 3916 'SN' => '/\b(?:fr|wo|fuc|mnk)\b/', 3917 'SO' => '/\b(?:so|ar|it)\b/', 3918 'SR' => '/\b(?:nl|srn|hns|jv)\b/', 3919 'ST' => '/\b(?:pt)\b/', 3920 'SV' => '/\b(?:es)\b/', 3921 'SX' => '/\b(?:nl)\b/', 3922 'SY' => '/\b(?:ar|ku|hy|arc|fr)\b/', 3923 'SZ' => '/\b(?:ss)\b/', 3924 'TD' => '/\b(?:fr|ar|sre)\b/', 3925 'TF' => '/\b(?:fr)\b/', 3926 'TG' => '/\b(?:fr|ee|hna|kbp|dag|ha)\b/', 3927 'TH' => '/\b(?:th)\b/', 3928 'TJ' => '/\b(?:tg|ru)\b/', 3929 'TK' => '/\b(?:tkl)\b/', 3930 'TL' => '/\b(?:tet|pt|id)\b/', 3931 'TM' => '/\b(?:tk|ru|uz)\b/', 3932 'TN' => '/\b(?:ar|fr)\b/', 3933 'TO' => '/\b(?:to)\b/', 3934 'TR' => '/\b(?:tr|ku|diq|az|av)\b/', 3935 'TT' => '/\b(?:hns|fr|es|zh)\b/', 3936 'TV' => '/\b(?:tvl|sm|gil)\b/', 3937 'TW' => '/\b(?:zh|zh|nan|hak)\b/', 3938 'TZ' => '/\b(?:sw|ar)\b/', 3939 'UA' => '/\b(?:uk|ru|rom|pl|hu)\b/', 3940 'UG' => '/\b(?:lg|sw|ar)\b/', 3941 'US' => '/\b(?:en|es)\b/', 3942 'UY' => '/\b(?:es)\b/', 3943 'UZ' => '/\b(?:uz|ru|tg)\b/', 3944 'VA' => '/\b(?:la|it|fr)\b/', 3945 'VC' => '/\b(?:fr)\b/', 3946 'VE' => '/\b(?:es)\b/', 3947 'VN' => '/\b(?:vi|fr|zh|km)\b/', 3948 'VU' => '/\b(?:bi|fr)\b/', 3949 'WF' => '/\b(?:wls|fud|fr)\b/', 3950 'WS' => '/\b(?:sm)\b/', 3951 'YE' => '/\b(?:ar)\b/', 3952 'YT' => '/\b(?:fr)\b/', 3953 'ZA' => '/\b(?:zu|xh|af|nso|tn|st|ts|ss|ve|nr)\b/', 3954 'ZM' => '/\b(?:bem|loz|lun|lue|ny|toi)\b/', 3955 'ZW' => '/\b(?:sn|nr|nd)\b/', 3956 'CS' => '/\b(?:cu|hu|sq|sr)\b/', 3957 'AN' => '/\b(?:nl|es)\b/' 3958 ]; 3959 3960 if (isset($codes[$country])) { 3961 return $codes[$country]; 3962 } 3963 3964 return null; 3965 }