/ imgboard.php
imgboard.php
    1  <?php
    2  require_once 'lib/util.php';
    3  /*
    4  if( isset( $_REQUEST["profile"] ) ) {
    5  	xhprof_enable( XHPROF_FLAGS_CPU | XHPROF_FLAGS_MEMORY );
    6  	register_shutdown_function( "xhprof_save" );
    7  }
    8  if ( isset($_REQUEST["sqlprofile"] )) {
    9  	$mysql_query_log = YES;
   10  }
   11  */
   12  require_once "yotsuba_config.php";
   13  
   14  require_once( "lib/ads.php" );
   15  
   16  if (TEST_BOARD) {
   17    require_once 'lib/admin-test.php'; 
   18    require_once( "lib/postfilter-test.php" );
   19    require_once 'lib/captcha-test.php';
   20    require_once 'lib/auth-test.php';
   21    require_once 'lib/geoip2-test.php';
   22    require_once 'lib/userpwd-test.php';
   23  }
   24  else {
   25    require_once 'lib/admin.php'; 
   26    require_once( "lib/postfilter.php" );
   27    require_once 'lib/captcha.php';
   28    require_once 'lib/auth.php';
   29    require_once 'lib/geoip2.php';
   30    require_once 'lib/userpwd.php';
   31  }
   32  
   33  require_once 'lib/phash.php';
   34  
   35  if (ENABLE_PAINTERJS) {
   36    if (TEST_BOARD) {
   37      require_once('lib/oekaki-test.php');
   38    }
   39    else {
   40      require_once('lib/oekaki.php');
   41    }
   42  }
   43  
   44  // HTML rendering unique to imgboard and upload board
   45  if (UPLOAD_BOARD) {
   46  	require_once "views/upboard.php";
   47  } else if (TEST_BOARD) {
   48  	require_once "views/imgboard-test.php";
   49  } else {
   50  	require_once "views/imgboard.php";
   51  }
   52  
   53  if( !function_exists( 'generate_catalog' ) ) {
   54  	if (TEST_BOARD) {
   55  		require_once 'catalog-test.php';
   56  	} else {
   57  		require_once 'catalog.php';
   58  	}
   59  }
   60  
   61  if( ENABLE_JSON ) {
   62  	$in_imgboard = 1;
   63  	
   64  	if (TEST_BOARD) {
   65  		require_once 'json-test.php';
   66  	} else {
   67  		require_once 'json.php';
   68  	}
   69  }
   70  
   71  if (!defined('SQLLOGMOD')) {
   72    define( 'SQLLOGBAN', 'banned_users' ); //Table (NOT DATABASE) used for holding banned users
   73    define( 'SQLLOGMOD', 'mod_users' ); //Table (NOT DATABASE) used for holding mod users
   74  }
   75  
   76  define( 'SQLLOGDEL', 'del_log' ); //Table (NOT DATABASE) used for holding deletion log
   77  
   78  auth_user();
   79  
   80  if (LOCKDOWN) {
   81    if (($_POST['mode'] == 'usrdel' || $_POST['mode'] == 'arcdel' || $_GET['mode'] == 'latest') && has_level('janitor')) {
   82      // Allow janitors to delete posts and update mod tools
   83    }
   84    else if (has_level('manager') || has_flag('developer')) {
   85      // Allow the manager to do anything
   86    }
   87    else {
   88      die('<span style="color: red;" id="errmsg">' . LOCKDOWN_MSG . '</span>');
   89    }
   90  }
   91  
   92  if (TEST_BOARD && (!has_level() || !has_flag('developer'))) {
   93  	die('');
   94  }
   95  
   96  // test
   97  if( TEST_BOARD || has_flag('developer') ) {
   98  	ini_set( 'display_errors', 1 );
   99  	//error_reporting(E_ALL & ~E_NOTICE);
  100  }
  101  
  102  extract( $_POST, EXTR_SKIP );
  103  extract( $_GET, EXTR_SKIP );
  104  extract( $_COOKIE, EXTR_SKIP );
  105  
  106  if (isset( $_COOKIE['4chan_pass']) && $_COOKIE['4chan_pass']) {
  107    $pwdc = $_COOKIE['4chan_pass'];
  108  }
  109  else {
  110    $pwdc = null;
  111  }
  112  
  113  // FIXME whitelist
  114  unset( $dest );
  115  unset( $log );
  116  unset( $update_avg_secs );
  117  
  118  if( $argv[1] ) $mode = $argv[1];
  119  $id = intval( $id );
  120  
  121  if( $_SERVER["REQUEST_METHOD"] == "POST" ) {
  122  	// bust cache
  123  	header( 'Cache-Control: private, no-cache, must-revalidate' );
  124  	header( 'Expires: -1' );
  125  	header( 'Vary: *' );
  126  }
  127  
  128  if( array_key_exists( 'upfile', $_FILES ) ) {
  129  	$upfile_name = $_FILES["upfile"]["name"];
  130  	$upfile      = $_FILES["upfile"]["tmp_name"];
  131  } else {
  132  	$upfile_name = $upfile = '';
  133  }
  134  
  135  $fwritetimer = 0.0;
  136  
  137  ignore_user_abort( true );
  138  
  139  $word_filters_enabled = false;
  140  if (WORD_FILT) {
  141    $word_filt_root = '/www/global/yotsuba/wordfilters/';
  142    
  143    if (file_exists($word_filt_root . BOARD_DIR . '.php')) {
  144      include_once($word_filt_root . BOARD_DIR . '.php');
  145      $word_filters_enabled = true;
  146    }
  147    else if (file_exists($word_filt_root . 'global.php')) {
  148      include_once($word_filt_root . 'global.php');
  149      $word_filters_enabled = true;
  150    }
  151  }
  152  
  153  if( JANITOR_BOARD == 1 && !has_level( 'janitor' ) ) {
  154  	die( '' );
  155  }
  156  
  157  if( JANITOR_BOARD == 1 )
  158  	include_once 'plugins/broomcloset.php';
  159  
  160  // QENHANCE
  161  if( META_BOARD ) {
  162  	include_once 'plugins/enhance_q.php';
  163  }
  164  
  165  $mysql_connect_opts = 0;
  166  mysql_board_connect(BOARD_DIR);
  167  
  168  $board_flags_array = null;
  169  
  170  if (ENABLE_BOARD_FLAGS) {
  171    $_flags_type = (defined('BOARD_FLAGS_TYPE') && BOARD_FLAGS_TYPE) ? BOARD_FLAGS_TYPE : BOARD_DIR;
  172    $_board_flags_path = '/www/global/yotsuba/lib/board_flags_' . $_flags_type . '.php';
  173    if (file_exists($_board_flags_path)) {
  174      include_once($_board_flags_path);
  175      $board_flags_array = get_board_flags_array();
  176    }
  177  }
  178  
  179  $thread_unique_ips = 0;
  180  
  181  $index_rbl         = PAGE_MAX;
  182  $index_last_thread = 0;
  183  $index_last_post   = 0;
  184  
  185  if (JANITOR_BOARD && PAGE_MAX == 0) {
  186    $index_rbl = ceil(LOG_MAX / DEF_PAGES);
  187  }
  188  
  189  $valid_boards = "3|aco|adv|an|biz|diy|fa|fit|gd|gif|int|lit|hc|hr|a|b|ck|co|cm|c|d|e|f|g|h|i|k|lgbt|m|n|o|out|p|r|s|t|u|vp|vg|vr|v|w|x|y|wg|ic|cgl|hm|mlp|mu|pol|po|r9k|s4s|sci|soc|tg|tv|toy|trv|jp|sp|wsg|qa|qst|his|trash|news|wsr|vip|bant|vrpg|vmg|vst|vt|vm|pw|xs";
  190  
  191  $boards_matching_arr = array();
  192  
  193  $captcha_bypass = null;
  194  $rangeban_bypass = false;
  195  $passid = '';
  196  
  197  // FIXME, this should be put somewhere else.
  198  function is_local() {
  199  	$longip = ip2long( $_SERVER[ 'REMOTE_ADDR' ] );
  200  
  201  	return !$longip || cidrtest( $longip, "10.0.0.0/24" ) || cidrtest( $longip, "204.152.204.0/24" ) || cidrtest( $longip, "127.0.0.0/24" );
  202  }
  203  
  204  /**
  205   * Abbreviates posts on index pages.
  206   * Truncate $str to $max_lines lines and return $str and $abbr
  207   * where $abbr = whether or not $str was actually truncated.
  208   * Expects well-formed HTML.
  209   */
  210  function abbreviate($str, $max_lines = 20) {
  211    $lines = explode('<br>', $str);
  212    
  213    if (count($lines) > $max_lines) {
  214      $abbr = 1;
  215      $lines = array_slice($lines, 0, $max_lines);
  216      $str = implode('<br>', $lines );
  217      
  218      $unpaired_tags = array(
  219        'img' => true,
  220        'br' => true,
  221        'input' => true,
  222        'hr' => true,
  223        'param' => true
  224      );
  225      
  226      preg_match_all('/<\/([^>]+)>/', $str, $closed_tags, PREG_SET_ORDER);
  227      $closed_count = count($closed_tags);
  228      
  229      $closed_map = array();
  230      
  231      foreach ($closed_tags as $m) {
  232        if (!isset($closed_map[$m[1]])) {
  233          $closed_map[$m[1]] = 1;
  234        }
  235        else {
  236          $closed_map[$m[1]] += 1;
  237        }
  238      }
  239      
  240      preg_match_all('/<([a-z0-9]+)(?: |>)/', $str, $open_tags, PREG_SET_ORDER);
  241      $open_count = count($open_tags);
  242      
  243      for ($i = 0; $i < $open_count; ++$i) {
  244        $tag = $open_tags[$i][1];
  245        
  246        if (isset($unpaired_tags[$tag])) {
  247          continue;
  248        }
  249        
  250        if (!isset($closed_map[$tag])) {
  251          $str .= "</$tag>";
  252        }
  253        else if ($closed_map[$tag] > 0) {
  254          $closed_map[$tag] -= 1;
  255        }
  256        else if ($closed_map[$tag] <= 0) {
  257          $str .= "</$tag>";
  258        }
  259      }
  260    }
  261    else {
  262      $abbr = 0;
  263    }
  264    
  265    return array($str, $abbr);
  266  }
  267  
  268  /**
  269   * Currently only used on /archive
  270     * strips html tags and replaces sjis art with [SJIS] placeholders
  271   */
  272  function truncate_comment($str, $length, $keep_spoilers = false) {
  273    // remove sjis
  274    if (SJIS_TAGS && strpos($str, '<span class="sjis"') !== false) {
  275      $str = preg_replace('/<span class="sjis".+?<\/span>/', '[SJIS]', $str);
  276    }
  277    
  278    $len = mb_strlen($str);
  279    
  280    if ($len <= $length) {
  281      return $str;
  282    }
  283    
  284    if (!$keep_spoilers) {
  285      $str = strip_tags($str);
  286    }
  287    else {
  288      $str = strip_tags($str, '<s>');
  289    }
  290    
  291    if ($len <= $length) {
  292      return $str;
  293    }
  294    
  295    $str = mb_substr($str, 0, $length);
  296    
  297    // remove truncated html entities
  298    $str = preg_replace('/&[^;]*$/', '', $str);
  299    
  300    if ($keep_spoilers) {
  301      $str = preg_replace('/<[^>]*$/', '', $str);
  302      
  303      $oc = substr_count($str, '<s>');
  304      
  305      if ($oc) {
  306        $cc = substr_count($str, '</s>');
  307        $dc = $oc - $cc;
  308        if ($dc > 0) {
  309          $str .= str_repeat('</s>', $dc);
  310        }
  311      }
  312    }
  313    
  314    $str .= '…';
  315    
  316    return $str;
  317  }
  318  
  319  function paranoid_rename( $src, $dest )
  320  {
  321  	$across_devices = false; //keep around for future use
  322  	$u              = false;
  323  
  324  	if( $across_devices ) {
  325  		// rename to dest dir, then over dest
  326  		$dsrc = dirname( $dest ) . "/" . basename( $src );
  327  		if( !@rename( $src, $dsrc ) ) $u = $src;
  328  		else if( !@rename( $dsrc, $dest ) ) $u = $dsrc;
  329  	} else {
  330  		if( !@rename( $src, $dest ) ) $u = $src;
  331  	}
  332  
  333  	if( $u )
  334  		unlink( $u );
  335  }
  336  
  337  function rename_across_device( $src, $dest )
  338  {
  339  	// FIXME: copy() does a chmod but we don't need that
  340  	copy($src, $dest);
  341  	unlink($src);
  342  }
  343  
  344  function getmypid_cached()
  345  {
  346  	static $pid = -1;
  347  	
  348  	if ($pid === -1) $pid = getmypid();
  349  	
  350  	return $pid;
  351  }
  352  
  353  // print $contents to $filename by using a temporary file and renaming it
  354  // may destroy $contents in the process
  355  // (makes *.html and *.gz if USE_GZIP is on)
  356  function print_page( $filename, &$contents, $force_nogzip = 0, $trim_whitespace = 1 )
  357  {
  358  	global $fwritetimer;
  359  
  360  	$timestarted = microtime( true );
  361  
  362  	if( NEW_HTML == 1 && $trim_whitespace ) {
  363  		$contents = str_replace( array("\r\n", "\n", "\t"), array('', '', ''), $contents );
  364  	}
  365  
  366  	$gzip = ( USE_GZIP == 1 && !$force_nogzip );
  367  
  368  	if( $gzip ) {
  369  		$tempname = dirname( $filename )."/gztmp".getmypid_cached();
  370  		
  371  		// FIXME: number of syscalls done by gzwrite is not optimal (it does a small one then 4KB writes after)
  372  		// for small files (how small?) do gzencode() and file_put_contents() instead.
  373  		
  374  		$fp = gzopen($tempname, "wb9");
  375  		if( $fp === false ) return;
  376  		gzwrite($fp, $contents);
  377  		gzclose($fp);
  378  		// chmod( $tempname, 0664 ); //it was created 0600
  379  		
  380  		paranoid_rename( $tempname, $filename . ".gz" );
  381  	} else {
  382  		$tempname = dirname( $filename )."/tmp".getmypid_cached();
  383  		if( file_put_contents( $tempname, $contents ) === false ) return;
  384  		// chmod( $tempname, 0664 ); //it was created 0600
  385  		paranoid_rename( $tempname, $filename );
  386  	}
  387  
  388  	$fwritetimer += ( microtime( true ) - $timestarted );
  389  }
  390  
  391  function file_get_contents_cached( $filename )
  392  {
  393  	static $cache = array();
  394  	if( isset( $cache[$filename] ) )
  395  		return $cache[$filename];
  396  	$cache[$filename] = @file_get_contents( $filename );
  397  
  398  	return $cache[$filename];
  399  }
  400  
  401  function file_array_cached( $filename )
  402  {
  403  	static $cache = array();
  404  	if( isset( $cache[$filename] ) )
  405  		return $cache[$filename];
  406  	$cache[$filename] = @file( $filename, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
  407  
  408  	return $cache[$filename];
  409  }
  410  
  411  function get_blotter() {
  412    if (!SHOW_BLOTTER) {
  413      return '';
  414    }
  415    
  416    $msg_limit = 3;
  417    
  418    $blotter = <<<HTML
  419  <table id="blotter" class="desktop"><thead><tr><td colspan="2"><hr class="aboveMidAd"></td></tr></thead><tbody id="blotter-msgs">
  420  HTML;
  421    
  422    $query = <<<SQL
  423  SELECT sql_cache `date`, content 
  424  FROM blotter_messages
  425  ORDER BY id DESC LIMIT $msg_limit
  426  SQL;
  427    
  428    $res = mysql_global_call($query);
  429    
  430    $mtime = 0;
  431    
  432    if ($res && mysql_num_rows($res) > 0) {
  433      while ($row = mysql_fetch_assoc($res)) {
  434        if ($mtime === 0) {
  435          $mtime = $row['date'];
  436        }
  437        
  438        $blotter .= '<tr><td data-utc="' .
  439          $row['date'] . '" class="blotter-date">' .
  440          date('m/d/y', $row['date']) . '</td><td class="blotter-content">' .
  441          $row['content'] . '</td></tr>';
  442      }
  443    }
  444    else {
  445      return '';
  446    }
  447    
  448    $blotter .= '</tbody><tfoot><tr><td colspan="2">[<a data-utc="' . $mtime
  449      . '" id="toggleBlotter" href="#">Hide</a>]<span> [<a href="//www.'
  450      . L::d(BOARD_DIR)
  451      . '/blotter" target="_blank">Show All</a>]</span></td></tr></tfoot></table>';
  452    
  453    return $blotter;
  454  }
  455  
  456  function blotter_contents()
  457  {
  458  	static $cache;
  459  	if( isset( $cache ) ) return $cache;
  460  	$ret      = "";
  461  	$topN     = 4; //how many lines to print
  462  	$bl_lines = file( BLOTTER_PATH );
  463  	$bl_top   = array_slice( $bl_lines, 0, $topN );
  464  	$date     = "";
  465  	foreach( $bl_top as $line ) {
  466  		if( !$date ) {
  467  			$lineparts = explode( ' - ', $line );
  468  			if( strpos( $lineparts[0], '<font' ) !== false ) {
  469  				$dateparts = explode( '>', $lineparts[0] );
  470  				$date      = $dateparts[1];
  471  				$date      = "<li><font color=\"red\">Blotter updated: $date</font>";
  472  			} else {
  473  				$date = $lineparts[0];
  474  				$date = "<li>Blotter updated: $date";
  475  			}
  476  		}
  477  		$line = trim( $line );
  478  		$line = str_replace( "\\", "\\\\", $line );
  479  		$line = str_replace( "'", "\'", $line );
  480  		$ret .= "'<li>$line'+\n";
  481  	}
  482  	$ret .= "''";
  483  	$cache = array($date, $ret);
  484  
  485  	return array($date, $ret);
  486  }
  487  
  488  function find_match_and_prefix( $regex, $str, $off, &$match )
  489  {
  490  	if( !preg_match( $regex, $str, $m, PREG_OFFSET_CAPTURE, $off ) ) return false;
  491  
  492  	$moff  = $m[0][1];
  493  	$match = array(substr( $str, $off, $moff - $off ), $m[0][0]);
  494  
  495  	return true;
  496  }
  497  
  498  // skip_on_spoilers will stop parsing and return the unmodified comment
  499  // if spoiler tags are found inside the string to wrap.
  500  // This is to avoid sjis.spoiler tags mixing mostly.
  501  function parse_bbcode_one( $com, $tn, $st, $et, $nest_limit = 2, $skip_on_spoilers = false )
  502  {
  503  	if( !find_match_and_prefix( "/\[$tn\]/", $com, 0, $m ) ) return $com;
  504  
  505  	$bracket_tn = "[$tn]";
  506  	$bl         = strlen( $bracket_tn );
  507  	$el         = $bl + 1;
  508  	$ret        = $m[0] . $st;
  509  	$lev        = 1;
  510  	$off        = strlen( $m[0] ) + $bl;
  511  
  512  	while( 1 ) {
  513  		if (!find_match_and_prefix( "@\[/?$tn\]@", $com, $off, $m)) break;
  514  		list( $txt, $tag ) = $m;
  515      
  516      if (!$skip_on_spoilers || $tag === $bracket_tn) {
  517        $ret .= $txt;
  518      }
  519      else if (preg_match('/\[\/?spoiler\]/', $txt)) {
  520        return $com;
  521      }
  522  		
  523  		$off += strlen( $txt ) + strlen( $tag );
  524  
  525  		if( $tag == $bracket_tn ) {
  526  			if( $lev < $nest_limit )
  527  				$ret .= $st;
  528  			$lev++;
  529  		} else if( $lev ) {
  530  			if( $lev <= $nest_limit )
  531  				$ret .= $et;
  532  			$lev--;
  533  		}
  534  	}
  535    
  536    $tail = substr($com, $off, strlen($com) - $off);
  537  	$ret .= $tail;
  538  	
  539    $lev = min( $lev, $nest_limit );
  540    
  541    if ($lev > 0) {
  542      if ($skip_on_spoilers && preg_match('/\[\/?spoiler\]/', $tail)) {
  543        return $com;
  544      }
  545      
  546      $ret .= str_repeat( $et, $lev );
  547    }
  548    
  549  	return $ret;
  550  }
  551  
  552  function spoiler_parse( $com )
  553  {
  554  	return parse_bbcode_one( $com, 'spoiler', '<s>', '</s>' );
  555  }
  556  
  557  function jsmath_parse( $com )
  558  {
  559  	$com = parse_bbcode_one( $com, "math", '<span class="math">', '</span>' );
  560  	$com = parse_bbcode_one( $com, "eqn", '<div class="math">', '</div>' );
  561  
  562  	return $com;
  563  }
  564  
  565  /* BBCode for bold, italic, and r/g/b color tags */
  566  function parse_op_markup($com) {
  567    $com = parse_bbcode_one($com, 'b', '<span class="mu-s">', '</span>', 1);
  568    $com = parse_bbcode_one($com, 'i', '<span class="mu-i">', '</span>', 1);
  569    
  570    $com = parse_bbcode_one($com, 'red', '<span class="mu-r">', '</span>', 1);
  571    $com = parse_bbcode_one($com, 'green', '<span class="mu-g">', '</span>', 1);
  572    $com = parse_bbcode_one($com, 'blue', '<span class="mu-b">', '</span>', 1);
  573    
  574    return $com;
  575  }
  576  
  577  function code_parse( $com )
  578  {
  579  	return parse_bbcode_one( $com, 'code', '<pre class="prettyprint">', '</pre>' );
  580  }
  581  
  582  function sjis_parse($com) {
  583    $skip_on_spoilers = strpos($com, '[spoiler]') !== false;
  584    
  585    return parse_bbcode_one($com, 'sjis', '<span class="sjis">', '</span>', 1, $skip_on_spoilers);
  586  }
  587  
  588  // convenience function for wordfilters.
  589  // text must be html escaped
  590  function random_color( $str, $background = 1, $foreground = 1 )
  591  {
  592  	$style = "";
  593  
  594  	if( $background ) {
  595  		$r     = rand( 0, 255 );
  596  		$g     = rand( 0, 255 );
  597  		$b     = rand( 0, 255 );
  598  		$style = $style . "background: #" . sprintf( "%02x%02x%02x", $r, $g, $b ) . "; ";
  599  	}
  600  
  601  	if( $foreground ) {
  602  		$r     = rand( 0, 255 );
  603  		$g     = rand( 0, 255 );
  604  		$b     = rand( 0, 255 );
  605  		$style = $style . "color: #" . sprintf( "%02x%02x%02x", $r, $g, $b ) . "; ";
  606  	}
  607  
  608  	if( $style ) {
  609  		return "<span style=\"$style\">$str</span>";
  610  	}
  611  
  612  	return $str;
  613  }
  614  
  615  function append_ban( $board, $ip )
  616  {
  617  	$cmd = "nohup /usr/local/bin/suid_run_global bin/appendban $board $ip >/dev/null 2>&1 &";
  618  	exec( $cmd );
  619  }
  620  
  621  // check whether the current user can perform $action (on $no, for some actions)
  622  // board-level access is cached in $valid_cache.
  623  // FIXME move to lib/admin.php
  624  function valid( $action = 'moderator', $no = 0 )
  625  {
  626  	static $valid_cache, $can_post_html; // the access level of the user
  627  	$access_level = array('none' => 0, 'janitor' => 1, 'janitor_this_board' => 2, 'moderator' => 5, 'manager' => 10, 'admin' => 20);
  628  	if( !isset( $valid_cache ) ) {
  629  		$valid_cache = $access_level['none'];
  630  		if( isset( $_COOKIE['4chan_auser'] ) && isset( $_COOKIE['apass'] ) ) {
  631  			$user = mysql_real_escape_string( $_COOKIE['4chan_auser'] );
  632  			$pass = $_COOKIE['apass'];
  633  		}
  634  		if( $user && $pass ) {
  635  			$result = mysql_global_call( "SELECT allow,deny,password_expired,username,password FROM " . SQLLOGMOD . " WHERE username='$user' LIMIT 1" );
  636  			list( $allow, $deny, $expired, $username, $password ) = mysql_fetch_row( $result );
  637  			mysql_free_result( $result );
  638  			
  639        $admin_salt = file_get_contents('/www/keys/2014_admin.salt');
  640        
  641        if (!$admin_salt) {
  642          die('Internal Server Error (s0)');
  643        }
  644        
  645        $hashed_admin_password = hash('sha256', $username . $password . $admin_salt);
  646      	
  647        if ($hashed_admin_password !== $pass) {
  648          return false;
  649        }
  650        
  651  			if( $expired ) {
  652  				error( 'Your password has expired; check IRC for instructions on changing it.' );
  653  			}
  654  			
  655  			if( $allow ) {
  656  				$allows             = explode( ',', $allow );
  657  				$seen_janitor_token = false;
  658  				// each token can increase the access level,
  659  				// except that we only know that they're a moderator or a janitor for another board
  660  				// AFTER we read all the tokens
  661  				$cphtml = false;
  662  
  663  				foreach( $allows as $token ) {
  664  					if( $token == 'janitor' ) {
  665  						$seen_janitor_token = true;
  666  					} else if( $token == 'manager' && $valid_cache < $access_level['manager'] ) {
  667  						$valid_cache = $access_level['manager'];
  668  					} else if( $token == 'admin' && $valid_cache < $access_level['admin'] ) {
  669  						$valid_cache = $access_level['admin'];
  670  					} else if( ( $token == BOARD_DIR || $token == 'all' ) && $valid_cache < $access_level['janitor_this_board'] ) {
  671  						$valid_cache = $access_level['janitor_this_board']; // or could be moderator, will be increased in next step
  672  					} elseif( $token == 'html' ) {
  673  						$cphtml = true;
  674  					}
  675  				}
  676  
  677  				$can_post_html = $cphtml;
  678  				// now we can set moderator or janitor status
  679  				if( !$seen_janitor_token ) {
  680  					if( $valid_cache < $access_level['moderator'] )
  681  						$valid_cache = $access_level['moderator'];
  682  				} else {
  683  					if( $valid_cache < $access_level['janitor'] )
  684  						$valid_cache = $access_level['janitor'];
  685  				}
  686  				if( $deny ) {
  687  					$denies = explode( ',', $deny );
  688  					if( in_array( BOARD_DIR, $denies ) ) {
  689  						$valid_cache = $access_level['none'];
  690  					}
  691  				}
  692  			}
  693  		}
  694  	}
  695  	{
  696  		// local rpc can do anything
  697  		$longip = ip2long( $_SERVER['REMOTE_ADDR'] );
  698  		if( !$longip || cidrtest( $longip, "10.0.0.0/24" ) ||
  699  			cidrtest( $longip, "204.152.204.0/24" ) || cidrtest( $longip, "127.0.0.0/24" )
  700  		)
  701  			return YES;
  702  	}
  703  	switch( $action ) {
  704  		case 'moderator':
  705  			return $valid_cache >= $access_level['moderator'];
  706  		case 'textonly':
  707  			return $valid_cache >= $access_level['moderator'];
  708  		case 'htmlnopw':
  709  			return $valid_cache >= $access_level['manager'];
  710  		case 'htmlpost':
  711  			return $can_post_html;
  712  		case 'janitor_board':
  713  			return $valid_cache >= $access_level['janitor'];
  714  		case 'delete':
  715  			if( $valid_cache >= $access_level['janitor_this_board'] ) {
  716  				return true;
  717  			} // if they're a janitor on another board, check for illegal post unlock
  718  			else if( $valid_cache >= $access_level['janitor'] ) {
  719  				$query         = mysql_global_do( "SELECT COUNT(*) from reports WHERE board='" . BOARD_DIR . "' AND no=$no AND cat=2" );
  720  				$illegal_count = mysql_result( $query, 0, 0 );
  721  				mysql_free_result( $query );
  722  
  723  				return $illegal_count >= 3;
  724  			}
  725  		case 'reportflood':
  726  			return $valid_cache >= $access_level['janitor'];
  727  		case 'floodbypass':
  728  			return $valid_cache >= $access_level['janitor'];
  729  		case 'rebuild':
  730  			return $valid_cache >= $access_level['janitor'];
  731  		case 'admin':
  732  			return $valid_cache >= $access_level['admin'];
  733  		default: // unsupported action
  734  			return false;
  735  	}
  736  }
  737  
  738  function iplog_add( $board, $no, $ip, $time, $is_thread, $tim, $had_image )
  739  {
  740  	mysql_global_call( "INSERT INTO user_actions (board,postno,ip,time,uploaded,action,had_image) VALUES ('%s',%d,%d,from_unixtime(%d),%d,'%s',%d)",$board, $no, ip2long($ip), $time, $tim, $is_thread ? "new_thread" : "new_reply", $had_image);
  741  }
  742  
  743  function clean_log_bool( &$row )
  744  {
  745  	static $bool_cols = array('sticky', 'permasage', 'closed', 'filedeleted', 'permaage', 'undead', 'archived');
  746  
  747  	foreach ($bool_cols as $col) {
  748  		if (!isset($row[$col])) continue; // FIXME split this function up to avoid this test
  749  		$c = &$row[$col];
  750  		settype($c, "bool");
  751  		// NOTES: $c = $c ? TRUE : FALSE allocates new bools each time
  752  		// $c = $c ? &$itrue : &$ifalse causes them to be converted to strings(!)
  753  		// Put this back to TRUE : FALSE instead of a settype call sometime so we can look at the php bytecodes
  754  	}
  755  }
  756  
  757  function clean_log_int( &$row )
  758  {
  759  	static $int_cols = array('no', 'w', 'h', 'tn_w', 'tn_h', 'last_modified', 'time', 'fsize', 'resto');
  760  
  761  	// turn columns into int (does this help?)
  762  	foreach( $int_cols as $col ) {
  763  		if (!isset($row[$col])) continue;
  764  		
  765  		$c = &$row[$col];
  766  		settype($c, "int");
  767  	}
  768  }
  769  
  770  function clean_log_delreply( &$row )
  771  {	
  772  	if (!$row['resto'])
  773  		return;	
  774  	
  775  	static $del_cols = array('sticky', 'permasage', 'closed', 'last_modified', 'root', 'undead', 'permaage');
  776  	
  777  	// delete fields not used for replies
  778  	foreach( $del_cols as $col ) {
  779  		unset($row[$col]);
  780  	}
  781  }
  782  
  783  function clean_log_intern( &$row )
  784  {
  785  	static $intern_cols = array('name', 'ext', 'capcode', 'country');
  786  
  787  	// Intern repeated strings that are usually the same value.
  788  	// In PHP 6 this... doesn't seem to do anything? Let's try again in 7.
  789  	static $log_intern;
  790  	if( !isset( $log_intern ) ) {
  791  		$log_intern = array();
  792  		foreach( $intern_cols as $col )
  793  			$log_intern[$col] = array();
  794  	}
  795  
  796  	foreach( $intern_cols as $col ) {
  797  		$intern_array = &$log_intern[$col];
  798  		$c = &$row[$col];
  799  		
  800  		$v = $c;
  801  		if( !isset($intern_array[$v]) )
  802  			$intern_array[$v] = $v;
  803  		$c = &$intern_array[$c];
  804  	}
  805  }
  806  
  807  function clean_log_row( &$row )
  808  {
  809  	//static $rn = 0;
  810  	clean_log_delreply( $row );
  811  	clean_log_bool( $row );
  812  	clean_log_int( $row );
  813  	//clean_log_intern( $row );
  814  	//if (++$rn == 100) {
  815  	//	debug_zval_dump($row);
  816  	//}
  817  }
  818  
  819  function log_bad_cache_entry($no)
  820  {
  821  	global $log_cache_level;
  822  	global $log;
  823  	
  824  	internal_error_log("logcache", "missing children for OP no $no, cache level $log_cache_level, cache contents ".count($log));
  825  }
  826  
  827  function invalidate_log($thread)
  828  {
  829  	global $log_cache_level;
  830  	global $log;
  831  	
  832  	if (isset($log[$thread])) {
  833  		if ($log[$thread]['resto']) {
  834  			die(S_ASSERT);
  835  		}
  836  		unset($log[$thread]);
  837  	}
  838  	
  839  	if ($log_cache_level==2)
  840  		$log_cache_level = 1;
  841  }
  842  
  843  // build a structure out of all the posts in the database.
  844  // this lets us replace a LOT of queries with a simple array access.
  845  // it only builds the first time it was called.
  846  // rather than calling log_cache(1) to rebuild everything,
  847  // you should just manipulate the structure directly.
  848  // $thread may be any postno in a thread
  849  // without a thread, $archive_mode fetches all live threads if 0 and all archived threads if 1
  850  function log_cache($invalidate = 0, $thread = 0, $archive_mode = 0) {
  851    global $log_cache_level; 
  852    global $log, $ipcount, $mysql_unbuffered_reads;
  853    
  854    if (!isset($log) || $invalidate) {
  855      $log = array();
  856      $log_cache_level = 0;
  857    }
  858    
  859    // Optimisation for index rebuilding when REPLIES_SHOWN is 0.
  860    // No need to fetch the entire board in this case.
  861    $optimised_indexes = !$thread && !$archive_mode && !REPLIES_SHOWN && IS_REBUILDD;
  862  
  863    // Handle cache
  864    // 1 = Live OPs are cached, 2 = Some threads are cached, 3 = Whole live board is cached
  865    if ($optimised_indexes) {
  866      $nlog_cache_level = 1;
  867    }
  868    else {
  869      $nlog_cache_level = $thread ? 2 : 3;
  870    }
  871    
  872    // Whole board is cached, nothing to do.
  873    if ($log_cache_level == 3 && $archive_mode === 0) {
  874      return;
  875    }
  876    
  877    // Thread is cached, nothing to do.
  878    if ($log_cache_level == 2 && isset($log[$thread])) {
  879      return;
  880    }
  881    
  882    // Live OPs are cached, nothing to do.
  883    if ($log_cache_level == 1 && $optimised_indexes) {
  884      return;
  885    }
  886    
  887    if ($nlog_cache_level > $log_cache_level) {
  888      $log_cache_level = $nlog_cache_level;
  889    }
  890  
  891    $ips = array();
  892    
  893  	mysql_board_call( "SET read_buffer_size=1048576" );
  894  	$mysql_unbuffered_reads = 1;
  895  	
  896  	$query_archived = false;
  897    
  898  	if ($thread) {
  899  	  if ($archive_mode === 0) {
  900        $where = " WHERE archived = 0 AND (resto = $thread OR no = $thread)";
  901  	  }
  902  	  else if ($archive_mode === 1) {
  903        $where = " WHERE archived = 1 AND (resto = $thread OR no = $thread)";
  904  	  }
  905  	  else {
  906        $query_archived = true;
  907        $where = " WHERE (archived = 0 AND resto = $thread) OR (archived = 1 AND resto = $thread) OR no = $thread";
  908  	  }
  909  	}
  910  	else {
  911  	if ($archive_mode === 1) {
  912          $query_archived = true;
  913  		$where = ' WHERE archived = 1';
  914  	}
  915      else if ($optimised_indexes) {
  916        $where = ' WHERE archived = 0 AND resto = 0';
  917        
  918        $_thread_ids = array();
  919      }
  920      else {
  921        $where = ' WHERE archived = 0';
  922      }
  923  	}
  924  	
  925  	$fields = "no,sticky,permasage,closed,now,name,sub,com,host,pwd,filename,ext,w,h,tn_w,tn_h,tim,time,md5,fsize,last_modified,root,resto,filedeleted,id,capcode,country,undead,permaage,since4pass";
  926  	
  927  	if ($query_archived) {
  928  	  $fields .= ",archived";
  929    }
  930  	
  931    if (MOBILE_IMG_RESIZE) {
  932      $fields .= ",m_img";
  933    }
  934  	
  935    if (ENABLE_BOARD_FLAGS) {
  936      $fields .= ",board_flag";
  937    }
  938    
  939  	$sql_cache = "sql_no_cache";
  940  	
  941  	$query  = mysql_board_call( "SELECT $sql_cache $fields FROM `" . SQLLOG . "`" . $where );
  942  	$offset = 0;
  943  	
  944  	while( $row = mysql_fetch_assoc( $query ) ) {
  945  		if (!$query_archived) $row['archived'] = $archive_mode;
  946  		clean_log_row( $row );
  947  		
  948  		$row_no    = $row['no'];
  949  		$row_resto = $row['resto'];
  950  		
  951  		// IF mysql returns rows in order by default then replies come after OP
  952  		// so if OP doesn't exist, this post is orphaned and should be skipped
  953  		// TODO: let's not skip for now, it seems more likely to cause bugs than anything
  954  		//if ($fetching_whole_threads && $row_resto && !isset($log[$row_resto])) continue;
  955  		
  956      if ($optimised_indexes) {
  957        $_thread_ids[] = (int)$row_no;
  958      }
  959      		
  960  		$ips[$row['host']] = TRUE;
  961  		
  962      if (!$row_resto) {
  963        $row['children'] = array();
  964        $row['imgreplycount'] = 0;
  965        $row['replycount'] = 0;
  966      }
  967      else {
  968        if (isset($log[$row_resto])) {
  969          $log[$row_resto]['children'][$row_no] = TRUE;
  970          
  971          if ($row['fsize'] && !$row['filedeleted']) {
  972            $log[$row_resto]['imgreplycount']++;
  973          }
  974          
  975          $log[$row_resto]['replycount']++;
  976        }
  977      }
  978  		
  979  		$log[$row_no] = $row;
  980  	}
  981    
  982  	$query = null;
  983  
  984  	$nrows = count($log);
  985  	//if (BOARD_DIR=="b" && !$thread) quick_log_to("/www/perhost/logcaches.log", "inefficient all-board log_cache run t=$thread r=$nrows inv=$invalidate lev=$log_cache_level", true);
  986  	// if (!STATIC_REBUILD && $thread) quick_log_to("/www/perhost/logcaches.log", "inefficient? one-thread log_cache run t=$thread r=$nrows inv=$invalidate lev=$log_cache_level", true);
  987    
  988  	$is_single_thread = $thread && isset($log[$thread]['children']);
  989  	$ipcount = count( $ips );
  990  	unset($ips);
  991  
  992  	$mysql_unbuffered_reads = 0;
  993  	mysql_board_call( "SET read_buffer_size=131072" );
  994  
  995  	if (!$thread) {
  996      if ($optimised_indexes) {
  997        if (empty($_thread_ids)) {
  998          return;
  999        }
 1000        
 1001        $_thread_ids = implode(',', $_thread_ids);
 1002        
 1003        $query = mysql_board_call("SELECT resto, COUNT(*) AS r_count, SUM(IF(fsize > 0 AND filedeleted = 0, 1, 0)) AS i_count FROM `" . SQLLOG . "` WHERE resto IN($_thread_ids) GROUP BY resto");
 1004        
 1005        while ($row = mysql_fetch_assoc($query)) {
 1006          $log[$row['resto']]['imgreplycount'] = (int)$row['i_count'];
 1007          $log[$row['resto']]['replycount'] = (int)$row['r_count'];
 1008        }
 1009        
 1010        $query = mysql_board_call("SELECT no FROM `" . SQLLOG . "` WHERE no IN($_thread_ids) ORDER BY root DESC");
 1011      }
 1012      else {
 1013        if ($archive_mode === 1) {
 1014          $archived = "archived = 1";
 1015        } else {
 1016          $archived = "archived = 0";
 1017        }
 1018        $query = mysql_board_call("SELECT no FROM `" . SQLLOG . "` WHERE $archived AND resto = 0 AND root > 0 ORDER BY root DESC");
 1019      }
 1020      
 1021      $threads = array(); // IDs
 1022      
 1023      while ($row = mysql_fetch_row($query)) {
 1024        $no = (int)$row[0];
 1025        
 1026        if (isset($log[$no])) {
 1027          $threads[] = $no;
 1028        }
 1029      }
 1030      
 1031      $log['THREADS'] = $threads;
 1032    
 1033      foreach( $threads as $thread ) {
 1034        $this_thread = $log[$thread];
 1035        
 1036        if (!$this_thread['permaage'] && !$this_thread['sticky'] && $this_thread['replycount'] >= MAX_RES) {
 1037          $log[$thread]['bumplimit'] = TRUE;
 1038        }
 1039        else {
 1040          $log[$thread]['bumplimit'] = FALSE;
 1041        }
 1042        
 1043        if ($this_thread['archived']) {
 1044          $log[$thread]['archived_on'] = strtotime($this_thread['root']);
 1045        }
 1046        
 1047        if (!$this_thread['permaage'] && !$this_thread['sticky'] && !$log[$thread]['undead'] && $this_thread['imgreplycount'] >= MAX_IMGRES) {
 1048          $log[$thread]['imagelimit'] = TRUE;
 1049        }
 1050        else {
 1051          $log[$thread]['imagelimit'] = FALSE;
 1052        }
 1053        
 1054        $log[$thread]['semantic_url'] = generate_href_context($this_thread['sub'], $this_thread['com']);
 1055      }
 1056    }
 1057    else if ($is_single_thread) {
 1058      if (!$log[$thread]['permaage'] && !$log[$thread]['sticky'] && $log[$thread]['replycount'] >= MAX_RES) {
 1059        $log[$thread]['bumplimit'] = TRUE;
 1060      }
 1061      else {
 1062        $log[$thread]['bumplimit'] = FALSE;
 1063      }
 1064      
 1065      if ($log[$thread]['archived']) {
 1066        $log[$thread]['archived_on'] = strtotime($log[$thread]['root']);
 1067      }
 1068      
 1069      if (!$log[$thread]['permaage'] && !$log[$thread]['sticky'] && !$log[$thread]['undead'] && $log[$thread]['imgreplycount'] >= MAX_IMGRES) {
 1070        $log[$thread]['imagelimit'] = TRUE;
 1071      }
 1072      else {
 1073        $log[$thread]['imagelimit'] = FALSE;
 1074      }
 1075      
 1076      $log[$thread]['semantic_url'] = generate_href_context($log[$thread]['sub'], $log[$thread]['com']);
 1077    }
 1078  
 1079  	// calculate old-status for PAGE_MAX mode
 1080  	//$threadcount = count( $threads );
 1081  
 1082  	/*if(EXPIRE_NEGLECTED != 1) {
 1083  		rsort($threads, SORT_NUMERIC);
 1084  
 1085  		if(PAGE_MAX > 0) // the lowest 5% of maximum threads get marked old
 1086  			for($i = floor(0.95*PAGE_MAX*DEF_PAGES); $i < $threadcount; $i++) {
 1087  				if(!$log[$threads[$i]]['sticky'])
 1088  					$log[$threads[$i]]['old'] = 1;
 1089  			}
 1090  		else { // threads w/numbers below 5% of LOG_MAX get marked old
 1091  			foreach($threads as $thread) {
 1092  				if($lastno-LOG_MAX*0.95>$thread)
 1093  					if(!$log[$thread]['sticky'])
 1094  						$log[$thread]['old'] = 1;
 1095  			}
 1096  		}
 1097  	} else {
 1098  		$rthreads = array();
 1099  		foreach ($threads as $t) {
 1100  			$root = $log[$t]['root'];
 1101  			$rthreads[$t] = $root;
 1102  		}
 1103  		
 1104  		arsort($rthreads);
 1105  		$rthreads = array_keys($rthreads);
 1106  		
 1107  		if (PAGE_MAX > 0) {
 1108  			$floor = (int)floor(0.95*PAGE_MAX*DEF_PAGES);
 1109  			for($i = $floor; $i < $threadcount; $i++) {
 1110  				if(!$log[$rthreads[$i]]['sticky'])
 1111  					$log[$rthreads[$i]]['old'] = 1;
 1112  			}
 1113  		}
 1114  	}*/
 1115  }
 1116  
 1117  function rebuildallthumb($archiveonly = false)
 1118  {
 1119  	global $log;
 1120  	if( !has_level() ) return;
 1121  	$starttime = microtime( true );
 1122  	set_time_limit( 0 );
 1123  	if (!$archiveonly) {
 1124  		log_cache();
 1125  		$nposts = count($log);
 1126  		echo "Rebuilding $nposts live posts<br>\n";
 1127  
 1128  		foreach( $log as $post ) {
 1129  			if( !$post["ext"] ) continue;
 1130  
 1131  			$ext   = $post["ext"];
 1132  			$tim   = $post["tim"];
 1133  			$resto = $post["resto"];
 1134  			$fname = IMG_DIR . $tim . $ext;
 1135  			make_thumb( $fname, $tim, $ext, $resto, $TN_W, $TN_H, $tmd5 );
 1136  		}
 1137  		$totaltime = microtime( true ) - $starttime;
 1138  		echo "Took $totaltime seconds for live posts<br>\n";
 1139  	}
 1140  	
 1141  	// Run again for archived thumbs
 1142  	$starttime = microtime( true );
 1143  	mysql_check_connections();
 1144  	log_cache(1, 0, 1); // fetch archives instead
 1145  	$nposts = count($log);
 1146  	echo "Rebuilding $nposts archived posts<br>\n";
 1147  	
 1148  	foreach( $log as $post ) {
 1149  		if( !$post["ext"] ) continue;
 1150  
 1151  		$ext   = $post["ext"];
 1152  		$tim   = $post["tim"];
 1153  		$resto = $post["resto"];
 1154  		$fname = IMG_DIR . $tim . $ext;
 1155  		make_thumb( $fname, $tim, $ext, $resto, $TN_W, $TN_H, $tmd5 );
 1156  	}
 1157  	$totaltime = microtime( true ) - $starttime;
 1158  	echo "Took $totaltime seconds for archived posts<br>\n";
 1159  }
 1160  
 1161  /**
 1162   * Displays some text on a lightweight page styled using the default stylesheet.
 1163   * $message should be html-escaped
 1164   */
 1165  function headless_message($message, $autoclose = false) {
 1166    if (DEFAULT_BURICHAN) {
 1167      $css = 'yotsubluenew';
 1168      $ws = 'ws';
 1169    }
 1170    else {
 1171      $css = 'yotsubanew';
 1172      $ws = '';
 1173    }
 1174    
 1175    $css_ver = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION;
 1176    
 1177    if ($error) {
 1178      $err_css = 'color: red;';
 1179    }
 1180    else {
 1181      $err_css = '';
 1182    }
 1183    
 1184    if ($autoclose) {
 1185      $js = <<<JS
 1186  <script>setTimeout(function() { self.close(); }, 3000);</script>
 1187  JS;
 1188    }
 1189    else {
 1190      $js = '';
 1191    }
 1192    
 1193    $html = <<<HTML
 1194  <!DOCTYPE html><html><head>
 1195  <meta http-equiv="content-type" content="text/html; charset=UTF-8">
 1196  <meta http-equiv="pragma" content="no-cache">
 1197  <link rel="icon" type="image/x-icon" href="//s.4cdn.org/image/favicon-team$ws.ico">
 1198  <link rel="stylesheet" style="text/css" href="//s.4cdn.org/css/$css.$css_ver.css">
 1199  </head>
 1200  <body>
 1201  <table style="text-align: center; width: 100%; height: 150px; border: 0;">
 1202    <tr valign="middle">
 1203      <td align="center" style="font-size: x-large; font-weight: bold; border: 0;$err_css">
 1204        <span>$message</span>
 1205      </td>
 1206    </tr>
 1207  </table>$js
 1208  </body>
 1209  </html>
 1210  HTML;
 1211    
 1212    return $html;
 1213  }
 1214  
 1215  function do_move_thread() {
 1216    if (!isset($_POST['id']) || !isset($_POST['board'])) {
 1217      updating_index();
 1218    }
 1219    
 1220    if (!has_level()) {
 1221      updating_index();
 1222    }
 1223    
 1224    $del = isset($_POST['move_del']) && $_POST['move_del'];
 1225    
 1226    $ret = move_thread($_POST['id'], $_POST['board'], $del);
 1227    
 1228    if (is_array($ret)) {
 1229      $message = 'Thread moved to <a target="_blank" href="//boards.'
 1230        . L::d($ret[0]) . '/' . $ret[0] . '/thread/' . $ret[1]
 1231        . '">/' . $ret[0] . '/' . $ret[1] . '</a>';
 1232      
 1233      echo headless_message($message, true);
 1234    }
 1235    else {
 1236      echo headless_message("<span style=\"color:red\">$ret</span>");
 1237    }
 1238  }
 1239  
 1240  function do_copy_threads() {
 1241    if (!has_level()) {
 1242      updating_index();
 1243      return;
 1244    }
 1245    global $argv;
 1246    
 1247    $to_board = $_REQUEST["board"];
 1248    if (!$to_board) $to_board = $argv[2];
 1249  
 1250    if (!$to_board) {
 1251      updating_index();
 1252      return;
 1253    }
 1254  
 1255    set_time_limit( 0 );
 1256    header( "Pragma: no-cache" );
 1257    _print(fancystyle());
 1258  
 1259    if (UPLOAD_BOARD || JANITOR_BOARD) {
 1260      $ret = "The current board doesn't support this feature.";
 1261    } else if ($to_board === 'f' || $to_board === 'j') {
 1262      $ret = "The destination board doesn't support this feature.";
 1263    } else {
 1264  	$threads = mysql_column_array(mysql_board_call("SELECT no FROM `%s` WHERE resto = 0 AND archived = 0", BOARD_DIR));
 1265  	foreach ($threads as $thread) {$ret = copy_thread($thread, $to_board); _print("$thread<br>\n");}
 1266    }
 1267    
 1268    if (is_array($ret)) {
 1269      $message = 'Thread copied to <a target="_blank" href="//boards.'
 1270        . L::d($ret[0]) . '/' . $ret[0] . '/thread/' . $ret[1]
 1271        . '">/' . $ret[0] . '/' . $ret[1] . '</a>';
 1272      
 1273      echo headless_message($message, true);
 1274    }
 1275    else {
 1276      echo headless_message("<span style=\"color:red\">$ret</span>");
 1277    }
 1278  }
 1279  
 1280  function copy_thread($thread_id, $to_board, $delete = false) {
 1281    $thread_id = (int)$thread_id;
 1282    
 1283    if (!$thread_id) {
 1284      return "Invalid thread ID.";
 1285    }
 1286    
 1287    // Validate destination board
 1288    if (!preg_match('/^[a-z0-9]+$/', $to_board)) {
 1289      return 'Invalid destination board.';
 1290    }
 1291    
 1292    $query = "SELECT COUNT(*) FROM boardlist WHERE dir = '%s' LIMIT 1";
 1293    
 1294    $res = mysql_global_call($query, $to_board);
 1295    
 1296    if (!$res) {
 1297      return "Database Error (1)";
 1298    }
 1299    
 1300    if (mysql_num_rows($res) < 1) {
 1301      return "Destination board doesn't exist.";
 1302    }
 1303    
 1304    // ---
 1305      
 1306    // Fetch the whole thread
 1307    $posts = array();
 1308    
 1309    $res = mysql_board_call("SELECT * FROM `%s` WHERE no = %d AND resto = 0", BOARD_DIR, $thread_id);
 1310    if (!$res) {
 1311      return "Database Error (3)";
 1312    }
 1313    
 1314    $row = mysql_fetch_assoc($res);
 1315    if (!$row) {
 1316      return "Thread not found.";
 1317    }
 1318    
 1319    $posts[] = $row;
 1320    $res = mysql_board_call("SELECT * FROM `%s` WHERE resto = %d", BOARD_DIR, $thread_id);
 1321    if (!$res) {
 1322      return "Database Error (4)";
 1323    }
 1324    
 1325    while ($row = mysql_fetch_assoc($res)) {
 1326      if (!$row) {
 1327        continue;
 1328      }
 1329      $posts[] = $row;
 1330    }
 1331    
 1332    // Copy posts to the other board
 1333    mysql_board_call('START TRANSACTION');
 1334    
 1335    $new_resto = 0;
 1336    
 1337    $from_pids = array();
 1338    $to_pids = array();
 1339    
 1340    foreach ($posts as &$post) {
 1341      $comment = str_replace($from_pids, $to_pids, $post['com']);
 1342      
 1343      if ($new_resto === 0) {
 1344        $root_time = $post['root'];
 1345      }
 1346      else {
 1347        $root_time = 0;
 1348      }
 1349      
 1350      $query = "INSERT INTO `$to_board`(now,name,sub,com,host,pwd,filename,ext,w,
 1351  h,tn_w,tn_h, tim,time,last_modified,md5,fsize,root,resto,capcode,
 1352  4pass_id,since4pass,filedeleted,tmd5,id,sticky,closed,country)
 1353  VALUE (" .
 1354      "'" . $post['now'] . "'," .
 1355      "'" . mysql_real_escape_string($post['name']) . "'," .
 1356      "'" . mysql_real_escape_string($post['sub']) . "'," .
 1357      "'" . mysql_real_escape_string($comment) . "'," .
 1358      "'" . mysql_real_escape_string($post['host']) . "'," .
 1359      "'" . mysql_real_escape_string($post['pwd']) . "'," .
 1360      "'" . mysql_real_escape_string($post['filename']) . "'," .
 1361      "'" . $post['ext'] . "'," .
 1362      (int)$post['w'] . "," .
 1363      (int)$post['h'] . "," .
 1364      (int)$post['tn_w'] . "," .
 1365      (int)$post['tn_h'] . "," .
 1366      "'" . $post['tim'] . "'," .
 1367      (int)$post['time'] . "," .
 1368      (int)$post['time'] . "," .
 1369      "'" . $post['md5'] . "'," .
 1370      (int)$post['fsize'] . "," .
 1371      "'" . $root_time . "'," .
 1372      $new_resto . "," .
 1373      "'" . $post['capcode'] . "'," .
 1374      "'" . $post['4pass_id'] . "'," .
 1375      (int)$post['since4pass'] . "," .
 1376      (int)$post['filedeleted'] . "," .
 1377      "'" . $post['tmd5'] . "'," .
 1378      "'" . mysql_real_escape_string($post['id']) . "'," .
 1379      (int)$post['sticky'] . "," .
 1380      (int)$post['closed'] . "," .
 1381      "'XX')";
 1382      
 1383      $res = mysql_board_call($query);
 1384      if (!$res) {
 1385        if ($new_resto === 0) {
 1386          mysql_board_call('ROLLBACK');
 1387          return 'Database Error (5)';
 1388        }
 1389        
 1390        $post['ext'] = null;
 1391        
 1392        continue;
 1393      }
 1394      
 1395      $new_pid = mysql_board_insert_id();
 1396      
 1397      if ($new_resto === 0) {
 1398        $new_resto = $new_pid;
 1399      }
 1400      
 1401      $from_pids[] = "&gt;&gt;{$post['no']}";
 1402      $to_pids[] = "&gt;&gt;$new_pid";
 1403      
 1404      $post['new_id'] = $new_pid;
 1405    }
 1406    
 1407    unset($post);
 1408    
 1409    mysql_board_call('COMMIT');
 1410    
 1411    // Copy files
 1412    // If the file already exists, update the database and set it as "deleted"
 1413    $to_img_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', IMG_ROOT) . $to_board . '/';
 1414    $to_thumb_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', THUMB_ROOT) . $to_board . '/';
 1415    
 1416    $dup_pids = array();
 1417    
 1418    foreach ($posts as $post) {
 1419      if (!$post['ext'] || $post['filedeleted']) {
 1420        continue;
 1421      }
 1422      
 1423      $src_thumb = THUMB_DIR . $post['tim'] . 's.jpg';
 1424      $dest_thumb = $to_thumb_dir . $post['tim'] . 's.jpg';
 1425      
 1426      if (file_exists($dest_thumb)) {
 1427        //$dup_pids[] = $post['new_id'];
 1428        continue;
 1429      }
 1430      
 1431      $src_img = IMG_DIR . $post['tim'] . $post['ext'];
 1432      $dest_img = $to_img_dir . $post['tim'] . $post['ext'];
 1433      
 1434      @copy($src_thumb, $dest_thumb);
 1435      @copy($src_img, $dest_img);
 1436    }
 1437    
 1438    if (!empty($dup_pids)) {
 1439      $dup_clause = implode(',', $dup_pids);
 1440      $query = "UPDATE `$to_board` SET filedeleted = 1, ext = '' WHERE no IN($dup_clause)";
 1441      $res = mysql_board_call($query);
 1442    }
 1443    
 1444    // Ask the destination board to build the new thread
 1445    remote_rebuild_live_thread($to_board, $new_resto);
 1446    
 1447    // Rebuild indexes
 1448    if (!STATIC_REBUILD) {
 1449      updatelog(0, 0);
 1450    }
 1451    
 1452    return array($to_board, $new_resto);
 1453  }
 1454  
 1455  function move_thread($thread_id, $to_board, $delete = false) {
 1456    if (UPLOAD_BOARD || BOARD_DIR === 'b' || JANITOR_BOARD) {
 1457      return "The current board doesn't support this feature.";
 1458    }
 1459    
 1460    if ($to_board === 'f' || $to_board === 'j') {
 1461      return "The destination board doesn't support this feature.";
 1462    }
 1463    
 1464    if ($to_board === BOARD_DIR) {
 1465      return "Invalid destination board.";
 1466    }
 1467    
 1468    $thread_id = (int)$thread_id;
 1469    
 1470    if (!$thread_id) {
 1471      return "Invalid thread ID.";
 1472    }
 1473    
 1474    // Validate destination board
 1475    if (!preg_match('/^[a-z0-9]+$/', $to_board)) {
 1476      return 'Invalid destination board.';
 1477    }
 1478    
 1479    $query = "SELECT COUNT(*) FROM boardlist WHERE dir = '%s' LIMIT 1";
 1480    
 1481    $res = mysql_global_call($query, $to_board);
 1482    
 1483    if (!$res) {
 1484      return "Database Error (1)";
 1485    }
 1486    
 1487    if (mysql_num_rows($res) < 1) {
 1488      return "Destination board doesn't exist.";
 1489    }
 1490    
 1491    // ---
 1492    
 1493    $board = mysql_real_escape_string(BOARD_DIR);
 1494    
 1495    // Lock the thread immediately
 1496    $query = "UPDATE `$board` SET closed = 1 WHERE no = $thread_id";
 1497    
 1498    $res = mysql_board_call($query);
 1499    
 1500    if (!$res) {
 1501      return "Database Error (2)";
 1502    }
 1503    
 1504    if (mysql_affected_rows() !== 1) {
 1505      return "This thread is locked.";
 1506    }
 1507    
 1508    // Fetch the whole thread
 1509    $posts = array();
 1510    
 1511    $query = "SELECT * FROM `$board` WHERE no = $thread_id AND resto = 0";
 1512    
 1513    $res = mysql_board_call($query);
 1514    
 1515    if (!$res) {
 1516      return "Database Error (3)";
 1517    }
 1518    
 1519    $row = mysql_fetch_assoc($res);
 1520    
 1521    if (!$row) {
 1522      return "Thread not found.";
 1523    }
 1524    
 1525    if ($row['archived'] !== '0') {
 1526      return "You cannot move archived threads.";
 1527    }
 1528    
 1529    $posts[] = $row;
 1530    
 1531    $query = "SELECT * FROM `$board` WHERE resto = $thread_id";
 1532    
 1533    $res = mysql_board_call($query);
 1534    
 1535    if (!$res) {
 1536      return "Database Error (4)";
 1537    }
 1538    
 1539    while ($row = mysql_fetch_assoc($res)) {
 1540      if (!$row) {
 1541        continue;
 1542      }
 1543      $posts[] = $row;
 1544    }
 1545    
 1546    // Copy posts to the other board
 1547    mysql_board_call('START TRANSACTION');
 1548    
 1549    $new_resto = 0;
 1550    
 1551    $from_pids = array();
 1552    $to_pids = array();
 1553    
 1554    foreach ($posts as &$post) {
 1555      $comment = str_replace($from_pids, $to_pids, $post['com']);
 1556      
 1557      if ($new_resto === 0) {
 1558        $root_time = 'NOW()';
 1559      }
 1560      else {
 1561        $root_time = 0;
 1562      }
 1563      
 1564      if (SHOW_COUNTRY_FLAGS && $post['board_flag'] == '') {
 1565        $flag_val = $post['country'];
 1566      }
 1567      else {
 1568        $flag_val = 'XX';
 1569      }
 1570      
 1571      $query = "INSERT INTO `$to_board`(now,name,sub,com,host,pwd,email,filename,ext,w,
 1572  h,tn_w,tn_h, tim,time,last_modified,md5,fsize,root,resto,capcode,
 1573  4pass_id,since4pass,filedeleted,tmd5,id,country)
 1574  VALUE (" .
 1575      "'" . $post['now'] . "'," .
 1576      "'" . mysql_real_escape_string($post['name']) . "'," .
 1577      "'" . mysql_real_escape_string($post['sub']) . "'," .
 1578      "'" . mysql_real_escape_string($comment) . "'," .
 1579      "'" . mysql_real_escape_string($post['host']) . "'," .
 1580      "'" . mysql_real_escape_string($post['pwd']) . "'," .
 1581      "'" . mysql_real_escape_string($post['email']) . "'," .
 1582      "'" . mysql_real_escape_string($post['filename']) . "'," .
 1583      "'" . $post['ext'] . "'," .
 1584      (int)$post['w'] . "," .
 1585      (int)$post['h'] . "," .
 1586      (int)$post['tn_w'] . "," .
 1587      (int)$post['tn_h'] . "," .
 1588      "'" . $post['tim'] . "'," .
 1589      (int)$post['time'] . "," .
 1590      (int)$post['time'] . "," .
 1591      "'" . $post['md5'] . "'," .
 1592      (int)$post['fsize'] . "," .
 1593      $root_time . "," .
 1594      $new_resto . "," .
 1595      "'" . $post['capcode'] . "'," .
 1596      "'" . $post['4pass_id'] . "'," .
 1597      (int)$post['since4pass'] . "," .
 1598      (int)$post['filedeleted'] . "," .
 1599      "'" . $post['tmd5'] . "'," .
 1600      "''," .
 1601      "'" . $flag_val . "')";
 1602      
 1603      $res = mysql_board_call($query);
 1604      
 1605      if (!$res) {
 1606        if ($new_resto === 0) {
 1607          mysql_board_call('ROLLBACK');
 1608          return 'Database Error (5)';
 1609        }
 1610        
 1611        $post['ext'] = null;
 1612        
 1613        continue;
 1614      }
 1615      
 1616      $new_pid = mysql_board_insert_id();
 1617      
 1618      if ($new_resto === 0) {
 1619        $new_resto = $new_pid;
 1620      }
 1621      
 1622      $from_pids[] = "&gt;&gt;{$post['no']}";
 1623      $to_pids[] = "&gt;&gt;$new_pid";
 1624      
 1625      $post['new_id'] = $new_pid;
 1626    }
 1627    
 1628    unset($post);
 1629    
 1630    mysql_board_call('COMMIT');
 1631    
 1632    // Log the action
 1633    $thread = $posts[0];
 1634    
 1635    $log_com = "<b>From /$board/$thread_id to /$to_board/$new_resto</b>";
 1636    
 1637    if ($thread['com'] !== '') {
 1638      $log_com = $thread['com'] . '<br><br>' . $log_com;
 1639    }
 1640    
 1641    $action_log_post = array(
 1642      'no' => $thread['no'],
 1643      'name' => $thread['name'],
 1644      'sub' => $thread['sub'],
 1645      'com' => $log_com,
 1646      'filename' => $thread['filename'],
 1647      'ext' => $thread['ext']
 1648    );
 1649    
 1650    log_mod_action(6, $action_log_post);
 1651    
 1652    // Copy files
 1653    // If the file already exists, update the database and set it as "deleted"
 1654    $to_img_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', IMG_ROOT) . $to_board . '/';
 1655    $to_thumb_dir = preg_replace('/' . BOARD_DIR . '\/$/', '', THUMB_ROOT) . $to_board . '/';
 1656    
 1657    $dup_pids = array();
 1658    
 1659    foreach ($posts as $post) {
 1660      if (!$post['ext'] || $post['filedeleted']) {
 1661        continue;
 1662      }
 1663      
 1664      $src_thumb = THUMB_DIR . $post['tim'] . 's.jpg';
 1665      $dest_thumb = $to_thumb_dir . $post['tim'] . 's.jpg';
 1666      
 1667      if (file_exists($dest_thumb)) {
 1668        $dup_pids[] = $post['new_id'];
 1669        continue;
 1670      }
 1671      
 1672      $src_img = IMG_DIR . $post['tim'] . $post['ext'];
 1673      $dest_img = $to_img_dir . $post['tim'] . $post['ext'];
 1674      
 1675      copy($src_thumb, $dest_thumb);
 1676      copy($src_img, $dest_img);
 1677    }
 1678    
 1679    if (!empty($dup_pids)) {
 1680      $dup_clause = implode(',', $dup_pids);
 1681      $query = "UPDATE `$to_board` SET filedeleted = 1, ext = '' WHERE no IN($dup_clause)";
 1682      $res = mysql_board_call($query);
 1683    }
 1684    
 1685    // Insert notification post if deletion is not requested
 1686    if (!$delete) {
 1687      $msg = sprintf(S_THREAD_MOVED, "&gt;&gt;&gt;/$to_board/$new_resto");
 1688      
 1689      $post_time = $_SERVER['REQUEST_TIME'];
 1690      $tim = generate_tim();
 1691      
 1692      $query = "INSERT INTO `$board`(now,name,sub,com,host,pwd,filename,ext,w,
 1693  h,tn_w,tn_h, tim,time,last_modified,md5,fsize,resto,capcode,
 1694  4pass_id,tmd5,id)
 1695  VALUE (" .
 1696    "'" . date('m/d/y(D)H:i:s', $post_time) . "'," .
 1697    "'" . S_ANONAME . "'," .
 1698    "''," .
 1699    "'" . mysql_real_escape_string($msg) . "'," .
 1700    "'', '', '', '', 0, 0, 0, 0, '" . $tim . "'," . $post_time . "," .
 1701    $post_time . ", '',0," . $thread_id . ", 'mod', '', '', '')";
 1702    
 1703      $res = mysql_board_call($query);
 1704    }
 1705    
 1706    // Ask the destination board to build the new thread
 1707    remote_rebuild_live_thread($to_board, $new_resto);
 1708    
 1709    // Delete source thread if deletion is requested
 1710    if ($delete) {
 1711      // id, pwd, imgonly, auto, die
 1712      delete_post($thread_id, 'trim', 0, 2, 1, 0);
 1713    }
 1714    // Archive source thread if archiving is enabled
 1715    else if (ENABLE_ARCHIVE) {
 1716      archive_thread($thread_id);
 1717      
 1718      if (!STATIC_REBUILD && ENABLE_JSON_THREADS) {
 1719        generate_board_archived_json();
 1720      }
 1721    }
 1722    // Rebuild source thread and indexes if archiving is disabled
 1723    else {
 1724      updatelog($thread_id, 1);
 1725    }
 1726    
 1727    // Rebuild indexes
 1728    if (!STATIC_REBUILD) {
 1729      updatelog(0, 0);
 1730    }
 1731    
 1732    return array($to_board, $new_resto);
 1733  }
 1734  
 1735  function remote_rebuild_live_thread($board, $pid) {
 1736    $post = array(
 1737      'mode' => 'rebuildadmin',
 1738      'no' => $pid
 1739    );
 1740    
 1741    rpc_start_request("https://sys.int/$board/imgboard.php", $post, $_COOKIE, true);
 1742    
 1743    return true;
 1744  }
 1745  
 1746  function archive_thread($thread_id) {
 1747    global $log;
 1748    
 1749    $thread_id = (int)$thread_id;
 1750    
 1751    $board = mysql_real_escape_string(BOARD_DIR);
 1752    
 1753    if (!$thread_id) {
 1754      return;
 1755    }
 1756    
 1757    // Regenerate the user ID before clearing the IP
 1758    $uid = '';
 1759    
 1760    if (DISP_ID && DISP_ID_PER_THREAD && !DISP_ID_RANDOM) {
 1761      $th = null;
 1762      
 1763      if (!IS_REBUILDD && isset($log[$thread_id])) {
 1764        $th = $log[$thread_id];
 1765      }
 1766      else {
 1767        $query = 'SELECT id, host FROM `' . BOARD_DIR . "` WHERE no = $thread_id";
 1768        
 1769        $res = mysql_board_call($query);
 1770        
 1771        if ($res) {
 1772          $th = mysql_fetch_assoc($res);
 1773        }
 1774      }
 1775      
 1776      if ($th && $th['id'] !== '' && $th['host']) {
 1777        $uid = generate_uid($thread_id, $_SERVER['REQUEST_TIME'], $th['host']);
 1778        $uid = ", id = '" . mysql_real_escape_string($uid) . "'";
 1779      }
 1780    }
 1781    
 1782    // Update the OP. "root" is used for archive pruning.
 1783    $query = <<<SQL
 1784  UPDATE `$board`
 1785  SET archived = 1, closed = 1, sticky = 0, email = '', host = '', 4pass_id = '', pwd = '', root = NOW()$uid
 1786  WHERE no = $thread_id
 1787  LIMIT 1
 1788  SQL;
 1789    
 1790    $res = mysql_board_call($query);
 1791    
 1792    if (!$res) {
 1793      return;
 1794    }
 1795    
 1796    // Update replies
 1797    $query = <<<SQL
 1798  UPDATE `$board`
 1799  SET archived = 1, email = '', host = '', 4pass_id = '', pwd = ''
 1800  WHERE resto = $thread_id
 1801  SQL;
 1802    
 1803    $res = mysql_board_call($query);
 1804    
 1805    // Update cached $log
 1806    if (isset($log[$thread_id])) {
 1807      $log[$thread_id]['archived'] = true;
 1808      $log[$thread_id]['archived_on'] = time();
 1809      
 1810      $thread_key = array_search($thread_id, $log['THREADS']);
 1811      
 1812      if ($thread_key !== false) {
 1813        unset($log['THREADS'][$thread_key]);
 1814      }
 1815    }
 1816    
 1817    // Rebuild the thread
 1818    rebuild_archived_thread($thread_id);
 1819    
 1820    /**
 1821     * Clear reports (only posts with less than 3 "illegal" reports)
 1822     */
 1823    // Get all post ids
 1824    $query = "SELECT no FROM `$board` WHERE no = $thread_id OR resto = $thread_id";
 1825    
 1826    $res = mysql_board_call($query);
 1827    
 1828    if (!$res || !mysql_num_rows($res)) {
 1829      return;
 1830    }
 1831    
 1832    // Get ids of reported posts, with less than 3 "illegal" reports
 1833    $post_ids = array();
 1834    
 1835    while ($row = mysql_fetch_row($res)) {
 1836      $post_ids[] = $row[0];
 1837    }
 1838    
 1839    $in_clause_all = implode(',', $post_ids);
 1840    
 1841    $query = <<<SQL
 1842  SELECT postid FROM reports_for_posts
 1843  WHERE board = '$board' AND num_illegal < 3 AND postid IN($in_clause_all)
 1844  SQL;
 1845    
 1846    $res = mysql_global_call($query);
 1847    
 1848    if (!$res || !mysql_num_rows($res)) {
 1849      return;
 1850    }
 1851    
 1852    // Delete reports for posts with less than 3 "illegal" reports
 1853    $post_ids = array();
 1854    
 1855    while ($row = mysql_fetch_row($res)) {
 1856      $post_ids[] = $row[0];
 1857    }
 1858    
 1859    $in_clause_reports = implode(',', $post_ids);
 1860    
 1861    $query = "DELETE FROM reports WHERE board = '$board' AND no IN($in_clause_reports)";
 1862    mysql_global_call($query);
 1863    
 1864    $query = "DELETE FROM reports_for_posts WHERE board = '$board' AND postid IN($in_clause_reports)";
 1865    mysql_global_call($query);
 1866    
 1867    // Handle XFF entries
 1868    if (SAVE_XFF) {
 1869      $query = "UPDATE xff SET is_live = 0 WHERE board = '$board' AND postno IN($in_clause_all)";
 1870      mysql_global_call($query);
 1871    }
 1872  }
 1873  
 1874  function thumb_url()
 1875  {
 1876  	return "//" . THUMB_DIR2_PART;
 1877  }
 1878  
 1879  function display_no( $no )
 1880  {
 1881  	if (FAKE_DOUBLES) {
 1882  		$last_digit = $no % 10;
 1883  		return $no.$last_digit;
 1884  	}
 1885  
 1886  	return $no;
 1887  }
 1888  
 1889  function display_uid( $id, $capcode = '' )
 1890  {
 1891  	if( DISP_ID == 0 || !$id ) return "";
 1892  	$normid = $id; // preg_replace( "#[^A-Za-z0-9]#", "_", $id );
 1893  
 1894  
 1895  	if( $id == "Mod" ) {
 1896  		$id = '<span style="color: #800080; font-weight: bold;" class="posteruid id_mod">Mod</span>' . $capcode;
 1897  	} else if( $id == "Admin" ) {
 1898  		$id = '<span style="color: #F00000; font-weight: bold;" class="posteruid id_admin">Admin</span>' . $capcode;
 1899  	} else if( $id == 'Developer' ) {
 1900  		$id = '<span style="color: #0000F0; font-weight: bold;" class="posteruid id_developer">Developer</span>' . $capcode;
 1901  	} else if( $id == 'Manager' ) {
 1902  		$id = '<span style="color: #FF0080; font-weight: bold;" class="posteruid id_manager">Manager</span>' . $capcode;
 1903  	} else {
 1904  		$id = htmlspecialchars( $id );
 1905  	}
 1906  
 1907  	return " <span class=\"posteruid id_$normid\">(ID: <span class=\"hand\" title=\"Highlight posts by this ID\">$id</span>)</span>";
 1908  }
 1909  
 1910  function emailencode( $str )
 1911  {
 1912  	return str_replace( "%40", "@", rawurlencode( $str ) );
 1913  }
 1914  
 1915  function renderPostHtml($no, $in_thread, $sorted_replies = null, $reply_count = null, $shown_replies = null, $is_archived = false) {
 1916  	global $log, $board_flags_array;
 1917  	
 1918  	extract($log[$no]);
 1919  	
 1920  	$namestyle = '';
 1921  
 1922  	if( JANITOR_BOARD == 1 ) {
 1923  		$namestyle = broomcloset_style( $name );
 1924  		$name = broomcloset_name( $name );
 1925  	}
 1926  	
 1927  	$mname = $name;
 1928  	$mname_truncated = '';
 1929  	if( $capcode == 'none' && mb_strlen( $name ) > 30 ) {
 1930  		$mname = explode( '</span>', $name, 2 );
 1931  		if( mb_strlen( $mname[0] ) > 30 ) {
 1932  			$mname[0] = htmlspecialchars_decode($mname[0], ENT_QUOTES);
 1933  			$mname[0] = mb_substr( $mname[0], 0, 30 ) . '(...)';
 1934  			$mname[0] = htmlspecialchars($mname[0], ENT_QUOTES);
 1935  		  $mname_truncated = ' data-tip data-tip-cb="mShowFull"';
 1936  		}
 1937  		
 1938  		$mname = implode( '</span>', $mname );
 1939  	}
 1940  	
 1941  	$hasCapcode = $capcode === 'none' ? '' : ' capcode';
 1942    
 1943  	// NEW CAPCODE STUFF
 1944  	switch( $capcode ) {
 1945  		case 'admin':
 1946  			$capcodeStart  = ' <strong class="capcode hand id_admin" title="Highlight posts by Administrators">## Admin</strong>';
 1947  			$capcode_class = ' capcodeAdmin';
 1948  
 1949  			$capcode   = ' <img src="' . STATIC_IMG_DIR2 . 'adminicon.gif" alt="Admin Icon" title="This user is a 4chan Administrator." class="identityIcon retina">';
 1950  			$highlight = '';
 1951  			break;
 1952  
 1953  		case 'founder':
 1954  			$capcodeStart  = ' <strong class="capcode hand" title="Highlight posts by the Founder">## Founder</strong>';
 1955  			$capcode_class = ' capcodeFounder';
 1956  
 1957  			$capcode   = ' <img src="' . STATIC_IMG_DIR2 . 'foundericon.gif" alt="Founder Icon" title="This user is 4chan\'s Founder." class="identityIcon retina">';
 1958  			$highlight = '';
 1959  			break;
 1960  
 1961  		case 'admin_highlight':
 1962  			$capcodeStart  = ' <strong class="capcode hand id_admin" title="Highlight posts by Administrators">## Admin</strong>';
 1963  			$capcode_class = ' capcodeAdmin';
 1964  
 1965  			$capcode   = ' <img src="' . STATIC_IMG_DIR2 . 'adminicon.gif" alt="Admin Icon" title="This user is a 4chan Administrator." class="identityIcon retina">';
 1966  			$highlight = ' highlightPost';
 1967  			break;
 1968  
 1969  		case 'mod':
 1970  			$capcodeStart  = ' <strong class="capcode hand id_mod" title="Highlight posts by Moderators">## Mod</strong>';
 1971  			$capcode_class = ' capcodeMod';
 1972  
 1973  			$capcode   = ' <img src="' . STATIC_IMG_DIR2 . 'modicon.gif" alt="Mod Icon" title="This user is a 4chan Moderator." class="identityIcon retina">';
 1974  			$highlight = '';
 1975  			break;
 1976  
 1977  		case 'developer':
 1978  			$capcodeStart  = ' <strong class="capcode hand id_developer" title="Highlight posts by Developers">## Developer</strong>';
 1979  			$capcode_class = ' capcodeDeveloper';
 1980  
 1981  			$capcode   = ' <img src="' . STATIC_IMG_DIR2 . 'developericon.gif" alt="Developer Icon" title="This user is a 4chan Developer." class="identityIcon retina">';
 1982  			$highlight = '';
 1983  			break;
 1984  		
 1985  		case 'manager':
 1986  			$capcodeStart  = ' <strong class="capcode hand id_manager" title="Highlight posts by Managers">## Manager</strong>';
 1987  			$capcode_class = ' capcodeManager';
 1988  
 1989  			$capcode   = ' <img src="' . STATIC_IMG_DIR2 . 'managericon.gif" alt="Manager Icon" title="This user is a 4chan Manager." class="identityIcon retina">';
 1990  			$highlight = '';
 1991  			break;
 1992  		
 1993  		case 'verified':
 1994  			$capcodeStart  = ' <strong class="capcode hand id_verified" title="Highlight posts by Verified Users">## Verified</strong>';
 1995  			$capcode_class = ' capcodeVerified';
 1996  
 1997  			$capcode   = '';
 1998  			$highlight = '';
 1999  			break;
 2000  
 2001  		default:
 2002  			$capcode = $capcodeStart = $highlight = $capcode_class = '';
 2003  			break;
 2004  	}
 2005  
 2006  	$spoiler = 0;
 2007  	
 2008  	if( strpos( $sub, 'SPOILER<>' ) === 0 ) {
 2009  		$sub     = substr( $sub, strlen( 'SPOILER<>' ) );
 2010  		$spoiler = 1;
 2011  	}
 2012  	
 2013    // Only OPs have subjects
 2014    if ($sorted_replies !== null) {
 2015      $subshortm = $sub;
 2016      if( mb_strlen( $sub ) > 30 ) {
 2017        $sub    = str_replace( array('&#44;'), ',', $sub );
 2018        $cutsub = htmlspecialchars_decode( $sub, ENT_QUOTES );
 2019        $cutsub = mb_substr( $cutsub, 0, 30 );
 2020        $cutsub = htmlspecialchars( $cutsub, ENT_QUOTES );
 2021        
 2022        $subshortm = '<span data-tip data-tip-cb="mShowFull">' . $cutsub . '(...)</span>';
 2023      }
 2024      
 2025      $subshortm = '<span class="subject">' . $subshortm . '</span> ';
 2026  		$sub = '<span class="subject">' . $sub . '</span> ';
 2027    }
 2028    else {
 2029      $sub = $subshortm = '';
 2030    }
 2031  
 2032  	$com = auto_link( $com, $in_thread );
 2033  
 2034  	if( !$in_thread ) {
 2035  		list( $com, $abbreviated ) = abbreviate( $com, MAX_LINES_SHOWN );
 2036  	}
 2037  
 2038  	if( isset( $abbreviated ) && $abbreviated ) {
 2039  		$com .= '<br><br><span class="abbr">Comment too long. <a href="' . RES_DIR2 . ( $resto ? $resto : $no ) . PHP_EXT2 . '#p' . $no . '">Click here</a> to view the full text.</span>';
 2040  	}
 2041  
 2042  	// Image tag creation
 2043  	$file = '';
 2044  	if( $ext ) {
 2045  		$img        = IMG_DIR . $tim . $ext;
 2046  		$displaysrc = IMG_DIR2 . $tim . $ext;
 2047      
 2048  		//if ($ext !== ".swf" && BOARD_DIR !== 'j') {
 2049  			//if ($no % 100 >= 74) {
 2050  				//$displaysrc = "//is2.4chan.org/" . BOARD_DIR . "/" . $tim . $ext;
 2051  			//}
 2052  		//}
 2053      
 2054  		$linksrc    = ( ( USE_SRC_CGI == 1 ) ? ( str_replace( '.cgi', '', IMG_DIR2 ) . $tim . $ext ) : $displaysrc );
 2055  		
 2056  		if( defined( 'INTERSTITIAL_LINK' ) ) {
 2057  			$linksrc = str_replace( INTERSTITIAL_LINK, '', $linksrc );
 2058  		}
 2059  		
 2060  		// Original filename truncation
 2061  		$unescaped_filename = htmlspecialchars_decode($filename, ENT_QUOTES);
 2062  		if( mb_strlen( $unescaped_filename, 'UTF-8' ) > 30 ) {
 2063  			$shortname = mb_substr($unescaped_filename, 0, 25, 'UTF-8');
 2064  			$shortname = htmlspecialchars($shortname, ENT_QUOTES). '(...)' . $ext;
 2065  			$longname = $filename . $ext;
 2066  			$need_file_tooltip = true;
 2067  		}
 2068  		else {
 2069  			$shortname = $longname = $filename . $ext;
 2070  			$need_file_tooltip = false;
 2071  		}
 2072  		
 2073  		if( THREAD_AD == 1 ) {
 2074  			if( defined( 'THREAD_AD_TXT' ) && THREAD_AD_TXT ) {
 2075  				$ad = text_link_ad( THREAD_AD_TXT );
 2076  				if( $ad )
 2077  					$dat .= "<span class=\"filesize\">" . S_ADNAME . " : $ad</span><br>";
 2078  			}
 2079  		}
 2080  		
 2081  		$s_src = IMG_DIR . $tim . $ext;
 2082  		
 2083  		// 32>24 byte ascii>base64 conversion
 2084  		$shortmd5 = base64_encode( pack( 'H*', $md5 ) );
 2085  		if( $fsize >= 1048576 ) {
 2086  			$size = round( ( $fsize / 1048576 ), 2 ) . ' M';
 2087  		}
 2088  		elseif( $fsize >= 1024 ) {
 2089  			$size = round( $fsize / 1024 ) . ' K';
 2090  		}
 2091  		else {
 2092  			$size = $fsize . ' ';
 2093  		}
 2094  		
 2095  		$ftype     = strtoupper( substr( $ext, 1 ) );
 2096  		$mFileInfo = '<div data-tip data-tip-cb="mShowFull" class="mFileInfo mobile">' . $size . 'B ' . $ftype . '</div>';
 2097  		
 2098  		if( !$tn_w && !$tn_h && $ext == '.gif' ) {
 2099  			$tn_w = $w;
 2100  			$tn_h = $h;
 2101  		}
 2102  		
 2103  		$class = '';
 2104  		if( $spoiler ) {
 2105  			$class = ' imgspoiler';
 2106  			// Replace 3 image tags with one, makes it easier to change in the future
 2107  			$imgthumb_src = SPOILER_THUMB;
 2108  			$tn_w         = '100';
 2109  			$tn_h         = '100';
 2110  		}
 2111  		else {
 2112  			//$imgthumb_src = thumb_url() . $tim . 's.jpg';
 2113  			$imgthumb_src = '//' . THUMB_DIR2_PART . $tim . 's.jpg';
 2114  		}
 2115  		
 2116      if (MOBILE_IMG_RESIZE && $m_img) {
 2117        $m_img_attr = ' data-m';
 2118      }
 2119      else {
 2120        $m_img_attr = '';
 2121      }
 2122  		
 2123      $imgsrc = '<a class="fileThumb' . $class . '" href="' . $displaysrc . '" target="_blank"' . $m_img_attr . '><img src="' . $imgthumb_src . '" alt="' . $size . 'B" data-md5="' . $shortmd5 . '" style="height: ' . $tn_h . 'px; width: ' . $tn_w . 'px;" loading="lazy">' . $mFileInfo . '</a>';
 2124  		
 2125  		if( $filedeleted ) {
 2126  			$fileinfo = '<span class="fileThumb"><img src="' . STATIC_IMG_DIR2 . 'filedeleted-res.gif" alt="File deleted." class="fileDeletedRes retina"></span>';
 2127  			$imgsrc   = '';
 2128  		}
 2129  		else {
 2130  			$dimensions = ( $ext == '.pdf' ) ? 'PDF' : $w . 'x' . $h;
 2131  			if( !$spoiler ) {
 2132  				$fileinfo = '<div class="fileText" id="fT' . $no . '">' . S_PICNAME . ': <a' . ($need_file_tooltip ? (' title="' . $longname . '"') : '') . ' href="' . $linksrc . '" target="_blank">' . $shortname . '</a> (' . $size . 'B, ' . $dimensions . ')</div>';
 2133  			}
 2134  			else {
 2135  				$fileinfo = '<div class="fileText" id="fT' . $no . '" title="' . $longname . '">' . S_PICNAME . ': <a href="' . $linksrc . '" target="_blank">Spoiler Image</a> (' . $size . 'B, ' . $dimensions . ')</div>';
 2136  			}
 2137  		}
 2138  		
 2139  		$file = <<<HTML
 2140  	<div class="file" id="f$no">
 2141  		$fileinfo
 2142  		$imgsrc
 2143  	</div>
 2144  HTML;
 2145  	}
 2146  	
 2147  	/**
 2148  	 * OP specific html
 2149  	 */
 2150  	if ($sorted_replies !== null) {
 2151  		if ($in_thread) {
 2152  			$href = '';
 2153  			$postinfo_extra = '';
 2154  		}
 2155  		else {
 2156  			$href = RES_DIR2 . $no . PHP_EXT2;
 2157  			
 2158  			if ($semantic_url !== '') {
 2159  			  $semantic_url = "/$semantic_url";
 2160  			}
 2161  			
 2162  			$postinfo_extra = ' &nbsp; <span>[<a href="'
 2163  			  . $href . $semantic_url
 2164  			  . '" class="replylink">' . S_REPLY . '</a>]</span>';
 2165  		}
 2166  		
 2167  		$oldtext = '';
 2168  		$extra   = '';
 2169  		
 2170  		// Marked for deletion (old)
 2171  		if (isset($log[$no]['old'])) {
 2172  			$oldtext .= '<span class="oldpost">' . S_OLD . '</span><br>';
 2173  		}
 2174  		
 2175  		$postInfo = '';
 2176  		
 2177  		// Count omitted replies and images
 2178  		if (!$in_thread) {
 2179        $s = $reply_count - $shown_replies;
 2180        
 2181        if ($shown_replies) {
 2182          $t = 0;
 2183          $total_t = 0;
 2184          
 2185          $cur = 1;
 2186          
 2187          while ($s >= $cur) {
 2188            list($row) = each($sorted_replies);
 2189            
 2190            if ($log[$row]['fsize'] && !$log[$row]['filedeleted']) {
 2191              $t++;
 2192            }
 2193            
 2194            $cur++;
 2195          }
 2196          
 2197          $total_t = $t;
 2198          
 2199          while ($reply_count >= $cur) {
 2200            list($row) = each($sorted_replies);
 2201            
 2202            if ($log[$row]['fsize'] && !$log[$row]['filedeleted']) {
 2203              $total_t++;
 2204            }
 2205            
 2206            $cur++;
 2207          }
 2208          
 2209          if ($reply_count != 0) {
 2210            reset($sorted_replies);
 2211          }
 2212        }
 2213        else {
 2214          $total_t = $t = $imgreplycount;
 2215        }
 2216  		  
 2217    		// desktop
 2218  			$posts   = ( $s < 2 ) ? ' reply' : ' replies';
 2219  			$replies = ( $t < 2 ) ? ' image' : ' images';
 2220  			
 2221  			if (( $s > 0 ) && ( $t == 0 )) {
 2222  				$extra .= '<span class="summary desktop">' . $s . $posts . ' omitted. <a href="' . $href . '" class="replylink">Click here</a> to view.</span>';
 2223  			}
 2224  			elseif (( $s > 0 ) && ( $t > 0 )) {
 2225  				$extra .= '<span class="summary desktop">' . $s . $posts . ' and ' . $t . $replies . ' omitted. <a href="' . $href . '" class="replylink">Click here</a> to view.</span>';
 2226  			}
 2227  			
 2228    		// mobile
 2229    		$posts   = ( $reply_count < 2 ) ? ' Reply' : ' Replies';
 2230    		$replies = ( $total_t < 2 ) ? ' Image' : ' Images';
 2231    		
 2232    		if (( $reply_count > 0 ) && ( $total_t == 0 )) {
 2233    			// Text replies only
 2234    			$info = '' . $reply_count . $posts . '';
 2235    		}
 2236    		elseif (( $reply_count > 0 ) && ( $total_t > 0 )) {
 2237    			// Image replies
 2238    			$info = '' . $reply_count . $posts . ' / ' . $total_t . $replies . '';
 2239    		}
 2240    		else {
 2241    			// nothing
 2242    			$info = '';
 2243    		}
 2244    		
 2245    		$postInfo = <<<HTML
 2246  		<div class="postLink mobile">
 2247  			<span class="info">
 2248  				$info
 2249  			</span>
 2250  			
 2251  			<a href="$href" class="button">View Thread</a>
 2252  		</div>
 2253  HTML;
 2254  		}
 2255  		else {
 2256  			$s = 0;
 2257  		}
 2258  		
 2259  		// Sticky - Closed
 2260  		$threadmodes = '';
 2261  		
 2262  		if ($sticky == 1) {
 2263  			$threadmodes .= ' <img src="' . STATIC_IMG_DIR2 . 'sticky.gif" alt="Sticky" title="Sticky" class="stickyIcon retina">';
 2264  		}
 2265  		
 2266      if ($closed == 1) {
 2267        if ($archived) {
 2268          $threadmodes .= ' <img src="' . STATIC_IMG_DIR2 . 'archived.gif" alt="Archived" title="Archived" class="archivedIcon retina">';
 2269        }
 2270        else {
 2271          $threadmodes .= ' <img src="' . STATIC_IMG_DIR2 . 'closed.gif" alt="Closed" title="Closed" class="closedIcon retina">';
 2272        }
 2273      }
 2274  		
 2275  		// Staff replies indicator
 2276  		if (META_BOARD) {
 2277  			$posts = meta_is_thread_flagged($sorted_replies);
 2278  			
 2279  			// admin = 0
 2280  			// dev = 1
 2281  			// mod = 2
 2282  			// manager = 3
 2283  			
 2284  			// array (css_class, text_name)
 2285  			$larr = array(
 2286  				0 => array('Admin', 'Administrator'),
 2287  				1 => array('Developer', 'Developer'),
 2288  				2 => array('Mod', 'Moderator'),
 2289  				3 => array('Manager', 'Manager')
 2290  			);
 2291  			
 2292  			if ($posts[0] || $posts[1] || $posts[2] || $posts[3]) {
 2293  				$com .= '<br><br><span class="capcodeReplies">';
 2294  				
 2295  				foreach ($posts as $key => $postlist) {
 2296  					if (!$postlist) {
 2297  						continue;
 2298  					}
 2299  					
 2300  					$postlist = explode(',', $postlist);
 2301  					
 2302  					$replies = count($postlist) > 1 ? 'Replies' : 'Reply';
 2303  					$com .= '<span class="smaller"><span class="bold">' . $larr[$key][1] . ' ' . $replies . ':</span> ';
 2304  					
 2305  					foreach ($postlist as $postnum) {
 2306  						$com .= '<a href="' . $href . '#p' . $postnum . '" class="quotelink">&gt;&gt;' . $postnum . '</a> ';
 2307  					}
 2308  					
 2309  					$com .= '</span><br>';
 2310  				}
 2311  				
 2312  				$com .= '</span>';
 2313  				
 2314  			}
 2315  		}
 2316  		
 2317  		if (DISP_ID && DISP_ID_PER_THREAD && !$is_archived && !DISP_ID_RANDOM && $id !== '') {
 2318  		  $id = generate_uid($no, $time, $host);
 2319  		}
 2320  		
 2321  		$reply_file = '';
 2322  		$op_file = $file;
 2323  		$post_class = 'op';
 2324  		$sidearrows = '';
 2325  	}
 2326  	/**
 2327  	 * Reply
 2328  	 */
 2329  	else {
 2330  		$href = $in_thread ? '' : RES_DIR2 . $resto . PHP_EXT2;
 2331  		$threadmodes = $postinfo_extra = $oldtext = $postInfo = $extra = $op_file = '';
 2332  		$reply_file = $file;
 2333  		$post_class = 'reply';
 2334  		$sidearrows = '<div class="sideArrows" id="sa' . $no . '">&gt;&gt;</div>';
 2335  		
 2336  		$sub = '';
 2337  	}
 2338  	
 2339  	$dispuid = '';
 2340  	$dispuid = display_uid( $id, $capcode );
 2341  	if( $dispuid == '' || $capcode != '' ) {
 2342  		$dispuid = $capcode;
 2343  	}
 2344  	else {
 2345  		$capcodeStart = '';
 2346  		if (FORCED_ANON) {
 2347  			$capcode_class = '';
 2348  		}
 2349  	}
 2350  
 2351    $countryFlag = '';
 2352    if ($capcode == '') {
 2353      if (ENABLE_BOARD_FLAGS && $board_flag != '' && isset($board_flags_array[$board_flag])) {
 2354        $cname = board_flag_code_to_name($board_flag);
 2355        $countryFlag = ' <span title="' . $cname . '" class="bfl bfl-' . strtolower($board_flag) . '"></span>';
 2356      }
 2357      else if (SHOW_COUNTRY_FLAGS) {
 2358        $cname = country_code_to_name($country);
 2359        $countryFlag = ' <span title="' . $cname . '" class="flag flag-' . strtolower($country) . '"></span>';
 2360      }
 2361    }
 2362    
 2363  	$quote = $in_thread ? 'javascript:quote(\'' . $no . '\');' : $href . '#q' . $no;
 2364  	
 2365  	$postM = '';
 2366  	
 2367  	// Forced anon on meta boards
 2368  	if (META_BOARD && $capcode_class != ' capcodeAdmin') {
 2369  		$name = $mname = S_ANONAME;
 2370  	}
 2371  	
 2372  	if (FORCE_COM && !$capcode) {
 2373  		$com = FORCE_COM_TEXT;
 2374  	}
 2375  	
 2376  	$display_no = display_no($no);
 2377  	
 2378  	if ($since4pass && $capcode == '' && $since4pass < 10000) {
 2379  	  $since4passTag = " <span title=\"Pass user since $since4pass\" class=\"n-pu\"></span>";
 2380  	}
 2381  	else {
 2382  	  $since4passTag = '';
 2383  	}
 2384    
 2385    // April 2024
 2386    if ($since4pass && $capcode == '' && $since4pass >= 10000) {
 2387      $since4passTag = april_2024_get_name_badge($since4pass);
 2388      $_xa24_post_cls = april_2024_get_post_cls($since4pass);
 2389    }
 2390    else {
 2391      $_xa24_post_cls = '';
 2392    }
 2393    
 2394  	return <<<HTML
 2395  		<div class="postContainer {$post_class}Container$postM$_xa24_post_cls" id="pc$no">$sidearrows
 2396  			<div id="p$no" class="post $post_class$highlight">
 2397  				<div class="postInfoM mobile" id="pim$no">
 2398            <span class="nameBlock$capcode_class">
 2399  						<span$mname_truncated class="name$hasCapcode"$namestyle>$mname</span>$since4passTag$capcodeStart$dispuid$countryFlag$threadmodes<br>
 2400  						$subshortm
 2401  					</span>
 2402  
 2403  					<span class="dateTime postNum" data-utc="$time">$now <a href="$href#p$no" title="Link to this post">No.</a><a href="$quote" title="Reply to this post">$display_no</a></span>
 2404  				</div>
 2405  				
 2406  				$op_file
 2407  				
 2408  				<div class="postInfo desktop" id="pi$no">
 2409  					<input type="checkbox" name="$no" value="delete"> 
 2410  					$sub
 2411  					<span class="nameBlock$capcode_class">
 2412  						<span class="name$hasCapcode"$namestyle>$name</span>$since4passTag$capcodeStart $dispuid$countryFlag
 2413  					</span> 
 2414  
 2415  					<span class="dateTime" data-utc="$time">$now</span> 
 2416  
 2417  					<span class="postNum desktop">
 2418  						<a href="$href#p$no" title="Link to this post">No.</a><a href="$quote" title="Reply to this post">$display_no</a>$threadmodes$postinfo_extra
 2419  					</span>
 2420  
 2421  				</div>
 2422  				$reply_file
 2423  				<blockquote class="postMessage" id="m$no">$com</blockquote>
 2424  			</div>
 2425  		$postInfo
 2426  		$oldtext
 2427  		</div>
 2428  		$extra
 2429  HTML;
 2430  }
 2431  
 2432  // deletes a post from the database
 2433  // imgonly: whether to just delete the file or to delete from the database as well
 2434  // automatic: always delete regardless of password/admin (for self-pruning)
 2435  // children: whether to delete just the parent post of a thread or also delete the children
 2436  // die: whether to die on error
 2437  // careful, setting children to 0 could leave orphaned posts.
 2438  function delete_post($resno, $pwd, $imgonly = 0, $automatic = 0, $children = 1, $die = 1, $lazy_rebuild = false, $archived_deletion = false, $tool = null, $user_is_known = true)
 2439  {
 2440  	global $log;
 2441  
 2442  	$resno = intval( $resno );
 2443  	log_cache( 0, $resno, $archived_deletion ? 1 : 0 );
 2444  	
 2445  	$post_exists = true;
 2446  	
 2447  	if (!isset( $log[$resno])) {
 2448  		if ($die) {
 2449  			//error( "Can't find the post $resno." );
 2450        updating_index();
 2451        die();
 2452  		}
 2453  		else {
 2454  		  if ($automatic) {
 2455  		    return 0;
 2456  		  }
 2457  		  
 2458  			$post_exists = false;
 2459  		}
 2460  	}
 2461  
 2462  	$row = $log[$resno];
 2463  
 2464    if (!$automatic && !has_level('janitor')) {
 2465      $cant_del = $cant_del_old = false;
 2466      
 2467      if ($row['resto']) {
 2468        $cant_del = NO_DELETE_REPLY;
 2469      }
 2470      else {
 2471        $cant_del = NO_DELETE_OP;
 2472      }
 2473      
 2474      if ($_SERVER['REQUEST_TIME'] - $log[$resno]['time'] >= RENZOKU_DEL_CANT_AFTER) {
 2475        $cant_del = true;
 2476        $cant_del_old = true;
 2477      }
 2478      
 2479      if ($cant_del) {
 2480        error($cant_del_old ? S_RENZOKU_DEL_CANT_AFTER : S_MAYNOTDEL);
 2481      }
 2482    }
 2483  	
 2484  	// if (!$row['pwd'] && BOARD_DIR=='c') {
 2485  	// 	echo "<!--";
 2486  	// 	var_dump($log);
 2487  	// 	echo "-->";
 2488  	// }
 2489  	
 2490    if ($archived_deletion) {
 2491      // Only authed users can delete archived posts
 2492      $pass_ok = false;
 2493      $host_ok = false;
 2494    }
 2495    else {
 2496      $pass_ok = $pwd && $pwd === $row['pwd'];
 2497      $host_ok = $row['host'] == $_SERVER['REMOTE_ADDR'];
 2498    }
 2499    
 2500    $admin_ok         = has_level() || can_delete( $resno );
 2501    $can_flood_delete = has_level( 'janitor' );
 2502  
 2503  	$can_delete = $automatic || $pass_ok || $host_ok || $admin_ok;
 2504  
 2505  	// quick_log_to( "/www/perhost/del.log", date( "r" ) . " deletion of #$resno by " . $_SERVER['REMOTE_ADDR'] . " pwd \"$pwd\" auto " . (int)$automatic . " adminok " . (int)$admin_ok . " hostok " . (int)$host_ok . " passok " . (int)$pass_ok . " orig ip " . $row['host'] . " pwd " . $row['pwd'] . " com " . substr( $row["com"], 0, 50 ) . " " . ( $can_delete ? "succeeded" : "failed" )."\n" );
 2506  
 2507  	if( !$can_delete ) error( S_BADDELPASS );
 2508  
 2509  	if( $row['sticky'] ) {
 2510  		if( has_level() && !has_level( 'admin' ) && !$automatic && !$archived_deletion) error( S_MAYNOTDELSTICKY );
 2511  		if( !has_level() ) error( S_MAYNOTDEL );
 2512  	}
 2513  
 2514  	if( BOARD_DIR == 'vg' && !$row['resto'] && !$admin_ok && !$archived_deletion ) error( S_MAYNOTDEL );
 2515  	
 2516  	if( !$row['resto'] && !$automatic && !$admin_ok ) {
 2517  		foreach( $row['children'] as $child => $unused ) {
 2518  			if( $log[$child]['capcode'] != 'none' ) error( S_MAYNOTDEL );
 2519  		}
 2520  	}
 2521  
 2522  	if( !$automatic && !$admin_ok && !$can_flood_delete ) {
 2523      if ($user_is_known) {
 2524        $_renzoku_del = RENZOKU_DEL;
 2525      }
 2526      else {
 2527        $_renzoku_del = 600; // FIXME: 10 minutes
 2528      }
 2529  		if( ( time() - (int)$row['time'] ) < $_renzoku_del ) {
 2530  			error(S_RENZOKU_DEL);
 2531  		}
 2532  	}
 2533    
 2534    // User is authed staff
 2535    if ($admin_ok && !IS_REBUILDD) {
 2536      $auser   = $_COOKIE['4chan_auser'];
 2537      
 2538      // Use POSTed IP instead of the local one if the deletion was triggered via RPC
 2539      if (isset($_POST['remote_addr']) && is_local()) {
 2540        $remote_addr = $_POST['remote_addr'];
 2541      }
 2542      else {
 2543        $remote_addr = $_SERVER['REMOTE_ADDR'];
 2544      }
 2545      
 2546      // Authed user is deleting a post that isn't his
 2547      if (!$pass_ok && !$host_ok) {
 2548        $adfsize = ( $row['fsize'] > 0 ) ? 1 : 0;
 2549        $adname  = str_replace( '</span> <span class="postertrip">!', '#', $row['name'] );
 2550        if( $imgonly ) {
 2551          $imgonly = 1;
 2552        } else {
 2553          $imgonly = 0;
 2554        }
 2555        
 2556        if (isset($_POST['template_id'])) {
 2557          $template_id = (int)$_POST['template_id'];
 2558        }
 2559        else {
 2560          $template_id = 0;
 2561        }
 2562        
 2563        if ($post_exists && (!$automatic || $automatic === 2)) {
 2564          validate_admin_cookies();
 2565          
 2566          if (!$tool || !in_array($tool, array('search', 'ban', 'ban-req', 'autopurge', 'threadban'))) {
 2567            $tool = '';
 2568          }
 2569          
 2570          mysql_global_do( "INSERT INTO " . SQLLOGDEL . " (imgonly,postno,resto,board,name,sub,com,img,filename,admin,admin_ip,template_id,tool) values('%s',%d, %d,'%s','%s','%s','%s','%s','%s','%s', '%s', %d, '%s')", $imgonly, $resno, $row['resto'], SQLLOG, $adname, $row["sub"], $row["com"], $adfsize, $row["filename"].$row["ext"], $auser, $remote_addr, $template_id, $tool );
 2571        }
 2572        
 2573        // Clear the report queue if only the file is deleted
 2574        if ($imgonly) {
 2575          $query = "DELETE FROM reports WHERE board = '" . SQLLOG . "' AND no = " . (int)$resno;
 2576          
 2577          $res = mysql_global_call($query);
 2578          
 2579          $query = "DELETE FROM reports_for_posts WHERE board = '" . SQLLOG . "' AND postid = " . (int)$resno;
 2580          
 2581          $res = mysql_global_call($query);
 2582        }
 2583      }
 2584      // Staff member is deleting a post that is his, or other type of deletions
 2585      else if (!$automatic || $automatic === 2) {
 2586        if ($auser) {
 2587          log_staff_event('staff_self_del', $auser, $remote_addr, $pwd, BOARD_DIR, $row);
 2588        }
 2589        else {
 2590          log_staff_event('staff_auto_del', 'Auto-ban', $remote_addr, $pwd, BOARD_DIR, $row);
 2591        }
 2592      }
 2593  	}
 2594    
 2595  	$delete_children = $row['resto'] == 0 && $children && !$imgonly;
 2596  
 2597  	$restoq = $delete_children ? "OR (archived = 0 and resto=$resno) OR (archived = 1 and resto=$resno)" : '';
 2598  	
 2599    if (UPLOAD_BOARD) {
 2600      $up_col = ',filename';
 2601    }
 2602    else {
 2603      $up_col = '';
 2604    }
 2605    
 2606    if (MOBILE_IMG_RESIZE) {
 2607      $up_col .= ',m_img';
 2608    }
 2609    
 2610  	$result = mysql_board_call( "select no,resto,tim,ext$up_col from `" . SQLLOG . "` where no=$resno $restoq" );
 2611  	
 2612  	// Array of threads to update after one or more replies were deleted.
 2613  	$updated_threads = array();
 2614  	// Array of post number for report and xff clearing
 2615  	$deleted_threads = array();
 2616  	$deleted_replies = array();
 2617  		
 2618  	$purge_files = array();
 2619  	
 2620  	$img_webroot = 'http://i.4cdn.org/' . BOARD_DIR . '/';
 2621  	
 2622  	while( $delrow = mysql_fetch_array( $result ) ) {
 2623  		// delete
 2624  		if( $delrow['ext'] ) {
 2625  			if (UPLOAD_BOARD) {
 2626  				$delfile  = IMG_DIR . $delrow['filename'] . $delrow['ext']; //path to delete
 2627  				@unlink($delfile); // delete image
 2628  				if( CLOUDFLARE_PURGE_ON_DEL ) {
 2629  				  cloudflare_purge_url($img_webroot . rawurlencode($delrow['filename']) . $delrow['ext'], true);
 2630  				}
 2631  			}
 2632  			else {
 2633  				$delfile  = IMG_DIR . $delrow['tim'] . $delrow['ext']; //path to delete
 2634  				$delthumb = THUMB_DIR . $delrow['tim'] . 's.jpg';
 2635  				@unlink( $delfile ); // delete image
 2636  				@unlink( $delthumb ); // delete thumb
 2637  	      
 2638          if (ENABLE_OEKAKI_REPLAYS && file_exists(IMG_DIR . $delrow['tim'] . '.tgkr')) {
 2639            unlink(IMG_DIR . $delrow['tim'] . '.tgkr');
 2640            
 2641            if (CLOUDFLARE_PURGE_ON_DEL) {
 2642              $purge_files[] = $img_webroot . $delrow['tim'] . '.tgkr';
 2643            }
 2644          }
 2645          
 2646          if (MOBILE_IMG_RESIZE && $delrow['m_img']) {
 2647            @unlink(IMG_DIR . $delrow['tim'] . 'm.jpg'); // delete mobile
 2648          }
 2649  				
 2650  				if (CLOUDFLARE_PURGE_ON_DEL) {
 2651  				  $purge_files[] = $img_webroot . $delrow['tim'] . $delrow['ext'];
 2652  				  $purge_files[] = $img_webroot . $delrow['tim'] . 's.jpg';
 2653  				  
 2654  				  if (MOBILE_IMG_RESIZE && $delrow['m_img']) {
 2655  				    $purge_files[] = $img_webroot . $delrow['tim'] . 'm.jpg';
 2656  				  }
 2657  				  
 2658  				}
 2659  			}
 2660  		}
 2661  		if( $imgonly ) {
 2662  			mysql_board_call( "UPDATE `" . SQLLOG . "` SET filedeleted=1,root=root,last_modified=%d WHERE no=%d", $_SERVER['REQUEST_TIME'], $delrow['no'] );
 2663  			$log[$delrow['no']]['filedeleted'] = TRUE;
 2664  			
 2665  			if ($delrow['resto']) {
 2666  				mysql_board_call( "UPDATE `" . SQLLOG . "` SET root=root,last_modified=%d WHERE no=%d", $_SERVER['REQUEST_TIME'], $delrow['resto'] );
 2667  				if (isset($log[$delrow['resto']]))
 2668  					$log[$delrow['resto']]['last_modified'] = (int)$_SERVER['REQUEST_TIME'];
 2669  			}
 2670  			
 2671  			//cloudflare_purge_by_basename(BOARD_DIR, $delrow['tim'] . $delrow['ext']);
 2672  		}
 2673  		else {
 2674  			// Thread
 2675  			if (!$delrow['resto']) {
 2676  				$thread_key = @array_search( $delrow['no'], $log['THREADS'] );
 2677  				if( $thread_key !== false ) {
 2678  					unset( $log['THREADS'][$thread_key] );
 2679  				}
 2680  				
 2681  				if( USE_GZIP == 1 ) {
 2682  					@unlink( RES_DIR . $delrow['no'] . PHP_EXT . '.gz' );
 2683  					@unlink( RES_DIR . $delrow['no'] . '.json.gz' );
 2684  				}
 2685  				else {
 2686    				@unlink( RES_DIR . $delrow['no'] . PHP_EXT );
 2687    				@unlink( RES_DIR . $delrow['no'] . '.json' );
 2688  				}
 2689  				
 2690  				update_json_tail_deletion($delrow['no'], true);
 2691  				
 2692  				$deleted_threads[] = (int)$delrow['no'];
 2693  			}
 2694  			// Reply. Thread's last_modified field will need to be updated
 2695  			else if (!isset($updated_threads[$delrow['resto']])) {
 2696  				$updated_threads[$delrow['resto']] = true;
 2697  				
 2698  				$deleted_replies[] = (int)$delrow['no'];
 2699  			}
 2700  			
 2701  			unset( $log[$delrow['no']] );
 2702  		}
 2703  	}
 2704  	
 2705  	if (!empty($purge_files)) {
 2706  	  cloudflare_purge_url($purge_files, true);
 2707  	}
 2708  	
 2709  	// Updating last_modified field (threads)
 2710  	foreach ($updated_threads as $thread_id => $true) {
 2711  		mysql_board_call("UPDATE `".SQLLOG."` set root=root,last_modified=%d where no=%d", $_SERVER['REQUEST_TIME'], $thread_id);
 2712  		
 2713  		if (isset($log[$thread_id]))
 2714  			$log[$thread_id]['last_modified'] = (int)$_SERVER['REQUEST_TIME'];
 2715  		
 2716  		unset( $log[$thread_id]['children'][$delrow['no']] );
 2717  	}
 2718  	
 2719  	// Clearing reports and xff
 2720  	if ($deleted_replies) {
 2721  		$in_clause = 'IN(' . implode(',', $deleted_replies) . ')';
 2722  		mysql_global_do("DELETE FROM reports WHERE board='" . BOARD_DIR . "' AND no " . $in_clause);
 2723  		mysql_global_do("DELETE FROM reports_for_posts WHERE board='" . BOARD_DIR . "' AND postid " . $in_clause);
 2724  		
 2725      if (SAVE_XFF) {
 2726        mysql_global_do("UPDATE xff SET is_live = 0 WHERE board='" . BOARD_DIR . "' AND postno " . $in_clause);
 2727      }
 2728  	}
 2729  	
 2730  	if ($deleted_threads) {
 2731  		$in_clause = 'IN(' . implode(',', $deleted_threads) . ')';
 2732  		mysql_global_do("DELETE FROM reports WHERE board='" . BOARD_DIR . "' AND (no $in_clause OR resto $in_clause)");
 2733  		mysql_global_do("DELETE FROM reports_for_posts WHERE board='" . BOARD_DIR . "' AND (postid $in_clause OR threadid $in_clause)");
 2734  		
 2735      if (SAVE_XFF) {
 2736        mysql_global_do("UPDATE xff SET is_live = 0 WHERE board='" . BOARD_DIR . "' AND postno " . $in_clause);
 2737      }
 2738  	}
 2739    
 2740    // Halloween 2017
 2741    /*
 2742    if ($tool && defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'spooky2017') {
 2743      if ($tool === 'ban') {
 2744        decrease_halloween_score($resno);
 2745      }
 2746      else if ($tool === 'ban-req') {
 2747        decrease_halloween_score($resno, 0.90);
 2748      }
 2749    }
 2750    */
 2751    
 2752  	//delete from DB
 2753  	if( $delete_children ) // delete thread and children
 2754  		$result = mysql_board_call( "delete from `" . SQLLOG . "` where no=$resno or resto=$resno" );
 2755  	elseif( !$imgonly ) // just delete the post
 2756  		$result = mysql_board_call( "delete from `" . SQLLOG . "` where no=$resno" );
 2757  
 2758  	rpc_task();
 2759  	if( $imgonly && $row['resto'] == 0 ) {
 2760  		return $resno; // return thread number to stop deletion silliness
 2761  	}
 2762  
 2763  	return $row['resto']; // so the caller can know what pages need to be rebuilt
 2764  }
 2765  
 2766  function rebuild_deletions($rebuild, $lazy_rebuild = false)
 2767  {
 2768    global $log;
 2769    
 2770  	foreach( $rebuild as $key => $val ) {
 2771  		log_cache( 0, $key );
 2772      if (!isset($log[$key]['children'])) {
 2773        internal_error_log("rebuild_deletions", "missing children for OP /" . BOARD_DIR . "/$key");
 2774        continue;
 2775      }
 2776  		updatelog( $key, 1 ); // leaving the second parameter as 0 rebuilds the index each time!
 2777  		update_json_tail_deletion($key);
 2778  	}
 2779    
 2780  	if( STATIC_REBUILD ) return;
 2781  
 2782  	updatelog(0, 0, $lazy_rebuild); // update the index page last
 2783  }
 2784  
 2785  /**
 2786   * Removes old archived posts
 2787   */
 2788  function trim_archive() {
 2789    if (STATIC_REBUILD && !IS_REBUILDD) {
 2790      return;
 2791    }
 2792    
 2793    if (!ARCHIVE_MAX_AGE) {
 2794      return;
 2795    }
 2796    
 2797    $interval = (int)ARCHIVE_MAX_AGE;
 2798    
 2799    $query = <<<SQL
 2800  SELECT no FROM `%s`
 2801  WHERE archived = 1
 2802  AND resto = 0
 2803  AND root < DATE_SUB(NOW(), INTERVAL $interval HOUR)
 2804  SQL;
 2805    
 2806    $res = mysql_board_call($query, BOARD_DIR);
 2807    
 2808    if (!$res || !mysql_num_rows($res)) {
 2809      return;
 2810    }
 2811    
 2812    while ($row = mysql_fetch_row($res)) {
 2813      delete_post((int)$row[0], '', 0, 1, 1, 0, false, true);
 2814    }
 2815  }
 2816  
 2817  // purge old posts
 2818  // should be called whenever a new post is added.
 2819  function trim_db()
 2820  {
 2821  	global $mode;
 2822  	if( JANITOR_BOARD == 1 ) return;
 2823  	if( STATIC_REBUILD && !IS_REBUILDD ) return;
 2824    
 2825    if (!IS_REBUILDD) {
 2826      log_cache();
 2827    }
 2828    
 2829  	$maxposts = LOG_MAX;
 2830  	// max threads = max pages times threads-per-page
 2831  	$maxthreads = ( PAGE_MAX > 0 ) ? ( PAGE_MAX * DEF_PAGES ) : 0;
 2832  
 2833  	$threads = array();
 2834  	
 2835  	$rebuild_archive_list = false;
 2836  	
 2837    $rebuild_archive_json = false;
 2838    
 2839    if (ENABLE_ARCHIVE) {
 2840      if (IS_REBUILDD) {
 2841        clearstatcache(true, INDEX_DIR . 'archive' . PHP_EXT . '.gz');
 2842      }
 2843      
 2844      if (filemtime(INDEX_DIR . 'archive' . PHP_EXT . '.gz') < time() - (int)ARCHIVE_REBUILD_DELAY) {
 2845        $rebuild_archive_list = true;
 2846      }
 2847    }
 2848  	
 2849  	// New max-page method
 2850  	if( $maxthreads ) {
 2851  		$exp_order = 'no';
 2852  		if( EXPIRE_NEGLECTED == 1 ) $exp_order = 'root';
 2853  		//logtime( 'trim_db before select threads' );
 2854  		$result = mysql_board_call( "SELECT no FROM `" . SQLLOG . "` WHERE archived=0 AND sticky=0 AND undead=0 AND resto=0 ORDER BY $exp_order ASC" );
 2855  		//logtime( 'trim_db after select threads' );
 2856  		$threadcount = mysql_num_rows( $result );
 2857  		
 2858  		if (!$threadcount && $rebuild_archive_list) {
 2859  		  $rebuild_archive_list = false;
 2860  		}
 2861  		
 2862  		while( $row = mysql_fetch_array( $result ) and $threadcount > $maxthreads ) {
 2863  			if (ENABLE_ARCHIVE) {
 2864          $rebuild_archive_json = true;
 2865  			  archive_thread($row['no']);
 2866  			}
 2867  			else {
 2868  			  delete_post( $row['no'], 'trim', 0, 1 ); // imgonly=0, automatic=1, children=1
 2869  		  }
 2870  			$threads[$row['no']] = 1;
 2871  			$threadcount--;
 2872  		}
 2873      
 2874  		mysql_free_result( $result );
 2875  		
 2876      if (ENABLE_ARCHIVE) {
 2877        if ($rebuild_archive_list) {
 2878          rebuild_archive_list();
 2879        }
 2880        
 2881        if ($rebuild_archive_json && ENABLE_JSON_THREADS) {
 2882          generate_board_archived_json();
 2883        }
 2884      }
 2885  		
 2886  		// Original max-posts method (note: cleans orphaned posts later than parent posts)
 2887  	} else {
 2888  		// make list of stickies
 2889  		$stickies = array(); // keys are stickied thread numbers
 2890  		$undead   = array();
 2891  		// COMBINE FOR MAXIMUM EFFICIENCY!
 2892  		$result = mysql_board_call( "SELECT no from `" . SQLLOG . "` where (sticky=1 OR undead=1) and resto=0" );
 2893  		while( $row = mysql_fetch_array( $result ) ) {
 2894  			if( $row['sticky'] ) $stickies[$row['no']] = 1;
 2895  			if( $row['undead'] ) $undead[$row['no']] = 1;
 2896  		}
 2897  
 2898  		// FIXME these if ... continue checks need to be SQL conditions!
 2899  		$result    = mysql_board_call( "SELECT no,resto,sticky FROM `" . SQLLOG . "` ORDER BY no ASC" );
 2900  		$postcount = mysql_num_rows( $result );
 2901  		while( $row = mysql_fetch_array( $result ) and $postcount >= $maxposts ) {
 2902  			// don't delete if this is a sticky thread or is undeletable
 2903  			if( $row['sticky'] == 1 || $row['undead'] == 1 ) continue;
 2904  			// don't delete if this is a REPLY to a sticky or is in an undeletable thread
 2905  			if( $row['resto'] != 0 && ( $stickies[$row['resto']] == 1 || $undead[$row['resto']] == 1 ) ) continue;
 2906  			delete_post( $row['no'], 'trim', 0, 1, 0 ); // imgonly=0, automatic=1, children=0
 2907  			$threads[$row['no']] = 1;
 2908  			$postcount--;
 2909  		}
 2910  		mysql_free_result( $result );
 2911  	}
 2912  }
 2913  
 2914  // FIXME archives
 2915  // debug function, deletes all archived threads
 2916  function purge_archive() {
 2917    $query = "SELECT no FROM `test` WHERE archived = 1 AND resto = 0";
 2918    
 2919    $res = mysql_board_call($query);
 2920    
 2921    if (!$res) {
 2922      return;
 2923    }
 2924    
 2925    while ($thread = mysql_fetch_assoc($res)) {
 2926      echo "Deleting {$thread['no']}<br>";
 2927      delete_post((int)$thread['no'], '', 0, 0, 1, true, false, true);
 2928    }
 2929  }
 2930  
 2931  function rebuild_archived_thread($thread_id) {
 2932    global $log;
 2933    
 2934    log_cache(0, $thread_id, 1);
 2935    
 2936    if (!isset($log[$thread_id])) {
 2937      return false;
 2938    }
 2939    
 2940    // Build the JSON
 2941    if (ENABLE_JSON) {
 2942      $tailSize = get_json_tail_size($thread_id);
 2943      
 2944      if ($tailSize) {
 2945        generate_thread_json($thread_id, false, false, false, $tailSize);
 2946      }
 2947      else {
 2948        update_json_tail_deletion($thread_id);
 2949      }
 2950      
 2951      generate_thread_json($thread_id);
 2952    }
 2953    
 2954    // Build the HTML
 2955    $dat = '';
 2956    
 2957    head($dat, $thread_id);
 2958    form($dat, $thread_id);
 2959    
 2960  	$dat .= '<hr>
 2961  <form name="delform" id="delform" action="' . SELF_PATH_ABS . '" method="post">
 2962  <div class="board">
 2963  ';
 2964    
 2965    $reply_count = $log[$thread_id]['replycount'];
 2966    
 2967    // Open thread tag and render OP
 2968    $sorted_replies = $log[$thread_id]['children'];
 2969    ksort($sorted_replies);
 2970    
 2971    $dat .= '<div class="thread" id="t' . $thread_id . '">'
 2972      . renderPostHtml($thread_id, $thread_id, $sorted_replies, $reply_count, null, true);
 2973    
 2974    // Render replies
 2975    $repCount = 0;
 2976    
 2977    while (list($resrow) = each($sorted_replies)) {
 2978      if (!$log[$resrow]['no']) {
 2979        break;
 2980      }
 2981      
 2982      $dat .= renderPostHtml($resrow, $thread_id, null, null, null, true);
 2983      
 2984      $repCount++;
 2985    }
 2986    
 2987    // Close thread tag
 2988    $dat .= '
 2989  </div>
 2990  <hr>
 2991  ';
 2992  
 2993    $dat .= '<div class="navLinks navLinksBot desktop">[<a href="/'
 2994      . BOARD_DIR . '/" accesskey="a">' . S_RETURN . '</a>] [<a href="/'
 2995      . BOARD_DIR . '/catalog">' . S_CATALOG . '</a>] [<a href="#top">'
 2996      . S_TOP . '</a>] </div><hr class="desktop">';
 2997  
 2998    // Close board tag
 2999    $lang = S_FORM_REPLY;
 3000  
 3001    $dat .= '
 3002  <div class="mobile center"><a class="mobilePostFormToggle button" href="#">'
 3003    . $lang . '</a></div>
 3004  </div>';
 3005  
 3006    $dat .= '<div class="navLinks mobile"><span class="mobileib button"><a href="/'
 3007      . BOARD_DIR . '/" accesskey="a">'
 3008      . S_RETURN . '</a></span> <span class="mobileib button"><a href="/'
 3009      . BOARD_DIR . '/catalog">'
 3010      . S_CATALOG . '</a></span> <span class="mobileib button"><a href="#top">'
 3011      . S_TOP . '</a></span> <span class="mobileib button"><a href="#bottom_r" id="refresh_bottom">'
 3012      . S_REFRESH . '</a></span></div><hr class="mobile">';
 3013    
 3014    /**
 3015     * ADS
 3016     */
 3017    
 3018    if (defined('AD_ADGLARE_BOTTOM') && AD_ADGLARE_BOTTOM) {
 3019      $dat .= '<div class="adg-rects desktop"><div class="adg adp-90" id=zone' . AD_ADGLARE_BOTTOM . '></div><hr></div>';
 3020    }
 3021    
 3022    if (defined('AD_ADGLARE_BOTTOM_MOBILE') && AD_ADGLARE_BOTTOM_MOBILE) {
 3023      $dat .= '<div class="adg-rects mobile"><div class="adg-m adp-250" id=zone' . AD_ADGLARE_BOTTOM_MOBILE . '></div><hr></div>';
 3024    }
 3025    
 3026    if (defined('AD_RC_BOTTOM') && AD_RC_BOTTOM) {
 3027      $dat .= '<div class="adg-rects desktop"><div class="adg adp-228" data-rc="' . AD_RC_BOTTOM . '" id="rcjsload_bottom"></div><hr></div>';
 3028    }
 3029    
 3030    if (defined('AD_BSA_BOTTOM') && AD_BSA_BOTTOM) {
 3031      $dat .= AD_BSA_BOTTOM;
 3032    }
 3033    
 3034    if (defined('AD_RC_BOTTOM_MOBILE') && AD_RC_BOTTOM_MOBILE) {
 3035      $dat .= '<div class="adg-rects mobile"><div class="adg-m adp-50" data-rc="' . AD_RC_BOTTOM_MOBILE . '" id="rcjsload_bottom_m"></div><hr></div>';
 3036    }
 3037    
 3038    if (defined('AD_ADNIUM_BOTTOM_MOBILE') && AD_ADNIUM_BOTTOM_MOBILE) {
 3039      $dat .= '<div class="adg-rects mobile"><div class="adg-m adp-250" id="adn-' . AD_ADNIUM_BOTTOM_MOBILE . '" data-adn></div><hr></div>';
 3040    }
 3041    
 3042    if (defined('AD_ABC_BOTTOM_MOBILE') && AD_ABC_BOTTOM_MOBILE) {
 3043      $dat .= '<div class="adg-rects mobile"><div class="adg-m adp-250" data-abc="' . AD_ABC_BOTTOM_MOBILE . '"></div><hr></div>';
 3044    }
 3045    /*
 3046    if (defined('AD_BIDGEAR_BOTTOM') && AD_BIDGEAR_BOTTOM)  {
 3047      $dat .= '<div class="adc-resp-bg" data-ad-bg="' . AD_BIDGEAR_BOTTOM . '"></div>';
 3048    }
 3049    else if (defined('AD_TERRA_BOTTOM_DESKTOP') && AD_TERRA_BOTTOM_DESKTOP) {
 3050      $_ad_info = explode(',', AD_TERRA_BOTTOM_DESKTOP);
 3051      $dat .= '<div class="adt-800" data-d="' . $_ad_info[0] . '" id="container-' . $_ad_info[1] . '" style="max-width:800px;margin:auto;"></div>';
 3052    }
 3053    */
 3054    if (defined('ADS_DANBO') && ADS_DANBO)  {
 3055      $dat .= '<div id="danbo-s-b" class="danbo-slot"></div><div class="adl">[<a target="_blank" href="https://www.4chan.org/advertise">Advertise on 4chan</a>]</div><hr>';
 3056    }
 3057    if (defined('AD_CUSTOM_BOTTOM') && AD_CUSTOM_BOTTOM) {
 3058      $dat .= '<div>' . AD_CUSTOM_BOTTOM . '<hr></div>';
 3059    }
 3060    
 3061    $resredir = '<input type="hidden" name="res" value="' . $thread_id . '">';
 3062    
 3063    // deletion mode is "arcdel" instead of "usrdel"
 3064    $dat .= '<div class="bottomCtrl desktop"><span class="deleteform"><input type="hidden" name="mode" value="arcdel">'
 3065      . S_REPDEL . $resredir . ' [<input type="checkbox" name="onlyimgdel" value="on">'
 3066      . S_DELPICONLY . ']<input type="hidden" id="delPassword" name="pwd"> <input type="submit" value="'
 3067      . S_DELETE . '"><input id="bottomReportBtn" type="button" value="Report"></span>';
 3068  
 3069    if (!defined('CSS_FORCE')) {
 3070      $dat .= '<span class="stylechanger">Style: 
 3071        <select id="styleSelector">
 3072          <option value="Yotsuba New">Yotsuba</option>
 3073          <option value="Yotsuba B New">Yotsuba B</option>
 3074          <option value="Futaba New">Futaba</option>
 3075          <option value="Burichan New">Burichan</option>
 3076          <option value="Tomorrow">Tomorrow</option>
 3077          <option value="Photon">Photon</option>';
 3078      
 3079      if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) {
 3080        $dat .= '<option value="_special">Special</option>';
 3081      }
 3082      
 3083      $dat .= '</select>
 3084      </span>';
 3085    }
 3086    
 3087    $dat .= '</div></form>';
 3088    
 3089    foot($dat);
 3090    
 3091    // Write the page
 3092    print_page(RES_DIR . $thread_id . PHP_EXT, $dat);
 3093  }
 3094  
 3095  function calculate_indexes_to_rebuild( $updated_thread )
 3096  {
 3097  	global $index_rbl;
 3098  	$query     = mysql_board_call( "SELECT COUNT(no) FROM `%s` WHERE archived = 0 AND root > (SELECT root FROM `%s` WHERE no=%d)", SQLLOG, SQLLOG, $updated_thread );
 3099  	$index_rbl = floor( mysql_result( $query, 0, 0 ) / DEF_PAGES );
 3100  }
 3101  
 3102  function rebuild_indexes_daemon()
 3103  {
 3104  	global $index_rbl, $index_last_thread, $index_last_post, $log;
 3105  	static $index_arr = array();
 3106  
 3107  	$index_rbl = PAGE_MAX;
 3108  
 3109  	// Get latest thread
 3110  	$query = mysql_board_call( "SELECT max(no) last_post, max(resto) last_thread FROM `%s` WHERE archived = 0", SQLLOG );
 3111  	$q = mysql_fetch_assoc( $query );
 3112  
 3113  	$latest_thread = $q['last_thread'];
 3114  	$latest_post   = $q['last_post'];
 3115  
 3116  	if( $index_last_thread != $latest_thread ) {
 3117  		// cry :(
 3118  		$index_last_thread = $latest_thread;
 3119  		$index_last_post   = $latest_post;
 3120      
 3121  		updatelog();
 3122      
 3123      if (ENABLE_JSON_THREADS && ENABLE_ARCHIVE) {
 3124        generate_board_archived_json();
 3125      }
 3126      
 3127  		return;
 3128  	}
 3129  
 3130  	if( $index_last_post != $latest_post ) {
 3131  		$index_last_thread = $latest_thread;
 3132  		$index_last_post   = $latest_post;
 3133  
 3134  		$post_arr = $log['THREADS'];
 3135  
 3136  		// Now we know we're not going to have identical arrays.
 3137  		$count            = count( $post_arr );
 3138  		$last_seen_thread = 0;
 3139  
 3140  		for( $i = 0; $i < $count; $i++ ) {
 3141  			if( $post_arr[$i] != $index_arr[$i] ) $last_seen_thread = $i;
 3142  		}
 3143  
 3144  		$index_rbl = floor( $last_seen_thread / DEF_PAGES );
 3145  		$index_arr = $post_arr;
 3146  
 3147  		updatelog();
 3148  
 3149  		return;
 3150  	}
 3151  
 3152  	// YAY NOTHING TO UPDATE!
 3153  	return;
 3154  }
 3155  
 3156  function style_group()
 3157  {
 3158  	return ( DEFAULT_BURICHAN == 1 ) ? "ws_style" : "nws_style";
 3159  }
 3160  
 3161  function rebuildd_stats()
 3162  {
 3163  	if (!IS_REBUILDD) return "";
 3164  	
 3165  	global $update_avg_secs;
 3166  	global $rpc_chs;
 3167  	
 3168  	$avgtime = $update_avg_secs;
 3169  	
 3170  	$memuse = (int)(memory_get_usage(true) / 1024);
 3171  	$peakmemuse = (int)(memory_get_peak_usage(true) / 1024);
 3172  	$rpccount = count($rpc_chs);
 3173  	
 3174  	return "<!-- t $avgtime m $memuse $peakmemuse rc $rpccount -->";
 3175  }
 3176  
 3177  // Changes relative board urls to absolute //board.4chan.org urls
 3178  // mostly for /j/ and error pages on sys.4chan
 3179  function fix_board_nav($nav, $fix_protocol = false) {
 3180    if ($fix_protocol) {
 3181      $protocol = (stripos($_SERVER["HTTP_REFERER"], "https") === 0) ? 'https:' : 'http:';
 3182    }
 3183    else {
 3184      $protocol = '';
 3185    }
 3186    
 3187    return preg_replace('/href="\/([a-z0-9]+)\/"/', "href=\"$protocol//boards." . L::d(BOARD_DIR) . "/$1/\"", $nav);
 3188  }
 3189  
 3190  // Same but for /archive lmao
 3191  function fix_board_nav_archive($nav) {
 3192    $nav = preg_replace('/href="\/([a-z0-9]+)\/"/', 'href="/$1/archive"', $nav);
 3193    $nav = preg_replace('/href="\/f\/archive"/', 'href="/f/"', $nav);
 3194    $nav = preg_replace('/href="\/b\/archive"/', 'href="/b/"', $nav);
 3195    
 3196    return $nav;
 3197  }
 3198  
 3199  function head( &$dat, $res, $error = 0, $page = 0, $npages = 0, $is_arclist = false )
 3200  {
 3201  	//( $dat, 0, 0, 0, 0, true )
 3202  	global $log, $thread_unique_ips;
 3203    
 3204  	$titlepart = $rta = $favicon = $css = $rss = $subtitle = $extra = '';
 3205  	
 3206  	$includenav = file_get_contents_cached(NAV_TXT);
 3207  	
 3208    if( JANITOR_BOARD == 1 ) {
 3209      $dat .= broomcloset_head( $dat );
 3210      $includenav = fix_board_nav($includenav);
 3211    }
 3212    else if ($error) {
 3213      $includenav = fix_board_nav($includenav, true);
 3214    }
 3215    else if ($is_arclist) {
 3216      $includenav = fix_board_nav_archive($includenav);
 3217    }
 3218  	
 3219  	if( TITLE_IMAGE_TYPE == 1 ) {
 3220  		$titleimg = rand_from_flatfile( YOTSUBA_DIR, 'title_banners.txt' );
 3221  		//$titleimg = STATIC_SERVER . 'image/title/' . $titleimg;
 3222  
 3223  		$titlepart .= '<div id="bannerCnt" class="title desktop" data-src="' . $titleimg . '"></div>';
 3224  	} elseif( TITLE_IMAGE_TYPE == 2 ) {
 3225  		$titlepart .= '<img class="title" src="' . TITLEIMG . '" onclick="this.src = this.src;">';
 3226  	}
 3227  	
 3228  	if( defined( 'SUBTITLE' ) ) {
 3229  		$subtitle = '<div class="boardSubtitle">' . SUBTITLE . '</div>';
 3230  	}
 3231  	
 3232  	// CSS Workings
 3233  	$cssVersion = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION;
 3234  	$defaultcss = ( DEFAULT_BURICHAN == 1 ) ? 'yotsubluenew' : 'yotsubanew';
 3235  	$mobilecss  = ( ( DEFAULT_BURICHAN == 1 ) ? 'yotsublue' : 'yotsuba' ) . 'mobile.' . $cssVersion . '.css';
 3236  	
 3237  	$styles = array(
 3238  		'Yotsuba New'   => "yotsubanew.$cssVersion.css",
 3239  		//'Yotsuba' => "yotsuba.$cssVersion.css",
 3240  		'Yotsuba B New' => "yotsubluenew.$cssVersion.css",
 3241  		'Futaba New'    => "futabanew.$cssVersion.css",
 3242  		'Burichan New'  => "burichannew.$cssVersion.css",
 3243  		'Photon'        => "photon.$cssVersion.css",
 3244  		'Tomorrow'      => "tomorrow.$cssVersion.css"
 3245  	);
 3246  
 3247  	// /j/ versioning fix
 3248  	if( BOARD_DIR == 'j' ) {
 3249  		$css = '<link rel="stylesheet" type="text/css" href="' . STATIC_SERVER . 'css/janichan.' . $cssVersion . '.css" title="Yotsuba New">';
 3250      $extra = <<<JJS
 3251  <script type="text/javascript">
 3252    document.addEventListener('mousedown', function(e) {
 3253      var t = e.target;
 3254      if (t === document) {
 3255        return;
 3256      }
 3257      if (/^>>>\//.test(t.textContent) && /sys\.4chan/.test(t.href)) {
 3258        t.href = t.href.replace('sys.4chan', 'boards.4chan');
 3259      }
 3260    }, false);
 3261  </script>
 3262  JJS;
 3263  	} else {
 3264  		if( defined( 'CSS_FORCE' ) ) {
 3265  			foreach( $styles as $style => $stylecss ) {
 3266  				$rel = ( $style == 'Yotsuba New' ) ? 'stylesheet' : 'alternate stylesheet';
 3267  				$css .= '<link rel="' . $rel . '" type="text/css" href="' . CSS_FORCE . '" title="' . $style . '">';
 3268  			}
 3269  		}
 3270  		else {
 3271  			$dcssl = $defaultcss . '.' . $cssVersion . '.css';
 3272  			$css .= '<link rel="stylesheet" title="switch" href="' . STATIC_SERVER . 'css/' . $dcssl . '">';
 3273  			foreach( $styles as $style => $stylecss ) {
 3274  				$css .= '<link rel="alternate stylesheet" style="text/css" href="' . STATIC_SERVER . 'css/' . $stylecss . '" title="' . $style . '">';
 3275  			}
 3276        
 3277        if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) {
 3278          $css .= '<link rel="alternate stylesheet" style="text/css" href="' . STATIC_SERVER . 'css/'
 3279            . CSS_EVENT_NAME . '.' . $cssVersion . '.css" title="_special">';
 3280        }
 3281  		}
 3282      
 3283      // Christmas 2021
 3284      if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'tomorrow') {
 3285        $extra = <<<JJS
 3286  <script src='//s.4cdn.org/js/snow.js'></script>
 3287  <script>
 3288    function fc_tomorrow_init() {
 3289      if (window.matchMedia && window.matchMedia('(min-width: 481px)').matches) {
 3290        fc_spawn_snow(Math.floor(Math.random() * 50) + 50);
 3291      }
 3292    }
 3293    function fc_tomorrow_cleanup() {
 3294      fc_remove_snow();
 3295    }
 3296  </script>
 3297  <style>
 3298  .boardBanner, #delform, .navLinksBot.desktop {
 3299    border-image-slice: 50 0 50 0;
 3300    border-image-width: 40px 0px 0px 0px;
 3301    border-image-outset: 0px 0px 0px 0px;
 3302    border-image-repeat: repeat repeat;
 3303    border-image-source: url('https://s.4cdn.org/image/temp/garland.png');
 3304    border-style: solid;
 3305    padding-top: 50px;
 3306  }
 3307  </style>
 3308  JJS;
 3309      }
 3310      
 3311      // Halloween spooky.css
 3312      if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'spooky') {
 3313        $extra = <<<JJS
 3314  <script>
 3315    function fc_skelrot(e) {
 3316      var el, idx, thres, max;
 3317      if (e && e.detail && e.detail.count) {
 3318        thres = 0.33;
 3319      }
 3320      else {
 3321        thres = 0.0;
 3322      }
 3323      max = 23;
 3324      if (Math.random() < thres) {
 3325        return;
 3326      }
 3327      if (el = document.getElementById('skellington')) {
 3328        el.parentNode.removeChild(el);
 3329      }
 3330      idx = 1 + Math.floor(Math.random() * max);
 3331      el = document.createElement('img');
 3332      el.id = 'skellington';
 3333      el.className = 'desktop' + (Math.random() < 0.25 ? ' topskel' : '');
 3334      el.alt = '';
 3335      if (Math.random() < 0.01) {
 3336        el.src = '//s.4cdn.org/image/temp/dinosaur.gif';
 3337      }
 3338      else {
 3339        el.src = '//s.4cdn.org/image/skeletons/' + idx + '.gif';
 3340      }
 3341      document.body.insertBefore(el, document.body.firstElementChild);
 3342    }
 3343    function fc_spooky_init() {
 3344      if (window.matchMedia && window.matchMedia('(min-width: 481px)').matches) {
 3345        document.addEventListener('4chanThreadUpdated', fc_skelrot, false);
 3346        window.dark_captcha = true;
 3347        fc_skelrot();
 3348      }
 3349    }
 3350    function fc_spooky_cleanup() {
 3351      var el = document.getElementById('skellington');
 3352      window.dark_captcha = false;
 3353      document.removeEventListener('4chanThreadUpdated', fc_skelrot, false);
 3354      el && el.parentNode.removeChild(el);
 3355    }
 3356  </script>
 3357  JJS;
 3358      }
 3359  	}
 3360  
 3361  	$css .= '<link rel="stylesheet" href="' . STATIC_SERVER . 'css/' . $mobilecss . '">';
 3362  	
 3363    // April 2024
 3364    $css .= '<link rel="stylesheet" href="' . STATIC_SERVER . 'css/xa24extra.css">';
 3365    
 3366  	if (SHOW_COUNTRY_FLAGS) {
 3367  		$css .= '<link rel="stylesheet" href="' . STATIC_SERVER . 'css/flags.' . CSS_VERSION_FLAGS . '.css">';
 3368  	}
 3369    
 3370    if (ENABLE_BOARD_FLAGS) {
 3371      $_flags_type = (defined('BOARD_FLAGS_TYPE') && BOARD_FLAGS_TYPE) ? BOARD_FLAGS_TYPE : BOARD_DIR;
 3372      $css .= '<link rel="stylesheet" href="' . STATIC_SERVER . 'image/flags/' . $_flags_type . '/flags.' . CSS_VERSION_BOARD_FLAGS . '.css">';
 3373    }
 3374    
 3375  	if( CODE_TAGS ) {
 3376  		$css .= '<link rel="stylesheet" href="' . STATIC_SERVER . 'js/prettify/prettify.' . CSS_VERSION . '.css">';
 3377  	}
 3378  
 3379  	// Various optional tags
 3380  	if( USE_RSS == 1 ) {
 3381  		$rss = '<link rel="alternate" title="RSS feed" href="/' . BOARD_DIR . '/index.rss" type="application/rss+xml">';
 3382  	}
 3383  
 3384  	if( RTA == 1 ) {
 3385  		$rta = '<meta name="rating" content="adult">';
 3386  	}
 3387  
 3388  	if( defined( 'FAVICON' ) ) {
 3389  		$favicon = '<link rel="shortcut icon" href="' . FAVICON . '">';
 3390  	}
 3391  	
 3392  	$thread_unique_ips = 0;
 3393  	$jsUniqueIps = '';
 3394  	
 3395  	if (SHOW_THREAD_UNIQUES) {
 3396      if ($res) {
 3397        $thread_unique_ips = get_unique_ip_count($res);
 3398      }
 3399      
 3400      if ($thread_unique_ips) {
 3401        $jsUniqueIps = 'var unique_ips = ' . $thread_unique_ips . ';';
 3402      }
 3403  	}
 3404    
 3405  	// js tags
 3406  	$jsVersion   = TEST_BOARD ? JS_VERSION_TEST : JS_VERSION;
 3407  	$comLen      = MAX_COM_CHARS;
 3408  	$styleGroup  = style_group();
 3409  	$maxFilesize = MAX_KB * 1024;
 3410  	$maxLines    = MAX_LINES;
 3411  	$jsCooldowns = json_encode(array(
 3412  		'thread' => RENZOKU3,
 3413  		'reply' => RENZOKU,
 3414  		'image' => RENZOKU2
 3415  	));
 3416    
 3417  	$tailSizeJs = '';
 3418  	
 3419    if ($res) {
 3420      $tailSize = get_json_tail_size($res);
 3421      
 3422      if ($tailSize) {
 3423        $tailSizeJs = ",tailSize = $tailSize";
 3424      }
 3425    }
 3426  	
 3427    $title = TITLE;
 3428    
 3429  	$scriptjs = <<<JS
 3430  <script type="text/javascript">
 3431  var style_group = "$styleGroup",
 3432  cssVersion = $cssVersion,
 3433  jsVersion = $jsVersion,
 3434  comlen = $comLen,
 3435  maxFilesize = $maxFilesize,
 3436  maxLines = $maxLines,
 3437  clickable_ids = 1,
 3438  cooldowns = $jsCooldowns
 3439  $tailSizeJs;
 3440  $jsUniqueIps
 3441  JS;
 3442    
 3443    if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) {
 3444      $scriptjs .= 'var css_event = "' . CSS_EVENT_NAME . '";';
 3445      
 3446      if (defined('CSS_EVENT_VERSION')) {
 3447        $_css_event_version = (int)CSS_EVENT_VERSION;
 3448      }
 3449      else {
 3450        $_css_event_version = 1;
 3451      }
 3452      
 3453      $scriptjs .= 'var css_event_v = ' . $_css_event_version . ';';
 3454    }
 3455    
 3456    if ((int)MAX_WEBM_FILESIZE < (int)MAX_KB) {
 3457      $scriptjs .= 'var maxWebmFilesize = ' . (MAX_WEBM_FILESIZE * 1024) . ';';
 3458    }
 3459    
 3460    $_is_archived = false;
 3461    
 3462    if (ENABLE_ARCHIVE) {
 3463      $scriptjs .= 'var board_archived = true;';
 3464      
 3465      if ($res && $log[$res]['archived']) {
 3466        $scriptjs .= 'var thread_archived = true;';
 3467        $_is_archived = true;
 3468      }
 3469    }
 3470    
 3471    if (DISP_ID) {
 3472      $scriptjs .= 'var user_ids = true;';
 3473    }
 3474    
 3475    if (JSMATH) {
 3476      $scriptjs .= 'var math_tags = true;';
 3477    }
 3478    
 3479    if (SJIS_TAGS) {
 3480      $scriptjs .= 'var sjis_tags = true;';
 3481    }
 3482    
 3483    if (SPOILERS) {
 3484      $scriptjs .= 'var spoilers = true;';
 3485    }
 3486    
 3487    if (CAPTCHA_TWISTER) {
 3488      $scriptjs .= 'var t_captcha = true;';
 3489    }
 3490    
 3491  	if( $res && $log[$res]['bumplimit'] ) {
 3492  		$scriptjs .= 'var bumplimit = 1;';
 3493  	}
 3494  
 3495  	if( $res && $log[$res]['imagelimit'] ) {
 3496  		$scriptjs .= 'var imagelimit = 1;';
 3497  	}
 3498  
 3499  	if( AD_PLEA ) $scriptjs .= 'var check_for_block = ' . (int)AD_PLEA . ';';
 3500  
 3501  	if( $error ) $scriptjs .= 'is_error = "true";';
 3502    
 3503    // Danbo ads
 3504    if (defined('ADS_DANBO') && ADS_DANBO) {
 3505      if (DEFAULT_BURICHAN) {
 3506        $scriptjs .= "var danbo_rating = '__SFW__';";
 3507      }
 3508      else {
 3509        $scriptjs .= "var danbo_rating = '__NSFW__';";
 3510      }
 3511      
 3512      // Set up fallbacks
 3513      $_danbo_fallbacks = [];
 3514      
 3515      if (defined('AD_BIDGEAR_TOP') && AD_BIDGEAR_TOP)  {
 3516        $_danbo_fallbacks['t_bg'] = AD_BIDGEAR_TOP;
 3517      }
 3518      else {
 3519        if (defined('AD_ABC_TOP_DESKTOP') && AD_ABC_TOP_DESKTOP)  {
 3520          $_danbo_fallbacks['t_abc_d'] = AD_ABC_TOP_DESKTOP;
 3521        }
 3522        
 3523        if (defined('AD_ABC_TOP_MOBILE') && AD_ABC_TOP_MOBILE)  {
 3524          $_danbo_fallbacks['t_abc_m'] = AD_ABC_TOP_MOBILE;
 3525        }
 3526      }
 3527      
 3528      if (defined('AD_BIDGEAR_BOTTOM') && AD_BIDGEAR_BOTTOM)  {
 3529        $_danbo_fallbacks['b_bg'] = AD_BIDGEAR_BOTTOM;
 3530      }
 3531      else if (defined('AD_ABC_BOTTOM_MOBILE') && AD_ABC_BOTTOM_MOBILE)  {
 3532        $_danbo_fallbacks['b_abc_m'] = AD_ABC_BOTTOM_MOBILE;
 3533      }
 3534      
 3535      if (!$_danbo_fallbacks) {
 3536        $_danbo_fallbacks = 'null';
 3537      }
 3538      else {
 3539        $_danbo_fallbacks = json_encode($_danbo_fallbacks);
 3540      }
 3541      
 3542      $scriptjs .= 'var danbo_fb = ' . $_danbo_fallbacks . ';';
 3543      
 3544      // Tag closed further below
 3545      $scriptjs .= '</script><script src="https://static.danbo.org/publisher/q2g345hq2g534-4chan/js/preload.4chan.js" defer>';
 3546    }
 3547    
 3548    // Close the main script tag /!\
 3549    $scriptjs .= '</script>';
 3550    
 3551    // PubFuture
 3552    if (DEFAULT_BURICHAN) {
 3553      $scriptjs .= '<script async data-cfasync="false" src="https://cdn.pubfuture-ad.com/v2/unit/pt.js"></script>';
 3554    }
 3555    
 3556  	$testjs    = ( TEST_BOARD ) ? 'test/core-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'core.min.' . JS_VERSION_CORE . '.js';
 3557  	$testextra = ( TEST_BOARD ) ? 'test/extension-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'extension.min.' . JS_VERSION_EXT . '.js';
 3558  
 3559  	$scriptjs .= '<script type="text/javascript" data-cfasync="false" src="' . STATIC_SERVER . 'js/' . $testjs . '"></script>';
 3560  	
 3561  	if( !$error ) $scriptjs .= '<script type="text/javascript" data-cfasync="false" src="' . STATIC_SERVER . 'js/' . $testextra . '"></script>';
 3562    
 3563    // April 2022
 3564    //$scriptjs .= '<script type="text/javascript" src="' . STATIC_SERVER . 'js/emotes2022.js?8"></script>';
 3565    
 3566    if (TEST_BOARD) {
 3567      $stylejs = '';
 3568    }
 3569    else {
 3570      $stylejs = '';
 3571    }
 3572    
 3573    if (ENABLE_PAINTERJS && $_GET['mode'] != 'oe_finish') {
 3574      if (TEST_BOARD) {
 3575        $scriptjs .= '<script type="text/javascript" src="' . STATIC_SERVER . 'js/test/tegaki-8psvqAqszI.' . JS_VERSION_TEST . '.js"></script>';
 3576        $css .= '<link rel="stylesheet" href="' . STATIC_SERVER . 'css/tegaki-8psvqAqszI.' . CSS_VERSION_TEST . '.css">';
 3577      }
 3578      else {
 3579        $scriptjs .= '<script type="text/javascript" src="' . STATIC_SERVER . 'js/tegaki.min.' . JS_VERSION_PAINTER . '.js"></script>';
 3580        $css .= '<link rel="stylesheet" href="' . STATIC_SERVER . 'css/tegaki.' . CSS_VERSION_PAINTER . '.css">';
 3581      }
 3582    }
 3583    /*
 3584    if (!$is_arclist && defined('CSS_MATERIAL') && CSS_MATERIAL) {
 3585      $css .= '<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet" type="text/css">';
 3586    }
 3587    */
 3588  	if( !$res ) {
 3589  		$prev = ( $page - DEF_PAGES ) / DEF_PAGES;
 3590  		$next = ( $page + DEF_PAGES ) / DEF_PAGES;
 3591  
 3592  		if( $prev == 0 ) {
 3593  			$prev_link = SELF_PATH2;
 3594  		} else if( $prev > 0 ) {
 3595  			$prev_link = $prev . PHP_EXT2;
 3596  		}
 3597  
 3598  		// maybe >= ?
 3599  		if( ( $npages - $page ) > DEF_PAGES ) {
 3600  			$next_link = $next . PHP_EXT2;
 3601  		}
 3602  	}
 3603  	
 3604  	if ($is_arclist) {
 3605  		$canonical = '<link rel="canonical" href="https://boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR.'/archive">';
 3606    }
 3607  	else if (!$res) {
 3608  	  if ($page > 0) {
 3609  		  $canonical = '<link rel="canonical" href="https://boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR.'/' . (($page / DEF_PAGES) + 1) . '">';
 3610  	  }
 3611  	  else {
 3612  		  $canonical = '<link rel="canonical" href="https://boards.' . L::d(BOARD_DIR) . '/' .BOARD_DIR.'/">';
 3613  	  }
 3614  	}
 3615  	elseif ($res) {
 3616      $href_context = $log[$res]['semantic_url'];
 3617      
 3618      if ($href_context !== '') {
 3619        $href_context = "/$href_context";
 3620      }
 3621  	  
 3622  		$canonical = '<link rel="canonical" href="https://boards.' . L::d(BOARD_DIR) . '/' .BOARD_DIR.'/thread/'.$res.$href_context . '">';
 3623  	}
 3624  	else {
 3625  	  $canonical = '';
 3626  	}
 3627    
 3628  	$clean_title = strip_tags(TITLE);
 3629  	
 3630    if ($res) {
 3631      $page_metatags = generate_page_metatags($log[$res]['sub'], $log[$res]['com']);
 3632      
 3633      if ($page_metatags) {
 3634        $page_description = $page_metatags[0] . ' - ' . META_DESCRIPTION;
 3635        $page_keywords = META_KEYWORDS . $page_metatags[1];
 3636      }
 3637      else {
 3638        $page_description = META_DESCRIPTION;
 3639        $page_keywords = META_KEYWORDS;
 3640      }
 3641      
 3642      $page_title = generate_page_title($res, $log[$res]['sub'], $log[$res]['com'])
 3643        . ' - ' . preg_replace('/^[\[\/][a-z0-9]+[\]\/] - /i', '', $clean_title);
 3644    }
 3645    else {
 3646      $page_description = META_DESCRIPTION;
 3647      $page_keywords = META_KEYWORDS;
 3648      
 3649      $page_title = $clean_title;
 3650      
 3651      if ($is_arclist) {
 3652        $page_title .= ' - Archive';
 3653      }
 3654      else if ($page > 0) {
 3655        $page_title .= ' - Page ' . (($page / DEF_PAGES) + 1);
 3656      }
 3657    }
 3658    
 3659    $page_title .= ' - 4chan';
 3660    
 3661    if (!$_is_archived) {
 3662      $_delegate_ch = '<meta http-equiv="Delegate-CH" content="Sec-CH-UA-Model https://sys.4chan.org">';
 3663    }
 3664    else {
 3665      $_delegate_ch = '';
 3666    }
 3667    
 3668  	$dat .= '<!DOCTYPE html>
 3669  <html>
 3670  <head>
 3671  <meta charset="utf-8">
 3672  <meta name="robots" content="' . META_ROBOTS . '">
 3673  <meta name="description" content="' . $page_description . '">
 3674  <meta name="keywords" content="' . $page_keywords . '">
 3675  <meta name="viewport" content="width=device-width,initial-scale=1">
 3676  ' . $rta . '
 3677  ' . $favicon . '
 3678  ' . $css . '
 3679  ' . $canonical . '
 3680  ' . $rss . $_delegate_ch . '
 3681  <title>' . $page_title . '</title>' . $scriptjs . $extra;
 3682  
 3683  	$embedearly = EMBEDEARLY;
 3684  
 3685  	$adembedearly = AD_EMBEDEARLY;
 3686  	
 3687    if (AD_ADBLOCK_TEXT && (DEFAULT_BURICHAN || BOARD_DIR === 'pol' || BOARD_DIR === 'bant')) {
 3688  	  $adembedearly .= file_get_contents_cached(AD_ADBLOCK_TEXT);
 3689  	}
 3690  	
 3691  	$board_class = 'board_' . BOARD_DIR;
 3692  	
 3693  	if (!$res) {
 3694  	  if ($is_arclist) {
 3695  	    $board_class = 'is_arclist ' . $board_class;
 3696  	  }
 3697  	  else {
 3698  	    $board_class = 'is_index ' . $board_class;
 3699  	  }
 3700  	}
 3701  	else {
 3702  	  $board_class = 'is_thread ' . $board_class;
 3703  	}
 3704  	
 3705  	if (TEXT_ONLY) {
 3706  	  $board_class = 'text_only ' . $board_class;
 3707  	}
 3708  	
 3709  	if (!$is_arclist) {
 3710  	  $abovePostForm = '<hr class="abovePostForm">';
 3711  	}
 3712  	else {
 3713  	  $abovePostForm = '';
 3714  	}
 3715    
 3716  	$dat .= <<<HTML
 3717  $embedearly
 3718  $adembedearly
 3719  <noscript><style type="text/css">#postForm { display: table !important; }#g-recaptcha { display: none; }</style></noscript>
 3720  </head>
 3721  <body class="$board_class">
 3722  <span id="id_css"></span>
 3723  $stylejs
 3724  $includenav
 3725  
 3726  <div class="boardBanner">
 3727  	$titlepart
 3728  	<div class="boardTitle">$title</div>
 3729  	$subtitle
 3730  </div>
 3731  $abovePostForm
 3732  HTML;
 3733    
 3734    if (!$error && !$is_arclist) {
 3735      /*
 3736      if (defined('ADS_BIDGLASS_TOP_MOBILE') && ADS_BIDGLASS_TOP_MOBILE) {
 3737        $dat .= '<div class="adg-rects mobile"><div class="ad-bgls adp-250 bidglass-unit-' . ADS_BIDGLASS_TOP_MOBILE . '" data-m data-u="' . ADS_BIDGLASS_TOP_MOBILE . '" style="pointer-events: none;"></div><div class="adl">[<a target="_blank" href="https://www.4chan.org/advertise">Advertise on 4chan</a>]</div><hr class="belowLeaderboard"></div>';
 3738      }
 3739      else if (defined('AD_ABC_TOP_MOBILE') && AD_ABC_TOP_MOBILE) {
 3740        $dat .= '<div class="adg-rects mobile"><div class="adg-m adp-250" data-abc="' . AD_ABC_TOP_MOBILE . '"></div><hr class="belowLeaderboard"></div>';
 3741      }*/
 3742    }
 3743  }
 3744  
 3745  function delete_uploaded_files()
 3746  {
 3747  	global $upfile_name, $upfile, $dest, $pchfile;
 3748  	if( $dest || $upfile ) {
 3749  		@unlink( $dest );
 3750  		@unlink( $upfile );
 3751  	}
 3752  }
 3753  
 3754  /* Footer */
 3755  function foot( &$dat, $error = false, $is_arclist = false )
 3756  {
 3757  	global $update_avg_secs;
 3758  	
 3759    $includenav = file_get_contents_cached(NAV2_TXT);
 3760    
 3761  	$dat .= $includenav;
 3762  	$dat .= rebuildd_stats();
 3763  
 3764  	if( CODE_TAGS ) {
 3765  		$dat .= '<script type="text/javascript" src="'
 3766  		. STATIC_SERVER . 'js/prettify/prettify.'
 3767  		. JS_VERSION . '.js"></script><script type="text/javascript">prettyPrint();</script>';
 3768  	}
 3769  
 3770  	$dat .= EMBEDLATE . '</body></html>';
 3771  }
 3772  
 3773  function error($mes, $unused = '') {
 3774  	global $mode;
 3775  	
 3776    if (isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] === 'application/json') {
 3777      error_json($mes);
 3778    }
 3779    
 3780  	if( $mode == "report" ) fancydie( $mes );
 3781  	
 3782  	delete_uploaded_files();
 3783  	
 3784  	head( $dat, 0, 1 );
 3785  	
 3786  	$protocol = (stripos($_SERVER["HTTP_REFERER"], "https") === 0) ? 'https:' : 'http:';
 3787  	
 3788  	$dat .= '<table style="text-align: center; width: 100%; height: 300px;"><tr valign="middle"><td align="center" style="font-size: x-large; font-weight: bold;"><span id="errmsg" style="color: red;">' . $mes . '</span><br><br>[<a href=';
 3789  	
 3790  	if (preg_match('#^' . $protocol . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . '/thread/([0-9]+)#', $_SERVER["HTTP_REFERER"], $m)) {
 3791  	  $thread_part = 'thread/' . (int)$m[1];
 3792  	}
 3793  	else {
 3794  	  $thread_part = '';
 3795  	}
 3796  	
 3797  	$dat .= $protocol . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . '/' . $thread_part . ">" . S_RELOAD . "</a>]</td></tr></table><br><br><hr size=1>";
 3798  	
 3799  	foot( $dat, true );
 3800  	
 3801  	if (TEST_BOARD==1) {
 3802  		internal_error_log("post error", $mes);
 3803  	}
 3804  	
 3805  	die( $dat );
 3806  }
 3807  
 3808  function error_json($msg) {
 3809    delete_uploaded_files();
 3810    
 3811    header('Content-Type: application/json');
 3812    
 3813    echo json_encode(['error' => $msg]);
 3814    
 3815    die();
 3816  }
 3817  
 3818  function error_redirect($mes, $redirect, $timeout = 3000) {
 3819  	delete_uploaded_files();
 3820  	head( $dat, 0, 1 );
 3821  	$dat .= <<<HTML
 3822  <script type="text/javascript">
 3823    setTimeout(function() { window.location = "$redirect"; }, $timeout);
 3824  </script>
 3825  HTML;
 3826  	$dat .= '<table style="text-align: center; width: 100%; height: 300px;"><tr valign="middle"><td align="center" style="font-size: x-large; font-weight: bold;"><span id="errmsg" style="color: red;">' . $mes . '</span><br><br>[<a href=';
 3827  	$dat .= '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/>"
 3828      . S_RELOAD . "</a>]</td></tr></table><br><br><hr size=1>";
 3829  	foot( $dat );
 3830  	
 3831  	if (TEST_BOARD==1) {
 3832  		internal_error_log("post error", $mes);
 3833  	}
 3834  	
 3835  	die( $dat );
 3836  }
 3837  
 3838  /* Auto Linker */
 3839  function normalize_link_cb( $m )
 3840  {
 3841  
 3842  	$subdomain = $m[1];
 3843  	$original  = $m[0];
 3844  	$board     = strtolower( $m[2] );
 3845  	$m[0]      = $m[1] = $m[2] = '';
 3846  
 3847  	$count = count( $m ) - 1;
 3848  	for( $i = $count; $i > 2; $i-- ) {
 3849  		if( $m[$i] ) {
 3850  			$no = $m[$i];
 3851  			break;
 3852  		}
 3853  	}
 3854  
 3855  	if( $subdomain != 'boards') {
 3856  		return $original;
 3857  	}
 3858  
 3859  	if( stripos( $no, 'catalog' ) === 0 ) {
 3860  
 3861  		if( ( $pos = stripos( $no, '#s=' ) ) !== false ) {
 3862  			$term = substr( $no, $pos + 3 );
 3863  		} else {
 3864  			$term = 'catalog';
 3865  		}
 3866  
 3867  		return "&gt;&gt;&gt;/$board/$term";
 3868  	}
 3869  
 3870  	if( $board == BOARD_DIR && $no && $no != 'catalog' ) {
 3871  		return "&gt;&gt;$no";
 3872  	} else {
 3873  		return "&gt;&gt;&gt;/$board/$no";
 3874  	}
 3875  }
 3876  
 3877  function normalize_links( $proto )
 3878  {
 3879  	// change http://xxx.4chan.org/board/res/no links into plaintext >># or >>>/board/#
 3880  	$proto = preg_replace_callback( '@https?://([a-z]*)[.](?:4chan|4channel)[.]org/(\w+)/(?:(res|thread)/(\d+)(?:\/[-a-z0-9]+)?(?:#[qp]?(\d*))?|(catalog(?:#s=[a-z0-9+]+)?)|\w+.php[?]res=(\d+)(?:#[qp]?(\d*))?|)(?=[\s.<!?,]|$)@i', 'normalize_link_cb', $proto );
 3881  
 3882  	return $proto;
 3883  }
 3884  
 3885  // TODO merge with get_resto_for
 3886  function post_resto($no) {
 3887    global $log;
 3888    
 3889    if (isset($log[$no])) {
 3890      return $log[$no]['resto'];
 3891    }
 3892    
 3893    $q = mysql_board_call('SELECT resto FROM `%s` WHERE no=%d', SQLLOG, $no);
 3894    if (!mysql_num_rows($q)) {
 3895      $log[$no] = array('resto' => false);
 3896      return false;
 3897    }
 3898    $r = (int)mysql_fetch_row($q)[0];
 3899    $log[$no] = array('resto' => $r);
 3900    return $r;
 3901    
 3902    return false;
 3903  }
 3904  
 3905  // FIXME
 3906  // This might not be used anywhere anymore
 3907  function intraboard_link_cb( $m )
 3908  {
 3909  	global $intraboard_cb_resno, $log;
 3910  	$no    = $m[1];
 3911  	$resno = $intraboard_cb_resno;
 3912  	$resto = post_resto( $no ); // doesn't like assignment in condition
 3913  	if( $resto !== false ) {
 3914  		$resdir = ( $resno ? '' : RES_DIR2 );
 3915  		$ext    = PHP_EXT2;
 3916  		$id     = NEW_HTML ? "p$no" : "$no";
 3917  		if( $resno && $resno == $resto ) // linking to a reply in the same thread
 3918  			return "<a href=\"#$id\" class=\"quotelink\">&gt;&gt;$no</a>";
 3919  		elseif( $resto == 0 ) // linking to a thread
 3920  			return "<a href=\"$resdir$no$ext#$id\" class=\"quotelink\">&gt;&gt;$no</a>"; else // linking to a reply in another thread
 3921  			return "<a href=\"$resdir$resto$ext#$id\" class=\"quotelink\">&gt;&gt;$no</a>";
 3922  	}
 3923  
 3924  	return '<span class="deadlink">' . $m[0] . '</span>';
 3925  }
 3926  
 3927  // FIXME
 3928  // This might not be used anywhere anymore, see parse_intraboard_link()
 3929  function intraboard_links( $proto, $resno )
 3930  {
 3931  	global $intraboard_cb_resno;
 3932  
 3933  	$intraboard_cb_resno = $resno;
 3934  
 3935  	$proto = preg_replace_callback( '/&gt;&gt;([0-9]+)/', 'intraboard_link_cb', $proto );
 3936  
 3937  	return $proto;
 3938  }
 3939  
 3940  function other_board_resto( $board, $resno )
 3941  {
 3942  	// this function is a little slow
 3943  	// board requirements - either is the current board (for /test/) or is public (in boardlist)
 3944  	// returns -
 3945  	// FALSE if resno does not exist
 3946  	// 0 if resno is a thread
 3947  	// an id if resno is a reply to a thread
 3948  
 3949  	static $boardlist = array();
 3950  
 3951  	if( !$boardlist )
 3952  		$boardlist = array_flip( mysql_column_array( mysql_global_call( "select sql_cache dir from boardlist" ) ) );
 3953  
 3954  	if( $board != BOARD_DIR && !isset( $boardlist[$board] ) )
 3955  		return false;
 3956  
 3957  	$q = mysql_board_call( "select resto from `%s` where no=%d", $board, $resno );
 3958  	if( !mysql_num_rows( $q ) )
 3959  		return false;
 3960  	$r = mysql_result( $q, 0 );
 3961  
 3962  	return $r;
 3963  }
 3964  
 3965  function interboard_link_cb( $m )
 3966  {
 3967  	// on one hand, we can link to imgboard.php, using any old subdomain,
 3968  	// and let apache & imgboard.php handle it when they click on the link
 3969  	// on the other hand, we can use the database to fetch the proper subdomain
 3970  	// and even the resto to construct a proper link to the html file (and whether it exists or not)
 3971  
 3972  	// for now, we'll assume there's more interboard links posted than interboard links visited.
 3973  	$board      = '/';
 3974  	$otherboard = mb_strtolower( $m[1] );
 3975  	if( $m[2] ) {
 3976  		$resto = other_board_resto( $otherboard, $m[2] );
 3977  		$id    = "#p";
 3978  
 3979  		if( $resto === false )
 3980  			$url = "";
 3981  		else if( $resto )
 3982  			$url = $board . $otherboard . '/thread/' . $resto . $id . $m[2];
 3983  		else
 3984  			$url = $board . $otherboard . '/thread/' . $m[2] . $id . $m[2];
 3985  	} else    $url = $board . $otherboard . '/';
 3986  
 3987  	$b        = $m[1];
 3988  	$mlp_hack = BOARD_DIR == 'mlp' && ( $b == 'b' || $b == 'co' );
 3989  	$original = mb_strtolower( $m[0] );
 3990  	if( !$url || $mlp_hack )
 3991  		return '<span class="deadlink">' . $original . '</span>';
 3992  	else
 3993  		return "<a href=\"$url\" class=\"quotelink\">{$original}</a>";
 3994  }
 3995  
 3996  function interboard_catalog_link_cb( $m )
 3997  {
 3998  	$board        = $m[1];
 3999  	if( $board == 'f' ) return $m[0];
 4000  
 4001  	$lsearchquery = strtolower( urlencode( urldecode( $m[2] ) ) );
 4002  	$original     = mb_strtolower( str_replace( "&gt;", "&gt;&nbsp;", $m[0] ) );
 4003  
 4004  	if( $lsearchquery == "catalog" ) {
 4005  		return "<a href=\"/$board/catalog\" class=\"quotelink\">$original</a>";
 4006  	} elseif( $lsearchquery == 'rules' ) {
 4007  		return '<a href="//www.' . L::d($board) . '/rules#' . $board . '" class="quotelink">' . $original . '</a>';
 4008  	} else {
 4009  		return "<a href=\"/$board/catalog#s=$lsearchquery\" class=\"quotelink\">$original</a>";
 4010  	}
 4011  }
 4012  
 4013  function boards_matching_arr()
 4014  {
 4015  	static $boards_matching_arr;
 4016  	global $valid_boards;
 4017  	
 4018  	if( empty( $boards_matching_arr ) ) $boards_matching_arr = explode( '|', $valid_boards );
 4019  
 4020  	return $boards_matching_arr;
 4021  }
 4022  
 4023  // Normalize and linkify internal and non-quote links
 4024  // before inserting the post into the database.
 4025  function normalize_and_linkify($proto) {
 4026    
 4027  	if (strpos($proto, "4chan") !== false || strpos($proto, "4cdn.org") !== false) {
 4028  		// normalize long links
 4029  		$proto = normalize_links($proto);
 4030  		
 4031  		// linkify other internal links
 4032  		if ((strpos($proto, "4chan") !== false && strpos($proto, "/derefer") === false) || strpos($proto, "4cdn.org") !== false) {
 4033  			$proto = preg_replace_callback( '/(https?:\/\/(?:[A-Za-z]*\.)?)(4chan|4channel|4cdn)(\.org)(\/[\w\-\.,@?^=%&;:\/~\+#\(\)]*[\w\-\@?^=%&;\/~\+#])?/i', 'clean_internal_link', $proto );
 4034  			$proto = preg_replace( '/([<][^>]*?)<a href="((https?:\/\/(?:[A-Za-z]*\.)?)(4chan|4channel|4cdn)(\.org)(\/[\w\-\.,@?^=%&:\/~\+#\(\)]*[\w\-\@?^=%&\/~\+#])?)" target="_blank">\\2<\/a>([^<]*?[>])/i', '\\1\\3\\4\\5\\6\\7\\8', $proto );
 4035  		}
 4036  	}
 4037  	
 4038  	if (strpos($proto, '&gt;&gt;&gt;') !== false) {
 4039  		$proto = preg_replace_callback('#&gt;&gt;&gt;/([a-z0-9]+)/([a-z0-9+/,l\-]*)#', 'auto_link_static_cb', $proto);
 4040  	}
 4041  	
 4042  	return $proto;
 4043  }
 4044  
 4045  // Removes >> links from internal 4chan.org links
 4046  function clean_internal_link($matches) {
 4047  	$link = preg_replace('/&gt;&gt;&gt;|&gt;&gt;/', '', $matches[0]);
 4048  	return "<a href=\"$link\" target=\"_blank\">$link</a>";
 4049  }
 4050  
 4051  function auto_link_static_cb($matches) {
 4052  	$post = $matches[0];
 4053  	$inter_board = $matches[1];
 4054  	$no = $matches[2];
 4055  	
 4056  	$boards_matching_arr = boards_matching_arr();
 4057  	
 4058  	$full_link = $post;
 4059  	
 4060  	$inter_board = strtolower( $inter_board );
 4061  	
 4062  	$is_board_link = ($no == '');
 4063  
 4064  	$resno = $no;
 4065  	
 4066  	// Text boards
 4067  	// Catalog, rules, and /rs/ links
 4068  	if (!is_numeric( $resno ) || $is_board_link) {
 4069  		$url = urlencode( urldecode( $resno ) );
 4070  		
 4071  		$target = '';
 4072  		
 4073  		if( strpos( $resno, 'rules' ) === 0 ) {
 4074  			$ruleno  = '';
 4075  			$ruleloc = strpos( $resno, '/' );
 4076  			
 4077  			if( $ruleloc !== false ) {
 4078  				$ruleno = substr( $resno, $ruleloc + 1 );
 4079  			}
 4080  			
 4081  			$parsed_link = '//www.' . L::d($inter_board) . "/rules#$inter_board$ruleno";
 4082  			$target      = ' target="_blank"';
 4083  		}
 4084  		else if (in_array($inter_board, $boards_matching_arr)) {
 4085  			$parsed_link = '//boards.' . L::d($inter_board) . "/$inter_board/";
 4086  			
 4087  			if( $inter_board == 'f' && $url == 'catalog' ) return $full_link;
 4088  			
 4089  			if( !$is_board_link ) {
 4090  				$parsed_link .= ($url == 'catalog' ? 'catalog' : "catalog#s=$url");
 4091  			}
 4092  		}
 4093  		else {
 4094  		  return $full_link;
 4095  		}
 4096  		
 4097  		return '<a href="' . $parsed_link . '" class="quotelink"' . $target . '>' . $full_link . '</a>';
 4098  	}
 4099  	
 4100  	return $full_link;
 4101  }
 4102  
 4103  function auto_link( $proto, $resno )
 4104  {
 4105  	global $current_resno;
 4106  	static $has_gen = 0;
 4107  
 4108  	if( !$has_gen ) {
 4109  		boards_matching_arr();
 4110  		$has_gen = 1;
 4111  	}
 4112  		
 4113  	// The majority of posts don't contain links, so don't go there and waste time on preg junk
 4114  	if( strpos( $proto, '&gt;&gt;' ) !== false ) {
 4115  		$current_resno = $resno;
 4116  		$proto = preg_replace_callback( '#(&gt;&gt;[0-9]+|&gt;&gt;&gt;/[a-z0-9]+/[a-z0-9+/-]*)#', 'auto_link_cb', $proto );
 4117  	}
 4118  	
 4119  	return $proto;
 4120  }
 4121  
 4122  function auto_link_cb( $post )
 4123  {
 4124  	global $current_resno;
 4125  
 4126  	//var_dump($post);
 4127  	$is_inter = ( strpos( $post[0], '&gt;&gt;&gt;/' ) === 0 );
 4128  	$post     = $post[0];
 4129  
 4130  	if( $is_inter ) {
 4131  		preg_match( '#&gt;&gt;&gt;/([a-z0-9]+)/(.*)#', $post, $match );
 4132  		
 4133  		return parse_interboard_link( $match[0], $match[1], $match[2] );
 4134  	} else {
 4135  		$no = explode( '&gt;&gt;', $post );
 4136  
 4137  		return parse_intraboard_link( $post, $no[1], $current_resno );
 4138  	}
 4139  }
 4140  
 4141  function parse_intraboard_link( $post, $no, $resno )
 4142  {
 4143  	$full_link = $post;
 4144  	//$no        = substr( $post, $i - $in_link_char, $in_link_char );
 4145  	$resto = post_resto( $no );
 4146  
 4147  	if( $resto === false ) {
 4148  		$parsed_link = '<span class="deadlink">' . $full_link . '</span>';
 4149  
 4150  		return $parsed_link;
 4151  	}
 4152  
 4153  	$ext    = PHP_EXT2;
 4154  	$id     = NEW_HTML ? "p$no" : "$no";
 4155    
 4156    // linking to a reply or the OP in the same thread
 4157    if ($resno && ($resno == $resto || $resno == $no)) { 
 4158      $parsed_link = "<a href=\"#$id\" class=\"quotelink\">&gt;&gt;$no</a>";
 4159    }
 4160    // linking to an OP in another thread or from indexes
 4161    elseif ($resto == 0) {
 4162      $parsed_link = "<a href=\"/" . BOARD_DIR . "/" . RES_DIR2 . "$no$ext#$id\" class=\"quotelink\">&gt;&gt;$no</a>";
 4163    }
 4164    // linking to a reply in another thread or from indexes
 4165    else {
 4166      $parsed_link = "<a href=\"/" . BOARD_DIR . "/" . RES_DIR2 . "$resto$ext#$id\" class=\"quotelink\">&gt;&gt;$no</a>";
 4167    }
 4168    
 4169  	return $parsed_link;
 4170  }
 4171  
 4172  function parse_interboard_link( $post, $inter_board, $no )
 4173  {
 4174  	global $valid_boards;
 4175  
 4176  	// make sure to account for &gt; not >
 4177  	$full_link   = $post;
 4178  	$inter_board = strtolower( $inter_board );
 4179  
 4180  	// Are we a board link?
 4181  	$is_board_link = ( $no == '' );
 4182  
 4183  	// ... to get resno!
 4184  	$resno = $no;
 4185  
 4186  	$valid_rule = $inter_board == 'global' && strpos( $resno, 'rules' ) !== false;
 4187  	
 4188  	// Skip static links (boards, catalog)
 4189  	if ($is_board_link
 4190  		|| $valid_rule
 4191  		|| !is_numeric($resno)
 4192  		|| !in_array($inter_board, boards_matching_arr())
 4193  		) {
 4194  		return $full_link;
 4195  	}
 4196  	
 4197  	// Valid board, now check post number
 4198  	$resto = other_board_resto( $inter_board, $resno );
 4199  
 4200  	if( $resto === false ) { // dead link
 4201  		$url = '';
 4202  	} elseif( $resto ) { // different thread
 4203  		$url = '//boards.' . L::d($inter_board) . "/{$inter_board}/thread/$resto#p$resno";
 4204  	} else { // same thread
 4205  		$url = '//boards.' . L::d($inter_board) . "/{$inter_board}/thread/$resno#p$resno";
 4206  	}
 4207  
 4208  	$disable = BOARD_DIR == 'mlp' && ( $inter_board == 'b' || $inter_board == 'co' );
 4209  	if( !$url || $disable ) {
 4210  		$parsed_link = '<span class="deadlink">' . $full_link . '</span>';
 4211  	} else {
 4212  		$parsed_link = "<a href=\"$url\" class=\"quotelink\">$full_link</a>";
 4213  	}
 4214  
 4215  	return $parsed_link;
 4216  }
 4217  
 4218  function trans_same_board_links( &$com )
 4219  {
 4220  	$match    = '>>>/' . BOARD_DIR . '/';
 4221  	$len      = strlen( $match );
 4222  	$i        = stripos( $com, $match );
 4223  	$boardlen = strlen( BOARD_DIR ) - 1;
 4224  	$dir      = BOARD_DIR;
 4225  	$com .= '~';
 4226  
 4227  	while( isset( $com{$i} ) ) {
 4228  
 4229  		if( is_numeric( $com{$i + $len} ) ) {
 4230  			// Match, replace out
 4231  			$com = substr_replace( $com, '>>', $i, $len );
 4232  
 4233  		}
 4234  
 4235  		$i = stripos( $com, $match, $i + $len );
 4236  		if( $i === false ) {
 4237  			$com = substr( $com, 0, strlen( $com ) - 1 );
 4238  
 4239  			return;
 4240  		}
 4241  	}
 4242  }
 4243  
 4244  function auto_link_parser( $post, $resno )
 4245  {
 4246  	$post .= '<';
 4247  
 4248  	$i            = 0;
 4249  	$in_link      = false;
 4250  	$in_link_char = 0;
 4251  
 4252  	$is_inter          = false;
 4253  	$inter_found_board = false;
 4254  	$inter_board       = '';
 4255  	$inter_is_catalog  = false;
 4256  
 4257  	$is_intra = false;
 4258  	$gt_count = 0;
 4259  	$mbcl     = 10; // forgot about dis links :(
 4260  
 4261  	$dbg = "";
 4262  
 4263  	while( isset( $post{$i} ) ) {
 4264  		$seen_gt_this = false;
 4265  		$c            = $post{$i};
 4266  
 4267  		if( !$in_link ) {
 4268  			// Not in a link, find &gt;
 4269  
 4270  			if( $c == '&' ) {
 4271  				if( $post{$i + 1} == 'g' && $post{$i + 2} == 't' && $post{$i + 3} == ';' ) {
 4272  					if( $gt_count < 3 ) $gt_count++;
 4273  
 4274  					$i = $i + 4;
 4275  					continue;
 4276  				}
 4277  			}
 4278  
 4279  			if( ( $c == '/' && $gt_count == 3 ) || ( $gt_count > 1 && is_numeric( $c ) ) ) {
 4280  				$in_link_char = 0;
 4281  				$in_link      = true;
 4282  				$i--; // shift us back a char to get the right match...
 4283  			}
 4284  
 4285  			$gt_count = 0;
 4286  		} else {
 4287  			// We can be sure we have a valid character for our link
 4288  			if( $in_link_char == 0 ) {
 4289  
 4290  				if( $c == '/' ) {
 4291  					$is_inter = true;
 4292  					$is_intra = false;
 4293  				} else {
 4294  					$is_intra = true;
 4295  					$is_inter = false;
 4296  				}
 4297  			}
 4298  
 4299  			if( $is_inter ) {
 4300  
 4301  				if( $in_link_char == 0 ) {
 4302  					$in_link_char++;
 4303  					$i++;
 4304  					continue;
 4305  				}
 4306  
 4307  				if( $in_link_char > $mbcl && $c != '/' && !$inter_found_board ) {
 4308  					// yup :(
 4309  
 4310  					$in_link  = false;
 4311  					$is_inter = false;
 4312  
 4313  					$i++;
 4314  					continue;
 4315  				}
 4316  
 4317  				if( !$inter_found_board && $c == '/' ) {
 4318  					$inter_board = substr( $post, $i - ( $in_link_char - 1 ), $in_link_char - 1 );
 4319  
 4320  					$inter_found_board = true;
 4321  					$in_link_char++;
 4322  					$i++;
 4323  					continue;
 4324  				}
 4325  
 4326  				if( $inter_found_board ) {
 4327  					$match = (
 4328  						ctype_alnum( $c ) ||
 4329  							$c == '+' ||
 4330  							$c == '/' ||
 4331  							$c == '-'
 4332  					);
 4333  
 4334  					if( $match ) {
 4335  						$in_link_char++;
 4336  						$i++;
 4337  						continue;
 4338  					}
 4339  				}
 4340  
 4341  				if( !$inter_found_board ) {
 4342  					$in_link_char++;
 4343  					$i++;
 4344  					continue;
 4345  				}
 4346  
 4347  
 4348  				parse_interboard_link( $post, $i, $in_link_char, $inter_board );
 4349  
 4350  				$is_inter          = false;
 4351  				$in_link           = false;
 4352  				$inter_found_board = false;
 4353  			}
 4354  
 4355  			if( $is_intra ) {
 4356  				if( is_numeric( $c ) ) {
 4357  					$in_link_char++;
 4358  				} else {
 4359  					// reached the end
 4360  					parse_intraboard_link( $post, $i, $in_link_char, $resno );
 4361  
 4362  					$in_link  = false;
 4363  					$is_intra = false;
 4364  				}
 4365  			}
 4366  		}
 4367  
 4368  		$i++;
 4369  	}
 4370  
 4371  	return substr( $post, 0, strlen( $post ) - 1 ) . $dbg;
 4372  }
 4373  
 4374  /**
 4375   * New version of check_blacklist()
 4376   * $post must be an array with the following fields:
 4377   * resto, filename, name, tripcode, password, 4pass_id
 4378   */
 4379  function check_md5_blacklist($md5, $original_md5, $post, $dest) {
 4380    if (!$md5) {
 4381      return false;
 4382    }
 4383    
 4384    $board = BOARD_DIR;
 4385    
 4386    if (DEFAULT_BURICHAN) {
 4387      $ws_clause = " OR boardrestrict = '_ws_'";
 4388    }
 4389    else {
 4390      $ws_clause = '';
 4391    }
 4392    
 4393    $sql =<<<SQL
 4394  SELECT SQL_NO_CACHE * FROM blacklist
 4395  WHERE active = 1 AND (boardrestrict = '' OR boardrestrict = '$board'$ws_clause)
 4396  AND field = 'md5' AND contents = '%s' LIMIT 1
 4397  SQL;
 4398    
 4399    // Check MD5
 4400    $query = mysql_global_call($sql, $md5);
 4401    
 4402    // Check original MD5 if provided
 4403    if (!mysql_num_rows($query)) {
 4404      if ($original_md5 && $original_md5 !== $md5) {
 4405        $query = mysql_global_call($sql, $original_md5);
 4406        
 4407        if (!mysql_num_rows($query)) {
 4408          return false;
 4409        }
 4410        
 4411        $md5 = $original_md5;
 4412      }
 4413      else {
 4414        return false;
 4415      }
 4416    }
 4417    
 4418    $row = mysql_fetch_assoc($query);
 4419    
 4420    if (!$row) {
 4421      return false;
 4422    }
 4423    
 4424    // Private reason
 4425    $private_reason = "Blacklisted md5 - " . htmlspecialchars($md5)
 4426      . ' - Filename: ' . htmlspecialchars($post['filename']);
 4427    
 4428    // Ban name
 4429    if (isset($post['name'])) {
 4430      $ban_name = $post['name'];
 4431    }
 4432    else {
 4433      $ban_name = S_ANONAME;
 4434    }
 4435    
 4436    if (isset($post['tripcode'])) {
 4437      $ban_name .= " #{$post['tripcode']}";
 4438    }
 4439    
 4440    // Ban password
 4441    if (isset($post['password']) && $post['password']) {
 4442      $pwd = $post['password'];
 4443    }
 4444    else {
 4445      $pwd = null;
 4446    }
 4447    
 4448    // Ban 4chan pass
 4449    if (isset($post['4pass_id']) && $post['4pass_id']) {
 4450      $pass_id = $post['4pass_id'];
 4451    }
 4452    else {
 4453      $pass_id = null;
 4454    }
 4455    
 4456    // Thread id for redirection
 4457    if (isset($post['resto'])) {
 4458      $resto = (int)$post['resto'];
 4459    }
 4460    else {
 4461      $resto = 0;
 4462    }
 4463    
 4464    // --------
 4465    
 4466    // Reject
 4467    if (!$row['ban']) {
 4468      if (TEST_BOARD) {
 4469        error($private_reason, $dest);
 4470      }
 4471    }
 4472    // Auto-ban
 4473    else if ($row['ban'] == '1') {
 4474      auto_ban_poster($ban_name, $row['banlength'], 1, $private_reason, $row['banreason'], false, $pwd, $pass_id);
 4475    }
 4476    // Show error (DMCA requests)
 4477    else if ($row['ban'] == '2') {
 4478      $ip = ip2long($_SERVER['REMOTE_ADDR']);
 4479      
 4480      $query = "SELECT ip FROM user_actions WHERE action = 'fail_dmca' AND ip = %d AND time >= DATE_SUB(NOW(), INTERVAL 1 DAY)";
 4481      
 4482      $res = mysql_global_call($query, $ip);
 4483      
 4484      if ($res && mysql_num_rows($res) > 0) {
 4485        $private_reason = "DMCA complaint from {$row['description']} (blacklist ID: {$row['id']})";
 4486        auto_ban_poster($ban_name, 3, 1, $private_reason, S_DMCABANREASON, true, $pwd, $pass_id);
 4487        error_redirect(S_BANNED, 'https://www.' . L::d(BOARD_DIR) . '/banned');
 4488      }
 4489      else {
 4490        $query = "INSERT INTO user_actions (board,postno,ip,time,uploaded,action) VALUES ('%s', %d, %d, NOW(), 0, 'fail_dmca')";
 4491        mysql_global_call($query, $board, 0, $ip);
 4492      }
 4493      
 4494      error(S_DMCAFAIL, $dest);
 4495    }
 4496    
 4497    if ($row['quiet']) {
 4498      show_post_successful_fake($resto);
 4499      die();
 4500    }
 4501    
 4502    error(S_FAILEDUPLOAD, $dest);
 4503  }
 4504  
 4505  function check_blacklist($post, $dest, $file_ext = '', $resto = 0, $pwd = null, $pass_id = null) {
 4506  	//if( has_level() ) return;
 4507  	
 4508  	$board    = BOARD_DIR;
 4509  	
 4510    if (DEFAULT_BURICHAN) {
 4511      $ws_clause = " OR boardrestrict = '_ws_'";
 4512    }
 4513    else {
 4514      $ws_clause = '';
 4515    }
 4516    
 4517  	$querystr = "SELECT SQL_NO_CACHE * FROM blacklist WHERE active=1 AND (boardrestrict='' or boardrestrict='$board'$ws_clause) AND (0 ";
 4518  	foreach( $post as $field => $contents ) {
 4519  		if( $contents ) {
 4520  			$contents = mysql_real_escape_string( html_entity_decode( $contents ) );
 4521  			$querystr .= "OR (field='$field' AND contents='$contents') ";
 4522  		}
 4523  	}
 4524  	$querystr .= ") LIMIT 1";
 4525  	
 4526  	$query = mysql_global_call( $querystr );
 4527  	if( mysql_num_rows( $query ) == 0 ) return false;
 4528  	
 4529  	$row       = mysql_fetch_assoc( $query );
 4530  	$prvreason = "Blacklisted ${row['field']} - " . htmlspecialchars( $row['contents'] );
 4531  	
 4532  	if ($row['field'] == 'md5') {
 4533  		$prvreason .= ' - Filename: ' . htmlspecialchars($post['filename']) . $file_ext;
 4534  	}
 4535  	
 4536    if (!$row['ban']) {
 4537      if (TEST_BOARD) {
 4538        error( "Blacklisted: " . $prvreason, $dest );
 4539      }
 4540    }
 4541    // Auto-ban
 4542    else if ($row['ban'] == '1') {
 4543      auto_ban_poster($post['trip'] ? $post['nametrip'] : $post['name'], $row['banlength'], 1, $prvreason, $row['banreason'], false, $pwd, $pass_id);
 4544    }
 4545    // Show error (DMCA requests)
 4546    else if ($row['ban'] == '2') {
 4547      $ip = ip2long($_SERVER['REMOTE_ADDR']);
 4548      
 4549      $query = "SELECT ip FROM user_actions WHERE action = 'fail_dmca' AND ip = %d AND time >= DATE_SUB(NOW(), INTERVAL 1 DAY)";
 4550      
 4551      $res = mysql_global_call($query, $ip);
 4552      
 4553      if ($res && mysql_num_rows($res) > 0) {
 4554        $prvreason = "DMCA complaint from {$row['description']} (blacklist ID: {$row['id']})";
 4555        
 4556        auto_ban_poster($post['trip'] ? $post['nametrip'] : $post['name'], 3, 1, $prvreason, S_DMCABANREASON, true, $pwd, $pass_id);
 4557        
 4558        error_redirect(S_BANNED, 'https://www.' . L::d(BOARD_DIR) . '/banned');
 4559      }
 4560      else {
 4561        $query = "INSERT INTO user_actions (board,postno,ip,time,uploaded,action) VALUES ('%s',%d,%d,NOW(),0,'fail_dmca')";
 4562        mysql_global_call($query, $board, 0, $ip);
 4563      }
 4564      
 4565      error(S_DMCAFAIL, $dest);
 4566    }
 4567    /*
 4568  	{
 4569  		$ip = $_SERVER['REMOTE_ADDR'];
 4570  		quick_log_to("/www/perhost/blacklist.log", "IP $ip board /$board/: $prvreason");
 4571  	}
 4572    */
 4573    
 4574    if ($row['quiet']) {
 4575      show_post_successful_fake($resto);
 4576      die();
 4577    }
 4578    
 4579  	error(S_FAILEDUPLOAD, $dest);
 4580  }
 4581  
 4582  // we've already failed the floodcheck, check if they're a repeat offender and ban them
 4583  function check_fail_floodcheck($info)
 4584  {
 4585  	$ip = ip2long($_SERVER['REMOTE_ADDR']);
 4586  	mysql_global_call("insert into user_actions (ip,board,action,time) values (%d,'%s','fail_floodcheck',now())", $ip, '');
 4587  	$query = mysql_global_call("select count(*)>%d from user_actions where ip=%d and action='fail_floodcheck' and time >= subdate(now(), interval 1 hour)", LOGIN_FAIL_HOURLY, $ip);
 4588  	quick_log_to("/www/perhost/floodchecks.log", $info);
 4589  	if(mysql_result($query,0,0)) {
 4590  		auto_ban_poster("Anonymous", 1, 1, "got a flood check warning 5 times in an hour", "Sending an excessive number of server requests");
 4591  	}
 4592  }
 4593  
 4594  // word-wrap without touching things inside of tags
 4595  function wordwrap2( $str, $cols, $cut )
 4596  {
 4597  	// if there's no runs of $cols non-space characters, wordwrap is a no-op
 4598  	if( mb_strlen( $str ) < $cols || !preg_match( '/[^ <>]{' . $cols . '}/', $str ) ) {
 4599  		return $str;
 4600  	}
 4601  	$sections = preg_split( '/[<>]/', $str );
 4602  	$str      = '';
 4603  	for( $i = 0; $i < count( $sections ); $i++ ) {
 4604  		if( $i % 2 ) { // inside a tag
 4605  			$str .= '<' . $sections[$i] . '>';
 4606  		} else { // outside a tag
 4607  			$words   = explode( ' ', $sections[$i] );/*
 4608  			$exclude = array(
 4609  				'http://',
 4610  				'https://',
 4611  				'www.'
 4612  			);
 4613  */
 4614  			foreach( $words as &$word ) {/*
 4615  				foreach( $exclude as $match ) {
 4616  					if( stripos( $word, $match ) === 0 && stripos( $word, '4chan.org' ) !== false ) continue 2;
 4617  				}*/
 4618  
 4619  				$word = htmlspecialchars_decode( $word, ENT_QUOTES );
 4620  				$word = utf8_wordwrap( $word, $cols, $cut, true );
 4621  				$word = htmlspecialchars( $word, ENT_QUOTES );
 4622  
 4623  			}
 4624  
 4625  			$str .= implode( ' ', $words );
 4626  		}
 4627  	}
 4628  
 4629  	return $str;
 4630  }
 4631  
 4632  function logtime( $desc )
 4633  {
 4634  	static $run = -1;
 4635  	if( !PROFILING ) return;
 4636  	if( $run == -1 ) {
 4637  		$run = getmypid_cached();
 4638  	}
 4639  	$board = BOARD_DIR;
 4640  	$time  = microtime( true );
 4641  	mysql_global_call( "INSERT INTO profiling_times VALUES ('$board',$run,$time,'$desc')" );
 4642  }
 4643  
 4644  function time_log($r) {
 4645    if (TEST_BOARD && $_SERVER['HTTP_ACCEPT'] !== 'application/json') {
 4646      echo "<!-- $r " . microtime(true) . " " . memory_get_usage(true) . " -->\n";
 4647    }
 4648  }
 4649  
 4650  function is_bad_xff( $xff )
 4651  {
 4652    if ($xff === '8.8.8.8' || $xff === '62.210.138.29' || $xff === '212.129.0.228') {
 4653      return true;
 4654    }
 4655    
 4656  	list( $xffs ) = post_filter_get( "xffwhitelist" );
 4657  	$ipnum = ip2long( $xff );
 4658  
 4659  	if( !$ipnum ) return true; // text in xff field
 4660  
 4661  	return find_ipxff_in( 0, $ipnum, $xffs );
 4662  }
 4663  
 4664  function has_doubles( $id )
 4665  {
 4666  	if( $id % 1000 == 0 ) return false;
 4667  
 4668  	$ones = $id % 10;
 4669  	$tens = ( $id / 10 ) % 10;
 4670  
 4671  	return $ones == $tens;
 4672  }
 4673  
 4674  function generate_uid($resto, $time, $ip = false) {
 4675    if (DISP_ID_RANDOM) {
 4676      $str = mt_rand();
 4677    }
 4678    else {
 4679      $str = !$ip ? $_SERVER["REMOTE_ADDR"] : $ip;
 4680      
 4681      if (DISP_ID_PER_THREAD) {
 4682        $str .= $resto ? $resto : date( 'Ymd', $time );
 4683      } else {
 4684        $str .= 'hats'; // we will put a hat on it to confuse people :)
 4685      }
 4686    }
 4687  
 4688  	$salt = file_get_contents_cached( SALTFILE );
 4689  	$hash = base64_encode( pack( "H*", sha1( $str . $salt ) ) );
 4690  
 4691  	return substr( $hash, 0, 8 );
 4692  }
 4693  
 4694  function parse_vip_capcode($capcode) {
 4695      // Flood check
 4696      $longip = ip2long($_SERVER['REMOTE_ADDR']);
 4697      
 4698      $query = <<<SQL
 4699  SELECT COUNT(*) FROM user_actions
 4700  WHERE ip = %d AND action = 'fail_login'
 4701  AND time >= SUBDATE(NOW(), INTERVAL 1 HOUR)
 4702  SQL;
 4703      
 4704      $res = mysql_global_call($query, $longip);
 4705      
 4706      if (!$res) {
 4707        return false;
 4708      }
 4709      
 4710      $count = mysql_fetch_row($res)[0];
 4711      
 4712      if ($count >= 3) {
 4713        return false;
 4714      }
 4715      
 4716      // Now check the capcode
 4717      list($_, $user_id, $user_key) = explode('!', $capcode, 3);
 4718      
 4719      if (!$user_id || !$user_key) {
 4720        return false;
 4721      }
 4722      
 4723      $query = "SELECT name, user_key FROM vip_capcodes WHERE active = 1 AND user_id = '%s' LIMIT 1";
 4724      
 4725      $res = mysql_global_call($query, $user_id);
 4726      
 4727      if (!$res) {
 4728        return false;
 4729      }
 4730      
 4731      $user = mysql_fetch_assoc($res);
 4732      
 4733      if ($user && password_verify($user_key, $user['user_key'])) {
 4734        $query = "UPDATE vip_capcodes SET last_used = %d, last_ip = '%s' WHERE user_id = '%s' LIMIT 1";
 4735        mysql_global_call($query, $_SERVER['REQUEST_TIME'], $_SERVER['REMOTE_ADDR'], $user_id);
 4736        return $user['name'];
 4737      }
 4738      
 4739      // Log the failure
 4740      $query = <<<SQL
 4741  INSERT INTO user_actions (ip, board, action, time)
 4742  VALUES (%d, '', 'fail_login', NOW())
 4743  SQL;
 4744      
 4745      mysql_global_call($query, $longip);
 4746      
 4747      return false;
 4748  }
 4749  
 4750  function parse_capcode($capcode, &$name = null)
 4751  {
 4752    if ($name !== null && strpos($capcode, 'capcode_!') === 0) {
 4753      $vip_name = parse_vip_capcode($capcode);
 4754      
 4755      if ($vip_name !== false) {
 4756        $name = $vip_name;
 4757        return 'verified';
 4758      }
 4759      
 4760      return 'none';
 4761    }
 4762    
 4763  	if (!has_level()) {
 4764  	  return 'none';
 4765  	}
 4766  	
 4767  	$user = strtolower( $_COOKIE['4chan_auser'] );
 4768  	
 4769  	$is_developer = has_flag( 'developer' ) && $capcode == 'capcode_dev';
 4770  	$is_mod    = ( has_level() && $capcode == 'capcode_mod' );
 4771  	$is_manager = has_level('manager') && $capcode == 'capcode_manager';
 4772  	
 4773  	$is_admin   = ( has_level( 'admin' ) && $capcode == 'capcode_admin' );
 4774  	$is_founder = ( has_level( 'admin' ) && $capcode == 'capcode_founder' );
 4775  	$highlight = ( has_level( 'admin' ) && $capcode == 'capcode_admin_hl' );
 4776    
 4777  	if ($is_founder) {
 4778  	  return 'founder';
 4779  	}
 4780  	
 4781  	if( $is_admin || $highlight ) {
 4782  		return ( $highlight ) ? 'admin_highlight' : 'admin';
 4783  	}
 4784  
 4785  	if( $is_developer ) {
 4786  		return 'developer';
 4787  	}
 4788    
 4789  	if ($is_manager) {
 4790  		return 'manager';
 4791  	}
 4792    
 4793    if (!has_flag('capcode') && !has_level('manager')) {
 4794      if ($capcode !== '') {
 4795        error(S_CANTCAPCODE);
 4796      }
 4797    }
 4798    
 4799  	if( $is_mod ) {
 4800  		return 'mod';
 4801  	}
 4802  
 4803  	return 'none';
 4804  }
 4805  
 4806  function generate_tim() {
 4807    $time = $_SERVER['REQUEST_TIME'];
 4808    $micro = substr(microtime(), 2, 4);
 4809    $tail = mt_rand(0, 99);
 4810    
 4811    if ($tail < 10) {
 4812      $tail = "0$tail";
 4813    }
 4814    
 4815    return "$time$micro$tail";
 4816  }
 4817  
 4818  /* Regist */
 4819  function new_post( $name, $email, $sub, $com, $url, $pwd, $upfile, $upfile_name, $resto, $age, $filetag )
 4820  {
 4821  	global $pwdc, $textonly, $admin, $spoiler, $dest, $pchfile, $word_filters_enabled, $silent_reject;
 4822  	global $captcha_bypass, $passid;
 4823  	global $board_flags_array;
 4824    
 4825  	// Fro HTML and capcode posting
 4826  	$log_mod_action = false;
 4827  	$log_html_post = false;
 4828  	$log_capcode_post = false;
 4829  	
 4830  	$mes = "";
 4831  
 4832  	/* VARIOUS CHECKS BEFORE ANY POSTING TAKES PLACE */
 4833  	
 4834  	$oldbanbuster = ( $_SERVER['HTTP_USER_AGENT'] == 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)' && $com == 'test' );
 4835  	$newbanbuster = ( $_SERVER['HTTP_USER_AGENT'] == 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.19 (KHTML, like Gecko) Chrome/25.0.1324.0 Safari/537.19' && ( preg_match( '#^[a-zA-Z]{15}$#', $com ) || $com == 'test' ) && !isset( $_COOKIE['4chan_pass'] ) );
 4836  	
 4837  	if( BOARD_DIR == 'b' && ( $oldbanbuster || $newbanbuster ) ) {
 4838  		$msg = $com == 'test' ? 'com = test' : 'com = 15 length string';
 4839  		auto_ban_poster( "", -1, 1, 'BanBuster proxy test match (Posting ' . $msg . ' from dodgy UA)', 'Proxy/Tor exit node.' );
 4840  		error( S_NOCAPTCHA );
 4841  	}
 4842  
 4843  	if( $_SERVER["REQUEST_METHOD"] != "POST" ) error( S_UNJUST );
 4844  	
 4845  	/* END CHECKS */
 4846  	
 4847  	$hasjs     = isset( $_POST['hasjs'] ) && $_POST['hasjs'] === 'yes';
 4848  	$stats_all = $hasjs ? 'with_js_all' : 'without_js_all';
 4849  	$stats_ok  = $hasjs ? 'with_js_success' : 'without_js_success';
 4850  
 4851  
 4852  	$host = $_SERVER["REMOTE_ADDR"];
 4853  
 4854  	time_log( "start" );
 4855  	
 4856  	// might be better to do this before the mysql connection
 4857  	if( !$upfile && !$resto ) { // allow textonly threads for moderators!
 4858  		if( has_level() ) $textonly = 1;
 4859  	}
 4860  	elseif( JANITOR_BOARD == 1 ) { // only allow mods/janitors to post, and textonly is always ok
 4861  		$textonly = 1;
 4862  		if( !has_level( 'janitor' ) )
 4863  			die();
 4864  	}
 4865  	
 4866  	if (TEXT_ONLY && $upfile && $resto) {
 4867  	  error(S_TEXT_ONLY);
 4868  	}
 4869  	
 4870  	if (UPLOAD_BOARD) {
 4871  	  if ($upfile && $resto) {
 4872  	    error(S_FAILEDUPLOAD);
 4873  	  }
 4874  	  
 4875  		$tags = upboard_tags();
 4876  		
 4877  		if( !$resto && !array_key_exists( (int)$filetag, $tags ) ) error( "Error: Invalid tag specified.", $filetag );
 4878  		if( !$resto && !is_numeric( $filetag ) ) error( "Error: Invalid tag specified.", $filetag );
 4879  		//if((int)$filetag==9999) error("No tag selected.",$filetag);
 4880  		if( !$resto ) $sub = (int)$filetag . '|' . $sub;
 4881  		
 4882  		//OPs must have files	
 4883  		if (!$upfile && !$resto) {
 4884  			error(S_NOUPLOAD);
 4885  		}
 4886  	}
 4887  	
 4888    // Password session
 4889    $userpwd = new UserPwd($host, MAIN_DOMAIN, $pwdc);
 4890    
 4891    $pass = $userpwd->getPwd();
 4892    
 4893    if (!$pass) {
 4894      error(S_GENERICERROR, $dest);
 4895    }
 4896    
 4897  	$resto = (int)$resto;
 4898  	
 4899  	// time
 4900  	$time = $_SERVER['REQUEST_TIME'];
 4901  	$tim  = generate_tim();
 4902    
 4903    $captcha_bypass_allow_credits = false;
 4904    
 4905    $memcached = null;
 4906    
 4907    if (isset($_POST['recaptcha_challenge_field'])) {
 4908      error('Legacy captcha is no longer supported.');
 4909    }
 4910    else if (isset($_POST['g-recaptcha-response'])) {
 4911      if (CAPTCHA_TWISTER) {
 4912        error('reCAPTCHA v2 is no longer supported.');
 4913      }
 4914      
 4915      // Recaptcha v2
 4916      start_auth_captcha();
 4917  
 4918      if (!$captcha_bypass) {
 4919        $_c_ret = end_recaptcha_verify();
 4920      }
 4921  	}
 4922  	else {
 4923  		if (valid_captcha_bypass() !== true) {
 4924  			$memcached = create_memcached_instance();	
 4925  			
 4926  			$_unsolved_count = 0;
 4927  			
 4928  			// Captcha bypass credits
 4929  			if ($resto && isset($_POST['t-challenge']) && $_POST['t-challenge'] === 'noop') {
 4930  				if (use_twister_captcha_credit($memcached, $host, $userpwd) === false) {
 4931  					error(S_CAPTCHATIMEOUT);
 4932  				}
 4933  			}
 4934  			// Captcha verification failed
 4935        else if (is_twister_captcha_valid($memcached, $host, $userpwd, BOARD_DIR, $resto, $_unsolved_count) === false) {
 4936          // Silent captcha failure for new suspicious users
 4937          //$_bad_actor = spam_filter_is_bad_actor();
 4938          //$_threat_score = spam_filter_get_threat_score($_SERVER['HTTP_X_GEO_COUNTRY'], !$resto, true);
 4939          if (isset($_SERVER['HTTP_X_BOT_SCORE'])) {
 4940            $_bot_score = (int)$_SERVER['HTTP_X_BOT_SCORE'];
 4941          }
 4942          else {
 4943            $_bot_score = 100;
 4944          }
 4945          
 4946          if (!$userpwd || $userpwd->isUserKnownOrVerified(1440, 1) === false) {
 4947            if ($_bot_score > 1 && $_bot_score < 80) {
 4948              $_meta = spam_filter_format_http_headers(htmlspecialchars($com), $_SERVER['HTTP_X_GEO_COUNTRY'], $upfile_name, $_threat_score);
 4949              if (isset($_POST['t-challenge']) && $_POST['t-response'] && isset($_COOKIE['_tcs'])) {
 4950                log_failed_captcha($host, $userpwd, BOARD_DIR, $resto, true, $_meta);
 4951              }
 4952              show_post_successful_fake($resto, false);
 4953              die();
 4954            }
 4955          }
 4956          error(S_BADCAPTCHA);
 4957        }
 4958  			// Captcha verification succeeded
 4959  			else if ($_unsolved_count < 2 && CAPTCHA_ALLOW_BYPASS && spam_filter_is_bad_actor() === false) {
 4960  				$captcha_bypass_allow_credits = true;
 4961  			}
 4962  		}
 4963  	}
 4964  	/*
 4965    if (spam_filter_is_bad_actor()) {
 4966      $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 4967      log_spam_filter_trigger('log_bad_actor', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 4968    }
 4969    */
 4970  	if (PASS_POST_ONLY && !$captcha_bypass) {
 4971  	  error(S_PASS_POST_ONLY);
 4972  	}
 4973  	
 4974  	// Validate 2FA if posting html
 4975    if ((isset($_POST['html']) && $_POST['html']) && (has_level('manager') || has_flag('html') || has_flag('developer'))) {
 4976      validate_otp();
 4977      $log_html_post = $log_mod_action = true;
 4978    }
 4979  	
 4980  	$locked_time = $time;
 4981  	// check closed
 4982  	if( $resto ) {
 4983  		if( !$cchk = mysql_board_call( "select closed,sticky,undead,archived,sub,com from `" . SQLLOG . "` where no=" . $resto ) ) {
 4984  			echo S_SQLFAIL;
 4985  		}
 4986  		list( $closed, $sticky, $undead, $is_archived, $_thread_sub, $_thread_com ) = mysql_fetch_row( $cchk );
 4987  		if ($is_archived) {
 4988  		  error(S_MAYNOTREPLY, $upfile);
 4989  		}
 4990      	$is_undead_sticky = $sticky == 1 && $undead == 1;
 4991  		if( $closed == 1 && !has_level() ) error( S_MAYNOTREPLY, $upfile );
 4992  		mysql_free_result( $cchk );
 4993  		
 4994  		$sub = '';
 4995  	}
 4996  	else {
 4997      $is_undead_sticky = false;
 4998  	}
 4999    
 5000  	$has_image = $upfile && file_exists( $upfile );
 5001  
 5002    $md5 = null;
 5003    $original_md5 = null; // MD5 before exif and other metadata stripping
 5004  
 5005  	if( $has_image ) {
 5006  		if (UPLOAD_BOARD) {			
 5007  			if( file_exists( IMG_DIR . $upfile_name ) ) error( "Filename already exists.", $upfile );
 5008  
 5009  			$dest = $upfile;
 5010  
 5011  			$upfile_name = sanitize_text( $upfile_name );
 5012  			if( !is_file( $dest ) ) error( S_FAILEDUPLOAD, $dest );
 5013  			if ($upfile_name[0] === '.') {
 5014  				error('Error: Invalid filename (first character cannot be a period).', $dest);
 5015  			}
 5016  			$size = getimagesize( $dest );
 5017  			if( !is_array( $size ) ) error( S_NOREC, $dest );
 5018  
 5019  			$W     = $size[0];
 5020  			$H     = $size[1];
 5021  			$fsize = filesize( $dest );
 5022  			if( $fsize > MAX_KB * 1024 ) error( S_TOOLARGE, $dest );
 5023  			if( $size[2] == 6 || $size[2] == 5 ) {
 5024  				error( S_FAILEDUPLOAD, $dest );
 5025  			}
 5026  			switch( $size[2] ) {
 5027  			case 4 :
 5028  			case 13 :
 5029  				$ext = ".swf";
 5030  			break;
 5031  			default :
 5032  				$ext = ".xxx";
 5033  				error( S_FAILEDUPLOAD, $dest );
 5034  			break;
 5035  			}
 5036  			
 5037  			time_log( "sfpi" );
 5038  			rpc_task();
 5039  			
 5040  			$len = strlen( $ext );
 5041  		}
 5042      else {
 5043  			// NOT upload board
 5044  			
 5045  			// check image limit
 5046  			if( $resto && !$sticky && !$undead && !has_level() ) {
 5047  				if( !$result = mysql_board_call( "SELECT COUNT(*) FROM `" . SQLLOG . "` WHERE archived = 0 AND resto=$resto AND fsize!=0 AND filedeleted=0" ) ) {
 5048  					echo S_SQLFAIL;
 5049  				}
 5050  				$countimgres = mysql_result( $result, 0, 0 );
 5051  				if( $countimgres >= MAX_IMGRES && !has_level() ) error(S_MAXIMAGESREACHED, $upfile );
 5052  				mysql_free_result( $result );
 5053  			}
 5054  
 5055  			//upload processing
 5056  			$dest = $upfile;
 5057        
 5058  			// TODO: what does that preg_replace do? those are probably utf8 codes
 5059  			$upfile_name = sanitize_text( preg_replace('/\xe2\x80(\xae|\xad|\x8f|\x8e)/', '', $upfile_name) );
 5060        
 5061        if (!is_file($dest)) {
 5062          error(S_FAILEDUPLOAD, $dest);
 5063        }
 5064        
 5065        // Use filesize() later as it's possible to trick jpegtrans to generate a much bigger file
 5066        $fsize = $_FILES['upfile']['size'];
 5067        
 5068        if (!$fsize || $fsize > MAX_KB * 1024) {
 5069          error( S_TOOLARGE, $dest );
 5070        }
 5071        
 5072  			$webm_sar = null;
 5073  
 5074  			// PDF processing
 5075  			if( ENABLE_PDF == 1 && strcasecmp( '.pdf', substr( $upfile_name, -4 ) ) == 0 ) {
 5076  				$ext = '.pdf';
 5077  				$W   = $H = 1;
 5078  				// run through ghostscript to check for validity
 5079  				if( pclose( popen( "/usr/local/bin/gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=nullpage $dest", 'w' ) ) ) {
 5080  					error( S_FAILEDUPLOAD, $dest );
 5081  				}
 5082  			}
 5083  			// Webm / MP4
 5084  			else if (ENABLE_WEBM && preg_match('/\.(webm|mp4)$/i', $upfile_name)) {
 5085  				if ($fsize > MAX_WEBM_FILESIZE * 1024) {
 5086  					error(S_TOOLARGE, $dest);
 5087  				}
 5088  				
 5089  				$original_md5 = md5_file($dest);
 5090  				
 5091  				$ext = '.' . strtolower(pathinfo($upfile_name, PATHINFO_EXTENSION));
 5092  
 5093  				//if ($ext == '.mp4' && BOARD_DIR != 'test') {
 5094  				//	error(S_NOREC, $dest);
 5095  				//}
 5096  
 5097  				$size = validate_webm($dest, $ext);
 5098  				$W = $size[0];
 5099  				$H = $size[1];
 5100  				$webm_sar = $size[2];
 5101  			}
 5102  			// PNG, GIF, JPEG
 5103  			else {
 5104  				$size = getimagesize( $dest );
 5105  				if( !is_array( $size ) ) {
 5106  					quick_log_to( "/www/perhost/bad-upload.log", "unrecognized file $upfile_name");
 5107  
 5108  					error( S_NOREC, $dest );
 5109  				}
 5110  
 5111  				$W = $size[0];
 5112  				$H = $size[1];
 5113  				switch( $size[2] ) {
 5114  				case 1 :
 5115  				$ext = ".gif";
 5116  				break;
 5117  				case 2 :
 5118  				$ext = ".jpg";
 5119  				break;
 5120  				case 3 :
 5121  				$ext = ".png";
 5122  				break;
 5123  				case 4 :
 5124  				$ext = ".swf";
 5125  				error( S_FAILEDUPLOAD, $dest );
 5126  				break;
 5127  				case 5 :
 5128  				$ext = ".psd";
 5129  				error( S_FAILEDUPLOAD, $dest );
 5130  				break;
 5131  				case 6 :
 5132  				$ext = ".bmp";
 5133  				error( S_FAILEDUPLOAD, $dest );
 5134  				break;
 5135  				case 7 :
 5136  				$ext = ".tiff";
 5137  				error( S_FAILEDUPLOAD, $dest );
 5138  				break;
 5139  				case 8 :
 5140  				$ext = ".tiff";
 5141  				error( S_FAILEDUPLOAD, $dest );
 5142  				break;
 5143  				case 9 :
 5144  				$ext = ".jpc";
 5145  				error( S_FAILEDUPLOAD, $dest );
 5146  				break;
 5147  				case 10 :
 5148  				$ext = ".jp2";
 5149  				error( S_FAILEDUPLOAD, $dest );
 5150  				break;
 5151  				case 11 :
 5152  				$ext = ".jpx";
 5153  				error( S_FAILEDUPLOAD, $dest );
 5154  				break;
 5155  				case 13 :
 5156  				$ext = ".swf";
 5157  				error( S_FAILEDUPLOAD, $dest );
 5158  				break;
 5159  				default :
 5160  				$ext = ".xxx";
 5161  				error( S_FAILEDUPLOAD, $dest );
 5162  				break;
 5163  				}
 5164  				if (GIF_ONLY == 1 && $size[2] != 1 && $ext != '.webm') error(S_FAILEDUPLOAD, $dest);
 5165  			} // end PDF processing -else
 5166  
 5167  			// This doesn't seem to use the $md5 arg
 5168  			if ($upfile_name === '') {
 5169  				error('Blank file names are not supported.', $dest);
 5170  			}
 5171  			
 5172  			time_log( "sfpi" );
 5173  			rpc_task();
 5174  
 5175  			// Picture reduction
 5176  			if( !$resto ) {
 5177  				$maxw = MAX_W;
 5178  				$maxh = MAX_H;
 5179  			} else {
 5180  				$maxw = MAXR_W;
 5181  				$maxh = MAXR_H;
 5182  			}
 5183  			if( defined( 'MIN_W' ) && MIN_W > $W ) error( S_TOOSMALL, $dest );
 5184  			if( defined( 'MIN_H' ) && MIN_H > $H ) error( S_TOOSMALL, $dest );
 5185  			if( defined( 'MAX_DIMENSION' ) )
 5186  				$maxdimension = MAX_DIMENSION;
 5187  			else
 5188  				$maxdimension = 5000;
 5189  			if( $W > $maxdimension || $H > $maxdimension ) {
 5190  				error( S_TOOLARGERES, $dest );
 5191  			} elseif( $W > $maxw || $H > $maxh ) {
 5192  				$W2 = $maxw / $W;
 5193  				$H2 = $maxh / $H;
 5194  				( $W2 < $H2 ) ? $key = $W2 : $key = $H2;
 5195  				$TN_W = ceil( $W * $key ) + 1;
 5196  				$TN_H = ceil( $H * $key ) + 1;
 5197  			}
 5198        
 5199        // Strip metadata, exif, comments and other embeddded extra data
 5200        if ($ext === '.jpg') {
 5201          // Embed detection is done later below if STRIP_EXIF is disabled
 5202          if (STRIP_EXIF) {
 5203            $original_md5 = md5_file($dest);
 5204            
 5205            if (strip_jpeg_exif($dest) === false) {
 5206              error(S_FAILEDUPLOAD, $dest);
 5207            }
 5208            
 5209            clearstatcache(true, $dest);
 5210          }
 5211        }
 5212        else if ($ext === '.png') {
 5213          $original_md5 = md5_file($dest);
 5214          $_ret = strip_png_chunks($dest, MAX_KB * 1024);
 5215          if ($_ret < 0) {
 5216            if ($_ret === -2) {
 5217              error('APNG format not supported.', $dest);
 5218            }
 5219            else {
 5220              error(S_FAILEDUPLOAD, $dest);
 5221            }
 5222          }
 5223          else if ($_ret > 0) {
 5224            clearstatcache(true, $dest);
 5225          }
 5226        }
 5227        else if ($ext === '.gif') {
 5228          $original_md5 = md5_file($dest);
 5229          $_ret = strip_gif_extra_data($dest, $fsize);
 5230          if ($_ret < 0) {
 5231            error(S_FAILEDUPLOAD, $dest);
 5232          }
 5233          clearstatcache(true, $dest);
 5234        }
 5235        
 5236        // It should be safe to check the filesize now
 5237        // clearstatcache() must be called if the file was modified
 5238        $fsize = filesize($dest);
 5239        
 5240        if (!$fsize) {
 5241          error(S_TOOLARGE, $dest);
 5242        }
 5243        else if ($ext === '.webm' && $fsize > MAX_WEBM_FILESIZE * 1024) {
 5244          error(S_TOOLARGE, $dest);
 5245        }
 5246        else if ($fsize > MAX_KB * 1024) {
 5247          error(S_TOOLARGE, $dest);
 5248        }
 5249        
 5250        // Check for JPEG embedded data. jpegtran seems to remove unknown data
 5251        // so this only needs to be done if STRIP_EXIF is disabled
 5252        if (!STRIP_EXIF && $ext === '.jpg' && $fsize > 204800) {
 5253          validate_jpeg_size($dest, $fsize);
 5254        }
 5255  		}
 5256  		
 5257  		$insfile = preg_replace('/\.[a-z0-9]+$/i', '', $upfile_name);
 5258  
 5259  		$md5 = md5_file( $dest );
 5260  		$mes = $upfile_name . ' ' . S_UPGOOD;
 5261  	}
 5262  
 5263  	if( $_FILES["upfile"]["error"] > 0 ) {
 5264  		if( $_FILES["upfile"]["error"] == UPLOAD_ERR_INI_SIZE )
 5265  			error( S_TOOLARGE, $dest );
 5266  		if( $_FILES["upfile"]["error"] == UPLOAD_ERR_FORM_SIZE )
 5267  			error( S_TOOLARGE, $dest );
 5268  		if( $_FILES["upfile"]["error"] == UPLOAD_ERR_PARTIAL )
 5269  			error( S_FAILEDUPLOAD, $dest );
 5270  		if( $_FILES["upfile"]["error"] == UPLOAD_ERR_CANT_WRITE )
 5271  			error( S_FAILEDUPLOAD, $dest );
 5272  	}
 5273  
 5274  	if( $upfile_name && $_FILES["upfile"]["size"] == 0 ) {
 5275  		error( S_TOOLARGEORNONE, $dest );
 5276  	}
 5277  	
 5278  	if( ENABLE_EXIF == 1 ) {
 5279  		$exif = htmlspecialchars( shell_exec( "/usr/local/bin/exiftags $dest" ) );
 5280  	}
 5281  
 5282  	$resto = (int)$resto;
 5283  	if( $resto ) {
 5284  		if( !mysql_result( mysql_board_call( "select count(no) from `" . SQLLOG . "` where root>0 and no=$resto" ), 0, 0 ) )
 5285  			error( S_NOTHREADERR, $dest );
 5286  	}
 5287    
 5288    $pass_is_bannable = !$userpwd->isNew();
 5289    
 5290  	// Most common errors checked, now check for post-block from ban requests
 5291    if (BLOCK_ON_BR && !has_level()) {
 5292      check_for_ban_request($host, $pass_is_bannable ? $pass : null);
 5293    }
 5294    
 5295  	// Standardize new character lines
 5296  	$com = str_replace( "\r\n", "\n", $com );
 5297  	$com = str_replace( "\r", "\n", $com );
 5298  
 5299  	$comlim  = has_level() ? MAX_COM_CHARS_AUTHED : MAX_COM_CHARS;
 5300  	$longlim = has_level() ? 255 : 100;
 5301  
 5302  	if( mb_strlen( $com ) > $comlim ) error( S_TOOLONG, $dest );
 5303  	if( strlen( $name ) > $longlim ) error( S_TOOLONG, $dest );
 5304  	if( strlen( $email ) > $longlim ) error( S_TOOLONG, $dest );
 5305  	if( strlen( $sub ) > $longlim ) error( S_TOOLONG, $dest );
 5306  	if( strlen( $resto ) > 10 ) error( S_GENERICERROR, $dest );
 5307  	if( strlen( $url ) > 10 ) error( S_GENERICERROR, $dest );
 5308  
 5309  	$sub = normalize_content( $sub );
 5310  
 5311  	// start of some attempt to get rid of *all* zero width bollocks
 5312  	if( BOARD_DIR != 'jp' && BOARD_DIR != 'a' && !SJIS_TAGS) {
 5313  		$com = normalize_content( $com );
 5314  		$com = strip_zerowidth( $com );
 5315  	}
 5316  	
 5317  	// strip no break spaces and soft hyphens
 5318  	$com = str_replace(array("\xC2\xAD", "\xC2\xA0"), '', $com);
 5319  	
 5320  	// name/subject too!
 5321  	$sub  = strip_zerowidth( $sub );
 5322  	$name = strip_zerowidth( $name );
 5323  	
 5324    // Strip unicode emoticons
 5325    $name = strip_emoticons($name, SJIS_TAGS);
 5326    
 5327    if ($sub !== '') {
 5328      $sub = strip_emoticons($sub, SJIS_TAGS);
 5329    }
 5330    
 5331    if ($com !== '') {
 5332      $com = strip_emoticons($com, SJIS_TAGS);
 5333    }
 5334  	
 5335  	// strip out ltr junk from name
 5336  	$name = preg_replace( '#([\x{2000}-\x{200F}]|[\x{2028}-\x{202F}])#u', '', $name );
 5337  	
 5338    if ($sub !== '') {
 5339      $sub = strip_fake_capcodes($sub);
 5340    }
 5341    
 5342  	if( !strlen( $name ) || preg_match( "/^[ | |]*$/", $name ) ) $name = "";
 5343  	if( !strlen( $com ) || preg_match( "/^[ | |\t]*$/", $com ) ) $com = "";
 5344  	if( !strlen( $sub ) || preg_match( "/^[ | |]*$/", $sub ) ) $sub = "";
 5345  
 5346  	//$name = str_replace( S_MANAGEMENT, "\"" . S_MANAGEMENT . "\"", $name );
 5347  	//$name = str_replace( S_DELETION, "\"" . S_DELETION . "\"", $name );
 5348    
 5349    // Remove intra spoilers
 5350  	if (SPOILERS && stripos($com, '[spoiler]') !== false) {
 5351  		//$com = preg_replace( '/\[spoiler\]\s+\[\/spoiler\]/', '', $com );
 5352  		$com = preg_replace('/(\S)\[spoiler\](.*?)\[\/spoiler\](\S)/', '\\1\\2\\3', $com);
 5353  	}
 5354    
 5355  	//lol /b/
 5356  	$xff = get_request_xff();
 5357  	//if( is_bad_xff( $xff ) ) $xff = "";
 5358  
 5359  	$youbi  = array(S_SUN, S_MON, S_TUE, S_WED, S_THU, S_FRI, S_SAT);
 5360  	$yd     = $youbi[date( "w", $time )];
 5361  	if( SHOW_SECONDS == 1 ) {
 5362  		$now = date( "m/d/y", $time ) . "(" . (string)$yd . ")" . date( "H:i:s", $time );
 5363  	} else {
 5364  		$now = date( "m/d/y", $time ) . "(" . (string)$yd . ")" . date( "H:i", $time );
 5365  	}
 5366  
 5367  	$c_name  = $name;
 5368  	$c_email = $email;
 5369  
 5370  	if (JANITOR_BOARD == 1) {
 5371  		$name = get_hashed_mod_name($_COOKIE['4chan_auser']);
 5372  		$email = '';
 5373  	}
 5374  
 5375    // April 2023
 5376    //$_has_xa23_content = $com && preg_match('/^[^>]{8,}/m', $com) > 0;
 5377  
 5378  	$com = preg_replace( '#>>>/' . BOARD_DIR . '/([0-9]+)#', '>>$1', $com );
 5379  
 5380  	$sub   = sanitize_text( $sub );
 5381  	$sub   = preg_replace( "/[\r\n]/", "", $sub );
 5382  	$sub   = strip_private_unicode($sub);
 5383  	$url   = sanitize_text( $url );
 5384  	$url   = preg_replace( "/[\r\n]/", "", $url );
 5385  	$resto = sanitize_text( $resto );
 5386  	$resto = preg_replace( "/[\r\n]/", "", $resto );
 5387  	$com   = sanitize_text( $com, 1, true );
 5388  	$com   = strip_private_unicode($com);
 5389  	
 5390  	if( FORCED_ANON == 1 ) {
 5391  		if( !has_level('admin') ) $name  = S_ANONAME;
 5392  		$sub = '';
 5393  	}
 5394  	
 5395  	if (UPLOAD_BOARD) {
 5396  		if( NO_TEXTONLY == 1 ) {
 5397  			if( !$resto && !$has_image ) error( S_NOPIC, $dest );
 5398  		} else {
 5399  			if( !$resto && !$textonly && !$has_image ) error( S_NOPIC, $dest );
 5400  		}
 5401  	} else {
 5402  	  if (!TEXT_ONLY) {
 5403    		if( NO_TEXTONLY == 1 && (!has_level() || $email === '') ) {
 5404    			if( !$resto && !$has_image ) error( S_NOPIC, $dest );
 5405    		} else {
 5406    			if( !$resto && !$textonly && !$has_image ) error( S_NOPIC, $dest );
 5407    		}
 5408  	  }
 5409  
 5410  		if( REQUIRE_SUBJECT && !$resto && !strlen( $sub ) ) error( S_NOSUB, $dest );
 5411    }
 5412    // Check for sage, nonoko and nonokosage
 5413    $is_sage = false;
 5414    
 5415    if (stripos($email, 'sage') !== false) {
 5416      $is_sage = true;
 5417      $email = str_ireplace('sage', '', $email);
 5418    }
 5419    
 5420    $email_lower = strtolower($email);
 5421    
 5422    if ($email_lower === 'nonoko') {
 5423      $is_nonoko = true;
 5424    }
 5425    
 5426  	if( SPOILERS == 1 && $spoiler ) {
 5427  		$sub = "SPOILER<>$sub";
 5428  	}
 5429  
 5430  	if( !has_level() ) {
 5431  		$match = array();
 5432  		if( substr_count( $com, "\n" ) > 6 ) preg_match_all( '#([^\n]+\n+)\1{5,}#', $com, $match );
 5433  		if( !empty( $match[0] ) ) {
 5434  			foreach( $match[1] as $key => $var ) {
 5435  				//auto_ban_poster( $name, 0, 1, 'Matched the same string 5 times or more in 1 post seperated by newlines (<b>Matched:</b> ' . $var . ')', 'Please do not spam.' );
 5436  				error( S_REJECTTEXTBAN );
 5437  			}
 5438  		}
 5439  	}
 5440  
 5441  	// FIXME sanitize_text() replaces repeated \n too, is this duplicate?
 5442  	// disable on code tag boards (we replace multiple brs instead)
 5443  	if (!CODE_TAGS && !SJIS_TAGS) {
 5444  		$com = preg_replace( "/\n(( | )*\n){3,}/", "\n", $com );
 5445  	}
 5446  
 5447  	if( !has_level() && substr_count( $com, "\n" ) > MAX_LINES ) error(S_TOOMANYLINES, $dest );
 5448  
 5449  	if( ENABLE_EXIF == 1 && $exif ) {
 5450  		//turn exif into a table
 5451  		$exiflines = explode( "\n", $exif );
 5452  		$exif      = "<table class=\"exif\" id=\"exif$tim\">";
 5453  		foreach( $exiflines as $exifline ) {
 5454  			list( $exiftag, $exifvalue ) = explode( ': ', $exifline );
 5455  			if( $exifvalue != '' )
 5456  				$exif .= "<tr><td>$exiftag</td><td>$exifvalue</td></tr>";
 5457  			else
 5458  				$exif .= "<tr><td colspan=\"2\"><b>$exiftag</b></td></tr>";
 5459  		}
 5460  		$exif .= '</table>';
 5461  		$exiftext .= '<br><br><span class="abbr">' . sprintf(S_EXIF_TOGGLE, $tim) . '</span><br>';
 5462  		$exiftext .= "$exif";
 5463  	}
 5464  
 5465  	$name  = preg_replace( "/[\r\n]/", "", $name );
 5466  	
 5467  	$names = mb_convert_encoding($name, 'CP932', 'UTF-8'); // convert to Windows Japanese #kami
 5468  	
 5469  	//start new tripcode crap
 5470  	list ( $name ) = explode( "#", $name );
 5471  	
 5472  	// Strip unicode point-of-interest and # characters
 5473    if (preg_match('/[\x{2318}\x{ff03}\x{FE5F}]/u', $name)) {
 5474      $name = preg_replace('/[\x{2318}\x{ff03}\x{FE5F}]/u', '', $name);
 5475    }
 5476  	
 5477  	$name = normalize_content( $name );
 5478  	$name = sanitize_text( $name );
 5479  	$name = strip_private_unicode($name);
 5480    
 5481    if (preg_match('/^\s+$/', $name)) {
 5482      $name = '';
 5483    }
 5484    
 5485  	$name = str_replace('!', '', $name);
 5486  	
 5487  	if( preg_match( "/\#+$/", $names ) ) {
 5488  		$names = preg_replace( "/\#+$/", "", $names );
 5489  	}
 5490  	if( preg_match( "/\#/", $names ) ) {
 5491  
 5492  		$names = str_replace( "&#", "&&", htmlspecialchars( $names, ENT_COMPAT | ENT_HTML401, 'Shift_JIS' ) ); # otherwise HTML numeric entities screw up explode()!
 5493  
 5494  		list ( $nametemp, $trip, $sectrip ) = str_replace( "&&", "&#", explode( "#", $names, 3 ) );
 5495  		
 5496  		if ($sectrip != '') {
 5497  			$trip = '';
 5498  		}
 5499  		
 5500  		$names = $nametemp;
 5501  		if( STRIP_TRIPCODE == 0 ) $name .= "</span>";
 5502  
 5503  		if ($trip != "" && STRIP_TRIPCODE == 0) {
 5504  			$salt = strtr( preg_replace( "/[^\.-z]/", ".", substr( $trip . "H.", 1, 2 ) ), ":;<=>?@[\\]^_`", "ABCDEFGabcdef" );
 5505  			$trip = substr( crypt( $trip, $salt ), -10 );
 5506  			$name .= " <span class=\"postertrip\">!" . $trip;
 5507  		}
 5508  
 5509  		if( $sectrip != "" && STRIP_TRIPCODE == 0 ) {
 5510  			$salt = file_get_contents_cached( SALTFILE );
 5511  			$sha  = base64_encode( pack( "H*", sha1( $sectrip . $salt ) ) );
 5512  			$sha  = substr( $sha, 0, 11 );
 5513  			$name .= " <span class=\"postertrip\">!!" . $sha;
 5514  		}
 5515  	} //end new tripcode crap
 5516    
 5517    // Check the length of the name field again to prevent truncation in the middle of tripcode HTML
 5518    if (strlen($name) > 255) {
 5519      error(S_TOOLONG, $dest);
 5520    }
 5521    
 5522  	//Cookies
 5523  	$cookie_domain = '.' . L::d(BOARD_DIR);
 5524  	setrawcookie( "4chan_name", rawurlencode( $c_name ), $time + ( $c_name ? ( 7 * 24 * 3600 ) : -3600 ), '/', $cookie_domain );
 5525    
 5526  	if( !strlen( $name ) ) $name = S_ANONAME;
 5527  	if( !strlen( $com ) ) $com = S_ANOTEXT;
 5528  	if( !strlen( $sub ) ) $sub = S_ANOTITLE;
 5529    
 5530  	/* since4pass */
 5531    if ($captcha_bypass && $passid && $email && strpos(" $email ", ' since4pass ') !== false) {
 5532      $since4pass = get_since_4chan($passid);
 5533    }
 5534    else {
 5535  	  $since4pass = 0;
 5536    }
 5537  	
 5538    // April 2024
 5539    /*
 5540    if ($email) {
 5541      $_xa24_since4pass = april_2024_parse_email($email);
 5542      $since4pass = $_xa24_since4pass;
 5543    }
 5544    else {
 5545      $_xa24_since4pass = false;
 5546    }
 5547    */
 5548    if (FORTUNE_TRIP == 1 && $email == 'fortune') {
 5549  		$fortunes   = array("Bad Luck", "Average Luck", "Good Luck", "Excellent Luck", "Reply hazy, try again", "Godly Luck", "Very Bad Luck", "Outlook good", "Better not tell you now", "You will meet a dark handsome stranger", "キタ━━━━━━(゚∀゚)━━━━━━ !!!!", "( ´_ゝ`)フーン ", "Good news will come to you by mail");
 5550      // Christmas 2021
 5551      /*
 5552      $fortunes = array("You're on the nice list!", "You're on the naughty list!", "Krampus is coming to your house!", "It's going to be a white Christmas!", "Merry Christmas!", "Happy Hanukkah!", "Happy Kwanzaa!", "Feliz Navidad!", "Happy Festivus!", "You're getting a lump of coal in your stocking!", "Blessed Yule!", "You're standing under the mistletoe!", "The poster above you has been very very naughty!", "You got an extra thick slice of fruitcake.", "You're on the Elf Watchlist.", "Your heart is two sizes too small.", "Your heart grew three sizes!", "Bah! Humbug.");
 5553      */
 5554  		$fortunenum = rand( 0, sizeof( $fortunes ) - 1 );
 5555  		$fortcol    = "#" . sprintf( "%02x%02x%02x",
 5556  			127 + 127 * sin( 2 * M_PI * $fortunenum / sizeof( $fortunes ) ),
 5557  			127 + 127 * sin( 2 * M_PI * $fortunenum / sizeof( $fortunes ) + 2 / 3 * M_PI ),
 5558  			127 + 127 * sin( 2 * M_PI * $fortunenum / sizeof( $fortunes ) + 4 / 3 * M_PI ) );
 5559  		$com        .= "<span class=\"fortune\" style=\"color:$fortcol\"><br><br><b>Your fortune: " . $fortunes[$fortunenum] . "</b></span>";
 5560    }
 5561    
 5562  	if (DICE_ROLL == 1) {
 5563  		if( $email ) {
 5564  			if( preg_match( "/dice[ +](\\d+)[ d+](\\d+)(([ +-]+?)(-?\\d+))?/", $email, $match ) ) {
 5565  				$dicetxt     = S_DICE_PFX . ' ';
 5566  				$dicenum     = min( 25, $match[1] );
 5567  				$diceside    = $match[2];
 5568  				$diceaddexpr = $match[3];
 5569  				$dicesign    = $match[4];
 5570  				$diceadd     = intval( $match[5] );
 5571  
 5572  				for( $i = 0; $i < $dicenum; $i++ ) {
 5573  					$dicerand = mt_rand( 1, $diceside );
 5574  					if( $i ) $dicetxt .= ", ";
 5575  					$dicetxt .= $dicerand;
 5576  					$dicesum += $dicerand;
 5577  				}
 5578  
 5579  				if( $diceaddexpr ) {
 5580  					if( strpos( $dicesign, "-" ) > 0 ) $diceadd *= -1;
 5581  					$diceadd_formatted = ( $diceadd >= 0 ? " + " : " - " ) . abs( $diceadd );
 5582  					$dicetxt .= $diceadd_formatted;
 5583  					$dicesum += $diceadd;
 5584  				}
 5585  
 5586  				if ($dicenum > 1) {
 5587  				  $dicetxt .= " = $dicesum";
 5588  			  }
 5589  			  
 5590  			  $dicetxt .= " ({$dicenum}d{$diceside}" . ($diceaddexpr ? $diceadd_formatted : "") . ")<br><br>";
 5591  			  
 5592  				$com = "<b>$dicetxt</b>" . $com;
 5593  			}
 5594  		}
 5595  	}
 5596  	
 5597  	// fixme: this is needed for bypassing the r9k filter. $email gets reset below.
 5598  	$options_field = $email;
 5599  	
 5600  	$emails          = $email;
 5601  	$admin_highlight = false;
 5602  	$uid             = null;
 5603  	$capcode         = 'none';
 5604  	$delay_refresh   = false;
 5605  	if (strpos($email, 'capcode_') === 0) {
 5606  		// are we trying to capcode?
 5607      if (!has_level('admin') && !has_flag('capcodename')) {
 5608        $name = S_ANONAME;
 5609      }
 5610  		// Only pass and authed users can use VIP capcodes
 5611  		if ($captcha_bypass) {
 5612        $capcode = parse_capcode($email, $name);
 5613  		}
 5614  		else {
 5615        $capcode = parse_capcode($email);
 5616  		}
 5617  		
 5618  		$ma      = ( $capcode == 'admin_highlight') ? 'admin' : $capcode;
 5619  		$ma      = ucfirst( $ma );
 5620  
 5621  		if( DISP_ID == 1 && $ma != 'None' ) {
 5622  			$uid = $ma;
 5623  		}
 5624  
 5625  		if( $ma != 'None' && STRIP_EXIF_ON_CAPCODE && $ext == ".jpg" ) {
 5626  			system( "/usr/local/bin/jpegtran -copy none -outfile '$dest' '$dest'" );
 5627  			$md5 = md5_file( $dest );
 5628  		}
 5629  		
 5630      if ($capcode !== 'none') {
 5631        $log_mod_action = $log_capcode_post = true;
 5632      }
 5633  		
 5634  		setcookie('options', $email, $time + (7 * 24 * 3600), '/', $cookie_domain);
 5635  	}
 5636  	else if (isset($_COOKIE['options'])) {
 5637  		setcookie('options', null, $time -3600, '/', $cookie_domain);
 5638  	}
 5639    
 5640  	$email = '';
 5641  
 5642  	$nameparts = explode( '</span> <span class="postertrip">!', $name );
 5643  
 5644  	time_log( "trip" );
 5645    
 5646  	//logtime( "starting autoban checks" );
 5647  	
 5648  	/**
 5649  	 * Ban check step
 5650  	 */
 5651  	$ban_fields = array();
 5652  	
 5653  	if ($pass_is_bannable) {
 5654  	  $ban_fields['password'] = $pass;
 5655  	}
 5656  	/*
 5657  	if ($nameparts[1]) {
 5658  	  $ban_fields['tripcode'] = $nameparts[1];
 5659  	}
 5660  	*/
 5661  	if ($passid) {
 5662  	  $ban_fields['4pass_id'] = $passid;
 5663  	}
 5664  	
 5665  	$user_is_banned = check_for_ban($host, $ban_fields, $resto, $userpwd && $userpwd->verifiedLevel());
 5666  	
 5667    if ($user_is_banned) {
 5668      if (!$captcha_bypass) {
 5669        mysql_global_call("INSERT INTO user_actions (board,ip,time,action) VALUES ('%s',%d,from_unixtime(%d),'%s')", BOARD_DIR, ip2long($host), $time, 'is_banned');
 5670      }
 5671      
 5672      $redirect = 'https://www.' . L::d(BOARD_DIR) . '/banned';
 5673      
 5674      if ($user_is_banned == 1) {
 5675        // Banned
 5676        error_redirect(S_BANNED, $redirect);
 5677      }
 5678      else if ($user_is_banned == 2) {
 5679        // Warned
 5680        error_redirect(S_WARNED, $redirect);
 5681      }
 5682      else {
 5683        // Ban evasion
 5684        error("Error: $user_is_banned");
 5685      }
 5686    }
 5687    
 5688    /**
 5689     * Validate the maximum number of allowed threads per user
 5690     */
 5691    if (!$resto) {
 5692      validate_user_thread_limit(
 5693        $_SERVER['REMOTE_ADDR'],
 5694        isset($ban_fields['password']) ? $pass : null,
 5695        $passid
 5696      );
 5697    }
 5698    
 5699    /*
 5700     * Embedded data detection and banned re-post block
 5701     */
 5702    if ($has_image) {
 5703      // Check if the file contains embedded data.
 5704      if (false && CLEANUP_UPLOADS) {
 5705        // Update the size if the file was modified
 5706        if (cleanup_uploaded_file($dest, $ext)) {
 5707          clearstatcache(true, $dest);
 5708          $fsize = filesize($dest);
 5709          if (!$fsize) {
 5710            error(S_TOOLARGE, $dest);
 5711          }
 5712          $md5 = md5_file($dest);
 5713        }
 5714      }
 5715      
 5716      // Check if the uploaded file should be blocked because of previous bans
 5717      if (check_for_banned_upload($md5)) {
 5718        //log_spam_filter_trigger('block_banned_reupload', BOARD_DIR, $resto, $host, 1, $md5);
 5719        error(S_FAILEDUPLOAD, $dest);
 5720      }
 5721    }
 5722    
 5723  	// ---
 5724  	
 5725  	$autosage = false;
 5726    
 5727    // See bellow wordwrap2()
 5728    if (strpos($com, 'rep?~') !== false) {
 5729      $com = str_replace(array('~?rep?~', '~?erep?~'), '', $com);
 5730    }
 5731    
 5732    if (!has_level() || $capcode === 'none' || BOARD_DIR == 'test') {
 5733      $autosage = spam_filter_post_content_new(BOARD_DIR, $resto, $com, $sub, $name, $upfile_name, $pass, ($captcha_bypass && $passid) ? $passid : null);
 5734      spam_filter_post_ip($userpwd, $resto, $has_image);
 5735    }
 5736    
 5737    if (!$capcode || $capcode == 'none') {
 5738      check_md5_blacklist($md5, $original_md5, [
 5739        'resto' => $resto,
 5740        'name' => $nameparts[0],
 5741        'tripcode' => $nameparts[1],
 5742        'filename' => "$insfile$ext",
 5743        'password' => $pass,
 5744        '4pass_id' => ($captcha_bypass && $passid) ? $passid : null
 5745      ], $dest);
 5746    }
 5747    
 5748  	spam_filter_post_trip( $name, $trip, $dest );
 5749  
 5750  	time_log( "ab" );
 5751    
 5752    // Only process linebreaks for non-html posts
 5753    if (!$log_html_post) {
 5754      $com = nl2br($com, false);
 5755      $com = str_replace("\n", "", $com); //\n is erased
 5756    }
 5757    
 5758  	$com .= $exiftext; // must be done after spam filter, since it has a javascript: link
 5759  	
 5760    if (SJIS_TAGS) {
 5761      $com = sjis_parse($com);
 5762    }
 5763    
 5764  	if( SPOILERS == 1 ) {
 5765  		$com = spoiler_parse( $com );
 5766  		if (stripos( $com, '<s>') !== false) {
 5767  			$com = preg_replace('/<s>(\s|<br>|(?R))*<\/s>/', '', $com);
 5768  			//$com = preg_replace('/(\S)<s>(.*?)<\/s>(\S)/', '\\1\\2\\3', $com);
 5769  		}
 5770  	}
 5771    /*
 5772  	if( JSMATH == 1 ) {
 5773  		$com = jsmath_parse( $com );
 5774  	}
 5775    */
 5776  	if( CODE_TAGS ) {
 5777  		$com = preg_replace( '#(\[code\](.{0,6})\[\/code\])#', '\\2', $com );
 5778  
 5779  		$com = code_parse( $com );
 5780  
 5781  		$com = str_replace( '<pre class="prettyprint"><br>', '<pre class="prettyprint">', $com );
 5782  		$com = preg_replace( '#(<br>){4,}#', '<br><br><br>', $com );
 5783  	}
 5784  	
 5785    if (OP_MARKUP && (!$resto || is_poster_op($host, $pass, $resto))) {
 5786      $com = parse_op_markup($com);
 5787    }
 5788  	
 5789  	// pull this down to here to get rid of any shenanigans
 5790    if (!$resto) { // new threads require subject or comment
 5791      if ($sub === '' && ($com === '' || preg_match('/^(?:<br>|\s)+$/', $com))) {
 5792        if ($options_field === '' || !$has_image || !has_level()) {
 5793          error(S_NOTEXT_OP, $dest);
 5794        }
 5795      }
 5796      else if (TEXT_ONLY && $sub === '') {
 5797        error(S_NOSUB, $dest);
 5798      }
 5799    }
 5800    else if (!$has_image && ($com === '' || preg_match('/^(?:<br>|\s)+$/', $com))) { // replies without image
 5801      error(S_NOTEXT, $dest);
 5802    }
 5803    
 5804  	if( WORD_FILT && $word_filters_enabled ) {
 5805  		$com = word_filter( $com, "com" );
 5806  		if( $sub )
 5807  			$sub = word_filter( $sub, "sub" );
 5808  		$namearr = explode( '</span> <span class="postertrip">', $name );
 5809  		if( strstr( $name, '</span> <span class="postertrip">' ) ) {
 5810  			$nametrip = '</span> <span class="postertrip">' . $namearr[1];
 5811  		} else {
 5812  			$nametrip = "";
 5813  		}
 5814  		if( $namearr[0] != S_ANONAME )
 5815  			$name = word_filter( $namearr[0], "name" ) . $nametrip;
 5816  	}
 5817  
 5818  	/*if( $html != 1 || ( !has_level('manager') ) ) {
 5819  		$com = wordwrap2( $com, 35, "{{w_br}}" );
 5820  	}*/
 5821    
 5822    // April 2022
 5823    /*
 5824    if (strpos($com, ':') !== false) {
 5825      $com = april_process_post_emotes($com, $_COOKIE['xa_sid']);
 5826    }
 5827    */
 5828  	$com = normalize_and_linkify($com);
 5829  	
 5830  	//$html = isset( $_POST['html'] ) && $_POST['html'] == 1;
 5831  	$html = 0;
 5832  	if( (!has_level('manager') && !has_flag('html')) || $html != 1 ) {
 5833  		$com = wordwrap2( $com, 35, "{{w_br}}" );
 5834  	}
 5835    
 5836  	$com = preg_replace( '#(&gt;&gt;&gt;/[a-z0-9]+/[^ <$]*|&gt;&gt;[0-9]+)#', '~?rep?~\\1~?erep?~', $com );
 5837  	$com = preg_replace( "!(^|r>|r> )(&gt;[^<]*)!", "\\1<span class=\"quote\">\\2</span>", $com );
 5838  	$com = preg_replace( '#~?rep?~<span class="quote">(.+?)</span>~?erep?~#', '\\1', $com );
 5839  	
 5840  	$com = str_replace( array( '~?rep?~', '~?erep?~' ), '', $com );
 5841  
 5842  	$com = str_replace( '{{w_br}}', '<wbr>', $com );
 5843  
 5844  	$admin_style = "padding: 5px;margin-left: .5em;border-color: #faa;border: 2px dashed rgba(255,0,0,.1);border-radius: 2px"; //FIXME make it easier to edit css so this can go in it
 5845  	if( $admin_highlight ) {
 5846  		$com = "<div style=\"$admin_style\">$com</div>";
 5847  	}
 5848  
 5849  	if( DISP_ID == 1 && !$uid ) {
 5850  		$uid = $is_sage && !META_BOARD && !DISP_ID_NO_HEAVEN ? "Heaven" : generate_uid( $resto, $time );
 5851  	}
 5852  	
 5853  	// Validate comment for OPs by regex
 5854  	if (!$resto && COM_REGEX) {
 5855  	  if (preg_match(COM_REGEX, $com) === 0) {
 5856  	    error(S_INVALID_COM);
 5857  	  }
 5858  	}
 5859  	
 5860  	//post text is now completely created, thumbnail not
 5861  
 5862  	if( !$silent_reject ) {
 5863  		//logtime( "Before flood check" );
 5864  		$may_flood = has_level( 'janitor' );
 5865  
 5866  		if (!$may_flood || (!has_level() && (META_BOARD || $_POST['name'] != ''))) {
 5867  			if( $com ) {
 5868  				// Check for duplicate comments
 5869  				$query  = "select sql_no_cache max(time) from `%s` where com='%s' " .
 5870  					"and host='%s' " .
 5871  					"and time>%d";
 5872  				$result = mysql_board_call( $query, SQLLOG, $com, $host, $time - RENZOKU_DUPE );
 5873  				if( $ltime = mysql_result( $result, 0, 0 ) ) {
 5874  					//check_fail_floodcheck($com);
 5875  					$str = sprintf(S_RENZOKU_DUP, sec2hms( ( $ltime + RENZOKU_DUPE ) - $time, false, true ) );
 5876  					error( $str, $dest );
 5877  				}
 5878  				mysql_free_result( $result );
 5879  			}
 5880  			
 5881  			/**
 5882  			 * Posting cooldowns
 5883  			 */
 5884  			if (!$resto) {
 5885  				/**
 5886  				 * New threads
 5887  				 */
 5888  				$query  = "select max(time) from `%s` where time>%d " .
 5889  					"and host='%s' and root>0"; //root>0 == non-sticky
 5890  				$result = mysql_board_call( $query, SQLLOG, ( $time - RENZOKU3 ), $host );
 5891  				if( $ltime = mysql_result( $result, 0, 0 ) ) {
 5892  					$str = sprintf(S_RENZOKU3, sec2hms( ( $ltime + RENZOKU3 ) - $time, false, true ) );
 5893  					error( $str, $dest );
 5894  				}
 5895  				mysql_free_result( $result );
 5896  				// Cross-board cooldown
 5897  				$query = "SELECT 1 FROM user_actions WHERE ip = %d AND action = 'new_thread' AND board != '%s' AND time >= DATE_SUB(NOW(), INTERVAL 5 MINUTE)";
 5898  				$result = mysql_global_call($query, ip2long($host), BOARD_DIR);
 5899  				if (mysql_num_rows($result) > 0) {
 5900  					error( S_RENZOKU3, $dest ); // You must wait longer before posting another thread
 5901  				}
 5902  			}
 5903  			else {
 5904  			  /**
 5905  			   * Replies
 5906  			   * Pass users have lower cooldowns
 5907  			   */
 5908          
 5909          // Check for same image flood first
 5910          if ($has_image && $resto) {
 5911            $query  = "SELECT time FROM `%s` WHERE host = '%s' AND md5 = '%s' AND resto != %d ORDER BY no DESC LIMIT 1";
 5912            
 5913            $result = mysql_board_call($query, SQLLOG, $host, $md5, $resto);
 5914            
 5915            if ($flood_row = mysql_fetch_assoc($result)) {
 5916              $last_time = (int)$flood_row['time'];
 5917              
 5918              $cooldown = RENZOKU_DUPE;
 5919              $cooldown_error = S_RENZOKU2_DUP;
 5920              
 5921              if ($captcha_bypass) {
 5922                $cooldown = ceil((int)$cooldown / 2);
 5923              }
 5924              else {
 5925                $cooldown_error .= S_RENZOKU_PASS;
 5926              }
 5927              
 5928              if ($last_time > $time - $cooldown) {
 5929                error(sprintf($cooldown_error, sec2hms($last_time + $cooldown - $time, false, true)), $dest);
 5930              }
 5931            }
 5932          }
 5933          
 5934          // Now the standard cooldown
 5935  				$query  = "SELECT time, resto, fsize FROM `%s` WHERE host = '%s' AND resto > 0 ORDER BY no DESC LIMIT 1";
 5936  				
 5937  				$result = mysql_board_call($query, SQLLOG, $host);
 5938  				
 5939  				if ($flood_row = mysql_fetch_assoc($result)) {
 5940  					$last_time = (int)$flood_row['time'];
 5941  					
 5942  					if ($has_image) {
 5943  						$cooldown = RENZOKU2;
 5944  						$cooldown_error = S_RENZOKU2;
 5945  					}
 5946  					else {
 5947  						$cooldown = RENZOKU;
 5948  						$cooldown_error = S_RENZOKU;
 5949  					}
 5950  					
 5951  					if ($captcha_bypass) {
 5952  					  $cooldown = ceil((int)$cooldown / 2);
 5953  					}
 5954  					else {
 5955  					  $cooldown_error .= S_RENZOKU_PASS;
 5956  					}
 5957  					
 5958  					if ($last_time > $time - $cooldown) {
 5959  						error(sprintf($cooldown_error, sec2hms($last_time + $cooldown - $time, false, true)), $dest);
 5960  					}
 5961  				}
 5962  			}
 5963  			/*
 5964  			if (SAVE_XFF == 1 && $xff) {
 5965  				// Check for multiple ips with same xff
 5966  				$result = mysql_global_call( "select count(distinct ip)>2 from xff where xff='%s' and is_live=1", $xff );
 5967  				if( mysql_result( $result, 0, 0 ) ) {
 5968  					auto_ban_poster( $name, 14, 1, "Detected 3 proxies for same IP", "Proxy/Tor exit node." );
 5969  					error( S_GENERICERROR, $dest );
 5970  				}
 5971  				// Check for multiple xffs with same ip?
 5972  			}
 5973        */
 5974  			// Check for OP bump limiting
 5975        if ($resto && RENZOKU_OP) {
 5976          $query = 'SELECT host, time FROM `%s` WHERE no = %d';
 5977          $query = mysql_board_call($query, SQLLOG, $resto);
 5978          if ($query) {
 5979            $result = mysql_fetch_assoc($query);
 5980            // Poster is OP
 5981            if ($result && $result['host'] === $host) {
 5982              // OP can only bump his thread RENZOKU_OP_TIME seconds after its creation
 5983              if ($result['time'] > ($time - RENZOKU_OP_TIME)) {
 5984                $is_sage = 1;
 5985              }
 5986              // OP can only bump his thread every RENZOKU_OP_TIME2 seconds
 5987              else {
 5988                $query2 = "SELECT time FROM `%s` WHERE host = '%s' AND resto = %d ORDER BY no DESC LIMIT 1";
 5989                $query2 = mysql_board_call($query2, SQLLOG, $host, $resto);
 5990                if ($query2) {
 5991                  $result = mysql_fetch_assoc($query2);
 5992                  if ($result && $result['time'] > ($time - RENZOKU_OP_TIME2)) {
 5993                    $is_sage = 1;
 5994                  }
 5995                }
 5996                mysql_free_result($query2);
 5997              }
 5998            }
 5999          }
 6000          mysql_free_result($query);
 6001        }
 6002  		}
 6003  		
 6004      // Minimal cooldowns for authed users (3s)
 6005      if ($may_flood) {
 6006        $query = "SELECT time FROM `%s` WHERE host = '%s' ORDER BY no DESC LIMIT 1";
 6007        
 6008        $result = mysql_board_call($query, SQLLOG, $host);
 6009        
 6010        if ($flood_row = mysql_fetch_assoc($result)) {
 6011          $last_time = (int)$flood_row['time'];
 6012          
 6013          $cooldown = 5;
 6014          
 6015          if ($last_time > $time - $cooldown) {
 6016            error(sprintf(S_RENZOKU, sec2hms($last_time + $cooldown - $time, false, true)), $dest);
 6017          }
 6018        }
 6019      }
 6020      
 6021      time_log( "fc" );
 6022      
 6023      $tensorchan_score = 0;
 6024      
 6025  		// thumbnail
 6026  		$image_path = "";
 6027  		$m_img = false;
 6028  		if ($has_image) {
 6029        if( USE_THUMB && !UPLOAD_BOARD ) {
 6030          // Detect and block NSFW content
 6031          $_need_inference = tensorchan_is_needed($userpwd, $resto, $W, $H, $ext);
 6032          
 6033          if ($_need_inference) {
 6034            $_tensor_png = false;
 6035          }
 6036          else {
 6037            $_tensor_png = null;
 6038          }
 6039          
 6040          $ret = make_thumb( $dest, $tim, $ext, $resto, $TN_W, $TN_H, $tmd5, $webm_sar, $_tensor_png);
 6041          
 6042          if (!$ret && $ext != ".pdf") {
 6043            error(S_IMGFAIL, $dest);
 6044          }
 6045          
 6046          if ($_need_inference && $_tensor_png) {
 6047            $tensorchan_score = tensorchan_check_nsfw($_tensor_png);
 6048            unset($_tensor_png);
 6049          }
 6050        }
 6051        
 6052  			$name_part = UPLOAD_BOARD ? $insfile : $tim;
 6053  			$image_path = IMG_DIR . $name_part . $ext;
 6054  			if( move_uploaded_file( $dest, $image_path ) === false ) {
 6055  				error( S_FAILEDUPLOAD, $dest );
 6056  			}
 6057  			chmod( $image_path, 0664 );
 6058  			
 6059  			if (MOBILE_IMG_RESIZE) {
 6060  			  $m_img = resize_mobile_image($image_path, $W, $H, $fsize, $tim, $ext);
 6061  			}
 6062        
 6063        // Oekaki
 6064        if (ENABLE_PAINTERJS) {
 6065          // Replays
 6066          if (ENABLE_OEKAKI_REPLAYS) {
 6067            $oe_replay_path = null;
 6068            
 6069            if (isset($_FILES['oe_replay']) && $_FILES['oe_replay']['name'] === 'tegaki.tgkr' && $insfile === 'tegaki') {
 6070              if (oekaki_validate_replay($_FILES['oe_replay']) === true) {
 6071                $oe_replay_path = IMG_DIR . $tim . '.tgkr';
 6072                
 6073                if (move_uploaded_file($_FILES['oe_replay']['tmp_name'], $oe_replay_path) === false) {
 6074                  error(S_FAILEDUPLOAD);
 6075                }
 6076                
 6077                chmod($oe_replay_path, 0664);
 6078              }
 6079            }
 6080            
 6081            // Oekaki meta
 6082            if (isset($_POST['oe_time'])) {
 6083              if (isset($_POST['oe_src']) && $resto) {
 6084                $oe_src_pid = oekaki_get_valid_src_pid($_POST['oe_src'], BOARD_DIR, $resto);
 6085              }
 6086              else {
 6087                $oe_src_pid = null;
 6088              }
 6089              
 6090              $com .= oekaki_format_info(
 6091                $_POST['oe_time'],
 6092                $oe_replay_path ? $tim : null,
 6093                $oe_src_pid
 6094              );
 6095            }
 6096          }
 6097        }
 6098  		}
 6099      
 6100  		//logtime( "Thumbnail created" );
 6101  		time_log( "t" );
 6102  
 6103  		// Infrequent flood check (dupe image)
 6104  		if( $has_image && (!$capcode || $capcode === 'none')) {
 6105  			if ($resto) {
 6106  			  $result = mysql_board_call("SELECT sql_no_cache `no`,`resto` FROM `" . SQLLOG . "` WHERE archived = 0 and (resto = %d OR no = %d) AND `md5`='%s' AND filedeleted=0 limit 1", $resto, $resto, $md5);
 6107  			}
 6108  			else {
 6109  			  $result = mysql_board_call("SELECT sql_no_cache `no`,`resto` FROM `" . SQLLOG . "` WHERE archived = 0 AND resto = 0 AND `md5`='%s' AND filedeleted=0 limit 1", $md5);
 6110  			}
 6111  			
 6112  			if( mysql_num_rows( $result ) ) {
 6113  				list( $dupeno, $duperesto ) = mysql_fetch_row( $result );
 6114  				if( !$duperesto ) $duperesto = $dupeno;
 6115  				error( '' . S_DUPE . ' <a href="//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $duperesto . PHP_EXT2 . '#p' . $dupeno . '">here</a>.', $dest );
 6116  			}
 6117  			
 6118  			if ($resto && MAX_IMG_REPOST_COUNT > 0) {
 6119  				$_query = 'SELECT COUNT(*) FROM `' . SQLLOG . "` WHERE archived = 0 AND resto != 0 AND `md5` = '%s' AND filedeleted = 0";
 6120  				
 6121  				$result = mysql_board_call($_query, $md5);
 6122  				
 6123  				if ($result) {
 6124  					$_count = (int)mysql_fetch_row($result)[0];
 6125  					
 6126  					if ($_count >= MAX_IMG_REPOST_COUNT) {
 6127  						error(S_DUPE);
 6128  					}
 6129  				}
 6130  			}
 6131  			
 6132  			if ( defined('SQLLOGMD5') ) {
 6133  				// TODO: There's a race here. This should just be INSERT and check for failure!
 6134  				$result = mysql_board_call("SELECT sql_no_cache * FROM `%s` WHERE md5='%s' AND now > DATE_SUB(NOW(), INTERVAL 1 DAY) limit 1", SQLLOGMD5, $md5);
 6135  				if ( mysql_num_rows( $result ) ) {
 6136  					list( $dc_now, $dc_filename, $dc_md5p ) = mysql_fetch_row( $result );
 6137  					
 6138  					if( $dc_now ) {
 6139  						error('Error: You must wait longer before reposting this file.', $dest );
 6140  					}
 6141  				}
 6142  			}
 6143  		}
 6144  
 6145  		$rootpredicate = $resto ? "0" : "now()";
 6146      
 6147  		// ROBOT9000
 6148      if (defined('ROBOT9000') && ROBOT9000) {
 6149        // Logged in uses can bypass r9k by using the "bypass_r9k" command in the Options field.
 6150        // Capcoded posts always bypass r9k.
 6151        if (($options_field !== 'bypass_r9k' || !has_level('janitor')) && $capcode === 'none') {
 6152          require_once 'plugins/robot9000.php';
 6153          $r9k_status = r9k_process($com, $md5, ip2long($host));
 6154          if ($r9k_status !== R9K_OK) {
 6155            error($r9k_status, $dest );
 6156          }
 6157        }
 6158      }
 6159  		
 6160      // FIX ME, comments with html get truncated and break the layout, but only mods and janitors can bypass the regular limits
 6161      if (strlen($com) > 65536) {
 6162        error(S_TOOLONG, $dest);
 6163      }
 6164      
 6165  		//logtime( "Before insertion" );
 6166  
 6167  		//find sticky & autosage
 6168  		// auto-sticky
 6169  		//$sticky   = false;
 6170  		// autosagin is now done in spam_filter_post_content
 6171  		//$autosage = spam_filter_should_autosage( $com, $sub, $name, $fsize, $resto, $W, $H, $dest, $insertid );
 6172  
 6173  		//old auto-sticky code -- disabled
 6174  		// if(defined('AUTOSTICKY') && AUTOSTICKY) {
 6175  		// 	$autosticky = preg_split("/,\s*/", AUTOSTICKY);
 6176  		// if($resto == 0) {
 6177  		// if($insertid % 1000000 == 0 || in_array($insertid,$autosticky))
 6178  		// 	$sticky = true;
 6179  		// }
 6180  		// }
 6181  
 6182  		$flag_cols = "";
 6183  		$flag_vals = "";
 6184  
 6185  		if( $captcha_bypass ) {
 6186  			$flag_cols .= ',4pass_id';
 6187  			$flag_vals .= ",'" . $passid . "'";
 6188  		}
 6189      
 6190  		if ($since4pass) {
 6191  			$flag_cols .= ',since4pass';
 6192  			$flag_vals .= ",$since4pass";
 6193  		}
 6194  		/*
 6195  		if( $sticky ) {
 6196  			$flag_cols .= ",sticky";
 6197  			$flag_vals .= ",1";
 6198  		}
 6199      */
 6200  		//permasage just means "is sage" for replies
 6201  		if( $resto ? $is_sage : $autosage ) {
 6202  			$flag_cols .= ",permasage";
 6203  			$flag_vals .= ",1";
 6204  		}
 6205  
 6206  		if( $capcode ) {
 6207  			$flag_cols .= ',capcode';
 6208  			$flag_vals .= ",'$capcode'";
 6209  		}
 6210  		
 6211      if ($m_img) {
 6212        $flag_cols .= ',m_img';
 6213        $flag_vals .= ",1";
 6214      }
 6215      
 6216  		//$country = geoip_country_code_by_addr( $_SERVER['REMOTE_ADDR'] );
 6217  		//if( !$country ) $country = 'XX';
 6218      $geo_data = GeoIP2::get_country($_SERVER['REMOTE_ADDR']);
 6219      
 6220      if ($geo_data && isset($geo_data['country_code'])) {
 6221        $country = $geo_data['country_code'];
 6222        
 6223        // FIXME: football cups
 6224        /*
 6225        if (BOARD_DIR === 'sp' && $country === 'GB' && isset($geo_data['sub_code'])) {
 6226          if ($geo_data['sub_code'] === 'WLS') {
 6227            $country = 'XW';
 6228          }
 6229          else if ($geo_data['sub_code'] === 'SCT') {
 6230            $country = 'XS';
 6231          }
 6232          else if ($geo_data['sub_code'] !== 'NIR') {
 6233            $country = 'XE';
 6234          }
 6235        }
 6236        */
 6237      }
 6238      else {
 6239        $country = 'XX';
 6240      }
 6241      
 6242      // User Agent ID
 6243      $browser_id = spam_filter_get_browser_id();
 6244      
 6245      // Request Signature
 6246      $_req_sig = spam_filter_get_req_sig();
 6247      
 6248      /**
 6249       * Flood checks
 6250       */
 6251      $_threat_score = 0;
 6252      
 6253      if (!$captcha_bypass) {
 6254        $_pwd_known = $userpwd && $userpwd->isUserKnownOrVerified(360);
 6255        $_pwd_trusted = $userpwd && ($userpwd->postCount() > 10 || $userpwd->verifiedLevel());
 6256        $_pwd_verified = $userpwd && $userpwd->verifiedLevel();
 6257        
 6258        if (!$_pwd_known) {
 6259          $_threat_score = spam_filter_get_threat_score($country, !$resto, true);
 6260        }
 6261        
 6262        // Sample the user agent of known  users
 6263        if (false && $userpwd && $userpwd->isUserKnownOrVerified(4320) && $userpwd->postCount() > 10) {
 6264          if (mt_rand(0, 9999) < 2000) {
 6265            $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6266            log_spam_filter_trigger('log_safe_req', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6267          }
 6268        }
 6269        
 6270        if ($userpwd && !$userpwd->verifiedLevel() && $userpwd->postCount() > 0 && $userpwd->pwdLifetime() < 86400 && $userpwd->maskChanged()) {
 6271          log_spam_filter_trigger('log_mask_changed', BOARD_DIR, $resto, $host, 1);
 6272        }
 6273        
 6274        if (!$_pwd_known && ($_threat_score > 0.31)) {
 6275          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6276          log_spam_filter_trigger('block_txt_threat', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6277          show_post_successful_fake($resto);
 6278          return;
 6279        }
 6280        
 6281        if (false && !$_pwd_known && $resto == 18361689 && BOARD_DIR === 'fa' && mt_rand(0, 9) >= 1 && $country != 'US') {
 6282          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6283          log_spam_filter_trigger('block_other', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6284          error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6285        }
 6286        
 6287        if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'g' && strpos($_thread_sub, '/aicg/') !== false) {
 6288          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6289          log_spam_filter_trigger('block_aicg', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6290          error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6291        }
 6292        
 6293        if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_com, '/lolg/') !== false) {
 6294          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6295          log_spam_filter_trigger('block_lolg', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6296          error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6297          //show_post_successful_fake($resto);
 6298          //return;
 6299        }
 6300        
 6301        if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_com, '/overwatch') !== false) {
 6302          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6303          log_spam_filter_trigger('block_owg', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6304          error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6305          //show_post_successful_fake($resto);
 6306          //return;
 6307        }
 6308        
 6309        if (false && !$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'fa' && strpos($_thread_sub, 'Workwear General') !== false) {
 6310          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6311          log_spam_filter_trigger('block_denim', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6312          error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6313          //show_post_successful_fake($resto);
 6314          //return;
 6315        }
 6316        
 6317        if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, '/bag/') !== false && $browser_id === '04d2237a2') {
 6318          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6319          log_spam_filter_trigger('block_bag', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6320          error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6321        }
 6322        
 6323        if (false && !$_pwd_known && !$resto && (BOARD_DIR === 'co' || BOARD_DIR === 'a') && $country !== 'XX' && $browser_id === '02b99990d' && ($country == 'GB' || $country == 'DE' || $country == 'AU' || strpos($_COOKIE['_tcs'], $_SERVER['HTTP_X_TIMEZONE']) === false)) {
 6324          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6325          log_spam_filter_trigger('block_peridot', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6326          error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6327        }
 6328        
 6329        if (!$_pwd_trusted && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, 'granblue') !== false) {
 6330          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6331          log_spam_filter_trigger('block_gbfg', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6332          error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6333          //show_post_successful_fake($resto);
 6334          //return;
 6335        }
 6336        
 6337        if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'v' && strpos($_thread_sub, 'gamesdonequick') !== false && $_threat_score >= 0.09 && mt_rand(0, 9) >= 1) {
 6338          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6339          log_spam_filter_trigger('block_adgq', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6340          show_post_successful_fake($resto);
 6341          return;
 6342        }
 6343        
 6344        if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, '/zzz/') !== false && $_threat_score >= 0.09 && mt_rand(0, 9) >= 1) {
 6345          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6346          log_spam_filter_trigger('block_zzz', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6347          show_post_successful_fake($resto);
 6348          return;
 6349        }
 6350        
 6351        if (!$_pwd_verified && $resto && $has_image && BOARD_DIR === 'vg' && strpos($_thread_sub, '/funkg/') !== false && $_threat_score >= 0.09) {
 6352          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6353          log_spam_filter_trigger('block_funkg', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6354          show_post_successful_fake($resto);
 6355          return;
 6356        }
 6357        
 6358        if (!$has_image) {
 6359          if (!$_pwd_verified && $_threat_score >= 0.09 && mt_rand(0, 9) >= 5 && BOARD_DIR !== 'f') {
 6360            $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6361            log_spam_filter_trigger('block_pub_prox', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6362            show_post_successful_fake($resto);
 6363            return;
 6364          }
 6365        }
 6366        else {
 6367          if (!$resto) {
 6368            $_thres = 3;
 6369          }
 6370          else {
 6371            $_thres = 4;
 6372          }
 6373          
 6374          if (!$_pwd_verified && $_threat_score >= 0.09 && mt_rand(0, 9) >= $_thres && BOARD_DIR !== 'f') {
 6375            $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6376            log_spam_filter_trigger('block_pub_prox', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6377            if (mt_rand(0, 1)) {
 6378              error(S_IPRANGE_BLOCKED_IMG . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6379            }
 6380            else {
 6381              show_post_successful_fake($resto);
 6382              return;
 6383            }
 6384          }
 6385        }
 6386        
 6387        if (!$_pwd_known && $resto && $has_image && BOARD_DIR === 'vg' && $country === 'US' && isset($_COOKIE['_tcs']) && strpos($_COOKIE['_tcs'], '.America/Bogota.') !== false) {
 6388          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6389          log_spam_filter_trigger('block_bag_scat', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6390          show_post_successful_fake($resto);
 6391          return;
 6392        }
 6393        
 6394        // FLOOD CHECK
 6395        $flood_status = 0;//spam_filter_is_post_flood($host, BOARD_DIR, $resto, null);
 6396  
 6397        if ($flood_status === 3) {
 6398          $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6399          
 6400          log_spam_filter_trigger('block_flood_check', BOARD_DIR, $resto, $host, $flood_status, $_bot_headers);
 6401          
 6402          // Raw thread flood
 6403          if ($flood_status === 3) {
 6404            error(S_FAILEDUPLOAD);
 6405          }
 6406          else {
 6407            show_post_successful_fake($resto);
 6408            return;
 6409          }
 6410        }
 6411        
 6412        // Check Cloudflare's bot score and block using the lenient rangeban message
 6413        if ($userpwd && !$userpwd->isUserKnownOrVerified(1440) && isset($_SERVER['HTTP_X_BOT_SCORE'])) {
 6414          if (spam_filter_is_pwd_blocked($userpwd->getPwd(), 'block_bm_bot', 24)) {
 6415            log_spam_filter_trigger('block_bm_bot_pwd', BOARD_DIR, $resto, $host, $_SERVER['HTTP_X_BOT_SCORE']);
 6416            error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6417          }
 6418          
 6419          if (spam_filter_is_likely_automated($memcached)) {
 6420            $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6421            write_to_event_log('block_bm_bot', $host, [
 6422              'pwd' => $userpwd->getPwd(),
 6423              'arg_num' => $_SERVER['HTTP_X_BOT_SCORE'],
 6424              'board' => BOARD_DIR,
 6425              'thread_id' => $resto,
 6426              'meta' => $_bot_headers
 6427            ]);
 6428            error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6429          }
 6430        }
 6431        
 6432        // If the country has changed, log the pwd and then block it for the next 24h
 6433        if ($userpwd && !$userpwd->verifiedLevel() && $userpwd->postCount() < 20) {
 6434          if (spam_filter_has_country_changed($userpwd->getPwd())) {
 6435            log_spam_filter_trigger('block_country_changed', BOARD_DIR, $resto, $host, 1);
 6436            error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6437          }
 6438          
 6439          if ($userpwd->envChanged()) {
 6440            write_to_event_log('country_changed', $host, [
 6441              'pwd' => $userpwd->getPwd(),
 6442              'arg_str' => $country,
 6443              'board' => BOARD_DIR,
 6444              'thread_id' => $resto
 6445            ]);
 6446            
 6447            log_spam_filter_trigger('block_country_changed', BOARD_DIR, $resto, $host, 1);
 6448            error(S_IPRANGE_BLOCKED . ' ' . S_IPRANGE_BLOCKED_TEMP . S_IPRANGE_BLOCKED_L1);
 6449          }
 6450        }
 6451      }
 6452      
 6453      /**
 6454       * Custom flag selection
 6455       */
 6456      if (ENABLE_BOARD_FLAGS) {
 6457        if ($_POST['flag'] === '0' || !isset($board_flags_array[$_POST['flag']])) {
 6458          $board_flag_code = '';
 6459          
 6460          // FIXME: remove this eventually as we use localStorage now
 6461          if (isset($_COOKIE['4chan_flag'])) {
 6462            setcookie('4chan_flag', '', $time - 3600, '/', $cookie_domain);
 6463          }
 6464        }
 6465        else {
 6466          $board_flag_code = mysql_real_escape_string($_POST['flag']);
 6467        }
 6468      }
 6469      else {
 6470        $board_flag_code = '';
 6471      }
 6472      
 6473      if ($board_flag_code) {
 6474        $board_flag_col = ',board_flag';
 6475        $board_flag_val = ",'$board_flag_code'";
 6476      }
 6477      else {
 6478        $board_flag_col = '';
 6479        $board_flag_val = '';
 6480      }
 6481      
 6482  		if( $resto ) calculate_indexes_to_rebuild( $resto );
 6483      
 6484      // Remove old replies if the thread is sticky+undead
 6485      if ($is_undead_sticky && STICKY_CAP > 1) {
 6486        $query = "SELECT MIN(no) FROM (SELECT no FROM `" . BOARD_DIR . "` WHERE resto = $resto ORDER BY no DESC LIMIT " . (STICKY_CAP - 1) . ") as subsel";
 6487        $result = mysql_board_call($query);
 6488        if ($result) {
 6489          $prune_row = mysql_fetch_row($result);
 6490          
 6491          mysql_free_result($result);
 6492          
 6493          $min_no = (int)$prune_row[0];
 6494          
 6495          if ($min_no > $resto) {
 6496            $query = "SELECT no FROM `" . BOARD_DIR . "` WHERE resto = $resto AND no < $min_no";
 6497            $result = mysql_board_call($query);
 6498            
 6499            if ($result) {
 6500              while ($prune_row = mysql_fetch_assoc($result)) {
 6501                delete_post((int)$prune_row['no'], '', 0, 1, 1, 0);
 6502              }
 6503              
 6504              mysql_free_result($result);
 6505            }
 6506          }
 6507        }
 6508      }
 6509      
 6510      // April 2024
 6511      /*
 6512      if ($_xa24_since4pass && !UPLOAD_BOARD && !JANITOR_BOARD) {
 6513        $name = april_2024_get_name();
 6514        
 6515        if ($emails == '$DESU' && strlen($com) < 10000) {
 6516          $com .= '<div class="xa24desu"></div>';
 6517        }
 6518      }
 6519      */
 6520      $user_meta = encode_user_meta($browser_id, substr($_req_sig, 0, 8), $userpwd);
 6521      
 6522  		$insert_tries = 2;
 6523  		do {
 6524  			if( SKIP_DOUBLES == 1 ) mysql_board_call( "START TRANSACTION" );
 6525  			$query = "insert into `" . SQLLOG . "` (now,name,sub,com,host,pwd,email,filename,ext,w,h,tn_w,tn_h,tim,time,last_modified,md5,fsize,root,resto$flag_cols,tmd5,id,country$board_flag_col) values (" .
 6526  				"'" . $now . "'," .
 6527  				"'" . mysql_real_escape_string( $name ) . "'," .
 6528  				mysql_nullify( mysql_real_escape_string( $sub ) ) . "," .
 6529  				"'" . mysql_real_escape_string( $com ) . "'," .
 6530  				"'" . mysql_real_escape_string( $host ) . "'," .
 6531  				"'" . mysql_real_escape_string( $pass ) . "'," .
 6532  				"'" . mysql_real_escape_string($user_meta) . "'," .
 6533  				"'" . mysql_real_escape_string( $insfile ) . "'," .
 6534  				mysql_nullify( $ext ) . "," .
 6535  				(int)$W . "," .
 6536  				(int)$H . "," .
 6537  				(int)$TN_W . "," .
 6538  				(int)$TN_H . "," .
 6539  				"'" . $tim . "'," .
 6540  				(int)$time . "," .
 6541  				(int)$time . "," .
 6542  				mysql_nullify( $md5 ) . "," .
 6543  				(int)$fsize . "," .
 6544  				$rootpredicate . "," .
 6545  				(int)$resto .
 6546  				$flag_vals . "," .
 6547  				mysql_nullify( $tmd5 ) . "," .
 6548  				mysql_nullify( $uid ) . "," .
 6549  				"'$country'$board_flag_val)";
 6550  
 6551  			if( !$result = mysql_board_call( $query ) ) {
 6552  				echo S_SQLFAIL;
 6553  			} //post registration
 6554  			time_log( "i" );
 6555  
 6556  			$insertid = mysql_board_insert_id();
 6557  			if( SKIP_DOUBLES == 1 ) {
 6558  				if( has_doubles( $insertid ) ) {
 6559  					mysql_board_call( "ROLLBACK" );
 6560  					// retry
 6561  				} else {
 6562  					mysql_board_call( "COMMIT" );
 6563  					$insert_tries = 0;
 6564  				}
 6565  			} else {
 6566  				$insert_tries = 0;
 6567  			}
 6568  		} while( $insert_tries-- );
 6569      
 6570      // Captcha bypass token
 6571      if ($captcha_bypass_allow_credits && $_threat_score < 0.15) {
 6572        set_twister_captcha_credits($memcached, $host, $userpwd, $time);
 6573      }
 6574      /*
 6575      if (!$captcha_bypass) {
 6576        if (mt_rand(0, 99) === 0) {
 6577          write_to_event_log('known_sample', $host, [
 6578            'board' => BOARD_DIR,
 6579            'thread_id' => $resto,
 6580            'arg_num' => $userpwd->isUserKnown(),
 6581          ]);
 6582        }
 6583      }
 6584      */
 6585      
 6586      $userpwd->updatePostActivity(!$resto, $has_image);
 6587      
 6588      $userpwd->setCookie($cookie_domain);
 6589      
 6590      // April 2019
 6591      /*
 6592      if (defined('LIKE_MAX_LIKES') && LIKE_MAX_LIKES > 0) {
 6593        like_update_post_score();
 6594      }
 6595      */
 6596      // Halloween 2017
 6597      /*
 6598      if ($resto && defined('CSS_EVENT_NAME') && CSS_EVENT_NAME === 'spooky2017') {
 6599        process_halloween_score($com, $resto, $passid, $pass, $pass_is_bannable);
 6600      }
 6601      */
 6602      // April 2018
 6603      //update_april_team_scores();
 6604      
 6605      // Log mod action if posted with html
 6606      if ($log_mod_action) {
 6607        $action_log_post = array(
 6608          'no' => $insertid,
 6609          'name' => $name,
 6610          'sub' => $sub,
 6611          'com' => $com,
 6612          'filename' => $insfile,
 6613          'ext' => $ext
 6614        );
 6615        
 6616        if ($log_html_post) {
 6617          $action_log_post['com'] = htmlspecialchars($com, ENT_QUOTES);
 6618          log_mod_action(4, $action_log_post);
 6619        }
 6620        // capcode posting
 6621        if ($log_capcode_post) {
 6622          $action_log_post['name'] .= ' ## ' . ucfirst($capcode);
 6623          log_mod_action(5, $action_log_post, $capcode === 'verified');
 6624        }
 6625      }
 6626      
 6627  		if( $resto ) { //sage or age action
 6628  			$resline  = mysql_board_call( "select count(no) from `" . SQLLOG . "` where archived=0 and resto=" . $resto );
 6629  			$countres = mysql_result( $resline, 0, 0 );
 6630        
 6631  			$permasage_hours = (int)PERMASAGE_HOURS;
 6632        
 6633  			if ($permasage_hours > 0) {
 6634  			  $time_col = 'time,';
 6635  			}
 6636  			else {
 6637  			  $time_col = '';
 6638  			}
 6639  			
 6640        // FIXME: a similar query is done at line ~4723
 6641  			$resline = mysql_board_call( "select {$time_col}sticky,permasage,permaage,root from `" . SQLLOG . "` where no=" . $resto );
 6642  			$resline = mysql_fetch_assoc($resline);
 6643  			
 6644  			if ($resline['sticky'] || $resline['permasage']) {
 6645  			  $root_col = '';
 6646  			}
 6647  			else if ($resline['permaage']) {
 6648  			  $root_col = 'root=now(),';
 6649  			}
 6650  			else if ($is_sage || $countres >= MAX_RES) {
 6651  			  $root_col = '';
 6652  			}
 6653  			else if ($permasage_hours && ($time - ($permasage_hours * 3600) >= $resline['time'])) {
 6654  			  $root_col = '';
 6655  		  }
 6656  		  else {
 6657  			  $root_col = 'root=now(),';
 6658          
 6659          if (!$captcha_bypass && BOARD_DIR === 'jp') {
 6660            if (!spam_filter_can_bump_thread($resline['root'])) {
 6661              $root_col = '';
 6662              $_bot_headers = spam_filter_format_http_headers($com, $country, "$insfile$ext", $_threat_score, $_req_sig);
 6663              log_spam_filter_trigger('necrobump', BOARD_DIR, $resto, $host, 1, $_bot_headers);
 6664            }
 6665          }
 6666  		  }
 6667  		  
 6668  			mysql_board_call("update `" . SQLLOG . "` set {$root_col}last_modified=%d where no=%d", $_SERVER['REQUEST_TIME'], $resto);
 6669  		}
 6670  
 6671  		if( defined( 'AUTOSTICKY' ) && AUTOSTICKY ) {
 6672  			$autosticky = preg_split( "/,\s*/", AUTOSTICKY );
 6673  			if( $resto == 0 ) {
 6674  				if( $insertid % 1000000 == 0 || in_array( $insertid, $autosticky ) ) {
 6675  					$sticky = true;
 6676  					mysql_board_call( "update " . SQLLOG . " set sticky=1,root=root where no=$insertid" );
 6677  				}
 6678  			}
 6679  		}
 6680      
 6681  		if( SAVE_XFF == 1 && $xff ) {
 6682  			mysql_global_do( "INSERT INTO xff (tim,board,xff,ip,postno,is_live) VALUES ('%s','%s','%s',%d,%d,1)", $tim, BOARD_DIR, $xff, ip2long( $host ), $insertid );
 6683  		}
 6684  		
 6685  		if (UPLOAD_BOARD && $md5 ) {
 6686  			$result = mysql_board_call( "insert ignore into `%s` (filename,md5) values('%s','%s')", SQLLOGMD5, $insfile, $md5 );
 6687  		}
 6688  		
 6689  		// determine url to redirect to
 6690  		$proto = ( stripos( $_SERVER["HTTP_REFERER"], "https" ) !== false ) ? "https:" : "http:";
 6691  		if( !$is_nonoko && !$resto ) {
 6692  			$redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $insertid . PHP_EXT2;
 6693  		} else if( !$is_nonoko ) {
 6694  			$redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $resto . PHP_EXT2 . '#p' . $insertid;
 6695  		} else {
 6696  			$redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . '/';
 6697  		}
 6698  		
 6699  		// To let the JavaScript thread watcher know the newly created thread ID
 6700  		if (!$resto && isset($_POST['awt'])) {
 6701  			setcookie('4chan_awt', $insertid, 0, '/' . BOARD_DIR . '/', $cookie_domain);
 6702  		}
 6703  		
 6704  		show_post_successful( $mes, $com, $insertid, $resto, $redirect, $delay_refresh );
 6705  		
 6706  		$static_rebuild = ( STATIC_REBUILD == 1 );
 6707  		//logtime( "Before trim_db" );
 6708  
 6709  		// trim database
 6710      if (!$resto) {
 6711        
 6712        if (!$static_rebuild) {
 6713          trim_db();
 6714          
 6715          if (ENABLE_ARCHIVE && ARCHIVE_MAX_AGE) {
 6716            trim_archive();
 6717          }
 6718        }
 6719      }
 6720      
 6721  		//logtime( "After trim_db" );
 6722  		//time_log( "tr" );
 6723      
 6724      $_need_updatelog = true;
 6725      
 6726      if (AUTOARCHIVE_CAP && $resto && !$sticky && !$undead && ENABLE_ARCHIVE) {
 6727        if (count_thread_replies(BOARD_DIR, $resto) >= AUTOARCHIVE_CAP) {
 6728          $_need_updatelog = false;
 6729          archive_thread($resto);
 6730        }
 6731      }
 6732      
 6733  		// update html
 6734      if ($_need_updatelog) {
 6735  		  updatelog( $resto ? $resto : $insertid );
 6736      }
 6737  		//logtime( "Pages rebuilt" );
 6738  		//time_log( "r" );
 6739  
 6740  		// late tasks happen below here
 6741  		iplog_add( BOARD_DIR, $insertid, $host, $time, $resto == 0, $tim, $has_image );
 6742  		
 6743      // Auto-report possibly nsfw post
 6744      if ($tensorchan_score && $tensorchan_score > 0.5) {
 6745        tensorchan_log(BOARD_DIR, $insertid, $resto, $tim, $ext, $tensorchan_score);
 6746      }
 6747  	} else {
 6748  		// silent reject
 6749  		$insertid = 0;
 6750  		$noko     = 0;
 6751  	}
 6752    /*
 6753  	if( STATS_USER_JS ) {
 6754  		mysql_global_do( "UPDATE `user_stats` SET `count` = `count`+1 WHERE name='%s'", $stats_ok );
 6755  	}
 6756    */
 6757  }
 6758  
 6759  // Redirects to the most rcently created thread
 6760  // This is to confuse spambots
 6761  function show_post_successful_fake($resto = 0, $captcha_passed = true) {
 6762    $thread_id = (int)$resto;
 6763    $insert_id = 0;
 6764    
 6765    if (!$resto) {
 6766      $query = 'SELECT resto FROM `' . BOARD_DIR . '` WHERE resto != 0 ORDER BY resto DESC LIMIT 1';
 6767      $res = mysql_board_call($query);
 6768      if ($res) {
 6769        $row = mysql_fetch_row($res);
 6770        $insert_id = (int)$row[0];
 6771      }
 6772    }
 6773    else {
 6774      $query = 'SELECT no FROM `' . BOARD_DIR . '` ORDER BY no DESC LIMIT 1';
 6775      $res = mysql_board_call($query);
 6776      if ($res) {
 6777        $row = mysql_fetch_row($res);
 6778        $insert_id = (int)$row[0] + 1;
 6779      }
 6780    }
 6781    
 6782    if (!$thread_id) {
 6783      $redirect = 'https://boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $insert_id . PHP_EXT2;
 6784    }
 6785    else {
 6786      $redirect = 'https://boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $thread_id . PHP_EXT2 . '#p' . $insert_id;
 6787    }
 6788    
 6789    $cookie_domain = '.' . L::d(BOARD_DIR);
 6790    
 6791    $now = $_SERVER['REQUEST_TIME'];
 6792    
 6793    // Name cookie
 6794    $c_name = $_POST['name'];
 6795    setrawcookie('4chan_name', rawurlencode($c_name), $now + ($c_name ? (7 * 24 * 3600) : -3600), '/', $cookie_domain);
 6796    
 6797    // Password cookie
 6798    $userpwd = UserPwd::getSession();
 6799    
 6800    if ($userpwd) {
 6801      if ($captcha_passed) {
 6802        $userpwd->setCookie($cookie_domain);
 6803      }
 6804      else if (!$userpwd->isFake() && !$userpwd->isNew()) {
 6805        $userpwd->setCookie($cookie_domain);
 6806      }
 6807      else {
 6808        UserPwd::setFakeCookie($now, $cookie_domain);
 6809      }
 6810    }
 6811    
 6812    show_post_successful(null, null, $insert_id, $thread_id, $redirect);
 6813  }
 6814  
 6815  function show_post_successful( $mes, $com, $insertid, $resto, $redirect, $delay_refresh = false ) {
 6816    if (isset($_SERVER['HTTP_ACCEPT']) && $_SERVER['HTTP_ACCEPT'] === 'application/json') {
 6817      return show_post_successful_json($insertid, $resto);
 6818    }
 6819    
 6820  	if( !$mes ) $mes = S_POSTING_DONE;
 6821  		$time_to_refresh = ( $delay_refresh ) ? 10 : 1;
 6822  		$script          = "<meta http-equiv=\"refresh\" content=\"$time_to_refresh;URL=$redirect\">";
 6823  	if( defined( 'POST_SUCCESSFUL_FILE' ) ) {
 6824  		$file    = file_get_contents( POST_SUCCESSFUL_FILE );
 6825  		$success = str_replace( "@REDIRECT@", $redirect, $file );
 6826  	} else {
 6827  		// FIXME templating
 6828  		$icon       = DEFAULT_BURICHAN ? 'favicon-ws.ico' : 'favicon.ico';
 6829  		$defaultcss = DEFAULT_BURICHAN ? 'yotsubluenew' : 'yotsubanew';
 6830  		$cssVersion = TEST_BOARD ? CSS_VERSION_TEST : CSS_VERSION;
 6831  		$sg         = style_group();
 6832  
 6833  		$styles = array(
 6834  			'Yotsuba New'   => "yotsubanew.$cssVersion.css",
 6835  			'Yotsuba B New' => "yotsubluenew.$cssVersion.css",
 6836  			'Futaba New'    => "futabanew.$cssVersion.css",
 6837  			'Burichan New'  => "burichannew.$cssVersion.css",
 6838  			'Photon'        => "photon.$cssVersion.css",
 6839  			'Tomorrow'      => "tomorrow.$cssVersion.css"
 6840  		);
 6841  
 6842  		$css = '';
 6843  
 6844  		if( isset( $_COOKIE[$sg] ) ) {
 6845  			if( isset( $styles[$_COOKIE[$sg]] ) ) {
 6846  				$css = '<link rel="stylesheet" title="switch" href="' . STATIC_SERVER . 'css/' . $styles[$_COOKIE[$sg]] . '">';
 6847  			}
 6848  		} else {
 6849  			$dcssl = $defaultcss . '.' . $cssVersion . '.css';
 6850  			$css   = '<link rel="stylesheet" title="switch" href="' . STATIC_SERVER . 'css/' . $dcssl . '">';
 6851  		}
 6852      
 6853      if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) {
 6854        $css = '<link rel="stylesheet" style="text/css" href="' . STATIC_SERVER . 'css/'
 6855          . CSS_EVENT_NAME . '.' . $cssVersion . '.css" title="switch">';
 6856      }
 6857      
 6858  		if( BOARD_DIR == 'j' ) {
 6859  			$css = '<link rel="stylesheet" type="text/css" href="' . STATIC_SERVER . 'css/janichan.' . $cssVersion . '.css" title="Yotsuba New">';
 6860  		}
 6861  
 6862  
 6863  		$script .= '<link rel="shortcut icon" href="//s.4cdn.org/image/' . $icon . '">';
 6864  		$success = "<!DOCTYPE html><head>$script<title>" . S_POSTING_DONE . "</title>$css</head><body style=\"margin-top: 20%; text-align: center;\"><h1 style=\"font-size:36pt;\">$mes</h1><!-- thread:$resto,no:$insertid --></body></html>";
 6865  	}
 6866  	echo $success;
 6867  	
 6868  	if ($resto) {
 6869  	  fastcgi_finish_request();
 6870  	}
 6871  }
 6872  
 6873  function show_post_successful_json($post_id, $thread_id) {
 6874    header('Content-Type: application/json');
 6875    
 6876    echo '{"tid":' . $thread_id . ',"pid":' . $post_id . '}';
 6877    
 6878    if ($thread_id) {
 6879      fastcgi_finish_request();
 6880    }
 6881  }
 6882  
 6883  function resredir( $res, $delete = 0, $no_exit = false ) {
 6884    if (!$_SERVER["HTTP_REFERER"]) {
 6885      $proto = 'https:';
 6886    }
 6887  	else {
 6888      $proto = ( stripos( $_SERVER["HTTP_REFERER"], "https" ) !== false ) ? "https:" : "http:";
 6889  	}
 6890  	
 6891  	$res = (int)$res;
 6892  	//mysql_board_lock( true );
 6893  	if( !$redir = mysql_board_call( "select no,resto from `" . SQLLOG . "` where no=" . $res ) ) {
 6894  		echo S_SQLFAIL;
 6895  	}
 6896  	list( $no, $resto ) = mysql_fetch_row( $redir );
 6897  	
 6898  	// if we're deleting and no post/resto (thread gone)
 6899  	if( !$no && $delete ) {
 6900  		// send us back to the board
 6901  		updating_index();
 6902  		//mysql_board_unlock();
 6903  		if (!$no_exit) {
 6904  		  die;
 6905  		}
 6906  		else {
 6907  		  return;
 6908  		}
 6909  	}
 6910  	
 6911  	if( !JANITOR_BOARD ) {
 6912  		header("Cache-Control: public, max-age=2");
 6913  	}
 6914  
 6915  	if( !$no ) {
 6916  		// If no < max(no) then this could be 410 Gone.
 6917  		http_response_code(404);
 6918  		error( S_NOTHREADERR, $dest );
 6919  	}
 6920  
 6921  	if( $resto == "0" ) { // thread
 6922  		$redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $no . PHP_EXT2 . '#p' . $no;
 6923  	} else {
 6924  		$redirect = $proto . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/thread/" . $resto . PHP_EXT2 . '#p' . $no;
 6925  	}
 6926  
 6927  	$redirect = JANITOR_BOARD ? str_replace( 'boards.', 'sys.', $redirect ) : $redirect;
 6928  
 6929  	header("Location: $redirect", true, 301);
 6930  	echo "<meta http-equiv=\"refresh\" content=\"0;URL=$redirect\">";
 6931  	//mysql_board_unlock();
 6932  }
 6933  
 6934  function tensorchan_is_needed($userpwd, $resto, $W, $H, $ext) {
 6935    // Inference is disabled
 6936    if (!defined('TENSORCHAN_MODE') || !TENSORCHAN_MODE) {
 6937      return false;
 6938    }
 6939    
 6940    // Can't check
 6941    if (!$userpwd) {
 6942      return false;
 6943    }
 6944    
 6945    // User is known or verified
 6946    if ($userpwd->isUserKnownOrVerified(240)) { // 4 hours
 6947      return false;
 6948    }
 6949    
 6950    // OPs only but the post is a reply
 6951    if (TENSORCHAN_MODE == 1 && $resto) {
 6952      return false;
 6953    }
 6954    
 6955    if ($W < 150 || $H < 150 || $ext == '.pdf') {
 6956      return false;
 6957    }
 6958    
 6959    return true;
 6960  }
 6961  
 6962  function tensorchan_check_nsfw($tensor_png) {
 6963    if (!$tensor_png) {
 6964      return false;
 6965    }
 6966    
 6967    $tensor_res = tensorchan_predict($tensor_png);
 6968    
 6969    if (!$tensor_res) {
 6970      return false;
 6971    }
 6972    
 6973    if (isset($tensor_res['error'])) {
 6974      write_to_event_log('tensor_err', $_SERVER['REMOTE_ADDR'], [
 6975        'board' => BOARD_DIR,
 6976        'meta' => htmlspecialchars($tensor_res['error'])
 6977      ]);
 6978      
 6979      return false;
 6980    }
 6981    else {
 6982      if (!isset($tensor_res['nsfw'])) {
 6983        return false;
 6984      }
 6985      
 6986      return (float)$tensor_res['nsfw'];
 6987    }
 6988  }
 6989  
 6990  function tensorchan_log($board, $post_id, $thread_id, $file_id, $file_ext, $score) {
 6991    $post_id = (int)$post_id;
 6992    $thread_id = (int)$thread_id;
 6993    $score = (float)$score;
 6994    
 6995    $sql =<<<SQL
 6996  INSERT INTO tensor_log(board, thread_id, post_id, file_id, file_ext, nsfw)
 6997  VALUES('%s', $thread_id, $post_id, '%s', '%s', $score)
 6998  SQL;
 6999    
 7000    return !!mysql_global_call($sql, $board, $file_id, $file_ext);
 7001  }
 7002  
 7003  function tensorchan_predict($data) {
 7004    if (!$data) {
 7005      return false;
 7006    }
 7007    
 7008    $curl = curl_init();
 7009    
 7010    $url = "http://danbo.int:8501/predict";
 7011    
 7012    curl_setopt($curl, CURLOPT_URL, $url);
 7013    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
 7014    curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 2);
 7015    curl_setopt($curl, CURLOPT_TIMEOUT, 4);
 7016    
 7017    curl_setopt($curl, CURLOPT_CUSTOMREQUEST , "POST");
 7018    curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
 7019    
 7020    $headers = array(
 7021      'Content-Type: application/octet-stream'
 7022    );
 7023    
 7024    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
 7025    curl_setopt($curl, CURLOPT_USERAGENT, '4chan.org');
 7026    
 7027    $resp = curl_exec($curl);
 7028    
 7029    if ($resp === false) {
 7030      if ($errno = curl_errno($curl)) {
 7031        $_err = 'Error (' . $errno . '): ' . curl_strerror($errno);
 7032      }
 7033      else {
 7034        $_err = 'Something went wrong';
 7035      }
 7036      
 7037      return ["error" => $_err];
 7038    }
 7039    
 7040    $resp_status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
 7041    
 7042    if ($resp_status >= 300) {
 7043      return ["error" => "HTTP $resp_status: $resp"];
 7044    }
 7045    
 7046    curl_close($curl);
 7047    
 7048    if ($resp[0] == '{') {
 7049      $resp = json_decode($resp, true);
 7050    }
 7051    else {
 7052      return ["error" => "Not a JSON response"];
 7053    }
 7054    
 7055    return $resp;
 7056  }
 7057  
 7058  function background_color( $im, $is_thread )
 7059  {
 7060  	if( DEFAULT_BURICHAN == 0 ) {
 7061  		if( $is_thread ) {
 7062  			list( $r, $g, $b ) = array(0xFF, 0xFF, 0xEE);
 7063  		} else {
 7064  			list( $r, $g, $b ) = array(0xF0, 0xE0, 0xD6);
 7065  		}
 7066  	} else {
 7067  		if( $is_thread ) {
 7068  			list( $r, $g, $b ) = array(0xEE, 0xF2, 0xFF);
 7069  		} else {
 7070  			list( $r, $g, $b ) = array(0xD6, 0xDA, 0xF0);
 7071  		}
 7072  	}
 7073  
 7074  	return imagecolorallocate( $im, $r, $g, $b );
 7075  }
 7076  
 7077  function optimize_thumb($tmppath) {
 7078    system("/usr/local/bin/jpegoptim -q --strip-all '$tmppath' >/dev/null 2>&1");
 7079  }
 7080  
 7081  // Calculates perceptual hash for a thumbnail
 7082  // $img is a reference to a GD resource
 7083  function get_thumb_dhash(&$img, $width, $height) {
 7084    if (!$img) {
 7085      return false;
 7086    }
 7087    
 7088    $data = imagecreatetruecolor(9, 8);
 7089    imagecopyresampled($data, $img, 0, 0, 0, 0, 9, 8, $width, $height);
 7090    imagefilter($data, IMG_FILTER_GRAYSCALE);
 7091    
 7092    $hash = 0;
 7093    $bit = 1;
 7094    
 7095    for ($y = 0; $y < 8; $y++) {
 7096      $previous = imagecolorat($data, 0, $y) & 0xFF;
 7097      
 7098      for ($x = 1; $x < 9; $x++) {
 7099        $current = imagecolorat($data, $x, $y) & 0xFF;
 7100        
 7101        if ($previous > $current) {
 7102          $hash |= $bit;
 7103        }
 7104        
 7105        $bit = $bit << 1;
 7106        $previous = $current;
 7107      }
 7108    }
 7109    
 7110    imagedestroy($data);
 7111    
 7112    return sprintf("%016x", $hash);
 7113  }
 7114  
 7115  //thumbnails
 7116  function make_thumb( $fname, $tim, $ext, $resto, &$TN_W, &$TN_H, &$tmd5, $webm_sar = null, &$tensor_png = null )
 7117  {
 7118  	$thumb_dir = THUMB_DIR; //thumbnail directory
 7119  	$outpath   = $thumb_dir . $tim . 's.jpg';
 7120  	if( !$resto ) {
 7121  		$width  = MAX_W; //output width
 7122  		$height = MAX_H; //output height
 7123  		$jpeg_quality = 50;
 7124  	} else {
 7125  		$width  = MAXR_W; //output width (imgreply)
 7126  		$height = MAXR_H; //output height (imgreply)
 7127  		$jpeg_quality = 40;
 7128  	}
 7129  
 7130  	if( ENABLE_PDF == 1 && $ext == '.pdf' ) {
 7131  		// create jpeg for the thumbnailer
 7132  		$pdfjpeg = IMG_DIR . $tim . '.pdf.tmp';
 7133  		@exec( "/usr/local/bin/gs -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=jpeg -sOutputFile=$pdfjpeg $fname" );
 7134  		if( !file_exists( $pdfjpeg ) ) unlink( $fname );
 7135  		$fname = $pdfjpeg;
 7136  	}
 7137    else if (ENABLE_WEBM && ($ext == '.webm' || $ext == '.mp4')) {
 7138      $webm_thumb = thumb_webm($fname, $ext);
 7139      
 7140      if (!$webm_thumb) {
 7141        unlink($fname);
 7142        return false;
 7143      }
 7144      
 7145      $fname = $webm_thumb;
 7146    }
 7147  	
 7148  	$size = @GetImageSize($fname);
 7149    
 7150  	if ($size === false) {
 7151  		return;
 7152  	}
 7153  	
 7154    // File size needs to be checked again because of cleanup_uploaded_file
 7155    if (defined('MAX_DIMENSION') && $ext == '.gif') {
 7156      if ($size[0] > MAX_DIMENSION || $size[1] > MAX_DIMENSION) {
 7157        error(S_TOOLARGERES);
 7158      }
 7159    }
 7160    
 7161  	$memory_limit_increased = false;
 7162  	$maybe_transparent      = true;
 7163  	// Don't increase memory limit on CLI so that the user can do it with a CLI parameter instead
 7164  	if( $size[0] * $size[1] > 3000000 && isset($_SERVER['REMOTE_ADDR']) ) {
 7165  		$memory_limit_increased = true;
 7166  		ini_set( 'memory_limit', memory_get_usage() + $size[0] * $size[1] * 15 ); // for huge images
 7167  	}
 7168  	switch( $size[2] ) {
 7169  		case 1 :
 7170  			$im_in = ImageCreateFromGIF( $fname );
 7171  			if (!$im_in) {
 7172  			  return;
 7173  		  }
 7174  			break;
 7175  		case 2 :
 7176  			$im_in = ImageCreateFromJPEG( $fname );
 7177  			if( !$im_in ) {
 7178  				return;
 7179  			}
 7180  			$maybe_transparent = false;
 7181  			break;
 7182  		case 3 :
 7183  			$im_in = ImageCreateFromPNG( $fname );
 7184  			if( !$im_in ) {
 7185  				return;
 7186  			}
 7187  			break;
 7188  		default :
 7189  			return;
 7190  	}
 7191  	
 7192  	$source_w = $size[0];
 7193  	$source_h = $size[1];
 7194  	
 7195    if ($webm_sar) {
 7196      if ($webm_sar > 1) {
 7197        $size[1] = round($size[1] / $webm_sar);
 7198        
 7199        if ($size[1] == 0) {
 7200          $size[1] = 1;
 7201        }
 7202      }
 7203      else {
 7204        $size[0] = round($size[0] * $webm_sar);
 7205        
 7206        if ($size[0] == 0) {
 7207          $size[0] = 1;
 7208        }
 7209      }
 7210    }
 7211    
 7212  	// Resizing
 7213  	if( $size[0] > $width || $size[1] > $height ) {
 7214  		$key_w = $width / $size[0];
 7215  		$key_h = $height / $size[1];
 7216  		( $key_w < $key_h ) ? $keys = $key_w : $keys = $key_h;
 7217  		$out_w = floor( $size[0] * $keys );
 7218  		$out_h = floor( $size[1] * $keys );
 7219  	} else {
 7220  		$out_w = $size[0];
 7221  		$out_h = $size[1];
 7222  	}
 7223  	
 7224  	// the thumbnail is created
 7225  	$im_out = ImageCreateTrueColor( $out_w, $out_h );
 7226  	if( !$im_out ) return;
 7227  	if( $maybe_transparent ) {
 7228  		$background = background_color( $im_out, $resto == 0 );
 7229  		ImageFill( $im_out, 0, 0, $background );
 7230  	}
 7231  	// copy resized original
 7232  	ImageCopyResampled( $im_out, $im_in, 0, 0, 0, 0, $out_w, $out_h, $source_w, $source_h );
 7233  	$tmppath = tempnam( ini_get( "upload_tmp_dir" ), "thumb" );
 7234  	// thumbnail saved
 7235  	ImageJPEG( $im_out, $tmppath, $jpeg_quality );
 7236    
 7237    // Generate a perceptual hash from the original image
 7238    $tmd5 = Phash::hash($im_in, $source_w, $source_h);
 7239    
 7240    if ($tmd5 === false) {
 7241      $tmd5 = '';
 7242    }
 7243    
 7244    // Create the PNG file for inference
 7245    if ($tensor_png !== null) {
 7246      $tensor_png_dim = TENSORCHAN_DIM;
 7247      $tensor_png_out = ImageCreateTrueColor($tensor_png_dim, $tensor_png_dim);
 7248      ImageCopyResampled($tensor_png_out, $im_in,
 7249        0, 0, 0, 0,
 7250        $tensor_png_dim, $tensor_png_dim, $source_w, $source_h
 7251      );
 7252      $stream = fopen('php://memory','r+');
 7253      imagepng($tensor_png_out, $stream);
 7254      rewind($stream);
 7255      $tensor_png = stream_get_contents($stream);
 7256      fclose($stream);
 7257      ImageDestroy($tensor_png_out);
 7258    }
 7259    
 7260    // Cleanup
 7261  	ImageDestroy( $im_in );
 7262  	ImageDestroy( $im_out );
 7263  	
 7264    optimize_thumb($tmppath);
 7265    
 7266  	//$tmd5 = md5_file( $tmppath );
 7267  	rename_across_device( $tmppath, $outpath );
 7268    
 7269  	// if PDF was thumbnailed delete the orig jpeg
 7270  	if (isset($pdfjpeg)) {
 7271  		unlink($pdfjpeg);
 7272  	}
 7273  	// delete original webm frame
 7274  	else if (isset($webm_thumb)) {
 7275  	  unlink($webm_thumb);
 7276  	}
 7277  	
 7278  	if( $memory_limit_increased )
 7279  		ini_restore( 'memory_limit' );
 7280  
 7281  	$TN_W = $out_w;
 7282  	$TN_H = $out_h;
 7283  
 7284  	return $outpath;
 7285  }
 7286  
 7287  /* text plastic surgery */
 7288  // you can call with skip_bidi=1 if cleaning a paragraph element (like $com)
 7289  function sanitize_text( $str, $skip_bidi = 0, $allow_html = false )
 7290  {
 7291  	global $admin, $html;
 7292  	// stupid unicode-hack removal
 7293  	if( BOARD_DIR != 'jp' && BOARD_DIR != 'a' && BOARD_DIR != 'b' && !SJIS_TAGS ) {
 7294  		$str = preg_replace( '#[\x{00A0}\x{3000}]#u', ' ', $str );
 7295  
 7296  	}
 7297  
 7298  	if( !CODE_TAGS && !SJIS_TAGS) {
 7299  		$str = preg_replace( "/([ \t\f]|\xE2\x80\x8B|\xE2\x80\xA9)+/", " ", $str ); //collapse multiple spaces like HTML does
 7300  	} else {
 7301  		// fix tabs for html compression
 7302  		$str = str_replace( "\t", "    ", $str );
 7303  	}
 7304  
 7305  	$str = trim( $str ); //blankspace removal
 7306  	if( get_magic_quotes_gpc() ) { //magic quotes is deleted (?)
 7307  		$str = stripslashes( $str );
 7308  	}
 7309  
 7310  	if ($allow_html && $html == 1 && (has_level('manager') || has_flag('html') || has_flag('developer'))) {
 7311  	  $str = purify_html($str);
 7312  	} else {
 7313  		$str = htmlspecialchars( $str, ENT_QUOTES );
 7314  	}
 7315  
 7316  	if( $skip_bidi == 0 ) {
 7317  		// fix malformed bidirectional overrides - insert as many PDFs as RLOs
 7318  		//RLO
 7319  		$str .= str_repeat( "\xE2\x80\xAC", substr_count( $str, "\xE2\x80\xAE" /* U+202E */ ) );
 7320  		$str .= str_repeat( "&#8236;", substr_count( $str, "&#8238;" ) );
 7321  		$str .= str_repeat( "&#x202c;", substr_count( $str, "&#x202e;" ) );
 7322  		//RLE
 7323  		$str .= str_repeat( "\xE2\x80\xAC", substr_count( $str, "\xE2\x80\xAB" /* U+202B */ ) );
 7324  		$str .= str_repeat( "&#8236;", substr_count( $str, "&#8235;" ) );
 7325  		$str .= str_repeat( "&#x202c;", substr_count( $str, "&#x202b;" ) );
 7326  	}
 7327  
 7328  	return $str;
 7329  }
 7330  
 7331  // TODO: rewrite this to use less SQL queries
 7332  function report() {
 7333    global $captcha_bypass;
 7334    
 7335    $host = $_SERVER['REMOTE_ADDR'];
 7336    
 7337    if (isset($_COOKIE['4chan_pass'])) {
 7338      $userpwd = new UserPwd($host, MAIN_DOMAIN, $_COOKIE['4chan_pass']);
 7339    }
 7340    else {
 7341      $userpwd = new UserPwd($host, MAIN_DOMAIN);
 7342    }
 7343    
 7344    if (BOARD_DIR === 'test') {
 7345      require_once('forms/report-test.php');
 7346      require_once('modes/report-test.php');
 7347    }
 7348    else {
 7349      require_once('forms/report.php');
 7350      require_once('modes/report.php');
 7351    }
 7352    
 7353    if (!CAN_REPORT_POSTS) {
 7354      fancydie(S_CANNOTREPORTPOSTS);
 7355    }
 7356    
 7357    $no = (int)$_GET['no'];
 7358    
 7359    if ($no <= 0) {
 7360      fancydie(S_POST_DEAD);
 7361    }
 7362    
 7363    $post = report_check_post(BOARD_DIR, $no);
 7364    
 7365    if (!isset($_COOKIE['4chan_auser']) && !isset($_COOKIE['pass_enabled'])) {
 7366      $no_captcha = report_can_bypass_captcha($host, $userpwd, $post);
 7367    }
 7368    else {
 7369      $no_captcha = false;
 7370    }
 7371    
 7372    if ($_SERVER['REQUEST_METHOD'] == 'GET') {
 7373      header( 'Cache-Control: private, no-cache, must-revalidate' );
 7374      header( 'Expires: -1' );
 7375      
 7376      // Doesn't check bans here
 7377      report_check_ip( BOARD_DIR, $no, false);
 7378      
 7379      form_report(BOARD_DIR, $no, $no_captcha);
 7380    }
 7381    else {
 7382      if (valid_captcha_bypass() !== true && $no_captcha !== true) {
 7383        if (CAPTCHA_TWISTER) {
 7384          $_m = create_memcached_instance();
 7385          
 7386          if (isset($_POST['t-challenge']) && $_POST['t-challenge'] === 'noop') {
 7387            if (use_twister_captcha_credit($_m, $host, $userpwd) === false) {
 7388              error(S_CAPTCHATIMEOUT);
 7389            }
 7390          }
 7391          else if (is_twister_captcha_valid($_m, $host, $userpwd, BOARD_DIR, 1) === false) {
 7392            error(S_BADCAPTCHA);
 7393          }
 7394        }
 7395        else {
 7396          start_recaptcha_verify();
 7397          
 7398          if (!$captcha_bypass) {
 7399            end_recaptcha_verify();
 7400          }
 7401        }
 7402      }
 7403      
 7404      // Also checks for bans
 7405      report_check_ip(BOARD_DIR, $no, true);
 7406      
 7407      if (!isset($_POST['cat']) && !isset($_POST['cat_id'])) {
 7408        fancydie('Invalid category selected.');
 7409      }
 7410      
 7411      if ($_POST['cat']) {
 7412        $cat_id = (int)$_POST['cat'];
 7413      }
 7414      else if ($_POST['cat_id']) {
 7415        $cat_id = (int)$_POST['cat_id'];
 7416      }
 7417      else {
 7418        $cat_id = null;
 7419      }
 7420      
 7421      if (!$cat_id) {
 7422        fancydie('Invalid category selected.');
 7423      }
 7424      /*
 7425      if ($no_captcha) {
 7426        write_to_event_log('skip_rep_captcha', $host, [
 7427          'board' => BOARD_DIR,
 7428          'thread_id' => $post['resto'] ? $post['resto'] : $post['no'],
 7429          'post_id' => $post['no']
 7430        ]);
 7431      }
 7432      */
 7433      report_submit(BOARD_DIR, $no, $cat_id); // script dies here
 7434    }
 7435    
 7436    die( '</body></html>' );
 7437  }
 7438  
 7439  /**
 7440   * Archive deletion function
 7441   * only works on archived posts, for authed users
 7442   */
 7443  function arcdel($no, $redirect = false, $redirect_res = null) {
 7444    global $onlyimgdel;
 7445  
 7446    $delno = array();
 7447    $time = $_SERVER['REQUEST_TIME'];
 7448    reset( $_POST );
 7449    
 7450    while ($item = each($_POST)) {
 7451      if ($item[1] == 'delete') {
 7452        $delno[] = $item[0];
 7453      }
 7454    }
 7455    
 7456    $numdeletions = count($delno);
 7457    
 7458    if (!$numdeletions) {
 7459      return;
 7460    }
 7461    
 7462    $rebuild_archive_json = false;
 7463    
 7464    $rebuild = array();
 7465    
 7466    for ($i = 0; $i < $numdeletions; $i++) {
 7467      $resto = delete_post($delno[$i], '', $onlyimgdel, 0, 1, $numdeletions == 1, false, true);
 7468      if ($resto) {
 7469        $rebuild[$resto] = true;
 7470      }
 7471      else if (!$onlyimgdel) {
 7472        $rebuild_archive_json = true;
 7473      }
 7474    }
 7475    
 7476    if (!has_level('janitor')) {
 7477      mysql_global_call("INSERT INTO user_actions (ip,board,action,postno,time) VALUES (%d,'%s','delete',%d,now())", ip2long( $_SERVER["REMOTE_ADDR"] ), BOARD_DIR, $delno[0]);
 7478    }
 7479    
 7480    if ($redirect) {
 7481      if ($redirect_res) {
 7482        resredir($redirect_res, 1, true);
 7483      }
 7484      else {
 7485        updating_index();
 7486      }
 7487      
 7488      fastcgi_finish_request();
 7489    }
 7490    
 7491    foreach ($rebuild as $thread_id => $true) {
 7492      rebuild_archived_thread($thread_id);
 7493    }
 7494    
 7495    if ($rebuild_archive_json && ENABLE_JSON_THREADS) {
 7496      generate_board_archived_json();
 7497    }
 7498  }
 7499  
 7500  function user_delete( $no, $pwd, $redirect = false, $redirect_res = null )
 7501  {
 7502  	global $pwdc, $onlyimgdel, $captcha_bypass;
 7503  	
 7504  	if (UPLOAD_BOARD && $onlyimgdel) {
 7505  		error("It doesn't make any sense to do a file-only delete on a file board!");
 7506  	}
 7507  
 7508  	$delno = array();
 7509  	$time = $_SERVER['REQUEST_TIME'];
 7510  	$delflag = false;
 7511  	reset( $_POST );
 7512  
 7513  	while( $item = each( $_POST ) ) {
 7514  		if( $item[1] == 'delete' ) {
 7515  			array_push( $delno, $item[0] );
 7516  			$delflag = true;
 7517  		}
 7518  	}
 7519  	
 7520    $user_is_known = false;
 7521    
 7522    if ($pwdc) {
 7523      $userpwd = new UserPwd($_SERVER['REMOTE_ADDR'], MAIN_DOMAIN, $pwdc);
 7524      
 7525      if ($userpwd) {
 7526        $pwd = $userpwd->getPwd();
 7527        $user_is_known = $userpwd->maskLifetime() >= 900;
 7528      }
 7529      else {
 7530        $pwd = null;
 7531      }
 7532    }
 7533    else {
 7534      $pwd = null;
 7535    }
 7536    
 7537  	$numdeletions = count( $delno );
 7538  	if( !$numdeletions ) return;
 7539  	$flag = false;
 7540  
 7541  	if( !has_level( 'janitor' ) ) {
 7542  		$n = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='delete' and time >= subdate(now(), interval 1 hour)", RENZOKU_DEL_HOURLY, ip2long( $_SERVER['REMOTE_ADDR'] ) );
 7543  		list( $h ) = mysql_fetch_row( $n );
 7544  
 7545  		if( $h ) {
 7546  			//check_fail_floodcheck($no);
 7547  			error(S_FLOOD_DEL);
 7548  		}
 7549  
 7550  		$n = mysql_global_call( "select count(*)>%d from user_actions where ip=%d and action='delete' and time >= subdate(now(), interval 1 day)", RENZOKU_DEL_DAILY, ip2long( $_SERVER['REMOTE_ADDR'] ) );
 7551  		list( $h ) = mysql_fetch_row( $n );
 7552  
 7553  		if( $h ) {
 7554  			//check_fail_floodcheck($no);
 7555  			error(S_FLOOD_DEL);
 7556  		}
 7557  	}
 7558  
 7559  	$rebuild = array(); // keys are pages that need to be rebuilt (0 is index, of course)
 7560  	
 7561  	$lazy_rebuild = false;
 7562  	
 7563  	if (isset($_POST['tool']) && $_POST['tool']) {
 7564  	  $tool = $_POST['tool'];
 7565  	}
 7566  	else {
 7567  	  $tool = null;
 7568  	}
 7569  	
 7570  	// Manual, single post deletion. Only rebuilds one page if deleting a reply.
 7571  	if ($numdeletions == 1) {
 7572  		$resto = delete_post( $delno[0], $pwd, $onlyimgdel, 0, 1, $numdeletions == 1, false, false, $tool, $user_is_known );
 7573  		if ($resto) {
 7574  			$rebuild[$resto] = 1;
 7575  		  calculate_indexes_to_rebuild($resto);
 7576    		$lazy_rebuild = true;
 7577  		}
 7578  	}
 7579  	// Other (multi, automatic, etc...)
 7580  	else {
 7581  		for( $i = 0; $i < $numdeletions; $i++ ) {
 7582  			$resto = delete_post( $delno[$i], $pwd, $onlyimgdel, 0, 1, $numdeletions == 1, false, false, $tool, $user_is_known );
 7583  			if( $resto ) {
 7584  				$rebuild[$resto] = 1;
 7585  			}
 7586  		}
 7587  	}
 7588  	
 7589  	if (!has_level('janitor')) {
 7590  		mysql_global_call("INSERT INTO user_actions (ip,board,action,postno,time) VALUES (%d,'%s','delete',%d,now())", ip2long( $_SERVER["REMOTE_ADDR"] ), BOARD_DIR, $delno[0]);
 7591  	}
 7592  	
 7593    if ($redirect) {
 7594      if ($redirect_res) {
 7595        resredir($redirect_res, 1, true);
 7596      }
 7597      else {
 7598        updating_index();
 7599      }
 7600      
 7601      fastcgi_finish_request();
 7602    }
 7603  	
 7604  	rebuild_deletions($rebuild, $lazy_rebuild);
 7605  }
 7606  
 7607  function updatelog_remote( $no, $noidx )
 7608  {
 7609  	if( !has_level() ) die( '' ); // anti dos
 7610  	$no    = intval( $no );
 7611  	$noidx = !!$noidx;
 7612    // FIXME, running this when $noidx is true breaks cross-thread quotelinks.
 7613  	updatelog( $no, $noidx );
 7614  }
 7615  
 7616  function _print( $s, $echo = 1 )
 7617  {
 7618  	if( $echo ) {
 7619  		echo $s;
 7620  
 7621  		return;
 7622  	}
 7623  
 7624  	ob_flush();
 7625  	flush();
 7626  
 7627  	echo $s;
 7628  	echo str_repeat( ' ', 256 ) . "\n";
 7629  
 7630  	ob_flush();
 7631  	flush();
 7632  }
 7633  
 7634  function fancystyle()
 7635  {
 7636  	$style = <<<HTML
 7637  <style type="text/css">
 7638  body {
 7639  	font-family: Helvetica, Arial, sans-serif;
 7640  	font-size: 12pt;
 7641  }
 7642  
 7643  h1 {
 7644  	margin: 0;
 7645  	padding: 0;
 7646  }
 7647  </style>
 7648  HTML;
 7649  
 7650  
 7651  	return $style;
 7652  }
 7653  
 7654  function rebuild_catalog( $shutup = false )
 7655  {
 7656  	if( !has_level() ) die();
 7657  	if( !$shutup ) {
 7658  		echo fancystyle();
 7659  		echo '<h1>Rebuilding catalog...</h1>';
 7660  	}
 7661  
 7662  	$start = microtime( true );
 7663  	generate_catalog();
 7664  	$time = round( microtime( true ) - $start, 6 );
 7665  
 7666  	if( !$shutup ) {
 7667  		echo 'Done!<br><br>Rebuilding took ' . $time . ' seconds.<br><br>Redirecting to catalog...<br><br><meta http-equiv="refresh" content="5;URL=//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR .  '/catalog">';
 7668  		die();
 7669  	}
 7670  }
 7671  
 7672  function rebuild_boards_json()
 7673  {
 7674  	if( !has_level() ) die();
 7675  	echo fancystyle();
 7676  	echo '<h1>Rebuilding boards.json...</h1>';
 7677  
 7678  	$start = microtime( true );
 7679  	$query = mysql_global_call( "SELECT dir as board,name as title FROM boardlist ORDER BY board ASC" );
 7680  
 7681  	$boards = array();
 7682  
 7683  	$host = 'https://sys.int';
 7684  
 7685  	$post = array(
 7686  	  'mode' => 'cataloginfo'
 7687  	);
 7688  
 7689  	while( $row = mysql_fetch_assoc( $query ) ) {
 7690  		if( $row['board'] == 'vp' ) $row['title'] = 'Pokémon';
 7691  		//cataloginfo
 7692  		$url = "$host/{$row['board']}/imgboard.php";
 7693  		
 7694  		$ch = rpc_start_request($url, $post, null, true);
 7695  		
 7696  		$response = rpc_finish_request($ch, $error, $httperror);
 7697  		
 7698  		if (!$response) {
 7699  			die( 'Could not generate info for /' . $row['board'] . '/; ' . $error );
 7700  		}
 7701  		
 7702  		$json = json_decode($response, true);
 7703  
 7704  		foreach( $json as $key => $val ) {
 7705  			if( $key != 'board' && ctype_digit( $val ) ) $val = (int)$val;
 7706  			$row[$key] = $val;
 7707  		}
 7708  
 7709  		$boards['boards'][] = $row;
 7710  	}
 7711  	
 7712  	// Dump resulting json on /test/
 7713    if (BOARD_DIR === 'test') {
 7714      echo '<br>';
 7715      echo json_encode($boards, JSON_HEX_AMP | JSON_PRETTY_PRINT);
 7716      echo '<br>';
 7717    }
 7718    else {
 7719      $_json = json_encode($boards, JSON_HEX_AMP);
 7720      print_page(BOARDS_ROOT . 'boards.json', $_json);
 7721    }
 7722  
 7723  	$time = round( microtime( true ) - $start, 6 );
 7724  	echo 'Done!<br><br>Rebuilding took ' . $time . ' seconds.<br><br>No redirect here boss. Off you go.';
 7725  	die();
 7726  }
 7727  
 7728  function rebuild( $all = 0 )
 7729  {
 7730  	global $rebuildall, $fwritetimer;
 7731  	if( !has_level() ) die( '' ); // anti dos
 7732    
 7733  	if (has_flag('developer')) {
 7734      error_reporting(E_ALL);
 7735  	}
 7736  	
 7737  	header( "Pragma: no-cache" );
 7738  	_print(fancystyle());
 7739  	$l = $all ? 'all' : 'missing';
 7740  
 7741  	_print( "Rebuilding $l replies and pages... <a href=\"//boards." . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/\">Go back</a><br><br>\n" );
 7742  	log_cache();
 7743  	trim_db();
 7744  	trim_archive();
 7745  	mysql_board_lock( true );
 7746  	$starttime = microtime( true );
 7747  	$query = "SELECT no, resto FROM `" . SQLLOG . "` WHERE resto = 0 AND archived = 0 ORDER BY root DESC";
 7748  	$treeline = mysql_board_call($query);
 7749  	if (!$treeline) {
 7750  		echo S_SQLFAIL;
 7751  	}
 7752  	mysql_board_unlock();
 7753  	_print( "Writing...<br>\n" );
 7754  	if( $all ) {
 7755  		while( list( $no, $resto ) = mysql_fetch_row( $treeline ) ) {
 7756  			if( !$resto ) {
 7757  				_print( "Writing No.$no... " );
 7758  				updatelog( $no, 1 );
 7759  				$ext = TEST_BOARD ? "rp cache: ".realpath_cache_size() : "";
 7760  				_print( "<b>DONE!</b><br> $ext\n" );
 7761  			}
 7762  		}
 7763  		_print( "Writing index pages... " );
 7764  		updatelog();
 7765  		_print( "<b>DONE!</b><br>" );
 7766  
 7767      if (ENABLE_CATALOG) {
 7768        _print( "Writing catalog..." );
 7769        generate_catalog( true );
 7770        _print( "<b>DONE!</b><br>" );
 7771      }
 7772      
 7773      if (ENABLE_ARCHIVE) {
 7774        _print( "Writing archive..." );
 7775        rebuild_archive_list();
 7776        _print( "<b>DONE!</b><br>" );
 7777      }
 7778  	}
 7779    
 7780  	$totaltime = microtime( true ) - $starttime;
 7781  	$proctimer = $totaltime - $fwritetimer;
 7782  	
 7783  	$peakmem = memory_get_peak_usage(true) / (1024*1024.0);
 7784  	$usedmem = memory_get_usage(true) / (1024*1024.0);
 7785  	
 7786  	$redir = '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . '/';
 7787  	
 7788  echo <<<END
 7789  <br>Total running time (lock excluded): $totaltime seconds.
 7790  <br>Composed of:
 7791  <br>Time spent writing files: $fwritetimer seconds.
 7792  <br>Time spent processing: $proctimer seconds.
 7793  <br>Memory: Peak: $peakmem MB Final: $usedmem MB
 7794  <br>Pages created.
 7795  <br><br>
 7796  END;
 7797  /*
 7798  if (!TEST_BOARD) {
 7799  echo <<<END
 7800  Redirecting back to board.
 7801  <meta http-equiv="refresh" content="10;URL=$redir">
 7802  END;
 7803  }
 7804  */
 7805  }
 7806  
 7807  function rebuild_after_deletion( $no )
 7808  {
 7809  	if( !has_level() ) die();
 7810  
 7811  	mysql_board_lock( true );
 7812  
 7813  	if( !$treeline = mysql_board_call( "SELECT no FROM `" . SQLLOG . "` WHERE no = %d", $no ) ) {
 7814  		mysql_board_unlock();
 7815  		die( S_POSTGONE );
 7816  	}
 7817  
 7818  	log_cache( 0, $no );
 7819  	mysql_board_unlock();
 7820  
 7821  	updatelog( $no, 1 );
 7822  
 7823  	die( $no . ' Rebuilt OK!' );
 7824  }
 7825  
 7826  function updating_index()
 7827  {
 7828  	$proto = ( stripos( $_SERVER["HTTP_REFERER"], "https" ) !== false ) ? "https:" : "http:";
 7829  	echo "<!doctype html><head><meta http-equiv=\"refresh\" content=\"2;URL=$proto"
 7830      . '//boards.' . L::d(BOARD_DIR) . '/' . BOARD_DIR . "/\"><title>"
 7831      . S_UPDATING_INDEX . "</title></head><body><table style=\"font-family:times,serif;font-size:36pt;text-align:center;width:100%;height:300px;\"><td><strong>"
 7832      . S_UPDATING_INDEX . "</strong></td></table>";
 7833  }
 7834  
 7835  function require_request_method( $method )
 7836  {
 7837  	if (!isset($_SERVER['REMOTE_ADDR'])) return;
 7838  	$umethod = $_SERVER["REQUEST_METHOD"];
 7839  	//$req     = htmlspecialchars( $_REQUEST["mode"] );
 7840  	if( $umethod == "OPTIONS" || ( $umethod == "HEAD" && $method == "GET" ) ) return;
 7841  	if( $umethod != $method ) {
 7842  		error( S_REJECTTEXTBAN );
 7843  	}
 7844  }
 7845  
 7846  function get_catalog_info() {
 7847    $arr = array(
 7848      'ws_board'            => (int)(CATEGORY == 'ws'),
 7849      'per_page'            => (int)DEF_PAGES,
 7850      'pages'               => (int)PAGE_MAX,
 7851      'max_filesize'        => ((int)MAX_KB) * 1024,
 7852      'max_webm_filesize'   => ((int)MAX_WEBM_FILESIZE) * 1024,
 7853      'max_comment_chars'   => (int)MAX_COM_CHARS,
 7854      'max_webm_duration'   => (int)MAX_WEBM_DURATION,
 7855      'bump_limit'          => (int)MAX_RES,
 7856      'image_limit'         => (int)MAX_IMGRES,
 7857      'cooldowns'           => array(
 7858        'threads'          => (int)RENZOKU3,
 7859        'replies'          => (int)RENZOKU,
 7860        'images'           => (int)RENZOKU2
 7861      )
 7862    );
 7863    
 7864    if (defined('META_DESCRIPTION')) {
 7865      $arr['meta_description'] = META_DESCRIPTION;
 7866    }
 7867    
 7868    if (SPOILERS) {
 7869      $arr['spoilers'] = 1;
 7870      if (SPOILER_NUM) {
 7871        $arr['custom_spoilers'] = (int)SPOILER_NUM;
 7872      }
 7873    }
 7874    
 7875    if (DISP_ID) {
 7876      $arr['user_ids'] = 1;
 7877    }
 7878    
 7879    if (ENABLE_ARCHIVE) {
 7880      $arr['is_archived'] = 1;
 7881    }
 7882    
 7883    if (CODE_TAGS) {
 7884      $arr['code_tags'] = 1;
 7885    }
 7886    
 7887    if (SJIS_TAGS) {
 7888      $arr['sjis_tags'] = 1;
 7889    }
 7890    
 7891    if (JSMATH) {
 7892      $arr['math_tags'] = 1;
 7893    }
 7894    
 7895    if (SHOW_COUNTRY_FLAGS) {
 7896      $arr['country_flags'] = 1;
 7897    }
 7898    
 7899    if (ENABLE_BOARD_FLAGS) {
 7900      $arr['board_flags'] = get_board_flags_selector();
 7901    }
 7902    
 7903    if (ENABLE_WEBM_AUDIO) {
 7904      $arr['webm_audio'] = 1;
 7905    }
 7906    
 7907    if (MIN_W > 1) {
 7908      $arr['min_image_width'] = (int)MIN_W;
 7909    }
 7910    
 7911    if (MIN_H > 1) {
 7912      $arr['min_image_height'] = (int)MIN_H;
 7913    }
 7914    
 7915    if (TEXT_ONLY) {
 7916      $arr['text_only'] = 1;
 7917      $arr['require_subject'] = 1;
 7918    }
 7919    
 7920    if (FORCED_ANON) {
 7921      $arr['forced_anon'] = 1;
 7922    }
 7923    
 7924    if (REQUIRE_SUBJECT) {
 7925      $arr['require_subject'] = 1;
 7926    }
 7927    
 7928    if (ENABLE_PAINTERJS) {
 7929      $arr['oekaki'] = 1;
 7930    }
 7931    
 7932    die(json_encode($arr));
 7933  }
 7934  
 7935  /**
 7936   * Generates context to append to thread links.
 7937   */
 7938  function generate_href_context($sub, $com) {
 7939    if (JANITOR_BOARD) {
 7940      return '';
 7941    }
 7942    
 7943    $context = '';
 7944    
 7945    if (strpos($sub, 'SPOILER<>') === 0) {
 7946      $sub = substr($sub, 9);
 7947    }
 7948    
 7949    if ($sub !== '') {
 7950      $context = cleanup_context_string($sub);
 7951    }
 7952    
 7953    if ($context === '' && $com !== '') {
 7954      $context = $com;
 7955      
 7956      if (strpos($context, '<br>') !== false) {
 7957        $context = str_replace('<br>', "\n", $context);
 7958        $has_br = true;
 7959      }
 7960      else {
 7961        $has_br = false;
 7962      }
 7963      
 7964      $context = preg_replace('/(^|\s)https?:\/\/[^\s]{4,}/', '', $context);
 7965      
 7966      if (strpos($context, '<span class="abbr">') !== false) {
 7967        $context = preg_replace('/<span class="abbr">.*<\/table>/', '', $context); // ???
 7968      }
 7969      if (strpos($context, '<strong') !== false) {
 7970        $context = preg_replace('/<strong [^>]+>.*<\/strong>/', '', $context); // ???
 7971      }
 7972      
 7973      if ($has_br) {
 7974        $context = ltrim($context);
 7975        $context = explode("\n", $context)[0];
 7976      }
 7977      
 7978      $context = preg_replace('/<[^>]+>/', ' ', $context);
 7979      $context = cleanup_context_string($context);
 7980    }
 7981  
 7982      
 7983    return $context;
 7984  }
 7985  
 7986  /**
 7987   * Generates page title from subjects and comments
 7988   * params must be already html-escaped.
 7989   */
 7990  function generate_page_title($thread_id, $sub, $com) {
 7991    if (JANITOR_BOARD) {
 7992      return strip_tags(TITLE);
 7993    }
 7994    
 7995    if (UPLOAD_BOARD) {
 7996      $sub = preg_replace('/^(\d+)\|/', '', $sub);
 7997    }
 7998    
 7999    $context = '';
 8000    
 8001    if (strpos($sub, 'SPOILER<>') === 0) {
 8002      $sub = substr($sub, 9);
 8003    }
 8004    
 8005    if ($sub !== '') {
 8006      $context = $sub;
 8007    }
 8008    
 8009    if ($context === '' && $com !== '') {
 8010      if (SJIS_TAGS && strpos($com, '<span class="sjis"') !== false) {
 8011        $com = preg_replace('/<span class="sjis".+?<\/span>/', '[SJIS]', $com);
 8012      }
 8013      $context = str_replace('<br>', ' ', $com);
 8014      $context = htmlspecialchars_decode($context, ENT_QUOTES);
 8015      $context = mb_substr(strip_tags($context), 0, 50);
 8016      $context = htmlspecialchars($context, ENT_QUOTES);
 8017    }
 8018    
 8019    if ($context === '') {
 8020      $context = 'No.' . $thread_id;
 8021    }
 8022    
 8023    if (BOARD_DIR === 's4s') {
 8024      return '[' . BOARD_DIR . '] - ' . $context;
 8025    }
 8026    else {
 8027      return '/' . BOARD_DIR . '/ - ' . $context;
 8028    }
 8029  }
 8030  
 8031  /**
 8032   * Generates metatags from subjects and comments
 8033   * params must be already html-escaped.
 8034   */
 8035  function generate_page_metatags($sub, $com) {
 8036    if (JANITOR_BOARD) {
 8037      return null;
 8038    }
 8039    
 8040    $context = '';
 8041    
 8042    if (strpos($sub, 'SPOILER<>') === 0) {
 8043      $sub = substr($sub, 9);
 8044    }
 8045    
 8046    if ($sub !== '') {
 8047      $context = $sub;
 8048    }
 8049    
 8050    $ell = '';
 8051    
 8052    if ($context === '' && $com !== '') {
 8053      $context = preg_replace('/(<br>|\s)+/', ' ', $com);
 8054      $context = htmlspecialchars_decode(strip_tags($context), ENT_QUOTES);
 8055      
 8056      if (mb_strlen($context) > 100) {
 8057        $ell = '...';
 8058        $context = mb_substr($context, 0, 100);
 8059      }
 8060    }
 8061    else {
 8062      $context = htmlspecialchars_decode($context, ENT_QUOTES);
 8063    }
 8064    
 8065    if (empty($context)) {
 8066      return null;
 8067    }
 8068    else {
 8069      $words = preg_split('/[[:punct:]\s]+/', $context);
 8070      $keywords = '';
 8071      foreach ($words as $word) {
 8072        if (strlen($word) > 3) {
 8073          $keywords .= ',' . $word;
 8074        }
 8075      }
 8076    }
 8077    
 8078    $context = htmlspecialchars($context, ENT_QUOTES);
 8079    $keywords = htmlspecialchars($keywords, ENT_QUOTES);
 8080    
 8081    return array($context.$ell, $keywords);
 8082  }
 8083  
 8084  function cleanup_context_string($context) {
 8085    $context = htmlspecialchars_decode($context, ENT_QUOTES);
 8086    
 8087    $context = strtolower(preg_replace('/[^a-zA-Z0-9\s]+/', '', $context));
 8088    
 8089    $length = 0;
 8090    
 8091    $words = explode(' ', $context);
 8092    
 8093    $context = array();
 8094    
 8095    foreach ($words as $word) {
 8096      if ($word === '') {
 8097        continue;
 8098      }
 8099      
 8100      $length += strlen($word) + 1;
 8101      
 8102      if ($length > 50) {
 8103        break;
 8104      }
 8105      
 8106      $context[] = $word;
 8107    }
 8108    
 8109    $context = implode('-', $context);
 8110    
 8111    return htmlspecialchars($context, ENT_QUOTES);
 8112  }
 8113  
 8114  /**
 8115   * Embedded data detection
 8116   * Dies if the PNG or JPG file contains embedded data.
 8117   * Returns true if the GIF file was modified, otherwise returns false.
 8118   */
 8119  function cleanup_uploaded_file($file, $type) {
 8120    $full_size = filesize($file);
 8121    
 8122    // 50KB
 8123    $max_delta = 51200;
 8124    
 8125    switch ($type) {
 8126      case '.png':
 8127        $clean_size = get_clean_png_size($file);
 8128        break;
 8129      case '.jpg':
 8130        if ($full_size < 204800) {
 8131          return false;
 8132        }
 8133        $clean_size = get_clean_jpg_size($file);
 8134        break;
 8135      case '.gif':
 8136        if ($full_size < 204800) {
 8137          return false;
 8138        }
 8139        $clean_size = get_clean_gif_size($file);
 8140        break;
 8141      case '.swf':
 8142        return true; // Why?
 8143      default:
 8144        return false;
 8145    }
 8146    
 8147    if ($clean_size === false) {
 8148      return false;
 8149    }
 8150    
 8151    // PNGs can still fail the check even if no size delta is found
 8152    if ($clean_size === -1) {
 8153    	if ($type === '.gif') {
 8154        $file = escapeshellcmd($file);
 8155        $res = system("/usr/local/bin/gifsicle --no-extensions \"$file\" -o \"$file\" >/dev/null 2>&1");
 8156        if ($res !== false) {
 8157          return true;
 8158        }
 8159        else {
 8160        	return false;
 8161        }
 8162    	}
 8163    	else {
 8164      	error(S_IMGCONTAINSFILE, $file);
 8165    	}
 8166    }
 8167    
 8168    $delta_size = $full_size - $clean_size;
 8169    
 8170    if ($delta_size > $max_delta) {
 8171      if ($type === '.gif') {
 8172        $file = escapeshellcmd($file);
 8173        $res = system("/usr/local/bin/gifsicle --no-extensions \"$file\" -o \"$file\" >/dev/null 2>&1");
 8174        if ($res !== false) {
 8175          return true;
 8176        }
 8177      }
 8178      else {
 8179        error(S_IMGCONTAINSFILE, $file);
 8180      }
 8181    }
 8182    
 8183    return false;
 8184  }
 8185  
 8186  // Returns the size of critical data or -1 if the file contains extensions.
 8187  function get_clean_gif_size($file) {
 8188    $file = escapeshellcmd($file);
 8189    
 8190    $binary = '/usr/local/bin/gifsicle';
 8191    
 8192    $res = shell_exec("$binary --sinfo \"$file\" 2>&1");
 8193    
 8194    if ($res !== null) {
 8195      $size = 0;
 8196      
 8197      if (preg_match('/  extensions [0-9]+/', $res)) {
 8198        return -1;
 8199      }
 8200      
 8201      if (preg_match_all('/compressed size ([0-9]+)/', $res, $m)) {
 8202        foreach ($m[1] as $frame_size) {
 8203          $size += (int)$frame_size;
 8204        }
 8205        
 8206        return $size;
 8207      }
 8208    }
 8209    
 8210    return false;
 8211  }
 8212  
 8213  // Returns the number of bytes in critical chunks or -1 if too many IDAT chunks are found
 8214  // Returns false on error
 8215  function get_clean_png_size($file) {
 8216    $file = escapeshellcmd($file);
 8217    
 8218    $binary = '/usr/local/bin/pngcrush';
 8219    
 8220    $res = shell_exec("$binary -m 1 -n -v \"$file\" 2>&1 1>/dev/null");
 8221    
 8222    if ($res !== null) {
 8223      if (preg_match('/Reading (?:iTXt|tEXt|zTXt) chunk,/', $res, $m)) {
 8224        if (preg_match('/  [a-z]oo: /i', $res)) {
 8225          return -1;
 8226        }
 8227        else if (preg_match('/  (?:Software|Creation Time): ([=a-zA-Z0-9]{10,})\n/', $res, $ct)) {
 8228          $_b64 = base64_decode($ct[0]);
 8229          if ($_b64 && preg_match('/[0-9]/', $_b64)) {
 8230            write_to_event_log('png_link', $_SERVER['REMOTE_ADDR'], [
 8231              'board' => BOARD_DIR,
 8232              'meta' => htmlspecialchars($res)
 8233            ]);
 8234            return -1;
 8235          }
 8236        }
 8237      }
 8238      
 8239      if (preg_match('/in critical chunks\s+=\s+([0-9]+)/', $res, $m)) {
 8240        return (int)$m[1];
 8241      }
 8242    }
 8243    
 8244    return false;
 8245  }
 8246  
 8247  function get_clean_jpg_size($file) {
 8248    $eof = false;
 8249    
 8250    $img = fopen($file, 'rb');
 8251    
 8252    $data = fread($img, 2);
 8253    
 8254    if ($data !== "\xff\xd8") {
 8255      fclose($img);
 8256      return false;
 8257    }
 8258    
 8259    while (!feof($img)) {
 8260      $data = fread($img, 1);
 8261      
 8262      if ($data !== "\xff") {
 8263        continue;
 8264      }
 8265      
 8266      while (!feof($img)) {
 8267        $data = fread($img, 1);
 8268        
 8269        if ($data !== "\xff") {
 8270          break;
 8271        }
 8272      }
 8273      
 8274      if (feof($img)) {
 8275        break;
 8276      }
 8277      
 8278      $byte = unpack('C', $data)[1];
 8279      
 8280      if ($byte === 217) {
 8281        $eof = ftell($img);
 8282        break;
 8283      }
 8284      
 8285      if ($byte === 0 || $byte === 1 || ($byte >= 208 && $byte <= 216)) {
 8286        continue;
 8287      }
 8288      
 8289      $data = fread($img, 2);
 8290      
 8291      $length = unpack('n', $data)[1];
 8292      
 8293      if ($length < 1) {
 8294        break;
 8295      }
 8296      
 8297      fseek($img, $length - 2, SEEK_CUR);
 8298    }
 8299    
 8300    fclose($img);
 8301    
 8302    return $eof;
 8303  }
 8304  
 8305  /**
 8306   * Generates a "Pass User Since YEAR" string for pass users
 8307   */
 8308  function get_since_4chan($pass_id) {
 8309    $query = "SELECT UNIX_TIMESTAMP(purchase_date) FROM pass_users WHERE user_hash = '%s' ORDER BY purchase_date ASC LIMIT 1";
 8310    
 8311    $res = mysql_global_call($query, $pass_id);
 8312    
 8313    if (!$res) {
 8314      return 0;
 8315    }
 8316    
 8317    $row = mysql_fetch_row($res);
 8318    
 8319    $ts = (int)$row[0];
 8320    
 8321    if (!$ts) {
 8322      return 0;
 8323    }
 8324    
 8325    $ts = date('Y', $ts);
 8326    
 8327    if (!$ts) {
 8328      return 0;
 8329    }
 8330    
 8331    return (int)$ts;
 8332  }
 8333  /*
 8334  // Halloween 2017
 8335  function get_halloween_score($pass_id) {
 8336    $query = "SELECT score FROM halloween_tricks WHERE user_hash = '%s' LIMIT 1";
 8337    
 8338    $res = mysql_global_call($query, $pass_id);
 8339    
 8340    if (!$res) {
 8341      return 0;
 8342    }
 8343    
 8344    $row = mysql_fetch_row($res);
 8345    
 8346    if (!$row) {
 8347      return 0;
 8348    }
 8349    
 8350    return (int)$row[0];
 8351  }
 8352  
 8353  // Halloween 2017
 8354  /*
 8355  function get_halloween_dummy_pass($name) {
 8356    if (!$name) {
 8357      return '';
 8358    }
 8359    
 8360    $hashed_bits = hash_hmac('sha1', $name, 'CIaPCUkJq9n3fskmKC06tquCV/2edWqbgBeY9pk7RlQ', true);
 8361    
 8362    $hashed_name = base64_encode($hashed_bits);
 8363    
 8364    if (!$hashed_name) {
 8365      return '';
 8366    }
 8367    
 8368    return '_' . substr($hashed_name, 0, 9);
 8369  }
 8370  
 8371  // Halloween 2017
 8372  function process_halloween_score($com, $thread_id, $this_pass_id, $this_pwd, $pass_is_bannable) {
 8373    if ($com === '') {
 8374      return;
 8375    }
 8376    
 8377    $thread_id = (int)$thread_id;
 8378    
 8379    if (!$thread_id) {
 8380      return;
 8381    }
 8382    
 8383    if (preg_match_all('/&gt;&gt;([0-9]{4,})/', $com, $m) === 1) {
 8384      $post_id = (int)$m[1][0];
 8385      
 8386      if (!$post_id || $post_id == $thread_id) {
 8387        return;
 8388      }
 8389      
 8390      $query = 'SELECT host, pwd, 4pass_id FROM `' . SQLLOG . '` WHERE no = ' . $post_id . ' AND resto = ' . $thread_id;
 8391      
 8392      $res = mysql_board_call($query);
 8393      
 8394      if (!$res) {
 8395        return;
 8396      }
 8397      
 8398      $row = mysql_fetch_assoc($res);
 8399      
 8400      if (!$row) {
 8401        return;
 8402      }
 8403      
 8404      // Check if same person
 8405      if (!$row['4pass_id'] || $row['4pass_id'] == $this_pass_id || $row['pwd'] == $this_pwd || $row['host'] == $_SERVER['REMOTE_ADDR']) {
 8406        return;
 8407      }
 8408      
 8409      $long_ip = ip2long($_SERVER['REMOTE_ADDR']);
 8410      
 8411      if (!$long_ip) {
 8412        return;
 8413      }
 8414      
 8415      // Check if already gave points
 8416      $query = "SELECT 1 FROM `halloween_votes` WHERE long_ip = $long_ip AND board = '" . BOARD_DIR . "' AND post_id = $post_id";
 8417      
 8418      $res = mysql_global_call($query);
 8419      
 8420      if (!$res) {
 8421        return;
 8422      }
 8423      
 8424      if (mysql_num_rows($res) > 0) {
 8425        return;
 8426      }
 8427      
 8428      // Check if the user is known
 8429      if (!spam_filter_is_user_known($long_ip, BOARD_DIR, $pass_is_bannable ? $this_pwd : null)) {
 8430        return;
 8431      }
 8432      
 8433      // Good to go
 8434      $query = <<<SQL
 8435  INSERT INTO halloween_tricks (user_hash, score) VALUES ('%s', 1)
 8436  ON DUPLICATE KEY UPDATE score = score + 1
 8437  SQL;
 8438      
 8439      mysql_global_call($query, $row['4pass_id']);
 8440      
 8441      $query = "INSERT INTO halloween_votes (long_ip, board, post_id) VALUES ($long_ip, '" . BOARD_DIR . "', $post_id)";
 8442      
 8443      mysql_global_call($query);
 8444    }
 8445  }
 8446  
 8447  // Halloween 2017
 8448  function decrease_halloween_score($post_id, $ratio = 0.75) {
 8449    $post_id = (int)$post_id;
 8450    
 8451    if (!$post_id) {
 8452      return;
 8453    }
 8454    
 8455    $query = 'SELECT 4pass_id FROM `' . SQLLOG . '` WHERE no = ' . $post_id;
 8456    
 8457    $res = mysql_board_call($query);
 8458    
 8459    if (!$res) {
 8460      return;
 8461    }
 8462    
 8463    $pass_id = mysql_fetch_row($res)[0];
 8464    
 8465    if (!$pass_id) {
 8466      return;
 8467    }
 8468    
 8469    $query = "UPDATE halloween_tricks SET score = FLOOR(score * %.2f) WHERE user_hash = '%s' LIMIT 1";
 8470    
 8471    mysql_global_call($query, $ratio, $pass_id);
 8472  }
 8473  
 8474  // Halloween 2017
 8475  function get_halloween_css_cls($trick_count) {
 8476    if ($trick_count >= 1000) {
 8477      return " n-jol-6";
 8478    }
 8479    if ($trick_count >= 500) {
 8480      return " n-jol-5";
 8481    }
 8482    if ($trick_count >= 200) {
 8483      return " n-jol-4";
 8484    }
 8485    if ($trick_count >= 100) {
 8486      return " n-jol-3";
 8487    }
 8488    if ($trick_count >= 50) {
 8489      return " n-jol-2";
 8490    }
 8491    if ($trick_count >= 25) {
 8492      return " n-jol-1";
 8493    }
 8494    return '';
 8495  }
 8496  */
 8497  
 8498  /**
 8499   * Checks if the user has a recent ban request.
 8500   * dies with an error if user needs to be blocked.
 8501   */
 8502  function check_for_ban_request($ip, $pwd = null) {
 8503    $time_lim = BLOCK_ON_BR_LEN;
 8504    
 8505    $clauses = [];
 8506    
 8507    $clauses[] = "host = '" . mysql_real_escape_string($ip) . "'";
 8508    
 8509    if ($pwd) {
 8510      $clauses[] = "pwd = '" . mysql_real_escape_string($pwd) . "'";
 8511    }
 8512    
 8513    $board_sql = mysql_real_escape_string(BOARD_DIR);
 8514    
 8515    $clauses = implode(' OR ', $clauses);
 8516    
 8517    $query = <<<SQL
 8518  SELECT tpl_name, board, TIMESTAMPDIFF(MINUTE, NOW() - $time_lim, ts) diff
 8519  FROM `ban_requests`
 8520  WHERE ($clauses)
 8521  AND (board = '$board_sql' OR global = 1)
 8522  AND warn_req = 0
 8523  AND (ts > DATE_SUB(NOW(), $time_lim) OR ban_template IN (1, 2, 123, 126))
 8524  LIMIT 1
 8525  SQL;
 8526    
 8527    $res = mysql_global_call($query);
 8528    
 8529    if (!$res || mysql_num_rows($res) !== 1) {
 8530      return false;
 8531    }
 8532    
 8533    $row = mysql_fetch_assoc($res);
 8534    
 8535    $time = (int)$row['diff'];
 8536    
 8537    $tpl_name = rtrim(preg_replace('/\[[^\]]+\]/', '', $row['tpl_name']));
 8538    
 8539    // Non-expiring blocks for some global templates
 8540    if ($time < 0) {
 8541      error(sprintf(S_BRBLOCKED_2, $tpl_name));
 8542    }
 8543    
 8544    $str = $time <= 1 ? '1 minute' : "$time minutes";
 8545    
 8546    error(sprintf(S_BRBLOCKED, $tpl_name, $str));
 8547  }
 8548  
 8549  /**
 8550   * Checks if the IP is banned, also checks for ban evasion
 8551   * return 0 for not banned, 1 for banned, 2 for warned
 8552   * If the ban has expired, delete the banned thumbnail and de-activate the ban
 8553   */
 8554  function check_for_ban($ip, $fields = array(), $thread_id = 0, $user_verified = false) {
 8555    global $captcha_bypass;
 8556    
 8557    // Skip IP bans when the user has a valid 4chan Pass
 8558    $skip_ip_bans = isset($fields['4pass_id']) && $captcha_bypass;
 8559    
 8560    if (!$skip_ip_bans) {
 8561      $fields['host'] = $ip;
 8562    }
 8563    
 8564    $expired = [];
 8565    
 8566    $is_banned = 0;
 8567    
 8568    foreach ($fields as $key => $value) {
 8569  $query =<<<SQL
 8570  SELECT no, global, board, post_num, template_id, 4pass_id, admin, reason,
 8571  UNIX_TIMESTAMP(now) as starts_on, UNIX_TIMESTAMP(length) as ends_on
 8572  FROM banned_users
 8573  WHERE active = 1 AND $key = '%s'
 8574  SQL;
 8575      
 8576      $result = mysql_global_call($query, $value);
 8577      
 8578      // Not banned
 8579      if (mysql_num_rows($result) < 1) {
 8580        continue;
 8581      }
 8582      
 8583      while ($ban = mysql_fetch_assoc($result)) {
 8584        $end = (int)$ban['ends_on'];
 8585        
 8586        // Warning
 8587        if ($end && ($end - (int)$ban['starts_on'] < 1)) {
 8588          $is_banned = 2;
 8589          break 2;
 8590        }
 8591        
 8592        // Ban has expired
 8593        if ($end && $end <= $_SERVER['REQUEST_TIME']) {
 8594          $expired[] = $ban;
 8595          continue;
 8596        }
 8597        
 8598        // Skip GR14 bans for pass users
 8599        if ($key == '4pass_id') {
 8600          if ($ban['template_id'] == 124) { // Global 14 - Proxy, VPN, or Tor Node
 8601            continue;
 8602          }
 8603        }
 8604        
 8605        // Skip proxy autobans for verified users
 8606        if ($user_verified && $key == 'host' && $ban['admin'] === 'Auto-ban') {
 8607          if (strpos($ban['reason'], 'Proxy') !== false) {
 8608            continue;
 8609          }
 8610        }
 8611        
 8612        if ($ban['global'] || $ban['board'] == BOARD_DIR) {
 8613          $is_banned = 1;
 8614          break 2;
 8615        }
 8616      }
 8617    }
 8618    
 8619    // Cleanup expired bans
 8620    if (!empty($expired)) {
 8621      $salt = file_get_contents_cached(SALTFILE);
 8622      
 8623      $expired_ids = [];
 8624      
 8625      foreach ($expired as $ban) {
 8626        $expired_ids[] = (int)$ban['no'];
 8627        
 8628        $hash = sha1($ban['board'] . $ban['post_num'] . $salt);
 8629        
 8630        $file_path = BANTHUMB_ROOT . $ban['board'] . '/' . $hash . 's.jpg';
 8631        
 8632        if (file_exists($file_path)) {
 8633          unlink($file_path);
 8634        }
 8635      }
 8636      
 8637      $lim = count($expired_ids);
 8638      
 8639      $expired_ids = implode(',', $expired_ids);
 8640      $query = "UPDATE banned_users SET active = 0, unbannedon = NOW(), unbannedby = 'expiration' WHERE no IN($expired_ids) LIMIT $lim";
 8641      $result = mysql_global_do($query);
 8642    }
 8643    
 8644    return $is_banned;
 8645  }
 8646  
 8647  /**
 8648   * Strips tags from webms and repairs broken streams.
 8649   * This is needed to prevent people from bypassing duration limits.
 8650   * $file must be safe to use as shell argument
 8651   */
 8652  function remux_webm($file, $format, $strip_metadata = true) {
 8653    $binary = '/usr/local/bin/ffmpeg-mp4';
 8654    
 8655    $out_file = $file . '_tmpff';
 8656    
 8657    if ($strip_metadata) {
 8658      $map_meta = '-map_metadata -1 ';
 8659    }
 8660    else {
 8661      $map_meta = '';
 8662    }
 8663    
 8664    if ($format !== 'webm' && $format !== 'mp4') {
 8665      return false;
 8666    }
 8667    
 8668    // $file and $format must be safe for shell_exec
 8669    shell_exec("$binary -f $format -i \"$file\" $map_meta-bitexact -c copy -f $format -y \"$out_file\"");
 8670    
 8671    if (!file_exists($out_file)) {
 8672      return false;
 8673    }
 8674    
 8675    $ret = rename($out_file, $file);
 8676    
 8677    clearstatcache(true, $file);
 8678    
 8679    return $ret;
 8680  }
 8681  
 8682  /**
 8683   * Remuxes and validates webm file
 8684   * Returns video info on success array(width, height, sar, extension)
 8685   * Dies if the file is invalid or not acceptable
 8686   * $file must be safe to use as shell argument
 8687   */
 8688  function validate_webm($file, $ext) {
 8689    $binary = '/usr/local/bin/ffprobe-mp4';
 8690    
 8691    if ($ext == '.webm') {
 8692      $format = 'webm';
 8693    }
 8694    else if ($ext == '.mp4') {
 8695      $format = 'mp4';
 8696    }
 8697    else {
 8698      error(S_NOREC, $file);
 8699    }
 8700    
 8701    // Remux webm to strip extra data and repair broken streams
 8702    if (!remux_webm($file, $format)) {
 8703      error(S_NOREC, $file);
 8704    }
 8705    
 8706    // $file and $format must be safe for proc_open
 8707    $cmd = "$binary -f $format -i \"$file\" -hide_banner -show_streams -show_format -of json";
 8708    
 8709    $desc = [ 1 => ['pipe', 'w'], 2 => ['pipe', 'w'] ];
 8710    
 8711    $pipes = [];
 8712    
 8713    $proc = proc_open($cmd, $desc, $pipes);
 8714    
 8715    if ($proc === false || !is_resource($proc)) {
 8716      error(S_FAILEDUPLOAD, $file);
 8717    }
 8718    
 8719    $stdout = stream_get_contents($pipes[1]);
 8720    fclose($pipes[1]);
 8721    
 8722    $stderr = stream_get_contents($pipes[2]);
 8723    fclose($pipes[2]);
 8724    
 8725    $status = proc_close($proc);
 8726    
 8727    if ($status !== 0) {
 8728      error(S_FAILEDUPLOAD, $file);
 8729    }
 8730    
 8731    // Check stderr
 8732    if (stripos($stderr, 'invalid') !== false) {
 8733      error(S_NOREC, $file);
 8734    }
 8735    
 8736    // Check stdout
 8737    $res = json_decode($stdout, true);
 8738    
 8739    if (json_last_error() !== JSON_ERROR_NONE) {
 8740      error(S_FAILEDUPLOAD, $file);
 8741    }
 8742    
 8743    //print_r($res);
 8744    
 8745    if ($res['format']['format_name'] === 'matroska,webm') {
 8746      $format = 'webm';
 8747    }
 8748    else if ($res['format']['format_name'] === 'mov,mp4,m4a,3gp,3g2,mj2') {
 8749      $format = 'mp4';
 8750    }
 8751    else {
 8752      error(S_NOREC, $file);
 8753    }
 8754    
 8755    // container duration, can be forged but we remux the file to fix this
 8756    $duration = (float)$res['format']['duration'];
 8757    
 8758    if ($duration <= 0 || $duration > MAX_WEBM_DURATION) {
 8759      error(S_VIDEOTOOLONG, $file); // Duration too long
 8760    }
 8761    
 8762    $has_audio = false;
 8763    $video_dims = false;
 8764    
 8765    foreach ($res['streams'] as $stream) {
 8766      $type = $stream['codec_type'];
 8767      
 8768      if ($type === 'audio') {
 8769        if (!ENABLE_WEBM_AUDIO) {
 8770          error(S_AUDIODISABLED, $file); // Audio streams are not allowed
 8771        }
 8772        
 8773        // Vorbis or Opus for webm audio
 8774        if ($format === 'webm') {
 8775          if ($stream['codec_name'] !== 'vorbis' && $stream['codec_name'] !== 'opus') {
 8776            error(S_BADAUDIO, $file); // Bad audio stream
 8777          }
 8778        }
 8779        // AAC for mp4 audio
 8780        else if ($format === 'mp4') {
 8781          if ($stream['codec_name'] !== 'aac') {
 8782            error(S_BADAUDIO, $file); // Bad audio stream
 8783          }
 8784        }
 8785        else {
 8786          error(S_BADAUDIO, $file); // Bad audio stream
 8787        }
 8788        
 8789        $has_audio = true;
 8790      }
 8791      else if ($type === 'video') {
 8792        if ($video_dims) {
 8793          error(S_BADSTREAM, $file); // Too many video streams
 8794        }
 8795        
 8796        // VP8 or VP9 for webm video
 8797        if ($format === 'webm') {
 8798          if ($stream['codec_name'] !== 'vp8' && $stream['codec_name'] !== 'vp9') {
 8799            error(S_BADVIDEO, $file); // Bad video stream
 8800          }
 8801        }
 8802        // H264 for mp4 video
 8803        else if ($format === 'mp4') {
 8804          if ($stream['codec_name'] !== 'h264') {
 8805            error(S_BADVIDEO, $file); // Bad video stream
 8806          }
 8807          
 8808          // Reject 10 bit streams
 8809          if ($stream['bits_per_raw_sample'] > 8) {
 8810            error(S_NOREC, $file);
 8811          }
 8812          
 8813          // Only accept yuv420p streams
 8814          if ($stream['pix_fmt'] !== 'yuv420p') {
 8815            error(S_NOREC, $file);
 8816          }
 8817        }
 8818        else {
 8819          error(S_BADVIDEO, $file); // Bad video stream
 8820        }
 8821        
 8822        $width = (int)$stream['width'];
 8823        $height = (int)$stream['height'];
 8824        
 8825        if (!$width || !$height || $width > MAX_WEBM_DIMENSION || $height > MAX_WEBM_DIMENSION) {
 8826          error(S_TOOLARGERES, $file); // Dimensions too big
 8827        }
 8828        
 8829        $sar = null;
 8830        
 8831        if (isset($stream['sample_aspect_ratio'])) {
 8832          $tmp_sar = explode(':', $stream['sample_aspect_ratio']);
 8833          
 8834          $tmp_sar[0] = (int)$tmp_sar[0];
 8835          $tmp_sar[1] = (int)$tmp_sar[1];
 8836          
 8837          if ($tmp_sar[1] && $tmp_sar[0] !== $tmp_sar[1]) {
 8838            $tmp_sar = $tmp_sar[0] / $tmp_sar[1];
 8839            
 8840            if ($tmp_sar < 2 && $tmp_sar > 0.5) {
 8841              $sar = $tmp_sar;
 8842            }
 8843          }
 8844        }
 8845        
 8846        $video_dims = array($width, $height, $sar);
 8847      }
 8848      else {
 8849        error(S_BADSTREAM, $file); // Bad stream
 8850      }
 8851    }
 8852    
 8853    if (!$video_dims) {
 8854      error(S_NOVIDEOSTREAM, $file); // No video streams
 8855    }
 8856    
 8857    return $video_dims;
 8858  }
 8859  
 8860  /**
 8861   * Generates thumbnails for webm files
 8862   * Returns the thumbnail filename on success.
 8863   * Returns false if the thumbnail couldn't be generated.
 8864   */
 8865  function thumb_webm($file, $ext) {
 8866    $binary = '/usr/local/bin/ffmpeg-mp4';
 8867    
 8868    $out_file = $file . '.tmp.jpg';
 8869    
 8870    if ($ext === '.webm') {
 8871      $format = 'webm';
 8872    }
 8873    else if ($ext === '.mp4') {
 8874      $format = 'mp4';
 8875    }
 8876    else {
 8877      return false;
 8878    }
 8879    
 8880    // $file and $format must be safe for shell_exec
 8881    $res = shell_exec("$binary -f $format -i \"$file\" -vframes 1 -an -y \"$out_file\" 2>&1");
 8882    
 8883    if (file_exists($out_file)) {
 8884      return $out_file;
 8885    }
 8886    
 8887    quick_log_to( "/www/perhost/bad-upload.log", "webm failure on $file:\n$res");
 8888  
 8889    return false;
 8890  }
 8891  
 8892  /**
 8893   * Contest banners 468x60
 8894   */
 8895  function get_contest_banner() {
 8896    $query = "SELECT file_id, file_ext, board FROM contest_banners WHERE is_live = 1 ORDER BY RAND() LIMIT 1";
 8897    
 8898    $res = mysql_global_call($query);
 8899    
 8900    if (!$res) {
 8901      return '';
 8902    }
 8903    
 8904    $banner = mysql_fetch_assoc($res);
 8905    
 8906    if (!$banner) {
 8907      return '';
 8908    }
 8909    
 8910    $img_url = STATIC_SERVER . "image/contest_banners/{$banner['file_id']}.{$banner['file_ext']}";
 8911    $link_url = '//boards.' . L::d($banner['board']) . '/' . $banner['board'] . '/';
 8912    
 8913    return '<div><a href="' . $link_url . '"><img alt="" src="' . $img_url . '"></a></div>';
 8914  }
 8915  
 8916  /**
 8917   * Get latest post number from /j/
 8918   * Returns a json { "no": 123 }
 8919   */
 8920  
 8921  function get_last_post_no() {
 8922    $no = 0;
 8923    $query = "SELECT no FROM `j` ORDER BY no DESC LIMIT 1";
 8924    $res = mysql_board_call($query);
 8925    if ($res) {
 8926      if ($row = mysql_fetch_row($res)) {
 8927        $no = (int)$row[0];
 8928      }
 8929    }
 8930    echo "{\"no\":$no}";
 8931  }
 8932  
 8933  /**
 8934   * Deletes partial jsons for all live threads
 8935   */
 8936  function purge_json_tails() {
 8937    $query = 'SELECT no FROM `' . SQLLOG . '` WHERE resto = 0 AND archived = 0';
 8938    
 8939    $res = mysql_board_call($query);
 8940    
 8941    if (!$res) {
 8942      return false;
 8943    }
 8944    
 8945    while ($row = mysql_fetch_row($res)) {
 8946      $thread_id = (int)$row[0];
 8947      update_json_tail_deletion($thread_id, true);
 8948    }
 8949    
 8950    return true;
 8951  }
 8952  
 8953  /**
 8954   * Deletes, if necessary, the partial json tail file after post deletion.
 8955   */
 8956  function update_json_tail_deletion($thread_id, $force = false) {
 8957    if (!JSON_TAIL_SIZE && !$force) {
 8958      return false;
 8959    }
 8960    
 8961    $tail_size = $force ? 0 : get_json_tail_size($thread_id);
 8962    
 8963    if (!$tail_size) {
 8964      $fname = RES_DIR . $thread_id . '-tail.json';
 8965      
 8966      if (USE_GZIP) {
 8967        $fname = "$fname.gz";
 8968      }
 8969      
 8970      if (file_exists($fname)) {
 8971        return unlink($fname);
 8972      }
 8973    }
 8974    
 8975    return false;
 8976  }
 8977  
 8978  /**
 8979   * Returns the number of posts in the partial -tail json.
 8980   * 0 if no tail json is available
 8981   */
 8982  function get_json_tail_size($thread_id) {
 8983    global $log;
 8984    
 8985    $tail_size = (int)JSON_TAIL_SIZE;
 8986    
 8987    if (!$tail_size || !isset($log[$thread_id])) {
 8988      return 0;
 8989    }
 8990    
 8991    $th = $log[$thread_id];
 8992    
 8993    if ($th['sticky'] && $th['undead']) {
 8994      $tail_size = $tail_size * 2;
 8995    }
 8996    
 8997    $post_count = count($th['children']);
 8998    
 8999    if ($post_count >= $tail_size * 2) {
 9000      return $tail_size;
 9001    }
 9002    else {
 9003      return 0;
 9004    }
 9005  }
 9006  
 9007  /**
 9008   * Test function for mobile image resizing
 9009   */
 9010  function resize_mobile_image($path, $w, $h, $fsize, $tim, $ext) {
 9011    if ($ext !== '.jpg' && $ext !== '.png') {
 9012      return false;
 9013    }
 9014    
 9015    $MAX_W = 1024;
 9016    $MAX_H = 1024;
 9017    $MAX_PXL = 524288;
 9018    $MAX_PNG_BYTES = 524288;
 9019    
 9020    if ($ext === '.png' && $fsize <= $MAX_PNG_BYTES) {
 9021      return;
 9022    }
 9023    
 9024    if (($w > $MAX_W || $h > $MAX_H) && $w * $h > $MAX_PXL) {
 9025      $jpeg_quality = 80;
 9026      
 9027      $memory_limit_increased = false;
 9028      
 9029      if ($w * $h > 3000000) {
 9030        $memory_limit_increased = true;
 9031        ini_set('memory_limit', memory_get_usage() + $w * $h * 15);
 9032      }
 9033      
 9034      if ($ext === '.jpg') {
 9035        $img_in = ImageCreateFromJPEG($path);
 9036      }
 9037      else {
 9038        $img_in = ImageCreateFromPNG($path);
 9039      }
 9040      
 9041      if (!$img_in) {
 9042        error(S_FAILEDUPLOAD . ' (rmi)', $path);
 9043      }
 9044      
 9045      $ratio = $w / $h;
 9046      
 9047      if ($ratio > 1) {
 9048        $out_w = $MAX_W;
 9049        $out_h = round($MAX_W / $ratio);
 9050      }
 9051      else {
 9052        $out_w = round($MAX_H * $ratio);
 9053        $out_h = $MAX_H;
 9054      }
 9055      
 9056      $img_out = ImageCreateTrueColor($out_w, $out_h);
 9057      
 9058      ImageCopyResampled($img_out, $img_in, 0, 0, 0, 0, $out_w, $out_h, $w, $h);
 9059      ImageDestroy($img_in);
 9060      
 9061      $out_path = IMG_DIR . $tim . 'm.jpg';
 9062      ImageJPEG($img_out, $out_path, $jpeg_quality);
 9063      ImageDestroy($img_out);
 9064      
 9065      if ($memory_limit_increased) {
 9066        ini_restore('memory_limit');
 9067      }
 9068      
 9069      return $out_path;
 9070    }
 9071  }
 9072  
 9073  /**
 9074   * Returns the number of unique IPs for a given thread id
 9075   * $thread_id needs to be cached in $log.
 9076   */
 9077  function get_unique_ip_count($thread_id) {
 9078    global $log;
 9079    
 9080    if (!isset($log[$thread_id]) || $log[$thread_id]['archived']) {
 9081      return false;
 9082    }
 9083    
 9084    $posts = $log[$thread_id]['children'];
 9085    
 9086    if (empty($posts)) {
 9087      return 1;
 9088    }
 9089    
 9090    $ip_map = array();
 9091    $ip_count = 1;
 9092    $ip_map[$log[$thread_id]['host']] = true;
 9093    
 9094    foreach ($posts as $pid => $val) {
 9095      if (!isset($log[$pid])) {
 9096        continue;
 9097      }
 9098      $post = $log[$pid];
 9099      if (!isset($ip_map[$post['host']])) {
 9100        ++$ip_count;
 9101        $ip_map[$post['host']] = true;
 9102      }
 9103    }
 9104    
 9105    return $ip_count;
 9106  }
 9107  
 9108  function generate_del_pwd() {
 9109    return '_' . substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, 32);
 9110  }
 9111  
 9112  function get_hashed_mod_name($name) {
 9113    if (!$name) {
 9114      die('Internal Server Error (ghmn0)');
 9115    }
 9116    
 9117    $admin_salt = file_get_contents('/www/keys/2014_admin.salt');
 9118    
 9119    if (!$admin_salt) {
 9120      die('Internal Server Error (ghmn1)');
 9121    }
 9122    
 9123    $hashed_bits = hash_hmac('sha256', $name, $admin_salt, true);
 9124    
 9125    $hashed_name = base64_encode($hashed_bits);
 9126    
 9127    if (!$hashed_name) {
 9128      die('Internal Server Error (ghmn2)');
 9129    }
 9130    
 9131    return $hashed_name;
 9132  }
 9133  
 9134  function create_memcached_instance() {
 9135    $m = new Memcached();
 9136    //$m->setOption(Memcached::OPT_TCP_NODELAY, true);
 9137    $m->setOption(Memcached::OPT_SERVER_FAILURE_LIMIT, 1);
 9138    $m->setOption(Memcached::OPT_SEND_TIMEOUT, 500000); // 500ms
 9139    $m->setOption(Memcached::OPT_RECV_TIMEOUT, 500000); // 500ms
 9140    $m->addServer(MEMCACHED_HOST, MEMCACHED_PORT);
 9141    return $m;
 9142  }
 9143  
 9144  function forcearchive() {
 9145    if (!has_level()) {
 9146      error("Can't let you do that.");
 9147    }
 9148    
 9149    if (!ENABLE_ARCHIVE) {
 9150      error('Archives are disabled on this board.');
 9151    }
 9152    
 9153    if (!isset($_POST['id'])) {
 9154      error('Bad Request.');
 9155    }
 9156    
 9157    $tid = (int)$_POST['id'];
 9158    
 9159    $query = 'SELECT resto, sticky, archived, no, name, sub, com, filename, ext FROM `%s` WHERE no = %d';
 9160    $res = mysql_board_call($query, BOARD_DIR, $tid);
 9161    
 9162    if (!$res) {
 9163      error('Database error.');
 9164    }
 9165    
 9166    $thread = mysql_fetch_assoc($res);
 9167    
 9168    if (!$thread || $thread['resto']) {
 9169      error('Thread not found.');
 9170    }
 9171    
 9172    if ($thread['archived']) {
 9173      error('This thread is already archived.');
 9174    }
 9175    
 9176    if ($thread['sticky']) {
 9177      error(S_MAYNOTDELSTICKY);
 9178    }
 9179    
 9180    archive_thread($tid);
 9181    
 9182    // Log the action
 9183    $action_log_post = array(
 9184      'no' => $thread['no'],
 9185      'name' => $thread['name'],
 9186      'sub' => $thread['sub'],
 9187      'com' => $thread['com'],
 9188      'filename' => $thread['filename'],
 9189      'ext' => $thread['ext']
 9190    );
 9191    
 9192    log_mod_action(3, $action_log_post);
 9193    
 9194    if (ENABLE_JSON_THREADS) {
 9195      generate_board_archived_json();
 9196    }
 9197    
 9198    updating_index();
 9199  }
 9200  
 9201  // Called remotely by other tools
 9202  function rebuild_threads_by_id() {
 9203    header('Content-Type: text/plain');
 9204    
 9205    if (!isset($_POST['ids']) || !is_array($_POST['ids']) || empty($_POST['ids'])) {
 9206      echo '0';
 9207      return;
 9208    }
 9209    
 9210    $live_ids = array();
 9211    
 9212    // Rebuild archived threads first
 9213    foreach ($_POST['ids'] as $id) {
 9214      $id = (int)$id;
 9215      
 9216      if (!$id) {
 9217        continue;
 9218      }
 9219      
 9220      $query = "SELECT archived FROM `" . SQLLOG . "` WHERE no = $id LIMIT 1";
 9221      $res = mysql_board_call($query);
 9222      
 9223      if (!$res) {
 9224        echo '0';
 9225        return;
 9226      }
 9227      
 9228      if (mysql_fetch_row($res)[0] === '1') {
 9229        rebuild_archived_thread($id);
 9230      }
 9231      else {
 9232        $live_ids[] = $id;
 9233      }
 9234    }
 9235    
 9236    // Rebuild live threads
 9237    if (!empty($live_ids)) {
 9238  	    
 9239      foreach ($live_ids as $id) {
 9240        updatelog($id, 1);
 9241      }
 9242      
 9243      if (STATIC_REBUILD) {
 9244        return;
 9245      }
 9246      
 9247      updatelog(0, 0); // rebuild indexes
 9248    }
 9249    
 9250    echo '1';
 9251  }
 9252  
 9253  function rebuild_archive_list($print = false) {
 9254    $board = BOARD_DIR;
 9255    
 9256    $html = '';
 9257    
 9258    $maxlen = 100;
 9259    
 9260    $max_age_in_days = 3;
 9261    
 9262    $hour_clause = $max_age_in_days * 24;
 9263    
 9264    $thread_limit = 3000;
 9265    
 9266    $query = <<<SQL
 9267  SELECT no, sub, com
 9268  FROM `$board`
 9269  WHERE archived = 1 AND resto = 0 AND root >= DATE_SUB(NOW(), INTERVAL $hour_clause HOUR)
 9270  ORDER BY root DESC
 9271  LIMIT $thread_limit
 9272  SQL;
 9273    
 9274    $res = mysql_board_call($query);
 9275    
 9276    $thread_count = mysql_num_rows($res);
 9277    
 9278    head($html, 0, 0, 0, 0, true);
 9279    
 9280    $html .= '<div class="navLinks mobile">
 9281      <span class="mobileib button"><a href="/' . BOARD_DIR . '/" accesskey="a">' . S_RETURN . '</a></span> <span class="mobileib button"><a href="/' . BOARD_DIR . '/catalog">' . S_CATALOG . '</a></span> <span class="mobileib button"><a href="#bottom">' . S_BOTTOM . '</a></span>
 9282    </div>';
 9283    
 9284    $html .= '<hr class="desktop">
 9285      <div class="navLinks desktop">
 9286      [<a href="/' . BOARD_DIR . '/" accesskey="a">' . S_RETURN . '</a>] [<a href="/' . BOARD_DIR . '/catalog">' . S_CATALOG . '</a>] [<a href="#bottom">' . S_BOTTOM . '</a>]
 9287    </div><hr>';
 9288    
 9289    $html .= '<h4 class="center">Displaying ' . number_format($thread_count) . ' expired thread'
 9290      . (!$thread_count || $thread_count > 1 ? 's' : '') . ' from the past ' . $max_age_in_days . ' day'
 9291      . ($max_age_in_days > 1 ? 's' : '') . '</h4>
 9292    <table id="arc-list" class="flashListing"><thead><tr>
 9293      <td class="postblock">No.</td>
 9294      <td class="postblock">Excerpt</td>
 9295      <td class="postblock"></td>
 9296    </tr></thead><tbody>';
 9297    
 9298    while ($row = mysql_fetch_assoc($res)) {
 9299      if (strpos($row['sub'], 'SPOILER<>') === 0) {
 9300        $row['sub'] = substr($row['sub'], 9);
 9301      }
 9302      
 9303      if (!empty($row['sub'])) {
 9304        if ($row['com'] !== '') {
 9305          $teaser = '<b>' . $row['sub'] . ':</b> ' . $row['com'];
 9306        }
 9307        else {
 9308          $teaser = $row['sub'];
 9309        }
 9310      }
 9311      else {
 9312        $teaser = $row['com'];
 9313      }
 9314      
 9315      $teaser = preg_replace('/(?:<br>)+/', ' ', str_replace('&quot;', "'", $teaser));
 9316      
 9317      $href_context = generate_href_context($row['sub'], $row['com']);
 9318      
 9319      if ($href_context !== '') {
 9320        $href_context = "/$href_context";
 9321      }
 9322      
 9323      $html .= '<tr>
 9324  <td>' . $row['no'] . '</td>
 9325  <td class="teaser-col">'
 9326    . truncate_comment($teaser, $maxlen) .
 9327  '</td>
 9328  <td>[<a class="quotelink" href="/' . $board . '/thread/'
 9329    . $row['no'] . $href_context . '">View</a>]
 9330  </td>
 9331  </tr>';
 9332    }
 9333    
 9334    $html .= '</tbody></table><hr>';
 9335    
 9336    $html .= '<div class="navLinks navLinksBot desktop">[<a href="/'
 9337        . BOARD_DIR . '/" accesskey="a">' . S_RETURN . '</a>] [<a href="/'
 9338        . BOARD_DIR . '/catalog">' . S_CATALOG . '</a>] [<a href="#top">'
 9339        . S_TOP . '</a>] </div><hr class="desktop">';
 9340    
 9341    $html .= '<div class="navLinks mobile"><span class="mobileib button"><a href="/'
 9342      . BOARD_DIR . '/" accesskey="a">'
 9343      . S_RETURN . '</a></span> <span class="mobileib button"><a href="/'
 9344      . BOARD_DIR . '/catalog">'
 9345      . S_CATALOG . '</a></span> <span class="mobileib button"><a href="#top">'
 9346      . S_TOP . '</a></span></div><hr class="mobile">';
 9347    
 9348    if (AD_BOTTOM_ENABLE == 1) {
 9349      $bottomad = '';
 9350      
 9351      if (defined('AD_BOTTOM_TEXT') && AD_BOTTOM_TEXT) {
 9352        $bottomad .= '<div class="bottomad center ad-cnt">'
 9353          . ad_text_for(AD_BOTTOM_TEXT) . '</div>'
 9354          . (defined('AD_BOTTOM_PLEA') ? AD_BOTTOM_PLEA : '');
 9355      }
 9356      
 9357      if ($bottomad) {
 9358        $html .= "$bottomad<hr>";
 9359      }
 9360    }
 9361    
 9362    $html .= '<div class="bottomCtrl desktop">';
 9363    
 9364    if (!defined('CSS_FORCE')) {
 9365      $html .= '<span class="stylechanger">Style: 
 9366        <select id="styleSelector">
 9367          <option value="Yotsuba New">Yotsuba</option>
 9368          <option value="Yotsuba B New">Yotsuba B</option>
 9369          <option value="Futaba New">Futaba</option>
 9370          <option value="Burichan New">Burichan</option>
 9371          <option value="Tomorrow">Tomorrow</option>
 9372          <option value="Photon">Photon</option>';
 9373      
 9374      if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) {
 9375        $html .= '<option value="_special">Special</option>';
 9376      }
 9377      
 9378      $html .= '</select>
 9379      </span>';
 9380    }
 9381    
 9382    $html .= '</div>';
 9383    
 9384    foot($html, false, true);
 9385    
 9386    if ($print) {
 9387      echo $html;
 9388    }
 9389    else {
 9390      print_page(INDEX_DIR . 'archive'. PHP_EXT, $html);
 9391    }
 9392  }
 9393  
 9394  function rebuild_syncframe_page($print = false) {
 9395    $tpl_file = YOTSUBA_DIR . 'views/syncframe.html';
 9396    
 9397    if (!file_exists($tpl_file)) {
 9398      die('Template file not found');
 9399    }
 9400    
 9401    $html = file_get_contents($tpl_file);
 9402    
 9403    if ($print) {
 9404      die($html);
 9405    }
 9406    else {
 9407      print_page(BOARDS_ROOT . 'syncframe' . PHP_EXT, $html);
 9408    }
 9409  }
 9410  
 9411  function rebuild_search_page($print = false) {
 9412    $html = '';
 9413    
 9414    // board select box
 9415    
 9416    $board_select_html = '<select class="g-search-ctrl" id="js-sf-bf"><option value="">All Boards</option>';
 9417    
 9418    $query = 'SELECT dir, name FROM boardlist ORDER BY dir ASC';
 9419    
 9420    $res = mysql_global_call($query);
 9421    
 9422    if (!$res) {
 9423      error('Database Error (rsp0)');
 9424    }
 9425    
 9426    while ($row = mysql_fetch_assoc($res)) {
 9427      $board_select_html .= '<option value="' . $row['dir'] . '">/' . $row['dir'] . '/ - ' . htmlspecialchars($row['name'], ENT_QUOTES) . '</option>';
 9428    }
 9429    
 9430    $board_select_html .= '</select>';
 9431    
 9432    // header ---
 9433    
 9434    $cssVersion = $print ? CSS_VERSION_TEST : CSS_VERSION;
 9435    $defaultcss = 'yotsubanew';
 9436    $mobilecss  = 'yotsubamobile.' . $cssVersion . '.css';
 9437    
 9438    $styles = array(
 9439      'Yotsuba New'   => "yotsubanew.$cssVersion.css",
 9440      'Yotsuba B New' => "yotsubluenew.$cssVersion.css",
 9441      'Futaba New'    => "futabanew.$cssVersion.css",
 9442      'Burichan New'  => "burichannew.$cssVersion.css",
 9443      'Photon'        => "photon.$cssVersion.css",
 9444      'Tomorrow'      => "tomorrow.$cssVersion.css"
 9445    );
 9446    
 9447    $dcssl = $defaultcss . '.' . $cssVersion . '.css';
 9448    
 9449    $css = '<link rel="stylesheet" title="switch" href="' . STATIC_SERVER . 'css/' . $dcssl . '">';
 9450    
 9451    foreach ($styles as $style => $stylecss) {
 9452      $css .= '<link rel="alternate stylesheet" style="text/css" href="' . STATIC_SERVER . 'css/' . $stylecss . '" title="' . $style . '">';
 9453    }
 9454    
 9455    $css .= '<link rel="stylesheet" href="' . STATIC_SERVER . 'css/' . $mobilecss . '">';
 9456    
 9457    $scriptjs = '<script type="text/javascript">var style_group = "nws_style";</script>';
 9458    
 9459    $testjs    = $print ? 'test/core-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'core.min.' . JS_VERSION_CORE . '.js';
 9460    $testextra = $print ? 'test/extension-8psvqAqszI.' . JS_VERSION_TEST . '.js' : 'extension.min.' . JS_VERSION_EXT . '.js';
 9461  
 9462    $scriptjs .= '<script type="text/javascript" data-cfasync="false" src="' . STATIC_SERVER . 'js/' . $testjs . '"></script>';
 9463    $scriptjs .= '<script type="text/javascript" data-cfasync="false" src="' . STATIC_SERVER . 'js/' . $testextra . '"></script>';
 9464    
 9465    if (defined('FAVICON')) {
 9466      $favicon = '<link rel="shortcut icon" href="' . FAVICON . '">';
 9467    }
 9468    else {
 9469      $favicon = '';
 9470    }
 9471    
 9472    $includenav = file_get_contents_cached(NAV_TXT);
 9473    
 9474    $html .= '<!DOCTYPE html>
 9475  <html>
 9476  <head>
 9477  <meta charset="utf-8">
 9478  <meta name="robots" content="' . META_ROBOTS . '">
 9479  <meta name="description" content="4chan Search">
 9480  <meta name="keywords" content="4chan,search">
 9481  <meta name="viewport" content="width=device-width,initial-scale=1">
 9482  ' . $favicon . '
 9483  ' . $css . '
 9484  <title>Search 4chan</title>' . $scriptjs .
 9485  '</head>
 9486  <body class="is_search">' . $stylejs . $includenav .
 9487  '<div class="boardBanner">
 9488    <div class="boardTitle">4chan Search</div>
 9489  </div>';
 9490  
 9491    // ---
 9492    
 9493    $html .= '<hr class="desktop"><form action="#" id="g-search-form"><input placeholder="Search" class="g-search-ctrl" id="js-sf-qf" type="text">' . $board_select_html . '<button class="g-search-ctrl" id="js-sf-btn">Search</button></form>';
 9494    
 9495    $html .= '<hr>
 9496    <form name="delform" id="delform">
 9497    <div class="board">';
 9498    
 9499    $html .= '</div><hr>';
 9500    
 9501    $html .= '<div class="bottomCtrl desktop">';
 9502    
 9503    if (!defined('CSS_FORCE')) {
 9504      $html .= '<span class="stylechanger">Style: 
 9505        <select id="styleSelector">
 9506          <option value="Yotsuba New">Yotsuba</option>
 9507          <option value="Yotsuba B New">Yotsuba B</option>
 9508          <option value="Futaba New">Futaba</option>
 9509          <option value="Burichan New">Burichan</option>
 9510          <option value="Tomorrow">Tomorrow</option>
 9511          <option value="Photon">Photon</option>';
 9512      
 9513      if (defined('CSS_EVENT_NAME') && CSS_EVENT_NAME) {
 9514        $html .= '<option value="_special">Special</option>';
 9515      }
 9516      
 9517      $html .= '</select>
 9518      </span>';
 9519    }
 9520    
 9521    $html .= '</div></form>';
 9522    
 9523    foot($html);
 9524    
 9525    if ($print) {
 9526      echo $html;
 9527    }
 9528    else {
 9529      print_page(BOARDS_ROOT . 'globalsearch' . PHP_EXT, $html);
 9530    }
 9531  }
 9532  
 9533  /**
 9534   * Enforce the maximum number of allowed threads per user, per board.
 9535   * error() if limit has been reached
 9536   */
 9537  function validate_user_thread_limit($ip, $password = null, $pass_id = null) {
 9538    $clauses = array();
 9539    
 9540    $clauses[] = "host = '" . mysql_real_escape_string($ip) . "'";
 9541    
 9542    if ($password) {
 9543      $clauses[] = "pwd = '" . mysql_real_escape_string($password) . "'";
 9544    }
 9545    
 9546    if ($pass_id) {
 9547      $clauses[] = "4pass_id = '" . mysql_real_escape_string($pass_id) . "'";
 9548    }
 9549    
 9550    $ts = $_SERVER['REQUEST_TIME'] - ((int)MAX_USER_THREADS_PERIOD * 3600);
 9551    
 9552    $clauses = implode(' OR ', $clauses);
 9553    
 9554    $query = 'SELECT COUNT(*) FROM `' . SQLLOG
 9555      . "` WHERE resto = 0 AND archived = 0 AND time > $ts AND ($clauses)";
 9556    
 9557    $res = mysql_board_call($query);
 9558    
 9559    if (!$res) {
 9560      return true;
 9561    }
 9562    
 9563    $count = (int)mysql_fetch_row($res)[0];
 9564    
 9565    if ($count >= (int)MAX_USER_THREADS) {
 9566      $plural = MAX_USER_THREADS > 1 ? 's' : '';
 9567      error(sprintf(S_TOOMANYTHREADS, MAX_USER_THREADS, $plural));
 9568    }
 9569    
 9570    return true;
 9571  }
 9572  
 9573  function is_poster_op($host, $hashed_pwd, $resto) {
 9574    $query = 'SELECT host, pwd FROM `%s` WHERE no = %d';
 9575    $res = mysql_board_call($query, SQLLOG, $resto);
 9576    
 9577    if (!$res) {
 9578      return false;
 9579    }
 9580    
 9581    $post = mysql_fetch_assoc($res);
 9582  
 9583    if (!$post) {
 9584      return false;
 9585    }
 9586    
 9587    return $post['host'] === $host || $post['pwd'] === $hashed_pwd;
 9588  }
 9589  
 9590  function spam_filter_check_qa_bot($board, $resto, $ip, $country, $com, $captcha_resp) {
 9591    if (preg_match('/Edge|Safari|WebKit|Firefox|Mozilla/', $_SERVER['HTTP_USER_AGENT']) && $captcha_resp['hostname'] === 'boards.4chan.org') {
 9592      return true;
 9593    }
 9594    
 9595    // Check if IP is known
 9596    $long_ip = ip2long($ip);
 9597    
 9598    if (spam_filter_is_user_known($long_ip)) {
 9599      return false;
 9600    }
 9601    
 9602    if (!preg_match('/Edge|Mobile/', $_SERVER['HTTP_USER_AGENT']) && preg_match('/WebKit/', $_SERVER['HTTP_USER_AGENT']) !== preg_match('/WebKit/', $_SERVER['HTTP_CONTENT_TYPE'])) {
 9603      return true;
 9604    }
 9605    
 9606    $bot_countries = array(
 9607      'AD','AE','AF','AG','AI','AL','AM','AN','AO','AR','AS','AW','AZ',
 9608      'BB','BD','BF','BG','BH','BI','BJ','BM','BN','BO','BR','BS','BT','BV','BW','BY','BZ',
 9609      'CC','CF','CG','CH','CI','CK','CL','CM','CN','CO','CR','CU','CV','CX','CY','CZ',
 9610      'DJ','DM','DO','DZ','EC','EE','EG','EH','ER','ET','FJ','FM','FO',
 9611      'GA','GD','GE','GF','GH','GI','GL','GM','GN','GP','GQ','GR','GS','GT','GU','GY',
 9612      'HK','HM','HN','HT','HU','HR','ID','IL','IN','IO','IQ','IR','IS','JM','JO',
 9613      'KE','KG','KH','KI','KM','KN','KR','KW','KY','KZ',
 9614      'LA','LB','LC','LI','LK','LR','LS','LU','LY',
 9615      'MA','MD','MG','MH','MK','ML','MM','MN','MO','MP','MQ','MR','MS','MT','MU','MV','MW','MY','MZ','NA',
 9616      'NE','NF','NG','NI','NP','NR','NU','NZ','OM','PA','PE','PF','PG','PH','PK','PM','PN','PR','PS','PT','PW',
 9617      'QA','RE','RS','RO','RU','RW','SA','SB','SC','SD','SH','SI','SJ','SK','SL','SM','SN','SO','SR','ST','SV','SY','SZ',
 9618      'TC','TD','TF','TG','TJ','TM','TN','TO','TP','TR','TT','TV','TW','TZ','UG','UM','UY','UZ',
 9619      'VA','VE','VC','VG','VI','VN','VU','WF','WS','YE','YT','ZA','ZM','ZR','ZW'
 9620    );
 9621    
 9622    // Check country
 9623    if (!in_array($country, $bot_countries)) {
 9624      return false;
 9625    }
 9626    
 9627    if ($resto) {
 9628      $query = 'SELECT time FROM %s WHERE resto = %d ORDER BY no DESC LIMIT 1';
 9629      
 9630      $res = mysql_board_call($query, $board, $resto);
 9631      
 9632      if (!$res) {
 9633        return false;
 9634      }
 9635      
 9636      $last_time = mysql_fetch_row($res);
 9637      
 9638      if (!$last_time) {
 9639        return false;
 9640      }
 9641      
 9642      $last_time = (int)$last_time[0];
 9643      
 9644      if ($_SERVER['REQUEST_TIME'] - $last_time < 14400) {
 9645        return false;
 9646      }
 9647    }
 9648    
 9649    return true;
 9650  }
 9651  
 9652  function log_qa_spam_filter($is_hit, $thread_id, $ip, $country, $captcha_resp) {
 9653    $_bot_headers = '';
 9654    
 9655    foreach ($_SERVER as $_h_name => $_h_val) {
 9656      if (substr($_h_name, 0, 5) == 'HTTP_') {
 9657        $_bot_headers .= "$_h_name: $_h_val\n";
 9658      }
 9659    }
 9660    
 9661    $_bot_headers .= "_Captcha: " . $captcha_resp['hostname'] . "\n";
 9662    
 9663    $_bot_headers .= "_Country: $country\n";
 9664  	
 9665  	log_spam_filter_trigger($is_hit ? 'blocked_qa' : 'ok_qa', BOARD_DIR, $thread_id, $ip, 1, $_bot_headers);
 9666  }
 9667  
 9668  function log_spam_filter_trigger($action, $board, $thread_id, $ip, $arg_num, $meta = '') {
 9669    $query = <<<SQL
 9670  INSERT INTO event_log(`type`, `board`, `arg_num`, `thread_id`, `ip`, `meta`)
 9671  VALUES('%s', '%s', %d, %d, '%s', '%s')
 9672  SQL;
 9673      
 9674    mysql_global_call($query, $action, $board, $arg_num, $thread_id, $ip, $meta);
 9675  }
 9676  
 9677  function preview_html() {
 9678    if ($_SERVER['REQUEST_METHOD'] != 'POST') {
 9679      updating_index();
 9680      return;
 9681    }
 9682    
 9683    if (!has_level('mod')) {
 9684      updating_index();
 9685      return;
 9686    }
 9687    
 9688    if (!has_flag('html') && !has_flag('developer')) {
 9689      updating_index();
 9690      return;
 9691    }
 9692    
 9693    header('Content-type: application/json');
 9694    
 9695    if (isset($_POST['com'])) {
 9696      $html = $_POST['com'];
 9697    }
 9698    else {
 9699      $data = array('status' => 'error', 'message' => 'Nothing to do.');
 9700      echo json_encode($data);
 9701      return;
 9702    }
 9703    
 9704    $html = purify_html($html);
 9705    
 9706    $data = array('status' => 'success', 'data' => $html);
 9707    
 9708    echo json_encode($data);
 9709  }
 9710  
 9711  function purify_html($html) {
 9712    static $purifier = null;
 9713    
 9714    if ($purifier === null) {
 9715      require_once 'lib/htmlpurifier/HTMLPurifier.standalone.php';
 9716      
 9717      $config = HTMLPurifier_Config::createDefault();
 9718      $config->set('Cache.DefinitionImpl', null);
 9719      
 9720      $config->set('HTML.Doctype', 'HTML 4.01 Transitional');
 9721      
 9722      $config->set('URI.AllowedSchemes', array('http' => true, 'https' => true));
 9723      $config->set('HTML.SafeIframe', true);
 9724      $config->set('URI.SafeIframeRegexp', HTML_IFRAME_WHITELIST);
 9725      $config->set('HTML.Allowed', HTML_WHITELIST);
 9726      $config->set('CSS.AllowTricky', true);
 9727      $config->set('CSS.Trusted', true);
 9728      $config->set('Attr.AllowedFrameTargets', array('_blank'));
 9729      
 9730      $def = $config->getHTMLDefinition(true);
 9731      $def->addAttribute('iframe', 'allowfullscreen', 'Bool');
 9732      
 9733      $def->addElement('video', 'Block', 'Flow', 'Common', array(
 9734        'controls'  => 'Bool',
 9735        'height'    => 'Length',
 9736        'width'     => 'Length',
 9737        'poster'    => 'URI',
 9738        'autoplay'  => 'Bool',
 9739        'loop'      => 'Bool',
 9740        'muted'     => 'Bool',
 9741        'src'       => 'URI'
 9742      ));
 9743      
 9744      $css_def = $config->getDefinition('CSS');
 9745      
 9746      $css_def->info['color'] = new HTMLPurifier_AttrDef_CSS_Composite(
 9747        array(
 9748          new HTMLPurifier_AttrDef_Enum(array('transparent')),
 9749          new HTMLPurifier_AttrDef_CSS_Color()
 9750        )
 9751      );
 9752      
 9753      $purifier = new HTMLPurifier($config);
 9754      
 9755      if (!$purifier) {
 9756        error('Internal Server Error');
 9757      }
 9758    }
 9759    
 9760    return $purifier->purify($html);
 9761  }
 9762  
 9763  function count_thread_replies($board, $thread_id) {
 9764    $thread_id = (int)$thread_id;
 9765    
 9766    if ($thread_id <= 0) {
 9767      return 0;
 9768    }
 9769    
 9770    $sql = "SELECT COUNT(*) as cnt FROM `%s` WHERE resto = $thread_id";
 9771    
 9772    $res = mysql_board_call($sql, $board);
 9773    
 9774    if (!$res) {
 9775      return 0;
 9776    }
 9777    
 9778    return (int)mysql_fetch_row($res)[0];
 9779  }
 9780  
 9781  // TODO: remove later
 9782  function check_safe_ua_sig($ua, $sig) {
 9783    if (!$ua || !$sig) {
 9784      return true;
 9785    }
 9786    
 9787    $thres = 3;
 9788    
 9789    $ua_sig = "$ua.$sig";
 9790    
 9791    $sql = "SELECT 1 FROM event_log WHERE type = 'log_safe_ua' AND ua_sig = '%s' LIMIT $thres";
 9792    
 9793    $res = mysql_global_call($sql, $ua_sig);
 9794    
 9795    if (!$res) {
 9796      return true;
 9797    }
 9798    
 9799    if (mysql_num_rows($res) < $thres) {
 9800      return false;
 9801    }
 9802    
 9803    return true;
 9804  }
 9805  
 9806  // April 2024
 9807  function april_2024_parse_email($email) {
 9808    if ($email[0] != '$') {
 9809      return 0;
 9810    }
 9811    
 9812    $tag = substr(trim($email), 1);
 9813    
 9814    $stocks = april_2024_get_stock_list();
 9815    
 9816    $idx = array_search($tag, $stocks);
 9817    
 9818    if ($idx === false) {
 9819      return 0;
 9820    }
 9821    
 9822    $count = april_2024_get_stock_count($tag);
 9823    
 9824    if ($count < 10) {
 9825      return 0;
 9826    }
 9827    
 9828    return 10000 + $idx;
 9829  }
 9830  
 9831  function april_2024_get_stock_list() {
 9832    static $stocks = [
 9833      'PEPE', 'WOJK', 'ANIME', 'CHAD', 'CLOWN', 'LOL', 'SICP', 'AUTSM', 'BANE',
 9834      'CIA', 'BOOB', 'RDDT', 'DESU', 'JANNY', 'GME', 'CHUCK', 'YTSB', 'GACHI'
 9835    ];
 9836    
 9837    return $stocks;
 9838  }
 9839  
 9840  function april_2024_get_stock_from_s4p($since4pass) {
 9841    if ($since4pass < 10000) {
 9842      return false;
 9843    }
 9844    
 9845    $val = $since4pass - 10000;
 9846    
 9847    if ($val < 0) {
 9848      return false;
 9849    }
 9850    
 9851    $badges = april_2024_get_stock_list();
 9852    
 9853    if ($val >= 0 && $val < count($badges)) {
 9854      return $badges[$val];
 9855    }
 9856    else {
 9857      return false;
 9858    }
 9859  }
 9860  
 9861  function april_2024_get_name() {
 9862    $net_worth = april_2024_get_net_worth();
 9863    
 9864    if ($net_worth < 500) {
 9865      return 'Destitute Investor';
 9866    }
 9867    else if ($net_worth < 1500) {
 9868      return 'Helpless Investor';
 9869    }
 9870    else if ($net_worth < 5000) {
 9871      return 'Poor Investor';
 9872    }
 9873    else if ($net_worth < 50000) {
 9874      return 'Fledgling Investor';
 9875    }
 9876    else if ($net_worth < 500000) {
 9877      return 'Aspiring Investor';
 9878    }
 9879    else if ($net_worth < 2000000) {
 9880      return 'Rich Investor';
 9881    }
 9882    else if ($net_worth < 5000000) {
 9883      return 'Anonymous Magnate';
 9884    }
 9885    else {
 9886      return 'Anonymous Mogul';
 9887    }
 9888  }
 9889  
 9890  function april_2024_get_post_cls($since4pass) {
 9891    $stock = april_2024_get_stock_from_s4p($since4pass);
 9892    
 9893    if ($stock) {
 9894      return " p-xa24-$stock";
 9895    }
 9896    else {
 9897      return '';
 9898    }
 9899  }
 9900  
 9901  function april_2024_get_name_badge($since4pass) {
 9902    $stock = april_2024_get_stock_from_s4p($since4pass);
 9903    
 9904    if ($stock) {
 9905      return " <span data-tip=\"$stock\" class=\"n-xa24 n-xa24-$stock\"></span>";
 9906    }
 9907    else {
 9908      return '';
 9909    }
 9910  }
 9911  
 9912  function april_2024_get_stock_count($stock) {
 9913    $userpwd = UserPwd::getSession();
 9914    
 9915    if (!$userpwd || $userpwd->isNew()) {
 9916      return 0;
 9917    }
 9918    
 9919    $user_id = $userpwd->getPwd();
 9920    
 9921    $sql =<<<SQL
 9922  SELECT SUM(amount) as amount FROM april_stock_users
 9923  WHERE user_id = '%s' AND stock = '%s'
 9924  SQL;
 9925  
 9926    $res = mysql_global_call($sql, $user_id, $stock);
 9927    
 9928    if (!$res) {
 9929      return 0;
 9930    }
 9931    
 9932    $val = (int)mysql_fetch_row($res)[0];
 9933    
 9934    if ($val < 0) {
 9935      $val = 0;
 9936    }
 9937    
 9938    return $val;
 9939  }
 9940  
 9941  function april_2024_get_net_worth() {
 9942    $userpwd = UserPwd::getSession();
 9943    
 9944    if (!$userpwd || $userpwd->isNew()) {
 9945      return 0;
 9946    }
 9947    
 9948    $user_id = $userpwd->getPwd();
 9949    
 9950    $sql =<<<SQL
 9951  SELECT stock, SUM(amount) as amount FROM april_stock_users
 9952  WHERE user_id = '%s' GROUP BY stock HAVING amount > 0
 9953  SQL;
 9954  
 9955    $res = mysql_global_call($sql, $user_id);
 9956    
 9957    if (!$res) {
 9958      return 0;
 9959    }
 9960    
 9961    $stocks = [];
 9962    
 9963    while ($row = mysql_fetch_row($res)) {
 9964      $stocks[$row[0]] = (int)$row[1];
 9965    }
 9966    
 9967    $sql =<<<SQL
 9968  SELECT stock, price FROM april_stock_prices
 9969  ORDER BY id DESC LIMIT 30
 9970  SQL;
 9971    
 9972    $res = mysql_global_call($sql);
 9973    
 9974    if (!$res) {
 9975      return 0;
 9976    }
 9977    
 9978    $prices = [];
 9979    
 9980    while ($row = mysql_fetch_row($res)) {
 9981      if (isset($prices[$row[0]])) {
 9982        continue;
 9983      }
 9984      $prices[$row[0]] = (int)$row[1];
 9985    }
 9986    
 9987    $net_worth = $stocks['_'];
 9988    
 9989    foreach ($stocks as $stock => $count) {
 9990      if (isset($prices[$stock])) {
 9991        $net_worth += ($prices[$stock] * $count);
 9992      }
 9993    }
 9994    
 9995    return $net_worth;
 9996  }
 9997  
 9998  // ---
 9999  
10000  function clear_no_captcha_token() {
10001    setcookie('_ct', null, -3600, '/', '.' . L::d(BOARD_DIR));
10002  }
10003  
10004  function generate_no_captcha_token() {
10005    if (BOARD_DIR === 'pol' || BOARD_DIR === 'b' || BOARD_DIR === 'r9k' || BOARD_DIR === 'bant') {
10006      return false;
10007    }
10008    
10009    $long_ip = ip2long($_SERVER['REMOTE_ADDR']);
10010    
10011    if (!$long_ip) {
10012      return false;
10013    }
10014    
10015    if (!spam_filter_is_user_known($long_ip, BOARD_DIR, null, 15)) {
10016      return false;
10017    }
10018    
10019    $salt = file_get_contents_cached(SALTFILE);
10020    
10021    if (!$salt) {
10022      return false;
10023    }
10024    
10025    $time = $_SERVER['REQUEST_TIME'];
10026    
10027    $msg = $_SERVER['REMOTE_ADDR'] . '.' . $time;
10028    
10029    $msg = hash_hmac('sha1', $msg, $salt);
10030    
10031    if (!$msg) {
10032      return false;
10033    }
10034    
10035    $msg = substr($msg, 0, 20) . '.' . $time;
10036    
10037    setcookie('_ct', $msg, $time + 300, '/', '.' . L::d(BOARD_DIR)); // 5 minutes
10038  }
10039  
10040  function verify_no_captcha_token($token) {
10041    list($hash, $ts) = explode('.', $token);
10042    
10043    $ts = (int)$ts;
10044    
10045    if (!$hash || !$ts) {
10046      return false;
10047    }
10048    
10049    if ($ts < $_SERVER['REQUEST_TIME'] - 300) { // 5 minutes
10050      return false;
10051    }
10052    
10053    $salt = file_get_contents_cached(SALTFILE);
10054    
10055    if (!$salt) {
10056      return false;
10057    }
10058    
10059    $msg = $_SERVER['REMOTE_ADDR'] . '.' . $ts;
10060    
10061    if (substr(hash_hmac('sha1', $msg, $salt), 0, 20) === $hash) {
10062      return true;
10063    }
10064    
10065    return false;
10066  }
10067  
10068  function get_random_real_name() {
10069    $first_name_nid = mt_rand(1, 1000);
10070    
10071    if (mt_rand(0, 999) < 10) {
10072      $type = 2;
10073    }
10074    else {
10075      $type = 1;
10076    }
10077    
10078    $query = "SELECT data FROM april_names WHERE nid = $first_name_nid AND type = $type";
10079    $res = mysql_global_call($query);
10080    $first_name = mysql_fetch_row($res)[0];
10081    
10082    if (!$first_name) {
10083      $first_name = 'Alberto';
10084    }
10085    
10086    $last_name_nid = mt_rand(1, 1000);
10087    
10088    $query = "SELECT data FROM april_names WHERE nid = $last_name_nid AND type = 3";
10089    $res = mysql_global_call($query);
10090    $last_name = mysql_fetch_row($res)[0];
10091    
10092    if (!$last_name) {
10093      $last_name = 'Barbosa';
10094    }
10095    
10096    return "$first_name $last_name";
10097  }
10098  
10099  
10100  function log_mod_action($action_type, $post, $vip_capcode = false) {
10101    $mask_shift = 128;
10102    $action_id = $mask_shift + $action_type;
10103    
10104    $query =<<<SQL
10105  INSERT INTO actions_log (oldmask, newmask, postno, board, name, sub, com, filename, admin)
10106  VALUES (0, %d, %d, '%s', '%s', '%s', '%s', '%s', '%s')
10107  SQL;
10108    
10109    mysql_global_call($query,
10110      $action_id,
10111      $post['no'],
10112      BOARD_DIR,
10113      $post['name'],
10114      $post['sub'],
10115      $post['com'],
10116      $post['filename'] . $post['ext'],
10117      $vip_capcode === false ? $_COOKIE['4chan_auser'] : ''
10118    );
10119    
10120    return true;
10121  }
10122  
10123  function get_request_xff() {
10124    $xff = $_SERVER['HTTP_X_FORWARDED_FOR'];
10125    
10126    if (!$xff) {
10127      return false;
10128    }
10129    
10130    if ($xff === $_SERVER['REMOTE_ADDR']) {
10131      return false;
10132    }
10133    
10134    // For Cloudflare
10135    if (strpos($xff, ',') !== false) {
10136      $xff = explode(',', $xff);
10137      return end($xff);
10138    }
10139    else {
10140      return $xff;
10141    }
10142  }
10143  
10144  function validate_otp() {
10145    if (!isset($_POST['otp']) || $_POST['otp'] == '') {
10146      error("Incorrect or expired OTP.");
10147    }
10148    
10149    $otp = $_POST['otp'];
10150    
10151    $query = "SELECT auth_secret FROM mod_users WHERE username = '%s' LIMIT 1";
10152    
10153    $res = mysql_global_call($query, $_COOKIE['4chan_auser']);
10154    
10155    if (!$res) {
10156      error("Database error.");
10157    }
10158    
10159    $user = mysql_fetch_assoc($res);
10160    
10161    if (!$user || !$user['auth_secret']) {
10162      error("Incorrect or expired OTP.");
10163    }
10164    
10165    require_once 'lib/GoogleAuthenticator.php';
10166    
10167    $ga = new PHPGangsta_GoogleAuthenticator();
10168    
10169    $dec_secret = auth_decrypt($user['auth_secret']);
10170    
10171    if ($dec_secret === false) {
10172      error('Internal Server Error.');
10173    }
10174    
10175    if (!$ga->verifyCode($dec_secret, $otp, 1)) {
10176      error("Incorrect or expired OTP.");
10177    }
10178  }
10179  
10180  function validate_csrf() {
10181    if ($_SERVER['REQUEST_METHOD'] != 'POST') {
10182      error('Bad Request.');
10183    }
10184    
10185    if (!isset($_COOKIE['_tkn']) || !isset($_POST['_tkn'])
10186      || $_COOKIE['_tkn'] == '' || $_POST['_tkn'] == ''
10187      || $_COOKIE['_tkn'] !== $_POST['_tkn']) {
10188      
10189      if (!is_local()) {
10190        error('Bad Request.');
10191      }
10192    }
10193  }
10194  
10195  function validate_referer($strict = false) {
10196    if (!$strict && (!isset($_SERVER['HTTP_REFERER']) || $_SERVER['HTTP_REFERER'] == '')) {
10197      return;
10198    }
10199    
10200    if (!preg_match('/^https?:\/\/([_a-z0-9]+)\.(4chan|4channel)\.org(\/|$)/', $_SERVER['HTTP_REFERER'])) {
10201      error('Bad Request.');
10202    }
10203  }
10204  
10205  function dev_make_remote_thumbnail() {
10206    if (!is_local()) {
10207      die('403');
10208    }
10209    
10210    $infile = $_FILES['file']['tmp_name'];
10211    $file_ext = $_POST['file_ext'];
10212    $src_width = $_POST['src_width'];
10213    $src_height = $_POST['src_height'];
10214    $th_width = $_POST['th_width'];
10215    $th_height = $_POST['th_height'];
10216    
10217    if (!$infile || !$file_ext || !$src_width || !$src_height || !$th_width || !$th_height) {
10218      return;
10219    }
10220    
10221    $jpeg_quality = 65;
10222    
10223    switch ($file_ext) {
10224      case 'gif':
10225        $img_in = ImageCreateFromGIF($infile);
10226        break;
10227      case 'jpg':
10228        $img_in = ImageCreateFromJPEG($infile);
10229        break;
10230      case 'png':
10231        $img_in = ImageCreateFromPNG($infile);
10232        break;
10233      default :
10234        return;
10235    }
10236    
10237    if (!$img_in) {
10238      return;
10239    }
10240    
10241    $img_out = ImageCreateTrueColor($th_width, $th_height);
10242    
10243    if (!$img_out) {
10244      return;
10245    }
10246    
10247    ImageCopyResampled($img_out, $img_in, 0, 0, 0, 0, $th_width, $th_height, $src_width, $src_height);
10248    
10249    ImageDestroy($img_in);
10250    
10251    ImageJPEG($img_out, NULL, $jpeg_quality);
10252    
10253    ImageDestroy($img_out);
10254  }
10255  
10256  /*-----------Main-------------*/
10257  switch( $mode ) {
10258    case 'make_remote_thumbnail':
10259      dev_make_remote_thumbnail();
10260      die();
10261    case 'purgejsontails':
10262      if (has_flag('developer')) {
10263        echo purge_json_tails() ? 'OK' : 'ERROR';
10264      }
10265      die();
10266    case 'listarchive':
10267      if (has_flag('developer')) {
10268        rebuild_archive_list(true);
10269      }
10270      die();
10271    case 'search':
10272      if (has_flag('developer')) {
10273        rebuild_search_page(true);
10274      }
10275      die();
10276    case 'rebuildsearchpage':
10277      if (has_flag('developer') || has_level('manager')) {
10278        rebuild_search_page();
10279        echo 'done';
10280      }
10281      die();
10282    case 'rebuildsyncframepage':
10283      if (has_flag('developer') || has_level('manager')) {
10284        rebuild_syncframe_page(isset($_GET['print']));
10285        echo 'done';
10286      }
10287      die();
10288    case 'rebuildarchivedthread':
10289      if (has_flag('developer') || has_level('manager')) {
10290        rebuild_archived_thread((int)$_GET['id']);
10291        echo 'done';
10292      }
10293      die();
10294    
10295  	case 'regist':
10296  	case 'post':
10297  		require_request_method( "POST" );
10298  		validate_referer();
10299  		new_post( $name, $email, $sub, $com, '', $pwd, $upfile, $upfile_name, $resto, $age, $filetag );
10300  		break;
10301  	case 'report':
10302  		report();
10303  		break;
10304  	
10305  	case 'preview_html':
10306  	  preview_html();
10307  	  break;
10308  	
10309  	case 'rebuild':
10310  		require_request_method( "GET" );
10311  		rebuild();
10312  		break;
10313  	case 'rebuildall':
10314  		rebuild( 1 );
10315  		break;
10316  
10317  	case 'rebuildadmin':
10318  		rebuild_deletions( array($no => '1') );
10319  		echo '<span style="display: none;">Rebuilt OK!</span>'; // shut rpc up
10320  		break;
10321  
10322  	case 'rebuildcatalog':
10323  	  if (ENABLE_CATALOG) {
10324        rebuild_catalog();
10325  	  }
10326  		break;
10327  
10328  	case 'rebuildboardsjson':
10329      if (has_flag('developer')) {
10330  		  rebuild_boards_json();
10331  	  }
10332  		break;
10333  
10334  	case 'rebuildthumb':
10335  		rebuildallthumb(isset($_GET['archiveonly']) || isset($_ENV['archiveonly']));
10336  		break;
10337  
10338  	case 'cataloginfo':
10339  		get_catalog_info();
10340  		break;
10341  
10342  	case 'admindel': case 'admindelete':
10343  		user_delete( $no, $pwd );
10344  		echo "<meta http-equiv=\"refresh\" content=\"0;URL=admin.php\">";
10345  		break;
10346  	case 'updatelog':
10347  		updatelog_remote( $no, $noidx );
10348  		break;
10349  	case 'nothing':
10350  		break;
10351    case 'arcdel':
10352      require_request_method( "POST" );
10353  		validate_referer();
10354      arcdel($no, true, $res);
10355      break;
10356  	case 'usrdel': case 'delete':
10357  		require_request_method( "POST" );
10358  		validate_referer();
10359  		user_delete( $no, $pwd, true, $res );
10360  		break;
10361  	case 'rebuild_threads_by_id':
10362      require_request_method( "POST" );
10363      if (is_local()) {
10364        rebuild_threads_by_id();
10365      }
10366      else {
10367        updating_index();
10368      }
10369  	  break;
10370  	case 'forcearchive':
10371  		require_request_method( "POST" );
10372  		validate_referer(); //validate_csrf();
10373  	  forcearchive();
10374  	  break;
10375  	case 'copythreads':
10376  	require_request_method( "GET" );
10377  	do_copy_threads();
10378  	break;
10379  	case 'movethread':
10380  		validate_csrf();
10381  	  do_move_thread();
10382  	  break;
10383  	case 'latest':
10384  	  if (has_level('janitor')) {
10385  	    get_last_post_no();
10386  	  }
10387  	  die();
10388    case 'rake_post':
10389    	//april_rake_commit();
10390      die('0\nNo');
10391      break;
10392  	default:
10393  		require_request_method( "GET" );
10394  		if( JANITOR_BOARD == 1 && !has_level( 'janitor' ) ) {
10395  			die( '' );
10396  		}
10397  		if( $res ) {
10398  			resredir( $res );
10399  		} else {
10400  			updating_index();
10401  		}
10402  }