Compare commits

..

1 commit

Author SHA1 Message Date
d6bb4d21d8 database.php: set utf8mb4 charset 2024-12-08 01:35:15 +01:00
89 changed files with 2631 additions and 2184 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

@ -28,6 +28,8 @@ services:
#MySQL Service #MySQL Service
db: db:
image: mysql:8.0.35 image: mysql:8.0.35
restart: unless-stopped
tty: true
ports: ports:
- "3306:3306" - "3306:3306"
environment: environment:

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,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,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,14 +1,19 @@
<?php <?php
namespace Vichan\Data; namespace Vichan\Data;
use Vichan\Data\Driver\CacheDriver;
class ReportQueries { class ReportQueries {
private const CACHE_KEY = "report_queries_valid_count";
private \PDO $pdo; private \PDO $pdo;
private CacheDriver $cache;
private bool $auto_maintenance; private bool $auto_maintenance;
private function deleteReportImpl(string $board, int $post_id) { private function deleteReportImpl(string $board, int $post_id) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `post` = :id AND `board` = :board'); $query = prepare("DELETE FROM ``reports`` WHERE `post` = :id AND `board` = :board");
$query->bindValue(':id', $post_id, \PDO::PARAM_INT); $query->bindValue(':id', $post_id, \PDO::PARAM_INT);
$query->bindValue(':board', $board); $query->bindValue(':board', $board);
$query->execute(); $query->execute();
@ -88,11 +93,9 @@ class ReportQueries {
if ($get_invalid) { if ($get_invalid) {
// Get the reports without a post. // Get the reports without a post.
$invalid = []; $invalid = [];
foreach ($raw_reports as $report) {
if (isset($report_posts[$report['board']][$report['post']])) { if (isset($report_posts[$report['board']][$report['post']])) {
$invalid[] = $report; $invalid[] = $report;
} }
}
return $invalid; return $invalid;
} else { } else {
// Filter out the reports without a valid post. // Filter out the reports without a valid post.
@ -113,10 +116,12 @@ class ReportQueries {
/** /**
* @param \PDO $pdo PDO connection. * @param \PDO $pdo PDO connection.
* @param CacheDriver $cache Cache driver.
* @param bool $auto_maintenance If the auto maintenance should be enabled. * @param bool $auto_maintenance If the auto maintenance should be enabled.
*/ */
public function __construct(\PDO $pdo, bool $auto_maintenance) { public function __construct(\PDO $pdo, CacheDriver $cache, bool $auto_maintenance) {
$this->pdo = $pdo; $this->pdo = $pdo;
$this->cache = $cache;
$this->auto_maintenance = $auto_maintenance; $this->auto_maintenance = $auto_maintenance;
} }
@ -126,14 +131,19 @@ class ReportQueries {
* @return int The number of reports. * @return int The number of reports.
*/ */
public function getCount(): int { public function getCount(): int {
$query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`'); $ret = $this->cache->get(self::CACHE_KEY);
if ($ret === null) {
$query = $this->pdo->prepare("SELECT `board`, `post`, `id` FROM `reports`");
$query->execute(); $query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC); $raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
$valid_reports = $this->filterReports($raw_reports, false, null); $valid_reports = $this->filterReports($raw_reports, false, null);
$count = \count($valid_reports); $count = \count($valid_reports);
$this->cache->set(self::CACHE_KEY, $count);
return $count; return $count;
} }
return $ret;
}
/** /**
* Get the report with the given id. DOES NOT PERFORM VALIDITY CHECK. * Get the report with the given id. DOES NOT PERFORM VALIDITY CHECK.
@ -141,8 +151,8 @@ class ReportQueries {
* @param int $id The id of the report to fetch. * @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. * @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 { public function getReportById(int $id): array {
$query = prepare('SELECT `board`, `ip` FROM ``reports`` WHERE `id` = :id'); $query = prepare("SELECT `board`, `ip` FROM ``reports`` WHERE `id` = :id");
$query->bindValue(':id', $id); $query->bindValue(':id', $id);
$query->execute(); $query->execute();
@ -161,7 +171,7 @@ class ReportQueries {
* @return array The reports with the associated post data. * @return array The reports with the associated post data.
*/ */
public function getReportsWithPosts(int $count): array { public function getReportsWithPosts(int $count): array {
$query = $this->pdo->prepare('SELECT * FROM `reports` ORDER BY `time`'); $query = $this->pdo->prepare("SELECT * FROM `reports` ORDER BY `time`");
$query->execute(); $query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC); $raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
return $this->joinReportPosts($raw_reports, $count); return $this->joinReportPosts($raw_reports, $count);
@ -173,7 +183,7 @@ class ReportQueries {
* @return int The number of reports deleted. * @return int The number of reports deleted.
*/ */
public function purge(): int { public function purge(): int {
$query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`'); $query = $this->pdo->prepare("SELECT `board`, `post`, `id` FROM `reports`");
$query->execute(); $query->execute();
$raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC); $raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
$invalid_reports = $this->filterReports($raw_reports, true, null); $invalid_reports = $this->filterReports($raw_reports, true, null);
@ -190,9 +200,11 @@ class ReportQueries {
* @param int $id The report id. * @param int $id The report id.
*/ */
public function deleteById(int $id) { public function deleteById(int $id) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `id` = :id'); $query = $this->pdo->prepare("DELETE FROM `reports` WHERE `id` = :id");
$query->bindValue(':id', $id, \PDO::PARAM_INT); $query->bindValue(':id', $id, \PDO::PARAM_INT);
$query->execute(); $query->execute();
// The caller may actually delete a valid post, so we need to invalidate the cache.
$this->cache->delete(self::CACHE_KEY);
} }
/** /**
@ -201,9 +213,11 @@ class ReportQueries {
* @param string $ip The reporter ip. * @param string $ip The reporter ip.
*/ */
public function deleteByIp(string $ip) { public function deleteByIp(string $ip) {
$query = $this->pdo->prepare('DELETE FROM `reports` WHERE `ip` = :ip'); $query = $this->pdo->prepare("DELETE FROM `reports` WHERE `ip` = :ip");
$query->bindValue(':ip', $ip); $query->bindValue(':ip', $ip);
$query->execute(); $query->execute();
// The caller may actually delete a valid post, so we need to invalidate the cache.
$this->cache->delete(self::CACHE_KEY);
} }
/** /**
@ -223,5 +237,7 @@ class ReportQueries {
$query->bindValue(':post', $post_id, \PDO::PARAM_INT); $query->bindValue(':post', $post_id, \PDO::PARAM_INT);
$query->bindValue(':reason', $reason); $query->bindValue(':reason', $reason);
$query->execute(); $query->execute();
$this->cache->delete(self::CACHE_KEY);
} }
} }

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

@ -195,8 +195,6 @@ function _create_antibot($pdo, $board, $thread) {
$antibot = new AntiBot(array($board, $thread)); $antibot = new AntiBot(array($board, $thread));
try {
retry_on_deadlock(3, function() use ($config, $pdo, $thread, $board, $antibot, $purged_old_antispam) {
try { try {
$pdo->beginTransaction(); $pdo->beginTransaction();
@ -206,6 +204,7 @@ function _create_antibot($pdo, $board, $thread) {
purge_old_antispam(); purge_old_antispam();
} }
retry_on_deadlock(4, function() use($config, $board, $thread) {
// Keep the now invalid timestamps around for a bit to enable users to post if they're still on an old version of // 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. // 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. // 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.
@ -222,10 +221,12 @@ function _create_antibot($pdo, $board, $thread) {
$query->bindValue(':expires', $config['spam']['hidden_inputs_expire']); $query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
// Throws on error. // Throws on error.
$query->execute(); $query->execute();
});
try {
$hash = $antibot->hash(); $hash = $antibot->hash();
retry_on_deadlock(2, function() use($board, $thread, $hash) {
// Insert an antispam with infinite life as the HTML page of a thread might last well beyond the expiry date. // 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 = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
$query->bindValue(':board', $board); $query->bindValue(':board', $board);
@ -233,21 +234,19 @@ function _create_antibot($pdo, $board, $thread) {
$query->bindValue(':hash', $hash); $query->bindValue(':hash', $hash);
// Throws on error. // Throws on error.
$query->execute(); $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');
} }
} }
} catch (\PDOException $e) {
$pdo->rollBack();
throw $e;
}
$pdo->commit();
return $antibot; return $antibot;
} }

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;
@ -220,9 +200,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
@ -943,6 +920,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 +962,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 +1192,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 +1212,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.
@ -1551,8 +1521,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 each page of ?/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;
@ -2030,6 +2000,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,8 +1,8 @@
<?php <?php
namespace Vichan; namespace Vichan;
use Vichan\Data\{IpNoteQueries, ReportQueries, UserPostQueries}; use Vichan\Data\Driver\CacheDriver;
use Vichan\Data\Driver\{CacheDriver, ErrorLogLogDriver, FileLogDriver, LogDriver, StderrLogDriver, SyslogLogDriver}; use Vichan\Data\ReportQueries;
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
@ -31,34 +31,6 @@ class Context {
function build_context(array $config): Context { function build_context(array $config): Context {
return new Context([ return new Context([
'config' => $config, '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) { CacheDriver::class => function($c) {
// Use the global for backwards compatibility. // Use the global for backwards compatibility.
return \cache::getCache(); return \cache::getCache();
@ -72,11 +44,8 @@ function build_context(array $config): Context {
ReportQueries::class => function($c) { ReportQueries::class => function($c) {
$auto_maintenance = (bool)$c->get('config')['auto_maintenance']; $auto_maintenance = (bool)$c->get('config')['auto_maintenance'];
$pdo = $c->get(\PDO::class); $pdo = $c->get(\PDO::class);
return new ReportQueries($pdo, $auto_maintenance); $cache = $c->get(CacheDriver::class);
}, return new ReportQueries($pdo, $cache, $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

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

View file

@ -4,9 +4,6 @@
* 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 {
@ -139,13 +136,17 @@ class Filter {
} }
} }
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':
@ -213,7 +214,7 @@ function purge_flood_table() {
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']))
@ -236,7 +237,7 @@ function do_filters(Context $ctx, array $post) {
$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();
} }
} }

View file

@ -745,23 +745,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;
} }
@ -2069,7 +2070,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);
@ -2168,14 +2169,11 @@ 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 +3083,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']);
}

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

@ -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

@ -3,20 +3,21 @@
* Copyright (c) 2010-2013 Tinyboard Development Group * Copyright (c) 2010-2013 Tinyboard Development Group
*/ */
use Vichan\Context; use Vichan\Context;
use Vichan\Data\{IpNoteQueries, UserPostQueries, ReportQueries}; use Vichan\Data\ReportQueries;
use Vichan\Data\Driver\LogDriver; use Vichan\Functions\Format;
use Vichan\Functions\Net; use Vichan\Functions\Net;
use function Vichan\Functions\Net\decode_cursor;
use function Vichan\Functions\Net\encode_cursor;
defined('TINYBOARD') or exit; defined('TINYBOARD') or exit;
function _link_or_copy_factory(Context $ctx): callable { function _link_or_copy(string $target, string $link): bool {
return function(string $target, string $link) use ($ctx) { if (!link($target, $link)) {
if (!\link($target, $link)) { error_log("Failed to link() $target to $link. FAlling back to copy()");
$ctx->get(LogDriver::class)->log(LogDriver::NOTICE, "Failed to link() $target to $link. FAlling back to copy()"); return copy($target, $link);
return \copy($target, $link);
} }
return true; return true;
};
} }
function mod_page($title, $template, $args, $subtitle = false) { function mod_page($title, $template, $args, $subtitle = false) {
@ -57,7 +58,8 @@ function mod_login(Context $ctx, $redirect = false) {
if (!isset($_POST['username'], $_POST['password']) || $_POST['username'] == '' || $_POST['password'] == '') { if (!isset($_POST['username'], $_POST['password']) || $_POST['username'] == '' || $_POST['password'] == '') {
$args['error'] = $config['error']['invalid']; $args['error'] = $config['error']['invalid'];
} elseif (!login($_POST['username'], $_POST['password'])) { } elseif (!login($_POST['username'], $_POST['password'])) {
$ctx->get(LogDriver::class)->log(LogDriver::INFO, 'Unauthorized login attempt!'); if ($config['syslog'])
_syslog(LOG_WARNING, 'Unauthorized login attempt!');
$args['error'] = $config['error']['invalid']; $args['error'] = $config['error']['invalid'];
} else { } else {
@ -98,8 +100,6 @@ function mod_logout(Context $ctx) {
function mod_dashboard(Context $ctx) { function mod_dashboard(Context $ctx) {
global $config, $mod; global $config, $mod;
$report_queries = $ctx->get(ReportQueries::class);
$args = []; $args = [];
$args['boards'] = listBoards(); $args['boards'] = listBoards();
@ -126,7 +126,8 @@ function mod_dashboard(Context $ctx) {
cache::set('pm_unreadcount_' . $mod['id'], $args['unread_pms']); cache::set('pm_unreadcount_' . $mod['id'], $args['unread_pms']);
} }
$args['reports'] = $report_queries->getCount(); $query = query('SELECT COUNT(*) FROM ``reports``') or error(db_error($query));
$args['reports'] = $query->fetchColumn();
$query = query('SELECT COUNT(*) FROM ``ban_appeals`` WHERE denied = 0') or error(db_error($query)); $query = query('SELECT COUNT(*) FROM ``ban_appeals`` WHERE denied = 0') or error(db_error($query));
$args['appeals'] = $query->fetchColumn(); $args['appeals'] = $query->fetchColumn();
@ -767,10 +768,7 @@ function mod_board_log(Context $ctx, $board, $page_no = 1, $hide_names = false,
} }
function mod_view_catalog(Context $ctx, $boardName) { function mod_view_catalog(Context $ctx, $boardName) {
global $mod; global $config;
$config = $ctx->get('config');
require_once($config['dir']['themes'].'/catalog/theme.php'); require_once($config['dir']['themes'].'/catalog/theme.php');
$settings = []; $settings = [];
$settings['boards'] = $boardName; $settings['boards'] = $boardName;
@ -853,118 +851,188 @@ function mod_view_thread50(Context $ctx, $boardName, $thread) {
} }
function mod_ip_remove_note(Context $ctx, $ip, $id) { function mod_ip_remove_note(Context $ctx, $ip, $id) {
$config = $ctx->get('config'); global $config;
if (!hasPermission($config['mod']['remove_notes'])) { if (!hasPermission($config['mod']['remove_notes']))
error($config['error']['noaccess']); error($config['error']['noaccess']);
if (filter_var($ip, FILTER_VALIDATE_IP) === false)
error("Invalid IP address.");
$query = prepare('DELETE FROM ``ip_notes`` WHERE `ip` = :ip AND `id` = :id');
$query->bindValue(':ip', $ip);
$query->bindValue(':id', $id);
$query->execute() or error(db_error($query));
modLog("Removed a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']);
} }
if (filter_var($ip, \FILTER_VALIDATE_IP) === false) { function mod_ip(Context $ctx, $ip, string $encoded_cursor = '') {
error('Invalid IP address'); global $config, $mod;
}
if (!is_numeric($id)) { if (filter_var($ip, FILTER_VALIDATE_IP) === false)
error('Invalid note ID'); error("Invalid IP address.");
}
$queries = $ctx->get(IpNoteQueries::class);
$deleted = $queries->deleteWhereIp((int)$id, $ip);
if (!$deleted) {
error("Note $id does not exist for $ip");
}
modLog("Removed a note for <a href=\"?/user_posts/ip/{$ip}\">{$ip}</a>");
\header("Location: ?/user_posts/ip/$ip#notes", true, $config['redirect_http']);
}
function mod_ip(Context $ctx, $ip, string $encoded_cursor = null) {
global $mod;
$config = $ctx->get('config');
if (filter_var($ip, FILTER_VALIDATE_IP) === false) {
error('Invalid IP address');
}
if (isset($_POST['ban_id'], $_POST['unban'])) { if (isset($_POST['ban_id'], $_POST['unban'])) {
if (!hasPermission($config['mod']['unban'])) { if (!hasPermission($config['mod']['unban']))
error($config['error']['noaccess']); error($config['error']['noaccess']);
}
Bans::delete($_POST['ban_id'], true, $mod['boards']); Bans::delete($_POST['ban_id'], true, $mod['boards']);
if (empty($encoded_cursor)) { if (empty($encoded_cursor)) {
\header("Location: ?/user_posts/ip/$ip#bans", true, $config['redirect_http']); header("Location: ?/IP/$ip#bans", true, $config['redirect_http']);
} else { } else {
\header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#bans", true, $config['redirect_http']); header("Location: ?/IP/$ip/cursor/$encoded_cursor#bans", true, $config['redirect_http']);
} }
return; return;
} }
if (isset($_POST['note'])) { if (isset($_POST['note'])) {
if (!hasPermission($config['mod']['create_notes'])) { if (!hasPermission($config['mod']['create_notes']))
error($config['error']['noaccess']); error($config['error']['noaccess']);
}
$_POST['note'] = escape_markup_modifiers($_POST['note']); $_POST['note'] = escape_markup_modifiers($_POST['note']);
markup($_POST['note']); markup($_POST['note']);
$query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
$note_queries = $ctx->get(IpNoteQueries::class); $query->bindValue(':ip', $ip);
$note_queries->add($ip, $mod['id'], $_POST['note']); $query->bindValue(':mod', $mod['id']);
$query->bindValue(':time', time());
$query->bindValue(':body', $_POST['note']);
$query->execute() or error(db_error($query));
Cache::delete("mod_page_ip_view_notes_$ip"); Cache::delete("mod_page_ip_view_notes_$ip");
modLog("Added a note for <a href=\"?/user_posts/ip/{$ip}\">{$ip}</a>"); modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
if (empty($encoded_cursor)) { if (empty($encoded_cursor)) {
\header("Location: ?/user_posts/ip/$ip#notes", true, $config['redirect_http']); header("Location: ?/IP/$ip#notes", true, $config['redirect_http']);
} else { } else {
\header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#notes", true, $config['redirect_http']); header("Location: ?/IP/$ip/cursor/$encoded_cursor#notes", true, $config['redirect_http']);
} }
return; return;
} }
// Temporary Redirect so to not to break the note and unban system.
if (empty($encoded_cursor)) {
\header("Location: ?/user_posts/ip/$ip", true, 307);
} else {
\header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor", true, 307);
}
}
function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = null) {
global $mod;
if (\filter_var($ip, \FILTER_VALIDATE_IP) === false){
error('Invalid IP address');
}
$config = $ctx->get('config');
$args = [ $args = [
'ip' => $ip, 'ip' => $ip,
'posts' => [] 'posts' => []
]; ];
if (isset($config['mod']['ip_recentposts'])) {
// TODO log to migrate.
$page_size = $config['mod']['ip_recentposts'];
} else {
$page_size = $config['mod']['recent_user_posts'];
}
if ($config['mod']['dns_lookup']) { if ($config['mod']['dns_lookup']) {
$args['hostname'] = rDNS($ip); $args['hostname'] = rDNS($ip);
} }
// Decode the cursor.
list($cursor_type, $board_id_cursor_map) = decode_cursor($encoded_cursor);
$post_per_page = $config['mod']['ip_recentposts'];
$next_cursor_map = [];
$prev_cursor_map = [];
$boards = listBoards();
foreach ($boards as $board) {
$uri = $board['uri'];
openBoard($uri);
if (hasPermission($config['mod']['show_ip'], $uri)) {
// Extract the cursor relative to the board.
$id_cursor = false;
if (isset($board_id_cursor_map[$uri])) {
$value = $board_id_cursor_map[$uri];
if (is_numeric($value)) {
$id_cursor = (int)$value;
}
}
if ($id_cursor === false) {
$query = 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', $post_per_page + 1, PDO::PARAM_INT); // Always fetch more.
$query->execute();
$posts = $query->fetchAll(PDO::FETCH_ASSOC);
} elseif ($cursor_type === 'n') {
$query = 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', $id_cursor, PDO::PARAM_INT);
$query->bindValue(':limit', $post_per_page + 2, PDO::PARAM_INT); // Always fetch more.
$query->execute();
$posts = $query->fetchAll(PDO::FETCH_ASSOC);
} elseif ($cursor_type === 'p') {
// FIXME
$query = 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', $id_cursor, PDO::PARAM_INT);
$query->bindValue(':limit', $post_per_page + 2, PDO::PARAM_INT); // Always fetch more.
$query->execute();
$posts = array_reverse($query->fetchAll(PDO::FETCH_ASSOC));
} else {
throw new RuntimeException("Unknown cursor type '$cursor_type'");
}
$posts_count = count($posts);
if ($posts_count === $post_per_page + 2) {
$has_extra_prev_post = true;
$has_extra_end_post = true;
} elseif ($posts_count === $post_per_page + 1) {
$has_extra_prev_post = $id_cursor !== false && $posts[0]['id'] == $id_cursor;
$has_extra_end_post = !$has_extra_prev_post;
} else {
$has_extra_prev_post = false;
$has_extra_end_post = false;
}
// Get the previous cursor, if any.
if ($has_extra_prev_post) {
// Select the most recent post.
$prev_cursor_map[$uri] = $posts[1]['id'];
array_shift($posts);
$posts_count--;
}
// Get the next cursor, if any.
if ($has_extra_end_post) {
// Since we fetched 1 above the limit, we always know if there are any posts after the current page.
// Query orders by DESC, so the SECOND last post has the lowest ID.
array_pop($posts);
$next_cursor_map[$uri] = $posts[$posts_count - 2]['id'];
}
// Finally load the post contents and build them.
foreach ($posts as $post) {
if (!$post['thread']) {
$po = new Thread($post, '?/', $mod, false);
} else {
$po = new Post($post, '?/', $mod);
}
if (!isset($args['posts'][$uri])) {
$args['posts'][$uri] = [ 'board' => $board, 'posts' => [] ];
}
$args['posts'][$uri]['posts'][] = $po->build(true);
}
}
}
// Build the cursors.
$args['cursor_prev'] = !empty($encoded_cursor) ? encode_cursor('p', $prev_cursor_map) : false;
$args['cursor_next'] = !empty($next_cursor_map) ? encode_cursor('n', $next_cursor_map) : false;
$args['boards'] = $boards;
$args['token'] = make_secure_link_token('ban');
if (hasPermission($config['mod']['view_ban'])) { if (hasPermission($config['mod']['view_ban'])) {
$args['bans'] = Bans::find($ip, false, true, $config['auto_maintenance']); $args['bans'] = Bans::find($ip, false, true, $config['auto_maintenance']);
} }
if (hasPermission($config['mod']['view_notes'])) { if (hasPermission($config['mod']['view_notes'])) {
$note_queries = $ctx->get(IpNoteQueries::class); $ret = Cache::get("mod_page_ip_view_notes_$ip");
$args['notes'] = $note_queries->getByIp($ip); if (!$ret) {
$query = 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() or error(db_error($query));
$ret = $query->fetchAll(PDO::FETCH_ASSOC);
Cache::set("mod_page_ip_view_notes_$ip", $ret, 900);
}
$args['notes'] = $ret;
} }
if (hasPermission($config['mod']['modlog_ip'])) { if (hasPermission($config['mod']['modlog_ip'])) {
@ -981,117 +1049,13 @@ function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor =
$args['logs'] = []; $args['logs'] = [];
} }
$boards = listBoards();
$queryable_uris = [];
foreach ($boards as $board) {
$uri = $board['uri'];
if (hasPermission($config['mod']['show_ip'], $uri)) {
$queryable_uris[] = $uri;
}
}
$queries = $ctx->get(UserPostQueries::class);
$result = $queries->fetchPaginatedByIp($queryable_uris, $ip, $page_size, $encoded_cursor);
$args['cursor_prev'] = $result->cursor_prev;
$args['cursor_next'] = $result->cursor_next;
foreach($boards as $board) {
$uri = $board['uri'];
// The Thread and Post classes rely on some implicit board parameter set by openBoard.
openBoard($uri);
// Finally load the post contents and build them.
foreach ($result->by_uri[$uri] as $post) {
if (!$post['thread']) {
$po = new Thread($post, '?/', $mod, false);
} else {
$po = new Post($post, '?/', $mod);
}
if (!isset($args['posts'][$uri])) {
$args['posts'][$uri] = [ 'board' => $board, 'posts' => [] ];
}
$args['posts'][$uri]['posts'][] = $po->build(true);
}
}
$args['boards'] = $boards;
$args['token'] = make_secure_link_token('ban');
// Since the security token is only used to send requests to create notes and remove bans, use "?/IP/" as the url.
if (empty($encoded_cursor)) { if (empty($encoded_cursor)) {
$args['security_token'] = make_secure_link_token("IP/$ip"); $args['security_token'] = make_secure_link_token("IP/$ip");
} else { } else {
$args['security_token'] = make_secure_link_token("IP/$ip/cursor/$encoded_cursor"); $args['security_token'] = make_secure_link_token("IP/$ip/cursor/$encoded_cursor");
} }
mod_page(\sprintf('%s: %s', _('IP'), \htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']); mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']);
}
function mod_user_posts_by_passwd(Context $ctx, string $passwd, string $encoded_cursor = null) {
global $mod;
// The current hashPassword implementation uses sha3-256, which has a 64 character output in non-binary mode.
if (\strlen($passwd) != 64) {
error('Invalid password');
}
$config = $ctx->get('config');
$args = [
'passwd' => $passwd,
'posts' => []
];
if (isset($config['mod']['ip_recentposts'])) {
// TODO log to migrate.
$page_size = $config['mod']['ip_recentposts'];
} else {
$page_size = $config['mod']['recent_user_posts'];
}
$boards = listBoards();
$queryable_uris = [];
foreach ($boards as $board) {
$uri = $board['uri'];
if (hasPermission($config['mod']['show_ip'], $uri)) {
$queryable_uris[] = $uri;
}
}
$queries = $ctx->get(UserPostQueries::class);
$result = $queries->fetchPaginateByPassword($queryable_uris, $passwd, $page_size, $encoded_cursor);
$args['cursor_prev'] = $result->cursor_prev;
$args['cursor_next'] = $result->cursor_next;
foreach($boards as $board) {
$uri = $board['uri'];
// The Thread and Post classes rely on some implicit board parameter set by openBoard.
openBoard($uri);
// Finally load the post contents and build them.
foreach ($result->by_uri[$uri] as $post) {
if (!$post['thread']) {
$po = new Thread($post, '?/', $mod, false);
} else {
$po = new Post($post, '?/', $mod);
}
if (!isset($args['posts'][$uri])) {
$args['posts'][$uri] = [ 'board' => $board, 'posts' => [] ];
}
$args['posts'][$uri]['posts'][] = $po->build(true);
}
}
$args['boards'] = $boards;
$args['token'] = make_secure_link_token('ban');
mod_page(\sprintf('%s: %s', _('Password'), \htmlspecialchars($passwd)), 'mod/view_passwd.html', $args);
} }
function mod_ban(Context $ctx) { function mod_ban(Context $ctx) {
@ -1491,9 +1455,8 @@ function mod_move(Context $ctx, $originBoard, $postID) {
if ($targetBoard === $originBoard) if ($targetBoard === $originBoard)
error(_('Target and source board are the same.')); error(_('Target and source board are the same.'));
$_link_or_copy = _link_or_copy_factory($ctx);
// link() if leaving a shadow thread behind; else, rename(). // link() if leaving a shadow thread behind; else, rename().
$clone = $shadow ? $_link_or_copy : 'rename'; $clone = $shadow ? '_link_or_copy' : 'rename';
// indicate that the post is a thread // indicate that the post is a thread
$post['op'] = true; $post['op'] = true;
@ -1787,8 +1750,7 @@ function mod_merge(Context $ctx, $originBoard, $postID) {
$op = $post; $op = $post;
$op['id'] = $newID; $op['id'] = $newID;
$_link_or_copy = _link_or_copy_factory($ctx); $clone = $shadow ? '_link_or_copy' : 'rename';
$clone = $shadow ? $_link_or_copy : 'rename';
if ($post['has_file']) { if ($post['has_file']) {
// copy image // copy image
@ -2004,10 +1966,14 @@ function mod_ban_post(Context $ctx, $board, $delete, $post, $token = false) {
$autotag .= "/${board}/" . " " . $filehash . " " . $filename ."\r\n"; $autotag .= "/${board}/" . " " . $filehash . " " . $filename ."\r\n";
$autotag .= $body . "\r\n"; $autotag .= $body . "\r\n";
$autotag = escape_markup_modifiers($autotag); $autotag = escape_markup_modifiers($autotag);
markup($autotag);
$note_queries = $ctx->get(IpNoteQueries::class); $query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
$note_queries->add($ip, $mod['id'], $autotag); $query->bindValue(':ip', $ip);
modLog("Added a note for <a href=\"?/user_posts/ip/{$ip}\">{$ip}</a>"); $query->bindValue(':mod', $mod['id']);
$query->bindValue(':time', time());
$query->bindValue(':body', $autotag);
$query->execute() or error(db_error($query));
modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
} }
} }
deletePost($post); deletePost($post);
@ -2112,10 +2078,13 @@ function mod_warning_post(Context $ctx, $board, $post, $token = false) {
$autotag .= $body . "\r\n"; $autotag .= $body . "\r\n";
$autotag = escape_markup_modifiers($autotag); $autotag = escape_markup_modifiers($autotag);
markup($autotag); markup($autotag);
$query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
$note_queries = $ctx->get(IpNoteQueries::class); $query->bindValue(':ip', $ip);
$note_queries->add($ip, $mod['id'], $autotag); $query->bindValue(':mod', $mod['id']);
modLog("Added a note for <a href=\"?/user_posts/ip/{$ip}\">{$ip}</a>"); $query->bindValue(':time', time());
$query->bindValue(':body', $autotag);
$query->execute() or error(db_error($query));
modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
} }
} }
} }
@ -2220,7 +2189,7 @@ function mod_edit_post(Context $ctx, $board, $edit_raw_html, $postID) {
} }
function mod_delete(Context $ctx, $board, $post) { function mod_delete(Context $ctx, $board, $post) {
global $config, $mod; global $config;
if (!openBoard($board)) if (!openBoard($board))
error($config['error']['noboard']); error($config['error']['noboard']);
@ -2261,10 +2230,13 @@ function mod_delete(Context $ctx, $board, $post) {
$autotag .= $body . "\r\n"; $autotag .= $body . "\r\n";
$autotag = escape_markup_modifiers($autotag); $autotag = escape_markup_modifiers($autotag);
markup($autotag); markup($autotag);
$query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
$note_queries = $ctx->get(IpNoteQueries::class); $query->bindValue(':ip', $ip);
$note_queries->add($ip, $mod['id'], $autotag); $query->bindValue(':mod', $mod['id']);
modLog("Added a note for <a href=\"?/user_posts/ip/{$ip}\">{$ip}</a>"); $query->bindValue(':time', time());
$query->bindValue(':body', $autotag);
$query->execute() or error(db_error($query));
modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
} }
} }
deletePost($post); deletePost($post);
@ -2351,7 +2323,7 @@ function mod_spoiler_image(Context $ctx, $board, $post, $file) {
} }
function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) { function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) {
global $config, $board, $mod; global $config, $board;
$global = (bool)$global; $global = (bool)$global;
@ -2429,10 +2401,13 @@ function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) {
$autotag .= $body . "\r\n"; $autotag .= $body . "\r\n";
$autotag = escape_markup_modifiers($autotag); $autotag = escape_markup_modifiers($autotag);
markup($autotag); markup($autotag);
$query2 = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
$note_queries = $ctx->get(IpNoteQueries::class); $query2->bindValue(':ip', $ip);
$note_queries->add($ip, $mod['id'], $autotag); $query2->bindValue(':mod', $mod['id']);
modLog("Added a note for <a href=\"?/user_posts/ip/{$ip}\">{$ip}</a>"); $query2->bindValue(':time', time());
$query2->bindValue(':body', $autotag);
$query2->execute() or error(db_error($query2));
modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
} }
} }
@ -2463,7 +2438,7 @@ function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) {
} }
// Record the action // Record the action
modLog("Deleted all posts by IP address: <a href=\"?/user_posts/ip/$ip\">$ip</a>"); modLog("Deleted all posts by IP address: <a href=\"?/IP/$ip\">$ip</a>");
// Redirect // Redirect
header('Location: ?/' . sprintf($config['board_path'], $boardName) . $config['file_index'], true, $config['redirect_http']); header('Location: ?/' . sprintf($config['board_path'], $boardName) . $config['file_index'], true, $config['redirect_http']);
@ -3000,7 +2975,7 @@ function mod_report_dismiss(Context $ctx, $id, $all = false) {
if ($all) { if ($all) {
$report_queries->deleteByIp($ip); $report_queries->deleteByIp($ip);
modLog("Dismissed all reports by <a href=\"?/user_posts/ip/$ip\">$ip</a>"); modLog("Dismissed all reports by <a href=\"?/IP/$ip\">$ip</a>");
} else { } else {
$report_queries->deleteById($id); $report_queries->deleteById($id);
modLog("Dismissed a report for post #{$id}", $board); modLog("Dismissed a report for post #{$id}", $board);

View file

@ -881,7 +881,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(

View file

@ -23,15 +23,6 @@ $(window).ready(function() {
$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)
@ -111,7 +102,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();
}, },
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);

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');
@ -75,7 +71,7 @@ $(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();
@ -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
if (!forcedAnon)
name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]); name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]);
if (hasTrip) 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

@ -11,48 +11,43 @@
* 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 = [];
let i = 1; var i = 1;
for (styleName in styles) { stylesDiv.children().each(function() {
if (styleName) { var name = this.innerHTML.replace(/(^\[|\]$)/g, '');
let opt = $('<option></option>') var opt = $('<option></option>')
.html(styleName) .html(name)
.val(i); .val(i);
if (selectedstyle == styleName) { if ($(this).hasClass('selected'))
opt.attr('selected', true); opt.attr('selected', true);
} options.push ([name.toUpperCase (), opt]);
opt.attr('id', 'style-select-' + i); $(this).attr('id', 'style-select-' + i);
options.push([styleName.toUpperCase (), opt]);
i++; i++;
} });
}
options.sort ((a, b) => { 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,11 +1,16 @@
/* /*
* Don't load the 3rd party embedded content player unless the image is clicked. * youtube
* https://github.com/savetheinternet/Tinyboard/blob/master/js/youtube.js
*
* Don't load the YouTube player unless the video image is clicked.
* This increases performance issues when many videos are embedded on the same page. * This increases performance issues when many videos are embedded on the same page.
* Currently only compatiable with YouTube.
*
* Proof of concept.
* *
* 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>
* Copyright (c) 2025 Zankaria Auxa <zankaria.auxa@mailu.io>
* *
* Usage: * Usage:
* $config['embedding'] = array(); * $config['embedding'] = array();
@ -14,27 +19,22 @@
* $config['youtube_js_html']); * $config['youtube_js_html']);
* $config['additional_javascript'][] = 'js/jquery.min.js'; * $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/youtube.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;')
+ '<input type="text" size=30></label>'
+ '</fieldset>'); + '</fieldset>');
$('#youtube-embed-proxy-url>input').val(localStorage.youtube_embed_proxy); $('#youtube-embed-proxy-url>input').val(localStorage.youtube_embed_proxy);
@ -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

@ -68,15 +68,9 @@ $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.:]+)/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

View file

@ -5,7 +5,6 @@
use Vichan\Context; use Vichan\Context;
use Vichan\Data\ReportQueries; use Vichan\Data\ReportQueries;
use Vichan\Data\Driver\LogDriver;
require_once 'inc/bootstrap.php'; require_once 'inc/bootstrap.php';
@ -222,7 +221,7 @@ function send_matrix_report(
$end = strlen($post['body_nomarkup']) > $max_msg_len ? ' [...]' : ''; $end = strlen($post['body_nomarkup']) > $max_msg_len ? ' [...]' : '';
$post_content = mb_substr($post['body_nomarkup'], 0, $max_msg_len) . $end; $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; $text_body = $reported_post_url . ($post['thread'] ? "#$post_id" : '') . " \nReason:\n" . $report_reason . " \nPost:\n" . $post_content;
$random_transaction_id = mt_rand(); $random_transaction_id = mt_rand();
$json_body = json_encode([ $json_body = json_encode([
@ -355,7 +354,7 @@ 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']) {
@ -434,7 +433,7 @@ 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 = '';
@ -531,12 +530,10 @@ 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 = []; $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)) {
@ -611,8 +608,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 : '')
); );
@ -700,7 +697,9 @@ function handle_report(Context $ctx)
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,8 +714,9 @@ function handle_report(Context $ctx)
error($error); error($error);
} }
$ctx->get(LogDriver::class)->log( if ($config['syslog'])
LogDriver::INFO, _syslog(
LOG_INFO,
'Reported post: ' . 'Reported post: ' .
'/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') . '/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
' for "' . $reason . '"' ' for "' . $reason . '"'
@ -1009,16 +1009,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) {
@ -1209,6 +1204,9 @@ function handle_post(Context $ctx)
error($config['error']['toolong_body']); error($config['error']['toolong_body']);
} }
} }
if (mb_strlen($post['password']) > 20) {
error(sprintf($config['error']['toolong'], 'password'));
}
} }
wordfilters($post['body']); wordfilters($post['body']);
@ -1313,7 +1311,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,13 +1400,13 @@ 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'])
) { ) {
// Copy, because there's nothing to resize // Copy, because there's nothing to resize
copy($file['tmp_name'], $file['thumb']); coopy($file['tmp_name'], $file['thumb']);
$file['thumbwidth'] = $image->size->width; $file['thumbwidth'] = $image->size->width;
$file['thumbheight'] = $image->size->height; $file['thumbheight'] = $image->size->height;
@ -1551,6 +1549,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 +1625,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']) {
@ -1747,10 +1779,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 . '"');
@ -1843,17 +1875,17 @@ 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");
} }
} }
$ctx = Vichan\build_context($config);
if (isset($_POST['delete'])) { if (isset($_POST['delete'])) {
handle_delete($ctx); handle_delete($ctx);
} elseif (isset($_POST['report'])) { } elseif (isset($_POST['report'])) {

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

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

@ -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

@ -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

@ -61,11 +61,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 {

View file

@ -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

@ -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;

View file

@ -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

@ -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;
@ -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

@ -56,10 +56,6 @@ div.post.reply {
border-top: none; border-top: none;
border-radius: 5px; border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35); box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
@media (max-width: 48em) {
border-left-style: none;
}
} }
div.post.reply.highlighted { div.post.reply.highlighted {
@ -69,10 +65,6 @@ div.post.reply.highlighted {
border-top: none; border-top: none;
border-radius: 5px; border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35); 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 { div.post.reply div.body a {

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
{ {

View file

@ -53,10 +53,6 @@ div.post.reply {
border-top: none; border-top: none;
border-radius: 5px; border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35); box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35);
@media (max-width: 48em) {
border-right-style: none;
}
} }
div.post.reply.highlighted { div.post.reply.highlighted {
@ -66,10 +62,6 @@ div.post.reply.highlighted {
border-top: none; border-top: none;
border-radius: 5px; border-radius: 5px;
box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.35); 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 { div.post.reply div.body a {

View file

@ -121,11 +121,6 @@ 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
{ {
@ -304,3 +299,5 @@ div.report
{ {
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 {

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

@ -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;

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 {

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;
} }

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;

View file

@ -1372,11 +1372,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

@ -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,27 @@ 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 = {
/**
* @returns {object} Opaque widget id.
*/
applyOn: (container, params) => hcaptcha.render(container, { applyOn: (container, params) => hcaptcha.render(container, {
sitekey: "{{ config.captcha.hcaptcha.public }}", sitekey: "{{ config.hcaptcha_public }}",
callback: params['on-success'], 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,16 +299,11 @@ 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 = {
/**
* @returns {object} Opaque widget id.
*/
applyOn: function(container, params) { 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'], callback: params['on-success'],
}); });
@ -316,22 +312,8 @@ function onCaptchaLoadTurnstile(action) {
} }
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);
@ -353,7 +335,6 @@ function onCaptchaLoad(renderer) {
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 +342,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 +355,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 +412,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 +528,7 @@ var script_settings = function(script_name) {
}; };
function init() { function init() {
initStyleChooser();
initCaptcha(); initCaptcha();
{% endraw %} {% endraw %}

View file

@ -1,4 +1,3 @@
{% if config.url_banner %}<img class="board_image" src="{{ config.url_banner }}" {% if config.banner_width or config.banner_height %}style="{% if config.banner_width %}width:{{ config.banner_width }}px{% endif %};{% if config.banner_width %}height:{{ config.banner_height }}px{% endif %}" {% endif %}alt="" />{% endif %}
<ul> <ul>
{% for board in boards %} {% for board in boards %}
<li> <li>

View file

@ -1,5 +1,4 @@
{% if error %}<h2 style="text-align:center">{{ error }}</h2>{% endif %} {% if error %}<h2 style="text-align:center">{{ error }}</h2>{% endif %}
{% if config.url_banner %}<img class="board_image" src="{{ config.url_banner }}" {% if config.banner_width or config.banner_height %}style="{% if config.banner_width %}width:{{ config.banner_width }}px{% endif %};{% if config.banner_width %}height:{{ config.banner_height }}px{% endif %}" {% endif %}alt="" />{% endif %}
<form action="" method="post"> <form action="" method="post">
<table style="margin-top:25px;"> <table style="margin-top:25px;">
<tr> <tr>

View file

@ -1,12 +0,0 @@
{% for board_posts in posts %}
<fieldset>
<legend>
<a href="?/{{ config.board_path|sprintf(board_posts.board.uri) }}{{ config.file_index }}">
{{ config.board_abbreviation|sprintf(board_posts.board.uri) }}
-
{{ board_posts.board.title|e }}
</a>
</legend>
{{ board_posts.posts|join('<hr>') }}
</fieldset>
{% endfor %}

View file

@ -1,4 +1,4 @@
{% if mod|hasPermission(config.mod.view_notes) and notes is not null %} {% if mod|hasPermission(config.mod.view_notes) %}
<fieldset id="notes"> <fieldset id="notes">
<legend> <legend>
{% set notes_on_record = 'note' ~ (notes|count != 1 ? 's' : '') ~ ' on record' %} {% set notes_on_record = 'note' ~ (notes|count != 1 ? 's' : '') ~ ' on record' %}
@ -43,7 +43,7 @@
{% endif %} {% endif %}
{% if mod|hasPermission(config.mod.create_notes) %} {% if mod|hasPermission(config.mod.create_notes) %}
<form action="?/IP/{{ ip|url_encode(true) }}" method="post" style="margin:0"> <form action="" method="post" style="margin:0">
<input type="hidden" name="token" value="{{ security_token }}"> <input type="hidden" name="token" value="{{ security_token }}">
<table> <table>
<tr> <tr>
@ -74,7 +74,7 @@
<legend>{{ bans|count }} {% trans bans_on_record %}</legend> <legend>{{ bans|count }} {% trans bans_on_record %}</legend>
{% for ban in bans %} {% for ban in bans %}
<form action="?/IP/{{ ip|url_encode(true) }}" method="post" style="text-align:center"> <form action="" method="post" style="text-align:center">
<input type="hidden" name="token" value="{{ security_token }}"> <input type="hidden" name="token" value="{{ security_token }}">
<table style="width:400px;margin-bottom:10px;border-bottom:1px solid #ddd;padding:5px"> <table style="width:400px;margin-bottom:10px;border-bottom:1px solid #ddd;padding:5px">
<tr> <tr>
@ -201,13 +201,24 @@
</fieldset> </fieldset>
{% endif %} {% endif %}
{{ include('mod/user_posts_list.html', {posts: posts}) }} {% for board_posts in posts %}
<fieldset>
<legend>
<a href="?/{{ config.board_path|sprintf(board_posts.board.uri) }}{{ config.file_index }}">
{{ config.board_abbreviation|sprintf(board_posts.board.uri) }}
-
{{ board_posts.board.title|e }}
</a>
</legend>
{{ board_posts.posts|join('<hr>') }}
</fieldset>
{% endfor %}
<div class="pages" style="margin-left: 50%"> <div class="pages" style="margin-left: 50%">
<a href="?/user_posts/ip/{{ ip }}">[Page 1]</a> <a href="?/IP/{{ ip }}">[Page 1]</a>
{% if cursor_prev %} {% if cursor_prev %}
<a href="?/user_posts/ip/{{ ip }}/cursor/{{ cursor_prev }}">[Previous Page]</a> <a href="?/IP/{{ ip }}/cursor/{{ cursor_prev }}">[Previous Page]</a>
{% endif %} {% endif %}
{% if cursor_next %} {% if cursor_next %}
<a href="?/user_posts/ip/{{ ip }}/cursor/{{ cursor_next }}">[Next Page]</a> <a href="?/IP/{{ ip }}/cursor/{{ cursor_next }}">[Next Page]</a>
{% endif %} {% endif %}
</div> </div>

View file

@ -1,50 +0,0 @@
{% if logs and logs|length > 0 %}
<fieldset id="history">
<legend>History</legend>
<table class="modlog" style="width:100%">
<tr>
<th>{% trans 'Staff' %}</th>
<th>{% trans 'Time' %}</th>
<th>{% trans 'Board' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
{% for log in logs %}
<tr>
<td class="minimal">
{% if log.username %}
<a href="?/log:{{ log.username|e }}">{{ log.username|e }}</a>
{% elseif log.mod == -1 %}
<em>system</em>
{% else %}
<em>{% trans 'deleted?' %}</em>
{% endif %}
</td>
<td class="minimal">
<span title="{{ log.time|date(config.post_date) }}">{{ log.time|ago }}</span>
</td>
<td class="minimal">
{% if log.board %}
<a href="?/{{ config.board_path|sprintf(log.board) }}{{ config.file_index }}">{{ config.board_abbreviation|sprintf(log.board) }}</a>
{% else %}
-
{% endif %}
</td>
<td>
{{ log.text }}
</td>
</tr>
{% endfor %}
</table>
</fieldset>
{% endif %}
{{ include('mod/user_posts_list.html', {posts: posts}) }}
<div class="pages" style="margin-left: 50%">
<a href="?/user_posts/passwd/{{ passwd }}">[Page 1]</a>
{% if cursor_prev %}
<a href="?/user_posts/passwd/{{ passwd }}/cursor/{{ cursor_prev }}">[Previous Page]</a>
{% endif %}
{% if cursor_next %}
<a href="?/user_posts/passwd/{{ passwd }}/cursor/{{ cursor_next }}">[Next Page]</a>
{% endif %}
</div>

View file

@ -1,3 +1,3 @@
{% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %} {% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %}
[<a class="ip-link" style="margin:0;" href="?/user_posts/ip/{{ post.ip }}">{{ post.ip }}</a>] [<a class="ip-link" style="margin:0;" href="?/user_posts/passwd/{{ post.password }}">{{ post.password[:15] }}</a>] {# Keep this space #} [<a class="ip-link" style="margin:0;" href="?/IP/{{ post.ip }}">{{ post.ip }}</a>]
{% endif %} {% endif %}

View file

@ -90,8 +90,8 @@
{% endif %} {% endif %}
</td> </td>
</tr>{% endif %} </tr>{% endif %}
{% if config.captcha.mode == 'recaptcha' %} {% if config.recaptcha %}
{% if config.captcha.dynamic %} {% if config.dynamic_captcha %}
<tr id="captcha" style="display: none;"> <tr id="captcha" style="display: none;">
{% else %} {% else %}
<tr> <tr>
@ -101,13 +101,13 @@
{{ antibot.html() }} {{ antibot.html() }}
</th> </th>
<td> <td>
<div class="g-recaptcha" data-sitekey="{{ config.captcha.recaptcha.public }}"></div> <div class="g-recaptcha" data-sitekey="{{ config.recaptcha_public }}"></div>
{{ antibot.html() }} {{ antibot.html() }}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% if config.captcha.mode == 'hcaptcha' or config.captcha.mode == 'turnstile' %} {% if config.hcaptcha or config.turnstile %}
{% if config.captcha.dynamic %} {% if config.dynamic_captcha %}
<tr id="captcha" style="display: none;"> <tr id="captcha" style="display: none;">
{% else %} {% else %}
<tr> <tr>

View file

@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS ``posts_{{ board }}`` (
`files` text DEFAULT NULL, `files` text DEFAULT NULL,
`num_files` int(11) DEFAULT 0, `num_files` int(11) DEFAULT 0,
`filehash` text CHARACTER SET ascii, `filehash` text CHARACTER SET ascii,
`password` varchar(64) DEFAULT NULL, `password` varchar(20) DEFAULT NULL,
`ip` varchar(39) CHARACTER SET ascii NOT NULL, `ip` varchar(39) CHARACTER SET ascii NOT NULL,
`sticky` int(1) NOT NULL, `sticky` int(1) NOT NULL,
`locked` int(1) NOT NULL, `locked` int(1) NOT NULL,

View file

@ -36,7 +36,7 @@ Opening posts with liberalism or reactionary topics will be treated with far mor
<p>9) Due to derailing, COVID denialism outside the COVID-19 thread will be deleted.</p> <p>9) Due to derailing, COVID denialism outside the COVID-19 thread will be deleted.</p>
<p>10) All boards except for /siberia/ (and potentially /roulette/) are 'Safe For Work' boards. Pornography should not be posted on them without good reason, and any pornography on these boards should be hidden using the Spoiler Image option. New threads on /siberia/ with pornographic topics should have a Spoiler Image on the opening post. Some kinds of pornographic content are always banned on every board, including /siberia/: cp/loli/jailbait/anything that could possibly interpreted a child, "feral" furry, zoophilia, murder/gore (photographic) and other suitably extreme fetishes.</p> <p>10) All boards except for /siberia/ (and potentially /roulette/) are 'Safe For Work' boards. Pornography should not be posted on them without good reason, and any pornography on these boards should be hidden using the Spoiler Image option. New threads on /siberia/ with pornographic topics should have a Spoiler Image on the opening post.</p>
<p>11) Posts should, overall, be conductive to an informed and productive discussion. /leftypol/ is not an academic journal, but it also should not be a cesspit of back and forth bickering and pointless insults. Users should attempt to argue for the point they are presenting in an honest and open way and should be receptive to information or arguments that do, in fact, challenge their views.</p> <p>11) Posts should, overall, be conductive to an informed and productive discussion. /leftypol/ is not an academic journal, but it also should not be a cesspit of back and forth bickering and pointless insults. Users should attempt to argue for the point they are presenting in an honest and open way and should be receptive to information or arguments that do, in fact, challenge their views.</p>

View file

@ -96,7 +96,6 @@
grid-column: 1; grid-column: 1;
grid-row: 3; grid-row: 3;
width: 100%; width: 100%;
word-break: break-all;
} }
.modlog { .modlog {

View file

@ -11,15 +11,12 @@
.home-description { .home-description {
margin: 20px auto 0 auto; margin: 20px auto 0 auto;
text-align: center; text-align: center;
max-width: 700px; max-width: 700px;"
} }
</style> </style>
{{ boardlist.top }} {{ boardlist.top }}
<header> <header>
<meta charset="utf-8"> <h1>{{ settings.title }}</h1>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
{% if config.meta_keywords %}<meta name="keywords" content="{{ config.meta_keywords }}">{% endif %}
<title>{{ settings.title }}</title>
<img src="{{ config.logo }}" alt="logo" class="home-logo"> <img src="{{ config.logo }}" alt="logo" class="home-logo">
<div class="subtitle">{{ settings.subtitle }}</div> <div class="subtitle">{{ settings.subtitle }}</div>
<link rel="stylesheet" media="screen" href="{{ config.url_stylesheet }}?v={{ config.resource_version }}"> <link rel="stylesheet" media="screen" href="{{ config.url_stylesheet }}?v={{ config.resource_version }}">

View file

@ -4,6 +4,7 @@
<head> <head>
<link rel="stylesheet" media="screen" href="/stylesheets/style.css?v={{ config.resource_version }}"> <link rel="stylesheet" media="screen" href="/stylesheets/style.css?v={{ config.resource_version }}">
<meta charset="utf-8"> <meta charset="utf-8">
<title>{{ settings.title }}</title>
{% if config.meta_keywords %}<meta name="keywords" content="{{ config.meta_keywords }}">{% endif %} {% if config.meta_keywords %}<meta name="keywords" content="{{ config.meta_keywords }}">{% endif %}
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
<script type='text/javascript'> <script type='text/javascript'>
@ -25,7 +26,7 @@
<h2 id="1"> <h2 id="1">
My question isn't here. What do I do ? My question isn't here. What do I do ?
</h2> </h2>
<p>Make a post in the <a href="/meta/">/meta/</a> board or ask via our <a href="https://matrix.to/#/%23gauchepol:matrix.org">Matrix Congress chat</a> </p> <p>Make a post in the <a href="/meta/">/meta/</a> board or ask via our <a href="https://matrix.to/#/!nTwQxlnWchxZsgDAgE:matrix.org?via=matrix.org">Matrix Congress chat</a> </p>
<h2 id="2"> <h2 id="2">
What are the rules ? What are the rules ?
</h2> </h2>
@ -88,7 +89,7 @@
<h2 id="9"> <h2 id="9">
How can I suggest or submit fixes to the site ? How can I suggest or submit fixes to the site ?
</h2> </h2>
<p>There is a /meta/ thread for this, and our <a href="https://forgejo.leftypol.org/leftypol/leftypol/">Forgejo repo</a>.</p> <p>There is a /meta/ thread for this, and our <a href="https://gitlab.leftypol.org/leftypol/leftypol/">Gitlab repo</a>.</p>
<h2 id="10"> <h2 id="10">
I don't trust Tor exit nodes. Do you have an .onion site ? I don't trust Tor exit nodes. Do you have an .onion site ?
</h2> </h2>
@ -120,7 +121,7 @@
<li>PDF Files (Supports thumbnail) </li> <li>PDF Files (Supports thumbnail) </li>
<li>EPUB Files</li> <li>EPUB Files</li>
<li>DJVU Files (Supports thumbnail) </li> <li>DJVU Files (Supports thumbnail) </li>
<li>Text Files</li> <li>Text Files (Supports thumbnail) </li>
<li>ZIP Files</li> <li>ZIP Files</li>
<li>GZ Files</li> <li>GZ Files</li>
<li>BZ2 Files</li> <li>BZ2 Files</li>
@ -129,10 +130,6 @@
What are the maximum filesize for attachments ? What are the maximum filesize for attachments ?
</h2> </h2>
<p>Maximum file size in megabytes for attachments to a single post is 80MB (e.g. 5 * 16MB), as most boards support uploading 5 attachments by default. Maximum file size in pixels for images is currently set to 20000 by 20000. </p> <p>Maximum file size in megabytes for attachments to a single post is 80MB (e.g. 5 * 16MB), as most boards support uploading 5 attachments by default. Maximum file size in pixels for images is currently set to 20000 by 20000. </p>
<h2 id="16">
Can I have an account on your git instance?
</h2>
<p>Create the account on <a href="https://forgejo.leftypol.org/">Forgejo</a>, then contact the staff via the Tech Team General thread on /meta/ to get your account approved.</p>
</div> </div>

View file

@ -12,21 +12,21 @@
'title' => 'Overboard', 'title' => 'Overboard',
'uri' => 'overboard', 'uri' => 'overboard',
'subtitle' => '30 most recently bumped threads', 'subtitle' => '30 most recently bumped threads',
'exclude' => [ 'gulag', 'roulette', 'roulette_archive' ], 'exclude' => array('assembly', 'assembly_archive', 'gulag'),
'thread_limit' => $thread_limit, 'thread_limit' => $thread_limit,
), ),
array( array(
'title' => 'SFW Overboard', 'title' => 'SFW Overboard',
'uri' => 'sfw', 'uri' => 'sfw',
'subtitle' => '30 most recently bumped threads from work-safe boards', 'subtitle' => '30 most recently bumped threads from work-safe boards',
'exclude' => [ 'gulag', 'b', 'siberia', 'roulette', 'roulette_archive' ], 'exclude' => array('assembly', 'assembly_archive', 'gulag', 'b', 'siberia'),
'thread_limit' => $thread_limit, 'thread_limit' => $thread_limit,
), ),
array( array(
'title' => 'Alternate Overboard', 'title' => 'Alternate Overboard',
'uri' => 'alt', 'uri' => 'alt',
'subtitle' => '30 most recently bumped threads from smaller interest boards', 'subtitle' => '30 most recently bumped threads from smaller interest boards',
'exclude' => [ 'gulag', 'leftypol', 'b', 'siberia', 'meta', 'roulette', 'roulette_archive' ], 'exclude' => array('assembly', 'assembly_archive', 'gulag', 'leftypol', 'b', 'siberia', 'meta'),
'thread_limit' => $thread_limit, 'thread_limit' => $thread_limit,
), ),
); );

