Compare commits

..

27 Commits

Author SHA1 Message Date
towards-a-new-leftypol c94774c9ab ajax POST: return whole thread 2024-02-26 20:04:46 +00:00
towards-a-new-leftypol cb7c818996 Fix posting images
- since we return the new post html now, we need the Post class to
  not try and decode files as json, since we're not loading that class
  from the db anymore.
2024-02-26 14:18:20 -05:00
towards-a-new-leftypol 7838520c63 thread_updater.js: listen to ajax_after_post event 2024-02-26 13:55:36 -05:00
Jon 00c37364c6
Merge remote-tracking branch 'refs/remotes/origin/deploy_spamnoticer' into deploy_spamnoticer 2024-02-26 18:48:32 +00:00
Jon 7ea730f14f
thread_autoupdater.js: add interop event 2024-02-26 18:39:41 +00:00
Jon a5969f4e80
thread_autoupdater.js: decrease tslp backoff 2024-02-26 18:13:20 +00:00
Jon e6ef9d0d83
js/lcn/classes.js: link toggle label to checkbox 2024-02-26 18:12:57 +00:00
Jon d9a2cd78e0
thread_autoupdater.js: fix for threads with only an op 2024-02-25 22:58:24 +00:00
Jon 97ff7914bb
thread_autoupdater.js: fix tslp calc 2024-02-25 22:39:22 +00:00
Jon e295c7b17c
js/lcn/classes.js: add semicolon 2024-02-25 22:28:05 +00:00
Jon fc733f694c
inc/instance-config.php: fix typo 2024-02-25 22:24:10 +00:00
Jon 0dad3dc61b
thread_autoupdater.js: Display status code on err 2024-02-25 22:21:16 +00:00
Jon a57e175b5a
thread_autoupdater.js: remove debug 2024-02-25 22:17:41 +00:00
Jon 3b0292f658
inc/instance-config.php: replace legacy with new autoreloader 2024-02-25 22:16:53 +00:00
Jon accae507a6
thread_autoupdater.js: add 2024-02-25 22:15:37 +00:00
Jon da8898624a
js/lcn/utils.js: remove unnessesary tab 2024-02-25 22:10:44 +00:00
Jon 9912a672f2
js/lcn/classes.js: add missing semicolon 2024-02-25 22:10:01 +00:00
Jon 650b8a8520
stylesheets/style.css: swap missing dashes 2024-02-25 22:07:48 +00:00
Jon 45c3fdfe4f
add supporting changes 2024-02-25 22:06:24 +00:00
Jon 2006aa6fd6
stylesheets/style.css: add space between barcol children 2024-02-25 22:04:55 +00:00
Jon b82269a54e
swap dashes in threadstat attribs 2024-02-25 21:44:58 +00:00
Jon 98856d6a6e
templates/thread.html: Replace top button with catalog button 2024-02-25 21:00:04 +00:00
Jon c772135db8
templates/thread.html: remove dev prefix 2024-02-25 20:58:30 +00:00
Jon 5b492e1d57
templates/thread.html: migrate threadbar and threadlinks 2024-02-25 20:52:30 +00:00
Jon 3a464a0c2a
stylesheets/style.css: add styles 2024-02-25 20:50:33 +00:00
Jon 34b72f25c5
stylesheets/style.css: add lcn_threadstats style 2024-02-24 22:17:55 +00:00
Jon c67fad9e6b
post_thread.html: use structured block for thread stats 2024-02-24 22:09:30 +00:00
48 changed files with 930 additions and 1238 deletions

2
.gitignore vendored
View File

