Compare commits

...
Sign in to create a new pull request.

96 commits

Author SHA1 Message Date
9b1f4debad README.md: trim 2025-06-02 00:51:17 +02:00
5bc1009dfb faq: remove broken link to readlist thread 2025-06-01 23:38:30 +02:00
b55c299842 Merge pull request 'Fix #124' (#126) from fix-catalog-hover into config
Reviewed-on: leftypol/leftypol#126
2025-06-01 16:26:13 -05:00
ca25c85984 tsuki.css: change hover color 2025-06-01 23:21:13 +02:00
ee84baf87d tsuki.css: remove duplicated css 2025-06-01 23:19:34 +02:00
d4bc625c05 szalet.css: change catalog hover color 2025-06-01 23:16:29 +02:00
34bf9b2261 dark_spook.css: change catalog hover color 2025-06-01 23:12:38 +02:00
f580100121 dark.css: change catalog hover color 2025-06-01 23:10:39 +02:00
46c5f17db8 dark_red.css: change catalog hover color 2025-06-01 23:08:19 +02:00
c9926802f7 database.php: force utf8mb4 for PDO 2025-06-01 16:05:43 +02:00
e76c4eeed4 static: add the logo (somehow it wasn't in the repo!) 2025-05-31 23:04:41 +02:00
3c9c86901a MemcacheCacheDriver.php: fix error check 2025-05-31 18:10:51 +02:00
7aae8be1ae MemcacheCacheDriver.php: add detailed errors 2025-05-31 18:08:54 +02:00
2b30929bc9 MemcacheCacheDriver.php: fix init again 2025-05-30 00:22:42 +02:00
eed2b2986a Merge pull request 'Fix memecached driver' (#123) from memcached-fix into config
Reviewed-on: leftypol/leftypol#123
2025-05-29 17:07:23 -05:00
de9d118390 cache.php: update MemcachedCacheDriver initialization 2025-05-30 00:06:23 +02:00
37e771f0be MemcacheCacheDriver.php: fix configuration 2025-05-30 00:01:55 +02:00
b9a29927f3 RedisCacheDriver.php: use CacheDriverTrait 2025-05-30 00:01:55 +02:00
84a22a788e CacheDriverTrait.php: add CacheDriverTrait to share code 2025-05-30 00:01:53 +02:00
126314846c banners: add new banner 2025-05-13 23:11:55 +02:00
e10cd5fab4 banners: add closeted dengoid flag 2025-05-12 22:43:53 +02:00
e5145cd98c banners: add thread on leftypol banner 2025-05-12 22:43:50 +02:00
dcb978e31a delete-stray-images.php: handle missing files 2025-05-11 22:47:19 +02:00
0c898abdfe flags: remove grace flag 2025-05-08 21:44:56 +02:00
6e5a56ff0e flags: remove incorrectly named rodina flags 2025-05-08 21:44:20 +02:00
a779f12144 flags: correct rodina naming 2025-05-03 22:21:06 +02:00
a586025a62 Merge pull request 'Set a minimum width for text in posts' (#122) from anon-css-fixes into config
Reviewed-on: leftypol/leftypol#122
2025-05-02 16:20:17 -05:00
18624963d5 style.css: set minimum width for text 2025-05-02 22:57:04 +02:00
2cc7a70e4c flags: fix typo in Stirner flag name 2025-04-25 23:31:33 +02:00
e3693f2d19 flags: add 420-tan flag 2025-04-25 14:23:52 +02:00
63228d04be pages.php: use Context provided config 2025-04-23 23:16:18 +02:00
4cf6fd3838 Merge pull request 'Better auth' (#121) from better-auth into config
Reviewed-on: leftypol/leftypol#121
2025-04-23 15:52:29 -05:00
f7dae74522 auth.php: format 2025-04-23 22:45:32 +02:00
3e3b71211a auth.php: use php 8.4 cost for bcrypt 2025-04-23 22:45:32 +02:00
2f69af8267 auth.php: remove unused global 2025-04-23 22:45:32 +02:00
fowr
4ef10e26fc auth.php: cleanup ununsed function 2025-04-23 22:45:32 +02:00
fowr
715005ec96 auth.php: no need to repass version anymore in test_password 2025-04-23 22:45:32 +02:00
f7bef11ac9 auth.php: use pre-hashing for BCRYPT 2025-04-23 22:45:32 +02:00
fowr
a8a947af65 config.php: bump password crypt version 2025-04-23 22:45:32 +02:00
fowr
3510f05fe8 auth.php: use password_hash with bcrypt and password_verify for login 2025-04-23 22:45:32 +02:00
2cac548b4d Merge pull request 'Remove all nntpchan support' (#120) from nntpchan-remove into config
Reviewed-on: leftypol/leftypol#120
2025-04-23 15:07:29 -05:00
24e43a5aa1 nntpchan: removed handler code 2025-04-23 22:03:32 +02:00
634f769592 config.php: remove nntpchan options 2025-04-23 22:02:57 +02:00
b9938d9513 post.php: remove nttpchan 2025-04-23 22:01:49 +02:00
55508e6210 Merge pull request 'Hande crossboard invalid links with strikethrough' (#119) from cross-strikethrough into config
Reviewed-on: leftypol/leftypol#119
2025-04-23 13:13:05 -05:00
a28d9a4246 functions.php: minor code semplification for crossbooard citations 2025-04-23 19:57:22 +02:00
439730f216 functions.php: add crossboard strikethrough for invalid cites 2025-04-23 19:52:10 +02:00
f2d0ac7341 functions.php: minor format 2025-04-23 19:34:49 +02:00
6763f7b416 Merge pull request 'Secured PDF and DJVU file handling and thumbnailing' (#118) from secure-file-handling into config
Reviewed-on: leftypol/leftypol#118
2025-04-21 09:45:48 -05:00
19c0868320 docker: add djvulibre and ghostscript tools 2025-04-21 16:42:20 +02:00
8cb6a76f0a post.php: add safe djvu thumbnail generation 2025-04-21 16:42:20 +02:00
8282d5cd63 post.php: implement safe PDF thumbnailing 2025-04-21 16:42:17 +02:00
28f75c8aed config.php: add missing djvu_file_thumbnail option 2025-04-21 16:42:17 +02:00
ff94e58f2e config.php: update pdf_file_thumbnail documentation 2025-04-21 16:42:17 +02:00
181f4ba49a config.php: add generic invalidfile error 2025-04-21 14:53:09 +02:00
bac5032b56 rules.html: make ordinance 2 into rule 15 2025-04-20 16:45:18 +02:00
8d189eb9c8 Merge pull request 'Better authentication token generation' (#116) from auth-sha256 into config
Reviewed-on: leftypol/leftypol#116
2025-04-16 07:47:29 -05:00
3c0779992a auth.php: use secure salt source, use a cryptographically secure hashing algorithm for login tokens 2025-04-16 14:44:21 +02:00
8cffb479fa functions.php: use secure_hash where appropriate 2025-04-16 14:44:21 +02:00
08c2d6f5d1 composer: add hide.php 2025-04-16 14:44:21 +02:00
acdf792daf hide.php: add hide.php to the functions 2025-04-16 14:44:21 +02:00
54dcf79a7f search.php: fix formatting 2025-04-15 22:31:18 +02:00
2e1cb7995f pages.php: better input validation in recent_posts page 2025-04-14 17:49:30 +02:00
8ee3f4c81d flags: add Tania's flag 2025-04-13 17:02:03 +02:00
68b2911dfd flags: add Rodinia's /leftypol/ flag 2025-04-13 16:50:49 +02:00
612d1cfc57 flags: add Rodinia's /get/ flag 2025-04-13 16:49:50 +02:00
5613baca05 flags: add grace flag 2025-04-13 16:49:13 +02:00
92fc2daa9c post.php: fix undefined exif_stripped 2025-03-29 08:55:15 +01:00
d224c0af23 post.php: skip resize only if already stripped 2025-03-29 00:34:37 +01:00
c1c20bdab2 post.php: fix typo 2025-03-29 00:32:56 +01:00
42e850091a config.php: remove minimum_copy_resize 2025-03-29 00:31:29 +01:00
87029580b6 post.php: default minimum_copy_resize to true by removing it 2025-03-29 00:30:59 +01:00
cf74272806 Merge pull request 'Remove functionality' (#114) from remove-tesseract into config
Reviewed-on: leftypol/leftypol#114
2025-03-28 09:06:52 -05:00
336c40b0f7 remove all tesseract traces 2025-03-28 15:05:01 +01:00
81c02be563 style.css: reduce margin between multiple files 2025-03-27 01:23:37 +01:00
fa56876c36 style.css: set minimum file container width and multifile margin 2025-03-27 00:17:17 +01:00
8da10af101 frames.html: break long words to prevent page getting cut 2025-03-26 13:55:04 +01:00
9431536112 frames.html: trim 2025-03-26 13:54:55 +01:00
025f1c4221 news.html: remove spurious stuff from css 2025-03-26 13:40:28 +01:00
501e696891 banners: remove just monika banner
This reverts commit 6bcf22aa7e.
2025-03-23 17:05:20 +01:00
557e43e38f Merge pull request 'Fix #66 Use equilateral triangle as the post menu button' (#113) from 10-post-menu-triangle into config
Reviewed-on: leftypol/leftypol#113
2025-03-17 15:24:36 -05:00
6ee8670401 post-menu.js: use unicode code with variant selector for equilateral triangle 2025-03-17 16:20:49 +01:00
c4d7bc39de Merge pull request 'WIP Port LogDriver from upstream' (#111) from log-driver-port into config
Reviewed-on: leftypol/leftypol#111
2025-03-16 12:37:21 -05:00
b2029d2533 context.php: log error if log_system is erroneous 2025-03-16 18:17:42 +01:00
5b4d1b7f4c pages.php: use LogDriver 2025-03-16 18:10:19 +01:00
665e3d339a post.php: use LogDriver 2025-03-16 18:07:58 +01:00
6be3f4bbff post.php: update LogDriver 2025-03-16 18:07:58 +01:00
8f7db3bdef FileLogDriver.php: flush writes to file 2025-03-16 18:07:58 +01:00
cca8d88d91 config.php: update LogDriver configuration 2025-03-16 18:07:58 +01:00
Zankaria
268bd84128 Refactor the logging system 2025-03-16 18:07:58 +01:00
2c0c003b2c context.php: report deprecation notice on syslog option 2025-03-16 18:07:58 +01:00
fe4813867b context.php: fix log init 2025-03-16 18:07:58 +01:00
6132084b4b context.php: update LogDriver 2025-03-16 18:07:57 +01:00
79523f8251 context.php: initial add LogDriver 2025-03-16 18:07:13 +01:00
707fb62c04 log-driver.php: split up log driver 2025-03-16 18:05:40 +01:00
6f9ea52212 faq: update git repo (again) 2025-02-24 16:14:53 +01:00
47 changed files with 1019 additions and 1079 deletions

View file

@ -14,7 +14,7 @@ Requirements
PHP 8.0 is explicitly supported. PHP 7.x should be compatable.
2. MySQL/MariaDB server >= 5.5.3
3. [Composer](https://getcomposer.org/) (To install various packages)
4. [mbstring](http://www.php.net/manual/en/mbstring.installation.php)
4. [mbstring](http://www.php.net/manual/en/mbstring.installation.php)
5. [PHP GD](http://www.php.net/manual/en/intro.image.php)
6. [PHP PDO](http://www.php.net/manual/en/intro.pdo.php)
@ -44,7 +44,7 @@ Installation
development version with:
git clone git://git.leftypol.org/leftypol/leftypol.git
2. run ```composer install``` inside the directory
3. Navigate to ```install.php``` in your web browser and follow the
prompts.
@ -80,7 +80,7 @@ find support from a variety of sources:
* For support, reply to the sticky on our [/tech/](https://leftypol.org/tech/) board.
### Tinyboard support
vichan, and by extension lainchan and leftypol, is based on a Tinyboard, so both engines have very much in common. These links may be helpful for you as well:
vichan, and by extension lainchan and leftypol, is based on a Tinyboard, so both engines have very much in common. These links may be helpful for you as well:
* Tinyboard documentation can be found [here](https://web.archive.org/web/20121016074303/http://tinyboard.org/docs/?p=Main_Page).

View file

@ -25,6 +25,7 @@
"inc/polyfill.php",
"inc/error.php",
"inc/functions.php",
"inc/functions/hide.php",
"inc/functions/net.php"
]
},

View file

@ -18,6 +18,8 @@ RUN apk add --no-cache \
graphicsmagick \
gifsicle \
ffmpeg \
djvulibre \
ghostscript \
bind-tools \
gettext \
gettext-dev \

View file

@ -0,0 +1,20 @@
<?php
namespace Vichan\Data\Driver;
trait CacheDriverTrait {
/**
* Tries to interpret the uri as a path to a unix socket.
*
* @param string $uri
* @return ?string The path to the socket, null if it cannot be interpreted as such.
*/
private static function asUnixSocketPath(string $uri): ?string {
if (str_starts_with($uri, 'unix:')) {
return \substr($uri, 5);
} elseif (str_starts_with($uri, ':')) {
return \substr($uri, 1);
}
return null;
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* Log via the php function error_log.
*/
class ErrorLogLogDriver implements LogDriver {
use LogTrait;
private string $name;
private int $level;
public function __construct(string $name, int $level) {
$this->name = $name;
$this->level = $level;
}
public function log(int $level, string $message): void {
if ($level <= $this->level) {
$lv = $this->levelToString($level);
$line = "{$this->name} $lv: $message";
\error_log($line, 0, null, null);
}
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* Log to a file.
*/
class FileLogDriver implements LogDriver {
use LogTrait;
private string $name;
private int $level;
private mixed $fd;
public function __construct(string $name, int $level, string $file_path) {
/*
* error_log is slow as hell in it's 3rd mode, so use fopen + file locking instead.
* https://grobmeier.solutions/performance-ofnonblocking-write-to-files-via-php-21082009.html
*
* Whatever file appending is atomic is contentious:
* - There are no POSIX guarantees: https://stackoverflow.com/a/7237901
* - But linus suggested they are on linux, on some filesystems: https://web.archive.org/web/20151201111541/http://article.gmane.org/gmane.linux.kernel/43445
* - But it doesn't seem to be always the case: https://www.notthewizard.com/2014/06/17/are-files-appends-really-atomic/
*
* So we just use file locking to be sure.
*/
$this->fd = \fopen($file_path, 'a');
if ($this->fd === false) {
throw new \RuntimeException("Unable to open log file at $file_path");
}
$this->name = $name;
$this->level = $level;
// In some cases PHP does not run the destructor.
\register_shutdown_function([$this, 'close']);
}
public function __destruct() {
$this->close();
}
public function log(int $level, string $message): void {
if ($level <= $this->level) {
$lv = $this->levelToString($level);
$line = "{$this->name} $lv: $message\n";
\flock($this->fd, LOCK_EX);
\fwrite($this->fd, $line);
\fflush($this->fd);
\flock($this->fd, LOCK_UN);
}
}
public function close() {
\flock($this->fd, LOCK_UN);
\fclose($this->fd);
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
interface LogDriver {
public const EMERG = \LOG_EMERG;
public const ERROR = \LOG_ERR;
public const WARNING = \LOG_WARNING;
public const NOTICE = \LOG_NOTICE;
public const INFO = \LOG_INFO;
public const DEBUG = \LOG_DEBUG;
/**
* Log a message if the level of relevancy is at least the minimum.
*
* @param int $level Message level. Use Log interface constants.
* @param string $message The message to log.
*/
public function log(int $level, string $message): void;
}

View file

@ -0,0 +1,26 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
trait LogTrait {
public static function levelToString(int $level): string {
switch ($level) {
case LogDriver::EMERG:
return 'EMERG';
case LogDriver::ERROR:
return 'ERROR';
case LogDriver::WARNING:
return 'WARNING';
case LogDriver::NOTICE:
return 'NOTICE';
case LogDriver::INFO:
return 'INFO';
case LogDriver::DEBUG:
return 'DEBUG';
default:
throw new \InvalidArgumentException('Not a logging level');
}
}
}

View file

@ -5,18 +5,49 @@ defined('TINYBOARD') or exit;
class MemcachedCacheDriver implements CacheDriver {
use CacheDriverTrait;
private \Memcached $inner;
public function __construct(string $prefix, string $memcached_server) {
public function __construct(string $prefix, string $server_uri, int $server_port, int $server_weight) {
$this->inner = new \Memcached();
if (!$this->inner->setOption(\Memcached::OPT_BINARY_PROTOCOL, true)) {
throw new \RuntimeException('Unable to set the memcached protocol!');
$err = $this->inner->getResultMessage();
throw new \RuntimeException("Unable to set the memcached protocol: '$err'");
}
if (!$this->inner->setOption(\Memcached::OPT_PREFIX_KEY, $prefix)) {
throw new \RuntimeException('Unable to set the memcached prefix!');
$err = $this->inner->getResultMessage();
throw new \RuntimeException("Unable to set the memcached prefix: '$err'");
}
if (!$this->inner->addServers($memcached_server)) {
throw new \RuntimeException('Unable to add the memcached server!');
$maybe_unix_path = self::asUnixSocketPath($server_uri);
$is_unix = $maybe_unix_path !== null;
if ($is_unix) {
$server_uri = $maybe_unix_path;
}
// Memcached keeps the server connections open across requests.
$current_servers = $this->inner->getServerList();
$found_in_curr = false;
foreach ($current_servers as $curr) {
// Ignore the port if the server is connected with a unix socket.
if ($curr['host'] === $server_uri && ($is_unix || $curr['port'] === $server_port)) {
$found_in_curr = true;
}
}
if (!$found_in_curr) {
if (!empty($current_servers)) {
if (!$this->inner->resetServerList()) {
$err = $this->inner->getResultMessage();
throw new \RuntimeException("Unable to reset the memcached server list: '$err'");
}
}
if (!$this->inner->addServer($server_uri, $server_port, $server_weight)) {
$err = $this->inner->getResultMessage();
throw new \RuntimeException("Unable to add memcached servers: '$err'");
}
}
}

View file

@ -5,18 +5,17 @@ defined('TINYBOARD') or exit;
class RedisCacheDriver implements CacheDriver {
use CacheDriverTrait;
private string $prefix;
private \Redis $inner;
public function __construct(string $prefix, string $host, ?int $port, ?string $password, int $database) {
$this->inner = new \Redis();
if (str_starts_with($host, 'unix:') || str_starts_with($host, ':')) {
$ret = \explode(':', $host);
if (count($ret) < 2) {
throw new \RuntimeException("Invalid unix socket path $host");
}
// Unix socket.
$this->inner->connect($ret[1]);
$maybe_unix = self::asUnixSocketPath($host);
if ($maybe_unix !== null) {
$this->inner->connect($maybe_unix);
} elseif ($port === null) {
$this->inner->connect($host);
} else {

View file

@ -0,0 +1,27 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* Log to php's standard error file stream.
*/
class StderrLogDriver implements LogDriver {
use LogTrait;
private string $name;
private int $level;
public function __construct(string $name, int $level) {
$this->name = $name;
$this->level = $level;
}
public function log(int $level, string $message): void {
if ($level <= $this->level) {
$lv = $this->levelToString($level);
\fwrite(\STDERR, "{$this->name} $lv: $message\n");
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* Log to syslog.
*/
class SyslogLogDriver implements LogDriver {
private int $level;
public function __construct(string $name, int $level, bool $print_stderr) {
$flags = \LOG_ODELAY;
if ($print_stderr) {
$flags |= \LOG_PERROR;
}
if (!\openlog($name, $flags, \LOG_USER)) {
throw new \RuntimeException('Unable to open syslog');
}
$this->level = $level;
}
public function log(int $level, string $message): void {
if ($level <= $this->level) {
if (isset($_SERVER['REMOTE_ADDR'], $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'])) {
// CGI
\syslog($level, "$message - client: {$_SERVER['REMOTE_ADDR']}, request: \"{$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']}\"");
} else {
\syslog($level, $message);
}
}
}
}

View file

@ -15,10 +15,18 @@ class Cache {
switch ($config['cache']['enabled']) {
case 'memcached':
return new MemcachedCacheDriver(
$config['cache']['prefix'],
$config['cache']['memcached']
);
$prefix = $config['cache']['prefix'];
$uri = $config['cache']['memcached'][0];
$port = 0;
$weight = 0;
if (isset($config['cache']['memcached'][1]) && $config['cache']['memcached'][1] !== null) {
$port = \intval($config['cache']['memcached'][1]);
}
if (isset($config['cache']['memcached'][2]) && $config['cache']['memcached'][2] !== null) {
$weight = \intval($config['cache']['memcached'][2]);
}
return new MemcachedCacheDriver($prefix, $uri, $port, $weight);
case 'redis':
$port = $config['cache']['redis'][1];
$port = empty($port) ? null : intval($port);

View file

@ -63,9 +63,29 @@
// been generated. This keeps the script from querying the database and causing strain when not needed.
$config['has_installed'] = '.installed';
// Use syslog() for logging all error messages and unauthorized login attempts.
// Deprecated, use 'log_system'.
$config['syslog'] = false;
$config['log_system'] = [
/*
* Log all error messages and unauthorized login attempts.
* Can be "syslog", "error_log" (default), "file", or "stderr".
*/
'type' => 'error_log',
// The application name used by the logging system. Defaults to "tinyboard" for backwards compatibility.
'name' => 'tinyboard',
/*
* Only relevant if 'log_system' is set to "syslog". If true, double print the logs also in stderr. Defaults to
* false.
*/
'syslog_stderr' => false,
/*
* Only relevant if "log_system" is set to `file`. Sets the file that vichan will log to. Defaults to
* '/var/log/vichan.log'.
*/
'file_path' => '/var/log/vichan.log',
];
// Use `host` via shell_exec() to lookup hostnames, avoiding query timeouts. May not work on your system.
// Requires safe_mode to be disabled.
$config['dns_system'] = false;
@ -923,10 +943,6 @@
// Location of thumbnail to use for deleted images.
$config['image_deleted'] = 'static/deleted.png';
// When a thumbnailed image is going to be the same (in dimension), just copy the entire file and use
// that as a thumbnail instead of resizing/redrawing.
$config['minimum_copy_resize'] = false;
// Maximum image upload size in bytes.
$config['max_filesize'] = 10 * 1024 * 1024; // 10MB
// Maximum image dimensions.
@ -965,15 +981,6 @@
// Set this to true if you're using Linux and you can execute `md5sum` binary.
$config['gnu_md5'] = false;
// Use Tesseract OCR to retrieve text from images, so you can use it as a spamfilter.
$config['tesseract_ocr'] = false;
// Tesseract parameters
$config['tesseract_params'] = '';
// Tesseract preprocess command
$config['tesseract_preprocess_command'] = 'convert -monochrome %s -';
// Number of posts in a "View Last X Posts" page
$config['noko50_count'] = 50;
// Number of posts a thread needs before it gets a "View Last X Posts" page.
@ -1291,6 +1298,7 @@
$config['error']['pendingappeal'] = _('There is already a pending appeal for this ban.');
$config['error']['invalidpassword'] = _('Wrong password…');
$config['error']['invalidimg'] = _('Invalid image.');
$config['error']['invalidfile'] = _('Invalid file.');
$config['error']['unknownext'] = _('Unknown file extension.');
$config['error']['filesize'] = _('Maximum file size: %maxsz% bytes<br>Your file\'s size: %filesz% bytes');
$config['error']['maxsize'] = _('The file was too big.');
@ -1888,45 +1896,6 @@
// Example: Adding the pre-markup post body to the API as "com_nomarkup".
// $config['api']['extra_fields'] = array('body_nomarkup' => 'com_nomarkup');
/*
* ==================
* NNTPChan settings
* ==================
*/
/*
* Please keep in mind that NNTPChan support in vichan isn't finished yet / is in an experimental
* state. Please join #nntpchan on Rizon in order to peer with someone.
*/
$config['nntpchan'] = array();
// Enable NNTPChan integration
$config['nntpchan']['enabled'] = false;
// NNTP server
$config['nntpchan']['server'] = "localhost:1119";
// Global dispatch array. Add your boards to it to enable them. Please make
// sure that this setting is set in a global context.
$config['nntpchan']['dispatch'] = array(); // 'overchan.test' => 'test'
// Trusted peer - an IP address of your NNTPChan instance. This peer will have
// increased capabilities, eg.: will evade spamfilter.
$config['nntpchan']['trusted_peer'] = '127.0.0.1';
// Salt for message ID generation. Keep it long and secure.
$config['nntpchan']['salt'] = 'change_me+please';
// A local message ID domain. Make sure to change it.
$config['nntpchan']['domain'] = 'example.vichan.net';
// An NNTPChan group name.
// Please set this setting in your board/config.php, not globally.
$config['nntpchan']['group'] = false; // eg. 'overchan.test'
/*
* ====================
* Other/uncategorized
@ -2041,7 +2010,7 @@
// Password hashing method version
// If set to 0, it won't upgrade hashes using old password encryption schema, only create new.
// You can set it to a higher value, to further migrate to other password hashing function.
$config['password_crypt_version'] = 1;
$config['password_crypt_version'] = 2;
// Use CAPTCHA for reports?
$config['report_captcha'] = false;
@ -2061,9 +2030,16 @@
// Enable auto IP note generation of moderator deleted posts
$config['autotagging'] = false;
// Enable PDF file thumbnail generation
// Enable PDF thumbnail generation.
// Requires a working installation of ghostscript and imagemagick.
// Imagemagick support of PDF files is not required.
$config['pdf_file_thumbnail'] = false;
// Enable djvu thumbnail generation.
// Requires djvulibre's tools and imagemagick.
// Imagemagick support of djvu files is not required.
$config['djvu_file_thumbnail'] = false;
// Enable TXT file thumbnail
$config['txt_file_thumbnail'] = false;

View file

@ -1,8 +1,8 @@
<?php
namespace Vichan;
use Vichan\Data\Driver\CacheDriver;
use Vichan\Data\{IpNoteQueries, ReportQueries, UserPostQueries};
use Vichan\Data\Driver\{CacheDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
defined('TINYBOARD') or exit;
@ -31,6 +31,34 @@ class Context {
function build_context(array $config): Context {
return new Context([
'config' => $config,
LogDriver::class => function($c) {
$config = $c->get('config');
$name = $config['log_system']['name'];
$level = $config['debug'] ? LogDriver::DEBUG : LogDriver::NOTICE;
$backend = $config['log_system']['type'];
$legacy_syslog = isset($config['syslog']) && $config['syslog'];
// Check 'syslog' for backwards compatibility.
if ($legacy_syslog || $backend === 'syslog') {
$log_driver = new SyslogLogDriver($name, $level, $config['log_system']['syslog_stderr']);
if ($legacy_syslog) {
$log_driver->log(LogDriver::NOTICE, 'The configuration setting \'syslog\' is deprecated. Please use \'log_system\' instead');
}
return $log_driver;
} elseif ($backend === 'file') {
return new FileLogDriver($name, $level, $config['log_system']['file_path']);
} elseif ($backend === 'stderr') {
return new StderrLogDriver($name, $level);
} elseif ($backend === 'error_log') {
return new ErrorLogLogDriver($name, $level);
} else {
$log_driver = new ErrorLogLogDriver($name, $level);
$log_driver->log(LogDriver::ERROR, "Unknown 'log_system' value '$backend', using 'error_log' default");
return $log_driver;
}
},
CacheDriver::class => function($c) {
// Use the global for backwards compatibility.
return \cache::getCache();

View file

@ -66,7 +66,7 @@ function sql_open() {
$dsn = $config['db']['type'] . ':' .
($unix_socket ? 'unix_socket=' . $unix_socket : 'host=' . $config['db']['server']) .
';dbname=' . $config['db']['database'];
';charset=utf8mb4;dbname=' . $config['db']['database'];
if (!empty($config['db']['dsn']))
$dsn .= ';' . $config['db']['dsn'];
try {
@ -85,7 +85,7 @@ function sql_open() {
if ($config['debug']) {
$debug['time']['db_connect'] = '~' . round((microtime(true) - $start) * 1000, 2) . 'ms';
if ($config['db']['type'] == "mysql") {
query('SET NAMES utf8') or error(db_error());
query('SET NAMES utf8mb4') or error(db_error());
}
}
return $pdo;

View file

@ -11,6 +11,7 @@ if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) {
$microtime_start = microtime(true);
use Vichan\Functions\Hide;
use Lifo\IP\IP; // for expanding IPv6 address in DNSBL()
// the user is not currently logged in as a moderator
@ -1691,7 +1692,7 @@ function checkSpam(array $extra_salt = array()) {
$_hash = sha1($_hash . $extra_salt);
if ($hash != $_hash) {
return true;
return true;
}
$query = prepare('SELECT `passed` FROM ``antispam`` WHERE `hash` = :hash');
@ -2227,20 +2228,15 @@ function markup(&$body, $track_cites = false) {
$clauses = array_unique($clauses);
if ($board['uri'] != $_board) {
if (!openBoard($_board)){
if (in_array($_board,array_keys($config['boards_alias']))){
$_board = $config['boards_alias'][$_board];
if (openBoard($_board)){
}
else {
if (!openBoard($_board)) {
if (\in_array($_board, \array_keys($config['boards_alias']))) {
$_board = $config['boards_alias'][$_board];
if (!openBoard($_board)) {
continue; // Unknown board
}
}
else {
}
} else {
continue; // Unknown board
}
}
}
@ -2281,38 +2277,31 @@ function markup(&$body, $track_cites = false) {
if ($cite) {
if (isset($cited_posts[$_board][$cite])) {
$link = $cited_posts[$_board][$cite];
if (isset($original_board)){
$replacement = '<a ' .
$replacement_board = $original_board ?? $_board;
$replacement = '<a ' .
($_board == $board['uri'] ?
'onclick="highlightReply(\''.$cite.'\', event);" '
: '') . 'href="' . $link . '">' .
'&gt;&gt;&gt;/' . $original_board . '/' . $cite .
'&gt;&gt;&gt;/' . $replacement_board . '/' . $cite .
'</a>';
if ($track_cites && $config['track_cites']) {
$tracked_cites[] = [ $_board, $cite ];
}
else {
$replacement = '<a ' .
($_board == $board['uri'] ?
'onclick="highlightReply(\''.$cite.'\', event);" '
: '') . 'href="' . $link . '">' .
'&gt;&gt;&gt;/' . $_board . '/' . $cite .
'</a>';
}
$body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
$skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]);
if ($track_cites && $config['track_cites'])
$tracked_cites[] = array($_board, $cite);
} else {
$replacement = "<s>&gt;&gt;&gt;/$_board/$cite</s>";
}
} elseif(isset($crossboard_indexes[$_board])) {
} elseif (isset($crossboard_indexes[$_board])) {
$replacement = '<a href="' . $crossboard_indexes[$_board] . '">' .
'&gt;&gt;&gt;/' . $_board . '/' .
'</a>';
$body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
$skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]);
} else {
$replacement = "<s>&gt;&gt;&gt;/$_board/$cite</s>";
}
$body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
$skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]);
}
}
@ -2583,11 +2572,11 @@ function rrmdir($dir) {
function poster_id($ip, $thread) {
global $config;
if ($id = event('poster-id', $ip, $thread))
if ($id = event('poster-id', $ip, $thread)) {
return $id;
}
// Confusing, hard to brute-force, but simple algorithm
return substr(sha1(sha1($ip . $config['secure_trip_salt'] . $thread) . $config['secure_trip_salt']), 0, $config['poster_id_length']);
return \substr(Hide\secure_hash($ip . $config['secure_trip_salt'] . $thread . $config['secure_trip_salt'], false), 0, $config['poster_id_length']);
}
function generate_tripcode($name) {
@ -2615,7 +2604,7 @@ function generate_tripcode($name) {
if (isset($config['custom_tripcode']["##{$trip}"]))
$trip = $config['custom_tripcode']["##{$trip}"];
else
$trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(base64_encode(sha1($trip . $config['secure_trip_salt'], true)), 0, 4))), -10);
$trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(Hide\secure_hash($trip . $config['secure_trip_salt'], false), 0, 4))), -10);
} else {
if (isset($config['custom_tripcode']["#{$trip}"]))
$trip = $config['custom_tripcode']["#{$trip}"];

6
inc/functions/hide.php Normal file
View file

@ -0,0 +1,6 @@
<?php
namespace Vichan\Functions\Hide;
function secure_hash(string $data, bool $binary): string {
return \hash('tiger160,3', $data, $binary);
}

View file

@ -5,108 +5,106 @@
*/
use Vichan\Context;
use Vichan\Functions\Net;
use Vichan\Functions\{Hide, Net};
defined('TINYBOARD') or exit;
// create a hash/salt pair for validate logins
function mkhash($username, $password, $salt = false) {
function mkhash(string $username, ?string $password, mixed $salt = false): array|string {
global $config;
if (!$salt) {
// create some sort of salt for the hash
$salt = substr(base64_encode(sha1(rand() . time(), true) . $config['cookies']['salt']), 0, 15);
// Create some salt for the hash.
$salt = \bin2hex(\random_bytes(15)); // 20 characters.
$generated_salt = true;
} else {
$generated_salt = false;
}
// generate hash (method is not important as long as it's strong)
$hash = substr(
base64_encode(
md5(
$username . $config['cookies']['salt'] . sha1(
$username . $password . $salt . (
$config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : ''
), true
) . sha1($config['password_crypt_version']) // Log out users being logged in with older password encryption schema
, true
)
), 0, 20
$hash = \substr(
Hide\secure_hash(
$username . $config['cookies']['salt'] . Hide\secure_hash(
$username . $password . $salt . (
$config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : ''
), true
) . Hide\secure_hash($config['password_crypt_version'], true), // Log out users being logged in with older password encryption schema
false
),
0,
40
);
if (isset($generated_salt))
return array($hash, $salt);
else
if ($generated_salt) {
return [ $hash, $salt ];
} else {
return $hash;
}
}
function crypt_password($password) {
function crypt_password(string $password): array {
global $config;
// `salt` database field is reused as a version value. We don't want it to be 0.
$version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1;
$new_salt = generate_salt();
$password = crypt($password, $config['password_crypt'] . $new_salt . "$");
return array($version, $password);
}
function test_password($password, $salt, $test) {
global $config;
// Version = 0 denotes an old password hashing schema. In the same column, the
// password hash was kept previously
$version = (strlen($salt) <= 8) ? (int) $salt : 0;
if ($version == 0) {
$comp = hash('sha256', $salt . sha1($test));
$pre_hash = \hash('tiger160,3', $password, false); // Note that it's truncated to 72 in the next line.
$r = \password_hash($pre_hash, \PASSWORD_BCRYPT, [ 'cost' => 12 ]);
if ($r === false) {
throw new \RuntimeException("Could not hash password");
}
else {
$comp = crypt($test, $password);
return [ $version, $r ];
}
function test_password(string $db_hash, string|int $version, string $input_password): bool {
$version = (int)$version;
if ($version < 2) {
$ok = \hash_equals($db_hash, \crypt($input_password, $db_hash));
} else {
$pre_hash = \hash('tiger160,3', $input_password, false);
$ok = \password_verify($pre_hash, $db_hash);
}
return array($version, hash_equals($password, $comp));
return $ok;
}
function generate_salt() {
return strtr(base64_encode(random_bytes(16)), '+', '.');
}
function login($username, $password) {
global $mod, $config;
function login(string $username, string $password): array|false {
global $mod;
$query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username");
$query->bindValue(':username', $username);
$query->execute() or error(db_error($query));
$query->execute();
if ($user = $query->fetch(PDO::FETCH_ASSOC)) {
list($version, $ok) = test_password($user['password'], $user['version'], $password);
$ok = test_password($user['password'], $user['version'], $password);
if ($ok) {
if ($config['password_crypt_version'] > $version) {
if ((int)$user['version'] < 2) {
// It's time to upgrade the password hashing method!
list ($user['version'], $user['password']) = crypt_password($password);
$query = prepare("UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id");
$query->bindValue(':password', $user['password']);
$query->bindValue(':version', $user['version']);
$query->bindValue(':id', $user['id']);
$query->execute() or error(db_error($query));
$query->execute();
}
return $mod = array(
return $mod = [
'id' => $user['id'],
'type' => $user['type'],
'username' => $username,
'hash' => mkhash($username, $user['password']),
'boards' => explode(',', $user['boards'])
);
];
}
}
return false;
}
function setCookies() {
function setCookies(): void {
global $mod, $config;
if (!$mod)
if (!$mod) {
error('setCookies() was called for a non-moderator!');
}
$is_https = Net\is_connection_https();
@ -119,14 +117,14 @@ function setCookies() {
time() + $config['cookies']['expire'], $config['cookies']['jail'] ? $config['cookies']['path'] : '/', null, $is_https, $config['cookies']['httponly']);
}
function destroyCookies() {
function destroyCookies(): void {
global $config;
$is_https = Net\is_connection_https();
// Delete the cookies
setcookie($config['cookies']['mod'], 'deleted', time() - $config['cookies']['expire'], $config['cookies']['jail']?$config['cookies']['path'] : '/', null, $is_https, true);
}
function modLog($action, $_board=null) {
function modLog(string $action, ?string $_board = null): void {
global $mod, $board, $config;
$query = prepare("INSERT INTO ``modlogs`` VALUES (:id, :ip, :board, :time, :text)");
$query->bindValue(':id', (isset($mod['id']) ? $mod['id'] : -1), PDO::PARAM_INT);
@ -141,16 +139,18 @@ function modLog($action, $_board=null) {
$query->bindValue(':board', null, PDO::PARAM_NULL);
$query->execute() or error(db_error($query));
if ($config['syslog'])
if ($config['syslog']) {
_syslog(LOG_INFO, '[mod/' . $mod['username'] . ']: ' . $action);
}
}
function create_pm_header() {
function create_pm_header(): mixed {
global $mod, $config;
if ($config['cache']['enabled'] && ($header = cache::get('pm_unread_' . $mod['id'])) != false) {
if ($header === true)
if ($header === true) {
return false;
}
return $header;
}
@ -159,26 +159,29 @@ function create_pm_header() {
$query->bindValue(':id', $mod['id'], PDO::PARAM_INT);
$query->execute() or error(db_error($query));
if ($pm = $query->fetch(PDO::FETCH_ASSOC))
$header = array('id' => $pm['id'], 'waiting' => $query->rowCount() - 1);
else
if ($pm = $query->fetch(PDO::FETCH_ASSOC)) {
$header = [ 'id' => $pm['id'], 'waiting' => $query->rowCount() - 1 ];
} else {
$header = true;
}
if ($config['cache']['enabled'])
if ($config['cache']['enabled']) {
cache::set('pm_unread_' . $mod['id'], $header);
}
if ($header === true)
if ($header === true) {
return false;
}
return $header;
}
function make_secure_link_token($uri) {
function make_secure_link_token(string $uri): string {
global $mod, $config;
return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8);
}
function check_login(Context $ctx, $prompt = false) {
function check_login(Context $ctx, bool $prompt = false): void {
global $config, $mod;
// Validate session
@ -188,7 +191,9 @@ function check_login(Context $ctx, $prompt = false) {
if (count($cookie) != 3) {
// Malformed cookies
destroyCookies();
if ($prompt) mod_login($ctx);
if ($prompt) {
mod_login($ctx);
}
exit;
}
@ -201,7 +206,9 @@ function check_login(Context $ctx, $prompt = false) {
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
// Malformed cookies
destroyCookies();
if ($prompt) mod_login($ctx);
if ($prompt) {
mod_login($ctx);
}
exit;
}

View file

@ -4,16 +4,19 @@
*/
use Vichan\Context;
use Vichan\Data\{IpNoteQueries, UserPostQueries, ReportQueries};
use Vichan\Data\Driver\LogDriver;
use Vichan\Functions\Net;
defined('TINYBOARD') or exit;
function _link_or_copy(string $target, string $link): bool {
if (!link($target, $link)) {
error_log("Failed to link() $target to $link. FAlling back to copy()");
return copy($target, $link);
}
return true;
function _link_or_copy_factory(Context $ctx): callable {
return function(string $target, string $link) use ($ctx) {
if (!\link($target, $link)) {
$ctx->get(LogDriver::class)->log(LogDriver::NOTICE, "Failed to link() $target to $link. FAlling back to copy()");
return \copy($target, $link);
}
return true;
};
}
function mod_page($title, $template, $args, $subtitle = false) {
@ -43,7 +46,7 @@ function clone_wrapped_with_exist_check($clonefn, $src, $dest) {
}
function mod_login(Context $ctx, $redirect = false) {
global $config;
$config = $ctx->get('config');
$args = [];
@ -54,8 +57,7 @@ function mod_login(Context $ctx, $redirect = false) {
if (!isset($_POST['username'], $_POST['password']) || $_POST['username'] == '' || $_POST['password'] == '') {
$args['error'] = $config['error']['invalid'];
} elseif (!login($_POST['username'], $_POST['password'])) {
if ($config['syslog'])
_syslog(LOG_WARNING, 'Unauthorized login attempt!');
$ctx->get(LogDriver::class)->log(LogDriver::INFO, 'Unauthorized login attempt!');
$args['error'] = $config['error']['invalid'];
} else {
@ -87,15 +89,16 @@ function mod_confirm(Context $ctx, $request) {
}
function mod_logout(Context $ctx) {
global $config;
$config = $ctx->get('config');
destroyCookies();
header('Location: ?/', true, $config['redirect_http']);
}
function mod_dashboard(Context $ctx) {
global $config, $mod;
global $mod;
$config = $ctx->get('config');
$report_queries = $ctx->get(ReportQueries::class);
$args = [];
@ -189,7 +192,7 @@ function mod_dashboard(Context $ctx) {
}
function mod_search_redirect(Context $ctx) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['search']))
error($config['error']['noaccess']);
@ -469,7 +472,9 @@ function mod_edit_board(Context $ctx, $boardName) {
}
function mod_new_board(Context $ctx) {
global $config, $board;
global $board;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['newboard']))
error($config['error']['noaccess']);
@ -535,7 +540,9 @@ function mod_new_board(Context $ctx) {
}
function mod_noticeboard(Context $ctx, $page_no = 1) {
global $config, $pdo, $mod;
global $pdo, $mod;
$config = $ctx->get('config');
if ($page_no < 1)
error($config['error']['404']);
@ -590,7 +597,7 @@ function mod_noticeboard(Context $ctx, $page_no = 1) {
}
function mod_noticeboard_delete(Context $ctx, $id) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['noticeboard_delete']))
error($config['error']['noaccess']);
@ -608,7 +615,9 @@ function mod_noticeboard_delete(Context $ctx, $id) {
}
function mod_news(Context $ctx, $page_no = 1) {
global $config, $pdo, $mod;
global $pdo, $mod;
$config = $ctx->get('config');
if ($page_no < 1)
error($config['error']['404']);
@ -655,7 +664,7 @@ function mod_news(Context $ctx, $page_no = 1) {
}
function mod_news_delete(Context $ctx, $id) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['news_delete']))
error($config['error']['noaccess']);
@ -670,7 +679,7 @@ function mod_news_delete(Context $ctx, $id) {
}
function mod_log(Context $ctx, $page_no = 1) {
global $config;
$config = $ctx->get('config');
if ($page_no < 1)
error($config['error']['404']);
@ -695,7 +704,7 @@ function mod_log(Context $ctx, $page_no = 1) {
}
function mod_user_log(Context $ctx, $username, $page_no = 1) {
global $config;
$config = $ctx->get('config');
if ($page_no < 1)
error($config['error']['404']);
@ -732,7 +741,7 @@ function protect_ip($entry) {
}
function mod_board_log(Context $ctx, $board, $page_no = 1, $hide_names = false, $public = false) {
global $config;
$config = $ctx->get('config');
if ($page_no < 1)
error($config['error']['404']);
@ -800,7 +809,9 @@ function mod_view_catalog(Context $ctx, $boardName) {
}
function mod_view_board(Context $ctx, $boardName, $page_no = 1) {
global $config, $mod;
global $mod;
$config = $ctx->get('config');
if (!openBoard($boardName)){
require "templates/themes/overboards/overboards.php";
@ -831,20 +842,24 @@ function mod_view_board(Context $ctx, $boardName, $page_no = 1) {
}
function mod_view_thread(Context $ctx, $boardName, $thread) {
global $config, $mod;
global $mod;
if (!openBoard($boardName))
if (!openBoard($boardName)) {
$config = $ctx->get('config');
error($config['error']['noboard']);
}
$page = buildThread($thread, true, $mod);
echo $page;
}
function mod_view_thread50(Context $ctx, $boardName, $thread) {
global $config, $mod;
global $mod;
if (!openBoard($boardName))
if (!openBoard($boardName)) {
$config = $ctx->get('config');
error($config['error']['noboard']);
}
$page = buildThread50($thread, true, $mod);
echo $page;
@ -1093,7 +1108,7 @@ function mod_user_posts_by_passwd(Context $ctx, string $passwd, string $encoded_
}
function mod_ban(Context $ctx) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['ban']))
error($config['error']['noaccess']);
@ -1114,7 +1129,7 @@ function mod_ban(Context $ctx) {
}
function mod_warning(Context $ctx) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['warning']))
error($config['error']['noaccess']);
@ -1131,9 +1146,10 @@ function mod_warning(Context $ctx) {
}
function mod_bans(Context $ctx) {
global $config;
global $mod;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['view_banlist']))
error($config['error']['noaccess']);
@ -1166,7 +1182,9 @@ function mod_bans(Context $ctx) {
}
function mod_bans_json(Context $ctx) {
global $config, $mod;
global $mod;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['ban']))
error($config['error']['noaccess']);
@ -1178,7 +1196,9 @@ function mod_bans_json(Context $ctx) {
}
function mod_ban_appeals(Context $ctx) {
global $config, $board;
global $board;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['view_ban_appeals']))
error($config['error']['noaccess']);
@ -1260,7 +1280,7 @@ function mod_ban_appeals(Context $ctx) {
}
function mod_lock(Context $ctx, $board, $unlock, $post) {
global $config;
$config = $ctx->get('config');
if (!openBoard($board))
error($config['error']['noboard']);
@ -1296,7 +1316,7 @@ function mod_lock(Context $ctx, $board, $unlock, $post) {
}
function mod_sticky(Context $ctx, $board, $unsticky, $post) {
global $config;
$config = $ctx->get('config');
if (!openBoard($board))
error($config['error']['noboard']);
@ -1320,7 +1340,7 @@ function mod_sticky(Context $ctx, $board, $unsticky, $post) {
}
function mod_cycle(Context $ctx, $board, $uncycle, $post) {
global $config;
$config = $ctx->get('config');
if (!openBoard($board))
error($config['error']['noboard']);
@ -1342,7 +1362,7 @@ function mod_cycle(Context $ctx, $board, $uncycle, $post) {
}
function mod_bumplock(Context $ctx, $board, $unbumplock, $post) {
global $config;
$config = $ctx->get('config');
if (!openBoard($board))
error($config['error']['noboard']);
@ -1489,8 +1509,9 @@ function mod_move(Context $ctx, $originBoard, $postID) {
if ($targetBoard === $originBoard)
error(_('Target and source board are the same.'));
$_link_or_copy = _link_or_copy_factory($ctx);
// link() if leaving a shadow thread behind; else, rename().
$clone = $shadow ? '_link_or_copy' : 'rename';
$clone = $shadow ? $_link_or_copy : 'rename';
// indicate that the post is a thread
$post['op'] = true;
@ -1784,7 +1805,8 @@ function mod_merge(Context $ctx, $originBoard, $postID) {
$op = $post;
$op['id'] = $newID;
$clone = $shadow ? '_link_or_copy' : 'rename';
$_link_or_copy = _link_or_copy_factory($ctx);
$clone = $shadow ? $_link_or_copy : 'rename';
if ($post['has_file']) {
// copy image
@ -1923,7 +1945,9 @@ function mod_merge(Context $ctx, $originBoard, $postID) {
}
function mod_ban_post(Context $ctx, $board, $delete, $post, $token = false) {
global $config, $mod;
global $mod;
$config = $ctx->get('config');
if (!openBoard($board))
error($config['error']['noboard']);
@ -2042,7 +2066,9 @@ function mod_ban_post(Context $ctx, $board, $delete, $post, $token = false) {
}
function mod_warning_post(Context $ctx, $board, $post, $token = false) {
global $config, $mod;
global $mod;
$config = $ctx->get('config');
if (!openBoard($board))
error($config['error']['noboard']);
@ -2139,7 +2165,7 @@ function mod_warning_post(Context $ctx, $board, $post, $token = false) {
}
function mod_edit_post(Context $ctx, $board, $edit_raw_html, $postID) {
global $config;
$config = $ctx->get('config');
if (!openBoard($board))
error($config['error']['noboard']);
@ -2216,7 +2242,9 @@ function mod_edit_post(Context $ctx, $board, $edit_raw_html, $postID) {
}
function mod_delete(Context $ctx, $board, $post) {
global $config, $mod;
global $mod;
$config = $ctx->get('config');
if (!openBoard($board))
error($config['error']['noboard']);
@ -2279,7 +2307,7 @@ function mod_delete(Context $ctx, $board, $post) {
}
function mod_deletefile(Context $ctx, $board, $post, $file) {
global $config;
$config = $ctx->get('config');
if (!openBoard($board))
error($config['error']['noboard']);
@ -2302,7 +2330,7 @@ function mod_deletefile(Context $ctx, $board, $post, $file) {
}
function mod_spoiler_image(Context $ctx, $board, $post, $file) {
global $config;
$config = $ctx->get('config');
if (!openBoard($board))
error($config['error']['noboard']);
@ -2347,8 +2375,9 @@ function mod_spoiler_image(Context $ctx, $board, $post, $file) {
}
function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) {
global $config, $board, $mod;
global $board, $mod;
$config = $ctx->get('config');
$global = (bool)$global;
if (!openBoard($boardName))
@ -2466,7 +2495,9 @@ function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) {
}
function mod_user(Context $ctx, $uid) {
global $config, $mod;
global $mod;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['editusers']) && !(hasPermission($config['mod']['change_password']) && $uid == $mod['id']))
error($config['error']['noaccess']);
@ -2644,7 +2675,7 @@ function mod_user_new(Context $ctx) {
function mod_users(Context $ctx) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['manageusers']))
error($config['error']['noaccess']);
@ -2665,7 +2696,7 @@ function mod_users(Context $ctx) {
}
function mod_user_promote(Context $ctx, $uid, $action) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['promoteusers']))
error($config['error']['noaccess']);
@ -2763,7 +2794,7 @@ function mod_pm(Context $ctx, $id, $reply = false) {
}
function mod_inbox(Context $ctx) {
global $config, $mod;
global $mod;
$query = prepare('SELECT `unread`,``pms``.`id`, `time`, `sender`, `to`, `message`, `username` FROM ``pms`` LEFT JOIN ``mods`` ON ``mods``.`id` = `sender` WHERE `to` = :mod ORDER BY `unread` DESC, `time` DESC');
$query->bindValue(':mod', $mod['id']);
@ -2787,7 +2818,9 @@ function mod_inbox(Context $ctx) {
function mod_new_pm(Context $ctx, $username) {
global $config, $mod;
global $mod;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['create_pm']))
error($config['error']['noaccess']);
@ -2835,7 +2868,9 @@ function mod_new_pm(Context $ctx, $username) {
}
function mod_rebuild(Context $ctx) {
global $config, $twig;
global $twig;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['rebuild']))
error($config['error']['noaccess']);
@ -2907,7 +2942,9 @@ function mod_rebuild(Context $ctx) {
}
function mod_reports(Context $ctx) {
global $config, $mod;
global $mod;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['reports']))
error($config['error']['noaccess']);
@ -2974,7 +3011,7 @@ function mod_reports(Context $ctx) {
}
function mod_report_dismiss(Context $ctx, $id, $all = false) {
global $config;
$config = $ctx->get('config');
$report_queries = $ctx->get(ReportQueries::class);
$report = $report_queries->getReportById($id);
@ -3006,13 +3043,27 @@ function mod_report_dismiss(Context $ctx, $id, $all = false) {
}
function mod_recent_posts(Context $ctx, $lim, $board_list = false, $json = false) {
global $config, $mod, $pdo;
global $mod, $pdo;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['recent']))
error($config['error']['noaccess']);
$limit = (is_numeric($lim))? $lim : 25;
$last_time = (isset($_GET['last']) && is_numeric($_GET['last'])) ? $_GET['last'] : 0;
$limit = 25;
if (\is_numeric($lim)) {
$lim = \intval($lim);
if ($lim > 0 && $lim < 1000) {
$limit = $lim;
}
}
$last_time = 0;
if (isset($_GET['last']) && \is_numeric($_GET['last'])) {
$last = \intval($_GET['last']);
if ($last > 0) {
$last_time = $last;
}
}
$mod_boards = [];
$boards = listBoards();
@ -3104,7 +3155,9 @@ function mod_recent_posts(Context $ctx, $lim, $board_list = false, $json = false
}
function mod_config(Context $ctx, $board_config = false) {
global $config, $mod, $board;
global $mod, $board;
$config = $ctx->get('config');
if ($board_config && !openBoard($board_config))
error($config['error']['noboard']);
@ -3244,7 +3297,7 @@ function mod_config(Context $ctx, $board_config = false) {
}
function mod_themes_list(Context $ctx) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['themes']))
error($config['error']['noaccess']);
@ -3278,7 +3331,7 @@ function mod_themes_list(Context $ctx) {
}
function mod_theme_configure(Context $ctx, $theme_name) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['themes']))
error($config['error']['noaccess']);
@ -3360,7 +3413,7 @@ function mod_theme_configure(Context $ctx, $theme_name) {
}
function mod_theme_uninstall(Context $ctx, $theme_name) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['themes']))
error($config['error']['noaccess']);
@ -3377,7 +3430,7 @@ function mod_theme_uninstall(Context $ctx, $theme_name) {
}
function mod_theme_rebuild(Context $ctx, $theme_name) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['themes']))
error($config['error']['noaccess']);
@ -3426,7 +3479,7 @@ function mod_delete_page_board(Context $ctx, $page = '', $board = false) {
}
function mod_edit_page(Context $ctx, $id) {
global $config, $mod, $board;
global $mod, $board;
$query = prepare('SELECT * FROM ``pages`` WHERE `id` = :id');
$query->bindValue(':id', $id);
@ -3436,6 +3489,8 @@ function mod_edit_page(Context $ctx, $id) {
if (!$page)
error(_('Could not find the page you are trying to edit.'));
$config = $ctx->get('config');
if (!$page['board'] && $mod['boards'][0] !== '*')
error($config['error']['noaccess']);
@ -3497,11 +3552,13 @@ function mod_edit_page(Context $ctx, $id) {
}
function mod_pages(Context $ctx, $board = false) {
global $config, $mod, $pdo;
global $mod, $pdo;
if (empty($board))
$board = false;
$config = $ctx->get('config');
if (!$board && $mod['boards'][0] !== '*')
error($config['error']['noaccess']);
@ -3622,7 +3679,7 @@ function mod_debug_recent_posts(Context $ctx) {
}
function mod_debug_sql(Context $ctx) {
global $config;
$config = $ctx->get('config');
if (!hasPermission($config['mod']['debug_sql']))
error($config['error']['noaccess']);

View file

@ -1,152 +0,0 @@
<?php
/*
* Copyright (c) 2016 vichan-devel
*/
defined('TINYBOARD') or exit;
function gen_msgid($board, $id) {
global $config;
$b = preg_replace("/[^0-9a-zA-Z$]/", 'x', $board);
$salt = sha1($board . "|" . $id . "|" . $config['nntpchan']['salt']);
$salt = substr($salt, 0, 7);
$salt = base_convert($salt, 16, 36);
return "<$b.$id.$salt@".$config['nntpchan']['domain'].">";
}
function gen_nntp($headers, $files) {
if (count($files) == 0) {
}
else if (count($files) == 1 && $files[0]['type'] == 'text/plain') {
$content = $files[0]['text'] . "\r\n";
$headers['Content-Type'] = "text/plain; charset=UTF-8";
}
else {
$boundary = sha1($headers['Message-Id']);
$content = "";
$headers['Content-Type'] = "multipart/mixed; boundary=$boundary";
foreach ($files as $file) {
$content .= "--$boundary\r\n";
if (isset($file['name'])) {
$file['name'] = preg_replace('/[\r\n\0"]/', '', $file['name']);
$content .= "Content-Disposition: form-data; filename=\"$file[name]\"; name=\"attachment\"\r\n";
}
$type = explode('/', $file['type'])[0];
if ($type == 'text') {
$file['type'] .= '; charset=UTF-8';
}
$content .= "Content-Type: $file[type]\r\n";
if ($type != 'text' && $type != 'message') {
$file['text'] = base64_encode($file['text']);
$content .= "Content-Transfer-Encoding: base64\r\n";
}
$content .= "\r\n";
$content .= $file['text'];
$content .= "\r\n";
}
$content .= "--$boundary--\r\n";
$headers['Mime-Version'] = '1.0';
}
//$headers['Content-Length'] = strlen($content);
$headers['Date'] = date('r', $headers['Date']);
$out = "";
foreach ($headers as $id => $val) {
$val = str_replace("\n", "\n\t", $val);
$out .= "$id: $val\r\n";
}
$out .= "\r\n";
$out .= $content;
return $out;
}
function nntp_publish($msg, $id) {
global $config;
$server = $config["nntpchan"]["server"];
$s = fsockopen("tcp://$server");
fgets($s);
fputs($s, "MODE STREAM\r\n");
fgets($s);
fputs($s, "TAKETHIS $id\r\n");
fputs($s, $msg);
fputs($s, "\r\n.\r\n");
fgets($s);
fputs($s, "QUIT\r\n");
fclose($s);
}
function post2nntp($post, $msgid) {
global $config;
$headers = array();
$files = array();
$headers['Message-Id'] = $msgid;
$headers['Newsgroups'] = $config['nntpchan']['group'];
$headers['Date'] = time();
$headers['Subject'] = $post['subject'] ? $post['subject'] : "None";
$headers['From'] = $post['name'] . " <poster@" . $config['nntpchan']['domain'] . ">";
if ($post['email'] == 'sage') {
$headers['X-Sage'] = true;
}
if (!$post['op']) {
// Get muh parent
$query = prepare("SELECT `message_id` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id");
$query->bindValue(':board', $post['board']);
$query->bindValue(':id', $post['thread']);
$query->execute() or error(db_error($query));
if ($result = $query->fetch(PDO::FETCH_ASSOC)) {
$headers['References'] = $result['message_id'];
}
else {
return false; // We don't have OP. Discarding.
}
}
// Let's parse the body a bit.
$body = trim($post['body_nomarkup']);
$body = preg_replace('/\r?\n/', "\r\n", $body);
$body = preg_replace_callback('@>>(>/([a-zA-Z0-9_+-]+)/)?([0-9]+)@', function($o) use ($post) {
if ($o[1]) {
$board = $o[2];
}
else {
$board = $post['board'];
}
$id = $o[3];
$query = prepare("SELECT `message_id_digest` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id");
$query->bindValue(':board', $board);
$query->bindValue(':id', $id);
$query->execute() or error(db_error($query));
if ($result = $query->fetch(PDO::FETCH_ASSOC)) {
return ">>".substr($result['message_id_digest'], 0, 18);
}
else {
return $o[0]; // Should send URL imo
}
}, $body);
$body = preg_replace('/>>>>([0-9a-fA-F])+/', '>>\1', $body);
$files[] = array('type' => 'text/plain', 'text' => $body);
foreach ($post['files'] as $id => $file) {
$fc = array();
$fc['type'] = $file['type'];
$fc['text'] = file_get_contents($file['file_path']);
$fc['name'] = $file['name'];
$files[] = $fc;
}
return array($headers, $files);
}

View file

@ -1,30 +0,0 @@
<?php
define('TINYBOARD', 'fuck yeah');
require_once('nntpchan.php');
die();
$time = time();
echo "\n@@@@ Thread:\n";
echo $m0 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.0000.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None"],
[['type' => 'text/plain', 'text' => "THIS IS A NEW TEST THREAD"]]);
echo "\n@@@@ Single msg:\n";
echo $m1 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.1234.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"],
[['type' => 'text/plain', 'text' => "hello world, with no image :("]]);
echo "\n@@@@ Single msg and pseudoimage:\n";
echo $m2 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.2137.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"],
[['type' => 'text/plain', 'text' => "hello world, now with an image!"],
['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"]]);
echo "\n@@@@ Single msg and two pseudoimages:\n";
echo $m3 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.1488.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"],
[['type' => 'text/plain', 'text' => "hello world, now WITH TWO IMAGES!!!"],
['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"],
['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif2.gif"]]);
shoveitup($m0, "<1234.0000.".$time."@example.vichan.net>");
sleep(1);
shoveitup($m1, "<1234.1234.".$time."@example.vichan.net>");
sleep(1);
shoveitup($m2, "<1234.2137.".$time."@example.vichan.net>");
shoveitup($m3, "<1234.1488.".$time."@example.vichan.net>");

View file

@ -104,8 +104,10 @@ function buildMenu(e) {
function addButton(post) {
var $ele = $(post);
// Use unicode code with ascii variant selector
// https://stackoverflow.com/questions/37906969/how-to-prevent-ios-from-converting-ascii-into-emoji
$ele.find('input.delete').after(
$('<a>', {href: '#', class: 'post-btn', title: 'Post menu'}).text('►')
$('<a>', {href: '#', class: 'post-btn', title: 'Post menu'}).text('\u{25B6}\u{fe0e}')
);
}

656
post.php
View file

@ -5,6 +5,7 @@
use Vichan\Context;
use Vichan\Data\ReportQueries;
use Vichan\Data\Driver\LogDriver;
require_once 'inc/bootstrap.php';
@ -352,166 +353,6 @@ function db_select_ban_appeals($ban_id)
* Method handling functions
*/
$dropped_post = false;
function handle_nntpchan()
{
global $config;
if ($_SERVER['REMOTE_ADDR'] != $config['nntpchan']['trusted_peer']) {
error("NNTPChan: Forbidden. $_SERVER[REMOTE_ADDR] is not a trusted peer");
}
$_POST = [];
$_POST['json_response'] = true;
$headers = json_encode($_GET);
if (!isset($_GET['Message-Id'])) {
if (!isset($_GET['Message-ID'])) {
error("NNTPChan: No message ID");
} else {
$msgid = $_GET['Message-ID'];
}
} else {
$msgid = $_GET['Message-Id'];
}
$groups = preg_split("/,\s*/", $_GET['Newsgroups']);
if (count($groups) != 1) {
error("NNTPChan: Messages can go to only one newsgroup");
}
$group = $groups[0];
if (!isset($config['nntpchan']['dispatch'][$group])) {
error("NNTPChan: We don't synchronize $group");
}
$xboard = $config['nntpchan']['dispatch'][$group];
$ref = null;
if (isset($_GET['References'])) {
$refs = preg_split("/,\s*/", $_GET['References']);
if (count($refs) > 1) {
error("NNTPChan: We don't support multiple references");
}
$ref = $refs[0];
$query = prepare("SELECT `board`,`id` FROM ``nntp_references`` WHERE `message_id` = :ref");
$query->bindValue(':ref', $ref);
$query->execute() or error(db_error($query));
$ary = $query->fetchAll(PDO::FETCH_ASSOC);
if (count($ary) == 0) {
error("NNTPChan: We don't have $ref that $msgid references");
}
$p_id = $ary[0]['id'];
$p_board = $ary[0]['board'];
if ($p_board != $xboard) {
error("NNTPChan: Cross board references not allowed. Tried to reference $p_board on $xboard");
}
$_POST['thread'] = $p_id;
}
$date = isset($_GET['Date']) ? strtotime($_GET['Date']) : time();
list($ct) = explode('; ', $_GET['Content-Type']);
$query = prepare("SELECT COUNT(*) AS `c` FROM ``nntp_references`` WHERE `message_id` = :msgid");
$query->bindValue(":msgid", $msgid);
$query->execute() or error(db_error($query));
$a = $query->fetch(PDO::FETCH_ASSOC);
if ($a['c'] > 0) {
error("NNTPChan: We already have this post. Post discarded.");
}
if ($ct == 'text/plain') {
$content = file_get_contents("php://input");
} elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') {
_syslog(LOG_INFO, "MM: Files: " . print_r($GLOBALS, true)); // Debug
$content = '';
$newfiles = [];
foreach ($_FILES['attachment']['error'] as $id => $error) {
if ($_FILES['attachment']['type'][$id] == 'text/plain') {
$content .= file_get_contents($_FILES['attachment']['tmp_name'][$id]);
} elseif ($_FILES['attachment']['type'][$id] == 'message/rfc822') {
// Signed message, ignore for now
} else {
// A real attachment :^)
$file = [];
$file['name'] = $_FILES['attachment']['name'][$id];
$file['type'] = $_FILES['attachment']['type'][$id];
$file['size'] = $_FILES['attachment']['size'][$id];
$file['tmp_name'] = $_FILES['attachment']['tmp_name'][$id];
$file['error'] = $_FILES['attachment']['error'][$id];
$newfiles["file$id"] = $file;
}
}
$_FILES = $newfiles;
} else {
error("NNTPChan: Wrong mime type: $ct");
}
$_POST['subject'] = isset($_GET['Subject']) ? ($_GET['Subject'] == 'None' ? '' : $_GET['Subject']) : '';
$_POST['board'] = $xboard;
if (isset($_GET['From'])) {
list($name, $mail) = explode(" <", $_GET['From'], 2);
$mail = preg_replace('/>\s+$/', '', $mail);
$_POST['name'] = $name;
//$_POST['email'] = $mail;
$_POST['email'] = '';
}
if (isset($_GET['X_Sage'])) {
$_POST['email'] = 'sage';
}
$content = preg_replace_callback('/>>([0-9a-fA-F]{6,})/', function ($id) use ($xboard) {
$id = $id[1];
$query = prepare("SELECT `board`,`id` FROM ``nntp_references`` WHERE `message_id_digest` LIKE :rule");
$idx = $id . "%";
$query->bindValue(':rule', $idx);
$query->execute() or error(db_error($query));
$ary = $query->fetchAll(PDO::FETCH_ASSOC);
if (count($ary) == 0) {
return ">>>>$id";
} else {
$ret = [];
foreach ($ary as $v) {
if ($v['board'] != $xboard) {
$ret[] = ">>>/" . $v['board'] . "/" . $v['id'];
} else {
$ret[] = ">>" . $v['id'];
}
}
return implode($ret, ", ");
}
}, $content);
$_POST['body'] = $content;
$dropped_post = array(
'date' => $date,
'board' => $xboard,
'msgid' => $msgid,
'headers' => $headers,
'from_nntp' => true,
);
}
function handle_delete(Context $ctx)
{
// Delete
@ -610,8 +451,8 @@ function handle_delete(Context $ctx)
modLog("User at $ip deleted his own post #$id");
}
_syslog(
LOG_INFO,
$ctx->get(LogDriver::class)->log(
LogDriver::INFO,
'Deleted post: ' .
'/' . $board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $post['thread'] ? $post['thread'] : $id) . ($post['thread'] ? '#' . $id : '')
);
@ -699,9 +540,7 @@ function handle_report(Context $ctx)
foreach ($report as $id) {
$post = db_select_post_minimal($board['uri'], $id);
if ($post === false) {
if ($config['syslog']) {
_syslog(LOG_INFO, "Failed to report non-existing post #{$id} in {$board['dir']}");
}
$ctx->get(LogDriver::class)->log(LogDriver::INFO, "Failed to report non-existing post #{$id} in {$board['dir']}");
error($config['error']['nopost']);
}
@ -716,13 +555,12 @@ function handle_report(Context $ctx)
error($error);
}
if ($config['syslog'])
_syslog(
LOG_INFO,
'Reported post: ' .
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
' for "' . $reason . '"'
);
$ctx->get(LogDriver::class)->log(
LogDriver::INFO,
'Reported post: ' .
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
' for "' . $reason . '"'
);
$report_queries->add($_SERVER['REMOTE_ADDR'], $board['uri'], $id, $reason);
@ -791,9 +629,9 @@ function handle_report(Context $ctx)
function handle_post(Context $ctx)
{
global $config, $dropped_post, $board, $mod, $pdo;
global $config, $board, $mod, $pdo;
if (!isset($_POST['body'], $_POST['board']) && !$dropped_post) {
if (!isset($_POST['body'], $_POST['board'])) {
error($config['error']['bot']);
}
@ -836,108 +674,104 @@ function handle_post(Context $ctx)
}
if (!$dropped_post) {
// Check for CAPTCHA right after opening the board so the "return" link is in there.
if ($config['captcha']['mode'] !== false) {
if (!isset($_POST['captcha-response'], $_POST['captcha-form-id'])) {
error($config['error']['bot']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
$ret = check_captcha($config['captcha'], $_POST['captcha-form-id'], $post['board'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $expected_action);
if (!$ret) {
error($config['error']['captcha']);
}
}
if (isset($config['simple_spam']) && $post['op']) {
if (!isset($_POST['simple_spam']) || $config['simple_spam']['answer'] != $_POST['simple_spam']) {
error($config['error']['simple_spam']);
}
}
if (isset($config['securimage']) && $config['securimage']) {
if (!isset($_POST['captcha'])) {
error($config['error']['securimage']['missing']);
}
if (empty($_POST['captcha'])) {
error($config['error']['securimage']['empty']);
}
if (!db_delete_captcha($_SERVER['REMOTE_ADDR'], $_POST['captcha'])) {
error($config['error']['securimage']['bad']);
}
}
if (
!(($post['op'] && $_POST['post'] == $config['button_newtopic']) ||
(!$post['op'] && $_POST['post'] == $config['button_reply']))
) {
// Check for CAPTCHA right after opening the board so the "return" link is in there.
if ($config['captcha']['mode'] !== false) {
if (!isset($_POST['captcha-response'], $_POST['captcha-form-id'])) {
error($config['error']['bot']);
}
// Check the referrer
if (
$config['referer_match'] !== false &&
(!isset($_SERVER['HTTP_REFERER']) || !preg_match($config['referer_match'], rawurldecode($_SERVER['HTTP_REFERER'])))
) {
error($config['error']['referer']);
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
$ret = check_captcha($config['captcha'], $_POST['captcha-form-id'], $post['board'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $expected_action);
if (!$ret) {
error($config['error']['captcha']);
}
}
if (isset($config['simple_spam']) && $post['op']) {
if (!isset($_POST['simple_spam']) || $config['simple_spam']['answer'] != $_POST['simple_spam']) {
error($config['error']['simple_spam']);
}
}
if (isset($config['securimage']) && $config['securimage']) {
if (!isset($_POST['captcha'])) {
error($config['error']['securimage']['missing']);
}
checkDNSBL();
// Check if banned
checkBan($board['uri']);
if ($config['op_require_history'] && $post['op'] && !isIPv6()) {
$has_any = has_any_history($_SERVER['REMOTE_ADDR'], $_POST['password']);
if (!$has_any) {
error($config['error']['opnohistory']);
}
if (empty($_POST['captcha'])) {
error($config['error']['securimage']['empty']);
}
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
check_login($ctx, false);
if (!$mod) {
// Liar. You're not a mod >:-[
error($config['error']['notamod']);
}
if (!db_delete_captcha($_SERVER['REMOTE_ADDR'], $_POST['captcha'])) {
error($config['error']['securimage']['bad']);
}
}
$post['sticky'] = $post['op'] && isset($_POST['sticky']);
$post['locked'] = $post['op'] && isset($_POST['lock']);
$post['raw'] = isset($_POST['raw']);
if (
!(($post['op'] && $_POST['post'] == $config['button_newtopic']) ||
(!$post['op'] && $_POST['post'] == $config['button_reply']))
) {
error($config['error']['bot']);
}
if ($post['sticky'] && !hasPermission($config['mod']['sticky'], $board['uri'])) {
error($config['error']['noaccess']);
}
if ($post['locked'] && !hasPermission($config['mod']['lock'], $board['uri'])) {
error($config['error']['noaccess']);
}
if ($post['raw'] && !hasPermission($config['mod']['rawhtml'], $board['uri'])) {
error($config['error']['noaccess']);
}
// Check the referrer
if (
$config['referer_match'] !== false &&
(!isset($_SERVER['HTTP_REFERER']) || !preg_match($config['referer_match'], rawurldecode($_SERVER['HTTP_REFERER'])))
) {
error($config['error']['referer']);
}
checkDNSBL();
// Check if banned
checkBan($board['uri']);
if ($config['op_require_history'] && $post['op'] && !isIPv6()) {
$has_any = has_any_history($_SERVER['REMOTE_ADDR'], $_POST['password']);
if (!$has_any) {
error($config['error']['opnohistory']);
}
}
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
check_login($ctx, false);
if (!$mod) {
// Liar. You're not a mod >:-[
error($config['error']['notamod']);
}
if (!$post['mod'] && $config['spam']['enabled'] == true) {
$post['antispam_hash'] = checkSpam(
array(
$board['uri'],
isset($post['thread']) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int) $_POST['page'] : null)
)
);
//$post['antispam_hash'] = checkSpam();
$post['sticky'] = $post['op'] && isset($_POST['sticky']);
$post['locked'] = $post['op'] && isset($_POST['lock']);
$post['raw'] = isset($_POST['raw']);
if ($post['antispam_hash'] === true) {
error($config['error']['spam']);
}
if ($post['sticky'] && !hasPermission($config['mod']['sticky'], $board['uri'])) {
error($config['error']['noaccess']);
}
if ($post['locked'] && !hasPermission($config['mod']['lock'], $board['uri'])) {
error($config['error']['noaccess']);
}
if ($post['raw'] && !hasPermission($config['mod']['rawhtml'], $board['uri'])) {
error($config['error']['noaccess']);
}
}
if ($config['robot_enable'] && $config['robot_mute']) {
checkMute();
if (!$post['mod'] && $config['spam']['enabled'] == true) {
$post['antispam_hash'] = checkSpam(
array(
$board['uri'],
isset($post['thread']) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int) $_POST['page'] : null)
)
);
//$post['antispam_hash'] = checkSpam();
if ($post['antispam_hash'] === true) {
error($config['error']['spam']);
}
} else {
$mod = $post['mod'] = false;
}
if ($config['robot_enable'] && $config['robot_mute']) {
checkMute();
}
// Check if thread exists.
@ -1023,40 +857,35 @@ function handle_post(Context $ctx)
$post['password'] = hashPassword($_POST['password']);
$post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || count($_FILES) > 0));
if (!$dropped_post) {
if (!($post['has_file'] || isset($post['embed'])) || (($post['op'] && $config['force_body_op']) || (!$post['op'] && $config['force_body']))) {
$stripped_whitespace = preg_replace('/[\s]/u', '', $post['body']);
if ($stripped_whitespace == '') {
error($config['error']['tooshort_body']);
}
}
if (!$post['op']) {
// Check if thread is locked
// but allow mods to post
if ($thread['locked'] && !hasPermission($config['mod']['postinlocked'], $board['uri'])) {
error($config['error']['locked']);
}
$numposts = numPosts($post['thread']);
$replythreshold = isset($thread['cycle']) && $thread['cycle'] ? $numposts['replies'] - 1 : $numposts['replies'];
$imagethreshold = isset($thread['cycle']) && $thread['cycle'] ? $numposts['images'] - 1 : $numposts['images'];
if ($config['reply_hard_limit'] != 0 && $config['reply_hard_limit'] <= $replythreshold) {
error($config['error']['reply_hard_limit']);
}
if ($post['has_file'] && $config['image_hard_limit'] != 0 && $config['image_hard_limit'] <= $imagethreshold) {
error($config['error']['image_hard_limit']);
}
}
} else {
if (!$post['op']) {
$numposts = numPosts($post['thread']);
if (!($post['has_file'] || isset($post['embed'])) || (($post['op'] && $config['force_body_op']) || (!$post['op'] && $config['force_body']))) {
$stripped_whitespace = preg_replace('/[\s]/u', '', $post['body']);
if ($stripped_whitespace == '') {
error($config['error']['tooshort_body']);
}
}
if (!$post['op']) {
// Check if thread is locked
// but allow mods to post
if ($thread['locked'] && !hasPermission($config['mod']['postinlocked'], $board['uri'])) {
error($config['error']['locked']);
}
$numposts = numPosts($post['thread']);
$replythreshold = isset($thread['cycle']) && $thread['cycle'] ? $numposts['replies'] - 1 : $numposts['replies'];
$imagethreshold = isset($thread['cycle']) && $thread['cycle'] ? $numposts['images'] - 1 : $numposts['images'];
if ($config['reply_hard_limit'] != 0 && $config['reply_hard_limit'] <= $replythreshold) {
error($config['error']['reply_hard_limit']);
}
if ($post['has_file'] && $config['image_hard_limit'] != 0 && $config['image_hard_limit'] <= $imagethreshold) {
error($config['error']['image_hard_limit']);
}
}
if ($post['has_file']) {
// Determine size sanity
$size = 0;
@ -1162,18 +991,16 @@ function handle_post(Context $ctx)
$post['has_file'] = false;
}
if (!$dropped_post) {
// Check for a file
if ($post['op'] && !isset($post['no_longer_require_an_image_for_op'])) {
if (!$post['has_file'] && $config['force_image_op']) {
error($config['error']['noimage']);
}
// Check for a file
if ($post['op'] && !isset($post['no_longer_require_an_image_for_op'])) {
if (!$post['has_file'] && $config['force_image_op']) {
error($config['error']['noimage']);
}
}
// Check for too many files
if (sizeof($post['files']) > $config['max_images']) {
error($config['error']['toomanyimages']);
}
// Check for too many files
if (sizeof($post['files']) > $config['max_images']) {
error($config['error']['toomanyimages']);
}
if ($config['strip_combining_chars']) {
@ -1183,33 +1010,31 @@ function handle_post(Context $ctx)
$post['body'] = strip_combining_chars($post['body']);
}
if (!$dropped_post) {
// Check string lengths
if (mb_strlen($post['name']) > 35) {
error(sprintf($config['error']['toolong'], 'name'));
}
if (mb_strlen($post['email']) > 40) {
error(sprintf($config['error']['toolong'], 'email'));
}
if (mb_strlen($post['subject']) > 100) {
error(sprintf($config['error']['toolong'], 'subject'));
}
if (!$mod) {
$body_mb_len = mb_strlen($post['body']);
$is_op = $post['op'];
// Check string lengths
if (mb_strlen($post['name']) > 35) {
error(sprintf($config['error']['toolong'], 'name'));
}
if (mb_strlen($post['email']) > 40) {
error(sprintf($config['error']['toolong'], 'email'));
}
if (mb_strlen($post['subject']) > 100) {
error(sprintf($config['error']['toolong'], 'subject'));
}
if (!$mod) {
$body_mb_len = mb_strlen($post['body']);
$is_op = $post['op'];
if (($is_op && $config['force_body_op']) || (!$is_op && $config['force_body'])) {
$min_body = $is_op ? $config['min_body_op'] : $config['min_body'];
if (($is_op && $config['force_body_op']) || (!$is_op && $config['force_body'])) {
$min_body = $is_op ? $config['min_body_op'] : $config['min_body'];
if ($body_mb_len < $min_body) {
error($config['error']['tooshort_body']);
}
if ($body_mb_len < $min_body) {
error($config['error']['tooshort_body']);
}
}
$max_body = $is_op ? $config['max_body_op'] : $config['max_body'];
if ($body_mb_len > $max_body) {
error($config['error']['toolong_body']);
}
$max_body = $is_op ? $config['max_body_op'] : $config['max_body'];
if ($body_mb_len > $max_body) {
error($config['error']['toolong_body']);
}
}
@ -1221,33 +1046,32 @@ function handle_post(Context $ctx)
$post['body'] .= "\n<tinyboard raw html>1</tinyboard>";
}
if (!$dropped_post)
if (($config['country_flags'] && !$config['allow_no_country']) || ($config['country_flags'] && $config['allow_no_country'] && !isset($_POST['no_country']))) {
$gi = geoip_open('inc/lib/geoip/GeoIPv6.dat', GEOIP_STANDARD);
if (($config['country_flags'] && !$config['allow_no_country']) || ($config['country_flags'] && $config['allow_no_country'] && !isset($_POST['no_country']))) {
$gi = geoip_open('inc/lib/geoip/GeoIPv6.dat', GEOIP_STANDARD);
function ipv4to6($ip)
{
if (strpos($ip, ':') !== false) {
if (strpos($ip, '.') > 0) {
$ip = substr($ip, strrpos($ip, ':') + 1);
} else {
// Native ipv6.
return $ip;
}
function ipv4to6($ip)
{
if (strpos($ip, ':') !== false) {
if (strpos($ip, '.') > 0) {
$ip = substr($ip, strrpos($ip, ':') + 1);
} else {
// Native ipv6.
return $ip;
}
$iparr = array_pad(explode('.', $ip), 4, 0);
$part7 = base_convert(($iparr[0] * 256) + $iparr[1], 10, 16);
$part8 = base_convert(($iparr[2] * 256) + $iparr[3], 10, 16);
return '::ffff:' . $part7 . ':' . $part8;
}
$iparr = array_pad(explode('.', $ip), 4, 0);
$part7 = base_convert(($iparr[0] * 256) + $iparr[1], 10, 16);
$part8 = base_convert(($iparr[2] * 256) + $iparr[3], 10, 16);
return '::ffff:' . $part7 . ':' . $part8;
}
if ($country_code = geoip_country_code_by_addr_v6($gi, ipv4to6($_SERVER['REMOTE_ADDR']))) {
if (!in_array(strtolower($country_code), array('eu', 'ap', 'o1', 'a1', 'a2'))) {
$post['body'] .= "\n<tinyboard flag>" . strtolower($country_code) . "</tinyboard>" .
"\n<tinyboard flag alt>" . geoip_country_name_by_addr_v6($gi, ipv4to6($_SERVER['REMOTE_ADDR'])) . "</tinyboard>";
}
if ($country_code = geoip_country_code_by_addr_v6($gi, ipv4to6($_SERVER['REMOTE_ADDR']))) {
if (!in_array(strtolower($country_code), array('eu', 'ap', 'o1', 'a1', 'a2'))) {
$post['body'] .= "\n<tinyboard flag>" . strtolower($country_code) . "</tinyboard>" .
"\n<tinyboard flag alt>" . geoip_country_name_by_addr_v6($gi, ipv4to6($_SERVER['REMOTE_ADDR'])) . "</tinyboard>";
}
}
}
if ($config['user_flag'] && isset($_POST['user_flag']))
if (!empty($_POST['user_flag'])) {
@ -1267,11 +1091,9 @@ function handle_post(Context $ctx)
$post['body'] .= "\n<tinyboard tag>" . $_POST['tag'] . "</tinyboard>";
}
if (!$dropped_post) {
if ($config['proxy_save'] && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$proxy = preg_replace("/[^0-9a-fA-F.,: ]/", '', $_SERVER['HTTP_X_FORWARDED_FOR']);
$post['body'] .= "\n<tinyboard proxy>" . $proxy . "</tinyboard>";
}
if ($config['proxy_save'] && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$proxy = preg_replace("/[^0-9a-fA-F.,: ]/", '', $_SERVER['HTTP_X_FORWARDED_FOR']);
$post['body'] .= "\n<tinyboard proxy>" . $proxy . "</tinyboard>";
}
$post['body_nomarkup'] = $post['body'];
@ -1313,7 +1135,7 @@ function handle_post(Context $ctx)
}
}
if (!hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) {
if (!hasPermission($config['mod']['bypass_filters'], $board['uri'])) {
require_once 'inc/filters.php';
do_filters($ctx, $post);
}
@ -1404,13 +1226,13 @@ function handle_post(Context $ctx)
$file['thumbwidth'] = $size[0];
$file['thumbheight'] = $size[1];
} elseif (
$config['minimum_copy_resize'] &&
(($config['strip_exif'] && isset($file['exif_stripped']) && $file['exif_stripped']) || !$config['strip_exif']) &&
$image->size->width <= $config['thumb_width'] &&
$image->size->height <= $config['thumb_height'] &&
$file['extension'] == ($config['thumb_ext'] ? $config['thumb_ext'] : $file['extension'])
) {
// Copy, because there's nothing to resize
coopy($file['tmp_name'], $file['thumb']);
copy($file['tmp_name'], $file['thumb']);
$file['thumbwidth'] = $image->size->width;
$file['thumbheight'] = $image->size->height;
@ -1449,17 +1271,35 @@ function handle_post(Context $ctx)
}
$image->destroy();
} else {
if (
($file['extension'] == "pdf" && $config['pdf_file_thumbnail']) ||
($file['extension'] == "djvu" && $config['djvu_file_thumbnail'])
) {
$path = $file['thumb'];
$error = shell_exec_error('convert -size ' . $config['thumb_width'] . 'x' . $config['thumb_height'] . ' -thumbnail ' . $config['thumb_width'] . 'x' . $config['thumb_height'] . ' -background white -alpha remove ' .
escapeshellarg($file['tmp_name'] . '[0]') . ' ' .
escapeshellarg($file['thumb']));
$mime = \mime_content_type($file['tmp_name']);
$pdf = $file['extension'] === "pdf" && $config['pdf_file_thumbnail'];
$djvu = $file['extension'] === "djvu" && $config['djvu_file_thumbnail'];
if ($pdf || $djvu) {
$e_thumb_path = \escapeshellarg($file['thumb']);
$e_file_path = \escapeshellarg($file['tmp_name']);
$thumb_width = $config['thumb_width'];
$thumb_height = $config['thumb_height'];
// Generates a PPM image and pipes it directly into convert for resizing + type conversion.
if ($pdf && $mime === 'application/pdf') {
$error = shell_exec_error("gs -dSAFER -dBATCH -dNOPAUSE -dQUIET \
-sDEVICE=ppmraw -r100 -dFirstPage=1 -dLastPage=1 -sOutputFile=- $e_file_path \
| convert -thumbnail {$thumb_width}x{$thumb_height} ppm:- $e_thumb_path");
} elseif ($djvu && $mime === 'image/vnd.djvu') {
$error = shell_exec_error("ddjvu -format=ppm -page 1 $e_file_path \
| convert -thumbnail {$thumb_width}x{$thumb_height} ppm:- $e_thumb_path");
} else {
// Mime check failed.
error($config['error']['invalidfile']);
}
if ($error) {
$path = sprintf($config['file_thumb'], isset($config['file_icons'][$file['extension']]) ? $config['file_icons'][$file['extension']] : $config['file_icons']['default']);
$log = $ctx->get(LogDriver::class);
$log->log(LogDriver::ERROR, 'Could not render thumbnail for PDF/DJVU file, using static fallback.');
$path = \sprintf($config['file_thumb'], isset($config['file_icons'][$file['extension']]) ? $config['file_icons'][$file['extension']] : $config['file_icons']['default']);
} else {
$path = $file['thumb'];
}
$file['thumb'] = basename($file['thumb']);
@ -1553,35 +1393,6 @@ function handle_post(Context $ctx)
}
}
if ($config['tesseract_ocr'] && $file['thumb'] != 'file') {
// Let's OCR it!
$fname = $file['tmp_name'];
if ($file['height'] > 500 || $file['width'] > 500) {
$fname = $file['thumb'];
}
if ($fname == 'spoiler') {
// We don't have that much CPU time, do we?
} else {
$tmpname = __DIR__ . "/tmp/tesseract/" . rand(0, 10000000);
// Preprocess command is an ImageMagick b/w quantization
$error = shell_exec_error(sprintf($config['tesseract_preprocess_command'], escapeshellarg($fname)) . " | " .
'tesseract stdin ' . escapeshellarg($tmpname) . ' ' . $config['tesseract_params']);
$tmpname .= ".txt";
$value = @file_get_contents($tmpname);
@unlink($tmpname);
if ($value && trim($value)) {
// This one has an effect, that the body is appended to a post body. So you can write a correct
// spamfilter.
$post['body_nomarkup'] .= "<tinyboard ocr image $key>" . htmlspecialchars($value) . "</tinyboard>";
}
}
}
if (!isset($dont_copy_file) || !$dont_copy_file) {
if (isset($file['file_tmp'])) {
if (!@rename($file['tmp_name'], $file['file'])) {
@ -1629,12 +1440,7 @@ function handle_post(Context $ctx)
}
}
// Do filters again if OCRing
if ($config['tesseract_ocr'] && !hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) {
do_filters($ctx, $post);
}
if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup']) && !$dropped_post) {
if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup'])) {
undoImage($post);
if ($config['robot_mute']) {
error(sprintf($config['error']['muted'], mute()));
@ -1680,41 +1486,6 @@ function handle_post(Context $ctx)
$post['id'] = $id = post($post);
$post['slug'] = slugify($post);
if ($dropped_post && $dropped_post['from_nntp']) {
$query = prepare("INSERT INTO ``nntp_references`` (`board`, `id`, `message_id`, `message_id_digest`, `own`, `headers`) VALUES " .
"(:board , :id , :message_id , :message_id_digest , false, :headers)");
$query->bindValue(':board', $dropped_post['board']);
$query->bindValue(':id', $id);
$query->bindValue(':message_id', $dropped_post['msgid']);
$query->bindValue(':message_id_digest', sha1($dropped_post['msgid']));
$query->bindValue(':headers', $dropped_post['headers']);
$query->execute() or error(db_error($query));
} // ^^^^^ For inbound posts ^^^^^
elseif ($config['nntpchan']['enabled'] && $config['nntpchan']['group']) {
// vvvvv For outbound posts vvvvv
require_once('inc/nntpchan/nntpchan.php');
$msgid = gen_msgid($post['board'], $post['id']);
list($headers, $files) = post2nntp($post, $msgid);
$message = gen_nntp($headers, $files);
$query = prepare("INSERT INTO ``nntp_references`` (`board`, `id`, `message_id`, `message_id_digest`, `own`, `headers`) VALUES " .
"(:board , :id , :message_id , :message_id_digest , true , :headers)");
$query->bindValue(':board', $post['board']);
$query->bindValue(':id', $post['id']);
$query->bindValue(':message_id', $msgid);
$query->bindValue(':message_id_digest', sha1($msgid));
$query->bindValue(':headers', json_encode($headers));
$query->execute() or error(db_error($query));
// Let's broadcast it!
nntp_publish($message, $msgid);
}
insertFloodPost($post);
// Handle cyclical threads
@ -1783,10 +1554,10 @@ function handle_post(Context $ctx)
buildThread($post['op'] ? $id : $post['thread']);
if ($config['syslog']) {
_syslog(LOG_INFO, 'New post: /' . $board['dir'] . $config['dir']['res'] .
link_for($post) . (!$post['op'] ? '#' . $id : ''));
}
$ctx->get(LogDriver::class)->log(
LogDriver::INFO,
'New post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . (!$post['op'] ? '#' . $id : '')
);
if (!$post['mod']) {
header('X-Associated-Content: "' . $redirect . '"');
@ -1879,22 +1650,13 @@ function handle_appeal(Context $ctx)
displayBan($ban);
}
// Is it a post coming from NNTP? Let's extract it and pretend it's a normal post.
if (isset($_GET['Newsgroups'])) {
if ($config['nntpchan']['enabled']) {
handle_nntpchan();
} else {
error("NNTPChan: NNTPChan support is disabled");
}
}
$ctx = Vichan\build_context($config);
if (isset($_POST['delete'])) {
handle_delete($ctx);
} elseif (isset($_POST['report'])) {
handle_report($ctx);
} elseif (isset($_POST['post']) || $dropped_post) {
} elseif (isset($_POST['post'])) {
handle_post($ctx);
} elseif (isset($_POST['appeal'])) {
handle_appeal($ctx);

View file

@ -1,174 +1,178 @@
<?php
require 'inc/bootstrap.php';
if (!$config['search']['enable']) {
die(_("Post search is disabled"));
require 'inc/bootstrap.php';
if (!$config['search']['enable']) {
die(_("Post search is disabled"));
}
$queries_per_minutes = $config['search']['queries_per_minutes'];
$queries_per_minutes_all = $config['search']['queries_per_minutes_all'];
$search_limit = $config['search']['search_limit'];
if (isset($config['search']['boards'])) {
$boards = $config['search']['boards'];
} else {
$boards = listBoards(TRUE);
}
$body = Element('search_form.html', Array('boards' => $boards, 'board' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '&quot;', utf8tohtml($_GET['search'])) : false));
if (isset($_GET['search']) && !empty($_GET['search']) && isset($_GET['board']) && in_array($_GET['board'], $boards)) {
$phrase = $_GET['search'];
$_body = '';
$query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `ip` = :ip AND `time` > :time");
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
$query->bindValue(':time', time() - ($queries_per_minutes[1] * 60));
$query->execute() or error(db_error($query));
if ($query->fetchColumn() > $queries_per_minutes[0])
error(_('Wait a while before searching again, please.'));
$query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `time` > :time");
$query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60));
$query->execute() or error(db_error($query));
if ($query->fetchColumn() > $queries_per_minutes_all[0])
error(_('Wait a while before searching again, please.'));
$query = prepare("INSERT INTO ``search_queries`` VALUES (:ip, :time, :query)");
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
$query->bindValue(':time', time());
$query->bindValue(':query', $phrase);
$query->execute() or error(db_error($query));
_syslog(LOG_NOTICE, 'Searched /' . $_GET['board'] . '/ for "' . $phrase . '"');
// Cleanup search queries table
$query = prepare("DELETE FROM ``search_queries`` WHERE `time` <= :time");
$query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60));
$query->execute() or error(db_error($query));
openBoard($_GET['board']);
$filters = Array();
function search_filters($m) {
global $filters;
$name = $m[2];
$value = isset($m[4]) ? $m[4] : $m[3];
if (!in_array($name, array('id', 'thread', 'subject', 'name'))) {
// unknown filter
return $m[0];
}
$filters[$name] = $value;
return $m[1];
}
$queries_per_minutes = $config['search']['queries_per_minutes'];
$queries_per_minutes_all = $config['search']['queries_per_minutes_all'];
$search_limit = $config['search']['search_limit'];
if (isset($config['search']['boards'])) {
$boards = $config['search']['boards'];
$phrase = trim(preg_replace_callback('/(^|\s)(\w+):("(.*)?"|[^\s]*)/', 'search_filters', $phrase));
if (!preg_match('/[^*^\s]/', $phrase) && empty($filters)) {
_syslog(LOG_WARNING, 'Query too broad.');
$body .= '<p class="unimportant" style="text-align:center">(Query too broad.)</p>';
echo Element('page.html', Array(
'config'=>$config,
'title'=>'Search',
'body'=>$body,
));
exit;
}
// Escape escape character
$phrase = str_replace('!', '!!', $phrase);
// Remove SQL wildcard
$phrase = str_replace('%', '!%', $phrase);
// Use asterisk as wildcard to suit convention
$phrase = str_replace('*', '%', $phrase);
// Remove `, it's used by table prefix magic
$phrase = str_replace('`', '!`', $phrase);
$like = '';
$match = Array();
// Find exact phrases
if (preg_match_all('/"(.+?)"/', $phrase, $m)) {
foreach($m[1] as &$quote) {
$phrase = str_replace("\"{$quote}\"", '', $phrase);
$match[] = $pdo->quote($quote);
}
}
$words = explode(' ', $phrase);
foreach($words as &$word) {
if (empty($word)) {
continue;
}
$match[] = $pdo->quote($word);
}
$like = '';
foreach($match as &$phrase) {
if (!empty($like)) {
$like .= ' AND ';
}
$phrase = preg_replace('/^\'(.+)\'$/', '\'%$1%\'', $phrase);
$like .= '`body` LIKE ' . $phrase . ' ESCAPE \'!\'';
}
foreach($filters as $name => $value) {
if (!empty($like)) {
$like .= ' AND ';
}
$like .= '`' . $name . '` = '. $pdo->quote($value);
}
$like = str_replace('%', '%%', $like);
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE " . $like . " ORDER BY `time` DESC LIMIT :limit", $board['uri']));
$query->bindValue(':limit', $search_limit, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
if ($query->rowCount() == $search_limit) {
_syslog(LOG_WARNING, 'Query too broad.');
$body .= '<p class="unimportant" style="text-align:center">('._('Query too broad.').')</p>';
echo Element('page.html', Array(
'config'=>$config,
'title'=>'Search',
'body'=>$body,
));
exit;
}
$temp = '';
while ($post = $query->fetch()) {
if (!$post['thread']) {
$po = new Thread($post);
} else {
$po = new Post($post);
}
$temp .= $po->build(true) . '<hr/>';
}
if (!empty($temp))
$_body .= '<fieldset><legend>' .
sprintf(ngettext('%d result in', '%d results in', $query->rowCount()),
$query->rowCount()) . ' <a href="/' .
sprintf($config['board_path'], $board['uri']) . $config['file_index'] .
'">' .
sprintf($config['board_abbreviation'], $board['uri']) . ' - ' . $board['title'] .
'</a></legend>' . $temp . '</fieldset>';
$body .= '<hr/>';
if (!empty($_body)) {
$body .= $_body;
} else {
$boards = listBoards(TRUE);
$body .= '<p style="text-align:center" class="unimportant">('._('No results.').')</p>';
}
$body = Element('search_form.html', Array('boards' => $boards, 'board' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '&quot;', utf8tohtml($_GET['search'])) : false));
if(isset($_GET['search']) && !empty($_GET['search']) && isset($_GET['board']) && in_array($_GET['board'], $boards)) {
$phrase = $_GET['search'];
$_body = '';
$query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `ip` = :ip AND `time` > :time");
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
$query->bindValue(':time', time() - ($queries_per_minutes[1] * 60));
$query->execute() or error(db_error($query));
if($query->fetchColumn() > $queries_per_minutes[0])
error(_('Wait a while before searching again, please.'));
$query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `time` > :time");
$query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60));
$query->execute() or error(db_error($query));
if($query->fetchColumn() > $queries_per_minutes_all[0])
error(_('Wait a while before searching again, please.'));
$query = prepare("INSERT INTO ``search_queries`` VALUES (:ip, :time, :query)");
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
$query->bindValue(':time', time());
$query->bindValue(':query', $phrase);
$query->execute() or error(db_error($query));
_syslog(LOG_NOTICE, 'Searched /' . $_GET['board'] . '/ for "' . $phrase . '"');
}
// Cleanup search queries table
$query = prepare("DELETE FROM ``search_queries`` WHERE `time` <= :time");
$query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60));
$query->execute() or error(db_error($query));
openBoard($_GET['board']);
$filters = Array();
function search_filters($m) {
global $filters;
$name = $m[2];
$value = isset($m[4]) ? $m[4] : $m[3];
if(!in_array($name, array('id', 'thread', 'subject', 'name'))) {
// unknown filter
return $m[0];
}
$filters[$name] = $value;
return $m[1];
}
$phrase = trim(preg_replace_callback('/(^|\s)(\w+):("(.*)?"|[^\s]*)/', 'search_filters', $phrase));
if(!preg_match('/[^*^\s]/', $phrase) && empty($filters)) {
_syslog(LOG_WARNING, 'Query too broad.');
$body .= '<p class="unimportant" style="text-align:center">(Query too broad.)</p>';
echo Element('page.html', Array(
'config'=>$config,
'title'=>'Search',
'body'=>$body,
));
exit;
}
// Escape escape character
$phrase = str_replace('!', '!!', $phrase);
// Remove SQL wildcard
$phrase = str_replace('%', '!%', $phrase);
// Use asterisk as wildcard to suit convention
$phrase = str_replace('*', '%', $phrase);
// Remove `, it's used by table prefix magic
$phrase = str_replace('`', '!`', $phrase);
$like = '';
$match = Array();
// Find exact phrases
if(preg_match_all('/"(.+?)"/', $phrase, $m)) {
foreach($m[1] as &$quote) {
$phrase = str_replace("\"{$quote}\"", '', $phrase);
$match[] = $pdo->quote($quote);
}
}
$words = explode(' ', $phrase);
foreach($words as &$word) {
if(empty($word))
continue;
$match[] = $pdo->quote($word);
}
$like = '';
foreach($match as &$phrase) {
if(!empty($like))
$like .= ' AND ';
$phrase = preg_replace('/^\'(.+)\'$/', '\'%$1%\'', $phrase);
$like .= '`body` LIKE ' . $phrase . ' ESCAPE \'!\'';
}
foreach($filters as $name => $value) {
if(!empty($like))
$like .= ' AND ';
$like .= '`' . $name . '` = '. $pdo->quote($value);
}
$like = str_replace('%', '%%', $like);
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE " . $like . " ORDER BY `time` DESC LIMIT :limit", $board['uri']));
$query->bindValue(':limit', $search_limit, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
if($query->rowCount() == $search_limit) {
_syslog(LOG_WARNING, 'Query too broad.');
$body .= '<p class="unimportant" style="text-align:center">('._('Query too broad.').')</p>';
echo Element('page.html', Array(
'config'=>$config,
'title'=>'Search',
'body'=>$body,
));
exit;
}
$temp = '';
while($post = $query->fetch()) {
if(!$post['thread']) {
$po = new Thread($post);
} else {
$po = new Post($post);
}
$temp .= $po->build(true) . '<hr/>';
}
if(!empty($temp))
$_body .= '<fieldset><legend>' .
sprintf(ngettext('%d result in', '%d results in', $query->rowCount()),
$query->rowCount()) . ' <a href="/' .
sprintf($config['board_path'], $board['uri']) . $config['file_index'] .
'">' .
sprintf($config['board_abbreviation'], $board['uri']) . ' - ' . $board['title'] .
'</a></legend>' . $temp . '</fieldset>';
$body .= '<hr/>';
if(!empty($_body))
$body .= $_body;
else
$body .= '<p style="text-align:center" class="unimportant">('._('No results.').')</p>';
}
echo Element('page.html', Array(
'config'=>$config,
'title'=>_('Search'),
'body'=>'' . $body
));
echo Element('page.html', Array(
'config'=>$config,
'title'=>_('Search'),
'body'=>'' . $body
));

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
static/flags/420.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 465 B

BIN
static/flags/rodina_get.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/flags/rodina_lp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

View file

Before

Width:  |  Height:  |  Size: 898 B

After

Width:  |  Height:  |  Size: 898 B

Before After
Before After

BIN
static/flags/tania.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

BIN
static/leftypol_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -203,6 +203,10 @@ div.boardlist:not(.bottom) {
div.report {
color: #666666;
}
theme-catalog div.thread:hover {
background: #555;
border-color: transparent;
}
/* options.js */
#options_div, #alert_div {

View file

@ -194,6 +194,10 @@ div.boardlist:not(.bottom) {
div.report {
color: #666666;
}
.theme-catalog div.thread:hover {
background: #4f4f4f;
border-color: transparent;
}
#options_div, #alert_div {
background: #333333;
}

View file

@ -209,6 +209,11 @@ div.boardlist:not(.bottom) {
background-color: #1E1E1E;
}
.theme-catalog div.thread:hover {
background: #555;
border-color: transparent;
}
div.report {
color: #666666;
}

View file

@ -380,6 +380,7 @@ form table tr td div.center {
.file {
float: left;
min-width: 100px;
}
.file:not(.multifile) .post-image {
@ -390,6 +391,10 @@ form table tr td div.center {
float: none;
}
.file.multifile {
margin: 0 10px 0 0;
}
.file.multifile > p {
width: 0px;
min-width: 100%;
@ -570,6 +575,13 @@ div.post div.body {
white-space: pre-wrap;
}
div.post div.body:before {
content: "";
width: 18ch;
display: block;
overflow: hidden;
}
div.post.reply.highlighted {
background: #D6BAD0;
}

View file

@ -206,3 +206,8 @@ div.pages {
border: 0;
background: none !important;
}
.theme-catalog div.thread:hover {
background: #583E28;
border-color: transparent;
}

View file

@ -303,10 +303,6 @@ div.post.reply div.body {
padding-bottom: 0.3em;
}
div.post.reply.highlighted {
background: #D6BAD0;
}
div.post.reply div.body a {
color: #D00;
}
@ -322,6 +318,8 @@ div.post div.body {
div.post.reply {
background: #D6DAF0;
border: #555555 1px solid;
box-shadow: 4px 4px #555;
margin: 0.2em 4px;
padding: 0.2em 0.3em 0.5em 0.6em;
border-width: 1px;
@ -692,8 +690,8 @@ pre {
}
.theme-catalog div.thread:hover {
background: #D6DAF0;
border-color: #B7C5D9;
background: #927C8E;
border-color: transparent;
}
.theme-catalog div.grid-size-vsmall img {
@ -1359,18 +1357,8 @@ a.post_no:hover {
color: #32DD72 !important;
text-decoration: underline overline;
}
div.post.reply {
background: #111;
border: #555555 1px solid;
box-shadow: 4px 4px #555;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply.highlighted {
background: #555;
background: #927C8E;
border: transparent 1px solid;
@media (max-width: 48em) {

View file

@ -62,9 +62,11 @@ Opening posts with liberalism or reactionary topics will be treated with far mor
<p>These examples are low quality posts that are considered, at best, bait, but are better described as spam. Any poster that violates this rule may be subject to a ban, and any post that violates this is subject to deletion at the discretion of moderators, if they feel that the topic may be an avenue for productive discussion.</p>
<p>15) In order to incentivize creativity and develop a genuine culture, non-original Wojaks, Pepes, or Groypers which are not /leftypol/ original content are considered spam and banned.</p>
<p><b>META:</b></p>
<p>15) Volunteers may remove other posts according to their own discretion which they feel do not contribute to the stated mission of /leftypol/, but they should try to adhere to the standards of the community and of their fellow moderators, and to refrain from arbitrary decisions. Where there is disagreement among moderators, the matter will be decided by informal consensus of currently active moderators. If there is still disagreement, the matter should be escalated to a formal vote.</p>
<p>16) Volunteers may remove other posts according to their own discretion which they feel do not contribute to the stated mission of /leftypol/, but they should try to adhere to the standards of the community and of their fellow moderators, and to refrain from arbitrary decisions. Where there is disagreement among moderators, the matter will be decided by informal consensus of currently active moderators. If there is still disagreement, the matter should be escalated to a formal vote.</p>
<p>16) Users have the right to question and challenge any bans or post removals, or other moderator actions, which they feel are unfair or do not live up to the spirit of the rules. This may be done in the moderation feedback threads on the various boards, on the /meta/ board, through the ban appeal feature, or in the Leftypol Matrix Congress chat, but comments should be considered and constructive, and should not devolve into polemics against the volunteers. Ultimately, the judgement of the moderation team is final.</p>
<p>17) Users have the right to question and challenge any bans or post removals, or other moderator actions, which they feel are unfair or do not live up to the spirit of the rules. This may be done in the moderation feedback threads on the various boards, on the /meta/ board, through the ban appeal feature, or in the Leftypol Matrix Congress chat, but comments should be considered and constructive, and should not devolve into polemics against the volunteers. Ultimately, the judgement of the moderation team is final.</p>
</div>

View file

@ -16,13 +16,13 @@
border-width: 2px;
margin-right: 15px;
}
.introduction {
grid-column: 2 / 9;
grid-row: 1;
width: 100%;
}
.content {
grid-column: 2 / 9;
grid-row: 2;
@ -35,7 +35,7 @@
gap: 20px;
height: 100vh;
}
.modlog {
width: 50%;
text-align: left;
@ -69,7 +69,7 @@
li a.system {
font-weight: bold;
}
@media (max-width:768px) {
body{
display: grid;
@ -78,7 +78,7 @@
height: 100vh;
width: 100%;
}
.introduction {
grid-column: 1;
grid-row: 1;
@ -96,17 +96,18 @@
grid-column: 1;
grid-row: 3;
width: 100%;
word-break: break-all;
}
.modlog {
width: 100%;
text-align: center;
}
table {
table-layout: fixed;
}
table.modlog tr th {
white-space: normal;
word-wrap: break-word;

View file

@ -11,7 +11,7 @@
.home-description {
margin: 20px auto 0 auto;
text-align: center;
max-width: 700px;"
max-width: 700px;
}
</style>
{{ boardlist.top }}

View file

@ -33,7 +33,7 @@
<h2 id="3">
I'm new here and <span class="strikethrough">learned politics from memes</span> would like to learn about leftism! Where should I start ?
</h2>
<p>There are some beginner lists in <a href="/leftypol/res/285223.html">the reading thread</a>. Meanwhile, consider asking your questions in <a href="/edu/">/edu/</a> or in a relevant /leftypol/ thread, such as <a href="https://leftypol.org/search.php?search=qtddtot&board=leftypol">QTDDTOT</a>.</p>
<p>Consider asking your questions in <a href="/edu/">/edu/</a> or in a relevant /leftypol/ thread, such as <a href="https://leftypol.org/search.php?search=qtddtot&board=leftypol">QTDDTOT</a>.</p>
<h2 id="4">
What is the purpose of leftypol.org ?
</h2>
@ -88,7 +88,7 @@
<h2 id="9">
How can I suggest or submit fixes to the site ?
</h2>
<p>There is a /meta/ thread for this, and our <a href="https://gitlab.leftypol.org/leftypol/leftypol/">Gitlab repo</a>.</p>
<p>There is a /meta/ thread for this, and our <a href="https://forgejo.leftypol.org/leftypol/leftypol/">Forgejo repo</a>.</p>
<h2 id="10">
I don't trust Tor exit nodes. Do you have an .onion site ?
</h2>
@ -129,6 +129,10 @@
What are the maximum filesize for attachments ?
</h2>
<p>Maximum file size in megabytes for attachments to a single post is 80MB (e.g. 5 * 16MB), as most boards support uploading 5 attachments by default. Maximum file size in pixels for images is currently set to 20000 by 20000. </p>
<h2 id="16">
Can I have an account on your git instance?
</h2>
<p>Create the account on <a href="https://forgejo.leftypol.org/">Forgejo</a>, then contact the staff via the Tech Team General thread on /meta/ to get your account approved.</p>
</div>

View file

View file

@ -44,20 +44,26 @@ foreach ($boards as $board) {
);
foreach ($stray_src as $src) {
$stats['deleted']++;
$stats['size'] += filesize($board['uri'] . "/" . $config['dir']['img'] . $src);
if (!file_unlink($board['uri'] . "/" . $config['dir']['img'] . $src)) {
$er = error_get_last();
die("error: " . $er['message'] . "\n");
$p = $board['uri'] . "/" . $config['dir']['img'] . $src;
if (file_exists($p)) {
$stats['deleted']++;
$stats['size'] += filesize($p);
if (!file_unlink($p)) {
$er = error_get_last();
die("error: " . $er['message'] . "\n");
}
}
}
foreach ($stray_thumb as $thumb) {
$stats['deleted']++;
$stats['size'] += filesize($board['uri'] . "/" . $config['dir']['thumb'] . $thumb);
if (!file_unlink($board['uri'] . "/" . $config['dir']['thumb'] . $thumb)) {
$er = error_get_last();
die("error: " . $er['message'] . "\n");
$p = $board['uri'] . "/" . $config['dir']['thumb'] . $thumb;
if (file_exists($p)) {
$stats['deleted']++;
$stats['size'] += filesize($p);
if (!file_unlink($p)) {
$er = error_get_last();
die("error: " . $er['message'] . "\n");
}
}
}