Compare commits

...

100 Commits

Author SHA1 Message Date
towards-a-new-leftypol 60d86d3a84 Fix commit 1c13ec98ba
- Unintentionally disabled that piece of code instead of removing adding
  the additional br tag that made double spaces between posts.
- add the postcontainers instead of post.reply (which is inside this
  container) to preserve the arrows
2024-12-26 02:39:08 -05:00
towards-a-new-leftypol 62e7a4b22e undo instance-config.php settings changes 2024-12-22 00:42:38 -05:00
towards-a-new-leftypol 29d429bd55 retab image.php (use vim, convert tabs to space) 2024-12-22 00:42:38 -05:00
towards-a-new-leftypol 221faac75b Improve posting error handling
- if spamnoticer was contacted, add cleanup callback to error handler if
  something goes wrong during the rest of the process (like thumbnail
  creation for example)

- add another allowed warning (string) that gm may safely report back
  and don't call error if we see it.
2024-12-22 00:42:38 -05:00
towards-a-new-leftypol f5a073f0b1 bunker_like: heading and subtitle font sizes 2024-12-01 13:07:37 +00:00
towards-a-new-leftypol a8be2a3fc3 Up the font sizes for bunker_like a little bit 2024-12-01 12:42:21 +00:00
towards-a-new-leftypol 66fe2659c7 Fix textarea size in bunker_like theme 2024-11-29 04:24:54 -05:00
towards-a-new-leftypol eb89db5246 bunker_like: submit button color 2024-11-27 19:23:13 -05:00
towards-a-new-leftypol 132aa54b84 bunker_like: submit button color 2024-11-27 19:21:31 -05:00
towards-a-new-leftypol 709b4ff32d bunker-like css fix 2024-11-27 19:17:23 -05:00
towards-a-new-leftypol 9f26de080e fix bunker-like css tweaks 2024-11-27 19:15:07 -05:00
towards-a-new-leftypol 7e189349a3 bunker_like: padding adjust 2024-11-27 19:12:41 -05:00
towards-a-new-leftypol 9fe1cf8713 bunker-like: form tweaks 2024-11-27 18:55:25 -05:00
towards-a-new-leftypol c864f3701e bunkerlike form css tweaks (by 1px) 2024-11-27 18:37:38 -05:00
towards-a-new-leftypol 958a58f016 retab 2024-11-27 18:37:28 -05:00
towards-a-new-leftypol 929e00675f bunker_like css form field padding tweak 2024-11-27 18:16:32 -05:00
towards-a-new-leftypol 91a0b3017b bunkerlike - fix form padding 2024-11-27 18:02:21 -05:00
towards-a-new-leftypol df01d50275 Bunker-like theme changes
- change post body font to 'serif'
- change border radius's to 2px (down from 5px, which was silly)
2024-11-27 14:18:25 -05:00
towards-a-new-leftypol d3fa904e11 Replace flag instead of creating a new one 2024-11-27 11:51:20 -05:00
towards-a-new-leftypol f39a7f6282 Add flags 2024-11-27 11:40:13 -05:00
towards-a-new-leftypol 6b7c7b6c6a Add music board and a link to nukechan 2024-09-17 15:41:38 -04:00
towards-a-new-leftypol e9fa6303ac expand.js: don't add weird spacing 2024-09-17 15:41:38 -04:00
towards-a-new-leftypol 8763b2cf40 Fix log.php 2024-08-25 17:24:34 -04:00
towards-a-new-leftypol ab1f4e3ca7 Make headings bolder and brighter for better readability 2024-08-25 15:09:24 -04:00
towards-a-new-leftypol 585029dbca Fix a rebuilding when moving (targetBoard -> originBoard) 2024-08-12 16:54:19 -04:00
towards-a-new-leftypol b989686b2c 8 new banners 2024-08-08 01:25:30 -04:00
towards-a-new-leftypol 66636df702 anti-bot: do not flag missing hash values as spam 2024-07-19 15:40:41 -04:00
towards-a-new-leftypol 3415319698 add banners 2024-07-19 15:22:11 -04:00
towards-a-new-leftypol 8ea0e124a3 add banners 2024-07-01 12:38:22 -04:00
towards-a-new-leftypol 499cdc279f Remove redundant leftypedia link 2024-06-18 18:12:18 -04:00
towards-a-new-leftypol 7ed48a1769 Update booru and wiki links 2024-06-18 18:03:12 -04:00
towards-a-new-leftypol ef01eef2a2 Enable anti-bot, except for the case where the hash is missing from the db (most bots don't seem to be that smart) 2024-06-15 20:29:25 -04:00
towards-a-new-leftypol 4349499ddd Up the anti-bot hash expiration time to 120 days 2024-06-15 20:06:32 -04:00
towards-a-new-leftypol 77aac51172 Autoformat (vim) anti-bot.php to use spaces instead of tabs and spaces 2024-06-11 18:51:10 -04:00
towards-a-new-leftypol ce93e6f442 Anti-bot: print out failure cases and construction
- enable anti-bot spam protection setting but comment out it's rejection
- will monitor the output log to check for false-positives and just see
  what it's rejecting...
2024-06-11 18:48:04 -04:00
towards-a-new-leftypol d478f65ba0 Fix a bug with i2p captcha
- if captcha_tor_only is set to true, posting over i2p requires a
  captcha but the javascript code removes it. Add a check to see if
  we're on a .i2p domain in this case.
2024-06-07 15:07:25 -04:00
towards-a-new-leftypol ad5383c196 Add missing import to auth.php 2024-06-07 15:06:50 -04:00
towards-a-new-leftypol 17bb78e17e Add maintenance message 2024-05-29 15:03:49 -04:00
towards-a-new-leftypol 0a04e747c4 Rebuild origin board themes if moving a thread and not leaving a shadow 2024-05-25 16:29:41 -04:00
towards-a-new-leftypol 1af3931a48 Don't forget to rebuild the themes on origin board when moving
- if you're moving a post to a different board it would only
  rebuildThemes of the target board
2024-05-21 16:19:12 -04:00
towards-a-new-leftypol bdd813cd77 Captcha for TOR users only 2024-05-08 17:16:07 -04:00
towards-a-new-leftypol b118588a82 Fix captcha generator securimage.php to work with php 8
- basically use intval() everywhere to avoid implicit conversion from
  float
2024-05-07 10:54:59 -04:00
towards-a-new-leftypol 4f5dcd1773 Add 4 new banners 2024-05-06 16:24:35 -04:00
towards-a-new-leftypol f027f87044 Revert "Add new banner"
This reverts commit a8dd6c57c8.
2024-05-05 18:26:10 -04:00
towards-a-new-leftypol a8dd6c57c8 Add new banner 2024-05-05 17:53:30 -04:00
towards-a-new-leftypol 495949666d Add opus to supported list of extensions 2024-05-02 17:06:07 -04:00
towards-a-new-leftypol e2b79a443e Fix banners (replace misspelt ones) 2024-04-29 17:00:55 -04:00
towards-a-new-leftypol 28258efd36 Add banners 2024-04-28 16:02:55 -04:00
towards-a-new-leftypol 6e8d85d5e7 Fix flag list ordering in config 2024-04-09 03:42:54 -04:00
towards-a-new-leftypol 669429863a Fix previous commit 2024-04-09 03:37:07 -04:00
towards-a-new-leftypol 9293f38bf5 Add flag 2024-04-09 03:29:23 -04:00
towards-a-new-leftypol 64c6a4d9f6 Add banners 2024-04-08 22:48:50 -04:00
towards-a-new-leftypol 93b8d216b1 API - threads.json last_modified is now really the timestamp of the last post, put bump time in a new field 'bump' 2024-03-31 17:22:42 -04:00
towards-a-new-leftypol 623e5929b6 Fix top bar on windows/firefox which caused the page to scroll horizontally. (jungle theme) 2024-03-31 03:28:39 +00:00
towards-a-new-leftypol bc4e64bcec homepage sidebar: remove padding 2024-03-18 23:43:45 -04:00
towards-a-new-leftypol bc22758e90 get rid of gap parameter in css layout 2024-03-18 23:39:41 -04:00
towards-a-new-leftypol 9fbe71f1f9 Rename sidebar heading to Boards for the boards list 2024-03-17 17:40:14 -04:00
towards-a-new-leftypol ac08768094 homepage sizing fixes 2024-03-17 17:36:53 -04:00
towards-a-new-leftypol 1095f773e0 Further homepage changes 2024-03-17 17:36:53 -04:00
towards-a-new-leftypol 548c613c12 Homepage: remove sidebar border, make sidebar wider (300px instead of
200px)
2024-03-17 17:36:53 -04:00
Jon 1f4c94fef1 thread_autoupdater.js: Use ip count from thread dom of /%b/catalog.json 2024-03-15 18:33:34 -04:00
Jon 61166cb4f1 instance-config.php: update shortener regex 2024-03-15 16:58:01 -04:00
towards-a-new-leftypol 10015def12 Add ip member variable to filter class 2024-03-14 17:30:39 -04:00
towards-a-new-leftypol 3158ce40cd Rate limit opening posts for specific ip (basically copy flood-time-any filter and add ip condition) 2024-03-14 17:11:36 -04:00
Jon 9127429b8f thread_autoupdater.js: spin-off update on reply 2024-02-29 22:56:22 -05:00
Jon dd80ea013a js/lcn/classes.js: re-add toggle label, set eventId on prefix change 2024-02-29 22:56:22 -05:00
towards-a-new-leftypol 9a5b0352e5 Fixed thread_updator bugs related to new threads without any replies 2024-02-29 22:53:09 +00:00
towards-a-new-leftypol b4e77bae40 Jungle theme - only change fonts on smaller screens 2024-02-27 16:49:10 -05:00
towards-a-new-leftypol ade9c47d25 Jungle theme: adjust bottom and top bars
- top bar: stay put.
- bottom bar: make the fonts a bit smaller to fit everything
2024-02-27 16:12:36 -05:00
Jon ebb7cb8cfb
thread_updater.js: cleanup, use thread_manual_refresh 2024-02-26 21:49:46 +00:00
towards-a-new-leftypol ba120d32f7 Thread updator - handle case where you're a mod
- mod triggers refresh
- not a mod uses the response
2024-02-26 21:32:04 +00:00
Jon 07db8e2db4 thread_autoupdater.js: update tslp on ajax post 2024-02-26 21:32:04 +00:00
Jon c5cf671929 thread_autoupdater.js: cleanup 2024-02-26 21:32:04 +00:00
Jon 733aad3bf2 js/lcn/utils.js: use captureStackTrace if available 2024-02-26 21:32:04 +00:00
towards-a-new-leftypol e9228892fa ajax POST: return whole thread 2024-02-26 20:13:29 +00:00
towards-a-new-leftypol 408e9ce30e 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 20:13:29 +00:00
towards-a-new-leftypol da03753a72 thread_updater.js: listen to ajax_after_post event 2024-02-26 20:13:29 +00:00
Jon 61b93aa927 thread_autoupdater.js: add interop event 2024-02-26 20:13:29 +00:00
Jon c07ae61548 thread_autoupdater.js: decrease tslp backoff 2024-02-26 20:13:29 +00:00
Jon 2b9185cd22 js/lcn/classes.js: link toggle label to checkbox 2024-02-26 20:13:29 +00:00
Jon 9298c1a674 thread_autoupdater.js: fix for threads with only an op 2024-02-26 20:13:29 +00:00
Jon 309d8698d4 thread_autoupdater.js: fix tslp calc 2024-02-26 20:13:29 +00:00
Jon c31e69e835 js/lcn/classes.js: add semicolon 2024-02-26 20:13:29 +00:00
Jon 68652d29a3 inc/instance-config.php: fix typo 2024-02-26 20:13:29 +00:00
Jon e33c6825fc thread_autoupdater.js: Display status code on err 2024-02-26 20:13:29 +00:00
Jon e64dd3f9ab thread_autoupdater.js: remove debug 2024-02-26 20:13:29 +00:00
Jon 497a0c9208 inc/instance-config.php: replace legacy with new autoreloader 2024-02-26 20:13:29 +00:00
Jon 78308cb7ce thread_autoupdater.js: add 2024-02-26 20:13:29 +00:00
Jon 37d6a7180d js/lcn/utils.js: remove unnessesary tab 2024-02-26 20:13:29 +00:00
Jon c63d1dfc8b js/lcn/classes.js: add missing semicolon 2024-02-26 20:13:29 +00:00
Jon ee4a95ee57 stylesheets/style.css: swap missing dashes 2024-02-26 20:13:29 +00:00
Jon 0d8ad0a786 add supporting changes 2024-02-26 20:13:29 +00:00
Jon 90004305a0 stylesheets/style.css: add space between barcol children 2024-02-26 20:13:29 +00:00
Jon cf3997e500 swap dashes in threadstat attribs 2024-02-26 20:13:29 +00:00
Jon e6ad88890a templates/thread.html: Replace top button with catalog button 2024-02-26 20:13:29 +00:00
Jon da7e030a53 templates/thread.html: remove dev prefix 2024-02-26 20:13:29 +00:00
Jon d15718958a templates/thread.html: migrate threadbar and threadlinks 2024-02-26 20:13:29 +00:00
Jon af47b2d9ac stylesheets/style.css: add styles 2024-02-26 20:13:29 +00:00
Jon eddb64a3d5 stylesheets/style.css: add lcn_threadstats style 2024-02-26 20:13:29 +00:00
Jon 9c695a267e post_thread.html: use structured block for thread stats 2024-02-26 20:13:29 +00:00
52 changed files with 1753 additions and 922 deletions

2
.gitignore vendored
View File

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

BIN
banners/BANNER _d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
banners/BANNER_58.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
banners/banner_pepe_che.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
banners/banner_soviet_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
banners/banner_soviet_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
banners/banner_soviet_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
banners/banner_soviet_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
banners/ronald_cross.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -11,302 +11,364 @@ $hidden_inputs_twig = array();
$logfile = "/tmp/lainchan_err.out";
function print_err($s) {
global $logfile;
file_put_contents($logfile, $s . "\n", FILE_APPEND);
global $logfile;
$datetime = new Datetime();
file_put_contents(
$logfile,
$datetime->format(DateTime::ATOM) . " " . $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 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 $salt, $inputs = array(), $index = 0;
// 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 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));
}
}
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));
return $antibot;
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");
}
function checkSpam(array $extra_salt = array()) {
global $config, $pdo;
global $config, $pdo;
if (!isset($_POST['hash']))
return true;
#print_err("checkSpam start");
$extra_salt_orig = $extra_salt;
$hash = $_POST['hash'];
/*
if (!isset($_POST['hash'])) {
print_err("checkSpam: _POST array doesn't have key 'hash', check failed.");
dumpVars($extra_salt_orig);
return true;
}
*/
if (!empty($extra_salt)) {
// create a salted hash of the "extra salt"
$extra_salt = implode(':', $extra_salt);
} else {
$extra_salt = '';
}
if (isset($_POST['hash'])) {
$hash = $_POST['hash'];
} else {
$hash = "";
}
// Reconsturct the $inputs array
$inputs = array();
if (!empty($extra_salt)) {
// create a salted hash of the "extra salt"
$extra_salt = implode(':', $extra_salt);
} else {
$extra_salt = '';
}
foreach ($_POST as $name => $value) {
if (in_array($name, $config['spam']['valid_inputs']))
continue;
// Reconsturct the $inputs array
$inputs = array();
$inputs[$name] = $value;
}
foreach ($_POST as $name => $value) {
if (in_array($name, $config['spam']['valid_inputs']))
continue;
// Sort the inputs in alphabetical order (A-Z)
ksort($inputs);
$inputs[$name] = $value;
}
$_hash = '';
// Sort the inputs in alphabetical order (A-Z)
ksort($inputs);
// Iterate through each input
foreach ($inputs as $name => $value) {
$_hash .= $name . '=' . $value;
}
$_hash = '';
// Add a salt to the hash
$_hash .= $config['cookies']['salt'];
// Iterate through each input
foreach ($inputs as $name => $value) {
$_hash .= $name . '=' . $value;
}
// Use SHA1 for the hash
$_hash = sha1($_hash . $extra_salt);
// Add a salt to the hash
$_hash .= $config['cookies']['salt'];
if ($hash != $_hash) {
return true;
}
// Use SHA1 for the hash
$_hash = sha1($_hash . $extra_salt);
$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;
}
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;
}
return $hash;
$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;
}
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' => 'last_modified',
'bump' => 'bump',
'board' => 'board',
);
@ -197,6 +197,7 @@ 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,6 +33,8 @@
// 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

