Merge branch '8n-tech-master'

This commit is contained in:
Fredrick Brennan 2015-04-20 10:56:04 +08:00
commit 7523c9bc04
43 changed files with 2799 additions and 808 deletions

6
.gitignore vendored
View File

@ -40,6 +40,12 @@ Thumbs.db
*.orig
*~
# tmp filesystem
/tmp/cache/*
/tmp/locks/*
!/tmp/cache/.gitkeep
!/tmp/locks/.gitkeep
#vichan custom
favicon.ico
/static/spoiler.png

View File

@ -3,6 +3,8 @@
require_once "inc/functions.php";
header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
global $config;
$dir = "static/404/";
if (!is_dir($dir))

313
board-search.php Normal file
View File

@ -0,0 +1,313 @@
<?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 = 1;
$board['pph'] = round( $boardActivity['average'][ $boardUri ], $precision );
$board['ppd'] = round( $boardActivity['today'][ $boardUri ], $precision );
unset( $precision );
}
}
// Sort boards by their popularity, then by their total posts.
$boardActivityValues = array();
$boardTotalPostsValues = array();
$boardWeightValues = array();
foreach ($response['boards'] as $boardUri => &$board) {
$boardActivityValues[$boardUri] = (int) $board['active'];
$boardTotalPostsValues[$boardUri] = (int) $board['posts_total'];
$boardWeightValues[$boardUri] = (int) $board['weight'];
}
array_multisort(
$boardWeightValues, SORT_DESC, SORT_NUMERIC, // Sort by weight
$boardActivityValues, SORT_DESC, SORT_NUMERIC, // Sort by number of active posters
$boardTotalPostsValues, SORT_DESC, SORT_NUMERIC, // Then, sort by total number of posts
$response['boards']
);
if (php_sapi_name() == 'cli') {
$response['boardsFull'] = $response['boards'];
}
$boardLimit = $search['index'] ? 50 : 100;
$response['omitted'] = count( $response['boards'] ) - $boardLimit;
$response['omitted'] = $response['omitted'] < 0 ? 0 : $response['omitted'];
$response['boards'] = array_splice( $response['boards'], $search['page'], $boardLimit );
$response['order'] = array_keys( $response['boards'] );
// Loop through the truncated array to compile tags.
$response['tags'] = array();
$tagUsage = array( 'boards' => array(), 'users' => array() );
foreach ($response['boards'] as $boardUri => &$board) {
if (isset($board['tags']) && count($board['tags']) > 0) {
foreach ($board['tags'] as $tag) {
if (!isset($tagUsage['boards'][$tag])) {
$tagUsage['boards'][$tag] = 0;
}
if (!isset($tagUsage['users'][$tag])) {
$tagUsage['users'][$tag] = 0;
}
$response['tags'][$tag] = true;
++$tagUsage['boards'][$tag];
$tagUsage['users'][$tag] += $board['active'];
}
}
}
// Get the top most popular tags.
if (count($response['tags']) > 0) {
arsort( $tagUsage['boards'] );
arsort( $tagUsage['users'] );
array_multisort(
$tagUsage['boards'], SORT_DESC, SORT_NUMERIC,
$tagUsage['users'], SORT_DESC, SORT_NUMERIC,
$response['tags']
);
// Get the first n most active tags.
$response['tags'] = array_splice( $response['tags'], 0, 100 );
$response['tagOrder'] = array_keys( $response['tags'] );
$response['tagWeight'] = array();
$tagsMostUsers = max( $tagUsage['users'] );
$tagsLeastUsers = min( $tagUsage['users'] );
$tagsAvgUsers = array_sum( $tagUsage['users'] ) / count( $tagUsage['users'] );
$weightDepartureFurthest = 0;
foreach ($tagUsage['users'] as $tagUsers) {
$weightDeparture = abs( $tagUsers - $tagsAvgUsers );
if( $weightDeparture > $weightDepartureFurthest ) {
$weightDepartureFurthest = $weightDeparture;
}
}
foreach ($tagUsage['users'] as $tagName => $tagUsers) {
if ($weightDepartureFurthest != 0) {
$weightDeparture = abs( $tagUsers - $tagsAvgUsers );
$response['tagWeight'][$tagName] = 75 + round( 100 * ( $weightDeparture / $weightDepartureFurthest ), 0);
}
else {
$response['tagWeight'][$tagName] = 100;
}
}
}
/* Include our interpreted search terms. */
$response['search'] = $search;
/* (Please) Respond */
if (!$Included) {
$json = json_encode( $response );
// Error Handling
switch (json_last_error()) {
case JSON_ERROR_NONE:
$jsonError = false;
break;
case JSON_ERROR_DEPTH:
$jsonError = 'Maximum stack depth exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$jsonError = 'Underflow or the modes mismatch';
break;
case JSON_ERROR_CTRL_CHAR:
$jsonError = 'Unexpected control character found';
break;
case JSON_ERROR_SYNTAX:
$jsonError = 'Syntax error, malformed JSON';
break;
case JSON_ERROR_UTF8:
$jsonError = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break;
default:
$jsonError = 'Unknown error';
break;
}
if ($jsonError) {
$json = "{\"error\":\"{$jsonError}\"}";
}
// Successful output
echo $json;
}
else {
return $response;
}

View File

@ -1,152 +1,109 @@
<?php
include "inc/functions.php";
include "inc/countries.php";
$admin = isset($mod["type"]) && $mod["type"]<=30;
$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;
}
/* Build parameters for page */
$searchJson = include "board-search.php";
$boards = array();
$tags = array();
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);
$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('<?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;
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 &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']);
}
/* Create and distribute page */
// buildJavascript();
array_splice($boards, 48);
$boardsHTML = Element("8chan/boards-table.html", array(
"config" => $config,
"boards" => $boards,
"tag_query" => $tag_query,
)
);
$boards = array_values($boards);
$tagsHTML = Element("8chan/boards-tags.html", array(
"config" => $config,
"tags" => $tags,
"tag_query" => $tag_query,
)
);
file_write("boards-top20.json", json_encode($boards));
file_write("boards.html", $html_top2k);
file_write("boards_full.html", $html);
echo 'Done';
$searchHTML = Element("8chan/boards-search.html", array(
"config" => $config,
"boards" => $boards,
"tags" => $tags,
"search" => $searchJson['search'],
"languages" => $config['languages'],
"boards_total" => $boards_total,
"boards_public" => $boards_public,
"boards_hidden" => $boards_hidden,
"boards_omitted" => $boards_omitted,
"posts_hour" => $posts_hour,
"posts_total" => $posts_total,
"founding_date" => $founding_date,
"page_updated" => date('r'),
"uptime" => shell_exec('uptime -p'),
"html_boards" => $boardsHTML,
"html_tags" => $tagsHTML
)
);
$pageHTML = Element("page.html", array(
"title" => _("Boardlist"),
"config" => $config,
"body" => $searchHTML
)
);
// We only want to cache if this is not a dynamic form request.
// Otherwise, our information will be skewed by the search criteria.
if (php_sapi_name() == 'cli') {
// Preserves the JSON output format of [{board},{board}].
$nonAssociativeBoardList = array_values($response['boardsFull']);
file_write("boards.html", $pageHTML);
file_write("boards.json", json_encode($nonAssociativeBoardList));
file_write("boards-top20.json", json_encode(array_splice($nonAssociativeBoardList, 0, 48)));
}
echo $pageHTML;

View File

@ -54,3 +54,23 @@ function human_time_diff( $from, $to = '' ) {
return $since;
}
function is_billion_laughs($arr1, $arr2) {
$arr = array();
foreach ($arr1 as $k => $v) {
$arr[$v] = $arr2[$k];
}
for ($i = 0; $i <= sizeof($arr); $i++) {
$cur = array_slice($arr, $i, 1);
$pst = array_slice($arr, 0, $i);
if (!$cur) continue;
$kk = array_keys($cur)[0];
$vv = array_values($cur)[0];
foreach ($pst as $k => $v) {
if (str_replace($kk, $vv, $v) != $v)
return true;
}
}
return false;
}

53
inc/8chan-mod-config.php Normal file
View File

@ -0,0 +1,53 @@
<?php
$config['mod']['show_ip'] = GLOBALVOLUNTEER;
$config['mod']['show_ip_less'] = BOARDVOLUNTEER;
$config['mod']['manageusers'] = GLOBALVOLUNTEER;
$config['mod']['noticeboard_post'] = GLOBALVOLUNTEER;
$config['mod']['search'] = GLOBALVOLUNTEER;
$config['mod']['clean_global'] = GLOBALVOLUNTEER;
$config['mod']['view_notes'] = DISABLED;
$config['mod']['create_notes'] = DISABLED;
$config['mod']['edit_config'] = DISABLED;
$config['mod']['debug_recent'] = ADMIN;
$config['mod']['debug_antispam'] = ADMIN;
$config['mod']['noticeboard_post'] = ADMIN;
$config['mod']['modlog'] = GLOBALVOLUNTEER;
$config['mod']['mod_board_log'] = MOD;
$config['mod']['editpost'] = BOARDVOLUNTEER;
$config['mod']['edit_banners'] = MOD;
$config['mod']['edit_flags'] = MOD;
$config['mod']['edit_settings'] = MOD;
$config['mod']['edit_volunteers'] = MOD;
$config['mod']['edit_tags'] = MOD;
$config['mod']['clean'] = BOARDVOLUNTEER;
// new perms
$config['mod']['ban'] = BOARDVOLUNTEER;
$config['mod']['bandelete'] = BOARDVOLUNTEER;
$config['mod']['unban'] = BOARDVOLUNTEER;
$config['mod']['deletebyip'] = BOARDVOLUNTEER;
$config['mod']['sticky'] = BOARDVOLUNTEER;
$config['mod']['cycle'] = BOARDVOLUNTEER;
$config['mod']['lock'] = BOARDVOLUNTEER;
$config['mod']['postinlocked'] = BOARDVOLUNTEER;
$config['mod']['bumplock'] = BOARDVOLUNTEER;
$config['mod']['view_bumplock'] = BOARDVOLUNTEER;
$config['mod']['bypass_field_disable'] = BOARDVOLUNTEER;
$config['mod']['view_banlist'] = BOARDVOLUNTEER;
$config['mod']['view_banstaff'] = BOARDVOLUNTEER;
$config['mod']['public_ban'] = BOARDVOLUNTEER;
$config['mod']['recent'] = BOARDVOLUNTEER;
$config['mod']['ban_appeals'] = BOARDVOLUNTEER;
$config['mod']['view_ban_appeals'] = BOARDVOLUNTEER;
$config['mod']['view_ban'] = BOARDVOLUNTEER;
$config['mod']['reassign_board'] = GLOBALVOLUNTEER;
$config['mod']['move'] = GLOBALVOLUNTEER;
$config['mod']['shadow_capcode'] = 'Global Volunteer';
// Mod pages assignment
$config['mod']['custom_pages']['/tags/(\%b)'] = '8_tags';
$config['mod']['custom_pages']['/reassign/(\%b)'] = '8_reassign';
$config['mod']['custom_pages']['/volunteers/(\%b)'] = '8_volunteers';
$config['mod']['custom_pages']['/flags/(\%b)'] = '8_flags';
$config['mod']['custom_pages']['/banners/(\%b)'] = '8_banners';
$config['mod']['custom_pages']['/settings/(\%b)'] = '8_settings';

