1
0
mirror of https://github.com/vichan-devel/vichan.git synced 2025-01-19 09:27:24 +01:00
This commit is contained in:
8chan 2015-04-19 05:37:51 -07:00
commit b59999a05d
22 changed files with 1942 additions and 546 deletions

319
board-search.php Normal file
View File

@ -0,0 +1,319 @@
<?php
// We want to return a value if we're included.
// Otherwise, we will be printing a JSON object-array.
$Included = defined("TINYBOARD");
if (!$Included) {
include "inc/functions.php";
}
$CanViewUnindexed = isset($mod["type"]) && $mod["type"] <= GlobalVolunteer;
/* The expected output of this page is JSON. */
$response = array();
/* Determine search parameters from $_GET */
$search = array(
'lang' => 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 = 4 - strlen( $boardActivity['average'][ $boardUri ] );
if( $precision < 0 ) {
$precision = 0;
}
$board['pph'] = round( $boardActivity['average'][ $boardUri ], 2 );
$board['ppd'] = round( $boardActivity['today'][ $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']
);
if (php_sapi_name() == 'cli') {
$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 );
}
else {
$response['omitted'] = 0;
}
$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;
}

View File

@ -1,152 +1,106 @@
<?php
include "inc/functions.php";
include "inc/countries.php";
$admin = isset($mod["type"]) && $mod["type"]<=30;
$founding_date = "October 23, 2013";
if (php_sapi_name() == 'fpm-fcgi' && !$admin) {
if (php_sapi_name() == 'fpm-fcgi' && !$admin && count($_GET) == 0) {
error('Cannot be run directly.');
}
$boards = listBoards();
$all_tags = array();
$total_posts_hour = 0;
$total_posts = 0;
$write_maxes = false;
function to_tag($str) {
$str = trim($str);
$str = strtolower($str);
$str = str_replace(['_', ' '], '-', $str);
return $str;
}
if (!file_exists('maxes.txt') || filemtime('maxes.txt') < (time() - (60*60))) {
$fp = fopen('maxes.txt', 'w+');
$write_maxes = true;
}
foreach ($boards as $i => $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);
/* Build parameters for page */
$searchJson = include "board-search.php";
$boards = array();
$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'];
}
if (isset($searchJson['tagWeight'])) {
$tags = $searchJson['tagWeight'];
}
}
$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('<?php', '', $board_config);
@eval($board_config);
}
$showboard = $board['indexed'];
$locale = isset($boardCONFIG['locale'])?$boardCONFIG['locale']:'en';
$board['title'] = utf8tohtml($board['title']);
$locale_arr = explode('_', $locale);
$locale_short = isset($locale_arr[1]) ? strtolower($locale_arr[1]) : strtolower($locale_arr[0]);
$locale_short = str_replace('.utf-8', '', $locale_short);
$country = get_country($locale_short);
if ($board['uri'] === 'int') {$locale_short = 'eo'; $locale = 'eo'; $country = 'Esperanto';}
$board['img'] = "<img class=\"flag flag-$locale_short\" src=\"/static/blank.gif\" style=\"width:16px;height:11px;\" alt=\"$country\" title=\"$country\">";
if ($showboard || $admin) {
if (!$showboard) {
$lock = ' <i class="fa fa-lock" title="No index"></i>';
} else {
$lock = '';
}
$board['ago'] = human_time_diff(strtotime($board['time']));
} else {
unset($boards[$i]);
$hidden_boards_total += 1;
}
}
$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 &infin;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 &infin;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']);
}
array_splice($boards, 48);
$boards = array_values($boards);
file_write("boards-top20.json", json_encode($boards));
file_write("boards.html", $html_top2k);
file_write("boards_full.html", $html);
echo 'Done';
/* Create and distribute page */
$boardsHTML = Element("8chan/boards-table.html", array(
"config" => $config,
"boards" => $boards,
"tag_query" => $tag_query,
)
);
$tagsHTML = Element("8chan/boards-tags.html", array(
"config" => $config,
"tags" => $tags,
"tag_query" => $tag_query,
)
);
$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(
"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 (count($_GET) == 0) {
// Preserves the JSON output format of [{board},{board}].
$nonAssociativeBoardList = array_values($boards);
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;

View File

@ -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))

View File

