diff --git a/inc/config.php b/inc/config.php index 2ddbe905..d359c234 100644 --- a/inc/config.php +++ b/inc/config.php @@ -873,9 +873,10 @@ // Custom stylesheets available for the user to choose. See the "stylesheets/" folder for a list of // available stylesheets (or create your own). $config['stylesheets']['Yotsuba B'] = ''; // Default; there is no additional/custom stylesheet for this. - $config['stylesheets']['Yotsuba'] = 'yotsuba.css'; - // $config['stylesheets']['Futaba'] = 'futaba.css'; - // $config['stylesheets']['Dark'] = 'dark.css'; + $config['stylesheets']['Yotsuba'] = 'yotsuba.css'; + // $config['stylesheets']['Futaba'] = 'futaba.css'; + // $config['stylesheets']['Dark'] = 'dark.css'; + $config['stylesheets']['Tomorrow'] = 'tomorrow.css'; // The prefix for each stylesheet URI. Defaults to $config['root']/stylesheets/ // $config['uri_stylesheets'] = 'http://static.example.org/stylesheets/'; @@ -1222,21 +1223,21 @@ $config['capcode'] = ' ## %s'; // "## Custom" becomes lightgreen, italic and bold: - //$config['custom_capcode']['Custom'] =' ## %s'; + $config['custom_capcode']['Custom'] =' ## %s'; // "## Mod" makes everything purple, including the name and tripcode: - //$config['custom_capcode']['Mod'] = array( - // ' ## %s', - // 'color:purple', // Change name style; optional - // 'color:purple' // Change tripcode style; optional - //); + $config['custom_capcode']['Mod'] = array( + ' ## %s', + 'color:purple', // Change name style; optional + 'color:purple' // Change tripcode style; optional + ); // "## Admin" makes everything red and bold, including the name and tripcode: - //$config['custom_capcode']['Admin'] = array( - // ' ## %s', - // 'color:red;font-weight:bold', // Change name style; optional - // 'color:red;font-weight:bold' // Change tripcode style; optional - //); + $config['custom_capcode']['Admin'] = array( + ' ## %s', + 'color:red;font-weight:bold', // Change name style; optional + 'color:red;font-weight:bold' // Change tripcode style; optional + ); // Enable the moving of single replies $config['move_replies'] = false; @@ -1381,14 +1382,30 @@ $config['mod']['flood'] = &$config['mod']['bypass_filters']; // Raw HTML posting $config['mod']['rawhtml'] = ADMIN; - + + // Clean System + // Post edits remove local clean? + $config['clean']['edits_remove_local'] = true; + // Post edits remove global clean? + $config['clean']['edits_remove_global'] = true; + // Mark post clean for board rule + $config['mod']['clean'] = JANITOR; + // Mark post clean for global rule + $config['mod']['clean_global'] = MOD; + /* Administration */ // View the report queue $config['mod']['reports'] = JANITOR; // Dismiss an abuse report $config['mod']['report_dismiss'] = JANITOR; + // Remove global status from a report + $config['mod']['report_demote'] = JANITOR; + // Elevate a global report to a local report. + $config['mod']['report_promote'] = JANITOR; // Dismiss all abuse reports by an IP $config['mod']['report_dismiss_ip'] = JANITOR; + // Dismiss all abuse reports on an individual post or thread + $config['mod']['report_dismiss_content'] = JANITOR; // View list of bans $config['mod']['view_banlist'] = MOD; // View the username of the mod who made a ban diff --git a/inc/display.php b/inc/display.php index 1a78e19b..9b4c2d83 100644 --- a/inc/display.php +++ b/inc/display.php @@ -343,6 +343,7 @@ function embed_html($link) { return 'Embedding error.'; } + class Post { public function __construct($post, $root=null, $mod=false) { global $config; @@ -389,11 +390,43 @@ class Post { public function build($index=false) { global $board, $config; - return Element('post_reply.html', array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'mod' => $this->mod)); + return Element('post_reply.html', array( + 'config' => $config, + 'board' => $board, + 'post' => &$this, + 'index' => $index, + 'mod' => $this->mod, + 'clean' => $this->getClean(), + )); + } + + public function getClean( ) { + global $board; + + if( !isset( $this->clean ) ) { + $query = prepare("SELECT * FROM `post_clean` WHERE `post_id` = :post AND `board_id` = :board"); + $query->bindValue( ':board', $board['uri'] ); + $query->bindValue( ':post', $this->id ); + + $query->execute() or error(db_error($query)); + + if( !($this->clean = $query->fetch(PDO::FETCH_ASSOC)) ) { + $this->clean = array( + 'post_id' => $this->id, + 'board_id' => $board['uri'], + 'clean_local' => "0", + 'clean_global' => "0", + 'clean_local_mod_id' => null, + 'clean_global_mod_id' => null, + ); + } + } + + return $this->clean; } }; -class Thread { +class Thread extends Post { public function __construct($post, $root = null, $mod = false, $hr = true) { global $config; if (!isset($root)) @@ -453,7 +486,16 @@ class Thread { event('show-thread', $this); - $built = Element('post_thread.html', array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'hasnoko50' => $hasnoko50, 'isnoko50' => $isnoko50, 'mod' => $this->mod)); + $built = Element('post_thread.html', array( + 'config' => $config, + 'board' => $board, + 'post' => &$this, + 'index' => $index, + 'hasnoko50' => $hasnoko50, + 'isnoko50' => $isnoko50, + 'mod' => $this->mod, + 'clean' => $this->getClean(), + )); return $built; } diff --git a/inc/functions.php b/inc/functions.php index b2d945d2..48a399ec 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -311,6 +311,7 @@ function _syslog($priority, $message) { function verbose_error_handler($errno, $errstr, $errfile, $errline) { if (error_reporting() == 0) return false; // Looks like this warning was suppressed by the @ operator. + error(utf8tohtml($errstr), true, array( 'file' => $errfile . ':' . $errline, 'errno' => $errno, @@ -894,10 +895,14 @@ function insertFloodPost(array $post) { $query->bindValue(':board', $board['uri']); $query->bindValue(':time', time()); $query->bindValue(':posthash', make_comment_hex($post['body_nomarkup'])); - if ($post['has_file']) + + if ($post['has_file']) { $query->bindValue(':filehash', $post['filehash']); - else + } + else { $query->bindValue(':filehash', null, PDO::PARAM_NULL); + } + $query->bindValue(':isreply', !$post['op'], PDO::PARAM_INT); $query->execute() or error(db_error($query)); } @@ -2318,24 +2323,38 @@ function DNS($host) { function shell_exec_error($command, $suppress_stdout = false) { global $config, $debug; - - if ($config['debug']) + + if( $config['debug'] ) { + $which = microtime(true); + } + + // Determine if $command is a valid command. If we don't, the following is considered valid output. + // '$command' is not recognized as an internal or external command, operable program or batch file. + if( empty( shell_exec("which $command") ) ) { + return false; + } + + if( $config['debug'] ) { $start = microtime(true); - + } + + $return = trim(shell_exec('PATH="' . escapeshellcmd($config['shell_path']) . ':$PATH";' . $command . ' 2>&1 ' . ($suppress_stdout ? '> /dev/null ' : '') . '&& echo "TB_SUCCESS"')); $return = preg_replace('/TB_SUCCESS$/', '', $return); - - if ($config['debug']) { - $time = microtime(true) - $start; + + if( $config['debug'] ) { + $time_which = $start - $which; + $time = microtime(true) - $start; + $debug['exec'][] = array( - 'command' => $command, - 'time' => '~' . round($time * 1000, 2) . 'ms', + 'command' => $command, + 'time' => '~' . round($time * 1000, 2) . 'ms + ~' . round($time_which * 1000, 2) . 'ms', 'response' => $return ? $return : null ); $debug['time']['exec'] += $time; } - + return $return === 'TB_SUCCESS' ? false : $return; } diff --git a/inc/instance-config.php b/inc/instance-config.php index c2df3b5b..4c8c707e 100644 --- a/inc/instance-config.php +++ b/inc/instance-config.php @@ -18,15 +18,15 @@ $config['db']['user'] = 'root'; $config['db']['password'] = ''; $config['timezone'] = 'UTC'; - $config['cache']['enabled'] = 'apc'; - - + $config['cache']['enabled'] = false; + + $config['cookies']['mod'] = 'mod'; $config['cookies']['salt'] = ''; - + $config['spam']['hidden_inputs_max_pass'] = 128; $config['spam']['hidden_inputs_expire'] = 60 * 60 * 4; // three hours - + $config['flood_time'] = 5; $config['flood_time_ip'] = 30; $config['flood_time_same'] = 2; @@ -46,10 +46,10 @@ $config['thread_subject_in_title'] = true; $config['spam']['hidden_inputs_max_pass'] = 128; $config['ayah_enabled'] = true; - + // Load database credentials require "secrets.php"; - + // Image shit $config['thumb_method'] = 'gm+gifsicle'; $config['thumb_ext'] = ''; diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 3cdf1374..6aaaa2fe 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -496,7 +496,7 @@ function mod_new_board() { error(sprintf($config['error']['boardexists'], $board['url'])); } - $query = prepare('INSERT INTO ``boards`` VALUES (:uri, :title, :subtitle)'); + $query = prepare('INSERT INTO ``boards`` (``uri``, ``title``, ``subtitle``) VALUES (:uri, :title, :subtitle)'); $query->bindValue(':uri', $_POST['uri']); $query->bindValue(':title', $_POST['title']); $query->bindValue(':subtitle', $_POST['subtitle']); @@ -1606,6 +1606,35 @@ function mod_edit_post($board, $edit_raw_html, $postID) { } $query->execute() or error(db_error($query)); + if( $config['clean']['edits_remove_local'] || $config['clean']['edits_remove_global'] ) { + + $query_global = "`clean_global` = :clean"; + $query_global_mod = "`clean_global_mod_id` = :mod"; + $query_local = "`clean_local` = :clean"; + $query_local_mod = "`clean_local_mod_id` = :mod"; + + if( $config['clean']['edits_remove_local'] && $config['clean']['edits_remove_global'] ) { + $query = prepare("UPDATE `post_clean` SET {$query_global}, {$query_global_mod}, {$query_local}, {$query_local_mod} WHERE `board_id` = :board AND `post_id` = :post"); + } + else if( $config['clean']['edits_remove_global'] ) { + $query = prepare("UPDATE `post_clean` SET {$query_global}, {$query_global_mod} WHERE `board_id` = :board AND `post_id` = :post"); + } + else { + $query = prepare("UPDATE `post_clean` SET {$query_local}, {$query_local_mod} WHERE `board_id` = :board AND `post_id` = :post"); + } + + $query->bindValue( ':clean', false ); + $query->bindValue( ':mod', NULL ); + $query->bindValue( ':board', $board ); + $query->bindValue( ':post', $postID ); + + $query->execute() or error(db_error($query)); + + // Finally, run a query to tidy up our records. + $cleanup = prepare("DELETE FROM `post_clean` WHERE `clean_local` = FALSE AND `clean_global` = FALSE"); + $query->execute() or error(db_error($query)); + } + if ($edit_raw_html) { modLog("Edited raw HTML of post #{$postID}"); } else { @@ -1614,7 +1643,7 @@ function mod_edit_post($board, $edit_raw_html, $postID) { } buildIndex(); - + rebuildThemes('post', $board); header('Location: ?/' . sprintf($config['board_path'], $board) . $config['dir']['res'] . sprintf($config['file_page'], $post['thread'] ? $post['thread'] : $postID) . '#' . $postID, true, $config['redirect_http']); @@ -2257,133 +2286,516 @@ function mod_rebuild() { )); } -function mod_reports($global = false) { + +function mod_reports() { global $config, $mod; - if (!hasPermission($config['mod']['reports'])) - error($config['error']['noaccess']); + // Parse arguments. + $urlArgs = func_get_args(); + $global = in_array( "global", $urlArgs ); - if ($mod['type'] == '20' and $global) + if( !hasPermission($config['mod']['reports']) ) { error($config['error']['noaccess']); - - $query = prepare("SELECT * FROM ``reports`` " . ($mod["type"] == "20" ? "WHERE board = :board" : "") . " ORDER BY `time` DESC LIMIT :limit"); - if ($mod['type'] == '20') - $query->bindValue(':board', $mod['boards'][0]); - - if ($global) { - $query = prepare("SELECT * FROM ``reports`` WHERE global = TRUE ORDER BY `time` DESC LIMIT :limit"); } - - $query->bindValue(':limit', $config['mod']['recent_reports'], PDO::PARAM_INT); - + if( $mod['type'] == '20' and $global ) { + error($config['error']['noaccess']); + } + + // Limit reports to ONLY those in our scope. + $report_scope = $global ? "global" : "local"; + + // Get REPORTS. + $query = prepare("SELECT * FROM ``reports`` " . ($mod["type"] == "20" ? "WHERE board = :board" : "") . " WHERE ``".($global ? "global" : "local")."`` = TRUE LIMIT :limit"); + + // Limit reports by board if the moderator is local. + if( $mod['type'] == '20' ) { + $query->bindValue(':board', $mod['boards'][0]); + } + + // Limit by config ceiling. + $query->bindValue( ':limit', $config['mod']['recent_reports'], PDO::PARAM_INT ); + $query->execute() or error(db_error($query)); $reports = $query->fetchAll(PDO::FETCH_ASSOC); - $report_queries = array(); - foreach ($reports as $report) { - if (!isset($report_queries[$report['board']])) - $report_queries[$report['board']] = array(); - $report_queries[$report['board']][] = $report['post']; - } - - $report_posts = array(); - foreach ($report_queries as $board => $posts) { - $report_posts[$board] = array(); + // Cut off here if we don't have any reports. + $reportCount = 0; + $reportHTML = ''; + if( count( $reports ) > 0 ) { - $query = query(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = ' . implode(' OR `id` = ', $posts), $board)) or error(db_error()); - while ($post = $query->fetch(PDO::FETCH_ASSOC)) { - $report_posts[$board][$post['id']] = $post; + // Build queries to fetch content. + $report_queries = array(); + foreach ($reports as $report) { + if (!isset($report_queries[$report['board']])) + $report_queries[$report['board']] = array(); + $report_queries[$report['board']][] = $report['post']; + } + + // Get reported CONTENT. + $report_posts = array(); + foreach ($report_queries as $board => $posts) { + $report_posts[$board] = array(); + + $query = query(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = ' . implode(' OR `id` = ', $posts), $board)) or error(db_error()); + while ($post = $query->fetch(PDO::FETCH_ASSOC)) { + $report_posts[$board][$post['id']] = $post; + } + } + + // Develop an associative array of posts to reports. + $report_index = array(); + foreach( $reports as &$report ) { + + // Delete reports which are for removed content. + if( !isset( $report_posts[ $report['board'] ][ $report['post'] ] ) ) { + // Invalid report (post has since been deleted) + $query = prepare("DELETE FROM ``reports`` WHERE `post` = :id AND `board` = :board"); + $query->bindValue(':id', $report['post'], PDO::PARAM_INT); + $query->bindValue(':board', $report['board']); + $query->execute() or error(db_error($query)); + continue; + } + + // Build a unique ID. + $content_key = "{$report['board']}.{$report['post']}"; + + // Create a dummy array if it doesn't already exist. + if( !isset( $report_index[ $content_key ] ) ) { + $report_index[ $content_key ] = array( + "board_id" => $report['board'], + "post_id" => $report['post'], + "content" => $report_posts[ $report['board'] ][ $report['post'] ], + "reports" => array(), + ); + } + + // Add the report to the list of reports. + $report_index[ $content_key ]['reports'][ $report['id'] ] = $report; + + // Increment the total report count. + ++$reportCount; + } + + // Only continue if we have something to do. + // If there are no valid reports left, we're done. + if( $reportCount > 0 ) { + + // Sort this report index by number of reports, desc. + usort( $report_index, function( $a, $b ) { + $ra = count( $a['reports'] ); + $rb = count( $b['reports'] ); + + if( $ra < $rb ) { + return 1; + } + else if( $rb > $ra ) { + return -1; + } + else { + return 0; + } + } ); + + // Loop through the custom index. + foreach( $report_index as &$report_item ) { + $content = $report_item['content']; + + // Load board content. + openBoard($report_item['board_id']); + + // Load the reported content. + if( !$content['thread'] ) { + // Still need to fix this: + $po = new Thread($content, '?/', $mod, false); + } + else { + $po = new Post($content, '?/', $mod); + } + + // Fetch clean status. + $po->getClean(); + $clean = $po->clean; + + + // Add each report's template to this container. + $report_html = ""; + $reports_can_demote = false; + $reports_can_promote = false; + $content_reports = 0; + foreach( $report_item['reports'] as $report ) { + $uri_report_base = "reports/" . ($global ? "global/" : "" ) . $report['id']; + $report_html .= Element('mod/report.html', array( + 'report' => $report, + 'config' => $config, + 'mod' => $mod, + 'global' => $global, + 'clean' => $clean, + + 'uri_dismiss' => "?/{$uri_report_base}/dismiss", + 'uri_ip' => "?/{$uri_report_base}/dismissall", + 'uri_demote' => "?/{$uri_report_base}/demote", + 'uri_promote' => "?/{$uri_report_base}/promote", + 'token_dismiss' => make_secure_link_token( $uri_report_base . '/dismiss' ), + 'token_ip' => make_secure_link_token( $uri_report_base . '/dismissall' ), + 'token_demote' => make_secure_link_token( $uri_report_base . '/demote' ), + 'token_promote' => make_secure_link_token( $uri_report_base . '/promote' ), + )); + + // Determines if we can "Demote All" / "Promote All" + // This logic only needs one instance of a demotable or promotable report to work. + // DEMOTE can occur when we're global and the report has a 1 for local (meaning locally, it's not dismissed) + // PROMOTE can occur when we're local and the report has a 0 for global (meaning it's not global). + if( $global && $report['local'] == "1" ) { + $reports_can_demote = true; + } + else if( !$global && $report['global'] != "1" ) { + $reports_can_promote = true; + } + + ++$content_reports; + } + + // Build the ">>>/b/ thread reported 3 times" title. + $report_title = sprintf( + _('>>>/%s/ %s reported %d time(s).'), + "?/{$report_item['board_id']}/res/" . ( $content['thread'] ?: $content['id'] ) . ".html#{$content['thread']}", + $report_item['board_id'], + _( $content['thread'] ? "reply" : "thread" ), + $content_reports + ); + + + // Figure out some stuff we need for the page. + $reports_can_demote = ( $clean['clean_local'] ? false : $reports_can_demote ); + $reports_can_promote = ( $clean['clean_global'] ? false : $reports_can_promote ); + $uri_content_base = "reports/" . ($global ? "global/" : "" ) . "content/"; + $uri_clean_base = "reports/" . ($global ? "global/" : "" ) . "{$report_item['board_id']}/clean/{$content['id']}"; + + // Build the actions page. + $content_html = Element('mod/report_content.html', array( + 'reports_html' => $report_html, + 'reports_can_demote' => $reports_can_demote, + 'reports_can_promote' => $reports_can_promote, + 'report_count' => $content_reports, + 'report_title' => $report_title, + + 'content_html' => $po->build(true), + 'content_board' => $report_item['board_id'], + 'content' => (array) $content, + + 'clean' => $clean, + + 'uri_content_demote' => "?/{$uri_content_base}{$report_item['board_id']}/{$content['id']}/demote", + 'uri_content_promote' => "?/{$uri_content_base}{$report_item['board_id']}/{$content['id']}/promote", + 'uri_content_dismiss' => "?/{$uri_content_base}{$report_item['board_id']}/{$content['id']}/dismiss", + 'token_content_demote' => make_secure_link_token( "{$uri_content_base}{$report_item['board_id']}/{$content['id']}/demote" ), + 'token_content_promote' => make_secure_link_token( "{$uri_content_base}{$report_item['board_id']}/{$content['id']}/promote" ), + 'token_content_dismiss' => make_secure_link_token( "{$uri_content_base}{$report_item['board_id']}/{$content['id']}/dismiss" ), + + 'uri_clean' => "?/{$uri_clean_base}/local", + 'uri_clean_global' => "?/{$uri_clean_base}/global", + 'uri_clean_both' => "?/{$uri_clean_base}/global+local", + 'token_clean' => make_secure_link_token( $uri_clean_base . '/local' ), + 'token_clean_global' => make_secure_link_token( $uri_clean_base . '/global' ), + 'token_clean_both' => make_secure_link_token( $uri_clean_base . '/global+local' ), + + 'global' => $global, + 'config' => $config, + 'mod' => $mod, + )); + + $reportHTML .= $content_html; + } } } - $count = 0; - $body = ''; - foreach ($reports as $report) { - if (!isset($report_posts[$report['board']][$report['post']])) { - // // Invalid report (post has since been deleted) - $query = prepare("DELETE FROM ``reports`` WHERE `post` = :id AND `board` = :board"); - $query->bindValue(':id', $report['post'], PDO::PARAM_INT); - $query->bindValue(':board', $report['board']); - $query->execute() or error(db_error($query)); - continue; - } - - openBoard($report['board']); - - $post = &$report_posts[$report['board']][$report['post']]; - - if (!$post['thread']) { - // Still need to fix this: - $po = new Thread($post, '?/', $mod, false); - } else { - $po = new Post($post, '?/', $mod); - } - - // a little messy and inefficient - $append_html = Element('mod/report.html', array( - 'report' => $report, - 'config' => $config, - 'mod' => $mod, - 'token' => make_secure_link_token('reports/' . $report['id'] . '/dismiss'), - 'token_all' => make_secure_link_token('reports/' . $report['id'] . '/dismissall') - )); - - // Bug fix for https://github.com/savetheinternet/Tinyboard/issues/21 - $po->body = truncate($po->body, $po->link(), $config['body_truncate'] - substr_count($append_html, '
')); - - if (mb_strlen($po->body) + mb_strlen($append_html) > $config['body_truncate_char']) { - // still too long; temporarily increase limit in the config - $__old_body_truncate_char = $config['body_truncate_char']; - $config['body_truncate_char'] = mb_strlen($po->body) + mb_strlen($append_html); - } - - $po->body .= $append_html; - - $body .= $po->build(true) . '
'; - - if (isset($__old_body_truncate_char)) - $config['body_truncate_char'] = $__old_body_truncate_char; - - $count++; - } + $pageArgs = array( + 'count' => $reportCount, + 'reports' => $reportHTML, + 'global' => $global, + ); - mod_page(sprintf('%s (%d)', _('Report queue'), $count), 'mod/reports.html', array('reports' => $body, 'count' => $count)); + mod_page( sprintf('%s (%d)', _( ( $global ? 'Global report queue' : 'Report queue' ) ), $reportCount), 'mod/reports.html', $pageArgs ); } -function mod_report_dismiss($id, $all = false) { - global $config; +function mod_report_dismiss() { + global $config, $mod; - $query = prepare("SELECT `post`, `board`, `ip` FROM ``reports`` WHERE `id` = :id"); - $query->bindValue(':id', $id); - $query->execute() or error(db_error($query)); - if ($report = $query->fetch(PDO::FETCH_ASSOC)) { - $ip = $report['ip']; - $board = $report['board']; - $post = $report['post']; - } else - error($config['error']['404']); + // Parse arguments. + $arguments = func_get_args(); + $global = in_array( "global", $arguments ); + $content = in_array( "content", $arguments ); - if (!$all && !hasPermission($config['mod']['report_dismiss'], $board)) + if( $mod['type'] == '20' and $global ) { error($config['error']['noaccess']); - - if ($all && !hasPermission($config['mod']['report_dismiss_ip'], $board)) - error($config['error']['noaccess']); - - if ($all) { - $query = prepare("DELETE FROM ``reports`` WHERE `ip` = :ip"); - $query->bindValue(':ip', $ip); - } else { - $query = prepare("DELETE FROM ``reports`` WHERE `id` = :id"); - $query->bindValue(':id', $id); } - $query->execute() or error(db_error($query)); + if( $content ) { + $board = @$arguments[2]; + $post = @$arguments[3]; + + if( !hasPermission($config['mod']['report_dismiss_content'], $board) ) { + error($config['error']['noaccess']); + } + + if( $board != "" && $post != "" ) { + + $query = prepare("SELECT `id` FROM `reports` WHERE `board` = :board AND `post` = :post"); + $query->bindValue(':board', $board); + $query->bindValue(':post', $post); + $query->execute() or error(db_error($query)); + if( count( $reports = $query->fetchAll(PDO::FETCH_ASSOC) ) > 0 ) { + + $report_ids = array(); + foreach( $reports as $report ) { + $report_ids[ $report['id'] ] = $report['id']; + } + + if( $global ) { + $scope = "``global`` = FALSE AND ``local`` = FALSE"; + } + else { + $scope = "``local`` = FALSE"; + } + + $query = prepare("UPDATE ``reports`` SET {$scope} WHERE `id` IN (".implode(',', array_map('intval', $report_ids)).")"); + $query->execute() or error(db_error($query)); + + modLog("Promoted " . count($report_ids) . " local report(s) for post #{$post}", $board); + } + else { + error($config['error']['404']); + } + } + else { + error($config['error']['404']); + } + } + else { + $report = @$arguments[1]; + $all = in_array( "all", $arguments ); + + if( $report != "" ) { + + $query = prepare("SELECT `post`, `board`, `ip` FROM ``reports`` WHERE `id` = :id"); + $query->bindValue(':id', $report); + $query->execute() or error(db_error($query)); + if ($reportobj = $query->fetch(PDO::FETCH_ASSOC)) { + $ip = $reportobj['ip']; + $board = $reportobj['board']; + $post = $reportobj['post']; + + if( !$all && !hasPermission($config['mod']['report_dismiss'], $board) ) { + error($config['error']['noaccess']); + } + if( $all && !hasPermission($config['mod']['report_dismiss_ip'], $board) ) { + error($config['error']['noaccess']); + } + + // Determine scope (local and global or just local) based on /global/ being in URI. + if( $global ) { + $scope = "`global` = FALSE"; + $boards = ""; + } + else { + $scope = "`local` = FALSE"; + $boards = "AND `board` = '{$board}'"; + } + + // Prepare query. + // We don't delete reports, only modify scope. + if( $all ) { + $query = prepare("UPDATE ``reports`` SET {$scope} WHERE `ip` = :ip {$boards}"); + $query->bindValue(':ip', $ip); + } + else { + $query = prepare("UPDATE ``reports`` SET {$scope} WHERE `id` = :id {$boards}"); + $query->bindValue(':id', $report); + } + + $query->execute() or error(db_error($query)); + + + // Cleanup - Remove reports that have been completely dismissed. + $query = prepare("DELETE FROM `reports` WHERE `local` = FALSE AND `global` = FALSE"); + $query->execute() or error(db_error($query)); + + + if( $all ) { + modLog("Dismissed all reports by {$ip}"); + } + else { + modLog("Dismissed a report for post #{$post}", $board); + } + } + else { + error($config['error']['404']); + } + } + else { + error($config['error']['404']); + } + } - if ($all) - modLog("Dismissed all reports by $ip"); - else - modLog("Dismissed a report for post #{$id}", $board); + if( $global ) { + header('Location: ?/reports/global', true, $config['redirect_http']); + } + else { + header('Location: ?/reports', true, $config['redirect_http']); + } +} + +function mod_report_demote() { + global $config, $mod; + + if( $mod['type'] == '20' ) { + error($config['error']['noaccess']); + } + + // Parse arguments. + $arguments = func_get_args(); + $content = in_array( "content", $arguments ); + + if( $content ) { + $board = @$arguments[2]; + $post = @$arguments[3]; + + if( !hasPermission($config['mod']['report_demote'], $board) ) { + error($config['error']['noaccess']); + } + + if( $board != "" && $post != "" ) { + + $query = prepare("SELECT `id` FROM `reports` WHERE `global` = TRUE AND `board` = :board AND `post` = :post"); + $query->bindValue(':board', $board); + $query->bindValue(':post', $post); + $query->execute() or error(db_error($query)); + if( count( $reports = $query->fetchAll(PDO::FETCH_ASSOC) ) > 0 ) { + + $report_ids = array(); + foreach( $reports as $report ) { + $report_ids[ $report['id'] ] = $report['id']; + } + + $query = prepare("UPDATE ``reports`` SET ``global`` = FALSE WHERE `id` IN (".implode(',', array_map('intval', $report_ids)).")"); + $query->execute() or error(db_error($query)); + + modLog("Demoted " . count($report_ids) . " global report(s) for post #{$post}", $board); + } + else { + error($config['error']['404']); + } + } + else { + error($config['error']['404']); + } + } + else { + $report = @$arguments[1]; + + if( $report != "" ) { + + $query = prepare("SELECT `post`, `board`, `ip` FROM ``reports`` WHERE `id` = :id AND ``global`` = TRUE"); + $query->bindValue(':id', $report); + $query->execute() or error(db_error($query)); + if( $reportobj = $query->fetch(PDO::FETCH_ASSOC) ) { + $ip = $reportobj['ip']; + $board = $reportobj['board']; + $post = $reportobj['post']; + + if( !hasPermission($config['mod']['report_demote'], $board) ) { + error($config['error']['noaccess']); + } + + $query = prepare("UPDATE ``reports`` SET ``global`` = FALSE WHERE `id` = :id"); + $query->bindValue(':id', $report); + $query->execute() or error(db_error($query)); + + modLog("Demoted a global report for post #{$report}", $board); + } + else { + error($config['error']['404']); + } + } + else { + error($config['error']['404']); + } + } + + header('Location: ?/reports/global', true, $config['redirect_http']); +} + +function mod_report_promote() { + global $config, $mod; + + // Parse arguments. + $arguments = func_get_args(); + $content = in_array( "content", $arguments ); + + if( $content ) { + $board = @$arguments[2]; + $post = @$arguments[3]; + + if( !hasPermission($config['mod']['report_promote'], $board) ) { + error($config['error']['noaccess']); + } + + if( $board != "" && $post != "" ) { + $query = prepare("SELECT `id` FROM `reports` WHERE `global` = FALSE AND `board` = :board AND `post` = :post"); + $query->bindValue(':board', $board); + $query->bindValue(':post', $post); + $query->execute() or error(db_error($query)); + if( count( $reports = $query->fetchAll(PDO::FETCH_ASSOC) ) > 0 ) { + + $report_ids = array(); + foreach( $reports as $report ) { + $report_ids[ $report['id'] ] = $report['id']; + } + + $query = prepare("UPDATE ``reports`` SET ``global`` = TRUE WHERE `id` IN (".implode(',', array_map('intval', $report_ids)).")"); + $query->execute() or error(db_error($query)); + + modLog("Promoted " . count($report_ids) . " local report(s) for post #{$post}", $board); + } + else { + error($config['error']['404']); + } + } + else { + error($config['error']['404']); + } + } + else { + $report = @$arguments[1]; + + if( $report != "" ) { + $query = prepare("SELECT `post`, `board`, `ip` FROM ``reports`` WHERE `id` = :id AND ``global`` = FALSE"); + $query->bindValue(':id', $report); + $query->execute() or error(db_error($query)); + if ($reportobj = $query->fetch(PDO::FETCH_ASSOC)) { + $ip = $reportobj['ip']; + $board = $reportobj['board']; + $post = $reportobj['post']; + + if( !hasPermission($config['mod']['report_promote'], $board) ) { + error($config['error']['noaccess']); + } + + $query = prepare("UPDATE ``reports`` SET ``global`` = TRUE WHERE `id` = :id"); + $query->bindValue(':id', $report); + $query->execute() or error(db_error($query)); + + modLog("Promoted a local report for post #{$report}", $board); + } + else { + error($config['error']['404']); + } + } + else { + error($config['error']['404']); + } + } header('Location: ?/reports', true, $config['redirect_http']); } @@ -2444,6 +2856,150 @@ function mod_recent_posts($lim) { } +function mod_report_clean( $global_reports, $board, $unclean, $post, $global, $local ) { + global $config, $mod; + + if( !openBoard($board) ) { + error($config['error']['noboard']); + } + + $query_global = ""; + $query_global_mod = ""; + if( $global ) { + if( !hasPermission($config['mod']['clean_global'], $board) ) { + error($config['error']['noaccess']); + } + + $query_global = "`clean_global` = :clean"; + $query_global_mod = "`clean_global_mod_id` = :mod"; + } + + $query_local = ""; + $query_local_mod = ""; + if( $local ) { + if( !hasPermission($config['mod']['clean'], $board) ) { + error($config['error']['noaccess']); + } + + $query_local = "`clean_local` = :clean"; + $query_local_mod = "`clean_local_mod_id` = :mod"; + } + + + // Marking this post as "Clean" (report immune?) + if( !$unclean ) { + // Attempt to find a `post_clean` row for this content. + $query = prepare("SELECT * FROM `post_clean` WHERE `board_id` = :board AND `post_id` = :post"); + $query->bindValue( ':board', $board ); + $query->bindValue( ':post', $post ); + + $query->execute() or error(db_error($query)); + + // If the $clean object doesn't exist we need to insert a row for this post. + if( !($cleanRecord = $query->fetch(PDO::FETCH_ASSOC)) ) { + $query = prepare("INSERT INTO `post_clean` (`post_id`, `board_id`) VALUES ( :post, :board )"); + $query->bindValue( ':board', $board ); + $query->bindValue( ':post', $post ); + + $query->execute() or error(db_error($query)); + + if( $query->rowCount() == 0 ) { + error("The database failed to create a record for this content in `post_clean` to record clean status."); + } + + $cleanRecord = true; + } + } + // Revoking clean status (open it to reports?) + else { + // Attempt to find a `post_clean` row for this content. + $query = prepare("SELECT * FROM `post_clean` WHERE `board_id` = :board AND `post_id` = :post"); + $query->bindValue( ':board', $board ); + $query->bindValue( ':post', $post ); + + $query->execute() or error(db_error($query)); + + if( !($cleanRecord = $query->fetch(PDO::FETCH_ASSOC)) ) { + error($config['error']['404']); + } + } + + // Update the `post_clean` row represented by $clean. + if( $cleanRecord ) { + // Build our query based on the URI arguments. + if( $global && $local ) { + $query = prepare("UPDATE `post_clean` SET {$query_global}, {$query_global_mod}, {$query_local}, {$query_local_mod} WHERE `board_id` = :board AND `post_id` = :post"); + } + else if( $global ) { + $query = prepare("UPDATE `post_clean` SET {$query_global}, {$query_global_mod} WHERE `board_id` = :board AND `post_id` = :post"); + } + else { + $query = prepare("UPDATE `post_clean` SET {$query_local}, {$query_local_mod} WHERE `board_id` = :board AND `post_id` = :post"); + } + + $query->bindValue( ':clean', !$unclean ); + $query->bindValue( ':mod', $unclean ? NULL : $mod['id'] ); + $query->bindValue( ':board', $board ); + $query->bindValue( ':post', $post ); + + $query->execute() or error(db_error($query)); + + // Finally, run a query to tidy up our records. + if( $unclean ) { + // Query is removing clean status from content. + // Remove any clean records that are now null. + $cleanup = prepare("DELETE FROM `post_clean` WHERE `clean_local` = FALSE AND `clean_global` = FALSE"); + $query->execute() or error(db_error($query)); + } + else { + // Content is clean, auto-handle all reports. + + // If this is a total clean, we don't need to update records first. + if( !($global && $local) ) { + $query = prepare("UPDATE `reports` SET `" . ($local ? "local" : "global") . "` = FALSE WHERE `board` = :board AND `post` = :post"); + $query->bindValue( ':board', $board ); + $query->bindValue( ':post', $post ); + + $query->execute() or error(db_error($query)); + + // If we didn't hit anything, this content doesn't have reports, so don't run the delete query. + $require_delete = ($query->rowCount() > 0); + + if( $require_delete ) { + $query = prepare("DELETE FROM `reports` WHERE `local` = FALSE and `global` = FALSE"); + + $query->execute() or error(db_error($query)); + } + } + // This is a total clean, so delete content by ID rather than via cleanup. + else { + $query = prepare("DELETE FROM `reports` WHERE `board` = :board AND `post` = :post"); + + $query->bindValue( ':board', $board ); + $query->bindValue( ':post', $post ); + + $query->execute() or error(db_error($query)); + } + } + + // Log the action. + // Having clear wording of ths log is very important because of the nature of clean status. + $log_action = ($unclean ? "Closed" : "Re-opened" ); + $log_scope = ($local && $global ? "local and global" : ($local ? "local" : "global" ) ); + modLog( "{$log_action} reports for post #{$post} in {$log_scope}.", $board); + + rebuildPost( $post ); + } + + // Redirect + if( $global_reports ) { + header('Location: ?/reports/global', true, $config['redirect_http']); + } + else { + header('Location: ?/reports', true, $config['redirect_http']); + } +} + function mod_config($board_config = false) { global $config, $mod, $board; diff --git a/install.php b/install.php index 8b3815a2..07e94e7f 100644 --- a/install.php +++ b/install.php @@ -579,7 +579,8 @@ if ($step == 0) {

