diff --git a/board-search.php b/board-search.php index 5f172036..c156fffb 100644 --- a/board-search.php +++ b/board-search.php @@ -26,6 +26,7 @@ $search = array( 'lang' => false, 'nsfw' => true, 'tags' => false, + 'time' => ( (int)( time() / 3600 ) * 3600 ) - 3600, 'title' => false, ); @@ -43,6 +44,12 @@ if (isset( $_GET['lang'] ) && $_GET['lang'] != "" && isset($languages[$search['l if (isset( $_GET['tags'] ) && $_GET['tags'] != "") { $search['tags'] = $_GET['tags']; } + +// 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']; @@ -80,6 +87,8 @@ foreach ($boards as $board) { $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) @@ -103,16 +112,33 @@ foreach ($response['boards'] as $boardUri => &$board) { } } +unset( $boardTags ); + /* Activity Fetching */ -$boardActivity = fetchBoardActivity( array_keys( $response['boards'] ) ); +$boardActivity = fetchBoardActivity( array_keys( $response['boards'] ), $search['time'], true ); $response['tags'] = array(); // 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'] = (int) $boardActivity['active'][ $boardUri ]; - $board['pph'] = (int) $boardActivity['average'][ $boardUri ]; + $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 ); + } if (isset($board['tags']) && count($board['tags']) > 0) { foreach ($board['tags'] as $tag) { @@ -126,6 +152,8 @@ foreach ($response['boards'] as $boardUri => &$board) { } } +unset( $boardActivity ); + // Sort boards by their popularity, then by their total posts. $boardActivityValues = array(); $boardTotalPostsValues = array(); @@ -152,6 +180,9 @@ if (count($response['tags']) > 0) { } +/* Include our interpreted search terms. */ +$response['search'] = $search; + /* (Please) Respond */ if (!$Included) { $json = json_encode( $response ); diff --git a/boards.php b/boards.php index afd96841..06e18086 100644 --- a/boards.php +++ b/boards.php @@ -1,9 +1,10 @@ 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); */ - -$boardQuery = prepare("SELECT COUNT(1) AS 'boards_total', COUNT(indexed) AS 'boards_public' FROM ``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_total = $boardResult['boards_total']; -$boards_public = $boardResult['boards_public']; -$boards_hidden = $boardResult['boards_total'] - $boardResult['boards_public']; +$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 ); -$posts_hour = 0; -$posts_total = 0; +$posts_hour = number_format( fetchBoardActivity(), 0 ); +$posts_total = number_format( $boardResult['posts_total'], 0 ); /* Create and distribute page */ $boardsHTML = Element("8chan/boards-table.html", array( @@ -66,6 +58,7 @@ $searchHTML = Element("8chan/boards-search.html", array( "posts_hour" => $posts_hour, "posts_total" => $posts_total, + "founding_date" => $founding_date, "page_updated" => date('r'), "uptime" => shell_exec('uptime -p'), diff --git a/inc/functions.php b/inc/functions.php index 7b572dfc..98f07377 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -833,18 +833,65 @@ function loadBoardConfig( $uri ) { return $config; } -function fetchBoardActivity( $uris ) { +function fetchBoardActivity( array $uris = array(), $forTime = false, $detailed = false ) { global $config; - $boardActivity = array(); - //$uris = "\"" . implode( (array) $uris, "\",\"" ) . "\""; + // 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. + $forHour = ( (int)( $forTime / 3600 ) * 3600 ) - 3600; - foreach ($uris as $uri) { - $random = 0;//rand( -1000, 1000 ); - if( $random < 0 ) $random = 0; + $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`, `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); - $boardActivity['active'][ $uri ] = $random; - $boardActivity['average'][ $uri ] = ($random * 72) / 72; + + // Format the results. + foreach ($bsResult as $bsRow) { + if (!isset($boardActivity['active'][$bsRow['stat_uri']])) { + $boardActivity['active'][$bsRow['stat_uri']] = unserialize( $bsRow['author_ip_array'] ); + $boardActivity['average'][$bsRow['stat_uri']] = $bsRow['post_count']; + } + else { + $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 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); + + $boardActivity = $bsResult[0]['post_count']; } return $boardActivity; @@ -854,7 +901,7 @@ function fetchBoardTags( $uris ) { global $config; $boardTags = array(); - $uris = "\"{$config['db']['prefix']}" . implode( (array) $uris, "\",\"" ) . "\""; + $uris = "\"" . implode( (array) $uris, "\",\"" ) . "\""; $tagQuery = prepare("SELECT * FROM ``board_tags`` WHERE `uri` IN ({$uris})"); $tagQuery->execute() or error(db_error($tagQuery)); @@ -1090,70 +1137,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']); @@ -1163,12 +1210,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(); } @@ -1462,6 +1509,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; diff --git a/post.php b/post.php index 177c2dba..38898e11 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; @@ -708,7 +713,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 +911,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 +1015,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/stylesheets/style.css b/stylesheets/style.css index 916067fc..ff29bb2a 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -1363,15 +1363,19 @@ table.board-list-table .board-title { } 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 div.board-cell { @@ -1435,4 +1439,14 @@ li.tag-item { list-style: none; margin: 0; padding: 0 0.6em 0 0; +} +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; } \ No newline at end of file diff --git a/templates/8chan/boards-search.html b/templates/8chan/boards-search.html index e7519dd0..57f5834c 100644 --- a/templates/8chan/boards-search.html +++ b/templates/8chan/boards-search.html @@ -72,6 +72,8 @@ {{html_boards}} + + diff --git a/templates/8chan/boards-table.html b/templates/8chan/boards-table.html index a43f3d67..f3451ae1 100644 --- a/templates/8chan/boards-table.html +++ b/templates/8chan/boards-table.html @@ -6,6 +6,6 @@
{{board['pph']}}
{{board['posts_total']}}
{{board['active']}} -
{% for tag in board.tags %}{{ tag }} {% endfor %}
+
{% for tag in board.tags %}{{ tag }}{% endfor %}
{% endfor %} \ No newline at end of file diff --git a/templates/8chan/boards.html b/templates/8chan/boards.html index 4687a6dd..db6b2682 100644 --- a/templates/8chan/boards.html +++ b/templates/8chan/boards.html @@ -1,36 +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 %}

+
+
+

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}}.

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

-
\ No newline at end of file +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + {{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/tools/migrate_board_stats.php b/tools/migrate_board_stats.php index 8ef7b9b4..8184e037 100644 --- a/tools/migrate_board_stats.php +++ b/tools/migrate_board_stats.php @@ -79,11 +79,10 @@ foreach ($boards as $board) { // Add to ip array. if (!isset($postDatum['author_ip_array'])) { - $postDatum['author_ip_array'][ less_ip( $post['ip'] ) ] = 1; + $postDatum['author_ip_array'] = array(); } - // Count ip array. - $postDatum['author_ip_count'] = count( $postDatum['post_id_array'] ); + $postDatum['author_ip_array'][ less_ip( $post['ip'] ) ] = 1; unset( $postHourTime ); } @@ -93,7 +92,8 @@ foreach ($boards as $board) { $postDatum = &$postHour[ $postHourTime ]; // Serialize arrays for TEXT insert. - $postDatum['post_id_array' ] = str_replace( "\"", "\\\"", serialize( $postDatum['post_id_array'] ) ); + $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'] ) ) ); }