View file

@ -74,21 +74,13 @@
private function fetchThreads($overboard) { private function fetchThreads($overboard) {
$query = ''; $query = '';
$boards = listBoards(true); $boards = listBoards(true);
$included_boards = [];
foreach ($boards as $b) { foreach ($boards as $b) {
if (in_array($b, $overboard['exclude'])) if (in_array($b, $overboard['exclude']))
continue; continue;
$included_boards[] = $b;
}
if (empty($included_boards)) {
return [];
}
foreach ($included_boards as $b) {
// Threads are those posts that have no parent thread // Threads are those posts that have no parent thread
$query .= "SELECT *, '$b' AS `board` FROM ``posts_$b`` WHERE `thread` IS NULL UNION ALL "; $query .= "SELECT *, '$b' AS `board` FROM ``posts_$b`` " .
"WHERE `thread` IS NULL UNION ALL ";
} }
$query = preg_replace('/UNION ALL $/', 'ORDER BY `bump` DESC', $query); $query = preg_replace('/UNION ALL $/', 'ORDER BY `bump` DESC', $query);

0
tmp/tesseract/.gitkeep Normal file
View file

View file

@ -1,16 +0,0 @@
<?php
require_once dirname(__FILE__) . '/inc/cli.php';
foreach (listBoards(true) as $uri) {
query(\sprintf('ALTER TABLE ``posts_%s`` MODIFY `password` varchar(64) DEFAULT NULL;', $uri)) or error(db_error());
$query = prepare(\sprintf("SELECT DISTINCT `password` FROM ``posts_%s``", $uri));
$query->execute() or error(db_error($query));
while($entry = $query->fetch(\PDO::FETCH_ASSOC)) {
$update_query = prepare(\sprintf("UPDATE ``posts_%s`` SET `password` = :password WHERE `password` = :password_org", $uri));
$update_query->bindValue(':password', hashPassword($entry['password']));
$update_query->bindValue(':password_org', $entry['password']);
$update_query->execute() or error(db_error());
}
}

View file

@ -3,11 +3,57 @@
* Performs maintenance tasks. Invoke this periodically if the auto_maintenance configuration option is turned off. * Performs maintenance tasks. Invoke this periodically if the auto_maintenance configuration option is turned off.
*/ */
use Vichan\Data\ReportQueries;
require dirname(__FILE__) . '/inc/cli.php'; require dirname(__FILE__) . '/inc/cli.php';
$ctx = Vichan\build_context($config); function get_reports_by_board(): array {
$query = prepare("SELECT `board`, `post`, `id` FROM ``reports``");
$query->execute() or error(db_error($query));
return $query->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_NUM);
}
function post_ids_by_board(array $reports): array {
$ret = [];
foreach ($reports as $board => $values) {
foreach ($values as $value) {
$ret[$board] ??= [];
$ret[$board][] = $value[0];
}
}
return $ret;
}
function filter_invalid_reports(array $board_post_ids, array $reports): array {
$invalid_reports = [];
foreach ($board_post_ids as $board => $post_ids) {
$query = query(sprintf('SELECT `id` FROM ``posts_%s`` WHERE `id` = ' . implode(' OR `id` = ', $post_ids), $board));
$existing_posts = $query->fetchAll(PDO::FETCH_COLUMN);
foreach ($reports[$board] as $values) {
list($post_id, $report_id) = $values;
if (!in_array($post_id, $existing_posts)) {
$invalid_reports[] = $report_id;
}
}
}
return $invalid_reports;
}
function get_report_ids(array $reports): array {
$ret = [];
foreach ($reports as $_board => $values) {
foreach ($values as $value) {
$report_id = $value[0];
$ret[] = $report_id;
}
}
return $ret;
}
function delete_reports(array $report_ids): int {
return query('DELETE FROM ``reports`` WHERE `id` = ' . implode(' OR `id` = ', $report_ids))->rowCount();
}
echo "Clearing expired bans...\n"; echo "Clearing expired bans...\n";
$start = microtime(true); $start = microtime(true);
@ -26,9 +72,16 @@ $time_tot = $delta;
$deleted_tot += $deleted_count; $deleted_tot += $deleted_count;
echo "Clearing invalid reports...\n"; echo "Clearing invalid reports...\n";
$report_queries = $ctx->get(ReportQueries::class);
$start = microtime(true); $start = microtime(true);
$deleted_count = $report_queries->purge(); $reports = get_reports_by_board();
$report_ids = get_report_ids($reports);
$board_post_ids = post_ids_by_board($reports);
$invalid_reports = filter_invalid_reports($board_post_ids, $reports);
if (!empty($invalid_reports)) {
$deleted_count = delete_reports($invalid_reports);
} else {
$deleted_count = 0;
}
$delta = microtime(true) - $start; $delta = microtime(true) - $start;
echo "Deleted $deleted_count invalid reports in $delta seconds!\n"; echo "Deleted $deleted_count invalid reports in $delta seconds!\n";
$time_tot += $delta; $time_tot += $delta;