diff --git a/board-search.php b/board-search.php new file mode 100644 index 00000000..a02c89bf --- /dev/null +++ b/board-search.php @@ -0,0 +1,308 @@ + false, + 'nsfw' => true, + 'page' => 0, + 'tags' => false, + 'time' => ( (int)( time() / 3600 ) * 3600 ) - 3600, + 'title' => false, + + 'index' => count( $_GET ) == 0, +); + +// Include NSFW boards? +if (isset( $_GET['sfw'] ) && $_GET['sfw'] != "") { + $search['nsfw'] = !$_GET['sfw']; +} + +// Bringing up more results +if (isset( $_GET['page'] ) && $_GET['page'] != "") { + $search['page'] = (int) $_GET['page']; + + if ($search['page'] < 0) { + $search['page'] = 0; + } +} + +// Include what language (if the language is not blank and we recognize it)? +if (isset( $_GET['lang'] ) && $_GET['lang'] != "" && isset($config['languages'][$_GET['lang']])) { + $search['lang'] = $_GET['lang']; +} + +// Include what tag? +if (isset( $_GET['tags'] ) && $_GET['tags'] != "") { + $search['tags'] = explode( " ", $_GET['tags'] ); + $search['tags'] = array_splice( $search['tags'], 0, 5 ); +} + +// What time range? +if (isset( $_GET['time'] ) && is_numeric( $_GET['time'] ) ) { + $search['time'] = ( (int)( $_GET['time'] / 3600 ) * 3600 ); +} + +// Include what in the uri / title / subtitle? +if (isset( $_GET['title'] ) && $_GET['title'] != "") { + $search['title'] = $_GET['title']; +} + +/* Search boards */ +$boards = listBoards(); +$response['boards'] = array(); + +// Loop through our available boards and filter out inapplicable ones based on standard filtering. +foreach ($boards as $board) { + // Checks we can do without looking at config. + if ( + // Indexed, or we are staff, + ( $CanViewUnindexed !== true && !$board['indexed'] ) + // Not filtering NSFW, or board is SFW. + || ( $search['nsfw'] !== true && $board['sfw'] != 1 ) + ) { + continue; + } + + // Are we searching by title? + if ($search['title'] !== false) { + // This checks each component of the board's identity against our search terms. + // The weight determines order. + // "left" would match /leftypol/ and /nkvd/ which has /leftypol/ in the title. + // /leftypol/ would always appear above it but it would match both. + if (strpos("/{$board['uri']}/", $search['title']) !== false) { + $board['weight'] = 30; + } + else if (strpos($board['title'], $search['title']) !== false) { + $board['weight'] = 20; + } + else if (strpos($board['subtitle'], $search['title']) !== false) { + $board['weight'] = 10; + } + else { + continue; + } + + unset( $boardTitleString ); + } + else { + $board['weight'] = 0; + } + + // Load board config. + $boardConfig = loadBoardConfig( $board['uri'] ); + + // Determine language/locale and tags. + $boardLang = strtolower( array_slice( explode( "_", $boardConfig['locale'] ?: "" ), 0 )[0] ); // en_US -> en OR en -> en + + // Check against our config search options. + if ($search['lang'] !== false && $search['lang'] != $boardLang) { + continue; + } + + if (isset($config['languages'][$boardLang])) { + $board['locale'] = $config['languages'][$boardLang]; + } + else { + $board['locale'] = $boardLang; + } + + $response['boards'][ $board['uri'] ] = $board; +} + +unset( $boards ); + + +/* Tag Fetching */ +// (We have do this even if we're not filtering by tags so that we know what each board's tags are) + +// Fetch all board tags for our boards. +$boardTags = fetchBoardTags( array_keys( $response['boards'] ) ); + +// Loop through each board and determine if there are tag matches. +foreach ($response['boards'] as $boardUri => &$board) { + // If we are filtering by tag and there is no match, remove from the response. + if ( $search['tags'] !== false && ( !isset( $boardTags[ $boardUri ] ) || count(array_intersect($search['tags'], $boardTags[ $boardUri ])) !== count($search['tags']) ) ) { + unset( $response['boards'][$boardUri] ); + } + // If we aren't filtering / there is a match AND we have tags, set the tags. + else if ( isset( $boardTags[ $boardUri ] ) && $boardTags[ $boardUri ] ) { + $board['tags'] = $boardTags[ $boardUri ]; + } + // Othrwise, just declare our tag array blank. + else { + $board['tags'] = array(); + } +} + +unset( $boardTags ); + + +/* Activity Fetching */ +$boardActivity = fetchBoardActivity( array_keys( $response['boards'] ), $search['time'], true ); + +// Loop through each board and record activity to it. +// We will also be weighing and building a tag list. +foreach ($response['boards'] as $boardUri => &$board) { + $board['active'] = 0; + $board['pph'] = 0; + + if (isset($boardActivity['active'][ $boardUri ])) { + $board['active'] = (int) $boardActivity['active'][ $boardUri ]; + } + if (isset($boardActivity['average'][ $boardUri ])) { + $precision = 4 - strlen( $boardActivity['average'][ $boardUri ] ); + + if( $precision < 0 ) { + $precision = 0; + } + + $board['pph'] = round( $boardActivity['average'][ $boardUri ], 2 ); + + unset( $precision ); + } +} + +// Sort boards by their popularity, then by their total posts. +$boardActivityValues = array(); +$boardTotalPostsValues = array(); +$boardWeightValues = array(); + +foreach ($response['boards'] as $boardUri => &$board) { + $boardActivityValues[$boardUri] = (int) $board['active']; + $boardTotalPostsValues[$boardUri] = (int) $board['posts_total']; + $boardWeightValues[$boardUri] = (int) $board['weight']; +} + +array_multisort( + $boardWeightValues, SORT_DESC, SORT_NUMERIC, // Sort by weight + $boardActivityValues, SORT_DESC, SORT_NUMERIC, // Sort by number of active posters + $boardTotalPostsValues, SORT_DESC, SORT_NUMERIC, // Then, sort by total number of posts + $response['boards'] +); + +$boardLimit = $search['index'] ? 50 : 100; + +$response['omitted'] = count( $response['boards'] ) - $boardLimit; +$response['omitted'] = $response['omitted'] < 0 ? 0 : $response['omitted']; +$response['boards'] = array_splice( $response['boards'], $search['page'], $boardLimit ); +$response['order'] = array_keys( $response['boards'] ); + + +// Loop through the truncated array to compile tags. +$response['tags'] = array(); +$tagUsage = array( 'boards' => array(), 'users' => array() ); + +foreach ($response['boards'] as $boardUri => &$board) { + if (isset($board['tags']) && count($board['tags']) > 0) { + foreach ($board['tags'] as $tag) { + if (!isset($tagUsage['boards'][$tag])) { + $tagUsage['boards'][$tag] = 0; + } + if (!isset($tagUsage['users'][$tag])) { + $tagUsage['users'][$tag] = 0; + } + + $response['tags'][$tag] = true; + ++$tagUsage['boards'][$tag]; + $tagUsage['users'][$tag] += $board['active']; + } + } +} + +// Get the top most popular tags. +if (count($response['tags']) > 0) { + arsort( $tagUsage['boards'] ); + arsort( $tagUsage['users'] ); + + array_multisort( + $tagUsage['boards'], SORT_DESC, SORT_NUMERIC, + $tagUsage['users'], SORT_DESC, SORT_NUMERIC, + $response['tags'] + ); + + // Get the first n most active tags. + $response['tags'] = array_splice( $response['tags'], 0, 100 ); + $response['tagOrder'] = array_keys( $response['tags'] ); + $response['tagWeight'] = array(); + + $tagsMostUsers = max( $tagUsage['users'] ); + $tagsLeastUsers = min( $tagUsage['users'] ); + $tagsAvgUsers = array_sum( $tagUsage['users'] ) / count( $tagUsage['users'] ); + + $weightDepartureFurthest = 0; + + foreach ($tagUsage['users'] as $tagUsers) { + $weightDeparture = abs( $tagUsers - $tagsAvgUsers ); + + if( $weightDeparture > $weightDepartureFurthest ) { + $weightDepartureFurthest = $weightDeparture; + } + } + + foreach ($tagUsage['users'] as $tagName => $tagUsers) { + if ($weightDepartureFurthest != 0) { + $weightDeparture = abs( $tagUsers - $tagsAvgUsers ); + $response['tagWeight'][$tagName] = 75 + round( 100 * ( $weightDeparture / $weightDepartureFurthest ), 0); + } + else { + $response['tagWeight'][$tagName] = 100; + } + } +} + +/* Include our interpreted search terms. */ +$response['search'] = $search; + +/* (Please) Respond */ +if (!$Included) { + $json = json_encode( $response ); + + // Error Handling + switch (json_last_error()) { + case JSON_ERROR_NONE: + $jsonError = false; + break; + case JSON_ERROR_DEPTH: + $jsonError = 'Maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $jsonError = 'Underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR: + $jsonError = 'Unexpected control character found'; + break; + case JSON_ERROR_SYNTAX: + $jsonError = 'Syntax error, malformed JSON'; + break; + case JSON_ERROR_UTF8: + $jsonError = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $jsonError = 'Unknown error'; + break; + } + + if ($jsonError) { + $json = "{\"error\":\"{$jsonError}\"}"; + } + + // Successful output + echo $json; +} +else { + return $response; +} \ No newline at end of file diff --git a/boards.php b/boards.php index 87ba7ddf..0acdfaf5 100644 --- a/boards.php +++ b/boards.php @@ -1,152 +1,96 @@ $board) { - $query = prepare(sprintf(" -SELECT IFNULL(MAX(id),0) max, -(SELECT COUNT(*) FROM ``posts_%s`` WHERE FROM_UNIXTIME(time) > DATE_SUB(NOW(), INTERVAL 1 HOUR)) pph, -(SELECT COUNT(DISTINCT ip) FROM ``posts_%s`` WHERE FROM_UNIXTIME(time) > DATE_SUB(NOW(), INTERVAL 3 DAY)) uniq_ip - FROM ``posts_%s`` -", $board['uri'], $board['uri'], $board['uri'], $board['uri'], $board['uri'])); - $query->execute() or error(db_error($query)); - $r = $query->fetch(PDO::FETCH_ASSOC); - - $tquery = prepare("SELECT `tag` FROM ``board_tags`` WHERE `uri` = :uri"); - $tquery->execute([":uri" => $board['uri']]) or error(db_error($tquery)); - $r2 = $tquery->fetchAll(PDO::FETCH_ASSOC); - - $tags = array(); - if ($r2) { - foreach ($r2 as $ii => $t) { - $tag=to_tag($t['tag']); - $tags[] = $tag; - if (!isset($all_tags[$tag])) { - $all_tags[$tag] = (int)$r['uniq_ip']; - } else { - $all_tags[$tag] += $r['uniq_ip']; - } - } +if (count($searchJson)) { + if (isset($searchJson['boards'])) { + $boards = $searchJson['boards']; } - - $pph = $r['pph']; - - $total_posts_hour += $pph; - $total_posts += $r['max']; - - $boards[$i]['pph'] = $pph; - $boards[$i]['ppd'] = $pph*24; - $boards[$i]['max'] = $r['max']; - $boards[$i]['uniq_ip'] = $r['uniq_ip']; - $boards[$i]['tags'] = $tags; - - if ($write_maxes) fwrite($fp, $board['uri'] . ':' . $boards[$i]['max'] . "\n"); -} -if ($write_maxes) fclose($fp); - -usort($boards, -function ($a, $b) { - $x = $b['uniq_ip'] - $a['uniq_ip']; - if ($x) { return $x; - //} else { return strcmp($a['uri'], $b['uri']); } - } else { return $b['max'] - $a['max']; } -}); - -$hidden_boards_total = 0; -$rows = array(); -foreach ($boards as $i => &$board) { - $board_config = @file_get_contents($board['uri'].'/config.php'); - $boardCONFIG = array(); - if ($board_config && $board['uri'] !== 'int') { - $board_config = str_replace('$config', '$boardCONFIG', $board_config); - $board_config = str_replace('"; - - if ($showboard || $admin) { - if (!$showboard) { - $lock = ' '; - } else { - $lock = ''; - } - $board['ago'] = human_time_diff(strtotime($board['time'])); - } else { - unset($boards[$i]); - $hidden_boards_total += 1; + if (isset($searchJson['tagWeight'])) { + $tags = $searchJson['tagWeight']; } } -$n_boards = sizeof($boards); -$t_boards = $hidden_boards_total + $n_boards; +$boardQuery = prepare("SELECT COUNT(1) AS 'boards_total', SUM(indexed) AS 'boards_public', SUM(posts_total) AS 'posts_total' FROM ``boards``"); +$boardQuery->execute() or error(db_error($tagQuery)); +$boardResult = $boardQuery->fetchAll(PDO::FETCH_ASSOC)[0]; -$boards = array_values($boards); -arsort($all_tags); +$boards_total = number_format( $boardResult['boards_total'], 0 ); +$boards_public = number_format( $boardResult['boards_public'], 0 ); +$boards_hidden = number_format( $boardResult['boards_total'] - $boardResult['boards_public'], 0 ); +$boards_omitted = (int) $searchJson['omitted']; -$config['additional_javascript'] = array('js/jquery.min.js', 'js/jquery.tablesorter.min.js'); -$body = Element("8chan/boards-tags.html", array("config" => $config, "n_boards" => $n_boards, "t_boards" => $t_boards, "hidden_boards_total" => $hidden_boards_total, "total_posts" => $total_posts, "total_posts_hour" => $total_posts_hour, "boards" => $boards, "last_update" => date('r'), "uptime_p" => shell_exec('uptime -p'), 'tags' => $all_tags, 'top2k' => false)); +$posts_hour = number_format( fetchBoardActivity(), 0 ); +$posts_total = number_format( $boardResult['posts_total'], 0 ); -$html = Element("page.html", array("config" => $config, "body" => $body, "title" => "Boards on ∞chan")); -$boards_top2k = $boards; -array_splice($boards_top2k, 2000); -$boards_top2k = array_values($boards_top2k); -$body = Element("8chan/boards-tags.html", array("config" => $config, "n_boards" => $n_boards, "t_boards" => $t_boards, "hidden_boards_total" => $hidden_boards_total, "total_posts" => $total_posts, "total_posts_hour" => $total_posts_hour, "boards" => $boards_top2k, "last_update" => date('r'), "uptime_p" => shell_exec('uptime -p'), 'tags' => $all_tags, 'top2k' => true)); -$html_top2k = Element("page.html", array("config" => $config, "body" => $body, "title" => "Boards on ∞chan")); +// This incredibly stupid looking chunk of code builds a query string using existing information. +// It's used to make clickable tags for users without JavaScript for graceful degredation. +// Because of how it orders tags, what you end up with is a prefix that always ends in tags=x+ +// ?tags= or ?sfw=1&tags= or ?title=foo&tags=bar+ - etc +$tagQueryGet = $_GET; +$tagQueryTags = isset($tagQueryGet['tags']) ? $tagQueryGet['tags'] : ""; +unset($tagQueryGet['tags']); +$tagQueryGet['tags'] = $tagQueryTags; +$tag_query = "?" . http_build_query( $tagQueryGet ) . ($tagQueryTags != "" ? "+" : ""); -if ($admin) { - echo $html; -} else { - foreach ($boards as $i => &$b) { unset($b['img']); } - file_write("boards.json", json_encode($boards)); - file_write("tags.json", json_encode($all_tags)); - foreach ($boards as $i => $b) { - if (in_array($b['uri'], $config['no_top_bar_boards'])) { - unset($boards[$i]); - } - unset($boards[$i]['img']); - } +/* Create and distribute page */ +$boardsHTML = Element("8chan/boards-table.html", array( + "config" => $config, + "boards" => $boards, + "tag_query" => $tag_query, + + ) +); - array_splice($boards, 48); +$tagsHTML = Element("8chan/boards-tags.html", array( + "config" => $config, + "tags" => $tags, + "tag_query" => $tag_query, + + ) +); - $boards = array_values($boards); +$searchHTML = Element("8chan/boards-search.html", array( + "config" => $config, + "boards" => $boards, + "tags" => $tags, + "search" => $searchJson['search'], + "languages" => $config['languages'], + + "boards_total" => $boards_total, + "boards_public" => $boards_public, + "boards_hidden" => $boards_hidden, + "boards_omitted" => $boards_omitted, + + "posts_hour" => $posts_hour, + "posts_total" => $posts_total, + + "founding_date" => $founding_date, + "page_updated" => date('r'), + "uptime" => shell_exec('uptime -p'), + + "html_boards" => $boardsHTML, + "html_tags" => $tagsHTML + ) +); - file_write("boards-top20.json", json_encode($boards)); - file_write("boards.html", $html_top2k); - file_write("boards_full.html", $html); - echo 'Done'; -} +$pageHTML = Element("page.html", array( + "config" => $config, + "body" => $searchHTML + ) +); +file_write("boards.html", $pageHTML); +echo $pageHTML; \ No newline at end of file diff --git a/inc/display.php b/inc/display.php index d595e5d0..3cbeb818 100644 --- a/inc/display.php +++ b/inc/display.php @@ -348,6 +348,8 @@ function embed_html($link) { class Post { + public $clean; + public function __construct($post, $root=null, $mod=false) { global $config; if (!isset($root)) diff --git a/inc/functions.php b/inc/functions.php index 480df77f..8a279b41 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -516,9 +516,9 @@ function setupBoard($array) { $board = array( 'uri' => $array['uri'], 'title' => $array['title'], - 'subtitle' => $array['subtitle'], - 'indexed' => $array['indexed'], - 'public_logs' => $array['public_logs'] + 'subtitle' => isset($array['subtitle']) ? $array['subtitle'] : "", + 'indexed' => isset($array['indexed']) ? $array['indexed'] : true, + 'public_logs' => isset($array['public_logs']) ? $array['public_logs'] : true, ); // older versions @@ -628,41 +628,47 @@ function purge($uri) { function file_write($path, $data, $simple = false, $skip_purge = false) { global $config, $debug; - + if (preg_match('/^remote:\/\/(.+)\:(.+)$/', $path, $m)) { if (isset($config['remote'][$m[1]])) { require_once 'inc/remote.php'; - + $remote = new Remote($config['remote'][$m[1]]); $remote->write($data, $m[2]); return; - } else { + } + else { error('Invalid remote server: ' . $m[1]); } } - - if (!$fp = dio_open($path, O_WRONLY | O_CREAT, 0644)) + else { + // This will convert a local, relative path like "b/index.html" to a full path. + // dio_open does not work with relative paths on Windows machines. + $path = realpath(dirname($path)) . DIRECTORY_SEPARATOR . basename($path); + } + + if (!$fp = dio_open( $path, O_WRONLY | O_CREAT | O_TRUNC, 0644)) { error('Unable to open file for writing: ' . $path); - + } + // File locking - if (dio_fcntl($fp, F_SETLKW, array('type' => F_WRLCK)) === -1) { + if (function_exists("dio_fcntl") && dio_fcntl($fp, F_SETLKW, array('type' => F_WRLCK)) === -1) { error('Unable to lock file: ' . $path); } - - // Truncate file - if (!dio_truncate($fp, 0)) - error('Unable to truncate file: ' . $path); - + // Write data - if (($bytes = dio_write($fp, $data)) === false) + if (($bytes = dio_write($fp, $data)) === false) { error('Unable to write to file: ' . $path); - + } + // Unlock - dio_fcntl($fp, F_SETLK, array('type' => F_UNLCK)); - + if (function_exists("dio_fcntl")) { + dio_fcntl($fp, F_SETLK, array('type' => F_UNLCK)); + } + // Close dio_close($fp); - + /** * Create gzipped file. * @@ -780,9 +786,23 @@ function listBoards($just_uri = false, $indexed_only = false) { return $boards; if (!$just_uri) { - $query = query("SELECT ``boards``.`uri` uri, ``boards``.`title` title, ``boards``.`subtitle` subtitle, ``board_create``.`time` time, ``boards``.`indexed` indexed, ``boards``.`sfw` sfw FROM ``boards``" . ( $indexed_only ? " WHERE `indexed` = 1 " : "" ) . "LEFT JOIN ``board_create`` ON ``boards``.`uri` = ``board_create``.`uri` ORDER BY ``boards``.`uri`") or error(db_error()); + $query = query( + "SELECT + ``boards``.`uri` uri, + ``boards``.`title` title, + ``boards``.`subtitle` subtitle, + ``board_create``.`time` time, + ``boards``.`indexed` indexed, + ``boards``.`sfw` sfw, + ``boards``.`posts_total` posts_total + FROM ``boards``" . ( $indexed_only ? " WHERE `indexed` = 1 " : "" ) . + "LEFT JOIN ``board_create`` + ON ``boards``.`uri` = ``board_create``.`uri` + ORDER BY ``boards``.`uri`") or error(db_error()); + $boards = $query->fetchAll(PDO::FETCH_ASSOC); - } else { + } + else { $boards = array(); $query = query("SELECT `uri` FROM ``boards``" . ( $indexed_only ? " WHERE `indexed` = 1" : "" ) . " ORDER BY ``boards``.`uri`") or error(db_error()); while (true) { @@ -798,6 +818,119 @@ function listBoards($just_uri = false, $indexed_only = false) { return $boards; } +function loadBoardConfig( $uri ) { + $config = array( + "locale" => "en_US", + ); + $configPath = "./{$uri}/config.php"; + + if (file_exists( $configPath ) && is_readable( $configPath )) { + include( $configPath ); + } + + // **DO NOT** use $config outside of this local scope. + // It's used by our global config array. + return $config; +} + +function fetchBoardActivity( array $uris = array(), $forTime = false, $detailed = false ) { + global $config; + + // Set our search time for now if we didn't pass one. + if (!is_integer($forTime)) { + $forTime = time(); + } + // Get the last hour for this timestamp. + $nowHour = ( (int)( time() / 3600 ) * 3600 ); + $forHour = ( (int)( $forTime / 3600 ) * 3600 ) - 3600; + + $boardActivity = array( + 'active' => array(), + 'average' => array(), + ); + + // Query for stats for these boards. + if (count($uris)) { + $uriSearch = "`stat_uri` IN (\"" . implode( (array) $uris, "\",\"" ) . "\") AND "; + } + else { + $uriSearch = ""; + } + + if ($detailed === true) { + $bsQuery = prepare("SELECT `stat_uri`, `stat_hour`, `post_count`, `author_ip_array` FROM ``board_stats`` WHERE {$uriSearch} ( `stat_hour` <= :hour AND `stat_hour` >= :hoursago )"); + $bsQuery->bindValue(':hour', $forHour, PDO::PARAM_INT); + $bsQuery->bindValue(':hoursago', $forHour - ( 3600 * 72 ), PDO::PARAM_INT); + $bsQuery->execute() or error(db_error($bsQuery)); + $bsResult = $bsQuery->fetchAll(PDO::FETCH_ASSOC); + + + // Format the results. + foreach ($bsResult as $bsRow) { + if (!isset($boardActivity['active'][$bsRow['stat_uri']])) { + if ($bsRow['stat_hour'] == $forHour) { + $boardActivity['active'][$bsRow['stat_uri']] = unserialize( $bsRow['author_ip_array'] ); + } + + $boardActivity['average'][$bsRow['stat_uri']] = $bsRow['post_count']; + } + else { + if ($bsRow['stat_hour'] == $forHour) { + $boardActivity['active'][$bsRow['stat_uri']] = array_merge( $boardActivity['active'][$bsRow['stat_uri']], unserialize( $bsRow['author_ip_array'] ) ); + } + + $boardActivity['average'][$bsRow['stat_uri']] = $bsRow['post_count']; + } + } + + foreach ($boardActivity['active'] as &$activity) { + $activity = count( array_unique( $activity ) ); + } + foreach ($boardActivity['average'] as &$activity) { + $activity /= 72; + } + } + // Simple return. + else { + $bsQuery = prepare("SELECT SUM(`post_count`) AS `post_count` FROM ``board_stats`` WHERE {$uriSearch} ( `stat_hour` = :hour )"); + $bsQuery->bindValue(':hour', $forHour, PDO::PARAM_INT); + $bsQuery->execute() or error(db_error($bsQuery)); + $bsResult = $bsQuery->fetchAll(PDO::FETCH_ASSOC); + + $boardActivity = $bsResult[0]['post_count']; + } + + return $boardActivity; +} + +function fetchBoardTags( $uris ) { + global $config; + + $boardTags = array(); + $uris = "\"" . implode( (array) $uris, "\",\"" ) . "\""; + + $tagQuery = prepare("SELECT * FROM ``board_tags`` WHERE `uri` IN ({$uris})"); + $tagQuery->execute() or error(db_error($tagQuery)); + $tagResult = $tagQuery->fetchAll(PDO::FETCH_ASSOC); + + if ($tagResult) { + foreach ($tagResult as $tagRow) { + $tag = $tagRow['tag']; + $tag = trim($tag); + $tag = strtolower($tag); + $tag = str_replace(['_', ' '], '-', $tag); + + if (!isset($boardTags[ $tagRow['uri'] ])) { + $boardTags[ $tagRow['uri'] ] = array(); + } + + $boardTags[ $tagRow['uri'] ][] = htmlentities( utf8_encode( $tag ) ); + } + } + + return $boardTags; +} + function until($timestamp) { $difference = $timestamp - time(); switch(TRUE){ @@ -1010,70 +1143,70 @@ function insertFloodPost(array $post) { function post(array $post) { global $pdo, $board; $query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, :cycle, 0, :embed, NULL)", $board['uri'])); - + // Basic stuff if (!empty($post['subject'])) { $query->bindValue(':subject', $post['subject']); } else { $query->bindValue(':subject', null, PDO::PARAM_NULL); } - + if (!empty($post['email'])) { $query->bindValue(':email', $post['email']); } else { $query->bindValue(':email', null, PDO::PARAM_NULL); } - + if (!empty($post['trip'])) { $query->bindValue(':trip', $post['trip']); } else { $query->bindValue(':trip', null, PDO::PARAM_NULL); } - + $query->bindValue(':name', $post['name']); $query->bindValue(':body', $post['body']); $query->bindValue(':body_nomarkup', $post['body_nomarkup']); $query->bindValue(':time', isset($post['time']) ? $post['time'] : time(), PDO::PARAM_INT); - $query->bindValue(':password', $post['password']); + $query->bindValue(':password', $post['password']); $query->bindValue(':ip', isset($post['ip']) ? $post['ip'] : $_SERVER['REMOTE_ADDR']); - + if ($post['op'] && $post['mod'] && isset($post['sticky']) && $post['sticky']) { $query->bindValue(':sticky', true, PDO::PARAM_INT); } else { $query->bindValue(':sticky', false, PDO::PARAM_INT); } - + if ($post['op'] && $post['mod'] && isset($post['locked']) && $post['locked']) { $query->bindValue(':locked', true, PDO::PARAM_INT); } else { $query->bindValue(':locked', false, PDO::PARAM_INT); } - + if ($post['op'] && $post['mod'] && isset($post['cycle']) && $post['cycle']) { $query->bindValue(':cycle', true, PDO::PARAM_INT); } else { $query->bindValue(':cycle', false, PDO::PARAM_INT); } - + if ($post['mod'] && isset($post['capcode']) && $post['capcode']) { $query->bindValue(':capcode', $post['capcode'], PDO::PARAM_INT); } else { $query->bindValue(':capcode', null, PDO::PARAM_NULL); } - + if (!empty($post['embed'])) { $query->bindValue(':embed', $post['embed']); } else { $query->bindValue(':embed', null, PDO::PARAM_NULL); } - + if ($post['op']) { // No parent thread, image $query->bindValue(':thread', null, PDO::PARAM_NULL); } else { $query->bindValue(':thread', $post['thread'], PDO::PARAM_INT); } - + if ($post['has_file']) { $query->bindValue(':files', json_encode($post['files'])); $query->bindValue(':num_files', $post['num_files']); @@ -1083,12 +1216,12 @@ function post(array $post) { $query->bindValue(':num_files', 0); $query->bindValue(':filehash', null, PDO::PARAM_NULL); } - + if (!$query->execute()) { undoImage($post); error(db_error($query)); } - + return $pdo->lastInsertId(); } @@ -1382,6 +1515,65 @@ function index($page, $mod=false) { ); } +// Handle statistic tracking for a new post. +function updateStatisticsForPost( $post, $new = true ) { + $postIp = isset($post['ip']) ? $post['ip'] : $_SERVER['REMOTE_ADDR']; + $postUri = $post['board']; + $postTime = (int)( $post['time'] / 3600 ) * 3600; + + $bsQuery = prepare("SELECT * FROM ``board_stats`` WHERE `stat_uri` = :uri AND `stat_hour` = :hour"); + $bsQuery->bindValue(':uri', $postUri); + $bsQuery->bindValue(':hour', $postTime, PDO::PARAM_INT); + $bsQuery->execute() or error(db_error($bsQuery)); + $bsResult = $bsQuery->fetchAll(PDO::FETCH_ASSOC); + + // Flesh out the new stats row. + $boardStats = array(); + + // If we already have a row, we're going to be adding this post to it. + if (count($bsResult)) { + $boardStats = $bsResult[0]; + $boardStats['stat_uri'] = $postUri; + $boardStats['stat_hour'] = $postTime; + $boardStats['post_id_array'] = unserialize( $boardStats['post_id_array'] ); + $boardStats['author_ip_array'] = unserialize( $boardStats['author_ip_array'] ); + + ++$boardStats['post_count']; + $boardStats['post_id_array'][] = (int) $post['id']; + $boardStats['author_ip_array'][] = less_ip( $postIp ); + $boardStats['author_ip_array'] = array_unique( $boardStats['author_ip_array'] ); + } + // If this a new row, we're building the stat to only reflect this first post. + else { + $boardStats['stat_uri'] = $postUri; + $boardStats['stat_hour'] = $postTime; + $boardStats['post_count'] = 1; + $boardStats['post_id_array'] = array( (int) $post['id'] ); + $boardStats['author_ip_count'] = 1; + $boardStats['author_ip_array'] = array( less_ip( $postIp ) ); + } + + // Cleanly serialize our array for insertion. + $boardStats['post_id_array'] = str_replace( "\"", "\\\"", serialize( $boardStats['post_id_array'] ) ); + $boardStats['author_ip_array'] = str_replace( "\"", "\\\"", serialize( $boardStats['author_ip_array'] ) ); + + + // Insert this data into our statistics table. + $statsInsert = "VALUES(\"{$boardStats['stat_uri']}\", \"{$boardStats['stat_hour']}\", \"{$boardStats['post_count']}\", \"{$boardStats['post_id_array']}\", \"{$boardStats['author_ip_count']}\", \"{$boardStats['author_ip_array']}\" )"; + + $postStatQuery = prepare( + "REPLACE INTO ``board_stats`` (stat_uri, stat_hour, post_count, post_id_array, author_ip_count, author_ip_array) {$statsInsert}" + ); + $postStatQuery->execute() or error(db_error($postStatQuery)); + + // Update the posts_total tracker on the board. + if ($new) { + query("UPDATE ``boards`` SET `posts_total`=`posts_total`+1 WHERE `uri`=\"{$postUri}\""); + } + + return $boardStats; +} + function getPageButtons($pages, $mod=false) { global $config, $board; @@ -2074,8 +2266,27 @@ function markup(&$body, $track_cites = false, $op = false) { if ($config['strip_superfluous_returns']) $body = preg_replace('/\s+$/', '', $body); - - $body = preg_replace("/\n/", '
', $body); + + if ($config['markup_paragraphs']) { + $paragraphs = explode("\n", $body); + $bodyNew = ""; + + foreach ($paragraphs as $paragraph) { + if (strlen(trim($paragraph)) > 0) { + $paragraphDirection = is_rtl($paragraph) ? "rtl" : "ltr"; + } + else { + $paragraphDirection = "empty"; + } + + $bodyNew .= "