@ -62,7 +62,7 @@ tf/
/random/
# Banners
# banners/*
banners/*
!banners/lain-bottom.png
#Fonts

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

View File

@ -11,364 +11,302 @@ $hidden_inputs_twig = array();
$logfile = "/tmp/lainchan_err.out";
function print_err($s) {
global $logfile;
$datetime = new Datetime();
file_put_contents(
$logfile,
$datetime->format(DateTime::ATOM) . " " . $s . "\n",
FILE_APPEND
);
global $logfile;
file_put_contents($logfile, $s . "\n", FILE_APPEND);
}
function getStackTraceAsString() {
$stackTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$stackTrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$traceString = '';
foreach ($stackTrace as $index => $entry) {
if ($index > 0) {
$traceString .= sprintf(
"#%d %s(%d): %s%s",
$index - 1,
isset($entry['file']) ? $entry['file'] : 'unknown',
isset($entry['line']) ? $entry['line'] : 0,
isset($entry['class']) ? $entry['class'] . $entry['type'] . $entry['function'] : $entry['function'],
PHP_EOL
);
$traceString = '';
foreach ($stackTrace as $index => $entry) {
if ($index > 0) {
$traceString .= sprintf(
"#%d %s(%d): %s%s",
$index - 1,
isset($entry['file']) ? $entry['file'] : 'unknown',
isset($entry['line']) ? $entry['line'] : 0,
isset($entry['class']) ? $entry['class'] . $entry['type'] . $entry['function'] : $entry['function'],
PHP_EOL
);
}
}
}
return $traceString;
return $traceString;
}
// print_err("\n\nSTART\n\n");
class AntiBot {
public $salt, $inputs = array(), $index = 0;
public $salt, $inputs = array(), $index = 0;
public static function randomString($length, $uppercase = false, $special_chars = false, $unicode_chars = false) {
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
if ($uppercase)
$chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if ($special_chars)
$chars .= ' ~!@#$%^&*()_+,./;\'[]\\{}|:<>?=-` ';
if ($unicode_chars) {
$len = strlen($chars) / 10;
for ($n = 0; $n < $len; $n++)
$chars .= mb_convert_encoding('&#' . mt_rand(0x2600, 0x26FF) . ';', 'UTF-8', 'HTML-ENTITIES');
}
$chars = preg_split('//u', $chars, -1, PREG_SPLIT_NO_EMPTY);
$ch = array();
// fill up $ch until we reach $length
while (count($ch) < $length) {
$n = $length - count($ch);
$keys = array_rand($chars, $n > count($chars) ? count($chars) : $n);
if ($n == 1) {
$ch[] = $chars[$keys];
break;
}
shuffle($keys);
foreach ($keys as $key)
$ch[] = $chars[$key];
}
$chars = $ch;
return implode('', $chars);
}
public static function make_confusing($string) {
$chars = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
foreach ($chars as &$c) {
if (mt_rand(0, 3) != 0)
$c = utf8tohtml($c);
else
$c = mb_encode_numericentity($c, array(0, 0xffff, 0, 0xffff), 'UTF-8');
}
return implode('', $chars);
}
public function __construct(array $salt = array()) {
global $config;
if (!empty($salt)) {
// create a salted hash of the "extra salt"
$this->salt = implode(':', $salt);
} else {
$this->salt = '';
}
shuffle($config['spam']['hidden_input_names']);
$input_count = mt_rand($config['spam']['hidden_inputs_min'], $config['spam']['hidden_inputs_max']);
$hidden_input_names_x = 0;
for ($x = 0; $x < $input_count ; $x++) {
if ($hidden_input_names_x === false || mt_rand(0, 2) == 0) {
// Use an obscure name
$name = $this->randomString(mt_rand(10, 40), false, false, $config['spam']['unicode']);
} else {
// Use a pre-defined confusing name
$name = $config['spam']['hidden_input_names'][$hidden_input_names_x++];
if ($hidden_input_names_x >= count($config['spam']['hidden_input_names']))
$hidden_input_names_x = false;
}
if (mt_rand(0, 2) == 0) {
// Value must be null
$this->inputs[$name] = '';
} elseif (mt_rand(0, 4) == 0) {
// Numeric value
$this->inputs[$name] = (string)mt_rand(0, 100000);
} else {
// Obscure value
$this->inputs[$name] = $this->randomString(mt_rand(5, 100), true, true, $config['spam']['unicode']);
}
}
}
public static function space() {
if (mt_rand(0, 3) != 0)
return ' ';
return str_repeat(' ', mt_rand(1, 3));
}
public function html($count = false) {
global $config;
$elements = array(
'<input type="hidden" name="%name%" value="%value%">',
'<input type="hidden" value="%value%" name="%name%">',
'<input name="%name%" value="%value%" type="hidden">',
'<input value="%value%" name="%name%" type="hidden">',
'<input style="display:none" type="text" name="%name%" value="%value%">',
'<input style="display:none" type="text" value="%value%" name="%name%">',
'<span style="display:none"><input type="text" name="%name%" value="%value%"></span>',
'<div style="display:none"><input type="text" name="%name%" value="%value%"></div>',
'<div style="display:none"><input type="text" name="%name%" value="%value%"></div>',
'<textarea style="display:none" name="%name%">%value%</textarea>',
'<textarea name="%name%" style="display:none">%value%</textarea>'
);
$html = '';
if ($count === false) {
$count = mt_rand(1, round(abs(count($this->inputs) / 15)) + 1);
}
if ($count === true) {
// all elements
$inputs = array_slice($this->inputs, $this->index);
} else {
$inputs = array_slice($this->inputs, $this->index, $count);
}
$this->index += count($inputs);
foreach ($inputs as $name => $value) {
$element = false;
while (!$element) {
$element = $elements[array_rand($elements)];
$element = str_replace(' ', self::space(), $element);
if (mt_rand(0, 5) == 0)
$element = str_replace('>', self::space() . '>', $element);
if (strpos($element, 'textarea') !== false && $value == '') {
// There have been some issues with mobile web browsers and empty <textarea>'s.
$element = false;
}
}
$element = str_replace('%name%', utf8tohtml($name), $element);
if (mt_rand(0, 2) == 0)
$value = $this->make_confusing($value);
else
$value = utf8tohtml($value);
if (strpos($element, 'textarea') === false)
$value = str_replace('"', '&quot;', $value);
$element = str_replace('%value%', $value, $element);
$html .= $element;
}
return $html;
}
public function reset() {
$this->index = 0;
}
public function hash() {
global $config;
public static function randomString($length, $uppercase = false, $special_chars = false, $unicode_chars = false) {
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
if ($uppercase)
$chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if ($special_chars)
$chars .= ' ~!@#$%^&*()_+,./;\'[]\\{}|:<>?=-` ';
if ($unicode_chars) {
$len = strlen($chars) / 10;
for ($n = 0; $n < $len; $n++)
$chars .= mb_convert_encoding('&#' . mt_rand(0x2600, 0x26FF) . ';', 'UTF-8', 'HTML-ENTITIES');
}
$chars = preg_split('//u', $chars, -1, PREG_SPLIT_NO_EMPTY);
$ch = array();
// fill up $ch until we reach $length
while (count($ch) < $length) {
$n = $length - count($ch);
$keys = array_rand($chars, $n > count($chars) ? count($chars) : $n);
if ($n == 1) {
$ch[] = $chars[$keys];
break;
}
shuffle($keys);
foreach ($keys as $key)
$ch[] = $chars[$key];
}
$chars = $ch;
return implode('', $chars);
}
public static function make_confusing($string) {
$chars = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
foreach ($chars as &$c) {
if (mt_rand(0, 3) != 0)
$c = utf8tohtml($c);
else
$c = mb_encode_numericentity($c, array(0, 0xffff, 0, 0xffff), 'UTF-8');
}
return implode('', $chars);
}
public function __construct(array $salt = array()) {
global $config;
if (!empty($salt)) {
// create a salted hash of the "extra salt"
$this->salt = implode(':', $salt);
} else {
$this->salt = '';
}
shuffle($config['spam']['hidden_input_names']);
$input_count = mt_rand($config['spam']['hidden_inputs_min'], $config['spam']['hidden_inputs_max']);
$hidden_input_names_x = 0;
for ($x = 0; $x < $input_count ; $x++) {
if ($hidden_input_names_x === false || mt_rand(0, 2) == 0) {
// Use an obscure name
$name = $this->randomString(mt_rand(10, 40), false, false, $config['spam']['unicode']);
} else {
// Use a pre-defined confusing name
$name = $config['spam']['hidden_input_names'][$hidden_input_names_x++];
if ($hidden_input_names_x >= count($config['spam']['hidden_input_names']))
$hidden_input_names_x = false;
}
if (mt_rand(0, 2) == 0) {
// Value must be null
$this->inputs[$name] = '';
} elseif (mt_rand(0, 4) == 0) {
// Numeric value
$this->inputs[$name] = (string)mt_rand(0, 100000);
} else {
// Obscure value
$this->inputs[$name] = $this->randomString(mt_rand(5, 100), true, true, $config['spam']['unicode']);
}
}
}
public static function space() {
if (mt_rand(0, 3) != 0)
return ' ';
return str_repeat(' ', mt_rand(1, 3));
}
public function html($count = false) {
global $config;
$elements = array(
'<input type="hidden" name="%name%" value="%value%">',
'<input type="hidden" value="%value%" name="%name%">',
'<input name="%name%" value="%value%" type="hidden">',
'<input value="%value%" name="%name%" type="hidden">',
'<input style="display:none" type="text" name="%name%" value="%value%">',
'<input style="display:none" type="text" value="%value%" name="%name%">',
'<span style="display:none"><input type="text" name="%name%" value="%value%"></span>',
'<div style="display:none"><input type="text" name="%name%" value="%value%"></div>',
'<div style="display:none"><input type="text" name="%name%" value="%value%"></div>',
'<textarea style="display:none" name="%name%">%value%</textarea>',
'<textarea name="%name%" style="display:none">%value%</textarea>'
);
$html = '';
if ($count === false) {
$count = mt_rand(1, round(abs(count($this->inputs) / 15)) + 1);
}
if ($count === true) {
// all elements
$inputs = array_slice($this->inputs, $this->index);
} else {
$inputs = array_slice($this->inputs, $this->index, $count);
}
$this->index += count($inputs);
foreach ($inputs as $name => $value) {
$element = false;
while (!$element) {
$element = $elements[array_rand($elements)];
$element = str_replace(' ', self::space(), $element);
if (mt_rand(0, 5) == 0)
$element = str_replace('>', self::space() . '>', $element);
if (strpos($element, 'textarea') !== false && $value == '') {
// There have been some issues with mobile web browsers and empty <textarea>'s.
$element = false;
}
}
$element = str_replace('%name%', utf8tohtml($name), $element);
if (mt_rand(0, 2) == 0)
$value = $this->make_confusing($value);
else
$value = utf8tohtml($value);
if (strpos($element, 'textarea') === false)
$value = str_replace('"', '&quot;', $value);
$element = str_replace('%value%', $value, $element);
$html .= $element;
}
return $html;
}
public function reset() {
$this->index = 0;
}
public function hash() {
global $config;
// This is the tricky part: create a hash to validate it after
// First, sort the keys in alphabetical order (A-Z)
$inputs = $this->inputs;
ksort($inputs);
$hash = '';
// Iterate through each input
foreach ($inputs as $name => $value) {
$hash .= $name . '=' . $value;
}
// Add a salt to the hash
$hash .= $config['cookies']['salt'];
// Use SHA1 for the hash
return sha1($hash . $this->salt);
}
public function printErrVars() { //DELETE ME
$inputs = $this->inputs;
ksort($inputs);
print_err("Antibot " . $this->hash() . " inputs: " . json_encode($inputs));
}
// This is the tricky part: create a hash to validate it after
// First, sort the keys in alphabetical order (A-Z)
$inputs = $this->inputs;
ksort($inputs);
$hash = '';
// Iterate through each input
foreach ($inputs as $name => $value) {
$hash .= $name . '=' . $value;
}
// Add a salt to the hash
$hash .= $config['cookies']['salt'];
// Use SHA1 for the hash
return sha1($hash . $this->salt);
}
}
function _create_antibot($board, $thread) {
global $config, $purged_old_antispam;
$antibot = new AntiBot(array($board, $thread));
if (!isset($purged_old_antispam)) {
$purged_old_antispam = true;
query('DELETE FROM ``antispam`` WHERE `expires` < UNIX_TIMESTAMP()') or error(db_error());
}
if ($thread)
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL');
else
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL');
$query->bindValue(':board', $board);
if ($thread)
$query->bindValue(':thread', $thread);
$query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
$query->execute() or error(db_error($query));
$query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
$query->bindValue(':board', $board);
$query->bindValue(':thread', $thread);
$query->bindValue(':hash', $antibot->hash());
$query->execute() or error(db_error($query));
//$antibot->printErrVars();
return $antibot;
}
function dumpVars($extra_salt) {
global $config;
$json_repr = json_encode($_POST);
print_err("Check Spam POST data: " . $json_repr);
if ($json_repr === false) {
print_err("Could not jsonify POST data: " . json_last_error_message());
}
/*
foreach ($_POST as $name => $value) {
$is_valid_input = in_array($name, $config['spam']['valid_inputs']) ? "valid" : "invalid";
print_err(" $name: $value ($is_valid_input)");
}
*/
if (!empty($extra_salt)) {
$extra_salt = implode(':', $extra_salt);
} else {
$extra_salt = '';
}
print_err("extra_salt: $extra_salt");
global $config, $purged_old_antispam;
$antibot = new AntiBot(array($board, $thread));
if (!isset($purged_old_antispam)) {
$purged_old_antispam = true;
query('DELETE FROM ``antispam`` WHERE `expires` < UNIX_TIMESTAMP()') or error(db_error());
}
if ($thread)
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL');
else
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL');
$query->bindValue(':board', $board);
if ($thread)
$query->bindValue(':thread', $thread);
$query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
$query->execute() or error(db_error($query));
$query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
$query->bindValue(':board', $board);
$query->bindValue(':thread', $thread);
$query->bindValue(':hash', $antibot->hash());
$query->execute() or error(db_error($query));
return $antibot;
}
function checkSpam(array $extra_salt = array()) {
global $config, $pdo;
global $config, $pdo;
#print_err("checkSpam start");
$extra_salt_orig = $extra_salt;
if (!isset($_POST['hash']))
return true;
/*
if (!isset($_POST['hash'])) {
print_err("checkSpam: _POST array doesn't have key 'hash', check failed.");
dumpVars($extra_salt_orig);
return true;
}
*/
$hash = $_POST['hash'];
if (isset($_POST['hash'])) {
$hash = $_POST['hash'];
} else {
$hash = "";
}
if (!empty($extra_salt)) {
// create a salted hash of the "extra salt"
$extra_salt = implode(':', $extra_salt);
} else {
$extra_salt = '';
}
if (!empty($extra_salt)) {
// create a salted hash of the "extra salt"
$extra_salt = implode(':', $extra_salt);
} else {
$extra_salt = '';
}
// Reconsturct the $inputs array
$inputs = array();
// Reconsturct the $inputs array
$inputs = array();
foreach ($_POST as $name => $value) {
if (in_array($name, $config['spam']['valid_inputs']))
continue;
foreach ($_POST as $name => $value) {
if (in_array($name, $config['spam']['valid_inputs']))
continue;
$inputs[$name] = $value;
}
$inputs[$name] = $value;
}
// Sort the inputs in alphabetical order (A-Z)
ksort($inputs);
// Sort the inputs in alphabetical order (A-Z)
ksort($inputs);
$_hash = '';
$_hash = '';
// Iterate through each input
foreach ($inputs as $name => $value) {
$_hash .= $name . '=' . $value;
}
// Iterate through each input
foreach ($inputs as $name => $value) {
$_hash .= $name . '=' . $value;
}
// Add a salt to the hash
$_hash .= $config['cookies']['salt'];
// Add a salt to the hash
$_hash .= $config['cookies']['salt'];
// Use SHA1 for the hash
$_hash = sha1($_hash . $extra_salt);
// Use SHA1 for the hash
$_hash = sha1($_hash . $extra_salt);
if ($hash != $_hash) {
return true;
}
if (empty($hash)) {
print_err("checkSpam: hash is either empty or was never present, check failed. Not flagging as spam however.");
dumpVars($extra_salt_orig);
// Ignore missing hash, because it was missing for some legitimate posters and bots tend to fill in any field.
return false;
} else if ($hash != $_hash) {
print_err("checkSpam: Hash values do not match! submitted hash value from POST data: $hash ; Computed hash value: $_hash");
dumpVars($extra_salt_orig);
return true;
}
$query = prepare('SELECT `passed` FROM ``antispam`` WHERE `hash` = :hash');
$query->bindValue(':hash', $hash);
$query->execute() or error(db_error($query));
if ((($passed = $query->fetchColumn(0)) === false) || ($passed > $config['spam']['hidden_inputs_max_pass'])) {
// there was no database entry for this hash. most likely expired.
return true;
}
$query = prepare('SELECT `passed` FROM ``antispam`` WHERE `hash` = :hash');
$query->bindValue(':hash', $hash);
$query->execute() or error(db_error($query));
if ((($passed = $query->fetchColumn(0)) === false) || ($passed > $config['spam']['hidden_inputs_max_pass'])) {
// there was no database entry for this hash. most likely expired.
print_err("checkSpam: there was no database entry for this hash. most likely expired. $hash");
dumpVars($extra_salt_orig);
return $hash; // do not consider this a failure case. (I would rather a spam post than a false-positive tbqh)
//return true;
}
return $hash;
return $hash;
}
function incrementSpamHash($hash) {
$query = prepare('UPDATE ``antispam`` SET `passed` = `passed` + 1 WHERE `hash` = :hash');
$query->bindValue(':hash', $hash);
$query->execute() or error(db_error($query));
$query = prepare('UPDATE ``antispam`` SET `passed` = `passed` + 1 WHERE `hash` = :hash');
$query->bindValue(':hash', $hash);
$query->execute() or error(db_error($query));
}

