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 FoundPage 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}}.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {% trans %}Board{% endtrans %} |
+ {% trans %}Title{% endtrans %} |
+ {% trans %}PPH{% endtrans %} |
+ {% trans %}Total posts{% endtrans %} |
+ {% trans %}Active users{% endtrans %} |
+ {% trans %}Tags{% endtrans %} |
+
+
+
+ {{html_boards}}
+
+
+
+ |
+
+
+
+
+
+ Displaying results {{search.page + 1}} through {{ boards|count + search.page}} out of {{ boards|count + boards_omitted }}. Click to load more. |
+
+ {% if boards_omitted > 0 %}
+
+ {% endif %}
+
+
+
+
+
+
\ 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['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 %}
-
-
-
-
-
- B |
- {% trans %}Board{% endtrans %} |
- {% trans %}Title{% endtrans %} |
- {% trans %}PPH{% endtrans %} |
- {% trans %}Total posts{% endtrans %} |
- {% trans %}Active users{% endtrans %} |
- {% trans %}Tags{% endtrans %} |
-
-{% for board in boards %}
-
- {{ board.img|raw }} {% if board['sfw'] %} {% else %} {% endif %} |
- |
- {{ board['title'] }} |
- {{board['pph']}} |
- {{board['max']}} |
- {{board['uniq_ip']}} |
- {% for tag in board.tags %}{{ tag }} {% endfor %} |
-{% 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 %}
-
-
-
- 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 %} |
-
-{% for board in boards %}
-
- {{ board.img|raw }} |
- /{{board['uri']}}/{{lock|raw}} |
- {{ board['title'] }} |
- {{board['pph']}} |
- {{board['max']}} |
- {{board['uniq_ip']}} |
- {{board['time']}} ({{board['ago']}} ago) |
-{% endfor %}
-
-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}}.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {% trans %}Board{% endtrans %} |
+ {% trans %}Title{% endtrans %} |
+ {% trans %}PPH{% endtrans %} |
+ {% trans %}Total posts{% endtrans %} |
+ {% trans %}Active users{% endtrans %} |
+ {% trans %}Tags{% endtrans %} |
+
+
+
+ {{html_boards}}
+
+
+
+
\ 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