diff --git a/.gitignore b/.gitignore index 3205c64b..93cac6d4 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,9 @@ tf/ /mod/ /random/ +# Banners +static/banners/* + #Fonts stylesheets/fonts diff --git a/compose.yml b/compose.yml index 526e18c6..c04c85f9 100644 --- a/compose.yml +++ b/compose.yml @@ -28,6 +28,8 @@ services: #MySQL Service db: image: mysql:8.0.35 + restart: unless-stopped + tty: true ports: - "3306:3306" environment: diff --git a/inc/Data/Driver/ErrorLogLogDriver.php b/inc/Data/Driver/ErrorLogLogDriver.php deleted file mode 100644 index e2050606..00000000 --- a/inc/Data/Driver/ErrorLogLogDriver.php +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index 2c9f14a0..00000000 --- a/inc/Data/Driver/FileLogDriver.php +++ /dev/null @@ -1,61 +0,0 @@ -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/LogDriver.php b/inc/Data/Driver/LogDriver.php deleted file mode 100644 index fddc3f27..00000000 --- a/inc/Data/Driver/LogDriver.php +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index c0df5304..00000000 --- a/inc/Data/Driver/SyslogLogDriver.php +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index ba6fdb15..00000000 --- a/inc/Data/IpNoteQueries.php +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index b33e7ac2..00000000 --- a/inc/Data/PageFetchResult.php +++ /dev/null @@ -1,15 +0,0 @@ -pdo->prepare('DELETE FROM `reports` WHERE `post` = :id AND `board` = :board'); + $query = prepare("DELETE FROM ``reports`` WHERE `post` = :id AND `board` = :board"); $query->bindValue(':id', $post_id, \PDO::PARAM_INT); $query->bindValue(':board', $board); $query->execute(); @@ -88,10 +93,8 @@ class ReportQueries { 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; - } + if (isset($report_posts[$report['board']][$report['post']])) { + $invalid[] = $report; } return $invalid; } else { @@ -113,10 +116,12 @@ class ReportQueries { /** * @param \PDO $pdo PDO connection. + * @param CacheDriver $cache Cache driver. * @param bool $auto_maintenance If the auto maintenance should be enabled. */ - public function __construct(\PDO $pdo, bool $auto_maintenance) { + public function __construct(\PDO $pdo, CacheDriver $cache, bool $auto_maintenance) { $this->pdo = $pdo; + $this->cache = $cache; $this->auto_maintenance = $auto_maintenance; } @@ -126,13 +131,18 @@ class ReportQueries { * @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); + $ret = $this->cache->get(self::CACHE_KEY); + if ($ret === null) { + $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; + $this->cache->set(self::CACHE_KEY, $count); + return $count; + } + return $ret; } /** @@ -141,8 +151,8 @@ class ReportQueries { * @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'); + public function getReportById(int $id): array { + $query = prepare("SELECT `board`, `ip` FROM ``reports`` WHERE `id` = :id"); $query->bindValue(':id', $id); $query->execute(); @@ -161,7 +171,7 @@ class ReportQueries { * @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 = $this->pdo->prepare("SELECT * FROM `reports` ORDER BY `time`"); $query->execute(); $raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC); return $this->joinReportPosts($raw_reports, $count); @@ -173,7 +183,7 @@ class ReportQueries { * @return int The number of reports deleted. */ public function purge(): int { - $query = $this->pdo->prepare('SELECT `board`, `post`, `id` FROM `reports`'); + $query = $this->pdo->prepare("SELECT `board`, `post`, `id` FROM `reports`"); $query->execute(); $raw_reports = $query->fetchAll(\PDO::FETCH_ASSOC); $invalid_reports = $this->filterReports($raw_reports, true, null); @@ -190,9 +200,11 @@ class ReportQueries { * @param int $id The report id. */ public function deleteById(int $id) { - $query = $this->pdo->prepare('DELETE FROM `reports` WHERE `id` = :id'); + $query = $this->pdo->prepare("DELETE FROM `reports` WHERE `id` = :id"); $query->bindValue(':id', $id, \PDO::PARAM_INT); $query->execute(); + // The caller may actually delete a valid post, so we need to invalidate the cache. + $this->cache->delete(self::CACHE_KEY); } /** @@ -201,9 +213,11 @@ class ReportQueries { * @param string $ip The reporter ip. */ public function deleteByIp(string $ip) { - $query = $this->pdo->prepare('DELETE FROM `reports` WHERE `ip` = :ip'); + $query = $this->pdo->prepare("DELETE FROM `reports` WHERE `ip` = :ip"); $query->bindValue(':ip', $ip); $query->execute(); + // The caller may actually delete a valid post, so we need to invalidate the cache. + $this->cache->delete(self::CACHE_KEY); } /** @@ -223,5 +237,7 @@ class ReportQueries { $query->bindValue(':post', $post_id, \PDO::PARAM_INT); $query->bindValue(':reason', $reason); $query->execute(); + + $this->cache->delete(self::CACHE_KEY); } } diff --git a/inc/Data/UserPostQueries.php b/inc/Data/UserPostQueries.php deleted file mode 100644 index 1c203431..00000000 --- a/inc/Data/UserPostQueries.php +++ /dev/null @@ -1,159 +0,0 @@ -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 cf82dcc8..e9fedc75 100644 --- a/inc/anti-bot.php +++ b/inc/anti-bot.php @@ -196,36 +196,37 @@ function _create_antibot($pdo, $board, $thread) { $antibot = new AntiBot(array($board, $thread)); try { - retry_on_deadlock(3, function() use ($config, $pdo, $thread, $board, $antibot, $purged_old_antispam) { - try { - $pdo->beginTransaction(); + $pdo->beginTransaction(); - // 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(); - } + // 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'); - } + retry_on_deadlock(4, function() use($config, $board, $thread) { + // Keep the now invalid timestamps around for a bit to enable users to post if they're still on an old version of + // 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(); + $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(); - $hash = $antibot->hash(); - + retry_on_deadlock(2, function() use($board, $thread, $hash) { // Insert an antispam with infinite life as the HTML page of a thread might last well beyond the expiry date. $query = prepare('INSERT INTO ``antispam`` VALUES (:board, :thread, :hash, UNIX_TIMESTAMP(), NULL, 0)'); $query->bindValue(':board', $board); @@ -233,21 +234,19 @@ function _create_antibot($pdo, $board, $thread) { $query->bindValue(':hash', $hash); // Throws on error. $query->execute(); - - $pdo->commit(); - } catch (\Exception $e) { - $pdo->rollBack(); + }); + } catch(\PDOException $e) { + if ($e->errorInfo === null || $e->errorInfo[1] != MYSQL_ER_LOCK_DEADLOCK) { throw $e; + } else { + error_log('Multiple deadlocks on _create_antibot while inserting, skipping'); } - }); + } } catch (\PDOException $e) { $pdo->rollBack(); - if ($e->errorInfo === null || $e->errorInfo[1] != MYSQL_ER_LOCK_DEADLOCK) { - throw $e; - } else { - \error_log('5 or more deadlocks on _create_antibot while inserting, skipping'); - } + throw $e; } + $pdo->commit(); return $antibot; } diff --git a/inc/config.php b/inc/config.php index 71b0fbf4..633cdd33 100644 --- a/inc/config.php +++ b/inc/config.php @@ -63,29 +63,9 @@ // been generated. This keeps the script from querying the database and causing strain when not needed. $config['has_installed'] = '.installed'; - // Deprecated, use 'log_system'. + // Use syslog() for logging all error messages and unauthorized login attempts. $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; @@ -220,9 +200,6 @@ // 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 @@ -943,6 +920,10 @@ // 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. @@ -981,6 +962,15 @@ // 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. @@ -1202,22 +1192,10 @@ // 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( - [ - '/^(?:(?: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+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', + '' + ), array( '/^https?:\/\/(\w+\.)?vimeo\.com\/(\d{2,10})(\?.+)?$/i', '' @@ -1234,18 +1212,10 @@ '/^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. @@ -1551,8 +1521,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 ?/user_posts/ip/x.x.x.x. and ?/user_posts/passwd/xxxxxxxx - $config['mod']['recent_user_posts'] = 5; + // How many recent posts, per board, to show in each page of ?/IP/x.x.x.x. + $config['mod']['ip_recentposts'] = 5; // Number of posts to display on the reports page. $config['mod']['recent_reports'] = 10; @@ -2030,6 +2000,12 @@ // 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 index 11a153ec..6ff656fe 100644 --- a/inc/context.php +++ b/inc/context.php @@ -1,8 +1,8 @@ $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(); @@ -72,11 +44,8 @@ function build_context(array $config): Context { 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)), + $cache = $c->get(CacheDriver::class); + return new ReportQueries($pdo, $cache, $auto_maintenance); + } ]); } diff --git a/inc/database.php b/inc/database.php index f5ea73bd..385a3135 100644 --- a/inc/database.php +++ b/inc/database.php @@ -66,7 +66,7 @@ function sql_open() { $dsn = $config['db']['type'] . ':' . ($unix_socket ? 'unix_socket=' . $unix_socket : 'host=' . $config['db']['server']) . - ';dbname=' . $config['db']['database']; + ';dbname=' . $config['db']['database'] . ';charset=utf8mb4'; if (!empty($config['db']['dsn'])) $dsn .= ';' . $config['db']['dsn']; try { @@ -84,9 +84,6 @@ function sql_open() { if ($config['debug']) { $debug['time']['db_connect'] = '~' . round((microtime(true) - $start) * 1000, 2) . 'ms'; - if ($config['db']['type'] == "mysql") { - query('SET NAMES utf8') or error(db_error()); - } } return $pdo; } catch(PDOException $e) { diff --git a/inc/filters.php b/inc/filters.php index 97cbc524..2a66cd2a 100644 --- a/inc/filters.php +++ b/inc/filters.php @@ -4,26 +4,23 @@ * 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)) @@ -32,11 +29,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) { @@ -72,10 +69,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) { @@ -138,42 +135,46 @@ class Filter { error('Unknown filter condition: ' . $condition); } } - - public function action(Context $ctx) { + + public function action() { global $board; $this->add_note = isset($this->add_note) ? $this->add_note : false; if ($this->add_note) { - $note_queries = $ctx->get(IpNoteQueries::class); - $note_queries->add($_SERVER['REMOTE_ADDR'], -1, 'Autoban message: ' . $this->post['body']); - } + $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)); + } 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) { @@ -183,7 +184,7 @@ class Filter { } else { $NOT = false; } - + if ($this->match($condition, $value) == $NOT) return false; } @@ -193,11 +194,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 { @@ -207,18 +208,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(Context $ctx, array $post) { +function do_filters(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; @@ -231,15 +232,15 @@ function do_filters(Context $ctx, 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($ctx); + $filter->action(); } } - + purge_flood_table(); } diff --git a/inc/functions.php b/inc/functions.php index def00287..a355e53c 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -745,23 +745,24 @@ function hasPermission($action = null, $board = null, $_mod = null) { function listBoards($just_uri = false) { global $config; - $cache_name = $just_uri ? 'all_boards_uri' : 'all_boards'; + $just_uri ? $cache_name = 'all_boards_uri' : $cache_name = 'all_boards'; - if ($config['cache']['enabled'] && ($boards = cache::get($cache_name))) { + if ($config['cache']['enabled'] && ($boards = cache::get($cache_name))) return $boards; - } if (!$just_uri) { - $query = query('SELECT * FROM ``boards`` ORDER BY `uri`'); + $query = query("SELECT * FROM ``boards`` ORDER BY `uri`") or error(db_error()); $boards = $query->fetchAll(); } else { - $query = query('SELECT `uri` FROM ``boards``'); - $boards = $query->fetchAll(\PDO::FETCH_COLUMN); + $boards = array(); + $query = query("SELECT `uri` FROM ``boards``") or error(db_error()); + while ($board = $query->fetchColumn()) { + $boards[] = $board; + } } - if ($config['cache']['enabled']) { + if ($config['cache']['enabled']) cache::set($cache_name, $boards); - } return $boards; } @@ -2069,7 +2070,7 @@ function remove_modifiers($body) { return preg_replace('@(.+?)@usm', '', $body); } -function markup(&$body, $track_cites = false) { +function markup(&$body, $track_cites = false, $op = false) { global $board, $config, $markup_urls; $modifiers = extract_modifiers($body); @@ -2168,15 +2169,12 @@ function markup(&$body, $track_cites = 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); } } } @@ -3085,8 +3083,3 @@ 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/lib/IP/LICENSE b/inc/lib/IP/LICENSE new file mode 100755 index 00000000..fb315548 --- /dev/null +++ b/inc/lib/IP/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2013 Jason Morriss + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/inc/lib/IP/Lifo/IP/BC.php b/inc/lib/IP/Lifo/IP/BC.php new file mode 100755 index 00000000..26a2c2b7 --- /dev/null +++ b/inc/lib/IP/Lifo/IP/BC.php @@ -0,0 +1,293 @@ + + * + * 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 new file mode 100755 index 00000000..e8fe32ce --- /dev/null +++ b/inc/lib/IP/Lifo/IP/CIDR.php @@ -0,0 +1,706 @@ + + * + * 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 new file mode 100755 index 00000000..4d22aa76 --- /dev/null +++ b/inc/lib/IP/Lifo/IP/IP.php @@ -0,0 +1,207 @@ + + * + * 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 5fb99b11..97fecb20 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; + return '' . $text . ''; } - function twig_secure_link($href) { return $href . '/' . make_secure_link_token($href); } diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 155c19a6..9f569c77 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -3,20 +3,21 @@ * Copyright (c) 2010-2013 Tinyboard Development Group */ use Vichan\Context; -use Vichan\Data\{IpNoteQueries, UserPostQueries, ReportQueries}; -use Vichan\Data\Driver\LogDriver; +use Vichan\Data\ReportQueries; +use Vichan\Functions\Format; use Vichan\Functions\Net; +use function Vichan\Functions\Net\decode_cursor; +use function Vichan\Functions\Net\encode_cursor; + defined('TINYBOARD') or exit; -function _link_or_copy_factory(Context $ctx): callable { - return function(string $target, string $link) use ($ctx) { - if (!\link($target, $link)) { - $ctx->get(LogDriver::class)->log(LogDriver::NOTICE, "Failed to link() $target to $link. FAlling back to copy()"); - return \copy($target, $link); - } - return true; - }; +function _link_or_copy(string $target, string $link): bool { + if (!link($target, $link)) { + error_log("Failed to link() $target to $link. FAlling back to copy()"); + return copy($target, $link); + } + return true; } function mod_page($title, $template, $args, $subtitle = false) { @@ -57,7 +58,8 @@ function mod_login(Context $ctx, $redirect = false) { if (!isset($_POST['username'], $_POST['password']) || $_POST['username'] == '' || $_POST['password'] == '') { $args['error'] = $config['error']['invalid']; } elseif (!login($_POST['username'], $_POST['password'])) { - $ctx->get(LogDriver::class)->log(LogDriver::INFO, 'Unauthorized login attempt!'); + if ($config['syslog']) + _syslog(LOG_WARNING, 'Unauthorized login attempt!'); $args['error'] = $config['error']['invalid']; } else { @@ -98,8 +100,6 @@ function mod_logout(Context $ctx) { function mod_dashboard(Context $ctx) { global $config, $mod; - $report_queries = $ctx->get(ReportQueries::class); - $args = []; $args['boards'] = listBoards(); @@ -126,7 +126,8 @@ function mod_dashboard(Context $ctx) { cache::set('pm_unreadcount_' . $mod['id'], $args['unread_pms']); } - $args['reports'] = $report_queries->getCount(); + $query = query('SELECT COUNT(*) FROM ``reports``') or error(db_error($query)); + $args['reports'] = $query->fetchColumn(); $query = query('SELECT COUNT(*) FROM ``ban_appeals`` WHERE denied = 0') or error(db_error($query)); $args['appeals'] = $query->fetchColumn(); @@ -767,10 +768,7 @@ function mod_board_log(Context $ctx, $board, $page_no = 1, $hide_names = false, } function mod_view_catalog(Context $ctx, $boardName) { - global $mod; - - $config = $ctx->get('config'); - + global $config; require_once($config['dir']['themes'].'/catalog/theme.php'); $settings = []; $settings['boards'] = $boardName; @@ -853,118 +851,188 @@ function mod_view_thread50(Context $ctx, $boardName, $thread) { } function mod_ip_remove_note(Context $ctx, $ip, $id) { - $config = $ctx->get('config'); + global $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."); - if (!is_numeric($id)) { - error('Invalid note ID'); - } + $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)); - $queries = $ctx->get(IpNoteQueries::class); - $deleted = $queries->deleteWhereIp((int)$id, $ip); + modLog("Removed a note for {$ip}"); - 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']); + header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']); } -function mod_ip(Context $ctx, $ip, string $encoded_cursor = null) { - global $mod; - $config = $ctx->get('config'); +function mod_ip(Context $ctx, $ip, string $encoded_cursor = '') { + global $config, $mod; - 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']); if (empty($encoded_cursor)) { - \header("Location: ?/user_posts/ip/$ip#bans", true, $config['redirect_http']); + header("Location: ?/IP/$ip#bans", true, $config['redirect_http']); } else { - \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#bans", true, $config['redirect_http']); + header("Location: ?/IP/$ip/cursor/$encoded_cursor#bans", true, $config['redirect_http']); } return; } 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']); - - $note_queries = $ctx->get(IpNoteQueries::class); - $note_queries->add($ip, $mod['id'], $_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)); Cache::delete("mod_page_ip_view_notes_$ip"); - modLog("Added a note for {$ip}"); + modLog("Added a note for {$ip}"); if (empty($encoded_cursor)) { - \header("Location: ?/user_posts/ip/$ip#notes", true, $config['redirect_http']); + header("Location: ?/IP/$ip#notes", true, $config['redirect_http']); } else { - \header("Location: ?/user_posts/ip/$ip/cursor/$encoded_cursor#notes", true, $config['redirect_http']); + header("Location: ?/IP/$ip/cursor/$encoded_cursor#notes", true, $config['redirect_http']); } return; } - // 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 (isset($config['mod']['ip_recentposts'])) { - // TODO log to migrate. - $page_size = $config['mod']['ip_recentposts']; - } else { - $page_size = $config['mod']['recent_user_posts']; - } - if ($config['mod']['dns_lookup']) { $args['hostname'] = rDNS($ip); } + // Decode the cursor. + list($cursor_type, $board_id_cursor_map) = decode_cursor($encoded_cursor); + $post_per_page = $config['mod']['ip_recentposts']; + $next_cursor_map = []; + $prev_cursor_map = []; + + $boards = listBoards(); + foreach ($boards as $board) { + $uri = $board['uri']; + openBoard($uri); + if (hasPermission($config['mod']['show_ip'], $uri)) { + // Extract the cursor relative to the board. + $id_cursor = false; + if (isset($board_id_cursor_map[$uri])) { + $value = $board_id_cursor_map[$uri]; + if (is_numeric($value)) { + $id_cursor = (int)$value; + } + } + + if ($id_cursor === false) { + $query = prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); + $query->bindValue(':ip', $ip); + $query->bindValue(':limit', $post_per_page + 1, PDO::PARAM_INT); // Always fetch more. + $query->execute(); + $posts = $query->fetchAll(PDO::FETCH_ASSOC); + } elseif ($cursor_type === 'n') { + $query = prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` <= :start_id ORDER BY `sticky` DESC, `id` DESC LIMIT :limit', $uri)); + $query->bindValue(':ip', $ip); + $query->bindValue(':start_id', $id_cursor, PDO::PARAM_INT); + $query->bindValue(':limit', $post_per_page + 2, PDO::PARAM_INT); // Always fetch more. + $query->execute(); + $posts = $query->fetchAll(PDO::FETCH_ASSOC); + } elseif ($cursor_type === 'p') { + // FIXME + $query = prepare(sprintf('SELECT * FROM `posts_%s` WHERE `ip` = :ip AND `id` >= :start_id ORDER BY `sticky` ASC, `id` ASC LIMIT :limit', $uri)); + $query->bindValue(':ip', $ip); + $query->bindValue(':start_id', $id_cursor, PDO::PARAM_INT); + $query->bindValue(':limit', $post_per_page + 2, PDO::PARAM_INT); // Always fetch more. + $query->execute(); + $posts = array_reverse($query->fetchAll(PDO::FETCH_ASSOC)); + } else { + throw new RuntimeException("Unknown cursor type '$cursor_type'"); + } + + $posts_count = count($posts); + + if ($posts_count === $post_per_page + 2) { + $has_extra_prev_post = true; + $has_extra_end_post = true; + } elseif ($posts_count === $post_per_page + 1) { + $has_extra_prev_post = $id_cursor !== false && $posts[0]['id'] == $id_cursor; + $has_extra_end_post = !$has_extra_prev_post; + } else { + $has_extra_prev_post = false; + $has_extra_end_post = false; + } + + // Get the previous cursor, if any. + if ($has_extra_prev_post) { + // Select the most recent post. + $prev_cursor_map[$uri] = $posts[1]['id']; + array_shift($posts); + $posts_count--; + } + // Get the next cursor, if any. + if ($has_extra_end_post) { + // Since we fetched 1 above the limit, we always know if there are any posts after the current page. + // Query orders by DESC, so the SECOND last post has the lowest ID. + array_pop($posts); + $next_cursor_map[$uri] = $posts[$posts_count - 2]['id']; + } + + // Finally load the post contents and build them. + foreach ($posts as $post) { + if (!$post['thread']) { + $po = new Thread($post, '?/', $mod, false); + } else { + $po = new Post($post, '?/', $mod); + } + + if (!isset($args['posts'][$uri])) { + $args['posts'][$uri] = [ 'board' => $board, 'posts' => [] ]; + } + $args['posts'][$uri]['posts'][] = $po->build(true); + } + } + } + + // Build the cursors. + $args['cursor_prev'] = !empty($encoded_cursor) ? encode_cursor('p', $prev_cursor_map) : false; + $args['cursor_next'] = !empty($next_cursor_map) ? encode_cursor('n', $next_cursor_map) : false; + + $args['boards'] = $boards; + $args['token'] = make_secure_link_token('ban'); + if (hasPermission($config['mod']['view_ban'])) { $args['bans'] = Bans::find($ip, false, true, $config['auto_maintenance']); } if (hasPermission($config['mod']['view_notes'])) { - $note_queries = $ctx->get(IpNoteQueries::class); - $args['notes'] = $note_queries->getByIp($ip); + $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; } if (hasPermission($config['mod']['modlog_ip'])) { @@ -981,117 +1049,13 @@ function mod_user_posts_by_ip(Context $ctx, string $ip, string $encoded_cursor = $args['logs'] = []; } - $boards = listBoards(); - - $queryable_uris = []; - foreach ($boards as $board) { - $uri = $board['uri']; - if (hasPermission($config['mod']['show_ip'], $uri)) { - $queryable_uris[] = $uri; - } - } - - $queries = $ctx->get(UserPostQueries::class); - $result = $queries->fetchPaginatedByIp($queryable_uris, $ip, $page_size, $encoded_cursor); - - $args['cursor_prev'] = $result->cursor_prev; - $args['cursor_next'] = $result->cursor_next; - - foreach($boards as $board) { - $uri = $board['uri']; - // The Thread and Post classes rely on some implicit board parameter set by openBoard. - openBoard($uri); - - // Finally load the post contents and build them. - foreach ($result->by_uri[$uri] as $post) { - if (!$post['thread']) { - $po = new Thread($post, '?/', $mod, false); - } else { - $po = new Post($post, '?/', $mod); - } - - if (!isset($args['posts'][$uri])) { - $args['posts'][$uri] = [ 'board' => $board, 'posts' => [] ]; - } - $args['posts'][$uri]['posts'][] = $po->build(true); - } - } - - $args['boards'] = $boards; - $args['token'] = make_secure_link_token('ban'); - - // Since the security token is only used to send requests to create notes and remove bans, use "?/IP/" as the url. if (empty($encoded_cursor)) { $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_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); + mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']); } function mod_ban(Context $ctx) { @@ -1491,9 +1455,8 @@ function mod_move(Context $ctx, $originBoard, $postID) { if ($targetBoard === $originBoard) error(_('Target and source board are the same.')); - $_link_or_copy = _link_or_copy_factory($ctx); // link() if leaving a shadow thread behind; else, rename(). - $clone = $shadow ? $_link_or_copy : 'rename'; + $clone = $shadow ? '_link_or_copy' : 'rename'; // indicate that the post is a thread $post['op'] = true; @@ -1787,8 +1750,7 @@ function mod_merge(Context $ctx, $originBoard, $postID) { $op = $post; $op['id'] = $newID; - $_link_or_copy = _link_or_copy_factory($ctx); - $clone = $shadow ? $_link_or_copy : 'rename'; + $clone = $shadow ? '_link_or_copy' : 'rename'; if ($post['has_file']) { // copy image @@ -2004,10 +1966,14 @@ function mod_ban_post(Context $ctx, $board, $delete, $post, $token = false) { $autotag .= "/${board}/" . " " . $filehash . " " . $filename ."\r\n"; $autotag .= $body . "\r\n"; $autotag = escape_markup_modifiers($autotag); - - $note_queries = $ctx->get(IpNoteQueries::class); - $note_queries->add($ip, $mod['id'], $autotag); - modLog("Added a note for {$ip}"); + 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}"); } } deletePost($post); @@ -2112,10 +2078,13 @@ function mod_warning_post(Context $ctx, $board, $post, $token = false) { $autotag .= $body . "\r\n"; $autotag = escape_markup_modifiers($autotag); markup($autotag); - - $note_queries = $ctx->get(IpNoteQueries::class); - $note_queries->add($ip, $mod['id'], $autotag); - modLog("Added a note for {$ip}"); + $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}"); } } } @@ -2220,7 +2189,7 @@ function mod_edit_post(Context $ctx, $board, $edit_raw_html, $postID) { } function mod_delete(Context $ctx, $board, $post) { - global $config, $mod; + global $config; if (!openBoard($board)) error($config['error']['noboard']); @@ -2261,10 +2230,13 @@ function mod_delete(Context $ctx, $board, $post) { $autotag .= $body . "\r\n"; $autotag = escape_markup_modifiers($autotag); markup($autotag); - - $note_queries = $ctx->get(IpNoteQueries::class); - $note_queries->add($ip, $mod['id'], $autotag); - modLog("Added a note for {$ip}"); + $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}"); } } deletePost($post); @@ -2351,7 +2323,7 @@ function mod_spoiler_image(Context $ctx, $board, $post, $file) { } function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) { - global $config, $board, $mod; + global $config, $board; $global = (bool)$global; @@ -2429,10 +2401,13 @@ function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) { $autotag .= $body . "\r\n"; $autotag = escape_markup_modifiers($autotag); markup($autotag); - - $note_queries = $ctx->get(IpNoteQueries::class); - $note_queries->add($ip, $mod['id'], $autotag); - modLog("Added a note for {$ip}"); + $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}"); } } @@ -2463,7 +2438,7 @@ function mod_deletebyip(Context $ctx, $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']); @@ -3000,7 +2975,7 @@ function mod_report_dismiss(Context $ctx, $id, $all = false) { if ($all) { $report_queries->deleteByIp($ip); - modLog("Dismissed all reports by $ip"); + modLog("Dismissed all reports by $ip"); } else { $report_queries->deleteById($id); modLog("Dismissed a report for post #{$id}", $board); diff --git a/install.php b/install.php index 7663a503..bc3ba7d4 100644 --- a/install.php +++ b/install.php @@ -881,7 +881,6 @@ 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( diff --git a/js/ajax.js b/js/ajax.js index 3cb06bf1..af795a57 100644 --- a/js/ajax.js +++ b/js/ajax.js @@ -18,25 +18,16 @@ $(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); @@ -103,15 +94,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"],input[name="embed"]').val('').change(); + textarea[name="body"],input[type="file"]').val('').change(); }, cache: false, contentType: false, @@ -123,7 +114,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"],input[name="embed"]').val('').change(); + textarea[name="body"],input[type="file"]').val('').change(); } else { alert(_('An unknown error occured when posting!')); $(form).find('input[type="submit"]').val(submit_txt); @@ -141,10 +132,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/inline-expanding.js b/js/inline-expanding.js index c44843b0..41625d2d 100644 --- a/js/inline-expanding.js +++ b/js/inline-expanding.js @@ -17,10 +17,6 @@ $(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'); @@ -60,12 +56,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 { @@ -73,15 +69,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 = ''; }); @@ -206,8 +202,6 @@ $(document).ready(function() { Options.extend_tab('general', '' + _('Number of simultaneous image downloads (0 to disable): ') + ''); - Options.extend_tab('general', ''); - $('#inline-expand-max input') .css('width', '50px') .val(localStorage.inline_expand_max || DEFAULT_MAX) @@ -218,21 +212,6 @@ $(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 6715ae1d..c0652269 100644 --- a/js/options/general.js +++ b/js/options/general.js @@ -43,6 +43,9 @@ $(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 78fd35ce..f3f161c6 100644 --- a/js/post-filter.js +++ b/js/post-filter.js @@ -237,8 +237,12 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata var postUid = $ele.find('.poster_id').text(); } - let postName = (typeof $ele.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($ele.find('.name')[0]); - let postTrip = $ele.find('.trip').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(); + } /* display logic and bind click handlers */ @@ -293,34 +297,39 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata } // name - if (!$ele.data('hiddenByName')) { + if (!pageData.forcedAnon && !$ele.data('hiddenByName')) { $buffer.find('#filter-add-name').click(function () { addFilter('name', postName, false); }); $buffer.find('#filter-remove-name').addClass('hidden'); - } else { + } else if (!pageData.forcedAnon) { $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 (!$ele.data('hiddenByTrip') && postTrip !== '') { + if (!pageData.forcedAnon && !$ele.data('hiddenByTrip') && postTrip !== '') { $buffer.find('#filter-add-trip').click(function () { addFilter('trip', postTrip, false); }); $buffer.find('#filter-remove-trip').addClass('hidden'); - } else if (postTrip !== '') { + } else if (!pageData.forcedAnon && 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'); } @@ -382,6 +391,7 @@ 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); @@ -422,8 +432,9 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata } // matches generalFilter - name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]); - if (hasTrip) + if (!forcedAnon) + name = (typeof $post.find('.name').contents()[0] == 'undefined') ? '' : nameSpanToString($post.find('.name')[0]); + if (!forcedAnon && hasTrip) trip = $post.find('.trip').text(); if (hasSub) subject = $post.find('.subject').text(); @@ -444,13 +455,13 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata pattern = new RegExp(rule.value); switch (rule.type) { case 'name': - if (pattern.test(name)) { + if (!forcedAnon && pattern.test(name)) { $post.data('hiddenByName', true); hide(post); } break; case 'trip': - if (hasTrip && pattern.test(trip)) { + if (!forcedAnon && hasTrip && pattern.test(trip)) { $post.data('hiddenByTrip', true); hide(post); } @@ -477,13 +488,13 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata } else { switch (rule.type) { case 'name': - if (rule.value == name) { + if (!forcedAnon && rule.value == name) { $post.data('hiddenByName', true); hide(post); } break; case 'trip': - if (hasTrip && rule.value == trip) { + if (!forcedAnon && hasTrip && rule.value == trip) { $post.data('hiddenByTrip', true); hide(post); } @@ -816,7 +827,8 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata boardId: board_name, // get the id from the global variable 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) + hasUID: (document.getElementsByClassName('poster_id').length > 0), + forcedAnon: ($('th:contains(Name)').length === 0) // tests by looking for the Name label on the reply form }; initStyle(); diff --git a/js/post-menu.js b/js/post-menu.js index c2155c00..79cfd868 100644 --- a/js/post-menu.js +++ b/js/post-menu.js @@ -104,10 +104,8 @@ 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('\u{25B6}\u{fe0e}') + $('', {href: '#', class: 'post-btn', title: 'Post menu'}).text('►') ); } diff --git a/js/show-backlinks.js b/js/show-backlinks.js index 607c24ab..5924124e 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').split('_')[1]; + let replyId = $(this).attr('id').replace(/^reply_/, ''); $(this).find('div.body a:not([rel="nofollow"])').each(function() { let id = $(this).text().match(/^>>(\d+)$/); @@ -25,15 +25,13 @@ $(document).ready(function() { return; } - let post = $('#reply_' + id + ', #op_' + id); - if (post.length == 0) { + let post = $('#reply_' + id); + if(post.length == 0) return; - } let mentioned = post.find('.head div.mentioned'); if (mentioned.length === 0) { - // The op has two "head"s divs, use the second. - mentioned = $('
').prependTo(post.find('.head').last()); + mentioned = $('
').prependTo(post.find('.head')); } if (mentioned.find('a.mentioned-' + replyId).length !== 0) { @@ -50,13 +48,13 @@ $(document).ready(function() { }); }; - $('div.post').each(showBackLinks); + $('div.post.reply').each(showBackLinks); $(document).on('new_post', function(e, post) { - if ($(post).hasClass('reply') || $(post).hasClass('op')) { + if ($(post).hasClass('reply')) { showBackLinks.call(post); } else { - $(post).find('div.post').each(showBackLinks); + $(post).find('div.post.reply').each(showBackLinks); } }); }); diff --git a/js/style-select-simple.js b/js/style-select-simple.js deleted file mode 100644 index 8b59fa0a..00000000 --- a/js/style-select-simple.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * style-select-simple.js - * - * Changes the stylesheet chooser links to a ').css({float:"none"}); - let options = []; + 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 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) => { + 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() { - let sel = $(this).find(":selected")[0]; - let styleName = sel.innerHTML; - changeStyle(styleName, sel); + $('#style-select-' + $(this).val()).click(); }); - + + stylesDiv.hide() pages.after( $('
') .append(_('Select theme: '), stylesSelect) diff --git a/js/youtube.js b/js/youtube.js index 4a5a5afe..4c31ed09 100644 --- a/js/youtube.js +++ b/js/youtube.js @@ -1,41 +1,41 @@ /* - * 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'; - */ +* 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'; +* +*/ -$(document).ready(function() { - const ON = '[Remove]'; - const YOUTUBE = 'www.youtube.com'; - - function makeEmbedNode(embedHost, videoId, width, height) { - return $(`