View File

@ -45,7 +45,7 @@ class Api {
$this->threadsPageFields = array(
'id' => 'no',
'bump' => 'bump',
'bump' => 'last_modified',
'board' => 'board',
);
@ -197,7 +197,6 @@ class Api {
$ips[] = $p->ip;
}
$apiPosts['posts'][0]['unique_ips'] = count(array_unique($ips));
$apiPosts['posts'][0]['last_modified'] = (empty($thread->posts) ? $thread : end($thread->posts))->time;
return $apiPosts;
}

View File

@ -33,8 +33,6 @@
// Enables captcha
$config['securimage'] = false;
// Limits captcha to TOR users
$config['captcha_tor_only'] = false;
// Global announcement -- the very simple version.
// This used to be wrongly named $config['blotter'] (still exists as an alias).

View File

@ -24,42 +24,6 @@ register_shutdown_function('fatal_error_handler');
$error_recursion=false;
/*
* Global anything is always a bad idea, but since all of this website's error handling comes
* down to calling this error function and quitting, we have no way of catching exceptions, for example
* during thumbnail creation.
*
* So push things to run in case of a crash into a list, and then run all of them in error.
*
* This will be exclusive to callbacks for posting a post callflow, not mod actions or anything else.
*/
function global_post_cleanup() {
global $post_cleanup_list;
foreach ($post_cleanup_list as $f) {
$f();
}
unset($post_cleanup_list);
}
function push_global_post_cleanup($f) {
global $post_cleanup_list;
if (!isset($post_cleanup_list)) {
$post_cleanup_list = array($f);
} else {
array_push($post_cleanup_list, $f);
}
}
function init_global_post_cleanup() {
global $post_cleanup_list;
$post_cleanup_list = array();
}
function error($message, $priority = true, $debug_stuff = false) {
global $board, $mod, $config, $db_error, $error_recursion;
@ -111,14 +75,11 @@ function error($message, $priority = true, $debug_stuff = false) {
$data['debug']=$debug_stuff;
}
print json_encode($data);
global_post_cleanup();
exit();
}
header($_SERVER['SERVER_PROTOCOL'] . ' 500 Internal Server Error');
global_post_cleanup();
die(Element('page.html', array(
'config' => $config,
'title' => _('Error'),

View File

@ -18,7 +18,6 @@ class Filter {
private bool $add_note;
private bool $noip;
private $find_time;
private string $ip;
public function __construct(array $arr) {
foreach ($arr as $key => $value) {

View File

@ -2905,34 +2905,3 @@ function strategy_first($fun, $array) {
return array('defer');
}
}
function ipIsLocal($ip) {
// Define the local IP ranges commonly used in private networks
$localRanges = [
'10.0.0.0/8', // Private network range 10.0.0.0 to 10.255.255.255
'172.16.0.0/12', // Private network range 172.16.0.0 to 172.31.255.255
'192.168.0.0/16', // Private network range 192.168.0.0 to 192.168.255.255
'127.0.0.0/8', // Loopback range for localhost
'169.254.0.0/16' // Link-local addresses
];
foreach ($localRanges as $range) {
if (ipInRange($ip, $range)) {
return true;
}
}
return false;
}
function ipInRange($ip, $range) {
// Split the range to get the base IP and the netmask
list($baseIP, $netmask) = explode('/', $range);
// Convert IPs into long format for easy comparison
$ipLong = ip2long($ip);
$rangeLong = ip2long($baseIP);
$maskLong = ~((1 << (32 - $netmask)) - 1);
// Check if the IP is in the given range
return (($ipLong & $maskLong) == ($rangeLong & $maskLong));
}

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,6 @@ $config['boards'] = array(
'edu',
'ga',
'ent',
'music',
'777',
'posad',
'i',
@ -42,7 +41,7 @@ $config['prepended_foreign_boards'] = array(
// Board categories. Only used in the "Categories" theme.
$config['categories'] = array(
'Boards' => array(
'Leftypol' => array(
'leftypol',
'b',
'WRK',
@ -51,7 +50,6 @@ $config['categories'] = array(
'edu',
'ga',
'ent',
'music',
'777',
'posad',
'i',
@ -66,19 +64,19 @@ $config['categories'] = array(
// with non-board links.
$config['custom_categories'] = array(
'Links' => array(
'New Multitude' => 'https://newmultitude.org',
'Booru image repository' => 'https://lefty.pictures',
'New Multitude' => 'https://newmultitude.org/',
'Booru image repository' => 'https://lefty.booru.org/',
'Leftypedia' => 'https://leftypedia.org/',
'Official chat room' => 'https://talk.leftychan.net/#/room/#welcome:matrix.leftychan.net',
'Nukechan' => 'https://nukechan.net',
#'Gitea instance' => 'https://git.leftychan.net',
'Gitea instance' => 'https://git.leftychan.net',
'Rules' => 'rules.html',
'Search' => 'search.php',
),
'Learning resources and blogs' => array(
'Michael Roberts\' blog' => 'https://thenextrecession.wordpress.com',
'A Critique Of Crisis Theory blog' => 'https://critiqueofcrisistheory.wordpress.com',
'Leftypedia' => 'https://wiki.leftypol.org',
'Marxist Internet Archive' => 'https://www.marxists.org'
'Michael Roberts\' blog' => 'https://thenextrecession.wordpress.com/',
'A Critique Of Crisis Theory blog' => 'https://critiqueofcrisistheory.wordpress.com/',
'Leftypedia' => 'https://leftypedia.org/',
'Marxist Internet Archive' => 'https://www.marxists.org/'
),
);
@ -134,8 +132,7 @@ $config['post_date'] = '%F (%a) %T';
$config['thread_subject_in_title'] = true;
$config['spam']['enabled'] = true;
$config['spam']['hidden_inputs_expire'] = 60 * 60 * 24 * 120; //keep hashes for 120 days in the database just in case someone posts on a slow board.
$config['spam']['enabled'] = false;
$config['spam_noticer']['enabled'] = true;
$config['spam_noticer']['base_url'] = 'http://localhost:8300';
$config['spam_noticer']['ui_url'] = 'https://spamnoticer.leftychan.net/static/index.html';
@ -145,8 +142,7 @@ $config['spam_noticer']['website_name'] = "leftychan";
/*
* Basic captcha. See also: captchaconfig.php
*/
$config['securimage'] = true;
$config['captcha_tor_only'] = true;
$config['securimage'] = false;
/*
* Permissions
@ -213,7 +209,6 @@ $config['allowed_ext_files'][] = 'pdf';
$config['allowed_ext_files'][] = 'txt';
$config['allowed_ext_files'][] = 'epub';
$config['allowed_ext_files'][] = 'djvu';
$config['allowed_ext_files'][] = 'opus';
// Compressed files
$config['allowed_ext_files'][] = 'zip';
$config['allowed_ext_files'][] = 'gz';
@ -262,7 +257,6 @@ $config['user_flags'] = array (
'egalitarianism' => 'Egalitarianism',
'egoism' => 'Egoism',
'eristocracy' => 'Έριστοκρατία',
'Eurasianism' => 'Eurasianism',
'eureka' => 'Eureka',
'eurocommunism' => 'Eurocommunism',
'farc' => 'Las FARC',
@ -286,10 +280,9 @@ $config['user_flags'] = array (
'luck_o_the_irish' => 'Luck O\' The Irish',
'luxemburg' => 'Luxemburg',
'marx' => 'Marx',
'marxism_blackpilism' => 'Marxism Blackpillism',
'mutualism' => 'Mutualism',
'naxalite' => 'Naxalite',
'nazbol' => 'National Bolshevik',
'nazbol' => 'Nazbol',
'nazi' => 'Nazi',
'ndfp' => 'NDFP',
'palestine' => 'Palestine',
@ -316,14 +309,13 @@ $config['user_flags'] = array (
'syndicalism' => 'Syndicalism',
'tankie' => 'Tankie',
'technocracy' => 'Technocracy',
'The_Other_Russia' => 'The Other Russia',
'think' => 'Think',
'transhumanism' => 'Transhumanism',
'united_farm_workers' => 'United Farm Workers',
'viet_cong' => 'Viet Cong',
'ypg' => 'YPG',
'yugoslavia' => 'Yugoslavia',
'zgang' => 'Z Gang'
'zapatista' => 'Zapatista'
);
@ -571,31 +563,11 @@ $config['filters'][] = array(
$config['filters'][] = array(
'condition' => array(
'body' => '/(^|\s)((https?):\/\/)?[\w-]{2,6}\.[a-z]{2,4}\/\w{2,8}(#[^\s]+)?(\s|$)/i', // url shorteners are not allowed
'body' => '/(^|\s)((https?):\/\/)?[\w-]{2,6}\.[a-z]{2,4}\/\w{2,8}(\s|$)/i', // url shorteners are not allowed
),
'action' => 'reject',
'message' => 'Url shorteners are not allowed'
);
// Rate limit posting new threads over Tor
$config['filters'][] = array(
'condition' => array(
/*
* Confusingly `isreply` is defined as:
* $flood_post['isreply'] == $post['op']
*
* We only want to look at OP posts in the flood table.
*/
'flood-match' => array('isreply'),
'OP' => true,
'flood-time-any' => 60 * 10 // 10 minutes
),
'noip' => true,
'ip' => '127.0.0.1',
'find_time' => 60 * 60 * 1,
'action' => 'reject',
'message' => 'New threads are being created too quickly. Wait [at most] 10 minutes'
);
$config['global_message'] = '<span><a href="https://talk.leftychan.net/#/room/#welcome:matrix.leftychan.net">Matrix</a></span> &nbsp; <span><a href="ircs://irc.leftychan.net:6697/#leftychan">IRC Chat</a></span> &nbsp; <span><a href="mumble://leftychan.net">Mumble</a></span> &nbsp; <span><a href="https://t.me/+RegtyzzrE0M1NDMx">Telegram</a></span> &nbsp; <span><a href="https://discord.gg/AcZeFKXPmZ">Discord</a></span>';
$config['global_message'] = '<span><a href="https://talk.leftychan.net/#/room/#welcome:matrix.leftychan.net" class="">Matrix</a></span> &nbsp; <span><a href="ircs://irc.leftychan.net:6697/#leftychan" class="">IRC Chat</a></span> &nbsp; <span><a href="mumble://leftychan.net" class="">Mumble</a></span> &nbsp; <span><a href="https://t.me/+RegtyzzrE0M1NDMx" class="">Telegram</a></span> &nbsp; <span><a href="https://discord.gg/AcZeFKXPmZ" class="">Discord</a></span>';
$config['debug'] = false;

View File

@ -338,8 +338,6 @@ class Securimage
/*%*********************************************************************%*/
// Properties
public $config_file;
public $gdnoisecolor;
/**
* The width of the captcha image
@ -2234,9 +2232,9 @@ class Securimage
$py = array(); // y coordinates of poles
$rad = array(); // radius of distortion from pole
$amp = array(); // amplitude
$x = round($this->image_width / 4); // lowest x coordinate of a pole
$x = ($this->image_width / 4); // lowest x coordinate of a pole
$maxX = $this->image_width - $x; // maximum x coordinate of a pole
$dx = mt_rand(intval($x / 10), $x); // horizontal distance between poles
$dx = mt_rand($x / 10, $x); // horizontal distance between poles
$y = mt_rand(20, $this->image_height - 20); // random y coord
$dy = mt_rand(20, $this->image_height * 0.7); // y distance
$minY = 20; // minimum y coordinate
@ -2279,7 +2277,7 @@ class Securimage
$x *= $this->iscale;
$y *= $this->iscale;
if ($x >= 0 && $x < $width2 && $y >= 0 && $y < $height2) {
$c = imagecolorat($this->tmpimg, intval($x), intval($y));
$c = imagecolorat($this->tmpimg, $x, $y);
}
if ($c != $bgCol) { // only copy pixels of letters to preserve any background image
imagesetpixel($this->im, $ix, $iy, $c);
@ -2300,7 +2298,7 @@ class Securimage
$theta = ($this->frand() - 0.5) * M_PI * 0.33;
$w = $this->image_width;
$len = mt_rand(intval($w * 0.4), intval($w * 0.7));
$len = mt_rand($w * 0.4, $w * 0.7);
$lwid = mt_rand(0, 4);
$k = $this->frand() * 0.6 + 0.2;
@ -2320,7 +2318,7 @@ class Securimage
for ($i = 0; $i < $n; ++ $i) {
$x = $x0 + $i * $dx + $amp * $dy * sin($k * $i * $step + $phi);
$y = $y0 + $i * $dy - $amp * $dx * sin($k * $i * $step + $phi);
imagefilledrectangle($this->im, intval($x), intval($y), intval($x + $lwid), intval($y + $lwid), $this->gdlinecolor);
imagefilledrectangle($this->im, $x, $y, $x + $lwid, $y + $lwid, $this->gdlinecolor);
}
}
}

View File

@ -6,8 +6,6 @@
defined('TINYBOARD') or exit;
require_once 'inc/mod/pages.php';
// create a hash/salt pair for validate logins
function mkhash($username, $password, $salt = false) {
global $config;

View File

@ -1501,7 +1501,6 @@ function mod_move($originBoard, $postID) {
} else {
deletePost($postID);
buildIndex();
rebuildThemes('post', $originBoard);
openBoard($targetBoard);
header('Location: ?/' . sprintf($config['board_path'], $newboard['uri']) . $config['dir']['res'] . link_for($op, false, $newboard), true, $config['redirect_http']);
@ -1563,7 +1562,7 @@ function mod_merge($originBoard, $postID) {
}
}
if ($targetBoard === $originBoard) {
if ($targetBoard === $originBoard){
// Just update the thread id for all posts in the original thread to new op
$query = prepare(sprintf('UPDATE ``posts_%s`` SET `thread` = :newthread WHERE `id` = :oldthread OR `thread` = :oldthread', $originBoard));
$query->bindValue(':newthread', $targetOp, PDO::PARAM_INT);
@ -1587,7 +1586,8 @@ function mod_merge($originBoard, $postID) {
// redirect
header('Location: ?/' . sprintf($config['board_path'], $board['uri']) . $config['dir']['res'] . link_for($newpost) . '#' . $targetOp, true, $config['redirect_http']);
} else {
}
else {
// Move thread to new board without shadow thread and then update the thread id for all posts in that thread to new op
// indicate that the post is a thread
if (count($boards) <= 1)
@ -1726,7 +1726,6 @@ function mod_merge($originBoard, $postID) {
deletePost($postID);
modLog("Deleted post #{$postID}");
buildIndex();
rebuildThemes('post', $originBoard);
openBoard($targetBoard);
// Just update the thread id for all posts in the original thread to new op

View File

@ -28,14 +28,14 @@ $(document).ready(function(){
context: document.body,
success: function(data) {
var last_expanded = false;
$(data).find('div.postcontainer').each(function() {
$(data).find('div.post.reply').each(function() {
thread.find('div.hidden').remove();
var post_in_doc = thread.find('#' + $(this).attr('id'));
if(post_in_doc.length == 0) {
if(last_expanded) {
$(this).addClass('expanded').insertAfter(last_expanded);
$(this).addClass('expanded').insertAfter(last_expanded).before('<br class="expanded">');
} else {
$(this).addClass('expanded').insertAfter(thread.find('div.post:first'));
$(this).addClass('expanded').insertAfter(thread.find('div.post:first')).after('<br class="expanded">');
}
last_expanded = $(this);
$(document).trigger('new_post', this);

View File

@ -356,7 +356,6 @@ globalThis.LCNSetting = class LCNSetting {
#id = null;
#eventId = null;
#label = null;
#hidden = false;
#value = null;
#valueDefault = null;
@ -376,9 +375,6 @@ globalThis.LCNSetting = class LCNSetting {
}
}
"isHidden" () { return this.#hidden; }
"setHidden" (v) { this.#hidden = v; return this; }
"getValue" () { return this.#value ?? (this.#value = this.#getValue()); }
"setValue" (v) {
if (this.#value !== v) {
@ -395,10 +391,7 @@ globalThis.LCNSetting = class LCNSetting {
"setDefaultValue" (vd) { this.#valueDefault = vd; return this; }
"onChange" (fn) { $(document).on(`${this.#eventId}::change`, (_,v,i) => fn(v, i)); }
__setIdPrefix (prefix) {
this.#id = `${prefix}_${this.#id}`
this.#eventId = `lcnsetting::${this.#id}`
}
__setIdPrefix (prefix) { this.#id = `${prefix}_${this.#id}`; }
}
globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting {
@ -408,10 +401,7 @@ globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting {
const div = document.createElement("div")
const chk = document.createElement("input")
const txt = document.createElement("label")
const id = `lcnts::${this.id}`
txt.id = id
txt.innerText = this.getLabel()
chk.id = id
chk.type = "checkbox"
chk.checked = this.getValue()
chk.addEventListener("click", e => {
@ -469,7 +459,7 @@ globalThis.LCNSettingsSubcategory = class LCNSettingsSubcategory {
"addSetting" (setting) {
assert.ok(setting instanceof LCNSetting)
setting.__setIdPrefix(`lcnsetting_${this.#tab_id}_${this.#id}`)
if (!setting.isHidden() && setting.__builtinDOMConstructor != null) {
if (setting.__builtinDOMConstructor != null) {
const div = setting.__builtinDOMConstructor()
div.classList.add("lcn-setting-entry")
this.#fieldset.appendChild(div)

View File

@ -6,17 +6,12 @@
$().ready(() => {
const kIsEnabled = LCNToggleSetting.build("enabled")
const kUpdateOnReplyEnabled = LCNToggleSetting.build("updateOnReplyEnabled")
//const kIsBellEnabled = LCNToggleSetting.build("bellEnabled")
void LCNSettingsSubcategory.for("general", "threadUpdater")
.setLabel("Thread Updater")
.addSetting(kIsEnabled
.setLabel(_("Fetch new replies in the background"))
.setDefaultValue(true))
.addSetting(kUpdateOnReplyEnabled
.setLabel(_("Update thread after sending a reply"))
.setHidden(true)
.setDefaultValue(true))
/*.addSetting(kIsBellEnabled
.setLabel(_("Play an audible chime when new replies are found"))
.setDefaultValue(false))*/;
@ -40,9 +35,9 @@ $().ready(() => {
}
const updateSecondsByTSLP = post_info => {
secondsCounter = Math.floor(((Date.now() - post_info.getCreatedAt().getTime()) / 120000))
secondsCounter = Math.min(1000, secondsCounter)
secondsCounter = Math.max(11, secondsCounter)
secondsCounter = Math.floor(((Date.now() - post_info.getCreatedAt().getTime()) / 30000))
secondsCounter = secondsCounter > 1000 ? 1000 : secondsCounter
secondsCounter = secondsCounter < 11 ? 11 : secondsCounter
}
const updateStatsFn = async thread => {
@ -63,24 +58,43 @@ $().ready(() => {
}
}
const findMissingReplies = (thread_op, thread_dom, thread_latest) => {
const lastPostTs = (thread_dom.at(-1)?.getInfo() ?? thread_op.getInfo()).getCreatedAt().getTime()
const missing = []
const handleThreadUpdate = async (thread) => {
const threadPost = thread.getContent()
for (const pc of thread_latest.reverse()) {
const res = await fetch(location.href, {
"signal": abortable.signal
})
if (res.ok) {
const dom = parser.parseFromString(await res.text(), "text/html")
const livePCList = Array.prototype.map.apply(dom.querySelectorAll(`#thread_${threadPost.getInfo().getThreadId()} > .postcontainer`), [ pc => LCNPostContainer.assign(pc) ])
updateThreadFn(thread, livePCList);
} else if (res.status == 404) {
threadState = String(res.status)
} else {
throw new Error(`Server responded with non-OK status '${res.status}'`)
}
}
function updateThreadFn(thread, lcn_pc_list) {
const threadPost = thread.getContent()
const threadReplies = thread.getReplies()
const lastPostC = threadReplies.at(-1).getParent()
const lastPostTs = lastPostC.getContent().getInfo().getCreatedAt().getTime()
const livePCList = lcn_pc_list;
const documentPCList = [ threadPost, ...threadReplies.map(p => p.getParent()) ]
const missingPCList = []
for (const pc of livePCList.reverse()) {
if (pc.getContent().getInfo().getCreatedAt().getTime() > lastPostTs) {
missing.unshift(pc)
missingPCList.unshift(pc)
} else {
break
}
}
return missing
}
const updateRepliesFn = (thread, missingPCList) => {
if (missingPCList.length) {
const documentPCList = [ thread.getContent(), ...(thread.getReplies()).map(p => p.getParent()) ]
for (const pc of missingPCList) {
documentPCList.at(-1).getElement().after(pc.getElement())
documentPCList.push(pc)
@ -92,30 +106,7 @@ $().ready(() => {
LCNSite.INSTANCE.setUnseen(LCNSite.INSTANCE.getUnseen() + missingPCList.length)
}
}
const updateThreadFn = async (thread, dom) => {
const threadPost = thread.getContent()
const threadReplies = thread.getReplies()
const missingPCList = findMissingReplies(
threadPost,
threadReplies,
LCNPostContainer.all(dom.querySelector(`#thread_${threadPost.getInfo().getThreadId()}`)))
updateRepliesFn(thread, missingPCList)
statUniqueIPs.innerText = dom.querySelector("#lcn-uniqueips").innerText
}
const fetchThreadFn = async () => {
const res = await fetch(location.href, { "signal": abortable.signal })
if (res.ok) {
return parser.parseFromString(await res.text(), "text/html")
} else {
if (res.status == 404) {
threadState = String(res.status)
}
throw new Error(`Server responded with non-OK status '${res.status}'`)
}
}
const onTickClean = () => {
@ -137,15 +128,16 @@ $().ready(() => {
const thread = LCNThread.first()
try {
await updateStatsFn(thread)
if (threadState == null && threadStats.last_modified > (thread.getPosts().at(-1).getInfo().getCreatedAt().getTime() / 1000)) {
updateThreadFn(thread, await fetchThreadFn())
if (threadState == null && threadStats.last_modified > (thread.getReplies().at(-1).getInfo().getCreatedAt().getTime() / 1000)) {
await handleThreadUpdate(thread)
}
const threadEl = thread.getElement()
statUniqueIPs.innerText = threadStats.unique_ips
statReplies.innerText = thread.getReplies().length
statFiles.innerText = threadEl.querySelectorAll(".files .file").length - threadEl.querySelectorAll(".files .file .post-image.deleted").length
statPage.innerText = threadStats.page + 1
updateSecondsByTSLP(thread.getPosts().at(-1).getInfo())
updateSecondsByTSLP(thread.getReplies().at(-1).getInfo())
} catch (error) {
console.error("threadAutoUpdater: Failed while processing update. Probably a network error", error)
secondsCounter = 60
@ -156,32 +148,6 @@ $().ready(() => {
}
}
const refreshFn = () => {
if (secondsCounter >= 0) {
secondsCounter = 0
onTickFn()
}
}
$(document).on("ajax_after_post", (_, xhr_body) => {
if (kUpdateOnReplyEnabled.getValue() && xhr_body != null) {
if (!xhr_body.mod) {
const thread = LCNThread.first()
const dom = parser.parseFromString(xhr_body.thread, "text/html")
updateThreadFn(thread, dom)
updateSecondsByTSLP(thread.getPosts().at(-1).getInfo())
} else {
refreshFn()
}
}
})
$(document).on("thread_manual_refresh", () => {
if (kIsEnabled.getValue()) {
refreshFn()
}
})
let floaterLinkBox = null
const onStateChangeFn = v => {
onTickClean()
@ -198,7 +164,10 @@ $().ready(() => {
threadUpdateStatus.innerText = "…"
threadUpdateLink.addEventListener("click", e => {
e.preventDefault()
$(document).trigger("thread_manual_refresh")
if (secondsCounter >= 0) {
secondsCounter = 0
onTickFn()
}
})
threadUpdateLink.href = "#"
threadUpdateLink.appendChild(new Text("Refresh: "))
@ -242,7 +211,8 @@ $().ready(() => {
}
}
$(document).trigger("thread_manual_refresh")
secondsCounter = 0
setTimeout(onTickFn, 1)
} else {
floaterLinkBox?.remove()
floaterLinkBox = null
@ -258,5 +228,26 @@ $().ready(() => {
kIsEnabled.onChange(onStateChangeFn)
onStateChangeFn(kIsEnabled.getValue())
$(document).on("ajax_after_post", onNewPost);
function onNewPost(_, post_response) {
if (post_response == null) {
console.log("onNewPost data is null, can't do anything.");
return;
}
const thread_dom = parser.parseFromString(
post_response['thread'],
"text/html");
const thread_id_sel = "#thread_" + post_response['thread_id'];
const post_containers = [...thread_dom.querySelectorAll(`${thread_id_sel} > .postcontainer`)]
.map(elem => LCNPostContainer.assign(elem));
const thread_elem = document.querySelector(thread_id_sel);
const lcn_thread = new LCNThread(thread_elem);
updateThreadFn(lcn_thread, post_containers);
}
}
})

View File

@ -8,7 +8,8 @@ const assert = {
if (actual !== expected) {
const err = new Error(`Assertion Failed. ${message}`)
err.data = { actual, expected}
Error.captureStackTrace?.(err, assert.equal)
// Seems like there's no such thing as captureStackTrace in firefox?
//Error.captureStackTrace(err, assert.equal)
debugger
throw err
}
@ -17,7 +18,7 @@ const assert = {
if (!actual) {
const err = new Error(`Assertion Failed. ${message}`)
err.data = { actual }
Error.captureStackTrace?.(err, assert.ok)
// Error.captureStackTrace(err, assert.ok)
debugger
throw err
}

View File

@ -1,6 +1,6 @@
<?php
require_once 'inc/functions.php';
require_once 'inc/mod/pages.php';
include 'inc/functions.php';
include 'inc/mod/pages.php';
if (!isset($_GET['board']) || !preg_match("/{$config['board_regex']}/u", $_GET['board'])) {
http_response_code(400);

View File

@ -457,8 +457,6 @@ function validate_images(array $post_array) {
function handle_post(){
global $config,$dropped_post,$board, $mod,$pdo;
init_global_post_cleanup();
if (!isset($_POST['body'], $_POST['board']) && !$dropped_post) {
error($config['error']['bot']);
}
@ -513,11 +511,7 @@ function handle_post(){
}
}
if((isset($config['securimage']) && $config['securimage'])
&& (
!(isset($config['captcha_tor_only']) && $config['captcha_tor_only'])
|| ipIsLocal($_SERVER['REMOTE_ADDR'])
)){
if(isset($config['securimage']) && $config['securimage']){
if(!isset($_POST['captcha'])){
error($config['error']['securimage']['missing']);
@ -1038,17 +1032,6 @@ function handle_post(){
$spam_noticer_result = checkWithSpamNoticer($config, $post, $board['uri']);
/*
* If we have an error with posting this later, send back the
* delete token to spamnoticer to remove the post from the recent
* posts table. (see error.php for the error cleanup function)
*/
$f_spamnoticer_cleanup_on_err = function() use ($config, $delete_token) {
removeRecentPostFromSpamnoticer($config, array($delete_token));
};
push_global_post_cleanup($f_spamnoticer_cleanup_on_err);
if ($spam_noticer_result->succeeded && $spam_noticer_result->noticed) {
error($config['error']['spam_noticer'] . $spam_noticer_result->reason);
}
@ -1512,8 +1495,7 @@ function handle_post(){
'noko' => $noko,
'id' => $id,
'thread_id' => $thread_id,
'thread' => $rendered_thread,
'mod' => !!$post['mod']
'thread' => $rendered_thread
));
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 B

View File

@ -3,220 +3,184 @@ div.sidearrows {
}
body {
background: #1D1F21;
color: #ACACAC;
font-family: Courier, monospace;
font-size: 15px;
}
.theme-catalog .replies {
font-family: serif;
}
div.post div.body {
font-family: serif;
}
span.heading {
font-size: 15px;
}
.theme-catalog .replies .meta,
.theme-catalog .replies .intro,
div.post div.body a,
div.post div.body .toolong {
font-family: Courier, monospace;
background: #1D1F21;
color: #ACACAC;
font-family: Courier, monospace;
font-size: 13px;
}
/* LINKS */
a, a:link, a:visited, .intro a.email span.name {
color: #FFB300;
text-decoration: none;
color: #FFB300;
text-decoration: none;
}
a:link:hover, a:visited:hover {
color: #FFB300;
text-shadow: 0px 0px 5px #117743;
color: #FFB300;
text-shadow: 0px 0px 5px #117743;
}
div.pages a.selected {
color: #FFB300;
color: #FFB300;
}
/* INTRO */
h1, div.title, header div.subtitle {
color: #FFB300;
font-family: Courier, monospace;
color: #663E11;
font-family: Courier, monospace;
}
h1 {
font-size: 18pt;
font-weight: bold;
letter-spacing: 0px;
font-size: 24pt;
font-weight: normal;
letter-spacing: 0px;
}
header div.subtitle {
font-size: 10pt;
font-size: 12pt;
}
/* FORMS AND BUTTONS */
div.banner {
background-color: inherit;
color: #ACACAC;
background-color: inherit;
color: #ACACAC;
}
form table {
border: 1px dashed #117743;
padding-right: 1px;
border-radius: 3px;
border: 1px dashed #117743;
padding-right: 1px;
}
form table tr th {
background: #282A2E;
border: 1px solid #117743;
border-radius: 2px;
padding: 3px;
background: #282A2E;
border: 1px solid #117743;
border-radius: 5px;
}
input[type="text"], input[type="password"], textarea, select {
border: 1px double #07371F;
border-radius: 2px;
background: #282A2E;
color: #ACACAC;
font-family: Courier, monospace;
margin: 0 1px;
padding: 3px !important;
box-sizing: border-box;
border: 1px double #07371F;
border-radius: 5px;
background: #282A2E;
color: #ACACAC;
font-family: Courier, monospace;
}
input[type="text"]:focus, input[type="password"]:focus, textarea:focus {
box-shadow: 0px 0px 5px 2px #117743;
box-shadow: 0px 0px 5px 2px #117743;
}
input[type="submit"] {
border: 3px double #07371F;
border-radius: 2px;
background-color: #07371F;
color: #ACACAC;
font-family: Courier, monospace;
font-weight: bold;
font-size: 16px;
border: 3px double #07371F;
border-radius: 5px;
background: #16171A;
color: #ACACAC;
font-family: Courier, monospace;
font-weight: bold;
}
input[type="submit"]:hover {
border-color: #117743;
background-color: #117743;
}
.dropzone {
background: #16171A;
border: 1px solid #07371F;
color: #ACACAC;
border-radius: 2px;
margin: 0 2px;
background: #16171A;
border: 3px double #07371F;
color: #ACACAC;
}
.dropzone .file-hint {
color: #ACACAC;
font-weight: bold;
color: #ACACAC;
font-weight: bold;
}
#quick-reply table {
background: #1D1F21 !important;
background: #1D1F21 !important;
}
fieldset {
border: 1px dashed #117743;
border: 1px dashed #117743;
}
/* POST IDENTIFIERS */
.intro span.subject {
color: #34ED3A;
color: #34ED3A;
}
.intro span.name {
color: #117743;
color: #117743;
}
.intro span.trip {
color: #117743;
color: #117743;
}
.intro a.capcode, p.intro a.nametag {
color: #FF0000;
font-weight: bold;
color: #FF0000;
font-weight: bold;
}
.intro a.email, p.intro a.email span.name, p.intro a.email:hover, p.intro a.email:hover span.name {
color: #34ED97;
color: #34ED97;
}
.intro time {
font-weight: bold;
font-weight: bold;
}
.intro a.post_no {
color: #ACACAC;
font-weight: bold;
color: #ACACAC;
font-weight: bold;
}
/* POST BOXES */
div.post.reply {
background: #282A2E;
border: 1px solid #117743;
border-radius: 2px;
background: #282A2E;
border: 1px solid #117743;
border-radius: 5px;
}
div.post.reply.highlighted {
background: rgba(59, 22, 43, 0.4);
border: 1px solid #117743;
border-radius: 2px;
background: rgba(59, 22, 43, 0.4);
border: 1px solid #117743;
border-radius: 5px;
}
/* POST CONTENT */
div.post.reply div.body a {
color: #FFB300;
color: #FFB300;
}
.quote {
color: #789922;
color: #789922;
}
/* BARS */
.bar {
background-color: #151515;
background-color: #151515;
}
.bar.top {
border-bottom: 1px solid #B0790A;
border-bottom: 1px solid #B0790A;
}
.bar.bottom {
border-top: 1px solid #B0790A;
border-top: 1px solid #B0790A;
}
div.boardlist {
color: #ACACAC;
color: #ACACAC;
}
hr {
border: none;
border-top: 1pt solid #117743;
border: none;
border-top: 1pt solid #117743;
}
/* CATALOG */
.theme-catalog h1 {
color: #ACACAC;
font-size: 18pt;
font-weight: bold;
color: #ACACAC;
font-size: 18pt;
font-weight: bold;
}
.theme-catalog h1 a {
font-weight: normal;
font-weight: normal;
}
.theme-catalog div.thread, .theme-catalog div.thread:hover {
background: #282A2E;
border: 1px solid #117743;
border-radius: 2px;
font-size: 11pt;
background: #282A2E;
border: 1px solid #117743;
border-radius: 5px;
font-size: 10pt;
}
/* OPTIONS */
#options_div, #alert_div {
background: #1D1F21;
border: 1px dashed #117743;
background: #1D1F21;
border: 1px dashed #117743;
}
#options_tablist {
border-right: 1px dashed #117743;
border-right: 1px dashed #117743;
}
.options_tab_icon {
color: #ACACAC;
color: #ACACAC;
}
.options_tab_icon.active {
color: #FFB300;
color: #FFB300;
}
/* FIXES */
div.ban {
background: #1D1F21;
background: #1D1F21;
border: 1px dashed #117743;
}
@ -261,7 +225,7 @@ table thead th {
}
.theme-catalog .thread .meta {
font-size: 13px;
font-size: 10pt;
}
.theme-catalog .thread.grid-size-small .replies {

View File

@ -4,39 +4,20 @@ body {
background: #ffe;
background-image: url('img/jungle_bg1.png'), url('img/jungle_bg.png');
background-repeat: repeat-x, repeat;
background-attachment: scroll, scroll;
background-attachment: scroll, scroll;
color: #242B23;
font-family: serif;
font-size: 16px;
}
.bar {
.bar
{
border-color: #E5D959!important;
background-image: url('img/jungle_td.png');
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none;
}
.bar.top {
position: relative;
width: calc(100% + 8px);
box-sizing: border-box;
padding: 0 .25em;
left: -4px;
}
@media screen and (max-width: 600px) {
.bar.bottom {
font-size: 14px;
}
#thread-update-status {
opacity: .8;
font-size: 8pt;
}
}
div.title h1 {
font-size: 24px;
}

View File

@ -5,10 +5,13 @@
method="post"
data-max-images="{{ config.max_images }}"
>
{{ antibot.html() }}
{% if id %}<input type="hidden" name="thread" value="{{ id }}">{% endif %}
{{ antibot.html() }}
{% if board.uri not in config.overboards|keys %}
<input type="hidden" name="board" value="{{ board.uri }}">
{% endif %}
{{ antibot.html() }}
{% if current_page %}
<input type="hidden" name="page" value="{{ current_page }}">
{% endif %}
@ -100,38 +103,15 @@
</tr>
{% endif %}
{% if config.securimage %}
<tr class="post_form_captcha_row">
<th>
Captcha
{% if config.captcha_tor_only %}
<br/>
<small>Tor Only</small>
{% endif %}
</th>
<td>
<img name="captcha-img" id="captcha-img" src="/captcha.php" onClick="this.src='/captcha.php?'+Date.now();document.getElementById('captcha').value = '';"><br />
<input type="text" name="captcha" id="captcha" size="25" maxlength="10" autocomplete="off">
</td>
</tr>
{% if config.captcha_tor_only %}
<script>
(() => {
function isHiddenService() {
const hostname = window.location.hostname;
return hostname.endsWith('.onion') || hostname.endsWith('.i2p');
}
function removeCaptchaField() {
document.querySelectorAll('.post_form_captcha_row')
.forEach(e => e.parentNode.removeChild(e));
}
if (!isHiddenService()) {
removeCaptchaField();
}
})();
</script>
{% endif %}
<tr>
<th>
Captcha
</th>
<td>
<img name="captcha-img" id="captcha-img" src="/captcha.php" onClick="this.src='/captcha.php?'+Date.now();document.getElementById('captcha').value = '';"><br />
<input type="text" name="captcha" id="captcha" size="25" maxlength="10" autocomplete="off">
</td>
</tr>
{% endif %}
{% if config.user_flag %}
<tr>
@ -224,6 +204,7 @@
</td>
</tr>{% endif %}
</table>
{{ antibot.html(true) }}
<input type="hidden" name="hash" value="{{ antibot.hash() }}">
</form>

View File

@ -8,18 +8,23 @@
<link rel="stylesheet" media="screen" href="/stylesheets/bunker_like.css">
<style type="text/css">
.sidebar {
grid-column: 1 / 3;
grid-column: 1;
grid-row: 1 / 3;
width: 200px;
border-right-color: gray;
border-right-style: solid;
border-width: 2px;
margin-right: 15px;
}
.introduction {
grid-column: 3 / 9;
grid-column: 2 / 9;
grid-row: 1;
width: 100%;
}
.content {
grid-column: 3 / 9;
grid-column: 2 / 9;
grid-row: 2;
width: 100%;
max-width: 100%;
@ -28,8 +33,8 @@
body {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(120px, 20%));
min-height: 100vh;
grid-template-columns: repeat(auto-fill,minmax(200px, 1fr));
gap: 20px;
}
.modlog {
@ -65,16 +70,6 @@
li a.system {
font-weight: bold;
}
footer {
grid-column: 1 / 9;
}
div.news.ban {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
@media (max-width:768px) {
body{

View File

@ -25,7 +25,7 @@
</header>
</div>
<div class="content">
<div class="ban news">
<div class="ban">
{% if not news %}
<p style="text-align:center" class="unimportant">{% trans %}(No news to show.){% endtrans %}</p>
{% else %}
@ -98,14 +98,14 @@
</tbody>
</table>
{% endif %}
<footer>
<p class="unimportant" style="margin-top:20px;text-align:center;">- Tinyboard +
<a href="https://engine.vichan.net/">vichan</a> {{ config.version }} -
<br>Tinyboard Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2016 vichan-devel
<br><br>
<br><b>Leftychan.net is not currently under investigation by any Federal, State, or Local Authorities.</b></p>
</footer>
</div>
<footer>
<p class="unimportant" style="margin-top:20px;text-align:center;">- Tinyboard +
<a href="https://engine.vichan.net/">vichan</a> {{ config.version }} -
<br>Tinyboard Copyright &copy; 2010-2014 Tinyboard Development Group
<br><a href="https://engine.vichan.net/">vichan</a> Copyright &copy; 2012-2016 vichan-devel
<br><br>
<br><b>Leftychan.net is not currently under investigation by any Federal, State, or Local Authorities.</b></p>
</footer>
{% endapply %}