@ -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
@ -636,29 +636,35 @@ function file_write($path, $data, $simple = false, $skip_purge = false) {
$remote = new Remote($config['remote'][$m[1]]);
$remote->write($data, $m[2]);
return;
} else {
}
else {
error('Invalid remote server: ' . $m[1]);
}
}
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, 0644))
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
if (function_exists("dio_fcntl")) {
dio_fcntl($fp, F_SETLK, array('type' => F_UNLCK));
}
// Close
dio_close($fp);
@ -780,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) {
@ -798,6 +819,139 @@ 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) {
$boardActivity['active'][$bsRow['stat_uri']] = unserialize( $bsRow['author_ip_array'] );
}
else {
$boardActivity['active'][$bsRow['stat_uri']] = array();
}
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['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'] ) );
}
if ($bsRow['stat_hour'] <= $forHour && $bsRow['stat_hour'] >= $yesterHour) {
$boardActivity['today'][$bsRow['stat_uri']] += $bsRow['post_count'];
}
$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){
@ -1382,6 +1536,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;
@ -2075,7 +2288,26 @@ function markup(&$body, $track_cites = false, $op = false) {
if ($config['strip_superfluous_returns'])
$body = preg_replace('/\s+$/', '', $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 .= "<p class=\"body-line {$paragraphDirection}\">" . $paragraph . "</p>";
}
$body = $bodyNew;
}
else {
$body = preg_replace("/\n/", '<br/>', $body);
}
if ($config['markup_repair_tidy']) {
$tidy = new tidy();
@ -2132,6 +2364,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 = '';

View File

@ -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';
@ -160,6 +162,9 @@
$config['markup'][] = array("/__(.+?)__/", "<u>\$1</u>");
$config['markup'][] = array("/###([^\s']+)###/", "<a href='/boards.html#\$1'>###\$1###</a>");
$config['markup_paragraphs'] = true;
$config['markup_rtl'] = true;
$config['boards'] = array(array('<i class="fa fa-home" title="Home"></i>' => '/', '<i class="fa fa-tags" title="Boards"></i>' => '/boards.html', '<i class="fa fa-question" title="FAQ"></i>' => '/faq.html', '<i class="fa fa-random" title="Random"></i>' => '/random.php', '<i class="fa fa-plus" title="New board"></i>' => '/create.php', '<i class="fa fa-ban" title="Public ban list"></i>' => '/bans.html', '<i class="fa fa-search" title="Search"></i>' => '/search.php', '<i class="fa fa-cog" title="Manage board"></i>' => '/mod.php', '<i class="fa fa-quote-right" title="Chat"></i>' => 'https://qchat.rizon.net/?channels=#8chan'), array('b', 'news+', 'boards'), array('operate', 'meta'), array('<i class="fa fa-twitter" title="Twitter"></i>'=>'https://twitter.com/infinitechan'));
//$config['boards'] = array(array('<i class="fa fa-home" title="Home"></i>' => '/', '<i class="fa fa-tags" title="Boards"></i>' => '/boards.html', '<i class="fa fa-question" title="FAQ"></i>' => '/faq.html', '<i class="fa fa-random" title="Random"></i>' => '/random.php', '<i class="fa fa-plus" title="New board"></i>' => '/create.php', '<i class="fa fa-search" title="Search"></i>' => '/search.php', '<i class="fa fa-cog" title="Manage board"></i>' => '/mod.php', '<i class="fa fa-quote-right" title="Chat"></i>' => 'https://qchat.rizon.net/?channels=#8chan'), array('b', 'meta', 'int'), array('v', 'a', 'tg', 'fit', 'pol', 'tech', 'mu', 'co', 'sp', 'boards'), array('<i class="fa fa-twitter" title="Twitter"></i>'=>'https://twitter.com/infinitechan'));
@ -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'] = "&infin;chan";
$config['site_logo'] = "/static/logo_33.svg";
// 8chan specific mod pages
require '8chan-mod-config.php';

View File

@ -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) {

View File

@ -9,3 +9,5 @@ if ($query) {
$index = Element("8chan/index.html", array("config" => $config, "newsplus" => $newsplus));
file_write('index.html', $index);
echo $index;

349
js/board-directory.js Normal file
View File

@ -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' : "<tr></tr>",
// Individual cell definitions
'board-cell-meta' : "<td class=\"board-meta\"></td>",
'board-cell-uri' : "<td class=\"board-uri\"></td>",
'board-cell-title' : "<td class=\"board-title\"></td>",
'board-cell-pph' : "<td class=\"board-pph\"></td>",
'board-cell-posts_total' : "<td class=\"board-max\"></td>",
'board-cell-active' : "<td class=\"board-unique\"></td>",
'board-cell-tags' : "<td class=\"board-tags\"></td>",
// Content wrapper
// Used to help constrain contents to their <td>.
'board-content-wrap' : "<div class=\"board-cell\"></div>",
// Individual items or parts of a single table cell.
'board-datum-lang' : "<span class=\"board-lang\"></span>",
'board-datum-uri' : "<a class=\"board-link\"></a>",
'board-datum-sfw' : "<i class=\"fa fa-briefcase board-sfw\" title=\"SFW\"></i>",
'board-datum-nsfw' : "<i class=\"fa fa-briefcase board-nsfw\" title=\"NSFW\"></i>",
'board-datum-tags' : "<a class=\"tag-link\" href=\"#\"></a>",
// Tag list.
'tag-list' : "<ul class=\"tag-list\"></ul>",
'tag-item' : "<li class=\"tag-item\"></li>",
'tag-link' : "<a class=\"tag-link\" href=\"#\"></a>"
}
},
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 );