'; echo Element('page.html', $page); -} elseif ($step == 1) { +} +elseif ($step == 1) { $page['title'] = 'Pre-installation test'; $can_exec = true; @@ -761,7 +762,8 @@ if ($step == 0) { 'title' => 'Checking environment', 'config' => $config )); -} elseif ($step == 2) { +} +elseif ($step == 2) { // Basic config $page['title'] = 'Configuration'; @@ -775,7 +777,8 @@ if ($step == 0) { 'title' => 'Configuration', 'config' => $config )); -} elseif ($step == 3) { +} +elseif ($step == 3) { $instance_config = 'I couldn\'t write to inc/instance-config.php with the new configuration, probably due to a permissions error.

@@ -826,7 +830,8 @@ if ($step == 0) { '; echo Element('page.html', $page); } -} elseif ($step == 4) { +} +elseif ($step == 4) { // SQL installation buildJavascript(); @@ -846,11 +851,15 @@ if ($step == 0) { $sql_errors = ''; foreach ($queries as $query) { - if ($mysql_version < 50503) + if ($mysql_version < 50503) { $query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query); + } + $query = preg_replace('/^([\w\s]*)`([0-9a-zA-Z$_\x{0080}-\x{FFFF}]+)`/u', '$1``$2``', $query); - if (!query($query)) + + if (!query($query)) { $sql_errors .= '
  • ' . db_error() . '
  • '; + } } $page['title'] = 'Installation complete'; @@ -858,7 +867,8 @@ if ($step == 0) { if (!empty($sql_errors)) { $page['body'] .= '

    SQL errors

    SQL errors were encountered when trying to install the database. This may be the result of using a database which is already occupied with a vichan installation; if so, you can probably ignore this.

    The errors encountered were:

    Ignore errors and complete installation.

    '; - } else { + } + else { $boards = listBoards(); foreach ($boards as &$_board) { setupBoard($_board); @@ -866,13 +876,11 @@ if ($step == 0) { } file_write($config['has_installed'], VERSION); - /*if (!file_unlink(__FILE__)) { - $page['body'] .= '

    Delete install.php!

    I couldn\'t remove install.php. You will have to remove it manually.

    '; - }*/ } echo Element('page.html', $page); -} elseif ($step == 5) { +} +elseif ($step == 5) { $page['title'] = 'Installation complete'; $page['body'] = '

    Thank you for using vichan. Please remember to report any bugs you discover.

    '; diff --git a/mod.php b/mod.php index 654d23fe..45c836ac 100644 --- a/mod.php +++ b/mod.php @@ -53,10 +53,23 @@ $pages = array( '/edit/(\%b)' => 'secure_POST edit_board', // edit board details '/new-board' => 'secure_POST new_board', // create a new board - '/rebuild' => 'secure_POST rebuild', // rebuild static files - '/reports' => 'reports', // report queue - '/reports/(global)' => 'reports', // global report queue - '/reports/(\d+)/dismiss(all)?' => 'secure report_dismiss', // dismiss a report + '/rebuild' => 'secure_POST rebuild', // rebuild static files + + // Report management + // (global) denotes if the action is being carried out from the global dashboard, + // and if the return address should also be the global dashboard. + // Important to note that (?:global) will make no argument. + // (global)? will make argument 0 either "global" or "". + '/reports(?:/)?' => 'reports', // report queue + '/reports/(global)?(?:/)?' => 'reports', // global report queue + '/reports/(global)?(?:/)?(content)/(\%b)/(\d+)(?:/)?' => 'reports', // specific reported content (also historic) + '/reports/(global)?(?:/)?(content)/(\%b)/(\d+)/dismiss(?:/)?' => 'secure report_dismiss', // dismiss all reports on content + '/reports/(global)?(?:/)?(content)/(\%b)/(\d+)/demote(?:/)?' => 'secure report_demote', // demote all reports on content + '/reports/(global)?(?:/)?(content)/(\%b)/(\d+)/promote(?:/)?' => 'secure report_promote', // demote all reports on content + '/reports/(global)?(?:/)?(\d+)/dismiss(all)?(?:/)?' => 'secure report_dismiss', // dismiss a report + '/reports/(global)?(?:/)?(\d+)/demote(?:/)?' => 'secure report_demote', // demote a global report to a local report + '/reports/(global)?(?:/)?(\d+)/promote(?:/)?' => 'secure report_promote', // promote a local report to a global report + '/reports/(global)?(?:/)?(\%b)/(un)?clean/(\d+)/(global)?(?:\+)?(local)?' => 'secure report_clean', // protect/unprotect from reports '/IP/([\w.:]+)' => 'secure_POST ip', // view ip address '/IP/([\w.:]+)/remove_note/(\d+)' => 'secure ip_remove_note', // remove note from ip address @@ -73,18 +86,19 @@ $pages = array( '/search' => 'search_redirect', // search '/search/(posts|IP_notes|bans|log)/(.+)/(\d+)' => 'search', // search '/search/(posts|IP_notes|bans|log)/(.+)' => 'search', // search - - '/(\%b)/ban(&delete)?/(\d+)' => 'secure_POST ban_post', // ban poster - '/(\%b)/move/(\d+)' => 'secure_POST move', // move thread - '/(\%b)/move_reply/(\d+)' => 'secure_POST move_reply', // move reply - '/(\%b)/edit(_raw)?/(\d+)' => 'secure_POST edit_post', // edit post - '/(\%b)/delete/(\d+)' => 'secure delete', // delete post - '/(\%b)/deletefile/(\d+)/(\d+)' => 'secure deletefile', // delete file from post - '/(\%b+)/spoiler/(\d+)/(\d+)' => 'secure spoiler_image', // spoiler file - '/(\%b)/deletebyip/(\d+)(/global)?' => 'secure deletebyip', // delete all posts by IP address - '/(\%b)/(un)?lock/(\d+)' => 'secure lock', // lock thread - '/(\%b)/(un)?sticky/(\d+)' => 'secure sticky', // sticky thread - '/(\%b)/bump(un)?lock/(\d+)' => 'secure bumplock', // "bumplock" thread + + // Content management + '/(\%b)/ban(&delete)?/(\d+)' => 'secure_POST ban_post', // ban poster + '/(\%b)/move/(\d+)' => 'secure_POST move', // move thread + '/(\%b)/move_reply/(\d+)' => 'secure_POST move_reply', // move reply + '/(\%b)/edit(_raw)?/(\d+)' => 'secure_POST edit_post', // edit post + '/(\%b)/delete/(\d+)' => 'secure delete', // delete post + '/(\%b)/deletefile/(\d+)/(\d+)' => 'secure deletefile', // delete file from post + '/(\%b+)/spoiler/(\d+)/(\d+)' => 'secure spoiler_image', // spoiler file + '/(\%b)/deletebyip/(\d+)(/global)?' => 'secure deletebyip', // delete all posts by IP address + '/(\%b)/(un)?lock/(\d+)' => 'secure lock', // lock thread + '/(\%b)/(un)?sticky/(\d+)' => 'secure sticky', // sticky thread + '/(\%b)/bump(un)?lock/(\d+)' => 'secure bumplock', // "bumplock" thread '/themes' => 'themes_list', // manage themes '/themes/(\w+)' => 'secure_POST theme_configure', // configure/reconfigure theme diff --git a/post.php b/post.php index 40f71555..099ea6d7 100644 --- a/post.php +++ b/post.php @@ -2,10 +2,13 @@ /* * Copyright (c) 2010-2014 Tinyboard Development Group */ + +require "./inc/functions.php"; +require "./inc/anti-bot.php"; -require 'inc/functions.php'; -require 'inc/anti-bot.php'; -include "inc/dnsbls.php"; +// The dnsbls is an optional DNS blacklist include. +// Squelch warnings if it doesn't exist. +@include "./inc/dnsbls.php"; // Fix for magic quotes if (get_magic_quotes_gpc()) { @@ -101,7 +104,8 @@ if (isset($_POST['delete'])) { header('Content-Type: text/json'); echo json_encode(array('success' => true)); } -} elseif (isset($_POST['report'])) { +} +elseif (isset($_POST['report'])) { if (!isset($_POST['board'], $_POST['reason'])) error($config['error']['bot']); @@ -131,25 +135,45 @@ if (isset($_POST['delete'])) { markup($reason); foreach ($report as &$id) { - $query = prepare(sprintf("SELECT `thread` FROM ``posts_%s`` WHERE `id` = :id", $board['uri'])); + $query = prepare( + "SELECT + `thread`, + `post_clean`.`clean_local`, + `post_clean`.`clean_global` + FROM `posts_{$board['uri']}` + LEFT JOIN `post_clean` + ON `post_clean`.`board_id` = '{$board['uri']}' + AND `post_clean`.`post_id` = :id + WHERE `id` = :id" + ); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); - $thread = $query->fetchColumn(); - - if ($config['syslog']) - _syslog(LOG_INFO, 'Reported post: ' . - '/' . $board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $thread ? $thread : $id) . ($thread ? '#' . $id : '') . - ' for "' . $reason . '"' - ); - $query = prepare("INSERT INTO ``reports`` VALUES (NULL, :time, :ip, :board, :post, :reason, :global)"); - $query->bindValue(':time', time(), PDO::PARAM_INT); - $query->bindValue(':ip', $_SERVER['REMOTE_ADDR'], PDO::PARAM_STR); - $query->bindValue(':board', $board['uri'], PDO::PARAM_INT); - $query->bindValue(':post', $id, PDO::PARAM_INT); - $query->bindValue(':reason', $reason, PDO::PARAM_STR); - $query->bindValue(':global', isset($_POST['global']), PDO::PARAM_BOOL); - $query->execute() or error(db_error($query)); + if( $post = $query->fetch(PDO::FETCH_ASSOC) ) { + $report_local = !$post['clean_local']; + $report_global = isset($_POST['global']) && !$post['clean_global']; + + if( $report_local || $report_global ) { + $thread = $post['thread']; + + if ($config['syslog']) { + _syslog(LOG_INFO, 'Reported post: ' . + '/' . $board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $thread ? $thread : $id) . ($thread ? '#' . $id : '') . + ' for "' . $reason . '"' + ); + } + + $query = prepare("INSERT INTO `reports` (`time`, `ip`, `board`, `post`, `reason`, `local`, `global`) VALUES (:time, :ip, :board, :post, :reason, :local, :global)"); + $query->bindValue(':time', time(), PDO::PARAM_INT); + $query->bindValue(':ip', $_SERVER['REMOTE_ADDR'], PDO::PARAM_STR); + $query->bindValue(':board', $board['uri'], PDO::PARAM_INT); + $query->bindValue(':post', $id, PDO::PARAM_INT); + $query->bindValue(':reason', $reason, PDO::PARAM_STR); + $query->bindValue(':local', $report_local, PDO::PARAM_BOOL); + $query->bindValue(':global', $report_global, PDO::PARAM_BOOL); + $query->execute() or error(db_error($query)); + } + } } $is_mod = isset($_POST['mod']) && $_POST['mod']; @@ -161,7 +185,8 @@ if (isset($_POST['delete'])) { header('Content-Type: text/json'); echo json_encode(array('success' => true)); } -} elseif (isset($_POST['post'])) { +} +elseif (isset($_POST['post'])) { if (!isset($_POST['body'], $_POST['board'])) error($config['error']['bot']); @@ -573,14 +598,16 @@ if (isset($_POST['delete'])) { } $md5cmd = $config['bsd_md5'] ? 'md5 -r' : 'md5sum'; - - if ($output = shell_exec_error("cat $filenames | $md5cmd")) { + + if( ($output = shell_exec_error("cat $filenames | $md5cmd")) !== false ) { $explodedvar = explode(' ', $output); $hash = $explodedvar[0]; $post['filehash'] = $hash; - } elseif ($config['max_images'] === 1) { + } + elseif ($config['max_images'] === 1) { $post['filehash'] = md5_file($upload); - } else { + } + else { $str_to_hash = ''; foreach (explode(' ', $filenames) as $i => $f) { $str_to_hash .= file_get_contents($f); @@ -884,7 +911,8 @@ if (isset($_POST['delete'])) { 'id' => $id )); } -} elseif (isset($_POST['appeal'])) { +} +elseif (isset($_POST['appeal'])) { if (!isset($_POST['ban_id'])) error($config['error']['bot']); @@ -925,7 +953,8 @@ if (isset($_POST['delete'])) { $query->execute() or error(db_error($query)); displayBan($ban); -} else { +} +else { if (!file_exists($config['has_installed'])) { header('Location: install.php', true, $config['redirect_http']); } else { diff --git a/stylesheets/mod/mod.css b/stylesheets/mod/mod.css new file mode 100644 index 00000000..3c3a9bd9 --- /dev/null +++ b/stylesheets/mod/mod.css @@ -0,0 +1,128 @@ +.mod-reports { + display: block; + list-style: none; + margin: 0; + padding: 0; +} + .mod-report { + border: none; + border-bottom: 1px solid #B7C5D9; + clear: left; + + padding: 0.5em; + } + .mod-report:last-child { + border-bottom: none; + } + +.report-header { + margin: 0 0 0.25em 0; +} + +.report-list { + display: block; + list-style: none; + margin: 0; + padding: 0; +} + .report-item { + display: inline-block; + margin: 0; + padding: 0; + } + .report-item .report { + background: #D6DAF0; + margin: 0.2em 4px 0.2em 0; + padding: 0.3em 0.3em 0.5em 0.6em; + border-width: 1px; + border-style: none solid solid none; + border-color: #B7C5D9; + display: inline-block; + max-width: 94% !important; + } + .report-reason { + display: block; + font-size: 115%; + line-height: 115%; + } + + .report-details { + display: block; + margin: 0.3em 0 0 0; + padding: 0.3em 0 0 0; + clear: left; + list-style: none; + } + .report-detail { + display: block; + margin: 0; + padding: 0; + } + .detail-name { + display: inline-block; + min-width: 6.25em; + } + + .report-actions { + display: block; + border: none; + border-top: 1px solid #B7C5D9; + margin: 0.3em 0 0 0; + padding: 0.3em 0 0 0; + clear: left; + list-style: none; + } + .report-action { + display: inline-block; + margin: 0 0.5em 0 0; + padding: 0; + } + .report-action::after { + display: inline-block; + margin: 0 0 0 0.5em; + padding: 0; + content: ' | '; + } + .report-action:last-child::after { + display: none; + content: ''; + } + + +.report-content div.post.reply, +.report-content div.thread { + background: #D6DAF0; + margin: 0.2em 4px 0.2em 0; + padding: 0.3em 0.3em 0.5em 0.6em; + border-width: 1px; + border-style: none solid solid none; + border-color: #B7C5D9; + display: inline-block; + max-width: 94% !important; +} +.mod-report:hover .report-content div.post.reply, +.mod-report:hover .report-content div.thread { + background: #FFC4C4; + border-color: #F88; +} + .report-content-actions { + display: block; + padding: 0.3em 0; + clear: left; + list-style: none; + } + .report-content-action { + display: inline-block; + margin: 0 0.5em 0 0; + padding: 0; + } + .report-content-action::after { + display: inline-block; + margin: 0 0 0 0.5em; + padding: 0; + content: ' | '; + } + .report-content-action:last-child::after { + display: none; + content: ''; + } \ No newline at end of file diff --git a/stylesheets/style.css b/stylesheets/style.css index 1df3c526..0a3ed8eb 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -288,6 +288,18 @@ div.post.reply { max-width: 94%!important; } +div.post_modified { + min-width: 47.5em; + margin-left: 1.8em; + padding-top: 0.8em; +} + +div.post_modified div.content-status { + margin-top: 0.5em; + padding-bottom: 0em; + font-size: 72%; +} + span.trip { color: #228854; } @@ -327,6 +339,7 @@ div#wrap { margin: 0 auto; } +div.module, div.ban { background: white; border: 1px solid #98E; @@ -334,7 +347,8 @@ div.ban { margin: 30px auto; } -div.ban p,div.ban h2 { +div.ban p, +div.ban h2 { padding: 3px 7px; } @@ -658,7 +672,7 @@ pre { margin-left: -20px; } -div.thread:hover { +.theme-catalog div.thread:hover { background: #D6DAF0; border-color: #B7C5D9; } diff --git a/stylesheets/tomorrow.css b/stylesheets/tomorrow.css new file mode 100644 index 00000000..1ddfcd3b --- /dev/null +++ b/stylesheets/tomorrow.css @@ -0,0 +1,152 @@ +/** TOMORROW, I'LL ... + +A cool dark skin by 7185. +https://github.com/7185/8chan-tomorrow/ + +**/ +body { + background:#1d1f21 none; + color:#C5C8C6 +} +h1,div.subtitle { + color:#C5C8C6!important +} +a:link,a:visited,p.intro a.email span.name { + color:#81a2be +} +a:link:hover { + color:#5F89AC +} +a.post_no { + color:#C5C8C6 +} +a.post_no:hover { + color:#5F89AC!important +} +div.banner { + background-color:#1d1f21 +} +div.post.reply { + background-color:#282a2e; + border:1px solid #282a2e; + margin-bottom:2px; + margin-left:16px; + margin-top:2px +} +div.post.reply.highlighted { + background-color:#1d1d21; + border:1px solid #111 +} +div.post.reply div.body a { + color:#81a2be +} +div.post.reply div.body a:hover { + color:#5F89AC +} +div.post-hover { + border:1px solid #000!important; + box-shadow:none!important +} +div.thread:hover { + background-color:#1d1f21; + border-color:#000 +} +p.intro span.subject { + color:#b294bb +} +p.intro span.name { + color:#C5C8C6 +} +span.quote { + color:#adbd68 +} +span.heading { + color:#F20 +} +form table tr th { + background:#282a2e; + border:1px solid #111; + color:#C5C8C6 +} +div.ban h2 { + background:#FCA; + color:inherit +} +div.ban { + border-color:#800 +} +div.ban p { + color:#000 +} +div.pages { + background:#1d1f21; + border-color:#1d1f21 +} +div.pages a.selected { + color:#81a2be; + font-weight:700 +} +div.boardlist { + background-color:#282a2e!important; + color:#C5C8C6 +} +div.boardlist:nth-of-type(1) { + border-bottom:1px solid #111!important; + box-shadow:0 0 3px 0 #111 +} +div.boardlist a { + color:#81a2be +} +hr { + background-color:#282a2e; + border:0; + height:1px +} +div#options_div { + background-color:#282a2e +} +div.options_tab_icon { + color: #AAA +} +div.options_tab_icon:hover { + background-color: #111 +} +div.options_tab_icon.active { + color: #F20 +} +div.blotter { + color:#F20 +} +span.omitted { + color:#707070 +} +p.intro a, span.omitted a { + text-decoration:none +} +form#quick-reply { + padding-right:1px; + border: 1px solid #111 +} +span.capcode { + background-color: #000; + padding:2px 5px; + border-radius: 10px +} +div#watchlist { + border:1px solid #111; + background-color:#282a2e +} +div#watchlist a,a.watchThread { + color:#81a2be; + text-decoration:none +} +div#watchlist a:hover,a.watchThread:hover { + color:#5F89AC +} +/* Keep small thumbnails */ +a:not([data-expanded="true"]) .post-image{ + width:auto!important; + height:auto!important; + max-height:200px!important; + max-width:200px!important +} diff --git a/templates/8chan/index.html b/templates/8chan/index.html index 2bad668d..3e97809d 100644 --- a/templates/8chan/index.html +++ b/templates/8chan/index.html @@ -223,8 +223,7 @@ - - +
    diff --git a/templates/generic_page.html b/templates/generic_page.html index 0fe1f9c5..4cd31162 100644 --- a/templates/generic_page.html +++ b/templates/generic_page.html @@ -6,7 +6,7 @@ {{ board.url }} - {{ board.name }} {% endblock %} - + {{ boardlist.top }} {% if pm %}
    You have an unread PM{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.

    {% endif %} {% if config.url_banner %}{% endif %} diff --git a/templates/index.html b/templates/index.html index aae87c17..5638fd65 100644 --- a/templates/index.html +++ b/templates/index.html @@ -14,7 +14,7 @@ {% include 'header.html' %} {{ board.url }} - {{ board.title|e }} - + {{ boardlist.top }} {% if pm %}
    You have an unread PM{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.

    {% endif %} diff --git a/templates/main.js b/templates/main.js index 63233fab..0156e23b 100644 --- a/templates/main.js +++ b/templates/main.js @@ -96,11 +96,6 @@ var saved = {}; var selectedstyle = '{% endraw %}{{ config.default_stylesheet.0|addslashes }}{% raw %}'; -/*var styles = { - {% endraw %} - {% for stylesheet in stylesheets %}{% raw %}'{% endraw %}{{ stylesheet.name|addslashes }}{% raw %}' : '{% endraw %}{{ stylesheet.uri|addslashes }}{% raw %}', - {% endraw %}{% endfor %}{% raw %} -};*/ var board_name = false; function changeStyle(styleName, link) { @@ -115,36 +110,78 @@ function changeStyle(styleName, link) { {% endif %} {% raw %} - if (!document.getElementById('stylesheet')) { - var s = document.createElement('link'); - s.rel = 'stylesheet'; - s.type = 'text/css'; - s.id = 'stylesheet'; + // Find the for the stylesheet. May be nothing. + var domStylesheet = document.getElementById('stylesheet'); + // Determine if this stylesheet is the default. + var setToDefault = ( styles[styleName] == "" || styles[styleName] == "/stylesheets/" ); + // Turn "Yotsuba B" to "yotsuba_b" + var attributeName = styleName.replace(/[^a-z0-9_\-]/gi, '_').toLowerCase(); + + if( !domStylesheet && !setToDefault ) { + domStylesheet = document.createElement('link'); + domStylesheet.rel = 'stylesheet'; + domStylesheet.type = 'text/css'; + domStylesheet.id = 'stylesheet'; + var x = document.getElementsByTagName('head')[0]; - x.appendChild(s); + x.appendChild(domStylesheet); } - - {% endraw %} - var root = "{{ config.root }}"; - {% raw %} - root = root.replace(/\/$/, ""); - document.getElementById('stylesheet').href = root + styles[styleName]; - selectedstyle = styleName; - - if (document.getElementsByClassName('styles').length != 0) { - var styleLinks = document.getElementsByClassName('styles')[0].childNodes; - for (var i = 0; i < styleLinks.length; i++) { - styleLinks[i].className = ''; + if( !setToDefault ) { + {% endraw %} + var root = "{{ config.root }}"; + {% raw %} + root = root.replace(/\/$/, ""); + + domStylesheet.href = root + styles[styleName]; + selectedstyle = styleName; + + if (document.getElementsByClassName('styles').length != 0) { + var styleLinks = document.getElementsByClassName('styles')[0].childNodes; + for (var i = 0; i < styleLinks.length; i++) { + styleLinks[i].className = ''; + } + } + + if (link) { + link.className = 'selected'; } } - - if (link) { - link.className = 'selected'; + else if( domStylesheet ) { + domStylesheet.parentNode.removeChild( domStylesheet ); } - if (typeof $ != 'undefined') + // Fix the classes on the body tag. + var body = document.getElementsByTagName('body')[0]; + + if( body ) { + var bodyClasses = document.getElementsByTagName('body')[0].getAttribute('class').split(" "); + var bodyClassesNew = []; + + for( i = 0; i < bodyClasses.length; ++i ) { + var bodyClass = bodyClasses[ i ]; + + // null class from a double-space. + if( bodyClass == "" ) { + continue; + } + + if( bodyClass.indexOf( "stylesheet-" ) == 0 ) { + continue; + } + + bodyClassesNew.push( bodyClass ); + } + + // Add stylesheet-yotsuba_b at the end. + bodyClassesNew.push( "stylesheet-" + attributeName ); + body.setAttribute( 'class', bodyClassesNew.join(" ") ); + body.setAttribute( 'data-stylesheet', attributeName ); + } + + if (typeof $ != 'undefined') { $(window).trigger('stylesheet', styleName); + } } @@ -190,7 +227,7 @@ function init_stylechooser() { } } } - {% endraw%} + {% endraw %} {% else %} {% raw %} if (localStorage.stylesheet) { @@ -208,10 +245,13 @@ function init_stylechooser() { function get_cookie(cookie_name) { var results = document.cookie.match ( '(^|;) ?' + cookie_name + '=([^;]*)(;|$)'); - if (results) + + if (results) { return (unescape(results[2])); - else + } + else { return null; + } } function highlightReply(id) { diff --git a/templates/mod/dashboard.html b/templates/mod/dashboard.html index 4f766254..03cfbb14 100644 --- a/templates/mod/dashboard.html +++ b/templates/mod/dashboard.html @@ -1,40 +1,5 @@ -
    - {% trans 'Boards' %} - -
      - {% for board in boards %} - {% if board.uri in mod.boards or mod.boards[0] == '*' %} -
    • - {{ config.board_abbreviation|sprintf(board.uri) }} - - - {{ board.title|e }} - {% if board.subtitle %} - — - {% if config.allow_subtitle_html %} - {{ board.subtitle }} - {% else %} - {{ board.subtitle|e }} - {% endif %} - - - {% endif %} - {% if mod.type == "20" %} - [{% trans 'settings' %}] - {% endif %} - {% if mod|hasPermission(config.mod.manageboards) %} - [{% trans 'edit' %}] - {% endif %} -
    • - {% endif %} - {% endfor %} - - {% if mod|hasPermission(config.mod.newboard) %} -
    • {% trans 'Create new board' %}
    • - {% endif %} -
    -
    - -
    + +
    {% trans 'Messages' %}
      {% if mod|hasPermission(config.mod.noticeboard) %} @@ -78,7 +43,8 @@
    -
    + +
    {% trans 'Administration' %}
    + {% if mod|hasPermission(config.mod.search) %} -
    - {% trans 'Search' %} - -
      -
    • - {% include 'mod/search_form.html' %} -
    • -
    -
    + {% endif %} -{% if config.mod.dashboard_links|count %} -
    - {% trans 'Other' %} + +
    + {% trans 'Boards' %} -
      - {% for label,link in config.mod.dashboard_links %} -
    • {{ label }}
    • - {% endfor %} -
    -
    -{% endif %} - -{% if config.debug %} -
    - {% trans 'Debug' %} - -
    -{% endif %} - -{% if newer_release %} -
    - Update -
      +
        + {% for board in boards %} + {% if board.uri in mod.boards or mod.boards[0] == '*' %}
      • - A newer version of Tinyboard - (v{{ newer_release.massive }}.{{ newer_release.major }}.{{ newer_release.minor }}) is available! - See http://tinyboard.org/ for upgrade instructions. + {{ config.board_abbreviation|sprintf(board.uri) }} + - + {{ board.title|e }} + {% if board.subtitle %} + — + {% if config.allow_subtitle_html %} + {{ board.subtitle }} + {% else %} + {{ board.subtitle|e }} + {% endif %} + + + {% endif %} + {% if mod.type == "20" %} + [{% trans 'settings' %}] + {% endif %} + {% if mod|hasPermission(config.mod.manageboards) %} + [{% trans 'edit' %}] + {% endif %}
      • -
      -
    + {% endif %} + {% endfor %} + + {% if mod|hasPermission(config.mod.newboard) %} +
  • {% trans 'Create new board' %}
  • + {% endif %} + +
    + + +{% if config.mod.dashboard_links|count %} +
    + {% trans 'Other' %} + +
      + {% for label,link in config.mod.dashboard_links %} +
    • {{ label }}
    • + {% endfor %} +
    +
    {% endif %} -
    + +{% if config.debug %} +
    + {% trans 'Debug' %} + +
    +{% endif %} + + +{% if newer_release %} +
    + Update +
      +
    • + A newer version of Tinyboard + (v{{ newer_release.massive }}.{{ newer_release.major }}.{{ newer_release.minor }}) is available! + See http://tinyboard.org/ for upgrade instructions. +
    • +
    +
    +{% endif %} + + +

    {% endfilter %} diff --git a/templates/post_thread.html b/templates/post_thread.html index 51d1fb0c..33e953ee 100644 --- a/templates/post_thread.html +++ b/templates/post_thread.html @@ -1,7 +1,7 @@ {% filter remove_whitespace %} {# tabs and new lines will be ignored #} -
    +
    {% if not index %}{% endif %} {% include 'post/fileinfo.html' %} @@ -55,7 +55,6 @@ {% if post.modifiers['ban message'] %} {{ config.mod.ban_message|sprintf(post.modifiers['ban message']) }} {% endif %} - {% include 'post/edited_at.html' %}
    {% if post.omitted or post.omitted_images %} @@ -78,6 +77,7 @@ {% endif %} {% trans %}omitted. Click reply to view.{% endtrans %} {% endif %} + {% include 'post/edited_at.html' %} {% if not index %} {% endif %}
    {% endfilter %} diff --git a/templates/themes/basic/index.html b/templates/themes/basic/index.html index 3376a68f..0ec18f9a 100644 --- a/templates/themes/basic/index.html +++ b/templates/themes/basic/index.html @@ -11,7 +11,7 @@ {% if config.default_stylesheet.1 != '' %}{% endif %} {% if config.font_awesome %}{% endif %} - + {{ boardlist.top }}

    {{ settings.title }}

    diff --git a/templates/thread.html b/templates/thread.html index af2a6e08..a3ace9b5 100644 --- a/templates/thread.html +++ b/templates/thread.html @@ -10,7 +10,7 @@ {% include 'header.html' %} {{ board.url }} - {% if config.thread_subject_in_title and thread.subject %}{{ thread.subject }}{% else %}{{ board.title|e }}{% endif %} - + {{ boardlist.top }} {% if pm %}
    You have an unread PM{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.

    {% endif %} {% if config.url_banner %}{% endif %}