diff --git a/board-search.php b/board-search.php
new file mode 100644
index 00000000..a02c89bf
--- /dev/null
+++ b/board-search.php
@@ -0,0 +1,308 @@
+ 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] );
+ }
+ // 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();
+ }
+}
+
+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;
+
+ if (isset($boardActivity['active'][ $boardUri ])) {
+ $board['active'] = (int) $boardActivity['active'][ $boardUri ];
+ }
+ if (isset($boardActivity['average'][ $boardUri ])) {
+ $precision = 4 - strlen( $boardActivity['average'][ $boardUri ] );
+
+ if( $precision < 0 ) {
+ $precision = 0;
+ }
+
+ $board['pph'] = round( $boardActivity['average'][ $boardUri ], 2 );
+
+ 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']
+);
+
+$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..0acdfaf5 100644
--- a/boards.php
+++ b/boards.php
@@ -1,152 +1,96 @@
$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 */
+$boardsHTML = Element("8chan/boards-table.html", array(
+ "config" => $config,
+ "boards" => $boards,
+ "tag_query" => $tag_query,
+
+ )
+);
- array_splice($boards, 48);
+$tagsHTML = Element("8chan/boards-tags.html", array(
+ "config" => $config,
+ "tags" => $tags,
+ "tag_query" => $tag_query,
+
+ )
+);
- $boards = array_values($boards);
+$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
+ )
+);
- file_write("boards-top20.json", json_encode($boards));
- file_write("boards.html", $html_top2k);
- file_write("boards_full.html", $html);
- echo 'Done';
-}
+$pageHTML = Element("page.html", array(
+ "config" => $config,
+ "body" => $searchHTML
+ )
+);
+file_write("boards.html", $pageHTML);
+echo $pageHTML;
\ No newline at end of file
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 480df77f..8a279b41 100755
--- a/inc/functions.php
+++ b/inc/functions.php
@@ -516,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
@@ -628,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.
*
@@ -780,9 +786,23 @@ 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``" . ( $indexed_only ? " WHERE `indexed` = 1 " : "" ) .
+ "LEFT JOIN ``board_create``
+ ON ``boards``.`uri` = ``board_create``.`uri`
+ 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) {
@@ -798,6 +818,119 @@ 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 );
+ $forHour = ( (int)( $forTime / 3600 ) * 3600 ) - 3600;
+
+ $boardActivity = array(
+ 'active' => 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) {
+ if (!isset($boardActivity['active'][$bsRow['stat_uri']])) {
+ if ($bsRow['stat_hour'] == $forHour) {
+ $boardActivity['active'][$bsRow['stat_uri']] = unserialize( $bsRow['author_ip_array'] );
+ }
+
+ $boardActivity['average'][$bsRow['stat_uri']] = $bsRow['post_count'];
+ }
+ else {
+ if ($bsRow['stat_hour'] == $forHour) {
+ $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){
@@ -1010,70 +1143,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']);
@@ -1083,12 +1216,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();
}
@@ -1382,6 +1515,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;
@@ -2074,8 +2266,27 @@ function markup(&$body, $track_cites = false, $op = false) {
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";
+ }
+
+ $bodyNew .= "
" . $paragraph . "
";
+ }
+
+ $body = $bodyNew;
+ }
+ else {
+ $body = preg_replace("/\n/", '
', $body);
+ }
if ($config['markup_repair_tidy']) {
$tidy = new tidy();
@@ -2132,6 +2343,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 = '';
diff --git a/inc/instance-config.php b/inc/instance-config.php
index 1701fc64..c6fb8cea 100644
--- a/inc/instance-config.php
+++ b/inc/instance-config.php
@@ -126,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';
@@ -140,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';
@@ -152,25 +154,28 @@
$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(
@@ -180,6 +185,32 @@
'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;
@@ -200,6 +231,10 @@ $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-config.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/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 177c2dba..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,7 +233,7 @@ elseif (isset($_POST['post'])) {
$_POST['subject'] = '';
if (!isset($_POST['password']))
- $_POST['password'] = '';
+ $_POST['password'] = '';
if (isset($_POST['thread'])) {
$post['op'] = false;
@@ -638,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
@@ -708,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
@@ -906,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))
@@ -1005,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/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/style.css b/stylesheets/style.css
index e067cc10..a46677bb 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;
}
@@ -297,12 +363,13 @@ p.intro a {
color: maroon;
}
-div.delete {
- float: right;
-}
-
div.post.reply p {
- margin: 0.3em 0 0 0;
+ display: block;
+ margin: 0;
+
+ line-height: 1.16em;
+ font-size: 13px;
+ min-height: 1.16em;
}
div.post.reply div.body {
@@ -347,12 +414,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;
@@ -536,50 +605,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 +632,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 +1024,6 @@ span.pln {
color:grey;
}
-
@media screen and (min-width: 768px) {
p.intro {
clear: none;
@@ -1021,8 +1034,169 @@ span.pln {
}
}
-/* threadwatcher */
+/* === SITE-WIDE ASSETS === */
+#logo {
+ display: block;
+ width: 100%;
+ padding: 0;
+ margin: 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 +1239,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 +1267,6 @@ div.mix {
}
/* Quick reply (why was most of this ever in the script?) */
-
#quick-reply {
position: fixed;
right: 5%;
@@ -1259,3 +1432,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/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