View File

@ -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;
} );
} );

View File

@ -209,10 +209,15 @@ 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']))
@ -236,10 +241,6 @@ elseif (isset($_POST['post'])) {
} 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
@ -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))
@ -1011,15 +1018,18 @@ elseif (isset($_POST['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'))
if (function_exists('fastcgi_finish_request')) {
@fastcgi_finish_request();
}
if ($post['op'])
if ($post['op']) {
rebuildThemes('post-thread', $board['uri']);
else
}
else {
rebuildThemes('post', $board['uri']);
} elseif (isset($_POST['appeal'])) {
}
}
elseif (isset($_POST['appeal'])) {
if (!isset($_POST['ban_id']))
error($config['error']['bot']);

BIN
static/infinity-small.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -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", " 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;
}
}

View File

@ -0,0 +1,111 @@
<main id="boardlist">
<section class="description box col col-12">
<h2 class="box-title">Global Statistics</h2>
<p class="box-content">{% trans %}There are currently <strong>{{boards_public}}</strong> public boards, <strong>{{boards_total}}</strong> total. Site-wide, <strong>{{posts_hour}}</strong> posts have been made in the last hour, with <strong>{{posts_total}}</strong> being made on all active boards since {{founding_date}}.{% endtrans %}</p>
{% if uptime %}<p class="box-content">{{uptime}} without interruption</p>{% endif %}
<p class="box-content">This page last updated {{page_updated}}.</p>
</section>
<div class="board-list">
<aside class="search-container col col-2">
<form id="search-form" class="box" method="get" action="/boards.php">
<h2 class="box-title">Search</h2>
<div class="board-search box-content">
<label class="search-item search-sfw">
<input type="checkbox" id="search-sfw-input" name="sfw" value="1" {% if not search.nsfw %}checked="checked"{% endif %} />&nbsp;Hide NSFW boards
</label>
<div class="search-item search-title">
<input type="text" id="search-title-input" name="title" name="title" value="{{search.title}}" placeholder="Search titles..." />
</div>
<div class="search-item search-lang">
<select id="search-lang-input" name="lang">
<optgroup label="Popular">
<option value="">All languages</option>
<option value="en">English</option>
<option value="es">Spanish</option>
</optgroup>
<optgroup label="All">
{% for lang_code, lang_name in languages %}
<option value="{{lang_code}}">{{lang_name}}</option>
{% endfor %}
</optgroup>
</select>
</div>
<div class="search-item search-tag">
<input type="text" id="search-tag-input" name="tags" value="{{ search.tags|join(' ') }}" placeholder="Search tags..." />
</div>
<div class="search-item search-submit">
<button id="search-submit">Search</button>
<span id="search-loading" class="loading-small board-list-loading" style="display: none;"></span>
<script type="text/javascript">
/* Cheeky hack.
DOM Mutation is now depreceated, but board-directory.js fires before this button is added.
Since .ready() only fires after the entire page loads, we have this here to disable it as soon
as we pass it in the DOM structure.
We don't just disable="disable" it because then it would be broken for all non-JS browsers. */
document.getElementById( 'search-submit' ).disabled = "disabled";
document.getElementById( 'search-loading' ).style.display = "inline-block";
</script>
</div>
</div>
<ul class="tag-list box-content">
{{html_tags}}
</ul>
</form>
</aside>
<section class="board-list col col-10">
<table class="board-list-table">
<colgroup>
<col class="board-meta" />
<col class="board-uri" />
<col class="board-title" />
<col class="board-pph" />
<col class="board-max" />
<col class="board-unique" />
<col class="board-tags" />
</colgroup>
<thead class="board-list-head">
<tr>
<th class="board-meta" data-column="meta"></th>
<th class="board-uri" data-column="uri">{% trans %}Board{% endtrans %}</th>
<th class="board-title" data-column="title">{% trans %}Title{% endtrans %}</th>
<th class="board-pph" data-column="pph" title="Posts per hour">{% trans %}PPH{% endtrans %}</th>
<th class="board-max" data-column="posts_total">{% trans %}Total posts{% endtrans %}</th>
<th class="board-unique" data-column="active" title="Unique IPs to post in the last 72 hours">{% trans %}Active users{% endtrans %}</th>
<th class="board-tags" data-column="tags">{% trans %}Tags{% endtrans %}</th>
</tr>
</thead>
<tbody class="board-list-tbody">{{html_boards}}</tbody>
<tbody class="board-list-loading">
<tr>
<td colspan="7" class="loading"></td>
</tr>
</tbody>
<tbody class="board-list-omitted" data-omitted="{{boards_omitted}}">
<tr>
<td colspan="7" id="board-list-more">Displaying results <span class="board-page-num">{{search.page + 1}}</span> through <span class="board-page-count">{{ boards|count + search.page}}</span> out of <span class="board-page-total">{{ boards|count + boards_omitted }}</span>. <span class="board-page-loadmore">Click to load more.</span></td>
{% if boards_omitted > 0 %}
<script type="text/javascript">
/* Cheeky hack redux.
We want to show the loadmore for JS users when we have omitted boards.
However, the board-directory.js isn't designed to manipulate the page index on immediate load. */
document.getElementById("board-list-more").className = "board-list-hasmore";
</script>
{% endif %}
</tr>
</tbody>
</table>
</section>
</div>
</main>