@ -384,6 +384,7 @@ class Post extends PostProps {
private $raw_body;
private $has_file;
private $tracked_cites;
private $filesize;
public function __construct($post, $root=null, $mod=false) {
global $config;
@ -394,8 +395,10 @@ class Post extends PostProps {
$this->$key = $value;
}
if (isset($this->files) && $this->files)
if (isset($this->files) && $this->files
&& is_string($this->files)) {
$this->files = @json_decode($this->files);
}
$this->subject = utf8tohtml($this->subject);
$this->name = utf8tohtml($this->name);

View File

@ -24,6 +24,42 @@ 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;
@ -75,11 +111,14 @@ 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,6 +18,7 @@ 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

@ -2303,6 +2303,8 @@ function buildThread($id, $return = false, $mod = false) {
$action = generation_strategy('sb_thread', array($board['uri'], $id));
$rendered_thread = null;
if ($action == 'rebuild' || $return || $mod) {
$query = prepare(sprintf("SELECT *,'%s' as board FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri'],$board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
@ -2323,10 +2325,12 @@ function buildThread($id, $return = false, $mod = false) {
$hasnoko50 = $thread->postCount() >= $config['noko50_min'];
$antibot = $mod || $return ? false : create_antibot($board['uri'], $id);
$rendered_thread = $thread->build();
$body = Element('thread.html', array(
'board' => $board,
'thread' => $thread,
'body' => $thread->build(),
'body' => $rendered_thread,
'config' => $config,
'id' => $id,
'mod' => $mod,
@ -2364,6 +2368,8 @@ function buildThread($id, $return = false, $mod = false) {
}
file_write($board['dir'] . $config['dir']['res'] . link_for($thread), $body);
return $rendered_thread;
}
}
@ -2899,3 +2905,34 @@ 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,6 +22,7 @@ $config['boards'] = array(
'edu',
'ga',
'ent',
'music',
'777',
'posad',
'i',
@ -41,7 +42,7 @@ $config['prepended_foreign_boards'] = array(
// Board categories. Only used in the "Categories" theme.
$config['categories'] = array(
'Leftypol' => array(
'Boards' => array(
'leftypol',
'b',
'WRK',
@ -50,6 +51,7 @@ $config['categories'] = array(
'edu',
'ga',
'ent',
'music',
'777',
'posad',
'i',
@ -64,19 +66,19 @@ $config['categories'] = array(
// with non-board links.
$config['custom_categories'] = array(
'Links' => array(
'New Multitude' => 'https://newmultitude.org/',
'Booru image repository' => 'https://lefty.booru.org/',
'Leftypedia' => 'https://leftypedia.org/',
'New Multitude' => 'https://newmultitude.org',
'Booru image repository' => 'https://lefty.pictures',
'Official chat room' => 'https://talk.leftychan.net/#/room/#welcome:matrix.leftychan.net',
'Gitea instance' => 'https://git.leftychan.net',
'Nukechan' => 'https://nukechan.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://leftypedia.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://wiki.leftypol.org',
'Marxist Internet Archive' => 'https://www.marxists.org'
),
);
@ -132,7 +134,8 @@ $config['post_date'] = '%F (%a) %T';
$config['thread_subject_in_title'] = true;
$config['spam']['enabled'] = false;
$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_noticer']['enabled'] = true;
$config['spam_noticer']['base_url'] = 'http://localhost:8300';
$config['spam_noticer']['ui_url'] = 'https://spamnoticer.leftychan.net/static/index.html';
@ -142,7 +145,8 @@ $config['spam_noticer']['website_name'] = "leftychan";
/*
* Basic captcha. See also: captchaconfig.php
*/
$config['securimage'] = false;
$config['securimage'] = true;
$config['captcha_tor_only'] = true;
/*
* Permissions
@ -209,6 +213,7 @@ $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';
@ -257,6 +262,7 @@ $config['user_flags'] = array (
'egalitarianism' => 'Egalitarianism',
'egoism' => 'Egoism',
'eristocracy' => 'Έριστοκρατία',
'Eurasianism' => 'Eurasianism',
'eureka' => 'Eureka',
'eurocommunism' => 'Eurocommunism',
'farc' => 'Las FARC',
@ -280,9 +286,10 @@ $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' => 'Nazbol',
'nazbol' => 'National Bolshevik',
'nazi' => 'Nazi',
'ndfp' => 'NDFP',
'palestine' => 'Palestine',
@ -309,13 +316,14 @@ $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',
'zapatista' => 'Zapatista'
'zgang' => 'Z Gang'
);
@ -403,8 +411,6 @@ $config['additional_javascript'][] = 'js/options/user-css.js';
$config['additional_javascript'][] = 'js/options/user-js.js';
$config['additional_javascript'][] = 'js/flag-preview.js';
$config['additional_javascript'][] = 'js/file-selector.js';
$config['additional_javascript_defer'][] = 'js/auto-reload.js';
$config['additional_javascript_defer'][] = 'js/thread-stats.js';
$config['additional_javascript_defer'][] = 'js/image-hover.js';
@ -419,6 +425,7 @@ $config['additional_javascript_defer'][] = 'js/expand-video.js';
// New LCN scripts
$config['additional_javascript'][] = 'js/lcn/utils.js';
$config['additional_javascript'][] = 'js/lcn/classes.js';
$config['additional_javascript'][] = 'js/lcn/thread_autoupdater.js';
$config['additional_javascript_compile'] = true;
$config['minify_js'] = true;
@ -564,11 +571,31 @@ $config['filters'][] = array(
$config['filters'][] = array(
'condition' => array(
'body' => '/(^|\s)((https?):\/\/)?[\w-]{2,6}\.[a-z]{2,4}\/\w{2,8}(\s|$)/i', // url shorteners are not allowed
'body' => '/(^|\s)((https?):\/\/)?[\w-]{2,6}\.[a-z]{2,4}\/\w{2,8}(#[^\s]+)?(\s|$)/i', // url shorteners are not allowed
),
'action' => 'reject',
'message' => 'Url shorteners are not allowed'
);
$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>';
// 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['debug'] = false;

View File

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

View File

@ -6,6 +6,8 @@
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,6 +1501,7 @@ 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']);
@ -1562,7 +1563,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);
@ -1586,8 +1587,7 @@ 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,6 +1726,7 @@ 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.post.reply').each(function() {
$(data).find('div.postcontainer').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).before('<br class="expanded">');
$(this).addClass('expanded').insertAfter(last_expanded);
} else {
$(this).addClass('expanded').insertAfter(thread.find('div.post:first')).after('<br class="expanded">');
$(this).addClass('expanded').insertAfter(thread.find('div.post:first'));
}
last_expanded = $(this);
$(document).trigger('new_post', this);

View File

@ -6,6 +6,34 @@
globalThis.LCNSite = class LCNSite {
static INSTANCE = null;
static "createAbortable" () {
const obj = { "abort": null, "controller": null, "signal": null }
const setupController = () => {
obj.controller = new AbortController()
obj.signal = obj.controller.signal
}
obj.abort = () => {
obj.controller.abort()
setupController()
}
setupController()
return obj
}
static "getThreadFromPages" (pages, thread_id) {
for (const page of pages) {
for (const thread of page.threads) {
if (thread_id == String(thread.no)) {
return { "page": page.page, ...thread }
}
}
}
return null
}
#isModerator = document.body.classList.contains("is-moderator");
#isThreadPage = document.body.classList.contains("active-thread");
#isBoardPage = document.body.classList.contains("active-board");
@ -53,10 +81,28 @@ globalThis.LCNSite = class LCNSite {
this.#favicon.href = `/favicon${type ? "-" + type : ""}.ico`
}
"getFloaterLContainer" () { return document.getElementById("bar-bottom-l"); }
"getFloaterRContainer" () { return document.getElementById("bar-bottom-r"); }
"getThreadStatsLContainer" () { return document.getElementById("lcn-threadstats-l"); }
"getThreadStatsRContainer" () { return document.getElementById("lcn-threadstats-r"); }
#generatedStyle = null;
"writeCSSStyle" (origin, stylesheet) {
if (this.#generatedStyle == null && (this.#generatedStyle = document.querySelector("head > style.generated-css")) == null) {
this.#generatedStyle = document.createElement("style")
this.#generatedStyle.classList.add("generated-css")
document.head.appendChild(this.#generatedStyle)
}
this.#generatedStyle.textContent += `${this.#generatedStyle.textContent.length ? "\n\n" : ""}/*** Generated by ${origin} ***/\n${stylesheet}`
}
}
globalThis.LCNPostInfo = class LCNPostInfo {
static nodeAttrib = "$LCNPostInfo";
static selector = ".post:not(.grid-li)";
#boardId = null;
#threadId = null;
#postId = null;
@ -74,7 +120,7 @@ globalThis.LCNPostInfo = class LCNPostInfo {
#isLocked = false;
#isSticky = false;
static "assign" (post) { return post.$LCNPostInfo ?? (post.$LCNPostInfo = this.from(post)); }
static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); }
static "from" (post) {
assert.ok(post.classList.contains("post"), "Arty must be expected Element.")
const inst = new this()
@ -130,6 +176,8 @@ globalThis.LCNPostInfo = class LCNPostInfo {
globalThis.LCNPost = class LCNPost {
static nodeAttrib = "$LCNPost";
static selector = ".post:not(.grid-li)";
#parent = null;
#post = null;
#info = null;
@ -137,7 +185,7 @@ globalThis.LCNPost = class LCNPost {
#controls = null;
#customControlsSeperatorNode = null;
static "assign" (post) { return post.$LCNPost ?? (post.$LCNPost = this.from(post)); }
static "assign" (post) { return post[this.nodeAttrib] ?? (post[this.nodeAttrib] = this.from(post)); }
static "from" (post) { return new this(post); }
"constructor" (post) {
@ -146,7 +194,7 @@ globalThis.LCNPost = class LCNPost {
this.#post = post
this.#info = LCNPostInfo.assign(post)
this.#ipLink = intro.querySelector(".ip-link")
this.#controls = arrLast(post.querySelectorAll(".controls"))
this.#controls = Array.prototype.at.apply(post.querySelectorAll(".controls"), [ -1 ])
assert.equal(this.#info.getParent(), null, "Info should not have parent.")
this.#info.__setParent(this)
@ -193,26 +241,28 @@ globalThis.LCNPost = class LCNPost {
globalThis.LCNThread = class LCNThread {
static nodeAttrib = "$LCNThread";
static selector = ".thread:not(.grid-li)";
#element = null;
#parent = null;
#thread = null;
#op = null;
static "assign" (thread) { return thread.$LCNThread ?? (thread.$LCNThread = this.from(thread)); }
static "assign" (thread) { return thread[this.nodeAttrib] ?? (thread[this.nodeAttrib] = this.from(thread)); }
static "from" (thread) { return new this(thread); }
"constructor" (thread) {
assert.ok(thread.classList.contains("thread"), "Arty must be expected Element.")
this.#thread = thread
this.#op = LCNPost.assign(this.#thread.querySelector(".post.op"))
this.#element = thread
this.#op = LCNPost.assign(this.#element.querySelector(".post.op"))
assert.equal(this.#op.getParent(), null, "Op should not have parent.")
//assert.equal(this.#op.getParent(), null, "Op should not have parent.")
this.#op.__setParent(this)
}
"getElement" () { return this.#thread; }
"getElement" () { return this.#element; }
"getContent" () { return this.#op; }
"getPosts" () { return Array.prototype.map.apply(this.#thread.querySelectorAll(".post"), [ el => LCNPost.assign(el) ]); }
"getReplies" () { return Array.prototype.map.apply(this.#thread.querySelectorAll(".post:not(.op)"), [ el => LCNPost.assign(el) ]); }
"getPosts" () { return Array.prototype.map.apply(this.#element.querySelectorAll(".post"), [ el => LCNPost.assign(el) ]); }
"getReplies" () { return Array.prototype.map.apply(this.#element.querySelectorAll(".post:not(.op)"), [ el => LCNPost.assign(el) ]); }
"getParent" () { return this.#parent; }
"__setParent" (inst) { return this.#parent = inst; }
@ -221,19 +271,21 @@ globalThis.LCNThread = class LCNThread {
globalThis.LCNPostContainer = class LCNPostContainer {
static nodeAttrib = "$LCNPostContainer";
static selector = ".postcontainer";
#parent = null;
#container = null;
#element = null;
#content = null;
#postId = null;
#boardId = null;
static "assign" (container) { return container.$LCNPostContainer ?? (container.$LCNPostContainer = this.from(container)); }
static "assign" (container) { return container[this.nodeAttrib] ?? (container[this.nodeAttrib] = this.from(container)); }
static "from" (container) { return new this(container); }
"constructor" (container) {
assert.ok(container.classList.contains("postcontainer"), "Arty must be expected Element.")
const child = container.querySelector(".thread, .post")
this.#container = container
this.#element = container
this.#content = child.classList.contains("thread") ? LCNThread.assign(child) : LCNPost.assign(child)
this.#boardId = container.dataset.board
this.#postId = container.id.slice(2)
@ -242,7 +294,7 @@ globalThis.LCNPostContainer = class LCNPostContainer {
this.#content.__setParent(this)
}
"getContainer" () { return this.#container; }
"getElement" () { return this.#element; }
"getContent" () { return this.#content; }
"getBoardId" () { return this.#boardId; }
"getPostId" () { return this.#postId; }
@ -254,13 +306,15 @@ globalThis.LCNPostContainer = class LCNPostContainer {
globalThis.LCNPostWrapper = class LCNPostWrapper {
static nodeAttrib = "$LCNPostWrapper";
static selector = ".post-wrapper";
#wrapper = null;
#eitaLink = null;
#eitaId = null;
#eitaHref = null
#content = null;
static "assign" (wrapper) { return wrapper.$LCNPostWrapper ?? (wrapper.$LCNPostWrapper = this.from(wrapper)); }
static "assign" (wrapper) { return wrapper[this.nodeAttrib] ?? (wrapper[this.nodeAttrib] = this.from(wrapper)); }
static "from" (wrapper) { return new this(wrapper); }
"constructor" (wrapper) {
@ -298,23 +352,150 @@ globalThis.LCNPostWrapper = class LCNPostWrapper {
}
globalThis.LCNPost.all = () => Array.prototype.map.apply(document.querySelectorAll(".post:not(.grid-li)"), [ node => LCNPost.assign(node) ]);
globalThis.LCNThread.all = () => Array.prototype.map.apply(document.querySelectorAll(".thread:not(.grid-li)"), [ node => LCNThread.assign(node) ]);
globalThis.LCNPostContainer.all = () => Array.prototype.map.apply(document.querySelectorAll(".postcontainer"), [ node => LCNPostContainer.assign(node) ]);
globalThis.LCNPostWrapper.all = () => Array.prototype.map.apply(document.querySelectorAll(".post-wrapper"), [ node => LCNPostWrapper.assign(node) ]);
globalThis.LCNSetting = class LCNSetting {
#id = null;
#eventId = null;
#label = null;
#hidden = false;
#value = null;
#valueDefault = null;
static "build" (id) { return new this(id); }
"constructor" (id) {
this.#id = id;
this.#eventId = `lcnsetting::${this.#id}`
}
#getValue () {
const v = localStorage.getItem(this.#id)
if (v != null) {
return this.__builtinValueImporter(v)
} else {
return this.#valueDefault
}
}
"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) {
this.#value = v
localStorage.setItem(this.#id, this.__builtinValueExporter(this.#value))
setTimeout(() => $(document).trigger(`${this.#eventId}::change`, [ v, this ]), 1)
}
}
"getLabel" () { return this.#label; }
"setLabel" (label) { this.#label = label; return this; }
"getDefaultValue" () { return this.#valueDefault; }
"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}`
}
}
globalThis.LCNToggleSetting = class LCNToggleSetting extends LCNSetting {
__builtinValueImporter (v) { return v == "1"; }
__builtinValueExporter (v) { return v ? "1" : ""; }
__builtinDOMConstructor () {
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 => {
e.preventDefault();
this.setValue(!this.getValue())
})
this.onChange(v => chk.checked = v)
div.appendChild(chk)
div.appendChild(txt)
return div
}
}
globalThis.LCNSettingsSubcategory = class LCNSettingsSubcategory {
#tab_id = null;
#id = null;
#fieldset = null;
#legend = null;
#label = null;
static "for" (tab_id, id) {
const domid = `lcnssc_${tab_id}_${id}`
const inst = document.getElementById(domid)?.$LCNSettingsSubcategory
if (inst == null) {
const fieldset = document.createElement("fieldset")
const legend = document.createElement("legend")
fieldset.id = domid
fieldset.appendChild(legend)
// XXX: extend_tab only takes a string so this hacky workaround is used to let us use the regular dom api
Options.extend_tab(tab_id, `<div id="__${domid}" hidden></div>`)
const div = document.getElementById(`__${domid}`)?.parentElement
assert.ok(div)
div.replaceChildren(fieldset)
return new this(tab_id, id, fieldset)
} else {
return inst
}
}
"constructor" (tab_id, id, fieldset) {
this.#tab_id = tab_id
this.#id = id
this.#fieldset = fieldset
this.#legend = this.#fieldset.querySelector("legend")
this.#fieldset.$LCNSettingsSubcategory = this
}
"getLabel" () { return this.#label; }
"setLabel" (label) { this.#legend.innerText = this.#label = label; return this; }
"addSetting" (setting) {
assert.ok(setting instanceof LCNSetting)
setting.__setIdPrefix(`lcnsetting_${this.#tab_id}_${this.#id}`)
if (!setting.isHidden() && setting.__builtinDOMConstructor != null) {
const div = setting.__builtinDOMConstructor()
div.classList.add("lcn-setting-entry")
this.#fieldset.appendChild(div)
}
return this
}
}
$().ready(() => {
LCNSite.INSTANCE = new LCNSite();
const clazzes = [ LCNPost, LCNThread, LCNPostContainer, LCNPostWrapper ]
for (const clazz of clazzes) {
clazz.forEach = fn => clazz.all().forEach(fn)
clazz.filter = fn => clazz.all().filter(fn)
for (const clazz of [ LCNPost, LCNPostInfo, LCNThread, LCNPostContainer, LCNPostWrapper ]) {
clazz.allNodes = (node=document) => node.querySelectorAll(clazz.selector)
clazz.all = (node=document) => Array.prototype.map.apply(clazz.allNodes(node), [ elem => clazz.assign(elem) ]);
clazz.clear = (node=document) => Array.prototype.forEach.apply(clazz.allNodes(node), [ elem => elem[clazz.nodeAttrib] = null ])
clazz.forEach = (fn, node=document) => clazz.allNodes(node).forEach(elem => fn(clazz.assign(elem)))
clazz.filter = (fn, node=document) => clazz.all(node).filter(fn)
clazz.find = fn => clazz.all().find(fn)
clazz.first = (node=document) => clazz.assign(node.querySelector(clazz.selector))
clazz.last = (node=document) => clazz.assign(Array.prototype.at.apply(clazz.allNodes(node), [ -1 ]))
}
// XXX: May be a cleaner way to do this but this should be fine for now.
for (const clazz of clazzes) { void clazz.all(); }
for (const clazz of [ LCNPostContainer, LCNPostWrapper, LCNThread, LCNPost ]) { void clazz.all(); }
$(document).on("new_post", (e, post) => {
if (LCNSite.INSTANCE.isModRecentsPage()) {
void LCNPostWrapper.all()
@ -322,4 +503,7 @@ $().ready(() => {
void LCNPostContainer.all()
}
})
$(window).on("focus", () => LCNSite.INSTANCE.clearUnseen())
$(document.body).on("mousemove", () => LCNSite.INSTANCE.clearUnseen())
})

View File

@ -0,0 +1,262 @@
/**
* @file Thread auto updater.
* @author jonsmy
*/
$().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))*/;
if (LCNSite.INSTANCE.isThreadPage()) {
let threadUpdateStatus = null
let secondsCounter = 0
let threadState = null
let threadStats = null
let statReplies = null
let statFiles = null
let statPage = null
let statUniqueIPs = null
const parser = new DOMParser()
const abortable = LCNSite.createAbortable()
const threadStatsItems = []
const updateDOMStatus = () => {
const text = threadState ?? (secondsCounter >= 0 ? `${secondsCounter}s` : "…")
threadUpdateStatus.innerText = text
}
const updateSecondsByTSLP = post_info => {
secondsCounter = Math.floor(((Date.now() - post_info.getCreatedAt().getTime()) / 120000))
secondsCounter = Math.min(1000, secondsCounter)
secondsCounter = Math.max(11, secondsCounter)
}
const updateStatsFn = async thread => {
// XXX: Using /%b/%d.json would be better however the page number isn't provided.
const res = await fetch(`/${thread.getContent().getInfo().getBoardId()}/threads.json`, {
"signal": abortable.signal
})
if (res.ok) {
const stats = LCNSite.getThreadFromPages(await res.json(), thread.getContent().getInfo().getThreadId())
if (stats != null) {
threadStats = stats
} else {
threadState = String(res.status)
}
} else {
throw new Error(`Server responded with non-OK status '${res.status}'`)
}
}
const findMissingReplies = (thread_op, thread_dom, thread_latest) => {
const lastPostTs = (thread_dom.at(-1)?.getInfo() ?? thread_op.getInfo()).getCreatedAt().getTime()
const missing = []
for (const pc of thread_latest.reverse()) {
if (pc.getContent().getInfo().getCreatedAt().getTime() > lastPostTs) {
missing.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)
}
for (const pc of missingPCList) {
$(document).trigger("new_post", [ pc.getContent().getElement() ])
}
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 = () => {
if (onTickId != null) {
clearTimeout(onTickId)
onTickId = null
}
abortable.abort()
}
let onTickId = null
const onTickFn = async () => {
void secondsCounter--;
onTickClean()
updateDOMStatus()
if (threadState == null) {
if (secondsCounter < 0) {
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())
}
const threadEl = thread.getElement()
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())
} catch (error) {
console.error("threadAutoUpdater: Failed while processing update. Probably a network error", error)
secondsCounter = 60
}
}
onTickId = setTimeout(onTickFn, 1000)
}
}
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()
if (v) {
_domsetup_btn: {
const container = LCNSite.INSTANCE.getFloaterLContainer()
floaterLinkBox = document.createElement("span")
const threadlink = document.createElement("span")
const threadUpdateLink = document.createElement("a")
threadUpdateStatus = document.createElement("span")
threadUpdateStatus.id = "thread-update-status"
threadUpdateStatus.innerText = "…"
threadUpdateLink.addEventListener("click", e => {
e.preventDefault()
$(document).trigger("thread_manual_refresh")
})
threadUpdateLink.href = "#"
threadUpdateLink.appendChild(new Text("Refresh: "))
threadUpdateLink.appendChild(threadUpdateStatus)
threadlink.classList.add("threadlink")
threadlink.appendChild(threadUpdateLink)
floaterLinkBox.classList.add("threadlinks")
floaterLinkBox.appendChild(threadlink)
container.appendChild(floaterLinkBox)
}
_domsetup_stats: {
const container = LCNSite.INSTANCE.getThreadStatsRContainer()
const span1 = document.createElement("span")
const span2 = document.createElement("span")
const span3 = document.createElement("span")
statUniqueIPs = document.getElementById("lcn-uniqueips")
statReplies = document.createElement("span")
statFiles = document.createElement("span")
statPage = document.createElement("span")
statReplies.id = "lcn_replies_n"
statReplies.innerText = "…"
statFiles.id = "lcn_files_n"
statReplies.innerText = "…"
statPage.id = "lcn_page_n"
statPage.innerText = "…"
span1.appendChild(new Text("Replies: "))
span1.appendChild(statReplies)
span2.appendChild(new Text("Files: "))
span2.appendChild(statFiles)
span3.appendChild(new Text("Page: "))
span3.appendChild(statPage)
for (const span of [ span1, span2, span3 ]) {
threadStatsItems.push(span)
container.appendChild(span)
}
}
$(document).trigger("thread_manual_refresh")
} else {
floaterLinkBox?.remove()
floaterLinkBox = null
statReplies = null
statFiles = null
statPage = null
while (threadStatsItems.length) {
threadStatsItems.shift().remove()
}
}
}
kIsEnabled.onChange(onStateChangeFn)
onStateChangeFn(kIsEnabled.getValue())
}
})

View File

@ -3,20 +3,12 @@
* @author jonsmy
*/
const arrLast = arr => arr[arr.length-1] ?? undefined;
const getConfigBool = (k,d) => { const v = localStorage.getItem(`jon-modjs::${k}`); return v ? v == "1" : d; }
const writeCSSStyle = textContent => {
const style = document.createElement("style")
style.textContent = textContent
document.head.appendChild(style)
}
const assert = {
"equal": (actual, expected, message="No message set") => {
if (actual !== expected) {
const err = new Error(`Assertion Failed. ${message}`)
err.data = { actual, expected}
Error.captureStackTrace(err, assert.equal)
Error.captureStackTrace?.(err, assert.equal)
debugger
throw err
}
@ -25,7 +17,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
include 'inc/functions.php';
include 'inc/mod/pages.php';
require_once 'inc/functions.php';
require_once 'inc/mod/pages.php';
if (!isset($_GET['board']) || !preg_match("/{$config['board_regex']}/u", $_GET['board'])) {
http_response_code(400);

View File

@ -457,6 +457,8 @@ 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']);
}
@ -511,7 +513,11 @@ function handle_post(){
}
}
if(isset($config['securimage']) && $config['securimage']){
if((isset($config['securimage']) && $config['securimage'])
&& (
!(isset($config['captcha_tor_only']) && $config['captcha_tor_only'])
|| ipIsLocal($_SERVER['REMOTE_ADDR'])
)){
if(!isset($_POST['captcha'])){
error($config['error']['securimage']['missing']);
@ -1032,6 +1038,17 @@ 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);
}
@ -1475,7 +1492,9 @@ function handle_post(){
}
buildThread($post['op'] ? $id : $post['thread']);
$thread_id = $post['op'] ? $id : $post['thread'];
$rendered_thread = buildThread($thread_id);
if ($config['syslog'])
_syslog(LOG_INFO, 'New post: /' . $board['dir'] . $config['dir']['res'] .
@ -1487,12 +1506,14 @@ function handle_post(){
header('Location: ' . $redirect, true, $config['redirect_http']);
} else {
header('Content-Type: text/json; charset=utf-8');
$api = new Api();
echo json_encode(array(
'redirect' => $redirect,
'noko' => $noko,
'id' => $id,
'post' => $api->translatePost(new Post($post))
'thread_id' => $thread_id,
'thread' => $rendered_thread,
'mod' => !!$post['mod']
));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 B

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
static/flags/zgang.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

View File

@ -3,184 +3,220 @@ div.sidearrows {
}
body {
background: #1D1F21;
color: #ACACAC;
font-family: Courier, monospace;
font-size: 13px;
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;
}
/* 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: #663E11;
font-family: Courier, monospace;
color: #FFB300;
font-family: Courier, monospace;
}
h1 {
font-size: 24pt;
font-weight: normal;
letter-spacing: 0px;
font-size: 18pt;
font-weight: bold;
letter-spacing: 0px;
}
header div.subtitle {
font-size: 12pt;
font-size: 10pt;
}
/* 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: 1px dashed #117743;
padding-right: 1px;
border-radius: 3px;
}
form table tr th {
background: #282A2E;
border: 1px solid #117743;
border-radius: 5px;
background: #282A2E;
border: 1px solid #117743;
border-radius: 2px;
padding: 3px;
}
input[type="text"], input[type="password"], textarea, select {
border: 1px double #07371F;
border-radius: 5px;
background: #282A2E;
color: #ACACAC;
font-family: Courier, monospace;
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;
}
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: 5px;
background: #16171A;
color: #ACACAC;
font-family: Courier, monospace;
font-weight: bold;
border: 3px double #07371F;
border-radius: 2px;
background-color: #07371F;
color: #ACACAC;
font-family: Courier, monospace;
font-weight: bold;
font-size: 16px;
}
input[type="submit"]:hover {
border-color: #117743;
background-color: #117743;
}
.dropzone {
background: #16171A;
border: 3px double #07371F;
color: #ACACAC;
background: #16171A;
border: 1px solid #07371F;
color: #ACACAC;
border-radius: 2px;
margin: 0 2px;
}
.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: 5px;
background: #282A2E;
border: 1px solid #117743;
border-radius: 2px;
}
div.post.reply.highlighted {
background: rgba(59, 22, 43, 0.4);
border: 1px solid #117743;
border-radius: 5px;
background: rgba(59, 22, 43, 0.4);
border: 1px solid #117743;
border-radius: 2px;
}
/* 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: 5px;
font-size: 10pt;
background: #282A2E;
border: 1px solid #117743;
border-radius: 2px;
font-size: 11pt;
}
/* 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;
}
@ -225,7 +261,7 @@ table thead th {
}
.theme-catalog .thread .meta {
font-size: 10pt;
font-size: 13px;
}
.theme-catalog .thread.grid-size-small .replies {

View File

@ -4,20 +4,39 @@ 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

@ -2015,3 +2015,71 @@ span.orangeQuote {
.options_general_tab--select_opt select {
float: none;
}
/* LCN */
.lcn-threadstats {
display: flex;
justify-content: space-between;
}
.lcn-threadstats-container {
display: flex;
gap: 8px;
}
.lcn-threadstats-container > :not(:first-child):before {
content: ' | ';
}
.lcn-setting-entry {
display: flex;
align-items: flex-end;
gap: 0.5em;
}
.lcn-bar {
display: flex!important;
justify-content: space-between;
gap: 6px;
}
#thread-update-status {
display: inline-block;
width: 2.5em;
text-align: right;
overflow-x: clip;
}
.bar-collection {
padding: 0px 3px;
}
.bar-collection .threadlinks:not(:first-child) {
margin-left:4px
}
.lcn-bar .threadlinks:before,
.lcn-bar .threadlinks:after,
.lcn-bar .threadlinks .threadlink:not(:first-child):before {
vertical-align: middle;
}
.threadlinks:before {
content: '[ ';
}
.threadlinks:after {
content: ' ]';
}
.threadlinks .threadlink:not(:first-child):before {
content: ' / ';
}
.indielinks-row .indielinks:not(:first-child):before {
content: ' | ';
}
.indielinks .indielink:not(:first-child) {
margin-left: 6px;
}

View File

@ -5,13 +5,10 @@
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 %}
@ -103,15 +100,38 @@
</tr>
{% endif %}
{% if config.securimage %}
<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>
<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 %}
{% endif %}
{% if config.user_flag %}
<tr>
@ -204,7 +224,6 @@
</td>
</tr>{% endif %}
</table>
{{ antibot.html(true) }}
<input type="hidden" name="hash" value="{{ antibot.hash() }}">
</form>

View File

@ -100,7 +100,12 @@
{% endfor %}
<br class="clear"/>
{% if not index %}
<div id="uniqueip"><span style="display: block; float: left;">Unique IPs: {{ iparray|length }} </span></div>
<div class="lcn-threadstats">
<div class="lcn-threadstats-container" id="lcn-threadstats-l">
<span>Unique IPs: <span id="lcn-uniqueips">{{ iparray|length }}</span></span>
</div>
<div class="lcn-threadstats-container" id="lcn-threadstats-r"></div>
</div>
{% endif %}
{% if hr %}<hr/>{% endif %}
</div>

View File

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

View File

@ -56,9 +56,12 @@
{% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %}
<hr />
<div class="threadlinks-noup">
<span class="threadlink">[ <a href="{{ return }}">{% trans %}Return{% endtrans %}</a> /</span>
<span class="threadlink"><a href="#bottom" style="padding-left: 10px"> {% trans %}Go to bottom{% endtrans %}</a> ]</span>
<div class="threadlinks">
<span class="threadlink"><a href="{{ return }}">{% trans %}Return{% endtrans %}</a></span>
{% if config.catalog_link %}
<span class="threadlink"><a href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">{% trans %}Catalog{% endtrans %}</a></span>
{% endif %}
<span class="threadlink"><a href="#bottom">{% trans %}Bottom{% endtrans %}</a></span>
</div>
<hr />
<form name="postcontrols" action="{{ config.post_url }}" method="post">
@ -68,15 +71,19 @@
{{ body }}
<div id="thread-interactions">
<span id="thread-links">
<a id="thread-return" href="{{ return }}">[{% trans %}Return{% endtrans %}]</a>
<a id="thread-top" href="#top">[{% trans %}Go to top{% endtrans %}]</a>
{% if config.catalog_link %}
<a id="thread-catalog" href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">[{% trans %}Catalog{% endtrans %}]</a>
{% endif %}
{% if config.home_link %}
| <a id="thread-home" href="{{ config.root }}">[{% trans %}Home{% endtrans %}]</a>
{% endif %}
<span class="indielinks-row">
<span class="indielinks" id="thread-indielinks-l">
<span class="indielink"><a href="{{ return }}">[{% trans %}Return{% endtrans %}]</a></span>
{% if config.catalog_link %}
<span class="indielink"><a href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">[{% trans %}Catalog{% endtrans %}]</a></span>
{% endif %}
<span class="indielink"><a href="#top">[{% trans %}Top{% endtrans %}]</a></span>
</span>
<span class="indielinks" id="thread-indielinks-r">
{% if config.home_link %}
<span class="indielink"><a href="{{ config.root }}">[{% trans %}Home{% endtrans %}]</a></span>
{% endif %}
</span>
</span>
<span id="thread-quick-reply">
@ -104,12 +111,24 @@
{% for footer in config.footer %}<p class="unimportant" style="text-align:center;">{{ footer }}</p>{% endfor %}
</footer>
</div>
<div class="bar bottom">
<div class="threadlinks">
<span class="threadlink">[ <a href="{{ return }}">{% trans %}Return{% endtrans %}</a> /</span>
<span class="threadlink"><a href="#" style="padding-left: 10px"> {% trans %}Go to top{% endtrans %}</a> /</span>
</div>
<div class=pages></div>
<div class="bar lcn-bar bottom">
<span class="bar-collection" id="bar-bottom-l">
<span class="threadlinks">
<span class="threadlink"><a href="{{ return }}">{% trans %}Return{% endtrans %}</a></span>
{% if config.catalog_link %}
<span class="threadlink"><a href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">{% trans %}Catalog{% endtrans %}</a></span>
{% endif %}
<span class="threadlink"><a href="#top">{% trans %}Top{% endtrans %}</a></span>
<span class="threadlink"><a href="#bottom">{% trans %}Bottom{% endtrans %}</a></span>
</span>
</span>
<span class="bar-collection" id="bar-bottom-r">
<span class="threadlinks">
{% if config.home_link %}
<span class="threadlink"><a href="{{ config.root }}">{% trans %}Home{% endtrans %}</a></span>
{% endif %}
</span>
</span>
</div>
<script type="text/javascript">{% verbatim %}
ready();