/ lib / admin-test.php
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  }