2382 lines
76 KiB
PHP
2382 lines
76 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This is the endboard software, version beta 0.71, index file for mobiles
|
|
* It is a textboard, written for the use in the darknets.
|
|
*
|
|
* The writing of this code started some time ago with another software
|
|
* called smolBBS. Although there is almost no original code left now,
|
|
* I still regard endboard as a fork of smolBBS.
|
|
* The author of smolBBS has required that the following text be
|
|
* distributed with any redistribution, so here it goes.
|
|
* The license and other conditions apply to endboard as well.
|
|
*
|
|
* IRC: *dulm @ irc.rizon.net
|
|
*
|
|
* Copyright (C) 2020 sandlind
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* (1) Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
*
|
|
* (2) Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in
|
|
* the documentation and/or other materials provided with the
|
|
* distribution.
|
|
*
|
|
* (3)The name of the author may not be used to
|
|
* endorse or promote products derived from this software without
|
|
* specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
|
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
|
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
* POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
//++++++++++ First code block to include the config file. ++++++++++++//
|
|
// This is at the top for your convenience (if you need to edit).
|
|
// the rest of the control flow is at the bottom (line 3679 to eof).
|
|
|
|
$config_file = '/etc/opt/endboard/config.php';
|
|
|
|
if ( file_exists($config_file) ) {
|
|
require_once $config_file;
|
|
// include the config file with the parameters and
|
|
// the landing page. No side effects.
|
|
$settings['config_file'] = $config_file;
|
|
} else {
|
|
echo "config file $config_file not found - exiting";
|
|
exit;
|
|
}
|
|
|
|
//++++++++++++ Function block starts here ++++++++++++++++++++++++++++//
|
|
|
|
// Give a new location to the browser.
|
|
// Does not work with lynx, unfortunately, but then the link can be used.
|
|
// For the mobile version, lynx does not matter, of course.
|
|
function answer_redirect($sub)
|
|
{
|
|
|
|
header( "refresh:3;url=/mob/s/$sub" );
|
|
// we wait 3 seconds with the redirection
|
|
$html_string = '<title>wait for it...</title></head>'
|
|
. '<body><div class="site-container">'
|
|
. '<div class="posts" id="posts">'
|
|
. '<div class="message first">'
|
|
. '<h3>Redirection in about 3 secs.'
|
|
. ' If that does not work, go'
|
|
. " <a href='/mob/s/$sub'>back</a>.</div></div>";
|
|
|
|
echo "$html_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 (
|
|
'<strong>$2</strong>',
|
|
'<em>$2</em>',
|
|
'<u>$2</u>',
|
|
'<s>$2</s>',
|
|
'<h2>$2</h2>',
|
|
'<span class=spoiler tabindex="0">$2</span>',
|
|
'<li>$2</li>',
|
|
'<a href="$2" target="_blank">$2</a>',
|
|
'<quote>>>$1$2</quote><br>'
|
|
);
|
|
|
|
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;
|
|
}
|
|
|
|
// If the post is a reply, put the original post on top.
|
|
function bump_message($db, $org_id, $sub)
|
|
{
|
|
return;
|
|
// REWRITE TO INCLUDE ORIGINAL
|
|
|
|
$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 <br>
|
|
function break_text($text, $settings)
|
|
{
|
|
|
|
$post_text = wordwrap($text, $settings['line_break'], "\n", TRUE);
|
|
$post_text = nl2br($post_text, FALSE);
|
|
return $post_text;
|
|
|
|
}
|
|
|
|
// Check the hashed captcha against the hashed solutions in the db.
|
|
function check_captcha($db, $settings)
|
|
{
|
|
if ( (!isset($_POST['post_token'])) ) {
|
|
quit($db, '<h3>What are you up to ? Use the postform.</h3>');
|
|
}
|
|
|
|
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, '<h3>What are you up to ? Use the postform.</h3>');
|
|
} 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 = '<h3>Unauthorized attempt to post.'
|
|
. 'Use a newly opened postform.</h3>';
|
|
quit($db, $quit_message);
|
|
} elseif ( ($counter < 1) ) {
|
|
quit($db, '<h3>wrong answer or captcha expired, try again</h3>');
|
|
} 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($settings['work_dir']);
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
|
|
// 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 org_id = '$org_id'");
|
|
$result = $statement->execute();
|
|
|
|
$counter = 0;
|
|
|
|
while ($row = $result->fetchArray(SQLITE3_NUM)) {
|
|
$counter++;
|
|
break;
|
|
}
|
|
|
|
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)
|
|
{
|
|
|
|
return;
|
|
|
|
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 {
|
|
$characters = 'abcdefghijklmnopqrstuvwxyz'
|
|
. 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
|
|
$random_string = '';
|
|
|
|
for ($i = 0; $i < 20; $i++) {
|
|
// token length is set to 20 characters
|
|
$index = random_int(0, 51);
|
|
// we have 62 to choose, so 0 to 61
|
|
$random_string .= $characters[$index];
|
|
}
|
|
$request = '/mob/' . $random_string;
|
|
// $request = $_SERVER['REQUEST_URI'] . '/' . $random_string;
|
|
// $request = $_SERVER['REQUEST_URI'];
|
|
header( 'HTTP/1.1 202 Accepted' );
|
|
header( 'Cache-Control: no-store', FALSE );
|
|
|
|
$html_string = "<!DOCTYPE html><html><head><style>"
|
|
. "body {font-size:30px;background-color: black;"
|
|
. "color: #33cccc;text-align: center;width: 30em;"
|
|
. "margin-left: auto;margin-right: auto;}"
|
|
. "input[type=submit] {padding:5px 15px;"
|
|
. "font-size:25px;"
|
|
. "background-color: black;"
|
|
. "color: #33cccc;"
|
|
. "cursor:pointer;"
|
|
. "border: 1px solid #11bbcc;"
|
|
. "-webkit-border-radius: 5px;"
|
|
. "border-radius: 5px;}"
|
|
. "</style></head><body><br><br><br>"
|
|
. "<code>Entry portal: "
|
|
. "Please click the button to proceed.</code>"
|
|
. "<br><br><br><br><br><br><div class='form'>"
|
|
. "<form action='/ep' method='post'>"
|
|
. "<input type='hidden' name='portal' value='$request'>"
|
|
. "<input type='submit' value='Enter'><br>"
|
|
. "</form></div>"
|
|
. "<br><br><br><br><br><br><code>Sorry for this,"
|
|
. " it's just a lowlevel protection "
|
|
. "against scraping bots.</code></body></html>";
|
|
|
|
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, '<h3>Spam detected!</h3>');
|
|
}
|
|
|
|
$post_length = strlen($text);
|
|
|
|
if ($post_length < $settings['min_char']) {
|
|
quit($db, '<h3>Post too short!</h3>');
|
|
}
|
|
|
|
if ($post_length > $settings['max_char']) {
|
|
quit($db, '<h3>Post too long!</h3>');
|
|
}
|
|
|
|
$text = str_replace(array("\n","\r"), '', $text);
|
|
|
|
if (substr_count($text, ' ') === strlen($text)) {
|
|
quit($db, '<h3>Spam detected! Post contained only spaces!</h3>');
|
|
}
|
|
//rewrite, does not work for all cases
|
|
if (ctype_space($text)) {
|
|
$quit_message = '<h3>Spam detected! Post contained only spaces '
|
|
. '(yeah, Unicode...)!</h3>';
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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' );
|
|
|
|
$json_dump = array();
|
|
|
|
if ( (!empty($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();
|
|
$json_dump['sub'] = $sub;
|
|
$json_dump['org_id'] = $org_id;
|
|
|
|
while ($row = $result->fetchArray(SQLITE3_NUM)) {
|
|
$post = array();
|
|
$post['post_id'] = "{$row[0]}";
|
|
$post['text'] = "{$row[1]}";
|
|
array_push($json_dump, $post);
|
|
}
|
|
} elseif ($sub == 'overboard') {
|
|
$statement = $db->prepare("SELECT post_id, org_id, sub, text
|
|
FROM threads
|
|
WHERE shadow = 'no'");
|
|
$result = $statement->execute();
|
|
|
|
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($json_dump, $post);
|
|
}
|
|
} else {
|
|
$statement = $db->prepare("SELECT post_id, org_id, text
|
|
FROM threads
|
|
WHERE sub = '$sub'
|
|
AND shadow = 'no'");
|
|
$result = $statement->execute();
|
|
$json_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($json_dump, $post);
|
|
}
|
|
}
|
|
|
|
echo json_encode($json_dump, JSON_PRETTY_PRINT
|
|
| JSON_NUMERIC_CHECK
|
|
| JSON_UNESCAPED_UNICODE);
|
|
|
|
}
|
|
|
|
// filter a variable according to different parameters and return the
|
|
// result
|
|
function filter($text, $type, $length)
|
|
{
|
|
if ( ( $type == 'alnum') ) {
|
|
$filtered_text = substr(
|
|
preg_replace("/[^0-9a-zA-Z]/", "", $text),
|
|
0, $length);
|
|
} elseif ( ($type == 'num') ) {
|
|
$filtered_text = substr(
|
|
preg_replace("/[^0-9]/", "", $text),
|
|
0, $length);
|
|
} elseif ( ($type == 'email') ) {
|
|
$filtered_text = substr(
|
|
preg_replace("/[^0-9a-zA-Z@._]/", "", $text),
|
|
0, $length);
|
|
}
|
|
|
|
return $filtered_text;
|
|
}
|
|
|
|
// 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++;
|
|
// we increase the largest number by one to get the new post_id
|
|
|
|
return $largest;
|
|
}
|
|
|
|
// Get the admin or mod token from the post stream.
|
|
function get_post_token()
|
|
{
|
|
|
|
$token = '';
|
|
|
|
if ( (!empty($_POST['token'])) ) {
|
|
$token = filter($_POST['token'], 'alnum', 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;
|
|
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
|
|
switch($post_id) {
|
|
case 1:
|
|
$id_text = 'first post - yeah';
|
|
break;
|
|
case 42:
|
|
$id_text = '...and thanks for all the fish...';
|
|
break;
|
|
case 69:
|
|
$id_text = "$post_id 😏";
|
|
break;
|
|
case 104:
|
|
$id_text = '10-4 affirmative';
|
|
break;
|
|
case 143:
|
|
$id_text = "$post_id 💌";
|
|
break;
|
|
case 404:
|
|
$id_text = 'content not found';
|
|
break;
|
|
case 420:
|
|
$id_text = '🌿🌿🌿';
|
|
break;
|
|
case 666:
|
|
$id_text = '👿👿👿 ';
|
|
break;
|
|
case 911:
|
|
$id_text = 'How can I help you ?';
|
|
break;
|
|
case 1312:
|
|
$id_text = 'all cats are beautiful';
|
|
break;
|
|
default:
|
|
$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_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,
|
|
"timestamp" TEXT,
|
|
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, "<h3>Post $org_id on sub $sub does not exist.</h3>");
|
|
}
|
|
|
|
$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,
|
|
timestamp, name,
|
|
tripcode, original)
|
|
VALUES ('$post_id', '$sub', ?, '$org_id',
|
|
'no', '$global_id', '$text_id',
|
|
'$timestamp', '$name', '$tripcode',
|
|
'$post_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'
|
|
. 'abcdefghijklmnopqrstuvwxyz'
|
|
. '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;
|
|
}
|
|
|
|
// 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, $settings, $org_id)
|
|
{
|
|
|
|
$html_string = '<div class="posts" id="posts">';
|
|
|
|
$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");
|
|
$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 .= "<div><div class='message'>#$id_text <br>"
|
|
. "<p id=\"$post_id\"></p><br>"
|
|
. "$post_text<br><br></div><br></div>";
|
|
}
|
|
|
|
$html_string .= '</div>';
|
|
|
|
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, $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 org_id = original
|
|
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 org_id = original
|
|
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 org_id = original
|
|
AND shadow = 'no'
|
|
AND sub NOT IN ($out)
|
|
ORDER BY ROWID DESC
|
|
LIMIT '$pagination'");
|
|
}
|
|
|
|
$result = $statement->execute();
|
|
|
|
echo '<div class="posts" id="posts">';
|
|
|
|
while ($row = $result->fetchArray(SQLITE3_NUM)) {
|
|
|
|
$html_string = '<div class="message first">';
|
|
|
|
$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 = "/mob/r/$sub/$org_id/op";
|
|
$link_string_2 = "/mob/r/$sub/$org_id";
|
|
$link_string_3 = "/mob/s/$sub";
|
|
|
|
$html_string .= "<a href='$link_string_3'>$sub</a> "
|
|
. "<a href='$link_string_1'>#$post_id</a>"
|
|
. "<br>$post_text<br>"
|
|
. "<a href='$link_string_2'>reply</a></div>";
|
|
|
|
echo "$html_string";
|
|
|
|
print_replies($db, $sub, $post_id, $org_id, $settings);
|
|
echo '<hr /><hr />';
|
|
}
|
|
|
|
}
|
|
|
|
// Show each post of an individual feed
|
|
function print_individual_feed($db, $settings, $ex_subs, $in_subs)
|
|
{
|
|
|
|
echo '<div class="posts" id="posts">';
|
|
|
|
$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 org_id = original
|
|
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 org_id = original
|
|
AND shadow = 'no'
|
|
AND sub IN ($in)
|
|
ORDER BY ROWID DESC");
|
|
}
|
|
|
|
$result = $statement->execute();
|
|
|
|
while ($row = $result->fetchArray(SQLITE3_NUM)) {
|
|
|
|
$html_string = '<div class="message first">';
|
|
|
|
$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 = "/mob/r/$sub/$org_id/op";
|
|
$link_string_2 = "/mob/r/$sub/$org_id";
|
|
$link_string_3 = "/mob/s/$sub";
|
|
|
|
$html_string .= "<a href='$link_string_3'>$sub</a> "
|
|
. "<a href='$link_string_1'>#$post_id</a>"
|
|
. "<br>$post_text<br>"
|
|
. "<a href='$link_string_2'>reply</a></div>";
|
|
|
|
echo "$html_string";
|
|
|
|
print_replies($db, $sub, $post_id, $org_id, $settings);
|
|
|
|
echo '<br>';
|
|
}
|
|
|
|
return $counter;
|
|
}
|
|
|
|
// Print the hamburger menu with the links
|
|
function print_hamburger_menu($db, $settings, $sub, $total_posts, $page)
|
|
{
|
|
$html_string = '<div class="menu-container">'
|
|
. "<h3>$sub</h3>"
|
|
. '<input type="checkbox" id="menu-toggle"'
|
|
. ' class="menu-toggle">'
|
|
. '<label for="menu-toggle" class="hamburger">'
|
|
. '<div class="bar"></div>'
|
|
. '<div class="bar"></div>'
|
|
. '<div class="bar"></div>'
|
|
. '</label>'
|
|
. '<nav class="nav">'
|
|
. '<ul>';
|
|
if ( $sub != 'overboard' ){
|
|
$html_string .= '<li><a href="/mob/s/overboard">overboard</a></li>';
|
|
} elseif ( $sub != 'main' ){
|
|
$html_string .= '<li><a href="/mob/s/main">main</a></li>';
|
|
}
|
|
$html_string .= '<li><a href="/mob/su">show subs</a></li>'
|
|
. '<div class="bar"></div>'
|
|
. "<li><a href='/d/$sub'>save $sub</a></li>";
|
|
|
|
|
|
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']);
|
|
|
|
$link_string_1 = "/mob/s/$sub/$next_page";
|
|
$link_string_2 = "/mob/s/$sub/$prev_page";
|
|
$link_string_3 = "/mob/s/$sub/all";
|
|
|
|
if ( ($number_first_message > 1)
|
|
&& ($number_last_message < $total_posts) ) {
|
|
$html_string .= "<li><a href='$link_string_1'>show older</a></li>"
|
|
. "<li><a href='$link_string_2'>show newer</a></li>"
|
|
. "<li><a href='$link_string_3'>show all</li>";
|
|
} elseif ($number_last_message == $total_posts) {
|
|
$html_string .= "<li><a href='$link_string_2'>show newer</a></li>"
|
|
. "<li><a href='$link_string_3'>show all</li>";
|
|
|
|
} else {
|
|
$html_string .= "<li><a href='$link_string_1'>show older</a></li>"
|
|
. "<li><a href='$link_string_3'>show all</li>";
|
|
|
|
}
|
|
}
|
|
|
|
$html_string .= '</ul></nav></div>';
|
|
|
|
echo "$html_string";
|
|
|
|
}
|
|
|
|
// Give all the http-headers to the client, mostly for opsec reasons.
|
|
// After, print the html header to open the document for the browser.
|
|
function print_header()
|
|
{
|
|
|
|
// header( 'Content-Type: text/html; charset=utf-8');
|
|
// header( 'X-Frame-Options: DENY', FALSE);
|
|
// header( 'HTTP Cross-Origin-Opener-Policy: same-origin', FALSE);
|
|
// header( 'Cross-Origin-Resource-Policy: same-site', FALSE);
|
|
// header( 'Permissions-Policy: geolocation=(), camera=(), microphone=()',
|
|
// FALSE);
|
|
// header( 'Permissions-Policy: interest-cohort=()', FALSE);
|
|
// header( 'Server: webserver', FALSE);
|
|
// header( 'X-DNS-Prefetch-Control: off', FALSE);
|
|
// header( 'Cache-Control: no-cache', FALSE);
|
|
// header( 'Pragma: no-cache', FALSE);
|
|
|
|
// nginx throws an error with those, and gives back 502 - bad gateway
|
|
// does not happen with other versions, and also not in the desktop
|
|
// version of the page - rather strange
|
|
|
|
$html_string = '<!DOCTYPE html><html lang="en"><head>'
|
|
. '<meta charset="UTF-8">'
|
|
. '<meta name="viewport" content="width=device-width,'
|
|
. ' initial-scale=1.0"><!-- Main stylesheet -->'
|
|
. ' <link rel="stylesheet" href="/css/mobile.css"> '
|
|
. '<!-- Themes are now modular and easy to modify -->'
|
|
. '<link rel="stylesheet" href="/css/dirtpath-theme.css">';
|
|
|
|
echo "$html_string";
|
|
}
|
|
|
|
// Show all replies to a given post
|
|
function print_replies($db, $sub, $post_id, $org_id, $settings)
|
|
{
|
|
// REWRITE TO FIX, css is different now.
|
|
|
|
$sub_statement = $db->prepare("SELECT post_id, org_id,
|
|
sub, text, timestamp,
|
|
name, tripcode
|
|
FROM threads
|
|
WHERE sub = '$sub'
|
|
AND org_id = '$org_id'
|
|
AND org_id != original
|
|
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]}";
|
|
$sub_timestamp = "{$row[4]}";
|
|
$sub_name = "{$row[5]}";
|
|
$sub_tripcode = "{$row[6]}";
|
|
if ($sub_post_id != $sub_org_id) {
|
|
$counter++;
|
|
$post = array();
|
|
$sub_post_text = break_text(bbcode_to_html
|
|
($sub_text, $settings, $sub),
|
|
$settings);
|
|
array_push($post, $sub_post_id);
|
|
array_push($post, $sub_org_id);
|
|
array_push($post, $sub_post_text);
|
|
array_push($post, $sub_timestamp);
|
|
array_push($post, $sub_name);
|
|
array_push($post, $sub_tripcode);
|
|
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];
|
|
$last_post_timestamp = $last_answer[3];
|
|
$last_post_name = $last_answer[4];
|
|
$last_post_tripcode = $last_answer[5];
|
|
|
|
$html_string = '';
|
|
|
|
if ($counter > 1) {
|
|
// we have at least one reply
|
|
$html_string .= "<details>"
|
|
. "<summary>Show $display_number more replies</summary>";
|
|
|
|
foreach ($answers as $display_msg) {
|
|
$answer_post_id = $display_msg[0];
|
|
$answer_post_text = $display_msg[2];
|
|
$answer_post_timestamp = $display_msg[3];
|
|
$answer_post_name = $display_msg[4];
|
|
$answer_post_tripcode = $display_msg[5];
|
|
|
|
$link_string_1 = "/mob/r/$sub/$org_id/$answer_post_id/5";
|
|
|
|
$html_string .= "<div class='message replies'>"
|
|
. "<p id=\"$answer_post_id" . "_" . "$sub\"></p>"
|
|
. "<a href='$link_string_1'>#$answer_post_id</a>";
|
|
|
|
if ( !empty($answer_post_timestamp) &&
|
|
$settings['enable_timestamps'] ) {
|
|
$html_string .= "<small>:$answer_post_timestamp</small>";
|
|
}
|
|
|
|
$html_string .= "<br><br><code>$answer_post_text</code><br><br>";
|
|
|
|
if ( !empty($answer_post_name) &&
|
|
$settings['enable_edit']) {
|
|
$html_string .= "<a href='/e/$sub/$answer_post_id'>edit</a> ";
|
|
}
|
|
|
|
if ( !empty($answer_post_name) &&
|
|
$settings['enable_tripcodes']) {
|
|
$name_string = $answer_post_name;
|
|
$link_string_4 = "/mob/u/$name_string";
|
|
$html_string .= "<a href='$link_string_4'>$name_string</a>";
|
|
}
|
|
|
|
$html_string .= '</div>';
|
|
}
|
|
|
|
$html_string .= "</details>";
|
|
}
|
|
|
|
$link_string_1 = "/mob/r/$sub/$org_id/$last_post_id";
|
|
|
|
$html_string .= "<div class='message last'>"
|
|
. "<p id=\"$last_post_id" . "_" . "$sub\"></p>"
|
|
. "<a href='$link_string_1'>#$last_post_id</a>";
|
|
|
|
if ( !empty($last_post_timestamp) &&
|
|
$settings['enable_timestamps'] ) {
|
|
$html_string .= "<small>:$last_post_timestamp</small>";
|
|
}
|
|
|
|
$html_string .= "<br><br>$last_post_text<br><br>";
|
|
|
|
if ( !empty($last_post_name) &&
|
|
$settings['enable_edit']) {
|
|
$html_string .= "<a href='/mob/e/$sub/$last_post_id'>edit</a> ";
|
|
}
|
|
|
|
if ( !empty($last_post_name) &&
|
|
$settings['enable_tripcodes']) {
|
|
$name_string = $last_post_name;
|
|
$link_string_4 = "/mob/u/$name_string";
|
|
$html_string .= "<a href='$link_string_4'>$name_string</a>";
|
|
}
|
|
|
|
$html_string .= '</div>';
|
|
|
|
echo "$html_string";
|
|
}
|
|
|
|
|
|
// Show each post in a sub
|
|
function print_sub($db, $sub, $settings, $page)
|
|
{
|
|
|
|
echo '<div class="posts" id="posts">';
|
|
|
|
$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 original = 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 original = 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 original = org_id
|
|
ORDER BY ROWID DESC
|
|
LIMIT '$pagination'");
|
|
}
|
|
|
|
$result = $statement->execute();
|
|
|
|
while ($row = $result->fetchArray(SQLITE3_NUM)) {
|
|
|
|
$html_string = '<div class="message first">';
|
|
|
|
$post_id = "{$row[0]}";
|
|
$org_id = "{$row[1]}";
|
|
$text = "{$row[3]}";
|
|
$post_text = break_text(bbcode_to_html($text, $settings), $settings);
|
|
|
|
$id_text = make_id_text($post_id);
|
|
$link_string_1 = "/mob/r/$sub/$org_id/op";
|
|
$link_string_2 = "/mob/r/$sub/$org_id";
|
|
|
|
$html_string .= "<a href='$link_string_1'>#$id_text</a>"
|
|
. "<br>$post_text<br>"
|
|
. "<a href='$link_string_2'>reply</a></div>";
|
|
|
|
echo "$html_string";
|
|
|
|
print_replies($db, $sub, $post_id, $org_id, $settings);
|
|
echo '<hr /><hr />';
|
|
}
|
|
|
|
echo "</div>";
|
|
|
|
}
|
|
|
|
// 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 positions 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;
|
|
|
|
}
|
|
|
|
// brings the user to the actual target that was requested before the
|
|
// portal took over
|
|
function redirect_target()
|
|
{
|
|
|
|
$target = $_POST['portal'];
|
|
|
|
header( "refresh:0;url=$target" );
|
|
|
|
$html_string = "<!DOCTYPE html><html><head><style>"
|
|
. "body {font-size:25px;"
|
|
. "background-color: black;"
|
|
. "color: #33cccc;"
|
|
. "text-align: center;"
|
|
. "width: 30em;"
|
|
. "margin-left: auto;"
|
|
. "margin-right: auto;}"
|
|
. "a:link, a:visited {color: red;}"
|
|
. "</style></head><body><br><br><br><code>"
|
|
. "Redirection ongoing. If that does not work, click"
|
|
. " <a href='$target'>here</a>.</code>";
|
|
|
|
echo "$html_string";
|
|
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
|
|
$short_modes = array (
|
|
'su', 'iv', 'p', 'r',
|
|
'v', 's', 'd'
|
|
);
|
|
|
|
$long_modes = array (
|
|
'subs', 'individual_view', 'post', 'reply',
|
|
'view', 'view', 'dump'
|
|
);
|
|
|
|
if ( (!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(4, '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(4, '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(4, '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;
|
|
}
|
|
|
|
// 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 = filter($_POST['org_id'], 'num', 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 = filter($_POST['sub'], 'alnum', $settings['max_name_sub']);
|
|
} elseif (get_pretty_vars_count() > 1) {
|
|
$sub = read_pretty_vars(3, '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() == 7) {
|
|
$quote = read_pretty_vars(5, 'alnum', 10);
|
|
// read from fourth position, and a post id does not need more
|
|
// than 10 digits
|
|
} else {
|
|
$quote = '';
|
|
}
|
|
|
|
return $quote;
|
|
|
|
}
|
|
|
|
// Show the postform with or without the captcha (according to setting),
|
|
// if on main show also the field to create new subs
|
|
function show_post_form($db, $msg, $sub, $settings, $org_id, $quote, $ip)
|
|
{
|
|
|
|
if ( (check_free_space($db, $settings) == FALSE) ) {
|
|
echo '<h3>No posting possible, no space on filesystem</h3>';
|
|
return;
|
|
}
|
|
|
|
if ( (post_block_user($db, $settings, $ip) != TRUE) ) {
|
|
echo '<h3>Max posts exhausted. Retry later.</h3>';
|
|
return;
|
|
}
|
|
|
|
$html_string = '<form action="/mob/p" method="POST" class="box-input">';
|
|
|
|
if ( ($sub != 'main') ) {
|
|
$html_string .= "<input type='hidden' name='sub' value='$sub'>";
|
|
}
|
|
|
|
if ( (!empty($org_id)) ) {
|
|
$html_string .= "<input type='hidden' name='org_id' value='$org_id'>";
|
|
}
|
|
|
|
$token = make_token();
|
|
|
|
$html_string .= "<input type='hidden' name='post_token' value='$token'>"
|
|
. '<textarea id="text" name="text"'
|
|
. ' placeholder="Say something"></textarea>'
|
|
. '<button type="submit" id="send-button">Send</button>'
|
|
. '</form>';
|
|
|
|
$current = time();
|
|
$hash = hash('sha512', $token);
|
|
|
|
$statement = $db->prepare("INSERT OR IGNORE INTO captchas(hash, unix_timestamp)
|
|
VALUES ('$hash', '$current')");
|
|
$statement->execute();
|
|
|
|
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, $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 = "<h3> Subs with some traffic:</h3>"
|
|
. '<div class="message first">'
|
|
. "<a href=/mob/s/overboard>overboard"
|
|
. "($counter_org/$replies)</a></div>";
|
|
|
|
$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 .= '<div class="message first">'
|
|
. "<a href=/mob/s/$display_sub[0]>$display_sub[0]"
|
|
. "($display_sub[1]/$display_sub[2])</a></div>";
|
|
}
|
|
|
|
$html_string .= '<br><br><br><h3>Other subs:</h3>';
|
|
|
|
$temp = array_reverse($low_traffic_subs);
|
|
$first_display_sub = array_pop($temp);
|
|
$low_traffic_subs = array_reverse($temp);
|
|
|
|
$html_string .= '<div class="message first">'
|
|
. "<a href=/mob/s/$first_display_sub[0]>"
|
|
. "$first_display_sub[0]"
|
|
. "($first_display_sub[1]/$first_display_sub[2])</a></div>";
|
|
|
|
foreach($low_traffic_subs as $display_sub) {
|
|
$html_string .= '<div class="message first">'
|
|
. "<a href=/mob/s/$display_sub[0]>$display_sub[0]"
|
|
. "($display_sub[1]/$display_sub[2])</a></div>";
|
|
}
|
|
|
|
$statement = $db->prepare("SELECT DISTINCT sub
|
|
FROM threads
|
|
WHERE shadow = 'no'
|
|
ORDER BY ROWID DESC
|
|
LIMIT 5");
|
|
$result = $statement->execute();
|
|
|
|
$html_string .= '<br><br><br><h3>Subs with recent posts:</h3>';
|
|
|
|
while ($row = $result->fetchArray(SQLITE3_NUM)) {
|
|
$sub = "{$row[0]}";
|
|
$html_string .= '<div class="message first">'
|
|
. "<a href=/mob/s/$sub>$sub</a></div>";
|
|
}
|
|
|
|
echo "$html_string";
|
|
}
|
|
|
|
// Show the existing subs to a user, without the count
|
|
function show_subs_no_count($db)
|
|
{
|
|
|
|
$html_string = "<h3><a href=/mob/s/overboard>overboard</a>";
|
|
|
|
$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 .= " | <a href=/mob/s/$sub>$sub</a>";
|
|
}
|
|
}
|
|
|
|
$html_string .= '</h3>';
|
|
|
|
echo "$html_string";
|
|
}
|
|
|
|
// Show the form that allows to set individual feeds.
|
|
function show_set_feeds_form($db, $settings)
|
|
{
|
|
|
|
$html_string = '<br><br><br><h3>Set your multifeed:<br></h3>'
|
|
. '<form action=\'/mob/iv\' method=\'post\'class="box-input">'
|
|
. 'Show everything except: '
|
|
. '<input type=\'text\' name=\'ex_subs\' placeholder=\''
|
|
. 'subs you do not want to see\'><br><br><br>'
|
|
. 'OR Show nothing but: '
|
|
. '<input type=\'text\' name=\'in_subs\' placeholder=\''
|
|
. 'the only subs you want to see\'><br><br><br>'
|
|
. '<button type=\'submit\' '
|
|
. 'value=\'Get my feeds\'>'
|
|
. '<br></form></div>';
|
|
|
|
echo "$html_string";
|
|
}
|
|
|
|
//++++++++++++++++ Actual control flow starts here +++++++++++++++++++//
|
|
|
|
$db_file = $settings['work_dir'] . $settings['board_file'];
|
|
// initialize db
|
|
$db = new SQLite3($db_file, SQLITE3_OPEN_CREATE | SQLITE3_OPEN_READWRITE);
|
|
$db->busyTimeout(50000);
|
|
// set a long timeout
|
|
|
|
// if this condition is fulfilled, we have most likely crashed before
|
|
// need to do real error handling (rewrite)
|
|
if ( (!file_exists($db_file)) ) {
|
|
echo "<h3>Cannot find file $db_file, are the settings correct ?</h3>";
|
|
exit;
|
|
}
|
|
|
|
//make tables if needed
|
|
make_tables($db);
|
|
|
|
// checks with which mode we are called
|
|
$short_mode = read_pretty_vars(2, 'alnum', 20);
|
|
// we read from the first position. Mode descriptions need 20 characters max.
|
|
|
|
// try to check the visitors (local) ip. On success, check if this ip should
|
|
// be blocked or not (according to the settings in the config file)
|
|
// also check if the portal has been passed or not, and display it
|
|
// if not (and it is enabled).
|
|
// Updated with more variables than just the (local) ip, it's now working
|
|
// surprisingly well, also on tor. The variable is still called 'ip'.
|
|
if ( (!empty($_SERVER['REMOTE_ADDR']))
|
|
&& ($settings['enable_logging'] == TRUE) ) {
|
|
$visitor_ip = $_SERVER['REMOTE_ADDR'];
|
|
|
|
$check_server = array (
|
|
'HTTP_USER_AGENT',
|
|
'HTTP_ACCEPT_ENCODING',
|
|
'HTTP_ACCEPT_LANGUAGE',
|
|
'HTTP_ACCEPT',
|
|
'HTTP_HOST',
|
|
'HTTP_X_REQUESTED_WITH',
|
|
'HTTP_X_I2P_DESTB64',
|
|
'HTTP_X_I2P_DESTB32',
|
|
'HTTP_X_I2P_DESTHASH',
|
|
'HTTP_UPGRADE_INSECURE_REQUESTS',
|
|
'HTTP_SEC_GPC',
|
|
'HTTP_PRIORITY',
|
|
'HTTP_SEC_FETCH_MODE',
|
|
'HTTP_SEC_FETCH_DEST'
|
|
);
|
|
|
|
foreach ($check_server as $id) {
|
|
if ( (!empty($_SERVER[$id])) ) {
|
|
$visitor_ip .= $_SERVER[$id];
|
|
}
|
|
}
|
|
|
|
$visitor_ip = hash('sha512', $visitor_ip);
|
|
|
|
if ( (empty($_POST['portal'])) && ($short_mode != 'b') ) {
|
|
check_portal($db, $settings, $visitor_ip);
|
|
} elseif ( (!empty($_POST['portal'])) ) {
|
|
$portal_message = 'pass';
|
|
log_event($db, $settings, 'portal', $portal_message, $visitor_ip);
|
|
redirect_target();
|
|
quit($db, '');
|
|
}
|
|
|
|
check_max_requests($db, $settings, $visitor_ip);
|
|
bot_block($db, $settings, $visitor_ip);
|
|
} else {
|
|
$visitor_ip = '';
|
|
}
|
|
|
|
$mode = set_mode($short_mode, $settings);
|
|
|
|
// First actions that are common to many modes: check which sub is
|
|
// requested and if it exists
|
|
$sub_wanted = array(
|
|
'dump', 'post', 'reply', 'view'
|
|
);
|
|
|
|
if ( (in_array($mode, $sub_wanted)) ) {
|
|
$sub = set_sub($settings);
|
|
}
|
|
|
|
$check_sub = array(
|
|
'dump', 'reply', 'view'
|
|
);
|
|
|
|
if ( (in_array($mode, $check_sub)) ) {
|
|
$sub_check = check_sub_exists($db, $sub);
|
|
|
|
if ($sub_check == FALSE) {
|
|
print_header();
|
|
quit($db, "<h3>Sub '$sub' does not exist.</h3>");
|
|
} else {
|
|
$sub = $sub_check;
|
|
}
|
|
}
|
|
|
|
// Show the header, this is for almost all modes
|
|
$show_header = array(
|
|
'individual_view', 'landing', 'post', 'reply',
|
|
'subs', 'view'
|
|
);
|
|
|
|
if ( (in_array($mode, $show_header)) ) {
|
|
print_header();
|
|
}
|
|
|
|
// Do particular actions for the mode we have been called with and exit.
|
|
// Note: there are no breaks in any of these cases, as there usually would.
|
|
// Instead, each case calls the quit function at some point, which exits
|
|
// the script.
|
|
switch($mode) {
|
|
|
|
// exports a thread, a sub or the whole board to a json file
|
|
// and sents it to the user
|
|
case 'dump':
|
|
$org_id = read_pretty_vars(3, 'number', 10);
|
|
// we read from the third postion, ten digits is enough
|
|
dump($db, $sub, $org_id, $settings);
|
|
quit($db, "");
|
|
|
|
// shows an individual feed
|
|
case 'individual_view':
|
|
$ex_subs = array();
|
|
$in_subs = array();
|
|
$subs = read_pretty_vars(2, 'alnumplus', 1500);
|
|
// we read from the second position. 1500 characters
|
|
// should be enough to describe the subs (at least ~70 subs).
|
|
$title = '';
|
|
|
|
if (strpos($subs, '-') !== FALSE) {
|
|
$ex_subs = explode('-', $subs);
|
|
} elseif (strpos($subs, '+') !== FALSE) {
|
|
$in_subs = explode('+', $subs);
|
|
} elseif ( (!empty($_POST['ex_subs'])) ) {
|
|
$ex_subs = explode(' ', $_POST['ex_subs']);
|
|
} elseif ( (!empty($_POST['in_subs'])) ) {
|
|
$in_subs = explode(' ', $_POST['in_subs']);
|
|
} else {
|
|
quit($db, '<h3>Please choose subs to include or exclude.</h3>');
|
|
}
|
|
|
|
if (count($ex_subs) > 0) {
|
|
// if we have at least one exsub
|
|
foreach($ex_subs as $ex_sub) {
|
|
$ex_sub = filter($ex_sub, 'alnum', $settings['max_name_sub']);
|
|
|
|
if ( ($ex_sub != '')
|
|
&& (check_sub_exists($db, $ex_sub) == TRUE) ) {
|
|
$title .= "-" . $ex_sub;
|
|
}
|
|
|
|
}
|
|
|
|
$description = "ex";
|
|
} elseif (count($in_subs) > 0) {
|
|
// otherwise, if we have at least one insub
|
|
foreach($in_subs as $in_sub) {
|
|
$in_sub = filter($in_sub, 'alnum', $settings['max_name_sub']);
|
|
|
|
if ( ($in_sub != '')
|
|
&& (check_sub_exists($db, $in_sub) == TRUE) ) {
|
|
$title .= "+" . $in_sub;
|
|
}
|
|
|
|
}
|
|
|
|
$description = "inc";
|
|
}
|
|
|
|
if ( (empty($title)) ) {
|
|
$quit_message = '<h3>The subs you chose do not exist. Please '
|
|
. 'choose existing subs to include or exclude.</h3>';
|
|
quit($db, $quit_message);
|
|
}
|
|
|
|
$html_string = "<title>$title</title></head>"
|
|
. '<body><div class="site-container">';
|
|
|
|
echo $html_string;
|
|
|
|
print_hamburger_menu($db, $settings, 'overboard', '0', '0');
|
|
|
|
$total_posts = print_individual_feed($db, $settings,
|
|
$ex_subs, $in_subs);
|
|
|
|
quit($db, "");
|
|
|
|
// displays the landing page as defined in the config file
|
|
// also: check if there was a request URI, if yes, mark as bot
|
|
case 'landing':
|
|
if ( ( (!empty($_SERVER['REQUEST_URI'])) )
|
|
&& ( (strlen($_SERVER['REQUEST_URI']) > 6) ) ) {
|
|
// if more than 6 chars to go to the landing page,
|
|
// this could be a bot, especially if repeated
|
|
$bot_message = 'landing page bot request';
|
|
log_event($db, $settings, 'bot', $bot_message, $visitor_ip);
|
|
}
|
|
$html_string = "<title>landing</title></head>"
|
|
. '<body><div class="site-container">'
|
|
. '<div class="posts" id="posts">';
|
|
|
|
echo "$html_string";
|
|
|
|
print_hamburger_menu($db, $settings, 'overboard', '', '');
|
|
echo '<div class="posts" id="posts">';
|
|
echo '<div class="message first">';
|
|
|
|
show_landing_page('');
|
|
show_subs_no_count($db);
|
|
echo '</div>';
|
|
|
|
quit($db, "");
|
|
|
|
// makes a new post
|
|
case 'post':
|
|
$org_id = set_post_org_id();
|
|
$text = strip_tags($_POST['text']);
|
|
$text_id = hash('sha512', $text);
|
|
|
|
check_spam($db, $text, $settings);
|
|
|
|
if (check_free_space($db, $settings) == FALSE) {
|
|
$quit_message = '<h3>Filesystem is almost full, '
|
|
. 'no posting possible!</h3>';
|
|
quit($db, $quit_message);
|
|
} elseif ( (check_original_content($db, $settings, $sub,
|
|
$text_id, $org_id) == FALSE) ) {
|
|
$quit_message = '<h3>This text has been posted before, '
|
|
. 'the admin requests original content.</h3>';
|
|
quit($db, $quit_message);
|
|
} elseif ( (mb_strtolower(substr( $sub, 0, 9 )) === 'overboard') ) {
|
|
// overboard has nine characters
|
|
$quit_message = '<h3>Subs cannot be named \'overboard\', also'
|
|
. ' their names cannot start with it.</h3>';
|
|
quit($db, $quit_message);
|
|
} elseif ( (mb_strtolower(substr( $sub, 0, 4 )) === 'main')
|
|
&& ($sub != 'main') ) {
|
|
// main has four characters
|
|
quit($db, '<h3>Subs names cannot start with \'main\'.</h3>');
|
|
} elseif ( (post_block_user($db, $settings, $visitor_ip) != TRUE) ) {
|
|
$post_block_message = '429 - too many user posts';
|
|
log_event($db, $settings, 'user', $post_block_message, $visitor_ip);
|
|
header( 'HTTP/1.1 429 Too Many Requests' );
|
|
quit($db, '429');
|
|
}
|
|
|
|
check_captcha($db, $settings);
|
|
|
|
$post_message = "post attempt";
|
|
log_event($db, $settings, 'user', $post_message, $visitor_ip);
|
|
|
|
make_post($db, $sub, $settings, $text, $org_id);
|
|
answer_redirect($sub);
|
|
|
|
quit($db, "");
|
|
|
|
// shows a thread
|
|
case 'reply':
|
|
$org_id = set_org_id();
|
|
|
|
if ( (!check_org_id_exists($db, $sub, $org_id)) ) {
|
|
quit($db, "<h3>Post $org_id on sub $sub does not exist.</h3>");
|
|
}
|
|
|
|
$quote = set_quote();
|
|
$msg = ($sub . '/' . $org_id);
|
|
|
|
$html_string = "<title>$msg</title></head>"
|
|
. '<body><div class="site-container">';
|
|
// . '<div class="posts" id="posts">';
|
|
|
|
echo "$html_string";
|
|
|
|
print_thread($db, $sub, $settings, $org_id);
|
|
show_post_form($db, $msg, $sub, $settings, $org_id,
|
|
$quote, $visitor_ip);
|
|
|
|
quit($db, '');
|
|
|
|
// displays all subs, including their message counts
|
|
case 'subs':
|
|
$html_string = "<title>available subs</title></head>"
|
|
. '<body><div class="site-container">'
|
|
. '<div class="posts" id="posts">';
|
|
echo $html_string;
|
|
show_subs_count($db, $settings);
|
|
// show_set_feeds_form($db, $settings);
|
|
echo '</div></div></body></html>';
|
|
quit($db, "");
|
|
|
|
// shows a sub or the overboard
|
|
case 'view':
|
|
$page = set_page($mode);
|
|
|
|
if ( $sub == 'overboard' ) {
|
|
$title = $settings['title'];
|
|
} else {
|
|
$title = "sub/$sub";
|
|
}
|
|
|
|
$html_string = "<title>$title</title></head>"
|
|
. '<body><div class="site-container">';
|
|
|
|
echo $html_string;
|
|
|
|
$total_posts = give_total_posts($db, $sub, TRUE, $settings);
|
|
|
|
if ( ($page != 'all')
|
|
&& ((($settings['pagination'] * $page) >
|
|
($total_posts + $settings['pagination']))) ) {
|
|
quit($db, "<h3>Not enough posts to display page = $page...!</h3>");
|
|
}
|
|
|
|
print_hamburger_menu($db, $settings, $sub, $total_posts, $page);
|
|
|
|
if ( ($sub != 'overboard') ) {
|
|
print_sub($db, $sub, $settings, $page);
|
|
show_post_form($db, '', $sub, $settings, '', '', $visitor_ip);
|
|
} else {
|
|
print_overboard($db, $settings, $page);
|
|
}
|
|
|
|
echo '</div></div></body></html>';
|
|
|
|
quit($db, "");
|
|
|
|
}
|
|
|
|
// EOF
|