" . $paragraph . "

"; + } + + $body = $bodyNew; + } + else { + $body = preg_replace("/\n/", '
', $body); + } if ($config['markup_repair_tidy']) { $tidy = new tidy(); @@ -2132,6 +2343,40 @@ function ordutf8($string, &$offset) { return $code; } +function uniord($u) { + $k = mb_convert_encoding($u, 'UCS-2LE', 'UTF-8'); + $k1 = ord(substr($k, 0, 1)); + $k2 = ord(substr($k, 1, 1)); + return $k2 * 256 + $k1; +} + +function is_rtl($str) { + if(mb_detect_encoding($str) !== 'UTF-8') { + $str = mb_convert_encoding($str, mb_detect_encoding($str),'UTF-8'); + } + + preg_match_all('/[^\n\s]+/', $str, $matches); + preg_match_all('/.|\n\s/u', $str, $matches); + $chars = $matches[0]; + $arabic_count = 0; + $latin_count = 0; + $total_count = 0; + + foreach ($chars as $char) { + $pos = uniord($char); + + if ($pos >= 1536 && $pos <= 1791) { + $arabic_count++; + } + else if ($pos > 123 && $pos < 123) { + $latin_count++; + } + $total_count++; + } + + return (($arabic_count/$total_count) > 0.5); +} + function strip_combining_chars($str) { $chars = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY); $str = ''; diff --git a/inc/instance-config.php b/inc/instance-config.php index 1701fc64..c6fb8cea 100644 --- a/inc/instance-config.php +++ b/inc/instance-config.php @@ -126,6 +126,7 @@ $config['additional_javascript'][] = 'js/thread-watcher.js'; $config['additional_javascript'][] = 'js/ajax.js'; $config['additional_javascript'][] = 'js/quick-reply.js'; + $config['additional_javascript'][] = 'js/quick-post-controls.js'; $config['additional_javascript'][] = 'js/show-own-posts.js'; $config['additional_javascript'][] = 'js/youtube.js'; $config['additional_javascript'][] = 'js/comment-toolbar.js'; @@ -140,6 +141,7 @@ $config['additional_javascript'][] = 'js/auto-scroll.js'; $config['additional_javascript'][] = 'js/twemoji/twemoji.js'; $config['additional_javascript'][] = 'js/file-selector.js'; + $config['additional_javascript'][] = 'js/board-directory.js'; // Oekaki (now depends on config.oekaki so can be in all scripts) $config['additional_javascript'][] = 'js/jquery-ui.custom.min.js'; $config['additional_javascript'][] = 'js/wPaint/8ch.js'; @@ -152,25 +154,28 @@ $config['stylesheets']['Dark'] = 'dark.css'; $config['stylesheets']['Photon'] = 'photon.css'; $config['stylesheets']['Redchanit'] = 'redchanit.css'; - + $config['stylesheets_board'] = true; $config['markup'][] = array("/^[ |\t]*==(.+?)==[ |\t]*$/m", "\$1"); $config['markup'][] = array("/\[spoiler\](.+?)\[\/spoiler\]/", "\$1"); $config['markup'][] = array("/~~(.+?)~~/", "\$1"); $config['markup'][] = array("/__(.+?)__/", "\$1"); $config['markup'][] = array("/###([^\s']+)###/", "###\$1###"); - + + $config['markup_paragraphs'] = true; + $config['markup_rtl'] = true; + $config['boards'] = array(array('' => '/', '' => '/boards.html', '' => '/faq.html', '' => '/random.php', '' => '/create.php', '' => '/bans.html', '' => '/search.php', '' => '/mod.php', '' => 'https://qchat.rizon.net/?channels=#8chan'), array('b', 'news+', 'boards'), array('operate', 'meta'), array(''=>'https://twitter.com/infinitechan')); //$config['boards'] = array(array('' => '/', '' => '/boards.html', '' => '/faq.html', '' => '/random.php', '' => '/create.php', '' => '/search.php', '' => '/mod.php', '' => 'https://qchat.rizon.net/?channels=#8chan'), array('b', 'meta', 'int'), array('v', 'a', 'tg', 'fit', 'pol', 'tech', 'mu', 'co', 'sp', 'boards'), array(''=>'https://twitter.com/infinitechan')); - + $config['footer'][] = 'All posts on 8chan are the responsibility of the individual poster and not the administration of 8chan, pursuant to 47 U.S.C. § 230.'; $config['footer'][] = 'We have not been served any secret court orders and are not under any gag orders.'; $config['footer'][] = 'To make a DMCA request or report illegal content, please email admin@8chan.co.'; - + $config['search']['enable'] = true; - + $config['syslog'] = true; - + $config['hour_max_threads'] = 10; $config['filters'][] = array( 'condition' => array( @@ -180,6 +185,32 @@ 'message' => 'On this board, to prevent raids the number of threads that can be created per hour is limited. Please try again later, or post in an existing thread.' ); + $config['languages'] = array( + 'ch' => "汉语", + 'cz' => "Čeština", + 'dk' => "Dansk", + 'de' => "Deutsch", + 'eo' => "Esperanto", + 'en' => "English", + 'es' => "Español", + 'fi' => "Suomi", + 'fr' => "Français", + 'hu' => "Magyar", + 'it' => "Italiano", + 'jp' => "日本語", + 'jbo' => "Lojban", + 'lt' => "Lietuvių Kalba", + 'lv' => "Latviešu Valoda", + 'no' => "Norsk", + 'nl' => "Nederlands Vlaams", + 'pl' => "Polski", + 'pt' => "Português", + 'ru' => "Русский", + 'sk' => "Slovenský Jazyk", + 'tw' => "Taiwanese", + ); + + $config['gzip_static'] = false; $config['hash_masked_ip'] = true; $config['force_subject_op'] = false; @@ -200,6 +231,10 @@ $config['report_captcha'] = true; $config['page_404'] = 'page_404'; +// Flavor and design. +$config['site_name'] = "∞chan"; +$config['site_logo'] = "/static/logo_33.svg"; + // 8chan specific mod pages require '8chan-mod-config.php'; diff --git a/inc/lib/Twig/Extensions/Extension/Tinyboard.php b/inc/lib/Twig/Extensions/Extension/Tinyboard.php index 1c5b8981..f41e0768 100644 --- a/inc/lib/Twig/Extensions/Extension/Tinyboard.php +++ b/inc/lib/Twig/Extensions/Extension/Tinyboard.php @@ -76,7 +76,7 @@ function twig_remove_whitespace_filter($data) { } function twig_date_filter($date, $format) { - return gmstrftime($format, $date); + return gmstrftime($format, (int) $date); } function twig_hasPermission_filter($mod, $permission, $board = null) { @@ -86,7 +86,7 @@ function twig_hasPermission_filter($mod, $permission, $board = null) { function twig_extension_filter($value, $case_insensitive = true) { $ext = mb_substr($value, mb_strrpos($value, '.') + 1); if($case_insensitive) - $ext = mb_strtolower($ext); + $ext = mb_strtolower($ext); return $ext; } diff --git a/index.php b/index.php index 12fec902..a44b56e5 100644 --- a/index.php +++ b/index.php @@ -9,3 +9,5 @@ if ($query) { $index = Element("8chan/index.html", array("config" => $config, "newsplus" => $newsplus)); file_write('index.html', $index); + +echo $index; \ No newline at end of file diff --git a/js/board-directory.js b/js/board-directory.js new file mode 100644 index 00000000..b278fbde --- /dev/null +++ b/js/board-directory.js @@ -0,0 +1,349 @@ +// ============================================================ +// Purpose : Board directory handling +// Contributors : 8n-tech +// ============================================================ + +;( function( window, $, undefined ) { + var boardlist = { + options : { + $boardlist : false, + + // Selectors for finding and binding elements. + selector : { + 'boardlist' : "#boardlist", + + 'board-head' : ".board-list-head", + 'board-body' : ".board-list-tbody", + 'board-loading' : ".board-list-loading", + 'board-omitted' : ".board-list-omitted", + + 'search' : "#search-form", + 'search-lang' : "#search-lang-input", + 'search-sfw' : "#search-sfw-input", + 'search-tag' : "#search-tag-input", + 'search-title' : "#search-title-input", + 'search-submit' : "#search-submit", + + 'tag-list' : ".tag-list", + 'tag-link' : ".tag-link", + + 'footer-page' : ".board-page-num", + 'footer-count' : ".board-page-count", + 'footer-total' : ".board-page-total", + 'footer-more' : "#board-list-more" + }, + + // HTML Templates for dynamic construction + template : { + // Board row item + 'board-row' : "", + + // Individual cell definitions + 'board-cell-meta' : "", + 'board-cell-uri' : "", + 'board-cell-title' : "", + 'board-cell-pph' : "", + 'board-cell-posts_total' : "", + 'board-cell-active' : "", + 'board-cell-tags' : "", + + // Content wrapper + // Used to help constrain contents to their . + 'board-content-wrap' : "
", + + // Individual items or parts of a single table cell. + 'board-datum-lang' : "", + 'board-datum-uri' : "", + 'board-datum-sfw' : "", + 'board-datum-nsfw' : "", + 'board-datum-tags' : "", + + + // Tag list. + 'tag-list' : "", + 'tag-item' : "
  • ", + 'tag-link' : "" + } + }, + + lastSearch : {}, + + bind : { + form : function() { + var selectors = boardlist.options.selector; + + var $search = $( selectors['search'] ), + $searchLang = $( selectors['search-lang'] ), + $searchSfw = $( selectors['search-sfw'] ), + $searchTag = $( selectors['search-tag'] ), + $searchTitle = $( selectors['search-title'] ), + $searchSubmit = $( selectors['search-submit'] ); + + var searchForms = { + 'boardlist' : boardlist.$boardlist, + 'search' : $search, + 'searchLang' : $searchLang, + 'searchSfw' : $searchSfw, + 'searchTag' : $searchTag, + 'searchTitle' : $searchTitle, + 'searchSubmit' : $searchSubmit + }; + + if ($search.length > 0) { + // Bind form events. + boardlist.$boardlist + // Load more + .on( 'click', selectors['board-omitted'], searchForms, boardlist.events.loadMore ) + // Tag click + .on( 'click', selectors['tag-link'], searchForms, boardlist.events.tagClick ) + // Form Submission + .on( 'submit', selectors['search'], searchForms, boardlist.events.searchSubmit ) + // Submit click + .on( 'click', selectors['search-submit'], searchForms, boardlist.events.searchSubmit ); + + $searchSubmit.prop( 'disabled', false ); + } + } + }, + + build : { + boardlist : function(data) { + boardlist.build.boards(data['boards'], data['order']); + boardlist.build.lastSearch(data['search']); + boardlist.build.footer(data); + boardlist.build.tags(data['tagWeight']); + + }, + + boards : function(boards, order) { + // Find our head, columns, and body. + var $head = $( boardlist.options.selector['board-head'], boardlist.$boardlist ), + $cols = $("[data-column]", $head ), + $body = $( boardlist.options.selector['board-body'], boardlist.$boardlist ); + + $.each( order, function( index, uri ) { + var row = boards[uri]; + $row = $( boardlist.options.template['board-row'] ); + + $cols.each( function( index, col ) { + boardlist.build.board( row, col ).appendTo( $row ); + } ); + + $row.appendTo( $body ); + } ); + + }, + board : function(row, col) { + var $col = $(col), + column = $col.attr('data-column'), + value = row[column] + $cell = $( boardlist.options.template['board-cell-' + column] ), + $wrap = $( boardlist.options.template['board-content-wrap'] ); + + if (typeof boardlist.build.boardcell[column] === "undefined") { + if (value instanceof Array) { + if (typeof boardlist.options.template['board-datum-' + column] !== "undefined") { + $.each( value, function( index, singleValue ) { + $( boardlist.options.template['board-datum-' + column] ) + .text( singleValue ) + .appendTo( $wrap ); + } ); + } + else { + $wrap.text( value.join(" ") ); + } + } + else { + $wrap.text( value ); + } + } + else { + var $content = boardlist.build.boardcell[column]( row, value ); + + if ($content instanceof jQuery) { + // We use .append() instead of .appendTo() as we do elsewhere + // because $content can be multiple elements. + $wrap.append( $content ); + } + else if (typeof $content === "string") { + $wrap.html( $content ); + } + else { + console.log("Special cell constructor returned a " + (typeof $content) + " that board-directory.js cannot interpret."); + } + } + + $wrap.appendTo( $cell ); + return $cell; + }, + boardcell : { + 'meta' : function(row, value) { + return $( boardlist.options.template['board-datum-lang'] ).text( row['locale'] ); + }, + 'uri' : function(row, value) { + var $link = $( boardlist.options.template['board-datum-uri'] ), + $sfw = $( boardlist.options.template['board-datum-' + (row['sfw'] == 1 ? "sfw" : "nsfw")] ); + + $link + .attr( 'href', "/"+row['uri']+"/" ) + .text( "/"+row['uri']+"/" ); + + // I decided against NSFW icons because it clutters the index. + // Blue briefcase = SFW. No briefcase = NSFW. Seems better. + if (row['sfw'] == 1) { + return $link[0].outerHTML + $sfw[0].outerHTML; + } + else { + return $link[0].outerHTML; + } + } + }, + + lastSearch : function(search) { + return boardlist.lastSearch = { + 'lang' : search.lang === false ? "" : search.lang, + 'page' : search.page, + 'tags' : search.tags === false ? "" : search.tags.join(" "), + 'time' : search.time, + 'title' : search.title === false ? "" : search.title, + 'sfw' : search.nsfw ? 0 : 1 + }; + }, + + footer : function(data) { + var selector = boardlist.options.selector, + $page = $( selector['footer-page'], boardlist.$boardlist ), + $count = $( selector['footer-count'], boardlist.$boardlist ), + $total = $( selector['footer-total'], boardlist.$boardlist ), + $more = $( selector['footer-more'], boardlist.$boardlist ), + $omitted = $( selector['board-omitted'], boardlist.$boardlist ); + + var boards = Object.keys(data['boards']).length, + omitted = data['omitted'] - data['search']['page']; + + if (omitted < 0) { + omitted = 0; + } + + var total = boards + omitted + data['search']['page']; + + //$page.text( data['search']['page'] ); + $count.text( data['search']['page'] + boards ); + $total.text( total ); + $more.toggleClass( "board-list-hasmore", omitted != 0 ); + $omitted.toggle( boards + omitted > 0 ); + }, + + tags : function(tags) { + var selector = boardlist.options.selector, + template = boardlist.options.template, + $list = $( selector['tag-list'], boardlist.$boardlist ); + + if ($list.length) { + + $.each( tags, function(tag, weight) { + var $item = $( template['tag-item'] ), + $link = $( template['tag-link'] ); + + $link + .css( 'font-size', weight+"%" ) + .text( tag ) + .appendTo( $item ); + + $item.appendTo( $list ); + } ); + } + } + }, + + events : { + loadMore : function(event) { + var parameters = $.extend( {}, boardlist.lastSearch ); + + parameters.page = $( boardlist.options.selector['board-body'], boardlist.$boardlist ).children().length; + + boardlist.submit( parameters ); + }, + + searchSubmit : function(event) { + event.preventDefault(); + + $( boardlist.options.selector['tag-list'], boardlist.$boardlist ).html(""); + $( boardlist.options.selector['board-body'], boardlist.$boardlist ).html(""); + + boardlist.submit( { + 'lang' : event.data.searchLang.val(), + 'tags' : event.data.searchTag.val(), + 'title' : event.data.searchTitle.val(), + 'sfw' : event.data.searchSfw.prop('checked') ? 1 : 0 + } ); + + return false; + }, + + tagClick : function(event) { + event.preventDefault(); + + var $this = $(this), + $input = $( boardlist.options.selector['search-tag'] ); + + $input + .val( ( $input.val() + " " + $this.text() ).replace(/\s+/g, " ").trim() ) + .trigger( 'change' ) + .focus(); + + return false; + } + }, + + submit : function( parameters ) { + var $boardlist = boardlist.$boardlist, + $boardload = $( boardlist.options.selector['board-loading'], $boardlist ), + $searchSubmit = $( boardlist.options.selector['search-submit'], $boardlist ), + $footerMore = $( boardlist.options.selector['board-omitted'], $boardlist ); + + $searchSubmit.prop( 'disabled', true ); + $boardload.show(); + $footerMore.hide(); + + return $.get( + "/board-search.php", + parameters, + function(data) { + $searchSubmit.prop( 'disabled', false ); + $boardload.hide(); + + boardlist.build.boardlist( $.parseJSON(data) ); + } + ); + }, + + init : function( target ) { + if (typeof target !== "string") { + target = boardlist.options.selector.boardlist; + } + + var $boardlist = $(target); + + if ($boardlist.length > 0 ) { + $( boardlist.options.selector['board-loading'], $boardlist ).hide(); + + boardlist.$boardlist = $boardlist; + boardlist.bind.form(); + } + } + }; + + // Tie to the vichan object. + if (typeof window.vichan === "undefined") { + window.vichan = {}; + } + window.vichan.boardlist = boardlist; + + // Initialize the boardlist when the document is ready. + $( document ).on( 'ready', window.vichan.boardlist.init ); + // Run it now if we're already ready. + if (document.readyState === 'complete') { + window.vichan.boardlist.init(); + } +} )( window, jQuery ); \ No newline at end of file diff --git a/js/quick-post-controls.js b/js/quick-post-controls.js index 4e8df1cb..67c4bc51 100644 --- a/js/quick-post-controls.js +++ b/js/quick-post-controls.js @@ -90,5 +90,12 @@ $(document).ready(function(){ $(document).on('new_post', function(e, post) { $(post).find('input[type=checkbox].delete').each(init_qpc); }); -}); - + + // Bottom of the page quick reply function + $("#thread-quick-reply").show(); + $("#link-quick-reply").on( 'click', function(event) { + event.preventDefault(); + $(window).trigger('cite', ['']); + return false; + } ); +} ); \ No newline at end of file diff --git a/post.php b/post.php index 177c2dba..ce6a5f47 100644 --- a/post.php +++ b/post.php @@ -209,11 +209,16 @@ if (isset($_POST['delete'])) { } } elseif (isset($_POST['post'])) { - if (!isset($_POST['body'], $_POST['board'])) + if (!isset($_POST['body'], $_POST['board'])) { error($config['error']['bot']); - - $post = array('board' => $_POST['board'], 'files' => array()); - + } + + $post = array( + 'board' => $_POST['board'], + 'files' => array(), + 'time' => time(), // Timezone independent UNIX timecode. + ); + // Check if board exists if (!openBoard($post['board'])) error($config['error']['noboard']); @@ -228,7 +233,7 @@ elseif (isset($_POST['post'])) { $_POST['subject'] = ''; if (!isset($_POST['password'])) - $_POST['password'] = ''; + $_POST['password'] = ''; if (isset($_POST['thread'])) { $post['op'] = false; @@ -638,7 +643,8 @@ elseif (isset($_POST['post'])) { if (mysql_version() >= 50503) { $post['body_nomarkup'] = $post['body']; // Assume we're using the utf8mb4 charset - } else { + } + else { // MySQL's `utf8` charset only supports up to 3-byte symbols // Remove anything >= 0x010000 @@ -708,7 +714,7 @@ elseif (isset($_POST['post'])) { do_filters($post); } - if ($post['has_file']) { + if ($post['has_file']) { foreach ($post['files'] as $key => &$file) { if ($file['is_an_image'] && $config['ie_mime_type_detection'] !== false) { // Check IE MIME type detection XSS exploit @@ -906,10 +912,15 @@ elseif (isset($_POST['post'])) { $post['files'] = $post['files']; $post['num_files'] = sizeof($post['files']); + // Commit the post to the database. $post['id'] = $id = post($post); insertFloodPost($post); - + + // Update statistics for this board. + updateStatisticsForPost( $post ); + + // Handle cyclical threads if (!$post['op'] && isset($thread['cycle']) && $thread['cycle']) { // Query is a bit weird due to "This version of MariaDB doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'" (MariaDB Ver 15.1 Distrib 10.0.17-MariaDB, for Linux (x86_64)) @@ -1005,17 +1016,20 @@ elseif (isset($_POST['post'])) { event('post-after', $post); buildIndex(); - - // We are already done, let's continue our heavy-lifting work in the background (if we run off FastCGI) - if (function_exists('fastcgi_finish_request')) - @fastcgi_finish_request(); - - if ($post['op']) - rebuildThemes('post-thread', $board['uri']); - else - rebuildThemes('post', $board['uri']); -} elseif (isset($_POST['appeal'])) { + // We are already done, let's continue our heavy-lifting work in the background (if we run off FastCGI) + if (function_exists('fastcgi_finish_request')) { + @fastcgi_finish_request(); + } + + if ($post['op']) { + rebuildThemes('post-thread', $board['uri']); + } + else { + rebuildThemes('post', $board['uri']); + } +} +elseif (isset($_POST['appeal'])) { if (!isset($_POST['ban_id'])) error($config['error']['bot']); diff --git a/static/infinity-small.gif b/static/infinity-small.gif new file mode 100644 index 00000000..c7a2978e Binary files /dev/null and b/static/infinity-small.gif differ diff --git a/stylesheets/style.css b/stylesheets/style.css index e067cc10..a46677bb 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -1,3 +1,6 @@ +/* === GENERAL TAG SETTINGS === */ + +/* Page Layouts */ body { background: #EEF2FF url('img/fade-blue.png') repeat-x 50% 0%; color: black; @@ -8,6 +11,69 @@ body { padding-right: 4px; } +main, +aside, +section { + display: block; + margin: 0 auto; + width: 100%; +} + +main { + max-width: 1110px; +} + +/* Tables */ +table { + margin: auto; + width: 100%; +} + +table { + margin: auto; + width: 100%; +} +table tbody td { + margin: 0; + padding: 4px 15px 4px 4px; + + text-align: left; +} +table thead th { + border: 1px solid #000333; + padding: 4px 15px 5px 5px; + + background: #98E; + color: #000333; + text-align: left; + white-space: nowrap; +} +table tbody tr:nth-of-type( even ) { + background-color: #D6DAF0; +} + +td.minimal,th.minimal { + width: 1%; + white-space: nowrap; +} + +table.mod.config-editor { + font-size: 9pt; + width: 100%; +} + +table.mod.config-editor td { + text-align: left; + padding: 5px; + border-bottom: 1px solid #98e; +} + +table.mod.config-editor input[type="text"] { + width: 98%; +} + + +/* Uncategorized */ #post-form-outer { text-align: center; } @@ -297,12 +363,13 @@ p.intro a { color: maroon; } -div.delete { - float: right; -} - div.post.reply p { - margin: 0.3em 0 0 0; + display: block; + margin: 0; + + line-height: 1.16em; + font-size: 13px; + min-height: 1.16em; } div.post.reply div.body { @@ -347,12 +414,14 @@ div.post.reply.has-file.body-not-empty { div.post_modified { margin-left: 1.8em; } - div.post_modified div.content-status { margin-top: 0.5em; padding-bottom: 0em; font-size: 72%; } +div.post_modified div.content-status:first-child { + margin-top: 1.3em; +} div.post_modified div.content-status:first-child { margin-top: 1.3em; @@ -536,50 +605,10 @@ hr { clear: left; } -div.boardlist { - color: #89A; - font-size: 9pt; - margin-top: 3px; -} - -div.boardlist.bottom { - margin-top: 20px; -} - -div.boardlist a { - text-decoration: none; -} - div.report { color: #333; } -table.modlog { - margin: auto; - width: 100%; -} - -table.modlog tr td { - text-align: left; - margin: 0; - padding: 4px 15px 0 0; -} - -table.modlog tr th { - text-align: left; - padding: 4px 15px 5px 5px; - white-space: nowrap; -} - -table.modlog tr th { - background: #98E; -} - -td.minimal,th.minimal { - width: 1%; - white-space: nowrap; -} - div.top_notice { text-align: center; margin: 5px auto; @@ -603,21 +632,6 @@ div.blotter { text-align: center; } -table.mod.config-editor { - font-size: 9pt; - width: 100%; -} - -table.mod.config-editor td { - text-align: left; - padding: 5px; - border-bottom: 1px solid #98e; -} - -table.mod.config-editor input[type="text"] { - width: 98%; -} - .desktop-style div.boardlist:not(.bottom) { position: fixed; top: 0; @@ -1010,7 +1024,6 @@ span.pln { color:grey; } - @media screen and (min-width: 768px) { p.intro { clear: none; @@ -1021,8 +1034,169 @@ span.pln { } } -/* threadwatcher */ +/* === SITE-WIDE ASSETS === */ +#logo { + display: block; + width: 100%; + padding: 0; + margin: 0; + text-align: center; +} +#logo-link { + display: inline; +} +#logo-img { + display: inline-block; + height: 128px; + width: auto; +} +/* === GENERAL CLASSES === */ +.loading { + background: none; + background-color: none; + background-image: url('/static/infinity.gif'); + background-position: center center; + background-repeat: no-repeat; + min-height: 76px; + min-width: 128px; +} +.loading-small { + background: none; + background-color: none; + background-image: url('/static/infinity-small.gif'); + background-position: center center; + background-repeat: no-repeat; + min-height: 24px; + min-width: 48px; +} + +/* Text and accessibility */ +.ltr { + direction: ltr; +} +.rtl { + direction: rtl; + font-family: Tahoma; +} + +/* Responsive helpers */ +.col { + box-sizing: border-box; + float: left; +} + +.col-12 { width: 100%; } +.col-11 { width: 91.66666667%; } +.col-10 { width: 83.33333333%; } +.col-9 { width: 75%; } +.col-8 { width: 66.66666667%; } +.col-7 { width: 58.33333333%; } +.col-6 { width: 50%; } +.col-5 { width: 41.66666667%; } +.col-4 { width: 33.33333333%; } +.col-3 { width: 25%; } +.col-2 { width: 16.66666667%; } +.col-1 { width: 8.33333333%; } + +.left-push { + float: left; +} +.right-push { + float: right; +} + +/* Layout design */ +.box { + background: #D6DAF0; + border: 1px solid #000333; + color: #000333; + margin: 0 0 12px 0; +} +.box-title { + background: #98E; + color: #000333; + font-size: 120%; + font-weight: bold; + padding: 4px 8px; +} +.box-content { + padding: 0 8px; + margin: 4px 0; +} + + +.clearfix { + display: block; + clear: both; + visibility: hidden; + overflow: hidden; + + font-size: 0px; + line-height: 0px; + + box-sizing: border-box; + border: none; + height: 0; + margin: 0; + padding: 0; + width: 100%; + zoom: 1; +} + +/* === SPECIFIC PAGES & FEATURES === */ + +/* Board List */ +div.boardlist { + margin-top: 3px; + + color: #89A; + font-size: 9pt; +} +div.boardlist.bottom { + margin-top: 12px; + clear: both; +} +div.boardlist a { + text-decoration: none; +} + +/* Threads */ +/* Thread Footer */ +#thread-interactions { + margin: 8px 0; + clear: both; +} +#thread-links { + float: left; +} +#thread-links > a { + padding-left: none; + padding-right: 10px; +} +#thread-quick-reply { + display: none; + position: absolute; + left: 50%; + right: 50%; + text-align: center; + width: 100px; + margin-left: -50px; +} +#thread_stats { + float: right; +} + +#post-moderation-fields { + float: right; + text-align: right; +} +#delete-fields { +} +#report-fields { +} + +/* threadwatcher */ #watchlist { display: none; max-height: 250px; @@ -1065,25 +1239,25 @@ div.mix { } /* Mona Font */ - .aa { font-family: Mona, "MS PGothic", "MS Pゴシック", sans-serif; display: block!important; font-size: 12pt; } - -.dx,.dy,.dz { +.dx, +.dy, +.dz { width: 30px; text-align: right; display: inline-block; } +/* Dice */ .dice-option table { border: 1px dotted black; margin: 0; border-collapse: collapse; } - .dice-option table td { text-align: center; border-left: 1px dotted black; @@ -1093,7 +1267,6 @@ div.mix { } /* Quick reply (why was most of this ever in the script?) */ - #quick-reply { position: fixed; right: 5%; @@ -1259,3 +1432,181 @@ div.mix { .dropzone .remove-btn:hover { color: rgba(125, 125, 125, 1); } + +table.board-list-table { + display: table; + margin: -2px; + margin-bottom: 10px; + overflow: hidden; + table-layout: fixed; +} + +table.board-list-table .board-meta { + padding-right: 4px; + width: 70px; +} +table.board-list-table .board-uri { + max-width: 196px; +} +table.board-list-table .board-title { + width: auto; +} +table.board-list-table .board-pph { + width: 55px; + padding: 4px; +} +table.board-list-table .board-max { + width: 90px; + padding: 4px; +} +table.board-list-table .board-unique { + width: 100px; + padding: 4px; +} +table.board-list-table .board-tags { + width: auto; + padding: 0 15px 0 4px; +} + +table.board-list-table .board-uri .board-nsfw { + color: rgb(230,0,0); + margin: 0 0 0 0.6em; + float: right; +} +table.board-list-table .board-uri .board-sfw { + /* I'm using blue instead of green to help users with Deuteranopia (most common form of colorblndness). */ + color: rgb(0,0,230); + margin: 0 0 0 0.6em; + float: right; +} + +table.board-list-table div.board-cell { + max-width: 100%; + overflow: hidden; +} + +tbody.board-list-loading { + display: none; +} +tbody.board-list-loading .loading { + height: 80px; +} + +tbody.board-list-omitted td { + background: #98E; + border-top: 1px solid #000333; + padding: 8px; + font-size: 125%; + text-align: center; +} +tbody.board-list-omitted #board-list-more { + cursor: default; +} +tbody.board-list-omitted #board-list-more.board-list-hasmore { + cursor: pointer; +} +tbody.board-list-omitted .board-page-loadmore { + display: none; +} +tbody.board-list-omitted .board-list-hasmore .board-page-loadmore { + display: inline; +} + +aside.search-container { + margin-bottom: 12px; +} +aside.search-container .box { + margin-right: 12px; +} + +.board-search { + margin: 8px 0; +} +.search-item { + margin: 8px 0; +} +.search-sfw { + display: block; + cursor: pointer; + font-size: 110%; + line-height: 120%; +} +#search-sfw-input { + margin: 0; + padding: 0; + transform: scale(1.20); +} +#search-lang-input, +#search-title-input, +#search-tag-input { + box-sizing: border-box; + font-size: 110%; + line-height: 120%; + vertical-align: top; + padding: 2px 0 2px 4px; + max-width: 100%; + min-width: 100%; + width: 100%: +} +#search-loading { + display: inline-block; + vertical-align: bottom; +} + +ul.tag-list { + display: block; + list-style: none; + margin: 8px 8px -9px 8px; + padding: 8px 0 0 0; + border-top: 1px solid #000333; +} +ul.tag-list::after { + content: ' '; + display: block; + clear: both; +} +li.tag-item { + display: inline-block; + float: left; + font-size: 100%; + list-style: none; + margin: 0; + padding: 0 4px 0 0; +} +li.tag-item:last-child { + padding-bottom: 17px; +} +a.tag-link { + overflow: hidden; + white-space: nowrap; +} +li.tag-item a.tag-link { +} +td.board-tags a.tag-link { + display: inline-block; + margin: 0 0.4em 0 0; +} + +@media screen and (max-width: 1100px) { + aside.search-container { + width: 100%; + margin-bottom: 12px; + } + aside.search-container .box { + margin-right: 0; + } + + section.board-list { + margin-top: 12px; + width: 100%; + } + + table.board-list-table .board-meta, + table.board-list-table .board-pph, + table.board-list-table .board-tags { + padding: 0; + margin: 0; + font-size: 0; + width: 0; + } +} \ No newline at end of file diff --git a/templates/8chan/boards-search.html b/templates/8chan/boards-search.html new file mode 100644 index 00000000..e4d70032 --- /dev/null +++ b/templates/8chan/boards-search.html @@ -0,0 +1,111 @@ +
    +
    +

    Global Statistics

    +

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

    + {% if uptime %}

    {{uptime}} without interruption

    {% endif %} +

    This page last updated {{page_updated}}.

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

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

    - -{% if top2k %} -

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

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

    Page last updated: {{last_update}}

    -

    {{uptime_p}} without interruption (read)

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

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

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

    Page last updated: {{last_update}}

    -

    {{uptime_p}} without interruption

    - - +
    +
    +

    Global Statistics

    +

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

    + {% if uptime %}

    {{uptime}} without interruption

    {% endif %} +

    This page last updated {{page_updated}}.

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