diff --git a/.gitignore b/.gitignore
index 93cac6d4..3205c64b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -70,9 +70,6 @@ tf/
/mod/
/random/
-# Banners
-static/banners/*
-
#Fonts
stylesheets/fonts
diff --git a/docker-compose.yml b/compose.yml
similarity index 81%
rename from docker-compose.yml
rename to compose.yml
index 1f785cb0..526e18c6 100644
--- a/docker-compose.yml
+++ b/compose.yml
@@ -7,7 +7,7 @@ services:
ports:
- "9091:80"
depends_on:
- - leftypol-db
+ - db
volumes:
- ./local-instances/${INSTANCE:-0}/www:/var/www/html
- ./docker/nginx/leftypol.conf:/etc/nginx/conf.d/default.conf
@@ -23,13 +23,11 @@ services:
volumes:
- ./local-instances/${INSTANCE:-0}/www:/var/www
- ./docker/php/www.conf:/usr/local/etc/php-fpm.d/www.conf
+ - redis-sock:/var/run/redis
#MySQL Service
- leftypol-db:
+ db:
image: mysql:8.0.35
- container_name: leftypol-db
- restart: unless-stopped
- tty: true
ports:
- "3306:3306"
environment:
@@ -37,3 +35,13 @@ services:
MYSQL_ROOT_PASSWORD: password
volumes:
- ./local-instances/${INSTANCE:-0}/mysql:/var/lib/mysql
+
+ redis:
+ build:
+ context: ./
+ dockerfile: ./docker/redis/Dockerfile
+ volumes:
+ - redis-sock:/var/run/redis
+
+volumes:
+ redis-sock:
diff --git a/composer.json b/composer.json
index e9ceac19..d0345e3b 100644
--- a/composer.json
+++ b/composer.json
@@ -11,7 +11,9 @@
"autoload": {
"classmap": ["inc/"],
"files": [
+ "inc/anti-bot.php",
"inc/bootstrap.php",
+ "inc/context.php",
"inc/display.php",
"inc/template.php",
"inc/database.php",
diff --git a/docker/redis/Dockerfile b/docker/redis/Dockerfile
new file mode 100644
index 00000000..9ab7f7fc
--- /dev/null
+++ b/docker/redis/Dockerfile
@@ -0,0 +1,6 @@
+FROM redis:7.4-alpine
+
+RUN mkdir -p /var/run/redis && chmod 777 /var/run/redis
+COPY ./docker/redis/redis.conf /etc/redis.conf
+
+ENTRYPOINT [ "docker-entrypoint.sh", "/etc/redis.conf" ]
diff --git a/docker/redis/redis.conf b/docker/redis/redis.conf
new file mode 100644
index 00000000..609d57ff
--- /dev/null
+++ b/docker/redis/redis.conf
@@ -0,0 +1,16 @@
+# Accept connections on the specified port, default is 6379 (IANA #815344).
+# If port 0 is specified Redis will not listen on a TCP socket.
+#port 6379
+port 0
+
+# Unix socket.
+#
+# Specify the path for the Unix socket that will be used to listen for
+# incoming connections. There is no default, so Redis will not listen
+# on a unix socket when not specified.
+#
+unixsocket /var/run/redis/redis-server.sock
+# Executig a socket is a no-op, and we need to share acces to other programs.
+# Shared the connection only with programs in the redis group for security.
+#unixsocketperm 700
+unixsocketperm 666
diff --git a/inc/Data/Driver/ApcuCacheDriver.php b/inc/Data/Driver/ApcuCacheDriver.php
new file mode 100644
index 00000000..a39bb656
--- /dev/null
+++ b/inc/Data/Driver/ApcuCacheDriver.php
@@ -0,0 +1,28 @@
+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);
+ }
+ }
+}
diff --git a/inc/Data/Driver/FileLogDriver.php b/inc/Data/Driver/FileLogDriver.php
new file mode 100644
index 00000000..2c9f14a0
--- /dev/null
+++ b/inc/Data/Driver/FileLogDriver.php
@@ -0,0 +1,61 @@
+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);
+ }
+}
diff --git a/inc/Data/Driver/FsCachedriver.php b/inc/Data/Driver/FsCachedriver.php
new file mode 100644
index 00000000..b543cfa6
--- /dev/null
+++ b/inc/Data/Driver/FsCachedriver.php
@@ -0,0 +1,155 @@
+prefix . $key;
+ }
+
+ private function sharedLockCache(): void {
+ \flock($this->lock_fd, LOCK_SH);
+ }
+
+ private function exclusiveLockCache(): void {
+ \flock($this->lock_fd, LOCK_EX);
+ }
+
+ private function unlockCache(): void {
+ \flock($this->lock_fd, LOCK_UN);
+ }
+
+ private function collectImpl(): int {
+ /*
+ * A read lock is ok, since it's alright if we delete expired items from under the feet of other processes, and
+ * no other process add new cache items or refresh existing ones.
+ */
+ $files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
+ $count = 0;
+ foreach ($files as $file) {
+ $data = \file_get_contents($file);
+ $wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
+ if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
+ if (@\unlink($file)) {
+ $count++;
+ }
+ }
+ }
+ return $count;
+ }
+
+ private function maybeCollect(): void {
+ if ($this->collect_chance_den !== false && \mt_rand(0, $this->collect_chance_den - 1) === 0) {
+ $this->collect_chance_den = false; // Collect only once per instance (aka process).
+ $this->collectImpl();
+ }
+ }
+
+ public function __construct(string $prefix, string $base_path, string $lock_file, int|false $collect_chance_den) {
+ if ($base_path[\strlen($base_path) - 1] !== '/') {
+ $base_path = "$base_path/";
+ }
+
+ if (!\is_dir($base_path)) {
+ throw new \RuntimeException("$base_path is not a directory!");
+ }
+
+ if (!\is_writable($base_path)) {
+ throw new \RuntimeException("$base_path is not writable!");
+ }
+
+ $this->lock_fd = \fopen($base_path . $lock_file, 'w');
+ if ($this->lock_fd === false) {
+ throw new \RuntimeException('Unable to open the lock file!');
+ }
+
+ $this->prefix = $prefix;
+ $this->base_path = $base_path;
+ $this->collect_chance_den = $collect_chance_den;
+ }
+
+ public function __destruct() {
+ $this->close();
+ }
+
+ public function get(string $key): mixed {
+ $key = $this->prepareKey($key);
+
+ $this->sharedLockCache();
+
+ // Collect expired items first so if the target key is expired we shortcut to failure in the next lines.
+ $this->maybeCollect();
+
+ $fd = \fopen($this->base_path . $key, 'r');
+ if ($fd === false) {
+ $this->unlockCache();
+ return null;
+ }
+
+ $data = \stream_get_contents($fd);
+ \fclose($fd);
+ $this->unlockCache();
+ $wrapped = \json_decode($data, true, 512, \JSON_THROW_ON_ERROR);
+
+ if ($wrapped['expires'] !== false && $wrapped['expires'] <= \time()) {
+ // Already expired, leave it there since we already released the lock and pretend it doesn't exist.
+ return null;
+ } else {
+ return $wrapped['inner'];
+ }
+ }
+
+ public function set(string $key, mixed $value, mixed $expires = false): void {
+ $key = $this->prepareKey($key);
+
+ $wrapped = [
+ 'expires' => $expires ? \time() + $expires : false,
+ 'inner' => $value
+ ];
+
+ $data = \json_encode($wrapped);
+ $this->exclusiveLockCache();
+ $this->maybeCollect();
+ \file_put_contents($this->base_path . $key, $data);
+ $this->unlockCache();
+ }
+
+ public function delete(string $key): void {
+ $key = $this->prepareKey($key);
+
+ $this->exclusiveLockCache();
+ @\unlink($this->base_path . $key);
+ $this->maybeCollect();
+ $this->unlockCache();
+ }
+
+ public function collect(): int {
+ $this->sharedLockCache();
+ $count = $this->collectImpl();
+ $this->unlockCache();
+ return $count;
+ }
+
+ public function flush(): void {
+ $this->exclusiveLockCache();
+ $files = \glob($this->base_path . $this->prefix . '*', \GLOB_NOSORT);
+ foreach ($files as $file) {
+ @\unlink($file);
+ }
+ $this->unlockCache();
+ }
+
+ public function close(): void {
+ \fclose($this->lock_fd);
+ }
+}
diff --git a/inc/Data/Driver/LogDriver.php b/inc/Data/Driver/LogDriver.php
new file mode 100644
index 00000000..fddc3f27
--- /dev/null
+++ b/inc/Data/Driver/LogDriver.php
@@ -0,0 +1,22 @@
+inner = new \Memcached();
+ if (!$this->inner->setOption(\Memcached::OPT_BINARY_PROTOCOL, true)) {
+ throw new \RuntimeException('Unable to set the memcached protocol!');
+ }
+ if (!$this->inner->setOption(\Memcached::OPT_PREFIX_KEY, $prefix)) {
+ throw new \RuntimeException('Unable to set the memcached prefix!');
+ }
+ if (!$this->inner->addServers($memcached_server)) {
+ throw new \RuntimeException('Unable to add the memcached server!');
+ }
+ }
+
+ public function get(string $key): mixed {
+ $ret = $this->inner->get($key);
+ // If the returned value is false but the retrival was a success, then the value stored was a boolean false.
+ if ($ret === false && $this->inner->getResultCode() !== \Memcached::RES_SUCCESS) {
+ return null;
+ }
+ return $ret;
+ }
+
+ public function set(string $key, mixed $value, mixed $expires = false): void {
+ $this->inner->set($key, $value, (int)$expires);
+ }
+
+ public function delete(string $key): void {
+ $this->inner->delete($key);
+ }
+
+ public function flush(): void {
+ $this->inner->flush();
+ }
+}
diff --git a/inc/Data/Driver/NoneCacheDriver.php b/inc/Data/Driver/NoneCacheDriver.php
new file mode 100644
index 00000000..8b260a50
--- /dev/null
+++ b/inc/Data/Driver/NoneCacheDriver.php
@@ -0,0 +1,26 @@
+inner = new \Redis();
+ if (str_starts_with($host, 'unix:') || str_starts_with($host, ':')) {
+ $ret = \explode(':', $host);
+ if (count($ret) < 2) {
+ throw new \RuntimeException("Invalid unix socket path $host");
+ }
+ // Unix socket.
+ $this->inner->connect($ret[1]);
+ } elseif ($port === null) {
+ $this->inner->connect($host);
+ } else {
+ // IP + port.
+ $this->inner->connect($host, $port);
+ }
+ if ($password) {
+ $this->inner->auth($password);
+ }
+ if (!$this->inner->setOption(\Redis::OPT_SERIALIZER, \Redis::SERIALIZER_JSON)) {
+ throw new \RuntimeException('Unable to configure Redis serializer');
+ }
+ if (!$this->inner->select($database)) {
+ throw new \RuntimeException('Unable to connect to Redis database!');
+ }
+
+ $this->prefix = $prefix;
+ }
+
+ public function get(string $key): mixed {
+ $ret = $this->inner->get($this->prefix . $key);
+ if ($ret === false) {
+ return null;
+ }
+ if ($ret === null) {
+ return false;
+ }
+ return $ret;
+ }
+
+ public function set(string $key, mixed $value, mixed $expires = false): void {
+ $value = $value === false ? null : $value;
+ if ($expires === false) {
+ $this->inner->set($this->prefix . $key, $value);
+ } else {
+ $this->inner->setEx($this->prefix . $key, $expires, $value);
+ }
+ }
+
+ public function delete(string $key): void {
+ $this->inner->del($this->prefix . $key);
+ }
+
+ public function flush(): void {
+ if (empty($this->prefix)) {
+ $this->inner->flushDB();
+ } else {
+ $this->inner->unlink($this->inner->keys("{$this->prefix}*"));
+ }
+ }
+}
diff --git a/inc/Data/Driver/StderrLogDriver.php b/inc/Data/Driver/StderrLogDriver.php
new file mode 100644
index 00000000..4c766033
--- /dev/null
+++ b/inc/Data/Driver/StderrLogDriver.php
@@ -0,0 +1,27 @@
+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");
+ }
+ }
+}
diff --git a/inc/Data/Driver/SyslogLogDriver.php b/inc/Data/Driver/SyslogLogDriver.php
new file mode 100644
index 00000000..c0df5304
--- /dev/null
+++ b/inc/Data/Driver/SyslogLogDriver.php
@@ -0,0 +1,35 @@
+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);
+ }
+ }
+ }
+}
diff --git a/inc/Data/IpNoteQueries.php b/inc/Data/IpNoteQueries.php
new file mode 100644
index 00000000..ba6fdb15
--- /dev/null
+++ b/inc/Data/IpNoteQueries.php
@@ -0,0 +1,76 @@
+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;
+ }
+}
diff --git a/inc/Data/PageFetchResult.php b/inc/Data/PageFetchResult.php
new file mode 100644
index 00000000..b33e7ac2
--- /dev/null
+++ b/inc/Data/PageFetchResult.php
@@ -0,0 +1,15 @@
+pdo->prepare('DELETE FROM `reports` WHERE `post` = :id AND `board` = :board');
+ $query->bindValue(':id', $post_id, \PDO::PARAM_INT);
+ $query->bindValue(':board', $board);
+ $query->execute();
+ }
+
+ private function joinReportPosts(array $raw_reports, ?int $limit): array {
+ // Group the reports rows by board.
+ $reports_by_boards = [];
+ foreach ($raw_reports as $report) {
+ if (!isset($reports_by_boards[$report['board']])) {
+ $reports_by_boards[$report['board']] = [];
+ }
+ $reports_by_boards[$report['board']][] = $report['post'];
+ }
+
+ // Join the reports with the actual posts.
+ $report_posts = [];
+ foreach ($reports_by_boards as $board => $posts) {
+ $report_posts[$board] = [];
+
+ $query = $this->pdo->prepare(\sprintf('SELECT * FROM `posts_%s` WHERE `id` IN (' . \implode(',', $posts) . ')', $board));
+ $query->execute();
+ while ($post = $query->fetch(\PDO::FETCH_ASSOC)) {
+ $report_posts[$board][$post['id']] = $post;
+ }
+ }
+
+ // Filter out the reports without a valid post.
+ $valid = [];
+ foreach ($raw_reports as $report) {
+ if (isset($report_posts[$report['board']][$report['post']])) {
+ $report['post_data'] = $report_posts[$report['board']][$report['post']];
+ $valid[] = $report;
+
+ if ($limit !== null && \count($valid) >= $limit) {
+ return $valid;
+ }
+ } else {
+ // Invalid report (post has been deleted).
+ if ($this->auto_maintenance != false) {
+ $this->deleteReportImpl($report['board'], $report['post']);
+ }
+ }
+ }
+ return $valid;
+ }
+
+ /**
+ * Filters out the invalid reports.
+ *
+ * @param array $raw_reports Array with the raw fetched reports. Must include a `board`, `post` and `id` fields.
+ * @param bool $get_invalid True to reverse the filter and get the invalid reports instead.
+ * @return array An array of filtered reports.
+ */
+ private function filterReports(array $raw_reports, bool $get_invalid): array {
+ // Group the reports rows by board.
+ $reports_by_boards = [];
+ foreach ($raw_reports as $report) {
+ if (!isset($reports_by_boards[$report['board']])) {
+ $reports_by_boards[$report['board']] = [];
+ }
+ $reports_by_boards[$report['board']][] = $report['post'];
+ }
+
+ // Join the reports with the actual posts.
+ $report_posts = [];
+ foreach ($reports_by_boards as $board => $posts) {
+ $report_posts[$board] = [];
+
+ $query = $this->pdo->prepare(\sprintf('SELECT `id` FROM `posts_%s` WHERE `id` IN (' . \implode(',', $posts) . ')', $board));
+ $query->execute();
+ while ($post = $query->fetch(\PDO::FETCH_ASSOC)) {
+ $report_posts[$board][$post['id']] = $post;
+ }
+ }
+
+ if ($get_invalid) {
+ // Get the reports without a post.
+ $invalid = [];
+ foreach ($raw_reports as $report) {
+ if (isset($report_posts[$report['board']][$report['post']])) {
+ $invalid[] = $report;
+ }
+ }
+ return $invalid;
+ } else {
+ // Filter out the reports without a valid post.
+ $valid = [];
+ foreach ($raw_reports as $report) {
+ if (isset($report_posts[$report['board']][$report['post']])) {
+ $valid[] = $report;
+ } else {
+ // Invalid report (post has been deleted).
+ if ($this->auto_maintenance != false) {
+ $this->deleteReportImpl($report['board'], $report['post']);
+ }
+ }
+ }
+ return $valid;
+ }
+ }
+
+ /**
+ * @param \PDO $pdo PDO connection.
+ * @param bool $auto_maintenance If the auto maintenance should be enabled.
+ */
+ public function __construct(\PDO $pdo, bool $auto_maintenance) {
+ $this->pdo = $pdo;
+ $this->auto_maintenance = $auto_maintenance;
+ }
+
+ /**
+ * Get the number of reports.
+ *
+ * @return int The number of reports.
+ */
+ public function getCount(): int {
+ $query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`');
+ $query->execute();
+ $raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
+ $valid_reports = $this->filterReports($raw_reports, false, null);
+ $count = \count($valid_reports);
+
+ return $count;
+ }
+
+ /**
+ * Get the report with the given id. DOES NOT PERFORM VALIDITY CHECK.
+ *
+ * @param int $id The id of the report to fetch.
+ * @return ?array An array of the given report with the `board` and `ip` fields. Null if no such report exists.
+ */
+ public function getReportById(int $id): ?array {
+ $query = prepare('SELECT `board`, `ip` FROM ``reports`` WHERE `id` = :id');
+ $query->bindValue(':id', $id);
+ $query->execute();
+
+ $ret = $query->fetch(\PDO::FETCH_ASSOC);
+ if ($ret !== false) {
+ return $ret;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get the reports with the associated post data.
+ *
+ * @param int $count The maximum number of rows in the return array.
+ * @return array The reports with the associated post data.
+ */
+ public function getReportsWithPosts(int $count): array {
+ $query = $this->pdo->prepare('SELECT * FROM `reports` ORDER BY `time`');
+ $query->execute();
+ $raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
+ return $this->joinReportPosts($raw_reports, $count);
+ }
+
+ /**
+ * Purge the invalid reports.
+ *
+ * @return int The number of reports deleted.
+ */
+ public function purge(): int {
+ $query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`');
+ $query->execute();
+ $raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC);
+ $invalid_reports = $this->filterReports($raw_reports, true, null);
+
+ foreach ($invalid_reports as $report) {
+ $this->deleteReportImpl($report['board'], $report['post']);
+ }
+ return \count($invalid_reports);
+ }
+
+ /**
+ * Deletes the given report.
+ *
+ * @param int $id The report id.
+ */
+ public function deleteById(int $id) {
+ $query = $this->pdo->prepare('DELETE FROM `reports` WHERE `id` = :id');
+ $query->bindValue(':id', $id, \PDO::PARAM_INT);
+ $query->execute();
+ }
+
+ /**
+ * Deletes all reports from the given ip.
+ *
+ * @param string $ip The reporter ip.
+ */
+ public function deleteByIp(string $ip) {
+ $query = $this->pdo->prepare('DELETE FROM `reports` WHERE `ip` = :ip');
+ $query->bindValue(':ip', $ip);
+ $query->execute();
+ }
+
+ /**
+ * Inserts a new report.
+ *
+ * @param string $ip Ip of the user sending the report.
+ * @param string $board_uri Board uri of the reported thread. MUST ALREADY BE SANITIZED.
+ * @param int $post_id Post reported.
+ * @param string $reason Reason of the report.
+ * @return void
+ */
+ public function add(string $ip, string $board_uri, int $post_id, string $reason) {
+ $query = $this->pdo->prepare('INSERT INTO `reports` VALUES (NULL, :time, :ip, :board, :post, :reason)');
+ $query->bindValue(':time', time(), \PDO::PARAM_INT);
+ $query->bindValue(':ip', $ip);
+ $query->bindValue(':board', $board_uri);
+ $query->bindValue(':post', $post_id, \PDO::PARAM_INT);
+ $query->bindValue(':reason', $reason);
+ $query->execute();
+ }
+}
diff --git a/inc/Data/UserPostQueries.php b/inc/Data/UserPostQueries.php
new file mode 100644
index 00000000..1c203431
--- /dev/null
+++ b/inc/Data/UserPostQueries.php
@@ -0,0 +1,159 @@
+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'");
+ }
+ });
+ }
+}
diff --git a/inc/anti-bot.php b/inc/anti-bot.php
index f21b6d5f..cf82dcc8 100644
--- a/inc/anti-bot.php
+++ b/inc/anti-bot.php
@@ -190,53 +190,62 @@ class AntiBot {
}
}
-function _create_antibot($board, $thread) {
+function _create_antibot($pdo, $board, $thread) {
global $config, $purged_old_antispam;
$antibot = new AntiBot(array($board, $thread));
- // Delete old expired antispam, skipping those with NULL expiration timestamps (infinite lifetime).
- if (!isset($purged_old_antispam) && $config['auto_maintenance']) {
- $purged_old_antispam = true;
- purge_old_antispam();
- }
-
- retry_on_deadlock(4, function() use($thread, $board, $config) {
- // Keep the now invalid timestamps around for a bit to enable users to post if they're still on an old version of
- // the HTML page.
- // By virtue of existing, we know that we're making a new version of the page, and the user from now on may just reload.
- if ($thread) {
- $query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL');
- } else {
- $query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL');
- }
-
- $query->bindValue(':board', $board);
- if ($thread) {
- $query->bindValue(':thread', $thread);
- }
- $query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
- // Throws on error.
- $query->execute();
- });
-
try {
- $hash = $antibot->hash();
+ retry_on_deadlock(3, function() use ($config, $pdo, $thread, $board, $antibot, $purged_old_antispam) {
+ try {
+ $pdo->beginTransaction();
- 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.
- $query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
- $query->bindValue(':board', $board);
- $query->bindValue(':thread', $thread);
- $query->bindValue(':hash', $hash);
- // Throws on error.
- $query->execute();
+ // Delete old expired antispam, skipping those with NULL expiration timestamps (infinite lifetime).
+ if (!isset($purged_old_antispam) && $config['auto_maintenance']) {
+ $purged_old_antispam = true;
+ purge_old_antispam();
+ }
+
+ // Keep the now invalid timestamps around for a bit to enable users to post if they're still on an old version of
+ // the HTML page.
+ // By virtue of existing, we know that we're making a new version of the page, and the user from now on may just reload.
+ if ($thread) {
+ $query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` = :thread AND `expires` IS NULL');
+ } else {
+ $query = prepare('UPDATE ``antispam`` SET `expires` = UNIX_TIMESTAMP() + :expires WHERE `board` = :board AND `thread` IS NULL AND `expires` IS NULL');
+ }
+
+ $query->bindValue(':board', $board);
+ if ($thread) {
+ $query->bindValue(':thread', $thread);
+ }
+ $query->bindValue(':expires', $config['spam']['hidden_inputs_expire']);
+ // Throws on error.
+ $query->execute();
+
+
+ $hash = $antibot->hash();
+
+ // Insert an antispam with infinite life as the HTML page of a thread might last well beyond the expiry date.
+ $query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)');
+ $query->bindValue(':board', $board);
+ $query->bindValue(':thread', $thread);
+ $query->bindValue(':hash', $hash);
+ // Throws on error.
+ $query->execute();
+
+ $pdo->commit();
+ } catch (\Exception $e) {
+ $pdo->rollBack();
+ throw $e;
+ }
});
- } catch(\PDOException $e) {
+ } catch (\PDOException $e) {
+ $pdo->rollBack();
if ($e->errorInfo === null || $e->errorInfo[1] != MYSQL_ER_LOCK_DEADLOCK) {
throw $e;
} else {
- error_log('Multiple deadlocks on _create_antibot while inserting, skipping');
+ \error_log('5 or more deadlocks on _create_antibot while inserting, skipping');
}
}
diff --git a/inc/cache.php b/inc/cache.php
index a8428846..d26f9201 100644
--- a/inc/cache.php
+++ b/inc/cache.php
@@ -4,182 +4,91 @@
* Copyright (c) 2010-2013 Tinyboard Development Group
*/
+use Vichan\Data\Driver\{CacheDriver, ApcuCacheDriver, ArrayCacheDriver, FsCacheDriver, MemcachedCacheDriver, NoneCacheDriver, RedisCacheDriver};
+
defined('TINYBOARD') or exit;
+
class Cache {
- private static $cache;
- public static function init() {
+ private static function buildCache(): CacheDriver {
global $config;
switch ($config['cache']['enabled']) {
case 'memcached':
- self::$cache = new Memcached();
- self::$cache->addServers($config['cache']['memcached']);
- break;
+ return new MemcachedCacheDriver(
+ $config['cache']['prefix'],
+ $config['cache']['memcached']
+ );
case 'redis':
- self::$cache = new Redis();
-
- $ret = explode(':', $config['cache']['redis'][0]);
- if (count($ret) > 0) {
- // Unix socket.
- self::$cache->connect($ret[1]);
- } else {
- // IP + port.
- self::$cache->connect($ret[0], $config['cache']['redis'][1]);
- }
-
- if ($config['cache']['redis'][2]) {
- self::$cache->auth($config['cache']['redis'][2]);
- }
- self::$cache->select($config['cache']['redis'][3]) or die('cache select failure');
- break;
+ $port = $config['cache']['redis'][1];
+ $port = empty($port) ? null : intval($port);
+ return new RedisCacheDriver(
+ $config['cache']['prefix'],
+ $config['cache']['redis'][0],
+ $port,
+ $config['cache']['redis'][2],
+ $config['cache']['redis'][3]
+ );
+ case 'apcu':
+ return new ApcuCacheDriver;
+ case 'fs':
+ return new FsCacheDriver(
+ $config['cache']['prefix'],
+ "tmp/cache/{$config['cache']['prefix']}",
+ '.lock',
+ $config['auto_maintenance'] ? 1000 : false
+ );
+ case 'none':
+ return new NoneCacheDriver();
case 'php':
- self::$cache = array();
- break;
+ default:
+ return new ArrayCacheDriver();
}
}
+
+ public static function getCache(): CacheDriver {
+ static $cache;
+ return $cache ??= self::buildCache();
+ }
+
public static function get($key) {
global $config, $debug;
- $key = $config['cache']['prefix'] . $key;
-
- $data = false;
- switch ($config['cache']['enabled']) {
- case 'memcached':
- if (!self::$cache)
- self::init();
- $data = self::$cache->get($key);
- break;
- case 'apc':
- $data = apc_fetch($key);
- break;
- case 'xcache':
- $data = xcache_get($key);
- break;
- case 'php':
- $data = isset(self::$cache[$key]) ? self::$cache[$key] : false;
- break;
- case 'fs':
- $key = str_replace('/', '::', $key);
- $key = str_replace("\0", '', $key);
- if (!file_exists('tmp/cache/'.$key)) {
- $data = false;
- }
- else {
- $data = file_get_contents('tmp/cache/'.$key);
- $data = json_decode($data, true);
- }
- break;
- case 'redis':
- if (!self::$cache)
- self::init();
- $data = json_decode(self::$cache->get($key), true);
- break;
+ $ret = self::getCache()->get($key);
+ if ($ret === null) {
+ $ret = false;
}
- if ($config['debug'])
- $debug['cached'][] = $key . ($data === false ? ' (miss)' : ' (hit)');
+ if ($config['debug']) {
+ $debug['cached'][] = $config['cache']['prefix'] . $key . ($ret === false ? ' (miss)' : ' (hit)');
+ }
- return $data;
+ return $ret;
}
public static function set($key, $value, $expires = false) {
global $config, $debug;
- $key = $config['cache']['prefix'] . $key;
-
- if (!$expires)
+ if (!$expires) {
$expires = $config['cache']['timeout'];
-
- switch ($config['cache']['enabled']) {
- case 'memcached':
- if (!self::$cache)
- self::init();
- self::$cache->set($key, $value, $expires);
- break;
- case 'redis':
- if (!self::$cache)
- self::init();
- self::$cache->setex($key, $expires, json_encode($value));
- break;
- case 'apc':
- apc_store($key, $value, $expires);
- break;
- case 'xcache':
- xcache_set($key, $value, $expires);
- break;
- case 'fs':
- $key = str_replace('/', '::', $key);
- $key = str_replace("\0", '', $key);
- file_put_contents('tmp/cache/'.$key, json_encode($value));
- break;
- case 'php':
- self::$cache[$key] = $value;
- break;
}
- if ($config['debug'])
- $debug['cached'][] = $key . ' (set)';
+ self::getCache()->set($key, $value, $expires);
+
+ if ($config['debug']) {
+ $debug['cached'][] = $config['cache']['prefix'] . $key . ' (set)';
+ }
}
public static function delete($key) {
global $config, $debug;
- $key = $config['cache']['prefix'] . $key;
+ self::getCache()->delete($key);
- switch ($config['cache']['enabled']) {
- case 'memcached':
- if (!self::$cache)
- self::init();
- self::$cache->delete($key);
- break;
- case 'redis':
- if (!self::$cache)
- self::init();
- self::$cache->del($key);
- break;
- case 'apc':
- apc_delete($key);
- break;
- case 'xcache':
- xcache_unset($key);
- break;
- case 'fs':
- $key = str_replace('/', '::', $key);
- $key = str_replace("\0", '', $key);
- @unlink('tmp/cache/'.$key);
- break;
- case 'php':
- unset(self::$cache[$key]);
- break;
+ if ($config['debug']) {
+ $debug['cached'][] = $config['cache']['prefix'] . $key . ' (deleted)';
}
-
- if ($config['debug'])
- $debug['cached'][] = $key . ' (deleted)';
}
public static function flush() {
- global $config;
-
- switch ($config['cache']['enabled']) {
- case 'memcached':
- if (!self::$cache)
- self::init();
- return self::$cache->flush();
- case 'apc':
- return apc_clear_cache('user');
- case 'php':
- self::$cache = array();
- break;
- case 'fs':
- $files = glob('tmp/cache/*');
- foreach ($files as $file) {
- unlink($file);
- }
- break;
- case 'redis':
- if (!self::$cache)
- self::init();
- return self::$cache->flushDB();
- }
-
+ self::getCache()->flush();
return false;
}
}
diff --git a/inc/config.php b/inc/config.php
index 3a406a44..71b0fbf4 100644
--- a/inc/config.php
+++ b/inc/config.php
@@ -63,9 +63,29 @@
// been generated. This keeps the script from querying the database and causing strain when not needed.
$config['has_installed'] = '.installed';
- // Use syslog() for logging all error messages and unauthorized login attempts.
+ // Deprecated, use 'log_system'.
$config['syslog'] = false;
+ $config['log_system'] = [
+ /*
+ * Log all error messages and unauthorized login attempts.
+ * Can be "syslog", "error_log" (default), "file", or "stderr".
+ */
+ 'type' => 'error_log',
+ // The application name used by the logging system. Defaults to "tinyboard" for backwards compatibility.
+ 'name' => 'tinyboard',
+ /*
+ * Only relevant if 'log_system' is set to "syslog". If true, double print the logs also in stderr. Defaults to
+ * false.
+ */
+ 'syslog_stderr' => false,
+ /*
+ * Only relevant if "log_system" is set to `file`. Sets the file that vichan will log to. Defaults to
+ * '/var/log/vichan.log'.
+ */
+ 'file_path' => '/var/log/vichan.log',
+ ];
+
// Use `host` via shell_exec() to lookup hostnames, avoiding query timeouts. May not work on your system.
// Requires safe_mode to be disabled.
$config['dns_system'] = false;
@@ -117,18 +137,26 @@
/*
* On top of the static file caching system, you can enable the additional caching system which is
- * designed to minimize SQL queries and can significantly increase speed when posting or using the
- * moderator interface. APC is the recommended method of caching.
+ * designed to minimize request processing can significantly increase speed when posting or using
+ * the moderator interface.
*
- * http://tinyboard.org/docs/index.php?p=Config/Cache
+ * https://github.com/vichan-devel/vichan/wiki/cache
*/
+ // Uses a PHP array. MUST NOT be used in multiprocess environments.
$config['cache']['enabled'] = 'php';
- // $config['cache']['enabled'] = 'xcache';
- // $config['cache']['enabled'] = 'apc';
+ // The recommended in-memory method of caching. Requires the extension. Due to how APCu works, this should be
+ // disabled when you run tools from the cli.
+ // $config['cache']['enabled'] = 'apcu';
+ // The Memcache server. Requires the memcached extension, with a final D.
// $config['cache']['enabled'] = 'memcached';
+ // The Redis server. Requires the extension.
// $config['cache']['enabled'] = 'redis';
+ // Use the local cache folder. Slower than native but available out of the box and compatible with multiprocess
+ // environments. You can mount a ram-based filesystem in the cache directory to improve performance.
// $config['cache']['enabled'] = 'fs';
+ // Technically available, offers a no-op fake cache. Don't use this outside of testing or debugging.
+ // $config['cache']['enabled'] = 'none';
// Timeout for cached objects such as posts and HTML.
$config['cache']['timeout'] = 60 * 60 * 48; // 48 hours
@@ -144,7 +172,7 @@
// Redis server to use. Location, port, password, database id.
// Note that Tinyboard may clear the database at times, so you may want to pick a database id just for
// Tinyboard to use.
- $config['cache']['redis'] = array('localhost', 6379, '', 1);
+ $config['cache']['redis'] = [ 'localhost', 6379, null, 1 ];
// EXPERIMENTAL: Should we cache configs? Warning: this changes board behaviour, i'd say, a lot.
// If you have any lambdas/includes present in your config, you should move them to instance-functions.php
@@ -192,6 +220,9 @@
// Used to salt secure tripcodes ("##trip") and poster IDs (if enabled).
$config['secure_trip_salt'] = ')(*&^%$#@!98765432190zyxwvutsrqponmlkjihgfedcba';
+ // Used to salt poster passwords.
+ $config['secure_password_salt'] = 'wKJSb7M5SyzMcFWD2gPO3j2RYUSO9B789!@#$%^&*()';
+
/*
* ====================
* Flood/spam settings
@@ -236,6 +267,9 @@
// To prevent bump atacks; returns the thread to last position after the last post is deleted.
$config['anti_bump_flood'] = false;
+ // Reject thread creation from IPs without any prior post history.
+ $config['op_require_history'] = false;
+
/*
* Introduction to Tinyboard's spam filter:
*
@@ -301,9 +335,8 @@
'lock',
'raw',
'embed',
- 'g-recaptcha-response',
- 'h-captcha-response',
- 'cf-turnstile-response',
+ 'captcha-response',
+ 'captcha-form-id',
'spoiler',
'page',
'file_url',
@@ -330,33 +363,40 @@
'answer' => '4'
);
*/
- /**
- * The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set
- * to 1.
- * Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses.
- */
- $config['dynamic_captcha'] = false;
-
- // Enable reCaptcha to make spam even harder. Rarely necessary.
- $config['recaptcha'] = false;
-
- // Public and private key pair from https://www.google.com/recaptcha/admin/create
- $config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f';
- $config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_';
-
- // Enable hCaptcha.
- $config['hcaptcha'] = false;
-
- // Public and private key pair for using hCaptcha.
- $config['hcaptcha_public'] = '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0';
- $config['hcaptcha_private'] = '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17';
-
- // Enable Cloudflare's Turnstile captcha.
- $config['turnstile'] = false;
-
- // Public and private key pair for turnstile.
- $config['turnstile_public'] = '';
- $config['turnstile_private'] = '';
+ // Enable a captcha system to make spam even harder. Rarely necessary.
+ $config['captcha'] = [
+ /**
+ * Select the captcha backend, false to disable.
+ * Can be false, "recaptcha", "hcaptcha" or "turnstile".
+ */
+ 'mode' => false,
+ /**
+ * The captcha is dynamically injected on the client if the server replies with the `captcha-required` cookie set
+ * to 1.
+ * Use false to disable this configuration, otherwise, set the IP that vichan should check for captcha responses.
+ */
+ 'dynamic' => false,
+ // Require to be non-zero if you use js/ajax.js (preferably no more than a few seconds), otherwise weird errors might occur.
+ 'passthrough_timeout' => 0,
+ // Configure Google reCAPTCHA.
+ 'recaptcha' => [
+ // Public and private key pair from https://www.google.com/recaptcha/admin/create
+ 'public' => '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f',
+ 'private' => '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_',
+ ],
+ // Configure hCaptcha.
+ 'hcaptcha' => [
+ // Public and private key pair for using hCaptcha.
+ 'public' => '7a4b21e0-dc53-46f2-a9f8-91d2e74b63a0',
+ 'private' => '0x4e9A01bE637b51dC41a7Ea9865C3fDe4aB72Cf17',
+ ],
+ // Configure Cloudflare Turnstile.
+ 'turnstile' => [
+ // Public and private key pair for turnstile.
+ 'public' => '',
+ 'private' => '',
+ ]
+ ];
// Ability to lock a board for normal users and still allow mods to post. Could also be useful for making an archive board
$config['board_locked'] = false;
@@ -547,6 +587,10 @@
// Requires $config['strip_combining_chars'] = true;
$config['max_combining_chars'] = 0;
+ // Maximum OP body length. Ignored if force_body_op is set to false.
+ $config['max_body_op'] = 1800;
+ // Minimum OP body length. Ignored if force_body_op is set to false.
+ $config['min_body_op'] = 0;
// Maximum post body length.
$config['max_body'] = 1800;
// Minimum post body length.
@@ -709,18 +753,18 @@
// a link to an email address or IRC chat room to appeal the ban.
$config['ban_page_extra'] = '';
- // Pre-configured ban reasons that pre-fill the ban form when clicked.
- // To disable, set $config['ban_reasons'] = false;
- $config['ban_reasons'] = array(
- array( 'reason' => 'Low-quality posting',
- 'length' => '1d'),
- array( 'reason' => 'Off-topic',
- 'length' => '1d'),
- array( 'reason' => 'Ban evasion',
- 'length' => '30d'),
- array( 'reason' => 'Illegal content',
- 'length' => ''),
- );
+ // Pre-configured ban reasons that pre-fill the ban form when clicked.
+ // To disable, set $config['ban_reasons'] = false;
+ $config['ban_reasons'] = array(
+ array( 'reason' => 'Low-quality posting',
+ 'length' => '1d'),
+ array( 'reason' => 'Off-topic',
+ 'length' => '1d'),
+ array( 'reason' => 'Ban evasion',
+ 'length' => '30d'),
+ array( 'reason' => 'Illegal content',
+ 'length' => ''),
+ );
// How often (minimum) to purge the ban list of expired bans (which have been seen).
$config['purge_bans'] = 60 * 60 * 12; // 12 hours
@@ -899,10 +943,6 @@
// Location of thumbnail to use for deleted images.
$config['image_deleted'] = 'static/deleted.png';
- // When a thumbnailed image is going to be the same (in dimension), just copy the entire file and use
- // that as a thumbnail instead of resizing/redrawing.
- $config['minimum_copy_resize'] = false;
-
// Maximum image upload size in bytes.
$config['max_filesize'] = 10 * 1024 * 1024; // 10MB
// Maximum image dimensions.
@@ -941,15 +981,6 @@
// Set this to true if you're using Linux and you can execute `md5sum` binary.
$config['gnu_md5'] = false;
- // Use Tesseract OCR to retrieve text from images, so you can use it as a spamfilter.
- $config['tesseract_ocr'] = false;
-
- // Tesseract parameters
- $config['tesseract_params'] = '';
-
- // Tesseract preprocess command
- $config['tesseract_preprocess_command'] = 'convert -monochrome %s -';
-
// Number of posts in a "View Last X Posts" page
$config['noko50_count'] = 50;
// Number of posts a thread needs before it gets a "View Last X Posts" page.
@@ -1171,10 +1202,22 @@
// Custom embedding (YouTube, vimeo, etc.)
// It's very important that you match the entire input (with ^ and $) or things will not work correctly.
$config['embedding'] = array(
- array(
- '/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i',
- 'VIDEO '
- ),
+ [
+ '/^(?:(?:https?:)?\/\/)?((?:www|m)\.)?(?:(?:youtube(?:-nocookie)?\.com|youtu\.be))(?:\/(?:[\w\-]+\?v=|embed\/|live\/|v\/)?)([\w\-]{11})((?:\?|\&)\S+)?$/i',
+ '
'
+ ],
+ [
+ '/^https?:\/\/(\w+\.)?youtube\.com\/shorts\/([a-zA-Z0-9\-_]{10,11})(\?.*)?$/i',
+ ''
+ ],
array(
'/^https?:\/\/(\w+\.)?vimeo\.com\/(\d{2,10})(\?.+)?$/i',
''
@@ -1191,10 +1234,18 @@
'/^https?:\/\/video\.google\.com\/videoplay\?docid=(\d+)([](.+)?)?$/i',
' '
),
- array(
+ [
'/^https?:\/\/(\w+\.)?vocaroo\.com\/i\/([a-zA-Z0-9]{2,15})$/i',
- ' '
- )
+ ''
+ ],
+ [
+ '/^https?:\/\/(\w+\.)?voca\.ro\/([a-zA-Z0-9]{2,15})$/i',
+ ''
+ ],
+ [
+ '/^https?:\/\/(\w+\.)?vocaroo\.com\/([a-zA-Z0-9]{2,15})#?$/i',
+ ''
+ ]
);
// Embedding width and height.
@@ -1210,6 +1261,7 @@
// Error messages
$config['error']['bot'] = _('You look like a bot.');
$config['error']['referer'] = _('Your browser sent an invalid or no HTTP referer.');
+ $config['error']['opnohistory'] = _('You must post at least once before creating thread.');
$config['error']['toolong'] = _('The %s field was too long.');
$config['error']['toolong_body'] = _('The body was too long.');
$config['error']['tooshort_body'] = _('The body was too short or empty.');
@@ -1499,8 +1551,8 @@
// 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;
- // How many recent posts, per board, to show in ?/IP/x.x.x.x.
- $config['mod']['ip_recentposts'] = 5;
+ // How many recent posts, per board, to show in ?/user_posts/ip/x.x.x.x. and ?/user_posts/passwd/xxxxxxxx
+ $config['mod']['recent_user_posts'] = 5;
// Number of posts to display on the reports page.
$config['mod']['recent_reports'] = 10;
@@ -1889,22 +1941,24 @@
*/
// Matrix integration for reports
- // $config['matrix'] = array(
- // 'access_token' => 'ACCESS_TOKEN',
- // 'room_id' => '%21askjdlkajsdlka:matrix.org',
- // 'host' => 'https://matrix.org',
- // 'max_message_length' => 240
- // );
+ $config['matrix'] = [
+ 'enabled' => false,
+ 'access_token' => 'ACCESS_TOKEN',
+ // Note: must be already url-escaped.
+ 'room_id' => '%21askjdlkajsdlka:matrix.org',
+ 'host' => 'https://matrix.org',
+ 'max_message_length' => 240
+ ];
- //Securimage captcha
- //Note from lainchan PR: "TODO move a bunch of things here"
+ //Securimage captcha
+ //Note from lainchan PR: "TODO move a bunch of things here"
- $config['spam']['valid_inputs'][]='captcha';
- $config['error']['securimage']=array(
- 'missing'=>'The captcha field was missing. Please try again',
- 'empty'=>'Please fill out the captcha',
- 'bad'=>'Incorrect or expired captcha',
- );
+ $config['spam']['valid_inputs'][]='captcha';
+ $config['error']['securimage']=array(
+ 'missing'=>'The captcha field was missing. Please try again',
+ 'empty'=>'Please fill out the captcha',
+ 'bad'=>'Incorrect or expired captcha',
+ );
// Meta keywords. It's probably best to include these in per-board configurations.
// $config['meta_keywords'] = 'chan,anonymous discussion,imageboard,tinyboard';
@@ -1976,12 +2030,6 @@
// 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}';
- // Youtube.js embed HTML code
- $config['youtube_js_html'] = '';
-
// Slack Report Notification
$config['slack'] = false;
$config['slack_channel'] = "";
diff --git a/inc/context.php b/inc/context.php
new file mode 100644
index 00000000..11a153ec
--- /dev/null
+++ b/inc/context.php
@@ -0,0 +1,82 @@
+definitions = $definitions;
+ }
+
+ public function get(string $name): mixed {
+ if (!isset($this->definitions[$name])) {
+ throw new \RuntimeException("Could not find a dependency named $name");
+ }
+
+ $ret = $this->definitions[$name];
+ if (is_callable($ret) && !is_string($ret) && !is_array($ret)) {
+ $ret = $ret($this);
+ $this->definitions[$name] = $ret;
+ }
+ return $ret;
+ }
+}
+
+function build_context(array $config): Context {
+ return new Context([
+ 'config' => $config,
+ LogDriver::class => function($c) {
+ $config = $c->get('config');
+
+ $name = $config['log_system']['name'];
+ $level = $config['debug'] ? LogDriver::DEBUG : LogDriver::NOTICE;
+ $backend = $config['log_system']['type'];
+
+ $legacy_syslog = isset($config['syslog']) && $config['syslog'];
+
+ // Check 'syslog' for backwards compatibility.
+ if ($legacy_syslog || $backend === 'syslog') {
+ $log_driver = new SyslogLogDriver($name, $level, $config['log_system']['syslog_stderr']);
+ if ($legacy_syslog) {
+ $log_driver->log(LogDriver::NOTICE, 'The configuration setting \'syslog\' is deprecated. Please use \'log_system\' instead');
+ }
+ return $log_driver;
+ } elseif ($backend === 'file') {
+ return new FileLogDriver($name, $level, $config['log_system']['file_path']);
+ } elseif ($backend === 'stderr') {
+ return new StderrLogDriver($name, $level);
+ } elseif ($backend === 'error_log') {
+ return new ErrorLogLogDriver($name, $level);
+ } else {
+ $log_driver = new ErrorLogLogDriver($name, $level);
+ $log_driver->log(LogDriver::ERROR, "Unknown 'log_system' value '$backend', using 'error_log' default");
+ return $log_driver;
+ }
+ },
+ CacheDriver::class => function($c) {
+ // Use the global for backwards compatibility.
+ return \cache::getCache();
+ },
+ \PDO::class => function($c) {
+ global $pdo;
+ // Ensure the PDO is initialized.
+ sql_open();
+ return $pdo;
+ },
+ ReportQueries::class => function($c) {
+ $auto_maintenance = (bool)$c->get('config')['auto_maintenance'];
+ $pdo = $c->get(\PDO::class);
+ return new ReportQueries($pdo, $auto_maintenance);
+ },
+ UserPostQueries::class => function($c) {
+ return new UserPostQueries($c->get(\PDO::class));
+ },
+ IpNoteQueries::class => fn($c) => new IpNoteQueries($c->get(\PDO::class), $c->get(CacheDriver::class)),
+ ]);
+}
diff --git a/inc/database.php b/inc/database.php
index 380a0837..f5ea73bd 100644
--- a/inc/database.php
+++ b/inc/database.php
@@ -72,6 +72,7 @@ function sql_open() {
try {
$options = [
PDO::ATTR_TIMEOUT => $config['db']['timeout'],
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // Set a consistent error mode between PHP versions.
];
if ($config['db']['type'] == "mysql")
@@ -100,12 +101,6 @@ function sql_open() {
}
}
-// 5.6.10 becomes 50610 HACK: hardcoded to be above critical value 50803 due to laziness
-function mysql_version() {
- // TODO delete all references of this function everywhere
- return 80504;
-}
-
function prepare($query) {
global $pdo, $debug, $config;
diff --git a/inc/filters.php b/inc/filters.php
index 2a66cd2a..97cbc524 100644
--- a/inc/filters.php
+++ b/inc/filters.php
@@ -4,23 +4,26 @@
* Copyright (c) 2010-2013 Tinyboard Development Group
*/
+use Vichan\Context;
+use Vichan\Data\IpNoteQueries;
+
defined('TINYBOARD') or exit;
class Filter {
public $flood_check;
private $condition;
private $post;
-
+
public function __construct(array $arr) {
foreach ($arr as $key => $value)
- $this->$key = $value;
+ $this->$key = $value;
}
-
+
public function match($condition, $match) {
$condition = strtolower($condition);
$post = &$this->post;
-
+
switch($condition) {
case 'custom':
if (!is_callable($match))
@@ -29,11 +32,11 @@ class Filter {
case 'flood-match':
if (!is_array($match))
error('Filter condition "flood-match" must be an array.');
-
+
// Filter out "flood" table entries which do not match this filter.
-
+
$flood_check_matched = array();
-
+
foreach ($this->flood_check as $flood_post) {
foreach ($match as $flood_match_arg) {
switch ($flood_match_arg) {
@@ -69,10 +72,10 @@ class Filter {
}
$flood_check_matched[] = $flood_post;
}
-
+
// is there any reason for this assignment?
$this->flood_check = $flood_check_matched;
-
+
return !empty($this->flood_check);
case 'flood-time':
foreach ($this->flood_check as $flood_post) {
@@ -135,46 +138,42 @@ class Filter {
error('Unknown filter condition: ' . $condition);
}
}
-
- public function action() {
+
+ public function action(Context $ctx) {
global $board;
$this->add_note = isset($this->add_note) ? $this->add_note : false;
if ($this->add_note) {
- $query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :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));
- }
+ $note_queries = $ctx->get(IpNoteQueries::class);
+ $note_queries->add($_SERVER['REMOTE_ADDR'], -1, 'Autoban message: ' . $this->post['body']);
+ }
if (isset ($this->action)) switch($this->action) {
case 'reject':
error(isset($this->message) ? $this->message : 'Posting blocked by filter.');
case 'ban':
if (!isset($this->reason))
error('The ban action requires a reason.');
-
+
$this->expires = isset($this->expires) ? $this->expires : false;
$this->reject = isset($this->reject) ? $this->reject : true;
$this->all_boards = isset($this->all_boards) ? $this->all_boards : false;
-
+
Bans::new_ban($_SERVER['REMOTE_ADDR'], $this->reason, $this->expires, $this->all_boards ? false : $board['uri'], -1);
if ($this->reject) {
if (isset($this->message))
error($message);
-
+
checkBan($board['uri']);
exit;
}
-
+
break;
default:
error('Unknown filter action: ' . $this->action);
}
}
-
+
public function check(array $post) {
$this->post = $post;
foreach ($this->condition as $condition => $value) {
@@ -184,7 +183,7 @@ class Filter {
} else {
$NOT = false;
}
-
+
if ($this->match($condition, $value) == $NOT)
return false;
}
@@ -194,11 +193,11 @@ class Filter {
function purge_flood_table() {
global $config;
-
+
// Determine how long we need to keep a cache of posts for flood prevention. Unfortunately, it is not
// aware of flood filters in other board configurations. You can solve this problem by settings the
// config variable $config['flood_cache'] (seconds).
-
+
if (isset($config['flood_cache'])) {
$max_time = &$config['flood_cache'];
} else {
@@ -208,18 +207,18 @@ function purge_flood_table() {
$max_time = max($max_time, $filter['condition']['flood-time']);
}
}
-
+
$time = time() - $max_time;
-
+
query("DELETE FROM ``flood`` WHERE `time` < $time") or error(db_error());
}
-function do_filters(array $post) {
+function do_filters(Context $ctx, array $post) {
global $config;
if (!isset($config['filters']) || empty($config['filters']))
return;
-
+
foreach ($config['filters'] as $filter) {
if (isset($filter['condition']['flood-match'])) {
$has_flood = true;
@@ -232,15 +231,15 @@ function do_filters(array $post) {
} else {
$flood_check = false;
}
-
+
foreach ($config['filters'] as $filter_array) {
$filter = new Filter($filter_array);
$filter->flood_check = $flood_check;
if ($filter->check($post)) {
- $filter->action();
+ $filter->action($ctx);
}
}
-
+
purge_flood_table();
}
diff --git a/inc/functions.php b/inc/functions.php
index 2328c1b7..def00287 100644
--- a/inc/functions.php
+++ b/inc/functions.php
@@ -355,9 +355,12 @@ function define_groups() {
}
function create_antibot($board, $thread = null) {
- require_once dirname(__FILE__) . '/anti-bot.php';
+ global $pdo;
- return _create_antibot($board, $thread);
+ // Ensure $pdo is initialized.
+ sql_open();
+
+ return _create_antibot($pdo, $board, $thread);
}
function rebuildThemes($action, $boardname = false) {
@@ -742,24 +745,23 @@ function hasPermission($action = null, $board = null, $_mod = null) {
function listBoards($just_uri = false) {
global $config;
- $just_uri ? $cache_name = 'all_boards_uri' : $cache_name = 'all_boards';
+ $cache_name = $just_uri ? 'all_boards_uri' : 'all_boards';
- if ($config['cache']['enabled'] && ($boards = cache::get($cache_name)))
+ if ($config['cache']['enabled'] && ($boards = cache::get($cache_name))) {
return $boards;
-
- if (!$just_uri) {
- $query = query("SELECT * FROM ``boards`` ORDER BY `uri`") or error(db_error());
- $boards = $query->fetchAll();
- } else {
- $boards = array();
- $query = query("SELECT `uri` FROM ``boards``") or error(db_error());
- while ($board = $query->fetchColumn()) {
- $boards[] = $board;
- }
}
- if ($config['cache']['enabled'])
+ if (!$just_uri) {
+ $query = query('SELECT * FROM ``boards`` ORDER BY `uri`');
+ $boards = $query->fetchAll();
+ } else {
+ $query = query('SELECT `uri` FROM ``boards``');
+ $boards = $query->fetchAll(\PDO::FETCH_COLUMN);
+ }
+
+ if ($config['cache']['enabled']) {
cache::set($cache_name, $boards);
+ }
return $boards;
}
@@ -918,6 +920,48 @@ function checkBan($board = false) {
}
}
+/**
+ * Checks if the given IP has any previous posts.
+ *
+ * @param string $ip The IP to check.
+ * @param ?string $passwd If not null, check also by password.
+ * @return bool True if the ip has already sent at least one post, false otherwise.
+ */
+function has_any_history(string $ip, ?string $passwd): bool {
+ global $config;
+
+ if ($config['cache']['enabled']) {
+ $ret = cache::get("post_history_$ip");
+ if ($ret !== false) {
+ return $ret !== 0x0;
+ }
+ }
+
+ foreach (listBoards(true) as $board_uri) {
+ if ($passwd === null) {
+ $query = prepare(sprintf('SELECT `id` FROM ``posts_%s`` WHERE `ip` = :ip LIMIT 1', $board_uri));
+ $query->bindValue(':ip', $ip);
+ } else {
+ $query = prepare(sprintf('SELECT `id` FROM ``posts_%s`` WHERE `ip` = :ip OR `password` = :passwd LIMIT 1', $board_uri));
+ $query->bindValue(':ip', $ip);
+ $query->bindValue(':passwd', $passwd);
+ }
+ $query->execute() or error(db_error());
+
+ if ($query->fetchColumn() !== false) {
+ // Found a post.
+ if ($config['cache']['enabled']) {
+ cache::set("post_history_$ip", 0xA);
+ }
+ return true;
+ }
+ }
+ if ($config['cache']['enabled']) {
+ cache::set("post_history_$ip", 0x0);
+ }
+ return false;
+}
+
function threadLocked($id) {
global $board;
@@ -2025,7 +2069,7 @@ function remove_modifiers($body) {
return preg_replace('@(.+?) @usm', '', $body);
}
-function markup(&$body, $track_cites = false, $op = false) {
+function markup(&$body, $track_cites = false) {
global $board, $config, $markup_urls;
$modifiers = extract_modifiers($body);
@@ -2040,9 +2084,6 @@ function markup(&$body, $track_cites = false, $op = false) {
$body = str_replace("\r", '', $body);
$body = utf8tohtml($body);
- if (mysql_version() < 50503)
- $body = mb_encode_numericentity($body, array(0x010000, 0xffffff, 0, 0xffffff), 'UTF-8');
-
if ($config['markup_code']) {
$code_markup = array();
$body = preg_replace_callback($config['markup_code'], function($matches) use (&$code_markup) {
@@ -2127,12 +2168,15 @@ function markup(&$body, $track_cites = false, $op = false) {
link_for(array('id' => $cite, 'thread' => $cited_posts[$cite])) . '#' . $cite . '">' .
'>>' . $cite .
'';
+ } else {
+ $replacement = ">>$cite ";
+ }
- $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]);
+ $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]);
- if ($track_cites && $config['track_cites'])
- $tracked_cites[] = array($board['uri'], $cite);
+ if ($track_cites && $config['track_cites']) {
+ $tracked_cites[] = array($board['uri'], $cite);
}
}
}
@@ -3041,3 +3085,8 @@ function strategy_first($fun, $array) {
return array('defer');
}
}
+
+function hashPassword($password) {
+ global $config;
+ return hash('sha3-256', $password . $config['secure_password_salt']);
+}
diff --git a/inc/functions/net.php b/inc/functions/net.php
index 4319a3f3..963ea524 100644
--- a/inc/functions/net.php
+++ b/inc/functions/net.php
@@ -15,3 +15,63 @@ function is_connection_https(): bool {
function is_connection_secure(): bool {
return is_connection_https() || (!empty($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === '127.0.0.1');
}
+
+/**
+ * Encodes a string into a base64 variant without characters illegal in urls.
+ */
+function base64_url_encode(string $input): string {
+ return str_replace([ '+', '/', '=' ], [ '-', '_', '' ], base64_encode($input));
+}
+
+/**
+ * Decodes a string from a base64 variant without characters illegal in urls.
+ */
+function base64_url_decode(string $input): string {
+ return base64_decode(strtr($input, '-_', '+/'));
+}
+
+/**
+ * Encodes a typed cursor.
+ *
+ * @param string $type The type for the cursor. Only the first character is considered.
+ * @param array $map A map of key-value pairs to encode.
+ * @return string An encoded string that can be sent through urls. Empty if either parameter is empty.
+ */
+function encode_cursor(string $type, array $map): string {
+ if (empty($type) || empty($map)) {
+ return '';
+ }
+
+ $acc = $type[0];
+ foreach ($map as $key => $value) {
+ $acc .= "|$key#$value";
+ }
+ return base64_url_encode($acc);
+}
+
+/**
+ * Decodes a typed cursor.
+ *
+ * @param string $cursor A string emitted by `encode_cursor`.
+ * @return array An array with the type of the cursor and an array of key-value pairs. The type is null and the map
+ * empty if either there are no key-value pairs or the encoding is incorrect.
+ */
+function decode_cursor(string $cursor): array {
+ $map = [];
+ $type = '';
+ $acc = base64_url_decode($cursor);
+ if ($acc === false || empty($acc)) {
+ return [ null, [] ];
+ }
+
+ $type = $acc[0];
+ foreach (explode('|', substr($acc, 2)) as $pair) {
+ $pair = explode('#', $pair);
+ if (count($pair) >= 2) {
+ $key = $pair[0];
+ $value = $pair[1];
+ $map[$key] = $value;
+ }
+ }
+ return [ $type, $map ];
+}
diff --git a/inc/lib/IP/LICENSE b/inc/lib/IP/LICENSE
deleted file mode 100755
index fb315548..00000000
--- a/inc/lib/IP/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-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.
-
diff --git a/inc/lib/IP/Lifo/IP/BC.php b/inc/lib/IP/Lifo/IP/BC.php
deleted file mode 100755
index 26a2c2b7..00000000
--- a/inc/lib/IP/Lifo/IP/BC.php
+++ /dev/null
@@ -1,293 +0,0 @@
-
- *
- * 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;
- }
-
-}
diff --git a/inc/lib/IP/Lifo/IP/CIDR.php b/inc/lib/IP/Lifo/IP/CIDR.php
deleted file mode 100755
index e8fe32ce..00000000
--- a/inc/lib/IP/Lifo/IP/CIDR.php
+++ /dev/null
@@ -1,706 +0,0 @@
-
- *
- * 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();
- }
-}
diff --git a/inc/lib/IP/Lifo/IP/IP.php b/inc/lib/IP/Lifo/IP/IP.php
deleted file mode 100755
index 4d22aa76..00000000
--- a/inc/lib/IP/Lifo/IP/IP.php
+++ /dev/null
@@ -1,207 +0,0 @@
-
- *
- * 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);
- }
-}
diff --git a/inc/lib/twig/extensions/Extension/Tinyboard.php b/inc/lib/twig/extensions/Extension/Tinyboard.php
index 97fecb20..5fb99b11 100644
--- a/inc/lib/twig/extensions/Extension/Tinyboard.php
+++ b/inc/lib/twig/extensions/Extension/Tinyboard.php
@@ -32,7 +32,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension
new Twig_SimpleFilter('addslashes', 'addslashes'),
);
}
-
+
/**
* Returns a list of functions to add to the existing list.
*
@@ -52,7 +52,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension
new Twig_SimpleFunction('link_for', 'link_for')
);
}
-
+
/**
* Returns the name of the extension.
*
@@ -88,7 +88,7 @@ function twig_hasPermission_filter($mod, $permission, $board = null) {
function twig_extension_filter($value, $case_insensitive = true) {
$ext = mb_substr($value, mb_strrpos($value, '.') + 1);
if($case_insensitive)
- $ext = mb_strtolower($ext);
+ $ext = mb_strtolower($ext);
return $ext;
}
@@ -113,7 +113,7 @@ function twig_filename_truncate_filter($value, $length = 30, $separator = '…')
$value = strrev($value);
$array = array_reverse(explode(".", $value, 2));
$array = array_map("strrev", $array);
-
+
$filename = &$array[0];
$extension = isset($array[1]) ? $array[1] : false;
@@ -127,11 +127,11 @@ function twig_filename_truncate_filter($value, $length = 30, $separator = '…')
function twig_ratio_function($w, $h) {
return fraction($w, $h, ':');
}
-function twig_secure_link_confirm($text, $title, $confirm_message, $href) {
- global $config;
+function twig_secure_link_confirm($text, $title, $confirm_message, $href) {
return '' . $text . ' ';
}
+
function twig_secure_link($href) {
return $href . '/' . make_secure_link_token($href);
}
diff --git a/inc/mod/auth.php b/inc/mod/auth.php
index 958b7ba5..01b234a1 100644
--- a/inc/mod/auth.php
+++ b/inc/mod/auth.php
@@ -4,6 +4,7 @@
* Copyright (c) 2010-2013 Tinyboard Development Group
*/
+use Vichan\Context;
use Vichan\Functions\Net;
defined('TINYBOARD') or exit;
@@ -177,7 +178,7 @@ function make_secure_link_token($uri) {
return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8);
}
-function check_login($prompt = false) {
+function check_login(Context $ctx, $prompt = false) {
global $config, $mod;
// Validate session
@@ -187,7 +188,7 @@ function check_login($prompt = false) {
if (count($cookie) != 3) {
// Malformed cookies
destroyCookies();
- if ($prompt) mod_login();
+ if ($prompt) mod_login($ctx);
exit;
}
@@ -200,7 +201,7 @@ function check_login($prompt = false) {
if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) {
// Malformed cookies
destroyCookies();
- if ($prompt) mod_login();
+ if ($prompt) mod_login($ctx);
exit;
}
diff --git a/inc/mod/pages.php b/inc/mod/pages.php
index 1bbe60d0..155c19a6 100644
--- a/inc/mod/pages.php
+++ b/inc/mod/pages.php
@@ -1,19 +1,22 @@
get(LogDriver::class)->log(LogDriver::NOTICE, "Failed to link() $target to $link. FAlling back to copy()");
+ return \copy($target, $link);
+ }
+ return true;
+ };
}
function mod_page($title, $template, $args, $subtitle = false) {
@@ -42,7 +45,7 @@ function clone_wrapped_with_exist_check($clonefn, $src, $dest) {
}
}
-function mod_login($redirect = false) {
+function mod_login(Context $ctx, $redirect = false) {
global $config;
$args = [];
@@ -54,8 +57,7 @@ function mod_login($redirect = false) {
if (!isset($_POST['username'], $_POST['password']) || $_POST['username'] == '' || $_POST['password'] == '') {
$args['error'] = $config['error']['invalid'];
} elseif (!login($_POST['username'], $_POST['password'])) {
- if ($config['syslog'])
- _syslog(LOG_WARNING, 'Unauthorized login attempt!');
+ $ctx->get(LogDriver::class)->log(LogDriver::INFO, 'Unauthorized login attempt!');
$args['error'] = $config['error']['invalid'];
} else {
@@ -78,25 +80,27 @@ function mod_login($redirect = false) {
mod_page(_('Login'), 'mod/login.html', $args);
}
-function mod_confirm($request) {
- $args = array('request' => $request, 'token' => make_secure_link_token($request));
+function mod_confirm(Context $ctx, $request) {
+ $args = [ 'request' => $request, 'token' => make_secure_link_token($request) ];
if(isset($_GET['thread'])) {
$args['rest'] = 'thread=' . $_GET['thread'];
}
mod_page(_('Confirm action'), 'mod/confirm.html', $args);
}
-function mod_logout() {
+function mod_logout(Context $ctx) {
global $config;
destroyCookies();
header('Location: ?/', true, $config['redirect_http']);
}
-function mod_dashboard() {
+function mod_dashboard(Context $ctx) {
global $config, $mod;
- $args = array();
+ $report_queries = $ctx->get(ReportQueries::class);
+
+ $args = [];
$args['boards'] = listBoards();
@@ -122,8 +126,7 @@ function mod_dashboard() {
cache::set('pm_unreadcount_' . $mod['id'], $args['unread_pms']);
}
- $query = query('SELECT COUNT(*) FROM ``reports``') or error(db_error($query));
- $args['reports'] = $query->fetchColumn();
+ $args['reports'] = $report_queries->getCount();
$query = query('SELECT COUNT(*) FROM ``ban_appeals`` WHERE denied = 0') or error(db_error($query));
$args['appeals'] = $query->fetchColumn();
@@ -187,7 +190,7 @@ function mod_dashboard() {
mod_page(_('Dashboard'), 'mod/dashboard.html', $args);
}
-function mod_search_redirect() {
+function mod_search_redirect(Context $ctx) {
global $config;
if (!hasPermission($config['mod']['search']))
@@ -210,7 +213,7 @@ function mod_search_redirect() {
}
}
-function mod_search($type, $search_query_escaped, $page_no = 1) {
+function mod_search(Context $ctx, $type, $search_query_escaped, $page_no = 1) {
global $pdo, $config;
if (!hasPermission($config['mod']['search']))
@@ -236,7 +239,7 @@ function mod_search($type, $search_query_escaped, $page_no = 1) {
$query = str_replace('`', '!`', $query);
// Array of phrases to match
- $match = array();
+ $match = [];
// Exact phrases ("like this")
if (preg_match_all('/"(.+?)"/', $query, $exact_phrases)) {
@@ -365,7 +368,7 @@ function mod_search($type, $search_query_escaped, $page_no = 1) {
));
}
-function mod_edit_board($boardName) {
+function mod_edit_board(Context $ctx, $boardName) {
global $board, $config;
if (!openBoard($boardName))
@@ -467,7 +470,7 @@ function mod_edit_board($boardName) {
}
}
-function mod_new_board() {
+function mod_new_board(Context $ctx) {
global $config, $board;
if (!hasPermission($config['mod']['newboard']))
@@ -517,9 +520,6 @@ function mod_new_board() {
$query = Element('posts.sql', array('board' => $board['uri']));
- if (mysql_version() < 50503)
- $query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query);
-
query($query) or error(db_error());
if ($config['cache']['enabled'])
@@ -536,7 +536,7 @@ function mod_new_board() {
mod_page(_('New board'), 'mod/board.html', array('new' => true, 'token' => make_secure_link_token('new-board')));
}
-function mod_noticeboard($page_no = 1) {
+function mod_noticeboard(Context $ctx, $page_no = 1) {
global $config, $pdo, $mod;
if ($page_no < 1)
@@ -591,7 +591,7 @@ function mod_noticeboard($page_no = 1) {
));
}
-function mod_noticeboard_delete($id) {
+function mod_noticeboard_delete(Context $ctx, $id) {
global $config;
if (!hasPermission($config['mod']['noticeboard_delete']))
@@ -609,7 +609,7 @@ function mod_noticeboard_delete($id) {
header('Location: ?/noticeboard', true, $config['redirect_http']);
}
-function mod_news($page_no = 1) {
+function mod_news(Context $ctx, $page_no = 1) {
global $config, $pdo, $mod;
if ($page_no < 1)
@@ -656,7 +656,7 @@ function mod_news($page_no = 1) {
mod_page(_('News'), 'mod/news.html', array('news' => $news, 'count' => $count, 'token' => make_secure_link_token('edit_news')));
}
-function mod_news_delete($id) {
+function mod_news_delete(Context $ctx, $id) {
global $config;
if (!hasPermission($config['mod']['news_delete']))
@@ -671,7 +671,7 @@ function mod_news_delete($id) {
header('Location: ?/edit_news', true, $config['redirect_http']);
}
-function mod_log($page_no = 1) {
+function mod_log(Context $ctx, $page_no = 1) {
global $config;
if ($page_no < 1)
@@ -696,7 +696,7 @@ function mod_log($page_no = 1) {
mod_page(_('Moderation log'), 'mod/log.html', array('logs' => $logs, 'count' => $count));
}
-function mod_user_log($username, $page_no = 1) {
+function mod_user_log(Context $ctx, $username, $page_no = 1) {
global $config;
if ($page_no < 1)
@@ -733,7 +733,7 @@ function protect_ip($entry) {
return preg_replace(array($ipv4_link_regex, $ipv6_link_regex), "xxxx", $entry);
}
-function mod_board_log($board, $page_no = 1, $hide_names = false, $public = false) {
+function mod_board_log(Context $ctx, $board, $page_no = 1, $hide_names = false, $public = false) {
global $config;
if ($page_no < 1)
@@ -766,10 +766,13 @@ function mod_board_log($board, $page_no = 1, $hide_names = false, $public = fals
mod_page(_('Board log'), 'mod/log.html', array('logs' => $logs, 'count' => $count, 'board' => $board, 'hide_names' => $hide_names, 'public' => $public));
}
-function mod_view_catalog($boardName) {
- global $config, $mod;
+function mod_view_catalog(Context $ctx, $boardName) {
+ global $mod;
+
+ $config = $ctx->get('config');
+
require_once($config['dir']['themes'].'/catalog/theme.php');
- $settings = array();
+ $settings = [];
$settings['boards'] = $boardName;
$settings['update_on_posts'] = true;
$settings['title'] = 'Catalog';
@@ -798,7 +801,7 @@ function mod_view_catalog($boardName) {
}
}
-function mod_view_board($boardName, $page_no = 1) {
+function mod_view_board(Context $ctx, $boardName, $page_no = 1) {
global $config, $mod;
if (!openBoard($boardName)){
@@ -829,7 +832,7 @@ function mod_view_board($boardName, $page_no = 1) {
echo Element('index.html', $page);
}
-function mod_view_thread($boardName, $thread) {
+function mod_view_thread(Context $ctx, $boardName, $thread) {
global $config, $mod;
if (!openBoard($boardName))
@@ -839,7 +842,7 @@ function mod_view_thread($boardName, $thread) {
echo $page;
}
-function mod_view_thread50($boardName, $thread) {
+function mod_view_thread50(Context $ctx, $boardName, $thread) {
global $config, $mod;
if (!openBoard($boardName))
@@ -849,111 +852,119 @@ function mod_view_thread50($boardName, $thread) {
echo $page;
}
-function mod_ip_remove_note($ip, $id) {
- global $config, $mod;
+function mod_ip_remove_note(Context $ctx, $ip, $id) {
+ $config = $ctx->get('config');
- if (!hasPermission($config['mod']['remove_notes']))
- error($config['error']['noaccess']);
+ if (!hasPermission($config['mod']['remove_notes'])) {
+ error($config['error']['noaccess']);
+ }
- if (filter_var($ip, FILTER_VALIDATE_IP) === false)
- error("Invalid IP address.");
+ 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));
+ if (!is_numeric($id)) {
+ error('Invalid note ID');
+ }
- modLog("Removed a note for {$ip} ");
+ $queries = $ctx->get(IpNoteQueries::class);
+ $deleted = $queries->deleteWhereIp((int)$id, $ip);
- header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']);
+ if (!$deleted) {
+ error("Note $id does not exist for $ip");
+ }
+
+ modLog("Removed a note for {$ip} ");
+
+ \header("Location: ?/user_posts/ip/$ip#notes", true, $config['redirect_http']);
}
-function mod_page_ip($ip) {
- global $config, $mod;
+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 (filter_var($ip, FILTER_VALIDATE_IP) === false) {
+ error('Invalid IP address');
+ }
if (isset($_POST['ban_id'], $_POST['unban'])) {
- if (!hasPermission($config['mod']['unban']))
+ if (!hasPermission($config['mod']['unban'])) {
error($config['error']['noaccess']);
+ }
Bans::delete($_POST['ban_id'], true, $mod['boards']);
- header('Location: ?/IP/' . $ip . '#bans', true, $config['redirect_http']);
+ if (empty($encoded_cursor)) {
+ \header("Location: ?/user_posts/ip/$ip#bans", true, $config['redirect_http']);
+ } else {
+ \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#bans", true, $config['redirect_http']);
+ }
return;
}
if (isset($_POST['note'])) {
- if (!hasPermission($config['mod']['create_notes']))
+ if (!hasPermission($config['mod']['create_notes'])) {
error($config['error']['noaccess']);
+ }
$_POST['note'] = escape_markup_modifiers($_POST['note']);
markup($_POST['note']);
- $query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
- $query->bindValue(':ip', $ip);
- $query->bindValue(':mod', $mod['id']);
- $query->bindValue(':time', time());
- $query->bindValue(':body', $_POST['note']);
- $query->execute() or error(db_error($query));
+
+ $note_queries = $ctx->get(IpNoteQueries::class);
+ $note_queries->add($ip, $mod['id'], $_POST['note']);
Cache::delete("mod_page_ip_view_notes_$ip");
- modLog("Added a note for {$ip} ");
+ modLog("Added a note for {$ip} ");
- header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']);
+ if (empty($encoded_cursor)) {
+ \header("Location: ?/user_posts/ip/$ip#notes", true, $config['redirect_http']);
+ } else {
+ \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#notes", true, $config['redirect_http']);
+ }
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 = [
'ip' => $ip,
'posts' => []
];
- if ($config['mod']['dns_lookup'])
- $args['hostname'] = rDNS($ip);
-
- $boards = listBoards();
- foreach ($boards as $board) {
- openBoard($board['uri']);
- if (!hasPermission($config['mod']['show_ip'], $board['uri']))
- continue;
- $query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $board['uri']));
- $query->bindValue(':ip', $ip);
- $query->bindValue(':limit', $config['mod']['ip_recentposts'], PDO::PARAM_INT);
- $query->execute() or error(db_error($query));
-
- while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
- if (!$post['thread']) {
- $po = new Thread($post, '?/', $mod, false);
- } else {
- $po = new Post($post, '?/', $mod);
- }
-
- if (!isset($args['posts'][$board['uri']])) {
- $args['posts'][$board['uri']] = [ 'board' => $board, 'posts' => [] ];
- }
- $args['posts'][$board['uri']]['posts'][] = $po->build(true);
- }
+ if (isset($config['mod']['ip_recentposts'])) {
+ // TODO log to migrate.
+ $page_size = $config['mod']['ip_recentposts'];
+ } else {
+ $page_size = $config['mod']['recent_user_posts'];
}
- $args['boards'] = $boards;
- $args['token'] = make_secure_link_token('ban');
+ if ($config['mod']['dns_lookup']) {
+ $args['hostname'] = rDNS($ip);
+ }
if (hasPermission($config['mod']['view_ban'])) {
$args['bans'] = Bans::find($ip, false, true, $config['auto_maintenance']);
}
if (hasPermission($config['mod']['view_notes'])) {
- $ret = Cache::get("mod_page_ip_view_notes_$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;
+ $note_queries = $ctx->get(IpNoteQueries::class);
+ $args['notes'] = $note_queries->getByIp($ip);
}
if (hasPermission($config['mod']['modlog_ip'])) {
@@ -970,12 +981,120 @@ function mod_page_ip($ip) {
$args['logs'] = [];
}
- $args['security_token'] = make_secure_link_token('IP/' . $ip);
+ $boards = listBoards();
- mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']);
+ $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)) {
+ $args['security_token'] = make_secure_link_token("IP/$ip");
+ } else {
+ $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']);
}
-function mod_ban() {
+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) {
global $config;
if (!hasPermission($config['mod']['ban']))
@@ -996,7 +1115,7 @@ function mod_ban() {
header('Location: ?/', true, $config['redirect_http']);
}
-function mod_warning() {
+function mod_warning(Context $ctx) {
global $config;
if (!hasPermission($config['mod']['warning']))
@@ -1013,7 +1132,7 @@ function mod_warning() {
header('Location: ?/', true, $config['redirect_http']);
}
-function mod_bans() {
+function mod_bans(Context $ctx) {
global $config;
global $mod;
@@ -1024,7 +1143,7 @@ function mod_bans() {
if (!hasPermission($config['mod']['unban']))
error($config['error']['noaccess']);
- $unban = array();
+ $unban = [];
foreach ($_POST as $name => $unused) {
if (preg_match('/^ban_(\d+)$/', $name, $match))
$unban[] = $match[1];
@@ -1048,7 +1167,7 @@ function mod_bans() {
));
}
-function mod_bans_json() {
+function mod_bans_json(Context $ctx) {
global $config, $mod;
if (!hasPermission($config['mod']['ban']))
@@ -1060,7 +1179,7 @@ function mod_bans_json() {
Bans::stream_json(false, false, !hasPermission($config['mod']['view_banstaff']), $mod['boards']);
}
-function mod_ban_appeals() {
+function mod_ban_appeals(Context $ctx) {
global $config, $board;
if (!hasPermission($config['mod']['view_ban_appeals']))
@@ -1113,16 +1232,16 @@ function mod_ban_appeals() {
$query = query(sprintf("SELECT `num_files`, `files` FROM ``posts_%s`` WHERE `id` = " .
(int)$ban['post']['id'], $board['uri']));
if ($_post = $query->fetch(PDO::FETCH_ASSOC)) {
- $_post['files'] = $_post['files'] ? json_decode($_post['files']) : array();
+ $_post['files'] = $_post['files'] ? json_decode($_post['files']) : [];
$ban['post'] = array_merge($ban['post'], $_post);
} else {
- $ban['post']['files'] = array(array());
+ $ban['post']['files'] = array([]);
$ban['post']['files'][0]['file'] = 'deleted';
$ban['post']['files'][0]['thumb'] = false;
$ban['post']['num_files'] = 1;
}
} else {
- $ban['post']['files'] = array(array());
+ $ban['post']['files'] = array([]);
$ban['post']['files'][0]['file'] = 'deleted';
$ban['post']['files'][0]['thumb'] = false;
$ban['post']['num_files'] = 1;
@@ -1142,7 +1261,7 @@ function mod_ban_appeals() {
));
}
-function mod_lock($board, $unlock, $post) {
+function mod_lock(Context $ctx, $board, $unlock, $post) {
global $config;
if (!openBoard($board))
@@ -1178,7 +1297,7 @@ function mod_lock($board, $unlock, $post) {
event('lock', $post);
}
-function mod_sticky($board, $unsticky, $post) {
+function mod_sticky(Context $ctx, $board, $unsticky, $post) {
global $config;
if (!openBoard($board))
@@ -1202,7 +1321,7 @@ function mod_sticky($board, $unsticky, $post) {
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
-function mod_cycle($board, $uncycle, $post) {
+function mod_cycle(Context $ctx, $board, $uncycle, $post) {
global $config;
if (!openBoard($board))
@@ -1224,7 +1343,7 @@ function mod_cycle($board, $uncycle, $post) {
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
-function mod_bumplock($board, $unbumplock, $post) {
+function mod_bumplock(Context $ctx, $board, $unbumplock, $post) {
global $config;
if (!openBoard($board))
@@ -1246,8 +1365,8 @@ function mod_bumplock($board, $unbumplock, $post) {
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
-function mod_move_reply($originBoard, $postID) {
- global $board, $config, $mod;
+function mod_move_reply(Context $ctx, $originBoard, $postID) {
+ global $board, $config;
if (!openBoard($originBoard))
error($config['error']['noboard']);
@@ -1350,8 +1469,8 @@ function mod_move_reply($originBoard, $postID) {
}
-function mod_move($originBoard, $postID) {
- global $board, $config, $mod, $pdo;
+function mod_move(Context $ctx, $originBoard, $postID) {
+ global $board, $config, $pdo;
if (!openBoard($originBoard))
error($config['error']['noboard']);
@@ -1372,8 +1491,9 @@ function mod_move($originBoard, $postID) {
if ($targetBoard === $originBoard)
error(_('Target and source board are the same.'));
+ $_link_or_copy = _link_or_copy_factory($ctx);
// link() if leaving a shadow thread behind; else, rename().
- $clone = $shadow ? '_link_or_copy' : 'rename';
+ $clone = $shadow ? $_link_or_copy : 'rename';
// indicate that the post is a thread
$post['op'] = true;
@@ -1420,7 +1540,7 @@ function mod_move($originBoard, $postID) {
$query->bindValue(':id', $postID, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
- $replies = array();
+ $replies = [];
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
$post['mod'] = true;
@@ -1487,7 +1607,7 @@ function mod_move($originBoard, $postID) {
if (!empty($post['tracked_cites'])) {
- $insert_rows = array();
+ $insert_rows = [];
foreach ($post['tracked_cites'] as $cite) {
$insert_rows[] = '(' .
$pdo->quote($board['uri']) . ', ' . $newPostID . ', ' .
@@ -1563,8 +1683,8 @@ function mod_move($originBoard, $postID) {
mod_page(_('Move thread'), 'mod/move.html', array('post' => $postID, 'board' => $originBoard, 'boards' => $boards, 'token' => $security_token));
}
-function mod_merge($originBoard, $postID) {
- global $board, $config, $mod, $pdo;
+function mod_merge(Context $ctx, $originBoard, $postID) {
+ global $board, $config, $pdo;
if (!openBoard($originBoard))
error($config['error']['noboard']);
@@ -1573,10 +1693,11 @@ function mod_merge($originBoard, $postID) {
error($config['error']['noaccess']);
$query = prepare(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL', $originBoard));
- $query->bindValue(':id', $postID);
+ $query->bindValue(':id', (int)$postID, \PDO::PARAM_INT);
$query->execute() or error(db_error($query));
- if (!$post = $query->fetch(PDO::FETCH_ASSOC))
+ if (!$post = $query->fetch(PDO::FETCH_ASSOC)) {
error($config['error']['404']);
+ }
$sourceOp = "";
if ($post['thread']){
$sourceOp = $post['thread'];
@@ -1666,7 +1787,8 @@ function mod_merge($originBoard, $postID) {
$op = $post;
$op['id'] = $newID;
- $clone = $shadow ? '_link_or_copy' : 'rename';
+ $_link_or_copy = _link_or_copy_factory($ctx);
+ $clone = $shadow ? $_link_or_copy : 'rename';
if ($post['has_file']) {
// copy image
@@ -1685,7 +1807,7 @@ function mod_merge($originBoard, $postID) {
$query->bindValue(':id', $postID, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
- $replies = array();
+ $replies = [];
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
$post['mod'] = true;
@@ -1748,7 +1870,7 @@ function mod_merge($originBoard, $postID) {
if (!empty($post['tracked_cites'])) {
- $insert_rows = array();
+ $insert_rows = [];
foreach ($post['tracked_cites'] as $cite) {
$insert_rows[] = '(' .
$pdo->quote($board['uri']) . ', ' . $newPostID . ', ' .
@@ -1804,7 +1926,7 @@ function mod_merge($originBoard, $postID) {
mod_page(_('Merge thread'), 'mod/merge.html', array('post' => $postID, 'board' => $originBoard, 'boards' => $boards, 'token' => $security_token));
}
-function mod_ban_post($board, $delete, $post, $token = false) {
+function mod_ban_post(Context $ctx, $board, $delete, $post, $token = false) {
global $config, $mod;
if (!openBoard($board))
@@ -1869,7 +1991,7 @@ function mod_ban_post($board, $delete, $post, $token = false) {
$name = $mypost["name"];
$subject = $mypost["subject"];
$filehash = $mypost["filehash"];
- $mypost['files'] = $mypost['files'] ? json_decode($mypost['files']) : array();
+ $mypost['files'] = $mypost['files'] ? json_decode($mypost['files']) : [];
// For each file append file name
for ($file_count = 0; $file_count < $mypost["num_files"];$file_count++){
$filename .= $mypost['files'][$file_count]->name . "\r\n";
@@ -1882,14 +2004,10 @@ function mod_ban_post($board, $delete, $post, $token = false) {
$autotag .= "/${board}/" . " " . $filehash . " " . $filename ."\r\n";
$autotag .= $body . "\r\n";
$autotag = escape_markup_modifiers($autotag);
- markup($autotag);
- $query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
- $query->bindValue(':ip', $ip);
- $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 {$ip} ");
+
+ $note_queries = $ctx->get(IpNoteQueries::class);
+ $note_queries->add($ip, $mod['id'], $autotag);
+ modLog("Added a note for {$ip} ");
}
}
deletePost($post);
@@ -1916,7 +2034,7 @@ function mod_ban_post($board, $delete, $post, $token = false) {
'board' => $board,
'delete' => (bool)$delete,
'boards' => listBoards(),
- 'reasons' => $config['ban_reasons'],
+ 'reasons' => $config['ban_reasons'],
'token' => $security_token
);
@@ -1927,7 +2045,7 @@ function mod_ban_post($board, $delete, $post, $token = false) {
mod_page(_('New ban'), 'mod/ban_form.html', $args);
}
-function mod_warning_post($board,$post, $token = false) {
+function mod_warning_post(Context $ctx, $board, $post, $token = false) {
global $config, $mod;
if (!openBoard($board))
@@ -1980,7 +2098,7 @@ function mod_warning_post($board,$post, $token = false) {
$name = $mypost["name"];
$subject = $mypost["subject"];
$filehash = $mypost["filehash"];
- $mypost['files'] = $mypost['files'] ? json_decode($mypost['files']) : array();
+ $mypost['files'] = $mypost['files'] ? json_decode($mypost['files']) : [];
// For each file append file name
for ($file_count = 0; $file_count < $mypost["num_files"];$file_count++){
$filename .= $mypost['files'][$file_count]->name . "\r\n";
@@ -1994,13 +2112,10 @@ function mod_warning_post($board,$post, $token = false) {
$autotag .= $body . "\r\n";
$autotag = escape_markup_modifiers($autotag);
markup($autotag);
- $query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
- $query->bindValue(':ip', $ip);
- $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 {$ip} ");
+
+ $note_queries = $ctx->get(IpNoteQueries::class);
+ $note_queries->add($ip, $mod['id'], $autotag);
+ modLog("Added a note for {$ip} ");
}
}
}
@@ -2027,8 +2142,8 @@ function mod_warning_post($board,$post, $token = false) {
mod_page(_('New warning'), 'mod/warning_form.html', $args);
}
-function mod_edit_post($board, $edit_raw_html, $postID) {
- global $config, $mod;
+function mod_edit_post(Context $ctx, $board, $edit_raw_html, $postID) {
+ global $config;
if (!openBoard($board))
error($config['error']['noboard']);
@@ -2104,8 +2219,8 @@ function mod_edit_post($board, $edit_raw_html, $postID) {
}
}
-function mod_delete($board, $post) {
- global $config, $mod;
+function mod_delete(Context $ctx, $board, $post) {
+ global $config, $mod;
if (!openBoard($board))
error($config['error']['noboard']);
@@ -2132,7 +2247,7 @@ function mod_delete($board, $post) {
$name = $mypost["name"];
$subject = $mypost["subject"];
$filehash = $mypost["filehash"];
- $mypost['files'] = $mypost['files'] ? json_decode($mypost['files']) : array();
+ $mypost['files'] = $mypost['files'] ? json_decode($mypost['files']) : [];
// For each file append file name
for ($file_count = 0; $file_count < $mypost["num_files"];$file_count++){
$filename .= $mypost['files'][$file_count]->name . "\r\n";
@@ -2146,13 +2261,10 @@ function mod_delete($board, $post) {
$autotag .= $body . "\r\n";
$autotag = escape_markup_modifiers($autotag);
markup($autotag);
- $query = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
- $query->bindValue(':ip', $ip);
- $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 {$ip} ");
+
+ $note_queries = $ctx->get(IpNoteQueries::class);
+ $note_queries->add($ip, $mod['id'], $autotag);
+ modLog("Added a note for {$ip} ");
}
}
deletePost($post);
@@ -2170,8 +2282,8 @@ function mod_delete($board, $post) {
}
}
-function mod_deletefile($board, $post, $file) {
- global $config, $mod;
+function mod_deletefile(Context $ctx, $board, $post, $file) {
+ global $config;
if (!openBoard($board))
error($config['error']['noboard']);
@@ -2193,8 +2305,8 @@ function mod_deletefile($board, $post, $file) {
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
-function mod_spoiler_image($board, $post, $file) {
- global $config, $mod;
+function mod_spoiler_image(Context $ctx, $board, $post, $file) {
+ global $config;
if (!openBoard($board))
error($config['error']['noboard']);
@@ -2238,8 +2350,8 @@ function mod_spoiler_image($board, $post, $file) {
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
-function mod_deletebyip($boardName, $post, $global = false) {
- global $config, $mod, $board;
+function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) {
+ global $config, $board, $mod;
$global = (bool)$global;
@@ -2279,9 +2391,9 @@ function mod_deletebyip($boardName, $post, $global = false) {
@set_time_limit($config['mod']['rebuild_timelimit']);
- $boards_to_rebuild = array();
- $threads_to_rebuild = array();
- $threads_deleted = array();
+ $boards_to_rebuild = [];
+ $threads_to_rebuild = [];
+ $threads_deleted = [];
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
$boards_to_rebuild[$post['board']] = true;
openBoard($post['board']);
@@ -2303,7 +2415,7 @@ function mod_deletebyip($boardName, $post, $global = false) {
$name = $mypost["name"];
$subject = $mypost["subject"];
$filehash = $mypost["filehash"];
- $mypost['files'] = $mypost['files'] ? json_decode($mypost['files']) : array();
+ $mypost['files'] = $mypost['files'] ? json_decode($mypost['files']) : [];
// For each file append file name
for ($file_count = 0; $file_count < $mypost["num_files"];$file_count++){
$filename .= $mypost['files'][$file_count]->name . "\r\n";
@@ -2317,13 +2429,10 @@ function mod_deletebyip($boardName, $post, $global = false) {
$autotag .= $body . "\r\n";
$autotag = escape_markup_modifiers($autotag);
markup($autotag);
- $query2 = prepare('INSERT INTO ``ip_notes`` VALUES (NULL, :ip, :mod, :time, :body)');
- $query2->bindValue(':ip', $ip);
- $query2->bindValue(':mod', $mod['id']);
- $query2->bindValue(':time', time());
- $query2->bindValue(':body', $autotag);
- $query2->execute() or error(db_error($query2));
- modLog("Added a note for {$ip} ");
+
+ $note_queries = $ctx->get(IpNoteQueries::class);
+ $note_queries->add($ip, $mod['id'], $autotag);
+ modLog("Added a note for {$ip} ");
}
}
@@ -2354,13 +2463,13 @@ function mod_deletebyip($boardName, $post, $global = false) {
}
// Record the action
- modLog("Deleted all posts by IP address: $ip ");
+ modLog("Deleted all posts by IP address: $ip ");
// Redirect
header('Location: ?/' . sprintf($config['board_path'], $boardName) . $config['file_index'], true, $config['redirect_http']);
}
-function mod_user($uid) {
+function mod_user(Context $ctx, $uid) {
global $config, $mod;
if (!hasPermission($config['mod']['editusers']) && !(hasPermission($config['mod']['change_password']) && $uid == $mod['id']))
@@ -2381,7 +2490,7 @@ function mod_user($uid) {
$board = $board['uri'];
}
- $boards = array();
+ $boards = [];
foreach ($_POST as $name => $value) {
if (preg_match('/^board_(' . $config['board_regex'] . ')$/u', $name, $matches) && in_array($matches[1], $_boards))
$boards[] = $matches[1];
@@ -2472,7 +2581,7 @@ function mod_user($uid) {
$query->execute() or error(db_error($query));
$log = $query->fetchAll(PDO::FETCH_ASSOC);
} else {
- $log = array();
+ $log = [];
}
$user['boards'] = explode(',', $user['boards']);
@@ -2485,7 +2594,7 @@ function mod_user($uid) {
));
}
-function mod_user_new() {
+function mod_user_new(Context $ctx) {
global $pdo, $config;
if (!hasPermission($config['mod']['createusers']))
@@ -2505,7 +2614,7 @@ function mod_user_new() {
$board = $board['uri'];
}
- $boards = array();
+ $boards = [];
foreach ($_POST as $name => $value) {
if (preg_match('/^board_(' . $config['board_regex'] . ')$/u', $name, $matches) && in_array($matches[1], $_boards))
$boards[] = $matches[1];
@@ -2538,7 +2647,7 @@ function mod_user_new() {
}
-function mod_users() {
+function mod_users(Context $ctx) {
global $config;
if (!hasPermission($config['mod']['manageusers']))
@@ -2559,7 +2668,7 @@ function mod_users() {
mod_page(sprintf('%s (%d)', _('Manage users'), count($users)), 'mod/users.html', array('users' => $users));
}
-function mod_user_promote($uid, $action) {
+function mod_user_promote(Context $ctx, $uid, $action) {
global $config;
if (!hasPermission($config['mod']['promoteusers']))
@@ -2602,7 +2711,7 @@ function mod_user_promote($uid, $action) {
header('Location: ?/users', true, $config['redirect_http']);
}
-function mod_pm($id, $reply = false) {
+function mod_pm(Context $ctx, $id, $reply = false) {
global $mod, $config;
if ($reply && !hasPermission($config['mod']['create_pm']))
@@ -2657,7 +2766,7 @@ function mod_pm($id, $reply = false) {
}
}
-function mod_inbox() {
+function mod_inbox(Context $ctx) {
global $config, $mod;
$query = prepare('SELECT `unread`,``pms``.`id`, `time`, `sender`, `to`, `message`, `username` FROM ``pms`` LEFT JOIN ``mods`` ON ``mods``.`id` = `sender` WHERE `to` = :mod ORDER BY `unread` DESC, `time` DESC');
@@ -2681,7 +2790,7 @@ function mod_inbox() {
}
-function mod_new_pm($username) {
+function mod_new_pm(Context $ctx, $username) {
global $config, $mod;
if (!hasPermission($config['mod']['create_pm']))
@@ -2729,7 +2838,7 @@ function mod_new_pm($username) {
));
}
-function mod_rebuild() {
+function mod_rebuild(Context $ctx) {
global $config, $twig;
if (!hasPermission($config['mod']['rebuild']))
@@ -2738,9 +2847,9 @@ function mod_rebuild() {
if (isset($_POST['rebuild'])) {
@set_time_limit($config['mod']['rebuild_timelimit']);
- $log = array();
+ $log = [];
$boards = listBoards();
- $rebuilt_scripts = array();
+ $rebuilt_scripts = [];
if (isset($_POST['rebuild_cache'])) {
if ($config['cache']['enabled']) {
@@ -2801,49 +2910,28 @@ function mod_rebuild() {
));
}
-function mod_reports() {
+function mod_reports(Context $ctx) {
global $config, $mod;
if (!hasPermission($config['mod']['reports']))
error($config['error']['noaccess']);
- $query = prepare("SELECT * FROM ``reports`` ORDER BY `time` DESC LIMIT :limit");
- $query->bindValue(':limit', $config['mod']['recent_reports'], PDO::PARAM_INT);
- $query->execute() or error(db_error($query));
- $reports = $query->fetchAll(PDO::FETCH_ASSOC);
+ $reports_limit = $config['mod']['recent_reports'];
+ $report_queries = $ctx->get(ReportQueries::class);
+ $report_rows = $report_queries->getReportsWithPosts($reports_limit);
- $report_queries = array();
- foreach ($reports as $report) {
- if (!isset($report_queries[$report['board']]))
- $report_queries[$report['board']] = array();
- $report_queries[$report['board']][] = $report['post'];
+ if (\count($report_rows) > $reports_limit) {
+ \array_pop($report_rows);
+ $has_extra = true;
+ } else {
+ $has_extra = false;
}
- $report_posts = array();
- foreach ($report_queries as $board => $posts) {
- $report_posts[$board] = array();
-
- $query = query(sprintf('SELECT * FROM ``posts_%s`` WHERE `id` = ' . implode(' OR `id` = ', $posts), $board)) or error(db_error());
- while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
- $report_posts[$board][$post['id']] = $post;
- }
- }
-
- $count = 0;
$body = '';
- foreach ($reports as $report) {
- if (!isset($report_posts[$report['board']][$report['post']])) {
- // // Invalid report (post has since been deleted)
- $query = prepare("DELETE FROM ``reports`` WHERE `post` = :id AND `board` = :board");
- $query->bindValue(':id', $report['post'], PDO::PARAM_INT);
- $query->bindValue(':board', $report['board']);
- $query->execute() or error(db_error($query));
- continue;
- }
-
+ foreach ($report_rows as $report) {
openBoard($report['board']);
- $post = &$report_posts[$report['board']][$report['post']];
+ $post = $report['post_data'];
if (!$post['thread']) {
// Still need to fix this:
@@ -2852,7 +2940,7 @@ function mod_reports() {
$po = new Post($post, '?/', $mod);
}
- // a little messy and inefficient
+ // A little messy and inefficient.
$append_html = Element('mod/report.html', array(
'report' => $report,
'config' => $config,
@@ -2864,63 +2952,64 @@ function mod_reports() {
// Bug fix for https://github.com/savetheinternet/Tinyboard/issues/21
$po->body = truncate($po->body, $po->link(), $config['body_truncate'] - substr_count($append_html, ' '));
- if (mb_strlen($po->body) + mb_strlen($append_html) > $config['body_truncate_char']) {
+ if (\mb_strlen($po->body) + \mb_strlen($append_html) > $config['body_truncate_char']) {
// still too long; temporarily increase limit in the config
$__old_body_truncate_char = $config['body_truncate_char'];
- $config['body_truncate_char'] = mb_strlen($po->body) + mb_strlen($append_html);
+ $config['body_truncate_char'] = \mb_strlen($po->body) + \mb_strlen($append_html);
}
$po->body .= $append_html;
$body .= $po->build(true) . ' ';
- if (isset($__old_body_truncate_char))
+ if (isset($__old_body_truncate_char)) {
$config['body_truncate_char'] = $__old_body_truncate_char;
-
- $count++;
+ }
}
- mod_page(sprintf('%s (%d)', _('Report queue'), $count), 'mod/reports.html', array('reports' => $body, 'count' => $count));
+ $count = \count($report_rows);
+ $header_count = $has_extra ? "{$count}+" : (string)$count;
+
+ mod_page(
+ \sprintf('%s (%s)', _('Report queue'), $header_count),
+ 'mod/reports.html',
+ [ 'reports' => $body, 'count' => $count ]
+ );
}
-function mod_report_dismiss($id, $all = false) {
+function mod_report_dismiss(Context $ctx, $id, $all = false) {
global $config;
- $query = prepare("SELECT `post`, `board`, `ip` FROM ``reports`` WHERE `id` = :id");
- $query->bindValue(':id', $id);
- $query->execute() or error(db_error($query));
- if ($report = $query->fetch(PDO::FETCH_ASSOC)) {
- $ip = $report['ip'];
- $board = $report['board'];
- $post = $report['post'];
- } else
+ $report_queries = $ctx->get(ReportQueries::class);
+ $report = $report_queries->getReportById($id);
+
+ if ($report === null) {
error($config['error']['404']);
+ }
- if (!$all && !hasPermission($config['mod']['report_dismiss'], $board))
- error($config['error']['noaccess']);
+ $ip = $report['ip'];
+ $board = $report['board'];
- if ($all && !hasPermission($config['mod']['report_dismiss_ip'], $board))
+ if (!$all && !hasPermission($config['mod']['report_dismiss'], $board)) {
error($config['error']['noaccess']);
+ }
+
+ if ($all && !hasPermission($config['mod']['report_dismiss_ip'], $board)) {
+ error($config['error']['noaccess']);
+ }
if ($all) {
- $query = prepare("DELETE FROM ``reports`` WHERE `ip` = :ip");
- $query->bindValue(':ip', $ip);
+ $report_queries->deleteByIp($ip);
+ modLog("Dismissed all reports by $ip ");
} else {
- $query = prepare("DELETE FROM ``reports`` WHERE `id` = :id");
- $query->bindValue(':id', $id);
- }
- $query->execute() or error(db_error($query));
-
-
- if ($all)
- modLog("Dismissed all reports by $ip ");
- else
+ $report_queries->deleteById($id);
modLog("Dismissed a report for post #{$id}", $board);
+ }
header('Location: ?/reports', true, $config['redirect_http']);
}
-function mod_recent_posts($lim,$board_list = false,$json=false) {
+function mod_recent_posts(Context $ctx, $lim, $board_list = false, $json = false) {
global $config, $mod, $pdo;
if (!hasPermission($config['mod']['recent']))
@@ -2929,7 +3018,7 @@ function mod_recent_posts($lim,$board_list = false,$json=false) {
$limit = (is_numeric($lim))? $lim : 25;
$last_time = (isset($_GET['last']) && is_numeric($_GET['last'])) ? $_GET['last'] : 0;
- $mod_boards = array();
+ $mod_boards = [];
$boards = listBoards();
//if not all boards
@@ -2943,7 +3032,7 @@ function mod_recent_posts($lim,$board_list = false,$json=false) {
}
if ($board_list != false){
$board_array = explode(",",$board_list);
- $new_board_array = array();
+ $new_board_array = [];
foreach ($board_array as $board) {
if (array_key_exists($board,$config['boards_alias'])){
$newboard = $config['boards_alias'][$board];
@@ -2953,7 +3042,7 @@ function mod_recent_posts($lim,$board_list = false,$json=false) {
}
$new_board_array[] = $newboard;
}
- $mod_boards = array();
+ $mod_boards = [];
foreach ($boards as $board) {
if (in_array($board['uri'], $new_board_array)){
$mod_boards[] = $board;
@@ -2974,7 +3063,7 @@ function mod_recent_posts($lim,$board_list = false,$json=false) {
$posts = $query->fetchAll(PDO::FETCH_ASSOC);
if ($config['api']['enabled']) {
- $apithreads = array();
+ $apithreads = [];
}
foreach ($posts as &$post) {
@@ -3018,7 +3107,7 @@ function mod_recent_posts($lim,$board_list = false,$json=false) {
}
}
-function mod_config($board_config = false) {
+function mod_config(Context $ctx, $board_config = false) {
global $config, $mod, $board;
if ($board_config && !openBoard($board_config))
@@ -3130,7 +3219,7 @@ function mod_config($board_config = false) {
if ($config['minify_html'])
$config_append = str_replace("\n", '
', $config_append);
- $page = array();
+ $page = [];
$page['title'] = 'Cannot write to file!';
$page['config'] = $config;
$page['body'] = '
@@ -3158,7 +3247,7 @@ function mod_config($board_config = false) {
));
}
-function mod_themes_list() {
+function mod_themes_list(Context $ctx) {
global $config;
if (!hasPermission($config['mod']['themes']))
@@ -3173,7 +3262,7 @@ function mod_themes_list() {
$themes_in_use = $query->fetchAll(PDO::FETCH_COLUMN);
// Scan directory for themes
- $themes = array();
+ $themes = [];
while ($file = readdir($dir)) {
if ($file[0] != '.' && is_dir($config['dir']['themes'] . '/' . $file)) {
$themes[$file] = loadThemeConfig($file);
@@ -3192,7 +3281,7 @@ function mod_themes_list() {
));
}
-function mod_theme_configure($theme_name) {
+function mod_theme_configure(Context $ctx, $theme_name) {
global $config;
if (!hasPermission($config['mod']['themes']))
@@ -3274,7 +3363,7 @@ function mod_theme_configure($theme_name) {
));
}
-function mod_theme_uninstall($theme_name) {
+function mod_theme_uninstall(Context $ctx, $theme_name) {
global $config;
if (!hasPermission($config['mod']['themes']))
@@ -3291,7 +3380,7 @@ function mod_theme_uninstall($theme_name) {
header('Location: ?/themes', true, $config['redirect_http']);
}
-function mod_theme_rebuild($theme_name) {
+function mod_theme_rebuild(Context $ctx, $theme_name) {
global $config;
if (!hasPermission($config['mod']['themes']))
@@ -3332,15 +3421,15 @@ function delete_page_base($page = '', $board = false) {
header('Location: ?/edit_pages' . ($board ? ('/' . $board) : ''), true, $config['redirect_http']);
}
-function mod_delete_page($page = '') {
- delete_page_base($page);
+function mod_delete_page(Context $ctx, $page = '') {
+ delete_page_base($ctx, $page);
}
-function mod_delete_page_board($page = '', $board = false) {
- delete_page_base($page, $board);
+function mod_delete_page_board(Context $ctx, $page = '', $board = false) {
+ delete_page_base($ctx, $page, $board);
}
-function mod_edit_page($id) {
+function mod_edit_page(Context $ctx, $id) {
global $config, $mod, $board;
$query = prepare('SELECT * FROM ``pages`` WHERE `id` = :id');
@@ -3411,7 +3500,7 @@ function mod_edit_page($id) {
mod_page(sprintf(_('Editing static page: %s'), $page['name']), 'mod/edit_page.html', array('page' => $page, 'token' => make_secure_link_token("edit_page/$id"), 'content' => prettify_textarea($content), 'board' => $board));
}
-function mod_pages($board = false) {
+function mod_pages(Context $ctx, $board = false) {
global $config, $mod, $pdo;
if (empty($board))
@@ -3465,10 +3554,10 @@ function mod_pages($board = false) {
mod_page(_('Pages'), 'mod/pages.html', array('pages' => $pages, 'token' => make_secure_link_token('edit_pages' . ($board ? ('/' . $board) : '')), 'board' => $board));
}
-function mod_debug_antispam() {
+function mod_debug_antispam(Context $ctx) {
global $pdo, $config;
- $args = array();
+ $args = [];
if (isset($_POST['board'], $_POST['thread'])) {
$where = '`board` = ' . $pdo->quote($_POST['board']);
@@ -3502,7 +3591,7 @@ function mod_debug_antispam() {
mod_page(_('Debug: Anti-spam'), 'mod/debug/antispam.html', $args);
}
-function mod_debug_recent_posts() {
+function mod_debug_recent_posts(Context $ctx) {
global $pdo, $config;
$limit = 500;
@@ -3536,7 +3625,7 @@ function mod_debug_recent_posts() {
mod_page(_('Debug: Recent posts'), 'mod/debug/recent_posts.html', array('posts' => $posts, 'flood_posts' => $flood_posts));
}
-function mod_debug_sql() {
+function mod_debug_sql(Context $ctx) {
global $config;
if (!hasPermission($config['mod']['debug_sql']))
@@ -3559,25 +3648,3 @@ function mod_debug_sql() {
mod_page(_('Debug: SQL'), 'mod/debug/sql.html', $args);
}
-
-function mod_debug_apc() {
- global $config;
-
- if (!hasPermission($config['mod']['debug_apc']))
- error($config['error']['noaccess']);
-
- if ($config['cache']['enabled'] != 'apc')
- error('APC is not enabled.');
-
- $cache_info = apc_cache_info('user');
-
- // $cached_vars = new APCIterator('user', '/^' . $config['cache']['prefix'] . '/');
- $cached_vars = array();
- foreach ($cache_info['cache_list'] as $var) {
- if ($config['cache']['prefix'] != '' && strpos(isset($var['key']) ? $var['key'] : $var['info'], $config['cache']['prefix']) !== 0)
- continue;
- $cached_vars[] = $var;
- }
-
- mod_page(_('Debug: APC'), 'mod/debug/apc.html', array('cached_vars' => $cached_vars));
-}
diff --git a/inc/polyfill.php b/inc/polyfill.php
index 2b14c4dc..4368c8e8 100644
--- a/inc/polyfill.php
+++ b/inc/polyfill.php
@@ -1,187 +1,10 @@
$i ? $i : 0]) ^ ord($theirs[$i]);
- }
-
- return $answer === 0 && $olen === $tlen;
- }
-}
-
-if (!function_exists('imagecreatefrombmp')) {
- /*********************************************/
- /* Fonction: imagecreatefrombmp */
- /* Author: DHKold */
- /* Contact: admin@dhkold.com */
- /* Date: The 15th of June 2005 */
- /* Version: 2.0B */
- /*********************************************/
- function imagecreatefrombmp($filename) {
- if (! $f1 = fopen($filename,"rb")) return FALSE;
- $FILE = unpack("vfile_type/Vfile_size/Vreserved/Vbitmap_offset", fread($f1,14));
- if ($FILE['file_type'] != 19778) return FALSE;
- $BMP = unpack('Vheader_size/Vwidth/Vheight/vplanes/vbits_per_pixel'.
- '/Vcompression/Vsize_bitmap/Vhoriz_resolution'.
- '/Vvert_resolution/Vcolors_used/Vcolors_important', fread($f1,40));
- $BMP['colors'] = pow(2,$BMP['bits_per_pixel']);
- if ($BMP['size_bitmap'] == 0) $BMP['size_bitmap'] = $FILE['file_size'] - $FILE['bitmap_offset'];
- $BMP['bytes_per_pixel'] = $BMP['bits_per_pixel']/8;
- $BMP['bytes_per_pixel2'] = ceil($BMP['bytes_per_pixel']);
- $BMP['decal'] = ($BMP['width']*$BMP['bytes_per_pixel']/4);
- $BMP['decal'] -= floor($BMP['width']*$BMP['bytes_per_pixel']/4);
- $BMP['decal'] = 4-(4*$BMP['decal']);
- if ($BMP['decal'] == 4) $BMP['decal'] = 0;
- $PALETTE = array();
- if ($BMP['colors'] < 16777216)
- {
- $PALETTE = unpack('V'.$BMP['colors'], fread($f1,$BMP['colors']*4));
- }
- $IMG = fread($f1,$BMP['size_bitmap']);
- $VIDE = chr(0);
- $res = imagecreatetruecolor($BMP['width'],$BMP['height']);
- $P = 0;
- $Y = $BMP['height']-1;
- while ($Y >= 0)
- {
- $X=0;
- while ($X < $BMP['width'])
- {
- if ($BMP['bits_per_pixel'] == 24)
- $COLOR = unpack("V",substr($IMG,$P,3).$VIDE);
- elseif ($BMP['bits_per_pixel'] == 16)
- {
- $COLOR = unpack("n",substr($IMG,$P,2));
- $COLOR[1] = $PALETTE[$COLOR[1]+1];
- }
- elseif ($BMP['bits_per_pixel'] == 8)
- {
- $COLOR = unpack("n",$VIDE.substr($IMG,$P,1));
- $COLOR[1] = $PALETTE[$COLOR[1]+1];
- }
- elseif ($BMP['bits_per_pixel'] == 4)
- {
- $COLOR = unpack("n",$VIDE.substr($IMG,floor($P),1));
- if (($P*2)%2 == 0) $COLOR[1] = ($COLOR[1] >> 4) ; else $COLOR[1] = ($COLOR[1] & 0x0F);
- $COLOR[1] = $PALETTE[$COLOR[1]+1];
- }
- elseif ($BMP['bits_per_pixel'] == 1)
- {
- $COLOR = unpack("n",$VIDE.substr($IMG,floor($P),1));
- if (($P*8)%8 == 0) $COLOR[1] = $COLOR[1] >>7;
- elseif (($P*8)%8 == 1) $COLOR[1] = ($COLOR[1] & 0x40)>>6;
- elseif (($P*8)%8 == 2) $COLOR[1] = ($COLOR[1] & 0x20)>>5;
- elseif (($P*8)%8 == 3) $COLOR[1] = ($COLOR[1] & 0x10)>>4;
- elseif (($P*8)%8 == 4) $COLOR[1] = ($COLOR[1] & 0x8)>>3;
- elseif (($P*8)%8 == 5) $COLOR[1] = ($COLOR[1] & 0x4)>>2;
- elseif (($P*8)%8 == 6) $COLOR[1] = ($COLOR[1] & 0x2)>>1;
- elseif (($P*8)%8 == 7) $COLOR[1] = ($COLOR[1] & 0x1);
- $COLOR[1] = $PALETTE[$COLOR[1]+1];
- }
- else
- return FALSE;
- imagesetpixel($res,$X,$Y,$COLOR[1]);
- $X++;
- $P += $BMP['bytes_per_pixel'];
- }
- $Y--;
- $P+=$BMP['decal'];
- }
- fclose($f1);
- return $res;
- }
-}
-
-if (!function_exists('imagebmp')) {
- function imagebmp(&$img, $filename='') {
- $widthOrig = imagesx($img);
- $widthFloor = ((floor($widthOrig/16))*16);
- $widthCeil = ((ceil($widthOrig/16))*16);
- $height = imagesy($img);
- $size = ($widthCeil*$height*3)+54;
- // Bitmap File Header
- $result = 'BM'; // header (2b)
- $result .= int_to_dword($size); // size of file (4b)
- $result .= int_to_dword(0); // reserved (4b)
- $result .= int_to_dword(54); // byte location in the file which is first byte of IMAGE (4b)
- // Bitmap Info Header
- $result .= int_to_dword(40); // Size of BITMAPINFOHEADER (4b)
- $result .= int_to_dword($widthCeil); // width of bitmap (4b)
- $result .= int_to_dword($height); // height of bitmap (4b)
- $result .= int_to_word(1); // biPlanes = 1 (2b)
- $result .= int_to_word(24); // biBitCount = {1 (mono) or 4 (16 clr ) or 8 (256 clr) or 24 (16 Mil)} (2b
- $result .= int_to_dword(0); // RLE COMPRESSION (4b)
- $result .= int_to_dword(0); // width x height (4b)
- $result .= int_to_dword(0); // biXPelsPerMeter (4b)
- $result .= int_to_dword(0); // biYPelsPerMeter (4b)
- $result .= int_to_dword(0); // Number of palettes used (4b)
- $result .= int_to_dword(0); // Number of important colour (4b)
- // is faster than chr()
- $arrChr = array();
- for ($i=0; $i<256; $i++){
- $arrChr[$i] = chr($i);
- }
- // creates image data
- $bgfillcolor = array('red'=>0, 'green'=>0, 'blue'=>0);
- // bottom to top - left to right - attention blue green red !!!
- $y=$height-1;
- for ($y2=0; $y2<$height; $y2++) {
- for ($x=0; $x<$widthFloor; ) {
- $rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
- $result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
- $rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
- $result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
- $rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
- $result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
- $rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
- $result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
- $rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
- $result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
- $rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
- $result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
- $rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
- $result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
- $rgb = imagecolorsforindex($img, imagecolorat($img, $x++, $y));
- $result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
- }
- for ($x=$widthFloor; $x<$widthCeil; $x++) {
- $rgb = ($x<$widthOrig) ? imagecolorsforindex($img, imagecolorat($img, $x, $y)) : $bgfillcolor;
- $result .= $arrChr[$rgb['blue']].$arrChr[$rgb['green']].$arrChr[$rgb['red']];
- }
- $y--;
- }
- // see imagegif
- if ($filename == '') {
- echo $result;
- } else {
- $file = fopen($filename, 'wb');
- fwrite($file, $result);
- fclose($file);
- }
- }
- // imagebmp helpers
- function int_to_dword($n) {
- return chr($n & 255).chr(($n >> 8) & 255).chr(($n >> 16) & 255).chr(($n >> 24) & 255);
- }
- function int_to_word($n) {
- return chr($n & 255).chr(($n >> 8) & 255);
+if (!function_exists('str_starts_with')) {
+ function str_starts_with(string $haystack, string $needle): bool {
+ // https://wiki.php.net/rfc/add_str_starts_with_and_ends_with_functions#str_starts_with
+ return \strncmp($haystack, $needle, \strlen($needle)) === 0;
}
}
diff --git a/install.php b/install.php
index 94a589b1..7663a503 100644
--- a/install.php
+++ b/install.php
@@ -52,11 +52,7 @@ if (file_exists($config['has_installed'])) {
function __query($sql) {
sql_open();
-
- if (mysql_version() >= 50503)
- return query($sql);
- else
- return query(str_replace('utf8mb4', 'utf8', $sql));
+ return query($sql);
}
$boards = listBoards();
@@ -885,6 +881,7 @@ if ($step == 0) {
$config['cookies']['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(
'body' => Element('installer/config.html', array(
@@ -939,7 +936,6 @@ if ($step == 0) {
$sql = @file_get_contents('install.sql') or error("Couldn't load install.sql.");
sql_open();
- $mysql_version = mysql_version();
// This code is probably horrible, but what I'm trying
// to do is find all of the SQL queires and put them
@@ -952,8 +948,6 @@ if ($step == 0) {
$sql_errors = '';
$sql_err_count = 0;
foreach ($queries as $query) {
- if ($mysql_version < 50503)
- $query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query);
$query = preg_replace('/^([\w\s]*)`([0-9a-zA-Z$_\x{0080}-\x{FFFF}]+)`/u', '$1``$2``', $query);
if (!query($query)) {
$sql_err_count++;
diff --git a/js/ajax.js b/js/ajax.js
index af795a57..3cb06bf1 100644
--- a/js/ajax.js
+++ b/js/ajax.js
@@ -18,16 +18,25 @@ $(window).ready(function() {
// Enable submit button if disabled (cache problem)
$('input[type="submit"]').removeAttr('disabled');
-
+
var setup_form = function($form) {
$form.submit(function() {
if (do_not_ajax)
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 submit_txt = $(this).find('input[type="submit"]').val();
if (window.FormData === undefined)
return true;
-
+
var formData = new FormData(this);
formData.append('json_response', '1');
formData.append('post', submit_txt);
@@ -94,15 +103,15 @@ $(window).ready(function() {
setTimeout(function() { $(window).trigger("scroll"); }, 100);
}
});
-
+
highlightReply(post_response.id);
window.location.hash = post_response.id;
$(window).scrollTop($(document).height());
-
+
$(form).find('input[type="submit"]').val(submit_txt);
$(form).find('input[type="submit"]').removeAttr('disabled');
$(form).find('input[name="subject"],input[name="file_url"],\
- textarea[name="body"],input[type="file"]').val('').change();
+ textarea[name="body"],input[type="file"],input[name="embed"]').val('').change();
},
cache: false,
contentType: false,
@@ -114,7 +123,7 @@ $(window).ready(function() {
$(form).find('input[type="submit"]').val(submit_txt);
$(form).find('input[type="submit"]').removeAttr('disabled');
$(form).find('input[name="subject"],input[name="file_url"],\
- textarea[name="body"],input[type="file"]').val('').change();
+ textarea[name="body"],input[type="file"],input[name="embed"]').val('').change();
} else {
alert(_('An unknown error occured when posting!'));
$(form).find('input[type="submit"]').val(submit_txt);
@@ -132,10 +141,10 @@ $(window).ready(function() {
contentType: false,
processData: false
}, 'json');
-
+
$(form).find('input[type="submit"]').val(_('Posting...'));
$(form).find('input[type="submit"]').attr('disabled', true);
-
+
return false;
});
};
diff --git a/js/catalog.js b/js/catalog.js
index c620918c..49fe413c 100644
--- a/js/catalog.js
+++ b/js/catalog.js
@@ -1,3 +1,9 @@
+/**
+ * Usage:
+ * $config['additional_javascript'][] = 'js/jquery.min.js';
+ * $config['additional_javascript'][] = 'js/jquery.mixitup.min.js';
+ * $config['additional_javascript'][] = 'js/catalog.js';
+ */
if (active_page == 'catalog') $(function(){
if (localStorage.catalog !== undefined) {
var catalog = JSON.parse(localStorage.catalog);
diff --git a/js/inline-expanding.js b/js/inline-expanding.js
index 41625d2d..c44843b0 100644
--- a/js/inline-expanding.js
+++ b/js/inline-expanding.js
@@ -17,6 +17,10 @@ $(document).ready(function() {
// Default maximum image loads.
const DEFAULT_MAX = 5;
+ if (localStorage.inline_expand_fit_height !== 'false') {
+ $('').appendTo($('head'));
+ }
+
let inline_expand_post = function() {
let link = this.getElementsByTagName('a');
@@ -56,12 +60,12 @@ $(document).ready(function() {
},
add: function(ele) {
ele.deferred = $.Deferred();
- ele.deferred.done(function () {
+ ele.deferred.done(function() {
let $loadstart = $.Deferred();
let thumb = ele.childNodes[0];
let img = ele.childNodes[1];
- let onLoadStart = function (img) {
+ let onLoadStart = function(img) {
if (img.naturalWidth) {
$loadstart.resolve(img, thumb);
} else {
@@ -69,15 +73,15 @@ $(document).ready(function() {
}
};
- $(img).one('load', function () {
- $.when($loadstart).done(function () {
- // Once fully loaded, update the waiting queue.
+ $(img).one('load', function() {
+ $.when($loadstart).done(function() {
+ // once fully loaded, update the waiting queue
--loading;
$(ele).data('imageLoading', 'false');
update();
});
});
- $loadstart.done(function (img, thumb) {
+ $loadstart.done(function(img, thumb) {
thumb.style.display = 'none';
img.style.display = '';
});
@@ -202,6 +206,8 @@ $(document).ready(function() {
Options.extend_tab('general', '' +
_('Number of simultaneous image downloads (0 to disable): ') +
' ');
+ Options.extend_tab('general', ' ' + _('Fit expanded images into screen height') + ' ');
+
$('#inline-expand-max input')
.css('width', '50px')
.val(localStorage.inline_expand_max || DEFAULT_MAX)
@@ -212,6 +218,21 @@ $(document).ready(function() {
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';
+ $('').appendTo($('head'));
+ }
+ });
+
+ if (localStorage.inline_expand_fit_height !== 'false') {
+ $('#inline-expand-fit-height input').prop('checked', true);
+ }
}
if (window.jQuery) {
diff --git a/js/options/general.js b/js/options/general.js
index c0652269..6715ae1d 100644
--- a/js/options/general.js
+++ b/js/options/general.js
@@ -43,9 +43,6 @@ $(function(){
document.location.reload();
}
});
-
-
- $("#style-select").detach().css({float:"none","margin-bottom":0}).appendTo(tab.content);
});
}();
diff --git a/js/post-filter.js b/js/post-filter.js
index f3f161c6..78fd35ce 100644
--- a/js/post-filter.js
+++ b/js/post-filter.js
@@ -237,12 +237,8 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
var postUid = $ele.find('.poster_id').text();
}
- let postName;
- let postTrip = '';
- if (!pageData.forcedAnon) {
- postName = (typeof $ele.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($ele.find('.name')[0]);
- postTrip = $ele.find('.trip').text();
- }
+ let postName = (typeof $ele.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($ele.find('.name')[0]);
+ let postTrip = $ele.find('.trip').text();
/* display logic and bind click handlers
*/
@@ -297,39 +293,34 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
}
// name
- if (!pageData.forcedAnon && !$ele.data('hiddenByName')) {
+ if (!$ele.data('hiddenByName')) {
$buffer.find('#filter-add-name').click(function () {
addFilter('name', postName, false);
});
$buffer.find('#filter-remove-name').addClass('hidden');
- } else if (!pageData.forcedAnon) {
+ } else {
$buffer.find('#filter-remove-name').click(function () {
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');
}
// tripcode
- if (!pageData.forcedAnon && !$ele.data('hiddenByTrip') && postTrip !== '') {
+ if (!$ele.data('hiddenByTrip') && postTrip !== '') {
$buffer.find('#filter-add-trip').click(function () {
addFilter('trip', postTrip, false);
});
$buffer.find('#filter-remove-trip').addClass('hidden');
- } else if (!pageData.forcedAnon && postTrip !== '') {
+ } else if (postTrip !== '') {
$buffer.find('#filter-remove-trip').click(function () {
removeFilter('trip', postTrip, false);
});
$buffer.find('#filter-add-trip').addClass('hidden');
} else {
- // board has forced anon
$buffer.find('#filter-remove-trip').addClass('hidden');
$buffer.find('#filter-add-trip').addClass('hidden');
}
@@ -391,7 +382,6 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
var localList = pageData.localList;
var noReplyList = pageData.noReplyList;
var hasUID = pageData.hasUID;
- var forcedAnon = pageData.forcedAnon;
var hasTrip = ($post.find('.trip').length > 0);
var hasSub = ($post.find('.subject').length > 0);
@@ -432,9 +422,8 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
}
// matches generalFilter
- if (!forcedAnon)
- name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]);
- if (!forcedAnon && hasTrip)
+ name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]);
+ if (hasTrip)
trip = $post.find('.trip').text();
if (hasSub)
subject = $post.find('.subject').text();
@@ -455,13 +444,13 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
pattern = new RegExp(rule.value);
switch (rule.type) {
case 'name':
- if (!forcedAnon && pattern.test(name)) {
+ if (pattern.test(name)) {
$post.data('hiddenByName', true);
hide(post);
}
break;
case 'trip':
- if (!forcedAnon && hasTrip && pattern.test(trip)) {
+ if (hasTrip && pattern.test(trip)) {
$post.data('hiddenByTrip', true);
hide(post);
}
@@ -488,13 +477,13 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
} else {
switch (rule.type) {
case 'name':
- if (!forcedAnon && rule.value == name) {
+ if (rule.value == name) {
$post.data('hiddenByName', true);
hide(post);
}
break;
case 'trip':
- if (!forcedAnon && hasTrip && rule.value == trip) {
+ if (hasTrip && rule.value == trip) {
$post.data('hiddenByTrip', true);
hide(post);
}
@@ -827,8 +816,7 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
boardId: board_name, // get the id from the global variable
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
- hasUID: (document.getElementsByClassName('poster_id').length > 0),
- forcedAnon: ($('th:contains(Name)').length === 0) // tests by looking for the Name label on the reply form
+ hasUID: (document.getElementsByClassName('poster_id').length > 0)
};
initStyle();
diff --git a/js/post-menu.js b/js/post-menu.js
index 79cfd868..c2155c00 100644
--- a/js/post-menu.js
+++ b/js/post-menu.js
@@ -104,8 +104,10 @@ function buildMenu(e) {
function addButton(post) {
var $ele = $(post);
+ // Use unicode code with ascii variant selector
+ // https://stackoverflow.com/questions/37906969/how-to-prevent-ios-from-converting-ascii-into-emoji
$ele.find('input.delete').after(
- $('', {href: '#', class: 'post-btn', title: 'Post menu'}).text('►')
+ $(' ', {href: '#', class: 'post-btn', title: 'Post menu'}).text('\u{25B6}\u{fe0e}')
);
}
diff --git a/js/show-backlinks.js b/js/show-backlinks.js
index 5924124e..607c24ab 100644
--- a/js/show-backlinks.js
+++ b/js/show-backlinks.js
@@ -15,7 +15,7 @@
$(document).ready(function() {
let showBackLinks = function() {
- let replyId = $(this).attr('id').replace(/^reply_/, '');
+ let replyId = $(this).attr('id').split('_')[1];
$(this).find('div.body a:not([rel="nofollow"])').each(function() {
let id = $(this).text().match(/^>>(\d+)$/);
@@ -25,13 +25,15 @@ $(document).ready(function() {
return;
}
- let post = $('#reply_' + id);
- if(post.length == 0)
+ let post = $('#reply_' + id + ', #op_' + id);
+ if (post.length == 0) {
return;
+ }
let mentioned = post.find('.head div.mentioned');
if (mentioned.length === 0) {
- mentioned = $('
').prependTo(post.find('.head'));
+ // The op has two "head"s divs, use the second.
+ mentioned = $('
').prependTo(post.find('.head').last());
}
if (mentioned.find('a.mentioned-' + replyId).length !== 0) {
@@ -48,13 +50,13 @@ $(document).ready(function() {
});
};
- $('div.post.reply').each(showBackLinks);
+ $('div.post').each(showBackLinks);
$(document).on('new_post', function(e, post) {
- if ($(post).hasClass('reply')) {
+ if ($(post).hasClass('reply') || $(post).hasClass('op')) {
showBackLinks.call(post);
} else {
- $(post).find('div.post.reply').each(showBackLinks);
+ $(post).find('div.post').each(showBackLinks);
}
});
});
diff --git a/js/style-select-simple.js b/js/style-select-simple.js
new file mode 100644
index 00000000..8b59fa0a
--- /dev/null
+++ b/js/style-select-simple.js
@@ -0,0 +1,36 @@
+/*
+ * style-select-simple.js
+ *
+ * Changes the stylesheet chooser links to a
+ *
+ * Released under the MIT license
+ * Copyright (c) 2025 Zankaria Auxa
+ *
+ * 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);
+});
diff --git a/js/style-select.js b/js/style-select.js
index 485da735..16bbae72 100644
--- a/js/style-select.js
+++ b/js/style-select.js
@@ -6,48 +6,53 @@
*
* Released under the MIT license
* Copyright (c) 2013 Michael Save
- * Copyright (c) 2013-2014 Marcin Łabanowski
+ * Copyright (c) 2013-2014 Marcin Łabanowski
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/style-select.js';
- *
*/
$(document).ready(function() {
- var stylesDiv = $('div.styles');
- var pages = $('div.pages');
- var stylesSelect = $(' ').css({float:"none"});
- var options = [];
-
- var i = 1;
- stylesDiv.children().each(function() {
- var name = this.innerHTML.replace(/(^\[|\]$)/g, '');
- var opt = $(' ')
- .html(name)
- .val(i);
- if ($(this).hasClass('selected'))
- opt.attr('selected', true);
- options.push ([name.toUpperCase (), opt]);
- $(this).attr('id', 'style-select-' + i);
- i++;
- });
+ let pages = $('div.pages');
+ let stylesSelect = $(' ').css({float:"none"});
+ let options = [];
- options.sort ((a, b) => {
+ let i = 1;
+ for (styleName in styles) {
+ if (styleName) {
+ let opt = $(' ')
+ .html(styleName)
+ .val(i);
+ if (selectedstyle == styleName) {
+ opt.attr('selected', true);
+ }
+ opt.attr('id', 'style-select-' + i);
+ options.push([styleName.toUpperCase (), opt]);
+ i++;
+ }
+ }
+
+ options.sort((a, b) => {
const keya = a [0];
const keyb = b [0];
- if (keya < keyb) { return -1; }
- if (keya > keyb) { return 1; }
+ if (keya < keyb) {
+ return -1;
+ }
+ if (keya > keyb) {
+ return 1;
+ }
return 0;
- }).forEach (([key, opt]) => {
+ }).forEach(([key, opt]) => {
stylesSelect.append(opt);
});
-
+
stylesSelect.change(function() {
- $('#style-select-' + $(this).val()).click();
+ let sel = $(this).find(":selected")[0];
+ let styleName = sel.innerHTML;
+ changeStyle(styleName, sel);
});
-
- stylesDiv.hide()
+
pages.after(
$('
')
.append(_('Select theme: '), stylesSelect)
diff --git a/js/youtube.js b/js/youtube.js
index 4c31ed09..4a5a5afe 100644
--- a/js/youtube.js
+++ b/js/youtube.js
@@ -1,41 +1,41 @@
/*
-* 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.
-* Currently only compatiable with YouTube.
-*
-* Proof of concept.
-*
-* Released under the MIT license
-* Copyright (c) 2013 Michael Save
-* Copyright (c) 2013-2014 Marcin Łabanowski
-*
-* Usage:
-* $config['embedding'] = array();
-* $config['embedding'][0] = array(
-* '/^https?:\/\/(\w+\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9\-_]{10,11})(&.+)?$/i',
-* $config['youtube_js_html']);
-* $config['additional_javascript'][] = 'js/jquery.min.js';
-* $config['additional_javascript'][] = 'js/youtube.js';
-*
-*/
+ * Don't load the 3rd party embedded content player unless the image is clicked.
+ * This increases performance issues when many videos are embedded on the same page.
+ *
+ * Released under the MIT license
+ * Copyright (c) 2013 Michael Save
+ * Copyright (c) 2013-2014 Marcin Łabanowski
+ * Copyright (c) 2025 Zankaria Auxa
+ *
+ * Usage:
+ * $config['embedding'] = array();
+ * $config['embedding'][0] = array(
+ * '/^https?:\/\/(\w+\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9\-_]{10,11})(&.+)?$/i',
+ * $config['youtube_js_html']);
+ * $config['additional_javascript'][] = 'js/jquery.min.js';
+ * $config['additional_javascript'][] = 'js/youtube.js';
+ */
-$(document).ready(function(){
- // Adds Options panel item
+$(document).ready(function() {
+ const ON = '[Remove]';
+ const YOUTUBE = 'www.youtube.com';
+
+ function makeEmbedNode(embedHost, videoId, width, height) {
+ return $(``);
+ }
+
+ // Adds Options panel item.
if (typeof localStorage.youtube_embed_proxy === 'undefined') {
- if (location.hostname.includes(".onion")){
- localStorage.youtube_embed_proxy = 'tuberyps2pn6dor6h47brof3w2asmauahhk4ei42krugybzzzo55klad.onion';
- } else {
- localStorage.youtube_embed_proxy = 'incogtube.com'; //default value
- }
+ localStorage.youtube_embed_proxy = 'incogtube.com'; // Default value.
}
if (window.Options && Options.get_tab('general')) {
- Options.extend_tab("general", ""+_("Media Proxy (requires refresh)")+" "
- + ('' + _('YouTube embed proxy url ')+' ')
- + ' ');
+ Options.extend_tab("general",
+ "" + _("Media Proxy (requires refresh)") + " "
+ + '' + _('YouTube embed proxy url ')
+ + ' '
+ + ' ');
$('#youtube-embed-proxy-url>input').val(localStorage.youtube_embed_proxy);
$('#youtube-embed-proxy-url>input').on('input', function() {
@@ -43,51 +43,65 @@ $(document).ready(function(){
});
}
- 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 = $("[Embed] ");
- var spanProxy = $("[Proxy] ");
+ const proxy = localStorage.youtube_embed_proxy;
- var makeEmbedNode = function(embedHost) {
- return $('');
- }
- var defaultEmbed = makeEmbedNode(location.hostname.includes(".onion") ? PROXY : YOUTUBE);
- var proxyEmbed = makeEmbedNode(PROXY);
- videoNode.click(function(e) {
+ function addEmbedButton(_i, node) {
+ node = $(node);
+ const contents = node.contents();
+ const embedUrl = node.data('video-id');
+ const embedWidth = node.data('iframe-width');
+ const embedHeight = node.data('iframe-height');
+ const span = $('[Embed] ');
+ const spanProxy = $("[Proxy] ");
+
+ let iframeDefault = null;
+ let iframeProxy = null;
+
+ node.click(function(e) {
e.preventDefault();
- if (span.text() == ON){
- videoNode.append(spanProxy);
- videoNode.append(contents);
- defaultEmbed.remove();
- proxyEmbed.remove();
- span.text(OFF);
+ if (span.text() == ON) {
+ contents.css('display', '');
+ spanProxy.css('display', '');
+
+ if (iframeDefault !== null) {
+ iframeDefault.remove();
+ }
+ if (iframeProxy !== null) {
+ iframeProxy.remove();
+ }
+
+ span.text('[Embed]');
} else {
- contents.detach();
+ let useProxy = e.target == spanProxy[0];
+
+ // 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);
- spanProxy.remove();
- videoNode.append(e.target == spanProxy[0] ? proxyEmbed : defaultEmbed);
}
});
- videoNode.append(span);
- videoNode.append(spanProxy);
+ node.append(span);
+ node.append(spanProxy);
}
$('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) {
$('div.video-container', post).each(addEmbedButton);
});
});
-
diff --git a/log.php b/log.php
index c48bae89..8d12478d 100644
--- a/log.php
+++ b/log.php
@@ -21,4 +21,4 @@ if (!isset($_GET['page'])) {
$page = (int)$_GET['page'];
};
-mod_board_log($board['uri'], $page, $hide_names, true);
+mod_board_log(Vichan\build_context($config), $board['uri'], $page, $hide_names, true);
diff --git a/mod.php b/mod.php
index c153b383..f08a465b 100644
--- a/mod.php
+++ b/mod.php
@@ -1,18 +1,21 @@
':?/', // redirect to dashboard
'/' => 'dashboard', // dashboard
'/confirm/(.+)' => 'confirm', // confirm action (if javascript didn't work)
@@ -65,8 +68,15 @@ $pages = array(
'/reports/(\d+)/dismiss(all)?' => 'secure report_dismiss', // dismiss a report
'/IP/([\w.:]+)' => 'secure_POST ip', // view ip address
+ '/IP/([\w.:]+)/cursor/([\w|-|_]+)' => 'secure_POST ip', // view ip address
'/IP/([\w.:]+)/remove_note/(\d+)' => 'secure ip_remove_note', // remove note from ip address
+ '/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
'/bans' => 'secure_POST bans', // ban list
'/bans.json' => 'secure bans_json', // ban list JSON
@@ -106,7 +116,6 @@ $pages = array(
// these pages aren't listed in the dashboard without $config['debug']
'/debug/antispam' => 'debug_antispam',
'/debug/recent' => 'debug_recent_posts',
- '/debug/apc' => 'debug_apc',
'/debug/sql' => 'secure_POST debug_sql',
// This should always be at the end:
@@ -120,14 +129,14 @@ $pages = array(
str_replace('%d', '(\d+)', preg_quote($config['file_page'], '!')) => 'view_thread',
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
- str_replace(array('%d','%s'), array('(\d+)', '[a-z0-9-]+'), preg_quote($config['file_page50_slug'], '!')) => 'view_thread50',
+ str_replace([ '%d','%s' ], [ '(\d+)', '[a-z0-9-]+' ], preg_quote($config['file_page50_slug'], '!')) => 'view_thread50',
'/(\%b)/' . preg_quote($config['dir']['res'], '!') .
- str_replace(array('%d','%s'), array('(\d+)', '[a-z0-9-]+'), preg_quote($config['file_page_slug'], '!')) => 'view_thread',
-);
+ str_replace([ '%d','%s' ], [ '(\d+)', '[a-z0-9-]+' ], preg_quote($config['file_page_slug'], '!')) => 'view_thread',
+];
if (!$mod) {
- $pages = array('!^(.+)?$!' => 'login');
+ $pages = [ '!^(.+)?$!' => 'login' ];
} elseif (isset($_GET['status'], $_GET['r'])) {
header('Location: ' . $_GET['r'], true, (int)$_GET['status']);
exit;
@@ -137,12 +146,11 @@ if (isset($config['mod']['custom_pages'])) {
$pages = array_merge($pages, $config['mod']['custom_pages']);
}
-$new_pages = array();
+$new_pages = [];
foreach ($pages as $key => $callback) {
if (is_string($callback) && preg_match('/^secure /', $callback)) {
$key .= '(/(?P[a-f0-9]{8}))?';
- }
-
+ }
$key = str_replace('\%b', '?P' . sprintf(substr($config['board_path'], 0, -1), $config['board_regex']), $key);
$new_pages[@$key[0] == '!' ? $key : '!^' . $key . '(?:&[^&=]+=[^&]*)*$!u'] = $callback;
}
@@ -150,7 +158,7 @@ $pages = $new_pages;
foreach ($pages as $uri => $handler) {
if (preg_match($uri, $query, $matches)) {
- $matches = array_slice($matches, 1);
+ $matches[0] = $ctx; // Replace the text captured by the full pattern with a reference to the context.
if (isset($matches['board'])) {
$board_match = $matches['board'];
@@ -170,7 +178,7 @@ foreach ($pages as $uri => $handler) {
if ($secure_post_only)
error($config['error']['csrf']);
else {
- mod_confirm(substr($query, 1));
+ mod_confirm($ctx, substr($query, 1));
exit;
}
}
@@ -185,24 +193,20 @@ foreach ($pages as $uri => $handler) {
}
if ($config['debug']) {
- $debug['mod_page'] = array(
+ $debug['mod_page'] = [
'req' => $query,
'match' => $uri,
'handler' => $handler,
- );
+ ];
$debug['time']['parse_mod_req'] = '~' . round((microtime(true) - $parse_start_time) * 1000, 2) . 'ms';
}
- if (is_array($matches)) {
- // we don't want to call named parameters (PHP 8)
- $matches = array_values($matches);
- }
+ // We don't want to call named parameters (PHP 8).
+ $matches = array_values($matches);
if (is_string($handler)) {
if ($handler[0] == ':') {
header('Location: ' . substr($handler, 1), true, $config['redirect_http']);
- } elseif (is_callable("mod_page_$handler")) {
- call_user_func_array("mod_page_$handler", $matches);
} elseif (is_callable("mod_$handler")) {
call_user_func_array("mod_$handler", $matches);
} else {
diff --git a/post.php b/post.php
index ae1361ce..5483600d 100644
--- a/post.php
+++ b/post.php
@@ -3,6 +3,10 @@
* Copyright (c) 2010-2014 Tinyboard Development Group
*/
+use Vichan\Context;
+use Vichan\Data\ReportQueries;
+use Vichan\Data\Driver\LogDriver;
+
require_once 'inc/bootstrap.php';
/**
@@ -35,35 +39,6 @@ function md5_hash_of_file($config, $file)
}
}
-/**
- * Strip the markup from the given string
- *
- * @param string $post_body The body of the post.
- * @return string
- */
-function strip_markup($post_body)
-{
- if (mysql_version() >= 50503) {
- // Assume we're using the utf8mb4 charset.
- return $post_body;
- } else {
- // MySQL's `utf8` charset only supports up to 3-byte symbols.
- // Remove anything >= 0x010000.
-
- $chars = preg_split('//u', $post_body, -1, PREG_SPLIT_NO_EMPTY);
- $res = '';
- foreach ($chars as $char) {
- $o = 0;
- $ord = ordutf8($char, $o);
- if ($ord >= 0x010000)
- continue;
- $res .= $char;
- }
-
- return $res;
- }
-}
-
/**
* Checks if the user at the remote ip passed the captcha.
*
@@ -167,6 +142,126 @@ function check_turnstile($secret, $response, $remote_ip, $expected_action)
return $json_ret['success'] === true && $json_ret['action'] === $expected_action;
}
+/**
+ * A "sophisticated" workaround to js/ajax.js calling post.php multiple times on error/ban.
+ */
+function check_captcha(array $captcha_config, string $form_id, string $board_uri, string $response, string $remote_ip, string $expected_action) {
+ $dynamic = $captcha_config['dynamic'];
+ if ($dynamic !== false && $remote_ip !== $dynamic) {
+ return true;
+ }
+
+ switch ($captcha_config['mode']) {
+ case 'recaptcha':
+ case 'hcaptcha':
+ case 'turnstile':
+ $mode = $captcha_config['mode'];
+ break;
+ case false:
+ return true;
+ default:
+ \error_log("Unknown captcha mode '{$captcha_config['mode']}'");
+ throw new \RuntimeException('Captcha configuration error');
+ }
+
+ $passthrough_timeout = $captcha_config['passthrough_timeout'];
+
+ if ($passthrough_timeout != 0) {
+ $pass = Cache::get("captcha_passthrough_{$remote_ip}_{$form_id}");
+ if ($pass !== false) {
+ $let_through = $pass['expires'] > time() && $pass['board_uri'] === $board_uri && $pass['captcha_response'] === $response;
+ if ($let_through) {
+ return true;
+ }
+ }
+ }
+
+ $remote_ip_send = $dynamic !== false ? null : $remote_ip;
+ $private_key = $captcha_config[$mode]['private'];
+ $public_key = $captcha_config[$mode]['public'];
+ switch ($mode) {
+ case 'recaptcha':
+ $ret = check_recaptcha($private_key, $response, $remote_ip_send);
+ break;
+ case 'hcaptcha':
+ $ret = check_hcaptcha($private_key, $response, $remote_ip_send, $public_key);
+ break;
+ case 'turnstile':
+ $ret = check_turnstile($private_key, $response, $remote_ip_send, $expected_action);
+ break;
+ }
+
+ if ($ret && $passthrough_timeout != 0) {
+ $pass = [
+ 'expires' => time() + $passthrough_timeout,
+ 'board_uri' => $board_uri,
+ 'captcha_response' => $response
+ ];
+
+ Cache::set("captcha_passthrough_{$remote_ip}_{$form_id}", $pass, $passthrough_timeout + 2);
+ }
+
+ return $ret;
+}
+
+function send_matrix_report(
+ string $matrix_host,
+ string $room_id,
+ string $access_token,
+ int $max_msg_len,
+ string $report_reason,
+ string $domain,
+ string $board_dir,
+ string $board_res_dir,
+ array $post,
+ int $id,
+) {
+ $post_id = $post['thread'] ? $post['thread'] : $id;
+
+ $reported_post_url = "$domain/mod.php?/{$board_dir}{$board_res_dir}{$post_id}.html";
+
+ $end = strlen($post['body_nomarkup']) > $max_msg_len ? ' [...]' : '';
+ $post_content = mb_substr($post['body_nomarkup'], 0, $max_msg_len) . $end;
+ $text_body = $reported_post_url . ($post['thread'] ? "#$id" : '') . " \nReason:\n" . $report_reason . " \nPost:\n" . $post_content;
+
+ $random_transaction_id = mt_rand();
+ $json_body = json_encode([
+ 'msgtype' => 'm.text',
+ 'body' => $text_body
+ ]);
+
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => "$matrix_host/_matrix/client/v3/rooms/$room_id/send/m.room.message/$random_transaction_id",
+ CURLOPT_CUSTOMREQUEST => 'PUT',
+ CURLOPT_HTTPHEADER => [
+ 'Content-Type: application/json',
+ "Authorization: Bearer $access_token"
+ ],
+ CURLOPT_POSTFIELDS => $json_body,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_TIMEOUT => 3,
+ ]);
+ $c_ret = curl_exec($ch);
+ if ($c_ret === false) {
+ $err_no = curl_errno($ch);
+ $err_str = curl_error($ch);
+
+ error_log("Failed to send report to matrix. Curl returned: $err_no ($err_str)");
+ return false;
+ }
+ curl_close($ch);
+
+ $json = json_decode($c_ret, true);
+ if ($json === null) {
+ error_log("Report forwarding failed, matrix returned a non-json value");
+ } elseif (!isset($json["event_id"])) {
+ $code = $json["errcode"] ?? '';
+ $desc = $json["error"] ?? '';
+ error_log("Report forwarding failed, matrix returned code '$code', with description '$desc'");
+ }
+}
+
/**
* Deletes the (single) captcha associated with the ip and code.
*
@@ -225,26 +320,6 @@ function db_select_post_minimal($board, $id)
return $post;
}
-/**
- * Inserts a new report.
- *
- * @param string $ip Ip of the user sending the report.
- * @param string $board Board of the reported thread. MUST ALREADY BE SANITIZED.
- * @param int $post_id Post reported.
- * @param string $reason Reason of the report.
- * @return void
- */
-function db_insert_report($ip, $board, $post_id, $reason)
-{
- $query = prepare("INSERT INTO ``reports`` VALUES (NULL, :time, :ip, :board, :post, :reason)");
- $query->bindValue(':time', time(), PDO::PARAM_INT);
- $query->bindValue(':ip', $ip, PDO::PARAM_STR);
- $query->bindValue(':board', $board, PDO::PARAM_STR);
- $query->bindValue(':post', $post_id, PDO::PARAM_INT);
- $query->bindValue(':reason', $reason, PDO::PARAM_STR);
- $query->execute() or error(db_error($query));
-}
-
/**
* Inserts a new ban appeal into the database.
*
@@ -280,14 +355,14 @@ function db_select_ban_appeals($ban_id)
$dropped_post = false;
-function handle_nntpchan()
+function handle_nntpchan(Context $ctx)
{
global $config;
if ($_SERVER['REMOTE_ADDR'] != $config['nntpchan']['trusted_peer']) {
error("NNTPChan: Forbidden. $_SERVER[REMOTE_ADDR] is not a trusted peer");
}
- $_POST = array();
+ $_POST = [];
$_POST['json_response'] = true;
$headers = json_encode($_GET);
@@ -359,11 +434,11 @@ function handle_nntpchan()
if ($ct == 'text/plain') {
$content = file_get_contents("php://input");
} elseif ($ct == 'multipart/mixed' || $ct == 'multipart/form-data') {
- _syslog(LOG_INFO, "MM: Files: " . print_r($GLOBALS, true)); // Debug
+ $ctx->get(LogDriver::class)->log(LogDriver::DEBUG, 'MM: Files: ' . print_r($GLOBALS, true));
$content = '';
- $newfiles = array();
+ $newfiles = [];
foreach ($_FILES['attachment']['error'] as $id => $error) {
if ($_FILES['attachment']['type'][$id] == 'text/plain') {
$content .= file_get_contents($_FILES['attachment']['tmp_name'][$id]);
@@ -371,7 +446,7 @@ function handle_nntpchan()
// Signed message, ignore for now
} else {
// A real attachment :^)
- $file = array();
+ $file = [];
$file['name'] = $_FILES['attachment']['name'][$id];
$file['type'] = $_FILES['attachment']['type'][$id];
$file['size'] = $_FILES['attachment']['size'][$id];
@@ -415,7 +490,7 @@ function handle_nntpchan()
if (count($ary) == 0) {
return ">>>>$id";
} else {
- $ret = array();
+ $ret = [];
foreach ($ary as $v) {
if ($v['board'] != $xboard) {
$ret[] = ">>>/" . $v['board'] . "/" . $v['id'];
@@ -438,7 +513,7 @@ function handle_nntpchan()
);
}
-function handle_delete()
+function handle_delete(Context $ctx)
{
// Delete
global $config, $board, $mod;
@@ -446,7 +521,7 @@ function handle_delete()
error($config['error']['bot']);
}
- check_login(false);
+ check_login($ctx, false);
$is_mod = !!$mod;
if (isset($_POST['mod']) && $_POST['mod'] && !$mod) {
@@ -456,11 +531,13 @@ function handle_delete()
$password = &$_POST['password'];
- if ($password == '') {
+ if (empty($password)) {
error($config['error']['invalidpassword']);
}
- $delete = array();
+ $password = hashPassword($_POST['password']);
+
+ $delete = [];
foreach ($_POST as $post => $value) {
if (preg_match('/^delete_(\d+)$/', $post, $m)) {
$delete[] = (int) $m[1];
@@ -534,8 +611,8 @@ function handle_delete()
modLog("User at $ip deleted his own post #$id");
}
- _syslog(
- LOG_INFO,
+ $ctx->get(LogDriver::class)->log(
+ LogDriver::INFO,
'Deleted post: ' .
'/' . $board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $post['thread'] ? $post['thread'] : $id) . ($post['thread'] ? '#' . $id : '')
);
@@ -562,13 +639,13 @@ function handle_delete()
rebuildThemes('post-delete', $board['uri']);
}
-function handle_report()
+function handle_report(Context $ctx)
{
global $config, $board;
if (!isset($_POST['board'], $_POST['reason']))
error($config['error']['bot']);
- $report = array();
+ $report = [];
foreach ($_POST as $post => $value) {
if (preg_match('/^delete_(\d+)$/', $post, $m)) {
$report[] = (int) $m[1];
@@ -618,12 +695,12 @@ function handle_report()
$reason = escape_markup_modifiers($_POST['reason']);
markup($reason);
+ $report_queries = $ctx->get(ReportQueries::class);
+
foreach ($report as $id) {
$post = db_select_post_minimal($board['uri'], $id);
if ($post === false) {
- if ($config['syslog']) {
- _syslog(LOG_INFO, "Failed to report non-existing post #{$id} in {$board['dir']}");
- }
+ $ctx->get(LogDriver::class)->log(LogDriver::INFO, "Failed to report non-existing post #{$id} in {$board['dir']}");
error($config['error']['nopost']);
}
@@ -638,15 +715,14 @@ function handle_report()
error($error);
}
- if ($config['syslog'])
- _syslog(
- LOG_INFO,
- 'Reported post: ' .
- '/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
- ' for "' . $reason . '"'
- );
+ $ctx->get(LogDriver::class)->log(
+ LogDriver::INFO,
+ 'Reported post: ' .
+ '/' . $board['dir'] . $config['dir']['res'] . link_for($post) . ($post['thread'] ? '#' . $id : '') .
+ ' for "' . $reason . '"'
+ );
- db_insert_report($_SERVER['REMOTE_ADDR'], $board['uri'], $id, $reason);
+ $report_queries->add($_SERVER['REMOTE_ADDR'], $board['uri'], $id, $reason);
if ($config['slack']) {
function slack($message, $room = "reports", $icon = ":no_entry_sign:")
@@ -680,25 +756,19 @@ function handle_report()
}
- if (isset($config['matrix'])) {
- $reported_post_url = $config['domain'] . "/mod.php?/" . $board['dir'] . $config['dir']['res'] . ($post['thread'] ? $post['thread'] : $id) . ".html";
- $post_url = $config['matrix']['host'] . "/_matrix/client/r0/rooms/" . $config['matrix']['room_id'] . "/send/m.room.message?access_token=" . $config['matrix']['access_token'];
-
- $trimmed_post = strlen($post['body_nomarkup']) > $config['matrix']['max_message_length'] ? ' [...]' : '';
- $postcontent = mb_substr($post['body_nomarkup'], 0, $config['matrix']['max_message_length']) . $trimmed_post;
- $matrix_message = $reported_post_url . ($post['thread'] ? '#' . $id : '') . " \nReason:\n" . $reason . " \nPost:\n" . $postcontent . " \n";
- $post_data = json_encode(
- array(
- "msgtype" => "m.text",
- "body" => $matrix_message
- )
+ if ($config['matrix']['enabled']) {
+ send_matrix_report(
+ $config['matrix']['host'],
+ $config['matrix']['room_id'],
+ $config['matrix']['access_token'],
+ $config['matrix']['max_message_length'],
+ $reason,
+ $config['domain'],
+ $board['dir'],
+ $config['dir']['res'],
+ $post,
+ $id
);
-
- $ch = curl_init($post_url);
- curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- $postResult = curl_exec($ch);
- curl_close($ch);
}
}
@@ -717,7 +787,7 @@ function handle_report()
}
}
-function handle_post()
+function handle_post(Context $ctx)
{
global $config, $dropped_post, $board, $mod, $pdo;
@@ -725,7 +795,7 @@ function handle_post()
error($config['error']['bot']);
}
- $post = array('board' => $_POST['board'], 'files' => array());
+ $post = array('board' => $_POST['board'], 'files' => []);
// Check if board exists
if (!openBoard($post['board'])) {
@@ -765,56 +835,16 @@ function handle_post()
if (!$dropped_post) {
- if ($config['dynamic_captcha'] !== false) {
- if ($_SERVER['REMOTE_ADDR'] === $config['dynamic_captcha']) {
- if ($config['recaptcha']) {
- if (!isset($_POST['g-recaptcha-response'])) {
- error($config['error']['bot']);
- }
- if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], null)) {
- error($config['error']['captcha']);
- }
- } elseif ($config['hcaptcha']) {
- if (!isset($_POST['h-captcha-response'])) {
- error($config['error']['bot']);
- }
- if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], null, $config['hcaptcha_public'])) {
- error($config['error']['captcha']);
- }
- } elseif ($config['turnstile']) {
- if (!isset($_POST['cf-turnstile-response'])) {
- error($config['error']['bot']);
- }
- $expected_action = $post['op'] ? 'post-thread' : 'post-reply';
- if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) {
- error($config['error']['captcha']);
- }
- }
+ // Check for CAPTCHA right after opening the board so the "return" link is in there.
+ if ($config['captcha']['mode'] !== false) {
+ if (!isset($_POST['captcha-response'], $_POST['captcha-form-id'])) {
+ error($config['error']['bot']);
}
- } else {
- // Check for CAPTCHA right after opening the board so the "return" link is in there.
- if ($config['recaptcha']) {
- if (!isset($_POST['g-recaptcha-response'])) {
- error($config['error']['bot']);
- }
- if (!check_recaptcha($config['recaptcha_private'], $_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR'])) {
- error($config['error']['captcha']);
- }
- } elseif ($config['hcaptcha']) {
- if (!isset($_POST['h-captcha-response'])) {
- error($config['error']['bot']);
- }
- if (!check_hcaptcha($config['hcaptcha_private'], $_POST['h-captcha-response'], $_SERVER['REMOTE_ADDR'], $config['hcaptcha_public'])) {
- error($config['error']['captcha']);
- }
- } elseif ($config['turnstile']) {
- if (!isset($_POST['cf-turnstile-response'])) {
- error($config['error']['bot']);
- }
- $expected_action = $post['op'] ? 'post-thread' : 'post-reply';
- if (!check_turnstile($config['turnstile_private'], $_POST['cf-turnstile-response'], null, $expected_action)) {
- error($config['error']['captcha']);
- }
+
+ $expected_action = $post['op'] ? 'post-thread' : 'post-reply';
+ $ret = check_captcha($config['captcha'], $_POST['captcha-form-id'], $post['board'], $_POST['captcha-response'], $_SERVER['REMOTE_ADDR'], $expected_action);
+ if (!$ret) {
+ error($config['error']['captcha']);
}
}
@@ -858,8 +888,15 @@ function handle_post()
// Check if banned
checkBan($board['uri']);
+ if ($config['op_require_history'] && $post['op'] && !isIPv6()) {
+ $has_any = has_any_history($_SERVER['REMOTE_ADDR'], $_POST['password']);
+ if (!$has_any) {
+ error($config['error']['opnohistory']);
+ }
+ }
+
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) {
- check_login(false);
+ check_login($ctx, false);
if (!$mod) {
// Liar. You're not a mod >:-[
error($config['error']['notamod']);
@@ -972,11 +1009,16 @@ function handle_post()
}
}
+ // 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['subject'] = $_POST['subject'];
$post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email']));
$post['body'] = $_POST['body'];
- $post['password'] = $_POST['password'];
+ $post['password'] = hashPassword($_POST['password']);
$post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || count($_FILES) > 0));
if (!$dropped_post) {
@@ -1150,14 +1192,22 @@ function handle_post()
if (mb_strlen($post['subject']) > 100) {
error(sprintf($config['error']['toolong'], 'subject'));
}
- if (!$mod && mb_strlen($post['body']) > $config['max_body']) {
- error($config['error']['toolong_body']);
- }
- if (!$mod && mb_strlen($post['body']) > 0 && (mb_strlen($post['body']) < $config['min_body'])) {
- error($config['error']['tooshort_body']);
- }
- if (mb_strlen($post['password']) > 20) {
- error(sprintf($config['error']['toolong'], 'password'));
+ if (!$mod) {
+ $body_mb_len = mb_strlen($post['body']);
+ $is_op = $post['op'];
+
+ if (($is_op && $config['force_body_op']) || (!$is_op && $config['force_body'])) {
+ $min_body = $is_op ? $config['min_body_op'] : $config['min_body'];
+
+ if ($body_mb_len < $min_body) {
+ error($config['error']['tooshort_body']);
+ }
+ }
+
+ $max_body = $is_op ? $config['max_body_op'] : $config['max_body'];
+ if ($body_mb_len > $max_body) {
+ error($config['error']['toolong_body']);
+ }
}
}
@@ -1222,7 +1272,7 @@ function handle_post()
}
}
- $post['body_nomarkup'] = strip_markup($post['body']);
+ $post['body_nomarkup'] = $post['body'];
$post['tracked_cites'] = markup($post['body'], true);
if ($post['has_file']) {
@@ -1263,7 +1313,7 @@ function handle_post()
if (!hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) {
require_once 'inc/filters.php';
- do_filters($post);
+ do_filters($ctx, $post);
}
if ($post['has_file']) {
@@ -1352,7 +1402,7 @@ function handle_post()
$file['thumbwidth'] = $size[0];
$file['thumbheight'] = $size[1];
} elseif (
- $config['minimum_copy_resize'] &&
+ (($config['strip_exif'] && isset($file['exif_stripped']) && $file['exif_stripped']) || !$config['strip_exif']) &&
$image->size->width <= $config['thumb_width'] &&
$image->size->height <= $config['thumb_height'] &&
$file['extension'] == ($config['thumb_ext'] ? $config['thumb_ext'] : $file['extension'])
@@ -1432,7 +1482,7 @@ function handle_post()
// getting all images and then choosing one.
for( $i = 0; $i < $zip->numFiles; $i++ ){
$stat = $zip->statIndex( $i );
- $matches = array();
+ $matches = [];
if (preg_match('/.*cover.*\.(jpg|jpeg|png)/', $stat['name'], $matches)) {
$filename = $matches[0];
break;
@@ -1501,35 +1551,6 @@ function handle_post()
}
}
- 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'] .= "" . htmlspecialchars($value) . " ";
- }
- }
- }
-
if (!isset($dont_copy_file) || !$dont_copy_file) {
if (isset($file['file_tmp'])) {
if (!@rename($file['tmp_name'], $file['file'])) {
@@ -1577,11 +1598,6 @@ function handle_post()
}
}
- // 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) {
undoImage($post);
if ($config['robot_mute']) {
@@ -1623,10 +1639,6 @@ function handle_post()
$post = (array) $post;
- if ($post['files']) {
- $post['files'] = $post['files'];
- }
-
$post['num_files'] = sizeof($post['files']);
$post['id'] = $id = post($post);
@@ -1683,7 +1695,7 @@ function handle_post()
}
if (isset($post['tracked_cites']) && !empty($post['tracked_cites'])) {
- $insert_rows = array();
+ $insert_rows = [];
foreach ($post['tracked_cites'] as $cite) {
$insert_rows[] = '(' .
$pdo->quote($board['uri']) . ', ' . (int) $id . ', ' .
@@ -1701,7 +1713,7 @@ function handle_post()
if (isset($_COOKIE[$config['cookies']['js']])) {
$js = json_decode($_COOKIE[$config['cookies']['js']]);
} else {
- $js = (object) array();
+ $js = (object) [];
}
// Tell it to delete the cached post for referer
$js->{$_SERVER['HTTP_REFERER']} = true;
@@ -1735,10 +1747,10 @@ function handle_post()
buildThread($post['op'] ? $id : $post['thread']);
- if ($config['syslog']) {
- _syslog(LOG_INFO, 'New post: /' . $board['dir'] . $config['dir']['res'] .
- link_for($post) . (!$post['op'] ? '#' . $id : ''));
- }
+ $ctx->get(LogDriver::class)->log(
+ LogDriver::INFO,
+ 'New post: /' . $board['dir'] . $config['dir']['res'] . link_for($post) . (!$post['op'] ? '#' . $id : '')
+ );
if (!$post['mod']) {
header('X-Associated-Content: "' . $redirect . '"');
@@ -1787,7 +1799,7 @@ function handle_post()
}
}
-function handle_appeal()
+function handle_appeal(Context $ctx)
{
global $config;
if (!isset($_POST['ban_id'])) {
@@ -1831,23 +1843,25 @@ function handle_appeal()
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.
if (isset($_GET['Newsgroups'])) {
if ($config['nntpchan']['enabled']) {
- handle_nntpchan();
+ handle_nntpchan($ctx);
} else {
error("NNTPChan: NNTPChan support is disabled");
}
}
if (isset($_POST['delete'])) {
- handle_delete();
+ handle_delete($ctx);
} elseif (isset($_POST['report'])) {
- handle_report();
+ handle_report($ctx);
} elseif (isset($_POST['post']) || $dropped_post) {
- handle_post();
+ handle_post($ctx);
} elseif (isset($_POST['appeal'])) {
- handle_appeal();
+ handle_appeal($ctx);
} else {
if (!file_exists($config['has_installed'])) {
header('Location: install.php', true, $config['redirect_http']);
diff --git a/spooks.php b/spooks.php
new file mode 100644
index 00000000..f7c146a6
--- /dev/null
+++ b/spooks.php
@@ -0,0 +1,8 @@
+ p {
width: 0px;
min-width: 100%;
@@ -429,19 +434,18 @@ img.banner,img.board_image {
.post-image {
display: block;
float: left;
- margin: 5px 20px 10px 20px;
border: none;
}
.full-image {
float: left;
- padding: 5px;
+ padding: 0.2em 0.2em 0.8em 0.2em;
margin: 0 20px 0 0;
max-width: 98%;
}
div.post .post-image {
- padding: 0.2em;
+ padding: 0.2em 0.2em 0.8em 0.2em;
margin: 0 20px 0 0;
}
@@ -538,8 +542,8 @@ div.post {
}
}
-div.post > div.head {
- margin: 0.1em 1em;
+div.post div.head {
+ margin: 0.1em 1em 0.8em 1.4em;
clear: both;
line-height: 1.3em;
}
@@ -564,17 +568,11 @@ div.post.op > p {
}
div.post div.body {
- margin-top: 0.8em;
+ margin-left: 1.4em;
padding-right: 3em;
padding-bottom: 0.3em;
-}
-div.post.op div.body {
- margin-left: 0.8em;
-}
-
-div.post.reply div.body {
- margin-left: 1.8em;
+ white-space: pre-wrap;
}
div.post.reply.highlighted {
@@ -585,19 +583,9 @@ div.post.reply div.body a {
color: #D00;
}
-div.post div.body {
- white-space: pre-wrap;
-}
-
div.post.op {
padding-top: 0px;
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 {
@@ -648,6 +636,7 @@ span.trip {
span.omitted {
display: block;
margin-top: 1em;
+ margin-left: 0.4em;
}
br.clear {
@@ -832,7 +821,7 @@ span.public_ban {
span.public_warning {
display: block;
- color: steelblue;
+ color: orange;
font-weight: bold;
margin-top: 15px;
}
@@ -923,10 +912,14 @@ form.ban-appeal textarea {
display:inline!important;
}
pre {
- margin:0
+ margin: 0;
display: inline!important;
}
+.theme-catalog .controls > span {
+ margin-right: 1em;
+}
+
.theme-catalog div.thread img {
float: none!important;
margin: auto;
@@ -936,13 +929,20 @@ pre {
border: 2px solid rgba(153,153,153,0);
}
+/* Still for the catalog theme */
+#Grid {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-content: center;
+ gap: 0.2em;
+}
+
.theme-catalog div.thread {
display: inline-block;
vertical-align: top;
text-align: center;
font-weight: normal;
- margin-top: 2px;
- margin-bottom: 2px;
padding: 2px;
height: 300px;
width: 205px;
@@ -961,7 +961,6 @@ pre {
.theme-catalog div.threads {
text-align: center;
- margin-left: -20px;
}
.theme-catalog div.thread:hover {
diff --git a/stylesheets/terminal2.css b/stylesheets/terminal2.css
index d4d52847..9add30ea 100644
--- a/stylesheets/terminal2.css
+++ b/stylesheets/terminal2.css
@@ -48,6 +48,11 @@ div.post.reply.highlighted {
background: transparent;
border: transparent 1px dashed;
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 {
color: #00FF00;
diff --git a/stylesheets/terminal_common.css b/stylesheets/terminal_common.css
index c7f34f51..53d04ca2 100644
--- a/stylesheets/terminal_common.css
+++ b/stylesheets/terminal_common.css
@@ -69,6 +69,11 @@ div.post.reply {
div.post.reply.highlighted {
background: transparent;
border: transparent 1px dotted;
+
+ @media (max-width: 48em) {
+ border-left-style: none;
+ border-right-style: none;
+ }
}
p.intro span.subject {
font-size: 12px;
diff --git a/stylesheets/test.css b/stylesheets/test.css
index 162e9c26..6835b23f 100644
--- a/stylesheets/test.css
+++ b/stylesheets/test.css
@@ -132,6 +132,11 @@ line-height: 1.4;
div.post.reply.highlighted {
background: #555;
border: transparent 1px solid;
+
+ @media (max-width: 48em) {
+ border-left-style: none;
+ border-right-style: none;
+ }
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #CCCCCC;
diff --git a/stylesheets/tsuki.css b/stylesheets/tsuki.css
index bffa78c3..523bdb71 100644
--- a/stylesheets/tsuki.css
+++ b/stylesheets/tsuki.css
@@ -5,15 +5,15 @@ color change by kalyx
@import url("/stylesheets/dark.css");
@font-face
{
- font-family: "DejaVuSansMono";
- src: url("/stylesheets/fonts/DejaVuSansMono.ttf") format("truetype");
+ font-family: "DejaVuSansMono";
+ src: url("/stylesheets/fonts/DejaVuSansMono.ttf") format("truetype");
}
@font-face
{
- font-family: 'lain';
- src: url('./fonts/nrdyyh.woff') format('woff'),
- url('./fonts/tojcxo.TTF') format('truetype');
+ font-family: 'lain';
+ src: url('./fonts/nrdyyh.woff') format('woff'),
+ url('./fonts/tojcxo.TTF') format('truetype');
}
/**
* volafile.css fuck you
@@ -22,15 +22,15 @@ color change by kalyx
@import url("/stylesheets/dark.css");
@font-face
{
- font-family: "DejaVuSansMono";
- src: url("/stylesheets/fonts/DejaVuSansMono.ttf") format("truetype");
+ font-family: "DejaVuSansMono";
+ src: url("/stylesheets/fonts/DejaVuSansMono.ttf") format("truetype");
}
@font-face
{
- font-family: 'lain';
- src: url('./fonts/nrdyyh.woff') format('woff'),
- url('./fonts/tojcxo.TTF') format('truetype');
+ font-family: 'lain';
+ src: url('./fonts/nrdyyh.woff') format('woff'),
+ url('./fonts/tojcxo.TTF') format('truetype');
}
@@ -40,419 +40,419 @@ color change by kalyx
}
.hidden {
- display:none;
+ display:none;
}
a,a:visited {
- text-decoration: underline;
- color: #34345C;
+ text-decoration: underline;
+ color: #34345C;
}
a:hover,.intro a.post_no:hover {
- color: #ff0000;
+ color: #ff0000;
}
a.post_no {
- text-decoration: none;
- margin: 0;
- padding: 0;
+ text-decoration: none;
+ margin: 0;
+ padding: 0;
}
.intro a.post_no {
- color: inherit;
+ color: inherit;
}
.intro a.post_no,p.intro a.email,p.intro a.post_anchor {
- margin: 0;
+ margin: 0;
}
.intro a.email span.name {
- color: #34345C;
+ color: #34345C;
}
.intro a.email:hover span.name {
- color: #ff0000;
+ color: #ff0000;
}
.intro label {
- display: inline;
+ display: inline;
}
.intro time,p.intro a.ip-link,p.intro a.capcode {
- direction: ltr;
- unicode-bidi: embed;
+ direction: ltr;
+ unicode-bidi: embed;
}
h2 {
- color: #AF0A0F;
- font-size: 11pt;
- margin: 0;
- padding: 0;
+ color: #AF0A0F;
+ font-size: 11pt;
+ margin: 0;
+ padding: 0;
}
header {
- margin: 1em 0;
+ margin: 1em 0;
}
h1 {
- font-family: tahoma;
- letter-spacing: -2px;
- font-size: 20pt;
- margin: 0;
+ font-family: tahoma;
+ letter-spacing: -2px;
+ font-size: 20pt;
+ margin: 0;
}
header div.subtitle,h1 {
- color: #888;
- text-align: center;
+ color: #888;
+ text-align: center;
}
header div.subtitle {
- font-size: 16px;
+ font-size: 16px;
}
form {
- margin-bottom: 4em;
+ margin-bottom: 4em;
}
form table {
- margin: auto;
+ margin: auto;
}
form table input {
- height: auto;
+ height: auto;
}
input[type="text"],input[type="password"],textarea {
- border: 1px solid #a9a9a9;
- text-indent: 0;
- text-shadow: none;
- text-transform: none;
- word-spacing: normal;
- max-width: 100%;
+ border: 1px solid #a9a9a9;
+ text-indent: 0;
+ text-shadow: none;
+ text-transform: none;
+ word-spacing: normal;
+ max-width: 100%;
}
#quick-reply input[type="text"], input[type="password"], #quick-reply textarea {
- max-width: 100%;
+ max-width: 100%;
}
textarea {
- width: 100%;
+ width: 100%;
}
form table tr td {
- text-align: left;
- margin: 0;
- padding: 0;
+ text-align: left;
+ margin: 0;
+ padding: 0;
}
form table.mod tr td {
- padding: 2px;
+ padding: 2px;
}
form table tr th {
- text-align: left;
- padding: 4px;
+ text-align: left;
+ padding: 4px;
}
form table tr th {
- background: #98E;
+ background: #98E;
}
form table tr td div.center {
- text-align: center;
- float: left;
- padding-left: 3px;
+ text-align: center;
+ float: left;
+ padding-left: 3px;
}
form table tr td div input {
- display: block;
- margin: 2px auto 0 auto;
+ display: block;
+ margin: 2px auto 0 auto;
}
form table tr td div label {
- font-size: 16px;
+ font-size: 16px;
}
.unimportant,.unimportant * {
- font-size: 16px;
+ font-size: 16px;
}
.file {
- float: left;
- margin-right: 2px;
+ float: left;
+ margin-right: 2px;
}
.file:not(.multifile) .post-image {
- float: left;
+ float: left;
}
.file:not(.multifile) {
- float: none;
+ float: none;
}
p.fileinfo {
- display: block;
- margin: 0 0 0 20px;
+ display: block;
+ margin: 0 0 0 20px;
}
div.post p.fileinfo {
- padding-left: 5px;
+ padding-left: 5px;
}
div.banner {
- background-color: #E04000;
- font-size: 16pt;
- font-weight: bold;
- text-align: center;
- margin: 1em 0;
+ background-color: #E04000;
+ font-size: 16pt;
+ font-weight: bold;
+ text-align: center;
+ margin: 1em 0;
}
div.banner,div.banner a {
- color: white;
+ color: white;
}
div.banner a:hover {
- color: #EEF2FF;
- text-decoration: none;
+ color: #EEF2FF;
+ text-decoration: none;
}
img.banner,img.board_image {
- display: block;
- border: 1px solid #a9a9a9;
- margin: 12px auto 0 auto;
+ display: block;
+ border: 1px solid #a9a9a9;
+ margin: 12px auto 0 auto;
}
.post-image {
- display: block;
- float: left;
- margin: 5px 20px 10px 20px;
- border: none;
+ display: block;
+ float: left;
+ margin: 5px 20px 10px 20px;
+ border: none;
}
.full-image {
- max-width: 98%;
+ max-width: 98%;
}
div.post .post-image {
- padding: 5px;
- margin: 0 20px 0 0;
+ padding: 5px;
+ margin: 0 20px 0 0;
}
div.post img.icon {
- display: inline;
- margin: 0 5px;
- padding: 0;
+ display: inline;
+ margin: 0 5px;
+ padding: 0;
}
div.post i.fa {
- margin: 0 4px;
- font-size: 16px;
+ margin: 0 4px;
+ font-size: 16px;
}
div.post.op {
- margin-right: 20px;
- margin-bottom: 5px;
+ margin-right: 20px;
+ margin-bottom: 5px;
}
div.post.op hr {
- border-color: #D9BFB7;
+ border-color: #D9BFB7;
}
.intro {
- margin: 0.5em 0;
- padding: 0;
- padding-bottom: 0.2em;
+ margin: 0.5em 0;
+ padding: 0;
+ padding-bottom: 0.2em;
}
input.delete {
- float: left;
- margin: 1px 6px 0 0;
+ float: left;
+ margin: 1px 6px 0 0;
}
.intro span.subject {
- color: #0F0C5D;
- font-weight: bold;
+ color: #0F0C5D;
+ font-weight: bold;
}
.intro span.name {
- color: #117743;
- font-weight: bold;
+ color: #117743;
+ font-weight: bold;
}
.intro span.capcode,p.intro a.capcode,p.intro a.nametag {
- color: #F00000;
- margin-left: 0;
+ color: #F00000;
+ margin-left: 0;
}
.intro a {
- margin-left: 8px;
+ margin-left: 8px;
}
div.delete {
- float: right;
+ float: right;
}
div.post.reply p {
- margin: 0.3em 0 0 0;
+ margin: 0.3em 0 0 0;
}
div.post.reply div.body {
- margin-left: 1.8em;
- margin-top: 0.8em;
- padding-right: 3em;
- padding-bottom: 0.3em;
+ margin-left: 1.8em;
+ margin-top: 0.8em;
+ padding-right: 3em;
+ padding-bottom: 0.3em;
}
div.post.reply.highlighted {
- background: #D6BAD0;
+ background: #D6BAD0;
}
div.post.reply div.body a {
- color: #D00;
+ color: #D00;
}
div.post {
- padding-left: 20px;
+ padding-left: 20px;
}
div.post div.body {
- word-wrap: break-word;
- white-space: pre-wrap;
+ word-wrap: break-word;
+ white-space: pre-wrap;
}
div.post.reply {
- background: #D6DAF0;
- margin: 0.2em 4px;
- padding: 0.2em 0.3em 0.5em 0.6em;
- border-width: 1px;
- border-style: none solid solid none;
- border-color: #B7C5D9;
- display: inline-block;
- max-width: 94%!important;
+ background: #D6DAF0;
+ margin: 0.2em 4px;
+ padding: 0.2em 0.3em 0.5em 0.6em;
+ border-width: 1px;
+ border-style: none solid solid none;
+ border-color: #B7C5D9;
+ display: inline-block;
+ max-width: 94%!important;
- @media (max-width: 48em) {
- border-left-style: none;
- border-right-style: none;
- }
+ @media (max-width: 48em) {
+ border-left-style: none;
+ border-right-style: none;
+ }
}
span.trip {
- color: #228854;
+ color: #228854;
}
span.quote {
- color: #789922;
+ color: #789922;
}
span.omitted {
- display: block;
- margin-top: 1em;
+ display: block;
+ margin-top: 1em;
}
br.clear {
- clear: left;
- display: block;
+ clear: left;
+ display: block;
}
div.controls {
- float: right;
- margin: 0;
- padding: 0;
- font-size: 80%;
+ float: right;
+ margin: 0;
+ padding: 0;
+ font-size: 80%;
}
div.controls.op {
- float: none;
- margin-left: 10px;
+ float: none;
+ margin-left: 10px;
}
div.controls a {
- margin: 0;
+ margin: 0;
}
div#wrap {
- width: 900px;
- margin: 0 auto;
+ width: 900px;
+ margin: 0 auto;
}
div.ban {
- background: white;
- border: 1px solid #98E;
- max-width: 700px;
- margin: 30px auto;
+ background: white;
+ border: 1px solid #98E;
+ max-width: 700px;
+ margin: 30px auto;
}
div.ban p,div.ban h2 {
- padding: 3px 7px;
+ padding: 3px 7px;
}
div.ban h2 {
- background: #98E;
- color: black;
- font-size: 16pt;
+ background: #98E;
+ color: black;
+ font-size: 16pt;
}
div.ban p {
- font-size: 16px;
- margin-bottom: 12px;
+ font-size: 16px;
+ margin-bottom: 12px;
}
div.ban p.reason {
- font-weight: bold;
+ font-weight: bold;
}
span.heading {
- color: #AF0A0F;
- font-size: 11pt;
- font-weight: bold;
+ color: #AF0A0F;
+ font-size: 11pt;
+ font-weight: bold;
}
span.spoiler {
- background: black;
- color: black;
- padding: 0 1px;
+ background: black;
+ color: black;
+ padding: 0 1px;
}
div.post.reply div.body span.spoiler a {
- color: black;
+ color: black;
}
span.spoiler:hover,div.post.reply div.body span.spoiler:hover a {
- color: white;
+ color: white;
}
div.styles {
- float: right;
- padding-bottom: 20px;
+ float: right;
+ padding-bottom: 20px;
}
div.styles a {
- margin: 0 10px;
+ margin: 0 10px;
}
div.styles a.selected {
- text-decoration: none;
+ text-decoration: none;
}
table.test {
- width: 100%;
+ width: 100%;
}
table.test td,table.test th {
- text-align: left;
- padding: 5px;
+ text-align: left;
+ padding: 5px;
}
table.test tr.h th {
- background: #98E;
+ background: #98E;
}
table.test td img {
- margin: 0;
+ margin: 0;
}
fieldset label {
- display: block;
+ display: block;
}
div.pages {
@@ -466,524 +466,524 @@ div.pages {
}
div.pages.top {
- display: block;
- padding: 5px 8px;
- margin-bottom: 5px;
- position: fixed;
- top: 0;
- right: 0;
- opacity: 0.9;
+ display: block;
+ padding: 5px 8px;
+ margin-bottom: 5px;
+ position: fixed;
+ top: 0;
+ right: 0;
+ opacity: 0.9;
}
@media screen and (max-width: 800px) {
- div.pages.top {
- display: none!important;
- }
+ div.pages.top {
+ display: none!important;
+ }
}
div.pages a.selected {
- color: black;
- font-weight: bolder;
+ color: black;
+ font-weight: bolder;
}
div.pages a {
- text-decoration: none;
+ text-decoration: none;
}
div.pages form {
- margin: 0;
- padding: 0;
- display: inline;
+ margin: 0;
+ padding: 0;
+ display: inline;
}
div.pages form input {
- margin: 0 5px;
- display: inline;
+ margin: 0 5px;
+ display: inline;
}
hr {
- border: none;
- border-top: 1px solid #B7C5D9;
- height: 0;
- clear: left;
+ border: none;
+ border-top: 1px solid #B7C5D9;
+ height: 0;
+ clear: left;
}
div.report {
- color: #333;
+ color: #333;
}
table.modlog {
- margin: auto;
- width: 100%;
+ margin: auto;
+ width: 100%;
}
table.modlog tr td {
- text-align: left;
- margin: 0;
- padding: 4px 15px 0 0;
+ text-align: left;
+ margin: 0;
+ padding: 4px 15px 0 0;
}
table.modlog tr th {
- text-align: left;
- padding: 4px 15px 5px 5px;
- white-space: nowrap;
+ text-align: left;
+ padding: 4px 15px 5px 5px;
+ white-space: nowrap;
}
table.modlog tr th {
- background: #98E;
+ background: #98E;
}
td.minimal,th.minimal {
- width: 1%;
- white-space: nowrap;
+ width: 1%;
+ white-space: nowrap;
}
div.top_notice {
- text-align: center;
- margin: 5px auto;
+ text-align: center;
+ margin: 5px auto;
}
span.public_ban {
- display: block;
- color: red;
- font-weight: bold;
- margin-top: 15px;
+ display: block;
+ color: red;
+ font-weight: bold;
+ margin-top: 15px;
}
span.toolong {
- display: block;
- margin-top: 15px;
+ display: block;
+ margin-top: 15px;
}
div.blotter {
- color: red;
- font-weight: bold;
- text-align: center;
+ color: red;
+ font-weight: bold;
+ text-align: center;
}
table.mod.config-editor {
- font-size: 9pt;
- width: 100%;
+ font-size: 9pt;
+ width: 100%;
}
table.mod.config-editor td {
- text-align: left;
- padding: 5px;
- border-bottom: 1px solid #98e;
+ text-align: left;
+ padding: 5px;
+ border-bottom: 1px solid #98e;
}
table.mod.config-editor input[type="text"] {
- width: 98%;
+ width: 98%;
}
.desktop-style div.boardlist:nth-child(1) {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- margin-top: 0;
- z-index: 30;
- box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
- border-bottom: 1px solid;
- background-color: #D6DAF0;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ margin-top: 0;
+ z-index: 30;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, .15);
+ border-bottom: 1px solid;
+ background-color: #D6DAF0;
}
.desktop-style body {
- padding-top: 20px;
+ padding-top: 20px;
}
.desktop-style .sub {
- background: inherit;
+ background: inherit;
}
.desktop-style .sub .sub {
- display: inline-block;
- text-indent: -9000px;
- width: 7px;
+ display: inline-block;
+ text-indent: -9000px;
+ width: 7px;
}
.desktop-style .sub .sub:hover,.desktop-style .sub .sub.hover {
- display: inline;
- text-indent: 0;
- background: inherit;
+ display: inline;
+ text-indent: 0;
+ background: inherit;
}
#attention_bar {
- height: 1.5em;
- max-height: 1.5em;
- width: 100%;
- max-width: 100%;
- text-align: center;
- overflow: hidden;
+ height: 1.5em;
+ max-height: 1.5em;
+ width: 100%;
+ max-width: 100%;
+ text-align: center;
+ overflow: hidden;
}
#attention_bar_form {
- display: none;
- padding: 0;
- margin: 0;
+ display: none;
+ padding: 0;
+ margin: 0;
}
#attention_bar_input {
- width: 100%;
- padding: 0;
- margin: 0;
- text-align: center;
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ text-align: center;
}
#attention_bar:hover {
- background-color: rgba(100%,100%,100%,0.2);
+ background-color: rgba(100%,100%,100%,0.2);
}
.intro.thread-hidden {
- margin: 0;
- padding: 0;
+ margin: 0;
+ padding: 0;
}
form.ban-appeal {
- margin: 9px 20px;
+ margin: 9px 20px;
}
form.ban-appeal textarea {
- display: block;
+ display: block;
}
.MathJax_Display {
- display: inline!important;
+ display: inline!important;
}
pre {
- margin: 0;
+ margin: 0;
}
.theme-catalog div.thread img {
- float: none!important;
- margin: auto;
- max-height: 150px;
- max-width: 200px;
- box-shadow: 0 0 4px rgba(0,0,0,0.55);
- border: 2px solid rgba(153,153,153,0);
+ float: none!important;
+ margin: auto;
+ max-height: 150px;
+ max-width: 200px;
+ box-shadow: 0 0 4px rgba(0,0,0,0.55);
+ border: 2px solid rgba(153,153,153,0);
}
.theme-catalog div.thread {
- display: inline-block;
- vertical-align: top;
- text-align: center;
- font-weight: normal;
- margin-top: 2px;
- margin-bottom: 2px;
- padding: 2px;
- height: 300px;
- width: 205px;
- overflow: hidden;
- position: relative;
- font-size: 11px;
- max-height: 300px;
- background: rgba(182,182,182,0.12);
- border: 2px solid rgba(111,111,111,0.34);
+ display: inline-block;
+ vertical-align: top;
+ text-align: center;
+ font-weight: normal;
+ margin-top: 2px;
+ margin-bottom: 2px;
+ padding: 2px;
+ height: 300px;
+ width: 205px;
+ overflow: hidden;
+ position: relative;
+ font-size: 11px;
+ max-height: 300px;
+ background: rgba(182,182,182,0.12);
+ border: 2px solid rgba(111,111,111,0.34);
}
.theme-catalog div.thread strong {
- display: block;
+ display: block;
}
.theme-catalog div.threads {
- text-align: center;
- margin-left: -20px;
+ text-align: center;
+ margin-left: -20px;
}
.theme-catalog div.thread:hover {
- background: #D6DAF0;
- border-color: #B7C5D9;
+ background: #D6DAF0;
+ border-color: #B7C5D9;
}
.theme-catalog div.grid-size-vsmall img {
- max-height: 33%;
- max-width: 95%
+ max-height: 33%;
+ max-width: 95%
}
.theme-catalog div.grid-size-vsmall {
- min-width:90px; max-width: 90px;
- max-height: 148px;
+ min-width:90px; max-width: 90px;
+ max-height: 148px;
}
.theme-catalog div.grid-size-small img {
- max-height: 33%;
- max-width: 95%
+ max-height: 33%;
+ max-width: 95%
}
.theme-catalog div.grid-size-small {
- min-width:140px; max-width: 140px;
- max-height: 192px;
+ min-width:140px; max-width: 140px;
+ max-height: 192px;
}
.theme-catalog div.grid-size-large img {
- max-height: 40%;
- max-width: 95%
+ max-height: 40%;
+ max-width: 95%
}
.theme-catalog div.grid-size-large {
- min-width: 256px; max-width: 256px;
- max-height: 384px;
+ min-width: 256px; max-width: 256px;
+ max-height: 384px;
}
.theme-catalog img.thread-image {
- height: auto;
- max-width: 100%;
+ height: auto;
+ max-width: 100%;
}
@media (max-width: 420px) {
- .theme-catalog ul#Grid {
- padding-left: 18px;
- }
+ .theme-catalog ul#Grid {
+ padding-left: 18px;
+ }
- .theme-catalog div.thread {
- width: auto;
- margin-left: 0;
- margin-right: 0;
- }
+ .theme-catalog div.thread {
+ width: auto;
+ margin-left: 0;
+ margin-right: 0;
+ }
- .theme-catalog div.threads {
- overflow: hidden;
- }
+ .theme-catalog div.threads {
+ overflow: hidden;
+ }
}
.compact-boardlist {
- padding: 3px;
- padding-bottom: 0;
+ padding: 3px;
+ padding-bottom: 0;
}
.compact-boardlist .cb-item {
- display: inline-block;
- vertical-align: middle;
+ display: inline-block;
+ vertical-align: middle;
}
.compact-boardlist .cb-icon {
- padding-bottom: 1px;
+ padding-bottom: 1px;
}
.compact-boardlist .cb-fa {
- font-size: 21px;
- padding: 2px;
- padding-top: 0;
+ font-size: 21px;
+ padding: 2px;
+ padding-top: 0;
}
.compact-boardlist .cb-cat {
- padding: 5px 6px 8px 6px;
+ padding: 5px 6px 8px 6px;
}
.cb-menuitem {
- display: table-row;
+ display: table-row;
}
.cb-menuitem span {
- padding: 5px;
- display: table-cell;
- text-align: left;
- border-top: 1px solid rgba(0,0,0,0.5);
+ padding: 5px;
+ display: table-cell;
+ text-align: left;
+ border-top: 1px solid rgba(0,0,0,0.5);
}
.cb-menuitem span.cb-uri {
- text-align: right;
+ text-align: right;
}
.boardlist:not(.compact-boardlist) #watch-pinned::before {
- content: " [ ";
+ content: " [ ";
}
.boardlist:not(.compact-boardlist) #watch-pinned::after {
- content: " ] ";
+ content: " ] ";
}
.boardlist:not(.compact-boardlist) #watch-pinned {
- display: inline;
+ display: inline;
}
.boardlist:not(.compact-boardlist) #watch-pinned a {
- margin-left: 3pt;
+ margin-left: 3pt;
}
.boardlist:not(.compact-boardlist) #watch-pinned a:first-child {
- margin-left: 0pt;
+ margin-left: 0pt;
}
.compact-boardlist #watch-pinned {
- display: inline-block;
- vertical-align: middle;
+ display: inline-block;
+ vertical-align: middle;
}
.new-posts {
- opacity: 0.6;
- margin-top: 1em;
+ opacity: 0.6;
+ margin-top: 1em;
}
.new-threads {
- text-align: center;
+ text-align: center;
}
#options_handler, #alert_handler {
- position: fixed;
- top: 0px;
- left: 0px;
- right: 0px;
- bottom: 0px;
- width: 100%;
- height: 100%;
- text-align: center;
- z-index: 9900;
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ z-index: 9900;
}
#options_background, #alert_background {
- background: black;
- opacity: 0.5;
- position: absolute;
- top: 0px;
- left: 0px;
- right: 0px;
- bottom: 0px;
- width: 100%;
- height: 100%;
- z-index: -1;
+ background: black;
+ opacity: 0.5;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ width: 100%;
+ height: 100%;
+ z-index: -1;
}
#options_div, #alert_div {
- background-color: #d6daf0;
- border: 1px solid black;
- display: inline-block;
- position: relative;
- margin-top: 20px;
+ background-color: #d6daf0;
+ border: 1px solid black;
+ display: inline-block;
+ position: relative;
+ margin-top: 20px;
}
#options_div {
- width: 620px;
- height: 400px;
- resize: both;
- overflow: auto;
+ width: 620px;
+ height: 400px;
+ resize: both;
+ overflow: auto;
}
#alert_div {
- width: 500px;
+ width: 500px;
}
#alert_message {
- text-align: center;
- margin: 13px;
- font-size: 110%;
+ text-align: center;
+ margin: 13px;
+ font-size: 110%;
}
.alert_button {
- margin-bottom: 13px;
+ margin-bottom: 13px;
}
#options_div textarea {
- max-width: 100%;
+ max-width: 100%;
}
#options_close, #alert_close {
- top: 0px;
- right: 0px;
- position: absolute;
- margin-right: 3px;
- font-size: 20px;
- z-index: 100;
+ top: 0px;
+ right: 0px;
+ position: absolute;
+ margin-right: 3px;
+ font-size: 20px;
+ z-index: 100;
}
#options_tablist {
- padding: 0px 5px;
- left: 0px;
- width: 90px;
- top: 0px;
- bottom: 0px;
- height: 100%;
- border-right: 1px solid black;
+ padding: 0px 5px;
+ left: 0px;
+ width: 90px;
+ top: 0px;
+ bottom: 0px;
+ height: 100%;
+ border-right: 1px solid black;
}
.options_tab_icon {
- padding: 5px;
- color: black;
- cursor: pointer;
+ padding: 5px;
+ color: black;
+ cursor: pointer;
}
.options_tab_icon.active {
- color: red;
+ color: red;
}
.options_tab_icon i {
- font-size: 20px;
+ font-size: 20px;
}
.options_tab_icon div {
- font-size: 11px;
+ font-size: 11px;
}
.options_tab {
- padding: 10px;
- position: absolute;
- top: 0px;
- bottom: 10px;
- left: 101px;
- right: 0px;
- text-align: left;
- font-size: 16px;
- overflow-y: auto;
+ padding: 10px;
+ position: absolute;
+ top: 0px;
+ bottom: 10px;
+ left: 101px;
+ right: 0px;
+ text-align: left;
+ font-size: 16px;
+ overflow-y: auto;
}
.options_tab h2 {
- text-align: center;
- margin-bottom: 5px;
+ text-align: center;
+ margin-bottom: 5px;
}
.mobile-style #options_div, .mobile-style #alert_div {
- display: block;
- width: 100%;
- height: 100%;
- margin-top: 0px;
+ display: block;
+ width: 100%;
+ height: 100%;
+ margin-top: 0px;
}
.mentioned {
- word-wrap: break-word;
+ word-wrap: break-word;
}
.poster_id {
- cursor: pointer;
- display: inline-block;
- -webkit-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- -o-user-select: none;
- user-select: none;
+ cursor: pointer;
+ display: inline-block;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
}
.poster_id:hover {
- color: #800000!important;
+ color: #800000!important;
}
.poster_id::before {
- content: " ID: ";
+ content: " ID: ";
}
pre {
/* Better code tags */
- max-width:inherit;
- word-wrap:normal;
- overflow:auto;
- display: block!important;
- font-size:9pt;
- font-family:monospace;
+ max-width:inherit;
+ word-wrap:normal;
+ overflow:auto;
+ display: block!important;
+ font-size:9pt;
+ font-family:monospace;
}
span.pln {
- color:grey;
+ color:grey;
}
@media screen and (min-width: 768px) {
- .intro {
- clear: none;
- }
+ .intro {
+ clear: none;
+ }
- div.post div.body {
- clear: none;
- }
+ div.post div.body {
+ clear: none;
+ }
}
.clearfix {
@@ -1088,150 +1088,150 @@ div.boardlist a {
/* File selector */
.dropzone {
- color: #000;
- cursor: default;
- margin: auto;
- padding: 0px 4px;
- text-align: center;
- min-height: 50px;
- max-height: 140px;
- transition: 0.2s;
- background-color: rgba(200, 200, 200, 0.5);
- overflow-y: auto;
+ color: #000;
+ cursor: default;
+ margin: auto;
+ padding: 0px 4px;
+ text-align: center;
+ min-height: 50px;
+ max-height: 140px;
+ transition: 0.2s;
+ background-color: rgba(200, 200, 200, 0.5);
+ overflow-y: auto;
}
.dropzone-wrap {
- width: 100%;
+ width: 100%;
}
.dropzone .file-hint {
- color: rgba(0, 0, 0, 0.5);
- cursor: pointer;
- position: relative;
- margin-bottom: 5px;
- padding: 10px 0px;
- top: 5px;
- transition: 0.2s;
- border: 2px dashed rgba(125, 125, 125, 0.4);
+ color: rgba(0, 0, 0, 0.5);
+ cursor: pointer;
+ position: relative;
+ margin-bottom: 5px;
+ padding: 10px 0px;
+ top: 5px;
+ transition: 0.2s;
+ border: 2px dashed rgba(125, 125, 125, 0.4);
}
.file-hint:hover, .dropzone.dragover .file-hint {
- color: rgba(0, 0, 0, 1);
- border-color: rgba(125, 125, 125, 0.8);
+ color: rgba(0, 0, 0, 1);
+ border-color: rgba(125, 125, 125, 0.8);
}
.dropzone.dragover {
- background-color: rgba(200, 200, 200, 1);
+ background-color: rgba(200, 200, 200, 1);
}
.dropzone .file-thumbs {
- text-align: left;
- width: 100%;
+ text-align: left;
+ width: 100%;
}
.dropzone .tmb-container {
- padding: 3px;
- overflow-x: hidden;
- white-space: nowrap;
+ padding: 3px;
+ overflow-x: hidden;
+ white-space: nowrap;
}
.dropzone .file-tmb {
- height: 40px;
- width: 70px;
- cursor: pointer;
- display: inline-block;
- text-align: center;
- background-color: rgba(187, 187, 187, 0.5);
- background-size: cover;
- background-position: center;
+ height: 40px;
+ width: 70px;
+ cursor: pointer;
+ display: inline-block;
+ text-align: center;
+ background-color: rgba(187, 187, 187, 0.5);
+ background-size: cover;
+ background-position: center;
}
.dropzone .file-tmb span {
- font-weight: 600;
- position: relative;
- top: 13px;
+ font-weight: 600;
+ position: relative;
+ top: 13px;
}
.dropzone .tmb-filename {
- display: inline-block;
- vertical-align: bottom;
- bottom: 16px;
- position: relative;
- margin-left: 5px;
+ display: inline-block;
+ vertical-align: bottom;
+ bottom: 16px;
+ position: relative;
+ margin-left: 5px;
}
.dropzone .remove-btn {
- cursor: pointer;
- color: rgba(125, 125, 125, 0.5);
- display: inline-block;
- vertical-align: bottom;
- bottom: 10px;
- position: relative;
- margin-right: 5px;
- font-size: 20px
+ cursor: pointer;
+ color: rgba(125, 125, 125, 0.5);
+ display: inline-block;
+ vertical-align: bottom;
+ bottom: 10px;
+ position: relative;
+ margin-right: 5px;
+ font-size: 20px
}
.dropzone .remove-btn:hover {
- color: rgba(125, 125, 125, 1);
+ color: rgba(125, 125, 125, 1);
}
#thread_stats {
- display: inline;
- margin-left: 10px;
- margin-right: 10px;
+ display: inline;
+ margin-left: 10px;
+ margin-right: 10px;
}
/* Fileboard */
table.fileboard th, table.fileboard td {
- padding: 2px;
- text-align: center;
+ padding: 2px;
+ text-align: center;
}
table.fileboard .intro a {
- margin-left: 0px;
+ margin-left: 0px;
}
/* Gallery view */
#gallery_images {
- position: absolute;
- right: 0px;
- bottom: 0px;
- top: 0px;
- width: 12%;
- background-color: rgba(0, 0, 0, 0.4);
- overflow: auto;
+ position: absolute;
+ right: 0px;
+ bottom: 0px;
+ top: 0px;
+ width: 12%;
+ background-color: rgba(0, 0, 0, 0.4);
+ overflow: auto;
}
#gallery_toolbar {
- position: absolute;
- right: 12%;
- left: 0px;
- bottom: 0px;
- height: 32px;
- background-color: rgba(0, 0, 0, 0.4);
- text-align: right;
+ position: absolute;
+ right: 12%;
+ left: 0px;
+ bottom: 0px;
+ height: 32px;
+ background-color: rgba(0, 0, 0, 0.4);
+ text-align: right;
}
#gallery_images img {
- width: 100%;
+ width: 100%;
}
#gallery_toolbar a {
- font-size: 28px;
- padding-right: 5px;
+ font-size: 28px;
+ padding-right: 5px;
}
#gallery_main {
- position: absolute;
- left: 0px;
- right: 12%;
- bottom: 32px;
- top: 0px;
- padding: 10px;
+ position: absolute;
+ left: 0px;
+ right: 12%;
+ bottom: 32px;
+ top: 0px;
+ padding: 10px;
}
#gallery_images img {
- opacity: 0.6;
- -webkit-transition: all 0.5s;
- transition: all 0.5s;
+ opacity: 0.6;
+ -webkit-transition: all 0.5s;
+ transition: all 0.5s;
}
#gallery_images img:hover, #gallery_images img.active {
- opacity: 1;
+ opacity: 1;
}
#gallery_images img.active {
- -webkit-box-shadow: 0px 0px 29px 2px rgba(255,255,255,1);
- -moz-box-shadow: 0px 0px 29px 2px rgba(255,255,255,1);
- box-shadow: 0px 0px 29px 2px rgba(255,255,255,1);
- z-index: 1;
+ -webkit-box-shadow: 0px 0px 29px 2px rgba(255,255,255,1);
+ -moz-box-shadow: 0px 0px 29px 2px rgba(255,255,255,1);
+ box-shadow: 0px 0px 29px 2px rgba(255,255,255,1);
+ z-index: 1;
}
#gallery_main img, #gallery_main video {
- max-width: 100%;
- max-height: 100%;
- position: absolute;
+ max-width: 100%;
+ max-height: 100%;
+ position: absolute;
}
.own_post {
font-style: italic;
@@ -1243,80 +1243,80 @@ div.mix {
}
body {
- background: #0A0C11 !important;
+ background: #0A0C11 !important;
}
fieldset {
- margin: 10px 0 !important;
+ margin: 10px 0 !important;
}
legend {
- background:#222 !important;
- border:1px solid #888 !important;
- color:#FFF !important;
+ background:#222 !important;
+ border:1px solid #888 !important;
+ color:#FFF !important;
}
h1 {
- color: #FFF !important;
- font-family: Heading !important;
+ color: #FFF !important;
+ font-family: Heading !important;
}
fieldset ul li a {
- color: #FFF !important;
+ color: #FFF !important;
}
p.unimportant {
- color: #888 !important;
+ color: #888 !important;
}
.ban {
- background-color: #111 !important;
- color: #AAA;
+ background-color: #111 !important;
+ color: #AAA;
}
.boardlinks {
- text-align: center !important;
+ text-align: center !important;
}
#overlay {
- position: fixed;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- pointer-events: none;
- z-index: 1000;
- background-repeat: all;
- background-position: 0px 0px;
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 1000;
+ background-repeat: all;
+ background-position: 0px 0px;
- animation-name: Static;
- animation-duration: 0.5s;
- animation-iteration-count: infinite;
- animation-timing-function: steps(32);
+ animation-name: Static;
+ animation-duration: 0.5s;
+ animation-iteration-count: infinite;
+ animation-timing-function: steps(32);
- box-shadow: inset 0px 0px 10em rgba(0,0,0,0.4);
+ box-shadow: inset 0px 0px 10em rgba(0,0,0,0.4);
}
@keyframes Static {
- 0% { background-position: 0px 0px; }
- 100% { background-position: 0px 32px; }
+ 0% { background-position: 0px 0px; }
+ 100% { background-position: 0px 32px; }
}
#overlay2 {
- position: fixed;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- pointer-events: none;
- z-index: 1000;
- background-repeat: all;
- background-position: 0px 0px;
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 1000;
+ background-repeat: all;
+ background-position: 0px 0px;
- animation-name: Static;
- animation-duration: 0.2s;
- animation-iteration-count: infinite;
- animation-timing-function: steps(32);
+ animation-name: Static;
+ animation-duration: 0.2s;
+ animation-iteration-count: infinite;
+ animation-timing-function: steps(32);
}
body {
@@ -1326,7 +1326,7 @@ body {
font-size: 16px;
}
span.quote {
- color:#B8D962;
+ color:#B8D962;
}
h1 {
font-size: 20pt;
@@ -1364,7 +1364,7 @@ div.post.reply {
border: #555555 1px solid;
box-shadow: 4px 4px #555;
- @media (max-width: 48em) {
+ @media (max-width: 48em) {
border-left-style: none;
border-right-style: none;
}
@@ -1372,6 +1372,11 @@ div.post.reply {
div.post.reply.highlighted {
background: #555;
border: transparent 1px solid;
+
+ @media (max-width: 48em) {
+ border-left-style: none;
+ border-right-style: none;
+ }
}
div.post.reply div.body a:link, div.post.reply div.body a:visited {
color: #CCCCCC;
@@ -1431,7 +1436,7 @@ div.banner {
font-size: 16px;
}
div.banner a {
- color:#000;
+ color:#000;
}
input[type="submit"] {
background: #333333;
@@ -1444,7 +1449,7 @@ input[type="submit"]:hover {
color: #32DD72;
}
input[type="text"]:focus {
- border:#aaa 1px solid;
+ border:#aaa 1px solid;
}
p.fileinfo a:hover {
text-decoration: underline;
@@ -1453,9 +1458,9 @@ span.trip {
color: #AAAAAA;
}
.bar.bottom {
- bottom: 0px;
- border-top: 1px solid #333333;
- background-color: #666666;
+ bottom: 0px;
+ border-top: 1px solid #333333;
+ background-color: #666666;
}
div.pages {
@@ -1474,7 +1479,7 @@ hr {
}
div.boardlist {
color: #999999;
- background-color: rgba(12%, 12%, 12%, 0.10);
+ background-color: rgba(12%, 12%, 12%, 0.10);
}
div.ban {
@@ -1492,13 +1497,13 @@ table.modlog tr th {
}
.desktop-style div.boardlist:not(.bottom) {
- text-shadow: black 1px 1px 1px, black -1px -1px 1px, black -1px 1px 1px, black 1px -1px 1px;
- background-color: #666666;
+ text-shadow: black 1px 1px 1px, black -1px -1px 1px, black -1px 1px 1px, black 1px -1px 1px;
+ background-color: #666666;
}
.desktop-style div.boardlist:not(.bottom):hover, .desktop-style div.boardlist:not(.bottom).cb-menu {
- background-color: rgba(30%, 30%, 30%, 0.65);
+ background-color: rgba(30%, 30%, 30%, 0.65);
}
div.report {
diff --git a/templates/captcha_script.html b/templates/captcha_script.html
index 79b0b230..c2f5d87f 100644
--- a/templates/captcha_script.html
+++ b/templates/captcha_script.html
@@ -1,6 +1,6 @@
-{% if config.hcaptcha %}
+{% if config.captcha.mode == 'hcaptcha' %}
{% endif %}
-{% if config.turnstile %}
+{% if config.captcha.mode == 'turnstile' %}
{% endif %}
diff --git a/templates/header.html b/templates/header.html
index 7e3f9c54..27b1a31f 100644
--- a/templates/header.html
+++ b/templates/header.html
@@ -28,6 +28,6 @@
{% endif %}
{% endif %}
- {% if config.recaptcha %}
+ {% if config.captcha.mode == 'recaptcha' %}
{% endif %}
diff --git a/templates/installer/config.html b/templates/installer/config.html
index 973328f5..00a5b241 100644
--- a/templates/installer/config.html
+++ b/templates/installer/config.html
@@ -88,6 +88,9 @@
Secure trip (##) salt:
+ Poster password salt:
+
+
Additional configuration:
diff --git a/templates/main.js b/templates/main.js
index 26505f7e..c4025646 100755
--- a/templates/main.js
+++ b/templates/main.js
@@ -231,28 +231,6 @@ var resourceVersion = document.currentScript.getAttribute('data-resource-version
{% endif %}
{% 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) {
let results = document.cookie.match('(^|;) ?' + cookie_name + '=([^;]*)(;|$)');
if (results) {
@@ -265,26 +243,48 @@ function getCookie(cookie_name) {
{% endraw %}
/* BEGIN CAPTCHA REGION */
-{% if config.hcaptcha or config.turnstile %} // If any captcha
+{% if config.captcha.mode == 'hcaptcha' or config.captcha.mode == 'turnstile' %} // If any captcha
// Global captcha object. Assigned by `onCaptchaLoad()`.
var captcha_renderer = null;
+// Captcha widget id of the post form.
+var postCaptchaId = null;
-{% if config.hcaptcha %} // If hcaptcha
+{% if config.captcha.mode == 'hcaptcha' %} // If hcaptcha
function onCaptchaLoadHcaptcha() {
- if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
+ if ((captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled()))
+ && captcha_renderer === null
+ && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
let renderer = {
- renderOn: (container) => hcaptcha.render(container, {
- sitekey: "{{ config.hcaptcha_public }}",
+ /**
+ * @returns {object} Opaque widget id.
+ */
+ applyOn: (container, params) => hcaptcha.render(container, {
+ sitekey: "{{ config.captcha.hcaptcha.public }}",
+ callback: params['on-success'],
}),
+ /**
+ * @returns {void}
+ */
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);
}
}
{% endif %} // End if hcaptcha
-{% if config.turnstile %} // If turnstile
+{% if config.captcha.mode == 'turnstile' %} // If turnstile
// Wrapper function to be called from thread.html
window.onCaptchaLoadTurnstile_post_reply = function() {
@@ -298,20 +298,40 @@ window.onCaptchaLoadTurnstile_post_thread = function() {
// Should be called by the captcha API when it's ready. Ugly I know... D:
function onCaptchaLoadTurnstile(action) {
- if (captcha_renderer === null && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
+ if ((captchaMode === 'static' || (captchaMode === 'dynamic' && isDynamicCaptchaEnabled()))
+ && captcha_renderer === null
+ && (active_page === 'index' || active_page === 'catalog' || active_page === 'thread')) {
let renderer = {
- renderOn: function(container) {
+ /**
+ * @returns {object} Opaque widget id.
+ */
+ applyOn: function(container, params) {
let widgetId = turnstile.render('#' + container, {
- sitekey: "{{ config.turnstile_public }}",
+ sitekey: "{{ config.captcha.turnstile.public }}",
action: action,
+ callback: params['on-success'],
});
if (widgetId === undefined) {
return null;
}
return widgetId;
},
+ /**
+ * @returns {void}
+ */
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);
@@ -320,12 +340,20 @@ function onCaptchaLoadTurnstile(action) {
{% endif %} // End if turnstile
function onCaptchaLoad(renderer) {
+ // Initialize the form identifier with a random password.
+ document.getElementById('captcha-form-id').value = generatePassword();
+
captcha_renderer = renderer;
- let widgetId = renderer.renderOn('captcha-container');
+ let widgetId = renderer.applyOn('captcha-container', {
+ 'on-success': function(token) {
+ document.getElementById('captcha-response').value = token;
+ }
+ });
if (widgetId === null) {
console.error('Could not render captcha!');
}
+ postCaptchaId = widgetId;
document.addEventListener('afterdopost', function(e) {
// User posted! Reset the captcha.
renderer.reset(widgetId);
@@ -333,6 +361,8 @@ function onCaptchaLoad(renderer) {
}
{% if config.dynamic_captcha %} // If dynamic captcha
+var captchaMode = 'dynamic';
+
function isDynamicCaptchaEnabled() {
let cookie = getCookie('captcha-required');
return cookie === '1';
@@ -346,8 +376,15 @@ function initCaptcha() {
}
}
}
+{% else %}
+var captchaMode = 'static';
{% endif %} // End if dynamic captcha
{% else %} // Else if any captcha
+var captchaMode = 'none';
+
+function isDynamicCaptchaEnabled() {
+ return false;
+}
// No-op for `init()`.
function initCaptcha() {}
{% endif %} // End if any captcha
@@ -403,6 +440,13 @@ function doPost(form) {
saved[document.location] = form.elements['body'].value;
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.
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 != "");
@@ -519,7 +563,6 @@ var script_settings = function(script_name) {
};
function init() {
- initStyleChooser();
initCaptcha();
{% endraw %}
diff --git a/templates/mod/ban_form.html b/templates/mod/ban_form.html
index 100c497f..29cd9533 100644
--- a/templates/mod/ban_form.html
+++ b/templates/mod/ban_form.html
@@ -45,7 +45,7 @@ $(document).ready(function(){
{% trans 'Reason' %}
-
+
{% if post and board and not delete %}
diff --git a/templates/mod/dashboard.html b/templates/mod/dashboard.html
index e36e3c66..b765ce5f 100644
--- a/templates/mod/dashboard.html
+++ b/templates/mod/dashboard.html
@@ -1,3 +1,4 @@
+{% if config.url_banner %} {% endif %}
{% for board in boards %}
diff --git a/templates/mod/login.html b/templates/mod/login.html
index 32869872..6ed8dc64 100644
--- a/templates/mod/login.html
+++ b/templates/mod/login.html
@@ -1,4 +1,5 @@
{% if error %}{{ error }} {% endif %}
+{% if config.url_banner %} {% endif %}