diff --git a/inc/config.php b/inc/config.php index 71b0fbf4..25031bfb 100644 --- a/inc/config.php +++ b/inc/config.php @@ -943,6 +943,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 +985,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. diff --git a/inc/error.php b/inc/error.php index 4c8eb19e..fed211ac 100644 --- a/inc/error.php +++ b/inc/error.php @@ -1,9 +1,222 @@ 0); + } + + private static function errnoToLogLevel(int $errno): int { + switch ($errno) { + default: + case \E_ERROR: + case \E_PARSE: + case \E_CORE_ERROR: + case \E_COMPILE_ERROR: + case \E_RECOVERABLE_ERROR: + return \LOG_EMERG; + case \E_USER_ERROR: + return \LOG_ERR; + case \E_WARNING: + case \E_CORE_WARNING: + case \E_COMPILE_WARNING: + case \E_USER_WARNING: + return \LOG_WARNING; + case \E_NOTICE: + case \E_DEPRECATED: + case \E_USER_NOTICE: + case \E_USER_DEPRECATED: + return \LOG_NOTICE; + } + } + + public static function handle_error( + int $level, + string $message, + callable $logger, + bool $display_details, + string $file, + int $line, + string $trace_str, + array $trace + ): void { + global $board, $mod, $config, $db_error; + + $newline = self::isCli() ? '\n' : '
'; + + if (self::$error_recursion !== false) { + list($o_message, $o_file, $o_line, $o_trace) = self::$error_recursion; + if ($display_details) { + echo("Error recursion detected with $message at $file:$line. Backtrace:$newline$trace_str{$newline}Original error: $o_message at $o_file:$o_line. Original backtrace:$newline$o_trace"); + } else { + echo("Error recursion detected with $message{$newline}Original error: $o_message"); + } + exit; + } + + self::$error_recursion = [ $message, $file, $line, $trace_str ]; + + // Message for the system, be it cli or error logging facility. + $sys_msg = "Error: $message at $file:$line.\nBacktrace: $trace_str\n"; + + if (self::isCli()) { + // Running from CLI. Always display the details. + echo($sys_msg); + exit; + } + + // Log the error if we aren't running from cli. + $logger($level, $sys_msg); + + // Recycled code. + $debug_stuff = []; + if ($config['debug']) { + if (isset($db_error)) { + $debug_stuff = \array_combine([ 'SQLSTATE', 'Error code', 'Error message' ], $db_error); + } + $debug_stuff['backtrace'] = $trace; + + // Remove the password from SQL errors. + $pw = $config['db']['password']; + $debug_callback = function($item) use (&$debug_callback, $pw) { + if (\is_array($item)) { + $item = \array_filter($item, $debug_callback); + } + return $item !== $pw || !$pw; + }; + $debug_stuff = \array_map($debug_callback, $debug_stuff); + } + + \header("{$_SERVER['SERVER_PROTOCOL']} 500 Internal Server Error"); + + // Error via json. + if (isset($_POST['json_response'])) { + \header('Content-Type: text/json; charset=utf-8'); + $data = [ 'error' => $message ]; + if (!empty($config) && $config['debug'] && !empty($debug_stuff)) { + $data['debug'] = $debug_stuff; + } + echo(\json_encode($data)); + exit; + } + + // Pretty print. + die(Element('page.html', [ + 'config' => $config, + 'title' => _('Error'), + 'subtitle' => _('An error has occured.'), + 'body' => Element('error.html', [ + 'config' => $config, + 'message' => $message, + 'mod' => $mod, + 'board' => isset($board) ? $board : false, + 'debug' => \str_replace("\n", ' ', utf8tohtml(print_r($debug_stuff, true))) + ]) + ])); + } + + private function default_logger(int $level, string $message) { + global $config; + + if (isset($config['syslog']) && $config['syslog']) { + _syslog($level, $message); + } else { + \error_log($message); + } + } + + public function __construct() { + $this->include_details = self::isCli(); + $this->logger_fn = [ __CLASS__, 'default_logger' ]; + } + + public function setIncludeDetails(bool $include_details) { + $this->include_details = $include_details; + } + + public function setLogger(?callable $logger_fn) { + $this->logger_fn = $logger_fn !== null ? $logger_fn : [ __CLASS__, 'default_logger' ]; + } + + public function installGlobally() { + $logger_fn = $this->logger_fn; + $include_details = $this->include_details || self::isCli(); + + \set_error_handler(function($errno, $errstr, $errfile, $errline) use ($logger_fn, $include_details) { + $errno = \error_reporting() & $errno; + if ($errno !== 0) { + $level = self::errnoToLogLevel($errno); + + // https://stackoverflow.com/a/35930682 + $e = new \Exception(); + $trace_str = $e->getTraceAsString(); + $trace = $e->getTrace(); + self::handle_error($level, $errstr, $logger_fn, $include_details, $errfile, $errline, $trace_str, $trace); + } + return false; + }); + + \set_exception_handler(function($exception) use ($logger_fn, $include_details) { + $message = $exception->getMessage(); + $file = $exception->getFile(); + $line = $exception->getLine(); + $trace_str = $exception->getTraceAsString(); + $trace = $exception->getTrace(); + + self::handle_error(\LOG_EMERG, $message, $logger_fn, $include_details, $file, $line, $trace_str, $trace); + }); + + \register_shutdown_function(function() use ($logger_fn, $include_details) { + $error = \error_get_last(); + if ($error !== null) { + $level = self::errnoToLogLevel($error['type']); + $message = $error['message']; + $file = $error['file']; + $line = $error['line']; + + self::handle_error($level, $message, $logger_fn, $include_details, $file, $line, '', []); + } + }); + } +} + +/** + * Install a fancy error handler with the given logger. + * + * @param ?callable $logger Called when there is to log something. Use null to use the minimal default implementation. + * The callable is invoke with two parameters, the first being the log level (integer), as + * listed by the syslog constants, the second is the log message (string). + * Consider that $config may not have been initialized yet. + */ +function install_error_handler(bool $basic, ?callable $logger_fn): void { + global $config; + + $logger_fn ??= function($level, $message) use ($config) { + if (isset($config['syslog']) && $config['syslog']) { + _syslog($level, $message); + } else { + \error_log($message); + } + }; + + if (!$basic) { + \set_error_handler(function($errno, $errstr, $errfile, $errline) use ($config) { + if (\error_reporting() & $errno) { + $config['debug'] = true; + \error("$errstr in $errfile at line $errline"); + } + return false; + }); } return false; } @@ -87,7 +300,7 @@ function error($message, $priority = true, $debug_stuff = false) { 'message' => $message, 'mod' => $mod, 'board' => isset($board) ? $board : false, - 'debug' => str_replace("\n", ' ', utf8tohtml(print_r($debug_stuff, true))) + 'debug' => \str_replace("\n", ' ', utf8tohtml(\print_r($debug_stuff, true))) )) ])); } 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/post.php b/post.php index 5483600d..27a45413 100644 --- a/post.php +++ b/post.php @@ -1402,13 +1402,13 @@ function handle_post(Context $ctx) $file['thumbwidth'] = $size[0]; $file['thumbheight'] = $size[1]; } elseif ( - (($config['strip_exif'] && isset($file['exif_stripped']) && $file['exif_stripped']) || !$config['strip_exif']) && + $config['minimum_copy_resize'] && $image->size->width <= $config['thumb_width'] && $image->size->height <= $config['thumb_height'] && $file['extension'] == ($config['thumb_ext'] ? $config['thumb_ext'] : $file['extension']) ) { // Copy, because there's nothing to resize - copy($file['tmp_name'], $file['thumb']); + coopy($file['tmp_name'], $file['thumb']); $file['thumbwidth'] = $image->size->width; $file['thumbheight'] = $image->size->height; @@ -1551,6 +1551,35 @@ function handle_post(Context $ctx) } } + if ($config['tesseract_ocr'] && $file['thumb'] != 'file') { + // Let's OCR it! + $fname = $file['tmp_name']; + + if ($file['height'] > 500 || $file['width'] > 500) { + $fname = $file['thumb']; + } + + if ($fname == 'spoiler') { + // We don't have that much CPU time, do we? + } else { + $tmpname = __DIR__ . "/tmp/tesseract/" . rand(0, 10000000); + + // Preprocess command is an ImageMagick b/w quantization + $error = shell_exec_error(sprintf($config['tesseract_preprocess_command'], escapeshellarg($fname)) . " | " . + 'tesseract stdin ' . escapeshellarg($tmpname) . ' ' . $config['tesseract_params']); + $tmpname .= ".txt"; + + $value = @file_get_contents($tmpname); + @unlink($tmpname); + + if ($value && trim($value)) { + // This one has an effect, that the body is appended to a post body. So you can write a correct + // spamfilter. + $post['body_nomarkup'] .= "" . htmlspecialchars($value) . ""; + } + } + } + if (!isset($dont_copy_file) || !$dont_copy_file) { if (isset($file['file_tmp'])) { if (!@rename($file['tmp_name'], $file['file'])) { @@ -1598,6 +1627,11 @@ function handle_post(Context $ctx) } } + // Do filters again if OCRing + if ($config['tesseract_ocr'] && !hasPermission($config['mod']['bypass_filters'], $board['uri']) && !$dropped_post) { + do_filters($ctx, $post); + } + if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup']) && !$dropped_post) { undoImage($post); if ($config['robot_mute']) { diff --git a/static/banners/just-monika-opt.webp b/static/banners/just-monika-opt.webp new file mode 100644 index 00000000..731f985a Binary files /dev/null and b/static/banners/just-monika-opt.webp differ diff --git a/stylesheets/style.css b/stylesheets/style.css index 4a090f33..815e1853 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -380,7 +380,6 @@ form table tr td div.center { .file { float: left; - min-width: 100px; } .file:not(.multifile) .post-image { @@ -391,10 +390,6 @@ form table tr td div.center { float: none; } -.file.multifile { - margin: 0 10px 0 0; -} - .file.multifile > p { width: 0px; min-width: 100%; diff --git a/templates/themes/categories/frames.html b/templates/themes/categories/frames.html index 1c4673cc..6b2fac38 100644 --- a/templates/themes/categories/frames.html +++ b/templates/themes/categories/frames.html @@ -16,13 +16,13 @@ border-width: 2px; margin-right: 15px; } - + .introduction { grid-column: 2 / 9; grid-row: 1; width: 100%; } - + .content { grid-column: 2 / 9; grid-row: 2; @@ -35,7 +35,7 @@ gap: 20px; height: 100vh; } - + .modlog { width: 50%; text-align: left; @@ -69,7 +69,7 @@ li a.system { font-weight: bold; } - + @media (max-width:768px) { body{ display: grid; @@ -78,7 +78,7 @@ height: 100vh; width: 100%; } - + .introduction { grid-column: 1; grid-row: 1; @@ -96,18 +96,17 @@ grid-column: 1; grid-row: 3; width: 100%; - word-break: break-all; } - + .modlog { width: 100%; text-align: center; } - + table { table-layout: fixed; } - + table.modlog tr th { white-space: normal; word-wrap: break-word; diff --git a/templates/themes/categories/news.html b/templates/themes/categories/news.html index 76722e41..484c4eb0 100644 --- a/templates/themes/categories/news.html +++ b/templates/themes/categories/news.html @@ -11,7 +11,7 @@ .home-description { margin: 20px auto 0 auto; text-align: center; - max-width: 700px; + max-width: 700px;" } {{ boardlist.top }} diff --git a/tmp/tesseract/.gitkeep b/tmp/tesseract/.gitkeep new file mode 100644 index 00000000..e69de29b