Compare commits

..

3 commits

Author SHA1 Message Date
Zankaria
026a70988c overboards: remove obsolete code 2024-10-20 14:01:30 +02:00
Zankaria
52dd8960a9 overboards: parametize post form inclusion 2024-10-20 14:01:30 +02:00
Zankaria
a88a2e647e overboards: minor format 2024-10-20 14:01:30 +02:00
119 changed files with 3310 additions and 4123 deletions

3
.gitignore vendored
View file

@ -70,6 +70,9 @@ tf/
/mod/ /mod/
/random/ /random/
# Banners
static/banners/*
#Fonts #Fonts
stylesheets/fonts stylesheets/fonts

View file

@ -11,9 +11,7 @@
"autoload": { "autoload": {
"classmap": ["inc/"], "classmap": ["inc/"],
"files": [ "files": [
"inc/anti-bot.php",
"inc/bootstrap.php", "inc/bootstrap.php",
"inc/context.php",
"inc/display.php", "inc/display.php",
"inc/template.php", "inc/template.php",
"inc/database.php", "inc/database.php",

View file

@ -7,7 +7,7 @@ services:
ports: ports:
- "9091:80" - "9091:80"
depends_on: depends_on:
- db - leftypol-db
volumes: volumes:
- ./local-instances/${INSTANCE:-0}/www:/var/www/html - ./local-instances/${INSTANCE:-0}/www:/var/www/html
- ./docker/nginx/leftypol.conf:/etc/nginx/conf.d/default.conf - ./docker/nginx/leftypol.conf:/etc/nginx/conf.d/default.conf
@ -23,11 +23,13 @@ services:
volumes: volumes:
- ./local-instances/${INSTANCE:-0}/www:/var/www - ./local-instances/${INSTANCE:-0}/www:/var/www
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf - ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
- redis-sock:/var/run/redis
#MySQL Service #MySQL Service
db: leftypol-db:
image: mysql:8.0.35 image: mysql:8.0.35
container_name: leftypol-db
restart: unless-stopped
tty: true
ports: ports:
- "3306:3306" - "3306:3306"
environment: environment:
@ -35,13 +37,3 @@ services:
MYSQL_ROOT_PASSWORD: password MYSQL_ROOT_PASSWORD: password
volumes: volumes:
- ./local-instances/${INSTANCE:-0}/mysql:/var/lib/mysql - ./local-instances/${INSTANCE:-0}/mysql:/var/lib/mysql
redis:
build:
context: ./
dockerfile: ./docker/redis/Dockerfile
volumes:
- redis-sock:/var/run/redis
volumes:
redis-sock:

View file

@ -1,6 +0,0 @@
FROM redis:7.4-alpine
RUN mkdir -p /var/run/redis && chmod 777 /var/run/redis
COPY ./docker/redis/redis.conf /etc/redis.conf
ENTRYPOINT [ "docker-entrypoint.sh", "/etc/redis.conf" ]

View file

@ -1,16 +0,0 @@
# Accept connections on the specified port, default is 6379 (IANA #815344).
# If port 0 is specified Redis will not listen on a TCP socket.
#port 6379
port 0
# Unix socket.
#
# Specify the path for the Unix socket that will be used to listen for
# incoming connections. There is no default, so Redis will not listen
# on a unix socket when not specified.
#
unixsocket /var/run/redis/redis-server.sock
# Executig a socket is a no-op, and we need to share acces to other programs.
# Shared the connection only with programs in the redis group for security.
#unixsocketperm 700
unixsocketperm 666

View file

@ -1,28 +0,0 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
class ApcuCacheDriver implements CacheDriver {
public function get(string $key): mixed {
$success = false;
$ret = \apcu_fetch($key, $success);
if ($success === false) {
return null;
}
return $ret;
}
public function set(string $key, mixed $value, mixed $expires = false): void {
\apcu_store($key, $value, (int)$expires);
}
public function delete(string $key): void {
\apcu_delete($key);
}
public function flush(): void {
\apcu_clear_cache();
}
}

View file

@ -1,28 +0,0 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* A simple process-wide PHP array.
*/
class ArrayCacheDriver implements CacheDriver {
private static array $inner = [];
public function get(string $key): mixed {
return isset(self::$inner[$key]) ? self::$inner[$key] : null;
}
public function set(string $key, mixed $value, mixed $expires = false): void {
self::$inner[$key] = $value;
}
public function delete(string $key): void {
unset(self::$inner[$key]);
}
public function flush(): void {
self::$inner = [];
}
}

View file

@ -1,38 +0,0 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
interface CacheDriver {
/**
* Get the value of associated with the key.
*
* @param string $key The key of the value.
* @return mixed|null The value associated with the key, or null if there is none.
*/
public function get(string $key): mixed;
/**
* Set a key-value pair.
*
* @param string $key The key.
* @param mixed $value The value.
* @param int|false $expires After how many seconds the pair will expire. Use false or ignore this parameter to keep
* the value until it gets evicted to make space for more items. Some drivers will always
* ignore this parameter and store the pair until it's removed.
*/
public function set(string $key, mixed $value, mixed $expires = false): void;
/**
* Delete a key-value pair.
*
* @param string $key The key.
*/
public function delete(string $key): void;
/**
* Delete all the key-value pairs.
*/
public function flush(): void;
}

View file

@ -1,28 +0,0 @@
<?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

@ -1,61 +0,0 @@
<?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

@ -1,155 +0,0 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
class FsCacheDriver implements CacheDriver {
private string $prefix;
private string $base_path;
private mixed $lock_fd;
private int|false $collect_chance_den;
private function prepareKey(string $key): string {
$key = \str_replace('/', '::', $key);
$key = \str_replace("\0", '', $key);
return $this->prefix . $key;
}
private function sharedLockCache(): void {
\flock($this->lock_fd, LOCK_SH);
}
private function exclusiveLockCache(): void {
\flock($this->lock_fd, LOCK_EX);
}
private function unlockCache(): void {
\flock($this->lock_fd, LOCK_UN);
}
private function collectImpl(): int {
/*
* A read lock is ok, since it's alright if we delete expired items from under the feet of other processes, and
* no other process add new cache items or refresh existing ones.
*/
$files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
$count = 0;
foreach ($files as $file) {
$data = \file_get_contents($file);
$wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
if (@\unlink($file)) {
$count++;
}
}
}
return $count;
}
private function maybeCollect(): void {
if ($this->collect_chance_den !== false && \mt_rand(0, $this->collect_chance_den - 1) === 0) {
$this->collect_chance_den = false; // Collect only once per instance (aka process).
$this->collectImpl();
}
}
public function __construct(string $prefix, string $base_path, string $lock_file, int|false $collect_chance_den) {
if ($base_path[\strlen($base_path) - 1] !== '/') {
$base_path = "$base_path/";
}
if (!\is_dir($base_path)) {
throw new \RuntimeException("$base_path is not a directory!");
}
if (!\is_writable($base_path)) {
throw new \RuntimeException("$base_path is not writable!");
}
$this->lock_fd = \fopen($base_path . $lock_file, 'w');
if ($this->lock_fd === false) {
throw new \RuntimeException('Unable to open the lock file!');
}
$this->prefix = $prefix;
$this->base_path = $base_path;
$this->collect_chance_den = $collect_chance_den;
}
public function __destruct() {
$this->close();
}
public function get(string $key): mixed {
$key = $this->prepareKey($key);
$this->sharedLockCache();
// Collect expired items first so if the target key is expired we shortcut to failure in the next lines.
$this->maybeCollect();
$fd = \fopen($this->base_path . $key, 'r');
if ($fd === false) {
$this->unlockCache();
return null;
}
$data = \stream_get_contents($fd);
\fclose($fd);
$this->unlockCache();
$wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
// Already expired, leave it there since we already released the lock and pretend it doesn't exist.
return null;
} else {
return $wrapped['inner'];
}
}
public function set(string $key, mixed $value, mixed $expires = false): void {
$key = $this->prepareKey($key);
$wrapped = [
'expires' => $expires ? \time() + $expires : false,
'inner' => $value
];
$data = \json_encode($wrapped);
$this->exclusiveLockCache();
$this->maybeCollect();
\file_put_contents($this->base_path . $key, $data);
$this->unlockCache();
}
public function delete(string $key): void {
$key = $this->prepareKey($key);
$this->exclusiveLockCache();
@\unlink($this->base_path . $key);
$this->maybeCollect();
$this->unlockCache();
}
public function collect(): int {
$this->sharedLockCache();
$count = $this->collectImpl();
$this->unlockCache();
return $count;
}
public function flush(): void {
$this->exclusiveLockCache();
$files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
foreach ($files as $file) {
@\unlink($file);
}
$this->unlockCache();
}
public function close(): void {
\fclose($this->lock_fd);
}
}

View file

@ -1,22 +0,0 @@
<?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

@ -1,26 +0,0 @@
<?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

@ -1,43 +0,0 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
class MemcachedCacheDriver implements CacheDriver {
private \Memcached $inner;
public function __construct(string $prefix, string $memcached_server) {
$this->inner = new \Memcached();
if (!$this->inner->setOption(\Memcached::OPT_BINARY_PROTOCOL, true)) {
throw new \RuntimeException('Unable to set the memcached protocol!');
}
if (!$this->inner->setOption(\Memcached::OPT_PREFIX_KEY, $prefix)) {
throw new \RuntimeException('Unable to set the memcached prefix!');
}
if (!$this->inner->addServers($memcached_server)) {
throw new \RuntimeException('Unable to add the memcached server!');
}
}
public function get(string $key): mixed {
$ret = $this->inner->get($key);
// If the returned value is false but the retrival was a success, then the value stored was a boolean false.
if ($ret === false && $this->inner->getResultCode() !== \Memcached::RES_SUCCESS) {
return null;
}
return $ret;
}
public function set(string $key, mixed $value, mixed $expires = false): void {
$this->inner->set($key, $value, (int)$expires);
}
public function delete(string $key): void {
$this->inner->delete($key);
}
public function flush(): void {
$this->inner->flush();
}
}

View file

@ -1,26 +0,0 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
/**
* No-op cache. Useful for testing.
*/
class NoneCacheDriver implements CacheDriver {
public function get(string $key): mixed {
return null;
}
public function set(string $key, mixed $value, mixed $expires = false): void {
// No-op.
}
public function delete(string $key): void {
// No-op.
}
public function flush(): void {
// No-op.
}
}

View file

@ -1,70 +0,0 @@
<?php
namespace Vichan\Data\Driver;
defined('TINYBOARD') or exit;
class RedisCacheDriver implements CacheDriver {
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]);
} elseif ($port === null) {
$this->inner->connect($host);
} else {
// IP + port.
$this->inner->connect($host, $port);
}
if ($password) {
$this->inner->auth($password);
}
if (!$this->inner->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_JSON)) {
throw new \RuntimeException('Unable to configure Redis serializer');
}
if (!$this->inner->select($database)) {
throw new \RuntimeException('Unable to connect to Redis database!');
}
$this->prefix = $prefix;
}
public function get(string $key): mixed {
$ret = $this->inner->get($this->prefix . $key);
if ($ret === false) {
return null;
}
if ($ret === null) {
return false;
}
return $ret;
}
public function set(string $key, mixed $value, mixed $expires = false): void {
$value = $value === false ? null : $value;
if ($expires === false) {
$this->inner->set($this->prefix . $key, $value);
} else {
$this->inner->setEx($this->prefix . $key, $expires, $value);
}
}
public function delete(string $key): void {
$this->inner->del($this->prefix . $key);
}
public function flush(): void {
if (empty($this->prefix)) {
$this->inner->flushDB();
} else {
$this->inner->unlink($this->inner->keys("{$this->prefix}*"));
}
}
}

View file

@ -1,27 +0,0 @@
<?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

@ -1,35 +0,0 @@
<?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

@ -1,76 +0,0 @@
<?php
namespace Vichan\Data;
use Vichan\Data\Driver\CacheDriver;
class IpNoteQueries {
private \PDO $pdo;
private CacheDriver $cache;
public function __construct(\PDO $pdo, CacheDriver $cache) {
$this->pdo = $pdo;
$this->cache = $cache;
}
/**
* Get all the notes relative to an IP.
*
* @param string $ip The IP of the notes. THE STRING IS NOT VALIDATED.
* @return array Returns an array of notes sorted by the most recent. Includes the username of the mods.
*/
public function getByIp(string $ip) {
$ret = $this->cache->get("ip_note_queries_$ip");
if ($ret !== null) {
return $ret;
}
$query = $this->pdo->prepare('SELECT `ip_notes`.*, `username` FROM `ip_notes` LEFT JOIN `mods` ON `mod` = `mods`.`id` WHERE `ip` = :ip ORDER BY `time` DESC');
$query->bindValue(':ip', $ip);
$query->execute();
$ret = $query->fetchAll(\PDO::FETCH_ASSOC);
$this->cache->set("ip_note_queries_$ip", $ret);
return $ret;
}
/**
* Creates a new note relative to the given ip.
*
* @param string $ip The IP of the note. THE STRING IS NOT VALIDATED.
* @param int $mod_id The id of the mod who created the note.
* @param string $body The text of the note.
* @return void
*/
public function add(string $ip, int $mod_id, string $body) {
$query = $this->pdo->prepare('INSERT INTO `ip_notes` (`ip`, `mod`, `time`, `body`) VALUES (:ip, :mod, :time, :body)');
$query->bindValue(':ip', $ip);
$query->bindValue(':mod', $mod_id);
$query->bindValue(':time', time());
$query->bindValue(':body', $body);
$query->execute();
$this->cache->delete("ip_note_queries_$ip");
}
/**
* Delete a note only if it's of a particular IP address.
*
* @param int $id The id of the note.
* @param int $ip The expected IP of the note. THE STRING IS NOT VALIDATED.
* @return bool True if any note was deleted.
*/
public function deleteWhereIp(int $id, string $ip): bool {
$query = $this->pdo->prepare('DELETE FROM `ip_notes` WHERE `ip` = :ip AND `id` = :id');
$query->bindValue(':ip', $ip);
$query->bindValue(':id', $id);
$query->execute();
$any = $query->rowCount() != 0;
if ($any) {
$this->cache->delete("ip_note_queries_$ip");
}
return $any;
}
}

View file

@ -1,15 +0,0 @@
<?php
namespace Vichan\Data;
/**
* A page of user posts.
*/
class PageFetchResult {
/**
* @var array[array] Posts grouped by board uri.
*/
public array $by_uri;
public ?string $cursor_prev;
public ?string $cursor_next;
}

View file

@ -1,227 +0,0 @@
<?php
namespace Vichan\Data;
class ReportQueries {
private \PDO $pdo;
private bool $auto_maintenance;
private function deleteReportImpl(string $board, int $post_id) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `post` = :id AND `board` = :board');
$query->bindValue(':id', $post_id, \PDO::PARAM_INT);
$query->bindValue(':board', $board);
$query->execute();
}
private function joinReportPosts(array $raw_reports, ?int $limit): array {
// Group the reports rows by board.
$reports_by_boards = [];
foreach ($raw_reports as $report) {
if (!isset($reports_by_boards[$report['board']])) {
$reports_by_boards[$report['board']] = [];
}
$reports_by_boards[$report['board']][] = $report['post'];
}
// Join the reports with the actual posts.
$report_posts = [];
foreach ($reports_by_boards as $board => $posts) {
$report_posts[$board] = [];
$query = $this->pdo->prepare(\sprintf('SELECT * FROM `posts_%s` WHERE `id` IN (' . \implode(',', $posts) . ')', $board));
$query->execute();
while ($post = $query->fetch(\PDO::FETCH_ASSOC)) {
$report_posts[$board][$post['id']] = $post;
}
}
// Filter out the reports without a valid post.
$valid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) {
$report['post_data'] = $report_posts[$report['board']][$report['post']];
$valid[] = $report;
if ($limit !== null && \count($valid) >= $limit) {
return $valid;
}
} else {
// Invalid report (post has been deleted).
if ($this->auto_maintenance != false) {
$this->deleteReportImpl($report['board'], $report['post']);
}
}
}
return $valid;
}
/**
* Filters out the invalid reports.
*
* @param array $raw_reports Array with the raw fetched reports. Must include a `board`, `post` and `id` fields.
* @param bool $get_invalid True to reverse the filter and get the invalid reports instead.
* @return array An array of filtered reports.
*/
private function filterReports(array $raw_reports, bool $get_invalid): array {
// Group the reports rows by board.
$reports_by_boards = [];
foreach ($raw_reports as $report) {
if (!isset($reports_by_boards[$report['board']])) {
$reports_by_boards[$report['board']] = [];
}
$reports_by_boards[$report['board']][] = $report['post'];
}
// Join the reports with the actual posts.
$report_posts = [];
foreach ($reports_by_boards as $board => $posts) {
$report_posts[$board] = [];
$query = $this->pdo->prepare(\sprintf('SELECT `id` FROM `posts_%s` WHERE `id` IN (' . \implode(',', $posts) . ')', $board));
$query->execute();
while ($post = $query->fetch(\PDO::FETCH_ASSOC)) {
$report_posts[$board][$post['id']] = $post;
}
}
if ($get_invalid) {
// Get the reports without a post.
$invalid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) {
$invalid[] = $report;
}
}
return $invalid;
} else {
// Filter out the reports without a valid post.
$valid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) {
$valid[] = $report;
} else {
// Invalid report (post has been deleted).
if ($this->auto_maintenance != false) {
$this->deleteReportImpl($report['board'], $report['post']);
}
}
}
return $valid;
}
}
/**
* @param \PDO $pdo PDO connection.
* @param bool $auto_maintenance If the auto maintenance should be enabled.
*/
public function __construct(\PDO $pdo, bool $auto_maintenance) {
$this->pdo = $pdo;
$this->auto_maintenance = $auto_maintenance;
}
/**
* Get the number of reports.
*
* @return int The number of reports.
*/
public function getCount(): int {
$query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`');
$query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
$valid_reports = $this->filterReports($raw_reports, false, null);
$count = \count($valid_reports);
return $count;
}
/**
* Get the report with the given id. DOES NOT PERFORM VALIDITY CHECK.
*
* @param int $id The id of the report to fetch.
* @return ?array An array of the given report with the `board` and `ip` fields. Null if no such report exists.
*/
public function getReportById(int $id): ?array {
$query = prepare('SELECT `board`, `ip` FROM ``reports`` WHERE `id` = :id');
$query->bindValue(':id', $id);
$query->execute();
$ret = $query->fetch(\PDO::FETCH_ASSOC);
if ($ret !== false) {
return $ret;
} else {
return null;
}
}
/**
* Get the reports with the associated post data.
*
* @param int $count The maximum number of rows in the return array.
* @return array The reports with the associated post data.
*/
public function getReportsWithPosts(int $count): array {
$query = $this->pdo->prepare('SELECT * FROM `reports` ORDER BY `time`');
$query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
return $this->joinReportPosts($raw_reports, $count);
}
/**
* Purge the invalid reports.
*
* @return int The number of reports deleted.
*/
public function purge(): int {
$query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`');
$query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
$invalid_reports = $this->filterReports($raw_reports, true, null);
foreach ($invalid_reports as $report) {
$this->deleteReportImpl($report['board'], $report['post']);
}
return \count($invalid_reports);
}
/**
* Deletes the given report.
*
* @param int $id The report id.
*/
public function deleteById(int $id) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `id` = :id');
$query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute();
}
/**
* Deletes all reports from the given ip.
*
* @param string $ip The reporter ip.
*/
public function deleteByIp(string $ip) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `ip` = :ip');
$query->bindValue(':ip', $ip);
$query->execute();
}
/**
* Inserts a new report.
*
* @param string $ip Ip of the user sending the report.
* @param string $board_uri Board uri of the reported thread. MUST ALREADY BE SANITIZED.
* @param int $post_id Post reported.
* @param string $reason Reason of the report.
* @return void
*/
public function add(string $ip, string $board_uri, int $post_id, string $reason) {
$query = $this->pdo->prepare('INSERT INTO `reports` VALUES (NULL, :time, :ip, :board, :post, :reason)');
$query->bindValue(':time', time(), \PDO::PARAM_INT);
$query->bindValue(':ip', $ip);
$query->bindValue(':board', $board_uri);
$query->bindValue(':post', $post_id, \PDO::PARAM_INT);
$query->bindValue(':reason', $reason);
$query->execute();
}
}

View file

@ -1,159 +0,0 @@
<?php
namespace Vichan\Data;
use Vichan\Functions\Net;
/**
* Browse user posts
*/
class UserPostQueries {
private const CURSOR_TYPE_PREV = 'p';
private const CURSOR_TYPE_NEXT = 'n';
private \PDO $pdo;
public function __construct(\PDO $pdo) {
$this->pdo = $pdo;
}
private function paginate(array $board_uris, int $page_size, ?string $cursor, callable $callback): PageFetchResult {
// Decode the cursor.
if ($cursor !== null) {
list($cursor_type, $uri_id_cursor_map) = Net\decode_cursor($cursor);
} else {
// Defaults if $cursor is an invalid string.
$cursor_type = null;
$uri_id_cursor_map = [];
}
$next_cursor_map = [];
$prev_cursor_map = [];
$rows = [];
foreach ($board_uris as $uri) {
// Extract the cursor relative to the board.
$start_id = null;
if ($cursor_type !== null && isset($uri_id_cursor_map[$uri])) {
$value = $uri_id_cursor_map[$uri];
if (\is_numeric($value)) {
$start_id = (int)$value;
}
}
$posts = $callback($uri, $cursor_type, $start_id, $page_size);
$posts_count = \count($posts);
// By fetching one extra post bellow and/or above the limit, we know if there are any posts beside the current page.
if ($posts_count === $page_size + 2) {
$has_extra_prev_post = true;
$has_extra_end_post = true;
} else {
/*
* If the id we start fetching from is also the first id fetched from the DB, then we exclude it from
* the results, noting that we fetched 1 more posts than we needed, and it was before the current page.
* Hence, we have no extra post at the end and no next page.
*/
$has_extra_prev_post = $start_id !== null && $start_id === (int)$posts[0]['id'];
$has_extra_end_post = !$has_extra_prev_post && $posts_count > $page_size;
}
// Get the previous cursor, if any.
if ($has_extra_prev_post) {
\array_shift($posts);
$posts_count--;
// Select the most recent post.
$prev_cursor_map[$uri] = $posts[0]['id'];
}
// Get the next cursor, if any.
if ($has_extra_end_post) {
\array_pop($posts);
// Select the oldest post.
$next_cursor_map[$uri] = $posts[$posts_count - 2]['id'];
}
$rows[$uri] = $posts;
}
$res = new PageFetchResult();
$res->by_uri = $rows;
$res->cursor_prev = !empty($prev_cursor_map) ? Net\encode_cursor(self::CURSOR_TYPE_PREV, $prev_cursor_map) : null;
$res->cursor_next = !empty($next_cursor_map) ? Net\encode_cursor(self::CURSOR_TYPE_NEXT, $next_cursor_map) : null;
return $res;
}
/**
* Fetch a page of user posts.
*
* @param array $board_uris The uris of the boards that should be included.
* @param string $ip The IP of the target user.
* @param integer $page_size The Number of posts that should be fetched.
* @param string|null $cursor The directional cursor to fetch the next or previous page. Null to start from the beginning.
* @return PageFetchResult
*/
public function fetchPaginatedByIp(array $board_uris, string $ip, int $page_size, ?string $cursor = null): PageFetchResult {
return $this->paginate($board_uris, $page_size, $cursor, function($uri, $cursor_type, $start_id, $page_size) use ($ip) {
if ($cursor_type === null) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri));
$query->bindValue(':ip', $ip);
$query->bindValue(':limit', $page_size + 1, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return $query->fetchAll(\PDO::FETCH_ASSOC);
} elseif ($cursor_type === self::CURSOR_TYPE_NEXT) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` <= :start_id ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri));
$query->bindValue(':ip', $ip);
$query->bindValue(':start_id', $start_id, \PDO::PARAM_INT);
$query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return $query->fetchAll(\PDO::FETCH_ASSOC);
} elseif ($cursor_type === self::CURSOR_TYPE_PREV) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` >= :start_id ORDER BY `sticky` ASC, `id` ASC LIMIT :limit', $uri));
$query->bindValue(':ip', $ip);
$query->bindValue(':start_id', $start_id, \PDO::PARAM_INT);
$query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return \array_reverse($query->fetchAll(\PDO::FETCH_ASSOC));
} else {
throw new \RuntimeException("Unknown cursor type '$cursor_type'");
}
});
}
/**
* Fetch a page of user posts.
*
* @param array $board_uris The uris of the boards that should be included.
* @param string $password The password of the target user.
* @param integer $page_size The Number of posts that should be fetched.
* @param string|null $cursor The directional cursor to fetch the next or previous page. Null to start from the beginning.
* @return PageFetchResult
*/
public function fetchPaginateByPassword(array $board_uris, string $password, int $page_size, ?string $cursor = null): PageFetchResult {
return $this->paginate($board_uris, $page_size, $cursor, function($uri, $cursor_type, $start_id, $page_size) use ($password) {
if ($cursor_type === null) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `password` = :password ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri));
$query->bindValue(':password', $password);
$query->bindValue(':limit', $page_size + 1, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return $query->fetchAll(\PDO::FETCH_ASSOC);
} elseif ($cursor_type === self::CURSOR_TYPE_NEXT) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `password` = :password AND `id` <= :start_id ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri));
$query->bindValue(':password', $password);
$query->bindValue(':start_id', $start_id, \PDO::PARAM_INT);
$query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return $query->fetchAll(\PDO::FETCH_ASSOC);
} elseif ($cursor_type === self::CURSOR_TYPE_PREV) {
$query = $this->pdo->prepare(sprintf('SELECT * FROM `posts_%s` WHERE `password` = :password AND `id` >= :start_id ORDER BY `sticky` ASC, `id` ASC LIMIT :limit', $uri));
$query->bindValue(':password', $password);
$query->bindValue(':start_id', $start_id, \PDO::PARAM_INT);
$query->bindValue(':limit', $page_size + 2, \PDO::PARAM_INT); // Always fetch more.
$query->execute();
return \array_reverse($query->fetchAll(\PDO::FETCH_ASSOC));
} else {
throw new \RuntimeException("Unknown cursor type '$cursor_type'");
}
});
}
}

View file

@ -190,62 +190,53 @@ class AntiBot {
} }
} }
function _create_antibot($pdo, $board, $thread) { function _create_antibot($board, $thread) {
global $config, $purged_old_antispam; global $config, $purged_old_antispam;
$antibot = new AntiBot(array($board, $thread)); $antibot = new AntiBot(array($board, $thread));
// Delete old expired antispam, skipping those with NULL expiration timestamps (infinite lifetime).
if (!isset($purged_old_antispam) && $config['auto_maintenance']) {
$purged_old_antispam = true;
purge_old_antispam();
}
retry_on_deadlock(4, function() use($thread, $board, $config) {
// Keep the now invalid timestamps around for a bit to enable users to post if they're still on an old version of
// the HTML page.
// By virtue of existing, we know that we're making a new version of the page, and the user from now on may just reload.
if ($thread) {
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL');
} else {
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL');
}
$query->bindValue(':board', $board);
if ($thread) {
$query->bindValue(':thread', $thread);
}
$query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
// Throws on error.
$query->execute();
});
try { try {
retry_on_deadlock(3, function() use ($config, $pdo, $thread, $board, $antibot, $purged_old_antispam) { $hash = $antibot->hash();
try {
$pdo->beginTransaction();
// Delete old expired antispam, skipping those with NULL expiration timestamps (infinite lifetime). retry_on_deadlock(2, function() use($board, $thread, $hash) {
if (!isset($purged_old_antispam) && $config['auto_maintenance']) { // Insert an antispam with infinite life as the HTML page of a thread might last well beyond the expiry date.
$purged_old_antispam = true; $query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
purge_old_antispam(); $query->bindValue(':board', $board);
} $query->bindValue(':thread', $thread);
$query->bindValue(':hash', $hash);
// Keep the now invalid timestamps around for a bit to enable users to post if they're still on an old version of // Throws on error.
// the HTML page. $query->execute();
// By virtue of existing, we know that we're making a new version of the page, and the user from now on may just reload.
if ($thread) {
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL');
} else {
$query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL');
}
$query->bindValue(':board', $board);
if ($thread) {
$query->bindValue(':thread', $thread);
}
$query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
// Throws on error.
$query->execute();
$hash = $antibot->hash();
// Insert an antispam with infinite life as the HTML page of a thread might last well beyond the expiry date.
$query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
$query->bindValue(':board', $board);
$query->bindValue(':thread', $thread);
$query->bindValue(':hash', $hash);
// Throws on error.
$query->execute();
$pdo->commit();
} catch (\Exception $e) {
$pdo->rollBack();
throw $e;
}
}); });
} catch (\PDOException $e) { } catch(\PDOException $e) {
$pdo->rollBack();
if ($e->errorInfo === null || $e->errorInfo[1] != MYSQL_ER_LOCK_DEADLOCK) { if ($e->errorInfo === null || $e->errorInfo[1] != MYSQL_ER_LOCK_DEADLOCK) {
throw $e; throw $e;
} else { } else {
\error_log('5 or more deadlocks on _create_antibot while inserting, skipping'); error_log('Multiple deadlocks on _create_antibot while inserting, skipping');
} }
} }

View file