View File

@ -0,0 +1,14 @@
{% for board in boards %}
<tr>
<td class="board-meta">{{ board.locale }}</td>
<td class="board-uri"><div class="board-cell">
<a href='/{{board['uri']}}/'>/{{board['uri']}}/</a>
{% if board['sfw'] %}<i class="fa fa-briefcase board-sfw" title="SFW"></i>{% endif %}
</div></td>
<td class="board-title"><div class="board-cell" title="Created {{board['time']}} ({{board['ago']}} ago)">{{ board['title'] }}</div></td>
<td class="board-pph"><div class="board-cell">{{board['pph']}}</td>
<td class="board-max"><div class="board-cell">{{board['posts_total']}}</td>
<td class="board-unique"><div class="board-cell">{{board['active']}}</td>
<td class="board-tags"><div class="board-cell">{% for tag in board.tags %}<a class="tag-link" href="{{ tag_query }}{{ tag }}">{{ tag }}</a>{% endfor %}</div></td>
</tr>
{% endfor %}

View File

@ -1,162 +1,5 @@
<style>
th.header {
background-image: url(/static/bg.gif);
cursor: pointer;
background-repeat: no-repeat;
background-position: center right;
padding-left: 20px;
margin-left: -1px;
}
th.headerSortUp {
background-image: url(/static/asc.gif);
}
th.headerSortDown {
background-image: url(/static/desc.gif);
}
table.modlog tr td.expand-td {
position: relative;
}
table.modlog tr td.expand-td:hover div{
background-color: #FFF;
position: absolute;
width: auto;
box-shadow: 0px 0px 5px #000;
padding: 0px 0 3px;
top: 5px;
left: 0;
z-index: 100;
}
.flag-eo {
background-image: url(/static/eo.png);
}
.flag-en {
background-image: url(/static/en.png);
}
.flag-jbo {
background-image: url(/static/jbo.png);
}
.uri {
overflow: hidden; width: 75px; white-space: nowrap;
}
.tags {
overflow: hidden; width: 150px; white-space: nowrap;
}
.board-name {
overflow: hidden; width: 200px; white-space: nowrap;
}
tr:nth-child(even) { background-color: #D6DAF0 }
</style>
<p style='text-align:center'>{% trans %}There are currently <strong>{{n_boards}}</strong> boards + <strong>{{hidden_boards_total}}</strong> unindexed boards = <strong>{{t_boards}}</strong> 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 %}</p>
{% if top2k %}
<p style='text-align:center'><a href="/boards_full.html">{% 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 %}</a></p>
{% endif %}
<div style='height:100px; overflow-y:scroll' class="tags-container">
<strong class="tags-strong">Tags:</strong>&nbsp;
{% for tag, pop in tags %}
{% if pop > 1000 %}
<a class="tag" href="#" style="font-size:1.75em">{{ tag }}</a>
{% elseif pop > 500 %}
<a class="tag" href="#" style="font-size:1.5em">{{ tag }}</a>
{% elseif pop > 100 %}
<a class="tag" href="#" style="font-size:1.25em">{{ tag }}</a>
{% else %}
<a class="tag" href="#">{{ tag }}</a>
{% endif %}
{% for tag, weight in tags %}
<li class="tag-item">
<a class="tag-link" href="{{ tag_query }}{{ tag }}" style="font-size: {{weight}}%;">{{tag}}</a>
</li>
{% endfor %}
</div>
<table class="modlog" style="width:auto"><thead>
<tr>
<th>B</th>
<th>{% trans %}Board{% endtrans %}</th>
<th>{% trans %}Title{% endtrans %}</th>
<th title="Posts per hour">{% trans %}PPH{% endtrans %}</th>
<th>{% trans %}Total posts{% endtrans %}</th>
<th title="Unique IPs to post in the last 72 hours">{% trans %}Active users{% endtrans %}</th>
<th>{% trans %}Tags{% endtrans %}</th>
</tr></thead><tbody>
{% for board in boards %}
<tr>
<td>{{ board.img|raw }} {% if board['sfw'] %}<img src="/static/sfw.png" title="Safe for work">{% else %}<img src="/static/nsfw.png" title="Not safe for work">{% endif %}</td>
<td><div class="uri"><a href='/{{board['uri']}}/'>/{{board['uri']}}/</a>{{lock|raw}}</div></td>
<td class="expand-td" title="Created {{board['time']}} ({{board['ago']}} ago)"><div class="board-name">{{ board['title'] }}</div></td>
<td style='text-align:right'>{{board['pph']}}</td>
<td style='text-align:right'>{{board['max']}}</td>
<td style='text-align:right'>{{board['uniq_ip']}}</td>
<td class="expand-td"><div class="tags">{% for tag in board.tags %}<span class="board-tag">{{ tag }}</span>&nbsp;{% endfor %}</div></td>
{% endfor %}
</tbody></table>
<p style='text-align:center'><em>Page last updated: {{last_update}}</em></p>
<p style='text-align:center'>{{uptime_p}} without interruption (read)</p>
<script>
$(function() {
$('table').tablesorter({sortList: [[5,1]],
textExtraction: function(node) {
childNode = node.childNodes[0];
if (!childNode) { return node.innerHTML; }
if (childNode.tagName == 'IMG') {
return childNode.getAttribute('class');
} else {
return (childNode.innerHTML ? childNode.innerHTML : childNode.textContent);
}
}
});
function filter_table(search) {
$("tbody>tr").css("display", "table-row");
if ($('#clear-selection').length === 0) {
$('.tags-strong').before('<a href="#" id="clear-selection">[clear selection]</a>');
$('#clear-selection').on('click', function(e){
e.preventDefault();
$("tbody>tr").css("display", "table-row");
window.location.hash = '';
});
}
window.location.hash = search;
var tags = $(".board-tag").filter(function() {
return $(this).text() === search;
});
$("tbody>tr").css("display", "none");
tags.parents("tr").css("display", "table-row");
}
$("a.tag").on("click", function(e) {
e.preventDefault();
filter_table($(this).text());
});
$('.tags-strong').before('<label>Filter tags: <input type="text" id="filter-tags"></label> ');
$('#filter-tags').on('keyup', function(e) {
$("a.tag").css("display", "inline-block");
var search = $(this).val();
if (!search) return;
var tags = $("a.tag").filter(function() {
return (new RegExp(search)).test($(this).text());
});
$("a.tag").css("display", "none");
tags.css("display", "inline-block");
});
if (window.location.hash) {
filter_table(window.location.hash.replace('#',''));
}
});
</script>

