diff --git a/.gitignore b/.gitignore index 8fecf08c..825799da 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,12 @@ Thumbs.db *.orig *~ +# tmp filesystem +/tmp/cache/* +/tmp/locks/* +!/tmp/cache/.gitkeep +!/tmp/locks/.gitkeep + #vichan custom favicon.ico /static/spoiler.png diff --git a/404.php b/404.php index e74eb65b..7422be23 100644 --- a/404.php +++ b/404.php @@ -3,6 +3,8 @@ require_once "inc/functions.php"; header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found"); +global $config; + $dir = "static/404/"; if (!is_dir($dir)) diff --git a/board-search.php b/board-search.php new file mode 100644 index 00000000..7df27f4f --- /dev/null +++ b/board-search.php @@ -0,0 +1,313 @@ + false, + 'nsfw' => true, + 'page' => 0, + 'tags' => false, + 'time' => ( (int)( time() / 3600 ) * 3600 ) - 3600, + 'title' => false, + + 'index' => count( $_GET ) == 0, +); + +// Include NSFW boards? +if (isset( $_GET['sfw'] ) && $_GET['sfw'] != "") { + $search['nsfw'] = !$_GET['sfw']; +} + +// Bringing up more results +if (isset( $_GET['page'] ) && $_GET['page'] != "") { + $search['page'] = (int) $_GET['page']; + + if ($search['page'] < 0) { + $search['page'] = 0; + } +} + +// Include what language (if the language is not blank and we recognize it)? +if (isset( $_GET['lang'] ) && $_GET['lang'] != "" && isset($config['languages'][$_GET['lang']])) { + $search['lang'] = $_GET['lang']; +} + +// Include what tag? +if (isset( $_GET['tags'] ) && $_GET['tags'] != "") { + $search['tags'] = explode( " ", $_GET['tags'] ); + $search['tags'] = array_splice( $search['tags'], 0, 5 ); +} + +// What time range? +if (isset( $_GET['time'] ) && is_numeric( $_GET['time'] ) ) { + $search['time'] = ( (int)( $_GET['time'] / 3600 ) * 3600 ); +} + +// Include what in the uri / title / subtitle? +if (isset( $_GET['title'] ) && $_GET['title'] != "") { + $search['title'] = $_GET['title']; +} + +/* Search boards */ +$boards = listBoards(); +$response['boards'] = array(); + +// Loop through our available boards and filter out inapplicable ones based on standard filtering. +foreach ($boards as $board) { + // Checks we can do without looking at config. + if ( + // Indexed, or we are staff, + ( $CanViewUnindexed !== true && !$board['indexed'] ) + // Not filtering NSFW, or board is SFW. + || ( $search['nsfw'] !== true && $board['sfw'] != 1 ) + ) { + continue; + } + + // Are we searching by title? + if ($search['title'] !== false) { + // This checks each component of the board's identity against our search terms. + // The weight determines order. + // "left" would match /leftypol/ and /nkvd/ which has /leftypol/ in the title. + // /leftypol/ would always appear above it but it would match both. + if (strpos("/{$board['uri']}/", $search['title']) !== false) { + $board['weight'] = 30; + } + else if (strpos($board['title'], $search['title']) !== false) { + $board['weight'] = 20; + } + else if (strpos($board['subtitle'], $search['title']) !== false) { + $board['weight'] = 10; + } + else { + continue; + } + + unset( $boardTitleString ); + } + else { + $board['weight'] = 0; + } + + // Load board config. + $boardConfig = loadBoardConfig( $board['uri'] ); + + // Determine language/locale and tags. + $boardLang = strtolower( array_slice( explode( "_", $boardConfig['locale'] ?: "" ), 0 )[0] ); // en_US -> en OR en -> en + + // Check against our config search options. + if ($search['lang'] !== false && $search['lang'] != $boardLang) { + continue; + } + + if (isset($config['languages'][$boardLang])) { + $board['locale'] = $config['languages'][$boardLang]; + } + else { + $board['locale'] = $boardLang; + } + + $response['boards'][ $board['uri'] ] = $board; +} + +unset( $boards ); + +/* Tag Fetching */ +// (We have do this even if we're not filtering by tags so that we know what each board's tags are) + +// Fetch all board tags for our boards. +$boardTags = fetchBoardTags( array_keys( $response['boards'] ) ); + +// Loop through each board and determine if there are tag matches. +foreach ($response['boards'] as $boardUri => &$board) { + // If we are filtering by tag and there is no match, remove from the response. + if ( $search['tags'] !== false && ( !isset( $boardTags[ $boardUri ] ) || count(array_intersect($search['tags'], $boardTags[ $boardUri ])) !== count($search['tags']) ) ) { + unset( $response['boards'][$boardUri] ); + continue; + } + // If we aren't filtering / there is a match AND we have tags, set the tags. + else if ( isset( $boardTags[ $boardUri ] ) && $boardTags[ $boardUri ] ) { + $board['tags'] = $boardTags[ $boardUri ]; + } + // Othrwise, just declare our tag array blank. + else { + $board['tags'] = array(); + } + + // Legacy support for API readers. + $board['max'] = &$board['posts_total']; +} + +unset( $boardTags ); + + +/* Activity Fetching */ +$boardActivity = fetchBoardActivity( array_keys( $response['boards'] ), $search['time'], true ); + +// Loop through each board and record activity to it. +// We will also be weighing and building a tag list. +foreach ($response['boards'] as $boardUri => &$board) { + $board['active'] = 0; + $board['pph'] = 0; + $board['ppd'] = 0; + + if (isset($boardActivity['active'][ $boardUri ])) { + $board['active'] = (int) $boardActivity['active'][ $boardUri ]; + } + if (isset($boardActivity['average'][ $boardUri ])) { + $precision = 1; + + $board['pph'] = round( $boardActivity['average'][ $boardUri ], $precision ); + $board['ppd'] = round( $boardActivity['today'][ $boardUri ], $precision ); + + unset( $precision ); + } +} + +// Sort boards by their popularity, then by their total posts. +$boardActivityValues = array(); +$boardTotalPostsValues = array(); +$boardWeightValues = array(); + +foreach ($response['boards'] as $boardUri => &$board) { + $boardActivityValues[$boardUri] = (int) $board['active']; + $boardTotalPostsValues[$boardUri] = (int) $board['posts_total']; + $boardWeightValues[$boardUri] = (int) $board['weight']; +} + +array_multisort( + $boardWeightValues, SORT_DESC, SORT_NUMERIC, // Sort by weight + $boardActivityValues, SORT_DESC, SORT_NUMERIC, // Sort by number of active posters + $boardTotalPostsValues, SORT_DESC, SORT_NUMERIC, // Then, sort by total number of posts + $response['boards'] +); + +if (php_sapi_name() == 'cli') { + $response['boardsFull'] = $response['boards']; +} + +$boardLimit = $search['index'] ? 50 : 100; + +$response['omitted'] = count( $response['boards'] ) - $boardLimit; +$response['omitted'] = $response['omitted'] < 0 ? 0 : $response['omitted']; +$response['boards'] = array_splice( $response['boards'], $search['page'], $boardLimit ); +$response['order'] = array_keys( $response['boards'] ); + + +// Loop through the truncated array to compile tags. +$response['tags'] = array(); +$tagUsage = array( 'boards' => array(), 'users' => array() ); + +foreach ($response['boards'] as $boardUri => &$board) { + if (isset($board['tags']) && count($board['tags']) > 0) { + foreach ($board['tags'] as $tag) { + if (!isset($tagUsage['boards'][$tag])) { + $tagUsage['boards'][$tag] = 0; + } + if (!isset($tagUsage['users'][$tag])) { + $tagUsage['users'][$tag] = 0; + } + + $response['tags'][$tag] = true; + ++$tagUsage['boards'][$tag]; + $tagUsage['users'][$tag] += $board['active']; + } + } +} + +// Get the top most popular tags. +if (count($response['tags']) > 0) { + arsort( $tagUsage['boards'] ); + arsort( $tagUsage['users'] ); + + array_multisort( + $tagUsage['boards'], SORT_DESC, SORT_NUMERIC, + $tagUsage['users'], SORT_DESC, SORT_NUMERIC, + $response['tags'] + ); + + // Get the first n most active tags. + $response['tags'] = array_splice( $response['tags'], 0, 100 ); + $response['tagOrder'] = array_keys( $response['tags'] ); + $response['tagWeight'] = array(); + + $tagsMostUsers = max( $tagUsage['users'] ); + $tagsLeastUsers = min( $tagUsage['users'] ); + $tagsAvgUsers = array_sum( $tagUsage['users'] ) / count( $tagUsage['users'] ); + + $weightDepartureFurthest = 0; + + foreach ($tagUsage['users'] as $tagUsers) { + $weightDeparture = abs( $tagUsers - $tagsAvgUsers ); + + if( $weightDeparture > $weightDepartureFurthest ) { + $weightDepartureFurthest = $weightDeparture; + } + } + + foreach ($tagUsage['users'] as $tagName => $tagUsers) { + if ($weightDepartureFurthest != 0) { + $weightDeparture = abs( $tagUsers - $tagsAvgUsers ); + $response['tagWeight'][$tagName] = 75 + round( 100 * ( $weightDeparture / $weightDepartureFurthest ), 0); + } + else { + $response['tagWeight'][$tagName] = 100; + } + } +} + +/* Include our interpreted search terms. */ +$response['search'] = $search; + +/* (Please) Respond */ +if (!$Included) { + $json = json_encode( $response ); + + // Error Handling + switch (json_last_error()) { + case JSON_ERROR_NONE: + $jsonError = false; + break; + case JSON_ERROR_DEPTH: + $jsonError = 'Maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $jsonError = 'Underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR: + $jsonError = 'Unexpected control character found'; + break; + case JSON_ERROR_SYNTAX: + $jsonError = 'Syntax error, malformed JSON'; + break; + case JSON_ERROR_UTF8: + $jsonError = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $jsonError = 'Unknown error'; + break; + } + + if ($jsonError) { + $json = "{\"error\":\"{$jsonError}\"}"; + } + + // Successful output + echo $json; +} +else { + return $response; +} \ No newline at end of file diff --git a/boards.php b/boards.php index 87ba7ddf..2ca61816 100644 --- a/boards.php +++ b/boards.php @@ -1,152 +1,109 @@ $board) { - $query = prepare(sprintf(" -SELECT IFNULL(MAX(id),0) max, -(SELECT COUNT(*) FROM ``posts_%s`` WHERE FROM_UNIXTIME(time) > DATE_SUB(NOW(), INTERVAL 1 HOUR)) pph, -(SELECT COUNT(DISTINCT ip) FROM ``posts_%s`` WHERE FROM_UNIXTIME(time) > DATE_SUB(NOW(), INTERVAL 3 DAY)) uniq_ip - FROM ``posts_%s`` -", $board['uri'], $board['uri'], $board['uri'], $board['uri'], $board['uri'])); - $query->execute() or error(db_error($query)); - $r = $query->fetch(PDO::FETCH_ASSOC); - - $tquery = prepare("SELECT `tag` FROM ``board_tags`` WHERE `uri` = :uri"); - $tquery->execute([":uri" => $board['uri']]) or error(db_error($tquery)); - $r2 = $tquery->fetchAll(PDO::FETCH_ASSOC); - - $tags = array(); - if ($r2) { - foreach ($r2 as $ii => $t) { - $tag=to_tag($t['tag']); - $tags[] = $tag; - if (!isset($all_tags[$tag])) { - $all_tags[$tag] = (int)$r['uniq_ip']; - } else { - $all_tags[$tag] += $r['uniq_ip']; - } - } +if (count($searchJson)) { + if (isset($searchJson['boards'])) { + $boards = $searchJson['boards']; } - - $pph = $r['pph']; - - $total_posts_hour += $pph; - $total_posts += $r['max']; - - $boards[$i]['pph'] = $pph; - $boards[$i]['ppd'] = $pph*24; - $boards[$i]['max'] = $r['max']; - $boards[$i]['uniq_ip'] = $r['uniq_ip']; - $boards[$i]['tags'] = $tags; - - if ($write_maxes) fwrite($fp, $board['uri'] . ':' . $boards[$i]['max'] . "\n"); -} -if ($write_maxes) fclose($fp); - -usort($boards, -function ($a, $b) { - $x = $b['uniq_ip'] - $a['uniq_ip']; - if ($x) { return $x; - //} else { return strcmp($a['uri'], $b['uri']); } - } else { return $b['max'] - $a['max']; } -}); - -$hidden_boards_total = 0; -$rows = array(); -foreach ($boards as $i => &$board) { - $board_config = @file_get_contents($board['uri'].'/config.php'); - $boardCONFIG = array(); - if ($board_config && $board['uri'] !== 'int') { - $board_config = str_replace('$config', '$boardCONFIG', $board_config); - $board_config = str_replace('"; - - if ($showboard || $admin) { - if (!$showboard) { - $lock = ' '; - } else { - $lock = ''; - } - $board['ago'] = human_time_diff(strtotime($board['time'])); - } else { - unset($boards[$i]); - $hidden_boards_total += 1; + if (isset($searchJson['tagWeight'])) { + $tags = $searchJson['tagWeight']; } } -$n_boards = sizeof($boards); -$t_boards = $hidden_boards_total + $n_boards; +$boardQuery = prepare("SELECT COUNT(1) AS 'boards_total', SUM(indexed) AS 'boards_public', SUM(posts_total) AS 'posts_total' FROM ``boards``"); +$boardQuery->execute() or error(db_error($tagQuery)); +$boardResult = $boardQuery->fetchAll(PDO::FETCH_ASSOC)[0]; -$boards = array_values($boards); -arsort($all_tags); +$boards_total = number_format( $boardResult['boards_total'], 0 ); +$boards_public = number_format( $boardResult['boards_public'], 0 ); +$boards_hidden = number_format( $boardResult['boards_total'] - $boardResult['boards_public'], 0 ); +$boards_omitted = (int) $searchJson['omitted']; -$config['additional_javascript'] = array('js/jquery.min.js', 'js/jquery.tablesorter.min.js'); -$body = Element("8chan/boards-tags.html", array("config" => $config, "n_boards" => $n_boards, "t_boards" => $t_boards, "hidden_boards_total" => $hidden_boards_total, "total_posts" => $total_posts, "total_posts_hour" => $total_posts_hour, "boards" => $boards, "last_update" => date('r'), "uptime_p" => shell_exec('uptime -p'), 'tags' => $all_tags, 'top2k' => false)); +$posts_hour = number_format( fetchBoardActivity(), 0 ); +$posts_total = number_format( $boardResult['posts_total'], 0 ); -$html = Element("page.html", array("config" => $config, "body" => $body, "title" => "Boards on ∞chan")); -$boards_top2k = $boards; -array_splice($boards_top2k, 2000); -$boards_top2k = array_values($boards_top2k); -$body = Element("8chan/boards-tags.html", array("config" => $config, "n_boards" => $n_boards, "t_boards" => $t_boards, "hidden_boards_total" => $hidden_boards_total, "total_posts" => $total_posts, "total_posts_hour" => $total_posts_hour, "boards" => $boards_top2k, "last_update" => date('r'), "uptime_p" => shell_exec('uptime -p'), 'tags' => $all_tags, 'top2k' => true)); -$html_top2k = Element("page.html", array("config" => $config, "body" => $body, "title" => "Boards on ∞chan")); +// This incredibly stupid looking chunk of code builds a query string using existing information. +// It's used to make clickable tags for users without JavaScript for graceful degredation. +// Because of how it orders tags, what you end up with is a prefix that always ends in tags=x+ +// ?tags= or ?sfw=1&tags= or ?title=foo&tags=bar+ - etc +$tagQueryGet = $_GET; +$tagQueryTags = isset($tagQueryGet['tags']) ? $tagQueryGet['tags'] : ""; +unset($tagQueryGet['tags']); +$tagQueryGet['tags'] = $tagQueryTags; +$tag_query = "?" . http_build_query( $tagQueryGet ) . ($tagQueryTags != "" ? "+" : ""); -if ($admin) { - echo $html; -} else { - foreach ($boards as $i => &$b) { unset($b['img']); } - file_write("boards.json", json_encode($boards)); - file_write("tags.json", json_encode($all_tags)); - foreach ($boards as $i => $b) { - if (in_array($b['uri'], $config['no_top_bar_boards'])) { - unset($boards[$i]); - } - unset($boards[$i]['img']); - } +/* Create and distribute page */ +// buildJavascript(); - array_splice($boards, 48); +$boardsHTML = Element("8chan/boards-table.html", array( + "config" => $config, + "boards" => $boards, + "tag_query" => $tag_query, + + ) +); - $boards = array_values($boards); +$tagsHTML = Element("8chan/boards-tags.html", array( + "config" => $config, + "tags" => $tags, + "tag_query" => $tag_query, + + ) +); - file_write("boards-top20.json", json_encode($boards)); - file_write("boards.html", $html_top2k); - file_write("boards_full.html", $html); - echo 'Done'; +$searchHTML = Element("8chan/boards-search.html", array( + "config" => $config, + "boards" => $boards, + "tags" => $tags, + "search" => $searchJson['search'], + "languages" => $config['languages'], + + "boards_total" => $boards_total, + "boards_public" => $boards_public, + "boards_hidden" => $boards_hidden, + "boards_omitted" => $boards_omitted, + + "posts_hour" => $posts_hour, + "posts_total" => $posts_total, + + "founding_date" => $founding_date, + "page_updated" => date('r'), + "uptime" => shell_exec('uptime -p'), + + "html_boards" => $boardsHTML, + "html_tags" => $tagsHTML + ) +); + +$pageHTML = Element("page.html", array( + "title" => _("Boardlist"), + "config" => $config, + "body" => $searchHTML + ) +); + +// We only want to cache if this is not a dynamic form request. +// Otherwise, our information will be skewed by the search criteria. +if (php_sapi_name() == 'cli') { + // Preserves the JSON output format of [{board},{board}]. + $nonAssociativeBoardList = array_values($response['boardsFull']); + + file_write("boards.html", $pageHTML); + file_write("boards.json", json_encode($nonAssociativeBoardList)); + file_write("boards-top20.json", json_encode(array_splice($nonAssociativeBoardList, 0, 48))); } +echo $pageHTML; \ No newline at end of file diff --git a/inc/8chan-functions.php b/inc/8chan-functions.php index 5a50ec74..ef289616 100644 --- a/inc/8chan-functions.php +++ b/inc/8chan-functions.php @@ -54,3 +54,23 @@ function human_time_diff( $from, $to = '' ) { return $since; } + +function is_billion_laughs($arr1, $arr2) { + $arr = array(); + foreach ($arr1 as $k => $v) { + $arr[$v] = $arr2[$k]; + } + + for ($i = 0; $i <= sizeof($arr); $i++) { + $cur = array_slice($arr, $i, 1); + $pst = array_slice($arr, 0, $i); + if (!$cur) continue; + $kk = array_keys($cur)[0]; + $vv = array_values($cur)[0]; + foreach ($pst as $k => $v) { + if (str_replace($kk, $vv, $v) != $v) + return true; + } + } + return false; +} diff --git a/inc/8chan-mod-config.php b/inc/8chan-mod-config.php new file mode 100644 index 00000000..1c2ed2ac --- /dev/null +++ b/inc/8chan-mod-config.php @@ -0,0 +1,53 @@ + $v) { - $arr[$v] = $arr2[$k]; - } - - for ($i = 0; $i <= sizeof($arr); $i++) { - $cur = array_slice($arr, $i, 1); - $pst = array_slice($arr, 0, $i); - if (!$cur) continue; - $kk = array_keys($cur)[0]; - $vv = array_values($cur)[0]; - foreach ($pst as $k => $v) { - if (str_replace($kk, $vv, $v) != $v) - return true; - } - } - return false; - } - } - - $config['mod']['show_ip'] = GLOBALVOLUNTEER; - $config['mod']['show_ip_less'] = BOARDVOLUNTEER; - $config['mod']['manageusers'] = GLOBALVOLUNTEER; - $config['mod']['noticeboard_post'] = GLOBALVOLUNTEER; - $config['mod']['search'] = GLOBALVOLUNTEER; - $config['mod']['clean_global'] = GLOBALVOLUNTEER; - $config['mod']['view_notes'] = DISABLED; - $config['mod']['create_notes'] = DISABLED; - $config['mod']['edit_config'] = DISABLED; - $config['mod']['debug_recent'] = ADMIN; - $config['mod']['debug_antispam'] = ADMIN; - $config['mod']['noticeboard_post'] = ADMIN; - $config['mod']['modlog'] = GLOBALVOLUNTEER; - $config['mod']['mod_board_log'] = MOD; - $config['mod']['editpost'] = BOARDVOLUNTEER; - $config['mod']['edit_banners'] = MOD; - $config['mod']['edit_flags'] = MOD; - $config['mod']['edit_settings'] = MOD; - $config['mod']['edit_volunteers'] = MOD; - $config['mod']['edit_tags'] = MOD; - $config['mod']['clean'] = BOARDVOLUNTEER; - // new perms - - $config['mod']['ban'] = BOARDVOLUNTEER; - $config['mod']['bandelete'] = BOARDVOLUNTEER; - $config['mod']['unban'] = BOARDVOLUNTEER; - $config['mod']['deletebyip'] = BOARDVOLUNTEER; - $config['mod']['sticky'] = BOARDVOLUNTEER; - $config['mod']['cycle'] = BOARDVOLUNTEER; - $config['mod']['lock'] = BOARDVOLUNTEER; - $config['mod']['postinlocked'] = BOARDVOLUNTEER; - $config['mod']['bumplock'] = BOARDVOLUNTEER; - $config['mod']['view_bumplock'] = BOARDVOLUNTEER; - $config['mod']['bypass_field_disable'] = BOARDVOLUNTEER; - $config['mod']['view_banlist'] = BOARDVOLUNTEER; - $config['mod']['view_banstaff'] = BOARDVOLUNTEER; - $config['mod']['public_ban'] = BOARDVOLUNTEER; - $config['mod']['recent'] = BOARDVOLUNTEER; - $config['mod']['ban_appeals'] = BOARDVOLUNTEER; - $config['mod']['view_ban_appeals'] = BOARDVOLUNTEER; - $config['mod']['view_ban'] = BOARDVOLUNTEER; - $config['mod']['reassign_board'] = GLOBALVOLUNTEER; - $config['mod']['move'] = GLOBALVOLUNTEER; - $config['mod']['shadow_capcode'] = 'Global Volunteer'; - - - $config['mod']['custom_pages']['/tags/(\%b)'] = function ($b) { + function mod_8_tags ($b) { global $board, $config; if (!openBoard($b)) @@ -114,9 +46,9 @@ $sfw = $query->fetchColumn(); mod_page(_('Edit tags'), 'mod/tags.html', array('board'=>$board,'token'=>make_secure_link_token('tags/'.$board['uri']), 'tags'=>$tags, 'sfw'=>$sfw)); - }; + } - $config['mod']['custom_pages']['/reassign/(\%b)'] = function($b) { + function mod_8_reassign($b) { global $board, $config; if (!openBoard($b)) @@ -147,9 +79,9 @@ modLog("Reassigned board /$b/"); mod_page(_('Edit reassign'), 'blank.html', array('board'=>$board,'token'=>make_secure_link_token('reassign/'.$board['uri']),'body'=>$body)); - }; + } - $config['mod']['custom_pages']['/volunteers/(\%b)'] = function($b) { + function mod_8_volunteers($b) { global $board, $config, $pdo; if (!hasPermission($config['mod']['edit_volunteers'], $b)) error($config['error']['noaccess']); @@ -228,9 +160,9 @@ mod_page(_('Edit volunteers'), 'mod/volunteers.html', array('board'=>$board,'token'=>make_secure_link_token('volunteers/'.$board['uri']),'volunteers'=>$volunteers)); - }; + } - $config['mod']['custom_pages']['/flags/(\%b)'] = function($b) { + function mod_8_flags($b) { global $config, $mod, $board; require_once 'inc/image.php'; if (!hasPermission($config['mod']['edit_flags'], $b)) @@ -341,6 +273,11 @@ \$config['user_flags'] = unserialize(file_get_contents('$b/flags.ser')); FLAGS; + if ($config['cache']['enabled']) { + cache::delete('config_' . $b); + cache::delete('events_' . $b); + } + file_write($b.'/flags.php', $flags); } @@ -364,9 +301,9 @@ FLAGS; $banners = array_diff(scandir($dir), array('..', '.')); mod_page(_('Edit flags'), 'mod/flags.html', array('board'=>$board,'banners'=>$banners,'token'=>make_secure_link_token('banners/'.$board['uri']))); - }; + } - $config['mod']['custom_pages']['/banners/(\%b)'] = function($b) { + function mod_8_banners($b) { global $config, $mod, $board; require_once 'inc/image.php'; @@ -427,9 +364,9 @@ FLAGS; $banners = array_diff(scandir($dir), array('..', '.')); mod_page(_('Edit banners'), 'mod/banners.html', array('board'=>$board,'banners'=>$banners,'token'=>make_secure_link_token('banners/'.$board['uri']))); - }; + } - $config['mod']['custom_pages']['/settings/(\%b)'] = function($b) { + function mod_8_settings($b) { global $config, $mod; //if ($b === 'infinity' && $mod['type'] !== ADMIN) @@ -661,6 +598,7 @@ EOT; // Faster than openBoard and bypasses cache...we're trusting the PHP output // to be safe enough to run with every request, we can eval it here. eval(str_replace('flags.php', "$b/flags.php", preg_replace('/^\<\?php$/m', '', $config_file))); + // czaks: maybe reconsider using it, now that config is cached? // be smarter about rebuilds...only some changes really require us to rebuild all threads if ($_config['captcha']['enabled'] != $config['captcha']['enabled'] @@ -683,13 +621,18 @@ EOT; $query->bindValue(':board', $b); $query->execute() or error(db_error($query)); $board = $query->fetchAll()[0]; - - $css = @file_get_contents('stylesheets/board/' . $board['uri'] . '.css'); - + + // Clean the cache if ($config['cache']['enabled']) { cache::delete('board_' . $board['uri']); cache::delete('all_boards'); - } + cache::delete('config_' . $board['uri']); + cache::delete('events_' . $board['uri']); + unlink('tmp/cache/locale_' . $board['uri']); + } + + $css = @file_get_contents('stylesheets/board/' . $board['uri'] . '.css'); + mod_page(_('Board configuration'), 'mod/settings.html', array('board'=>$board, 'css'=>prettify_textarea($css), 'token'=>make_secure_link_token('settings/'.$board['uri']), 'languages'=>$possible_languages,'allowed_urls'=>$config['allowed_offsite_urls'])); - }; + } diff --git a/inc/cache.php b/inc/cache.php index d1200919..852aefa2 100644 --- a/inc/cache.php +++ b/inc/cache.php @@ -50,6 +50,17 @@ class Cache { case 'php': $data = isset(self::$cache[$key]) ? self::$cache[$key] : false; break; + case 'fs': + $key = str_replace('/', '::', $key); + $key = str_replace("\0", '', $key); + if (!file_exists('tmp/cache/'.$key)) { + $data = false; + } + else { + $data = file_get_contents('tmp/cache/'.$key); + $data = json_decode($data, true); + } + break; case 'redis': if (!self::$cache) self::init(); @@ -87,6 +98,11 @@ class Cache { case 'xcache': xcache_set($key, $value, $expires); break; + case 'fs': + $key = str_replace('/', '::', $key); + $key = str_replace("\0", '', $key); + file_put_contents('tmp/cache/'.$key, json_encode($value)); + break; case 'php': self::$cache[$key] = $value; break; @@ -113,6 +129,11 @@ class Cache { case 'xcache': xcache_unset($key); break; + case 'fs': + $key = str_replace('/', '::', $key); + $key = str_replace("\0", '', $key); + @unlink('tmp/cache/'.$key); + break; case 'php': unset(self::$cache[$key]); break; @@ -134,6 +155,12 @@ class Cache { case 'php': self::$cache = array(); break; + case 'fs': + $files = glob('tmp/cache/*'); + foreach ($files as $file) { + unlink($file); + } + break; case 'redis': if (!self::$cache) self::init(); diff --git a/inc/config.php b/inc/config.php index b8e7617d..e1ad3eec 100644 --- a/inc/config.php +++ b/inc/config.php @@ -132,6 +132,11 @@ // Tinyboard to use. $config['cache']['redis'] = array('localhost', 6379, '', 1); + // EXPERIMENTAL: Should we cache configs? Warning: this changes board behaviour, i'd say, a lot. + // If you have any lambdas/includes present in your config, you should move them to instance-functions.php + // (this file will be explicitly loaded during cache hit, but not during cache miss). + $config['cache_config'] = false; + /* * ==================== * Cookie settings @@ -1239,9 +1244,20 @@ // Website favicon. $config['url_favicon'] = 'static/favicon.ico'; - // EXPERIMENTAL: Try not to build pages when we shouldn't have to. + // Try not to build pages when we shouldn't have to. $config['try_smarter'] = true; + // EXPERIMENTAL: Defer static HTML building to a moment, when a given file is actually accessed. + // Warning: This option won't run out of the box. You need to tell your webserver, that a file + // for serving 403 and 404 pages is /smart_build.php. Also, you need to turn off indexes. + $config['smart_build'] = false; + + // Smart build related: when a file doesn't exist, where should we redirect? + $config['page_404'] = '/404.html'; + + // Smart build related: extra entrypoints. + $config['smart_build_entrypoints'] = array(); + /* * ==================== * Mod settings diff --git a/inc/display.php b/inc/display.php index d595e5d0..3cbeb818 100644 --- a/inc/display.php +++ b/inc/display.php @@ -348,6 +348,8 @@ function embed_html($link) { class Post { + public $clean; + public function __construct($post, $root=null, $mod=false) { global $config; if (!isset($root)) diff --git a/inc/functions.php b/inc/functions.php index 8674ecd0..b7da4c7f 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -19,7 +19,9 @@ require_once 'inc/database.php'; require_once 'inc/events.php'; require_once 'inc/api.php'; require_once 'inc/bans.php'; -require_once 'inc/lib/gettext/gettext.inc'; +if (!extension_loaded('gettext')) { + require_once 'inc/lib/gettext/gettext.inc'; +} require_once 'inc/lib/parsedown/Parsedown.php'; // todo: option for parsedown instead of Tinyboard/STI markup require_once 'inc/mod/auth.php'; @@ -50,15 +52,42 @@ $current_locale = 'en'; function loadConfig() { - global $board, $config, $__ip, $debug, $__version, $microtime_start, $current_locale; + global $board, $config, $__ip, $debug, $__version, $microtime_start, $current_locale, $events; $error = function_exists('error') ? 'error' : 'basic_error_function_because_the_other_isnt_loaded_yet'; - reset_events(); + $boardsuffix = isset($board['uri']) ? $board['uri'] : ''; if (!isset($_SERVER['REMOTE_ADDR'])) $_SERVER['REMOTE_ADDR'] = '0.0.0.0'; + if (file_exists('tmp/cache/cache_config.php')) { + require_once('tmp/cache/cache_config.php'); + } + + + if (isset($config['cache_config']) && + $config['cache_config'] && + $config = Cache::get('config_' . $boardsuffix ) ) { + $events = Cache::get('events_' . $boardsuffix ); + + define_groups(); + + if (file_exists('inc/instance-functions.php')) { + require_once('inc/instance-functions.php'); + } + + if ($config['locale'] != $current_locale) { + $current_locale = $config['locale']; + init_locale($config['locale'], $error); + } + } + else { + $config = array(); + // We will indent that later. + + reset_events(); + $arrays = array( 'db', 'api', @@ -86,7 +115,6 @@ function loadConfig() { 'dashboard_links' ); - $config = array(); foreach ($arrays as $key) { $config[$key] = array(); } @@ -96,18 +124,28 @@ function loadConfig() { // Initialize locale as early as possible - $config['locale'] = 'en'; + // Those calls are expensive. Unfortunately, our cache system is not initialized at this point. + // So, we may store the locale in a tmp/ filesystem. - $configstr = file_get_contents('inc/instance-config.php'); + if (file_exists($fn = 'tmp/cache/locale_' . $boardsuffix ) ) { + $config['locale'] = file_get_contents($fn); + } + else { + $config['locale'] = 'en'; + + $configstr = file_get_contents('inc/instance-config.php'); if (isset($board['dir']) && file_exists($board['dir'] . '/config.php')) { - $configstr .= file_get_contents($board['dir'] . '/config.php'); + $configstr .= file_get_contents($board['dir'] . '/config.php'); } - $matches = array(); - preg_match_all('/[^\/*#]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches); - if ($matches && isset ($matches[2]) && $matches[2]) { - $matches = $matches[2]; - $config['locale'] = $matches[count($matches)-1]; + $matches = array(); + preg_match_all('/[^\/*#]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches); + if ($matches && isset ($matches[2]) && $matches[2]) { + $matches = $matches[2]; + $config['locale'] = $matches[count($matches)-1]; + } + + file_put_contents($fn, $config['locale']); } if ($config['locale'] != $current_locale) { @@ -128,18 +166,13 @@ function loadConfig() { init_locale($config['locale'], $error); } - if (!isset($__version)) - $__version = file_exists('.installed') ? trim(file_get_contents('.installed')) : false; - $config['version'] = $__version; - - date_default_timezone_set($config['timezone']); - if (!isset($config['global_message'])) $config['global_message'] = false; if (!isset($config['post_url'])) $config['post_url'] = $config['root'] . $config['file_post']; + if (!isset($config['referer_match'])) if (isset($_SERVER['HTTP_HOST'])) { $config['referer_match'] = '/^' . @@ -210,19 +243,26 @@ function loadConfig() { if (!isset($config['user_flags'])) $config['user_flags'] = array(); + if (!isset($__version)) + $__version = file_exists('.installed') ? trim(file_get_contents('.installed')) : false; + $config['version'] = $__version; + + if ($config['allow_roll']) + event_handler('post', 'diceRoller'); + + if (is_array($config['anonymous'])) + $config['anonymous'] = $config['anonymous'][array_rand($config['anonymous'])]; + + + } + // Effectful config processing below: + + date_default_timezone_set($config['timezone']); + if ($config['root_file']) { chdir($config['root_file']); } - if ($config['verbose_errors']) { - set_error_handler('verbose_error_handler'); - error_reporting(E_ALL); - ini_set('display_errors', true); - ini_set('html_errors', false); - } else { - ini_set('display_errors', false); - } - // Keep the original address to properly comply with other board configurations if (!isset($__ip)) $__ip = $_SERVER['REMOTE_ADDR']; @@ -231,11 +271,21 @@ function loadConfig() { if (preg_match('/^\:\:(ffff\:)?(\d+\.\d+\.\d+\.\d+)$/', $__ip, $m)) $_SERVER['REMOTE_ADDR'] = $m[2]; + if ($config['verbose_errors']) { + set_error_handler('verbose_error_handler'); + error_reporting(E_ALL); + ini_set('display_errors', true); + ini_set('html_errors', false); + } else { + ini_set('display_errors', false); + } + if ($config['syslog']) openlog('tinyboard', LOG_ODELAY, LOG_SYSLOG); // open a connection to sysem logger if ($config['recaptcha']) require_once 'inc/lib/recaptcha/recaptchalib.php'; + if ($config['cache']['enabled']) require_once 'inc/cache.php'; @@ -244,13 +294,22 @@ function loadConfig() { event_handler('post', 'postHandler'); } - if (is_array($config['anonymous'])) - $config['anonymous'] = $config['anonymous'][array_rand($config['anonymous'])]; - - if ($config['allow_roll']) - event_handler('post', 'diceRoller'); - event('load-config'); + + if ($config['cache_config'] && !isset ($config['cache_config_loaded'])) { + file_put_contents('tmp/cache/cache_config.php', ' $group_name) - defined($group_name) or define($group_name, $group_value, true); + foreach ($config['mod']['groups'] as $group_value => $group_name) { + $group_name = strtoupper($group_name); + if(!defined($group_name)) { + define($group_name, $group_value, true); + } + } ksort($config['mod']['groups']); } @@ -347,9 +410,22 @@ function rebuildThemes($action, $boardname = false) { $_board = $board; // List themes - $query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error()); + if ($themes = Cache::get("themes")) { + // OK, we already have themes loaded + } + else { + $query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error()); - while ($theme = $query->fetch(PDO::FETCH_ASSOC)) { + $themes = array(); + + while ($theme = $query->fetch(PDO::FETCH_ASSOC)) { + $themes[] = $theme; + } + + Cache::set("themes", $themes); + } + + foreach ($themes as $theme) { // Restore them $config = $_config; $board = $_board; @@ -403,6 +479,10 @@ function rebuildTheme($theme, $action, $board = false) { function themeSettings($theme) { + if ($settings = Cache::get("theme_settings_".$theme)) { + return $settings; + } + $query = prepare("SELECT `name`, `value` FROM ``theme_settings`` WHERE `theme` = :theme AND `name` IS NOT NULL"); $query->bindValue(':theme', $theme); $query->execute() or error(db_error($query)); @@ -412,6 +492,8 @@ function themeSettings($theme) { $settings[$s['name']] = $s['value']; } + Cache::set("theme_settings_".$theme, $settings); + return $settings; } @@ -434,9 +516,9 @@ function setupBoard($array) { $board = array( 'uri' => $array['uri'], 'title' => $array['title'], - 'subtitle' => $array['subtitle'], - 'indexed' => $array['indexed'], - 'public_logs' => $array['public_logs'] + 'subtitle' => isset($array['subtitle']) ? $array['subtitle'] : "", + 'indexed' => isset($array['indexed']) ? $array['indexed'] : true, + 'public_logs' => isset($array['public_logs']) ? $array['public_logs'] : true, ); // older versions @@ -469,6 +551,11 @@ function openBoard($uri) { $board = getBoardInfo($uri); if ($board) { setupBoard($board); + + if (function_exists('after_open_board')) { + after_open_board(); + } + return true; } return false; @@ -541,41 +628,47 @@ function purge($uri) { function file_write($path, $data, $simple = false, $skip_purge = false) { global $config, $debug; - + if (preg_match('/^remote:\/\/(.+)\:(.+)$/', $path, $m)) { if (isset($config['remote'][$m[1]])) { require_once 'inc/remote.php'; - + $remote = new Remote($config['remote'][$m[1]]); $remote->write($data, $m[2]); return; - } else { + } + else { error('Invalid remote server: ' . $m[1]); } } - - if (!$fp = dio_open($path, O_WRONLY | O_CREAT, 0644)) + else { + // This will convert a local, relative path like "b/index.html" to a full path. + // dio_open does not work with relative paths on Windows machines. + $path = realpath(dirname($path)) . DIRECTORY_SEPARATOR . basename($path); + } + + if (!$fp = dio_open( $path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) { error('Unable to open file for writing: ' . $path); - + } + // File locking - if (dio_fcntl($fp, F_SETLKW, array('type' => F_WRLCK)) === -1) { + if (function_exists("dio_fcntl") && dio_fcntl($fp, F_SETLKW, array('type' => F_WRLCK)) === -1) { error('Unable to lock file: ' . $path); } - - // Truncate file - if (!dio_truncate($fp, 0)) - error('Unable to truncate file: ' . $path); - + // Write data - if (($bytes = dio_write($fp, $data)) === false) + if (($bytes = dio_write($fp, $data)) === false) { error('Unable to write to file: ' . $path); - + } + // Unlock - dio_fcntl($fp, F_SETLK, array('type' => F_UNLCK)); - + if (function_exists("dio_fcntl")) { + dio_fcntl($fp, F_SETLK, array('type' => F_UNLCK)); + } + // Close dio_close($fp); - + /** * Create gzipped file. * @@ -630,6 +723,13 @@ function file_unlink($path) { } $ret = @unlink($path); + + if ($config['gzip_static']) { + $gzpath = "$path.gz"; + + @unlink($gzpath); + } + if (isset($config['purge']) && $path[0] != '/' && isset($_SERVER['HTTP_HOST'])) { // Purge cache if (basename($path) == $config['file_index']) { @@ -686,9 +786,24 @@ function listBoards($just_uri = false, $indexed_only = false) { return $boards; if (!$just_uri) { - $query = query("SELECT ``boards``.`uri` uri, ``boards``.`title` title, ``boards``.`subtitle` subtitle, ``board_create``.`time` time, ``boards``.`indexed` indexed, ``boards``.`sfw` sfw FROM ``boards``" . ( $indexed_only ? " WHERE `indexed` = 1 " : "" ) . "LEFT JOIN ``board_create`` ON ``boards``.`uri` = ``board_create``.`uri` ORDER BY ``boards``.`uri`") or error(db_error()); + $query = query( + "SELECT + ``boards``.`uri` uri, + ``boards``.`title` title, + ``boards``.`subtitle` subtitle, + ``board_create``.`time` time, + ``boards``.`indexed` indexed, + ``boards``.`sfw` sfw, + ``boards``.`posts_total` posts_total + FROM ``boards`` + LEFT JOIN ``board_create`` + ON ``boards``.`uri` = ``board_create``.`uri`" . + ( $indexed_only ? " WHERE `indexed` = 1 " : "" ) . + "ORDER BY ``boards``.`uri`") or error(db_error()); + $boards = $query->fetchAll(PDO::FETCH_ASSOC); - } else { + } + else { $boards = array(); $query = query("SELECT `uri` FROM ``boards``" . ( $indexed_only ? " WHERE `indexed` = 1" : "" ) . " ORDER BY ``boards``.`uri`") or error(db_error()); while (true) { @@ -704,6 +819,130 @@ function listBoards($just_uri = false, $indexed_only = false) { return $boards; } +function loadBoardConfig( $uri ) { + $config = array( + "locale" => "en_US", + ); + $configPath = "./{$uri}/config.php"; + + if (file_exists( $configPath ) && is_readable( $configPath )) { + include( $configPath ); + } + + // **DO NOT** use $config outside of this local scope. + // It's used by our global config array. + return $config; +} + +function fetchBoardActivity( array $uris = array(), $forTime = false, $detailed = false ) { + global $config; + + // Set our search time for now if we didn't pass one. + if (!is_integer($forTime)) { + $forTime = time(); + } + + // Get the last hour for this timestamp. + $nowHour = ( (int)( time() / 3600 ) * 3600 ); + // Get the hour before. This is what we actually use for pulling data. + $forHour = ( (int)( $forTime / 3600 ) * 3600 ) - 3600; + // Get the hour from yesterday to calculate posts per day. + $yesterHour = $forHour - ( 3600 * 23 ); + + $boardActivity = array( + 'active' => array(), + 'today' => array(), + 'average' => array(), + ); + + // Query for stats for these boards. + if (count($uris)) { + $uriSearch = "`stat_uri` IN (\"" . implode( (array) $uris, "\",\"" ) . "\") AND "; + } + else { + $uriSearch = ""; + } + + if ($detailed === true) { + $bsQuery = prepare("SELECT `stat_uri`, `stat_hour`, `post_count`, `author_ip_array` FROM ``board_stats`` WHERE {$uriSearch} ( `stat_hour` <= :hour AND `stat_hour` >= :hoursago )"); + $bsQuery->bindValue(':hour', $forHour, PDO::PARAM_INT); + $bsQuery->bindValue(':hoursago', $forHour - ( 3600 * 72 ), PDO::PARAM_INT); + $bsQuery->execute() or error(db_error($bsQuery)); + $bsResult = $bsQuery->fetchAll(PDO::FETCH_ASSOC); + + + // Format the results. + foreach ($bsResult as $bsRow) { + // Do we need to define the arrays for this URI? + if (!isset($boardActivity['active'][$bsRow['stat_uri']])) { + if ($bsRow['stat_hour'] <= $forHour && $bsRow['stat_hour'] >= $yesterHour) { + $boardActivity['today'][$bsRow['stat_uri']] = $bsRow['post_count']; + } + else { + $boardActivity['today'][$bsRow['stat_uri']] = 0; + } + + $boardActivity['active'][$bsRow['stat_uri']] = unserialize( $bsRow['author_ip_array'] ); + $boardActivity['average'][$bsRow['stat_uri']] = $bsRow['post_count']; + } + else { + if ($bsRow['stat_hour'] <= $forHour && $bsRow['stat_hour'] >= $yesterHour) { + $boardActivity['today'][$bsRow['stat_uri']] += $bsRow['post_count']; + } + + $boardActivity['active'][$bsRow['stat_uri']] = array_merge( $boardActivity['active'][$bsRow['stat_uri']], unserialize( $bsRow['author_ip_array'] ) ); + $boardActivity['average'][$bsRow['stat_uri']] += $bsRow['post_count']; + } + } + + foreach ($boardActivity['active'] as &$activity) { + $activity = count( array_unique( $activity ) ); + } + foreach ($boardActivity['average'] as &$activity) { + $activity /= 72; + } + } + // Simple return. + else { + $bsQuery = prepare("SELECT SUM(`post_count`) AS `post_count` FROM ``board_stats`` WHERE {$uriSearch} ( `stat_hour` = :hour )"); + $bsQuery->bindValue(':hour', $forHour, PDO::PARAM_INT); + $bsQuery->execute() or error(db_error($bsQuery)); + $bsResult = $bsQuery->fetchAll(PDO::FETCH_ASSOC); + + $boardActivity = $bsResult[0]['post_count']; + } + + return $boardActivity; +} + +function fetchBoardTags( $uris ) { + global $config; + + $boardTags = array(); + $uris = "\"" . implode( (array) $uris, "\",\"" ) . "\""; + + $tagQuery = prepare("SELECT * FROM ``board_tags`` WHERE `uri` IN ({$uris})"); + $tagQuery->execute() or error(db_error($tagQuery)); + $tagResult = $tagQuery->fetchAll(PDO::FETCH_ASSOC); + + if ($tagResult) { + foreach ($tagResult as $tagRow) { + $tag = $tagRow['tag']; + $tag = trim($tag); + $tag = strtolower($tag); + $tag = str_replace(['_', ' '], '-', $tag); + + if (!isset($boardTags[ $tagRow['uri'] ])) { + $boardTags[ $tagRow['uri'] ] = array(); + } + + $boardTags[ $tagRow['uri'] ][] = htmlentities( utf8_encode( $tag ) ); + } + } + + return $boardTags; +} + function until($timestamp) { $difference = $timestamp - time(); switch(TRUE){ @@ -916,70 +1155,70 @@ function insertFloodPost(array $post) { function post(array $post) { global $pdo, $board; $query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, :cycle, 0, :embed, NULL)", $board['uri'])); - + // Basic stuff if (!empty($post['subject'])) { $query->bindValue(':subject', $post['subject']); } else { $query->bindValue(':subject', null, PDO::PARAM_NULL); } - + if (!empty($post['email'])) { $query->bindValue(':email', $post['email']); } else { $query->bindValue(':email', null, PDO::PARAM_NULL); } - + if (!empty($post['trip'])) { $query->bindValue(':trip', $post['trip']); } else { $query->bindValue(':trip', null, PDO::PARAM_NULL); } - + $query->bindValue(':name', $post['name']); $query->bindValue(':body', $post['body']); $query->bindValue(':body_nomarkup', $post['body_nomarkup']); $query->bindValue(':time', isset($post['time']) ? $post['time'] : time(), PDO::PARAM_INT); - $query->bindValue(':password', $post['password']); + $query->bindValue(':password', $post['password']); $query->bindValue(':ip', isset($post['ip']) ? $post['ip'] : $_SERVER['REMOTE_ADDR']); - + if ($post['op'] && $post['mod'] && isset($post['sticky']) && $post['sticky']) { $query->bindValue(':sticky', true, PDO::PARAM_INT); } else { $query->bindValue(':sticky', false, PDO::PARAM_INT); } - + if ($post['op'] && $post['mod'] && isset($post['locked']) && $post['locked']) { $query->bindValue(':locked', true, PDO::PARAM_INT); } else { $query->bindValue(':locked', false, PDO::PARAM_INT); } - + if ($post['op'] && $post['mod'] && isset($post['cycle']) && $post['cycle']) { $query->bindValue(':cycle', true, PDO::PARAM_INT); } else { $query->bindValue(':cycle', false, PDO::PARAM_INT); } - + if ($post['mod'] && isset($post['capcode']) && $post['capcode']) { $query->bindValue(':capcode', $post['capcode'], PDO::PARAM_INT); } else { $query->bindValue(':capcode', null, PDO::PARAM_NULL); } - + if (!empty($post['embed'])) { $query->bindValue(':embed', $post['embed']); } else { $query->bindValue(':embed', null, PDO::PARAM_NULL); } - + if ($post['op']) { // No parent thread, image $query->bindValue(':thread', null, PDO::PARAM_NULL); } else { $query->bindValue(':thread', $post['thread'], PDO::PARAM_INT); } - + if ($post['has_file']) { $query->bindValue(':files', json_encode($post['files'])); $query->bindValue(':num_files', $post['num_files']); @@ -989,12 +1228,12 @@ function post(array $post) { $query->bindValue(':num_files', 0); $query->bindValue(':filehash', null, PDO::PARAM_NULL); } - + if (!$query->execute()) { undoImage($post); error(db_error($query)); } - + return $pdo->lastInsertId(); } @@ -1004,8 +1243,9 @@ function bumpThread($id) { if (event('bump', $id)) return true; - if ($config['try_smarter']) - $build_pages[] = thread_find_page($id); + if ($config['try_smarter']) { + $build_pages = array_merge(range(1, thread_find_page($id)), $build_pages); + } $query = prepare(sprintf("UPDATE ``posts_%s`` SET `bump` = :time WHERE `id` = :id AND `thread` IS NULL", $board['uri'])); $query->bindValue(':time', time(), PDO::PARAM_INT); @@ -1287,6 +1527,65 @@ function index($page, $mod=false) { ); } +// Handle statistic tracking for a new post. +function updateStatisticsForPost( $post, $new = true ) { + $postIp = isset($post['ip']) ? $post['ip'] : $_SERVER['REMOTE_ADDR']; + $postUri = $post['board']; + $postTime = (int)( $post['time'] / 3600 ) * 3600; + + $bsQuery = prepare("SELECT * FROM ``board_stats`` WHERE `stat_uri` = :uri AND `stat_hour` = :hour"); + $bsQuery->bindValue(':uri', $postUri); + $bsQuery->bindValue(':hour', $postTime, PDO::PARAM_INT); + $bsQuery->execute() or error(db_error($bsQuery)); + $bsResult = $bsQuery->fetchAll(PDO::FETCH_ASSOC); + + // Flesh out the new stats row. + $boardStats = array(); + + // If we already have a row, we're going to be adding this post to it. + if (count($bsResult)) { + $boardStats = $bsResult[0]; + $boardStats['stat_uri'] = $postUri; + $boardStats['stat_hour'] = $postTime; + $boardStats['post_id_array'] = unserialize( $boardStats['post_id_array'] ); + $boardStats['author_ip_array'] = unserialize( $boardStats['author_ip_array'] ); + + ++$boardStats['post_count']; + $boardStats['post_id_array'][] = (int) $post['id']; + $boardStats['author_ip_array'][] = less_ip( $postIp ); + $boardStats['author_ip_array'] = array_unique( $boardStats['author_ip_array'] ); + } + // If this a new row, we're building the stat to only reflect this first post. + else { + $boardStats['stat_uri'] = $postUri; + $boardStats['stat_hour'] = $postTime; + $boardStats['post_count'] = 1; + $boardStats['post_id_array'] = array( (int) $post['id'] ); + $boardStats['author_ip_count'] = 1; + $boardStats['author_ip_array'] = array( less_ip( $postIp ) ); + } + + // Cleanly serialize our array for insertion. + $boardStats['post_id_array'] = str_replace( "\"", "\\\"", serialize( $boardStats['post_id_array'] ) ); + $boardStats['author_ip_array'] = str_replace( "\"", "\\\"", serialize( $boardStats['author_ip_array'] ) ); + + + // Insert this data into our statistics table. + $statsInsert = "VALUES(\"{$boardStats['stat_uri']}\", \"{$boardStats['stat_hour']}\", \"{$boardStats['post_count']}\", \"{$boardStats['post_id_array']}\", \"{$boardStats['author_ip_count']}\", \"{$boardStats['author_ip_array']}\" )"; + + $postStatQuery = prepare( + "REPLACE INTO ``board_stats`` (stat_uri, stat_hour, post_count, post_id_array, author_ip_count, author_ip_array) {$statsInsert}" + ); + $postStatQuery->execute() or error(db_error($postStatQuery)); + + // Update the posts_total tracker on the board. + if ($new) { + query("UPDATE ``boards`` SET `posts_total`=`posts_total`+1 WHERE `uri`=\"{$postUri}\""); + } + + return $boardStats; +} + function getPageButtons($pages, $mod=false) { global $config, $board; @@ -1491,56 +1790,65 @@ function checkMute() { } } -function buildIndex() { +function buildIndex($global_api = "yes") { global $board, $config, $build_pages; - $pages = getPages(); - if (!$config['try_smarter']) - $antibot = create_antibot($board['uri']); + if (!$config['smart_build']) { + $pages = getPages(); + if (!$config['try_smarter']) + $antibot = create_antibot($board['uri']); - if ($config['api']['enabled']) { - $api = new Api(); - $catalog = array(); + if ($config['api']['enabled']) { + $api = new Api(); + $catalog = array(); + } } for ($page = 1; $page <= $config['max_pages']; $page++) { $filename = $board['dir'] . ($page == 1 ? $config['file_index'] : sprintf($config['file_page'], $page)); + $jsonFilename = $board['dir'] . ($page - 1) . '.json'; // pages should start from 0 - if (!$config['api']['enabled'] && $config['try_smarter'] && isset($build_pages) && !empty($build_pages) - && !in_array($page, $build_pages) && is_file($filename)) - continue; - $content = index($page); - if (!$content) - break; - - // json api - if ($config['api']['enabled']) { - $threads = $content['threads']; - $json = json_encode($api->translatePage($threads)); - $jsonFilename = $board['dir'] . ($page - 1) . '.json'; // pages should start from 0 - file_write($jsonFilename, $json); - - $catalog[$page-1] = $threads; - } - - if ($config['api']['enabled'] && $config['try_smarter'] && isset($build_pages) && !empty($build_pages) - && !in_array($page, $build_pages) && is_file($filename)) + if ((!$config['api']['enabled'] || $global_api == "skip" || $config['smart_build']) && $config['try_smarter'] + && isset($build_pages) && !empty($build_pages) && !in_array($page, $build_pages) ) continue; - if ($config['try_smarter']) { - $antibot = create_antibot($board['uri'], 0 - $page); - $content['current_page'] = $page; - } - $antibot->reset(); - $content['pages'] = $pages; - $content['pages'][$page-1]['selected'] = true; - $content['btn'] = getPageButtons($content['pages']); - $content['antibot'] = $antibot; + if (!$config['smart_build']) { + $content = index($page); + if (!$content) + break; - file_write($filename, Element('index.html', $content)); + // json api + if ($config['api']['enabled']) { + $threads = $content['threads']; + $json = json_encode($api->translatePage($threads)); + file_write($jsonFilename, $json); + + $catalog[$page-1] = $threads; + } + + if ($config['api']['enabled'] && $global_api != "skip" && $config['try_smarter'] && isset($build_pages) + && !empty($build_pages) && !in_array($page, $build_pages) ) + continue; + + if ($config['try_smarter']) { + $antibot = create_antibot($board['uri'], 0 - $page); + $content['current_page'] = $page; + } + $antibot->reset(); + $content['pages'] = $pages; + $content['pages'][$page-1]['selected'] = true; + $content['btn'] = getPageButtons($content['pages']); + $content['antibot'] = $antibot; + + file_write($filename, Element('index.html', $content)); + } + else { + file_unlink($filename); + file_unlink($jsonFilename); + } } - if ($page < $config['max_pages']) { + if (!$config['smart_build'] && $page < $config['max_pages']) { for (;$page<=$config['max_pages'];$page++) { $filename = $board['dir'] . ($page==1 ? $config['file_index'] : sprintf($config['file_page'], $page)); file_unlink($filename); @@ -1553,14 +1861,22 @@ function buildIndex() { } // json api catalog - if ($config['api']['enabled']) { - $json = json_encode($api->translateCatalog($catalog)); - $jsonFilename = $board['dir'] . 'catalog.json'; - file_write($jsonFilename, $json); + if ($config['api']['enabled'] && $global_api != "skip") { + if ($config['smart_build']) { + $jsonFilename = $board['dir'] . 'catalog.json'; + file_unlink($jsonFilename); + $jsonFilename = $board['dir'] . 'threads.json'; + file_unlink($jsonFilename); + } + else { + $json = json_encode($api->translateCatalog($catalog)); + $jsonFilename = $board['dir'] . 'catalog.json'; + file_write($jsonFilename, $json); - $json = json_encode($api->translateCatalog($catalog, true)); - $jsonFilename = $board['dir'] . 'threads.json'; - file_write($jsonFilename, $json); + $json = json_encode($api->translateCatalog($catalog, true)); + $jsonFilename = $board['dir'] . 'threads.json'; + file_write($jsonFilename, $json); + } } if ($config['try_smarter']) @@ -1958,12 +2274,38 @@ function markup(&$body, $track_cites = false, $op = false) { $tracked_cites = array_unique($tracked_cites, SORT_REGULAR); - $body = preg_replace("/^\s*>.*$/m", '$0', $body); + //$body = preg_replace("/^\s*>.*$/m", '$0', $body); if ($config['strip_superfluous_returns']) $body = preg_replace('/\s+$/', '', $body); - - $body = preg_replace("/\n/", '
', $body); + + if ($config['markup_paragraphs']) { + $paragraphs = explode("\n", $body); + $bodyNew = ""; + + foreach ($paragraphs as $paragraph) { + if (strlen(trim($paragraph)) > 0) { + $paragraphDirection = is_rtl($paragraph) ? "rtl" : "ltr"; + } + else { + $paragraphDirection = "empty"; + } + + if (strpos($paragraph, ">")===0) { + $quoteClass = "quote"; + } + else { + $quoteClass = ""; + } + + $bodyNew .= "