View File

@ -1,73 +1,5 @@
<?php
if (!function_exists('is_billion_laughs')){
function is_billion_laughs($arr1, $arr2) {
$arr = array();
foreach ($arr1 as $k => $v) {
$arr[$v] = $arr2[$k];
}
for ($i = 0; $i <= sizeof($arr); $i++) {
$cur = array_slice($arr, $i, 1);
$pst = array_slice($arr, 0, $i);
if (!$cur) continue;
$kk = array_keys($cur)[0];
$vv = array_values($cur)[0];
foreach ($pst as $k => $v) {
if (str_replace($kk, $vv, $v) != $v)
return true;
}
}
return false;
}
}
$config['mod']['show_ip'] = GLOBALVOLUNTEER;
$config['mod']['show_ip_less'] = BOARDVOLUNTEER;
$config['mod']['manageusers'] = GLOBALVOLUNTEER;
$config['mod']['noticeboard_post'] = GLOBALVOLUNTEER;
$config['mod']['search'] = GLOBALVOLUNTEER;
$config['mod']['clean_global'] = GLOBALVOLUNTEER;
$config['mod']['view_notes'] = DISABLED;
$config['mod']['create_notes'] = DISABLED;
$config['mod']['edit_config'] = DISABLED;
$config['mod']['debug_recent'] = ADMIN;
$config['mod']['debug_antispam'] = ADMIN;
$config['mod']['noticeboard_post'] = ADMIN;
$config['mod']['modlog'] = GLOBALVOLUNTEER;
$config['mod']['mod_board_log'] = MOD;
$config['mod']['editpost'] = BOARDVOLUNTEER;
$config['mod']['edit_banners'] = MOD;
$config['mod']['edit_flags'] = MOD;
$config['mod']['edit_settings'] = MOD;
$config['mod']['edit_volunteers'] = MOD;
$config['mod']['edit_tags'] = MOD;
$config['mod']['clean'] = BOARDVOLUNTEER;
// new perms
$config['mod']['ban'] = BOARDVOLUNTEER;
$config['mod']['bandelete'] = BOARDVOLUNTEER;
$config['mod']['unban'] = BOARDVOLUNTEER;
$config['mod']['deletebyip'] = BOARDVOLUNTEER;
$config['mod']['sticky'] = BOARDVOLUNTEER;
$config['mod']['cycle'] = BOARDVOLUNTEER;
$config['mod']['lock'] = BOARDVOLUNTEER;
$config['mod']['postinlocked'] = BOARDVOLUNTEER;
$config['mod']['bumplock'] = BOARDVOLUNTEER;
$config['mod']['view_bumplock'] = BOARDVOLUNTEER;
$config['mod']['bypass_field_disable'] = BOARDVOLUNTEER;
$config['mod']['view_banlist'] = BOARDVOLUNTEER;
$config['mod']['view_banstaff'] = BOARDVOLUNTEER;
$config['mod']['public_ban'] = BOARDVOLUNTEER;
$config['mod']['recent'] = BOARDVOLUNTEER;
$config['mod']['ban_appeals'] = BOARDVOLUNTEER;
$config['mod']['view_ban_appeals'] = BOARDVOLUNTEER;
$config['mod']['view_ban'] = BOARDVOLUNTEER;
$config['mod']['reassign_board'] = GLOBALVOLUNTEER;
$config['mod']['move'] = GLOBALVOLUNTEER;
$config['mod']['shadow_capcode'] = 'Global Volunteer';
$config['mod']['custom_pages']['/tags/(\%b)'] = function ($b) {
function mod_8_tags ($b) {
global $board, $config;
if (!openBoard($b))
@ -114,9 +46,9 @@
$sfw = $query->fetchColumn();
mod_page(_('Edit tags'), 'mod/tags.html', array('board'=>$board,'token'=>make_secure_link_token('tags/'.$board['uri']), 'tags'=>$tags, 'sfw'=>$sfw));
};
}
$config['mod']['custom_pages']['/reassign/(\%b)'] = function($b) {
function mod_8_reassign($b) {
global $board, $config;
if (!openBoard($b))
@ -147,9 +79,9 @@
modLog("Reassigned board /$b/");
mod_page(_('Edit reassign'), 'blank.html', array('board'=>$board,'token'=>make_secure_link_token('reassign/'.$board['uri']),'body'=>$body));
};
}
$config['mod']['custom_pages']['/volunteers/(\%b)'] = function($b) {
function mod_8_volunteers($b) {
global $board, $config, $pdo;
if (!hasPermission($config['mod']['edit_volunteers'], $b))
error($config['error']['noaccess']);
@ -228,9 +160,9 @@
mod_page(_('Edit volunteers'), 'mod/volunteers.html', array('board'=>$board,'token'=>make_secure_link_token('volunteers/'.$board['uri']),'volunteers'=>$volunteers));
};
}
$config['mod']['custom_pages']['/flags/(\%b)'] = function($b) {
function mod_8_flags($b) {
global $config, $mod, $board;
require_once 'inc/image.php';
if (!hasPermission($config['mod']['edit_flags'], $b))
@ -341,6 +273,11 @@
\$config['user_flags'] = unserialize(file_get_contents('$b/flags.ser'));
FLAGS;
if ($config['cache']['enabled']) {
cache::delete('config_' . $b);
cache::delete('events_' . $b);
}
file_write($b.'/flags.php', $flags);
}
@ -364,9 +301,9 @@ FLAGS;
$banners = array_diff(scandir($dir), array('..', '.'));
mod_page(_('Edit flags'), 'mod/flags.html', array('board'=>$board,'banners'=>$banners,'token'=>make_secure_link_token('banners/'.$board['uri'])));
};
}
$config['mod']['custom_pages']['/banners/(\%b)'] = function($b) {
function mod_8_banners($b) {
global $config, $mod, $board;
require_once 'inc/image.php';
@ -427,9 +364,9 @@ FLAGS;
$banners = array_diff(scandir($dir), array('..', '.'));
mod_page(_('Edit banners'), 'mod/banners.html', array('board'=>$board,'banners'=>$banners,'token'=>make_secure_link_token('banners/'.$board['uri'])));
};
}
$config['mod']['custom_pages']['/settings/(\%b)'] = function($b) {
function mod_8_settings($b) {
global $config, $mod;
//if ($b === 'infinity' && $mod['type'] !== ADMIN)
@ -661,6 +598,7 @@ EOT;
// Faster than openBoard and bypasses cache...we're trusting the PHP output
// to be safe enough to run with every request, we can eval it here.
eval(str_replace('flags.php', "$b/flags.php", preg_replace('/^\<\?php$/m', '', $config_file)));
// czaks: maybe reconsider using it, now that config is cached?
// be smarter about rebuilds...only some changes really require us to rebuild all threads
if ($_config['captcha']['enabled'] != $config['captcha']['enabled']
@ -683,13 +621,18 @@ EOT;
$query->bindValue(':board', $b);
$query->execute() or error(db_error($query));
$board = $query->fetchAll()[0];
$css = @file_get_contents('stylesheets/board/' . $board['uri'] . '.css');
// Clean the cache
if ($config['cache']['enabled']) {
cache::delete('board_' . $board['uri']);
cache::delete('all_boards');
}
cache::delete('config_' . $board['uri']);
cache::delete('events_' . $board['uri']);
unlink('tmp/cache/locale_' . $board['uri']);
}
$css = @file_get_contents('stylesheets/board/' . $board['uri'] . '.css');
mod_page(_('Board configuration'), 'mod/settings.html', array('board'=>$board, 'css'=>prettify_textarea($css), 'token'=>make_secure_link_token('settings/'.$board['uri']), 'languages'=>$possible_languages,'allowed_urls'=>$config['allowed_offsite_urls']));
};
}

View File

