diff --git a/srv/index.php b/srv/index.php new file mode 100644 index 0000000..1b78050 --- /dev/null +++ b/srv/index.php @@ -0,0 +1,4384 @@ +Redirection in about 3 secs."; + $print_string .= " If that does not work, go"; + $print_string .= " back."; + + echo "$print_string"; +} + +// Translate simple bbcode to html, and highlight quotes, like so: +// [b bold],[i italic],[u underlined],[s strikethrough] +// [h headline],[sp spoiler],[li list element],[url link],>>quote\r\n +function bbcode_to_html($text, $settings) +{ + if ( ($settings['enable_bbcode'] == FALSE) ) { + return $text; + } + + $search = array ( + '/(\[b\ )(.*)(\])/', + '/(\[i\ )(.*)(\])/', + '/(\[u\ )(.*)(\])/', + '/(\[s\ )(.*)(\])/', + '/(\[h\ )(.*)(\])/', + '/(\[sp\ )(.*)(\])/', + '/(\[li\ )(.*)(\])/', + '/(\[url\ )(.*)(\])/', + '/>>(.*)\r\n/' + ); + + $replace = array ( + '$2', + '$2', + '$2', + '$2', + '

$2

', + '$2', + '
  • $2
  • ', + '$2', + '>>$1$2
    ' + ); + + return preg_replace($search, $replace, $text); + +} + +// checks if the bot trap has been called recently from the ip. +// according to the parameters in the config file, the request is then +// either granted or blocked. +// if the blocking of tor is enabled, 127.0.0.1 will be included in the +// blocking, which can mean that no connections from tor or local are taken +// during the block time. +function bot_block($db, $settings, $ip) +{ + if ( ($settings['enable_bot_block'] != TRUE) ) { + return; + } + + if ( ($settings['enable_tor_block'] != TRUE) + && ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') ) { + return; + } + + $current = time(); + $max_age = $current - ($settings['block_time'] * 60); + // max age is in minutes, so times 60 to go to seconds + + if ($settings['superstrict_block'] == TRUE) { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type = 'bot' + AND ip = '$ip' + AND event in ('Level 2', + 'Level 1', + '429') + AND unix_timestamp > '$max_age'"); + } else { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type = 'bot' + AND ip = '$ip' + AND event in ('Level 2', + 'Level 1') + AND unix_timestamp > '$max_age'"); + } + + $result = $statement->execute(); + + $trap_visits = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $trap_visits++; + } + + if ( ($trap_visits > $settings['max_trap_visits']) ) { + $bot_block_message = "429"; + log_event($db, $settings, "bot", $bot_block_message, $ip); + header("HTTP/1.1 429 Too Many Requests"); + quit($db, "429"); + } + + if ( ($settings['max_landing'] > 0) + && ($settings['superstrict_block'] == TRUE) ) { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type = 'bot' + AND ip = '$ip' + AND event in ('landing page bot request', + '429') + AND unix_timestamp > '$max_age'"); + } elseif ( ($settings['max_landing'] > 0) ) { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type = 'bot' + AND ip = '$ip' + AND event = 'landing page bot request' + AND unix_timestamp > '$max_age'"); + } else { + return; + } + + $result = $statement->execute(); + + $landing_visits = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $landing_visits++; + } + + if ( ($landing_visits > $settings['max_landing']) ) { + $bot_block_message = "429"; + log_event($db, $settings, "bot", $bot_block_message, $ip); + header("HTTP/1.1 429 Too Many Requests"); + quit($db, "429"); + } + + return; +} +// receive a message from bot, check if it's ok to post, and do it (or quit) +function bot_me($db, $settings) +{ + + $json_data = json_decode(file_get_contents('php://input'), TRUE); + + if ( (!isset($json_data['text'])) || (!isset($json_data['sub'])) ) { + header("HTTP/1.1 400 Bad Request"); + quit($db,"400"); + } + + $sub = substr(preg_replace("/[^0-9a-zA-Z]/", "", $json_data['sub']), + 0, $settings['max_name_sub']); + + if (!in_array($sub, $settings['anonymous_bot_subs'])) { + if (isset($json_data['key'])) { + $key = substr(preg_replace("/[^0-9a-zA-Z]/", "", + $json_data['key']), 0, 20); + // 20 chars is enough for a bot key + if (!in_array($key, $settings['bot_keys'])) { + $auth_message = "bot used wrong key"; + log_event($db, $settings, "auth", $auth_message, ''); + sleep(10); + header("HTTP/1.1 401 Unauthorized"); + quit($db,"401"); + } + } else { + header("HTTP/1.1 400 Bad Request"); + quit($db,"400"); + } + } + + if (isset($json_data['org_id'])) { + $org_id = substr(preg_replace("/[^0-9a-zA-Z]/", "", + $json_data['org_id']), 0, $settings['max_name_sub']); + } else { + $org_id = ''; + } + + $text = strip_tags($json_data['text']); + check_spam($db, $text, $settings); + $text_id = hash("sha512", $text); + + if ( (check_original_content + ($db, $settings, $sub, $text_id, $org_id) == FALSE) ) { + header("HTTP/1.1 403 Forbidden"); + $content_message = "This text has been posted before, the admin"; + $content_message .= " requests original content."; + quit($db, $content_message); + } + + make_post($db, $sub, $settings, $text, $org_id); +} + +// If the post is a reply, put the original post on top. +function bump_message($db, $org_id, $sub) +{ + + $statement = $db->prepare("SELECT text, global_id, text_id + FROM threads + WHERE post_id = '$org_id' + AND org_id = '$org_id' + AND sub = '$sub'"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $text = "{$row[0]}"; + $global_id = "{$row[1]}"; + $text_id = "{$row[2]}"; + } + + $statement = $db->prepare("DELETE FROM threads + WHERE post_id = '$org_id' + AND org_id = '$org_id' + AND sub = '$sub'"); + $result = $statement->execute(); + + $statement = $db->prepare("INSERT INTO threads(post_id, sub, text, + org_id, shadow, + global_id, text_id) + VALUES ('$org_id', '$sub', ?, '$org_id', + 'no', '$global_id', '$text_id')"); + $statement->bindParam(1, $text); + + $statement->execute(); + +} + +// Break text according to config.php, also transform \r\n to
    +function break_text($text, $settings) +{ + + $post_text = wordwrap($text, $settings['line_break'], "\n", TRUE); + $post_text = nl2br($post_text, FALSE); + return $post_text; + +} + +// Lets the admin change the status of a mod or an applicant +function change_mods($db, $css, $settings, $token) +{ + + if ( (isset($_POST['disable_mod'])) ) { + $mod = strip_tags($_POST['disable_mod']); + $statement = $db->prepare("UPDATE keys + SET type = 'disabled' + WHERE name = '$mod'"); + $log_message = "Moderator account $mod was disabled"; + } elseif ( (isset($_POST['enable_mod'])) ) { + $mod = strip_tags($_POST['enable_mod']); + $statement = $db->prepare("UPDATE keys + SET type = 'mod' + WHERE name = '$mod'"); + $log_message = "Moderator account $mod was enabled"; + } elseif ( (isset($_POST['delete_mod'])) ) { + $mod = strip_tags($_POST['delete_mod']); + $statement = $db->prepare("DELETE FROM keys + WHERE name = '$mod'"); + $log_message = "Moderator account $mod was deleted"; + } + + $result = $statement->execute(); + + log_event($db, $settings, "sys", $log_message, ''); + +} + +// Check if the password for the admin is set or not. +// If it was not done already, generate token and write to db and the +// file /var/opt/endboard/admin_$name_token.txt +function check_admin($db, $settings) +{ + + if ( ($settings['admin'] == 'change-me') + || ($settings['admin'] == '') ) { + $config_file = $settings['config_file']; + $admin_message = "

    The name of the admin is not updated "; + $admin_message .= "in the configfile, or it is empty. Set "; + $admin_message .= "the variable \$admin in the file "; + $admin_message .= " $config_file and retry.

    "; + quit($db, $admin_message); + } + + $admin = $settings['admin']; + $filename = $settings['work_dir'] . "admin_" . $admin . "_token.txt"; + + $statement = $db->prepare("SELECT key FROM keys + WHERE type = 'admin' + AND name = '$admin'"); + $result = $statement->execute(); + + $key = ''; + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $key = "{$row[0]}"; + $counter++; + } + + if ( ($key == '') && (!file_exists($filename)) ) { + $token = make_token(); + $token_hash = password_hash($token, PASSWORD_DEFAULT); + + if ($counter == 0) { + $statement = $db->prepare("INSERT INTO keys(token, type, name) + VALUES ('$token_hash', + 'admin', + '$admin')"); + } else { + $statement = $db->prepare("UPDATE keys + SET token = '$token_hash' + WHERE name = '$admin' + AND type = 'admin'"); + } + + $result = $statement->execute(); + + $content = "Token = \r\n$token\r\n"; + file_put_contents($filename, $content); + + return FALSE; + } elseif ( ($key == '') ) { + return FALSE; + } else { + return TRUE; + } + +} + +// Check if access to the admin panel is enabled in the config file. +function check_admin_panel($db, $settings) +{ + + if ($settings['enable_admin_panel'] != TRUE) { + header("HTTP/1.1 403 Forbidden"); + quit($db, "403"); + } + +} + +// Checks if the number of moderators applications is more than 10. +function check_application_count($db) +{ + + $statement = $db->prepare("SELECT name + FROM keys + WHERE type = 'application'"); + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + if ($counter > 10) { + // 10 seems reasonable in order not to loose the overview + return FALSE; + } else { + return TRUE; + } +} + +// Check if access to the admin panel and its functions can be given +// or not. First, a quick check if there is an admin defined, and if +// the default name was changed. Second, the token is checked, if there +// is one. If there is none, user/password is checked against the db. +// If the login is successful, a token is created and given back. +function check_auth_admin($db, $settings) +{ + + $token = read_pretty_vars('last', 'alnum', 250); + // The length of the token is 250 characters + + if ( (mb_strlen($token) !== 250) ) { + // The length of the token is 250 characters + $token = get_post_token(); + } + + if ( (mb_strlen($token) === 250) ) { + // The length of the token is 250 characters + $statement = $db->prepare("SELECT timestamp_token, token + FROM keys + WHERE type = 'admin'"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $timestamp_token = "{$row[0]}"; + $access_token = "{$row[1]}"; + } + + $current = time(); + $max_age = $current - ($settings['lifetime_token'] * 60); + // max lifetime is defined in minutes, times 60 to go to seconds + + if ( ($timestamp_token < $max_age) ) { + $auth_message = "outdated token used"; + log_event($db, $settings, "auth", $auth_message, ''); + $quit_message = "

    Token expired.Log in "; + $quit_message .= "again to get a new one."; + $quit_message .= "This incident was logged.

    "; + quit($db, $quit_message); + } + + if ( (!password_verify($token, $access_token)) ) { + $auth_message = "invalid token used"; + log_event($db, $settings, "auth", $auth_message, ''); + sleep(10); + $quit_message = "

    Invalid token used."; + $quit_message .= "This incident was logged.

    "; + quit($db, $quit_message); + } + + } elseif ( (!isset($_POST['auth_name'])) + || (!isset($_POST['auth_password'])) ) { + $quit_message = "

    Please provide the name and"; + $quit_message .= " password for your account.

    "; + quit($db, $quit_message); + } else { + $auth_name = substr(preg_replace("/[^0-9a-zA-Z]/", "", + $_POST['auth_name']), 0, 50); + // 50 seems to be a generous for the length of the username + $statement = $db->prepare("SELECT key + FROM keys + WHERE type = 'admin' + AND name = '$auth_name'"); + $result = $statement->execute(); + + $key = ''; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $key = "{$row[0]}"; + } + + if ( (!password_verify($_POST['auth_password'], $key)) ) { + $auth_message = "wrong combination user/password"; + log_event($db, $settings, "auth", $auth_message, ''); + sleep(10); + $quit_message = "

    Combination of password and username "; + $quit_message .= "is invalid. This incident was logged.

    "; + quit($db, $quit_message); + } + + $token = make_token(); + $token_hash = password_hash($token, PASSWORD_DEFAULT); + + $current = time(); + + $statement = $db->prepare("UPDATE keys + SET timestamp_token = '$current', + token = '$token_hash' + WHERE name = '$auth_name' + AND type = 'admin'"); + $result = $statement->execute(); + + $auth_message = "admin $auth_name logged on"; + log_event($db, $settings, "auth", $auth_message, ''); + } + + return $token; +} + +// Check if access to the mod panel and can be given or not. +// The token is checked, if there is one. If there is none, +// user/password is checked against the db. +// If the login is successful, a token is created and given back. +function check_auth_mod($db, $settings) +{ + + $token = read_pretty_vars('last', 'alnum', 250); + // the length of the token is defined to 250 characters, so that it + // cannot be memorized, + // and so that it does never show fully in the addressbar + + if ( (mb_strlen($token) !== 250) ) { + // token length = 250 + $token = get_post_token(); + } + + if ( (mb_strlen($token) === 250) ) { + // token length = 250 + $statement = $db->prepare("SELECT timestamp_token, token + FROM keys + WHERE type = 'mod'"); + $result = $statement->execute(); + + $found_token = FALSE; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $timestamp_token = "{$row[0]}"; + $access_token = "{$row[1]}"; + if ( (password_verify($token, $access_token)) ) { + $found_token = TRUE; + break; + } + } + + if ( ($found_token == FALSE) ) { + $auth_message = "invalid token used"; + log_event($db, $settings, "auth", $auth_message, ''); + quit($db, "

    Token invalid. This incident was logged.

    "); + } + + $current = time(); + $max_age = $current - ($settings['lifetime_token'] * 60); + // lifetime of token is in minutes, times 60 to go to seconds + + if ($timestamp_token < $max_age) { + $auth_message = ("outdated token used"); + log_event($db, $settings, "auth", $auth_message, ''); + $quit_message = "

    Token expired.Log in"; + $quit_message .= " again to get a new one. "; + $quit_message .= "This incident was logged.

    "; + quit($db, $quit_message); + } + } elseif ( (!isset($_POST['auth_name'])) + || (!isset($_POST['auth_password'])) ) { + $quit_message = "

    Please provide the name"; + $quit_message .= " and password for your account.

    "; + quit($db, $quit_message); + } else { + $auth_name = substr(preg_replace("/[^0-9a-zA-Z]/", "", + $_POST['auth_name']), 0, 50); + // 50 seems to be a generous for the length of the username + $statement = $db->prepare("SELECT key FROM keys + WHERE type = 'mod' + AND name = '$auth_name'"); + $result = $statement->execute(); + + $key = ''; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $key = "{$row[0]}"; + } + + if ( (!password_verify($_POST['auth_password'], $key)) ) { + $auth_message = "wrong combination user/password"; + log_event($db, $settings, "auth", $auth_message, ''); + sleep(10); + $quit_message = "

    Combination of password and username is"; + $quit_message .= " invalid. This incident was logged.

    "; + quit($db, $quit_message); + } + + $token = make_token(); + $token_hash = password_hash($token, PASSWORD_DEFAULT); + + $current = time(); + + $statement = $db->prepare("UPDATE keys + SET timestamp_token = '$current', + token = '$token_hash' + WHERE name = '$auth_name'"); + $result = $statement->execute(); + + $auth_message = "mod $auth_name logged on"; + log_event($db, $settings, "auth", $auth_message, ''); + } + + return $token; +} + +// Check the hashed captcha against the hashed solutions in the db. +function check_captcha($db, $settings) +{ + if ( (!isset($_POST['post_token'])) ) { + quit($db, "

    What are you up to ? Use the postform.

    "); + } + + if ( ($settings['use_captcha'] == FALSE) ){ + $post_hash = hash("sha512", $_POST['post_token']); + } elseif ( (!isset($_POST['math_one'])) + || (!isset($_POST['math_two'])) + || (!isset($_POST['math_type'])) + || (!isset($_POST['math_answer'])) ) { + quit($db, "

    What are you up to ? Use the postform.

    "); + } else { + $post_summary = ($_POST['math_one'] . $_POST['math_two'] . + $_POST['math_type'] . $_POST['math_answer'] . + $_POST['post_token']); + $post_hash = hash("sha512", $post_summary); + } + + $current = time(); + $max_age = $current - $settings['lifetime_captcha'] * 60 * 60; + // lifetime is in hours in the configfile, + // so times 60 * 60 to go to seconds + + $statement = $db->prepare("SELECT hash, unix_timestamp + FROM captchas + WHERE hash = '$post_hash' + AND unix_timestamp > '$max_age'"); + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + if ( ($settings['use_captcha'] == FALSE) && ($counter < 1) ) { + $quit_message = "

    Unauthorized attempt to post."; + $quit_message .= "Use a newly opened postform.

    "; + quit($db, $quit_message); + } elseif ( ($counter < 1) ) { + quit($db, "

    wrong answer or captcha expired, try again

    "); + } else { + $statement = $db->prepare("DELETE FROM captchas + WHERE hash = '$post_hash'"); + $result = $statement->execute(); + } +} + +// Check if we have enough free space on the harddisk to allow new posts +function check_free_space($db, $settings) +{ + + $free_space = disk_free_space("/"); + + if ($free_space < ($settings['min_space'] * 1024 * 1024)) { + // the setting is in Megabyte, free space operates in bytes + return FALSE; + } + + return TRUE; +} + +// Check if the maximum requests as defined in the config file are +// exhausted or not +function check_max_requests($db, $settings, $ip) +{ + + $current = time(); + + if ( ($settings['max_requests_ip'] > 0) + && ($_SERVER['REMOTE_ADDR'] != '127.0.0.1') ) { + $max_age = $current - ($settings['max_requests_timeframe'] * 60); + // max age is in minutes, so times 60 to go to seconds + $max_visits = $settings['max_requests_ip']; + } elseif ( ($settings['max_requests_tor'] > 0) + && ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') ) { + $max_age = $current - ($settings['max_requests_tor_timeframe'] * 60); + // max age is in minutes, so times 60 to go to seconds + $max_visits = $settings['max_requests_tor']; + } else { + return; + } + + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type = 'portal' + AND ip = '$ip' + AND event in ('visit') + AND unix_timestamp > '$max_age'"); + + $result = $statement->execute(); + + $visits = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $visits++; + } + + if ( ($visits > $max_visits) ) { + $block_message = "429"; + log_event($db, $settings, "user", $block_message, $ip); + header("HTTP/1.1 429 Too Many Requests"); + quit($db, "429"); + } +} + +// Check if access to the mod panel is enabled in the config file. +function check_mod_panel($db, $settings) +{ + + if ($settings['enable_mod_panel'] != TRUE) { + header("HTTP/1.1 403 Forbidden"); + quit($db, "403"); + } + +} + +// checks if content has been posted before, according the config file. +function check_original_content($db, $settings, $sub, $text_id, $org_id) +{ + $statement = $db->prepare("SELECT post_id, sub, org_id + FROM threads + WHERE text_id = '$text_id'"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $result_post_id = "{$row[0]}"; + $result_sub = "{$row[1]}"; + $result_org_id = "{$row[2]}"; + if ( ($settings['original_content_global'] == TRUE) ){ + return FALSE; + } elseif ( ($settings['original_content_sub'] == TRUE) + && ($sub == $result_sub) ){ + return FALSE; + } elseif ( ($settings['original_content_thread'] == TRUE) + && ($sub == $result_sub) + && ($org_id == $result_org_id) ){ + return FALSE; + } + } + + return TRUE; +} + +// Check if a message actually exists in the database when replying. +// Note that if the links of the site are used for navigation, this is +// always the case (unless it was deleted meanwhile). +// This routine is mostly to prevent malicious users from creating +// ghost messages that are in the db but are never displayed +// (because the threadstart is missing). +function check_org_id_exists($db, $sub, $org_id) +{ + + $statement = $db->prepare("SELECT post_id + FROM threads + WHERE sub = '$sub' + AND shadow = 'no' + AND post_id = '$org_id'"); + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + if ( ($counter < 1) ) { + // if the counter is smaller 1, there is no match + return FALSE; + } else { + return TRUE; + } +} + +// check if the ip has already passed the portal, in this case return. +// if not, display a simple text and button to click to proceed. +// the page displayed is done with inline styling, so that no +// additional files will be requested. +// Update: what started with the checking of the ip, has now expanded to +// up to six parameters, which are concantenated and hashed. +function check_portal($db, $settings, $ip) +{ + if ( ($settings['enable_portal'] != TRUE) ) { + return; + } + + if ( ($settings['enable_portal_tor'] != TRUE) + && ($_SERVER['REMOTE_ADDR'] == '127.0.0.1') ) { + return; + } + + $current = time(); + $max_age = $current - ($settings['portal_lifetime'] * 60); + // lifetime is in minutes, so times 60 to go to seconds + + if ($settings['auto_prolong_portal'] == TRUE) { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type = 'portal' + AND ip = '$ip' + AND event in ('pass', + 'visit') + AND unix_timestamp > '$max_age' + ORDER BY ROWID DESC LIMIT 1"); + } else { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type = 'portal' + AND ip = '$ip' + AND event in ('pass') + AND unix_timestamp > '$max_age' + ORDER BY ROWID DESC LIMIT 1"); + } + + $result = $statement->execute(); + + $portal_pass = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $portal_pass++; + } + + if ( ($portal_pass > 0) ) { + // bigger zero means we have the last hit, meaning the ip + // is known + $portal_message = "visit"; + log_event($db, $settings, "portal", $portal_message, $ip); + return; + } else { + $request = $_SERVER['REQUEST_URI']; + header("HTTP/1.1 202 Accepted"); + $html_string = "


    "; + $html_string .= "Entry portal: "; + $html_string .= "Please click the button to proceed."; + $html_string .= "





    "; + $html_string .= "
    "; + $html_string .= ""; + $html_string .= "
    "; + $html_string .= "
    "; + $html_string .= "





    Sorry for this,"; + $html_string .= " it's just a lowlevel protection "; + $html_string .= "against scraping bots."; + echo "$html_string"; + quit($db, ''); + } +} + +// Check if a message exists already in the database when importing. +function check_post_exists($db, $sub, $org_id, $post_id) +{ + + $statement = $db->prepare("SELECT post_id + FROM threads + WHERE sub = '$sub' + AND org_id = '$org_id' + AND post_id = '$post_id'"); + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + if ( ($counter < 1) ) { + // if the counter is smaller 1, there is no match + return FALSE; + } else { + return TRUE; + } +} + +// A simple check if the post is spam or not, also if it is too long +// or too short. +// These are a few of the only original lines of code left from smolBBS. +function check_spam($db, $text, $settings) +{ + + if (preg_match('/^(.)\1*$/u ', $text)) { + quit($db, "

    Spam detected!

    "); + } + + $post_length = strlen($text); + + if ($post_length < $settings['min_char']) { + quit($db, "

    Post too short!

    "); + } + + if ($post_length > $settings['max_char']) { + quit($db, "

    Post too long!

    "); + } + + $text = str_replace(array("\n","\r"), '', $text); + + if (substr_count($text, ' ') === strlen($text)) { + quit($db, "

    Spam detected! Post contained only spaces!

    "); + } +//rewrite, does not work for all cases + if (ctype_space($text)) { + $quit_message = "

    Spam detected! Post contained only spaces "; + $quit_message .= "(yeah, Unicode...)!

    "; + quit($db, $quit_message); + } + +} + +// Check if a sub exists before display +// If the name does not exist, name* is searched +function check_sub_exists($db, $sub) +{ + + if ( ($sub == 'overboard') || ($sub == 'main') ) { + return $sub; + } + + $statement = $db->prepare("SELECT sub + FROM threads + WHERE sub = '$sub' + AND shadow = 'no'"); + + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + $sub = "{$row[0]}"; + } + + if ( ($counter < 1) ) { + // if the counter is smaller 1, there is no match + $statement = $db->prepare("SELECT sub + FROM threads + WHERE sub GLOB '$sub*' + AND shadow = 'no' + ORDER BY sub + ASC LIMIT 1"); + + $result = $statement->execute(); + + $counter_2 = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter_2++; + $sub = "{$row[0]}"; + } + + if ( ($counter_2 < 1) ) { + // if the counter is smaller 1, there is no match + return FALSE; + } else { + return $sub; + } + + } else { + return $sub; + } +} + +// record $_SERVER info + +function debug_server($db, $settings) + +{ + + + + $filename = $settings['work_dir'] . 'debug_server.txt'; + + $content = print_r($_SERVER, TRUE); + + file_put_contents($filename, $content, FILE_APPEND | LOCK_EX); + + + +} + + +// Delete selected logs +function delete_logs($db, $type, $token) +{ + + if ($type == 'all') { + $statement = $db->prepare("DELETE FROM logs"); + $delete_message = "all logs were deleted"; + } else { + $statement = $db->prepare("DELETE FROM logs + WHERE type = '$type'"); + $delete_message = "logs of type $type were deleted"; + } + + $result = $statement->execute(); + + echo "$delete_message"; +} + +// Delete one post or a range of posts. +// If you use php8.* you can change to str_contains below. +function delete_posts($db, $sub, $settings, $token, $delete_mode) +{ +//rewrite to check if post exists first, +// and only move it if it is not shadowed + if ($delete_mode == 'shadow') { + $posts = strip_tags($_POST['shadow_posts']); + } elseif ($delete_mode == 'move') { + $posts = strip_tags($_POST['shadow_posts']); + $target_sub = substr(preg_replace("/[^0-9a-zA-Z]/", "", + $_POST['target_sub']), 0, + $settings['max_name_sub']); + if ( (check_sub_exists($db, $target_sub) != TRUE) ) { + quit($db, "

    Sub $target_sub does not exist.

    "); + } + } else { + $posts = strip_tags($_POST['delete_posts']); + } + +// if (str_contains($posts, '-')) { +// only uncomment if you use php8.* + if (strpos($posts, '-') !== FALSE) { + $delete_minmax = explode('-', $posts); + $delete_array = array(); + for($delete = $delete_minmax['0']; + $delete <= $delete_minmax['1']; $delete++) { + array_push($delete_array, $delete); + } + } else { + $delete_array = explode(' ', $posts); + } + + $statement = $db->prepare("SELECT name, token + FROM keys + WHERE type = 'mod'"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $name = "{$row[0]}"; + $access_token = "{$row[1]}"; + if ( (password_verify($token, $access_token)) ) { + break; + } + } + + $html_string = ''; + + if ( (!empty($delete_array)) && ($delete_mode == 'delete') ) { + + foreach($delete_array as $delete_post) { + $statement = $db->prepare("DELETE FROM threads + WHERE post_id = '$delete_post' + AND sub = '$sub'"); + $result = $statement->execute(); + + $delete_message = "message $delete_post from "; + $delete_message .= "sub $sub deleted by $name"; + log_event($db, $settings, "del", $delete_message, ''); + $html_string .= "

    $delete_message

    "; + + } + + } elseif ( (!empty($delete_array)) && ($delete_mode == 'shadow') ) { + + foreach($delete_array as $delete_post) { + $statement = $db->prepare("UPDATE threads + SET shadow = 'yes' + WHERE post_id = '$delete_post' + AND sub = '$sub'"); + $result = $statement->execute(); + + $delete_message = "message $delete_post from "; + $delete_message .= "sub $sub shadowed by $name"; + log_event($db, $settings, "del", $delete_message, ''); + + $html_string .= "

    $delete_message

    "; + } + + } elseif ( (!empty($delete_array)) + && ($delete_mode == 'move') ) { + + move_post($db, $settings, $delete_array, $sub, $target_sub, $name); + + } + + $html_string .= "

    Done.

    "; + + if ($delete_mode == 'delete') { + $html_string .= "

    Back

    "; + } else { + $html_string .= "

    Back

    "; + } + + echo "$html_string"; +} + +// Delete or shadow a whole sub +function delete_sub($db, $sub, $settings, $token, $shadow_only) +{ + + $token_hash = hash("sha512",$token); + $statement = $db->prepare("SELECT name + FROM keys + WHERE token = '$token_hash'"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $name = "{$row[0]}"; + } + + $html_string = ''; + + if ( ($sub != '') && ($shadow_only == 'no') ) { + + $statement = $db->prepare("DELETE FROM threads + WHERE sub = '$sub'"); + $result = $statement->execute(); + + $delete_message = "sub $sub deleted by $name"; + log_event($db, $settings, "del", $delete_message, ''); + + $html_string .= "

    $delete_message

    "; + + } elseif ( ($sub != '') && ($shadow_only == 'yes') ) { + + $statement = $db->prepare("UPDATE threads + SET shadow = 'yes' + WHERE sub = '$sub'"); + $result = $statement->execute(); + + $delete_message = "sub $sub shadowed by $name"; + log_event($db, $settings, "del", $delete_message, ''); + + $html_string .= "

    $delete_message

    "; + } + + $html_string .= "

    Done.

    "; + + if ($shadow_only == 'yes') { + $html_string .= "

    Back

    "; + } else { + $html_string .= "

    Back

    "; + } + + echo "$html_string"; +} + +// Delete an admin or mod token to end the session +function destroy_token($db, $token, $settings) +{ + $name = ''; + $token_hash = hash("sha512", $token); + $statement = $db->prepare("SELECT name + FROM keys + WHERE token = '$token_hash'"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $name = "{$row[0]}"; + } + + if ($name == '') { + $logoff_message = "token used to log off was invalid"; + } else { + $logoff_message = "$name logged off"; + } + + log_event($db, $settings, "auth", $logoff_message, ''); + + $statement = $db->prepare("UPDATE keys + SET token = 'none' + WHERE token = '$token_hash'"); + $result = $statement->execute(); + +} + +// Dump the contents of a sub,a thread or the whole board to a json +// file and send it to the browser. +// If used on the overboard, it will dump everything, including the +// messages from subs that are not actually displayed on the overboard. +function dump($db, $sub, $org_id, $settings) +{ +// rewrite to include ranges + header('Content-Type: application/json'); + + if ($org_id != '') { + $statement = $db->prepare("SELECT post_id, text + FROM threads + WHERE sub = '$sub' + AND shadow = 'no' + AND org_id = '$org_id'"); + $result = $statement->execute(); + $thread_dump = array(); + $thread_dump['sub'] = $sub; + $thread_dump['org_id'] = $org_id; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $post = array(); + $post['post_id'] = "{$row[0]}"; + $post['text'] = "{$row[1]}"; + array_push($thread_dump, $post); + } + + echo json_encode($thread_dump, JSON_PRETTY_PRINT + | JSON_NUMERIC_CHECK + | JSON_UNESCAPED_UNICODE); + + } elseif ($sub == 'overboard') { + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE shadow = 'no'"); + $result = $statement->execute(); + $overboard_dump = array(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $post = array(); + $post['post_id'] = "{$row[0]}"; + $post['org_id'] = "{$row[1]}"; + $post['sub'] = "{$row[2]}"; + $post['text'] = "{$row[3]}"; + array_push($overboard_dump, $post); + } + + echo json_encode($overboard_dump, JSON_PRETTY_PRINT + | JSON_NUMERIC_CHECK + | JSON_UNESCAPED_UNICODE); + + } else { + $statement = $db->prepare("SELECT post_id, org_id, text + FROM threads + WHERE sub = '$sub' + AND shadow = 'no'"); + $result = $statement->execute(); + $sub_dump = array(); + $sub_dump['sub'] = $sub; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $post = array(); + $post['post_id'] = "{$row[0]}"; + $post['org_id'] = "{$row[1]}"; + $post['text'] = "{$row[2]}"; + array_push($sub_dump, $post); + } + + echo json_encode($sub_dump, JSON_PRETTY_PRINT + | JSON_NUMERIC_CHECK + | JSON_UNESCAPED_UNICODE); + + } +} + +// Prevents additional tries to log in after too many failed tries. +// To be configured in the config file. +// Note: if enabled, this function can be used to dos the access to the +// admin and mod panels by sending constant junk requests. +// Still better than someone trying millions of user/password +// combinations, right ? +// If really paranoid, enable the admin panel only if you use it or +// let it listen on another address altogether. +// Read your logfiles to check if attacks are ongoing. +function fail2ban($db, $settings) +{ + + if ($settings['enable_fail2ban'] != TRUE) { + return; + } + + $current = time(); + $max_age = $current - ($settings['auth_time_frame'] * 60); + // max age is in minutes, so times 60 to go to seconds + + if ($settings['superstrict'] == TRUE) { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type = 'auth' + AND event in ( + 'wrong combination user/password', + 'invalid token used', + 'wrong combination name/token', + 'fail2ban triggered') + AND unix_timestamp > '$max_age'"); + } else { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type = 'auth' + AND event in ( + 'wrong combination user/password', + 'invalid token used', + 'wrong combination name/token') + AND unix_timestamp > '$max_age'"); + } + + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + if ($counter > $settings['auth_max']) { + $fail2ban_message = "fail2ban triggered"; + log_event($db, $settings, "auth", $fail2ban_message, ''); + header("HTTP/1.1 429 Too Many Requests"); + quit($db, "429"); + } +} + +// Select a new post_id for a new post. It is one higher than the +// previous existing highest number. +// This means in theory that if a message is deleted the number could +// be assigned to a different one (if it was the latest message that +// was deleted). +// This behavior can be prevented when working with a moderators account. +// Note that in contrast to previous versions, replies are inside +// the same numbering system. +function get_new_post_id($db, $sub) +{ + + $largest = 0; + + $statement = $db->prepare("SELECT post_id + FROM threads + WHERE sub = '$sub' + ORDER BY post_id DESC + LIMIT 1"); + // we just want the highest element + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $largest = "{$row[0]}"; + } + + $largest++; + + return $largest; +} + +// Get the admin or mod token from the post stream. +function get_post_token() +{ + + $token = ''; + + if ( (!empty($_POST['token'])) ) { + $token = substr(preg_replace("/[^0-9a-zA-Z]/", "", + $_POST['token']), 0, 250); + // length of token is 250 characters + } + + return $token; +} + +// Check how many pretty vars have been sent +function get_pretty_vars_count() +{ + + $raw_vars = explode('/', $_SERVER['REQUEST_URI']); + $count = count($raw_vars); + + return $count; + +} + +// Return number of posts in sub or overboard, with or without replies. +function give_total_posts($db, $sub, $original_only, $settings) +{ + + if ( ($original_only) && ($sub != 'overboard') ) { + $statement = $db->prepare("SELECT post_id + FROM threads + WHERE sub = '$sub' + AND post_id = org_id + AND shadow = 'no'"); + $result = $statement->execute(); + $counter = 0; + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + } elseif ( ($sub == 'overboard') ) { + + $no_overboard = ''; + $last = array_pop($settings['no_overboard']); + + foreach($settings['no_overboard'] as $ex_sub) { + $str = "'" . $ex_sub . "', "; + $no_overboard .= $str; + } + + $no_overboard .= "'" . $last . "'"; + $statement = $db->prepare("SELECT post_id + FROM threads + WHERE post_id = org_id + AND sub NOT IN ($no_overboard) + AND shadow = 'no'"); + $result = $statement->execute(); + $counter = 0; + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + } else { + $statement = $db->prepare("SELECT post_id + FROM threads + WHERE sub = '$sub' + AND shadow = 'no'"); + $result = $statement->execute(); + $counter = 0; + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + } + + return $counter; + +} + +// This function can be used to replicate an existing board. +// Save the dump from the overboard of the existing board, then put +// it under /var/opt/endboard/board.json of your new server +// (or whatever name you defined in the config-file). +// Log in to your admin panel, click on link "import" in the footer +function import_overboard($db, $settings) +{ + + $board_file = $settings['work_dir'] . $settings['import_file']; + + if ( file_exists($board_file) ) { + $html_string = "
    Found importfile $board_file
    "; + } else { + $html_string = "
    Could not find importfile "; + $html_string .= "$board_file, aborting.
    "; + echo "$html_string"; + return; + } + + $board_json = file_get_contents($board_file); + $board_list = json_decode($board_json, TRUE); + $counter = 0; + + foreach($board_list as $post) { + $post_id = $post['post_id']; + $text = $post['text']; + $sub = $post['sub']; + $org_id = $post['org_id']; + + if ( (!empty($text)) && (!empty($sub)) + && (!empty($post_id)) && (!empty($org_id)) ) { + if (!check_post_exists($db, $sub, $org_id, $post_id)) { + $global_id = hash("sha512", $sub . $post_id . $org_id . $text); + $text_id = hash("sha512", $text); + + $statement = $db->prepare("INSERT OR IGNORE INTO threads + (post_id, sub, text, org_id, + shadow, global_id, text_id) + VALUES ('$post_id', '$sub', ?, + '$org_id', 'no', '$global_id', + '$text_id')"); + $statement->bindParam(1, $text); + + $statement->execute(); + + $counter++; + $import_message = "message $post_id from sub $sub imported"; + log_event($db, $settings, "import", $import_message, ''); + $html_string .= "
    $import_message
    "; + } else { + $import_message = "message $post_id from sub $sub"; + $import_message .= " existed already and was not imported"; + log_event($db, $settings, "import", $import_message, ''); + $html_string .= "
    $import_message
    "; + } + } + } + + rename($board_file, $board_file . '.done'); + + $html_string .= "
    Finished, imported "; + $html_string .= "$counter messages altogether.
    "; + $html_string .= "
    The boardfile was "; + $html_string .= "renamed to avoid another import.
    "; + + echo "$html_string"; +} + +// Log an event to the db. Also, delete the overflow of logs as +// defined in the config file. +function log_event($db, $settings, $type, $text, $ip) +{ + + if ( ($settings['enable_logging'] != TRUE) ) { + return; + } + + $current = time(); + $timestamp = date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']); + + $statement = $db->prepare("INSERT INTO logs(event, type, timestamp, + unix_timestamp, ip) + VALUES (?, '$type', '$timestamp', + '$current', ?)"); + $statement->bindParam(1, $text); + $statement->bindParam(2, $ip); + $statement->execute(); + + if ( ($settings['cap_logs'] > 0) ) { + $statement = $db->prepare("DELETE FROM logs + WHERE ROWID IN + (SELECT ROWID FROM logs + ORDER BY ROWID DESC + LIMIT -1 OFFSET ?)"); + // to prevent the db from bloating (and to prevent attacks), we + // allow only so many lines of logs at any one time, and we check + // this with each call. + $statement->bindParam(1, $settings['cap_logs']); + $result = $statement->execute(); +} + +} + +// Do some magic number trickery (easteregg) +function make_id_text($post_id) +{ + + if ($post_id == 1) { + $id_text = "first post - yeah"; + } elseif ($post_id == 42) { + $id_text = "...and thanks for all the fish..."; + } elseif ($post_id == 69) { + $id_text = "$post_id 😏"; + } elseif ($post_id == 104) { + $id_text = "10-4 affirmative"; + } elseif ($post_id == 143) { + $id_text = "$post_id πŸ’Œ"; + } elseif ($post_id == 404) { + $id_text = "content not found"; + } elseif ($post_id == 420) { + $id_text = "🌿🌿🌿"; + } elseif ($post_id == 666) { + $id_text = "πŸ‘ΏπŸ‘ΏπŸ‘Ώ "; + } elseif ($post_id == 911) { + $id_text = "How can I help you ?"; + } elseif ($post_id == 1312) { + $id_text = "all cats are beautiful"; + } else { + $id_text = $post_id; + } + + return $id_text; + +} + +// Make all tables that are needed (one each for posts, keys, hashes +// (captchas), hashes (passwords) and logs). +// Also, the hashes for the captchas are cropped to 20000. +function make_basic_tables($db) +{ + + // make basic tables: threads, captchas, logs, keys + $db->exec('CREATE TABLE IF NOT EXISTS "captchas" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "hash" TEXT UNIQUE, + "unix_timestamp" INTEGER + )'); + + $db->exec('CREATE TABLE IF NOT EXISTS "logs" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "timestamp" TEXT, + "unix_timestamp" INTEGER, + "type" TEXT, + "event" TEXT, + "ip" TEXT + )'); + + $db->exec('CREATE TABLE IF NOT EXISTS "keys" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "name" TEXT UNIQUE NOT NULL, + "type" INTEGER NOT NULL, + "email" TEXT, + "key" TEXT, + "subs" TEXT, + "token" TEXT, + "timestamp_token" INTEGER + )'); + + $db->exec('CREATE TABLE IF NOT EXISTS "threads" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "post_id" INTEGER NOT NULL, + "shadow" TEXT NOT NULL, + "sub" TEXT NOT NULL, + "global_id" TEXT NOT NULL UNIQUE, + "text_id" TEXT NOT NULL, + "text" TEXT NOT NULL, + "org_id" INTEGER NOT NULL, + UNIQUE(post_id, sub) ON CONFLICT IGNORE + )'); + + $statement = $db->prepare("DELETE FROM captchas + WHERE ROWID IN + (SELECT ROWID FROM captchas + ORDER BY ROWID DESC + LIMIT -1 OFFSET 20000)"); + // to prevent the db from bloating (and to prevent attacks), we + // allow only 20000 captchas at any one time, and we check this + // with each call. This should be enough if your site is getting + // less or equal to 100.000 visitors a day. + // Total combinations of captcha and token are ca. 2000 * 62^250. + $result = $statement->execute(); + +} + +// Make a new post to a sub +function make_post($db, $sub, $settings, $text, $org_id) +{ + + $post_id = get_new_post_id($db, $sub); + + if ($org_id == '') { + $org_id = $post_id; + } elseif ( (!check_org_id_exists($db, $sub, $org_id)) ) { + quit($db, "

    Post $org_id on sub $sub does not exist.

    "); + } + + $global_id = hash("sha512", $sub . $post_id . $org_id . $text); + $text_id = hash("sha512", $text); + + $statement = $db->prepare("INSERT INTO threads(post_id, sub, text, + org_id, shadow, + global_id, text_id) + VALUES ('$post_id', '$sub', ?, '$org_id', + 'no', '$global_id', '$text_id')"); + $statement->bindParam(1, $text); + $statement->execute(); + + if ( ($org_id != $post_id) && ($settings['enable_bumping'] == TRUE) ){ + bump_message($db, $org_id, $sub); + } + + return $post_id; +} + +// Make a token to grant access to the admin panel or the mod panel for +// a limited time. A way of having sessions without cookies. +// Also used as a hidden field in the post form to prevent double +// posting by sending the same input twice. +function make_token() +{ + + $characters = '0123456789'; + $characters .= 'abcdefghijklmnopqrstuvwxyz'; + $characters .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $random_string = ''; + + for ($i = 0; $i < 250; $i++) { + // token length is set to 250 characters + $index = random_int(0, 61); + // we have 62 to choose, so 0 to 61 + $random_string .= $characters[$index]; + } + + return $random_string; +} + +// Moves a post to a different sub. In case it is an original post, all +// replies re moved as well. +function move_post($db, $settings, $delete_array, $sub, $target_sub, $name) +{ + foreach($delete_array as $delete_post) { + $statement = $db->prepare("UPDATE threads + SET shadow = 'yes' + WHERE post_id = '$delete_post' + AND sub = '$sub'"); + $result = $statement->execute(); + + $statement = $db->prepare("SELECT post_id, org_id, text + FROM threads + WHERE post_id = '$delete_post' + AND sub = '$sub'"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $post_id = "{$row[0]}"; + $org_id = "{$row[1]}"; + $text = "{$row[2]}"; + } + + if ( ($post_id == $org_id) ) { + $text .= "\r\n\r\n\r\n\r\n [system generated message: \r\n"; + $text .= "post $post_id from sub \"$sub\" was moved \r\n"; + $text .= "to sub \"$target_sub\"]"; + $new_id = make_post($db, $target_sub, $settings, $text, ''); + $delete_message = "message $delete_post from sub $sub"; + $delete_message .= " moved to $target_sub by $name"; + log_event($db, $settings, "del", $delete_message, ''); + + echo "

    $delete_message

    "; + + $statement = $db->prepare("UPDATE threads + SET shadow = 'yes' + WHERE org_id = '$post_id' + AND sub = '$sub'"); + $result = $statement->execute(); + + $statement = $db->prepare("SELECT post_id, org_id, text + FROM threads + WHERE org_id = '$post_id' + AND post_id != '$post_id' + AND sub = '$sub'"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $post_id = "{$row[0]}"; + $org_id = "{$row[1]}"; + $text = "{$row[2]}"; + + $text .= "\r\n\r\n\r\n\r\n [system generated message:\r\n"; + $text .= "post $post_id from sub \"$sub\" was moved \r\n"; + $text .= "to sub \"$target_sub\"]"; + make_post($db, $target_sub, $settings, $text, $new_id); + + $delete_message = "message $post_id from sub $sub moved "; + $delete_message .= "to $target_sub by $name"; + log_event($db, $settings, "del", $delete_message, ''); + + echo "

    $delete_message

    "; + } + } else { + $text .= "\r\n\r\n\r\n\r\n [system generated message: post\r\n"; + $text .= "$post_id from sub \"$sub\" was moved \r\n"; + $text .= "to sub \"$target_sub\"]"; + make_post($db, $target_sub, $settings, $text, ''); + $delete_message = "message $post_id from sub $sub moved"; + $delete_message .= " to $target_sub by $name"; + log_event($db, $settings, "del", $delete_message, ''); + + echo "

    $delete_message

    "; + } + + } + +} + +// checks if posts from bots can be received or not +function post_block_bot($db, $settings, $visitor_ip) +{ + + $current = time(); + $max_age = $current - ($settings['max_post_timeframe'] * 60); + // the number from settings is in minutes, so times 60 for secs + + if ( ($settings['max_post_global'] > 0) ) { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type in ('bot', 'user') + AND event = 'post attempt' + AND unix_timestamp > '$max_age'"); + + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + if ( ($counter > $settings['max_post_global']) ) { + return FALSE; + } + } + + if ( ($settings['max_post_ip'] > 0) ) { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type in ('bot', 'user') + AND event = 'post attempt' + AND ip = '$visitor_ip' + AND unix_timestamp > '$max_age'"); + + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + if ( ($counter > $settings['max_post_ip']) ) { + return FALSE; + } + } + + if ( ($settings['max_post_bot'] > 0) ) { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type = 'bot' + AND event = 'post attempt' + AND unix_timestamp > '$max_age'"); + + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + if ( ($counter > $settings['max_post_bot']) ) { + return FALSE; + } + + } + + return TRUE; +} +// checks if posts from users can be received or not +function post_block_user($db, $settings, $visitor_ip) +{ + + $current = time(); + $max_age = $current - ($settings['max_post_timeframe'] * 60); + // the number from settings is in minutes, so times 60 for secs + + if ( ($settings['max_post_global'] > 0) ) { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type in ('bot', 'user') + AND event = 'post attempt' + AND unix_timestamp > '$max_age'"); + + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + if ( ($counter > $settings['max_post_global']) ) { + return FALSE; + } + } + + if ( ($settings['max_post_ip'] > 0) ) { + $statement = $db->prepare("SELECT unix_timestamp + FROM logs + WHERE type in ('bot', 'user') + AND event = 'post attempt' + AND ip = '$visitor_ip' + AND unix_timestamp > '$max_age'"); + + $result = $statement->execute(); + + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + if ( ($counter > $settings['max_post_ip']) ) { + return FALSE; + } + } + + return TRUE; +} + +// Show each post in a thread +function print_thread($db, $sub, $css, $settings, $org_id) +{ + + $html_string = "
    "; + + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads WHERE sub = '$sub' + AND org_id = '$org_id' + AND shadow = 'no' + ORDER BY post_id DESC"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $post_id = "{$row[0]}"; + $org_id = "{$row[1]}"; + $post_text = "{$row[3]}"; + $post_text = break_text(bbcode_to_html($post_text, $settings), + $settings); + $id_text = make_id_text($post_id); + + $html_string .= "
    "; + $html_string .= "
    #$id_text

    "; + $html_string .= "$post_text"; + $html_string .= "


    "; + } + + $html_string .= "
    "; + + echo "$html_string"; + +} + +// Show each post of the overboard (so all original posts in their +// sequence, including bumps, except for the subs that are excluded) +function print_overboard($db, $css, $settings, $page) +{ + + $out = ''; + $pagination = $settings['pagination']; + + if ( (!empty($settings['no_overboard'])) ) { + $last = array_pop($settings['no_overboard']); + + foreach($settings['no_overboard'] as $no_overboard) { + $str = "'" . $no_overboard . "', "; + $out .= $str; + } + + $out .= "'" . $last . "'"; + + } + + if ($page == 'all') { + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE post_id = org_id + AND shadow = 'no' + AND sub NOT IN ($out) + ORDER BY ROWID DESC"); + } elseif ($page > 0) { + // if the page is defined + $page_start = ($page - 1) * $settings['pagination']; + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE post_id = org_id + AND shadow = 'no' + AND sub NOT IN ($out) + ORDER BY ROWID DESC + LIMIT '$page_start', '$pagination'"); + } else { + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE post_id = org_id + AND shadow = 'no' + AND sub NOT IN ($out) + ORDER BY ROWID DESC + LIMIT '$pagination'"); + } + + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + + $html_string = ''; + + $post_id = "{$row[0]}"; + $org_id = "{$row[1]}"; + $sub = "{$row[2]}"; + $text = "{$row[3]}"; + $post_text = break_text(bbcode_to_html($text, $settings), $settings); + + $html_string .= "
    "; + $link_string_1 = "/r/$sub/$org_id/op/$css"; + $link_string_2 = "/r/$sub/$org_id/$css"; + $link_string_3 = "/s/$sub/$css"; + $html_string .= "
    "; + $html_string .= "$sub "; + $html_string .= ""; + } + +} + +// Show each post of an individual feed +function print_individual_feed($db, $css, $settings, $ex_subs, $in_subs) +{ + + $counter = 0; + + if ( (!empty($ex_subs)) ) { + $out = ''; + $last = array_pop($ex_subs); + + foreach($ex_subs as $ex_sub) { + $str = "'" . $ex_sub . "', "; + $out .= $str; + } + + $out .= "'" . $last . "'"; + + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE post_id = org_id + AND shadow = 'no' + AND sub NOT IN ($out) + ORDER BY ROWID DESC"); + } elseif ( (!empty($in_subs)) ) { + $in = ''; + $last = array_pop($in_subs); + + foreach($in_subs as $in_sub) { + $str = "'" . $in_sub . "', "; + $in .= $str; + } + + $in .= "'" . $last . "'"; + + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE post_id = org_id + AND shadow = 'no' + AND sub IN ($in) + ORDER BY ROWID DESC"); + } + + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + + $html_string = ''; + + $counter++; + + $post_id = "{$row[0]}"; + $org_id = "{$row[1]}"; + $sub = "{$row[2]}"; + $text = "{$row[3]}"; + + $post_text = break_text(bbcode_to_html($text, $settings), $settings); + + $link_string_1 = "/r/$sub/$org_id/op/$css"; + $link_string_2 = "/r/$sub/$org_id/$css"; + $link_string_3 = "/s/$sub/$css"; + + $html_string .= "
    "; + $html_string .= "$sub "; + $html_string .= ""; + $html_string .= "#$post_id

    $post_text"; + $html_string .= "

    reply
    "; + + echo "$html_string"; + + print_replies($db, $sub, $post_id, $org_id, $settings, $css); + + echo "
    "; + } + + return $counter; +} + +// Show each post in a sub +function print_sub($db, $sub, $css, $settings, $page) +{ + + echo "
    "; + + $pagination = $settings['pagination']; + + if ($page == 'all') { + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE sub = '$sub' + AND shadow = 'no' + AND post_id = org_id + ORDER BY ROWID DESC"); + } elseif ($page > 0) { + // if the page is defined + $page_start = ($page - 1) * $settings['pagination']; + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE sub = '$sub' + AND shadow = 'no' + AND post_id = org_id + ORDER BY ROWID DESC + LIMIT '$page_start', '$pagination'"); + } else { + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE sub = '$sub' + AND shadow = 'no' + AND post_id = org_id + ORDER BY ROWID DESC + LIMIT '$pagination'"); + } + + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + + $html_string = ''; + + $post_id = "{$row[0]}"; + $org_id = "{$row[1]}"; + $text = "{$row[3]}"; + $post_text = break_text(bbcode_to_html($text, $settings), $settings); + + $html_string .= "
    "; + + $id_text = make_id_text($post_id); + $link_string_1 = "/r/$sub/$org_id/op/$css"; + $link_string_2 = "/r/$sub/$org_id/$css"; + + $html_string .= "
    "; + $html_string .= "#$id_text"; + $html_string .= "

    $post_text

    "; + $html_string .= "reply
    "; + + echo "$html_string"; + + print_replies($db, $sub, $post_id, $org_id, $settings, $css); + + echo "
    "; + echo "
    "; + } + + echo "
    "; + +} + +// Print footer, with the total messages and some links, for the subs +function print_footer_sub($sub, $total_posts, $css, $page, $settings) +{ + + if ( ($total_posts > $settings['pagination']) && ($page != 'all') ) { + $number_first_message = ($page - 1) * $settings['pagination'] + 1; + $number_last_message = + $number_first_message + $settings['pagination'] - 1; + + if ($number_last_message > $total_posts) { + $number_last_message = $total_posts; + } + + $next_page = $page + 1; + $prev_page = $page - 1; + $pages_total = ceil($total_posts / $settings['pagination']); + + $show_string_1 = "page:$page/$pages_total|posts:"; + $show_string_1 .= "$number_first_message"; + $show_string_1 .= "-$number_last_message/$total_posts|"; + + $link_string_1 = "/s/$sub/$next_page/$css"; + $link_string_2 = "/s/$sub/$prev_page/$css"; + $link_string_3 = "/s/$sub/all/$css"; + + if ( ($number_first_message > 1) + && ($number_last_message < $total_posts) ) { + $left_section = "$show_string_1
    "; + $html_string .= "$left_section
    "; + $html_string .= "
    auth" . + "|user" . + "|bot" . + "|del" . + "|import" . + "|sys"; + + $html_string = "
    "; + $html_string .= "$left_section
    "; + + $mid_section = "/dt/$token'>log out" . + "|" . + "show shadowed posts and subs
    "; + $right_section = "/im/$css/$token'>import" . + "|view mods
    "; + + $html_string .= ""; + } else { + $mid_section = "**********
    "; + } + + if ( ($settings['enable_admin_panel'] == TRUE) + && (!check_admin($db, $settings)) ) { + $right_section = "set admin password
    "; + } elseif ($settings['take_applications'] == TRUE) { + $right_section = "apply for mod account"; + } else { + $right_section = "**********"; + } + + $html_string .= "
    $mid_section"; + $html_string .= "
    $right_section"; + $html_string .= "
    "; + + echo "$html_string"; +} + +// Print the footer for the mod +function print_footer_mod($css, $settings, $token, $sub) +{ + + $left_section = "***************"; + + $html_string = "
    "; + $html_string .= "
    $left_section
    "; + + $mid_section = "/dt/$token'>log out
    "; + $right_section = "***************"; + + $html_string .= "
    "; + $html_string .= "
    $left_section
    "; + + $mid_section = "/s/overboard/$css'>overboard
    |"; + $mid_section .= "show subs
    "; + + $right_section = "/iv/$subs_string/$css'>get link for multifeed"; + + $html_string .= "
    "; + $html_string .= "
    $left_section
    "; + + $mid_section = "/s/$sub/$css'>back to $sub
    |"; + $mid_section .= "overboard
    "; + + $right_section = "/d/$sub/$org_id'>save $msg"; + + $html_string .= "
    "; + } else { + $html_string = ""; + $html_string .= ""; + } + + echo "$html_string"; +} + +// Show all replies to a given post +function print_replies($db, $sub, $post_id, $org_id, $settings, $css) +{ + + $sub_statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE sub = '$sub' + AND org_id = '$org_id' + AND shadow = 'no'"); + $sub_result = $sub_statement->execute(); + + $answers = array(); + $counter = 0; + + while ($row = $sub_result->fetchArray(SQLITE3_NUM)) { + $sub_post_id = "{$row[0]}"; + $sub_org_id = "{$row[1]}"; + $sub_text = "{$row[3]}"; + if ($sub_post_id != $sub_org_id) { + $counter++; + $post = array(); + $sub_post_text = break_text(bbcode_to_html + ($sub_text, $settings), $settings); + array_push($post, $sub_post_id); + array_push($post, $sub_org_id); + array_push($post, $sub_post_text); + array_push($answers, $post); + } + } + + $display_number = $counter - 1; + + if ($counter == 0) { + // no replies exist for this message + return; + } + + $last_answer = array_pop($answers); + $last_post_id = $last_answer[0]; + $last_post_text = $last_answer[2]; + + $html_string = ''; + + if ($counter > 1) { + // we have at least one reply + $html_string .= "
    "; + $html_string .= "Show $display_number more replies"; + + foreach ($answers as $display_msg) { + $answer_post_id = $display_msg[0]; + $answer_post_text = $display_msg[2]; + $link_string_1 = "/r/$sub/$org_id/$answer_post_id/$css"; + $html_string .= ""; + } + + $html_string .= "
    "; + } + + $link_string_1 = "/r/$sub/$org_id/$last_post_id/$css"; + + $html_string .= "

    "; + $html_string .= "#$last_post_id

    "; + $html_string .= "$last_post_text
    "; + + echo "$html_string"; +} + +// prints the fixed topheader with some text +function print_top_header($text) +{ + echo "

    $text

    "; +} + +// Close the database, so to not let this work for the garbage collector +// and loose time. +// Also display a final message (optionally), and exit. +function quit($db, $text) +{ + + $db->close(); + + if ($text != '') { + echo "$text"; + } + + exit; +} + +// Parse pretty vars at different oositions and with different filters +function read_pretty_vars($position, $filter, $length) +{ + + $var = ''; + + $raw_vars = explode('/', $_SERVER['REQUEST_URI']); + $pretty_vars = array(); + + foreach($raw_vars as $raw_var) { + if ($filter == 'number') { + $pretty_var = substr(preg_replace("/[^0-9]/", "", + $raw_var), 0, $length); + } elseif ($filter == 'alnum') { + $pretty_var = substr(preg_replace("/[^0-9a-zA-Z]/", "", + $raw_var), 0, $length); + } elseif ($filter == 'alnumplus') { + $pretty_var = substr(preg_replace("/[^0-9a-zA-Z+-]/", "", + $raw_var), 0, $length); + } + array_push($pretty_vars, $pretty_var); + } + + if ($position == 'last') { + $var = array_pop($pretty_vars); + } elseif ( (isset($pretty_vars[$position])) ) { + $var = $pretty_vars[$position]; + } + + return $var; + +} + +// Receives an application and writes it to the db. +function set_application($db, $name, $email, $password, $settings) +{ + + $statement = $db->prepare("INSERT INTO keys(type, name, email, key) + VALUES ('application', '$name', + '$email', '$password')"); + $statement->execute(); + +} + +// Set the css that we use +function set_css($mode) +{ + + $admin_css = array( + 'auth_admin', + 'admin', + 'delete_admin', + 'delete_logs', + 'logs', + 'password', + 'setup', + 'shadow', + 'view_mods', + 'change_mods', + 'unshadow_post', + 'unshadow_sub', + 'import'); + $mod_css = array( + 'apply', + 'auth_mod', + 'mod', + 'delete_mod'); + + if (in_array($mode, $admin_css)) { + $css = 8; + // the css for the admin + } elseif (in_array($mode, $mod_css)) { + $css = 7; + // the css for the mods + } else { + $css = read_pretty_vars("last", "number", 2); + // we will not have more than 99 styles, so two digits + } + + if ( (!empty($_POST['css'])) ) { + $css = substr(preg_replace("/[^0-9]/", "", $_POST['css']), 0, 2); + // we will not have more than 99 styles, so two digits + } + + if ( (empty($css)) || ($css < 1) || ($css > 9) ) { + // if no css is given, or the value is out of range, + $css = 6; + // resort to the default style + } + + return $css; +} + +// Set the stylesheet we use +function set_css_file($css) +{ + + if ($css == 9) { + // nine means no style + $css_file = 'no_file'; + } elseif ($css == 8) { + // eight is admin + $css_file = '/css/admin.css'; + } elseif ($css == 7) { + // seven is moderator + $css_file = '/css/mod.css'; + } elseif ($css == 6) { + // six is the default (Forest) + $css_file = '/css/style4.css'; + } elseif ($css == 5) { + $css_file = '/css/style20.css'; + } elseif ($css > 2) { + // 3,4,5 is Santa Muerte + $css_file = '/css/style8.css'; + } elseif ($css > 1) { + // 2 is Kali + $css_file = '/css/style12.css'; + } else{ + // 1 is Skull + $css_file = '/css/style10.css'; + } + + return $css_file; + +} + +// Check what is expected from the request, and set mode accordingly. +// The variable "$short_mode" is read from the request. +// The elements in the triggers array other than 'tr', capture requests +// that are standard scans for vulns + target discovery. +// Part of the bot trap, which can be disabled in the config file +function set_mode($short_mode, $settings) +{ + + $triggers = array ( + 'tr', 'login', 'wellknown', 'wplogin', + 'wpjson', 'wp', 'products', 'wpusers', + 'wpadmin', 'wpadminer', 'adminer', 'phpmyadmin', + 'wpuploads', 'wpcontent', 'wpconfig', 'wpincludes', + 'static', 'img', 'images', 'uploads', + 'styles', 'style', 'serverinfo', 'privatekey', + 'serverstatus' + ); + + $short_modes = array ( + 'cm', 'dim', 'pa', 'ush', + 'dsh', 'usha', 'dsa', 'aa', + 'am', 'dt', 'a', 'm', + 'ap', 'sp', 'im', 'da', + 'dm', 'dl', 'su', 'lo', + 'sh', 'iv', 'p', 'r', + 'v', 's', 'b', 'd' + ); + + $long_modes = array ( + 'view_mods', 'change_mods', 'password', 'unshadow_post', + 'delete_post', 'unshadow_sub', 'delete_sub', 'auth_admin', + 'auth_mod', 'destroy_token', 'admin', 'mod', + 'apply', 'setup', 'import', 'delete_admin', + 'delete_mod', 'delete_logs', 'subs', 'logs', + 'shadow', 'individual_view', 'post', 'reply', + 'view', 'view', 'bot', 'dump' + ); + + if ( (in_array($short_mode, $triggers)) + && ($settings['enable_bot_trap']) ) { + $mode = 'trap'; + } elseif ( (!in_array($short_mode, $short_modes)) ) { + $mode = 'landing'; + // if nothing fits, we display the landing page + } else { + for ($i = 0; $i < count($short_modes); $i++) { + if ( ($short_modes[$i] == $short_mode) ) { + $mode = $long_modes[$i]; + break; + } + } + } + + return $mode; +} + +// In case of replies, check which post we are replying to +// (based on the pretty vars in GET requests) +function set_org_id() +{ + + $org_id = read_pretty_vars(3, "number", 10); + // we read from the third position, and a message does not need more + // than 10 digits (=max 9 999 999 999 messages) + return $org_id; + +} + +// Get the page if it is defined, otherwise set to 1 +function set_page() +{ + + if (get_pretty_vars_count() == 5) { + // the page is only defined if there are five vars + $page = read_pretty_vars(3, "number", 10); + // read from third position, and a page does not need more + // than 10 digits + } else { + $page = 0; + // if there is no page given, we set it to zero + } + + if ( ($page < 1) ) { + // if the page is zero, it means we did not + // get a number before + + $page = read_pretty_vars(3, "alnum", 3); + // read from the third position, and a page does not need more + // than 10 digits, this time we allow letters, too. + + if ( ($page != 'all') ) { + + $page = 1; + // if page is not "all", we start with the first + } + } + + return $page; +} + +// Set the password for the admin, delete all old admin accounts +// (there can only be one...) +function set_password($db, $name, $password_hash, $token, $settings) +{ + + $db_token = ''; + $db_name = ''; + + $statement = $db->prepare("SELECT name, token + FROM keys + WHERE type = 'admin' + AND name = '$name'"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $db_name = "{$row[0]}"; + $db_token = "{$row[1]}"; + } + + if ( (!password_verify($token, $db_token)) ) { + $auth_message = "wrong combination name/token"; + log_event($db, $settings, "auth", $auth_message, ''); + sleep(10); + $quit_message = "

    Combination of username and token is invalid."; + $quit_message .= "This incident was logged.

    "; + quit($db, $quit_message); + } else { + $statement = $db->prepare("UPDATE keys + SET key = '$password_hash' + WHERE name = '$name'"); + $result = $statement->execute(); + + $statement = $db->prepare("DELETE FROM keys + WHERE type = 'admin' + AND name != '$name'"); + $result = $statement->execute(); + + unlink($settings['work_dir'] . "admin_" . $name . "_token.txt"); + } + +} + +// Determine if there is an original post_id given in the poststream +// (in case of replies) +function set_post_org_id() +{ + + $org_id = ''; + + if ( (!empty($_POST['org_id'])) ) { + $org_id = substr(preg_replace("/[^0-9]/", "", + $_POST['org_id']), 0, 10); + // no post id needs more than 10 digits + // (= 9 999 999 999 messages) + + } + + return $org_id; +} + +// Determine which sub we use currently, first read from POST, +// than from the pretty vars (GET) +function set_sub($settings) +{ + + $sub = ''; + + if ( (!empty($_POST['sub'])) ) { + $sub = substr(preg_replace("/[^0-9a-zA-Z]/", "", $_POST['sub']), + 0, $settings['max_name_sub']); + } elseif (get_pretty_vars_count() > 1) { + $sub = read_pretty_vars(2, "alnum", $settings['max_name_sub']); + // read the sub from the second position + } + + if ($sub == '') { + $sub = 'main'; + } + + return $sub; +} + +// Get the quote, if there is one. +function set_quote() +{ + + if (get_pretty_vars_count() == 6) { + $quote = read_pretty_vars(4, "alnum", 10); + // read from fourth position, and a post id does not need more + // than 10 digits + } else { + $quote = ''; + } + + return $quote; + +} + +// Show the form that allows the admin and mods to log in. +// Can be disabled in the config file. +function show_auth_form($db, $css, $settings, $type) +{ + $html_string = "

    Give your account name and password to log in.

    "; + $html_string .= "

    "; + + if ( ($type == 'admin') ) { + $html_string .= "

    "; + $html_string .= "
    "; + } else { + $html_string .= "
    "; + $html_string .= ""; + } + + $html_string .= ""; + $html_string .= "
    NameNo posting possible, no space on filesystem"; + return; + } + + if ( (post_block_user($db, $settings, $ip) != TRUE) ) { + echo "

    Max posts exhausted. Retry later.

    "; + return; + } + + $html_string = "
    "; + $html_string .= ""; + $html_string .= ""; + + if ( (!$org_id) && $sub == 'main') { + $html_string .= ""; + $html_string .= "
    prepare("INSERT OR IGNORE INTO captchas(hash, unix_timestamp) + VALUES ('$hash', '$current')"); + $statement->execute(); + + echo "$html_string"; +} + +// Show all posts that have been shadowed by moderators +function show_shadowed($db, $css, $settings, $token) +{ + + print_top_header('Shadowed messages and subs'); + + $statement = $db->prepare("SELECT post_id, org_id, sub, text + FROM threads + WHERE shadow = 'yes' + ORDER BY sub"); + + $result = $statement->execute(); + + $prev_sub = ''; + + $html_string = ''; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $post_id = "{$row[0]}"; + $org_id = "{$row[1]}"; + $sub = "{$row[2]}"; + $text = "{$row[3]}"; + $post_text = break_text(bbcode_to_html($text, $settings), $settings); + + $html_string .= "
    "; + $html_string .= "
    "; + $html_string .= ""; + $html_string .= ""; + $html_string .= "prepare("SELECT DISTINCT sub + FROM threads + WHERE shadow = 'no' + ORDER BY sub + COLLATE NOCASE"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $sub = "{$row[0]}"; + $total_posts = give_total_posts($db, $sub, FALSE, $settings); + $html_string .= " | $sub"; + $html_string .= "($total_posts)"; + } + + $html_string .= ""; + + echo "$html_string"; +} + +// Show the existing subs to a user, including their count +// Differentiates between subs with > 10 posts (high-traffic) +// and lower (low-traffic). Also shows the last five subs that +// were posted to. +function show_subs_count($db, $css, $settings) +{ + $out = ''; + + if ( (!empty($settings['no_overboard'])) ) { + $last = array_pop($settings['no_overboard']); + + foreach($settings['no_overboard'] as $no_overboard) { + $str = "'" . $no_overboard . "', "; + $out .= $str; + } + + $out .= "'" . $last . "'"; + + } + + $statement = $db->prepare("SELECT post_id + FROM threads + WHERE sub NOT IN ($out) + AND shadow = 'no'"); + $result = $statement->execute(); + $counter = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter++; + } + + $statement = $db->prepare("SELECT post_id + FROM threads + WHERE sub NOT IN ($out) + AND shadow = 'no' + AND post_id = org_id"); + $result = $statement->execute(); + $counter_org = 0; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $counter_org++; + } + + $replies = $counter - $counter_org; + + $html_string = "

    Subs with some traffic:

    "; + $html_string .= "overboard"; + $html_string .= "($counter_org/$replies)"; + + $statement = $db->prepare("SELECT DISTINCT sub + FROM threads + WHERE shadow = 'no' + ORDER BY sub + COLLATE NOCASE"); + $result = $statement->execute(); + + $high_traffic_subs = array(); + $low_traffic_subs = array(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $display_sub = array(); + $sub = "{$row[0]}"; + $total_posts = give_total_posts($db, $sub, FALSE, $settings); + $total_org_posts = give_total_posts($db, $sub, TRUE, $settings); + $replies = $total_posts - $total_org_posts; + + array_push($display_sub, $sub); + array_push($display_sub, $total_org_posts); + array_push($display_sub, $replies); + + if ( ($total_posts > 10) ) { + // we define any sub with more than ten messages as high traffic + // anything below as low traffic + array_push($high_traffic_subs, $display_sub); + } else { + array_push($low_traffic_subs, $display_sub); + } + } + + + foreach($high_traffic_subs as $display_sub) { + $html_string .= " | $display_sub[0]"; + $html_string .= "($display_sub[1]/$display_sub[2])"; + } + + $html_string .= "



    "; + $html_string .= "

    Other subs:

    "; + + $temp = array_reverse($low_traffic_subs); + $first_display_sub = array_pop($temp); + $low_traffic_subs = array_reverse($temp); + + $html_string .= ""; + $html_string .= "$first_display_sub[0]"; + $html_string .= "($first_display_sub[1]/$first_display_sub[2])"; + + foreach($low_traffic_subs as $display_sub) { + $html_string .= " | "; + $html_string .= "$display_sub[0]($display_sub[1]/$display_sub[2])"; + } + + $html_string .= "


    "; + + $statement = $db->prepare("SELECT DISTINCT sub + FROM threads + WHERE shadow = 'no' + ORDER BY ROWID DESC + LIMIT 5"); + $result = $statement->execute(); + + $html_string .= "

    Subs with recent posts:

    "; + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $sub = "{$row[0]}"; + $html_string .= "$sub "; + } + + $html_string .= "

    "; + + echo "$html_string"; +} + +// If enabled, inserts an invisible link, leading to the bot tarpit +function lay_trap($settings) +{ + if ( ($settings['enable_bot_trap'] == TRUE) ) { + $triggers = array( + 'tr', 'login', 'wellknown', + 'wp-login', 'wp-json', 'wp', + 'products', 'wp-users', 'wp-admin', + 'wp-adminer', 'adminer', 'php-myadmin', + 'wp-uploads', 'wp-content', 'wp-config', + 'wp-includes', 'static', 'img', + 'images', 'uploads', 'styles', + 'style', 'server-info', 'private_key', + 'server-status' + ); + $count_triggers = count($triggers) - 1; + $fake_link = $triggers[rand(0, $count_triggers)]; + echo "
    "; + } +} + +// Show the existing subs to a user, without the count +function show_subs_no_count($db, $css) +{ + + $html_string = "

    overboard"; + + $statement = $db->prepare("SELECT DISTINCT sub + FROM threads + WHERE shadow = 'no' + ORDER BY sub + COLLATE NOCASE"); + $result = $statement->execute(); + + while ($row = $result->fetchArray(SQLITE3_NUM)) { + $sub = "{$row[0]}"; + if ( ($sub != '') ) { + $html_string .= " | $sub"; + } + } + + $html_string .= "

    "; + + echo "$html_string"; +} + +// Show the form that allows to set individual feeds. +function show_set_feeds_form($db, $settings, $css) +{ +// rewrite: page ? + $html_string = "


    Set your multifeed:

    "; + $html_string .= "

    "; + $html_string .= "

    "; + $html_string .= ""; + $html_string .= "
    Show everything except: "; + $html_string .= ""; + $html_string .= "
    OR Show nothing but: "; + $html_string .= ""; + $html_string .= ""; + $html_string .= "
    "; + $html_string .= "
    "; + $html_string .= ""; + $html_string .= "
    Name"; + $html_string .= "
    "; + $html_string .= ""; + $html_string .= "
    Name"; + + if ( ($type == 'admin') ) { + $html_string .= "
    "; + $html_string .= ""; + $html_string .= "
    Sub to delete