@ -4,91 +4,182 @@
* Copyright (c) 2010-2013 Tinyboard Development Group * Copyright (c) 2010-2013 Tinyboard Development Group
*/ */
use Vichan\Data\Driver\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver};
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
class Cache { class Cache {
private static function buildCache(): CacheDriver { private static $cache;
public static function init() {
global $config; global $config;
switch ($config['cache']['enabled']) { switch ($config['cache']['enabled']) {
case 'memcached': case 'memcached':
return new MemcachedCacheDriver( self::$cache = new Memcached();
$config['cache']['prefix'], self::$cache->addServers($config['cache']['memcached']);
$config['cache']['memcached'] break;
);
case 'redis': case 'redis':
$port = $config['cache']['redis'][1]; self::$cache = new Redis();
$port = empty($port) ? null : intval($port);
return new RedisCacheDriver( $ret = explode(':', $config['cache']['redis'][0]);
$config['cache']['prefix'], if (count($ret) > 0) {
$config['cache']['redis'][0], // Unix socket.
$port, self::$cache->connect($ret[1]);
$config['cache']['redis'][2], } else {
$config['cache']['redis'][3] // IP + port.
); self::$cache->connect($ret[0], $config['cache']['redis'][1]);
case 'apcu': }
return new ApcuCacheDriver;
case 'fs': if ($config['cache']['redis'][2]) {
return new FsCacheDriver( self::$cache->auth($config['cache']['redis'][2]);
$config['cache']['prefix'], }
"tmp/cache/{$config['cache']['prefix']}", self::$cache->select($config['cache']['redis'][3]) or die('cache select failure');
'.lock', break;
$config['auto_maintenance'] ? 1000 : false
);
case 'none':
return new NoneCacheDriver();
case 'php': case 'php':
default: self::$cache = array();
return new ArrayCacheDriver(); break;
} }
} }
public static function getCache(): CacheDriver {
static $cache;
return $cache ??= self::buildCache();
}
public static function get($key) { public static function get($key) {
global $config, $debug; global $config, $debug;
$ret = self::getCache()->get($key); $key = $config['cache']['prefix'] . $key;
if ($ret === null) {
$ret = false; $data = false;
switch ($config['cache']['enabled']) {
case 'memcached':
if (!self::$cache)
self::init();
$data = self::$cache->get($key);
break;
case 'apc':
$data = apc_fetch($key);
break;
case 'xcache':
$data = xcache_get($key);
break;
case 'php':
$data = isset(self::$cache[$key]) ? self::$cache[$key] : false;
break;
case 'fs':
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
if (!file_exists('tmp/cache/'.$key)) {
$data = false;
}
else {
$data = file_get_contents('tmp/cache/'.$key);
$data = json_decode($data, true);
}
break;
case 'redis':
if (!self::$cache)
self::init();
$data = json_decode(self::$cache->get($key), true);
break;
} }
if ($config['debug']) { if ($config['debug'])
$debug['cached'][] = $config['cache']['prefix'] . $key . ($ret === false ? ' (miss)' : ' (hit)'); $debug['cached'][] = $key . ($data === false ? ' (miss)' : ' (hit)');
}
return $ret; return $data;
} }
public static function set($key, $value, $expires = false) { public static function set($key, $value, $expires = false) {
global $config, $debug; global $config, $debug;
if (!$expires) { $key = $config['cache']['prefix'] . $key;
if (!$expires)
$expires = $config['cache']['timeout']; $expires = $config['cache']['timeout'];
switch ($config['cache']['enabled']) {
case 'memcached':
if (!self::$cache)
self::init();
self::$cache->set($key, $value, $expires);
break;
case 'redis':
if (!self::$cache)
self::init();
self::$cache->setex($key, $expires, json_encode($value));
break;
case 'apc':
apc_store($key, $value, $expires);
break;
case 'xcache':
xcache_set($key, $value, $expires);
break;
case 'fs':
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
file_put_contents('tmp/cache/'.$key, json_encode($value));
break;
case 'php':
self::$cache[$key] = $value;
break;
} }
self::getCache()->set($key, $value, $expires); if ($config['debug'])
$debug['cached'][] = $key . ' (set)';
if ($config['debug']) {
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (set)';
}
} }
public static function delete($key) { public static function delete($key) {
global $config, $debug; global $config, $debug;
self::getCache()->delete($key); $key = $config['cache']['prefix'] . $key;
if ($config['debug']) { switch ($config['cache']['enabled']) {
$debug['cached'][] = $config['cache']['prefix'] . $key . ' (deleted)'; case 'memcached':
if (!self::$cache)
self::init();
self::$cache->delete($key);
break;
case 'redis':
if (!self::$cache)
self::init();
self::$cache->del($key);
break;
case 'apc':
apc_delete($key);
break;
case 'xcache':
xcache_unset($key);
break;
case 'fs':
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
@unlink('tmp/cache/'.$key);
break;
case 'php':
unset(self::$cache[$key]);
break;
} }
if ($config['debug'])
$debug['cached'][] = $key . ' (deleted)';
} }
public static function flush() { public static function flush() {
self::getCache()->flush(); global $config;
switch ($config['cache']['enabled']) {
case 'memcached':
if (!self::$cache)
self::init();
return self::$cache->flush();
case 'apc':
return apc_clear_cache('user');
case 'php':
self::$cache = array();
break;
case 'fs':
$files = glob('tmp/cache/*');
foreach ($files as $file) {
unlink($file);
}
break;
case 'redis':
if (!self::$cache)
self::init();
return self::$cache->flushDB();
}
return false; return false;
} }
} }

View file

@ -63,29 +63,9 @@
// been generated. This keeps the script from querying the database and causing strain when not needed. // been generated. This keeps the script from querying the database and causing strain when not needed.
$config['has_installed'] = '.installed'; $config['has_installed'] = '.installed';
// Deprecated, use 'log_system'. // Use syslog() for logging all error messages and unauthorized login attempts.
$config['syslog'] = false; $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. // Use `host` via shell_exec() to lookup hostnames, avoiding query timeouts. May not work on your system.
// Requires safe_mode to be disabled. // Requires safe_mode to be disabled.
$config['dns_system'] = false; $config['dns_system'] = false;
@ -137,26 +117,18 @@
/* /*
* On top of the static file caching system, you can enable the additional caching system which is * On top of the static file caching system, you can enable the additional caching system which is
* designed to minimize request processing can significantly increase speed when posting or using * designed to minimize SQL queries and can significantly increase speed when posting or using the
* the moderator interface. * moderator interface. APC is the recommended method of caching.
* *
* https://github.com/vichan-devel/vichan/wiki/cache * http://tinyboard.org/docs/index.php?p=Config/Cache
*/ */
// Uses a PHP array. MUST NOT be used in multiprocess environments.
$config['cache']['enabled'] = 'php'; $config['cache']['enabled'] = 'php';
// The recommended in-memory method of caching. Requires the extension. Due to how APCu works, this should be // $config['cache']['enabled'] = 'xcache';
// disabled when you run tools from the cli. // $config['cache']['enabled'] = 'apc';
// $config['cache']['enabled'] = 'apcu';
// The Memcache server. Requires the memcached extension, with a final D.
// $config['cache']['enabled'] = 'memcached'; // $config['cache']['enabled'] = 'memcached';
// The Redis server. Requires the extension.
// $config['cache']['enabled'] = 'redis'; // $config['cache']['enabled'] = 'redis';
// Use the local cache folder. Slower than native but available out of the box and compatible with multiprocess
// environments. You can mount a ram-based filesystem in the cache directory to improve performance.
// $config['cache']['enabled'] = 'fs'; // $config['cache']['enabled'] = 'fs';
// Technically available, offers a no-op fake cache. Don't use this outside of testing or debugging.
// $config['cache']['enabled'] = 'none';
// Timeout for cached objects such as posts and HTML. // Timeout for cached objects such as posts and HTML.
$config['cache']['timeout'] = 60 * 60 * 48; // 48 hours $config['cache']['timeout'] = 60 * 60 * 48; // 48 hours
@ -172,7 +144,7 @@
// Redis server to use. Location, port, password, database id. // Redis server to use. Location, port, password, database id.
// Note that Tinyboard may clear the database at times, so you may want to pick a database id just for // Note that Tinyboard may clear the database at times, so you may want to pick a database id just for
// Tinyboard to use. // Tinyboard to use.
$config['cache']['redis'] = [ 'localhost', 6379, null, 1 ]; $config['cache']['redis'] = array('localhost', 6379, '', 1);
// EXPERIMENTAL: Should we cache configs? Warning: this changes board behaviour, i'd say, a lot. // EXPERIMENTAL: Should we cache configs? Warning: this changes board behaviour, i'd say, a lot.
// If you have any lambdas/includes present in your config, you should move them to instance-functions.php // If you have any lambdas/includes present in your config, you should move them to instance-functions.php
@ -220,9 +192,6 @@
// Used to salt secure tripcodes ("##trip") and poster IDs (if enabled). // Used to salt secure tripcodes ("##trip") and poster IDs (if enabled).
$config['secure_trip_salt'] = ')(*&^%$#@!98765432190zyxwvutsrqponmlkjihgfedcba'; $config['secure_trip_salt'] = ')(*&^%$#@!98765432190zyxwvutsrqponmlkjihgfedcba';
// Used to salt poster passwords.
$config['secure_password_salt'] = 'wKJSb7M5SyzMcFWD2gPO3j2RYUSO9B789!@#$%^&*()';
/* /*
* ==================== * ====================
* Flood/spam settings * Flood/spam settings
@ -267,9 +236,6 @@
// To prevent bump atacks; returns the thread to last position after the last post is deleted. // To prevent bump atacks; returns the thread to last position after the last post is deleted.
$config['anti_bump_flood'] = false; $config['anti_bump_flood'] = false;
// Reject thread creation from IPs without any prior post history.
$config['op_require_history'] = false;
/* /*
* Introduction to Tinyboard's spam filter: * Introduction to Tinyboard's spam filter:
* *
@ -335,8 +301,9 @@
'lock', 'lock',
'raw', 'raw',
'embed', 'embed',
'captcha-response', 'g-recaptcha-response',
'captcha-form-id', 'h-captcha-response',
'cf-turnstile-response',
'spoiler', 'spoiler',
'page', 'page',
'file_url', 'file_url',
@ -363,40 +330,33 @@
'answer' => '4' 'answer' => '4'
); );
*/ */
// Enable a captcha system to make spam even harder. Rarely necessary. /**
$config['captcha'] = [ * The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set
/** * to 1.
* Select the captcha backend, false to disable. * Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses.
* Can be false, "recaptcha", "hcaptcha" or "turnstile". */
*/ $config['dynamic_captcha'] = false;
'mode' => false,
/** // Enable reCaptcha to make spam even harder. Rarely necessary.
* The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set $config['recaptcha'] = false;
* to 1.
* Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses. // Public and private key pair from https://www.google.com/recaptcha/admin/create
*/ $config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f';
'dynamic' => false, $config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_';
// Require to be non-zero if you use js/ajax.js (preferably no more than a few seconds), otherwise weird errors might occur.
'passthrough_timeout' => 0, // Enable hCaptcha.
// Configure Google reCAPTCHA. $config['hcaptcha'] = false;
'recaptcha' => [
// Public and private key pair from https://www.google.com/recaptcha/admin/create // Public and private key pair for using hCaptcha.
'public' => '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f', $config['hcaptcha_public'] = '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0';
'private' => '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_', $config['hcaptcha_private'] = '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17';
],
// Configure hCaptcha. // Enable Cloudflare's Turnstile captcha.
'hcaptcha' => [ $config['turnstile'] = false;
// Public and private key pair for using hCaptcha.
'public' => '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0', // Public and private key pair for turnstile.
'private' => '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17', $config['turnstile_public'] = '';
], $config['turnstile_private'] = '';
// Configure Cloudflare Turnstile.
'turnstile' => [
// Public and private key pair for turnstile.
'public' => '',
'private' => '',
]
];
// Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board // Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board
$config['board_locked'] = false; $config['board_locked'] = false;
@ -587,10 +547,6 @@
// Requires $config['strip_combining_chars'] = true; // Requires $config['strip_combining_chars'] = true;
$config['max_combining_chars'] = 0; $config['max_combining_chars'] = 0;
// Maximum OP body length. Ignored if force_body_op is set to false.
$config['max_body_op'] = 1800;
// Minimum OP body length. Ignored if force_body_op is set to false.
$config['min_body_op'] = 0;
// Maximum post body length. // Maximum post body length.
$config['max_body'] = 1800; $config['max_body'] = 1800;
// Minimum post body length. // Minimum post body length.
@ -753,18 +709,18 @@
// a link to an email address or IRC chat room to appeal the ban. // a link to an email address or IRC chat room to appeal the ban.
$config['ban_page_extra'] = ''; $config['ban_page_extra'] = '';
// Pre-configured ban reasons that pre-fill the ban form when clicked. // Pre-configured ban reasons that pre-fill the ban form when clicked.
// To disable, set $config['ban_reasons'] = false; // To disable, set $config['ban_reasons'] = false;
$config['ban_reasons'] = array( $config['ban_reasons'] = array(
array( 'reason' => 'Low-quality posting', array( 'reason' => 'Low-quality posting',
'length' => '1d'), 'length' => '1d'),
array( 'reason' => 'Off-topic', array( 'reason' => 'Off-topic',
'length' => '1d'), 'length' => '1d'),
array( 'reason' => 'Ban evasion', array( 'reason' => 'Ban evasion',
'length' => '30d'), 'length' => '30d'),
array( 'reason' => 'Illegal content', array( 'reason' => 'Illegal content',
'length' => ''), 'length' => ''),
); );
// How often (minimum) to purge the ban list of expired bans (which have been seen). // How often (minimum) to purge the ban list of expired bans (which have been seen).
$config['purge_bans'] = 60 * 60 * 12; // 12 hours $config['purge_bans'] = 60 * 60 * 12; // 12 hours
@ -943,6 +899,10 @@
// Location of thumbnail to use for deleted images. // Location of thumbnail to use for deleted images.
$config['image_deleted'] = 'static/deleted.png'; $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. // Maximum image upload size in bytes.
$config['max_filesize'] = 10 * 1024 * 1024; // 10MB $config['max_filesize'] = 10 * 1024 * 1024; // 10MB
// Maximum image dimensions. // Maximum image dimensions.
@ -981,6 +941,15 @@
// Set this to true if you're using Linux and you can execute `md5sum` binary. // Set this to true if you're using Linux and you can execute `md5sum` binary.
$config['gnu_md5'] = false; $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 // Number of posts in a "View Last X Posts" page
$config['noko50_count'] = 50; $config['noko50_count'] = 50;
// Number of posts a thread needs before it gets a "View Last X Posts" page. // Number of posts a thread needs before it gets a "View Last X Posts" page.
@ -1202,22 +1171,10 @@
// Custom embedding (YouTube, vimeo, etc.) // Custom embedding (YouTube, vimeo, etc.)
// It's very important that you match the entire input (with ^ and $) or things will not work correctly. // It's very important that you match the entire input (with ^ and $) or things will not work correctly.
$config['embedding'] = array( $config['embedding'] = array(
[ array(
'/^(?:(?:https?:)?\/\/)?((?:www|m)\.)?(?:(?:youtube(?:-nocookie)?\.com|youtu\.be))(?:\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]{11})((?:\?|\&)\S+)?$/i', '/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i',
'<div class="video-container" data-video-id="$2" data-iframe-width="360" data-iframe-height="202"> '<iframe style="float: left;margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" id="ytplayer" src="http://www.youtube.com/embed/$2"></iframe>'
<a href="https://youtu.be/$2" target="_blank" class="file"> ),
<img style="width:360px;height:202px;object-fit:cover" src="https://img.youtube.com/vi/$2/0.jpg" class="post-image"/>
</a>
</div>'
],
[
'/^https?:\/\/(\w+\.)?youtube\.com\/shorts\/([a-zA-Z0-9\-_]{10,11})(\?.*)?$/i',
'<div class="video-container" data-video-id="$2" data-iframe-width="202" data-iframe-height="360">
<a href="https://youtu.be/$2" target="_blank" class="file">
<img style="width:202px;height:360px;object-fit:cover" src="https://img.youtube.com/vi/$2/0.jpg" class="post-image"/>
</a>
</div>'
],
array( array(
'/^https?:\/\/(\w+\.)?vimeo\.com\/(\d{2,10})(\?.+)?$/i', '/^https?:\/\/(\w+\.)?vimeo\.com\/(\d{2,10})(\?.+)?$/i',
'<iframe src="https://player.vimeo.com/video/$2" style="float: left;margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>' '<iframe src="https://player.vimeo.com/video/$2" style="float: left;margin: 10px 20px;" width="%%tb_width%%" height="%%tb_height%%" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>'
@ -1234,18 +1191,10 @@
'/^https?:\/\/video\.google\.com\/videoplay\?docid=(\d+)([&#](.+)?)?$/i', '/^https?:\/\/video\.google\.com\/videoplay\?docid=(\d+)([&#](.+)?)?$/i',
'<embed src="http://video.google.com/googleplayer.swf?docid=$1&hl=en&fs=true" style="width:%%tb_width%%px;height:%%tb_height%%px;float:left;margin:10px 20px" allowFullScreen="true" allowScriptAccess="always" type="application/x-shockwave-flash"></embed>' '<embed src="http://video.google.com/googleplayer.swf?docid=$1&hl=en&fs=true" style="width:%%tb_width%%px;height:%%tb_height%%px;float:left;margin:10px 20px" allowFullScreen="true" allowScriptAccess="always" type="application/x-shockwave-flash"></embed>'
), ),
[ array(
'/^https?:\/\/(\w+\.)?vocaroo\.com\/i\/([a-zA-Z0-9]{2,15})$/i', '/^https?:\/\/(\w+\.)?vocaroo\.com\/i\/([a-zA-Z0-9]{2,15})$/i',
'<iframe style="float:left;margin:10px 1em 10px 0" width="300" height="60" src="https://vocaroo.com/embed/$2?autoplay=0" frameborder="0" allow="autoplay"></iframe>' '<object style="float: left;margin: 10px 20px;" width="148" height="44"><param name="movie" value="http://vocaroo.com/player.swf?playMediaID=$2&autoplay=0"><param name="wmode" value="transparent"><embed src="http://vocaroo.com/player.swf?playMediaID=$2&autoplay=0" width="148" height="44" wmode="transparent" type="application/x-shockwave-flash"></object>'
], )
[
'/^https?:\/\/(\w+\.)?voca\.ro\/([a-zA-Z0-9]{2,15})$/i',
'<iframe style="float:left;margin:10px 1em 10px 0" width="300" height="60" src="https://vocaroo.com/embed/$2?autoplay=0" frameborder="0" allow="autoplay"></iframe>'
],
[
'/^https?:\/\/(\w+\.)?vocaroo\.com\/([a-zA-Z0-9]{2,15})#?$/i',
'<iframe style="float:left;margin:10px 1em 10px 0" width="300" height="60" src="https://vocaroo.com/embed/$2?autoplay=0" frameborder="0" allow="autoplay"></iframe>'
]
); );
// Embedding width and height. // Embedding width and height.
@ -1261,7 +1210,6 @@
// Error messages // Error messages
$config['error']['bot'] = _('You look like a bot.'); $config['error']['bot'] = _('You look like a bot.');
$config['error']['referer'] = _('Your browser sent an invalid or no HTTP referer.'); $config['error']['referer'] = _('Your browser sent an invalid or no HTTP referer.');
$config['error']['opnohistory'] = _('You must post at least once before creating thread.');
$config['error']['toolong'] = _('The %s field was too long.'); $config['error']['toolong'] = _('The %s field was too long.');
$config['error']['toolong_body'] = _('The body was too long.'); $config['error']['toolong_body'] = _('The body was too long.');
$config['error']['tooshort_body'] = _('The body was too short or empty.'); $config['error']['tooshort_body'] = _('The body was too short or empty.');
@ -1551,8 +1499,8 @@
// Do DNS lookups on IP addresses to get their hostname for the moderator IP pages (?/IP/x.x.x.x). // Do DNS lookups on IP addresses to get their hostname for the moderator IP pages (?/IP/x.x.x.x).
$config['mod']['dns_lookup'] = true; $config['mod']['dns_lookup'] = true;
// How many recent posts, per board, to show in ?/user_posts/ip/x.x.x.x. and ?/user_posts/passwd/xxxxxxxx // How many recent posts, per board, to show in ?/IP/x.x.x.x.
$config['mod']['recent_user_posts'] = 5; $config['mod']['ip_recentposts'] = 5;
// Number of posts to display on the reports page. // Number of posts to display on the reports page.
$config['mod']['recent_reports'] = 10; $config['mod']['recent_reports'] = 10;
@ -1941,24 +1889,22 @@
*/ */
// Matrix integration for reports // Matrix integration for reports
$config['matrix'] = [ // $config['matrix'] = array(
'enabled' => false, // 'access_token' => 'ACCESS_TOKEN',
'access_token' => 'ACCESS_TOKEN', // 'room_id' => '%21askjdlkajsdlka:matrix.org',
// Note: must be already url-escaped. // 'host' => 'https://matrix.org',
'room_id' => '%21askjdlkajsdlka:matrix.org', // 'max_message_length' => 240
'host' => 'https://matrix.org', // );
'max_message_length' => 240
];
//Securimage captcha //Securimage captcha
//Note from lainchan PR: "TODO move a bunch of things here" //Note from lainchan PR: "TODO move a bunch of things here"
$config['spam']['valid_inputs'][]='captcha'; $config['spam']['valid_inputs'][]='captcha';
$config['error']['securimage']=array( $config['error']['securimage']=array(
'missing'=>'The captcha field was missing. Please try again', 'missing'=>'The captcha field was missing. Please try again',
'empty'=>'Please fill out the captcha', 'empty'=>'Please fill out the captcha',
'bad'=>'Incorrect or expired captcha', 'bad'=>'Incorrect or expired captcha',
); );
// Meta keywords. It's probably best to include these in per-board configurations. // Meta keywords. It's probably best to include these in per-board configurations.
// $config['meta_keywords'] = 'chan,anonymous discussion,imageboard,tinyboard'; // $config['meta_keywords'] = 'chan,anonymous discussion,imageboard,tinyboard';
@ -2030,6 +1976,12 @@
// is the absolute maximum, because MySQL cannot handle table names greater than 64 characters. // is the absolute maximum, because MySQL cannot handle table names greater than 64 characters.
$config['board_regex'] = '[0-9a-zA-Z$_\x{0080}-\x{FFFF}]{1,58}'; $config['board_regex'] = '[0-9a-zA-Z$_\x{0080}-\x{FFFF}]{1,58}';
// Youtube.js embed HTML code
$config['youtube_js_html'] = '<div class="video-container" data-video="$2">'.
'<a href="https://youtu.be/$2" target="_blank" class="file">'.
'<img style="width:360px;height:270px;" src="//img.youtube.com/vi/$2/0.jpg" class="post-image"/>'.
'</a></div>';
// Slack Report Notification // Slack Report Notification
$config['slack'] = false; $config['slack'] = false;
$config['slack_channel'] = ""; $config['slack_channel'] = "";

View file

@ -1,82 +0,0 @@
<?php
namespace Vichan;
use Vichan\Data\{IpNoteQueries, ReportQueries, UserPostQueries};
use Vichan\Data\Driver\{CacheDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver};
defined('TINYBOARD') or exit;
class Context {
private array $definitions;
public function __construct(array $definitions) {
$this->definitions = $definitions;
}
public function get(string $name): mixed {
if (!isset($this->definitions[$name])) {
throw new \RuntimeException("Could not find a dependency named $name");
}
$ret = $this->definitions[$name];
if (is_callable($ret) && !is_string($ret) && !is_array($ret)) {
$ret = $ret($this);
$this->definitions[$name] = $ret;
}
return $ret;
}
}
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();
},
\PDO::class => function($c) {
global $pdo;
// Ensure the PDO is initialized.
sql_open();
return $pdo;
},
ReportQueries::class => function($c) {
$auto_maintenance = (bool)$c->get('config')['auto_maintenance'];
$pdo = $c->get(\PDO::class);
return new ReportQueries($pdo, $auto_maintenance);
},
UserPostQueries::class => function($c) {
return new UserPostQueries($c->get(\PDO::class));
},
IpNoteQueries::class => fn($c) => new IpNoteQueries($c->get(\PDO::class), $c->get(CacheDriver::class)),
]);
}

View file