View File

@ -1,68 +1,80 @@
<style>
th.header {
background-image: url(/static/bg.gif);
cursor: pointer;
background-repeat: no-repeat;
background-position: center right;
padding-left: 20px;
margin-left: -1px;
}
th.headerSortUp {
background-image: url(/static/asc.gif);
}
th.headerSortDown {
background-image: url(/static/desc.gif);
}
.flag-eo {
background-image: url(/static/eo.png);
}
.flag-en {
background-image: url(/static/en.png);
}
.flag-jbo {
background-image: url(/static/jbo.png);
}
</style>
<main id="boardlist">
<section class="description box col col-12">
<h2 class="box-title">Global Statistics</h2>
<p class="box-content">{% trans %}There are currently <strong>{{boards_public}}</strong> public boards, <strong>{{boards_total}}</strong> 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 %}</p>
{% if uptime %}<p class="box-content">{{uptime}} without interruption</p>{% endif %}
<p class="box-content">This page last updated {{page_updated}}.</p>
</section>
<p style='text-align:center'>{% trans %}There are currently <strong>{{n_boards}}</strong> boards + <strong>{{hidden_boards_total}}</strong> unindexed boards = <strong>{{t_boards}}</strong> 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 %}</p>
<div class="board-list">
<aside class="search-container col col-2">
<form id="search-form" class="box" method="post" target="/board-search.php">
<h2 class="box-title">Search</h2>
<table class="modlog" style="width:auto"><thead>
<div class="board-search box-content">
<label class="search-item search-sfw">
<input type="checkbox" id="search-sfw-input" name="sfw" checked="checked" />&nbsp;NSFW boards
</label>
<div class="search-item search-title">
<input type="text" id="search-title-input" name="title" placeholder="Search titles..." />
</div>
<div class="search-item search-lang">
<select id="search-lang-input" name="lang">
<optgroup label="Popular">
<option>All languages</option>
<option>English</option>
<option>Spanish</option>
</optgroup>
<optgroup label="All">
<option>Chinese</option>
</optgroup>
</select>
</div>
<div class="search-item search-tag">
<input type="text" id="search-tag-input" name="tag" placeholder="Search tags..." />
</div>
<div class="search-item search-submit">
<button id="search-submit">Search</button>
</div>
</div>
<ul class="tag-list box-content">
<li class="tag-item">
<a class="tag-link" href="#">{{html_tags}}</a>
</li>
</ul>
</form>
</aside>
<section class="board-list col col-10">
<table class="board-list-table">
<colgroup>
<col class="board-meta" />
<col class="board-uri" />
<col class="board-title" />
<col class="board-pph" />
<col class="board-max" />
<col class="board-unique" />
<col class="board-tags" />
</colgroup>
<thead>
<tr>
<th>L</th>
<th>{% trans %}Board{% endtrans %}</th>
<th>{% trans %}Board title{% endtrans %}</th>
<th>{% trans %}Posts in last hour{% endtrans %}</th>
<th>{% trans %}Total posts{% endtrans %}</th>
<th>{% trans %}Unique IPs{% endtrans %}</th>
<th>{% trans %}Created{% endtrans %}</th>
</tr></thead><tbody>
{% for board in boards %}
<tr>
<td>{{ board.img|raw }}</td>
<td><a href='/{{board['uri']}}/'>/{{board['uri']}}/</a>{{lock|raw}}</td>
<td>{{ board['title'] }}</td>
<td style='text-align:right'>{{board['pph']}}</td>
<td style='text-align:right'>{{board['max']}}</td>
<td style='text-align:right'>{{board['uniq_ip']}}</td>
<td>{{board['time']}} ({{board['ago']}} ago)</td></tr>
{% endfor %}
</tbody></table>
<p style='text-align:center'><em>Page last updated: {{last_update}}</em></p>
<p style='text-align:center'>{{uptime_p}} without interruption</p>
<script>
$(function() {
$('table').tablesorter({sortList: [[5,1]],
textExtraction: function(node) {
childNode = node.childNodes[0];
if (!childNode) { return node.innerHTML; }
if (childNode.tagName == 'IMG') {
return childNode.getAttribute('class');
} else {
return (childNode.innerHTML ? childNode.innerHTML : childNode.textContent);
}
}
});
});
</script>
<th class="board-meta" data-column="meta"></th>
<th class="board-uri" data-column="uri">{% trans %}Board{% endtrans %}</th>
<th class="board-title" data-column="title">{% trans %}Title{% endtrans %}</th>
<th class="board-pph" data-column="pph" title="Posts per hour">{% trans %}PPH{% endtrans %}</th>
<th class="board-max" data-column="max">{% trans %}Total posts{% endtrans %}</th>
<th class="board-unique" data-column="unique" title="Unique IPs to post in the last 72 hours">{% trans %}Active users{% endtrans %}</th>
<th class="board-tags" data-column="tags">{% trans %}Tags{% endtrans %}</th>
</tr>
</thead>
<tbody class="board-list-tbody">{{html_boards}}</tbody>
</table>
</section>
</div>
</main>

