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