" . $paragraph . "

"; + } + + $body = $bodyNew; + } + else { + $body = preg_replace("/\n/", '
', $body); + } if ($config['markup_repair_tidy']) { $tidy = new tidy(); @@ -2020,6 +2362,40 @@ function ordutf8($string, &$offset) { return $code; } +function uniord($u) { + $k = mb_convert_encoding($u, 'UCS-2LE', 'UTF-8'); + $k1 = ord(substr($k, 0, 1)); + $k2 = ord(substr($k, 1, 1)); + return $k2 * 256 + $k1; +} + +function is_rtl($str) { + if(mb_detect_encoding($str) !== 'UTF-8') { + $str = mb_convert_encoding($str, mb_detect_encoding($str),'UTF-8'); + } + + preg_match_all('/[^\n\s]+/', $str, $matches); + preg_match_all('/.|\n\s/u', $str, $matches); + $chars = $matches[0]; + $arabic_count = 0; + $latin_count = 0; + $total_count = 0; + + foreach ($chars as $char) { + $pos = uniord($char); + + if ($pos >= 1536 && $pos <= 1791) { + $arabic_count++; + } + else if ($pos > 123 && $pos < 123) { + $latin_count++; + } + $total_count++; + } + + return (($arabic_count/$total_count) > 0.5); +} + function strip_combining_chars($str) { $chars = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY); $str = ''; @@ -2049,51 +2425,62 @@ function buildThread($id, $return = false, $mod = false) { cache::delete("thread_{$board['uri']}_{$id}"); } - $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri'])); - $query->bindValue(':id', $id, PDO::PARAM_INT); - $query->execute() or error(db_error($query)); - - while ($post = $query->fetch(PDO::FETCH_ASSOC)) { - if (!isset($thread)) { - $thread = new Thread($post, $mod ? '?/' : $config['root'], $mod); - } else { - $thread->add(new Post($post, $mod ? '?/' : $config['root'], $mod)); - } - } - - // Check if any posts were found - if (!isset($thread)) - error($config['error']['nonexistant']); - - $hasnoko50 = $thread->postCount() >= $config['noko50_min']; - $antibot = $mod || $return ? false : create_antibot($board['uri'], $id); - - $body = Element('thread.html', array( - 'board' => $board, - 'thread' => $thread, - 'body' => $thread->build(), - 'config' => $config, - 'id' => $id, - 'mod' => $mod, - 'hasnoko50' => $hasnoko50, - 'isnoko50' => false, - 'antibot' => $antibot, - 'boardlist' => createBoardlist($mod), - 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index']) - )); - if ($config['try_smarter'] && !$mod) $build_pages[] = thread_find_page($id); - // json api - if ($config['api']['enabled']) { - $api = new Api(); - $json = json_encode($api->translateThread($thread)); + if (!$config['smart_build'] || $return || $mod) { + $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri'])); + $query->bindValue(':id', $id, PDO::PARAM_INT); + $query->execute() or error(db_error($query)); + + while ($post = $query->fetch(PDO::FETCH_ASSOC)) { + if (!isset($thread)) { + $thread = new Thread($post, $mod ? '?/' : $config['root'], $mod); + } else { + $thread->add(new Post($post, $mod ? '?/' : $config['root'], $mod)); + } + } + + // Check if any posts were found + if (!isset($thread)) + error($config['error']['nonexistant']); + + $hasnoko50 = $thread->postCount() >= $config['noko50_min']; + $antibot = $mod || $return ? false : create_antibot($board['uri'], $id); + + $body = Element('thread.html', array( + 'board' => $board, + 'thread' => $thread, + 'body' => $thread->build(), + 'config' => $config, + 'id' => $id, + 'mod' => $mod, + 'hasnoko50' => $hasnoko50, + 'isnoko50' => false, + 'antibot' => $antibot, + 'boardlist' => createBoardlist($mod), + 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index']) + )); + + // json api + if ($config['api']['enabled']) { + $api = new Api(); + $json = json_encode($api->translateThread($thread)); + $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json'; + file_write($jsonFilename, $json); + } + } + else { $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json'; - file_write($jsonFilename, $json); + file_unlink($jsonFilename); } - if ($return) { + if ($config['smart_build'] && !$return && !$mod) { + $noko50fn = $board['dir'] . $config['dir']['res'] . sprintf($config['file_page50'], $id); + file_unlink($noko50fn); + + file_unlink($board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $id)); + } else if ($return) { return $body; } else { $noko50fn = $board['dir'] . $config['dir']['res'] . sprintf($config['file_page50'], $id); diff --git a/inc/instance-config.php b/inc/instance-config.php index bc8d63da..c6fb8cea 100644 --- a/inc/instance-config.php +++ b/inc/instance-config.php @@ -7,9 +7,6 @@ * * You can copy values from config.php (defaults) and paste them here. */ - require_once "lib/htmlpurifier-4.6.0/library/HTMLPurifier.auto.php"; - require_once "8chan-functions.php"; - // Note - you may want to change some of these in secrets.php instead of here // See the secrets.example.php file $config['db']['server'] = 'localhost'; @@ -129,6 +126,7 @@ $config['additional_javascript'][] = 'js/thread-watcher.js'; $config['additional_javascript'][] = 'js/ajax.js'; $config['additional_javascript'][] = 'js/quick-reply.js'; + $config['additional_javascript'][] = 'js/quick-post-controls.js'; $config['additional_javascript'][] = 'js/show-own-posts.js'; $config['additional_javascript'][] = 'js/youtube.js'; $config['additional_javascript'][] = 'js/comment-toolbar.js'; @@ -143,6 +141,7 @@ $config['additional_javascript'][] = 'js/auto-scroll.js'; $config['additional_javascript'][] = 'js/twemoji/twemoji.js'; $config['additional_javascript'][] = 'js/file-selector.js'; + $config['additional_javascript'][] = 'js/board-directory.js'; // Oekaki (now depends on config.oekaki so can be in all scripts) $config['additional_javascript'][] = 'js/jquery-ui.custom.min.js'; $config['additional_javascript'][] = 'js/wPaint/8ch.js'; @@ -155,46 +154,63 @@ $config['stylesheets']['Dark'] = 'dark.css'; $config['stylesheets']['Photon'] = 'photon.css'; $config['stylesheets']['Redchanit'] = 'redchanit.css'; - + $config['stylesheets_board'] = true; $config['markup'][] = array("/^[ |\t]*==(.+?)==[ |\t]*$/m", "\$1"); $config['markup'][] = array("/\[spoiler\](.+?)\[\/spoiler\]/", "\$1"); $config['markup'][] = array("/~~(.+?)~~/", "\$1"); $config['markup'][] = array("/__(.+?)__/", "\$1"); $config['markup'][] = array("/###([^\s']+)###/", "###\$1###"); - + + $config['markup_paragraphs'] = true; + $config['markup_rtl'] = true; + $config['boards'] = array(array('' => '/', '' => '/boards.html', '' => '/faq.html', '' => '/random.php', '' => '/create.php', '' => '/bans.html', '' => '/search.php', '' => '/mod.php', '' => 'https://qchat.rizon.net/?channels=#8chan'), array('b', 'news+', 'boards'), array('operate', 'meta'), array(''=>'https://twitter.com/infinitechan')); //$config['boards'] = array(array('' => '/', '' => '/boards.html', '' => '/faq.html', '' => '/random.php', '' => '/create.php', '' => '/search.php', '' => '/mod.php', '' => 'https://qchat.rizon.net/?channels=#8chan'), array('b', 'meta', 'int'), array('v', 'a', 'tg', 'fit', 'pol', 'tech', 'mu', 'co', 'sp', 'boards'), array(''=>'https://twitter.com/infinitechan')); - + $config['footer'][] = 'All posts on 8chan are the responsibility of the individual poster and not the administration of 8chan, pursuant to 47 U.S.C. § 230.'; $config['footer'][] = 'We have not been served any secret court orders and are not under any gag orders.'; $config['footer'][] = 'To make a DMCA request or report illegal content, please email admin@8chan.co.'; - + $config['search']['enable'] = true; - + $config['syslog'] = true; - + $config['hour_max_threads'] = 10; $config['filters'][] = array( 'condition' => array( - 'custom' => function($post) { - global $config, $board; - if (!$config['hour_max_threads']) return false; - - if ($post['op']) { - $query = prepare(sprintf('SELECT COUNT(*) AS `count` FROM ``posts_%s`` WHERE `thread` IS NULL AND FROM_UNIXTIME(`time`) > DATE_SUB(NOW(), INTERVAL 1 HOUR);', $board['uri'])); - $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); - $query->execute() or error(db_error($query)); - $r = $query->fetch(PDO::FETCH_ASSOC); - - return ($r['count'] > $config['hour_max_threads']); - } - } + 'custom' => 'max_posts_per_hour' ), 'action' => 'reject', 'message' => 'On this board, to prevent raids the number of threads that can be created per hour is limited. Please try again later, or post in an existing thread.' ); + $config['languages'] = array( + 'ch' => "汉语", + 'cz' => "Čeština", + 'dk' => "Dansk", + 'de' => "Deutsch", + 'eo' => "Esperanto", + 'en' => "English", + 'es' => "Español", + 'fi' => "Suomi", + 'fr' => "Français", + 'hu' => "Magyar", + 'it' => "Italiano", + 'jp' => "日本語", + 'jbo' => "Lojban", + 'lt' => "Lietuvių Kalba", + 'lv' => "Latviešu Valoda", + 'no' => "Norsk", + 'nl' => "Nederlands Vlaams", + 'pl' => "Polski", + 'pt' => "Português", + 'ru' => "Русский", + 'sk' => "Slovenský Jazyk", + 'tw' => "Taiwanese", + ); + + $config['gzip_static'] = false; $config['hash_masked_ip'] = true; $config['force_subject_op'] = false; @@ -212,9 +228,18 @@ $config['enable_antibot'] = false; $config['spam']['unicode'] = false; $config['twig_cache'] = false; $config['report_captcha'] = true; + +$config['page_404'] = 'page_404'; + +// Flavor and design. +$config['site_name'] = "∞chan"; +$config['site_logo'] = "/static/logo_33.svg"; + // 8chan specific mod pages -require '8chan-mod-pages.php'; +require '8chan-mod-config.php'; + +// Load instance functions later on +require_once 'instance-functions.php'; // Load database credentials require "secrets.php"; - diff --git a/inc/instance-functions.php b/inc/instance-functions.php new file mode 100644 index 00000000..71e933d1 --- /dev/null +++ b/inc/instance-functions.php @@ -0,0 +1,24 @@ + DATE_SUB(NOW(), INTERVAL 1 HOUR);', $board['uri'])); + $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); + $query->execute() or error(db_error($query)); + $r = $query->fetch(PDO::FETCH_ASSOC); + + return ($r['count'] > $config['hour_max_threads']); + } +} + +function page_404() { + include('404.php'); +} diff --git a/inc/lib/Twig/Extensions/Extension/Tinyboard.php b/inc/lib/Twig/Extensions/Extension/Tinyboard.php index 1c5b8981..f41e0768 100644 --- a/inc/lib/Twig/Extensions/Extension/Tinyboard.php +++ b/inc/lib/Twig/Extensions/Extension/Tinyboard.php @@ -76,7 +76,7 @@ function twig_remove_whitespace_filter($data) { } function twig_date_filter($date, $format) { - return gmstrftime($format, $date); + return gmstrftime($format, (int) $date); } function twig_hasPermission_filter($mod, $permission, $board = null) { @@ -86,7 +86,7 @@ function twig_hasPermission_filter($mod, $permission, $board = null) { function twig_extension_filter($value, $case_insensitive = true) { $ext = mb_substr($value, mb_strrpos($value, '.') + 1); if($case_insensitive) - $ext = mb_strtolower($ext); + $ext = mb_strtolower($ext); return $ext; } diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 7efabae6..97a33487 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -3333,10 +3333,14 @@ function mod_theme_configure($theme_name) { $query->bindValue(':value', $_POST[$conf['name']]); $query->execute() or error(db_error($query)); } - + $query = prepare("INSERT INTO ``theme_settings`` VALUES(:theme, NULL, NULL)"); $query->bindValue(':theme', $theme_name); $query->execute() or error(db_error($query)); + + // Clean cache + Cache::delete("themes"); + Cache::delete("theme_settings_".$theme); $result = true; $message = false; @@ -3384,11 +3388,15 @@ function mod_theme_uninstall($theme_name) { if (!hasPermission($config['mod']['themes'])) error($config['error']['noaccess']); - + $query = prepare("DELETE FROM ``theme_settings`` WHERE `theme` = :theme"); $query->bindValue(':theme', $theme_name); $query->execute() or error(db_error($query)); + // Clean cache + Cache::delete("themes"); + Cache::delete("theme_settings_".$theme); + header('Location: ?/themes', true, $config['redirect_http']); } diff --git a/index.php b/index.php index 12fec902..a44b56e5 100644 --- a/index.php +++ b/index.php @@ -9,3 +9,5 @@ if ($query) { $index = Element("8chan/index.html", array("config" => $config, "newsplus" => $newsplus)); file_write('index.html', $index); + +echo $index; \ No newline at end of file diff --git a/js/board-directory.js b/js/board-directory.js new file mode 100644 index 00000000..b278fbde --- /dev/null +++ b/js/board-directory.js @@ -0,0 +1,349 @@ +// ============================================================ +// Purpose : Board directory handling +// Contributors : 8n-tech +// ============================================================ + +;( function( window, $, undefined ) { + var boardlist = { + options : { + $boardlist : false, + + // Selectors for finding and binding elements. + selector : { + 'boardlist' : "#boardlist", + + 'board-head' : ".board-list-head", + 'board-body' : ".board-list-tbody", + 'board-loading' : ".board-list-loading", + 'board-omitted' : ".board-list-omitted", + + 'search' : "#search-form", + 'search-lang' : "#search-lang-input", + 'search-sfw' : "#search-sfw-input", + 'search-tag' : "#search-tag-input", + 'search-title' : "#search-title-input", + 'search-submit' : "#search-submit", + + 'tag-list' : ".tag-list", + 'tag-link' : ".tag-link", + + 'footer-page' : ".board-page-num", + 'footer-count' : ".board-page-count", + 'footer-total' : ".board-page-total", + 'footer-more' : "#board-list-more" + }, + + // HTML Templates for dynamic construction + template : { + // Board row item + 'board-row' : "", + + // Individual cell definitions + 'board-cell-meta' : "", + 'board-cell-uri' : "", + 'board-cell-title' : "", + 'board-cell-pph' : "", + 'board-cell-posts_total' : "", + 'board-cell-active' : "", + 'board-cell-tags' : "", + + // Content wrapper + // Used to help constrain contents to their . + 'board-content-wrap' : "
", + + // Individual items or parts of a single table cell. + 'board-datum-lang' : "", + 'board-datum-uri' : "", + 'board-datum-sfw' : "", + 'board-datum-nsfw' : "", + 'board-datum-tags' : "", + + + // Tag list. + 'tag-list' : "", + 'tag-item' : "
  • ", + 'tag-link' : "" + } + }, + + lastSearch : {}, + + bind : { + form : function() { + var selectors = boardlist.options.selector; + + var $search = $( selectors['search'] ), + $searchLang = $( selectors['search-lang'] ), + $searchSfw = $( selectors['search-sfw'] ), + $searchTag = $( selectors['search-tag'] ), + $searchTitle = $( selectors['search-title'] ), + $searchSubmit = $( selectors['search-submit'] ); + + var searchForms = { + 'boardlist' : boardlist.$boardlist, + 'search' : $search, + 'searchLang' : $searchLang, + 'searchSfw' : $searchSfw, + 'searchTag' : $searchTag, + 'searchTitle' : $searchTitle, + 'searchSubmit' : $searchSubmit + }; + + if ($search.length > 0) { + // Bind form events. + boardlist.$boardlist + // Load more + .on( 'click', selectors['board-omitted'], searchForms, boardlist.events.loadMore ) + // Tag click + .on( 'click', selectors['tag-link'], searchForms, boardlist.events.tagClick ) + // Form Submission + .on( 'submit', selectors['search'], searchForms, boardlist.events.searchSubmit ) + // Submit click + .on( 'click', selectors['search-submit'], searchForms, boardlist.events.searchSubmit ); + + $searchSubmit.prop( 'disabled', false ); + } + } + }, + + build : { + boardlist : function(data) { + boardlist.build.boards(data['boards'], data['order']); + boardlist.build.lastSearch(data['search']); + boardlist.build.footer(data); + boardlist.build.tags(data['tagWeight']); + + }, + + boards : function(boards, order) { + // Find our head, columns, and body. + var $head = $( boardlist.options.selector['board-head'], boardlist.$boardlist ), + $cols = $("[data-column]", $head ), + $body = $( boardlist.options.selector['board-body'], boardlist.$boardlist ); + + $.each( order, function( index, uri ) { + var row = boards[uri]; + $row = $( boardlist.options.template['board-row'] ); + + $cols.each( function( index, col ) { + boardlist.build.board( row, col ).appendTo( $row ); + } ); + + $row.appendTo( $body ); + } ); + + }, + board : function(row, col) { + var $col = $(col), + column = $col.attr('data-column'), + value = row[column] + $cell = $( boardlist.options.template['board-cell-' + column] ), + $wrap = $( boardlist.options.template['board-content-wrap'] ); + + if (typeof boardlist.build.boardcell[column] === "undefined") { + if (value instanceof Array) { + if (typeof boardlist.options.template['board-datum-' + column] !== "undefined") { + $.each( value, function( index, singleValue ) { + $( boardlist.options.template['board-datum-' + column] ) + .text( singleValue ) + .appendTo( $wrap ); + } ); + } + else { + $wrap.text( value.join(" ") ); + } + } + else { + $wrap.text( value ); + } + } + else { + var $content = boardlist.build.boardcell[column]( row, value ); + + if ($content instanceof jQuery) { + // We use .append() instead of .appendTo() as we do elsewhere + // because $content can be multiple elements. + $wrap.append( $content ); + } + else if (typeof $content === "string") { + $wrap.html( $content ); + } + else { + console.log("Special cell constructor returned a " + (typeof $content) + " that board-directory.js cannot interpret."); + } + } + + $wrap.appendTo( $cell ); + return $cell; + }, + boardcell : { + 'meta' : function(row, value) { + return $( boardlist.options.template['board-datum-lang'] ).text( row['locale'] ); + }, + 'uri' : function(row, value) { + var $link = $( boardlist.options.template['board-datum-uri'] ), + $sfw = $( boardlist.options.template['board-datum-' + (row['sfw'] == 1 ? "sfw" : "nsfw")] ); + + $link + .attr( 'href', "/"+row['uri']+"/" ) + .text( "/"+row['uri']+"/" ); + + // I decided against NSFW icons because it clutters the index. + // Blue briefcase = SFW. No briefcase = NSFW. Seems better. + if (row['sfw'] == 1) { + return $link[0].outerHTML + $sfw[0].outerHTML; + } + else { + return $link[0].outerHTML; + } + } + }, + + lastSearch : function(search) { + return boardlist.lastSearch = { + 'lang' : search.lang === false ? "" : search.lang, + 'page' : search.page, + 'tags' : search.tags === false ? "" : search.tags.join(" "), + 'time' : search.time, + 'title' : search.title === false ? "" : search.title, + 'sfw' : search.nsfw ? 0 : 1 + }; + }, + + footer : function(data) { + var selector = boardlist.options.selector, + $page = $( selector['footer-page'], boardlist.$boardlist ), + $count = $( selector['footer-count'], boardlist.$boardlist ), + $total = $( selector['footer-total'], boardlist.$boardlist ), + $more = $( selector['footer-more'], boardlist.$boardlist ), + $omitted = $( selector['board-omitted'], boardlist.$boardlist ); + + var boards = Object.keys(data['boards']).length, + omitted = data['omitted'] - data['search']['page']; + + if (omitted < 0) { + omitted = 0; + } + + var total = boards + omitted + data['search']['page']; + + //$page.text( data['search']['page'] ); + $count.text( data['search']['page'] + boards ); + $total.text( total ); + $more.toggleClass( "board-list-hasmore", omitted != 0 ); + $omitted.toggle( boards + omitted > 0 ); + }, + + tags : function(tags) { + var selector = boardlist.options.selector, + template = boardlist.options.template, + $list = $( selector['tag-list'], boardlist.$boardlist ); + + if ($list.length) { + + $.each( tags, function(tag, weight) { + var $item = $( template['tag-item'] ), + $link = $( template['tag-link'] ); + + $link + .css( 'font-size', weight+"%" ) + .text( tag ) + .appendTo( $item ); + + $item.appendTo( $list ); + } ); + } + } + }, + + events : { + loadMore : function(event) { + var parameters = $.extend( {}, boardlist.lastSearch ); + + parameters.page = $( boardlist.options.selector['board-body'], boardlist.$boardlist ).children().length; + + boardlist.submit( parameters ); + }, + + searchSubmit : function(event) { + event.preventDefault(); + + $( boardlist.options.selector['tag-list'], boardlist.$boardlist ).html(""); + $( boardlist.options.selector['board-body'], boardlist.$boardlist ).html(""); + + boardlist.submit( { + 'lang' : event.data.searchLang.val(), + 'tags' : event.data.searchTag.val(), + 'title' : event.data.searchTitle.val(), + 'sfw' : event.data.searchSfw.prop('checked') ? 1 : 0 + } ); + + return false; + }, + + tagClick : function(event) { + event.preventDefault(); + + var $this = $(this), + $input = $( boardlist.options.selector['search-tag'] ); + + $input + .val( ( $input.val() + " " + $this.text() ).replace(/\s+/g, " ").trim() ) + .trigger( 'change' ) + .focus(); + + return false; + } + }, + + submit : function( parameters ) { + var $boardlist = boardlist.$boardlist, + $boardload = $( boardlist.options.selector['board-loading'], $boardlist ), + $searchSubmit = $( boardlist.options.selector['search-submit'], $boardlist ), + $footerMore = $( boardlist.options.selector['board-omitted'], $boardlist ); + + $searchSubmit.prop( 'disabled', true ); + $boardload.show(); + $footerMore.hide(); + + return $.get( + "/board-search.php", + parameters, + function(data) { + $searchSubmit.prop( 'disabled', false ); + $boardload.hide(); + + boardlist.build.boardlist( $.parseJSON(data) ); + } + ); + }, + + init : function( target ) { + if (typeof target !== "string") { + target = boardlist.options.selector.boardlist; + } + + var $boardlist = $(target); + + if ($boardlist.length > 0 ) { + $( boardlist.options.selector['board-loading'], $boardlist ).hide(); + + boardlist.$boardlist = $boardlist; + boardlist.bind.form(); + } + } + }; + + // Tie to the vichan object. + if (typeof window.vichan === "undefined") { + window.vichan = {}; + } + window.vichan.boardlist = boardlist; + + // Initialize the boardlist when the document is ready. + $( document ).on( 'ready', window.vichan.boardlist.init ); + // Run it now if we're already ready. + if (document.readyState === 'complete') { + window.vichan.boardlist.init(); + } +} )( window, jQuery ); \ No newline at end of file diff --git a/js/quick-post-controls.js b/js/quick-post-controls.js index 4e8df1cb..67c4bc51 100644 --- a/js/quick-post-controls.js +++ b/js/quick-post-controls.js @@ -90,5 +90,12 @@ $(document).ready(function(){ $(document).on('new_post', function(e, post) { $(post).find('input[type=checkbox].delete').each(init_qpc); }); -}); - + + // Bottom of the page quick reply function + $("#thread-quick-reply").show(); + $("#link-quick-reply").on( 'click', function(event) { + event.preventDefault(); + $(window).trigger('cite', ['']); + return false; + } ); +} ); \ No newline at end of file diff --git a/post.php b/post.php index 65e7db13..ce6a5f47 100644 --- a/post.php +++ b/post.php @@ -209,11 +209,16 @@ if (isset($_POST['delete'])) { } } elseif (isset($_POST['post'])) { - if (!isset($_POST['body'], $_POST['board'])) + if (!isset($_POST['body'], $_POST['board'])) { error($config['error']['bot']); - - $post = array('board' => $_POST['board'], 'files' => array()); - + } + + $post = array( + 'board' => $_POST['board'], + 'files' => array(), + 'time' => time(), // Timezone independent UNIX timecode. + ); + // Check if board exists if (!openBoard($post['board'])) error($config['error']['noboard']); @@ -228,17 +233,13 @@ elseif (isset($_POST['post'])) { $_POST['subject'] = ''; if (!isset($_POST['password'])) - $_POST['password'] = ''; + $_POST['password'] = ''; if (isset($_POST['thread'])) { $post['op'] = false; $post['thread'] = round($_POST['thread']); } else $post['op'] = true; - - // Check if board exists - if (!openBoard($post['board'])) - error($config['error']['noboard']); // Check if banned checkBan($board['uri']); @@ -642,7 +643,8 @@ elseif (isset($_POST['post'])) { if (mysql_version() >= 50503) { $post['body_nomarkup'] = $post['body']; // Assume we're using the utf8mb4 charset - } else { + } + else { // MySQL's `utf8` charset only supports up to 3-byte symbols // Remove anything >= 0x010000 @@ -712,7 +714,7 @@ elseif (isset($_POST['post'])) { do_filters($post); } - if ($post['has_file']) { + if ($post['has_file']) { foreach ($post['files'] as $key => &$file) { if ($file['is_an_image'] && $config['ie_mime_type_detection'] !== false) { // Check IE MIME type detection XSS exploit @@ -910,10 +912,15 @@ elseif (isset($_POST['post'])) { $post['files'] = $post['files']; $post['num_files'] = sizeof($post['files']); + // Commit the post to the database. $post['id'] = $id = post($post); insertFloodPost($post); - + + // Update statistics for this board. + updateStatisticsForPost( $post ); + + // Handle cyclical threads if (!$post['op'] && isset($thread['cycle']) && $thread['cycle']) { // Query is a bit weird due to "This version of MariaDB doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" (MariaDB Ver 15.1 Distrib 10.0.17-MariaDB, for Linux (x86_64)) @@ -1009,17 +1016,20 @@ elseif (isset($_POST['post'])) { event('post-after', $post); buildIndex(); - - // We are already done, let's continue our heavy-lifting work in the background (if we run off FastCGI) - if (function_exists('fastcgi_finish_request')) - @fastcgi_finish_request(); - - if ($post['op']) - rebuildThemes('post-thread', $board['uri']); - else - rebuildThemes('post', $board['uri']); -} elseif (isset($_POST['appeal'])) { + // We are already done, let's continue our heavy-lifting work in the background (if we run off FastCGI) + if (function_exists('fastcgi_finish_request')) { + @fastcgi_finish_request(); + } + + if ($post['op']) { + rebuildThemes('post-thread', $board['uri']); + } + else { + rebuildThemes('post', $board['uri']); + } +} +elseif (isset($_POST['appeal'])) { if (!isset($_POST['ban_id'])) error($config['error']['bot']); diff --git a/smart_build.php b/smart_build.php new file mode 100644 index 00000000..ac016480 --- /dev/null +++ b/smart_build.php @@ -0,0 +1,200 @@ + $config['max_pages']) return false; + $config['try_smarter'] = true; + $build_pages = array($page); + buildIndex("skip"); + return true; +} + +function sb_api_board($b, $page = 0) { $page = (int)$page; + return sb_board($b, $page + 1); +} + +function sb_thread($b, $thread, $slugcheck = false) { global $config; $thread = (int)$thread; + if ($thread < 1) return false; + + if (!preg_match('/^'.$config['board_regex'].'$/u', $b)) return false; + + if (Cache::get("thread_exists_".$b."_".$thread) == "no") return false; + + $query = prepare(sprintf("SELECT MAX(`id`) AS `max` FROM ``posts_%s``", $b)); + if (!$query->execute()) return false; + + $s = $query->fetch(PDO::FETCH_ASSOC); + $max = $s['max']; + + if ($thread > $max) return false; + + $query = prepare(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL", $b)); + $query->bindValue(':id', $thread); + + if (!$query->execute() || !$query->fetch(PDO::FETCH_ASSOC) ) { + Cache::set("thread_exists_".$b."_".$thread, "no"); + return false; + } + + if ($slugcheck == 50) { // Should we really generate +50 page? Maybe there are not enough posts anyway + global $request; + $r = str_replace("+50", "", $request); + $r = substr($r, 1); // Cut the slash + + if (file_exists($r)) return false; + } + + if (!openBoard($b)) return false; + buildThread($thread); + return true; +} + +function sb_thread_slugcheck50($b, $thread) { + return sb_thread($b, $thread, 50); +} + +function sb_api($b) { global $config; + if (!openBoard($b)) return false; + $config['try_smarter'] = true; + $build_pages = array(-1); + buildIndex(); + return true; +} + +function sb_ukko() { + rebuildTheme("ukko", "post-thread"); + return true; +} + +function sb_catalog($b) { + if (!openBoard($b)) return false; + + rebuildTheme("catalog", "post-thread", $b); + return true; +} + +function sb_recent() { + rebuildTheme("recent", "post-thread"); + return true; +} + +function sb_sitemap() { + rebuildTheme("sitemap", "all"); + return true; +} + +$entrypoints = array(); + +$entrypoints['/%b/'] = 'sb_board'; +$entrypoints['/%b/'.$config['file_index']] = 'sb_board'; +$entrypoints['/%b/'.$config['file_page']] = 'sb_board'; +$entrypoints['/%b/%d.json'] = 'sb_api_board'; +if ($config['api']['enabled']) { + $entrypoints['/%b/threads.json'] = 'sb_api'; + $entrypoints['/%b/catalog.json'] = 'sb_api'; +} + +$entrypoints['/%b/'.$config['dir']['res'].$config['file_page']] = 'sb_thread'; +$entrypoints['/%b/'.$config['dir']['res'].$config['file_page50']] = 'sb_thread_slugcheck50'; +if ($config['api']['enabled']) { + $entrypoints['/%b/'.$config['dir']['res'].'%d.json'] = 'sb_thread'; +} + +$entrypoints['/*/'] = 'sb_ukko'; +$entrypoints['/*/index.html'] = 'sb_ukko'; +$entrypoints['/recent.html'] = 'sb_recent'; +$entrypoints['/%b/catalog.html'] = 'sb_catalog'; +$entrypoints['/sitemap.xml'] = 'sb_sitemap'; + +$reached = false; + +$request = $_SERVER['REQUEST_URI']; +list($request) = explode('?', $request); + +foreach ($entrypoints as $id => $fun) { + $id = '@^' . preg_quote($id, '@') . '$@u'; + + $id = str_replace('%b', '('.$config['board_regex'].')', $id); + $id = str_replace('%d', '([0-9]+)', $id); + $id = str_replace('%s', '[a-zA-Z0-9-]+', $id); + + $matches = null; + + if (preg_match ($id, $request, $matches)) { + array_shift($matches); + + $reached = call_user_func_array($fun, $matches); + + break; + } +} + +function die_404() { global $config; + if (!$config['page_404']) { + header("HTTP/1.1 404 Not Found"); + header("Status: 404 Not Found"); + echo "

    404 Not Found

    Page doesn't exist


    vichan
    "; + } + elseif (is_callable($config['page_404'])) { + $config['page_404'](); + } + else { + header("Location: ".$config['page_404']); + } + header("X-Accel-Expires: 120"); + die(); +} + +if ($reached) { + if ($request[strlen($request)-1] == '/') { + $request .= 'index.html'; + } + $request = '.'.$request; + + if (!file_exists($request)) { + die_404(); + } + + header("HTTP/1.1 200 OK"); + header("Status: 200 OK"); + if (preg_match('/\.json$/', $request)) { + header("Content-Type", "application/json"); + } + elseif (preg_match('/\.js$/', $request)) { + header("Content-Type", "text/javascript; charset=utf-8"); + } + elseif (preg_match('/\.xml$/', $request)) { + header("Content-Type", "application/xml"); + } + else { + header("Content-Type", "text/html; charset=utf-8"); + } + header("Cache-Control: public, nocache, no-cache, max-age=0, must-revalidate"); + header("Expires: Fri, 22 Feb 1991 06:00:00 GMT"); + header("Last-Modified: ".date('r', filemtime($request))); + + //if (isset ($_SERVER['HTTP_ACCEPT_ENCODING']) && preg_match('/gzip/', $_SERVER['HTTP_ACCEPT_ENCODING']) && file_exists($request.".gz")) { + // header("Content-Encoding: gzip"); + // $file = fopen($request.".gz", 'r'); + //} + //else { + $file = fopen($request, 'r'); + //} + fpassthru($file); + fclose($file); +} +else { + die_404(); +} diff --git a/static/infinity-small.gif b/static/infinity-small.gif new file mode 100644 index 00000000..c7a2978e Binary files /dev/null and b/static/infinity-small.gif differ diff --git a/stylesheets/dark.css b/stylesheets/dark.css index 0287959e..0cf4c4f4 100644 --- a/stylesheets/dark.css +++ b/stylesheets/dark.css @@ -198,3 +198,34 @@ div.report { .modlog tr:nth-child(even), .modlog th { background-color: #282A2E; } + + + +.box { + background: #333333; + border-color: #555555; + color: #C5C8C6; + border-radius: 10px; +} +.box-title { + background: transparent; + color: #32DD72; +} + +table thead th { + background: #333333; + border-color: #555555; + color: #C5C8C6; + border-radius: 4px; +} +table tbody tr:nth-of-type( even ) { + background-color: #333333; +} + +table.board-list-table .board-uri .board-sfw { + color: #446655; +} +tbody.board-list-omitted td { + background: #333333; + border-color: #555555; +} \ No newline at end of file diff --git a/stylesheets/photon.css b/stylesheets/photon.css index a487e4dd..4d876ea0 100644 --- a/stylesheets/photon.css +++ b/stylesheets/photon.css @@ -329,3 +329,31 @@ form table tr td div { .desktop-style div.boardlist:not(.bottom) { background-color: #DDDDDD; } + + + +.box { + background: #DDDDDD; + border-color: #CCCCCC; + color: #333333; + border-radius: 7px; +} +.box-title { + border-radius: 7px; +} + +table thead th { + background: transparent; + border: none; +} +table tbody tr:nth-of-type( even ) { + background-color: #DDDDDD; +} + +table.board-list-table .board-uri .board-sfw { + color: #333333; +} +tbody.board-list-omitted td { + background: transparent; + border: none; +} \ No newline at end of file diff --git a/stylesheets/redchanit.css b/stylesheets/redchanit.css index 9577730f..78456f31 100644 --- a/stylesheets/redchanit.css +++ b/stylesheets/redchanit.css @@ -57,4 +57,33 @@ p.intro a.email span.name { a { color: #8020FF; +} + + + +.box { + background: #343C4E; + border: none; + color: #FFF; +} +.box-title { + background: #7F8CA8; + color: #0F0C5D; +} + +table thead th { + background: #343C4E; + border: none; + color: #FFF; +} +table tbody tr:nth-of-type( even ) { + background-color: #343C4E; +} + +table.board-list-table .board-uri .board-sfw { + color: #D00; +} +tbody.board-list-omitted td { + background: #343C4E; + border: none; } \ No newline at end of file diff --git a/stylesheets/style.css b/stylesheets/style.css index e067cc10..4e0846e4 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -1,3 +1,6 @@ +/* === GENERAL TAG SETTINGS === */ + +/* Page Layouts */ body { background: #EEF2FF url('img/fade-blue.png') repeat-x 50% 0%; color: black; @@ -8,6 +11,69 @@ body { padding-right: 4px; } +main, +aside, +section { + display: block; + margin: 0 auto; + width: 100%; +} + +main { + max-width: 1110px; +} + +/* Tables */ +table { + margin: auto; + width: 100%; +} + +table { + margin: auto; + width: 100%; +} +table tbody td { + margin: 0; + padding: 4px 15px 4px 4px; + + text-align: left; +} +table thead th { + border: 1px solid #000333; + padding: 4px 15px 5px 5px; + + background: #98E; + color: #000333; + text-align: left; + white-space: nowrap; +} +table tbody tr:nth-of-type( even ) { + background-color: #D6DAF0; +} + +td.minimal,th.minimal { + width: 1%; + white-space: nowrap; +} + +table.mod.config-editor { + font-size: 9pt; + width: 100%; +} + +table.mod.config-editor td { + text-align: left; + padding: 5px; + border-bottom: 1px solid #98e; +} + +table.mod.config-editor input[type="text"] { + width: 98%; +} + + +/* Uncategorized */ #post-form-outer { text-align: center; } @@ -20,6 +86,10 @@ body { width: 100%; } +#post-form-inner .post-table tr { + background-color: transparent; +} + .post-table th, .post-table-options th { width: 85px; } @@ -297,15 +367,17 @@ p.intro a { color: maroon; } -div.delete { - float: right; +p.body-line, +div.post p { + display: block; + margin: 0; + + line-height: 1.16em; + font-size: 13px; + min-height: 1.16em; } -div.post.reply p { - margin: 0.3em 0 0 0; -} - -div.post.reply div.body { +div.post div.body { margin-left: 1.8em; margin-top: 0.8em; padding-right: 3em; @@ -332,7 +404,7 @@ div.post div.body { div.post.reply { background: #D6DAF0; margin: 0.2em 4px; - padding: 0.2em 0.3em 0.5em 0.6em; + padding: 0.5em 0.3em 0.5em 0.6em; border-width: 1px; border-style: none solid solid none; border-color: #B7C5D9; @@ -347,12 +419,14 @@ div.post.reply.has-file.body-not-empty { div.post_modified { margin-left: 1.8em; } - div.post_modified div.content-status { margin-top: 0.5em; padding-bottom: 0em; font-size: 72%; } +div.post_modified div.content-status:first-child { + margin-top: 1.3em; +} div.post_modified div.content-status:first-child { margin-top: 1.3em; @@ -367,7 +441,7 @@ span.trip { color: #228854; } -span.quote { +.quote { color: #789922; } @@ -536,50 +610,10 @@ hr { clear: left; } -div.boardlist { - color: #89A; - font-size: 9pt; - margin-top: 3px; -} - -div.boardlist.bottom { - margin-top: 20px; -} - -div.boardlist a { - text-decoration: none; -} - div.report { color: #333; } -table.modlog { - margin: auto; - width: 100%; -} - -table.modlog tr td { - text-align: left; - margin: 0; - padding: 4px 15px 0 0; -} - -table.modlog tr th { - text-align: left; - padding: 4px 15px 5px 5px; - white-space: nowrap; -} - -table.modlog tr th { - background: #98E; -} - -td.minimal,th.minimal { - width: 1%; - white-space: nowrap; -} - div.top_notice { text-align: center; margin: 5px auto; @@ -603,21 +637,6 @@ div.blotter { text-align: center; } -table.mod.config-editor { - font-size: 9pt; - width: 100%; -} - -table.mod.config-editor td { - text-align: left; - padding: 5px; - border-bottom: 1px solid #98e; -} - -table.mod.config-editor input[type="text"] { - width: 98%; -} - .desktop-style div.boardlist:not(.bottom) { position: fixed; top: 0; @@ -1010,7 +1029,6 @@ span.pln { color:grey; } - @media screen and (min-width: 768px) { p.intro { clear: none; @@ -1021,8 +1039,169 @@ span.pln { } } -/* threadwatcher */ +/* === SITE-WIDE ASSETS === */ +#logo { + display: block; + width: 100%; + padding: 0; + margin: 0 0 0 0; + text-align: center; +} +#logo-link { + display: inline; +} +#logo-img { + display: inline-block; + height: 128px; + width: auto; +} +/* === GENERAL CLASSES === */ +.loading { + background: none; + background-color: none; + background-image: url('/static/infinity.gif'); + background-position: center center; + background-repeat: no-repeat; + min-height: 76px; + min-width: 128px; +} +.loading-small { + background: none; + background-color: none; + background-image: url('/static/infinity-small.gif'); + background-position: center center; + background-repeat: no-repeat; + min-height: 24px; + min-width: 48px; +} + +/* Text and accessibility */ +.ltr { + direction: ltr; +} +.rtl { + direction: rtl; + font-family: Tahoma; +} + +/* Responsive helpers */ +.col { + box-sizing: border-box; + float: left; +} + +.col-12 { width: 100%; } +.col-11 { width: 91.66666667%; } +.col-10 { width: 83.33333333%; } +.col-9 { width: 75%; } +.col-8 { width: 66.66666667%; } +.col-7 { width: 58.33333333%; } +.col-6 { width: 50%; } +.col-5 { width: 41.66666667%; } +.col-4 { width: 33.33333333%; } +.col-3 { width: 25%; } +.col-2 { width: 16.66666667%; } +.col-1 { width: 8.33333333%; } + +.left-push { + float: left; +} +.right-push { + float: right; +} + +/* Layout design */ +.box { + background: #D6DAF0; + border: 1px solid #000333; + color: #000333; + margin: 0 0 12px 0; +} +.box-title { + background: #98E; + color: #000333; + font-size: 120%; + font-weight: bold; + padding: 4px 8px; +} +.box-content { + padding: 0 8px; + margin: 4px 0; +} + + +.clearfix { + display: block; + clear: both; + visibility: hidden; + overflow: hidden; + + font-size: 0px; + line-height: 0px; + + box-sizing: border-box; + border: none; + height: 0; + margin: 0; + padding: 0; + width: 100%; + zoom: 1; +} + +/* === SPECIFIC PAGES & FEATURES === */ + +/* Board List */ +div.boardlist { + margin-top: 3px; + + color: #89A; + font-size: 9pt; +} +div.boardlist.bottom { + margin-top: 12px; + clear: both; +} +div.boardlist a { + text-decoration: none; +} + +/* Threads */ +/* Thread Footer */ +#thread-interactions { + margin: 8px 0; + clear: both; +} +#thread-links { + float: left; +} +#thread-links > a { + padding-left: none; + padding-right: 10px; +} +#thread-quick-reply { + display: none; + position: absolute; + left: 50%; + right: 50%; + text-align: center; + width: 100px; + margin-left: -50px; +} +#thread_stats { + float: right; +} + +#post-moderation-fields { + float: right; + text-align: right; +} +#delete-fields { +} +#report-fields { +} + +/* threadwatcher */ #watchlist { display: none; max-height: 250px; @@ -1065,25 +1244,25 @@ div.mix { } /* Mona Font */ - .aa { font-family: Mona, "MS PGothic", "MS Pゴシック", sans-serif; display: block!important; font-size: 12pt; } - -.dx,.dy,.dz { +.dx, +.dy, +.dz { width: 30px; text-align: right; display: inline-block; } +/* Dice */ .dice-option table { border: 1px dotted black; margin: 0; border-collapse: collapse; } - .dice-option table td { text-align: center; border-left: 1px dotted black; @@ -1093,7 +1272,6 @@ div.mix { } /* Quick reply (why was most of this ever in the script?) */ - #quick-reply { position: fixed; right: 5%; @@ -1259,3 +1437,181 @@ div.mix { .dropzone .remove-btn:hover { color: rgba(125, 125, 125, 1); } + +table.board-list-table { + display: table; + margin: -2px; + margin-bottom: 10px; + overflow: hidden; + table-layout: fixed; +} + +table.board-list-table .board-meta { + padding-right: 4px; + width: 70px; +} +table.board-list-table .board-uri { + max-width: 196px; +} +table.board-list-table .board-title { + width: auto; +} +table.board-list-table .board-pph { + width: 55px; + padding: 4px; +} +table.board-list-table .board-max { + width: 90px; + padding: 4px; +} +table.board-list-table .board-unique { + width: 100px; + padding: 4px; +} +table.board-list-table .board-tags { + width: auto; + padding: 0 15px 0 4px; +} + +table.board-list-table .board-uri .board-nsfw { + color: rgb(230,0,0); + margin: 0 0 0 0.6em; + float: right; +} +table.board-list-table .board-uri .board-sfw { + /* I'm using blue instead of green to help users with Deuteranopia (most common form of colorblndness). */ + color: rgb(0,0,230); + margin: 0 0 0 0.6em; + float: right; +} + +table.board-list-table div.board-cell { + max-width: 100%; + overflow: hidden; +} + +tbody.board-list-loading { + display: none; +} +tbody.board-list-loading .loading { + height: 80px; +} + +tbody.board-list-omitted td { + background: #98E; + border-top: 1px solid #000333; + padding: 8px; + font-size: 125%; + text-align: center; +} +tbody.board-list-omitted #board-list-more { + cursor: default; +} +tbody.board-list-omitted #board-list-more.board-list-hasmore { + cursor: pointer; +} +tbody.board-list-omitted .board-page-loadmore { + display: none; +} +tbody.board-list-omitted .board-list-hasmore .board-page-loadmore { + display: inline; +} + +aside.search-container { + margin-bottom: 12px; +} +aside.search-container .box { + margin-right: 12px; +} + +.board-search { + margin: 8px 0; +} +.search-item { + margin: 8px 0; +} +.search-sfw { + display: block; + cursor: pointer; + font-size: 110%; + line-height: 120%; +} +#search-sfw-input { + margin: 0; + padding: 0; + transform: scale(1.20); +} +#search-lang-input, +#search-title-input, +#search-tag-input { + box-sizing: border-box; + font-size: 110%; + line-height: 120%; + vertical-align: top; + padding: 2px 0 2px 4px; + max-width: 100%; + min-width: 100%; + width: 100%: +} +#search-loading { + display: inline-block; + vertical-align: bottom; +} + +ul.tag-list { + display: block; + list-style: none; + margin: 8px 8px -9px 8px; + padding: 8px 0 0 0; + border-top: 1px solid #000333; +} +ul.tag-list::after { + content: ' '; + display: block; + clear: both; +} +li.tag-item { + display: inline-block; + float: left; + font-size: 100%; + list-style: none; + margin: 0; + padding: 0 4px 0 0; +} +li.tag-item:last-child { + padding-bottom: 17px; +} +a.tag-link { + overflow: hidden; + white-space: nowrap; +} +li.tag-item a.tag-link { +} +td.board-tags a.tag-link { + display: inline-block; + margin: 0 0.4em 0 0; +} + +@media screen and (max-width: 1100px) { + aside.search-container { + width: 100%; + margin-bottom: 12px; + } + aside.search-container .box { + margin-right: 0; + } + + section.board-list { + margin-top: 12px; + width: 100%; + } + + table.board-list-table .board-meta, + table.board-list-table .board-pph, + table.board-list-table .board-tags { + padding: 0; + margin: 0; + font-size: 0; + width: 0; + } +} \ No newline at end of file diff --git a/stylesheets/tomorrow.css b/stylesheets/tomorrow.css index 04860284..17b95957 100644 --- a/stylesheets/tomorrow.css +++ b/stylesheets/tomorrow.css @@ -156,3 +156,32 @@ div#watchlist a:hover,a.watchThread:hover { .modlog tr:nth-child(even), .modlog th { background-color: #282A2E; } + + + +.box { + background: #282a2e; + border-color: #111; + color: #C5C8C6; +} +.box-title { + background: #282a2e; + color: #C5C8C6; +} + +table thead th { + background: #282a2e; + border-color: #111; + color: #C5C8C6; +} +table tbody tr:nth-of-type( even ) { + background-color: #282a2e; +} + +table.board-list-table .board-uri .board-sfw { + color: #C5C8C6; +} +tbody.board-list-omitted td { + background: #282a2e; + border-color: #111; +} \ No newline at end of file diff --git a/stylesheets/yotsuba.css b/stylesheets/yotsuba.css index 8997b696..34e4d079 100644 --- a/stylesheets/yotsuba.css +++ b/stylesheets/yotsuba.css @@ -3,10 +3,10 @@ body { color: #800000; } a:link, a:visited, p.intro a.email span.name { - color: #0000ff; + color: #800; } a:link:hover { - color: #d00; + color: #e00; } a.post_no { color: #800000; @@ -66,3 +66,32 @@ table.modlog tr th { .desktop-style div.boardlist:nth-child(1) { background-color: #F0E0D6; } + + + +.box { + background: #fff; + border-color: #800; + color: #000; +} +.box-title { + background: #fca; + color: #800; +} + +table thead th { + border-color: #800; + background: #fca; + color: #800; +} +table tbody tr:nth-of-type( even ) { + background-color: #ede2d4; +} + +table.board-list-table .board-uri .board-sfw { + color: #800; +} +tbody.board-list-omitted td { + background: #fca; + border-color: #800; +} \ No newline at end of file diff --git a/templates/8chan/boards-search.html b/templates/8chan/boards-search.html new file mode 100644 index 00000000..e4d70032 --- /dev/null +++ b/templates/8chan/boards-search.html @@ -0,0 +1,111 @@ +
    +
    +

    Global Statistics

    +

    {% trans %}There are currently {{boards_public}} public boards, {{boards_total}} total. Site-wide, {{posts_hour}} posts have been made in the last hour, with {{posts_total}} being made on all active boards since {{founding_date}}.{% endtrans %}

    + {% if uptime %}

    {{uptime}} without interruption

    {% endif %} +

    This page last updated {{page_updated}}.

    +
    + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + {{html_boards}} + + + + + + + + + + + + {% if boards_omitted > 0 %} + + {% endif %} + + +
    {% trans %}Board{% endtrans %}{% trans %}Title{% endtrans %}{% trans %}PPH{% endtrans %}{% trans %}Total posts{% endtrans %}{% trans %}Active users{% endtrans %}{% trans %}Tags{% endtrans %}
    Displaying results {{search.page + 1}} through {{ boards|count + search.page}} out of {{ boards|count + boards_omitted }}. Click to load more.
    +
    +
    +
    \ No newline at end of file diff --git a/templates/8chan/boards-table.html b/templates/8chan/boards-table.html new file mode 100644 index 00000000..82e48436 --- /dev/null +++ b/templates/8chan/boards-table.html @@ -0,0 +1,14 @@ +{% for board in boards %} + + {{ board.locale }} +
    + /{{board['uri']}}/ + {% if board['sfw'] %}{% endif %} +
    +
    {{ board['title'] }}
    +
    {{board['pph']}} +
    {{board['posts_total']}} +
    {{board['active']}} +
    {% for tag in board.tags %}{{ tag }}{% endfor %}
    + +{% endfor %} \ No newline at end of file diff --git a/templates/8chan/boards-tags.html b/templates/8chan/boards-tags.html index baf1e6d0..e3453516 100644 --- a/templates/8chan/boards-tags.html +++ b/templates/8chan/boards-tags.html @@ -1,162 +1,5 @@ - - -

    {% trans %}There are currently {{n_boards}} boards + {{hidden_boards_total}} unindexed boards = {{t_boards}} total boards. Site-wide, {{total_posts_hour}} posts have been made in the last hour, with {{total_posts}} being made on all active boards since October 23, 2013.{% endtrans %}

    - -{% if top2k %} -

    {% trans %}This list only shows the top 2000 boards. Until we can move tag searching onto the server side, click here for the full list.{% endtrans %}

    -{% endif %} - -
    - Tags:  - {% for tag, pop in tags %} - {% if pop > 1000 %} - {{ tag }} - {% elseif pop > 500 %} - {{ tag }} - {% elseif pop > 100 %} - {{ tag }} - {% else %} - {{ tag }} - {% endif %} - {% endfor %} -
    - - - - - - - - - - - -{% for board in boards %} - - - - - - - - -{% endfor %} -
    B{% trans %}Board{% endtrans %}{% trans %}Title{% endtrans %}{% trans %}PPH{% endtrans %}{% trans %}Total posts{% endtrans %}{% trans %}Active users{% endtrans %}{% trans %}Tags{% endtrans %}
    {{ board.img|raw }} {% if board['sfw'] %}{% else %}{% endif %}
    /{{board['uri']}}/{{lock|raw}}
    {{ board['title'] }}
    {{board['pph']}}{{board['max']}}{{board['uniq_ip']}}
    {% for tag in board.tags %}{{ tag }} {% endfor %}
    -

    Page last updated: {{last_update}}

    -

    {{uptime_p}} without interruption (read)

    - - +{% for tag, weight in tags %} +
  • + {{tag}} +
  • +{% endfor %} \ No newline at end of file diff --git a/templates/8chan/boards.html b/templates/8chan/boards.html index 15fdad01..db6b2682 100644 --- a/templates/8chan/boards.html +++ b/templates/8chan/boards.html @@ -1,68 +1,80 @@ - - -

    {% trans %}There are currently {{n_boards}} boards + {{hidden_boards_total}} unindexed boards = {{t_boards}} total boards. Site-wide, {{total_posts_hour}} posts have been made in the last hour, with {{total_posts}} being made on all active boards since October 23, 2013.{% endtrans %}

    - - - - - - - - - - - -{% for board in boards %} - - - - - - - - -{% endfor %} -
    L{% trans %}Board{% endtrans %}{% trans %}Board title{% endtrans %}{% trans %}Posts in last hour{% endtrans %}{% trans %}Total posts{% endtrans %}{% trans %}Unique IPs{% endtrans %}{% trans %}Created{% endtrans %}
    {{ board.img|raw }}/{{board['uri']}}/{{lock|raw}}{{ board['title'] }}{{board['pph']}}{{board['max']}}{{board['uniq_ip']}}{{board['time']}} ({{board['ago']}} ago)
    -

    Page last updated: {{last_update}}

    -

    {{uptime_p}} without interruption

    - - +
    +
    +

    Global Statistics

    +

    {% trans %}There are currently {{boards_public}} public boards, {{boards_total}} total. Site-wide, {{posts_hour}} posts have been made in the last hour, with {{posts_total}} being made on all active boards since {{founding_date}}.{% endtrans %}

    + {% if uptime %}

    {{uptime}} without interruption

    {% endif %} +

    This page last updated {{page_updated}}.

    +
    + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + {{html_boards}} +
    {% trans %}Board{% endtrans %}{% trans %}Title{% endtrans %}{% trans %}PPH{% endtrans %}{% trans %}Total posts{% endtrans %}{% trans %}Active users{% endtrans %}{% trans %}Tags{% endtrans %}
    +
    +
    +
    \ No newline at end of file diff --git a/templates/8chan/index.html b/templates/8chan/index.html index 19b42dc1..d8c6d40a 100644 --- a/templates/8chan/index.html +++ b/templates/8chan/index.html @@ -4,33 +4,6 @@ ∞chan