View File

@ -4,33 +4,6 @@
<meta charset="utf-8">
<title>∞chan</title>
<style type="text/css">
/* Responsive helpers */
.col {
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;
}
/* Main */
* {

View File

@ -177,7 +177,7 @@ function highlightReply(id, event) {
post.className += ' highlighted';
if (history.pushState) {
history.pushState(null, null, window.document.location.origin + window.document.location.pathname + window.document.location.search + '#' + id);
history.pushState(null, null, window.document.location.protocol + "//" + window.document.location.host + window.document.location.pathname + window.document.location.search + '#' + id);
} else {
window.location.hash = id;
}

View File

@ -12,7 +12,15 @@
<body class="8chan {% if mod %}is-moderator{% else %}is-not-moderator{% endif %} stylesheet-{% if config.default_stylesheet.1 != '' and not mod %}{{ config.default_stylesheet.1 }}{% else %}default{% endif %}">
{% if pm %}<div class="top_notice">You have <a href="?/PM/{{ pm.id }}">an unread PM</a>{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.</div><hr>{% endif %}
<header>
<h1>{{ title }}</h1>
{% if config.site_logo %}
<figure id="logo">
<a id="logo-link" href="/" title="Return to the front page">
<img id="logo-img" src="{{config.site_logo}}" alt="{{config.site_nane}}" />
</a>
</figure>
{% endif %}
{% if title %}<h1>{{ title }}</h1>{% endif %}
<div class="subtitle">
{% if subtitle %}
{{ subtitle }}

View File

@ -1,14 +1,17 @@
<div id="post-moderation-fields">
{% if config.allow_delete %}
<div class="delete">
<div id="delete-fields">
{% trans %}Delete Post{% endtrans %} [<input title="Delete file only" type="checkbox" name="file" id="delete_file" />
<label for="delete_file">{% trans %}File{% endtrans %}</label>] <label for="password">{% trans %}Password{% endtrans %}</label>
<input id="password" type="password" name="password" size="12" maxlength="18" />
<input type="submit" name="delete" value="{% trans %}Delete{% endtrans %}" />
</div>
{% endif %}
<div class="delete" style="clear:both">
<div id="report-fields">
<label for="reason">{% trans %}Reason{% endtrans %}</label>
<input id="reason" type="text" name="reason" size="20" maxlength="30" />
[<input title="Global Report" type="checkbox" name="global" id="global_report" /><label for="global_report" title="Report rule violation (CP, etc) to global staff">{% trans %}Global{% endtrans %}</label>]
<input type="submit" name="report" value="{% trans %}Report{% endtrans %}" />
</div>
</div>

View File

@ -22,6 +22,7 @@
<title>{{ board.url }} - {{ meta_subject }}</title>
</head>
<body class="8chan {% if mod %}is-moderator{% else %}is-not-moderator{% endif %}" data-stylesheet="{% if config.default_stylesheet.1 != '' and not mod %}{{ config.default_stylesheet.1 }}{% else %}default{% endif %}">
<a name="top"></a>
{{ boardlist.top }}
{% if pm %}<div class="top_notice">You have <a href="?/PM/{{ pm.id }}">an unread PM</a>{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.</div><hr />{% endif %}
{% if config.url_banner %}<img class="board_image" src="{{ config.url_banner }}?board={{ board.uri|url_encode }}" {% if config.banner_width or config.banner_height %}style="{% if config.banner_width %}width:{{ config.banner_width }}px{% endif %};{% if config.banner_width %}height:{{ config.banner_height }}px{% endif %}" {% endif %}alt="" />{% endif %}
@ -53,19 +54,30 @@
{% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %}
<hr />
<form name="postcontrols" action="{{ config.post_url }}" method="post">
<input type="hidden" name="board" value="{{ board.uri }}" />
{% if mod %}<input type="hidden" name="mod" value="1" />{% endif %}
{{ body }}
{% include 'report_delete.html' %}
</form>
{{ body }}
<div id="thread-interactions">
<span id="thread-links">
<a id="thread-return" href="{{ return }}">[{% trans %}Return{% endtrans %}]</a>
<a id="thread-top" href="#" style="padding-left: 10px">[{% trans %}Go to top{% endtrans %}]</a>
<a id="thread-catalog" style="padding-left: 10px" href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">[{% trans %}Catalog{% endtrans %}]</a>
<a id="thread-top" href="#top">[{% trans %}Go to top{% endtrans %}]</a>
<a id="thread-catalog" href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">[{% trans %}Catalog{% endtrans %}]</a>
</span>
<span id="thread-quick-reply">
<a id="link-quick-reply" href="#">[{% trans %}Post a Reply{% endtrans %}]</a>
</span>
{% include 'report_delete.html' %}
</div>
<div class="clearfix"></div>
</form>
{{ boardlist.bottom }}
{% if board.uri not in config.banned_ad_boards %}

View File

@ -0,0 +1,125 @@
<?php
require dirname(__FILE__) . '/inc/cli.php';
/* Convert AI value to colun value for ez access */
// Add column `posts_total` to `boards`.
// This can potentially error if ran multiple times.. but that shouldn't kill the script
echo "Altering `boards` to add `posts_total`...\n";
query( "ALTER TABLE `boards` ADD COLUMN `posts_total` INT(11) UNSIGNED NOT NULL DEFAULT 0" );
// Set the value for posts_total for each board.
echo "Updating `boards` to include `posts_total` values...\n";
$tablePrefix = "{$config['db']['prefix']}posts_";
$aiQuery = prepare("SELECT `TABLE_NAME`, `AUTO_INCREMENT` FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"{$config['db']['database']}\"");
$aiQuery->execute() or error(db_error($aiQuery));
$aiResult = $aiQuery->fetchAll(PDO::FETCH_ASSOC);
foreach ($aiResult as $aiRow) {
$uri = str_replace( $tablePrefix, "", $aiRow['TABLE_NAME'] );
$posts = (int)($aiRow['AUTO_INCREMENT'] - 1); // Don't worry! The column is unsigned. -1 becomes 0.
echo " {$uri} has {$posts} post".($posts!=1?"s":"")."\n";
query( "UPDATE `boards` SET `posts_total`={$posts} WHERE `uri`=\"{$uri}\";" );
}
unset( $aiQuery, $aiResult, $uri, $posts );
/* Add statistics table and transmute post information to that */
// Add `board_stats`
echo "Adding `board_stats` ...\n";
query(
"CREATE TABLE IF NOT EXISTS ``board_stats`` (
`stat_uri` VARCHAR(58) NOT NULL,
`stat_hour` INT(11) UNSIGNED NOT NULL,
`post_count` INT(11) UNSIGNED NULL,
`post_id_array` TEXT NULL,
`author_ip_count` INT(11) UNSIGNED NULL,
`author_ip_array` TEXT NULL,
PRIMARY KEY (`stat_uri`, `stat_hour`)
);"
);
$boards = listBoards();
echo "Translating posts to stats ...\n";
foreach ($boards as $board) {
$postQuery = prepare("SELECT `id`, `time`, `ip` FROM ``posts_{$board['uri']}``");
$postQuery->execute() or error(db_error($postQuery));
$postResult = $postQuery->fetchAll(PDO::FETCH_ASSOC);
// Determine the number of posts for each hour.
$postHour = array();
foreach ($postResult as $post) {
// Winds back timestamp to last hour. (1428947438 -> 1428944400)
$postHourTime = (int)($post['time'] / 3600) * 3600;
if (!isset($postHour[ $postHourTime ])) {
$postHour[ $postHourTime ] = array();
}
$postDatum = &$postHour[ $postHourTime ];
// Add to post count.
if (!isset($postDatum['post_count'])) {
$postDatum['post_count'] = 1;
}
else {
++$postDatum['post_count'];
}
// Add to post id array.
if (!isset($postDatum['post_id_array'])) {
$postDatum['post_id_array'] = array( (int)$post['id'] );
}
else {
$postDatum['post_id_array'][] = (int)$post['id'];
}
// Add to ip array.
if (!isset($postDatum['author_ip_array'])) {
$postDatum['author_ip_array'] = array();
}
$postDatum['author_ip_array'][ less_ip( $post['ip'] ) ] = 1;
unset( $postHourTime );
}
// Prep data for insert.
foreach ($postHour as $postHourTime => &$postHourData) {
$postDatum = &$postHour[ $postHourTime ];
// Serialize arrays for TEXT insert.
$postDatum['post_id_array'] = str_replace( "\"", "\\\"", serialize( $postDatum['post_id_array'] ) );
$postDatum['author_ip_count'] = count( array_keys( $postDatum['author_ip_array'] ) );
$postDatum['author_ip_array'] = str_replace( "\"", "\\\"", serialize( array_keys( $postDatum['author_ip_array'] ) ) );
}
// Bash this shit together into a set of insert statements.
$statsInserts = array();
foreach ($postHour as $postHourTime => $postHourData) {
$statsInserts[] = "(\"{$board['uri']}\", \"{$postHourTime}\", \"{$postHourData['post_count']}\", \"{$postHourData['post_id_array']}\", \"{$postHourData['author_ip_count']}\", \"{$postHourData['author_ip_array']}\" )";
}
if (count($statsInserts) > 0) {
$statsInsert = "VALUES" . implode( ", ", $statsInserts );
echo " {$board['uri']} is building " . count($statsInserts) . " stat rows.\n";
// Insert this data into our statistics table.
$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));
}
else {
echo " {$board['uri']} has no posts!\n";
}
unset( $postQuery, $postResult, $postStatQuery, $postHour, $statsInserts, $statsInsert );
}
echo "Done! ^^;";