array_search($f2, $pkeys); } // style 1 - no spaces, just letters+numbers in fields function is_random_text($fields) { foreach($fields as $field) { $num = preg_match('/[0-9]/', $field); $lc = preg_match('/[a-z]/', $field); $uc = preg_match('/[A-Z]/', $field); $spc = preg_match('/ /', $field); if ($spc || ($num + $lc + $uc)<2) return false; } return true; } // style 2 - uc+lc+spaces in fields function is_random_word_text($fields) { foreach($fields as $field) { $other = preg_match('/[^a-zA-Z \n]/', $field); $lc = preg_match('/[a-z]/', $field); $uc = preg_match('/[A-Z]/', $field); $spc = preg_match('/ /', $field); if ($other || !$spc || !$lc || !$uc) return false; } return true; } function has_cyrillic($txt) { return preg_match('/[\xD0-\xD3][\x80-\xBF]/', $txt) > 0; } function expand_short_urls($com) { $urls = array(); $urltext = ""; $com = preg_replace("/\(dot\)|DOT/", ".", $com); 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); foreach($urls as $url) { $com .= strtolower(rpc_find_real_url(str_replace("preview.tinyurl","tinyurl",$url))); } return $com; } function normalize_ascii($text, $preserve_case = 0) { $text = preg_replace( '#[(\[={]dot[)\]=}]#i', '.', $text ); $t = Transliterator::create("Any-Latin; nfd; [:nonspacing mark:] remove; nfkc; Latin-ASCII"); if (!$t) return $text; $text = $t->transliterate($text); if (!$preserve_case) $text = strtolower($text); return $text; } function strip_zerowidth($text) { $text = preg_replace('/ [\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}| \x{115F}|\x{1160}|\x{FFA0}|\x{034f}|\x{180e}|\x{17B4}|\x{17B5}| [\x{0001}-\x{0008}\x{000E}\x{000F}\x{0010}-\x{001F}\x{007F}-\x{009F}]| [\x{2000}-\x{200F}]| [\x{2028}-\x{202F}]| [\x{2060}-\x{206F}]| [\x{fe00}-\x{fe0f}]| [\x{FFF0}-\x{FFFB}]| [\x{E0100}-\x{E01EF}]| [\x{E0001}-\x{E007F}] /ux', '', $text); return $text; } /** * Removes codepoints above 3134F: * E0000..E007F; Tags * E0100..E01EF; Variation Selectors Supplement * F0000..FFFFF; Supplementary Private Use Area-A * 100000..10FFFF; Supplementary Private Use Area-B */ function strip_private_unicode($str) { if ($str === '') { return $str; } return preg_replace('/[^\x{0000}-\x{3134F}]/u', '', $str); } function strip_emoticons($text, $has_sjis_art = false) { $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}]'; if (!$has_sjis_art) { $regex .= '|[\x{2502}-\x{257F}]'; } return preg_replace("/$regex/u", '', $text); } function strip_fake_capcodes($str) { // double FULLWIDTH NUMBER SIGN or # // PLACE OF INTEREST SIGN return preg_replace('/[\x{FF03}#]{2,}|\x{2318}/u', '', $str); } function normalize_text($text, $filter = '') { $text = normalize_ascii($text); $text = strip_zerowidth($text); //if ($filter) $text = $filter($text); $text = preg_replace('@[^a-zA-Z0-9.,/&:;?=~_-]@', '', $text); return $text; } function normalize_content( $name ) { // this needs some absolutely retarded shit to get this to not suck, however // it is an almost fool proof way of translating to ascii letters // without breaking kanji, cyrillic etc $name = preg_replace( '#[\x{2600}-\x{26FF}]#u', '', $name ); // set internal incoding to utf-8 //die($name); $oldEncoding = mb_internal_encoding(); mb_internal_encoding('UTF-8'); $name = convert_to_utf8($name); // Done, back to old encoding $newname = ''; $len = mb_strlen( $name ); for( $i = 0; $i < $len; $i++ ) { trans_similar_to_ascii( $newname, mb_substr( $name, $i, 1 ) ); } mb_internal_encoding($oldEncoding); return $newname; } function convert_to_utf8( $content ) { if( !mb_check_encoding( $content, 'UTF-8' ) || !($content === mb_convert_encoding(mb_convert_encoding($content, 'UTF-32', 'UTF-8' ), 'UTF-8', 'UTF-32')) ) { $content = mb_convert_encoding($content, 'UTF-8'); } return $content; } function mb_ord( $char ) { mb_detect_order( array( 'UTF-8', 'ISO-8859-15', 'ISO-8859-1', 'ASCII' ) ); $result = unpack( 'N', mb_convert_encoding( $char, 'UCS-4BE', 'UTF-8' ) ); if( is_array( $result ) === true ) return $result[1]; return ord($char); } function normalize_check($com,$sub,$f) { $n = normalize_text($com.$sub, "expand_short_urls"); $n2 = preg_replace("/[\x80-\xFF]/", "", html_entity_decode($com, ENT_QUOTES, "UTF-8")); record_post_info($f, "from: $com$sub\nto: $n\ndeutf8: $n2"); } function match_banned_text($links, $text, $is_re) { $badlink = ""; $should_ban = false; foreach ($links as $l) { if ($l == '#') continue; $badlink = $l; if ($is_re) { $should_ban = preg_match($l, $text, $m) > 0; $badlink = TEST_BOARD ? "'$l' ({$m[0]})" : "'{$m[0]}'"; } else { $should_ban = strpos($text, $l) !== FALSE; } if ($should_ban) break; } return $should_ban ? $badlink : ""; } function check_banned_links($text, $links, $priv, $pub, $is_re, $name, $dest, $ban, $long, $perm = false) { //check_banned_links($normalized_com, $sex, "sex spam links", S_BANNEDLINK, false, $name, $dest, true, false); $badlink = match_banned_text($links, $text, $is_re); $should_ban = ($badlink != ""); $len = $long ? 14 : 1; $len = $perm ? -1 : $len; if ($should_ban == true) { $privres = sprintf("banned %s %s: %s", $is_re?"regex":"string",htmlspecialchars($badlink),$priv); if ($ban) { $pub = str_replace('Error: ', '', $pub); auto_ban_poster($name, $len, 1, $privres, $pub, true); } if (TEST_BOARD) $pub .= "
".$privres; error($pub, $dest); } } // this used to check the autobans table but now it just permabans function auto_ban($name, $reason) { auto_ban_poster($name, 7, 1, $reason); } function get_jpeg_dimensions($contents) { // this is faster than getimagesize $i = 0; $len = strlen($contents); if( ord($contents{0}) == 0xFF & ord($contents{1}) == 0xD8 && ord($contents{2}) == 0xFF & ord($contents{3}) == 0xE0 ) { $i = 4; if( $contents{$i+2} == 'J' && $contents{$i+3} == 'F' && $contents{$i+4} == 'I' && $contents{$i+5} == 'F' && ord($contents{$i+6}) == 0x00 ) { // valid image. $block_length = ord($contents{$i}) * 256 + ord($contents{$i+1}); while( $i < $len ) { $i += $block_length; if( $i > $len ) { return false; } if( ord($contents{$i}) != 0xFF ) { return false; } if( ord($contents{$i+1}) == 0xC0 ) { $width = ord($contents{$i+7})*256 + ord($contents{$i+8}); $height = ord($contents{$i+5})*256 + ord($contents{$i+6}); return array($width, $height); } else { $i+=2; $block_length = ord($contents{$i}) * 256 + ord($contents{$i+1}); } } } } return false; } function file_too_big_for_type( $ext, $w, $h, $fsize ) { if ($ext === ".gif" || $ext === ".pdf" || $ext === '.webm' || $ext === '.mp4') { return NO; } $uncompressed_size = $w * $h * 4; return ($fsize > (3*$uncompressed_size)) ? YES : NO; } function regex_ignoring_nulls($words) { $rwords = preg_replace("/./", "$0[^\\\\x01-\\\\xFF]*", $words); $rwords = str_replace(".", "\\.", $rwords); return "/".implode("|", $rwords)."/i"; } /** * Strips exif from JPEG images * $file needs to be safe to use as shell argument */ function strip_jpeg_exif($file) { return system("/usr/local/bin/jpegtran -copy none -outfile '$file' '$file'") !== false; } /** * Strips non-whitelisted PNG chunks. * Returns an error if an animated PNG is detected. * Overwrites input file if modifications have been made. * $file needs to be safe to use as shell argument * Returns the number of chunks skipped or an error code (negative value). */ function strip_png_chunks($file, $max_chunk_len = 16 * 1024 * 1024) { $keep_chunks = [ 'ihdr', 'plte', 'idat', 'iend', 'trns', 'gama', 'sbit', 'phys', 'srgb', 'bkgd', 'time', 'chrm', 'iccp' ]; $img = fopen($file, 'rb'); if (!$img) { return -9; } $data = fread($img, 8); if ($data !== "\x89PNG\r\n\x1a\n") { fclose($img); return -1; } $output = ''; $skip_count = 0; while (!feof($img)) { $chunk_len_buf = fread($img, 4); if (!$chunk_len_buf) { break; } if (strlen($chunk_len_buf) !== 4) { return -1; } $chunk_len = unpack('N', $chunk_len_buf)[1]; if ($chunk_len > $max_chunk_len) { return -1; } $chunk_type_buf = fread($img, 4); if (strlen($chunk_type_buf) !== 4) { return -1; } $chunk_type = strtolower($chunk_type_buf); // aPNG is not supported if ($chunk_type === 'actl' || $chink_type === 'fctl' || $chink_type === 'fdat') { return -2; } if (in_array($chunk_type, $keep_chunks)) { if ($chunk_len > 0) { $data = fread($img, $chunk_len); if (strlen($data) !== $chunk_len) { return -1; } } else { $data = ''; } $crc = fread($img, 4); if (strlen($crc) !== 4) { return -1; } $output .= $chunk_len_buf . $chunk_type_buf . $data . $crc; if ($chunk_type === 'iend') { fread($img, 1); if (!feof($img)) { $skip_count++; } break; } } else { fseek($img, $chunk_len + 4, SEEK_CUR); $skip_count++; } } fclose($img); if ($output === '') { return -1; } if ($skip_count === 0) { return 0; } $out_file = $file . '_pngtmp'; $out = fopen($out_file, 'wb'); if (!$out) { return -9; } if (fwrite($out, "\x89PNG\r\n\x1a\n") === false) { return -9; } if (fwrite($out, $output) === false) { return -9; } fclose($out); if (rename($out_file, $file) === false) { return -9; } return $skip_count; } // Calculates the actual image data inside a JPEG file and errors out // if it's smaller than the reported size. function validate_jpeg_size($file, $reported_size) { $eof = false; $img = fopen($file, 'rb'); $data = fread($img, 2); if ($data !== "\xff\xd8") { fclose($img); return false; } while (!feof($img)) { $data = fread($img, 1); if ($data !== "\xff") { continue; } while (!feof($img)) { $data = fread($img, 1); if ($data !== "\xff") { break; } } if (feof($img)) { break; } $byte = unpack('C', $data)[1]; if ($byte === 217) { $eof = ftell($img); break; } if ($byte === 0 || $byte === 1 || ($byte >= 208 && $byte <= 216)) { continue; } $data = fread($img, 2); $length = unpack('n', $data)[1]; if ($length < 1) { break; } fseek($img, $length - 2, SEEK_CUR); } fclose($img); // 50 KB if ($reported_size - $eof >= 51200) { error(S_IMGCONTAINSFILE, $file); } return $eof; } /** * Checks for extensions and comments * and calculates actual GIF data. * Strips extra data if it exists. * $file needs to be safe to use as shell argument. */ function strip_gif_extra_data($file, $reported_size) { $binary = '/usr/local/bin/gifsicle'; $res = shell_exec("$binary --sinfo \"$file\" 2>&1"); if ($res !== null) { $size = 0; $need_strip = false; if (preg_match('/ extensions [0-9]+| comment /', $res)) { $need_strip = true; } else if (preg_match_all('/compressed size ([0-9]+)/', $res, $m)) { foreach ($m[1] as $frame_size) { $size += (int)$frame_size; } // Strip if 50+ KB of extra data is found if ($reported_size - $size >= 51200) { $need_strip = true; } } if ($need_strip) { if (system("$binary --no-comments --no-extensions \"$file\" -o \"$file\" >/dev/null 2>&1") === false) { // gifsicle error return -1; } else { // file was modified return 1; } } } else { // gifsicle error return -1; } // nothing changed return 0; } // No longer used function spam_filter_post_image($name, $dest, $md5, $upfile_name, $ext, $w, $h, $fsize) { if( $upfile_name == '' ) error('Blank file names are not supported.'); if (file_too_big_for_type($ext, $w, $h, $fsize) === YES) { $lim = 3*4*$w*$h; error(S_IMGCONTAINSFILE, $dest); } $img_bytes = file_get_contents($dest); $img_beginning = strlen($img_bytes) > 0x50000 ? substr($img_bytes, 0, 0x40000).substr($img_bytes, -(0x10000)) : $img_bytes; global $silent_reject; $silent_reject = 0; // protect against IE's retarded MIME-sniffing XSS vulnerability // by doing our own sniffing and rejecting exploitable files { $negative_match = regex_ignoring_nulls(array("minitokyonet", "urchin.js")); //except minitokyo from this, it causes false positives if (preg_match($negative_match, $img_beginning)===0) { // ' DATE_SUB(NOW(), INTERVAL 1 HOUR) LIMIT 1 SQL; $res = mysql_global_call($query); if ($res && mysql_num_rows($res) === 1) { return true; } $query = "INSERT INTO postfilter_hits (filter_id, board, long_ip) VALUES($filter_id, '%s', $long_ip)"; return mysql_global_call($query, BOARD_DIR); } function log_postfilter_hit($filter, $board, $thread_id, $name, $sub, $com, $upfile_name) { $ip = $_SERVER['REMOTE_ADDR']; $country = $_SERVER['HTTP_X_GEO_COUNTRY']; $threat_score = spam_filter_get_threat_score($country, !$thread_id, true); $meta = spam_filter_format_http_headers("$name\n$sub\n$com", $country, $upfile_name, $threat_score); $action = "filter_{$filter['id']}"; $query = <<\/]+|>/', ' ', $sub . ' ' . $com . ' '. $name); $normalized_com_sage = ucwords(strtolower($normalized_com_sage)); $normalized_com_sage = normalize_ascii($normalized_com_sage, 1); } $userpwd = UserPwd::getSession(); $matched_filter = false; while ($filter = mysql_fetch_assoc($res)) { // Counter mode: triggers when the number of matches is at least $min_count $min_count = (int)$filter['min_count']; if ($min_count < 1) { $min_count = 1; } // Lenient filter if ($filter['lenient']) { if ($userpwd) { if ($filter['updated_on']) { $since_ts = (int)$filter['updated_on']; } else { $since_ts = (int)$filter['created_on']; } if ($userpwd->isUserKnownOrVerified(60, $since_ts)) { // 1 hour continue; } } } // OPs-only filter if ($filter['ops_only'] && $resto) { continue; } if ($filter['autosage']) { // Autosage filter but the post is a reply if ($resto) { continue; } // Regex filter if ($filter['regex']) { if ($min_count > 1) { if (preg_match_all($filter['pattern'], $expanded_com) >= $min_count) { $matched_filter = $filter; break; } } else { if (preg_match($filter['pattern'], $expanded_com) === 1) { $matched_filter = $filter; break; } } } // String filter for autosaging else { if ($min_count > 1) { if (substr_count($normalized_com_sage, $filter['pattern']) >= $min_count) { $matched_filter = $filter; break; } } else { if (strpos($normalized_com_sage, $filter['pattern']) !== false) { $matched_filter = $filter; break; } } } } // Regex filter if ($filter['regex']) { if ($min_count > 1) { if (preg_match_all($filter['pattern'], $expanded_com) >= $min_count) { $matched_filter = $filter; break; } } else { if (preg_match($filter['pattern'], $expanded_com) === 1) { $matched_filter = $filter; break; } } } // String filter else { if ($min_count > 1) { if (substr_count($normalized_com, $filter['pattern']) >= $min_count) { $matched_filter = $filter; break; } } else { if (strpos($normalized_com, $filter['pattern']) !== false) { $matched_filter = $filter; break; } } } } if ($matched_filter !== false) { // Update hit stats register_postfilter_hit($matched_filter['id']); // Autosage if ($matched_filter['autosage']) { return true; } // Log else if ($matched_filter['log']) { log_postfilter_hit($matched_filter, $board, $resto, $name, $sub, $com, $upfile_name); } // Reject else { if ($matched_filter['ban_days']) { $err = S_BANNEDTEXT; $ban_days = (int)$matched_filter['ban_days']; $private_reason = 'banned string in comment (filter ID: ' . $matched_filter['id'] . ')'; $public_reason = $err; auto_ban_poster($name, $ban_days, 1, $private_reason, $public_reason, true, $pwd, $pass_id); } else { $err = S_REJECTTEXT; } if ($matched_filter['quiet']) { show_post_successful_fake($resto); die(); } if (TEST_BOARD) { $err .= ' (filter ID: ' . $matched_filter['id'] . ')'; } error($err); } } // Other if ($sub !== '') { $normalized_sub = normalize_text($sub); if (stripos($sub, 'moot') !== false) { error("You can't post with that subject."); } if (stripos($normalized_com, '##') !== false || stripos($sub, 'admin') !== false) { error("You can't post with that subject."); } } return false; } function isIPRangeBannedReport($long_ip, $asn, $board, $userpwd = null) { return isIPRangeBanned($long_ip, $asn, [ 'board' => $board, 'is_report' => true, 'userpwd' => $userpwd, ] ); } // Checks if the IP is rangebanned // options: // board(string), is_sfw(bool, requires board) // userpwd(UserPwd): instance of UserPwd or null, // is_report(bool), is_op(bool), has_img(bool), // browser_id(string), // op_content(string): content of the thread OP for per-thread bans (unused) // returns the rangeban database entry if the IP is banned, false otherwise function isIPRangeBanned($long_ip, $asn, $options = []) { $long_ip = (int)$long_ip; $asn = (int)$asn; $now = (int)$_SERVER['REQUEST_TIME']; $cols = 'created_on, updated_on, expires_on, active, boards, ops_only, img_only, lenient, report_only, ua_ids'; $query = <<= $long_ip AND active = 1 AND (expires_on = 0 OR expires_on > $now)) SQL; if ($asn > 0) { $query .= << $now)) SQL; } $query .= ' ORDER BY lenient ASC'; $res = mysql_global_call($query); if (!$res) { return false; } // Parameters if (isset($options['board'])) { $board = $options['board']; $is_sfw = isset($options['is_sfw']) && $options['is_sfw']; } else { $board = null; $is_sfw = false; } if (isset($options['browser_id'])) { $browser_id = $options['browser_id']; } else { $browser_id = null; } if (isset($options['req_sig'])) { $req_sig = $options['req_sig']; } else { $req_sig = null; } if (isset($options['op_content']) && $options['op_content'] !== '') { $op_content = $options['op_content']; } else { $op_content = null; } $is_op = isset($options['is_op']) && $options['is_op']; $is_report = isset($options['is_report']) && $options['is_report']; $has_img = isset($options['has_img']) && $options['has_img']; if (isset($options['userpwd']) && $options['userpwd'] && $options['userpwd'] instanceof UserPwd) { $userpwd = $options['userpwd']; } else { $userpwd = null; } // OP-only and Image-only lenient rangebans also require a certain number of posts $post_count_ok = $userpwd && $userpwd->postCount() >= 3 && ($userpwd->maskLifetime() > 900 || $userpwd->postCount() >= 15); while ($range = mysql_fetch_assoc($res)) { if ($range['boards']) { if ($board === null) { continue; } $board_matcher = ",{$range['boards']},"; if (strpos($board_matcher, ",$board,") === false) { // _ws_ scope affects all work safe boards if ($is_sfw) { if (strpos($board_matcher, ",_ws_,") === false) { continue; } } else { continue; } } } $post_count_check = true; if ($range['report_only'] && !$is_report) { continue; } if ($range['ops_only']) { if (!$is_op) { continue; } else { $post_count_check = $post_count_ok; } } if ($range['img_only']) { if (!$has_img) { continue; } else { $post_count_check = $post_count_ok; } } if ($range['ua_ids']) { $_skip = true; if ($browser_id && strpos($range['ua_ids'], $browser_id) !== false) { $_skip = false; } if ($_skip && $req_sig && strpos($range['ua_ids'], $req_sig) !== false) { $_skip = false; } if ($_skip) { continue; } } if ($userpwd && $range['lenient']) { $lenient = (int)$range['lenient']; if ($range['updated_on']) { $since_ts = (int)$range['updated_on']; } else { $since_ts = (int)$range['created_on']; } // Mode 1: Known 24h or Verified if ($lenient === 1 && ($userpwd->verifiedLevel() || ($userpwd->isUserKnown(1440, $since_ts) && $post_count_check))) { continue; } // Mode 2: Known 24h only else if ($lenient === 2 && $userpwd->isUserKnown(1440, $since_ts) && $post_count_check) { continue; } // Mode 3: Verified only else if ($lenient === 3 && $userpwd->verifiedLevel()) { continue; } } return $range; } return false; } /** * Checks if the IP has enough posting history * $mode: 0 = check for replies, 1 = check for image replies, 2 = check of threads * Caches results. */ function spam_filter_is_ip_known($long_ip, $board = null, $mode = 0, $minutes_min = 0, $posts_min = 1) { static $cache = array(); $long_ip = (int)$long_ip; if (!$long_ip) { return false; } $cache_key = "$long_ip.$board.$mode.$minutes_min.$posts_min"; if (isset($cache[$cache_key])) { return $cache[$cache_key]; } // Not after (3 days) $minutes_max = 4320; // Not before $minutes_min = (int)$minutes_min; // At least X replies $posts_min = (int)$posts_min; // Board if ($board) { $board_clause = "AND board = '" . mysql_real_escape_string($board) . "'"; } else { $board_clause = ''; } // Mode: 1 = image replies, 2 = threads, 0 = any reply if ($mode === 1) { $action_clause = "AND action = 'new_reply' AND had_image = 1"; } else if ($mode === 2) { $action_clause = "AND action = 'new_thread'"; } else { $action_clause = "AND action = 'new_reply'"; } // Not before if (!$minutes_min) { $time_clause = "time >= DATE_SUB(NOW(), INTERVAL $minutes_max MINUTE)"; } else { $time_clause = "(time BETWEEN DATE_SUB(NOW(), INTERVAL $minutes_max MINUTE) AND DATE_SUB(NOW(), INTERVAL $minutes_min MINUTE))"; } // Check posting history $query = <<= DATE_SUB(NOW(), INTERVAL 48 HOUR) SQL; $res = mysql_global_call($query); if (!$res) { return false; } $count = (int)mysql_fetch_row($res)[0]; if ($count > 0) { $cache[$cache_key] = false; return false; } */ $cache[$cache_key] = true; return true; } /* * Checks if the user has a valid posting history to bypass a rangeban (FIXME: deprecated) */ function spam_filter_is_user_known($long_ip, $board = null, $pwd = null, $minutes = 15, $count = 1) { static $cache = array(); $pwd = null; // FIXME $interval = (int)$minutes; $cache_key_ip = "{$long_ip}:{$interval}"; if (isset($cache[$cache_key_ip])) { return $cache[$cache_key_ip]; } if ($pwd && $board) { $cache_key_pwd = "{$board}:{$pwd}:{$interval}"; if (isset($cache[$cache_key_pwd])) { return $cache[$cache_key_pwd]; } } $count = (int)$count; if ($count < 1) { $count = 1; } // Check the IP $query = << BOARD_DIR, 'is_sfw' => DEFAULT_BURICHAN, 'userpwd' => $userpwd, 'is_op' => $thread_id == 0, 'has_img' => $has_img, 'browser_id' => $browser_id, 'req_sig' => $req_sig ]; if ($range = isIPRangeBanned($long_ip, $asn, $options)) { if ($range['lenient'] && $userpwd) { $userpwd->setCookie('.' . L::d(BOARD_DIR)); } // Images only if ($range['img_only']) { $_err = S_IPRANGE_BLOCKED_IMG; } // Threads only else if ($range['ops_only']) { $_err = S_IPRANGE_BLOCKED_OP; } else { $_err = S_IPRANGE_BLOCKED; } // Temporarily or Permanently if ($range['expires_on'] || $range['lenient']) { $_err .= ' ' . S_IPRANGE_BLOCKED_TEMP; } else { $_err .= ' ' . S_IPRANGE_BLOCKED_PERM; } // Bypassed by verified or known users if ($range['lenient'] == 1) { $_err .= S_IPRANGE_BLOCKED_L1; } // Bypassed by known users only else if ($range['lenient'] == 2) { $_err .= S_IPRANGE_BLOCKED_L2; } // Bypassed by verified users only else if ($range['lenient'] == 3) { $_err .= S_IPRANGE_BLOCKED_L3; } // 4chan pass mention $_err .= S_IPRANGE_BLOCKED_PASS; error($_err); } // Auto-rangebans, Mobile only // Bypassed by verified users or known users for at least 2h // or users who have made at least one post on the board 15 minutes ago if ($thread_id !== null && $browser_id[0] === '1') { $since_ts = 0; if ($userpwd) { $user_known = $userpwd->isUserKnownOrVerified(120, 1); $now = $_SERVER['REQUEST_TIME']; if ($userpwd->postCount() > 0 && $userpwd->maskTs() <= $now - 900) { $since_ts = $userpwd->maskTs(); } } else { $user_known = false; } if (!$user_known) { if (is_ip_auto_rangebanned($ip, BOARD_DIR, $thread_id, $browser_id, $since_ts)) { write_to_event_log('auto_range_hit', $ip, [ 'board' => BOARD_DIR, 'thread_id' => $thread_id, 'ua_sig' => $browser_id ]); if ($userpwd) { $userpwd->setCookie('.' . L::d(BOARD_DIR)); } // Temporary, bypassablee by known or verified users error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1 . S_IPRANGE_BLOCKED_PASS); } } } return false; } function is_ip_auto_rangebanned($ip, $board, $thread_id, $browser_id, $since_ts = 0) { $range_sql = explode('.', $ip); $range_sql = "{$range_sql[0]}.{$range_sql[1]}.%"; $thread_id = (int)$thread_id; if ($since_ts > 0) { $since_sql = ' AND created_on <= FROM_UNIXTIME(' . ((int)$since_ts) . ')'; } else { $since_sql = ''; } $sql =<< DATE_SUB(NOW(), INTERVAL 120 MINUTE)$since_sql LIMIT 1 SQL; $res = mysql_global_call($sql, $board, $browser_id, $range_sql); if (!$res) { return false; } if (mysql_num_rows($res)) { return true; } return false; } /** * Dumps and formats HTTP headers and other request information for logging */ function spam_filter_format_http_headers($com = null, $country = null, $filename = null, $threat_score = null, $req_sig = null) { $bot_headers = ''; foreach ($_SERVER as $_h_name => $_h_val) { if (substr($_h_name, 0, 5) == 'HTTP_') { if ($_h_name === 'HTTP_COOKIE') { $_cookies = array_keys($_COOKIE); $_cookies = array_intersect($_cookies, ['ws_style', 'nws_style', '4chan_pass', '_tcs', '_ga', 'cf_clearance' ]); $_cookie_count = count($_COOKIE); $bot_headers .= "HTTP_COOKIE: " . htmlspecialchars(implode(', ', $_cookies)) . " ($_cookie_count in total)\n"; } else if (strpos($_h_name, 'AUTH') !== false) { continue; } else { $bot_headers .= "$_h_name: " . htmlspecialchars($_h_val) . "\n"; } } } $bot_headers .= "_POST: " . htmlspecialchars(implode(', ', array_keys($_POST))) . "\n"; if ($country !== null) { $bot_headers .= "_Country: $country\n"; } if ($threat_score !== null) { $bot_headers .= "_Score: " . $threat_score . "\n"; } if ($req_sig !== null) { $bot_headers .= "_Sig: " . $req_sig . "\n"; } if (isset($_COOKIE['_tcs'])) { $bot_headers .= "_TCS: " . htmlspecialchars($_COOKIE['_tcs']) . "\n"; } if (isset($_COOKIE['4chan_pass'])) { $userpwd = UserPwd::getSession(); if ($userpwd) { $bot_headers .= "_Pwd: " . htmlspecialchars($userpwd->getPwd()) . "\n"; } } if ($filename !== null) { $bot_headers .= "_File: " . htmlspecialchars($filename) . "\n"; } if ($com !== null) { $bot_headers .= "_Comment: $com"; } return $bot_headers; } function spam_filter_get_req_sig() { static $cache = null; if ($cache !== null) { return $cache; } $pick_headers = [ 'HTTP_SEC_CH_UA_PLATFORM', 'HTTP_SEC_CH_UA_MOBILE', 'HTTP_SEC_CH_UA_MODEL', 'HTTP_USER_AGENT', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_SEC_FETCH_SITE', 'HTTP_SEC_FETCH_MODE', 'HTTP_SEC_FETCH_DEST', ]; $need_headers = [ 'HTTP_USER_AGENT', 'HTTP_ACCEPT', 'HTTP_REFERER', 'HTTP_ACCEPT_LANGUAGE', ]; $datapoints = []; $keys = []; foreach ($_SERVER as $k => $v) { if (in_array($k, $pick_headers)) { $keys[] = $k; } } $keyline = implode('.', $keys); $pointlines = [ 'fetch_smd' => 'HTTP_SEC_FETCH_SITE.HTTP_SEC_FETCH_MODE.HTTP_SEC_FETCH_DEST', 'fetch_dms' => 'HTTP_SEC_FETCH_DEST.HTTP_SEC_FETCH_MODE.HTTP_SEC_FETCH_SITE', 'fetch_any' => 'HTTP_SEC_FETCH_' ]; foreach ($pointlines as $key => $value) { if (strpos($keyline, $value) !== false) { $datapoints[] = $key; } } if (isset($_SERVER['HTTP_SEC_GPC']) || isset($_SERVER['HTTP_DNT'])) { $datapoints[] = 'dnt_gpc'; } if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { $datapoints[] = 'xrw'; } if (preg_match('/HTTP_SEC_CH_UA_[^.]+\.HTTP_SEC_CH_UA_[^.]+\.HTTP_SEC_CH_UA_/', $keyline)) { $datapoints[] = 'ch_ua_block'; } foreach ($need_headers as $k) { if (!isset($_SERVER[$k])) { $datapoints[] = 'missing'; break; } } $sig = implode('+', $datapoints); if (!$sig) { $sig = 'deadbeef'; } $cache = substr(md5($sig), 0, 8); return $cache; } // Covers 251044 (79.14 %) unique IPs and 10454 (77.09 %) unique bans // IPs %: 0.1 | Bans %: 0 | GR1 all: 0 | Any EU: false function spam_filter_is_asn_whitelisted() { static $val = null; static $whitelist = [ 21928, 6167, 7922, 7018, 812, 701, 1221, 20115, 22773, 2856, 3320, 6805, 577, 3209, 852, 20057, 20001, 5089, 4804, 10796, 8151, 5617, 7545, 11427, 209, 16086, 719, 33363, 15557, 5650, 133612, 6327, 6128, 5607, 206067, 1267, 26599, 6830, 8881, 2119, 14593, 28573, 3215, 26615, 3352, 1759, 7303, 11426, 35228, 55836, 1136, 22085, 11351, 8708, 1257, 3269, 5410, 7418, 23693, 30722, 9299, 13285, 17676, 3301, 7713, 5391, 33915, 44034, 8374, 4764, 51207, 29447, 27651, 7552, 12389, 19108, 8359, 11315, 6057, 16591, 12479, 5769, 17072, 31615, 12271, 28403, 11664, 6147, 15704, 10139, 39603, 12874, 25135, 5483, 5378, 4771, 4775, 12912, 6871, 12322, 6614, 132199, 5432, 212238, 12929, 27699, 22927, 8473, 2860, 12430, 7029, 6848, 5645, 8412, 62240, 8447, 15502, 174, 30036, 27747, 14638, 4230, 45727, 9009, 17639, 4788, 10030, 11492, 45143, 18881, 2516, 21334, 15895, 4766, 16232, 6799, 8400, 9443, 6079, 13999, 5610, 45899, 4761, 2586, 12353, 20845, 20365, 13280, 22047, 3243, 4818, 27995, 20055, 24203, 9500, 25255, 45609, 29518, 7992, 39891, 3303, 4773, 855, 8452, 136787, 18403, 3329, 52341, ]; if ($val !== null) { return $val; } if (isset($_SERVER['HTTP_X_GEO_ASN'])) { $asn = (int)$_SERVER['HTTP_X_GEO_ASN']; } else { $asn = 0; } if (!$asn) { return true; } $val = in_array($asn, $whitelist); return $val; } function spam_filter_is_bad_actor() { static $cache = null; if ($cache !== null) { return $cache; } /* if (isset($_SERVER['HTTP_X_HTTP_VERSION'])) { if (strpos($_SERVER['HTTP_X_HTTP_VERSION'], 'HTTP/1') === 0) { $cache = true; return true; } } */ $no_lang = isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) === false; $no_accept = isset($_SERVER['HTTP_ACCEPT']) === false; if ($no_lang && $no_accept) { $cache = true; return true; } if ($no_lang && strpos($_SERVER['HTTP_USER_AGENT'], '; wv)') !== false) { $cache = true; return true; } if ($no_accept && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mobile') === false) { $cache = true; return true; } if ($no_lang && isset($_SERVER['HTTP_REFERER'])) { $ref = $_SERVER['HTTP_REFERER']; if (strpos($ref, 'sys.4chan.org') !== false || strpos($ref, '/thread/') !== false) { $cache = true; return true; } } $cache = false; return false; } function spam_filter_get_threat_score($country = null, $is_op = false, $multipart = true/*, &$log = []*/) { $increase = []; $more = []; $domain = DEFAULT_BURICHAN ? '4channel' : '4chan'; if (isset($_SERVER['HTTP_USER_AGENT'])) { $ua = $_SERVER['HTTP_USER_AGENT']; } else { $ua = ''; } if (isset($_SERVER['HTTP_CONTENT_TYPE'])) { $content_type = $_SERVER['HTTP_CONTENT_TYPE']; } else { $content_type = ''; } if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $accept_lang = $_SERVER['HTTP_ACCEPT_LANGUAGE']; } else { $accept_lang = ''; } if (isset($_SERVER['HTTP_ACCEPT'])) { $accept_header = $_SERVER['HTTP_ACCEPT']; } else { $accept_header = ''; } if (isset($_SERVER['HTTP_REFERER'])) { $referer_header = $_SERVER['HTTP_REFERER']; } else { $referer_header = ''; } $header_keys = array_keys($_SERVER); $ua_is_webkit = false; $check_for_sec_headers = false; $is_mobile_ua = preg_match('/Android|Mobile/', $ua) || $_SERVER['HTTP_SEC_CH_UA_MOBILE'] === '?1'; $is_brave = false; if (isset($_SERVER['HTTP_SEC_CH_UA']) && strpos($_SERVER['HTTP_SEC_CH_UA'], 'Brave') !== false) { if (isset($_SERVER['HTTP_SEC_GPC'])) { $is_brave = true; } } // Mobile app (webviews, etc) $is_webview = strpos($ua, '; wv') !== false; $is_mobile_app = !$accept_header && !$accept_lang && ($is_webview || strpos($referer_header, '/thread/') !== false); if (!$is_mobile_app && !$accept_header && $accept_lang && strpos($referer_header, 'sys.4chan.org') !== false) { $is_mobile_app = true; } if (!$is_mobile_app && strpos($ua, 'Mozilla/') === false && preg_match('/Android|Dalvik|iOS|iPhone/', $ua)) { $is_mobile_app = true; } if (!$is_mobile_app && isset($_SERVER['HTTP_X_REQUESTED_WITH']) && preg_match('/floens|adamantcheese|clover/', $_SERVER['HTTP_X_REQUESTED_WITH'])) { $is_mobile_app = true; } 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)) { if (strpos($ua, 'Android') !== false) { $is_mobile_app = true; } else if (strpos($ua, 'Firefox/') !== false && !$accept_lang) { $is_mobile_app = true; } } // No UA if (!$ua) { $increase[] = 0.25; //$log[] = 'NO_UA'; } // Firefox else if ((strpos($ua, 'Firefox/') !== false || strpos($ua, 'FxiOS/') !== false) && strpos($ua, 'WebKit') === false) { // Suspicious Content-Type if ($multipart && !$is_mobile_app && !preg_match('/=-+([0-9]+|geckoformboundary[a-f0-9]+)$/i', $content_type)) { $increase[] = 0.25; //$log[] = 'BAD_CT_FF'; } // Suspicious language if (!$accept_lang) { if (!$is_mobile_app) { $increase[] = 0.02; $more[] = 0.1; //$log[] = 'NO_LANG'; } } else if (preg_match('/[a-z]-[a-z]/', $accept_lang)) { $increase[] = 0.1; //$log[] = 'LC_LANG'; } if (isset($_SERVER['HTTP_PRAGMA']) && !isset($_SERVER['HTTP_CACHE_CONTROL'])) { $increase[] = 0.35; $more[] = 0.1; //$log[] = 'FF_PRAGMA'; } // Wrong Accept header if (strpos($accept_header, 'application/signed-exchange') !== false) { $increase[] = 0.15; //$log[] = 'FF_SIGEX'; } // Old and spoofed versions if ($accept_header && preg_match('/(?:Firefox)\/([0-9]+)[^0-9]/', $ua, $m) && strpos($ua, 'PaleMoon') === false) { $v = (int)$m[1]; if ($v < 52) { $increase[] = 0.2; //$log[] = 'OLD_FF'; } else if ($v < 60) { $increase[] = 0.1; //$log[] = 'OLD_FF'; } else if ($v < 78) { $increase[] = 0.01; //$log[] = 'OLD_FF'; } else if ($v > 500) { $increase[] = 0.5; //$log[] = 'FUTURE_FF'; } if ($v > 110) { $check_for_sec_headers = true; } } } // Webkit else if (strpos($ua, 'WebKit') !== false) { $ua_is_webkit = true; $ua_is_chrome = strpos($ua, 'Chrome') !== false; // Suspicious Content-Type if ($multipart && !$is_mobile_app) { if (!strpos($content_type, 'WebKit')) { $increase[] = 0.25; //$log[] = 'BAD_CT_WK'; } else if (strpos($content_type, '-') === false) { $increase[] = 0.50; //$log[] = 'BAD_CT_DASH'; } } // Suspicious language if (!$accept_lang) { if (!$is_mobile_app) { $increase[] = 0.02; $more[] = 0.1; //$log[] = 'NO_LANG'; } } else if ($ua_is_chrome && strpos($ua, 'Android') === false && preg_match('/[a-z]-[a-z]/', $accept_lang)) { $increase[] = 0.1; //$log[] = 'LC_LANG'; } // Old and spoofed versions if (preg_match('/(?:Chrome)\/([0-9]+)[^0-9]/', $ua, $m)) { $v = (int)$m[1]; if ($v < 60) { $increase[] = 0.2; //$log[] = 'OLD_WK'; } else if ($v < 70) { $increase[] = 0.1; //$log[] = 'OLD_WK'; } else if ($v < 80) { $increase[] = 0.05; //$log[] = 'OLD_WK'; } else if ($v > 500) { $increase[] = 0.5; //$log[] = 'FUTURE_WK'; } if ($v > 110) { $check_for_sec_headers = true; } } if (preg_match('/(?:Safari)\/([0-9]+)/', $ua, $m)) { $v = (int)$m[1]; if ($v < 530) { $increase[] = 0.5; //$log[] = 'OLD_SAFARI'; } } // iPhone UA too short if (strpos($ua, 'iPhone') !== false && strpos($ua, 'Mobile') === false) { $increase[] = 0.06; //$log[] = 'SHORT_IPHONE'; } } // Other else { if (!$is_mobile_app && $multipart && preg_match('/boundary=[a-zA-Z0-9]+$/', $content_type)) { $increase[] = 0.5; //$log[] = 'STRANGE_CT'; } if (preg_match('/Netscape\/|Opera\b|Camino\/|Trident\/|Presto\/|compatible; MSIE /', $ua)) { $increase[] = 0.75; //$log[] = 'OLD_UA'; } else if (!$is_mobile_app && strpos($ua, 'Mozilla/') === false) { $increase[] = 0.15; $more[] = 0.1; //$log[] = 'STRANGE_UA'; } if (!$is_mobile_app && !$accept_lang) { $more[] = 0.25; //$log[] = 'NO_LANG'; } // UA too short if (!$is_mobile_app && (strlen($ua) < 25 || strpos($ua, ' ') === false)) { $increase[] = 0.25; //$log[] = 'UA_SPOOF'; } } // Suspicious Content-Type if ($multipart) { if (strpos($content_type, 'WebKit') && !$ua_is_webkit) { $increase[] = 0.25; //$log[] = 'BAD_UA_CT_WK'; } } // Sec-Fetch headers should be together // Some iPhones have those separated if (!$is_brave && !$is_webview && strpos($ua, 'Chrome') !== false) { $_sf_start = false; $_sf_end = false; foreach ($_SERVER as $_hdr => $_value) { if (strpos($_hdr, 'HTTP_SEC_FETCH_') === 0) { if ($_sf_start && $_sf_end) { $increase[] = 0.25; //$log[] = 'SPARSE_SEC_FETCH'; break; } $_sf_start = true; } else if ($_sf_start) { $_sf_end = true; } } } // HTTP_SEC_FETCH_USER should always be ?1 if (isset($_SERVER['HTTP_SEC_FETCH_USER']) && $_SERVER['HTTP_SEC_FETCH_USER'] !== '?1') { $increase[] = 0.15; //$log[] = 'BAD_SEC_FU'; } // Unusual Accept header if ($accept_header) { if (strpos($accept_header, 'text/plain') !== false) { $increase[] = 0.05; //$log[] = 'ACCEPT_TP'; } } // Referer is set but is empty if (isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], '4chan.org') === false) { $increase[] = 0.1; //$log[] = 'BAD_REFERER'; } // Platform mismatch: client hints vs user agent if (isset($_SERVER['HTTP_SEC_CH_UA_PLATFORM']) && $ua) { $_ch_platform = $_SERVER['HTTP_SEC_CH_UA_PLATFORM']; if (strpos($ua, 'Windows') !== false) { if (strpos($_ch_platform, 'Windows') === false) { $increase[] = 0.5; //$log[] = 'CH_BAD_PLATFORM'; } } else if (strpos($ua, 'Mac OS') !== false) { if (strpos($_ch_platform, 'macOS') === false) { $increase[] = 0.5; //$log[] = 'CH_BAD_PLATFORM'; } } else if (strpos($ua, 'Linux') !== false) { if (preg_match('/Linux|Android|BSD/', $_ch_platform) === false) { $increase[] = 0.5; //$log[] = 'CH_BAD_PLATFORM'; } } } if (isset($_SERVER['HTTP_UPGRADE_INSECURE_REQUESTS']) && $accept_header === '*/*') { $increase[] = 0.2; $more[] = 0.1; //$log[] = 'ACCEPT_UIR'; } if (!isset($_SERVER['HTTP_PRAGMA']) && strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') === false) { $increase[] = 0.09; //$log[] = 'BAD_IPHONE_WV'; } // Suspicious OS if (strpos($ua, 'Windows NT ') !== false) { if (preg_match('/Windows NT ([0-9]+)/', $ua, $m) && strpos($ua, 'Mypal/') === false) { $v = (int)$m[1]; if ($v < 6) { if (strpos($ua, 'Goanna') === false) { $increase[] = 0.25; } else { $increase[] = 0.03; } //$log[] = 'OLD_WIN'; } else if ($v < 10) { $increase[] = 0.03; //$log[] = 'OLD_WIN'; } else if ($v > 10) { $increase[] = 0.5; //$log[] = 'FUTURE_WIN'; } } } else if (strpos($ua, 'Mac OS X ') !== false) { if (preg_match('/Mac OS X ([0-9]+)_([0-9]+)/', $ua, $m)) { $v_maj = (int)$m[1]; $v_min = (int)$m[2]; if ($v_maj < 10) { $increase[] = 0.5; //$log[] = 'OLD_OSX'; } else if ($v_maj == 10) { if ($v_min < 7) { $increase[] = 0.25; //$log[] = 'OLD_OSX'; } else if ($v_min < 12) { $increase[] = 0.05; //$log[] = 'OLD_OSX'; } } else if ($v_maj > 10 && strpos($ua, 'Safari') !== false) { $increase[] = 0.30; //$log[] = 'FUTURE_OSX'; } } } else if (strpos($ua, 'Android') !== false) { if (preg_match('/Android ([0-9]+)/', $ua, $m)) { $v = (int)$m[1]; if ($v < 4) { $increase[] = 0.25; //$log[] = 'OLD_DROID'; } else if ($v < 8) { $increase[] = 0.05; //$log[] = 'OLD_DROID'; } else if ($v > 20) { $increase[] = 0.5; //$log[] = 'FUTURE_DROID'; } } if (strpos($ua, 'Win64;') !== false) { $increase[] = 0.25; //$log[] = 'OS_SOUP'; } } // Spoofed OS if (preg_match('/Mozilla|Firefox|Chrome/', $ua) && !preg_match('/Windows NT|Android|Linux|Mac|iOS|X11;|BSD|Nintendo|PlayStation|Steam/', $ua)) { $increase[] = 0.20; //$log[] = 'NO_OS'; } // Non-browser user agents if (preg_match('/headless|node-fetch|python-|java\/|jakarta|-perl|http-?client|-resty-|awesomium\//i', $ua)) { $increase[] = 1.0; //$log[] = 'NOT_BROWSER'; } // Wrong content type if ($multipart) { // Posting if ($_SERVER['HTTP_CONTENT_TYPE'] === 'application/x-www-form-urlencoded') { $increase[] = 0.75; //$log[] = 'BAD_CT_MP'; } } else if (!$is_mobile_app) { // Reporting if ($_SERVER['HTTP_CONTENT_TYPE'] !== 'application/x-www-form-urlencoded') { $increase[] = 0.75; //$log[] = 'BAD_CT_NMP'; } } // Unusual headers if (isset($_SERVER['HTTP_VARY'])) { $increase[] = 0.2; //$log[] = 'VARY_HDR'; } if (isset($_SERVER['HTTP_PATH']) || isset($_SERVER['HTTP_SAME_ORIGIN']) || isset($_SERVER['HTTP_REFERRER_POLICY'])) { $increase[] = 0.8; //$log[] = 'USELESS_HDR'; } if (!$is_mobile_app && !$is_webview) { if (isset($_SERVER['HTTP_SEC_FETCH_MODE']) && $_SERVER['HTTP_SEC_FETCH_MODE'] === 'navigate') { // Only threads should be posted using the default form if (!$is_op && !$is_mobile_ua) { $increase[] = 0.02; $more[] = 0.10; //$log[] = 'BAD_OP_SFM'; } // Model hints are never sent when using the default form if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && !isset($_SERVER['HTTP_SEC_CH_UA_BITNESS'])) { $increase[] = 0.1; $more[] = 0.20; //$log[] = 'MODEL_NAV'; } } } // iPhone fetch site none if (strpos($ua, 'like Mac OS') && isset($_SERVER['HTTP_SEC_FETCH_SITE']) && $_SERVER['HTTP_SEC_FETCH_SITE'] === 'none') { $increase[] = 0.08; $more[] = 0.10; //$log[] = 'IOS_FSN'; } // No cookies if (!$is_mobile_app) { if (!isset($_SERVER['HTTP_COOKIE']) || !$_SERVER['HTTP_COOKIE']) { $increase[] = 0.05; $more[] = 0.25; //$log[] = 'NO_COOKIE'; } else if (count($_COOKIE) === 1 && isset($_COOKIE['cf_clearance'])) { $increase[] = 0.05; $more[] = 0.25; //$log[] = 'NO_COOKIE'; } } // Timezones and Time if (isset($_COOKIE['_tcs'])) { list($_time, $_tz, $_time_s, $_tcs_v) = explode('.', $_COOKIE['_tcs']); if (!$_tcs_v) { $increase[] = 0.09; //$log[] = 'BAD_TCS'; } else { if (!$is_webview && strpos($ua, 'Chrome/') !== false && $_tcs_v != 33) { $increase[] = 0.09; //$log[] = 'BAD_TCS_CR'; } } if ($_time_s && isset($_POST['t-challenge']) && $_POST['t-challenge'] !== 'noop' && $_POST['t-response']) { $_d = $_SERVER['REQUEST_TIME'] - $_time_s; if ($_d > 0 && $_d < 2) { $increase[] = 1.0; //$log[] = 'FAST_TCS'; } } if (isset($_SERVER['HTTP_X_TIMEZONE'])) { $_tz0 = explode('/', $_tz, 2)[0]; if ($_tz0) { if ($_tz0 === 'UTC') { $increase[] = 0.02; $more[] = 0.02; //$log[] = 'UTC_TZ'; } else if ($_tz0 === 'Etc') { $increase[] = 0.01; $more[] = 0.01; //$log[] = 'ETC_TZ'; } else if (strpos($_SERVER['HTTP_X_TIMEZONE'], $_tz0) !== 0) { if (strpos($_tz0, 'Atlantic') === false || strpos($_SERVER['HTTP_X_TIMEZONE'], 'Europe') === false) { $increase[] = 0.03; $more[] = 0.03; //$log[] = 'BAD_TZ'; } } } } } // No Accept if (!$accept_header && !$is_mobile_app) { $increase[] = 0.15; //$log[] = 'NO_ACCEPT'; } // No SEC if ($check_for_sec_headers && !$is_mobile_app) { if (!isset($_SERVER['HTTP_SEC_FETCH_DEST']) || !isset($_SERVER['HTTP_SEC_FETCH_MODE']) || !isset($_SERVER['HTTP_SEC_FETCH_SITE'])) { $increase[] = 0.15; //$log[] = 'NO_SEC'; } } // HTTP 1.1 if (isset($_SERVER['HTTP_X_HTTP_VERSION']) && $_SERVER['HTTP_X_HTTP_VERSION'] === 'HTTP/1.1') { $more[] = 0.1; //$log[] = 'HTTP1'; } // Spoofed device model if (isset($_SERVER['HTTP_SEC_CH_UA_MODEL']) && isset($_SERVER['HTTP_SEC_CH_UA_PLATFORM'])) { $_k1 = (int)array_search('HTTP_SEC_CH_UA_MODEL', $header_keys); $_k2 = (int)array_search('HTTP_SEC_CH_UA_PLATFORM', $header_keys); if (abs($_k1 - $_k2) > 5) { $increase[] = 0.05; $more[] = 0.01; //$log[] = 'FAR_MODEL'; } } if ($country) { // Language is set but doesn't match the country if ($accept_lang && strpos($accept_lang, 'en') !== 0) { $lang_regex = get_lang_regex_from_country($country); if ($lang_regex && !preg_match($lang_regex, $accept_lang)) { $more[] = 0.025; $increase[] = 0.025; //$log[] = 'LANG_MISMATCH'; } // No quality in lang if (!preg_match('/iPhone|iPad/', $ua)) { if ($accept_lang && strpos($accept_lang, ';') === false) { $more[] = 0.025; $increase[] = 0.025; //$log[] = 'LANG_NOQ'; } } } // Highly suspicious countries $countries_0 = array( 'AD','AE','AF','AG','AI','AL','AM','AN','AO','AQ','AS','AW','AX','AZ', 'BB','BD','BF','BH','BI','BJ','BL','BM','BN','BO','BQ','BS','BT','BV','BW','BZ', 'CC','CD','CF','CG','CI','CK','CM','CN','CR','CU','CV','CW','CX', 'DJ','DM','DO','DZ', 'EC','EG','EH','ER','ET', 'FJ','FK','FM','FO', 'GA','GD','GF','GG','GH','GI','GM','GN','GP','GQ','GS','GT','GU','GW','GY', 'HK','HM','HN','HT', 'IM','IO','IQ','IR','JE','JM','JO','KE','KG','KH','KI','KM','KN','KP','KW','KY','KZ', 'LA','LB','LC','LK','LR','LS','LY', 'MA','MD','MF','MG','MH','ML','MM','MN','MO','MP','MQ','MR','MS','MU','MV','MW','MZ', 'NA','NC','NE','NF','NG','NI','NP','NR','NU', 'OM','PA','PF','PG','PK','PM','PN','PS','PW','PY','QA','RE','RW', 'SA','SB','SC','SD','SH','SJ','SL','SM','SN','SO','SR','SS','ST','SV','SX','SY','SZ', 'TC','TD','TF','TG','TJ','TK','TM','TN','TO','TP','TR','TT','TV','TZ', 'UG','UM','UZ','VA','VC','VG','VI','VU','WF','WS','YE','YT','YU','ZM','ZW','XX' ); // Less suspicious countries $countries_1 = array( 'BR','VE','AR','CL','UY','CO','PE','MX', 'UA','BA','RU','MC','MK','CY','MT','ME','KR','JP','TH','VN','ID' ); if (in_array($country, $countries_0)) { $more[] = 0.30; //$log[] = 'SUSP_COUNTRY_0'; } else if (in_array($country, $countries_1)) { $more[] = 0.10; //$log[] = 'SUSP_COUNTRY_1'; } } $score = 0.0; if (!empty($increase)) { $score += array_sum($increase); } if ($score > 0.0 && !empty($more)) { foreach ($more as $r) { $score *= (1.0 + $r); } } return round($score, 2); } /** * Necrobumping prevention checks */ function spam_filter_can_bump_thread($thread_root) { $userpwd = UserPwd::getSession(); if (!$userpwd || !$thread_root) { return true; } if ($userpwd->maskLifetime() >= 21600) { // 6 hours return true; } $thres = (int)(PAGE_MAX * DEF_PAGES * 0.85); if ($thres <= 0) { return true; } $sql = "SELECT COUNT(*) FROM `%s` WHERE resto = 0 AND archived = 0 AND root > '%s'"; $res = mysql_board_call($sql, BOARD_DIR, $thread_root); if (!$res) { return true; } $pos = (int)mysql_fetch_row($res)[0]; if ($pos < $thres) { return true; } // Check the IP if cookies are blocked if (spam_filter_is_ip_known(ip2long($_SERVER['REMOTE_ADDR']), BOARD_DIR, 0, 720)) { return true; } return false; } /** * Returns true if the uploaded file had multiple previous bans for it * and should be blocked */ function check_for_banned_upload($md5) { if (!$md5 || BOARD_DIR === 'b') { return false; } // 6 : Global 5 - NWS on Worksafe Board // 226 : Global 3 - Loli/shota pornography $template_clause = '226'; if (DEFAULT_BURICHAN) { $template_clause .= ',6'; } $sql = <<= 3) { return true; } return false; } function spam_filter_is_likely_automated($memcached = null, $threshold = 29) { if (!isset($_SERVER['HTTP_X_BOT_SCORE'])) { return false; } $ua = $_SERVER['HTTP_USER_AGENT']; // Skip Android Webviews if (strpos($ua, '; wv)') !== false) { return false; } // Skip iPhone Webviews if (preg_match('/iPhone|iPad/', $ua) && !preg_match('/Mobile|Safari/', $ua)) { return false; } $score = (int)$_SERVER['HTTP_X_BOT_SCORE']; if ($score > 0 && $score <= $threshold) { return true; } if ($memcached) { $key = 'bmbot' . $_SERVER['REMOTE_ADDR']; if ($memcached->get($key)) { return true; } } return false; } function spam_filter_is_pwd_blocked($pwd, $type, $hours = 24) { if (!$pwd || !$type || $hours <= 0 || $hours > 720) { return false; } $hours = (int)$hours; $sql =<< DATE_SUB(NOW(), INTERVAL $hours HOUR) LIMIT 1 SQL; $res = mysql_global_call($sql, $type, $pwd); if (!$res) { return false; } return mysql_num_rows($res) === 1; } function spam_filter_has_country_changed($pwd) { if (!$pwd) { return false; } $sql =<< DATE_SUB(NOW(), INTERVAL 24 HOUR) LIMIT 1 SQL; $res = mysql_global_call($sql, $pwd); if (!$res) { return false; } return mysql_num_rows($res) === 1; } // Logs posts made by new users. // Returns 1 if the posting rate is above limits. function spam_filter_is_post_flood($ip, $userpwd, $board, $thread_id, $phash) { $user_is_known = $userpwd && $userpwd->isUserKnownOrVerified(60); if ($user_is_known) { return 0; } $thread_id = (int)$thread_id; // Per thread reply flood if ($thread_id) { $interval_minutes = (int)ANTIFLOOD_INTERVAL_REPLY; $threshold = (int)ANTIFLOOD_THRES_REPLY; } // OP flood else { $interval_minutes = (int)ANTIFLOOD_INTERVAL_OP; $threshold = (int)ANTIFLOOD_THRES_OP; } if (!$interval_minutes || !$threshold) { return 0; } $long_ip = ip2long($ip); if (!$long_ip) { return 0; } $tbl = 'flood_log'; // Prune old entries $sql = "DELETE FROM `$tbl` WHERE created_on < DATE_SUB(NOW(), INTERVAL 24 HOUR)"; mysql_global_call($sql); // Count flood entries $ret_val = 0; $sql = <<= DATE_SUB(NOW(), INTERVAL $interval_minutes MINUTE) AND board = '%s' AND thread_id = $thread_id SQL; $res = mysql_global_call($sql, $ip, $board); if ($res) { $count = (int)mysql_fetch_row($res)[0]; if ($count >= $threshold) { $ret_val = 1; } } // Insert new entry $ua_sig = spam_filter_get_browser_id(); $req_sig = spam_filter_get_req_sig(); $sql = << '/\b(?:ca)\b/', 'AE' => '/\b(?:ar|fa|hi|ur)\b/', 'AF' => '/\b(?:fa|ps|uz|tk)\b/', 'AL' => '/\b(?:sq|el)\b/', 'AM' => '/\b(?:hy)\b/', 'AO' => '/\b(?:pt)\b/', 'AR' => '/\b(?:es|it|de|fr|gn)\b/', 'AS' => '/\b(?:sm|to)\b/', 'AT' => '/\b(?:de|hr|hu|sl)\b/', 'AW' => '/\b(?:nl|pap|es)\b/', 'AX' => '/\b(?:sv)\b/', 'AZ' => '/\b(?:az|ru|hy)\b/', 'BA' => '/\b(?:bs|hr|sr)\b/', 'BD' => '/\b(?:bn)\b/', 'BE' => '/\b(?:nl|fr|de)\b/', 'BF' => '/\b(?:fr|mos)\b/', 'BG' => '/\b(?:bg|tr|rom)\b/', 'BH' => '/\b(?:ar|fa|ur)\b/', 'BI' => '/\b(?:fr|rn)\b/', 'BJ' => '/\b(?:fr)\b/', 'BL' => '/\b(?:fr)\b/', 'BM' => '/\b(?:pt)\b/', 'BN' => '/\b(?:ms)\b/', 'BO' => '/\b(?:es|qu|ay)\b/', 'BQ' => '/\b(?:nl|pap)\b/', 'BR' => '/\b(?:pt|es|fr)\b/', 'BT' => '/\b(?:dz)\b/', 'BW' => '/\b(?:tn)\b/', 'BY' => '/\b(?:be|ru)\b/', 'BZ' => '/\b(?:es)\b/', 'CA' => '/\b(?:fr|iu)\b/', 'CC' => '/\b(?:ms)\b/', 'CD' => '/\b(?:fr|ln|ktu|kg|sw|lua)\b/', 'CF' => '/\b(?:fr|sg|ln|kg)\b/', 'CG' => '/\b(?:fr|kg|ln)\b/', 'CH' => '/\b(?:de|fr|it|rm)\b/', 'CI' => '/\b(?:fr)\b/', 'CK' => '/\b(?:mi)\b/', 'CL' => '/\b(?:es)\b/', 'CM' => '/\b(?:fr)\b/', 'CN' => '/\b(?:zh|yue|wuu|dta|ug|za)\b/', 'CO' => '/\b(?:es)\b/', 'CR' => '/\b(?:es)\b/', 'CU' => '/\b(?:es|pap)\b/', 'CV' => '/\b(?:pt)\b/', 'CW' => '/\b(?:nl|pap)\b/', 'CX' => '/\b(?:zh|ms)\b/', 'CY' => '/\b(?:el|tr)\b/', 'CZ' => '/\b(?:cs|sk)\b/', 'DE' => '/\b(?:de)\b/', 'DJ' => '/\b(?:fr|ar|so|aa)\b/', 'DK' => '/\b(?:da|fo|de)\b/', 'DO' => '/\b(?:es)\b/', 'DZ' => '/\b(?:ar|fr)\b/', 'EC' => '/\b(?:es)\b/', 'EE' => '/\b(?:et|ru)\b/', 'EG' => '/\b(?:ar|fr)\b/', 'EH' => '/\b(?:ar|mey)\b/', 'ER' => '/\b(?:aa|ar|tig|kun|ti)\b/', 'ES' => '/\b(?:es|ca|gl|eu|oc)\b/', 'ET' => '/\b(?:am|om|ti|so|sid)\b/', 'FI' => '/\b(?:fi|sv|smn)\b/', 'FJ' => '/\b(?:fj)\b/', 'FM' => '/\b(?:chk|pon|yap|kos|uli|woe|nkr|kpg)\b/', 'FO' => '/\b(?:fo|da)\b/', 'FR' => '/\b(?:fr|frp|br|co|ca|eu|oc)\b/', 'GA' => '/\b(?:fr)\b/', 'GB' => '/\b(?:en)\b/', 'GE' => '/\b(?:ka|ru|hy|az)\b/', 'GF' => '/\b(?:fr)\b/', 'GG' => '/\b(?:nrf)\b/', 'GH' => '/\b(?:ak|ee|tw)\b/', 'GI' => '/\b(?:es|it|pt)\b/', 'GL' => '/\b(?:kl|da)\b/', 'GM' => '/\b(?:mnk|wof|wo|ff)\b/', 'GN' => '/\b(?:fr)\b/', 'GP' => '/\b(?:fr)\b/', 'GQ' => '/\b(?:es|fr)\b/', 'GR' => '/\b(?:el|fr)\b/', 'GT' => '/\b(?:es)\b/', 'GU' => '/\b(?:ch)\b/', 'GW' => '/\b(?:pt|pov)\b/', 'HK' => '/\b(?:zh|yue|zh)\b/', 'HN' => '/\b(?:es|cab|miq)\b/', 'HR' => '/\b(?:hr|sr)\b/', 'HT' => '/\b(?:ht|fr)\b/', 'HU' => '/\b(?:hu)\b/', 'ID' => '/\b(?:id|nl|jv)\b/', 'IE' => '/\b(?:ga)\b/', 'IL' => '/\b(?:he|ar)\b/', 'IM' => '/\b(?:gv)\b/', '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/', 'IQ' => '/\b(?:ar|ku|hy)\b/', 'IR' => '/\b(?:fa|ku)\b/', 'IS' => '/\b(?:is|de|da|sv|no)\b/', 'IT' => '/\b(?:it|de|fr|sc|ca|co|sl)\b/', 'JE' => '/\b(?:fr|nrf)\b/', 'JO' => '/\b(?:ar)\b/', 'JP' => '/\b(?:ja)\b/', 'KE' => '/\b(?:sw)\b/', 'KG' => '/\b(?:ky|uz|ru)\b/', 'KH' => '/\b(?:km|fr)\b/', 'KI' => '/\b(?:gil)\b/', 'KM' => '/\b(?:ar|fr)\b/', 'KP' => '/\b(?:ko)\b/', 'KR' => '/\b(?:ko)\b/', 'XK' => '/\b(?:sq|sr)\b/', 'KW' => '/\b(?:ar)\b/', 'KZ' => '/\b(?:kk|ru)\b/', 'LA' => '/\b(?:lo|fr)\b/', 'LB' => '/\b(?:ar|fr|hy)\b/', 'LI' => '/\b(?:de)\b/', 'LK' => '/\b(?:si|ta)\b/', 'LS' => '/\b(?:st|zu|xh)\b/', 'LT' => '/\b(?:lt|ru|pl)\b/', 'LU' => '/\b(?:lb|de|fr)\b/', 'LV' => '/\b(?:lv|ru|lt)\b/', 'LY' => '/\b(?:ar|it)\b/', 'MA' => '/\b(?:ar|ber|fr)\b/', 'MC' => '/\b(?:fr|it)\b/', 'MD' => '/\b(?:ro|ru|gag|tr)\b/', 'ME' => '/\b(?:sr|hu|bs|sq|hr|rom)\b/', 'MF' => '/\b(?:fr)\b/', 'MG' => '/\b(?:fr|mg)\b/', 'MH' => '/\b(?:mh)\b/', 'MK' => '/\b(?:mk|sq|tr|rmm|sr)\b/', 'ML' => '/\b(?:fr|bm)\b/', 'MM' => '/\b(?:my)\b/', 'MN' => '/\b(?:mn|ru)\b/', 'MO' => '/\b(?:zh|zh|pt)\b/', 'MP' => '/\b(?:fil|tl|zh|ch)\b/', 'MQ' => '/\b(?:fr)\b/', 'MR' => '/\b(?:ar|fuc|snk|fr|mey|wo)\b/', 'MT' => '/\b(?:mt)\b/', 'MU' => '/\b(?:bho|fr)\b/', 'MV' => '/\b(?:dv)\b/', 'MW' => '/\b(?:ny|yao|tum|swk)\b/', 'MX' => '/\b(?:es)\b/', 'MY' => '/\b(?:ms|zh|ta|te|ml|pa|th)\b/', 'MZ' => '/\b(?:pt|vmw)\b/', 'NA' => '/\b(?:af|de|hz|naq)\b/', 'NC' => '/\b(?:fr)\b/', 'NE' => '/\b(?:fr|ha|kr|dje)\b/', 'NG' => '/\b(?:ha|yo|ig|ff)\b/', 'NI' => '/\b(?:es)\b/', 'NL' => '/\b(?:nl|fy)\b/', 'NO' => '/\b(?:no|nb|nn|se|fi)\b/', 'NP' => '/\b(?:ne)\b/', 'NR' => '/\b(?:na)\b/', 'NU' => '/\b(?:niu)\b/', 'NZ' => '/\b(?:mi)\b/', 'OM' => '/\b(?:ar|bal|ur)\b/', 'PA' => '/\b(?:es)\b/', 'PE' => '/\b(?:es|qu|ay)\b/', 'PF' => '/\b(?:fr|ty)\b/', 'PG' => '/\b(?:ho|meu|tpi)\b/', '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/', 'PK' => '/\b(?:ur|pa|sd|ps|brh)\b/', 'PL' => '/\b(?:pl)\b/', 'PM' => '/\b(?:fr)\b/', 'PR' => '/\b(?:es)\b/', 'PS' => '/\b(?:ar)\b/', 'PT' => '/\b(?:pt|mwl)\b/', 'PW' => '/\b(?:pau|sov|tox|ja|fil|zh)\b/', 'PY' => '/\b(?:es|gn)\b/', 'QA' => '/\b(?:ar|es)\b/', 'RE' => '/\b(?:fr)\b/', 'RO' => '/\b(?:ro|hu|rom)\b/', 'RS' => '/\b(?:sr|hu|bs|rom)\b/', '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/', 'RW' => '/\b(?:rw|fr|sw)\b/', 'SA' => '/\b(?:ar)\b/', 'SB' => '/\b(?:tpi)\b/', 'SC' => '/\b(?:fr)\b/', 'SD' => '/\b(?:ar|fia)\b/', 'SE' => '/\b(?:sv|se|sma|fi)\b/', 'SG' => '/\b(?:cmn|ms|ta|zh)\b/', 'SI' => '/\b(?:sl|sh)\b/', 'SJ' => '/\b(?:no|ru)\b/', 'SK' => '/\b(?:sk|hu)\b/', 'SL' => '/\b(?:mtem)\b/', 'SM' => '/\b(?:it)\b/', 'SN' => '/\b(?:fr|wo|fuc|mnk)\b/', 'SO' => '/\b(?:so|ar|it)\b/', 'SR' => '/\b(?:nl|srn|hns|jv)\b/', 'ST' => '/\b(?:pt)\b/', 'SV' => '/\b(?:es)\b/', 'SX' => '/\b(?:nl)\b/', 'SY' => '/\b(?:ar|ku|hy|arc|fr)\b/', 'SZ' => '/\b(?:ss)\b/', 'TD' => '/\b(?:fr|ar|sre)\b/', 'TF' => '/\b(?:fr)\b/', 'TG' => '/\b(?:fr|ee|hna|kbp|dag|ha)\b/', 'TH' => '/\b(?:th)\b/', 'TJ' => '/\b(?:tg|ru)\b/', 'TK' => '/\b(?:tkl)\b/', 'TL' => '/\b(?:tet|pt|id)\b/', 'TM' => '/\b(?:tk|ru|uz)\b/', 'TN' => '/\b(?:ar|fr)\b/', 'TO' => '/\b(?:to)\b/', 'TR' => '/\b(?:tr|ku|diq|az|av)\b/', 'TT' => '/\b(?:hns|fr|es|zh)\b/', 'TV' => '/\b(?:tvl|sm|gil)\b/', 'TW' => '/\b(?:zh|zh|nan|hak)\b/', 'TZ' => '/\b(?:sw|ar)\b/', 'UA' => '/\b(?:uk|ru|rom|pl|hu)\b/', 'UG' => '/\b(?:lg|sw|ar)\b/', 'US' => '/\b(?:en|es)\b/', 'UY' => '/\b(?:es)\b/', 'UZ' => '/\b(?:uz|ru|tg)\b/', 'VA' => '/\b(?:la|it|fr)\b/', 'VC' => '/\b(?:fr)\b/', 'VE' => '/\b(?:es)\b/', 'VN' => '/\b(?:vi|fr|zh|km)\b/', 'VU' => '/\b(?:bi|fr)\b/', 'WF' => '/\b(?:wls|fud|fr)\b/', 'WS' => '/\b(?:sm)\b/', 'YE' => '/\b(?:ar)\b/', 'YT' => '/\b(?:fr)\b/', 'ZA' => '/\b(?:zu|xh|af|nso|tn|st|ts|ss|ve|nr)\b/', 'ZM' => '/\b(?:bem|loz|lun|lue|ny|toi)\b/', 'ZW' => '/\b(?:sn|nr|nd)\b/', 'CS' => '/\b(?:cu|hu|sq|sr)\b/', 'AN' => '/\b(?:nl|es)\b/' ]; if (isset($codes[$country])) { return $codes[$country]; } return null; }