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