diff --git a/README.md b/README.md index e5f8ead0..e0f985fe 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Requirements PHP 8.0 is explicitly supported. PHP 7.x should be compatable. 2. MySQL/MariaDB server >= 5.5.3 3. [Composer](https://getcomposer.org/) (To install various packages) -4. [mbstring](http://www.php.net/manual/en/mbstring.installation.php) +4. [mbstring](http://www.php.net/manual/en/mbstring.installation.php) 5. [PHP GD](http://www.php.net/manual/en/intro.image.php) 6. [PHP PDO](http://www.php.net/manual/en/intro.pdo.php) @@ -44,7 +44,7 @@ Installation development version with: git clone git://git.leftypol.org/leftypol/leftypol.git - + 2. run ```composer install``` inside the directory 3. Navigate to ```install.php``` in your web browser and follow the prompts. @@ -80,7 +80,7 @@ find support from a variety of sources: * For support, reply to the sticky on our [/tech/](https://leftypol.org/tech/) board. ### Tinyboard support -vichan, and by extension lainchan and leftypol, is based on a Tinyboard, so both engines have very much in common. These links may be helpful for you as well: +vichan, and by extension lainchan and leftypol, is based on a Tinyboard, so both engines have very much in common. These links may be helpful for you as well: * Tinyboard documentation can be found [here](https://web.archive.org/web/20121016074303/http://tinyboard.org/docs/?p=Main_Page). diff --git a/composer.json b/composer.json index d0345e3b..21fff51a 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "inc/polyfill.php", "inc/error.php", "inc/functions.php", + "inc/functions/hide.php", "inc/functions/net.php" ] }, diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index a884650c..fffd868d 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -18,6 +18,8 @@ RUN apk add --no-cache \ graphicsmagick \ gifsicle \ ffmpeg \ + djvulibre \ + ghostscript \ bind-tools \ gettext \ gettext-dev \ diff --git a/inc/Data/Driver/CacheDriverTrait.php b/inc/Data/Driver/CacheDriverTrait.php new file mode 100644 index 00000000..16e56ce0 --- /dev/null +++ b/inc/Data/Driver/CacheDriverTrait.php @@ -0,0 +1,20 @@ +name = $name; + $this->level = $level; + } + + public function log(int $level, string $message): void { + if ($level <= $this->level) { + $lv = $this->levelToString($level); + $line = "{$this->name} $lv: $message"; + \error_log($line, 0, null, null); + } + } +} diff --git a/inc/Data/Driver/FileLogDriver.php b/inc/Data/Driver/FileLogDriver.php new file mode 100644 index 00000000..2c9f14a0 --- /dev/null +++ b/inc/Data/Driver/FileLogDriver.php @@ -0,0 +1,61 @@ +fd = \fopen($file_path, 'a'); + if ($this->fd === false) { + throw new \RuntimeException("Unable to open log file at $file_path"); + } + + $this->name = $name; + $this->level = $level; + + // In some cases PHP does not run the destructor. + \register_shutdown_function([$this, 'close']); + } + + public function __destruct() { + $this->close(); + } + + public function log(int $level, string $message): void { + if ($level <= $this->level) { + $lv = $this->levelToString($level); + $line = "{$this->name} $lv: $message\n"; + \flock($this->fd, LOCK_EX); + \fwrite($this->fd, $line); + \fflush($this->fd); + \flock($this->fd, LOCK_UN); + } + } + + public function close() { + \flock($this->fd, LOCK_UN); + \fclose($this->fd); + } +} diff --git a/inc/Data/Driver/LogDriver.php b/inc/Data/Driver/LogDriver.php new file mode 100644 index 00000000..fddc3f27 --- /dev/null +++ b/inc/Data/Driver/LogDriver.php @@ -0,0 +1,22 @@ +inner = new \Memcached(); if (!$this->inner->setOption(\Memcached::OPT_BINARY_PROTOCOL, true)) { - throw new \RuntimeException('Unable to set the memcached protocol!'); + $err = $this->inner->getResultMessage(); + throw new \RuntimeException("Unable to set the memcached protocol: '$err'"); } if (!$this->inner->setOption(\Memcached::OPT_PREFIX_KEY, $prefix)) { - throw new \RuntimeException('Unable to set the memcached prefix!'); + $err = $this->inner->getResultMessage(); + throw new \RuntimeException("Unable to set the memcached prefix: '$err'"); } - if (!$this->inner->addServers($memcached_server)) { - throw new \RuntimeException('Unable to add the memcached server!'); + + $maybe_unix_path = self::asUnixSocketPath($server_uri); + $is_unix = $maybe_unix_path !== null; + if ($is_unix) { + $server_uri = $maybe_unix_path; + } + + // Memcached keeps the server connections open across requests. + $current_servers = $this->inner->getServerList(); + $found_in_curr = false; + foreach ($current_servers as $curr) { + // Ignore the port if the server is connected with a unix socket. + if ($curr['host'] === $server_uri && ($is_unix || $curr['port'] === $server_port)) { + $found_in_curr = true; + } + } + + if (!$found_in_curr) { + if (!empty($current_servers)) { + if (!$this->inner->resetServerList()) { + $err = $this->inner->getResultMessage(); + throw new \RuntimeException("Unable to reset the memcached server list: '$err'"); + } + } + if (!$this->inner->addServer($server_uri, $server_port, $server_weight)) { + $err = $this->inner->getResultMessage(); + throw new \RuntimeException("Unable to add memcached servers: '$err'"); + } } } diff --git a/inc/Data/Driver/RedisCacheDriver.php b/inc/Data/Driver/RedisCacheDriver.php index 389fff32..c71c8779 100644 --- a/inc/Data/Driver/RedisCacheDriver.php +++ b/inc/Data/Driver/RedisCacheDriver.php @@ -5,18 +5,17 @@ defined('TINYBOARD') or exit; class RedisCacheDriver implements CacheDriver { + use CacheDriverTrait; + private string $prefix; private \Redis $inner; public function __construct(string $prefix, string $host, ?int $port, ?string $password, int $database) { $this->inner = new \Redis(); - if (str_starts_with($host, 'unix:') || str_starts_with($host, ':')) { - $ret = \explode(':', $host); - if (count($ret) < 2) { - throw new \RuntimeException("Invalid unix socket path $host"); - } - // Unix socket. - $this->inner->connect($ret[1]); + $maybe_unix = self::asUnixSocketPath($host); + + if ($maybe_unix !== null) { + $this->inner->connect($maybe_unix); } elseif ($port === null) { $this->inner->connect($host); } else { diff --git a/inc/Data/Driver/StderrLogDriver.php b/inc/Data/Driver/StderrLogDriver.php new file mode 100644 index 00000000..4c766033 --- /dev/null +++ b/inc/Data/Driver/StderrLogDriver.php @@ -0,0 +1,27 @@ +name = $name; + $this->level = $level; + } + + public function log(int $level, string $message): void { + if ($level <= $this->level) { + $lv = $this->levelToString($level); + \fwrite(\STDERR, "{$this->name} $lv: $message\n"); + } + } +} diff --git a/inc/Data/Driver/SyslogLogDriver.php b/inc/Data/Driver/SyslogLogDriver.php new file mode 100644 index 00000000..c0df5304 --- /dev/null +++ b/inc/Data/Driver/SyslogLogDriver.php @@ -0,0 +1,35 @@ +level = $level; + } + + public function log(int $level, string $message): void { + if ($level <= $this->level) { + if (isset($_SERVER['REMOTE_ADDR'], $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'])) { + // CGI + \syslog($level, "$message - client: {$_SERVER['REMOTE_ADDR']}, request: \"{$_SERVER['REQUEST_METHOD']} {$_SERVER['REQUEST_URI']}\""); + } else { + \syslog($level, $message); + } + } + } +} diff --git a/inc/cache.php b/inc/cache.php index d26f9201..182d6fda 100644 --- a/inc/cache.php +++ b/inc/cache.php @@ -15,10 +15,18 @@ class Cache { switch ($config['cache']['enabled']) { case 'memcached': - return new MemcachedCacheDriver( - $config['cache']['prefix'], - $config['cache']['memcached'] - ); + $prefix = $config['cache']['prefix']; + $uri = $config['cache']['memcached'][0]; + $port = 0; + $weight = 0; + if (isset($config['cache']['memcached'][1]) && $config['cache']['memcached'][1] !== null) { + $port = \intval($config['cache']['memcached'][1]); + } + if (isset($config['cache']['memcached'][2]) && $config['cache']['memcached'][2] !== null) { + $weight = \intval($config['cache']['memcached'][2]); + } + + return new MemcachedCacheDriver($prefix, $uri, $port, $weight); case 'redis': $port = $config['cache']['redis'][1]; $port = empty($port) ? null : intval($port); diff --git a/inc/config.php b/inc/config.php index def2cd27..fb4b4493 100644 --- a/inc/config.php +++ b/inc/config.php @@ -63,9 +63,29 @@ // been generated. This keeps the script from querying the database and causing strain when not needed. $config['has_installed'] = '.installed'; - // Use syslog() for logging all error messages and unauthorized login attempts. + // Deprecated, use 'log_system'. $config['syslog'] = false; + $config['log_system'] = [ + /* + * Log all error messages and unauthorized login attempts. + * Can be "syslog", "error_log" (default), "file", or "stderr". + */ + 'type' => 'error_log', + // The application name used by the logging system. Defaults to "tinyboard" for backwards compatibility. + 'name' => 'tinyboard', + /* + * Only relevant if 'log_system' is set to "syslog". If true, double print the logs also in stderr. Defaults to + * false. + */ + 'syslog_stderr' => false, + /* + * Only relevant if "log_system" is set to `file`. Sets the file that vichan will log to. Defaults to + * '/var/log/vichan.log'. + */ + 'file_path' => '/var/log/vichan.log', + ]; + // Use `host` via shell_exec() to lookup hostnames, avoiding query timeouts. May not work on your system. // Requires safe_mode to be disabled. $config['dns_system'] = false; @@ -923,10 +943,6 @@ // Location of thumbnail to use for deleted images. $config['image_deleted'] = 'static/deleted.png'; - // When a thumbnailed image is going to be the same (in dimension), just copy the entire file and use - // that as a thumbnail instead of resizing/redrawing. - $config['minimum_copy_resize'] = false; - // Maximum image upload size in bytes. $config['max_filesize'] = 10 * 1024 * 1024; // 10MB // Maximum image dimensions. @@ -965,15 +981,6 @@ // Set this to true if you're using Linux and you can execute `md5sum` binary. $config['gnu_md5'] = false; - // Use Tesseract OCR to retrieve text from images, so you can use it as a spamfilter. - $config['tesseract_ocr'] = false; - - // Tesseract parameters - $config['tesseract_params'] = ''; - - // Tesseract preprocess command - $config['tesseract_preprocess_command'] = 'convert -monochrome %s -'; - // Number of posts in a "View Last X Posts" page $config['noko50_count'] = 50; // Number of posts a thread needs before it gets a "View Last X Posts" page. @@ -1291,6 +1298,7 @@ $config['error']['pendingappeal'] = _('There is already a pending appeal for this ban.'); $config['error']['invalidpassword'] = _('Wrong password…'); $config['error']['invalidimg'] = _('Invalid image.'); + $config['error']['invalidfile'] = _('Invalid file.'); $config['error']['unknownext'] = _('Unknown file extension.'); $config['error']['filesize'] = _('Maximum file size: %maxsz% bytes
Your file\'s size: %filesz% bytes'); $config['error']['maxsize'] = _('The file was too big.'); @@ -1888,45 +1896,6 @@ // Example: Adding the pre-markup post body to the API as "com_nomarkup". // $config['api']['extra_fields'] = array('body_nomarkup' => 'com_nomarkup'); -/* - * ================== - * NNTPChan settings - * ================== - */ - -/* - * Please keep in mind that NNTPChan support in vichan isn't finished yet / is in an experimental - * state. Please join #nntpchan on Rizon in order to peer with someone. - */ - - $config['nntpchan'] = array(); - - // Enable NNTPChan integration - $config['nntpchan']['enabled'] = false; - - // NNTP server - $config['nntpchan']['server'] = "localhost:1119"; - - // Global dispatch array. Add your boards to it to enable them. Please make - // sure that this setting is set in a global context. - $config['nntpchan']['dispatch'] = array(); // 'overchan.test' => 'test' - - // Trusted peer - an IP address of your NNTPChan instance. This peer will have - // increased capabilities, eg.: will evade spamfilter. - $config['nntpchan']['trusted_peer'] = '127.0.0.1'; - - // Salt for message ID generation. Keep it long and secure. - $config['nntpchan']['salt'] = 'change_me+please'; - - // A local message ID domain. Make sure to change it. - $config['nntpchan']['domain'] = 'example.vichan.net'; - - // An NNTPChan group name. - // Please set this setting in your board/config.php, not globally. - $config['nntpchan']['group'] = false; // eg. 'overchan.test' - - - /* * ==================== * Other/uncategorized @@ -2041,7 +2010,7 @@ // Password hashing method version // If set to 0, it won't upgrade hashes using old password encryption schema, only create new. // You can set it to a higher value, to further migrate to other password hashing function. - $config['password_crypt_version'] = 1; + $config['password_crypt_version'] = 2; // Use CAPTCHA for reports? $config['report_captcha'] = false; @@ -2061,9 +2030,16 @@ // Enable auto IP note generation of moderator deleted posts $config['autotagging'] = false; - // Enable PDF file thumbnail generation + // Enable PDF thumbnail generation. + // Requires a working installation of ghostscript and imagemagick. + // Imagemagick support of PDF files is not required. $config['pdf_file_thumbnail'] = false; + // Enable djvu thumbnail generation. + // Requires djvulibre's tools and imagemagick. + // Imagemagick support of djvu files is not required. + $config['djvu_file_thumbnail'] = false; + // Enable TXT file thumbnail $config['txt_file_thumbnail'] = false; diff --git a/inc/context.php b/inc/context.php index 3207d7ea..11a153ec 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(); diff --git a/inc/database.php b/inc/database.php index f5ea73bd..4b0bb6c3 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']; + ';charset=utf8mb4;dbname=' . $config['db']['database']; if (!empty($config['db']['dsn'])) $dsn .= ';' . $config['db']['dsn']; try { @@ -85,7 +85,7 @@ function sql_open() { if ($config['debug']) { $debug['time']['db_connect'] = '~' . round((microtime(true) - $start) * 1000, 2) . 'ms'; if ($config['db']['type'] == "mysql") { - query('SET NAMES utf8') or error(db_error()); + query('SET NAMES utf8mb4') or error(db_error()); } } return $pdo; diff --git a/inc/functions.php b/inc/functions.php index def00287..c9f6657e 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -11,6 +11,7 @@ if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { $microtime_start = microtime(true); +use Vichan\Functions\Hide; use Lifo\IP\IP; // for expanding IPv6 address in DNSBL() // the user is not currently logged in as a moderator @@ -1691,7 +1692,7 @@ function checkSpam(array $extra_salt = array()) { $_hash = sha1($_hash . $extra_salt); if ($hash != $_hash) { - return true; + return true; } $query = prepare('SELECT `passed` FROM ``antispam`` WHERE `hash` = :hash'); @@ -2227,20 +2228,15 @@ function markup(&$body, $track_cites = false) { $clauses = array_unique($clauses); if ($board['uri'] != $_board) { - if (!openBoard($_board)){ - if (in_array($_board,array_keys($config['boards_alias']))){ - $_board = $config['boards_alias'][$_board]; - if (openBoard($_board)){ - - } - else { + if (!openBoard($_board)) { + if (\in_array($_board, \array_keys($config['boards_alias']))) { + $_board = $config['boards_alias'][$_board]; + if (!openBoard($_board)) { continue; // Unknown board - } - } - else { + } + } else { continue; // Unknown board } - } } @@ -2281,38 +2277,31 @@ function markup(&$body, $track_cites = false) { if ($cite) { if (isset($cited_posts[$_board][$cite])) { $link = $cited_posts[$_board][$cite]; - if (isset($original_board)){ - $replacement = '' . - '>>>/' . $original_board . '/' . $cite . + '>>>/' . $replacement_board . '/' . $cite . ''; + if ($track_cites && $config['track_cites']) { + $tracked_cites[] = [ $_board, $cite ]; } - else { - $replacement = '' . - '>>>/' . $_board . '/' . $cite . - ''; - - } - - $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0])); - $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]); - - if ($track_cites && $config['track_cites']) - $tracked_cites[] = array($_board, $cite); + } else { + $replacement = ">>>/$_board/$cite"; } - } elseif(isset($crossboard_indexes[$_board])) { + } elseif (isset($crossboard_indexes[$_board])) { $replacement = '' . '>>>/' . $_board . '/' . ''; - $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0])); - $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]); + } else { + $replacement = ">>>/$_board/$cite"; } + + $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0])); + $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]); } } @@ -2583,11 +2572,11 @@ function rrmdir($dir) { function poster_id($ip, $thread) { global $config; - if ($id = event('poster-id', $ip, $thread)) + if ($id = event('poster-id', $ip, $thread)) { return $id; + } - // Confusing, hard to brute-force, but simple algorithm - return substr(sha1(sha1($ip . $config['secure_trip_salt'] . $thread) . $config['secure_trip_salt']), 0, $config['poster_id_length']); + return \substr(Hide\secure_hash($ip . $config['secure_trip_salt'] . $thread . $config['secure_trip_salt'], false), 0, $config['poster_id_length']); } function generate_tripcode($name) { @@ -2615,7 +2604,7 @@ function generate_tripcode($name) { if (isset($config['custom_tripcode']["##{$trip}"])) $trip = $config['custom_tripcode']["##{$trip}"]; else - $trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(base64_encode(sha1($trip . $config['secure_trip_salt'], true)), 0, 4))), -10); + $trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(Hide\secure_hash($trip . $config['secure_trip_salt'], false), 0, 4))), -10); } else { if (isset($config['custom_tripcode']["#{$trip}"])) $trip = $config['custom_tripcode']["#{$trip}"]; diff --git a/inc/functions/hide.php b/inc/functions/hide.php new file mode 100644 index 00000000..bf972751 --- /dev/null +++ b/inc/functions/hide.php @@ -0,0 +1,6 @@ + 12 ]); + if ($r === false) { + throw new \RuntimeException("Could not hash password"); } - else { - $comp = crypt($test, $password); + + return [ $version, $r ]; +} + +function test_password(string $db_hash, string|int $version, string $input_password): bool { + $version = (int)$version; + if ($version < 2) { + $ok = \hash_equals($db_hash, \crypt($input_password, $db_hash)); + } else { + $pre_hash = \hash('tiger160,3', $input_password, false); + $ok = \password_verify($pre_hash, $db_hash); } - return array($version, hash_equals($password, $comp)); + return $ok; } -function generate_salt() { - return strtr(base64_encode(random_bytes(16)), '+', '.'); -} - -function login($username, $password) { - global $mod, $config; +function login(string $username, string $password): array|false { + global $mod; $query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username"); $query->bindValue(':username', $username); - $query->execute() or error(db_error($query)); + $query->execute(); if ($user = $query->fetch(PDO::FETCH_ASSOC)) { - list($version, $ok) = test_password($user['password'], $user['version'], $password); + $ok = test_password($user['password'], $user['version'], $password); if ($ok) { - if ($config['password_crypt_version'] > $version) { + if ((int)$user['version'] < 2) { // It's time to upgrade the password hashing method! list ($user['version'], $user['password']) = crypt_password($password); $query = prepare("UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id"); $query->bindValue(':password', $user['password']); $query->bindValue(':version', $user['version']); $query->bindValue(':id', $user['id']); - $query->execute() or error(db_error($query)); + $query->execute(); } - return $mod = array( + return $mod = [ 'id' => $user['id'], 'type' => $user['type'], 'username' => $username, 'hash' => mkhash($username, $user['password']), 'boards' => explode(',', $user['boards']) - ); + ]; } } return false; } -function setCookies() { +function setCookies(): void { global $mod, $config; - if (!$mod) + if (!$mod) { error('setCookies() was called for a non-moderator!'); + } $is_https = Net\is_connection_https(); @@ -119,14 +117,14 @@ function setCookies() { time() + $config['cookies']['expire'], $config['cookies']['jail'] ? $config['cookies']['path'] : '/', null, $is_https, $config['cookies']['httponly']); } -function destroyCookies() { +function destroyCookies(): void { global $config; $is_https = Net\is_connection_https(); // Delete the cookies setcookie($config['cookies']['mod'], 'deleted', time() - $config['cookies']['expire'], $config['cookies']['jail']?$config['cookies']['path'] : '/', null, $is_https, true); } -function modLog($action, $_board=null) { +function modLog(string $action, ?string $_board = null): void { global $mod, $board, $config; $query = prepare("INSERT INTO ``modlogs`` VALUES (:id, :ip, :board, :time, :text)"); $query->bindValue(':id', (isset($mod['id']) ? $mod['id'] : -1), PDO::PARAM_INT); @@ -141,16 +139,18 @@ function modLog($action, $_board=null) { $query->bindValue(':board', null, PDO::PARAM_NULL); $query->execute() or error(db_error($query)); - if ($config['syslog']) + if ($config['syslog']) { _syslog(LOG_INFO, '[mod/' . $mod['username'] . ']: ' . $action); + } } -function create_pm_header() { +function create_pm_header(): mixed { global $mod, $config; if ($config['cache']['enabled'] && ($header = cache::get('pm_unread_' . $mod['id'])) != false) { - if ($header === true) + if ($header === true) { return false; + } return $header; } @@ -159,26 +159,29 @@ function create_pm_header() { $query->bindValue(':id', $mod['id'], PDO::PARAM_INT); $query->execute() or error(db_error($query)); - if ($pm = $query->fetch(PDO::FETCH_ASSOC)) - $header = array('id' => $pm['id'], 'waiting' => $query->rowCount() - 1); - else + if ($pm = $query->fetch(PDO::FETCH_ASSOC)) { + $header = [ 'id' => $pm['id'], 'waiting' => $query->rowCount() - 1 ]; + } else { $header = true; + } - if ($config['cache']['enabled']) + if ($config['cache']['enabled']) { cache::set('pm_unread_' . $mod['id'], $header); + } - if ($header === true) + if ($header === true) { return false; + } return $header; } -function make_secure_link_token($uri) { +function make_secure_link_token(string $uri): string { global $mod, $config; return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8); } -function check_login(Context $ctx, $prompt = false) { +function check_login(Context $ctx, bool $prompt = false): void { global $config, $mod; // Validate session @@ -188,7 +191,9 @@ function check_login(Context $ctx, $prompt = false) { if (count($cookie) != 3) { // Malformed cookies destroyCookies(); - if ($prompt) mod_login($ctx); + if ($prompt) { + mod_login($ctx); + } exit; } @@ -201,7 +206,9 @@ function check_login(Context $ctx, $prompt = false) { if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) { // Malformed cookies destroyCookies(); - if ($prompt) mod_login($ctx); + if ($prompt) { + mod_login($ctx); + } exit; } diff --git a/inc/mod/pages.php b/inc/mod/pages.php index ec33b128..17d2ef74 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -4,16 +4,19 @@ */ use Vichan\Context; use Vichan\Data\{IpNoteQueries, UserPostQueries, ReportQueries}; +use Vichan\Data\Driver\LogDriver; use Vichan\Functions\Net; defined('TINYBOARD') or exit; -function _link_or_copy(string $target, string $link): bool { - if (!link($target, $link)) { - error_log("Failed to link() $target to $link. FAlling back to copy()"); - return copy($target, $link); - } - return true; +function _link_or_copy_factory(Context $ctx): callable { + return function(string $target, string $link) use ($ctx) { + if (!\link($target, $link)) { + $ctx->get(LogDriver::class)->log(LogDriver::NOTICE, "Failed to link() $target to $link. FAlling back to copy()"); + return \copy($target, $link); + } + return true; + }; } function mod_page($title, $template, $args, $subtitle = false) { @@ -43,7 +46,7 @@ function clone_wrapped_with_exist_check($clonefn, $src, $dest) { } function mod_login(Context $ctx, $redirect = false) { - global $config; + $config = $ctx->get('config'); $args = []; @@ -54,8 +57,7 @@ function mod_login(Context $ctx, $redirect = false) { if (!isset($_POST['username'], $_POST['password']) || $_POST['username'] == '' || $_POST['password'] == '') { $args['error'] = $config['error']['invalid']; } elseif (!login($_POST['username'], $_POST['password'])) { - if ($config['syslog']) - _syslog(LOG_WARNING, 'Unauthorized login attempt!'); + $ctx->get(LogDriver::class)->log(LogDriver::INFO, 'Unauthorized login attempt!'); $args['error'] = $config['error']['invalid']; } else { @@ -87,15 +89,16 @@ function mod_confirm(Context $ctx, $request) { } function mod_logout(Context $ctx) { - global $config; + $config = $ctx->get('config'); destroyCookies(); header('Location: ?/', true, $config['redirect_http']); } function mod_dashboard(Context $ctx) { - global $config, $mod; + global $mod; + $config = $ctx->get('config'); $report_queries = $ctx->get(ReportQueries::class); $args = []; @@ -189,7 +192,7 @@ function mod_dashboard(Context $ctx) { } function mod_search_redirect(Context $ctx) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['search'])) error($config['error']['noaccess']); @@ -469,7 +472,9 @@ function mod_edit_board(Context $ctx, $boardName) { } function mod_new_board(Context $ctx) { - global $config, $board; + global $board; + + $config = $ctx->get('config'); if (!hasPermission($config['mod']['newboard'])) error($config['error']['noaccess']); @@ -535,7 +540,9 @@ function mod_new_board(Context $ctx) { } function mod_noticeboard(Context $ctx, $page_no = 1) { - global $config, $pdo, $mod; + global $pdo, $mod; + + $config = $ctx->get('config'); if ($page_no < 1) error($config['error']['404']); @@ -590,7 +597,7 @@ function mod_noticeboard(Context $ctx, $page_no = 1) { } function mod_noticeboard_delete(Context $ctx, $id) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['noticeboard_delete'])) error($config['error']['noaccess']); @@ -608,7 +615,9 @@ function mod_noticeboard_delete(Context $ctx, $id) { } function mod_news(Context $ctx, $page_no = 1) { - global $config, $pdo, $mod; + global $pdo, $mod; + + $config = $ctx->get('config'); if ($page_no < 1) error($config['error']['404']); @@ -655,7 +664,7 @@ function mod_news(Context $ctx, $page_no = 1) { } function mod_news_delete(Context $ctx, $id) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['news_delete'])) error($config['error']['noaccess']); @@ -670,7 +679,7 @@ function mod_news_delete(Context $ctx, $id) { } function mod_log(Context $ctx, $page_no = 1) { - global $config; + $config = $ctx->get('config'); if ($page_no < 1) error($config['error']['404']); @@ -695,7 +704,7 @@ function mod_log(Context $ctx, $page_no = 1) { } function mod_user_log(Context $ctx, $username, $page_no = 1) { - global $config; + $config = $ctx->get('config'); if ($page_no < 1) error($config['error']['404']); @@ -732,7 +741,7 @@ function protect_ip($entry) { } function mod_board_log(Context $ctx, $board, $page_no = 1, $hide_names = false, $public = false) { - global $config; + $config = $ctx->get('config'); if ($page_no < 1) error($config['error']['404']); @@ -800,7 +809,9 @@ function mod_view_catalog(Context $ctx, $boardName) { } function mod_view_board(Context $ctx, $boardName, $page_no = 1) { - global $config, $mod; + global $mod; + + $config = $ctx->get('config'); if (!openBoard($boardName)){ require "templates/themes/overboards/overboards.php"; @@ -831,20 +842,24 @@ function mod_view_board(Context $ctx, $boardName, $page_no = 1) { } function mod_view_thread(Context $ctx, $boardName, $thread) { - global $config, $mod; + global $mod; - if (!openBoard($boardName)) + if (!openBoard($boardName)) { + $config = $ctx->get('config'); error($config['error']['noboard']); + } $page = buildThread($thread, true, $mod); echo $page; } function mod_view_thread50(Context $ctx, $boardName, $thread) { - global $config, $mod; + global $mod; - if (!openBoard($boardName)) + if (!openBoard($boardName)) { + $config = $ctx->get('config'); error($config['error']['noboard']); + } $page = buildThread50($thread, true, $mod); echo $page; @@ -1093,7 +1108,7 @@ function mod_user_posts_by_passwd(Context $ctx, string $passwd, string $encoded_ } function mod_ban(Context $ctx) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['ban'])) error($config['error']['noaccess']); @@ -1114,7 +1129,7 @@ function mod_ban(Context $ctx) { } function mod_warning(Context $ctx) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['warning'])) error($config['error']['noaccess']); @@ -1131,9 +1146,10 @@ function mod_warning(Context $ctx) { } function mod_bans(Context $ctx) { - global $config; global $mod; + $config = $ctx->get('config'); + if (!hasPermission($config['mod']['view_banlist'])) error($config['error']['noaccess']); @@ -1166,7 +1182,9 @@ function mod_bans(Context $ctx) { } function mod_bans_json(Context $ctx) { - global $config, $mod; + global $mod; + + $config = $ctx->get('config'); if (!hasPermission($config['mod']['ban'])) error($config['error']['noaccess']); @@ -1178,7 +1196,9 @@ function mod_bans_json(Context $ctx) { } function mod_ban_appeals(Context $ctx) { - global $config, $board; + global $board; + + $config = $ctx->get('config'); if (!hasPermission($config['mod']['view_ban_appeals'])) error($config['error']['noaccess']); @@ -1260,7 +1280,7 @@ function mod_ban_appeals(Context $ctx) { } function mod_lock(Context $ctx, $board, $unlock, $post) { - global $config; + $config = $ctx->get('config'); if (!openBoard($board)) error($config['error']['noboard']); @@ -1296,7 +1316,7 @@ function mod_lock(Context $ctx, $board, $unlock, $post) { } function mod_sticky(Context $ctx, $board, $unsticky, $post) { - global $config; + $config = $ctx->get('config'); if (!openBoard($board)) error($config['error']['noboard']); @@ -1320,7 +1340,7 @@ function mod_sticky(Context $ctx, $board, $unsticky, $post) { } function mod_cycle(Context $ctx, $board, $uncycle, $post) { - global $config; + $config = $ctx->get('config'); if (!openBoard($board)) error($config['error']['noboard']); @@ -1342,7 +1362,7 @@ function mod_cycle(Context $ctx, $board, $uncycle, $post) { } function mod_bumplock(Context $ctx, $board, $unbumplock, $post) { - global $config; + $config = $ctx->get('config'); if (!openBoard($board)) error($config['error']['noboard']); @@ -1489,8 +1509,9 @@ function mod_move(Context $ctx, $originBoard, $postID) { if ($targetBoard === $originBoard) error(_('Target and source board are the same.')); + $_link_or_copy = _link_or_copy_factory($ctx); // link() if leaving a shadow thread behind; else, rename(). - $clone = $shadow ? '_link_or_copy' : 'rename'; + $clone = $shadow ? $_link_or_copy : 'rename'; // indicate that the post is a thread $post['op'] = true; @@ -1784,7 +1805,8 @@ function mod_merge(Context $ctx, $originBoard, $postID) { $op = $post; $op['id'] = $newID; - $clone = $shadow ? '_link_or_copy' : 'rename'; + $_link_or_copy = _link_or_copy_factory($ctx); + $clone = $shadow ? $_link_or_copy : 'rename'; if ($post['has_file']) { // copy image @@ -1923,7 +1945,9 @@ function mod_merge(Context $ctx, $originBoard, $postID) { } function mod_ban_post(Context $ctx, $board, $delete, $post, $token = false) { - global $config, $mod; + global $mod; + + $config = $ctx->get('config'); if (!openBoard($board)) error($config['error']['noboard']); @@ -2042,7 +2066,9 @@ function mod_ban_post(Context $ctx, $board, $delete, $post, $token = false) { } function mod_warning_post(Context $ctx, $board, $post, $token = false) { - global $config, $mod; + global $mod; + + $config = $ctx->get('config'); if (!openBoard($board)) error($config['error']['noboard']); @@ -2139,7 +2165,7 @@ function mod_warning_post(Context $ctx, $board, $post, $token = false) { } function mod_edit_post(Context $ctx, $board, $edit_raw_html, $postID) { - global $config; + $config = $ctx->get('config'); if (!openBoard($board)) error($config['error']['noboard']); @@ -2216,7 +2242,9 @@ function mod_edit_post(Context $ctx, $board, $edit_raw_html, $postID) { } function mod_delete(Context $ctx, $board, $post) { - global $config, $mod; + global $mod; + + $config = $ctx->get('config'); if (!openBoard($board)) error($config['error']['noboard']); @@ -2279,7 +2307,7 @@ function mod_delete(Context $ctx, $board, $post) { } function mod_deletefile(Context $ctx, $board, $post, $file) { - global $config; + $config = $ctx->get('config'); if (!openBoard($board)) error($config['error']['noboard']); @@ -2302,7 +2330,7 @@ function mod_deletefile(Context $ctx, $board, $post, $file) { } function mod_spoiler_image(Context $ctx, $board, $post, $file) { - global $config; + $config = $ctx->get('config'); if (!openBoard($board)) error($config['error']['noboard']); @@ -2347,8 +2375,9 @@ function mod_spoiler_image(Context $ctx, $board, $post, $file) { } function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) { - global $config, $board, $mod; + global $board, $mod; + $config = $ctx->get('config'); $global = (bool)$global; if (!openBoard($boardName)) @@ -2466,7 +2495,9 @@ function mod_deletebyip(Context $ctx, $boardName, $post, $global = false) { } function mod_user(Context $ctx, $uid) { - global $config, $mod; + global $mod; + + $config = $ctx->get('config'); if (!hasPermission($config['mod']['editusers']) && !(hasPermission($config['mod']['change_password']) && $uid == $mod['id'])) error($config['error']['noaccess']); @@ -2644,7 +2675,7 @@ function mod_user_new(Context $ctx) { function mod_users(Context $ctx) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['manageusers'])) error($config['error']['noaccess']); @@ -2665,7 +2696,7 @@ function mod_users(Context $ctx) { } function mod_user_promote(Context $ctx, $uid, $action) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['promoteusers'])) error($config['error']['noaccess']); @@ -2763,7 +2794,7 @@ function mod_pm(Context $ctx, $id, $reply = false) { } function mod_inbox(Context $ctx) { - global $config, $mod; + global $mod; $query = prepare('SELECT `unread`,``pms``.`id`, `time`, `sender`, `to`, `message`, `username` FROM ``pms`` LEFT JOIN ``mods`` ON ``mods``.`id` = `sender` WHERE `to` = :mod ORDER BY `unread` DESC, `time` DESC'); $query->bindValue(':mod', $mod['id']); @@ -2787,7 +2818,9 @@ function mod_inbox(Context $ctx) { function mod_new_pm(Context $ctx, $username) { - global $config, $mod; + global $mod; + + $config = $ctx->get('config'); if (!hasPermission($config['mod']['create_pm'])) error($config['error']['noaccess']); @@ -2835,7 +2868,9 @@ function mod_new_pm(Context $ctx, $username) { } function mod_rebuild(Context $ctx) { - global $config, $twig; + global $twig; + + $config = $ctx->get('config'); if (!hasPermission($config['mod']['rebuild'])) error($config['error']['noaccess']); @@ -2907,7 +2942,9 @@ function mod_rebuild(Context $ctx) { } function mod_reports(Context $ctx) { - global $config, $mod; + global $mod; + + $config = $ctx->get('config'); if (!hasPermission($config['mod']['reports'])) error($config['error']['noaccess']); @@ -2974,7 +3011,7 @@ function mod_reports(Context $ctx) { } function mod_report_dismiss(Context $ctx, $id, $all = false) { - global $config; + $config = $ctx->get('config'); $report_queries = $ctx->get(ReportQueries::class); $report = $report_queries->getReportById($id); @@ -3006,13 +3043,27 @@ function mod_report_dismiss(Context $ctx, $id, $all = false) { } function mod_recent_posts(Context $ctx, $lim, $board_list = false, $json = false) { - global $config, $mod, $pdo; + global $mod, $pdo; + + $config = $ctx->get('config'); if (!hasPermission($config['mod']['recent'])) error($config['error']['noaccess']); - $limit = (is_numeric($lim))? $lim : 25; - $last_time = (isset($_GET['last']) && is_numeric($_GET['last'])) ? $_GET['last'] : 0; + $limit = 25; + if (\is_numeric($lim)) { + $lim = \intval($lim); + if ($lim > 0 && $lim < 1000) { + $limit = $lim; + } + } + $last_time = 0; + if (isset($_GET['last']) && \is_numeric($_GET['last'])) { + $last = \intval($_GET['last']); + if ($last > 0) { + $last_time = $last; + } + } $mod_boards = []; $boards = listBoards(); @@ -3104,7 +3155,9 @@ function mod_recent_posts(Context $ctx, $lim, $board_list = false, $json = false } function mod_config(Context $ctx, $board_config = false) { - global $config, $mod, $board; + global $mod, $board; + + $config = $ctx->get('config'); if ($board_config && !openBoard($board_config)) error($config['error']['noboard']); @@ -3244,7 +3297,7 @@ function mod_config(Context $ctx, $board_config = false) { } function mod_themes_list(Context $ctx) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['themes'])) error($config['error']['noaccess']); @@ -3278,7 +3331,7 @@ function mod_themes_list(Context $ctx) { } function mod_theme_configure(Context $ctx, $theme_name) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['themes'])) error($config['error']['noaccess']); @@ -3360,7 +3413,7 @@ function mod_theme_configure(Context $ctx, $theme_name) { } function mod_theme_uninstall(Context $ctx, $theme_name) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['themes'])) error($config['error']['noaccess']); @@ -3377,7 +3430,7 @@ function mod_theme_uninstall(Context $ctx, $theme_name) { } function mod_theme_rebuild(Context $ctx, $theme_name) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['themes'])) error($config['error']['noaccess']); @@ -3426,7 +3479,7 @@ function mod_delete_page_board(Context $ctx, $page = '', $board = false) { } function mod_edit_page(Context $ctx, $id) { - global $config, $mod, $board; + global $mod, $board; $query = prepare('SELECT * FROM ``pages`` WHERE `id` = :id'); $query->bindValue(':id', $id); @@ -3436,6 +3489,8 @@ function mod_edit_page(Context $ctx, $id) { if (!$page) error(_('Could not find the page you are trying to edit.')); + $config = $ctx->get('config'); + if (!$page['board'] && $mod['boards'][0] !== '*') error($config['error']['noaccess']); @@ -3497,11 +3552,13 @@ function mod_edit_page(Context $ctx, $id) { } function mod_pages(Context $ctx, $board = false) { - global $config, $mod, $pdo; + global $mod, $pdo; if (empty($board)) $board = false; + $config = $ctx->get('config'); + if (!$board && $mod['boards'][0] !== '*') error($config['error']['noaccess']); @@ -3622,7 +3679,7 @@ function mod_debug_recent_posts(Context $ctx) { } function mod_debug_sql(Context $ctx) { - global $config; + $config = $ctx->get('config'); if (!hasPermission($config['mod']['debug_sql'])) error($config['error']['noaccess']); diff --git a/inc/nntpchan/nntpchan.php b/inc/nntpchan/nntpchan.php deleted file mode 100644 index de67a193..00000000 --- a/inc/nntpchan/nntpchan.php +++ /dev/null @@ -1,152 +0,0 @@ -"; -} - - -function gen_nntp($headers, $files) { - if (count($files) == 0) { - } - else if (count($files) == 1 && $files[0]['type'] == 'text/plain') { - $content = $files[0]['text'] . "\r\n"; - $headers['Content-Type'] = "text/plain; charset=UTF-8"; - } - else { - $boundary = sha1($headers['Message-Id']); - $content = ""; - $headers['Content-Type'] = "multipart/mixed; boundary=$boundary"; - foreach ($files as $file) { - $content .= "--$boundary\r\n"; - if (isset($file['name'])) { - $file['name'] = preg_replace('/[\r\n\0"]/', '', $file['name']); - $content .= "Content-Disposition: form-data; filename=\"$file[name]\"; name=\"attachment\"\r\n"; - } - $type = explode('/', $file['type'])[0]; - if ($type == 'text') { - $file['type'] .= '; charset=UTF-8'; - } - $content .= "Content-Type: $file[type]\r\n"; - if ($type != 'text' && $type != 'message') { - $file['text'] = base64_encode($file['text']); - $content .= "Content-Transfer-Encoding: base64\r\n"; - } - $content .= "\r\n"; - $content .= $file['text']; - $content .= "\r\n"; - } - $content .= "--$boundary--\r\n"; - - $headers['Mime-Version'] = '1.0'; - } - //$headers['Content-Length'] = strlen($content); - $headers['Date'] = date('r', $headers['Date']); - $out = ""; - foreach ($headers as $id => $val) { - $val = str_replace("\n", "\n\t", $val); - $out .= "$id: $val\r\n"; - } - $out .= "\r\n"; - $out .= $content; - return $out; -} - -function nntp_publish($msg, $id) { - global $config; - $server = $config["nntpchan"]["server"]; - $s = fsockopen("tcp://$server"); - fgets($s); - fputs($s, "MODE STREAM\r\n"); - fgets($s); - fputs($s, "TAKETHIS $id\r\n"); - fputs($s, $msg); - fputs($s, "\r\n.\r\n"); - fgets($s); - fputs($s, "QUIT\r\n"); - fclose($s); -} - -function post2nntp($post, $msgid) { - global $config; - - $headers = array(); - $files = array(); - - $headers['Message-Id'] = $msgid; - $headers['Newsgroups'] = $config['nntpchan']['group']; - $headers['Date'] = time(); - $headers['Subject'] = $post['subject'] ? $post['subject'] : "None"; - $headers['From'] = $post['name'] . " "; - - if ($post['email'] == 'sage') { - $headers['X-Sage'] = true; - } - - if (!$post['op']) { - // Get muh parent - $query = prepare("SELECT `message_id` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id"); - $query->bindValue(':board', $post['board']); - $query->bindValue(':id', $post['thread']); - $query->execute() or error(db_error($query)); - - if ($result = $query->fetch(PDO::FETCH_ASSOC)) { - $headers['References'] = $result['message_id']; - } - else { - return false; // We don't have OP. Discarding. - } - } - - // Let's parse the body a bit. - $body = trim($post['body_nomarkup']); - $body = preg_replace('/\r?\n/', "\r\n", $body); - $body = preg_replace_callback('@>>(>/([a-zA-Z0-9_+-]+)/)?([0-9]+)@', function($o) use ($post) { - if ($o[1]) { - $board = $o[2]; - } - else { - $board = $post['board']; - } - $id = $o[3]; - - $query = prepare("SELECT `message_id_digest` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id"); - $query->bindValue(':board', $board); - $query->bindValue(':id', $id); - $query->execute() or error(db_error($query)); - - if ($result = $query->fetch(PDO::FETCH_ASSOC)) { - return ">>".substr($result['message_id_digest'], 0, 18); - } - else { - return $o[0]; // Should send URL imo - } - }, $body); - $body = preg_replace('/>>>>([0-9a-fA-F])+/', '>>\1', $body); - - - $files[] = array('type' => 'text/plain', 'text' => $body); - - foreach ($post['files'] as $id => $file) { - $fc = array(); - - $fc['type'] = $file['type']; - $fc['text'] = file_get_contents($file['file_path']); - $fc['name'] = $file['name']; - - $files[] = $fc; - } - - return array($headers, $files); -} diff --git a/inc/nntpchan/tests.php b/inc/nntpchan/tests.php deleted file mode 100644 index a63789d7..00000000 --- a/inc/nntpchan/tests.php +++ /dev/null @@ -1,30 +0,0 @@ - "czaks ", "Message-Id" => "<1234.0000.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None"], -[['type' => 'text/plain', 'text' => "THIS IS A NEW TEST THREAD"]]); -echo "\n@@@@ Single msg:\n"; -echo $m1 = gennntp(["From" => "czaks ", "Message-Id" => "<1234.1234.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"], -[['type' => 'text/plain', 'text' => "hello world, with no image :("]]); -echo "\n@@@@ Single msg and pseudoimage:\n"; -echo $m2 = gennntp(["From" => "czaks ", "Message-Id" => "<1234.2137.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"], -[['type' => 'text/plain', 'text' => "hello world, now with an image!"], - ['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"]]); -echo "\n@@@@ Single msg and two pseudoimages:\n"; -echo $m3 = gennntp(["From" => "czaks ", "Message-Id" => "<1234.1488.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"], -[['type' => 'text/plain', 'text' => "hello world, now WITH TWO IMAGES!!!"], - ['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"], - ['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif2.gif"]]); -shoveitup($m0, "<1234.0000.".$time."@example.vichan.net>"); -sleep(1); -shoveitup($m1, "<1234.1234.".$time."@example.vichan.net>"); -sleep(1); -shoveitup($m2, "<1234.2137.".$time."@example.vichan.net>"); -shoveitup($m3, "<1234.1488.".$time."@example.vichan.net>"); - diff --git a/js/post-menu.js b/js/post-menu.js index 79cfd868..c2155c00 100644 --- a/js/post-menu.js +++ b/js/post-menu.js @@ -104,8 +104,10 @@ function buildMenu(e) { function addButton(post) { var $ele = $(post); + // Use unicode code with ascii variant selector + // https://stackoverflow.com/questions/37906969/how-to-prevent-ios-from-converting-ascii-into-emoji $ele.find('input.delete').after( - $('', {href: '#', class: 'post-btn', title: 'Post menu'}).text('►') + $('', {href: '#', class: 'post-btn', title: 'Post menu'}).text('\u{25B6}\u{fe0e}') ); } diff --git a/js/youtube.js b/js/youtube.js index 4a5a5afe..08989b5c 100644 --- a/js/youtube.js +++ b/js/youtube.js @@ -18,7 +18,7 @@ $(document).ready(function() { const ON = '[Remove]'; - const YOUTUBE = 'www.youtube.com'; + const YOUTUBE = 'www.youtube-nocookie.com'; function makeEmbedNode(embedHost, videoId, width, height) { return $(`