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