@ -72,7 +72,6 @@ function sql_open() {
try { try {
$options = [ $options = [
PDO::ATTR_TIMEOUT => $config['db']['timeout'], PDO::ATTR_TIMEOUT => $config['db']['timeout'],
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // Set a consistent error mode between PHP versions.
]; ];
if ($config['db']['type'] == "mysql") if ($config['db']['type'] == "mysql")
@ -101,6 +100,12 @@ function sql_open() {
} }
} }
// 5.6.10 becomes 50610 HACK: hardcoded to be above critical value 50803 due to laziness
function mysql_version() {
// TODO delete all references of this function everywhere
return 80504;
}
function prepare($query) { function prepare($query) {
global $pdo, $debug, $config; global $pdo, $debug, $config;

View file

@ -4,26 +4,23 @@
* Copyright (c) 2010-2013 Tinyboard Development Group * Copyright (c) 2010-2013 Tinyboard Development Group
*/ */
use Vichan\Context;
use Vichan\Data\IpNoteQueries;
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
class Filter { class Filter {
public $flood_check; public $flood_check;
private $condition; private $condition;
private $post; private $post;
public function __construct(array $arr) { public function __construct(array $arr) {
foreach ($arr as $key => $value) foreach ($arr as $key => $value)
$this->$key = $value; $this->$key = $value;
} }
public function match($condition, $match) { public function match($condition, $match) {
$condition = strtolower($condition); $condition = strtolower($condition);
$post = &$this->post; $post = &$this->post;
switch($condition) { switch($condition) {
case 'custom': case 'custom':
if (!is_callable($match)) if (!is_callable($match))
@ -32,11 +29,11 @@ class Filter {
case 'flood-match': case 'flood-match':
if (!is_array($match)) if (!is_array($match))
error('Filter condition "flood-match" must be an array.'); error('Filter condition "flood-match" must be an array.');
// Filter out "flood" table entries which do not match this filter. // Filter out "flood" table entries which do not match this filter.
$flood_check_matched = array(); $flood_check_matched = array();
foreach ($this->flood_check as $flood_post) { foreach ($this->flood_check as $flood_post) {
foreach ($match as $flood_match_arg) { foreach ($match as $flood_match_arg) {
switch ($flood_match_arg) { switch ($flood_match_arg) {
@ -72,10 +69,10 @@ class Filter {
} }
$flood_check_matched[] = $flood_post; $flood_check_matched[] = $flood_post;
} }
// is there any reason for this assignment? // is there any reason for this assignment?
$this->flood_check = $flood_check_matched; $this->flood_check = $flood_check_matched;
return !empty($this->flood_check); return !empty($this->flood_check);
case 'flood-time': case 'flood-time':
foreach ($this->flood_check as $flood_post) { foreach ($this->flood_check as $flood_post) {
@ -138,42 +135,46 @@ class Filter {
error('Unknown filter condition: ' . $condition); error('Unknown filter condition: ' . $condition);
} }
} }
public function action(Context $ctx) { public function action() {
global $board; global $board;
$this->add_note = isset($this->add_note) ? $this->add_note : false; $this->add_note = isset($this->add_note) ? $this->add_note : false;
if ($this->add_note) { if ($this->add_note) {
$note_queries = $ctx->get(IpNoteQueries::class); $query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
$note_queries->add($_SERVER['REMOTE_ADDR'], -1, 'Autoban message: ' . $this->post['body']); $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
} $query->bindValue(':mod', -1);
$query->bindValue(':time', time());
$query->bindValue(':body', "Autoban message: ".$this->post['body']);
$query->execute() or error(db_error($query));
}
if (isset ($this->action)) switch($this->action) { if (isset ($this->action)) switch($this->action) {
case 'reject': case 'reject':
error(isset($this->message) ? $this->message : 'Posting blocked by filter.'); error(isset($this->message) ? $this->message : 'Posting blocked by filter.');
case 'ban': case 'ban':
if (!isset($this->reason)) if (!isset($this->reason))
error('The ban action requires a reason.'); error('The ban action requires a reason.');
$this->expires = isset($this->expires) ? $this->expires : false; $this->expires = isset($this->expires) ? $this->expires : false;
$this->reject = isset($this->reject) ? $this->reject : true; $this->reject = isset($this->reject) ? $this->reject : true;
$this->all_boards = isset($this->all_boards) ? $this->all_boards : false; $this->all_boards = isset($this->all_boards) ? $this->all_boards : false;
Bans::new_ban($_SERVER['REMOTE_ADDR'], $this->reason, $this->expires, $this->all_boards ? false : $board['uri'], -1); Bans::new_ban($_SERVER['REMOTE_ADDR'], $this->reason, $this->expires, $this->all_boards ? false : $board['uri'], -1);
if ($this->reject) { if ($this->reject) {
if (isset($this->message)) if (isset($this->message))
error($message); error($message);
checkBan($board['uri']); checkBan($board['uri']);
exit; exit;
} }
break; break;
default: default:
error('Unknown filter action: ' . $this->action); error('Unknown filter action: ' . $this->action);
} }
} }
public function check(array $post) { public function check(array $post) {
$this->post = $post; $this->post = $post;
foreach ($this->condition as $condition => $value) { foreach ($this->condition as $condition => $value) {
@ -183,7 +184,7 @@ class Filter {
} else { } else {
$NOT = false; $NOT = false;
} }
if ($this->match($condition, $value) == $NOT) if ($this->match($condition, $value) == $NOT)
return false; return false;
} }
@ -193,11 +194,11 @@ class Filter {
function purge_flood_table() { function purge_flood_table() {
global $config; global $config;
// Determine how long we need to keep a cache of posts for flood prevention. Unfortunately, it is not // Determine how long we need to keep a cache of posts for flood prevention. Unfortunately, it is not
// aware of flood filters in other board configurations. You can solve this problem by settings the // aware of flood filters in other board configurations. You can solve this problem by settings the
// config variable $config['flood_cache'] (seconds). // config variable $config['flood_cache'] (seconds).
if (isset($config['flood_cache'])) { if (isset($config['flood_cache'])) {
$max_time = &$config['flood_cache']; $max_time = &$config['flood_cache'];
} else { } else {
@ -207,18 +208,18 @@ function purge_flood_table() {
$max_time = max($max_time, $filter['condition']['flood-time']); $max_time = max($max_time, $filter['condition']['flood-time']);
} }
} }
$time = time() - $max_time; $time = time() - $max_time;
query("DELETE FROM ``flood`` WHERE `time` < $time") or error(db_error()); query("DELETE FROM ``flood`` WHERE `time` < $time") or error(db_error());
} }
function do_filters(Context $ctx, array $post) { function do_filters(array $post) {
global $config; global $config;
if (!isset($config['filters']) || empty($config['filters'])) if (!isset($config['filters']) || empty($config['filters']))
return; return;
foreach ($config['filters'] as $filter) { foreach ($config['filters'] as $filter) {
if (isset($filter['condition']['flood-match'])) { if (isset($filter['condition']['flood-match'])) {
$has_flood = true; $has_flood = true;
@ -231,15 +232,15 @@ function do_filters(Context $ctx, array $post) {
} else { } else {
$flood_check = false; $flood_check = false;
} }
foreach ($config['filters'] as $filter_array) { foreach ($config['filters'] as $filter_array) {
$filter = new Filter($filter_array); $filter = new Filter($filter_array);
$filter->flood_check = $flood_check; $filter->flood_check = $flood_check;
if ($filter->check($post)) { if ($filter->check($post)) {
$filter->action($ctx); $filter->action();
} }
} }
purge_flood_table(); purge_flood_table();
} }

View file

@ -355,12 +355,9 @@ function define_groups() {
} }
function create_antibot($board, $thread = null) { function create_antibot($board, $thread = null) {
global $pdo; require_once dirname(__FILE__) . '/anti-bot.php';
// Ensure $pdo is initialized. return _create_antibot($board, $thread);
sql_open();
return _create_antibot($pdo, $board, $thread);
} }
function rebuildThemes($action, $boardname = false) { function rebuildThemes($action, $boardname = false) {
@ -745,23 +742,24 @@ function hasPermission($action = null, $board = null, $_mod = null) {
function listBoards($just_uri = false) { function listBoards($just_uri = false) {
global $config; global $config;
$cache_name = $just_uri ? 'all_boards_uri' : 'all_boards'; $just_uri ? $cache_name = 'all_boards_uri' : $cache_name = 'all_boards';
if ($config['cache']['enabled'] && ($boards = cache::get($cache_name))) { if ($config['cache']['enabled'] && ($boards = cache::get($cache_name)))
return $boards; return $boards;
}
if (!$just_uri) { if (!$just_uri) {
$query = query('SELECT * FROM ``boards`` ORDER BY `uri`'); $query = query("SELECT * FROM ``boards`` ORDER BY `uri`") or error(db_error());
$boards = $query->fetchAll(); $boards = $query->fetchAll();
} else { } else {
$query = query('SELECT `uri` FROM ``boards``'); $boards = array();
$boards = $query->fetchAll(\PDO::FETCH_COLUMN); $query = query("SELECT `uri` FROM ``boards``") or error(db_error());
while ($board = $query->fetchColumn()) {
$boards[] = $board;
}
} }
if ($config['cache']['enabled']) { if ($config['cache']['enabled'])
cache::set($cache_name, $boards); cache::set($cache_name, $boards);
}
return $boards; return $boards;
} }
@ -920,48 +918,6 @@ function checkBan($board = false) {
} }
} }
/**
* Checks if the given IP has any previous posts.
*
* @param string $ip The IP to check.
* @param ?string $passwd If not null, check also by password.
* @return bool True if the ip has already sent at least one post, false otherwise.
*/
function has_any_history(string $ip, ?string $passwd): bool {
global $config;
if ($config['cache']['enabled']) {
$ret = cache::get("post_history_$ip");
if ($ret !== false) {
return $ret !== 0x0;
}
}
foreach (listBoards(true) as $board_uri) {
if ($passwd === null) {
$query = prepare(sprintf('SELECT `id` FROM ``posts_%s`` WHERE `ip` = :ip LIMIT 1', $board_uri));
$query->bindValue(':ip', $ip);
} else {
$query = prepare(sprintf('SELECT `id` FROM ``posts_%s`` WHERE `ip` = :ip OR `password` = :passwd LIMIT 1', $board_uri));
$query->bindValue(':ip', $ip);
$query->bindValue(':passwd', $passwd);
}
$query->execute() or error(db_error());
if ($query->fetchColumn() !== false) {
// Found a post.
if ($config['cache']['enabled']) {
cache::set("post_history_$ip", 0xA);
}
return true;
}
}
if ($config['cache']['enabled']) {
cache::set("post_history_$ip", 0x0);
}
return false;
}
function threadLocked($id) { function threadLocked($id) {
global $board; global $board;
@ -2069,7 +2025,7 @@ function remove_modifiers($body) {
return preg_replace('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@usm', '', $body); return preg_replace('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@usm', '', $body);
} }
function markup(&$body, $track_cites = false) { function markup(&$body, $track_cites = false, $op = false) {
global $board, $config, $markup_urls; global $board, $config, $markup_urls;
$modifiers = extract_modifiers($body); $modifiers = extract_modifiers($body);
@ -2084,6 +2040,9 @@ function markup(&$body, $track_cites = false) {
$body = str_replace("\r", '', $body); $body = str_replace("\r", '', $body);
$body = utf8tohtml($body); $body = utf8tohtml($body);
if (mysql_version() < 50503)
$body = mb_encode_numericentity($body, array(0x010000, 0xffffff, 0, 0xffffff), 'UTF-8');
if ($config['markup_code']) { if ($config['markup_code']) {
$code_markup = array(); $code_markup = array();
$body = preg_replace_callback($config['markup_code'], function($matches) use (&$code_markup) { $body = preg_replace_callback($config['markup_code'], function($matches) use (&$code_markup) {
@ -2168,15 +2127,12 @@ function markup(&$body, $track_cites = false) {
link_for(array('id' => $cite, 'thread' => $cited_posts[$cite])) . '#' . $cite . '">' . link_for(array('id' => $cite, 'thread' => $cited_posts[$cite])) . '#' . $cite . '">' .
'&gt;&gt;' . $cite . '&gt;&gt;' . $cite .
'</a>'; '</a>';
} else {
$replacement = "<s>&gt;&gt;$cite</s>";
}
$body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[3][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0])); $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[3][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
$skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[3][0]) - mb_strlen($matches[0][0]); $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[3][0]) - mb_strlen($matches[0][0]);
if ($track_cites && $config['track_cites']) { if ($track_cites && $config['track_cites'])
$tracked_cites[] = array($board['uri'], $cite); $tracked_cites[] = array($board['uri'], $cite);
} }
} }
} }
@ -3085,8 +3041,3 @@ function strategy_first($fun, $array) {
return array('defer'); return array('defer');
} }
} }
function hashPassword($password) {
global $config;
return hash('sha3-256', $password . $config['secure_password_salt']);
}

View file

@ -15,63 +15,3 @@ function is_connection_https(): bool {
function is_connection_secure(): bool { function is_connection_secure(): bool {
return is_connection_https() || (!empty($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === '127.0.0.1'); return is_connection_https() || (!empty($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === '127.0.0.1');
} }
/**
* Encodes a string into a base64 variant without characters illegal in urls.
*/
function base64_url_encode(string $input): string {
return str_replace([ '+', '/', '=' ], [ '-', '_', '' ], base64_encode($input));
}
/**
* Decodes a string from a base64 variant without characters illegal in urls.
*/
function base64_url_decode(string $input): string {
return base64_decode(strtr($input, '-_', '+/'));
}
/**
* Encodes a typed cursor.
*
* @param string $type The type for the cursor. Only the first character is considered.
* @param array $map A map of key-value pairs to encode.
* @return string An encoded string that can be sent through urls. Empty if either parameter is empty.
*/
function encode_cursor(string $type, array $map): string {
if (empty($type) || empty($map)) {
return '';
}
$acc = $type[0];
foreach ($map as $key => $value) {
$acc .= "|$key#$value";
}
return base64_url_encode($acc);
}
/**
* Decodes a typed cursor.
*
* @param string $cursor A string emitted by `encode_cursor`.
* @return array An array with the type of the cursor and an array of key-value pairs. The type is null and the map
* empty if either there are no key-value pairs or the encoding is incorrect.
*/
function decode_cursor(string $cursor): array {
$map = [];
$type = '';
$acc = base64_url_decode($cursor);
if ($acc === false || empty($acc)) {
return [ null, [] ];
}
$type = $acc[0];
foreach (explode('|', substr($acc, 2)) as $pair) {
$pair = explode('#', $pair);
if (count($pair) >= 2) {
$key = $pair[0];
$value = $pair[1];
$map[$key] = $value;
}
}
return [ $type, $map ];
}

20
inc/lib/IP/LICENSE Executable file
View file

@ -0,0 +1,20 @@
Copyright (c) 2013 Jason Morriss
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

293
inc/lib/IP/Lifo/IP/BC.php Executable file
View file

@ -0,0 +1,293 @@
<?php
/**
* This file is part of the Lifo\IP PHP Library.
*
* (c) Jason Morriss <lifo2013@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Lifo\IP;
/**
* BCMath helper class.
*
* Provides a handful of BCMath routines that are not included in the native
* PHP library.
*
* Note: The Bitwise functions operate on fixed byte boundaries. For example,
* comparing the following numbers uses X number of bits:
* 0xFFFF and 0xFF will result in comparison of 16 bits.
* 0xFFFFFFFF and 0xF will result in comparison of 32 bits.
* etc...
*
*/
abstract class BC
{
// Some common (maybe useless) constants
const MAX_INT_32 = '2147483647'; // 7FFFFFFF
const MAX_UINT_32 = '4294967295'; // FFFFFFFF
const MAX_INT_64 = '9223372036854775807'; // 7FFFFFFFFFFFFFFF
const MAX_UINT_64 = '18446744073709551615'; // FFFFFFFFFFFFFFFF
const MAX_INT_96 = '39614081257132168796771975167'; // 7FFFFFFFFFFFFFFFFFFFFFFF
const MAX_UINT_96 = '79228162514264337593543950335'; // FFFFFFFFFFFFFFFFFFFFFFFF
const MAX_INT_128 = '170141183460469231731687303715884105727'; // 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
const MAX_UINT_128 = '340282366920938463463374607431768211455'; // FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
/**
* BC Math function to convert a HEX string into a DECIMAL
*/
public static function bchexdec($hex)
{
if (strlen($hex) == 1) {
return hexdec($hex);
}
$remain = substr($hex, 0, -1);
$last = substr($hex, -1);
return bcadd(bcmul(16, self::bchexdec($remain), 0), hexdec($last), 0);
}
/**
* BC Math function to convert a DECIMAL string into a BINARY string
*/
public static function bcdecbin($dec, $pad = null)
{
$bin = '';
while ($dec) {
$m = bcmod($dec, 2);
$dec = bcdiv($dec, 2, 0);
$bin = abs($m) . $bin;
}
return $pad ? sprintf("%0{$pad}s", $bin) : $bin;
}
/**
* BC Math function to convert a BINARY string into a DECIMAL string
*/
public static function bcbindec($bin)
{
$dec = '0';
for ($i=0, $j=strlen($bin); $i<$j; $i++) {
$dec = bcmul($dec, '2', 0);
$dec = bcadd($dec, $bin[$i], 0);
}
return $dec;
}
/**
* BC Math function to convert a BINARY string into a HEX string
*/
public static function bcbinhex($bin, $pad = 0)
{
return self::bcdechex(self::bcbindec($bin));
}
/**
* BC Math function to convert a DECIMAL into a HEX string
*/
public static function bcdechex($dec)
{
$last = bcmod($dec, 16);
$remain = bcdiv(bcsub($dec, $last, 0), 16, 0);
return $remain == 0 ? dechex($last) : self::bcdechex($remain) . dechex($last);
}
/**
* Bitwise AND two arbitrarily large numbers together.
*/
public static function bcand($left, $right)
{
$len = self::_bitwise($left, $right);
$value = '';
for ($i=0; $i<$len; $i++) {
$value .= (($left[$i] + 0) & ($right[$i] + 0)) ? '1' : '0';
}
return self::bcbindec($value != '' ? $value : '0');
}
/**
* Bitwise OR two arbitrarily large numbers together.
*/
public static function bcor($left, $right)
{
$len = self::_bitwise($left, $right);
$value = '';
for ($i=0; $i<$len; $i++) {
$value .= (($left[$i] + 0) | ($right[$i] + 0)) ? '1' : '0';
}
return self::bcbindec($value != '' ? $value : '0');
}
/**
* Bitwise XOR two arbitrarily large numbers together.
*/
public static function bcxor($left, $right)
{
$len = self::_bitwise($left, $right);
$value = '';
for ($i=0; $i<$len; $i++) {
$value .= (($left[$i] + 0) ^ ($right[$i] + 0)) ? '1' : '0';
}
return self::bcbindec($value != '' ? $value : '0');
}
/**
* Bitwise NOT two arbitrarily large numbers together.
*/
public static function bcnot($left, $bits = null)
{
$right = 0;
$len = self::_bitwise($left, $right, $bits);
$value = '';
for ($i=0; $i<$len; $i++) {
$value .= $left[$i] == '1' ? '0' : '1';
}
return self::bcbindec($value);
}
/**
* Shift number to the left
*
* @param integer $bits Total bits to shift
*/
public static function bcleft($num, $bits) {
return bcmul($num, bcpow('2', $bits));
}
/**
* Shift number to the right
*
* @param integer $bits Total bits to shift
*/
public static function bcright($num, $bits) {
return bcdiv($num, bcpow('2', $bits));
}
/**
* Determine how many bits are needed to store the number rounded to the
* nearest bit boundary.
*/
public static function bits_needed($num, $boundary = 4)
{
$bits = 0;
while ($num > 0) {
$num = bcdiv($num, '2', 0);
$bits++;
}
// round to nearest boundrary
return $boundary ? ceil($bits / $boundary) * $boundary : $bits;
}
/**
* BC Math function to return an arbitrarily large random number.
*/
public static function bcrand($min, $max = null)
{
if ($max === null) {
$max = $min;
$min = 0;
}
// swap values if $min > $max
if (bccomp($min, $max) == 1) {
list($min,$max) = array($max,$min);
}
return bcadd(
bcmul(
bcdiv(
mt_rand(0, mt_getrandmax()),
mt_getrandmax(),
strlen($max)
),
bcsub(
bcadd($max, '1'),
$min
)
),
$min
);
}
/**
* Computes the natural logarithm using a series.
* @author Thomas Oldbury.
* @license Public domain.
*/
public static function bclog($num, $iter = 10, $scale = 100)
{
$log = "0.0";
for($i = 0; $i < $iter; $i++) {
$pow = 1 + (2 * $i);
$mul = bcdiv("1.0", $pow, $scale);
$fraction = bcmul($mul, bcpow(bcsub($num, "1.0", $scale) / bcadd($num, "1.0", $scale), $pow, $scale), $scale);
$log = bcadd($fraction, $log, $scale);
}
return bcmul("2.0", $log, $scale);
}
/**
* Computes the base2 log using baseN log.
*/
public static function bclog2($num, $iter = 10, $scale = 100)
{
return bcdiv(self::bclog($num, $iter, $scale), self::bclog("2", $iter, $scale), $scale);
}
public static function bcfloor($num)
{
if (substr($num, 0, 1) == '-') {
return bcsub($num, 1, 0);
}
return bcadd($num, 0, 0);
}
public static function bcceil($num)
{
if (substr($num, 0, 1) == '-') {
return bcsub($num, 0, 0);
}
return bcadd($num, 1, 0);
}
/**
* Compare two numbers and return -1, 0, 1 depending if the LEFT number is
* < = > the RIGHT.
*
* @param string|integer $left Left side operand
* @param string|integer $right Right side operand
* @return integer Return -1,0,1 for <=> comparison
*/
public static function cmp($left, $right)
{
// @todo could an optimization be done to determine if a normal 32bit
// comparison could be done instead of using bccomp? But would
// the number verification cause too much overhead to be useful?
return bccomp($left, $right, 0);
}
/**
* Internal function to prepare for bitwise operations
*/
private static function _bitwise(&$left, &$right, $bits = null)
{
if ($bits === null) {
$bits = max(self::bits_needed($left), self::bits_needed($right));
}
$left = self::bcdecbin($left);
$right = self::bcdecbin($right);
$len = max(strlen($left), strlen($right), (int)$bits);
$left = sprintf("%0{$len}s", $left);
$right = sprintf("%0{$len}s", $right);
return $len;
}
}

706
inc/lib/IP/Lifo/IP/CIDR.php Executable file
View file

@ -0,0 +1,706 @@
<?php
/**
* This file is part of the Lifo\IP PHP Library.
*
* (c) Jason Morriss <lifo2013@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Lifo\IP;
/**
* CIDR Block helper class.
*
* Most routines can be used statically or by instantiating an object and
* calling its methods.
*
* Provides routines to do various calculations on IP addresses and ranges.
* Convert to/from CIDR to ranges, etc.
*/
class CIDR
{
const INTERSECT_NO = 0;
const INTERSECT_YES = 1;
const INTERSECT_LOW = 2;
const INTERSECT_HIGH = 3;
protected $start;
protected $end;
protected $prefix;
protected $version;
protected $istart;
protected $iend;
private $cache;
/**
* Create a new CIDR object.
*
* The IP range can be arbitrary and does not have to fall on a valid CIDR
* range. Some methods will return different values depending if you ignore
* the prefix or not. By default all prefix sensitive methods will assume
* the prefix is used.
*
* @param string $cidr An IP address (1.2.3.4), CIDR block (1.2.3.4/24),
* or range "1.2.3.4-1.2.3.10"
* @param string $end Ending IP in range if no cidr/prefix is given
*/
public function __construct($cidr, $end = null)
{
if ($end !== null) {
$this->setRange($cidr, $end);
} else {
$this->setCidr($cidr);
}
}
/**
* Returns the string representation of the CIDR block.
*/
public function __toString()
{
// do not include the prefix if its a single IP
try {
if ($this->isTrueCidr() && (
($this->version == 4 and $this->prefix != 32) ||
($this->version == 6 and $this->prefix != 128)
)
) {
return $this->start . '/' . $this->prefix;
}
} catch (\Exception $e) {
// isTrueCidr() calls getRange which can throw an exception
}
if (strcmp($this->start, $this->end) == 0) {
return $this->start;
}
return $this->start . ' - ' . $this->end;
}
public function __clone()
{
// do not clone the cache. No real reason why. I just want to keep the
// memory foot print as low as possible, even though this is trivial.
$this->cache = array();
}
/**
* Set an arbitrary IP range.
* The closest matching prefix will be calculated but the actual range
* stored in the object can be arbitrary.
* @param string $start Starting IP or combination "start-end" string.
* @param string $end Ending IP or null.
*/
public function setRange($ip, $end = null)
{
if (strpos($ip, '-') !== false) {
list($ip, $end) = array_map('trim', explode('-', $ip, 2));
}
if (false === filter_var($ip, FILTER_VALIDATE_IP) ||
false === filter_var($end, FILTER_VALIDATE_IP)) {
throw new \InvalidArgumentException("Invalid IP range \"$ip-$end\"");
}
// determine version (4 or 6)
$this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
$this->istart = IP::inet_ptod($ip);
$this->iend = IP::inet_ptod($end);
// fix order
if (bccomp($this->istart, $this->iend) == 1) {
list($this->istart, $this->iend) = array($this->iend, $this->istart);
list($ip, $end) = array($end, $ip);
}
$this->start = $ip;
$this->end = $end;
// calculate real prefix
$len = $this->version == 4 ? 32 : 128;
$this->prefix = $len - strlen(BC::bcdecbin(BC::bcxor($this->istart, $this->iend)));
}
/**
* Returns true if the current IP is a true cidr block
*/
public function isTrueCidr()
{
return $this->start == $this->getNetwork() && $this->end == $this->getBroadcast();
}
/**
* Set the CIDR block.
*
* The prefix length is optional and will default to 32 ot 128 depending on
* the version detected.
*
* @param string $cidr CIDR block string, eg: "192.168.0.0/24" or "2001::1/64"
* @throws \InvalidArgumentException If the CIDR block is invalid
*/
public function setCidr($cidr)
{
if (strpos($cidr, '-') !== false) {
return $this->setRange($cidr);
}
list($ip, $bits) = array_pad(array_map('trim', explode('/', $cidr, 2)), 2, null);
if (false === filter_var($ip, FILTER_VALIDATE_IP)) {
throw new \InvalidArgumentException("Invalid IP address \"$cidr\"");
}
// determine version (4 or 6)
$this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
$this->start = $ip;
$this->istart = IP::inet_ptod($ip);
if ($bits !== null and $bits !== '') {
$this->prefix = $bits;
} else {
$this->prefix = $this->version == 4 ? 32 : 128;
}
if (($this->prefix < 0)
|| ($this->prefix > 32 and $this->version == 4)
|| ($this->prefix > 128 and $this->version == 6)) {
throw new \InvalidArgumentException("Invalid IP address \"$cidr\"");
}
$this->end = $this->getBroadcast();
$this->iend = IP::inet_ptod($this->end);
$this->cache = array();
}
/**
* Get the IP version. 4 or 6.
*
* @return integer
*/
public function getVersion()
{
return $this->version;
}
/**
* Get the prefix.
*
* Always returns the "proper" prefix, even if the IP range is arbitrary.
*
* @return integer
*/
public function getPrefix()
{
return $this->prefix;
}
/**
* Return the starting presentational IP or Decimal value.
*
* Ignores prefix
*/
public function getStart($decimal = false)
{
return $decimal ? $this->istart : $this->start;
}
/**
* Return the ending presentational IP or Decimal value.
*
* Ignores prefix
*/
public function getEnd($decimal = false)
{
return $decimal ? $this->iend : $this->end;
}
/**
* Return the next presentational IP or Decimal value (following the
* broadcast address of the current CIDR block).
*/
public function getNext($decimal = false)
{
$next = bcadd($this->getEnd(true), '1');
return $decimal ? $next : new self(IP::inet_dtop($next));
}
/**
* Returns true if the IP is an IPv4
*
* @return boolean
*/
public function isIPv4()
{
return $this->version == 4;
}
/**
* Returns true if the IP is an IPv6
*
* @return boolean
*/
public function isIPv6()
{
return $this->version == 6;
}
/**
* Get the cidr notation for the subnet block.
*
* This is useful for when you want a string representation of the IP/prefix
* and the starting IP is not on a valid network boundrary (eg: Displaying
* an IP from an interface).
*
* @return string IP in CIDR notation "ipaddr/prefix"
*/
public function getCidr()
{
return $this->start . '/' . $this->prefix;
}
/**
* Get the [low,high] range of the CIDR block
*
* Prefix sensitive.
*
* @param boolean $ignorePrefix If true the arbitrary start-end range is
* returned. default=false.
*/
public function getRange($ignorePrefix = false)
{
$range = $ignorePrefix
? array($this->start, $this->end)
: self::cidr_to_range($this->start, $this->prefix);
// watch out for IP '0' being converted to IPv6 '::'
if ($range[0] == '::' and strpos($range[1], ':') == false) {
$range[0] = '0.0.0.0';
}
return $range;
}
/**
* Return the IP in its fully expanded form.
*
* For example: 2001::1 == 2007:0000:0000:0000:0000:0000:0000:0001
*
* @see IP::inet_expand
*/
public function getExpanded()
{
return IP::inet_expand($this->start);
}
/**
* Get network IP of the CIDR block
*
* Prefix sensitive.
*
* @param boolean $ignorePrefix If true the arbitrary start-end range is
* returned. default=false.
*/
public function getNetwork($ignorePrefix = false)
{
// micro-optimization to prevent calling getRange repeatedly
$k = $ignorePrefix ? 1 : 0;
if (!isset($this->cache['range'][$k])) {
$this->cache['range'][$k] = $this->getRange($ignorePrefix);
}
return $this->cache['range'][$k][0];
}
/**
* Get broadcast IP of the CIDR block
*
* Prefix sensitive.
*
* @param boolean $ignorePrefix If true the arbitrary start-end range is
* returned. default=false.
*/
public function getBroadcast($ignorePrefix = false)
{
// micro-optimization to prevent calling getRange repeatedly
$k = $ignorePrefix ? 1 : 0;
if (!isset($this->cache['range'][$k])) {
$this->cache['range'][$k] = $this->getRange($ignorePrefix);
}
return $this->cache['range'][$k][1];
}
/**
* Get the network mask based on the prefix.
*
*/
public function getMask()
{
return self::prefix_to_mask($this->prefix, $this->version);
}
/**
* Get total hosts within CIDR range
*
* Prefix sensitive.
*
* @param boolean $ignorePrefix If true the arbitrary start-end range is
* returned. default=false.
*/
public function getTotal($ignorePrefix = false)
{
// micro-optimization to prevent calling getRange repeatedly
$k = $ignorePrefix ? 1 : 0;
if (!isset($this->cache['range'][$k])) {
$this->cache['range'][$k] = $this->getRange($ignorePrefix);
}
return bcadd(bcsub(IP::inet_ptod($this->cache['range'][$k][1]),
IP::inet_ptod($this->cache['range'][$k][0])), '1');
}
public function intersects($cidr)
{
return self::cidr_intersect((string)$this, $cidr);
}
/**
* Determines the intersection between an IP (with optional prefix) and a
* CIDR block.
*
* The IP will be checked against the CIDR block given and will either be
* inside or outside the CIDR completely, or partially.
*
* NOTE: The caller should explicitly check against the INTERSECT_*
* constants because this method will return a value > 1 even for partial
* matches.
*
* @param mixed $ip The IP/cidr to match
* @param mixed $cidr The CIDR block to match within
* @return integer Returns an INTERSECT_* constant
* @throws \InvalidArgumentException if either $ip or $cidr is invalid
*/
public static function cidr_intersect($ip, $cidr)
{
// use fixed length HEX strings so we can easily do STRING comparisons
// instead of using slower bccomp() math.
list($lo,$hi) = array_map(function($v){ return sprintf("%032s", IP::inet_ptoh($v)); }, CIDR::cidr_to_range($ip));
list($min,$max) = array_map(function($v){ return sprintf("%032s", IP::inet_ptoh($v)); }, CIDR::cidr_to_range($cidr));
/** visualization of logic used below
lo-hi = $ip to check
min-max = $cidr block being checked against
--- --- --- lo --- --- hi --- --- --- --- --- IP/prefix to check
--- min --- --- max --- --- --- --- --- --- --- Partial "LOW" match
--- --- --- --- --- min --- --- max --- --- --- Partial "HIGH" match
--- --- --- --- min max --- --- --- --- --- --- No match "NO"
--- --- --- --- --- --- --- --- min --- max --- No match "NO"
min --- max --- --- --- --- --- --- --- --- --- No match "NO"
--- --- min --- --- --- --- max --- --- --- --- Full match "YES"
*/
// IP is exact match or completely inside the CIDR block
if ($lo >= $min and $hi <= $max) {
return self::INTERSECT_YES;
}
// IP is completely outside the CIDR block
if ($max < $lo or $min > $hi) {
return self::INTERSECT_NO;
}
// @todo is it useful to return LOW/HIGH partial matches?
// IP matches the lower end
if ($max <= $hi and $min <= $lo) {
return self::INTERSECT_LOW;
}
// IP matches the higher end
if ($min >= $lo and $max >= $hi) {
return self::INTERSECT_HIGH;
}
return self::INTERSECT_NO;
}
/**
* Converts an IPv4 or IPv6 CIDR block into its range.
*
* @todo May not be the fastest way to do this.
*
* @static
* @param string $cidr CIDR block or IP address string.
* @param integer|null $bits If /bits is not specified on string they can be
* passed via this parameter instead.
* @return array A 2 element array with the low, high range
*/
public static function cidr_to_range($cidr, $bits = null)
{
if (strpos($cidr, '/') !== false) {
list($ip, $_bits) = array_pad(explode('/', $cidr, 2), 2, null);
} else {
$ip = $cidr;
$_bits = $bits;
}
if (false === filter_var($ip, FILTER_VALIDATE_IP)) {
throw new \InvalidArgumentException("IP address \"$cidr\" is invalid");
}
// force bit length to 32 or 128 depending on type of IP
$bitlen = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 128 : 32;
if ($bits === null) {
// if no prefix is given use the length of the binary string which
// will give us 32 or 128 and result in a single IP being returned.
$bits = $_bits !== null ? $_bits : $bitlen;
}
if ($bits > $bitlen) {
throw new \InvalidArgumentException("IP address \"$cidr\" is invalid");
}
$ipdec = IP::inet_ptod($ip);
$ipbin = BC::bcdecbin($ipdec, $bitlen);
// calculate network
$netmask = BC::bcbindec(str_pad(str_repeat('1',$bits), $bitlen, '0'));
$ip1 = BC::bcand($ipdec, $netmask);
// calculate "broadcast" (not technically a broadcast in IPv6)
$ip2 = BC::bcor($ip1, BC::bcnot($netmask));
return array(IP::inet_dtop($ip1), IP::inet_dtop($ip2));
}
/**
* Return the CIDR string from the range given
*/
public static function range_to_cidr($start, $end)
{
$cidr = new CIDR($start, $end);
return (string)$cidr;
}
/**
* Return the maximum prefix length that would fit the IP address given.
*
* This is useful to determine how my bit would be needed to store the IP
* address when you don't already have a prefix for the IP.
*
* @example 216.240.32.0 would return 27
*
* @param string $ip IP address without prefix
* @param integer $bits Maximum bits to check; defaults to 32 for IPv4 and 128 for IPv6
*/
public static function max_prefix($ip, $bits = null)
{
static $mask = array();
$ver = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
$max = $ver == 6 ? 128 : 32;
if ($bits === null) {
$bits = $max;
}
$int = IP::inet_ptod($ip);
while ($bits > 0) {
// micro-optimization; calculate mask once ...
if (!isset($mask[$ver][$bits-1])) {
// 2^$max - 2^($max - $bits);
if ($ver == 4) {
$mask[$ver][$bits-1] = pow(2, $max) - pow(2, $max - ($bits-1));
} else {
$mask[$ver][$bits-1] = bcsub(bcpow(2, $max), bcpow(2, $max - ($bits-1)));
}
}
$m = $mask[$ver][$bits-1];
//printf("%s/%d: %s & %s == %s\n", $ip, $bits-1, BC::bcdecbin($m, 32), BC::bcdecbin($int, 32), BC::bcdecbin(BC::bcand($int, $m)));
//echo "$ip/", $bits-1, ": ", IP::inet_dtop($m), " ($m) & $int == ", BC::bcand($int, $m), "\n";
if (bccomp(BC::bcand($int, $m), $int) != 0) {
return $bits;
}
$bits--;
}
return $bits;
}
/**
* Return a contiguous list of true CIDR blocks that span the range given.
*
* Note: It's not a good idea to call this with IPv6 addresses. While it may
* work for certain ranges this can be very slow. Also an IPv6 list won't be
* as accurate as an IPv4 list.
*
* @example
* range_to_cidrlist(192.168.0.0, 192.168.0.15) ==
* 192.168.0.0/28
* range_to_cidrlist(192.168.0.0, 192.168.0.20) ==
* 192.168.0.0/28
* 192.168.0.16/30
* 192.168.0.20/32
*/
public static function range_to_cidrlist($start, $end)
{
$ver = (false === filter_var($start, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4;
$start = IP::inet_ptod($start);
$end = IP::inet_ptod($end);
$len = $ver == 4 ? 32 : 128;
$log2 = $ver == 4 ? log(2) : BC::bclog(2);
$list = array();
while (BC::cmp($end, $start) >= 0) { // $end >= $start
$prefix = self::max_prefix(IP::inet_dtop($start), $len);
if ($ver == 4) {
$diff = $len - floor( log($end - $start + 1) / $log2 );
} else {
// this is not as accurate due to the bclog function
$diff = bcsub($len, BC::bcfloor(bcdiv(BC::bclog(bcadd(bcsub($end, $start), '1')), $log2)));
}
if ($prefix < $diff) {
$prefix = $diff;
}
$list[] = IP::inet_dtop($start) . "/" . $prefix;
if ($ver == 4) {
$start += pow(2, $len - $prefix);
} else {
$start = bcadd($start, bcpow(2, $len - $prefix));
}
}
return $list;
}
/**
* Return an list of optimized CIDR blocks by collapsing adjacent CIDR
* blocks into larger blocks.
*
* @param array $cidrs List of CIDR block strings or objects
* @param integer $maxPrefix Maximum prefix to allow
* @return array Optimized list of CIDR objects
*/
public static function optimize_cidrlist($cidrs, $maxPrefix = 32)
{
// all indexes must be a CIDR object
$cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs);
// sort CIDR blocks in proper order so we can easily loop over them
$cidrs = self::cidr_sort($cidrs);
$list = array();
while ($cidrs) {
$c = array_shift($cidrs);
$start = $c->getStart();
$max = bcadd($c->getStart(true), $c->getTotal());
// loop through each cidr block until its ending range is more than
// the current maximum.
while (!empty($cidrs) and $cidrs[0]->getStart(true) <= $max) {
$b = array_shift($cidrs);
$newmax = bcadd($b->getStart(true), $b->getTotal());
if ($newmax > $max) {
$max = $newmax;
}
}
// add the new cidr range to the optimized list
$list = array_merge($list, self::range_to_cidrlist($start, IP::inet_dtop(bcsub($max, '1'))));
}
return $list;
}
/**
* Sort the list of CIDR blocks, optionally with a custom callback function.
*
* @param array $cidrs A list of CIDR blocks (strings or objects)
* @param Closure $callback Optional callback to perform the sorting.
* See PHP usort documentation for more details.
*/
public static function cidr_sort($cidrs, $callback = null)
{
// all indexes must be a CIDR object
$cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs);
if ($callback === null) {
$callback = function($a, $b) {
if (0 != ($o = BC::cmp($a->getStart(true), $b->getStart(true)))) {
return $o; // < or >
}
if ($a->getPrefix() == $b->getPrefix()) {
return 0;
}
return $a->getPrefix() < $b->getPrefix() ? -1 : 1;
};
} elseif (!($callback instanceof \Closure) or !is_callable($callback)) {
throw new \InvalidArgumentException("Invalid callback in CIDR::cidr_sort, expected Closure, got " . gettype($callback));
}
usort($cidrs, $callback);
return $cidrs;
}
/**
* Return the Prefix bits from the IPv4 mask given.
*
* This is only valid for IPv4 addresses since IPv6 addressing does not
* have a concept of network masks.
*
* Example: 255.255.255.0 == 24
*
* @param string $mask IPv4 network mask.
*/
public static function mask_to_prefix($mask)
{
if (false === filter_var($mask, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
throw new \InvalidArgumentException("Invalid IP netmask \"$mask\"");
}
return strrpos(IP::inet_ptob($mask, 32), '1') + 1;
}
/**
* Return the network mask for the prefix given.
*
* Normally this is only useful for IPv4 addresses but you can generate a
* mask for IPv6 addresses as well, only because its mathematically
* possible.
*
* @param integer $prefix CIDR prefix bits (0-128)
* @param integer $version IP version. If null the version will be detected
* based on the prefix length given.
*/
public static function prefix_to_mask($prefix, $version = null)
{
if ($version === null) {
$version = $prefix > 32 ? 6 : 4;
}
if ($prefix < 0 or $prefix > 128) {
throw new \InvalidArgumentException("Invalid prefix length \"$prefix\"");
}
if ($version != 4 and $version != 6) {
throw new \InvalidArgumentException("Invalid version \"$version\". Must be 4 or 6");
}
if ($version == 4) {
return long2ip($prefix == 0 ? 0 : (0xFFFFFFFF >> (32 - $prefix)) << (32 - $prefix));
} else {
return IP::inet_dtop($prefix == 0 ? 0 : BC::bcleft(BC::bcright(BC::MAX_UINT_128, 128-$prefix), 128-$prefix));
}
}
/**
* Return true if the $ip given is a true CIDR block.
*
* A true CIDR block is one where the $ip given is the actual Network
* address and broadcast matches the prefix appropriately.
*/
public static function cidr_is_true($ip)
{
$ip = new CIDR($ip);
return $ip->isTrueCidr();
}
}

207
inc/lib/IP/Lifo/IP/IP.php Executable file
View file

@ -0,0 +1,207 @@
<?php
/**
* This file is part of the Lifo\IP PHP Library.
*
* (c) Jason Morriss <lifo2013@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Lifo\IP;
/**
* IP Address helper class.
*
* Provides routines to translate IPv4 and IPv6 addresses between human readable
* strings, decimal, hexidecimal and binary.
*
* Requires BCmath extension and IPv6 PHP support
*/
abstract class IP
{
/**
* Convert a human readable (presentational) IP address string into a decimal string.
*/
public static function inet_ptod($ip)
{
// shortcut for IPv4 addresses
if (strpos($ip, ':') === false && strpos($ip, '.') !== false) {
return sprintf('%u', ip2long($ip));
}
// remove any cidr block notation
if (($o = strpos($ip, '/')) !== false) {
$ip = substr($ip, 0, $o);
}
// unpack into 4 32bit integers
$parts = unpack('N*', inet_pton($ip));
foreach ($parts as &$part) {
if ($part < 0) {
// convert signed int into unsigned
$part = sprintf('%u', $part);
//$part = bcadd($part, '4294967296');
}
}
// add each 32bit integer to the proper bit location in our big decimal
$decimal = $parts[4]; // << 0
$decimal = bcadd($decimal, bcmul($parts[3], '4294967296')); // << 32
$decimal = bcadd($decimal, bcmul($parts[2], '18446744073709551616')); // << 64
$decimal = bcadd($decimal, bcmul($parts[1], '79228162514264337593543950336')); // << 96
return $decimal;
}
/**
* Convert a decimal string into a human readable IP address.
*/
public static function inet_dtop($decimal, $expand = false)
{
$parts = array();
$parts[1] = bcdiv($decimal, '79228162514264337593543950336', 0); // >> 96
$decimal = bcsub($decimal, bcmul($parts[1], '79228162514264337593543950336'));
$parts[2] = bcdiv($decimal, '18446744073709551616', 0); // >> 64
$decimal = bcsub($decimal, bcmul($parts[2], '18446744073709551616'));
$parts[3] = bcdiv($decimal, '4294967296', 0); // >> 32
$decimal = bcsub($decimal, bcmul($parts[3], '4294967296'));
$parts[4] = $decimal; // >> 0
foreach ($parts as &$part) {
if (bccomp($part, '2147483647') == 1) {
$part = bcsub($part, '4294967296');
}
$part = (int) $part;
}
// if the first 96bits is all zeros then we can safely assume we
// actually have an IPv4 address. Even though it's technically possible
// you're not really ever going to see an IPv6 address in the range:
// ::0 - ::ffff
// It's feasible to see an IPv6 address of "::", in which case the
// caller is going to have to account for that on their own.
if (($parts[1] | $parts[2] | $parts[3]) == 0) {
$ip = long2ip($parts[4]);
} else {
$packed = pack('N4', $parts[1], $parts[2], $parts[3], $parts[4]);
$ip = inet_ntop($packed);
}
// Turn IPv6 to IPv4 if it's IPv4
if (preg_match('/^::\d+\./', $ip)) {
return substr($ip, 2);
}
return $expand ? self::inet_expand($ip) : $ip;
}
/**
* Convert a human readable (presentational) IP address into a HEX string.
*/
public static function inet_ptoh($ip)
{
return bin2hex(inet_pton($ip));
//return BC::bcdechex(self::inet_ptod($ip));
}
/**
* Convert a human readable (presentational) IP address into a BINARY string.
*/
public static function inet_ptob($ip, $bits = 128)
{
return BC::bcdecbin(self::inet_ptod($ip), $bits);
}
/**
* Convert a binary string into an IP address (presentational) string.
*/
public static function inet_btop($bin)
{
return self::inet_dtop(BC::bcbindec($bin));
}
/**
* Convert a HEX string into a human readable (presentational) IP address
*/
public static function inet_htop($hex)
{
return self::inet_dtop(BC::bchexdec($hex));
}
/**
* Expand an IP address. IPv4 addresses are returned as-is.
*
* Example:
* 2001::1 expands to 2001:0000:0000:0000:0000:0000:0000:0001
* ::127.0.0.1 expands to 0000:0000:0000:0000:0000:0000:7f00:0001
* 127.0.0.1 expands to 127.0.0.1
*/
public static function inet_expand($ip)
{
// strip possible cidr notation off
if (($pos = strpos($ip, '/')) !== false) {
$ip = substr($ip, 0, $pos);
}
$bytes = unpack('n*', inet_pton($ip));
if (count($bytes) > 2) {
return implode(':', array_map(function ($b) {
return sprintf("%04x", $b);
}, $bytes));
}
return $ip;
}
/**
* Convert an IPv4 address into an IPv6 address.
*
* One use-case for this is IP 6to4 tunnels used in networking.
*
* @example
* to_ipv4("10.10.10.10") == a0a:a0a
*
* @param string $ip IPv4 address.
* @param boolean $mapped If true a Full IPv6 address is returned within the
* official ipv4to6 mapped space "0:0:0:0:0:ffff:x:x"
*/
public static function to_ipv6($ip, $mapped = false)
{
if (!self::isIPv4($ip)) {
throw new \InvalidArgumentException("Invalid IPv4 address \"$ip\"");
}
$num = IP::inet_ptod($ip);
$o1 = dechex($num >> 16);
$o2 = dechex($num & 0x0000FFFF);
return $mapped ? "0:0:0:0:0:ffff:$o1:$o2" : "$o1:$o2";
}
/**
* Returns true if the IP address is a valid IPv4 address
*/
public static function isIPv4($ip)
{
return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
}
/**
* Returns true if the IP address is a valid IPv6 address
*/
public static function isIPv6($ip)
{
return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}
/**
* Compare two IP's (v4 or v6) and return -1, 0, 1 if the first is < = >
* the second.
*
* @param string $ip1 IP address
* @param string $ip2 IP address to compare against
* @return integer Return -1,0,1 depending if $ip1 is <=> $ip2
*/
public static function cmp($ip1, $ip2)
{
return bccomp(self::inet_ptod($ip1), self::inet_ptod($ip2), 0);
}
}

View file

@ -32,7 +32,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension
new Twig_SimpleFilter('addslashes', 'addslashes'), new Twig_SimpleFilter('addslashes', 'addslashes'),
); );
} }
/** /**
* Returns a list of functions to add to the existing list. * Returns a list of functions to add to the existing list.
* *
@ -52,7 +52,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension
new Twig_SimpleFunction('link_for', 'link_for') new Twig_SimpleFunction('link_for', 'link_for')
); );
} }
/** /**
* Returns the name of the extension. * Returns the name of the extension.
* *
@ -88,7 +88,7 @@ function twig_hasPermission_filter($mod, $permission, $board = null) {
function twig_extension_filter($value, $case_insensitive = true) { function twig_extension_filter($value, $case_insensitive = true) {
$ext = mb_substr($value, mb_strrpos($value, '.') + 1); $ext = mb_substr($value, mb_strrpos($value, '.') + 1);
if($case_insensitive) if($case_insensitive)
$ext = mb_strtolower($ext); $ext = mb_strtolower($ext);
return $ext; return $ext;
} }
@ -113,7 +113,7 @@ function twig_filename_truncate_filter($value, $length = 30, $separator = '…')
$value = strrev($value); $value = strrev($value);
$array = array_reverse(explode(".", $value, 2)); $array = array_reverse(explode(".", $value, 2));
$array = array_map("strrev", $array); $array = array_map("strrev", $array);
$filename = &$array[0]; $filename = &$array[0];
$extension = isset($array[1]) ? $array[1] : false; $extension = isset($array[1]) ? $array[1] : false;
@ -127,11 +127,11 @@ function twig_filename_truncate_filter($value, $length = 30, $separator = '…')
function twig_ratio_function($w, $h) { function twig_ratio_function($w, $h) {
return fraction($w, $h, ':'); return fraction($w, $h, ':');
} }
function twig_secure_link_confirm($text, $title, $confirm_message, $href) { function twig_secure_link_confirm($text, $title, $confirm_message, $href) {
global $config;
return '<a onclick="if (event.which==2) return true;if (confirm(\'' . htmlentities(addslashes($confirm_message)) . '\')) document.location=\'?/' . htmlspecialchars(addslashes($href . '/' . make_secure_link_token($href))) . '\';return false;" title="' . htmlentities($title) . '" href="?/' . $href . '">' . $text . '</a>'; return '<a onclick="if (event.which==2) return true;if (confirm(\'' . htmlentities(addslashes($confirm_message)) . '\')) document.location=\'?/' . htmlspecialchars(addslashes($href . '/' . make_secure_link_token($href))) . '\';return false;" title="' . htmlentities($title) . '" href="?/' . $href . '">' . $text . '</a>';
} }
function twig_secure_link($href) { function twig_secure_link($href) {
return $href . '/' . make_secure_link_token($href); return $href . '/' . make_secure_link_token($href);
} }

View file

@ -4,7 +4,6 @@
* Copyright (c) 2010-2013 Tinyboard Development Group * Copyright (c) 2010-2013 Tinyboard Development Group
*/ */
use Vichan\Context;
use Vichan\Functions\Net; use Vichan\Functions\Net;
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
@ -178,7 +177,7 @@ function make_secure_link_token($uri) {
return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8); return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8);
} }
function check_login(Context $ctx, $prompt = false) { function check_login($prompt = false) {
global $config, $mod; global $config, $mod;
// Validate session // Validate session
@ -188,7 +187,7 @@ function check_login(Context $ctx, $prompt = false) {
if (count($cookie) != 3) { if (count($cookie) != 3) {
// Malformed cookies // Malformed cookies
destroyCookies(); destroyCookies();
if ($prompt) mod_login($ctx); if ($prompt) mod_login();
exit; exit;
} }
@ -201,7 +200,7 @@ function check_login(Context $ctx, $prompt = false) {
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) { if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
// Malformed cookies // Malformed cookies
destroyCookies(); destroyCookies();
if ($prompt) mod_login($ctx); if ($prompt) mod_login();
exit; exit;
} }

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,187 @@
<?php <?php
// PHP 8.0 // PHP 5.4
if (!function_exists('str_starts_with')) { if (!function_exists('hex2bin')) {
function str_starts_with(string $haystack, string $needle): bool { function hex2bin($data) {
// https://wiki.php.net/rfc/add_str_starts_with_and_ends_with_functions#str_starts_with return pack("H*" , $hex_string);
return \strncmp($haystack, $needle, \strlen($needle)) === 0; }
}
// PHP 5.6
if (!function_exists('hash_equals')) {
function hash_equals($ours, $theirs) {
$ours = (string)$ours;
$theirs = (string)$theirs;
$tlen = strlen($theirs);
$olen = strlen($ours);
$answer = 0;
for ($i = 0; $i < $tlen; $i++) {
$answer |= ord($ours[$olen > $i ? $i : 0]) ^ ord($theirs[$i]);
}
return $answer === 0 && $olen === $tlen;
}
}
if (!function_exists('imagecreatefrombmp')) {
/*********************************************/
/* Fonction: imagecreatefrombmp */
/* Author: DHKold */
/* Contact: admin@dhkold.com */
/* Date: The 15th of June 2005 */
/* Version: 2.0B */
/*********************************************/
function imagecreatefrombmp($filename) {
if (! $f1 = fopen($filename,"rb")) return FALSE;
$FILE = unpack("vfile_type/Vfile_size/Vreserved/Vbitmap_offset", fread($f1,14));
if ($FILE['file_type'] != 19778) return FALSE;
$BMP = unpack('Vheader_size/Vwidth/Vheight/vplanes/vbits_per_pixel'.
'/Vcompression/Vsize_bitmap/Vhoriz_resolution'.
'/Vvert_resolution/Vcolors_used/Vcolors_important', fread($f1,40));
$BMP['colors'] = pow(2,$BMP['bits_per_pixel']);
if ($BMP['size_bitmap'] == 0) $BMP['size_bitmap'] = $FILE['file_size'] - $FILE['bitmap_offset'];
$BMP['bytes_per_pixel'] = $BMP['bits_per_pixel']/8;
$BMP['bytes_per_pixel2'] = ceil($BMP['bytes_per_pixel']);
$BMP['decal'] = ($BMP['width']*$BMP['bytes_per_pixel']/4);
$BMP['decal'] -= floor($BMP['width']*$BMP['bytes_per_pixel']/4);
$BMP['decal'] = 4-(4*$BMP['decal']);
if ($BMP['decal'] == 4) $BMP['decal'] = 0;
$PALETTE = array();
if ($BMP['colors'] < 16777216)
{
$PALETTE = unpack('V'.$BMP['colors'], fread($f1,$BMP['colors']*4));
}
$IMG = fread($f1,$BMP['size_bitmap']);
$VIDE = chr(0);
$res = imagecreatetruecolor($BMP['width'],$BMP['height']);
$P = 0;
$Y = $BMP['height']-1;
while ($Y >= 0)
{
$X=0;
while ($X < $BMP['width'])
{
if ($BMP['bits_per_pixel'] == 24)
$COLOR = unpack("V",substr($IMG,$P,3).$VIDE);
elseif ($BMP['bits_per_pixel'] == 16)
{
$COLOR = unpack("n",substr($IMG,$P,2));
$COLOR[1] = $PALETTE[$COLOR[1]+1];
}
elseif ($BMP['bits_per_pixel'] == 8)
{
$COLOR = unpack("n",$VIDE.substr($IMG,$P,1));
$COLOR[1] = $PALETTE[$COLOR[1]+1];
}
elseif ($BMP['bits_per_pixel'] == 4)
{
$COLOR = unpack("n",$VIDE.substr($IMG,floor($P),1));
if (($P*2)%2 == 0) $COLOR[1] = ($COLOR[1] >> 4) ; else $COLOR[1] = ($COLOR[1] & 0x0F);
$COLOR[1] = $PALETTE[$COLOR[1]+1];
}
elseif ($BMP['bits_per_pixel'] == 1)
{
$COLOR = unpack("n",$VIDE.substr($IMG,floor($P),1));
if (($P*8)%8 == 0) $COLOR[1] = $COLOR[1] >>7;
elseif (($P*8)%8 == 1) $COLOR[1] = ($COLOR[1] & 0x40)>>6;
elseif (($P*8)%8 == 2) $COLOR[1] = ($COLOR[1] & 0x20)>>5;
elseif (($P*8)%8 == 3) $COLOR[1] = ($COLOR[1] & 0x10)>>4;
elseif (($P*8)%8 == 4) $COLOR[1] = ($COLOR[1] & 0x8)>>3;
elseif (($P*8)%8 == 5) $COLOR[1] = ($COLOR[1] & 0x4)>>2;
elseif (($P*8)%8 == 6) $COLOR[1] = ($COLOR[1] & 0x2)>>1;
elseif (($P*8)%8 == 7) $COLOR[1] = ($COLOR[1] & 0x1);
$COLOR[1] = $PALETTE[$COLOR[1]+1];
}
else
return FALSE;
imagesetpixel($res,$X,$Y,$COLOR[1]);
$X++;
$P += $BMP['bytes_per_pixel'];
}
$Y--;
$P+=$BMP['decal'];
}
fclose($f1);
return $res;
}
}
if (!function_exists('imagebmp')) {
function imagebmp(&$img, $filename='') {
$widthOrig = imagesx($img);
$widthFloor = ((floor($widthOrig/16))*16);
$widthCeil = ((ceil($widthOrig/16))*16);
$height = imagesy($img);
$size = ($widthCeil*$height*3)+54;
// Bitmap File Header
$result = 'BM'; // header (2b)
$result .= int_to_dword($size); // size of file (4b)
$result .= int_to_dword(0); // reserved (4b)
$result .= int_to_dword(54); // byte location in the file which is first byte of IMAGE (4b)
// Bitmap Info Header
$result .= int_to_dword(40); // Size of BITMAPINFOHEADER (4b)
$result .= int_to_dword($widthCeil); // width of bitmap (4b)
$result .= int_to_dword($height); // height of bitmap (4b)
$result .= int_to_word(1); // biPlanes = 1 (2b)
$result .= int_to_word(24); // biBitCount = {1 (mono) or 4 (16 clr ) or 8 (256 clr) or 24 (16 Mil)} (2b
$result .= int_to_dword(0); // RLE COMPRESSION (4b)
$result .= int_to_dword(0); // width x height (4b)
$result .= int_to_dword(0); // biXPelsPerMeter (4b)
$result .= int_to_dword(0); // biYPelsPerMeter (4b)
$result .= int_to_dword(0); // Number of palettes used (4b)
$result .= int_to_dword(0); // Number of important colour (4b)
// is faster than chr()
$arrChr = array();
for ($i=0; $i<256; $i++){
$arrChr[$i] = chr($i);
}
// creates image data
$bgfillcolor = array('red'=>0, 'green'=>0, 'blue'=>0);
// bottom to top - left to right - attention blue green red !!!
$y=$height-1;
for ($y2=0; $y2<$height; $y2++) {
for ($x=0; $x<$widthFloor; ) {
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
$rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
}
for ($x=$widthFloor; $x<$widthCeil; $x++) {
$rgb = ($x<$widthOrig) ? imagecolorsforindex($img, imagecolorat($img, $x, $y)) : $bgfillcolor;
$result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
}
$y--;
}
// see imagegif
if ($filename == '') {
echo $result;
} else {
$file = fopen($filename, 'wb');
fwrite($file, $result);
fclose($file);
}
}
// imagebmp helpers
function int_to_dword($n) {
return chr($n & 255).chr(($n >> 8) & 255).chr(($n >> 16) & 255).chr(($n >> 24) & 255);
}
function int_to_word($n) {
return chr($n & 255).chr(($n >> 8) & 255);
} }
} }

View file

@ -52,7 +52,11 @@ if (file_exists($config['has_installed'])) {
function __query($sql) { function __query($sql) {
sql_open(); sql_open();
return query($sql);
if (mysql_version() >= 50503)
return query($sql);
else
return query(str_replace('utf8mb4', 'utf8', $sql));
} }
$boards = listBoards(); $boards = listBoards();
@ -881,7 +885,6 @@ if ($step == 0) {
$config['cookies']['salt'] = substr(base64_encode(sha1(rand())), 0, 30); $config['cookies']['salt'] = substr(base64_encode(sha1(rand())), 0, 30);
$config['secure_trip_salt'] = substr(base64_encode(sha1(rand())), 0, 30); $config['secure_trip_salt'] = substr(base64_encode(sha1(rand())), 0, 30);
$config['secure_password_salt'] = substr(base64_encode(sha1(rand())), 0, 30);
echo Element('page.html', array( echo Element('page.html', array(
'body' => Element('installer/config.html', array( 'body' => Element('installer/config.html', array(
@ -936,6 +939,7 @@ if ($step == 0) {
$sql = @file_get_contents('install.sql') or error("Couldn't load install.sql."); $sql = @file_get_contents('install.sql') or error("Couldn't load install.sql.");
sql_open(); sql_open();
$mysql_version = mysql_version();
// This code is probably horrible, but what I'm trying // This code is probably horrible, but what I'm trying
// to do is find all of the SQL queires and put them // to do is find all of the SQL queires and put them
@ -948,6 +952,8 @@ if ($step == 0) {
$sql_errors = ''; $sql_errors = '';
$sql_err_count = 0; $sql_err_count = 0;
foreach ($queries as $query) { foreach ($queries as $query) {
if ($mysql_version < 50503)
$query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query);
$query = preg_replace('/^([\w\s]*)`([0-9a-zA-Z$_\x{0080}-\x{FFFF}]+)`/u', '$1``$2``', $query); $query = preg_replace('/^([\w\s]*)`([0-9a-zA-Z$_\x{0080}-\x{FFFF}]+)`/u', '$1``$2``', $query);
if (!query($query)) { if (!query($query)) {
$sql_err_count++; $sql_err_count++;

View file

@ -18,25 +18,16 @@ $(window).ready(function() {
// Enable submit button if disabled (cache problem) // Enable submit button if disabled (cache problem)
$('input[type="submit"]').removeAttr('disabled'); $('input[type="submit"]').removeAttr('disabled');
var setup_form = function($form) { var setup_form = function($form) {
$form.submit(function() { $form.submit(function() {
if (do_not_ajax) if (do_not_ajax)
return true; return true;
// If the captcha is present, halt if it does not have a response.
if (captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled())) {
if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) {
captcha_renderer.execute(postCaptchaId);
return false;
}
}
var form = this; var form = this;
var submit_txt = $(this).find('input[type="submit"]').val(); var submit_txt = $(this).find('input[type="submit"]').val();
if (window.FormData === undefined) if (window.FormData === undefined)
return true; return true;
var formData = new FormData(this); var formData = new FormData(this);
formData.append('json_response', '1'); formData.append('json_response', '1');
formData.append('post', submit_txt); formData.append('post', submit_txt);
@ -103,15 +94,15 @@ $(window).ready(function() {
setTimeout(function() { $(window).trigger("scroll"); }, 100); setTimeout(function() { $(window).trigger("scroll"); }, 100);
} }
}); });
highlightReply(post_response.id); highlightReply(post_response.id);
window.location.hash = post_response.id; window.location.hash = post_response.id;
$(window).scrollTop($(document).height()); $(window).scrollTop($(document).height());
$(form).find('input[type="submit"]').val(submit_txt); $(form).find('input[type="submit"]').val(submit_txt);
$(form).find('input[type="submit"]').removeAttr('disabled'); $(form).find('input[type="submit"]').removeAttr('disabled');
$(form).find('input[name="subject"],input[name="file_url"],\ $(form).find('input[name="subject"],input[name="file_url"],\
textarea[name="body"],input[type="file"],input[name="embed"]').val('').change(); textarea[name="body"],input[type="file"]').val('').change();
}, },
cache: false, cache: false,
contentType: false, contentType: false,
@ -123,7 +114,7 @@ $(window).ready(function() {
$(form).find('input[type="submit"]').val(submit_txt); $(form).find('input[type="submit"]').val(submit_txt);
$(form).find('input[type="submit"]').removeAttr('disabled'); $(form).find('input[type="submit"]').removeAttr('disabled');
$(form).find('input[name="subject"],input[name="file_url"],\ $(form).find('input[name="subject"],input[name="file_url"],\
textarea[name="body"],input[type="file"],input[name="embed"]').val('').change(); textarea[name="body"],input[type="file"]').val('').change();
} else { } else {
alert(_('An unknown error occured when posting!')); alert(_('An unknown error occured when posting!'));
$(form).find('input[type="submit"]').val(submit_txt); $(form).find('input[type="submit"]').val(submit_txt);
@ -141,10 +132,10 @@ $(window).ready(function() {
contentType: false, contentType: false,
processData: false processData: false
}, 'json'); }, 'json');
$(form).find('input[type="submit"]').val(_('Posting...')); $(form).find('input[type="submit"]').val(_('Posting...'));
$(form).find('input[type="submit"]').attr('disabled', true); $(form).find('input[type="submit"]').attr('disabled', true);
return false; return false;
}); });
}; };

View file

@ -1,9 +1,3 @@
/**
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/jquery.mixitup.min.js';
* $config['additional_javascript'][] = 'js/catalog.js';
*/
if (active_page == 'catalog') $(function(){ if (active_page == 'catalog') $(function(){
if (localStorage.catalog !== undefined) { if (localStorage.catalog !== undefined) {
var catalog = JSON.parse(localStorage.catalog); var catalog = JSON.parse(localStorage.catalog);

View file

@ -17,10 +17,6 @@ $(document).ready(function() {
// Default maximum image loads. // Default maximum image loads.
const DEFAULT_MAX = 5; const DEFAULT_MAX = 5;
if (localStorage.inline_expand_fit_height !== 'false') {
$('<style id="expand-fit-height-style">.full-image { max-height: ' + window.innerHeight + 'px; }</style>').appendTo($('head'));
}
let inline_expand_post = function() { let inline_expand_post = function() {
let link = this.getElementsByTagName('a'); let link = this.getElementsByTagName('a');
@ -60,12 +56,12 @@ $(document).ready(function() {
}, },
add: function(ele) { add: function(ele) {
ele.deferred = $.Deferred(); ele.deferred = $.Deferred();
ele.deferred.done(function() { ele.deferred.done(function () {
let $loadstart = $.Deferred(); let $loadstart = $.Deferred();
let thumb = ele.childNodes[0]; let thumb = ele.childNodes[0];
let img = ele.childNodes[1]; let img = ele.childNodes[1];
let onLoadStart = function(img) { let onLoadStart = function (img) {
if (img.naturalWidth) { if (img.naturalWidth) {
$loadstart.resolve(img, thumb); $loadstart.resolve(img, thumb);
} else { } else {
@ -73,15 +69,15 @@ $(document).ready(function() {
} }
}; };
$(img).one('load', function() { $(img).one('load', function () {
$.when($loadstart).done(function() { $.when($loadstart).done(function () {
// once fully loaded, update the waiting queue // Once fully loaded, update the waiting queue.
--loading; --loading;
$(ele).data('imageLoading', 'false'); $(ele).data('imageLoading', 'false');
update(); update();
}); });
}); });
$loadstart.done(function(img, thumb) { $loadstart.done(function (img, thumb) {
thumb.style.display = 'none'; thumb.style.display = 'none';
img.style.display = ''; img.style.display = '';
}); });
@ -206,8 +202,6 @@ $(document).ready(function() {
Options.extend_tab('general', '<span id="inline-expand-max">' + Options.extend_tab('general', '<span id="inline-expand-max">' +
_('Number of simultaneous image downloads (0 to disable): ') + _('Number of simultaneous image downloads (0 to disable): ') +
'<input type="number" step="1" min="0" size="4"></span>'); '<input type="number" step="1" min="0" size="4"></span>');
Options.extend_tab('general', '<label id="inline-expand-fit-height"><input type="checkbox">' + _('Fit expanded images into screen height') + '</label>');
$('#inline-expand-max input') $('#inline-expand-max input')
.css('width', '50px') .css('width', '50px')
.val(localStorage.inline_expand_max || DEFAULT_MAX) .val(localStorage.inline_expand_max || DEFAULT_MAX)
@ -218,21 +212,6 @@ $(document).ready(function() {
localStorage.inline_expand_max = val; localStorage.inline_expand_max = val;
}); });
$('#inline-expand-fit-height input').on('change', function() {
if (localStorage.inline_expand_fit_height !== 'false') {
localStorage.inline_expand_fit_height = 'false';
$('#expand-fit-height-style').remove();
}
else {
localStorage.inline_expand_fit_height = 'true';
$('<style id="expand-fit-height-style">.full-image { max-height: ' + window.innerHeight + 'px; }</style>').appendTo($('head'));
}
});
if (localStorage.inline_expand_fit_height !== 'false') {
$('#inline-expand-fit-height input').prop('checked', true);
}
} }
if (window.jQuery) { if (window.jQuery) {

View file

@ -43,6 +43,9 @@ $(function(){
document.location.reload(); document.location.reload();
} }
}); });
$("#style-select").detach().css({float:"none","margin-bottom":0}).appendTo(tab.content);
}); });
}(); }();

View file

@ -237,8 +237,12 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
var postUid = $ele.find('.poster_id').text(); var postUid = $ele.find('.poster_id').text();
} }
let postName = (typeof $ele.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($ele.find('.name')[0]); let postName;
let postTrip = $ele.find('.trip').text(); let postTrip = '';
if (!pageData.forcedAnon) {
postName = (typeof $ele.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($ele.find('.name')[0]);
postTrip = $ele.find('.trip').text();
}
/* display logic and bind click handlers /* display logic and bind click handlers
*/ */
@ -293,34 +297,39 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
} }
// name // name
if (!$ele.data('hiddenByName')) { if (!pageData.forcedAnon && !$ele.data('hiddenByName')) {
$buffer.find('#filter-add-name').click(function () { $buffer.find('#filter-add-name').click(function () {
addFilter('name', postName, false); addFilter('name', postName, false);
}); });
$buffer.find('#filter-remove-name').addClass('hidden'); $buffer.find('#filter-remove-name').addClass('hidden');
} else { } else if (!pageData.forcedAnon) {
$buffer.find('#filter-remove-name').click(function () { $buffer.find('#filter-remove-name').click(function () {
removeFilter('name', postName, false); removeFilter('name', postName, false);
}); });
$buffer.find('#filter-add-name').addClass('hidden');
} else {
// board has forced anon
$buffer.find('#filter-remove-name').addClass('hidden');
$buffer.find('#filter-add-name').addClass('hidden'); $buffer.find('#filter-add-name').addClass('hidden');
} }
// tripcode // tripcode
if (!$ele.data('hiddenByTrip') && postTrip !== '') { if (!pageData.forcedAnon && !$ele.data('hiddenByTrip') && postTrip !== '') {
$buffer.find('#filter-add-trip').click(function () { $buffer.find('#filter-add-trip').click(function () {
addFilter('trip', postTrip, false); addFilter('trip', postTrip, false);
}); });
$buffer.find('#filter-remove-trip').addClass('hidden'); $buffer.find('#filter-remove-trip').addClass('hidden');
} else if (postTrip !== '') { } else if (!pageData.forcedAnon && postTrip !== '') {
$buffer.find('#filter-remove-trip').click(function () { $buffer.find('#filter-remove-trip').click(function () {
removeFilter('trip', postTrip, false); removeFilter('trip', postTrip, false);
}); });
$buffer.find('#filter-add-trip').addClass('hidden'); $buffer.find('#filter-add-trip').addClass('hidden');
} else { } else {
// board has forced anon
$buffer.find('#filter-remove-trip').addClass('hidden'); $buffer.find('#filter-remove-trip').addClass('hidden');
$buffer.find('#filter-add-trip').addClass('hidden'); $buffer.find('#filter-add-trip').addClass('hidden');
} }
@ -382,6 +391,7 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
var localList = pageData.localList; var localList = pageData.localList;
var noReplyList = pageData.noReplyList; var noReplyList = pageData.noReplyList;
var hasUID = pageData.hasUID; var hasUID = pageData.hasUID;
var forcedAnon = pageData.forcedAnon;
var hasTrip = ($post.find('.trip').length > 0); var hasTrip = ($post.find('.trip').length > 0);
var hasSub = ($post.find('.subject').length > 0); var hasSub = ($post.find('.subject').length > 0);
@ -422,8 +432,9 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
} }
// matches generalFilter // matches generalFilter
name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]); if (!forcedAnon)
if (hasTrip) name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]);
if (!forcedAnon && hasTrip)
trip = $post.find('.trip').text(); trip = $post.find('.trip').text();
if (hasSub) if (hasSub)
subject = $post.find('.subject').text(); subject = $post.find('.subject').text();
@ -444,13 +455,13 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
pattern = new RegExp(rule.value); pattern = new RegExp(rule.value);
switch (rule.type) { switch (rule.type) {
case 'name': case 'name':
if (pattern.test(name)) { if (!forcedAnon && pattern.test(name)) {
$post.data('hiddenByName', true); $post.data('hiddenByName', true);
hide(post); hide(post);
} }
break; break;
case 'trip': case 'trip':
if (hasTrip && pattern.test(trip)) { if (!forcedAnon && hasTrip && pattern.test(trip)) {
$post.data('hiddenByTrip', true); $post.data('hiddenByTrip', true);
hide(post); hide(post);
} }
@ -477,13 +488,13 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
} else { } else {
switch (rule.type) { switch (rule.type) {
case 'name': case 'name':
if (rule.value == name) { if (!forcedAnon && rule.value == name) {
$post.data('hiddenByName', true); $post.data('hiddenByName', true);
hide(post); hide(post);
} }
break; break;
case 'trip': case 'trip':
if (hasTrip && rule.value == trip) { if (!forcedAnon && hasTrip && rule.value == trip) {
$post.data('hiddenByTrip', true); $post.data('hiddenByTrip', true);
hide(post); hide(post);
} }
@ -816,7 +827,8 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
boardId: board_name, // get the id from the global variable boardId: board_name, // get the id from the global variable
localList: [], // all the blacklisted post IDs or UIDs that apply to the current page localList: [], // all the blacklisted post IDs or UIDs that apply to the current page
noReplyList: [], // any posts that replies to the contents of this list shall be hidden noReplyList: [], // any posts that replies to the contents of this list shall be hidden
hasUID: (document.getElementsByClassName('poster_id').length > 0) hasUID: (document.getElementsByClassName('poster_id').length > 0),
forcedAnon: ($('th:contains(Name)').length === 0) // tests by looking for the Name label on the reply form
}; };
initStyle(); initStyle();

View file

@ -104,10 +104,8 @@ function buildMenu(e) {
function addButton(post) { function addButton(post) {
var $ele = $(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( $ele.find('input.delete').after(
$('<a>', {href: '#', class: 'post-btn', title: 'Post menu'}).text('\u{25B6}\u{fe0e}') $('<a>', {href: '#', class: 'post-btn', title: 'Post menu'}).text('►')
); );
} }

View file

@ -15,7 +15,7 @@
$(document).ready(function() { $(document).ready(function() {
let showBackLinks = function() { let showBackLinks = function() {
let replyId = $(this).attr('id').split('_')[1]; let replyId = $(this).attr('id').replace(/^reply_/, '');
$(this).find('div.body a:not([rel="nofollow"])').each(function() { $(this).find('div.body a:not([rel="nofollow"])').each(function() {
let id = $(this).text().match(/^>>(\d+)$/); let id = $(this).text().match(/^>>(\d+)$/);
@ -25,15 +25,13 @@ $(document).ready(function() {
return; return;
} }
let post = $('#reply_' + id + ', #op_' + id); let post = $('#reply_' + id);
if (post.length == 0) { if(post.length == 0)
return; return;
}
let mentioned = post.find('.head div.mentioned'); let mentioned = post.find('.head div.mentioned');
if (mentioned.length === 0) { if (mentioned.length === 0) {
// The op has two "head"s divs, use the second. mentioned = $('<div class="mentioned unimportant"></div>').prependTo(post.find('.head'));
mentioned = $('<div class="mentioned unimportant"></div>').prependTo(post.find('.head').last());
} }
if (mentioned.find('a.mentioned-' + replyId).length !== 0) { if (mentioned.find('a.mentioned-' + replyId).length !== 0) {
@ -50,13 +48,13 @@ $(document).ready(function() {
}); });
}; };
$('div.post').each(showBackLinks); $('div.post.reply').each(showBackLinks);
$(document).on('new_post', function(e, post) { $(document).on('new_post', function(e, post) {
if ($(post).hasClass('reply') || $(post).hasClass('op')) { if ($(post).hasClass('reply')) {
showBackLinks.call(post); showBackLinks.call(post);
} else { } else {
$(post).find('div.post').each(showBackLinks); $(post).find('div.post.reply').each(showBackLinks);
} }
}); });
}); });

View file

@ -1,36 +0,0 @@
/*
* style-select-simple.js
*
* Changes the stylesheet chooser links to a <select>
*
* Released under the MIT license
* Copyright (c) 2025 Zankaria Auxa <zankaria.auxa@mailu.io>
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* // $config['additional_javascript'][] = 'js/style-select.js'; // Conflicts with this file.
* $config['additional_javascript'][] = 'js/style-select-simple.js';
*/
$(document).ready(function() {
let newElement = document.createElement('div');
newElement.className = 'styles';
// styles is defined in main.js.
for (styleName in styles) {
if (styleName) {
let style = document.createElement('a');
style.innerHTML = '[' + styleName + ']';
style.onclick = function() {
changeStyle(this.innerHTML.substring(1, this.innerHTML.length - 1), this);
};
if (styleName == selectedstyle) {
style.className = 'selected';
}
style.href = 'javascript:void(0);';
newElement.appendChild(style);
}
}
document.getElementById('bottom-hud').before(newElement);
});

View file

@ -6,53 +6,48 @@
* *
* Released under the MIT license * Released under the MIT license
* Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org> * Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org>
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net> * Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
* *
* Usage: * Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js'; * $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/style-select.js'; * $config['additional_javascript'][] = 'js/style-select.js';
*
*/ */
$(document).ready(function() { $(document).ready(function() {
let pages = $('div.pages'); var stylesDiv = $('div.styles');
let stylesSelect = $('<select></select>').css({float:"none"}); var pages = $('div.pages');
let options = []; var stylesSelect = $('<select></select>').css({float:"none"});
var options = [];
var i = 1;
stylesDiv.children().each(function() {
var name = this.innerHTML.replace(/(^\[|\]$)/g, '');
var opt = $('<option></option>')
.html(name)
.val(i);
if ($(this).hasClass('selected'))
opt.attr('selected', true);
options.push ([name.toUpperCase (), opt]);
$(this).attr('id', 'style-select-' + i);
i++;
});
let i = 1; options.sort ((a, b) => {
for (styleName in styles) {
if (styleName) {
let opt = $('<option></option>')
.html(styleName)
.val(i);
if (selectedstyle == styleName) {
opt.attr('selected', true);
}
opt.attr('id', 'style-select-' + i);
options.push([styleName.toUpperCase (), opt]);
i++;
}
}
options.sort((a, b) => {
const keya = a [0]; const keya = a [0];
const keyb = b [0]; const keyb = b [0];
if (keya < keyb) { if (keya < keyb) { return -1; }
return -1; if (keya > keyb) { return 1; }
}
if (keya > keyb) {
return 1;
}
return 0; return 0;
}).forEach(([key, opt]) => { }).forEach (([key, opt]) => {
stylesSelect.append(opt); stylesSelect.append(opt);
}); });
stylesSelect.change(function() { stylesSelect.change(function() {
let sel = $(this).find(":selected")[0]; $('#style-select-' + $(this).val()).click();
let styleName = sel.innerHTML;
changeStyle(styleName, sel);
}); });
stylesDiv.hide()
pages.after( pages.after(
$('<div id="style-select"></div>') $('<div id="style-select"></div>')
.append(_('Select theme: '), stylesSelect) .append(_('Select theme: '), stylesSelect)

View file

@ -1,41 +1,41 @@
/* /*
* Don't load the 3rd party embedded content player unless the image is clicked. * youtube
* This increases performance issues when many videos are embedded on the same page. * https://github.com/savetheinternet/Tinyboard/blob/master/js/youtube.js
* *
* Released under the MIT license * Don't load the YouTube player unless the video image is clicked.
* Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org> * This increases performance issues when many videos are embedded on the same page.
* Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net> * Currently only compatiable with YouTube.
* Copyright (c) 2025 Zankaria Auxa <zankaria.auxa@mailu.io> *
* * Proof of concept.
* Usage: *
* $config['embedding'] = array(); * Released under the MIT license
* $config['embedding'][0] = array( * Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org>
* '/^https?:\/\/(\w+\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', * Copyright (c) 2013-2014 Marcin Łabanowski <marcin@6irc.net>
* $config['youtube_js_html']); *
* $config['additional_javascript'][] = 'js/jquery.min.js'; * Usage:
* $config['additional_javascript'][] = 'js/youtube.js'; * $config['embedding'] = array();
*/ * $config['embedding'][0] = array(
* '/^https?:\/\/(\w+\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9\-_]{10,11})(&.+)?$/i',
* $config['youtube_js_html']);
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/youtube.js';
*
*/
$(document).ready(function() { $(document).ready(function(){
const ON = '[Remove]'; // Adds Options panel item
const YOUTUBE = 'www.youtube.com';
function makeEmbedNode(embedHost, videoId, width, height) {
return $(`<iframe type="text/html" width="${width}" height="${height}" class="full-image"
src="https://${embedHost}/embed/${videoId}?autoplay=1" allow="fullscreen" frameborder="0" referrerpolicy="strict-origin"/>`);
}
// Adds Options panel item.
if (typeof localStorage.youtube_embed_proxy === 'undefined') { if (typeof localStorage.youtube_embed_proxy === 'undefined') {
localStorage.youtube_embed_proxy = 'incogtube.com'; // Default value. if (location.hostname.includes(".onion")){
localStorage.youtube_embed_proxy = 'tuberyps2pn6dor6h47brof3w2asmauahhk4ei42krugybzzzo55klad.onion';
} else {
localStorage.youtube_embed_proxy = 'incogtube.com'; //default value
}
} }
if (window.Options && Options.get_tab('general')) { if (window.Options && Options.get_tab('general')) {
Options.extend_tab("general", Options.extend_tab("general", "<fieldset id='media-proxy-fs'><legend>"+_("Media Proxy (requires refresh)")+"</legend>"
"<fieldset id='media-proxy-fs'><legend>" + _("Media Proxy (requires refresh)") + "</legend>" + ('<label id="youtube-embed-proxy-url">' + _('YouTube embed proxy url&nbsp;&nbsp;')+'<input type="text" size=30></label>')
+ '<label id="youtube-embed-proxy-url">' + _('YouTube embed proxy url&nbsp;&nbsp;') + '</fieldset>');
+ '<input type="text" size=30></label>'
+ '</fieldset>');
$('#youtube-embed-proxy-url>input').val(localStorage.youtube_embed_proxy); $('#youtube-embed-proxy-url>input').val(localStorage.youtube_embed_proxy);
$('#youtube-embed-proxy-url>input').on('input', function() { $('#youtube-embed-proxy-url>input').on('input', function() {
@ -43,65 +43,51 @@ $(document).ready(function() {
}); });
} }
const proxy = localStorage.youtube_embed_proxy; const ON = "[Remove]";
const OFF = "[Embed]";
const YOUTUBE = 'www.youtube.com';
const PROXY = localStorage.youtube_embed_proxy;
function addEmbedButton(index, videoNode) {
videoNode = $(videoNode);
var contents = videoNode.contents();
var videoId = videoNode.data('video');
var span = $("<span>[Embed]</span>");
var spanProxy = $("<span>[Proxy]</span>");
function addEmbedButton(_i, node) { var makeEmbedNode = function(embedHost) {
node = $(node); return $('<iframe style="float:left;margin: 10px 20px" type="text/html" '+
const contents = node.contents(); 'width="360" height="270" src="//' + embedHost + '/embed/' + videoId +
const embedUrl = node.data('video-id'); '?autoplay=1&html5=1" allowfullscreen frameborder="0"/>');
const embedWidth = node.data('iframe-width'); }
const embedHeight = node.data('iframe-height'); var defaultEmbed = makeEmbedNode(location.hostname.includes(".onion") ? PROXY : YOUTUBE);
const span = $('<span>[Embed]</span>'); var proxyEmbed = makeEmbedNode(PROXY);
const spanProxy = $("<span>[Proxy]</span>"); videoNode.click(function(e) {
let iframeDefault = null;
let iframeProxy = null;
node.click(function(e) {
e.preventDefault(); e.preventDefault();
if (span.text() == ON) { if (span.text() == ON){
contents.css('display', ''); videoNode.append(spanProxy);
spanProxy.css('display', ''); videoNode.append(contents);
defaultEmbed.remove();
if (iframeDefault !== null) { proxyEmbed.remove();
iframeDefault.remove(); span.text(OFF);
}
if (iframeProxy !== null) {
iframeProxy.remove();
}
span.text('[Embed]');
} else { } else {
let useProxy = e.target == spanProxy[0]; contents.detach();
// Lazily create the iframes.
if (useProxy) {
if (iframeProxy === null) {
iframeProxy = makeEmbedNode(proxy, embedUrl, embedWidth, embedHeight);
}
node.prepend(iframeProxy);
} else {
if (iframeDefault === null) {
iframeDefault = makeEmbedNode(YOUTUBE, embedUrl, embedWidth, embedHeight);
}
node.prepend(iframeDefault);
}
contents.css('display', 'none');
spanProxy.css('display', 'none');
span.text(ON); span.text(ON);
spanProxy.remove();
videoNode.append(e.target == spanProxy[0] ? proxyEmbed : defaultEmbed);
} }
}); });
node.append(span); videoNode.append(span);
node.append(spanProxy); videoNode.append(spanProxy);
} }
$('div.video-container', document).each(addEmbedButton); $('div.video-container', document).each(addEmbedButton);
// Allow to work with auto-reload.js, etc. // allow to work with auto-reload.js, etc.
$(document).on('new_post', function(e, post) { $(document).on('new_post', function(e, post) {
$('div.video-container', post).each(addEmbedButton); $('div.video-container', post).each(addEmbedButton);
}); });
}); });

View file

@ -21,4 +21,4 @@ if (!isset($_GET['page'])) {
$page = (int)$_GET['page']; $page = (int)$_GET['page'];
}; };
mod_board_log(Vichan\build_context($config), $board['uri'], $page, $hide_names, true); mod_board_log($board['uri'], $page, $hide_names, true);

48
mod.php
View file

@ -1,21 +1,18 @@
<?php <?php
/* /*
* Copyright (c) 2010-2014 Tinyboard Development Group * Copyright (c) 2010-2014 Tinyboard Development Group
*/ */
require_once 'inc/bootstrap.php'; require_once 'inc/bootstrap.php';
if ($config['debug']) { if ($config['debug'])
$parse_start_time = microtime(true); $parse_start_time = microtime(true);
}
require_once 'inc/bans.php'; require_once 'inc/bans.php';
require_once 'inc/mod/pages.php'; require_once 'inc/mod/pages.php';
check_login(true);
$ctx = Vichan\build_context($config);
check_login($ctx, true);
$query = isset($_SERVER['QUERY_STRING']) ? rawurldecode($_SERVER['QUERY_STRING']) : ''; $query = isset($_SERVER['QUERY_STRING']) ? rawurldecode($_SERVER['QUERY_STRING']) : '';
@ -25,7 +22,7 @@ if(isset($_GET['thread'])) {
$query = explode("&thread=", $query)[0]; $query = explode("&thread=", $query)[0];
} }
$pages = [ $pages = array(
'' => ':?/', // redirect to dashboard '' => ':?/', // redirect to dashboard
'/' => 'dashboard', // dashboard '/' => 'dashboard', // dashboard
'/confirm/(.+)' => 'confirm', // confirm action (if javascript didn't work) '/confirm/(.+)' => 'confirm', // confirm action (if javascript didn't work)
@ -68,15 +65,8 @@ $pages = [
'/reports/(\d+)/dismiss(all)?' => 'secure report_dismiss', // dismiss a report '/reports/(\d+)/dismiss(all)?' => 'secure report_dismiss', // dismiss a report
'/IP/([\w.:]+)' => 'secure_POST ip', // view ip address '/IP/([\w.:]+)' => 'secure_POST ip', // view ip address
'/IP/([\w.:]+)/cursor/([\w|-|_]+)' => 'secure_POST ip', // view ip address
'/IP/([\w.:]+)/remove_note/(\d+)' => 'secure ip_remove_note', // remove note from ip address '/IP/([\w.:]+)/remove_note/(\d+)' => 'secure ip_remove_note', // remove note from ip address
'/user_posts/ip/([\w.:]+)' => 'secure_POST user_posts_by_ip', // view user posts by ip address
'/user_posts/ip/([\w.:]+)/cursor/([\w|-|_]+)' => 'secure_POST user_posts_by_ip', // remove note from ip address
'/user_posts/passwd/(\w+)' => 'secure_POST user_posts_by_passwd', // view user posts by ip address
'/user_posts/passwd/(\w+)/cursor/([\w|-|_]+)' => 'secure_POST user_posts_by_passwd', // remove note from ip address
'/ban' => 'secure_POST ban', // new ban '/ban' => 'secure_POST ban', // new ban
'/bans' => 'secure_POST bans', // ban list '/bans' => 'secure_POST bans', // ban list
'/bans.json' => 'secure bans_json', // ban list JSON '/bans.json' => 'secure bans_json', // ban list JSON
@ -116,6 +106,7 @@ $pages = [
// these pages aren't listed in the dashboard without $config['debug'] // these pages aren't listed in the dashboard without $config['debug']
'/debug/antispam' => 'debug_antispam', '/debug/antispam' => 'debug_antispam',
'/debug/recent' => 'debug_recent_posts', '/debug/recent' => 'debug_recent_posts',
'/debug/apc' => 'debug_apc',
'/debug/sql' => 'secure_POST debug_sql', '/debug/sql' => 'secure_POST debug_sql',
// This should always be at the end: // This should always be at the end:
@ -129,14 +120,14 @@ $pages = [
str_replace('%d', '(\d+)', preg_quote($config['file_page'], '!')) => 'view_thread', str_replace('%d', '(\d+)', preg_quote($config['file_page'], '!')) => 'view_thread',
'/(\%b)/' . preg_quote($config['dir']['res'], '!') . '/(\%b)/' . preg_quote($config['dir']['res'], '!') .
str_replace([ '%d','%s' ], [ '(\d+)', '[a-z0-9-]+' ], preg_quote($config['file_page50_slug'], '!')) => 'view_thread50', str_replace(array('%d','%s'), array('(\d+)', '[a-z0-9-]+'), preg_quote($config['file_page50_slug'], '!')) => 'view_thread50',
'/(\%b)/' . preg_quote($config['dir']['res'], '!') . '/(\%b)/' . preg_quote($config['dir']['res'], '!') .
str_replace([ '%d','%s' ], [ '(\d+)', '[a-z0-9-]+' ], preg_quote($config['file_page_slug'], '!')) => 'view_thread', str_replace(array('%d','%s'), array('(\d+)', '[a-z0-9-]+'), preg_quote($config['file_page_slug'], '!')) => 'view_thread',
]; );
if (!$mod) { if (!$mod) {
$pages = [ '!^(.+)?$!' => 'login' ]; $pages = array('!^(.+)?$!' => 'login');
} elseif (isset($_GET['status'], $_GET['r'])) { } elseif (isset($_GET['status'], $_GET['r'])) {
header('Location: ' . $_GET['r'], true, (int)$_GET['status']); header('Location: ' . $_GET['r'], true, (int)$_GET['status']);
exit; exit;
@ -146,11 +137,12 @@ if (isset($config['mod']['custom_pages'])) {
$pages = array_merge($pages, $config['mod']['custom_pages']); $pages = array_merge($pages, $config['mod']['custom_pages']);
} }
$new_pages = []; $new_pages = array();
foreach ($pages as $key => $callback) { foreach ($pages as $key => $callback) {
if (is_string($callback) && preg_match('/^secure /', $callback)) { if (is_string($callback) && preg_match('/^secure /', $callback)) {
$key .= '(/(?P<token>[a-f0-9]{8}))?'; $key .= '(/(?P<token>[a-f0-9]{8}))?';
} }
$key = str_replace('\%b', '?P<board>' . sprintf(substr($config['board_path'], 0, -1), $config['board_regex']), $key); $key = str_replace('\%b', '?P<board>' . sprintf(substr($config['board_path'], 0, -1), $config['board_regex']), $key);
$new_pages[@$key[0] == '!' ? $key : '!^' . $key . '(?:&[^&=]+=[^&]*)*$!u'] = $callback; $new_pages[@$key[0] == '!' ? $key : '!^' . $key . '(?:&[^&=]+=[^&]*)*$!u'] = $callback;
} }
@ -158,7 +150,7 @@ $pages = $new_pages;
foreach ($pages as $uri => $handler) { foreach ($pages as $uri => $handler) {
if (preg_match($uri, $query, $matches)) { if (preg_match($uri, $query, $matches)) {
$matches[0] = $ctx; // Replace the text captured by the full pattern with a reference to the context. $matches = array_slice($matches, 1);
if (isset($matches['board'])) { if (isset($matches['board'])) {
$board_match = $matches['board']; $board_match = $matches['board'];
@ -178,7 +170,7 @@ foreach ($pages as $uri => $handler) {
if ($secure_post_only) if ($secure_post_only)
error($config['error']['csrf']); error($config['error']['csrf']);
else { else {
mod_confirm($ctx, substr($query, 1)); mod_confirm(substr($query, 1));
exit; exit;
} }
} }
@ -193,20 +185,24 @@ foreach ($pages as $uri => $handler) {
} }
if ($config['debug']) { if ($config['debug']) {
$debug['mod_page'] = [ $debug['mod_page'] = array(
'req' => $query, 'req' => $query,
'match' => $uri, 'match' => $uri,
'handler' => $handler, 'handler' => $handler,
]; );
$debug['time']['parse_mod_req'] = '~' . round((microtime(true) - $parse_start_time) * 1000, 2) . 'ms'; $debug['time']['parse_mod_req'] = '~' . round((microtime(true) - $parse_start_time) * 1000, 2) . 'ms';
} }
// We don't want to call named parameters (PHP 8). if (is_array($matches)) {
$matches = array_values($matches); // we don't want to call named parameters (PHP 8)
$matches = array_values($matches);
}
if (is_string($handler)) { if (is_string($handler)) {
if ($handler[0] == ':') { if ($handler[0] == ':') {
header('Location: ' . substr($handler, 1), true, $config['redirect_http']); header('Location: ' . substr($handler, 1), true, $config['redirect_http']);
} elseif (is_callable("mod_page_$handler")) {
call_user_func_array("mod_page_$handler", $matches);
} elseif (is_callable("mod_$handler")) { } elseif (is_callable("mod_$handler")) {
call_user_func_array("mod_$handler", $matches); call_user_func_array("mod_$handler", $matches);
} else { } else {

428
post.php
View file

@ -3,10 +3,6 @@
* Copyright (c) 2010-2014 Tinyboard Development Group * Copyright (c) 2010-2014 Tinyboard Development Group
*/ */
use Vichan\Context;
use Vichan\Data\ReportQueries;
use Vichan\Data\Driver\LogDriver;
require_once 'inc/bootstrap.php'; require_once 'inc/bootstrap.php';
/** /**
@ -39,6 +35,35 @@ function md5_hash_of_file($config, $file)
} }
} }
/**
* Strip the markup from the given string
*
* @param string $post_body The body of the post.
* @return string
*/
function strip_markup($post_body)
{
if (mysql_version() >= 50503) {
// Assume we're using the utf8mb4 charset.
return $post_body;
} else {
// MySQL's `utf8` charset only supports up to 3-byte symbols.
// Remove anything >= 0x010000.
$chars = preg_split('//u', $post_body, -1, PREG_SPLIT_NO_EMPTY);
$res = '';
foreach ($chars as $char) {
$o = 0;
$ord = ordutf8($char, $o);
if ($ord >= 0x010000)
continue;
$res .= $char;
}
return $res;
}
}
/** /**
* Checks if the user at the remote ip passed the captcha. * Checks if the user at the remote ip passed the captcha.
* *
@ -142,126 +167,6 @@ function check_turnstile($secret, $response, $remote_ip, $expected_action)
return $json_ret['success'] === true && $json_ret['action'] === $expected_action; return $json_ret['success'] === true && $json_ret['action'] === $expected_action;
} }
/**
* A "sophisticated" workaround to js/ajax.js calling post.php multiple times on error/ban.
*/
function check_captcha(array $captcha_config, string $form_id, string $board_uri, string $response, string $remote_ip, string $expected_action) {
$dynamic = $captcha_config['dynamic'];
if ($dynamic !== false && $remote_ip !== $dynamic) {
return true;
}
switch ($captcha_config['mode']) {
case 'recaptcha':
case 'hcaptcha':
case 'turnstile':
$mode = $captcha_config['mode'];
break;
case false:
return true;
default:
\error_log("Unknown captcha mode '{$captcha_config['mode']}'");
throw new \RuntimeException('Captcha configuration error');
}
$passthrough_timeout = $captcha_config['passthrough_timeout'];
if ($passthrough_timeout != 0) {
$pass = Cache::get("captcha_passthrough_{$remote_ip}_{$form_id}");
if ($pass !== false) {
$let_through = $pass['expires'] > time() && $pass['board_uri'] === $board_uri && $pass['captcha_response'] === $response;
if ($let_through) {
return true;
}
}
}
$remote_ip_send = $dynamic !== false ? null : $remote_ip;
$private_key = $captcha_config[$mode]['private'];
$public_key = $captcha_config[$mode]['public'];
switch ($mode) {
case 'recaptcha':
$ret = check_recaptcha($private_key, $response, $remote_ip_send);
break;
case 'hcaptcha':
$ret = check_hcaptcha($private_key, $response, $remote_ip_send, $public_key);
break;
case 'turnstile':
$ret = check_turnstile($private_key, $response, $remote_ip_send, $expected_action);
break;
}
if ($ret && $passthrough_timeout != 0) {
$pass = [
'expires' => time() + $passthrough_timeout,
'board_uri' => $board_uri,
'captcha_response' => $response
];
Cache::set("captcha_passthrough_{$remote_ip}_{$form_id}", $pass, $passthrough_timeout + 2);
}
return $ret;
}
function send_matrix_report(
string $matrix_host,
string $room_id,
string $access_token,
int $max_msg_len,
string $report_reason,
string $domain,
string $board_dir,
string $board_res_dir,
array $post,
int $id,
) {
$post_id = $post['thread'] ? $post['thread'] : $id;
$reported_post_url = "$domain/mod.php?/{$board_dir}{$board_res_dir}{$post_id}.html";
$end = strlen($post['body_nomarkup']) > $max_msg_len ? ' [...]' : '';
$post_content = mb_substr($post['body_nomarkup'], 0, $max_msg_len) . $end;
$text_body = $reported_post_url . ($post['thread'] ? "#$id" : '') . " \nReason:\n" . $report_reason . " \nPost:\n" . $post_content;
$random_transaction_id = mt_rand();
$json_body = json_encode([
'msgtype' => 'm.text',
'body' => $text_body
]);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => "$matrix_host/_matrix/client/v3/rooms/$room_id/send/m.room.message/$random_transaction_id",
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"Authorization: Bearer $access_token"
],
CURLOPT_POSTFIELDS => $json_body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 3,
]);
$c_ret = curl_exec($ch);
if ($c_ret === false) {
$err_no = curl_errno($ch);
$err_str = curl_error($ch);
error_log("Failed to send report to matrix. Curl returned: $err_no ($err_str)");
return false;
}
curl_close($ch);
$json = json_decode($c_ret, true);
if ($json === null) {
error_log("Report forwarding failed, matrix returned a non-json value");
} elseif (!isset($json["event_id"])) {
$code = $json["errcode"] ?? '';
$desc = $json["error"] ?? '';
error_log("Report forwarding failed, matrix returned code '$code', with description '$desc'");
}
}
/** /**
* Deletes the (single) captcha associated with the ip and code. * Deletes the (single) captcha associated with the ip and code.
* *
@ -320,6 +225,26 @@ function db_select_post_minimal($board, $id)
return $post; return $post;
} }
/**
* Inserts a new report.
*
* @param string $ip Ip of the user sending the report.
* @param string $board Board of the reported thread. MUST ALREADY BE SANITIZED.
* @param int $post_id Post reported.
* @param string $reason Reason of the report.
* @return void
*/
function db_insert_report($ip, $board, $post_id, $reason)
{
$query = prepare("INSERT INTO ``reports`` VALUES (NULL, :time, :ip, :board, :post, :reason)");
$query->bindValue(':time', time(), PDO::PARAM_INT);
$query->bindValue(':ip', $ip, PDO::PARAM_STR);
$query->bindValue(':board', $board, PDO::PARAM_STR);
$query->bindValue(':post', $post_id, PDO::PARAM_INT);
$query->bindValue(':reason', $reason, PDO::PARAM_STR);
$query->execute() or error(db_error($query));
}
/** /**
* Inserts a new ban appeal into the database. * Inserts a new ban appeal into the database.
* *
@ -355,14 +280,14 @@ function db_select_ban_appeals($ban_id)
$dropped_post = false; $dropped_post = false;
function handle_nntpchan(Context $ctx) function handle_nntpchan()
{ {
global $config; global $config;
if ($_SERVER['REMOTE_ADDR'] != $config['nntpchan']['trusted_peer']) { if ($_SERVER['REMOTE_ADDR'] != $config['nntpchan']['trusted_peer']) {
error("NNTPChan: Forbidden. $_SERVER[REMOTE_ADDR] is not a trusted peer"); error("NNTPChan: Forbidden. $_SERVER[REMOTE_ADDR] is not a trusted peer");
} }
$_POST = []; $_POST = array();
$_POST['json_response'] = true; $_POST['json_response'] = true;
$headers = json_encode($_GET); $headers = json_encode($_GET);
@ -434,11 +359,11 @@ function handle_nntpchan(Context $ctx)
if ($ct == 'text/plain') { if ($ct == 'text/plain') {
$content = file_get_contents("php://input"); $content = file_get_contents("php://input");
} elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') { } elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') {
$ctx->get(LogDriver::class)->log(LogDriver::DEBUG, 'MM: Files: ' . print_r($GLOBALS, true)); _syslog(LOG_INFO, "MM: Files: " . print_r($GLOBALS, true)); // Debug
$content = ''; $content = '';
$newfiles = []; $newfiles = array();
foreach ($_FILES['attachment']['error'] as $id => $error) { foreach ($_FILES['attachment']['error'] as $id => $error) {
if ($_FILES['attachment']['type'][$id] == 'text/plain') { if ($_FILES['attachment']['type'][$id] == 'text/plain') {
$content .= file_get_contents($_FILES['attachment']['tmp_name'][$id]); $content .= file_get_contents($_FILES['attachment']['tmp_name'][$id]);
@ -446,7 +371,7 @@ function handle_nntpchan(Context $ctx)
// Signed message, ignore for now // Signed message, ignore for now
} else { } else {
// A real attachment :^) // A real attachment :^)
$file = []; $file = array();
$file['name'] = $_FILES['attachment']['name'][$id]; $file['name'] = $_FILES['attachment']['name'][$id];
$file['type'] = $_FILES['attachment']['type'][$id]; $file['type'] = $_FILES['attachment']['type'][$id];
$file['size'] = $_FILES['attachment']['size'][$id]; $file['size'] = $_FILES['attachment']['size'][$id];
@ -490,7 +415,7 @@ function handle_nntpchan(Context $ctx)
if (count($ary) == 0) { if (count($ary) == 0) {
return ">>>>$id"; return ">>>>$id";
} else { } else {
$ret = []; $ret = array();
foreach ($ary as $v) { foreach ($ary as $v) {
if ($v['board'] != $xboard) { if ($v['board'] != $xboard) {
$ret[] = ">>>/" . $v['board'] . "/" . $v['id']; $ret[] = ">>>/" . $v['board'] . "/" . $v['id'];
@ -513,7 +438,7 @@ function handle_nntpchan(Context $ctx)
); );
} }
function handle_delete(Context $ctx) function handle_delete()
{ {
// Delete // Delete
global $config, $board, $mod; global $config, $board, $mod;
@ -521,7 +446,7 @@ function handle_delete(Context $ctx)
error($config['error']['bot']); error($config['error']['bot']);
} }
check_login($ctx, false); check_login(false);
$is_mod = !!$mod; $is_mod = !!$mod;
if (isset($_POST['mod']) && $_POST['mod'] && !$mod) { if (isset($_POST['mod']) && $_POST['mod'] && !$mod) {
@ -531,13 +456,11 @@ function handle_delete(Context $ctx)
$password = &$_POST['password']; $password = &$_POST['password'];
if (empty($password)) { if ($password == '') {
error($config['error']['invalidpassword']); error($config['error']['invalidpassword']);
} }
$password = hashPassword($_POST['password']); $delete = array();
$delete = [];
foreach ($_POST as $post => $value) { foreach ($_POST as $post => $value) {
if (preg_match('/^delete_(\d+)$/', $post, $m)) { if (preg_match('/^delete_(\d+)$/', $post, $m)) {
$delete[] = (int) $m[1]; $delete[] = (int) $m[1];
@ -611,8 +534,8 @@ function handle_delete(Context $ctx)
modLog("User at $ip deleted his own post #$id"); modLog("User at $ip deleted his own post #$id");
} }
$ctx->get(LogDriver::class)->log( _syslog(
LogDriver::INFO, LOG_INFO,
'Deleted post: ' . 'Deleted post: ' .
'/' . $board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $post['thread'] ? $post['thread'] : $id) . ($post['thread'] ? '#' . $id : '') '/' . $board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $post['thread'] ? $post['thread'] : $id) . ($post['thread'] ? '#' . $id : '')
); );
@ -639,13 +562,13 @@ function handle_delete(Context $ctx)
rebuildThemes('post-delete', $board['uri']); rebuildThemes('post-delete', $board['uri']);
} }
function handle_report(Context $ctx) function handle_report()
{ {
global $config, $board; global $config, $board;
if (!isset($_POST['board'], $_POST['reason'])) if (!isset($_POST['board'], $_POST['reason']))
error($config['error']['bot']); error($config['error']['bot']);
$report = []; $report = array();
foreach ($_POST as $post => $value) { foreach ($_POST as $post => $value) {
if (preg_match('/^delete_(\d+)$/', $post, $m)) { if (preg_match('/^delete_(\d+)$/', $post, $m)) {
$report[] = (int) $m[1]; $report[] = (int) $m[1];
@ -695,12 +618,12 @@ function handle_report(Context $ctx)
$reason = escape_markup_modifiers($_POST['reason']); $reason = escape_markup_modifiers($_POST['reason']);
markup($reason); markup($reason);
$report_queries = $ctx->get(ReportQueries::class);
foreach ($report as $id) { foreach ($report as $id) {
$post = db_select_post_minimal($board['uri'], $id); $post = db_select_post_minimal($board['uri'], $id);
if ($post === false) { if ($post === false) {
$ctx->get(LogDriver::class)->log(LogDriver::INFO, "Failed to report non-existing post #{$id} in {$board['dir']}"); if ($config['syslog']) {
_syslog(LOG_INFO, "Failed to report non-existing post #{$id} in {$board['dir']}");
}
error($config['error']['nopost']); error($config['error']['nopost']);
} }
@ -715,14 +638,15 @@ function handle_report(Context $ctx)
error($error); error($error);
} }
$ctx->get(LogDriver::class)->log( if ($config['syslog'])
LogDriver::INFO, _syslog(
'Reported post: ' . LOG_INFO,
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') . 'Reported post: ' .
' for "' . $reason . '"' '/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
); ' for "' . $reason . '"'
);
$report_queries->add($_SERVER['REMOTE_ADDR'], $board['uri'], $id, $reason); db_insert_report($_SERVER['REMOTE_ADDR'], $board['uri'], $id, $reason);
if ($config['slack']) { if ($config['slack']) {
function slack($message, $room = "reports", $icon = ":no_entry_sign:") function slack($message, $room = "reports", $icon = ":no_entry_sign:")
@ -756,19 +680,25 @@ function handle_report(Context $ctx)
} }
if ($config['matrix']['enabled']) { if (isset($config['matrix'])) {
send_matrix_report( $reported_post_url = $config['domain'] . "/mod.php?/" . $board['dir'] . $config['dir']['res'] . ($post['thread'] ? $post['thread'] : $id) . ".html";
$config['matrix']['host'], $post_url = $config['matrix']['host'] . "/_matrix/client/r0/rooms/" . $config['matrix']['room_id'] . "/send/m.room.message?access_token=" . $config['matrix']['access_token'];
$config['matrix']['room_id'],
$config['matrix']['access_token'], $trimmed_post = strlen($post['body_nomarkup']) > $config['matrix']['max_message_length'] ? ' [...]' : '';
$config['matrix']['max_message_length'], $postcontent = mb_substr($post['body_nomarkup'], 0, $config['matrix']['max_message_length']) . $trimmed_post;
$reason, $matrix_message = $reported_post_url . ($post['thread'] ? '#' . $id : '') . " \nReason:\n" . $reason . " \nPost:\n" . $postcontent . " \n";
$config['domain'], $post_data = json_encode(
$board['dir'], array(
$config['dir']['res'], "msgtype" => "m.text",
$post, "body" => $matrix_message
$id )
); );
$ch = curl_init($post_url);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$postResult = curl_exec($ch);
curl_close($ch);
} }
} }
@ -787,7 +717,7 @@ function handle_report(Context $ctx)
} }
} }
function handle_post(Context $ctx) function handle_post()
{ {
global $config, $dropped_post, $board, $mod, $pdo; global $config, $dropped_post, $board, $mod, $pdo;
@ -795,7 +725,7 @@ function handle_post(Context $ctx)
error($config['error']['bot']); error($config['error']['bot']);
} }
$post = array('board' => $_POST['board'], 'files' => []); $post = array('board' => $_POST['board'], 'files' => array());
// Check if board exists // Check if board exists
if (!openBoard($post['board'])) { if (!openBoard($post['board'])) {
@ -835,16 +765,56 @@ function handle_post(Context $ctx)
if (!$dropped_post) { if (!$dropped_post) {
// Check for CAPTCHA right after opening the board so the "return" link is in there. if ($config['dynamic_captcha'] !== false) {
if ($config['captcha']['mode'] !== false) { if ($_SERVER['REMOTE_ADDR'] === $config['dynamic_captcha']) {
if (!isset($_POST['captcha-response'], $_POST['captcha-form-id'])) { if ($config['recaptcha']) {
error($config['error']['bot']); if (!isset($_POST['g-recaptcha-response'])) {
error($config['error']['bot']);
}
if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], null)) {
error($config['error']['captcha']);
}
} elseif ($config['hcaptcha']) {
if (!isset($_POST['h-captcha-response'])) {
error($config['error']['bot']);
}
if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], null, $config['hcaptcha_public'])) {
error($config['error']['captcha']);
}
} elseif ($config['turnstile']) {
if (!isset($_POST['cf-turnstile-response'])) {
error($config['error']['bot']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) {
error($config['error']['captcha']);
}
}
} }
} else {
$expected_action = $post['op'] ? 'post-thread' : 'post-reply'; // Check for CAPTCHA right after opening the board so the "return" link is in there.
$ret = check_captcha($config['captcha'], $_POST['captcha-form-id'], $post['board'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $expected_action); if ($config['recaptcha']) {
if (!$ret) { if (!isset($_POST['g-recaptcha-response'])) {
error($config['error']['captcha']); error($config['error']['bot']);
}
if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR'])) {
error($config['error']['captcha']);
}
} elseif ($config['hcaptcha']) {
if (!isset($_POST['h-captcha-response'])) {
error($config['error']['bot']);
}
if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], $_SERVER['REMOTE_ADDR'], $config['hcaptcha_public'])) {
error($config['error']['captcha']);
}
} elseif ($config['turnstile']) {
if (!isset($_POST['cf-turnstile-response'])) {
error($config['error']['bot']);
}
$expected_action = $post['op'] ? 'post-thread' : 'post-reply';
if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) {
error($config['error']['captcha']);
}
} }
} }
@ -888,15 +858,8 @@ function handle_post(Context $ctx)
// Check if banned // Check if banned
checkBan($board['uri']); 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']) { if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
check_login($ctx, false); check_login(false);
if (!$mod) { if (!$mod) {
// Liar. You're not a mod >:-[ // Liar. You're not a mod >:-[
error($config['error']['notamod']); error($config['error']['notamod']);
@ -1009,16 +972,11 @@ function handle_post(Context $ctx)
} }
} }
// We must do this check now before the passowrd is hashed and overwritten.
if (\mb_strlen($_POST['password']) > 20) {
error(\sprintf($config['error']['toolong'], 'password'));
}
$post['name'] = $_POST['name'] != '' ? $_POST['name'] : $config['anonymous']; $post['name'] = $_POST['name'] != '' ? $_POST['name'] : $config['anonymous'];
$post['subject'] = $_POST['subject']; $post['subject'] = $_POST['subject'];
$post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email'])); $post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email']));
$post['body'] = $_POST['body']; $post['body'] = $_POST['body'];
$post['password'] = hashPassword($_POST['password']); $post['password'] = $_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)); $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 (!$dropped_post) {
@ -1192,22 +1150,14 @@ function handle_post(Context $ctx)
if (mb_strlen($post['subject']) > 100) { if (mb_strlen($post['subject']) > 100) {
error(sprintf($config['error']['toolong'], 'subject')); error(sprintf($config['error']['toolong'], 'subject'));
} }
if (!$mod) { if (!$mod && mb_strlen($post['body']) > $config['max_body']) {
$body_mb_len = mb_strlen($post['body']); error($config['error']['toolong_body']);
$is_op = $post['op']; }
if (!$mod && mb_strlen($post['body']) > 0 && (mb_strlen($post['body']) < $config['min_body'])) {
if (($is_op && $config['force_body_op']) || (!$is_op && $config['force_body'])) { error($config['error']['tooshort_body']);
$min_body = $is_op ? $config['min_body_op'] : $config['min_body']; }
if (mb_strlen($post['password']) > 20) {
if ($body_mb_len < $min_body) { error(sprintf($config['error']['toolong'], 'password'));
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']);
}
} }
} }
@ -1272,7 +1222,7 @@ function handle_post(Context $ctx)
} }
} }
$post['body_nomarkup'] = $post['body']; $post['body_nomarkup'] = strip_markup($post['body']);
$post['tracked_cites'] = markup($post['body'], true); $post['tracked_cites'] = markup($post['body'], true);
if ($post['has_file']) { if ($post['has_file']) {
@ -1313,7 +1263,7 @@ function handle_post(Context $ctx)
if (!hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) { if (!hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) {
require_once 'inc/filters.php'; require_once 'inc/filters.php';
do_filters($ctx, $post); do_filters($post);
} }
if ($post['has_file']) { if ($post['has_file']) {
@ -1402,7 +1352,7 @@ function handle_post(Context $ctx)
$file['thumbwidth'] = $size[0]; $file['thumbwidth'] = $size[0];
$file['thumbheight'] = $size[1]; $file['thumbheight'] = $size[1];
} elseif ( } elseif (
(($config['strip_exif'] && isset($file['exif_stripped']) && $file['exif_stripped']) || !$config['strip_exif']) && $config['minimum_copy_resize'] &&
$image->size->width <= $config['thumb_width'] && $image->size->width <= $config['thumb_width'] &&
$image->size->height <= $config['thumb_height'] && $image->size->height <= $config['thumb_height'] &&
$file['extension'] == ($config['thumb_ext'] ? $config['thumb_ext'] : $file['extension']) $file['extension'] == ($config['thumb_ext'] ? $config['thumb_ext'] : $file['extension'])
@ -1482,7 +1432,7 @@ function handle_post(Context $ctx)
// getting all images and then choosing one. // getting all images and then choosing one.
for( $i = 0; $i < $zip->numFiles; $i++ ){ for( $i = 0; $i < $zip->numFiles; $i++ ){
$stat = $zip->statIndex( $i ); $stat = $zip->statIndex( $i );
$matches = []; $matches = array();
if (preg_match('/.*cover.*\.(jpg|jpeg|png)/', $stat['name'], $matches)) { if (preg_match('/.*cover.*\.(jpg|jpeg|png)/', $stat['name'], $matches)) {
$filename = $matches[0]; $filename = $matches[0];
break; break;
@ -1551,6 +1501,35 @@ 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($dont_copy_file) || !$dont_copy_file) {
if (isset($file['file_tmp'])) { if (isset($file['file_tmp'])) {
if (!@rename($file['tmp_name'], $file['file'])) { if (!@rename($file['tmp_name'], $file['file'])) {
@ -1598,6 +1577,11 @@ 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($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']) && !$dropped_post) {
undoImage($post); undoImage($post);
if ($config['robot_mute']) { if ($config['robot_mute']) {
@ -1639,6 +1623,10 @@ function handle_post(Context $ctx)
$post = (array) $post; $post = (array) $post;
if ($post['files']) {
$post['files'] = $post['files'];
}
$post['num_files'] = sizeof($post['files']); $post['num_files'] = sizeof($post['files']);
$post['id'] = $id = post($post); $post['id'] = $id = post($post);
@ -1695,7 +1683,7 @@ function handle_post(Context $ctx)
} }
if (isset($post['tracked_cites']) && !empty($post['tracked_cites'])) { if (isset($post['tracked_cites']) && !empty($post['tracked_cites'])) {
$insert_rows = []; $insert_rows = array();
foreach ($post['tracked_cites'] as $cite) { foreach ($post['tracked_cites'] as $cite) {
$insert_rows[] = '(' . $insert_rows[] = '(' .
$pdo->quote($board['uri']) . ', ' . (int) $id . ', ' . $pdo->quote($board['uri']) . ', ' . (int) $id . ', ' .
@ -1713,7 +1701,7 @@ function handle_post(Context $ctx)
if (isset($_COOKIE[$config['cookies']['js']])) { if (isset($_COOKIE[$config['cookies']['js']])) {
$js = json_decode($_COOKIE[$config['cookies']['js']]); $js = json_decode($_COOKIE[$config['cookies']['js']]);
} else { } else {
$js = (object) []; $js = (object) array();
} }
// Tell it to delete the cached post for referer // Tell it to delete the cached post for referer
$js->{$_SERVER['HTTP_REFERER']} = true; $js->{$_SERVER['HTTP_REFERER']} = true;
@ -1747,10 +1735,10 @@ function handle_post(Context $ctx)
buildThread($post['op'] ? $id : $post['thread']); buildThread($post['op'] ? $id : $post['thread']);
$ctx->get(LogDriver::class)->log( if ($config['syslog']) {
LogDriver::INFO, _syslog(LOG_INFO, 'New post: /' . $board['dir'] . $config['dir']['res'] .
'New post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . (!$post['op'] ? '#' . $id : '') link_for($post) . (!$post['op'] ? '#' . $id : ''));
); }
if (!$post['mod']) { if (!$post['mod']) {
header('X-Associated-Content: "' . $redirect . '"'); header('X-Associated-Content: "' . $redirect . '"');
@ -1799,7 +1787,7 @@ function handle_post(Context $ctx)
} }
} }
function handle_appeal(Context $ctx) function handle_appeal()
{ {
global $config; global $config;
if (!isset($_POST['ban_id'])) { if (!isset($_POST['ban_id'])) {
@ -1843,25 +1831,23 @@ function handle_appeal(Context $ctx)
displayBan($ban); displayBan($ban);
} }
$ctx = Vichan\build_context($config);
// Is it a post coming from NNTP? Let's extract it and pretend it's a normal post. // Is it a post coming from NNTP? Let's extract it and pretend it's a normal post.
if (isset($_GET['Newsgroups'])) { if (isset($_GET['Newsgroups'])) {
if ($config['nntpchan']['enabled']) { if ($config['nntpchan']['enabled']) {
handle_nntpchan($ctx); handle_nntpchan();
} else { } else {
error("NNTPChan: NNTPChan support is disabled"); error("NNTPChan: NNTPChan support is disabled");
} }
} }
if (isset($_POST['delete'])) { if (isset($_POST['delete'])) {
handle_delete($ctx); handle_delete();
} elseif (isset($_POST['report'])) { } elseif (isset($_POST['report'])) {
handle_report($ctx); handle_report();
} elseif (isset($_POST['post']) || $dropped_post) { } elseif (isset($_POST['post']) || $dropped_post) {
handle_post($ctx); handle_post();
} elseif (isset($_POST['appeal'])) { } elseif (isset($_POST['appeal'])) {
handle_appeal($ctx); handle_appeal();
} else { } else {
if (!file_exists($config['has_installed'])) { if (!file_exists($config['has_installed'])) {
header('Location: install.php', true, $config['redirect_http']); header('Location: install.php', true, $config['redirect_http']);

View file

@ -1,8 +0,0 @@
<?php
$files = scandir(__dir__ . '/static/spooks/', SCANDIR_SORT_NONE);
$files = array_diff($files, ['.', '..']);
$filename = $files[array_rand($files)];
header("Location: /static/spooks/$filename", true, 307);
header('Cache-Control: no-cache');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View file

@ -152,11 +152,6 @@ div.post.reply.highlighted
box-shadow: 3px 5px #5c8c8e; box-shadow: 3px 5px #5c8c8e;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited div.post.reply div.body a:link, div.post.reply div.body a:visited
{ {

View file

@ -219,11 +219,6 @@ background-color: #2b2b2b;
background-color: #2b2b2b; background-color: #2b2b2b;
border: solid 1px #93e0e3; border: solid 1px #93e0e3;
box-shadow: 3px 5px #93e0e3; box-shadow: 3px 5px #93e0e3;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
/*dont touch this*/ /*dont touch this*/

View file

@ -161,11 +161,6 @@ div.post.reply.highlighted
box-shadow: 3px 5px #5c8c8e; box-shadow: 3px 5px #5c8c8e;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited div.post.reply div.body a:link, div.post.reply div.body a:visited
{ {

View file

@ -120,11 +120,6 @@ div.post.reply.highlighted {
background: rgba(59, 22, 43, 0.4); background: rgba(59, 22, 43, 0.4);
border: 1px solid #117743; border: 1px solid #117743;
border-radius: 5px; border-radius: 5px;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
/* POST CONTENT */ /* POST CONTENT */

View file

@ -1,7 +1,7 @@
/*cyberpunk mod of ferus by kalyx /*cyberpunk mod of ferus by kalyx
B332E6 = dark purp B332E6 = dark purp
33cccc = teal 33cccc = teal
00ff00 = green 00ff00 = green
FF46D2 = pink FF46D2 = pink
dark blue = 00080C dark blue = 00080C
@ -37,7 +37,7 @@ div.boardlist{
background-color: #0C0001; background-color: #0C0001;
} }
@font-face @font-face
{ {
font-family: 'lain'; font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'), src: url('./fonts/nrdyyh.woff') format('woff'),
@ -84,7 +84,7 @@ a:link, a:visited, p.intro a.email span.name
-ms-transition: 0.15s text-shadow, 0.15s color; -ms-transition: 0.15s text-shadow, 0.15s color;
transition: 0.15s text-shadow, 0.15s color; transition: 0.15s text-shadow, 0.15s color;
} }
input[type="text"], textarea input[type="text"], textarea
{ {
-moz-transition: 0.15s border-color; -moz-transition: 0.15s border-color;
-webkit-transition: 0.15s border-color; -webkit-transition: 0.15s border-color;
@ -93,7 +93,7 @@ input[type="text"], textarea
-ms-transition: 0.15s border-color; -ms-transition: 0.15s border-color;
transition: 0.15s border-color; transition: 0.15s border-color;
} }
input[type="submit"] input[type="submit"]
{ {
-moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
-webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
@ -117,7 +117,7 @@ a.post_no {
color: #33cccc; color: #33cccc;
text-decoration: none; text-decoration: none;
} }
span.quote span.quote
{ {
color:#00ff00; color:#00ff00;
} }
@ -136,11 +136,6 @@ div.post.reply {
div.post.reply.highlighted { div.post.reply.highlighted {
background: transparent; background: transparent;
border: #1A6666 2px solid; border: #1A6666 2px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited { div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464; color: #646464;

View file

@ -1,7 +1,7 @@
/*cyberpunk mod of ferus by kalyx /*cyberpunk mod of ferus by kalyx
B332E6 = dark purp B332E6 = dark purp
33cccc = teal 33cccc = teal
00ff00 = green 00ff00 = green
FF46D2 = pink FF46D2 = pink
dark blue = 00080C dark blue = 00080C
@ -14,7 +14,7 @@ body {
font-size: 11px; font-size: 11px;
/*text-shadow: 0px 0px 3px #FF46D2;*/ /*text-shadow: 0px 0px 3px #FF46D2;*/
} }
@font-face @font-face
{ {
font-family: 'lain'; font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'), src: url('./fonts/nrdyyh.woff') format('woff'),
@ -61,7 +61,7 @@ a:link, a:visited, p.intro a.email span.name
-ms-transition: 0.15s text-shadow, 0.15s color; -ms-transition: 0.15s text-shadow, 0.15s color;
transition: 0.15s text-shadow, 0.15s color; transition: 0.15s text-shadow, 0.15s color;
} }
input[type="text"], textarea input[type="text"], textarea
{ {
-moz-transition: 0.15s border-color; -moz-transition: 0.15s border-color;
-webkit-transition: 0.15s border-color; -webkit-transition: 0.15s border-color;
@ -70,7 +70,7 @@ input[type="text"], textarea
-ms-transition: 0.15s border-color; -ms-transition: 0.15s border-color;
transition: 0.15s border-color; transition: 0.15s border-color;
} }
input[type="submit"] input[type="submit"]
{ {
-moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
-webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
@ -94,7 +94,7 @@ a.post_no {
color: #33cccc; color: #33cccc;
text-decoration: none; text-decoration: none;
} }
span.quote span.quote
{ {
color:#00ff00; color:#00ff00;
} }
@ -113,11 +113,6 @@ div.post.reply {
div.post.reply.highlighted { div.post.reply.highlighted {
background: transparent; background: transparent;
border: #33cccc 2px solid; border: #33cccc 2px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited { div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464; color: #646464;

View file

@ -63,11 +63,6 @@ div.post.reply {
div.post.reply.highlighted { div.post.reply.highlighted {
background: #555; background: #555;
border: transparent 1px solid; border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited { div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #CCCCCC; color: #CCCCCC;

View file

@ -5,7 +5,7 @@
/*dark.css has been prepended (2021-11-11) instead of @import'd for performance*/ /*dark.css has been prepended (2021-11-11) instead of @import'd for performance*/
body { body {
background: #1E1E1E; background: #1E1E1E;
color: #C0C0C0; color: #A7A7A7;
font-family: Verdana, sans-serif; font-family: Verdana, sans-serif;
font-size: 14px; font-size: 14px;
} }
@ -31,28 +31,26 @@ div.title p {
font-size: 10px; font-size: 10px;
} }
a, a:link, a:visited, .intro a.email span.name { a, a:link, a:visited, .intro a.email span.name {
color: #EEE; color: #CCCCCC;
text-decoration: none; text-decoration: none;
font-family: Verdana, sans-serif; font-family: sans-serif;
} }
a:link:hover, a:visited:hover { a:link:hover, a:visited:hover {
color: #fff; color: #fff;
font-family: Verdana, sans-serif; font-family: sans-serif;
text-decoration: none; text-decoration: none;
} }
a.post_no { a.post_no {
color: #AAAAAA;
text-decoration: none; text-decoration: none;
} }
a.post_no:hover { a.post_no:hover {
color: #32DD72 !important; color: #32DD72 !important;
text-decoration: underline overline; text-decoration: underline overline;
} }
.intro a.post_no {
color: #EEE;
}
div.post.reply { div.post.reply {
background: #333333; background: #333333;
border: #4f4f4f 1px solid; border: #555555 1px solid;
@media (max-width: 48em) { @media (max-width: 48em) {
border-left-style: none; border-left-style: none;
@ -60,26 +58,21 @@ div.post.reply {
} }
} }
div.post.reply.highlighted { div.post.reply.highlighted {
background: #4f4f4f; background: #555;
border: transparent 1px solid; border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited { div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #EEE; color: #CCCCCC;
} }
div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover { div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover {
color: #32DD72; color: #32DD72;
} }
div.post.inline { div.post.inline {
border: #4f4f4f 1px solid; border: #555555 1px solid;
} }
.intro span.subject { .intro span.subject {
font-size: 12px; font-size: 12px;
font-family: Verdana, sans-serif; font-family: sans-serif;
color: #446655; color: #446655;
font-weight: 800; font-weight: 800;
} }
@ -96,16 +89,16 @@ div.post.inline {
} }
input[type="text"], textarea, select { input[type="text"], textarea, select {
background: #333333; background: #333333;
color: #EEE; color: #CCCCCC;
border: #666666 1px solid; border: #666666 1px solid;
padding-left: 5px; padding-left: 5px;
padding-right: -5px; padding-right: -5px;
font-family: Verdana, sans-serif; font-family: sans-serif;
font-size: 10pt; font-size: 10pt;
} }
input[type="password"] { input[type="password"] {
background: #333333; background: #333333;
color: #EEE; color: #CCCCCC;
border: #666666 1px solid; border: #666666 1px solid;
} }
form table tr th { form table tr th {
@ -133,10 +126,10 @@ div.banner a {
input[type="submit"] { input[type="submit"] {
background: #333333; background: #333333;
border: #888888 1px solid; border: #888888 1px solid;
color: #EEE; color: #CCCCCC;
} }
input[type="submit"]:hover { input[type="submit"]:hover {
background: #4f4f4f; background: #555555;
border: #888888 1px solid; border: #888888 1px solid;
color: #32DD72; color: #32DD72;
} }
@ -151,7 +144,7 @@ span.trip {
} }
div.pages { div.pages {
background: #1E1E1E; background: #1E1E1E;
font-family: Verdana, sans-serif; font-family: sans-serif;
} }
.bar.bottom { .bar.bottom {
bottom: 0px; bottom: 0px;
@ -159,7 +152,7 @@ div.pages {
background-color: #1E1E1E; background-color: #1E1E1E;
} }
div.pages a.selected { div.pages a.selected {
color: #EEE; color: #CCCCCC;
} }
hr { hr {
height: 1px; height: 1px;
@ -167,7 +160,7 @@ hr {
} }
div.boardlist { div.boardlist {
text-align: center; text-align: center;
color: #C0C0C0; color: #A7A7A7;
} }
div.ban { div.ban {
background-color: transparent; background-color: transparent;
@ -188,7 +181,7 @@ div.boardlist:not(.bottom) {
} }
.desktop-style div.boardlist:not(.bottom) { .desktop-style div.boardlist:not(.bottom) {
text-shadow: black 1px 1px 1px, black -1px -1px 1px, black -1px 1px 1px, black 1px -1px 1px; text-shadow: black 1px 1px 1px, black -1px -1px 1px, black -1px 1px 1px, black 1px -1px 1px;
color: #C0C0C0; color: #A7A7A7;
background-color: #1E1E1E; background-color: #1E1E1E;
} }
div.report { div.report {
@ -211,7 +204,7 @@ div.report {
} }
.box { .box {
background: #333333; background: #333333;
border-color: #4f4f4f; border-color: #555555;
color: #C5C8C6; color: #C5C8C6;
border-radius: 10px; border-radius: 10px;
} }
@ -221,7 +214,7 @@ div.report {
} }
table thead th { table thead th {
background: #333333; background: #333333;
border-color: #4f4f4f; border-color: #555555;
color: #C5C8C6; color: #C5C8C6;
border-radius: 4px; border-radius: 4px;
} }
@ -229,11 +222,11 @@ table tbody tr:nth-of-type( even ) {
background-color: #333333; background-color: #333333;
} }
table.board-list-table .board-uri .board-sfw { table.board-list-table .board-uri .board-sfw {
color: #EEE; color: #CCCCCC;
} }
tbody.board-list-omitted td { tbody.board-list-omitted td {
background: #333333; background: #333333;
border-color: #4f4f4f; border-color: #555555;
} }
table.board-list-table .board-tags .board-cell:hover { table.board-list-table .board-tags .board-cell:hover {
background: #1e1e1e; background: #1e1e1e;

View file

@ -1,294 +0,0 @@
/**
* Based on dark.css, with a teal extension.
* Clumps all rules into three rules to determine the new accent color
*/
body {
background: #1E1E1E;
color: #999999;
font-family: Verdana, sans-serif;
font-size: 14px;
}
@font-face {
font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'),
url('./fonts/tojcxo.TTF') format('truetype');
}
h1 {
letter-spacing: -2px;
font-size: 20pt;
text-align: center;
}
div.title p {
font-size: 10px;
}
a:link, a:visited, .intro a.email span.name {
color: #CCCCCC;
text-decoration: none;
font-family: sans-serif;
}
a:visited:hover {
color: #fff;
font-family: sans-serif;
text-decoration: none;
}
a.post_no {
color: #AAAAAA;
text-decoration: none;
}
a.post_no:hover {
color: #32DD72 !important;
text-decoration: underline overline;
}
div.post.reply {
background: #333333;
border: #555555 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply.highlighted {
background: #555;
border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #CCCCCC;
}
.intro span.subject {
font-size: 12px;
font-family: sans-serif;
color: #446655;
font-weight: 800;
}
.intro span.name {
color: #32DD72;
font-weight: 800;
}
.intro a.capcode, p.intro a.nametag {
color: magenta;
margin-left: 0;
}
.intro a.email, p.intro a.email span.name, p.intro a.email:hover, p.intro a.email:hover span.name {
color: #32ddaf;
}
input[type="text"], textarea, select {
background: #333333;
color: #CCCCCC;
border: #666666 1px solid;
padding-left: 5px;
padding-right: -5px;
font-family: sans-serif;
font-size: 10pt;
}
input[type="password"] {
background: #333333;
color: #CCCCCC;
border: #666666 1px solid;
}
form table tr th {
background: #333333;
color: #AAAAAA;
font-weight: 800;
text-align: left;
padding: 0;
}
div.banner {
background: #32DD72;
color: #000;
text-align: center;
width: 250px;
padding: 4px;
padding-left: 12px;
padding-right: 12px;
margin-left: auto;
margin-right: auto;
font-size: 12px;
}
div.banner a {
color:#000;
}
input[type="submit"] {
background: #333333;
border: #888888 1px solid;
color: #CCCCCC;
}
input[type="submit"]:hover {
background: #555555;
border: #888888 1px solid;
color: #32DD72;
}
input[type="text"]:focus {
border:#aaa 1px solid;
}
p.fileinfo a:hover {
text-decoration: underline;
}
span.trip {
color: #AAAAAA;
}
div.pages {
background: #1E1E1E;
font-family: sans-serif;
}
.bar.bottom {
bottom: 0px;
border-top: 1px solid #333333;
background-color: #1E1E1E;
}
div.pages a.selected {
color: #CCCCCC;
}
hr {
height: 1px;
border: #333333 1px solid;
}
div.boardlist {
text-align: center;
color: #999999;
}
div.ban {
background-color: transparent;
border: transparent 0px solid;
}
div.ban h2 {
background: transparent;
color: lime;
font-size: 12px;
}
table.modlog tr th {
background: #333333;
color: #AAAAAA;
}
div.boardlist:not(.bottom) {
background-color: #1E1E1E;
}
.desktop-style div.boardlist:not(.bottom) {
text-shadow: black 1px 1px 1px, black -1px -1px 1px, black -1px 1px 1px, black 1px -1px 1px;
color: #999999;
background-color: #1E1E1E;
}
div.report {
color: #666666;
}
/* options.js */
#options_div, #alert_div {
background: #333333;
}
.options_tab_icon {
color: #AAAAAA;
}
.options_tab_icon.active {
color: #FFFFFF;
}
#quick-reply table {
background: none repeat scroll 0% 0% #333 !important;
}
.modlog tr:nth-child(even), .modlog th {
background-color: #282A2E;
}
.box {
background: #333333;
border-color: #555555;
color: #C5C8C6;
border-radius: 10px;
}
.box-title {
background: transparent;
color: #32DD72;
}
table thead th {
background: #333333;
border-color: #555555;
color: #C5C8C6;
border-radius: 4px;
}
table tbody tr:nth-of-type( even ) {
background-color: #333333;
}
table.board-list-table .board-uri .board-sfw {
color: #CCCCCC;
}
tbody.board-list-omitted td {
background: #333333;
border-color: #555555;
}
table.board-list-table .board-tags .board-cell:hover {
background: #1e1e1e;
}
table.board-list-table tr:nth-of-type( even ) .board-tags .board-cell {
background: #333333;
}
.quote {
color:#3C827A;
}
div.blotter, h1, h2, header div.subtitle, div.title, a:link:hover, a:visited:hover p.intro a.post_no:hover,
div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover, p.intro span.name,
p.intro a.email, p.intro a.email span.name, p.intro a.email:hover, p.intro a.email:hover span.name,
input[type="submit"]:hover, div.ban h2 {
color: #59938D;
}
p.intro span.subject, .intro span.capcode, p.intro a.capcode, p.intro a.nametag, span.heading {
color: #50C4B8;
}
#pagewrap{
background: url('/spooks.php') bottom 20px right 20px fixed no-repeat; background-size: 20%;
}

View file

@ -1,7 +1,7 @@
/*cyberpunk mod of ferus by kalyx /*cyberpunk mod of ferus by kalyx
B332E6 = dark purp B332E6 = dark purp
293728 = teal 293728 = teal
59B451 = green 59B451 = green
FF46D2 = pink FF46D2 = pink
dark blue = 293728 dark blue = 293728
@ -19,13 +19,13 @@ div.boardlist{
} }
@font-face @font-face
{ {
font-family: 'lain'; font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'), src: url('./fonts/nrdyyh.woff') format('woff'),
url('./fonts/tojcxo.TTF') format('truetype'); url('./fonts/tojcxo.TTF') format('truetype');
} }
@font-face @font-face
{ {
font-family: 'DejaVuSansMono'; font-family: 'DejaVuSansMono';
src: url('./fonts/DejaVuSansMono.ttf') format('truetype'); src: url('./fonts/DejaVuSansMono.ttf') format('truetype');
@ -79,7 +79,7 @@ a:link, a:visited, p.intro a.email span.name
-ms-transition: 0.15s text-shadow, 0.15s color; -ms-transition: 0.15s text-shadow, 0.15s color;
transition: 0.15s text-shadow, 0.15s color; transition: 0.15s text-shadow, 0.15s color;
} }
input[type="text"], textarea input[type="text"], textarea
{ {
-moz-transition: 0.15s border-color; -moz-transition: 0.15s border-color;
-webkit-transition: 0.15s border-color; -webkit-transition: 0.15s border-color;
@ -88,7 +88,7 @@ input[type="text"], textarea
-ms-transition: 0.15s border-color; -ms-transition: 0.15s border-color;
transition: 0.15s border-color; transition: 0.15s border-color;
} }
input[type="submit"] input[type="submit"]
{ {
-moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
-webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
@ -112,7 +112,7 @@ a.post_no {
color: #293728; color: #293728;
text-decoration: none; text-decoration: none;
} }
span.quote span.quote
{ {
color:#4CADA7; color:#4CADA7;
} }
@ -131,11 +131,6 @@ div.post.reply {
div.post.reply.highlighted { div.post.reply.highlighted {
background: transparent; background: transparent;
border: #293728 1px solid; border: #293728 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited { div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464; color: #646464;

Binary file not shown.

View file

@ -25,7 +25,7 @@ div.boardlist{
background-color: #000!important; background-color: #000!important;
} }
@font-face @font-face
{ {
font-family: 'lain'; font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'), src: url('./fonts/nrdyyh.woff') format('woff'),
@ -74,7 +74,7 @@ a.post_no {
color: #d2738a; color: #d2738a;
text-decoration: none; text-decoration: none;
} }
span.quote span.quote
{ {
color:#d2738a; color:#d2738a;
} }
@ -93,11 +93,6 @@ div.post.reply {
div.post.reply.highlighted { div.post.reply.highlighted {
background: transparent; background: transparent;
border: #EDC7D0 1px solid; border: #EDC7D0 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited { div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464; color: #646464;
@ -221,11 +216,11 @@ table.modlog tr th {
#options_div { #options_div {
background-color: #000000; background-color: #000000;
} }
.options_tab_icon { .options_tab_icon {
color: #c1b492; color: #c1b492;
} }
.options_tab_icon.active { .options_tab_icon.active {
color: #d2738a; color: #d2738a;
} }

View file

@ -4,7 +4,7 @@ body {
font-family: monospace; font-family: monospace;
font-size: 11px; font-size: 11px;
} }
@font-face @font-face
{ {
font-family: 'lain'; font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'), src: url('./fonts/nrdyyh.woff') format('woff'),
@ -41,7 +41,7 @@ a.post_no {
color: #B332E6; color: #B332E6;
text-decoration: none; text-decoration: none;
} }
span.quote span.quote
{ {
color:#00ff00; color:#00ff00;
} }
@ -59,11 +59,6 @@ div.post.reply {
div.post.reply.highlighted { div.post.reply.highlighted {
background: transparent; background: transparent;
border: #B332E6 2px solid; border: #B332E6 2px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited { div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464; color: #646464;

View file

@ -1,7 +1,7 @@
/*cyberpunk mod of ferus by kalyx /*cyberpunk mod of ferus by kalyx
B332E6 = dark purp B332E6 = dark purp
293728 = teal 293728 = teal
59B451 = green 59B451 = green
FF46D2 = pink FF46D2 = pink
dark blue = 293728 dark blue = 293728
@ -19,13 +19,13 @@ div.boardlist{
} }
@font-face @font-face
{ {
font-family: 'lain'; font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'), src: url('./fonts/nrdyyh.woff') format('woff'),
url('./fonts/tojcxo.TTF') format('truetype'); url('./fonts/tojcxo.TTF') format('truetype');
} }
@font-face @font-face
{ {
font-family: 'DejaVuSansMono'; font-family: 'DejaVuSansMono';
src: url('./fonts/DejaVuSansMono.ttf') format('truetype'); src: url('./fonts/DejaVuSansMono.ttf') format('truetype');
@ -79,7 +79,7 @@ a:link, a:visited, p.intro a.email span.name
-ms-transition: 0.15s text-shadow, 0.15s color; -ms-transition: 0.15s text-shadow, 0.15s color;
transition: 0.15s text-shadow, 0.15s color; transition: 0.15s text-shadow, 0.15s color;
} }
input[type="text"], textarea input[type="text"], textarea
{ {
-moz-transition: 0.15s border-color; -moz-transition: 0.15s border-color;
-webkit-transition: 0.15s border-color; -webkit-transition: 0.15s border-color;
@ -88,7 +88,7 @@ input[type="text"], textarea
-ms-transition: 0.15s border-color; -ms-transition: 0.15s border-color;
transition: 0.15s border-color; transition: 0.15s border-color;
} }
input[type="submit"] input[type="submit"]
{ {
-moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
-webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
@ -112,7 +112,7 @@ a.post_no {
color: #293728; color: #293728;
text-decoration: none; text-decoration: none;
} }
span.quote span.quote
{ {
color:#4CADA7; color:#4CADA7;
} }
@ -131,11 +131,6 @@ div.post.reply {
div.post.reply.highlighted { div.post.reply.highlighted {
background: transparent; background: transparent;
border: #293728 1px solid; border: #293728 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited { div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #646464; color: #646464;

View file

@ -34,11 +34,6 @@ div.post.reply {
border: 0px; border: 0px;
background: #FAE8D4; background: #FAE8D4;
border: 1px solid #E2C5B1; border: 1px solid #E2C5B1;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply.highlighted { div.post.reply.highlighted {
background: #f0c0b0; background: #f0c0b0;
@ -67,7 +62,7 @@ div.pages {
padding: 7px 5px; padding: 7px 5px;
color: maroon; color: maroon;
font-size: 12pt; font-size: 12pt;
background: none; background: none;
border-width: 1px; border-width: 1px;
border-style: inset; border-style: inset;
@ -106,3 +101,4 @@ table.modlog tr th {
#options_div, #alert_div { #options_div, #alert_div {
background: rgb(240, 224, 214); background: rgb(240, 224, 214);
} }

View file

@ -23,14 +23,6 @@ div.post.reply, input, textarea, select {
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 2px; border-radius: 2px;
} }
@media (max-width: 48em) {
div.post.reply {
border-left-style: none;
border-right-style: none;
}
}
div.post.reply.post-hover { div.post.reply.post-hover {
background: rgba(200, 200, 200, 0.85); background: rgba(200, 200, 200, 0.85);
} }
@ -80,3 +72,4 @@ table.modlog tr th {
#quick-reply table { #quick-reply table {
background: #0E0E0E url(data:image/gif;base64,R0lGODlhGAAMAKEEAOXl5ebm5vDw8PHx8SH+EUNyZWF0ZWQgd2l0aCBHSU1QACwAAAAAGAAMAAACRpQiY6cLa146MyY1EJQKjG81lNGRUPOIkgMJHtquBgIO7xwvpbrpduUSuXq8ntEC0bBEylYitdDAdM1ViaobkgKgZwyDLAAAOw==) repeat 0 0 !important; background: #0E0E0E url(data:image/gif;base64,R0lGODlhGAAMAKEEAOXl5ebm5vDw8PHx8SH+EUNyZWF0ZWQgd2l0aCBHSU1QACwAAAAAGAAMAAACRpQiY6cLa146MyY1EJQKjG81lNGRUPOIkgMJHtquBgIO7xwvpbrpduUSuXq8ntEC0bBEylYitdDAdM1ViaobkgKgZwyDLAAAOw==) repeat 0 0 !important;
} }

View file

@ -1,162 +0,0 @@
/* based on jungle.css from brchan.org */
div.post.op {
margin-right: 20px;
margin-bottom: 5px;
background-image: url("img/pizza_pattern.png");
}
body {
background: #ffe;
background-image: url('img/pizza_pattern1.png'), url('img/pizza_pattern1.png');
background-repeat: repeat-x, repeat;
background-attachment: scroll, scroll;
color: #242B23;
font-family: serif;
font-size: 16px;
}
.bar {
border-color: #E5D959!important;
background-image: url('img/pizza_pattern.png');
box-shadow: none;
}
div.title h1 {
font-size: 24px;
}
div.title p {
font-size: 10px;
}
a:hover {
color: red !important;
}
a.post_no {
color: #800000;
}
desktop-style .bl-menu {
background-image: url('img/pizza_pattern1.png'), url('img/pizza_pattern1.png');
background-repeat: repeat-x, repeat;
background-attachment: scroll, scroll;
}
.boardlist .board a {
background: #65AB6B;
border: 1px solid #054500;
color: #054500;
font-weight: 600;
}
div.post.reply {
background-image: url('img/pizza_pattern.png');
border: 1px solid #E5D959;
border-left: none;
border-top: none;
border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
@media (max-width: 48em) {
border-left-style: none;
}
}
div.post.reply.highlighted {
background-image: url('img/pizza_pattern1.png');
border: 1px solid #E5D959;
border-left: none;
border-top: none;
border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
@media (max-width: 48em) {
border-right-style: none;
}
}
div.post.reply div.body a {
color: #00E;
}
.intro span.subject {
color: #d00;
}
form table tr th {
background: #65AB6B;
border: 1px solid #054500;
font-weight: 600;
}
div.ban h2 {
background: #FCA;
color: inherit;
}
div.ban {
border-color: #800;
}
div.ban p {
color: black;
}
.topbar {
background-image: url('img/pizza_pattern.png');
}
div.pages {
padding: 7px 5px;
color: #054500;
font-size: 12pt;
background-image: url('img/pizza_pattern.png');
border-width: 1px;
border-style: inset;
}
div.pages a.selected {
color: #800;
}
hr {
border-width: 1px;
border-style: inset;
}
div.boardlist {
color: #52794F;
font-size: 11pt;
}
div.boardlist a {
color: #195319;
}
.post-hover {
border: solid 1px #265026 !important;
}
unimportant, .unimportant * {
font-size: 13px;
}
table.modlog tr th {
background: #EA8;
}
.intro span.name {
color: maroon;
font-weight: 600;
}
header div.subtitle, h1 {
color: #054500;
}
.desktop-style div.boardlist:nth-child(1) {
text-shadow: #fff 1px 1px 1px, #fff -1px -1px 1px;
}
.desktop-style div.boardlist:nth-child(1):hover, .desktop-style div.boardlist:nth-child(1).cb-menu {
background-color: rgba(90%, 90%, 90%, 0.55);
}

View file

@ -164,11 +164,6 @@ div.post.reply.highlighted
box-shadow: 3px 5px #5c8c8e; box-shadow: 3px 5px #5c8c8e;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited div.post.reply div.body a:link, div.post.reply div.body a:visited
{ {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View file

@ -5,167 +5,143 @@ body {
background-image: url('img/jungle_bg1.png'), url('img/jungle_bg.png'); background-image: url('img/jungle_bg1.png'), url('img/jungle_bg.png');
background-repeat: repeat-x, repeat; background-repeat: repeat-x, repeat;
background-attachment: scroll, scroll; background-attachment: scroll, scroll;
color: #242B23; color: #242B23;
font-family: serif; font-family: serif;
font-size: 16px; font-size: 16px;
} }
.bar
.bar { {
border-color: #E5D959!important; border-color: #E5D959!important;
background-image: url('img/jungle_td.png'); background-image: url('img/jungle_td.png');
-moz-box-shadow: none;
-webkit-box-shadow: none;
box-shadow: none; box-shadow: none;
} }
div.title h1 { div.title h1 {
font-size: 24px; font-size: 24px;
} }
div.title p { div.title p {
font-size: 10px; font-size: 10px;
} }
a:hover { a:hover {
color: red !important; color: red !important;
} }
a.post_no { a.post_no {
color: #800000; color: #800000;
} }
desktop-style .bl-menu { desktop-style .bl-menu{
background-image: url('img/jungle_bg1.png'), url('img/jungle_bg.png'); background-image: url('img/jungle_bg1.png'), url('img/jungle_bg.png');
background-repeat: repeat-x, repeat; background-repeat: repeat-x, repeat;
background-attachment: scroll, scroll; background-attachment: scroll, scroll;
} }
.boardlist .board a { .boardlist .board a {
background: #65AB6B; background: #65AB6B;
border: 1px solid #054500; border: 1px solid #054500;
color: #054500; color: #054500;
font-weight: 600; font-weight: 600;
} }
div.post.reply { div.post.reply {
background-image: url('img/jungle_td.png'); background-image: url('img/jungle_td.png');
border: 1px solid #E5D959; border: 1px solid #E5D959;
border-left: none; border-left: none;
border-top: none; border-top: none;
border-radius: 5px; webkit-border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35); -moz-border-radius: 5px;
border-radius: 5px;
@media (max-width: 48em) { -webkit-box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
border-right-style: none; -moz-box-shadow: 0px 2px 3px rgba(0,0,0,0.35);
} -o-box-shadow: 0px 2px 3px rgba(0,0,0,0.35);
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
} }
div.post.reply.highlighted { div.post.reply.highlighted {
background-image: url('img/jungle_td2.png'); background-image: url('img/jungle_td2.png');
border: 1px solid #E5D959; border: 1px solid #E5D959;
border-left: none; border-left: none;
border-top: none; border-top: none;
border-radius: 5px; webkit-border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35); -moz-border-radius: 5px;
border-radius: 5px;
@media (max-width: 48em) { -webkit-box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
border-right-style: none; -moz-box-shadow: 0px 2px 3px rgba(0,0,0,0.35);
} -o-box-shadow: 0px 2px 3px rgba(0,0,0,0.35);
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
} }
div.post.reply div.body a { div.post.reply div.body a {
color: #00E; color: #00E;
} }
.intro span.subject { .intro span.subject {
color: #d00; color: #d00;
} }
span.orangeQuote {
color: #FF8C00;
text-shadow: 0.05em 0.05em orange;
}
.quote {
color: #789922;
text-shadow: 0.05em 0.05em green;
}
form table tr th { form table tr th {
background: #65AB6B; background: #65AB6B;
border: 1px solid #054500; border: 1px solid #054500;
font-weight: 600; font-weight: 600;
} }
div.ban h2 { div.ban h2 {
background: #FCA; background: #FCA;
color: inherit; color: inherit;
} }
div.ban { div.ban {
border-color: #800; border-color: #800;
} }
div.ban p { div.ban p {
color: black; color: black;
} }
.topbar { .topbar {
background-image: url('img/jungle_td.png'); background-image: url('img/jungle_td.png');
} }
div.pages { div.pages {
padding: 7px 5px; padding: 7px 5px;
color: #054500; color: #054500;
font-size: 12pt; font-size: 12pt;
background-image: url('img/jungle_td.png'); background-image: url('img/jungle_td.png');
border-width: 1px; border-width: 1px;
border-style: inset; border-style: inset;
}
}
div.pages a.selected { div.pages a.selected {
color: #800; color: #800;
} }
hr { hr {
border-width: 1px; border-width: 1px;
border-style: inset; border-style: inset;
} }
div.boardlist { div.boardlist {
color: #52794F; color: #52794F;
font-size: 11pt; font-size: 11pt;
} }
div.boardlist a { div.boardlist a {
color: #195319; color: #195319;
} }
.post-hover { .post-hover {
border: solid 1px #265026 !important; border: solid 1px #265026 !important;
} }
unimportant, .unimportant * { unimportant, .unimportant * {
font-size: 13px; font-size: 13px;
} }
table.modlog tr th { table.modlog tr th {
background: #EA8; background: #EA8;
} }
.intro span.name { .intro span.name {
color: maroon; color: maroon;
font-weight: 600; font-weight: 600;
} }
header div.subtitle, h1 { header div.subtitle, h1 {
color: #054500; color: #054500;
}
.desktop-style div.boardlist:nth-child(1) {
text-shadow: #fff 1px 1px 1px, #fff -1px -1px 1px;
} }
.desktop-style div.boardlist:nth-child(1) {
text-shadow: #fff 1px 1px 1px, #fff -1px -1px 1px;
}
.desktop-style div.boardlist:nth-child(1):hover, .desktop-style div.boardlist:nth-child(1).cb-menu { .desktop-style div.boardlist:nth-child(1):hover, .desktop-style div.boardlist:nth-child(1).cb-menu {
background-color: rgba(90%, 90%, 90%, 0.55); background-color: rgba(90%, 90%, 90%, 0.55);
} }

View file

@ -2,27 +2,27 @@
* dark.css * dark.css
* Stolen from circlepuller who stole it from derpcat * Stolen from circlepuller who stole it from derpcat
*/ */
body body
{ {
background: #1E1E1E; background: #1E1E1E;
color: #999999; color: #999999;
font-family: sans-serif; font-family: sans-serif;
font-size: 11px; font-size: 11px;
} }
span.quote span.quote
{ {
color:#B8D962; color:#B8D962;
} }
@font-face @font-face
{ {
font-family: 'lain'; font-family: 'lain';
src: url('./fonts/nrdyyh.woff') format('woff'), src: url('./fonts/nrdyyh.woff') format('woff'),
url('./fonts/tojcxo.TTF') format('truetype'); url('./fonts/tojcxo.TTF') format('truetype');
} }
h1 h1
{ {
font-family: 'lain', tahoma; font-family: 'lain', tahoma;
letter-spacing: -2px; letter-spacing: -2px;
@ -30,20 +30,20 @@ h1
text-align: center; text-align: center;
color: #32DD72; color: #32DD72;
} }
header div.subtitle header div.subtitle
{ {
color: #32DD72; color: #32DD72;
} }
div.title div.title
{ {
color: #32DD72; color: #32DD72;
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} }
div.title p div.title p
{ {
font-size: 10px; font-size: 10px;
} }
a:link, a:visited, p.intro a.email span.name a:link, a:visited, p.intro a.email span.name
{ {
color: #CCCCCC; color: #CCCCCC;
text-decoration: none; text-decoration: none;
@ -58,7 +58,7 @@ a:link, a:visited, p.intro a.email span.name
-ms-transition: 0.15s text-shadow, 0.15s color; -ms-transition: 0.15s text-shadow, 0.15s color;
transition: 0.15s text-shadow, 0.15s color; transition: 0.15s text-shadow, 0.15s color;
} }
input[type="text"], textarea input[type="text"], textarea
{ {
-moz-transition: 0.15s border-color; -moz-transition: 0.15s border-color;
-webkit-transition: 0.15s border-color; -webkit-transition: 0.15s border-color;
@ -67,7 +67,7 @@ input[type="text"], textarea
-ms-transition: 0.15s border-color; -ms-transition: 0.15s border-color;
transition: 0.15s border-color; transition: 0.15s border-color;
} }
input[type="submit"] input[type="submit"]
{ {
-moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -moz-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
-webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -webkit-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
@ -76,23 +76,23 @@ input[type="submit"]
-ms-transition: 0.15s border-color, 0.15s background-color, 0.15s color; -ms-transition: 0.15s border-color, 0.15s background-color, 0.15s color;
transition: 0.15s border-color, 0.15s background-color, 0.15s color; transition: 0.15s border-color, 0.15s background-color, 0.15s color;
} }
a:link:hover, a:visited:hover a:link:hover, a:visited:hover
{ {
color: #32DD72; color: #32DD72;
font-family: sans-serif; font-family: sans-serif;
text-decoration: none; text-decoration: none;
text-shadow: 0px 0px 5px #fff; text-shadow: 0px 0px 5px #fff;
} }
a.post_no a.post_no
{ {
color: #AAA; color: #AAA;
text-decoration: none; text-decoration: none;
} }
p.intro a.post_no:hover p.intro a.post_no:hover
{ {
color: #32DD72!important; color: #32DD72!important;
} }
div.post.reply div.post.reply
{ {
background: #181818; background: #181818;
border: #555555 0px solid; border: #555555 0px solid;
@ -117,64 +117,59 @@ div.sidearrows
display:none; display:none;
} }
div.post.reply.highlighted div.post.reply.highlighted
{ {
background: #555; background: #555;
border: transparent 1px solid; border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited div.post.reply div.body a:link, div.post.reply div.body a:visited
{ {
color: #CCCCCC; color: #CCCCCC;
} }
div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover div.post.reply div.body a:link:hover, div.post.reply div.body a:visited:hover
{ {
color: #32DD72; color: #32DD72;
} }
p.intro span.subject p.intro span.subject
{ {
font-size: 12px; font-size: 12px;
font-family: sans-serif; font-family: sans-serif;
color: #446655; color: #446655;
font-weight: 800; font-weight: 800;
} }
p.intro span.name p.intro span.name
{ {
color: #32DD72; color: #32DD72;
font-weight: 800; font-weight: 800;
} }
p.intro a.capcode, p.intro a.nametag p.intro a.capcode, p.intro a.nametag
{ {
color: magenta; color: magenta;
margin-left: 0; margin-left: 0;
} }
p.intro a.email, p.intro a.email span.name, p.intro a.email:hover, p.intro a.email:hover span.name p.intro a.email, p.intro a.email span.name, p.intro a.email:hover, p.intro a.email:hover span.name
{ {
color: #32ddaf; color: #32ddaf;
} }
input[type="text"], textarea, select input[type="text"], textarea, select
{ {
background: #333333!important; background: #333333!important;
color: #CCCCCC!important; color: #CCCCCC!important;
border: #666666 1px solid!important; border: #666666 1px solid!important;
} }
input[type="password"] input[type="password"]
{ {
background: #333333!important; background: #333333!important;
color: #CCCCCC!important; color: #CCCCCC!important;
border: #666666 1px solid!important; border: #666666 1px solid!important;
} }
form table tr th form table tr th
{ {
background: #333333!important; background: #333333!important;
color: #AAAAAA!important; color: #AAAAAA!important;
border: #333333 1px solid!important;; border: #333333 1px solid!important;;
} }
div.banner div.banner
{ {
background: #E04000; background: #E04000;
border: 1px solid hsl(17, 100%, 60%); border: 1px solid hsl(17, 100%, 60%);
@ -185,31 +180,31 @@ div.banner
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
div.banner a div.banner a
{ {
color:#000; color:#000;
} }
input[type="submit"] input[type="submit"]
{ {
background: #333333; background: #333333;
border: #666 1px solid; border: #666 1px solid;
color: #CCCCCC; color: #CCCCCC;
} }
input[type="submit"]:hover input[type="submit"]:hover
{ {
background: #555; background: #555;
border: #888 1px solid; border: #888 1px solid;
color: #32DD72; color: #32DD72;
} }
input[type="text"]:focus, textarea:focus input[type="text"]:focus, textarea:focus
{ {
border:#888 1px solid!important; border:#888 1px solid!important;
} }
p.fileinfo a:hover p.fileinfo a:hover
{ {
text-decoration: underline; text-decoration: underline;
} }
span.trip span.trip
{ {
color: #AAA; color: #AAA;
} }
@ -219,7 +214,7 @@ span.trip
border-bottom: 0px solid #666; border-bottom: 0px solid #666;
widh:100%; widh:100%;
} }
div.pages div.pages
{ {
color: #AAA; color: #AAA;
background: #333; background: #333;
@ -227,21 +222,21 @@ div.pages
font-family: sans-serif; font-family: sans-serif;
font-size: 10pt; font-size: 10pt;
} }
div.pages a.selected div.pages a.selected
{ {
color: #CCC; color: #CCC;
} }
hr hr
{ {
height: 0px; height: 0px;
border: #333 1px solid; border: #333 1px solid;
} }
div.boardlist div.boardlist
{ {
color: #999; color: #999;
} }
div.ban div.ban
{ {
background-color: #1e1e1e; background-color: #1e1e1e;
border: 1px solid #555; border: 1px solid #555;
@ -250,7 +245,7 @@ div.ban
border-radius: 2px; border-radius: 2px;
text-align: left!important; text-align: left!important;
} }
div.ban h2 div.ban h2
{ {
background: #333; background: #333;
color: #32DD72; color: #32DD72;
@ -258,17 +253,17 @@ div.ban h2
font-size: 12pt; font-size: 12pt;
border-bottom: 1px solid #555; border-bottom: 1px solid #555;
} }
div.ban h2:not(:nth-child(1)) div.ban h2:not(:nth-child(1))
{ {
border-top: 1px solid #555; border-top: 1px solid #555;
} }
table.modlog tr th table.modlog tr th
{ {
background: #333; background: #333;
color: #AAA; color: #AAA;
} }
div.report div.report
{ {
color: #666; color: #666;
} }
@ -281,7 +276,7 @@ div.report
-ms-border-radius: 2px; -ms-border-radius: 2px;
border-radius: 2px; border-radius: 2px;
} }
.blur .blur
{ {
filter: blur(20px); filter: blur(20px);
-webkit-filter: blur(23px); -webkit-filter: blur(23px);
@ -292,15 +287,17 @@ div.report
} }
/* options.js */ /* options.js */
#options_div #options_div
{ {
background: #333333; background: #333333;
} }
.options_tab_icon .options_tab_icon
{ {
color: #AAAAAA; color: #AAAAAA;
} }
.options_tab_icon.active .options_tab_icon.active
{ {
color: #FFFFFF; color: #FFFFFF;
} }

View file

@ -265,11 +265,6 @@ div.post.reply,
background: #220022; background: #220022;
border: #555555 1px solid; border: #555555 1px solid;
border-radius: 10px; border-radius: 10px;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
.post.highlighted { .post.highlighted {
@ -280,11 +275,6 @@ div.post.reply,
div.post.reply.highlighted { div.post.reply.highlighted {
background: #3A003A; background: #3A003A;
border: transparent 1px solid; border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
.post.highlighted { .post.highlighted {
@ -462,10 +452,10 @@ table.modlog tr th {
/* leftypol edits */ /* leftypol edits */
.bar, .bar,
.bar.top, .bar.top,
.bar.bottom, .bar.bottom,
div.boardlist, div.boardlist,
div.boardlist:not(.bottom) { div.boardlist:not(.bottom) {
background-color: rgba(30%, 0%, 30%, 1.0); background-color: rgba(30%, 0%, 30%, 1.0);
} }

View file

@ -215,11 +215,6 @@ margin-left: 10px;
margin-top: 20px; margin-top: 20px;
border: double 3px #000; border: double 3px #000;
background-color: rgb(194, 194, 194); background-color: rgb(194, 194, 194);
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
/*unfucks highlighting replies and gives border/shadow*/ /*unfucks highlighting replies and gives border/shadow*/

View file

@ -32,7 +32,7 @@ a:hover,
header div.subtitle, header div.subtitle,
h1 { h1 {
color: #d93f42; color: #d93f42;
font-size: 20pt; font-size: 20pt;
font-family: "Open Sans", sans-serif; font-family: "Open Sans", sans-serif;
} }
header div.subtitle { header div.subtitle {
@ -48,7 +48,7 @@ p.intro {
border-color: #cccccc; border-color: #cccccc;
border-style: solid; border-style: solid;
border-width: 0.8px; border-width: 0.8px;
border-radius: 5px; border-radius: 5px;
} }
/* Replies */ /* Replies */
/* Background color and border */ /* Background color and border */
@ -58,11 +58,6 @@ div.post.reply {
border-style: solid; border-style: solid;
border-width: 0.8px; border-width: 0.8px;
border-radius: 5px; border-radius: 5px;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply.highlighted { div.post.reply.highlighted {
background: #d5dada; background: #d5dada;
@ -70,11 +65,6 @@ div.post.reply.highlighted {
border-style: solid; border-style: solid;
border-width: 0.8px; border-width: 0.8px;
border-radius: 5px; border-radius: 5px;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a { div.post.reply div.body a {
color: #477085; color: #477085;
@ -106,7 +96,7 @@ orangeText {
background-color: #e9eced; background-color: #e9eced;
border: 1px solid #cccccc; border: 1px solid #cccccc;
} }
.thread.grid-li.grid-size-vsmall:hover, .thread.grid-li.grid-size-vsmall:hover,
.thread.grid-li.grid-size-small:hover, .thread.grid-li.grid-size-small:hover,
.thread.grid-li.grid-size-large:hover { .thread.grid-li.grid-size-large:hover {
background: #d5dada; background: #d5dada;
@ -204,7 +194,7 @@ span.heading {
} }
/* Fix OP file bleeding out of the border*/ /* Fix OP file bleeding out of the border*/
.post-image { .post-image {
margin: 37px margin: 37px
} }
/* Quick reply */ /* Quick reply */
/* Quick reply banner */ /* Quick reply banner */

View file

@ -28,22 +28,14 @@ div.post.reply {
background: #343439; background: #343439;
border-color: #3070A9; border-color: #3070A9;
border-top: 1px solid #3070A9; border-top: 1px solid #3070A9;
border-left: 1px solid #3070A9;
border-radius: 3px; border-radius: 3px;
padding: 0px; padding: 0px;
@media (min-width: 48em) {
border-left: 1px solid #3070A9;
}
} }
div.post.reply.highlighted { div.post.reply.highlighted {
background: #44444f; background: #44444f;
border: 3px dashed #3070a9; border: 3px dashed #3070a9;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a, .mentioned { div.post.reply div.body a, .mentioned {
@ -228,30 +220,30 @@ span.heading {
right: 1em !important; right: 1em !important;
position: absolute !important; position: absolute !important;
} }
#expand-all-images{ #expand-all-images{
margin-top: 4em !important; margin-top: 4em !important;
} }
#treeview{ #treeview{
margin-top: 5em !important; margin-top: 5em !important;
} }
#shrink-all-images{ #shrink-all-images{
margin-top: 6em !important; margin-top: 6em !important;
} }
#expand-all-images + hr, #expand-all-images + hr,
#shrink-all-images + hr{ #shrink-all-images + hr{
opacity: 0 !important; opacity: 0 !important;
margin: 0 !important; margin: 0 !important;
} }
#treeview + hr{ #treeview + hr{
opacity: 0 !important; opacity: 0 !important;
clear: both !important; clear: both !important;
} }
#options_handler{ #options_handler{
margin-top: 3em !important; margin-top: 3em !important;
} }

View file

@ -380,7 +380,6 @@ form table tr td div.center {
.file { .file {
float: left; float: left;
min-width: 100px;
} }
.file:not(.multifile) .post-image { .file:not(.multifile) .post-image {
@ -391,10 +390,6 @@ form table tr td div.center {
float: none; float: none;
} }
.file.multifile {
margin: 0 10px 0 0;
}
.file.multifile > p { .file.multifile > p {
width: 0px; width: 0px;
min-width: 100%; min-width: 100%;
@ -434,18 +429,19 @@ img.banner,img.board_image {
.post-image { .post-image {
display: block; display: block;
float: left; float: left;
margin: 5px 20px 10px 20px;
border: none; border: none;
} }
.full-image { .full-image {
float: left; float: left;
padding: 0.2em 0.2em 0.8em 0.2em; padding: 5px;
margin: 0 20px 0 0; margin: 0 20px 0 0;
max-width: 98%; max-width: 98%;
} }
div.post .post-image { div.post .post-image {
padding: 0.2em 0.2em 0.8em 0.2em; padding: 0.2em;
margin: 0 20px 0 0; margin: 0 20px 0 0;
} }
@ -542,8 +538,8 @@ div.post {
} }
} }
div.post div.head { div.post > div.head {
margin: 0.1em 1em 0.8em 1.4em; margin: 0.1em 1em;
clear: both; clear: both;
line-height: 1.3em; line-height: 1.3em;
} }
@ -568,11 +564,17 @@ div.post.op > p {
} }
div.post div.body { div.post div.body {
margin-left: 1.4em; margin-top: 0.8em;
padding-right: 3em; padding-right: 3em;
padding-bottom: 0.3em; padding-bottom: 0.3em;
}
white-space: pre-wrap; div.post.op div.body {
margin-left: 0.8em;
}
div.post.reply div.body {
margin-left: 1.8em;
} }
div.post.reply.highlighted { div.post.reply.highlighted {
@ -583,9 +585,19 @@ div.post.reply div.body a {
color: #D00; color: #D00;
} }
div.post div.body {
white-space: pre-wrap;
}
div.post.op { div.post.op {
padding-top: 0px; padding-top: 0px;
vertical-align: top; vertical-align: top;
/* Add back in the padding that is provided by body on large screens */
@media (max-width: 48em) {
padding-left: 4px;
padding-right: 4px;
}
} }
div.post.reply { div.post.reply {
@ -636,7 +648,6 @@ span.trip {
span.omitted { span.omitted {
display: block; display: block;
margin-top: 1em; margin-top: 1em;
margin-left: 0.4em;
} }
br.clear { br.clear {
@ -821,7 +832,7 @@ span.public_ban {
span.public_warning { span.public_warning {
display: block; display: block;
color: orange; color: steelblue;
font-weight: bold; font-weight: bold;
margin-top: 15px; margin-top: 15px;
} }
@ -912,14 +923,10 @@ form.ban-appeal textarea {
display:inline!important; display:inline!important;
} }
pre { pre {
margin: 0; margin:0
display: inline!important; display: inline!important;
} }
.theme-catalog .controls > span {
margin-right: 1em;
}
.theme-catalog div.thread img { .theme-catalog div.thread img {
float: none!important; float: none!important;
margin: auto; margin: auto;
@ -929,20 +936,13 @@ pre {
border: 2px solid rgba(153,153,153,0); border: 2px solid rgba(153,153,153,0);
} }
/* Still for the catalog theme */
#Grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-content: center;
gap: 0.2em;
}
.theme-catalog div.thread { .theme-catalog div.thread {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
text-align: center; text-align: center;
font-weight: normal; font-weight: normal;
margin-top: 2px;
margin-bottom: 2px;
padding: 2px; padding: 2px;
height: 300px; height: 300px;
width: 205px; width: 205px;
@ -961,6 +961,7 @@ pre {
.theme-catalog div.threads { .theme-catalog div.threads {
text-align: center; text-align: center;
margin-left: -20px;
} }
.theme-catalog div.thread:hover { .theme-catalog div.thread:hover {

View file

@ -48,11 +48,6 @@ div.post.reply.highlighted {
background: transparent; background: transparent;
border: transparent 1px dashed; border: transparent 1px dashed;
border-color:#00FF00; border-color:#00FF00;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited { div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #00FF00; color: #00FF00;

View file

@ -69,11 +69,6 @@ div.post.reply {
div.post.reply.highlighted { div.post.reply.highlighted {
background: transparent; background: transparent;
border: transparent 1px dotted; border: transparent 1px dotted;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
p.intro span.subject { p.intro span.subject {
font-size: 12px; font-size: 12px;

View file

@ -132,11 +132,6 @@ line-height: 1.4;
div.post.reply.highlighted { div.post.reply.highlighted {
background: #555; background: #555;
border: transparent 1px solid; border: transparent 1px solid;
@media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
} }
div.post.reply div.body a:link, div.post.reply div.body a:visited { div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #CCCCCC; color: #CCCCCC;

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{% if config.captcha.mode == 'hcaptcha' %} {% if config.hcaptcha %}
<script src="https://js.hcaptcha.com/1/api.js?recaptchacompat=off&render=explicit&onload=onCaptchaLoadHcaptcha" async defer></script> <script src="https://js.hcaptcha.com/1/api.js?recaptchacompat=off&render=explicit&onload=onCaptchaLoadHcaptcha" async defer></script>
{% endif %} {% endif %}
{% if config.captcha.mode == 'turnstile' %} {% if config.turnstile %}
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onCaptchaLoadTurnstile_{{ form_action_type }}" async defer></script> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit&onload=onCaptchaLoadTurnstile_{{ form_action_type }}" async defer></script>
{% endif %} {% endif %}

View file

@ -28,6 +28,6 @@
<script type="text/javascript" src="/js/mod/mod_snippets.js?v={{ config.resource_version }}"></script> <script type="text/javascript" src="/js/mod/mod_snippets.js?v={{ config.resource_version }}"></script>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if config.captcha.mode == 'recaptcha' %} {% if config.recaptcha %}
<script src="//www.google.com/recaptcha/api.js"></script> <script src="//www.google.com/recaptcha/api.js"></script>
{% endif %} {% endif %}

View file

@ -88,9 +88,6 @@
<label for="secure_trip_salt">Secure trip (##) salt:</label> <label for="secure_trip_salt">Secure trip (##) salt:</label>
<input type="text" id="secure_trip_salt" name="secure_trip_salt" value="{{ config.secure_trip_salt }}" size="40"> <input type="text" id="secure_trip_salt" name="secure_trip_salt" value="{{ config.secure_trip_salt }}" size="40">
<label for="secure_password_salt">Poster password salt:</label>
<input type="text" id="secure_password_salt" name="secure_password_salt" value="{{ config.secure_password_salt }}" size="40">
<label for="more">Additional configuration:</label> <label for="more">Additional configuration:</label>
<textarea id="more" name="more">{{ more }}</textarea> <textarea id="more" name="more">{{ more }}</textarea>
</fieldset> </fieldset>

View file

@ -231,6 +231,28 @@ var resourceVersion = document.currentScript.getAttribute('data-resource-version
{% endif %} {% endif %}
{% raw %} {% raw %}
function initStyleChooser() {
var newElement = document.createElement('div');
newElement.className = 'styles';
for (styleName in styles) {
if (styleName) {
var style = document.createElement('a');
style.innerHTML = '[' + styleName + ']';
style.onclick = function() {
changeStyle(this.innerHTML.substring(1, this.innerHTML.length - 1), this);
};
if (styleName == selectedstyle) {
style.className = 'selected';
}
style.href = 'javascript:void(0);';
newElement.appendChild(style);
}
}
document.getElementById('bottom-hud').before(newElement);
}
function getCookie(cookie_name) { function getCookie(cookie_name) {
let results = document.cookie.match('(^|;) ?' + cookie_name + '=([^;]*)(;|$)'); let results = document.cookie.match('(^|;) ?' + cookie_name + '=([^;]*)(;|$)');
if (results) { if (results) {
@ -243,48 +265,26 @@ function getCookie(cookie_name) {
{% endraw %} {% endraw %}
/* BEGIN CAPTCHA REGION */ /* BEGIN CAPTCHA REGION */
{% if config.captcha.mode == 'hcaptcha' or config.captcha.mode == 'turnstile' %} // If any captcha {% if config.hcaptcha or config.turnstile %} // If any captcha
// Global captcha object. Assigned by `onCaptchaLoad()`. // Global captcha object. Assigned by `onCaptchaLoad()`.
var captcha_renderer = null; var captcha_renderer = null;
// Captcha widget id of the post form.
var postCaptchaId = null;
{% if config.captcha.mode == 'hcaptcha' %} // If hcaptcha {% if config.hcaptcha %} // If hcaptcha
function onCaptchaLoadHcaptcha() { function onCaptchaLoadHcaptcha() {
if ((captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled())) if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
&& captcha_renderer === null
&& (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
let renderer = { let renderer = {
/** renderOn: (container) => hcaptcha.render(container, {
* @returns {object} Opaque widget id. sitekey: "{{ config.hcaptcha_public }}",
*/
applyOn: (container, params) => hcaptcha.render(container, {
sitekey: "{{ config.captcha.hcaptcha.public }}",
callback: params['on-success'],
}), }),
/**
* @returns {void}
*/
remove: (widgetId) => { /* Not supported */ }, remove: (widgetId) => { /* Not supported */ },
/** reset: (widgetId) => hcaptcha.reset(widgetId)
* @returns {void}
*/
reset: (widgetId) => hcaptcha.reset(widgetId),
/**
* @returns {bool}
*/
hasResponse: (widgetId) => !!hcaptcha.getResponse(widgetId),
/**
* @returns {void}
*/
execute: (widgetId) => hcaptcha.execute(widgetId)
}; };
onCaptchaLoad(renderer); onCaptchaLoad(renderer);
} }
} }
{% endif %} // End if hcaptcha {% endif %} // End if hcaptcha
{% if config.captcha.mode == 'turnstile' %} // If turnstile {% if config.turnstile %} // If turnstile
// Wrapper function to be called from thread.html // Wrapper function to be called from thread.html
window.onCaptchaLoadTurnstile_post_reply = function() { window.onCaptchaLoadTurnstile_post_reply = function() {
@ -298,40 +298,20 @@ window.onCaptchaLoadTurnstile_post_thread = function() {
// Should be called by the captcha API when it's ready. Ugly I know... D: // Should be called by the captcha API when it's ready. Ugly I know... D:
function onCaptchaLoadTurnstile(action) { function onCaptchaLoadTurnstile(action) {
if ((captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled())) if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
&& captcha_renderer === null
&& (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
let renderer = { let renderer = {
/** renderOn: function(container) {
* @returns {object} Opaque widget id.
*/
applyOn: function(container, params) {
let widgetId = turnstile.render('#' + container, { let widgetId = turnstile.render('#' + container, {
sitekey: "{{ config.captcha.turnstile.public }}", sitekey: "{{ config.turnstile_public }}",
action: action, action: action,
callback: params['on-success'],
}); });
if (widgetId === undefined) { if (widgetId === undefined) {
return null; return null;
} }
return widgetId; return widgetId;
}, },
/**
* @returns {void}
*/
remove: (widgetId) => turnstile.remove(widgetId), remove: (widgetId) => turnstile.remove(widgetId),
/** reset: (widgetId) => turnstile.reset(widgetId)
* @returns {void}
*/
reset: (widgetId) => turnstile.reset(widgetId),
/**
* @returns {bool}
*/
hasResponse: (widgetId) => !!turnstile.getResponse(widgetId),
/**
* @returns {void}
*/
execute: (widgetId) => turnstile.execute(widgetId)
}; };
onCaptchaLoad(renderer); onCaptchaLoad(renderer);
@ -340,20 +320,12 @@ function onCaptchaLoadTurnstile(action) {
{% endif %} // End if turnstile {% endif %} // End if turnstile
function onCaptchaLoad(renderer) { function onCaptchaLoad(renderer) {
// Initialize the form identifier with a random password.
document.getElementById('captcha-form-id').value = generatePassword();
captcha_renderer = renderer; captcha_renderer = renderer;
let widgetId = renderer.applyOn('captcha-container', { let widgetId = renderer.renderOn('captcha-container');
'on-success': function(token) {
document.getElementById('captcha-response').value = token;
}
});
if (widgetId === null) { if (widgetId === null) {
console.error('Could not render captcha!'); console.error('Could not render captcha!');
} }
postCaptchaId = widgetId;
document.addEventListener('afterdopost', function(e) { document.addEventListener('afterdopost', function(e) {
// User posted! Reset the captcha. // User posted! Reset the captcha.
renderer.reset(widgetId); renderer.reset(widgetId);
@ -361,8 +333,6 @@ function onCaptchaLoad(renderer) {
} }
{% if config.dynamic_captcha %} // If dynamic captcha {% if config.dynamic_captcha %} // If dynamic captcha
var captchaMode = 'dynamic';
function isDynamicCaptchaEnabled() { function isDynamicCaptchaEnabled() {
let cookie = getCookie('captcha-required'); let cookie = getCookie('captcha-required');
return cookie === '1'; return cookie === '1';
@ -376,15 +346,8 @@ function initCaptcha() {
} }
} }
} }
{% else %}
var captchaMode = 'static';
{% endif %} // End if dynamic captcha {% endif %} // End if dynamic captcha
{% else %} // Else if any captcha {% else %} // Else if any captcha
var captchaMode = 'none';
function isDynamicCaptchaEnabled() {
return false;
}
// No-op for `init()`. // No-op for `init()`.
function initCaptcha() {} function initCaptcha() {}
{% endif %} // End if any captcha {% endif %} // End if any captcha
@ -440,13 +403,6 @@ function doPost(form) {
saved[document.location] = form.elements['body'].value; saved[document.location] = form.elements['body'].value;
sessionStorage.body = JSON.stringify(saved); sessionStorage.body = JSON.stringify(saved);
if (captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled())) {
if (captcha_renderer && postCaptchaId && !captcha_renderer.hasResponse(postCaptchaId)) {
captcha_renderer.execute(postCaptchaId);
return false;
}
}
// Needs to be delayed by at least 1 frame, otherwise it may reset the form (read captcha) fields before they're sent. // Needs to be delayed by at least 1 frame, otherwise it may reset the form (read captcha) fields before they're sent.
setTimeout(() => document.dispatchEvent(new Event('afterdopost'))); setTimeout(() => document.dispatchEvent(new Event('afterdopost')));
return form.elements['body'].value != "" || (form.elements['file'] && form.elements['file'].value != "") || (form.elements.file_url && form.elements['file_url'].value != ""); return form.elements['body'].value != "" || (form.elements['file'] && form.elements['file'].value != "") || (form.elements.file_url && form.elements['file_url'].value != "");
@ -563,6 +519,7 @@ var script_settings = function(script_name) {
}; };
function init() { function init() {
initStyleChooser();
initCaptcha(); initCaptcha();
{% endraw %} {% endraw %}

View file

@ -45,7 +45,7 @@ $(document).ready(function(){
<label for="reason">{% trans 'Reason' %}</label> <label for="reason">{% trans 'Reason' %}</label>
</th> </th>
<td> <td>
<textarea name="reason" id="reason" rows="5" cols="30" autofocus>{{ reason|e }}</textarea> <textarea name="reason" id="reason" rows="5" cols="30">{{ reason|e }}</textarea>
</td> </td>
</tr> </tr>
{% if post and board and not delete %} {% if post and board and not delete %}

Some files were not shown because too many files have changed in this diff Show more