@ -50,6 +50,17 @@ class Cache {
case 'php':
$data = isset(self::$cache[$key]) ? self::$cache[$key] : false;
break;
case 'fs':
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
if (!file_exists('tmp/cache/'.$key)) {
$data = false;
}
else {
$data = file_get_contents('tmp/cache/'.$key);
$data = json_decode($data, true);
}
break;
case 'redis':
if (!self::$cache)
self::init();
@ -87,6 +98,11 @@ class Cache {
case 'xcache':
xcache_set($key, $value, $expires);
break;
case 'fs':
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
file_put_contents('tmp/cache/'.$key, json_encode($value));
break;
case 'php':
self::$cache[$key] = $value;
break;
@ -113,6 +129,11 @@ class Cache {
case 'xcache':
xcache_unset($key);
break;
case 'fs':
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
@unlink('tmp/cache/'.$key);
break;
case 'php':
unset(self::$cache[$key]);
break;
@ -134,6 +155,12 @@ class Cache {
case 'php':
self::$cache = array();
break;
case 'fs':
$files = glob('tmp/cache/*');
foreach ($files as $file) {
unlink($file);
}
break;
case 'redis':
if (!self::$cache)
self::init();

View File

@ -132,6 +132,11 @@
// Tinyboard to use.
$config['cache']['redis'] = array('localhost', 6379, '', 1);
// EXPERIMENTAL: Should we cache configs? Warning: this changes board behaviour, i'd say, a lot.
// If you have any lambdas/includes present in your config, you should move them to instance-functions.php
// (this file will be explicitly loaded during cache hit, but not during cache miss).
$config['cache_config'] = false;
/*
* ====================
* Cookie settings
@ -1239,9 +1244,20 @@
// Website favicon.
$config['url_favicon'] = 'static/favicon.ico';
// EXPERIMENTAL: Try not to build pages when we shouldn't have to.
// Try not to build pages when we shouldn't have to.
$config['try_smarter'] = true;
// EXPERIMENTAL: Defer static HTML building to a moment, when a given file is actually accessed.
// Warning: This option won't run out of the box. You need to tell your webserver, that a file
// for serving 403 and 404 pages is /smart_build.php. Also, you need to turn off indexes.
$config['smart_build'] = false;
// Smart build related: when a file doesn't exist, where should we redirect?
$config['page_404'] = '/404.html';
// Smart build related: extra entrypoints.
$config['smart_build_entrypoints'] = array();
/*
* ====================
* Mod settings

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

@ -19,7 +19,9 @@ require_once 'inc/database.php';
require_once 'inc/events.php';
require_once 'inc/api.php';
require_once 'inc/bans.php';
require_once 'inc/lib/gettext/gettext.inc';
if (!extension_loaded('gettext')) {
require_once 'inc/lib/gettext/gettext.inc';
}
require_once 'inc/lib/parsedown/Parsedown.php'; // todo: option for parsedown instead of Tinyboard/STI markup
require_once 'inc/mod/auth.php';
@ -50,15 +52,42 @@ $current_locale = 'en';
function loadConfig() {
global $board, $config, $__ip, $debug, $__version, $microtime_start, $current_locale;
global $board, $config, $__ip, $debug, $__version, $microtime_start, $current_locale, $events;
$error = function_exists('error') ? 'error' : 'basic_error_function_because_the_other_isnt_loaded_yet';
reset_events();
$boardsuffix = isset($board['uri']) ? $board['uri'] : '';
if (!isset($_SERVER['REMOTE_ADDR']))
$_SERVER['REMOTE_ADDR'] = '0.0.0.0';
if (file_exists('tmp/cache/cache_config.php')) {
require_once('tmp/cache/cache_config.php');
}
if (isset($config['cache_config']) &&
$config['cache_config'] &&
$config = Cache::get('config_' . $boardsuffix ) ) {
$events = Cache::get('events_' . $boardsuffix );
define_groups();
if (file_exists('inc/instance-functions.php')) {
require_once('inc/instance-functions.php');
}
if ($config['locale'] != $current_locale) {
$current_locale = $config['locale'];
init_locale($config['locale'], $error);
}
}
else {
$config = array();
// We will indent that later.
reset_events();
$arrays = array(
'db',
'api',
@ -86,7 +115,6 @@ function loadConfig() {
'dashboard_links'
);
$config = array();
foreach ($arrays as $key) {
$config[$key] = array();
}
@ -96,18 +124,28 @@ function loadConfig() {
// Initialize locale as early as possible
$config['locale'] = 'en';
// Those calls are expensive. Unfortunately, our cache system is not initialized at this point.
// So, we may store the locale in a tmp/ filesystem.
$configstr = file_get_contents('inc/instance-config.php');
if (file_exists($fn = 'tmp/cache/locale_' . $boardsuffix ) ) {
$config['locale'] = file_get_contents($fn);
}
else {
$config['locale'] = 'en';
$configstr = file_get_contents('inc/instance-config.php');
if (isset($board['dir']) && file_exists($board['dir'] . '/config.php')) {
$configstr .= file_get_contents($board['dir'] . '/config.php');
$configstr .= file_get_contents($board['dir'] . '/config.php');
}
$matches = array();
preg_match_all('/[^\/*#]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches);
if ($matches && isset ($matches[2]) && $matches[2]) {
$matches = $matches[2];
$config['locale'] = $matches[count($matches)-1];
$matches = array();
preg_match_all('/[^\/*#]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches);
if ($matches && isset ($matches[2]) && $matches[2]) {
$matches = $matches[2];
$config['locale'] = $matches[count($matches)-1];
}
file_put_contents($fn, $config['locale']);
}
if ($config['locale'] != $current_locale) {
@ -128,18 +166,13 @@ function loadConfig() {
init_locale($config['locale'], $error);
}
if (!isset($__version))
$__version = file_exists('.installed') ? trim(file_get_contents('.installed')) : false;
$config['version'] = $__version;
date_default_timezone_set($config['timezone']);
if (!isset($config['global_message']))
$config['global_message'] = false;
if (!isset($config['post_url']))
$config['post_url'] = $config['root'] . $config['file_post'];
if (!isset($config['referer_match']))
if (isset($_SERVER['HTTP_HOST'])) {
$config['referer_match'] = '/^' .
@ -210,19 +243,26 @@ function loadConfig() {
if (!isset($config['user_flags']))
$config['user_flags'] = array();
if (!isset($__version))
$__version = file_exists('.installed') ? trim(file_get_contents('.installed')) : false;
$config['version'] = $__version;
if ($config['allow_roll'])
event_handler('post', 'diceRoller');
if (is_array($config['anonymous']))
$config['anonymous'] = $config['anonymous'][array_rand($config['anonymous'])];
}
// Effectful config processing below:
date_default_timezone_set($config['timezone']);
if ($config['root_file']) {
chdir($config['root_file']);
}
if ($config['verbose_errors']) {
set_error_handler('verbose_error_handler');
error_reporting(E_ALL);
ini_set('display_errors', true);
ini_set('html_errors', false);
} else {
ini_set('display_errors', false);
}
// Keep the original address to properly comply with other board configurations
if (!isset($__ip))
$__ip = $_SERVER['REMOTE_ADDR'];
@ -231,11 +271,21 @@ function loadConfig() {
if (preg_match('/^\:\:(ffff\:)?(\d+\.\d+\.\d+\.\d+)$/', $__ip, $m))
$_SERVER['REMOTE_ADDR'] = $m[2];
if ($config['verbose_errors']) {
set_error_handler('verbose_error_handler');
error_reporting(E_ALL);
ini_set('display_errors', true);
ini_set('html_errors', false);
} else {
ini_set('display_errors', false);
}
if ($config['syslog'])
openlog('tinyboard', LOG_ODELAY, LOG_SYSLOG); // open a connection to sysem logger
if ($config['recaptcha'])
require_once 'inc/lib/recaptcha/recaptchalib.php';
if ($config['cache']['enabled'])
require_once 'inc/cache.php';
@ -244,13 +294,22 @@ function loadConfig() {
event_handler('post', 'postHandler');
}
if (is_array($config['anonymous']))
$config['anonymous'] = $config['anonymous'][array_rand($config['anonymous'])];
if ($config['allow_roll'])
event_handler('post', 'diceRoller');
event('load-config');
if ($config['cache_config'] && !isset ($config['cache_config_loaded'])) {
file_put_contents('tmp/cache/cache_config.php', '<?php '.
'$config = array();'.
'$config[\'cache\'] = '.var_export($config['cache'], true).';'.
'$config[\'cache_config\'] = true;'.
'$config[\'debug\'] = '.var_export($config['debug'], true).';'.
'require_once(\'inc/cache.php\');'
);
$config['cache_config_loaded'] = true;
Cache::set('config_'.$boardsuffix, $config);
Cache::set('events_'.$boardsuffix, $events);
}
if ($config['debug']) {
if (!isset($debug)) {
@ -327,8 +386,12 @@ function verbose_error_handler($errno, $errstr, $errfile, $errline) {
function define_groups() {
global $config;
foreach ($config['mod']['groups'] as $group_value => $group_name)
defined($group_name) or define($group_name, $group_value, true);
foreach ($config['mod']['groups'] as $group_value => $group_name) {
$group_name = strtoupper($group_name);
if(!defined($group_name)) {
define($group_name, $group_value, true);
}
}
ksort($config['mod']['groups']);
}
@ -347,9 +410,22 @@ function rebuildThemes($action, $boardname = false) {
$_board = $board;
// List themes
$query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error());
if ($themes = Cache::get("themes")) {
// OK, we already have themes loaded
}
else {
$query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error());
while ($theme = $query->fetch(PDO::FETCH_ASSOC)) {
$themes = array();
while ($theme = $query->fetch(PDO::FETCH_ASSOC)) {
$themes[] = $theme;
}
Cache::set("themes", $themes);
}
foreach ($themes as $theme) {
// Restore them
$config = $_config;
$board = $_board;
@ -403,6 +479,10 @@ function rebuildTheme($theme, $action, $board = false) {
function themeSettings($theme) {
if ($settings = Cache::get("theme_settings_".$theme)) {
return $settings;
}
$query = prepare("SELECT `name`, `value` FROM ``theme_settings`` WHERE `theme` = :theme AND `name` IS NOT NULL");
$query->bindValue(':theme', $theme);
$query->execute() or error(db_error($query));
@ -412,6 +492,8 @@ function themeSettings($theme) {
$settings[$s['name']] = $s['value'];
}
Cache::set("theme_settings_".$theme, $settings);
return $settings;
}
@ -434,9 +516,9 @@ function setupBoard($array) {
$board = array(
'uri' => $array['uri'],
'title' => $array['title'],
'subtitle' => $array['subtitle'],
'indexed' => $array['indexed'],
'public_logs' => $array['public_logs']
'subtitle' => isset($array['subtitle']) ? $array['subtitle'] : "",
'indexed' => isset($array['indexed']) ? $array['indexed'] : true,
'public_logs' => isset($array['public_logs']) ? $array['public_logs'] : true,
);
// older versions
@ -469,6 +551,11 @@ function openBoard($uri) {
$board = getBoardInfo($uri);
if ($board) {
setupBoard($board);
if (function_exists('after_open_board')) {
after_open_board();
}
return true;
}
return false;
@ -541,41 +628,47 @@ function purge($uri) {
function file_write($path, $data, $simple = false, $skip_purge = false) {
global $config, $debug;
if (preg_match('/^remote:\/\/(.+)\:(.+)$/', $path, $m)) {
if (isset($config['remote'][$m[1]])) {
require_once 'inc/remote.php';
$remote = new Remote($config['remote'][$m[1]]);
$remote->write($data, $m[2]);
return;
} else {
}
else {
error('Invalid remote server: ' . $m[1]);
}
}
if (!$fp = dio_open($path, O_WRONLY | O_CREAT, 0644))
else {
// This will convert a local, relative path like "b/index.html" to a full path.
// dio_open does not work with relative paths on Windows machines.
$path = realpath(dirname($path)) . DIRECTORY_SEPARATOR . basename($path);
}
if (!$fp = dio_open( $path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) {
error('Unable to open file for writing: ' . $path);
}
// File locking
if (dio_fcntl($fp, F_SETLKW, array('type' => F_WRLCK)) === -1) {
if (function_exists("dio_fcntl") && dio_fcntl($fp, F_SETLKW, array('type' => F_WRLCK)) === -1) {
error('Unable to lock file: ' . $path);
}
// Truncate file
if (!dio_truncate($fp, 0))
error('Unable to truncate file: ' . $path);
// Write data
if (($bytes = dio_write($fp, $data)) === false)
if (($bytes = dio_write($fp, $data)) === false) {
error('Unable to write to file: ' . $path);
}
// Unlock
dio_fcntl($fp, F_SETLK, array('type' => F_UNLCK));
if (function_exists("dio_fcntl")) {
dio_fcntl($fp, F_SETLK, array('type' => F_UNLCK));
}
// Close
dio_close($fp);
/**
* Create gzipped file.
*
@ -630,6 +723,13 @@ function file_unlink($path) {
}
$ret = @unlink($path);
if ($config['gzip_static']) {
$gzpath = "$path.gz";
@unlink($gzpath);
}
if (isset($config['purge']) && $path[0] != '/' && isset($_SERVER['HTTP_HOST'])) {
// Purge cache
if (basename($path) == $config['file_index']) {
@ -686,9 +786,24 @@ function listBoards($just_uri = false, $indexed_only = false) {
return $boards;
if (!$just_uri) {
$query = query("SELECT ``boards``.`uri` uri, ``boards``.`title` title, ``boards``.`subtitle` subtitle, ``board_create``.`time` time, ``boards``.`indexed` indexed, ``boards``.`sfw` sfw FROM ``boards``" . ( $indexed_only ? " WHERE `indexed` = 1 " : "" ) . "LEFT JOIN ``board_create`` ON ``boards``.`uri` = ``board_create``.`uri` ORDER BY ``boards``.`uri`") or error(db_error());
$query = query(
"SELECT
``boards``.`uri` uri,
``boards``.`title` title,
``boards``.`subtitle` subtitle,
``board_create``.`time` time,
``boards``.`indexed` indexed,
``boards``.`sfw` sfw,
``boards``.`posts_total` posts_total
FROM ``boards``
LEFT JOIN ``board_create``
ON ``boards``.`uri` = ``board_create``.`uri`" .
( $indexed_only ? " WHERE `indexed` = 1 " : "" ) .
"ORDER BY ``boards``.`uri`") or error(db_error());
$boards = $query->fetchAll(PDO::FETCH_ASSOC);
} else {
}
else {
$boards = array();
$query = query("SELECT `uri` FROM ``boards``" . ( $indexed_only ? " WHERE `indexed` = 1" : "" ) . " ORDER BY ``boards``.`uri`") or error(db_error());
while (true) {
@ -704,6 +819,130 @@ function listBoards($just_uri = false, $indexed_only = false) {
return $boards;
}
function loadBoardConfig( $uri ) {
$config = array(
"locale" => "en_US",
);
$configPath = "./{$uri}/config.php";
if (file_exists( $configPath ) && is_readable( $configPath )) {
include( $configPath );
}
// **DO NOT** use $config outside of this local scope.
// It's used by our global config array.
return $config;
}
function fetchBoardActivity( array $uris = array(), $forTime = false, $detailed = false ) {
global $config;
// Set our search time for now if we didn't pass one.
if (!is_integer($forTime)) {
$forTime = time();
}
// Get the last hour for this timestamp.
$nowHour = ( (int)( time() / 3600 ) * 3600 );
// Get the hour before. This is what we actually use for pulling data.
$forHour = ( (int)( $forTime / 3600 ) * 3600 ) - 3600;
// Get the hour from yesterday to calculate posts per day.
$yesterHour = $forHour - ( 3600 * 23 );
$boardActivity = array(
'active' => array(),
'today' => array(),
'average' => array(),
);
// Query for stats for these boards.
if (count($uris)) {
$uriSearch = "`stat_uri` IN (\"" . implode( (array) $uris, "\",\"" ) . "\") AND ";
}
else {
$uriSearch = "";
}
if ($detailed === true) {
$bsQuery = prepare("SELECT `stat_uri`, `stat_hour`, `post_count`, `author_ip_array` FROM ``board_stats`` WHERE {$uriSearch} ( `stat_hour` <= :hour AND `stat_hour` >= :hoursago )");
$bsQuery->bindValue(':hour', $forHour, PDO::PARAM_INT);
$bsQuery->bindValue(':hoursago', $forHour - ( 3600 * 72 ), PDO::PARAM_INT);
$bsQuery->execute() or error(db_error($bsQuery));
$bsResult = $bsQuery->fetchAll(PDO::FETCH_ASSOC);
// Format the results.
foreach ($bsResult as $bsRow) {
// Do we need to define the arrays for this URI?
if (!isset($boardActivity['active'][$bsRow['stat_uri']])) {
if ($bsRow['stat_hour'] <= $forHour && $bsRow['stat_hour'] >= $yesterHour) {
$boardActivity['today'][$bsRow['stat_uri']] = $bsRow['post_count'];
}
else {
$boardActivity['today'][$bsRow['stat_uri']] = 0;
}
$boardActivity['active'][$bsRow['stat_uri']] = unserialize( $bsRow['author_ip_array'] );
$boardActivity['average'][$bsRow['stat_uri']] = $bsRow['post_count'];
}
else {
if ($bsRow['stat_hour'] <= $forHour && $bsRow['stat_hour'] >= $yesterHour) {
$boardActivity['today'][$bsRow['stat_uri']] += $bsRow['post_count'];
}
$boardActivity['active'][$bsRow['stat_uri']] = array_merge( $boardActivity['active'][$bsRow['stat_uri']], unserialize( $bsRow['author_ip_array'] ) );
$boardActivity['average'][$bsRow['stat_uri']] += $bsRow['post_count'];
}
}
foreach ($boardActivity['active'] as &$activity) {
$activity = count( array_unique( $activity ) );
}
foreach ($boardActivity['average'] as &$activity) {
$activity /= 72;
}
}
// Simple return.
else {
$bsQuery = prepare("SELECT SUM(`post_count`) AS `post_count` FROM ``board_stats`` WHERE {$uriSearch} ( `stat_hour` = :hour )");
$bsQuery->bindValue(':hour', $forHour, PDO::PARAM_INT);
$bsQuery->execute() or error(db_error($bsQuery));
$bsResult = $bsQuery->fetchAll(PDO::FETCH_ASSOC);
$boardActivity = $bsResult[0]['post_count'];
}
return $boardActivity;
}
function fetchBoardTags( $uris ) {
global $config;
$boardTags = array();
$uris = "\"" . implode( (array) $uris, "\",\"" ) . "\"";
$tagQuery = prepare("SELECT * FROM ``board_tags`` WHERE `uri` IN ({$uris})");
$tagQuery->execute() or error(db_error($tagQuery));
$tagResult = $tagQuery->fetchAll(PDO::FETCH_ASSOC);
if ($tagResult) {
foreach ($tagResult as $tagRow) {
$tag = $tagRow['tag'];
$tag = trim($tag);
$tag = strtolower($tag);
$tag = str_replace(['_', ' '], '-', $tag);
if (!isset($boardTags[ $tagRow['uri'] ])) {
$boardTags[ $tagRow['uri'] ] = array();
}
$boardTags[ $tagRow['uri'] ][] = htmlentities( utf8_encode( $tag ) );
}
}
return $boardTags;
}
function until($timestamp) {
$difference = $timestamp - time();
switch(TRUE){
@ -916,70 +1155,70 @@ function insertFloodPost(array $post) {
function post(array $post) {
global $pdo, $board;
$query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, :cycle, 0, :embed, NULL)", $board['uri']));
// Basic stuff
if (!empty($post['subject'])) {
$query->bindValue(':subject', $post['subject']);
} else {
$query->bindValue(':subject', null, PDO::PARAM_NULL);
}
if (!empty($post['email'])) {
$query->bindValue(':email', $post['email']);
} else {
$query->bindValue(':email', null, PDO::PARAM_NULL);
}
if (!empty($post['trip'])) {
$query->bindValue(':trip', $post['trip']);
} else {
$query->bindValue(':trip', null, PDO::PARAM_NULL);
}
$query->bindValue(':name', $post['name']);
$query->bindValue(':body', $post['body']);
$query->bindValue(':body_nomarkup', $post['body_nomarkup']);
$query->bindValue(':time', isset($post['time']) ? $post['time'] : time(), PDO::PARAM_INT);
$query->bindValue(':password', $post['password']);
$query->bindValue(':password', $post['password']);
$query->bindValue(':ip', isset($post['ip']) ? $post['ip'] : $_SERVER['REMOTE_ADDR']);
if ($post['op'] && $post['mod'] && isset($post['sticky']) && $post['sticky']) {
$query->bindValue(':sticky', true, PDO::PARAM_INT);
} else {
$query->bindValue(':sticky', false, PDO::PARAM_INT);
}
if ($post['op'] && $post['mod'] && isset($post['locked']) && $post['locked']) {
$query->bindValue(':locked', true, PDO::PARAM_INT);
} else {
$query->bindValue(':locked', false, PDO::PARAM_INT);
}
if ($post['op'] && $post['mod'] && isset($post['cycle']) && $post['cycle']) {
$query->bindValue(':cycle', true, PDO::PARAM_INT);
} else {
$query->bindValue(':cycle', false, PDO::PARAM_INT);
}
if ($post['mod'] && isset($post['capcode']) && $post['capcode']) {
$query->bindValue(':capcode', $post['capcode'], PDO::PARAM_INT);
} else {
$query->bindValue(':capcode', null, PDO::PARAM_NULL);
}
if (!empty($post['embed'])) {
$query->bindValue(':embed', $post['embed']);
} else {
$query->bindValue(':embed', null, PDO::PARAM_NULL);
}
if ($post['op']) {
// No parent thread, image
$query->bindValue(':thread', null, PDO::PARAM_NULL);
} else {
$query->bindValue(':thread', $post['thread'], PDO::PARAM_INT);
}
if ($post['has_file']) {
$query->bindValue(':files', json_encode($post['files']));
$query->bindValue(':num_files', $post['num_files']);
@ -989,12 +1228,12 @@ function post(array $post) {
$query->bindValue(':num_files', 0);
$query->bindValue(':filehash', null, PDO::PARAM_NULL);
}
if (!$query->execute()) {
undoImage($post);
error(db_error($query));
}
return $pdo->lastInsertId();
}
@ -1004,8 +1243,9 @@ function bumpThread($id) {
if (event('bump', $id))
return true;
if ($config['try_smarter'])
$build_pages[] = thread_find_page($id);
if ($config['try_smarter']) {
$build_pages = array_merge(range(1, thread_find_page($id)), $build_pages);
}
$query = prepare(sprintf("UPDATE ``posts_%s`` SET `bump` = :time WHERE `id` = :id AND `thread` IS NULL", $board['uri']));
$query->bindValue(':time', time(), PDO::PARAM_INT);
@ -1287,6 +1527,65 @@ function index($page, $mod=false) {
);
}
// Handle statistic tracking for a new post.
function updateStatisticsForPost( $post, $new = true ) {
$postIp = isset($post['ip']) ? $post['ip'] : $_SERVER['REMOTE_ADDR'];
$postUri = $post['board'];
$postTime = (int)( $post['time'] / 3600 ) * 3600;
$bsQuery = prepare("SELECT * FROM ``board_stats`` WHERE `stat_uri` = :uri AND `stat_hour` = :hour");
$bsQuery->bindValue(':uri', $postUri);
$bsQuery->bindValue(':hour', $postTime, PDO::PARAM_INT);
$bsQuery->execute() or error(db_error($bsQuery));
$bsResult = $bsQuery->fetchAll(PDO::FETCH_ASSOC);
// Flesh out the new stats row.
$boardStats = array();
// If we already have a row, we're going to be adding this post to it.
if (count($bsResult)) {
$boardStats = $bsResult[0];
$boardStats['stat_uri'] = $postUri;
$boardStats['stat_hour'] = $postTime;
$boardStats['post_id_array'] = unserialize( $boardStats['post_id_array'] );
$boardStats['author_ip_array'] = unserialize( $boardStats['author_ip_array'] );
++$boardStats['post_count'];
$boardStats['post_id_array'][] = (int) $post['id'];
$boardStats['author_ip_array'][] = less_ip( $postIp );
$boardStats['author_ip_array'] = array_unique( $boardStats['author_ip_array'] );
}
// If this a new row, we're building the stat to only reflect this first post.
else {
$boardStats['stat_uri'] = $postUri;
$boardStats['stat_hour'] = $postTime;
$boardStats['post_count'] = 1;
$boardStats['post_id_array'] = array( (int) $post['id'] );
$boardStats['author_ip_count'] = 1;
$boardStats['author_ip_array'] = array( less_ip( $postIp ) );
}
// Cleanly serialize our array for insertion.
$boardStats['post_id_array'] = str_replace( "\"", "\\\"", serialize( $boardStats['post_id_array'] ) );
$boardStats['author_ip_array'] = str_replace( "\"", "\\\"", serialize( $boardStats['author_ip_array'] ) );
// Insert this data into our statistics table.
$statsInsert = "VALUES(\"{$boardStats['stat_uri']}\", \"{$boardStats['stat_hour']}\", \"{$boardStats['post_count']}\", \"{$boardStats['post_id_array']}\", \"{$boardStats['author_ip_count']}\", \"{$boardStats['author_ip_array']}\" )";
$postStatQuery = prepare(
"REPLACE INTO ``board_stats`` (stat_uri, stat_hour, post_count, post_id_array, author_ip_count, author_ip_array) {$statsInsert}"
);
$postStatQuery->execute() or error(db_error($postStatQuery));
// Update the posts_total tracker on the board.
if ($new) {
query("UPDATE ``boards`` SET `posts_total`=`posts_total`+1 WHERE `uri`=\"{$postUri}\"");
}
return $boardStats;
}
function getPageButtons($pages, $mod=false) {
global $config, $board;
@ -1491,56 +1790,65 @@ function checkMute() {
}
}
function buildIndex() {
function buildIndex($global_api = "yes") {
global $board, $config, $build_pages;
$pages = getPages();
if (!$config['try_smarter'])
$antibot = create_antibot($board['uri']);
if (!$config['smart_build']) {
$pages = getPages();
if (!$config['try_smarter'])
$antibot = create_antibot($board['uri']);
if ($config['api']['enabled']) {
$api = new Api();
$catalog = array();
if ($config['api']['enabled']) {
$api = new Api();
$catalog = array();
}
}
for ($page = 1; $page <= $config['max_pages']; $page++) {
$filename = $board['dir'] . ($page == 1 ? $config['file_index'] : sprintf($config['file_page'], $page));
$jsonFilename = $board['dir'] . ($page - 1) . '.json'; // pages should start from 0
if (!$config['api']['enabled'] && $config['try_smarter'] && isset($build_pages) && !empty($build_pages)
&& !in_array($page, $build_pages) && is_file($filename))
continue;
$content = index($page);
if (!$content)
break;
// json api
if ($config['api']['enabled']) {
$threads = $content['threads'];
$json = json_encode($api->translatePage($threads));
$jsonFilename = $board['dir'] . ($page - 1) . '.json'; // pages should start from 0
file_write($jsonFilename, $json);
$catalog[$page-1] = $threads;
}
if ($config['api']['enabled'] && $config['try_smarter'] && isset($build_pages) && !empty($build_pages)
&& !in_array($page, $build_pages) && is_file($filename))
if ((!$config['api']['enabled'] || $global_api == "skip" || $config['smart_build']) && $config['try_smarter']
&& isset($build_pages) && !empty($build_pages) && !in_array($page, $build_pages) )
continue;
if ($config['try_smarter']) {
$antibot = create_antibot($board['uri'], 0 - $page);
$content['current_page'] = $page;
}
$antibot->reset();
$content['pages'] = $pages;
$content['pages'][$page-1]['selected'] = true;
$content['btn'] = getPageButtons($content['pages']);
$content['antibot'] = $antibot;
if (!$config['smart_build']) {
$content = index($page);
if (!$content)
break;
file_write($filename, Element('index.html', $content));
// json api
if ($config['api']['enabled']) {
$threads = $content['threads'];
$json = json_encode($api->translatePage($threads));
file_write($jsonFilename, $json);
$catalog[$page-1] = $threads;
}
if ($config['api']['enabled'] && $global_api != "skip" && $config['try_smarter'] && isset($build_pages)
&& !empty($build_pages) && !in_array($page, $build_pages) )
continue;
if ($config['try_smarter']) {
$antibot = create_antibot($board['uri'], 0 - $page);
$content['current_page'] = $page;
}
$antibot->reset();
$content['pages'] = $pages;
$content['pages'][$page-1]['selected'] = true;
$content['btn'] = getPageButtons($content['pages']);
$content['antibot'] = $antibot;
file_write($filename, Element('index.html', $content));
}
else {
file_unlink($filename);
file_unlink($jsonFilename);
}
}
if ($page < $config['max_pages']) {
if (!$config['smart_build'] && $page < $config['max_pages']) {
for (;$page<=$config['max_pages'];$page++) {
$filename = $board['dir'] . ($page==1 ? $config['file_index'] : sprintf($config['file_page'], $page));
file_unlink($filename);
@ -1553,14 +1861,22 @@ function buildIndex() {
}
// json api catalog
if ($config['api']['enabled']) {
$json = json_encode($api->translateCatalog($catalog));
$jsonFilename = $board['dir'] . 'catalog.json';
file_write($jsonFilename, $json);
if ($config['api']['enabled'] && $global_api != "skip") {
if ($config['smart_build']) {
$jsonFilename = $board['dir'] . 'catalog.json';
file_unlink($jsonFilename);
$jsonFilename = $board['dir'] . 'threads.json';
file_unlink($jsonFilename);
}
else {
$json = json_encode($api->translateCatalog($catalog));
$jsonFilename = $board['dir'] . 'catalog.json';
file_write($jsonFilename, $json);
$json = json_encode($api->translateCatalog($catalog, true));
$jsonFilename = $board['dir'] . 'threads.json';
file_write($jsonFilename, $json);
$json = json_encode($api->translateCatalog($catalog, true));
$jsonFilename = $board['dir'] . 'threads.json';
file_write($jsonFilename, $json);
}
}
if ($config['try_smarter'])
@ -1958,12 +2274,38 @@ function markup(&$body, $track_cites = false, $op = false) {
$tracked_cites = array_unique($tracked_cites, SORT_REGULAR);
$body = preg_replace("/^\s*&gt;.*$/m", '<span class="quote">$0</span>', $body);
//$body = preg_replace("/^\s*&gt;.*$/m", '<span class="quote">$0</span>', $body);
if ($config['strip_superfluous_returns'])
$body = preg_replace('/\s+$/', '', $body);
$body = preg_replace("/\n/", '<br/>', $body);
if ($config['markup_paragraphs']) {
$paragraphs = explode("\n", $body);
$bodyNew = "";
foreach ($paragraphs as $paragraph) {
if (strlen(trim($paragraph)) > 0) {
$paragraphDirection = is_rtl($paragraph) ? "rtl" : "ltr";
}
else {
$paragraphDirection = "empty";
}
if (strpos($paragraph, "&gt;")===0) {
$quoteClass = "quote";
}
else {
$quoteClass = "";
}
$bodyNew .= "<p class=\"body-line {$paragraphDirection} {$quoteClass}\">" . $paragraph . "</p>";
}
$body = $bodyNew;
}
else {
$body = preg_replace("/\n/", '<br/>', $body);
}
if ($config['markup_repair_tidy']) {
$tidy = new tidy();
@ -2020,6 +2362,40 @@ function ordutf8($string, &$offset) {
return $code;
}
function uniord($u) {
$k = mb_convert_encoding($u, 'UCS-2LE', 'UTF-8');
$k1 = ord(substr($k, 0, 1));
$k2 = ord(substr($k, 1, 1));
return $k2 * 256 + $k1;
}
function is_rtl($str) {
if(mb_detect_encoding($str) !== 'UTF-8') {
$str = mb_convert_encoding($str, mb_detect_encoding($str),'UTF-8');
}
preg_match_all('/[^\n\s]+/', $str, $matches);
preg_match_all('/.|\n\s/u', $str, $matches);
$chars = $matches[0];
$arabic_count = 0;
$latin_count = 0;
$total_count = 0;
foreach ($chars as $char) {
$pos = uniord($char);
if ($pos >= 1536 && $pos <= 1791) {
$arabic_count++;
}
else if ($pos > 123 && $pos < 123) {
$latin_count++;
}
$total_count++;
}
return (($arabic_count/$total_count) > 0.5);
}
function strip_combining_chars($str) {
$chars = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);
$str = '';
@ -2049,51 +2425,62 @@ function buildThread($id, $return = false, $mod = false) {
cache::delete("thread_{$board['uri']}_{$id}");
}
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
if (!isset($thread)) {
$thread = new Thread($post, $mod ? '?/' : $config['root'], $mod);
} else {
$thread->add(new Post($post, $mod ? '?/' : $config['root'], $mod));
}
}
// Check if any posts were found
if (!isset($thread))
error($config['error']['nonexistant']);
$hasnoko50 = $thread->postCount() >= $config['noko50_min'];
$antibot = $mod || $return ? false : create_antibot($board['uri'], $id);
$body = Element('thread.html', array(
'board' => $board,
'thread' => $thread,
'body' => $thread->build(),
'config' => $config,
'id' => $id,
'mod' => $mod,
'hasnoko50' => $hasnoko50,
'isnoko50' => false,
'antibot' => $antibot,
'boardlist' => createBoardlist($mod),
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
));
if ($config['try_smarter'] && !$mod)
$build_pages[] = thread_find_page($id);
// json api
if ($config['api']['enabled']) {
$api = new Api();
$json = json_encode($api->translateThread($thread));
if (!$config['smart_build'] || $return || $mod) {
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
if (!isset($thread)) {
$thread = new Thread($post, $mod ? '?/' : $config['root'], $mod);
} else {
$thread->add(new Post($post, $mod ? '?/' : $config['root'], $mod));
}
}
// Check if any posts were found
if (!isset($thread))
error($config['error']['nonexistant']);
$hasnoko50 = $thread->postCount() >= $config['noko50_min'];
$antibot = $mod || $return ? false : create_antibot($board['uri'], $id);
$body = Element('thread.html', array(
'board' => $board,
'thread' => $thread,
'body' => $thread->build(),
'config' => $config,
'id' => $id,
'mod' => $mod,
'hasnoko50' => $hasnoko50,
'isnoko50' => false,
'antibot' => $antibot,
'boardlist' => createBoardlist($mod),
'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
));
// json api
if ($config['api']['enabled']) {
$api = new Api();
$json = json_encode($api->translateThread($thread));
$jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json';
file_write($jsonFilename, $json);
}
}
else {
$jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json';
file_write($jsonFilename, $json);
file_unlink($jsonFilename);
}
if ($return) {
if ($config['smart_build'] && !$return && !$mod) {
$noko50fn = $board['dir'] . $config['dir']['res'] . sprintf($config['file_page50'], $id);
file_unlink($noko50fn);
file_unlink($board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $id));
} else if ($return) {
return $body;
} else {
$noko50fn = $board['dir'] . $config['dir']['res'] . sprintf($config['file_page50'], $id);

View File

@ -7,9 +7,6 @@
*
* You can copy values from config.php (defaults) and paste them here.
*/
require_once "lib/htmlpurifier-4.6.0/library/HTMLPurifier.auto.php";
require_once "8chan-functions.php";
// Note - you may want to change some of these in secrets.php instead of here
// See the secrets.example.php file
$config['db']['server'] = 'localhost';
@ -129,6 +126,7 @@
$config['additional_javascript'][] = 'js/thread-watcher.js';
$config['additional_javascript'][] = 'js/ajax.js';
$config['additional_javascript'][] = 'js/quick-reply.js';
$config['additional_javascript'][] = 'js/quick-post-controls.js';
$config['additional_javascript'][] = 'js/show-own-posts.js';
$config['additional_javascript'][] = 'js/youtube.js';
$config['additional_javascript'][] = 'js/comment-toolbar.js';
@ -143,6 +141,7 @@
$config['additional_javascript'][] = 'js/auto-scroll.js';
$config['additional_javascript'][] = 'js/twemoji/twemoji.js';
$config['additional_javascript'][] = 'js/file-selector.js';
$config['additional_javascript'][] = 'js/board-directory.js';
// Oekaki (now depends on config.oekaki so can be in all scripts)
$config['additional_javascript'][] = 'js/jquery-ui.custom.min.js';
$config['additional_javascript'][] = 'js/wPaint/8ch.js';
@ -155,46 +154,63 @@
$config['stylesheets']['Dark'] = 'dark.css';
$config['stylesheets']['Photon'] = 'photon.css';
$config['stylesheets']['Redchanit'] = 'redchanit.css';
$config['stylesheets_board'] = true;
$config['markup'][] = array("/^[ |\t]*==(.+?)==[ |\t]*$/m", "<span class=\"heading\">\$1</span>");
$config['markup'][] = array("/\[spoiler\](.+?)\[\/spoiler\]/", "<span class=\"spoiler\">\$1</span>");
$config['markup'][] = array("/~~(.+?)~~/", "<s>\$1</s>");
$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'));
$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. &sect; 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 <a href="mailto:admin@8chan.co">admin@8chan.co</a>.';
$config['search']['enable'] = true;
$config['syslog'] = true;
$config['hour_max_threads'] = 10;
$config['filters'][] = array(
'condition' => array(
'custom' => function($post) {
global $config, $board;
if (!$config['hour_max_threads']) return false;
if ($post['op']) {
$query = prepare(sprintf('SELECT COUNT(*) AS `count` FROM ``posts_%s`` WHERE `thread` IS NULL AND FROM_UNIXTIME(`time`) > DATE_SUB(NOW(), INTERVAL 1 HOUR);', $board['uri']));
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
$query->execute() or error(db_error($query));
$r = $query->fetch(PDO::FETCH_ASSOC);
return ($r['count'] > $config['hour_max_threads']);
}
}
'custom' => 'max_posts_per_hour'
),
'action' => 'reject',
'message' => 'On this board, to prevent raids the number of threads that can be created per hour is limited. Please try again later, or post in an existing thread.'
);
$config['languages'] = array(
'ch' => "汉语",
'cz' => "Čeština",
'dk' => "Dansk",
'de' => "Deutsch",
'eo' => "Esperanto",
'en' => "English",
'es' => "Español",
'fi' => "Suomi",
'fr' => "Français",
'hu' => "Magyar",
'it' => "Italiano",
'jp' => "日本語",
'jbo' => "Lojban",
'lt' => "Lietuvių Kalba",
'lv' => "Latviešu Valoda",
'no' => "Norsk",
'nl' => "Nederlands Vlaams",
'pl' => "Polski",
'pt' => "Português",
'ru' => "Русский",
'sk' => "Slovenský Jazyk",
'tw' => "Taiwanese",
);
$config['gzip_static'] = false;
$config['hash_masked_ip'] = true;
$config['force_subject_op'] = false;
@ -212,9 +228,18 @@ $config['enable_antibot'] = false;
$config['spam']['unicode'] = false;
$config['twig_cache'] = false;
$config['report_captcha'] = true;
$config['page_404'] = 'page_404';
// Flavor and design.
$config['site_name'] = "&infin;chan";
$config['site_logo'] = "/static/logo_33.svg";
// 8chan specific mod pages
require '8chan-mod-pages.php';
require '8chan-mod-config.php';
// Load instance functions later on
require_once 'instance-functions.php';
// Load database credentials
require "secrets.php";

View File

@ -0,0 +1,24 @@
<?php
require_once("inc/8chan-functions.php");
require_once("inc/8chan-mod-pages.php");
require_once "lib/htmlpurifier-4.6.0/library/HTMLPurifier.auto.php";
function max_posts_per_hour($post) {
global $config, $board;
if (!$config['hour_max_threads']) return false;
if ($post['op']) {
$query = prepare(sprintf('SELECT COUNT(*) AS `count` FROM ``posts_%s`` WHERE `thread` IS NULL AND FROM_UNIXTIME(`time`) > DATE_SUB(NOW(), INTERVAL 1 HOUR);', $board['uri']));
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
$query->execute() or error(db_error($query));
$r = $query->fetch(PDO::FETCH_ASSOC);
return ($r['count'] > $config['hour_max_threads']);
}
}
function page_404() {
include('404.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) {
@ -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;
}

View File

@ -3333,10 +3333,14 @@ function mod_theme_configure($theme_name) {
$query->bindValue(':value', $_POST[$conf['name']]);
$query->execute() or error(db_error($query));
}
$query = prepare("INSERT INTO ``theme_settings`` VALUES(:theme, NULL, NULL)");
$query->bindValue(':theme', $theme_name);
$query->execute() or error(db_error($query));
// Clean cache
Cache::delete("themes");
Cache::delete("theme_settings_".$theme);
$result = true;
$message = false;
@ -3384,11 +3388,15 @@ function mod_theme_uninstall($theme_name) {
if (!hasPermission($config['mod']['themes']))
error($config['error']['noaccess']);
$query = prepare("DELETE FROM ``theme_settings`` WHERE `theme` = :theme");
$query->bindValue(':theme', $theme_name);
$query->execute() or error(db_error($query));
// Clean cache
Cache::delete("themes");
Cache::delete("theme_settings_".$theme);
header('Location: ?/themes', true, $config['redirect_http']);
}

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,11 +209,16 @@ if (isset($_POST['delete'])) {
}
}
elseif (isset($_POST['post'])) {
if (!isset($_POST['body'], $_POST['board']))
if (!isset($_POST['body'], $_POST['board'])) {
error($config['error']['bot']);
$post = array('board' => $_POST['board'], 'files' => array());
}
$post = array(
'board' => $_POST['board'],
'files' => array(),
'time' => time(), // Timezone independent UNIX timecode.
);
// Check if board exists
if (!openBoard($post['board']))
error($config['error']['noboard']);
@ -228,17 +233,13 @@ elseif (isset($_POST['post'])) {
$_POST['subject'] = '';
if (!isset($_POST['password']))
$_POST['password'] = '';
$_POST['password'] = '';
if (isset($_POST['thread'])) {
$post['op'] = false;
$post['thread'] = round($_POST['thread']);
} else
$post['op'] = true;
// Check if board exists
if (!openBoard($post['board']))
error($config['error']['noboard']);
// Check if banned
checkBan($board['uri']);
@ -642,7 +643,8 @@ elseif (isset($_POST['post'])) {
if (mysql_version() >= 50503) {
$post['body_nomarkup'] = $post['body']; // Assume we're using the utf8mb4 charset
} else {
}
else {
// MySQL's `utf8` charset only supports up to 3-byte symbols
// Remove anything >= 0x010000
@ -712,7 +714,7 @@ elseif (isset($_POST['post'])) {
do_filters($post);
}
if ($post['has_file']) {
if ($post['has_file']) {
foreach ($post['files'] as $key => &$file) {
if ($file['is_an_image'] && $config['ie_mime_type_detection'] !== false) {
// Check IE MIME type detection XSS exploit
@ -910,10 +912,15 @@ elseif (isset($_POST['post'])) {
$post['files'] = $post['files'];
$post['num_files'] = sizeof($post['files']);
// Commit the post to the database.
$post['id'] = $id = post($post);
insertFloodPost($post);
// Update statistics for this board.
updateStatisticsForPost( $post );
// Handle cyclical threads
if (!$post['op'] && isset($thread['cycle']) && $thread['cycle']) {
// Query is a bit weird due to "This version of MariaDB doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" (MariaDB Ver 15.1 Distrib 10.0.17-MariaDB, for Linux (x86_64))
@ -1009,17 +1016,20 @@ elseif (isset($_POST['post'])) {
event('post-after', $post);
buildIndex();
// We are already done, let's continue our heavy-lifting work in the background (if we run off FastCGI)
if (function_exists('fastcgi_finish_request'))
@fastcgi_finish_request();
if ($post['op'])
rebuildThemes('post-thread', $board['uri']);
else
rebuildThemes('post', $board['uri']);
} elseif (isset($_POST['appeal'])) {
// We are already done, let's continue our heavy-lifting work in the background (if we run off FastCGI)
if (function_exists('fastcgi_finish_request')) {
@fastcgi_finish_request();
}
if ($post['op']) {
rebuildThemes('post-thread', $board['uri']);
}
else {
rebuildThemes('post', $board['uri']);
}
}
elseif (isset($_POST['appeal'])) {
if (!isset($_POST['ban_id']))
error($config['error']['bot']);

200
smart_build.php Normal file
View File

@ -0,0 +1,200 @@
<?php
require_once("inc/functions.php");
if (!$config['smart_build']) {
die('You need to enable $config["smart_build"]');
}
$config['smart_build'] = false; // Let's disable it, so we can build the page for real
function after_open_board() { global $config;
$config['smart_build'] = false;
};
function sb_board($b, $page = 1) { global $config, $build_pages; $page = (int)$page;
if ($page < 1) return false;
if (!openBoard($b)) return false;
if ($page > $config['max_pages']) return false;
$config['try_smarter'] = true;
$build_pages = array($page);
buildIndex("skip");
return true;
}
function sb_api_board($b, $page = 0) { $page = (int)$page;
return sb_board($b, $page + 1);
}
function sb_thread($b, $thread, $slugcheck = false) { global $config; $thread = (int)$thread;
if ($thread < 1) return false;
if (!preg_match('/^'.$config['board_regex'].'$/u', $b)) return false;
if (Cache::get("thread_exists_".$b."_".$thread) == "no") return false;
$query = prepare(sprintf("SELECT MAX(`id`) AS `max` FROM ``posts_%s``", $b));
if (!$query->execute()) return false;
$s = $query->fetch(PDO::FETCH_ASSOC);
$max = $s['max'];
if ($thread > $max) return false;
$query = prepare(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL", $b));
$query->bindValue(':id', $thread);
if (!$query->execute() || !$query->fetch(PDO::FETCH_ASSOC) ) {
Cache::set("thread_exists_".$b."_".$thread, "no");
return false;
}
if ($slugcheck == 50) { // Should we really generate +50 page? Maybe there are not enough posts anyway
global $request;
$r = str_replace("+50", "", $request);
$r = substr($r, 1); // Cut the slash
if (file_exists($r)) return false;
}
if (!openBoard($b)) return false;
buildThread($thread);
return true;
}
function sb_thread_slugcheck50($b, $thread) {
return sb_thread($b, $thread, 50);
}
function sb_api($b) { global $config;
if (!openBoard($b)) return false;
$config['try_smarter'] = true;
$build_pages = array(-1);
buildIndex();
return true;
}
function sb_ukko() {
rebuildTheme("ukko", "post-thread");
return true;
}
function sb_catalog($b) {
if (!openBoard($b)) return false;
rebuildTheme("catalog", "post-thread", $b);
return true;
}
function sb_recent() {
rebuildTheme("recent", "post-thread");
return true;
}
function sb_sitemap() {
rebuildTheme("sitemap", "all");
return true;
}
$entrypoints = array();
$entrypoints['/%b/'] = 'sb_board';
$entrypoints['/%b/'.$config['file_index']] = 'sb_board';
$entrypoints['/%b/'.$config['file_page']] = 'sb_board';
$entrypoints['/%b/%d.json'] = 'sb_api_board';
if ($config['api']['enabled']) {
$entrypoints['/%b/threads.json'] = 'sb_api';
$entrypoints['/%b/catalog.json'] = 'sb_api';
}
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page']] = 'sb_thread';
$entrypoints['/%b/'.$config['dir']['res'].$config['file_page50']] = 'sb_thread_slugcheck50';
if ($config['api']['enabled']) {
$entrypoints['/%b/'.$config['dir']['res'].'%d.json'] = 'sb_thread';
}
$entrypoints['/*/'] = 'sb_ukko';
$entrypoints['/*/index.html'] = 'sb_ukko';
$entrypoints['/recent.html'] = 'sb_recent';
$entrypoints['/%b/catalog.html'] = 'sb_catalog';
$entrypoints['/sitemap.xml'] = 'sb_sitemap';
$reached = false;
$request = $_SERVER['REQUEST_URI'];
list($request) = explode('?', $request);
foreach ($entrypoints as $id => $fun) {
$id = '@^' . preg_quote($id, '@') . '$@u';
$id = str_replace('%b', '('.$config['board_regex'].')', $id);
$id = str_replace('%d', '([0-9]+)', $id);
$id = str_replace('%s', '[a-zA-Z0-9-]+', $id);
$matches = null;
if (preg_match ($id, $request, $matches)) {
array_shift($matches);
$reached = call_user_func_array($fun, $matches);
break;
}
}
function die_404() { global $config;
if (!$config['page_404']) {
header("HTTP/1.1 404 Not Found");
header("Status: 404 Not Found");
echo "<h1>404 Not Found</h1><p>Page doesn't exist<hr><address>vichan</address>";
}
elseif (is_callable($config['page_404'])) {
$config['page_404']();
}
else {
header("Location: ".$config['page_404']);
}
header("X-Accel-Expires: 120");
die();
}
if ($reached) {
if ($request[strlen($request)-1] == '/') {
$request .= 'index.html';
}
$request = '.'.$request;
if (!file_exists($request)) {
die_404();
}
header("HTTP/1.1 200 OK");
header("Status: 200 OK");
if (preg_match('/\.json$/', $request)) {
header("Content-Type", "application/json");
}
elseif (preg_match('/\.js$/', $request)) {
header("Content-Type", "text/javascript; charset=utf-8");
}
elseif (preg_match('/\.xml$/', $request)) {
header("Content-Type", "application/xml");
}
else {
header("Content-Type", "text/html; charset=utf-8");
}
header("Cache-Control: public, nocache, no-cache, max-age=0, must-revalidate");
header("Expires: Fri, 22 Feb 1991 06:00:00 GMT");
header("Last-Modified: ".date('r', filemtime($request)));
//if (isset ($_SERVER['HTTP_ACCEPT_ENCODING']) && preg_match('/gzip/', $_SERVER['HTTP_ACCEPT_ENCODING']) && file_exists($request.".gz")) {
// header("Content-Encoding: gzip");
// $file = fopen($request.".gz", 'r');
//}
//else {
$file = fopen($request, 'r');
//}
fpassthru($file);
fclose($file);
}
else {
die_404();
}

BIN
static/infinity-small.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -198,3 +198,34 @@ div.report {
.modlog tr:nth-child(even), .modlog th {
background-color: #282A2E;
}
.box {
background: #333333;
border-color: #555555;
color: #C5C8C6;
border-radius: 10px;
}
.box-title {
background: transparent;
color: #32DD72;
}
table thead th {
background: #333333;
border-color: #555555;
color: #C5C8C6;
border-radius: 4px;
}
table tbody tr:nth-of-type( even ) {
background-color: #333333;
}
table.board-list-table .board-uri .board-sfw {
color: #446655;
}
tbody.board-list-omitted td {
background: #333333;
border-color: #555555;
}

View File

@ -329,3 +329,31 @@ form table tr td div {
.desktop-style div.boardlist:not(.bottom) {
background-color: #DDDDDD;
}
.box {
background: #DDDDDD;
border-color: #CCCCCC;
color: #333333;
border-radius: 7px;
}
.box-title {
border-radius: 7px;
}
table thead th {
background: transparent;
border: none;
}
table tbody tr:nth-of-type( even ) {
background-color: #DDDDDD;
}
table.board-list-table .board-uri .board-sfw {
color: #333333;
}
tbody.board-list-omitted td {
background: transparent;
border: none;
}

View File

@ -57,4 +57,33 @@ p.intro a.email span.name {
a {
color: #8020FF;
}
.box {
background: #343C4E;
border: none;
color: #FFF;
}
.box-title {
background: #7F8CA8;
color: #0F0C5D;
}
table thead th {
background: #343C4E;
border: none;
color: #FFF;
}
table tbody tr:nth-of-type( even ) {
background-color: #343C4E;
}
table.board-list-table .board-uri .board-sfw {
color: #D00;
}
tbody.board-list-omitted td {
background: #343C4E;
border: none;
}

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;
}
@ -20,6 +86,10 @@ body {
width: 100%;
}
#post-form-inner .post-table tr {
background-color: transparent;
}
.post-table th, .post-table-options th {
width: 85px;
}
@ -297,15 +367,17 @@ p.intro a {
color: maroon;
}
div.delete {
float: right;
p.body-line,
div.post p {
display: block;
margin: 0;
line-height: 1.16em;
font-size: 13px;
min-height: 1.16em;
}
div.post.reply p {
margin: 0.3em 0 0 0;
}
div.post.reply div.body {
div.post div.body {
margin-left: 1.8em;
margin-top: 0.8em;
padding-right: 3em;
@ -332,7 +404,7 @@ div.post div.body {
div.post.reply {
background: #D6DAF0;
margin: 0.2em 4px;
padding: 0.2em 0.3em 0.5em 0.6em;
padding: 0.5em 0.3em 0.5em 0.6em;
border-width: 1px;
border-style: none solid solid none;
border-color: #B7C5D9;
@ -347,12 +419,14 @@ div.post.reply.has-file.body-not-empty {
div.post_modified {
margin-left: 1.8em;
}
div.post_modified div.content-status {
margin-top: 0.5em;
padding-bottom: 0em;
font-size: 72%;
}
div.post_modified div.content-status:first-child {
margin-top: 1.3em;
}
div.post_modified div.content-status:first-child {
margin-top: 1.3em;
@ -367,7 +441,7 @@ span.trip {
color: #228854;
}
span.quote {
.quote {
color: #789922;
}
@ -536,50 +610,10 @@ hr {
clear: left;
}
div.boardlist {
color: #89A;
font-size: 9pt;
margin-top: 3px;
}
div.boardlist.bottom {
margin-top: 20px;
}
div.boardlist a {
text-decoration: none;
}
div.report {
color: #333;
}
table.modlog {
margin: auto;
width: 100%;
}
table.modlog tr td {
text-align: left;
margin: 0;
padding: 4px 15px 0 0;
}
table.modlog tr th {
text-align: left;
padding: 4px 15px 5px 5px;
white-space: nowrap;
}
table.modlog tr th {
background: #98E;
}
td.minimal,th.minimal {
width: 1%;
white-space: nowrap;
}
div.top_notice {
text-align: center;
margin: 5px auto;
@ -603,21 +637,6 @@ div.blotter {
text-align: center;
}
table.mod.config-editor {
font-size: 9pt;
width: 100%;
}
table.mod.config-editor td {
text-align: left;
padding: 5px;
border-bottom: 1px solid #98e;
}
table.mod.config-editor input[type="text"] {
width: 98%;
}
.desktop-style div.boardlist:not(.bottom) {
position: fixed;
top: 0;
@ -1010,7 +1029,6 @@ span.pln {
color:grey;
}
@media screen and (min-width: 768px) {
p.intro {
clear: none;
@ -1021,8 +1039,169 @@ span.pln {
}
}
/* threadwatcher */
/* === SITE-WIDE ASSETS === */
#logo {
display: block;
width: 100%;
padding: 0;
margin: 0 0 0 0;
text-align: center;
}
#logo-link {
display: inline;
}
#logo-img {
display: inline-block;
height: 128px;
width: auto;
}
/* === GENERAL CLASSES === */
.loading {
background: none;
background-color: none;
background-image: url('/static/infinity.gif');
background-position: center center;
background-repeat: no-repeat;
min-height: 76px;
min-width: 128px;
}
.loading-small {
background: none;
background-color: none;
background-image: url('/static/infinity-small.gif');
background-position: center center;
background-repeat: no-repeat;
min-height: 24px;
min-width: 48px;
}
/* Text and accessibility */
.ltr {
direction: ltr;
}
.rtl {
direction: rtl;
font-family: Tahoma;
}
/* Responsive helpers */
.col {
box-sizing: border-box;
float: left;
}
.col-12 { width: 100%; }
.col-11 { width: 91.66666667%; }
.col-10 { width: 83.33333333%; }
.col-9 { width: 75%; }
.col-8 { width: 66.66666667%; }
.col-7 { width: 58.33333333%; }
.col-6 { width: 50%; }
.col-5 { width: 41.66666667%; }
.col-4 { width: 33.33333333%; }
.col-3 { width: 25%; }
.col-2 { width: 16.66666667%; }
.col-1 { width: 8.33333333%; }
.left-push {
float: left;
}
.right-push {
float: right;
}
/* Layout design */
.box {
background: #D6DAF0;
border: 1px solid #000333;
color: #000333;
margin: 0 0 12px 0;
}
.box-title {
background: #98E;
color: #000333;
font-size: 120%;
font-weight: bold;
padding: 4px 8px;
}
.box-content {
padding: 0 8px;
margin: 4px 0;
}
.clearfix {
display: block;
clear: both;
visibility: hidden;
overflow: hidden;
font-size: 0px;
line-height: 0px;
box-sizing: border-box;
border: none;
height: 0;
margin: 0;
padding: 0;
width: 100%;
zoom: 1;
}
/* === SPECIFIC PAGES & FEATURES === */
/* Board List */
div.boardlist {
margin-top: 3px;
color: #89A;
font-size: 9pt;
}
div.boardlist.bottom {
margin-top: 12px;
clear: both;
}
div.boardlist a {
text-decoration: none;
}
/* Threads */
/* Thread Footer */
#thread-interactions {
margin: 8px 0;
clear: both;
}
#thread-links {
float: left;
}
#thread-links > a {
padding-left: none;
padding-right: 10px;
}
#thread-quick-reply {
display: none;
position: absolute;
left: 50%;
right: 50%;
text-align: center;
width: 100px;
margin-left: -50px;
}
#thread_stats {
float: right;
}
#post-moderation-fields {
float: right;
text-align: right;
}
#delete-fields {
}
#report-fields {
}
/* threadwatcher */
#watchlist {
display: none;
max-height: 250px;
@ -1065,25 +1244,25 @@ div.mix {
}
/* Mona Font */
.aa {
font-family: Mona, "MS PGothic", " Pゴシック", sans-serif;
display: block!important;
font-size: 12pt;
}
.dx,.dy,.dz {
.dx,
.dy,
.dz {
width: 30px;
text-align: right;
display: inline-block;
}
/* Dice */
.dice-option table {
border: 1px dotted black;
margin: 0;
border-collapse: collapse;
}
.dice-option table td {
text-align: center;
border-left: 1px dotted black;
@ -1093,7 +1272,6 @@ div.mix {
}
/* Quick reply (why was most of this ever in the script?) */
#quick-reply {
position: fixed;
right: 5%;
@ -1259,3 +1437,181 @@ div.mix {
.dropzone .remove-btn:hover {
color: rgba(125, 125, 125, 1);
}
table.board-list-table {
display: table;
margin: -2px;
margin-bottom: 10px;
overflow: hidden;
table-layout: fixed;
}
table.board-list-table .board-meta {
padding-right: 4px;
width: 70px;
}
table.board-list-table .board-uri {
max-width: 196px;
}
table.board-list-table .board-title {
width: auto;
}
table.board-list-table .board-pph {
width: 55px;
padding: 4px;
}
table.board-list-table .board-max {
width: 90px;
padding: 4px;
}
table.board-list-table .board-unique {
width: 100px;
padding: 4px;
}
table.board-list-table .board-tags {
width: auto;
padding: 0 15px 0 4px;
}
table.board-list-table .board-uri .board-nsfw {
color: rgb(230,0,0);
margin: 0 0 0 0.6em;
float: right;
}
table.board-list-table .board-uri .board-sfw {
/* I'm using blue instead of green to help users with Deuteranopia (most common form of colorblndness). */
color: rgb(0,0,230);
margin: 0 0 0 0.6em;
float: right;
}
table.board-list-table div.board-cell {
max-width: 100%;
overflow: hidden;
}
tbody.board-list-loading {
display: none;
}
tbody.board-list-loading .loading {
height: 80px;
}
tbody.board-list-omitted td {
background: #98E;
border-top: 1px solid #000333;
padding: 8px;
font-size: 125%;
text-align: center;
}
tbody.board-list-omitted #board-list-more {
cursor: default;
}
tbody.board-list-omitted #board-list-more.board-list-hasmore {
cursor: pointer;
}
tbody.board-list-omitted .board-page-loadmore {
display: none;
}
tbody.board-list-omitted .board-list-hasmore .board-page-loadmore {
display: inline;
}
aside.search-container {
margin-bottom: 12px;
}
aside.search-container .box {
margin-right: 12px;
}
.board-search {
margin: 8px 0;
}
.search-item {
margin: 8px 0;
}
.search-sfw {
display: block;
cursor: pointer;
font-size: 110%;
line-height: 120%;
}
#search-sfw-input {
margin: 0;
padding: 0;
transform: scale(1.20);
}
#search-lang-input,
#search-title-input,
#search-tag-input {
box-sizing: border-box;
font-size: 110%;
line-height: 120%;
vertical-align: top;
padding: 2px 0 2px 4px;
max-width: 100%;
min-width: 100%;
width: 100%:
}
#search-loading {
display: inline-block;
vertical-align: bottom;
}
ul.tag-list {
display: block;
list-style: none;
margin: 8px 8px -9px 8px;
padding: 8px 0 0 0;
border-top: 1px solid #000333;
}
ul.tag-list::after {
content: ' ';
display: block;
clear: both;
}
li.tag-item {
display: inline-block;
float: left;
font-size: 100%;
list-style: none;
margin: 0;
padding: 0 4px 0 0;
}
li.tag-item:last-child {
padding-bottom: 17px;
}
a.tag-link {
overflow: hidden;
white-space: nowrap;
}
li.tag-item a.tag-link {
}
td.board-tags a.tag-link {
display: inline-block;
margin: 0 0.4em 0 0;
}
@media screen and (max-width: 1100px) {
aside.search-container {
width: 100%;
margin-bottom: 12px;
}
aside.search-container .box {
margin-right: 0;
}
section.board-list {
margin-top: 12px;
width: 100%;
}
table.board-list-table .board-meta,
table.board-list-table .board-pph,
table.board-list-table .board-tags {
padding: 0;
margin: 0;
font-size: 0;
width: 0;
}
}

View File

@ -156,3 +156,32 @@ div#watchlist a:hover,a.watchThread:hover {
.modlog tr:nth-child(even), .modlog th {
background-color: #282A2E;
}
.box {
background: #282a2e;
border-color: #111;
color: #C5C8C6;
}
.box-title {
background: #282a2e;
color: #C5C8C6;
}
table thead th {
background: #282a2e;
border-color: #111;
color: #C5C8C6;
}
table tbody tr:nth-of-type( even ) {
background-color: #282a2e;
}
table.board-list-table .board-uri .board-sfw {
color: #C5C8C6;
}
tbody.board-list-omitted td {
background: #282a2e;
border-color: #111;
}

View File

@ -3,10 +3,10 @@ body {
color: #800000;
}
a:link, a:visited, p.intro a.email span.name {
color: #0000ff;
color: #800;
}
a:link:hover {
color: #d00;
color: #e00;
}
a.post_no {
color: #800000;
@ -66,3 +66,32 @@ table.modlog tr th {
.desktop-style div.boardlist:nth-child(1) {
background-color: #F0E0D6;
}
.box {
background: #fff;
border-color: #800;
color: #000;
}
.box-title {
background: #fca;
color: #800;
}
table thead th {
border-color: #800;
background: #fca;
color: #800;
}
table tbody tr:nth-of-type( even ) {
background-color: #ede2d4;
}
table.board-list-table .board-uri .board-sfw {
color: #800;
}
tbody.board-list-omitted td {
background: #fca;
border-color: #800;
}

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 %}
{% 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>
{% 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 %}

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>
<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>
<table class="modlog" style="width:auto"><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>
<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>
<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>
<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 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

@ -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 @@
{% if config.allow_delete %}
<div class="delete">
{% 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>
<div id="post-moderation-fields">
{% if config.allow_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">
<label for="reason">{% trans %}Reason{% endtrans %}</label>
</div>
{% endif %}
<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>
</div>

View File

@ -20,12 +20,25 @@
if ($action == 'all') {
foreach ($boards as $board) {
$b = new Catalog();
$b->build($settings, $board);
if ($config['smart_build']) {
file_unlink($config['dir']['home'] . $board . '/catalog.html');
}
else {
$b->build($settings, $board);
}
if (php_sapi_name() === "cli") echo "Rebuilding $board catalog...\n";
}
} elseif ($action == 'post-thread' || ($settings['update_on_posts'] && $action == 'post') || ($settings['update_on_posts'] && $action == 'post-delete') && (in_array($board, $boards) | $settings['all'])) {
$b = new Catalog();
$b->build($settings, $board);
if ($config['smart_build']) {
file_unlink($config['dir']['home'] . $board . '/catalog.html');
}
else {
$b->build($settings, $board);
}
}
}
@ -33,8 +46,12 @@
class Catalog {
public function build($settings, $board_name) {
global $config, $board;
openBoard($board_name);
if ($board['uri'] != $board_name) {
if (!openBoard($board_name)) {
error(sprintf(_("Board %s doesn't exist"), $board_name));
}
}
$recent_images = array();
$recent_posts = array();

View File

@ -24,8 +24,14 @@
$this->excluded = explode(' ', $settings['exclude']);
if ($action == 'all' || $action == 'post' || $action == 'post-thread' || $action == 'post-delete')
file_write($config['dir']['home'] . $settings['html'], $this->homepage($settings));
if ($action == 'all' || $action == 'post' || $action == 'post-thread' || $action == 'post-delete') {
if ($config['smart_build']) {
file_unlink($config['dir']['home'] . $settings['html']);
}
else {
file_write($config['dir']['home'] . $settings['html'], $this->homepage($settings));
}
}
}
// Build news page

View File

@ -10,30 +10,37 @@
// - boards (board list changed)
// - post (a post has been made)
// - thread (a thread has been made)
if ($action != 'all') {
if ($action != 'post-thread' && $action != 'post-delete')
return;
if ($action != 'post-thread' && $action != 'post-delete')
return;
if (isset($settings['regen_time']) && $settings['regen_time'] > 0) {
if ($last_gen = @filemtime($settings['path'])) {
if (time() - $last_gen < (int)$settings['regen_time'])
return; // Too soon
if (isset($settings['regen_time']) && $settings['regen_time'] > 0) {
if ($last_gen = @filemtime($settings['path'])) {
if (time() - $last_gen < (int)$settings['regen_time'])
return; // Too soon
}
}
}
$boards = explode(' ', $settings['boards']);
$threads = array();
foreach ($boards as $board) {
$query = query(sprintf("SELECT `id` AS `thread_id`, (SELECT `time` FROM ``posts_%s`` WHERE `thread` = `thread_id` OR `id` = `thread_id` ORDER BY `time` DESC LIMIT 1) AS `lastmod` FROM ``posts_%s`` WHERE `thread` IS NULL", $board, $board)) or error(db_error());
$threads[$board] = $query->fetchAll(PDO::FETCH_ASSOC);
if ($config['smart_build']) {
file_unlink($settings['path']);
}
else {
$boards = explode(' ', $settings['boards']);
$threads = array();
foreach ($boards as $board) {
$query = query(sprintf("SELECT `id` AS `thread_id`, (SELECT `time` FROM ``posts_%s`` WHERE `thread` = `thread_id` OR `id` = `thread_id` ORDER BY `time` DESC LIMIT 1) AS `lastmod` FROM ``posts_%s`` WHERE `thread` IS NULL", $board, $board)) or error(db_error());
$threads[$board] = $query->fetchAll(PDO::FETCH_ASSOC);
}
file_write($settings['path'], Element('themes/sitemap/sitemap.xml', Array(
'settings' => $settings,
'config' => $config,
'threads' => $threads,
'boards' => $boards,
)));
file_write($settings['path'], Element('themes/sitemap/sitemap.xml', Array(
'settings' => $settings,
'config' => $config,
'threads' => $threads,
'boards' => $boards,
)));
}
}

View File

@ -49,6 +49,7 @@
function ukko_install($settings) {
if (!file_exists($settings['uri']))
@mkdir($settings['uri'], 0777) or error("Couldn't create " . $settings['uri'] . ". Check permissions.", true);
file_write($settings['uri'] . '/ukko.js', Element('themes/ukko/ukko.js', array()));
}
}

View File

@ -2,6 +2,8 @@
require 'info.php';
function ukko_build($action, $settings) {
global $config;
$ukko = new ukko();
$ukko->settings = $settings;
@ -9,8 +11,12 @@
return;
}
file_write($settings['uri'] . '/index.html', $ukko->build());
file_write($settings['uri'] . '/ukko.js', Element('themes/ukko/ukko.js', array()));
if ($config['smart_build']) {
file_unlink($settings['uri'] . '/index.html');
}
else {
file_write($settings['uri'] . '/index.html', $ukko->build());
}
}
class ukko {

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' %}
<input type="hidden" name="board" value="{{ board.uri }}" />
{% if mod %}<input type="hidden" name="mod" value="1" />{% endif %}
{{ body }}
<div id="thread-interactions">
<span id="thread-links">
<a id="thread-return" href="{{ return }}">[{% trans %}Return{% 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>
<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>
</span>
{{ boardlist.bottom }}
{% if board.uri not in config.banned_ad_boards %}

0
tmp/cache/.gitkeep vendored Normal file
View File

0
tmp/locks/.gitkeep Normal file
View File

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! ^^;";