admin-test.php
1 <? 2 require_once 'db.php'; 3 require_once 'rpc.php'; 4 5 if( !defined( "SQLLOGMOD" ) ) { 6 define( 'SQLLOGBAN', 'banned_users' ); // FIXME move to config_db.php? 7 define( 'SQLLOGMOD', 'mod_users' ); 8 } 9 10 // Parses the "email" field and returns a hash 11 function decode_user_meta($data) { 12 if (!$data) { 13 return []; 14 } 15 16 $data = explode(':', $data); 17 18 $fields = []; 19 20 $fields['browser_id'] = $data[0]; 21 $fields['is_mobile'] = $data[0] && $data[0][0] === '1'; 22 $fields['req_sig'] = $data[1]; 23 24 $fields['known_status'] = (int)$data[2]; 25 $fields['verified_level'] = (int)$data[3]; 26 27 return $fields; 28 } 29 30 function user_known_status_to_str($status) { 31 if ($status === 0) { 32 return 'Trusted'; 33 } 34 else if ($status === 1) { 35 return 'New'; 36 } 37 else if ($status === 2) { 38 return 'Recent'; 39 } 40 else if ($status === 3) { 41 return 'Regular'; 42 } 43 44 return 'N/A'; 45 } 46 47 // Encodes email field data for storage in the database 48 // Entries are separates by ":" 49 // known status: 1 = new user, 2 unknown user 50 function encode_user_meta($browser_id, $req_sig, $userpwd) { 51 // Default status is Trusted - above 7 days and 20 posts 52 $known_status = 0; 53 54 $verified_level = 0; 55 56 if ($userpwd) { 57 $post_count = $userpwd->postCount(); 58 59 // New - below 1 hour and 1 post 60 if (!$userpwd->isUserKnown(60, 1) || $post_count < 1) { 61 $known_status = 1; 62 } 63 // Recent - above 1h and 1 post / below 3h and 6 posts 64 else if (!$userpwd->isUserKnown(4320) || $post_count < 6) { 65 $known_status = 2; 66 } 67 // Regular - above 3 days and 5 posts / below 7 days and 21 posts 68 else if (!$userpwd->isUserKnown(10080) || $post_count < 21) { 69 $known_status = 3; 70 } 71 72 if ($userpwd->verifiedLevel()) { 73 $verified_level = 1; 74 } 75 } 76 77 $data = [ $browser_id, $req_sig, $known_status, $verified_level ]; 78 $data = implode(':', $data); 79 80 return $data; 81 } 82 83 function _grep_notjanitor( $a ) 84 { 85 return ( $a != 'janitor' ); 86 } 87 88 function get_random_string( $len = 16 ) 89 { 90 $str = mt_rand( 1000000, 9999999 ); 91 $str = hash( 'sha256', $str ); 92 93 return substr( $str, -$len ); 94 } 95 96 function derefer_url($url) { 97 return 'https://www.4chan.org/derefer?url=' . rawurlencode($url); 98 } 99 100 function access_check() 101 { 102 global $access; 103 104 $user = $_COOKIE['4chan_auser']; 105 $pass = $_COOKIE['apass']; 106 107 if( !$user || !$pass ) return; 108 109 $query = mysql_global_call( "SELECT allow,password_expired,level,flags,username,password,signed_agreement FROM mod_users WHERE username='%s' LIMIT 1", $user ); 110 111 if (!mysql_num_rows($query)) { 112 return ''; 113 } 114 115 list($allow, $expired, $level, $flags, $username, $password, $signed_agreement) = mysql_fetch_row($query); 116 117 $admin_salt = file_get_contents('/www/keys/2014_admin.salt'); 118 119 if (!$admin_salt) { 120 die('Internal Server Error (s0)'); 121 } 122 123 $hashed_admin_password = hash('sha256', $username . $password . $admin_salt); 124 125 if ($hashed_admin_password !== $pass) { 126 return ''; 127 } 128 129 if( $expired ) { 130 die( 'Your password has expired; check IRC for instructions on changing it.' ); 131 } 132 133 if ($signed_agreement == 0 && basename($_SERVER['SELF_PATH']) !== 'agreement.php') { 134 die('You must agree to the 4chan Volunteer Moderator Agreement in order to access moderation tools. Please check your e-mail for more information.'); 135 } 136 137 if( $allow ) { 138 if( $level == 'janitor' ) { 139 $a = $access['janitor']; 140 $a['board'] = array_filter( explode( ',', $allow ), '_grep_notjanitor' ); 141 if( in_array( "all", $a['board'] ) ) 142 unset( $a['board'] ); 143 144 return $a; 145 } elseif( $level == 'manager' ) { 146 return $access['manager']; 147 } elseif( $level == 'admin' ) { 148 return $access['admin']; 149 } elseif( $level == 'mod' ) { 150 if (is_array($access['mod'])) { 151 $flags = explode(',', $flags); 152 $access['mod']['is_developer'] = in_array('developer', $flags); 153 } 154 return $access['mod']; 155 } else { 156 die( 'oh no you are not a right user!' ); 157 } 158 } else { 159 return ''; 160 } 161 } 162 163 //based on team pages' valid(), need to merge with above! 164 //this sets different globals and respects deny 165 function access_check2( $func = 0 ) 166 { 167 global $is_admin, $user, $pass; 168 $is_admin = 0; 169 $user = ""; 170 $pass = ""; 171 if( isset( $_COOKIE['4chan_auser'] ) && isset( $_COOKIE['4chan_apass'] ) ) { 172 $user = $_COOKIE['4chan_auser']; 173 $pass = $_COOKIE['4chan_apass']; 174 } 175 if( isset( $user ) && $user && $pass ) { 176 $result = mysql_global_call( "SELECT allow,deny,password_expired FROM " . SQLLOGMOD . " WHERE username='%s' and password='%s' limit 1", $user, $pass ); 177 if( mysql_num_rows( $result ) != 0 ) { 178 list( $allowed, $denied, $expired ) = mysql_fetch_array( $result ); 179 if( $expired ) { 180 die( 'Your password has expired; check IRC for instructions on changing it.' ); 181 } 182 if( $func == "unban" ) { 183 $deny_arr = explode( ",", $denied ); 184 if( in_array( "unban", $deny_arr ) ) die( "You do not have access to unban users." ); 185 } 186 $allow_arr = explode( ",", $allowed ); 187 if( in_array( "admin", $allow_arr ) || in_array( "manager", $allow_arr ) ) $is_admin = 1; 188 } else { 189 die( "Please login via admin panel first. (admin user not found)" ); 190 } 191 if( $user && !$pass ) { 192 die( "Please login via admin panel first. (no pass specified)" ); 193 } elseif( !$user && $pass ) { 194 die( "Please login via admin panel first. (no user specified)" ); 195 } 196 } else { 197 die( "Please login via admin panel first." ); 198 } 199 } 200 201 function form_post_values( $names ) 202 { 203 $a = array(); 204 205 foreach( $names as $n ) { 206 $v = $_REQUEST[$n]; 207 if( $v ) $a[$n] = $v; 208 } 209 210 return $a; 211 } 212 213 //rebuild the bans for board $boards 214 function rebuild_bans( $boards ) 215 { 216 // run in background 217 $cmd = "nohup /usr/local/bin/suid_run_global bin/rebuildbans $boards >/dev/null 2>&1 &"; 218 // print "<br>Rebuilding bans in $boards<br>"; 219 exec( $cmd ); 220 } 221 222 //add list of bans to the file for $boards 223 function append_bans( $boards, $bans ) 224 { 225 $str = is_array( $bans ) ? implode( ",", $bans ) : $bans; 226 $cmd = "nohup /usr/local/bin/suid_run_global bin/appendban $boards $str >/dev/null 2>&1 &"; 227 // print "<br>Added new bans to $boards<br>"; 228 exec( $cmd ); 229 } 230 231 // IPs that can't be banned because they're known good proxy servers 232 // e.g. cloudflare, singapore 233 function whitelisted_ip( $ip = 0 ) 234 { 235 list( $ips ) = post_filter_get( "ipwhitelist" ); 236 if( $ip === 0 ) $ip = $_SERVER["REMOTE_ADDR"]; 237 238 return find_ipxff_in( ip2long( $ip ), 0, $ips ); 239 } 240 241 // add a global ban (indefinite for now) 242 // returns true if it was new (not already inserted) 243 function add_ban( $ip, $reason, $days = -1, $zonly = false, $origname = 'Anonymous', &$error, $no = 0, $pass = '', $no_reverse = false ) 244 { 245 global $user; 246 if( ip2long( $ip ) === false ) { 247 $error = "invalid IP address"; 248 249 return false; 250 } 251 if( whitelisted_ip( $ip ) ) { 252 $error = "IP is whitelisted"; 253 254 return false; 255 } 256 257 // FIXME add unique index to banned_users instead 258 $prev = mysql_global_call( "SELECT COUNT(*)>0 FROM " . SQLLOGBAN . " WHERE active=1 AND global=1 AND host='%s'", $ip ); 259 list( $nprev ) = mysql_fetch_array( $prev ); 260 if( $nprev > 0 ) return false; 261 262 if ($no_reverse) { 263 $rev = $ip; 264 } 265 else { 266 $rev = gethostbyaddr( $ip ); 267 } 268 269 $tripcode = ''; 270 271 $name_bits = explode('</span> <span class="postertrip">!', $origname); 272 273 if ($name_bits[1]) { 274 $tripcode = preg_replace('/<[^>]+>/', '', $name_bits[1]); 275 } 276 277 $origname = str_replace( '</span> <span class="postertrip">!', ' #', $origname ); 278 $origname = preg_replace( '/<[^>]+>/', '', $origname ); // remove all remaining html crap 279 280 $board = defined( 'BOARD_DIR' ) ? BOARD_DIR : ""; 281 282 if( $days == -1 ) 283 $length = "00000000000000"; 284 else 285 $length = date( "Ymd", time() + $days * ( 24 * 60 * 60 ) ) . '000000'; 286 287 echo "Banned $ip (" . htmlspecialchars( $rev ) . ")<br>\n"; 288 289 if (!isset($user)) { 290 $banned_by = $_COOKIE['4chan_auser']; 291 } 292 else { 293 $banned_by = $user; 294 } 295 296 mysql_global_do( "INSERT INTO " . SQLLOGBAN . " (global,board,host,reverse,reason,admin,zonly,length,name,tripcode,4pass_id,post_num,admin_ip) values (%d,'%s','%s','%s','%s','%s',%d,'%s','%s','%s','%s',%d,'%s')", !$zonly, $board, $ip, $rev, "$reason", $banned_by, $zonly, $length, $origname, $tripcode, $pass, $no, $_SERVER['REMOTE_ADDR'] ); 297 298 return true; 299 } 300 301 function is_real_board( $board ) 302 { 303 // no board 304 if( $board === "-" || $board === '' ) return true; 305 306 $res = mysql_global_call( "select count(*) from boardlist where dir='%s'", $board ); 307 $row = mysql_fetch_row( $res ); 308 309 return ( $row[0] > 0 ); 310 } 311 312 function remote_delete_things( $board, $nos, $tool = null ) 313 { 314 // see reports/actions.php, action_delete() 315 $url = "https://sys.int/$board/"; 316 317 if( $board != 'f' ) // XXX dumb. :( XXX 318 $url .= 'imgboard.php'; 319 else 320 $url .= 'up.php'; 321 322 // Build the appropriate POST and cookie... 323 $post = array(); 324 $post['mode'] = 'usrdel'; 325 $post['onlyimgdel'] = ''; // never delete only img 326 327 if ($tool) { 328 $post['tool'] = $tool; 329 } 330 331 // note multiple post number deletions 332 foreach( $nos as $no ) 333 $post[$no] = 'delete'; 334 335 $post['remote_addr'] = $_SERVER['REMOTE_ADDR']; 336 337 rpc_start_request($url, $post, $_COOKIE, true); 338 339 return ""; 340 } 341 342 function clear_cookies() 343 { 344 if( strstr( $_SERVER["HTTP_HOST"], ".4chan.org" ) ) { 345 setcookie( "4chan_auser", "", time() - 3600, "/", ".4chan.org", true ); 346 setcookie( "4chan_apass", "", time() - 3600, "/", ".4chan.org", true ); 347 setcookie( "4chan_aflags", "", time() - 3600, "/", ".4chan.org", true ); 348 349 } elseif( strstr( $_SERVER["HTTP_HOST"], ".4channel.org" ) ) { 350 setcookie( "4chan_auser", "", time() - 24 * 3600, "/", ".4channel.org", true ); 351 setcookie( "4chan_apass", "", time() - 24 * 3600, "/", ".4channel.org", true ); 352 } else { 353 setcookie( "4chan_auser", "", time() - 24 * 3600, "/", true ); 354 setcookie( "4chan_apass", "", time() - 24 * 3600, "/", true ); 355 setcookie( "4chan_aflags", "", time() - 24 * 3600, "/", true ); 356 } 357 358 setcookie( 'extra_path', '', 1, '/', '.4chan.org' ); 359 } 360 361 // record and autoban failed logins. assumes admin or imgboard.php as caller 362 function admin_login_fail() 363 { 364 $ip = ip2long( $_SERVER["REMOTE_ADDR"] ); 365 clear_cookies(); 366 367 mysql_global_call( "insert into user_actions (ip,board,action,time) values (%d,'%s','fail_login',now())", $ip, BOARD_DIR ); 368 369 $query = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='fail_login' and time >= subdate(now(), interval 1 hour)", LOGIN_FAIL_HOURLY, $ip ); 370 if( mysql_result( $query, 0, 0 ) ) { 371 auto_ban_poster( "", -1, 1, "failed to login to /" . BOARD_DIR . "/admin.php " . LOGIN_FAIL_HOURLY . " times", "Repeated admin login failures." ); 372 } 373 374 error( S_WRONGPASS ); 375 } 376 377 // delete all posts everywhere by the poster's IP 378 // for autobans 379 function del_all_posts( $ip = false ) 380 { 381 $q = mysql_global_call( "select sql_cache dir from boardlist" ); 382 $boards = mysql_column_array( $q ); 383 384 $host = $ip ? $ip : $_SERVER['REMOTE_ADDR']; 385 386 foreach( $boards as $b ) { 387 $q = mysql_board_call( "select no from `%s` where host='%s'", $b, $host ); 388 $posts = mysql_column_array( $q ); 389 if( !count( $posts ) ) continue; 390 remote_delete_things( $b, $posts ); 391 } 392 } 393 394 function auto_ban_poster($nametrip, $banlength, $global, $reason, $pubreason = '', $is_filter = false, $pwd = null, $pass_id = null) { 395 if (!$nametrip) { 396 $nametrip = S_ANONAME; 397 } 398 399 if (strpos($nametrip, '</span> <span class="postertrip">!') !== false) { 400 $nameparts = explode('</span> <span class="postertrip">!', $nametrip); 401 $nametrip = "{$nameparts[0]} #{$nameparts[1]}"; 402 } 403 404 $host = $_SERVER['REMOTE_ADDR']; 405 $reverse = mysql_real_escape_string(gethostbyaddr($host)); 406 407 $nametrip = mysql_real_escape_string($nametrip); 408 $global = ($global ? 1 : 0); 409 $board = defined( 'BOARD_DIR' ) ? BOARD_DIR : ''; 410 $reason = mysql_real_escape_string($reason); 411 $pubreason = mysql_real_escape_string($pubreason); 412 413 if ($pubreason) { 414 $pubreason .= "<>"; 415 } 416 417 if ($pass_id) { 418 $pass_id = mysql_real_escape_string($pass_id); 419 } 420 else { 421 $pass_id = ''; 422 } 423 424 if ($pwd) { 425 $pwd = mysql_real_escape_string($pwd); 426 } 427 else { 428 $pwd = ''; 429 } 430 431 // check for whitelisted ban 432 if( whitelisted_ip() ) return; 433 434 //if they're already banned on this board, don't insert again 435 //since this is just a spam post 436 //i don't think it matters if the active ban is global=0 and this one is global=1 437 /* 438 if ($banlength == -1) { 439 $existingq = mysql_global_do("select count(*)>0 from " . SQLLOGBAN . " where host='$host' and active=1 AND global = 1 AND length = 0"); 440 } 441 else { 442 $existingq = mysql_global_do("select count(*)>0 from " . SQLLOGBAN . " where host='$host' and active=1 and (board='$board' or global=1)"); 443 } 444 $existingban = mysql_result( $existingq, 0, 0 ); 445 if( $existingban > 0 ) { 446 delete_uploaded_files(); 447 die(); 448 } 449 */ 450 /* 451 if( $banlength == 0 ) { // warning 452 // check for recent warnings to punish spammers 453 $autowarnq = mysql_global_call( "SELECT COUNT(*) FROM " . SQLLOGBAN . " WHERE host='$host' AND admin='Auto-ban' AND now > DATE_SUB(NOW(),INTERVAL 3 DAY) AND reason like '%$reason'" ); 454 $autowarncount = mysql_result( $autowarnq, 0, 0 ); 455 if( $autowarncount > 3 ) { 456 $banlength = 14; 457 } 458 } 459 */ 460 461 if ($banlength == -1) { // permanent 462 $length = '0000' . '00' . '00'; // YYYY/MM/DD 463 } 464 else { 465 $banlength = (int)$banlength; 466 467 if ($banlength < 0) { 468 $banlength = 0; 469 } 470 471 $length = date('Ymd', time() + $banlength * (24 * 60 * 60)); 472 } 473 474 $length .= "00" . "00" . "00"; // H:M:S 475 476 $sql = "INSERT INTO " . SQLLOGBAN . " (board,global,name,host,reason,length,admin,reverse,post_time,4pass_id,password) VALUES('$board','$global','$nametrip','$host','{$pubreason}Auto-ban: $reason','$length','Auto-ban','$reverse',NOW(),'$pass_id','$pwd')"; 477 478 $res = mysql_global_call($sql); 479 480 if (!$res) { 481 die(S_SQLFAIL); 482 } 483 484 //append_bans( $global ? "global" : $board, array($host) ); 485 486 //$child = stripos($pubreason, 'child') !== false || stripos($reason, 'child') !== false; 487 488 //if ($global && $child && !$is_filter) { 489 // del_all_posts(); 490 //} 491 } 492 493 function cloudflare_purge_url_old($file,$secondary = false) 494 { 495 global $purges; 496 497 if (!defined('CLOUDFLARE_API_TOKEN')) { 498 internal_error_log('cf', "tried purging but token isn't set"); 499 return null; 500 } 501 502 $post = array( 503 "tkn" => CLOUDFLARE_API_TOKEN, 504 "email" => CLOUDFLARE_EMAIL, 505 "a" => "zone_file_purge", 506 "z" => $secondary ? CLOUDFLARE_ZONE_2 : CLOUDFLARE_ZONE, 507 "url" => $file 508 ); 509 510 //quick_log_to("/www/perhost/cf-purge.log", print_r($post, true)); 511 512 $ch = rpc_start_request("https://www.cloudflare.com/api_json.html", $post, array(), false); 513 return $ch; 514 } 515 516 function write_to_event_log($event, $ip, $args = []) { 517 $sql = <<<SQL 518 INSERT INTO event_log(`type`, ip, board, thread_id, post_id, arg_num, 519 arg_str, pwd, req_sig, ua_sig, meta) 520 VALUES('%s', '%s', '%s', '%d', '%d', '%d', 521 '%s', '%s', '%s', '%s', '%s') 522 SQL; 523 524 return mysql_global_call($sql, $event, $ip, 525 $args['board'], $args['thread_id'], $args['post_id'], $args['arg_num'], 526 $args['arg_str'], $args['pwd'], $args['req_sig'], $args['ua_sig'], $args['meta'] 527 ); 528 } 529 530 function log_staff_event($event, $username, $ip, $pwd, $board, $post) { 531 $json_post = []; 532 533 if ($post['sub'] !== '') { 534 $json_post['sub'] = $post['sub']; 535 } 536 537 if ($post['name'] !== '') { 538 $json_post['name'] = $post['name']; 539 } 540 541 if ($post['com'] !== '') { 542 $json_post['com'] = $post['com']; 543 } 544 545 if ($post['fsize'] > 0) { 546 $json_post['file'] = $post["filename"].$post["ext"]; 547 $json_post['md5'] = $post["md5"]; 548 } 549 550 $json_post = json_encode($json_post, JSON_PARTIAL_OUTPUT_ON_ERROR); 551 552 return write_to_event_log($event, $ip, [ 553 'board' => $board, 554 'thread_id' => $post['resto'] ? $post['resto'] : $post['no'], 555 'post_id' => $post['no'], 556 'arg_str' => $username, 557 'pwd' => $pwd, 558 'meta' => $json_post 559 ]); 560 } 561 562 function cloudflare_purge_url($files, $zone2 = false) { 563 // 4cdn = ca66ca34d08802412ae32ee20b7e98af (zone2) 564 // 4chan = 363d1b9b6be563ffd5143c8cfcc29d52 565 566 $url = 'https://api.cloudflare.com/client/v4/zones/' 567 . ($zone2 ? 'ca66ca34d08802412ae32ee20b7e98af' : '363d1b9b6be563ffd5143c8cfcc29d52') 568 . '/purge_cache'; 569 570 $opts = array( 571 CURLOPT_CUSTOMREQUEST => 'POST', 572 CURLOPT_HTTPHEADER => array( 573 'Authorization: Bearer iTf0pQMTvn0zSHAN9vg5S1m_tiwmPKYDjepq8za9', 574 'Content-Type: application/json' 575 ) 576 ); 577 578 // Multiple files 579 if (is_array($files)) { 580 // Batching 581 if (count($files) > 30) { 582 $files = array_chunk($files, 30); 583 584 foreach ($files as $batch) { 585 $opts[CURLOPT_POSTFIELDS] = '{"files":' . json_encode($batch, JSON_UNESCAPED_SLASHES) . '}'; 586 //print_r($opts[CURLOPT_POSTFIELDS]); 587 rpc_start_request_with_options($url, $opts); 588 } 589 } 590 else { 591 $opts[CURLOPT_POSTFIELDS] = '{"files":' . json_encode($files, JSON_UNESCAPED_SLASHES) . '}'; 592 //print_r($opts[CURLOPT_POSTFIELDS]); 593 rpc_start_request_with_options($url, $opts); 594 } 595 } 596 // Single file 597 else { 598 $opts[CURLOPT_POSTFIELDS] = '{"files":["' . $files . '"]}'; 599 //print_r($opts[CURLOPT_POSTFIELDS]); 600 rpc_start_request_with_options($url, $opts); 601 } 602 } 603 604 function cloudflare_purge_by_basename($board, $basename) { 605 preg_match("/([0-9]+)[sm]?\\.([a-z]{3,4})/", $basename, $m); 606 $tim = $m[1]; 607 $ext = $m[2]; 608 609 cloudflare_purge_url("https://i.4cdn.org/$board/$tim.$ext", true); 610 cloudflare_purge_url("https://i.4cdn.org/$board/${tim}s.jpg", true); 611 cloudflare_purge_url("https://i.4cdn.org/$board/${tim}m.jpg", true); 612 }