From c483e1258cb5d168812a6e509e982a172f6c36c6 Mon Sep 17 00:00:00 2001 From: copypaste Date: Sun, 27 Apr 2014 15:48:47 +0200 Subject: [PATCH] multiimage posting --- inc/api.php | 49 ++-- inc/config.php | 46 ++-- inc/display.php | 21 +- inc/functions.php | 89 +++---- .../Twig/Extensions/Extension/Tinyboard.php | 6 +- inc/mod/pages.php | 6 +- install.php | 25 +- js/multi_image.js | 30 +++ post.php | 224 +++++++++++------- stylesheets/style.css | 3 + templates/main.js | 1 + templates/post/fileinfo.html | 38 +++ templates/post_reply.html | 34 +-- templates/post_thread.html | 34 +-- templates/posts.sql | 12 +- templates/themes/catalog/theme.php | 5 +- templates/themes/recent/theme.php | 12 +- templates/themes/ukko/theme.php | 4 +- 18 files changed, 366 insertions(+), 273 deletions(-) create mode 100644 js/multi_image.js create mode 100644 templates/post/fileinfo.html diff --git a/inc/api.php b/inc/api.php index 3337c613..a8ae9eed 100644 --- a/inc/api.php +++ b/inc/api.php @@ -26,12 +26,6 @@ class Api { 'trip' => 'trip', 'capcode' => 'capcode', 'time' => 'time', - 'thumbheight' => 'tn_w', - 'thumbwidth' => 'tn_h', - 'fileheight' => 'w', - 'filewidth' => 'h', - 'filesize' => 'fsize', - 'filename' => 'filename', 'omitted' => 'omitted_posts', 'omitted_images' => 'omitted_images', 'replies' => 'replies', @@ -46,6 +40,15 @@ class Api { 'bump' => 'last_modified' ); + $this->fileFields = array( + 'thumbheight' => 'tn_w', + 'thumbwidth' => 'tn_h', + 'height' => 'w', + 'width' => 'h', + 'size' => 'fsize', + 'file' => 'filename', + ); + if (isset($config['api']['extra_fields']) && gettype($config['api']['extra_fields']) == 'array'){ $this->postFields = array_merge($this->postFields, $config['api']['extra_fields']); } @@ -67,30 +70,26 @@ class Api { 'last_modified' => 1 ); - private function translatePost($post, $threadsPage = false) { - $apiPost = array(); - $fields = $threadsPage ? $this->threadsPageFields : $this->postFields; + private function translateFields($fields, $object, &$apiPost) { foreach ($fields as $local => $translated) { - if (!isset($post->$local)) + if (!isset($object->$local)) continue; $toInt = isset(self::$ints[$translated]); - $val = $post->$local; + $val = $object->$local; if ($val !== null && $val !== '') { $apiPost[$translated] = $toInt ? (int) $val : $val; } } + } - if ($threadsPage) return $apiPost; + private function translatePost($post, $threadsPage = false) { + $apiPost = array(); + $fields = $threadsPage ? $this->threadsPageFields : $this->postFields; + $this->translateFields($fields, $post, $apiPost); - if (isset($post->filename)) { - $dotPos = strrpos($post->filename, '.'); - $apiPost['filename'] = substr($post->filename, 0, $dotPos); - $apiPost['ext'] = substr($post->filename, $dotPos); - $dotPos = strrpos($post->file, '.'); - $apiPost['tim'] = substr($post->file, 0, $dotPos); - } + if ($threadsPage) return $apiPost; // Handle country field if (isset($post->body_nomarkup) && $this->config['country_flags']) { @@ -104,6 +103,18 @@ class Api { } } + // Handle files + // Note: 4chan only supports one file, so only the first file is taken into account for 4chan-compatible API. + if (isset($post->files) && $post->files && !$threadsPage) { + $file = $post->files[0]; + $this->translateFields($this->fileFields, $file, $apiPost); + $dotPos = strrpos($file->file, '.'); + $apiPost['filename'] = substr($file->file, 0, $dotPos); + $apiPost['ext'] = substr($file->file, $dotPos); + $dotPos = strrpos($file->file, '.'); + $apiPost['tim'] = substr($file->file, 0, $dotPos); + } + return $apiPost; } diff --git a/inc/config.php b/inc/config.php index a4d1c6d5..6ec08a09 100644 --- a/inc/config.php +++ b/inc/config.php @@ -607,6 +607,17 @@ * Image settings * ==================== */ + // Maximum number of images allowed. Increasing this number enabled multi image. + // If you make it more than 1, make sure to enable the below script for the post form to change. + // $config['additional_javascript'][] = 'js/multi_image.js'; + $config['max_images'] = 1; + + // Method to use for determing the max filesize. + // "split" means that your max filesize is split between the images. For example, if your max filesize + // is 2MB, the filesizes of all files must add up to 2MB for it to work. + // "each" means that each file can be 2MB, so if your max_images is 3, each post could contain 6MB of + // images. "split" is recommended. + $config['multiimage_method'] = 'split'; // For resizing, maximum thumbnail dimensions. $config['thumb_width'] = 255; @@ -628,22 +639,22 @@ /* * Thumbnailing method: * - * 'gd' PHP GD (default). Only handles the most basic image formats (GIF, JPEG, PNG). - * GD is a prerequisite for Tinyboard no matter what method you choose. + * 'gd' PHP GD (default). Only handles the most basic image formats (GIF, JPEG, PNG). + * GD is a prerequisite for Tinyboard no matter what method you choose. * - * 'imagick' PHP's ImageMagick bindings. Fast and efficient, supporting many image formats. - * A few minor bugs. http://pecl.php.net/package/imagick + * 'imagick' PHP's ImageMagick bindings. Fast and efficient, supporting many image formats. + * A few minor bugs. http://pecl.php.net/package/imagick * - * 'convert' The command line version of ImageMagick (`convert`). Fixes most of the bugs in - * PHP Imagick. `convert` produces the best still thumbnails and is highly recommended. + * 'convert' The command line version of ImageMagick (`convert`). Fixes most of the bugs in + * PHP Imagick. `convert` produces the best still thumbnails and is highly recommended. * - * 'gm' GraphicsMagick (`gm`) is a fork of ImageMagick with many improvements. It is more - * efficient and gets thumbnailing done using fewer resources. + * 'gm' GraphicsMagick (`gm`) is a fork of ImageMagick with many improvements. It is more + * efficient and gets thumbnailing done using fewer resources. * * 'convert+gifscale' - * OR 'gm+gifsicle' Same as above, with the exception of using `gifsicle` (command line application) - * instead of `convert` for resizing GIFs. It's faster and resulting animated - * thumbnails have less artifacts than if resized with ImageMagick. + * OR 'gm+gifsicle' Same as above, with the exception of using `gifsicle` (command line application) + * instead of `convert` for resizing GIFs. It's faster and resulting animated + * thumbnails have less artifacts than if resized with ImageMagick. */ $config['thumb_method'] = 'gd'; // $config['thumb_method'] = 'convert'; @@ -693,7 +704,7 @@ // An alternative function for generating image filenames, instead of the default UNIX timestamp. // $config['filename_func'] = function($post) { - // return sprintf("%s", time() . substr(microtime(), 2, 3)); + // return sprintf("%s", time() . substr(microtime(), 2, 3)); // }; // Thumbnail to use for the non-image file uploads. @@ -986,6 +997,7 @@ $config['error']['toolong_body'] = _('The body was too long.'); $config['error']['tooshort_body'] = _('The body was too short or empty.'); $config['error']['noimage'] = _('You must upload an image.'); + $config['error']['toomanyimages'] = _('You have attempted to upload too many images!'); $config['error']['nomove'] = _('The server failed to handle your upload.'); $config['error']['fileext'] = _('Unsupported image format.'); $config['error']['noboard'] = _('Invalid board!'); @@ -1444,16 +1456,16 @@ $config['search']['enable'] = false; // Maximal number of queries per IP address per minutes - $config['search']['queries_per_minutes'] = Array(15, 2); + $config['search']['queries_per_minutes'] = Array(15, 2); // Global maximal number of queries per minutes - $config['search']['queries_per_minutes_all'] = Array(50, 2); + $config['search']['queries_per_minutes_all'] = Array(50, 2); // Limit of search results - $config['search']['search_limit'] = 100; - + $config['search']['search_limit'] = 100; + // Boards for searching - //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); + //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); /* * ==================== diff --git a/inc/display.php b/inc/display.php index f7fe1620..0ae0ac05 100644 --- a/inc/display.php +++ b/inc/display.php @@ -94,7 +94,7 @@ function error($message, $priority = true, $debug_stuff = false) { // Return the bad request header, necessary for AJAX posts // czaks: is it really so? the ajax errors only work when this is commented out - // better yet use it when ajax is disabled + // better yet use it when ajax is disabled if (!isset ($_POST['json_response'])) { header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); } @@ -338,6 +338,9 @@ class Post { foreach ($post as $key => $value) { $this->{$key} = $value; } + + if (isset($this->files)) + $this->files = json_decode($this->files); $this->subject = utf8tohtml($this->subject); $this->name = utf8tohtml($this->name); @@ -396,7 +399,7 @@ class Post { $built .= ' ' . $config['mod']['link_bandelete'] . ''; // Delete file (keep post) - if (!empty($this->file) && hasPermission($config['mod']['deletefile'], $board['uri'], $this->mod)) + if (!empty($this->files) && hasPermission($config['mod']['deletefile'], $board['uri'], $this->mod)) $built .= ' ' . secure_link_confirm($config['mod']['link_deletefile'], _('Delete file'), _('Are you sure you want to delete this file?'), $board['dir'] . 'deletefile/' . $this->id); // Spoiler file (keep post) @@ -418,9 +421,6 @@ class Post { return $built; } - public function ratio() { - return fraction($this->filewidth, $this->fileheight, ':'); - } public function build($index=false) { global $board, $config; @@ -439,6 +439,9 @@ class Thread { $this->{$key} = $value; } + if (isset($this->files)) + $this->files = json_decode($this->files); + $this->subject = utf8tohtml($this->subject); $this->name = utf8tohtml($this->name); $this->mod = $mod; @@ -477,7 +480,7 @@ class Thread { $this->posts[] = $post; } public function postCount() { - return count($this->posts) + $this->omitted; + return count($this->posts) + $this->omitted; } public function postControls() { global $board, $config; @@ -506,7 +509,7 @@ class Thread { $built .= ' ' . $config['mod']['link_bandelete'] . ''; // Delete file (keep post) - if (!empty($this->file) && $this->file != 'deleted' && hasPermission($config['mod']['deletefile'], $board['uri'], $this->mod)) + if (!empty($this->files) && $this->files[0]->file != 'deleted' && hasPermission($config['mod']['deletefile'], $board['uri'], $this->mod)) $built .= ' ' . secure_link_confirm($config['mod']['link_deletefile'], _('Delete file'), _('Are you sure you want to delete this file?'), $board['dir'] . 'deletefile/' . $this->id); // Spoiler file (keep post) @@ -546,10 +549,6 @@ class Thread { return $built; } - public function ratio() { - return fraction($this->filewidth, $this->fileheight, ':'); - } - public function build($index=false, $isnoko50=false) { global $board, $config, $debug; diff --git a/inc/functions.php b/inc/functions.php index 820ee838..1db2b7f3 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -96,9 +96,9 @@ function loadConfig() { $configstr = file_get_contents('inc/instance-config.php'); - if (isset($board['dir']) && file_exists($board['dir'] . '/config.php')) { - $configstr .= file_get_contents($board['dir'] . '/config.php'); - } + if (isset($board['dir']) && file_exists($board['dir'] . '/config.php')) { + $configstr .= file_get_contents($board['dir'] . '/config.php'); + } $matches = array(); preg_match_all('/[^\/*#]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches); if ($matches && isset ($matches[2]) && $matches[2]) { @@ -859,7 +859,7 @@ function insertFloodPost(array $post) { function post(array $post) { global $pdo, $board; - $query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :thumb, :thumbwidth, :thumbheight, :file, :width, :height, :filesize, :filename, :filehash, :password, :ip, :sticky, :locked, 0, :embed)", $board['uri'])); + $query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, 0, :embed)", $board['uri'])); // Basic stuff if (!empty($post['subject'])) { @@ -919,31 +919,12 @@ function post(array $post) { } if ($post['has_file']) { - $query->bindValue(':thumb', $post['thumb']); - $query->bindValue(':thumbwidth', $post['thumbwidth'], PDO::PARAM_INT); - $query->bindValue(':thumbheight', $post['thumbheight'], PDO::PARAM_INT); - $query->bindValue(':file', $post['file']); - - if (isset($post['width'], $post['height'])) { - $query->bindValue(':width', $post['width'], PDO::PARAM_INT); - $query->bindValue(':height', $post['height'], PDO::PARAM_INT); - } else { - $query->bindValue(':width', null, PDO::PARAM_NULL); - $query->bindValue(':height', null, PDO::PARAM_NULL); - } - - $query->bindValue(':filesize', $post['filesize'], PDO::PARAM_INT); - $query->bindValue(':filename', $post['filename']); + $query->bindValue(':files', json_encode($post['files'])); + $query->bindValue(':num_files', $post['num_files']); $query->bindValue(':filehash', $post['filehash']); } else { - $query->bindValue(':thumb', null, PDO::PARAM_NULL); - $query->bindValue(':thumbwidth', null, PDO::PARAM_NULL); - $query->bindValue(':thumbheight', null, PDO::PARAM_NULL); - $query->bindValue(':file', null, PDO::PARAM_NULL); - $query->bindValue(':width', null, PDO::PARAM_NULL); - $query->bindValue(':height', null, PDO::PARAM_NULL); - $query->bindValue(':filesize', null, PDO::PARAM_NULL); - $query->bindValue(':filename', null, PDO::PARAM_NULL); + $query->bindValue(':files', null, PDO::PARAM_NULL); + $query->bindValue(':num_files', 0); $query->bindValue(':filehash', null, PDO::PARAM_NULL); } @@ -974,28 +955,31 @@ function bumpThread($id) { function deleteFile($id, $remove_entirely_if_already=true) { global $board, $config; - $query = prepare(sprintf("SELECT `thread`,`thumb`,`file` FROM ``posts_%s`` WHERE `id` = :id LIMIT 1", $board['uri'])); + $query = prepare(sprintf("SELECT `thread`, `files` FROM ``posts_%s`` WHERE `id` = :id LIMIT 1", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); if (!$post = $query->fetch(PDO::FETCH_ASSOC)) error($config['error']['invalidpost']); + $files = json_decode($post['files']); - if ($post['file'] == 'deleted' && !$post['thread']) + if ($files[0]->file == 'deleted' && !$post['thread']) return; // Can't delete OP's image completely. - $query = prepare(sprintf("UPDATE ``posts_%s`` SET `thumb` = NULL, `thumbwidth` = NULL, `thumbheight` = NULL, `filewidth` = NULL, `fileheight` = NULL, `filesize` = NULL, `filename` = NULL, `filehash` = NULL, `file` = :file WHERE `id` = :id", $board['uri'])); - if ($post['file'] == 'deleted' && $remove_entirely_if_already) { + $query = prepare(sprintf("UPDATE ``posts_%s`` SET `files` = :file WHERE `id` = :id", $board['uri'])); + if ($files[0]->file == 'deleted' && $remove_entirely_if_already) { // Already deleted; remove file fully $query->bindValue(':file', null, PDO::PARAM_NULL); } else { - // Delete thumbnail - file_unlink($board['dir'] . $config['dir']['thumb'] . $post['thumb']); + foreach ($files as $i => $file) { + // Delete thumbnail + file_unlink($board['dir'] . $config['dir']['thumb'] . $file->thumb); - // Delete file - file_unlink($board['dir'] . $config['dir']['img'] . $post['file']); + // Delete file + file_unlink($board['dir'] . $config['dir']['img'] . $file->file); + } // Set file to 'deleted' - $query->bindValue(':file', 'deleted', PDO::PARAM_INT); + $query->bindValue(':file', '[{"file":"deleted"}]', PDO::PARAM_STR); } $query->bindValue(':id', $id, PDO::PARAM_INT); @@ -1035,7 +1019,7 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { global $board, $config; // Select post and replies (if thread) in one query - $query = prepare(sprintf("SELECT `id`,`thread`,`thumb`,`file` FROM ``posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri'])); + $query = prepare(sprintf("SELECT `id`,`thread`,`files` FROM ``posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); @@ -1065,13 +1049,12 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { // Rebuild thread $rebuild = &$post['thread']; } - if ($post['thumb']) { - // Delete thumbnail - file_unlink($board['dir'] . $config['dir']['thumb'] . $post['thumb']); - } - if ($post['file']) { + if ($post['files']) { // Delete file - file_unlink($board['dir'] . $config['dir']['img'] . $post['file']); + foreach (json_decode($post['files']) as $i => $f) { + file_unlink($board['dir'] . $config['dir']['img'] . $f->file); + file_unlink($board['dir'] . $config['dir']['thumb'] . $f->thumb); + } } $ids[] = (int)$post['id']; @@ -1188,8 +1171,8 @@ function index($page, $mod=false) { $num_images = 0; foreach ($replies as $po) { - if ($po['file']) - $num_images++; + if ($po['num_files']) + $num_images+=$po['num_files']; $thread->add(new Post($po, $mod ? '?/' : $config['root'], $mod)); } @@ -1347,7 +1330,7 @@ function checkRobot($body) { // Returns an associative array with 'replies' and 'images' keys function numPosts($id) { global $board; - $query = prepare(sprintf("SELECT COUNT(*) AS `replies`, COUNT(NULLIF(`file`, 0)) AS `images` FROM ``posts_%s`` WHERE `thread` = :thread", $board['uri'], $board['uri'])); + $query = prepare(sprintf("SELECT COUNT(*) AS `replies`, SUM(`num_files`) AS `images` FROM ``posts_%s`` WHERE `thread` = :thread", $board['uri'], $board['uri'])); $query->bindValue(':thread', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); @@ -1905,7 +1888,7 @@ function markup(&$body, $track_cites = false) { } // replace tabs with 8 spaces - $body = str_replace("\t", ' ', $body); + $body = str_replace("\t", ' ', $body); return $tracked_cites; } @@ -2232,13 +2215,15 @@ function getPostByHashInThread($hash, $thread) { } function undoImage(array $post) { - if (!$post['has_file']) + if (!$post['has_file'] || !isset($post['files'])) return; - if (isset($post['file_path'])) - file_unlink($post['file_path']); - if (isset($post['thumb_path'])) - file_unlink($post['thumb_path']); + foreach ($post['files'] as $key => $file) { + if (isset($file['file_path'])) + file_unlink($file['file_path']); + if (isset($file['thumb_path'])) + file_unlink($file['thumb_path']); + } } function rDNS($ip_addr) { diff --git a/inc/lib/Twig/Extensions/Extension/Tinyboard.php b/inc/lib/Twig/Extensions/Extension/Tinyboard.php index 727d5d2c..4063bfd4 100644 --- a/inc/lib/Twig/Extensions/Extension/Tinyboard.php +++ b/inc/lib/Twig/Extensions/Extension/Tinyboard.php @@ -25,7 +25,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension new Twig_SimpleFilter('until', 'until'), new Twig_SimpleFilter('push', 'twig_push_filter'), new Twig_SimpleFilter('bidi_cleanup', 'bidi_cleanup'), - new Twig_SimpleFilter('addslashes', 'addslashes') + new Twig_SimpleFilter('addslashes', 'addslashes'), ); } @@ -42,6 +42,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension new Twig_SimpleFunction('timezone', 'twig_timezone_function'), new Twig_SimpleFunction('hiddenInputs', 'hiddenInputs'), new Twig_SimpleFunction('hiddenInputsHash', 'hiddenInputsHash'), + new Twig_SimpleFunction('ratio', 'twig_ratio_function') ); } @@ -100,3 +101,6 @@ function twig_truncate_filter($value, $length = 30, $preserve = false, $separato return $value; } +function twig_ratio_function($w, $h) { + return fraction($w, $h, ':'); +} diff --git a/inc/mod/pages.php b/inc/mod/pages.php index a247e35c..89bc6e41 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -1538,10 +1538,10 @@ function mod_deletefile($board, $post) { function mod_spoiler_image($board, $post) { global $config, $mod; - + if (!openBoard($board)) error($config['error']['noboard']); - + if (!hasPermission($config['mod']['spoilerimage'], $board)) error($config['error']['noaccess']); @@ -1572,7 +1572,7 @@ function mod_spoiler_image($board, $post) { // Rebuild themes rebuildThemes('post-delete', $board); - + // Redirect header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']); } diff --git a/install.php b/install.php index 16ef9fcb..4a5a54cb 100644 --- a/install.php +++ b/install.php @@ -1,7 +1,7 @@ execute() or error(db_error($_query)); } } - case 'v0.9.6-dev-9': + case 'v0.9.6-dev-9': case 'v0.9.6-dev-9 + vichan-devel-4.0.3': case 'v0.9.6-dev-9 + vichan-devel-4.0.4-gold': case 'v0.9.6-dev-9 + vichan-devel-4.0.5-gold': @@ -521,6 +521,14 @@ if (file_exists($config['has_installed'])) { case '4.4.98-pre': if (!$twig) load_twig(); $twig->clearCacheFiles(); + case '4.4.98': + foreach ($boards as &$board) { + query(sprintf('ALTER TABLE ``posts_%s`` ADD `files` text DEFAULT NULL AFTER `bump`;', $board['uri'])) or error(db_error()); + query(sprintf('ALTER TABLE ``posts_%s`` ADD `num_files` int(11) DEFAULT 0 AFTER `files`;', $board['uri'])) or error(db_error()); + query(sprintf('UPDATE ``posts_%s`` SET `files` = CONCAT(\'[{"file":"\',`filename`,\'", "size":"\',`filesize`,\'", "width":"\',`filewidth`,\'","height":"\',`fileheight`,\'","thumbwidth":"\',`thumbwidth`,\'","thumbheight":"\',`thumbheight`,\'", "file_path":"%s\/src\/\',`filename`,\'","thumb_path":"%s\/thumb\/\',`filename`,\'"}]\') WHERE `file` IS NOT NULL', $board['uri'], $board['uri'], $board['uri'])) or error(db_error()); + query(sprintf('ALTER TABLE ``posts_%s`` DROP COLUMN `thumb`, DROP COLUMN `thumbwidth`, DROP COLUMN `thumbheight`, DROP COLUMN `file`, DROP COLUMN `fileheight`, DROP COLUMN `filesize`, DROP COLUMN `filename`', $board['uri'])) or error(db_error()); + query(sprintf('ALTER TABLE ``posts_%s`` REBUILD', $board['uri'])) or error(db_error()); + } case false: // TODO: enhance Tinyboard -> vichan upgrade path. query("CREATE TABLE IF NOT EXISTS ``search_queries`` ( `ip` varchar(39) NOT NULL, `time` int(11) NOT NULL, `query` text NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=utf8;") or error(db_error()); @@ -586,7 +594,7 @@ if ($step == 0) { 'required' => false ) ); - + $tests = array( array( 'category' => 'PHP', @@ -679,6 +687,13 @@ if ($step == 0) { 'required' => false, 'message' => '(Optional) `gifsicle` was not found or executable; you may not use `convert+gifsicle` for better animated GIF thumbnailing.', ), + array( + 'category' => 'Image processing', + 'name' => '`md5sum` (quick file hashing)', + 'result' => $can_exec && shell_exec('echo "vichan" | md5sum') == "141225c362da02b5c359c45b665168de -\n", + 'required' => false, + 'message' => '(Optional) `md5sum` was not found or executable; file hashing for multiple images will be slower.', + ), array( 'category' => 'File permissions', 'name' => getcwd(), @@ -833,9 +848,9 @@ if ($step == 0) { } file_write($config['has_installed'], VERSION); - if (!file_unlink(__FILE__)) { + /*if (!file_unlink(__FILE__)) { $page['body'] .= '

Delete install.php!

I couldn\'t remove install.php. You will have to remove it manually.

'; - } + }*/ } echo Element('page.html', $page); diff --git a/js/multi_image.js b/js/multi_image.js new file mode 100644 index 00000000..cb2f2ccf --- /dev/null +++ b/js/multi_image.js @@ -0,0 +1,30 @@ +/* + * multi-image.js - Add support for multiple images to the post form + * + * Copyright (c) 2014 Fredrick Brennan + * + * Usage: + * $config['max_images'] = 3; + * $config['additional_javascript'][] = 'js/jquery.min.js'; + * $config['additional_javascript'][] = 'js/multi-image.js'; + */ + +function multi_image() { + $('input[type=file]').after('+'); + + $(document).on('click', 'a.add_image', function(e) { + e.preventDefault(); + $('#upload_url').remove(); + + var images_len = $('input[type=file]').length; + + if (!(images_len >= max_images)) { + $('.add_image').after('
'); + if (typeof setup_form !== 'undefined') setup_form($('form[name="post"]')); + } + }) +} + +if (active_page == 'thread' || active_page == 'index' && max_images > 1) { + $(document).ready(multi_image); +} diff --git a/post.php b/post.php index 91abe7e6..3420f7f4 100644 --- a/post.php +++ b/post.php @@ -150,7 +150,7 @@ if (isset($_POST['delete'])) { if (!isset($_POST['body'], $_POST['board'])) error($config['error']['bot']); - $post = array('board' => $_POST['board']); + $post = array('board' => $_POST['board'], 'files' => array()); // Check if board exists if (!openBoard($post['board'])) @@ -178,7 +178,7 @@ if (isset($_POST['delete'])) { $post['op'] = true; if (!(($post['op'] && $_POST['post'] == $config['button_newtopic']) || - (!$post['op'] && $_POST['post'] == $config['button_reply']))) + (!$post['op'] && $_POST['post'] == $config['button_reply']))) error($config['error']['bot']); // Check the referrer @@ -328,21 +328,12 @@ if (isset($_POST['delete'])) { ); } - // Check for a file - if ($post['op'] && !isset($post['no_longer_require_an_image_for_op'])) { - if (!isset($_FILES['file']['tmp_name']) || $_FILES['file']['tmp_name'] == '' && $config['force_image_op']) - error($config['error']['noimage']); - } - $post['name'] = $_POST['name'] != '' ? $_POST['name'] : $config['anonymous']; $post['subject'] = $_POST['subject']; $post['email'] = str_replace(' ', '%20', htmlspecialchars($_POST['email'])); $post['body'] = $_POST['body']; $post['password'] = $_POST['password']; - $post['has_file'] = !isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || (isset($_FILES['file']) && $_FILES['file']['tmp_name'] != '')); - - if ($post['has_file']) - $post['filename'] = urldecode(get_magic_quotes_gpc() ? stripslashes($_FILES['file']['name']) : $_FILES['file']['name']); + $post['has_file'] = (!isset($post['embed']) && (($post['op'] && !isset($post['no_longer_require_an_image_for_op']) && $config['force_image_op']) || !empty($_FILES))); if (!($post['has_file'] || isset($post['embed'])) || (($post['op'] && $config['force_body_op']) || (!$post['op'] && $config['force_body']))) { $stripped_whitespace = preg_replace('/[\s]/u', '', $post['body']); @@ -367,6 +358,22 @@ if (isset($_POST['delete'])) { } if ($post['has_file']) { + // Determine size sanity + $size = 0; + if ($config['multiimage_method'] == 'split') { + foreach ($_FILES as $key => $file) { + $size += $file['size']; + } + } elseif ($config['multiimage_method'] == 'each') { + foreach ($_FILES as $key => $file) { + if ($file['size'] > $size) { + $size = $file['size']; + } + } + } else { + error(_('Unrecognized file size determination method.')); + } + $size = $_FILES['file']['size']; if ($size > $config['max_filesize']) error(sprintf3($config['error']['filesize'], array( @@ -374,6 +381,7 @@ if (isset($_POST['delete'])) { 'filesz' => number_format($size), 'maxsz' => number_format($config['max_filesize']) ))); + $post['filesize'] = $size; } @@ -409,16 +417,39 @@ if (isset($_POST['delete'])) { } else $noko = $config['always_noko']; if ($post['has_file']) { - $post['extension'] = strtolower(mb_substr($post['filename'], mb_strrpos($post['filename'], '.') + 1)); - if (isset($config['filename_func'])) - $post['file_id'] = $config['filename_func']($post); - else - $post['file_id'] = time() . substr(microtime(), 2, 3); - - $post['file'] = $board['dir'] . $config['dir']['img'] . $post['file_id'] . '.' . $post['extension']; - $post['thumb'] = $board['dir'] . $config['dir']['thumb'] . $post['file_id'] . '.' . ($config['thumb_ext'] ? $config['thumb_ext'] : $post['extension']); + $i = 0; + foreach ($_FILES as $key => $file) { + if ($file['size'] && $file['tmp_name']) { + $file['filename'] = urldecode(get_magic_quotes_gpc() ? stripslashes($file['name']) : $file['name']); + $file['extension'] = strtolower(mb_substr($file['filename'], mb_strrpos($file['filename'], '.') + 1)); + if (isset($config['filename_func'])) + $file['file_id'] = $config['filename_func']($file); + else + $file['file_id'] = time() . substr(microtime(), 2, 3); + + if (sizeof($_FILES) > 1) + $file['file_id'] .= "-$i"; + + $file['file'] = $board['dir'] . $config['dir']['img'] . $file['file_id'] . '.' . $file['extension']; + $file['thumb'] = $board['dir'] . $config['dir']['thumb'] . $file['file_id'] . '.' . ($config['thumb_ext'] ? $config['thumb_ext'] : $file['extension']); + $post['files'][] = $file; + $i++; + } + } } - + + if (empty($post['files'])) $post['has_file'] = false; + + // Check for a file + if ($post['op'] && !isset($post['no_longer_require_an_image_for_op'])) { + if (!$post['has_file'] && $config['force_image_op']) + error($config['error']['noimage']); + } + + // Check for too many files + if (sizeof($post['files']) > $config['max_images']) + error($config['error']['toomanyimages']); + if ($config['strip_combining_chars']) { $post['name'] = strip_combining_chars($post['name']); $post['email'] = strip_combining_chars($post['email']); @@ -475,7 +506,7 @@ if (isset($_POST['delete'])) { $user_flag = $_POST['user_flag']; if (!isset($config['user_flags'][$user_flag])) - error('Invalid flag selection!'); + error(_('Invalid flag selection!')); $flag_alt = isset($user_flag_alt) ? $user_flag_alt : $config['user_flags'][$user_flag]; @@ -505,21 +536,38 @@ if (isset($_POST['delete'])) { if ($post['has_file']) { - if (!in_array($post['extension'], $config['allowed_ext']) && !in_array($post['extension'], $config['allowed_ext_files'])) - error($config['error']['unknownext']); - - $is_an_image = !in_array($post['extension'], $config['allowed_ext_files']); - - // Truncate filename if it is too long - $post['filename'] = mb_substr($post['filename'], 0, $config['max_filename_len']); - - $upload = $_FILES['file']['tmp_name']; - - if (!is_readable($upload)) - error($config['error']['nomove']); + foreach ($post['files'] as $key => &$file) { + if (!in_array($file['extension'], $config['allowed_ext']) && !in_array($file['extension'], $config['allowed_ext_files'])) + error($config['error']['unknownext']); + + $file['is_an_image'] = !in_array($file['extension'], $config['allowed_ext_files']); + + // Truncate filename if it is too long + $file['filename'] = mb_substr($file['filename'], 0, $config['max_filename_len']); + + if (!isset($filenames)) { + $filenames = escapeshellarg($file['tmp_name']); + } else { + $filenames .= (' ' . escapeshellarg($file['tmp_name'])); + } + $upload = $file['tmp_name']; + + if (!is_readable($upload)) + error($config['error']['nomove']); + } - $post['filehash'] = md5_file($upload); - $post['filesize'] = filesize($upload); + if ($output = shell_exec_error("cat $filenames | md5sum")) { + $hash = explode(' ', $output)[0]; + $post['filehash'] = $hash; + } elseif ($config['max_images'] === 1) { + $post['filehash'] = md5_file($upload); + } else { + $str_to_hash = ''; + foreach (explode(' ', $filenames) as $i => $f) { + $str_to_hash .= file_get_contents($f); + } + $post['filehash'] = md5($str_to_hash); + } } if (!hasPermission($config['mod']['bypass_filters'], $board['uri'])) { @@ -529,7 +577,8 @@ if (isset($_POST['delete'])) { } if ($post['has_file']) { - if ($is_an_image && $config['ie_mime_type_detection'] !== false) { + foreach ($post['files'] as $key => &$file) { + if ($file['is_an_image'] && $config['ie_mime_type_detection'] !== false) { // Check IE MIME type detection XSS exploit $buffer = file_get_contents($upload, null, null, null, 255); if (preg_match($config['ie_mime_type_detection'], $buffer)) { @@ -540,7 +589,7 @@ if (isset($_POST['delete'])) { require_once 'inc/image.php'; // find dimensions of an image using GD - if (!$size = @getimagesize($upload)) { + if (!$size = @getimagesize($file['tmp_name'])) { error($config['error']['invalidimg']); } if ($size[0] > $config['max_width'] || $size[1] > $config['max_height']) { @@ -548,93 +597,93 @@ if (isset($_POST['delete'])) { } - if ($config['convert_auto_orient'] && ($post['extension'] == 'jpg' || $post['extension'] == 'jpeg')) { + if ($config['convert_auto_orient'] && ($file['extension'] == 'jpg' || $file['extension'] == 'jpeg')) { // The following code corrects the image orientation. // Currently only works with the 'convert' option selected but it could easily be expanded to work with the rest if you can be bothered. - if (!($config['redraw_image'] || (($config['strip_exif'] && !$config['use_exiftool']) && ($post['extension'] == 'jpg' || $post['extension'] == 'jpeg')))) { + if (!($config['redraw_image'] || (($config['strip_exif'] && !$config['use_exiftool']) && ($file['extension'] == 'jpg' || $file['extension'] == 'jpeg')))) { if (in_array($config['thumb_method'], array('convert', 'convert+gifsicle', 'gm', 'gm+gifsicle'))) { - $exif = @exif_read_data($upload); + $exif = @exif_read_data($file['tmp_name']); $gm = in_array($config['thumb_method'], array('gm', 'gm+gifsicle')); if (isset($exif['Orientation']) && $exif['Orientation'] != 1) { if ($config['convert_manual_orient']) { $error = shell_exec_error(($gm ? 'gm ' : '') . 'convert ' . - escapeshellarg($upload) . ' ' . + escapeshellarg($file['tmp_name']) . ' ' . ImageConvert::jpeg_exif_orientation(false, $exif) . ' ' . ($config['strip_exif'] ? '+profile "*"' : ($config['use_exiftool'] ? '' : '+profile "*"') ) . ' ' . - escapeshellarg($upload)); + escapeshellarg($file['tmp_name'])); if ($config['use_exiftool'] && !$config['strip_exif']) { if ($exiftool_error = shell_exec_error( 'exiftool -overwrite_original -q -q -orientation=1 -n ' . - escapeshellarg($upload))) - error('exiftool failed!', null, $exiftool_error); + escapeshellarg($file['tmp_name']))) + error(_('exiftool failed!'), null, $exiftool_error); } else { // TODO: Find another way to remove the Orientation tag from the EXIF profile // without needing `exiftool`. } } else { $error = shell_exec_error(($gm ? 'gm ' : '') . 'convert ' . - escapeshellarg($upload) . ' -auto-orient ' . escapeshellarg($upload)); + escapeshellarg($file['tmp_name']) . ' -auto-orient ' . escapeshellarg($upload)); } if ($error) - error('Could not auto-orient image!', null, $error); - $size = @getimagesize($upload); + error(_('Could not auto-orient image!'), null, $error); + $size = @getimagesize($file['tmp_name']); if ($config['strip_exif']) - $post['exif_stripped'] = true; + $file['exif_stripped'] = true; } } } } // create image object - $image = new Image($upload, $post['extension'], $size); + $image = new Image($file['tmp_name'], $file['extension'], $size); if ($image->size->width > $config['max_width'] || $image->size->height > $config['max_height']) { $image->delete(); error($config['error']['maxsize']); } - $post['width'] = $image->size->width; - $post['height'] = $image->size->height; + $file['width'] = $image->size->width; + $file['height'] = $image->size->height; if ($config['spoiler_images'] && isset($_POST['spoiler'])) { - $post['thumb'] = 'spoiler'; + $file['thumb'] = 'spoiler'; $size = @getimagesize($config['spoiler_image']); - $post['thumbwidth'] = $size[0]; - $post['thumbheight'] = $size[1]; + $file['thumbwidth'] = $size[0]; + $file['thumbheight'] = $size[1]; } elseif ($config['minimum_copy_resize'] && $image->size->width <= $config['thumb_width'] && $image->size->height <= $config['thumb_height'] && - $post['extension'] == ($config['thumb_ext'] ? $config['thumb_ext'] : $post['extension'])) { + $file['extension'] == ($config['thumb_ext'] ? $config['thumb_ext'] : $file['extension'])) { // Copy, because there's nothing to resize - copy($upload, $post['thumb']); + copy($file['tmp_name'], $file['thumb']); - $post['thumbwidth'] = $image->size->width; - $post['thumbheight'] = $image->size->height; + $file['thumbwidth'] = $image->size->width; + $file['thumbheight'] = $image->size->height; } else { $thumb = $image->resize( - $config['thumb_ext'] ? $config['thumb_ext'] : $post['extension'], + $config['thumb_ext'] ? $config['thumb_ext'] : $file['extension'], $post['op'] ? $config['thumb_op_width'] : $config['thumb_width'], $post['op'] ? $config['thumb_op_height'] : $config['thumb_height'] ); - $thumb->to($post['thumb']); + $thumb->to($file['thumb']); - $post['thumbwidth'] = $thumb->width; - $post['thumbheight'] = $thumb->height; + $file['thumbwidth'] = $thumb->width; + $file['thumbheight'] = $thumb->height; $thumb->_destroy(); } - if ($config['redraw_image'] || (!@$post['exif_stripped'] && $config['strip_exif'] && ($post['extension'] == 'jpg' || $post['extension'] == 'jpeg'))) { + if ($config['redraw_image'] || (!@$file['exif_stripped'] && $config['strip_exif'] && ($file['extension'] == 'jpg' || $file['extension'] == 'jpeg'))) { if (!$config['redraw_image'] && $config['use_exiftool']) { if($error = shell_exec_error('exiftool -overwrite_original -ignoreMinorErrors -q -q -all= ' . - escapeshellarg($upload))) - error('Could not strip EXIF metadata!', null, $error); + escapeshellarg($file['tmp_name']))) + error(_('Could not strip EXIF metadata!', null, $error)); } else { - $image->to($post['file']); + $image->to($file['file']); $dont_copy_file = true; } } @@ -642,26 +691,25 @@ if (isset($_POST['delete'])) { } else { // not an image //copy($config['file_thumb'], $post['thumb']); - $post['thumb'] = 'file'; + $file['thumb'] = 'file'; $size = @getimagesize(sprintf($config['file_thumb'], - isset($config['file_icons'][$post['extension']]) ? - $config['file_icons'][$post['extension']] : $config['file_icons']['default'])); - $post['thumbwidth'] = $size[0]; - $post['thumbheight'] = $size[1]; + isset($config['file_icons'][$file['extension']]) ? + $config['file_icons'][$file['extension']] : $config['file_icons']['default'])); + $file['thumbwidth'] = $size[0]; + $file['thumbheight'] = $size[1]; } if (!isset($dont_copy_file) || !$dont_copy_file) { - if (isset($post['file_tmp'])) { - if (!@rename($upload, $post['file'])) + if (isset($file['file_tmp'])) { + if (!@rename($file['tmp_name'], $file['file'])) error($config['error']['nomove']); - chmod($post['file'], 0644); - } elseif (!@move_uploaded_file($upload, $post['file'])) + chmod($file['file'], 0644); + } elseif (!@move_uploaded_file($file['tmp_name'], $file['file'])) error($config['error']['nomove']); } - } - - if ($post['has_file']) { + } + if ($config['image_reject_repost']) { if ($p = getPostByHash($post['filehash'])) { undoImage($post); @@ -689,7 +737,7 @@ if (isset($_POST['delete'])) { )); } } - } + } if (!hasPermission($config['mod']['postunoriginal'], $board['uri']) && $config['robot_enable'] && checkRobot($post['body_nomarkup'])) { undoImage($post); @@ -702,11 +750,13 @@ if (isset($_POST['delete'])) { // Remove board directories before inserting them into the database. if ($post['has_file']) { - $post['file_path'] = $post['file']; - $post['thumb_path'] = $post['thumb']; - $post['file'] = mb_substr($post['file'], mb_strlen($board['dir'] . $config['dir']['img'])); - if ($is_an_image && $post['thumb'] != 'spoiler') - $post['thumb'] = mb_substr($post['thumb'], mb_strlen($board['dir'] . $config['dir']['thumb'])); + foreach ($post['files'] as $key => &$file) { + $file['file_path'] = $file['file']; + $file['thumb_path'] = $file['thumb']; + $file['file'] = mb_substr($file['file'], mb_strlen($board['dir'] . $config['dir']['img'])); + if ($file['is_an_image'] && $file['thumb'] != 'spoiler') + $file['thumb'] = mb_substr($file['thumb'], mb_strlen($board['dir'] . $config['dir']['thumb'])); + } } $post = (object)$post; @@ -715,6 +765,10 @@ if (isset($_POST['delete'])) { error($error); } $post = (array)$post; + + if ($post['files']) + $post['files'] = $post['files']; + $post['num_files'] = sizeof($post['files']); $post['id'] = $id = post($post); diff --git a/stylesheets/style.css b/stylesheets/style.css index b167dbb0..df873e68 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -109,6 +109,9 @@ form table tr td div label { .unimportant, .unimportant * { font-size: 10px; } +.file { + float: left; +} p.fileinfo { display: block; margin: 0; diff --git a/templates/main.js b/templates/main.js index 7796565c..953d8849 100644 --- a/templates/main.js +++ b/templates/main.js @@ -291,6 +291,7 @@ function ready() { {% endraw %} var post_date = "{{ config.post_date }}"; +var max_images = {{ config.max_images }}; onready(init); diff --git a/templates/post/fileinfo.html b/templates/post/fileinfo.html new file mode 100644 index 00000000..2136169b --- /dev/null +++ b/templates/post/fileinfo.html @@ -0,0 +1,38 @@ + {% if post.embed %} + {{ post.embed }} + {% else %} +
+ {% for file in post.files %} +
+ {% if file.file == 'deleted' %} + + {% else %} +

File: {{ file.file }} + ( + {% if file.thumb == 'spoiler' %} + {% trans %}Spoiler Image{% endtrans %}, + {% endif %} + {{ file.size|filesize }} + {% if file.width and file.height %} + , {{ file.width}}x{{ file.height }} + {% if config.show_ratio %} + , {{ ratio(file.width, file.height) }} + {% endif %} + {% endif %} + {% if config.show_filename and file.filename %} + , + {% if file.filename|length > config.max_filename_display %} + {{ file.filename|truncate(config.max_filename_display)|bidi_cleanup }} + {% else %} + {{ file.filename|e|bidi_cleanup }} + {% endif %} + {% endif %} + {% include "post/image_identification.html" %} + ) +

+ {% include "post/image.html" with {'post':file} %} + {% endif %} +
+ {% endfor %} +
+ {% endif %} diff --git a/templates/post_reply.html b/templates/post_reply.html index 90ac5065..3d860872 100644 --- a/templates/post_reply.html +++ b/templates/post_reply.html @@ -1,7 +1,6 @@ {% filter remove_whitespace %} {# tabs and new lines will be ignored #}
-

- {% if post.embed %} - {{ post.embed }} - {% elseif post.file == 'deleted' %} - - {% elseif post.file and post.file %} -

File: {{ post.file }} - ( - {% if post.thumb == 'spoiler' %} - {% trans %}Spoiler Image{% endtrans %}, - {% endif %} - {{ post.filesize|filesize }} - {% if post.filewidth and post.fileheight %} - , {{ post.filewidth}}x{{ post.fileheight }} - {% if config.show_ratio %} - , {{ post.ratio }} - {% endif %} - {% endif %} - {% if config.show_filename and post.filename %} - , - {% if post.filename|length > config.max_filename_display %} - {{ post.filename|truncate(config.max_filename_display)|bidi_cleanup }} - {% else %} - {{ post.filename|e|bidi_cleanup }} - {% endif %} - {% endif %} - {% include "post/image_identification.html" %} - ) -

- {% include "post/image.html" %} - {% endif %} + {% include 'post/fileinfo.html' %} {{ post.postControls }} -
+
1 %}style="clear:both"{% endif %}> {% endfilter %}{% if index %}{{ post.body|truncate_body(post.link) }}{% else %}{{ post.body }}{% endif %}{% filter remove_whitespace %} {% if post.modifiers['ban message'] %} {{ config.mod.ban_message|sprintf(post.modifiers['ban message']) }} diff --git a/templates/post_thread.html b/templates/post_thread.html index f74427e4..a5bf8e08 100644 --- a/templates/post_thread.html +++ b/templates/post_thread.html @@ -2,38 +2,8 @@ {# tabs and new lines will be ignored #}
- -{% if post.embed %} - {{ post.embed }} -{% elseif post.file == 'deleted' %} - -{% elseif post.file and post.file %} -

{% trans %}File:{% endtrans %} {{ post.file }} - ( - {% if post.thumb == 'spoiler' %} - {% trans %}Spoiler Image{% endtrans %}, - {% endif %} - {{ post.filesize|filesize }} - {% if post.filewidth and post.fileheight %} - , {{ post.filewidth}}x{{ post.fileheight }} - {% if config.show_ratio %} - , {{ post.ratio }} - {% endif %} - {% endif %} - {% if config.show_filename and post.filename %} - , - {% if post.filename|length > config.max_filename_display %} - {{ post.filename|truncate(config.max_filename_display)|bidi_cleanup }} - {% else %} - {{ post.filename|e|bidi_cleanup }} - {% endif %} - {% endif %} - {% include "post/image_identification.html" %} - ) -

- {% include "post/image.html" %} -{% endif %} -

+{% include 'post/fileinfo.html' %} +

1%}style='clear:both'{%endif%}>