diff --git a/core/extension.php b/core/extension.php index fb7864e3..13822a83 100644 --- a/core/extension.php +++ b/core/extension.php @@ -309,35 +309,25 @@ abstract class DataHandlerExtension extends Extension throw new UploadException("Invalid or corrupted file"); } - /* Check if we are replacing an image */ - if (!is_null($event->replace_id)) { - $existing = Image::by_id($event->replace_id); - if (is_null($existing)) { - throw new UploadException("Post to replace does not exist!"); + $this->move_upload_to_archive($event); + $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata); + + $existing = Image::by_hash($image->hash); + if (!is_null($existing)) { + $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER); + if ($handler == ImageConfig::COLLISION_MERGE) { + $image = $existing; + } else { + throw new UploadException(">>{$existing->id} already has hash {$image->hash}"); } - send_event(new ImageReplaceEvent($existing, $event->tmpname)); - $event->images[] = $existing; - } else { - $this->move_upload_to_archive($event); - $image = $this->create_image_from_data(warehouse_path(Image::IMAGE_DIR, $event->hash), $event->metadata); - - $existing = Image::by_hash($image->hash); - if (!is_null($existing)) { - $handler = $config->get_string(ImageConfig::UPLOAD_COLLISION_HANDLER); - if ($handler == ImageConfig::COLLISION_MERGE) { - $image = $existing; - } else { - throw new UploadException(">>{$existing->id} already has hash {$image->hash}"); - } - } - - // ensure $image has a database-assigned ID number - // before anything else happens - $image->save_to_db(); - - $iae = send_event(new ImageAdditionEvent($image, $event->metadata, !is_null($existing))); - $event->images[] = $iae->image; } + + // ensure $image has a database-assigned ID number + // before anything else happens + $image->save_to_db(); + + $iae = send_event(new ImageAdditionEvent($image, $event->metadata, !is_null($existing))); + $event->images[] = $iae->image; } } diff --git a/core/imageboard/image.php b/core/imageboard/image.php index 921bade0..a1091edc 100644 --- a/core/imageboard/image.php +++ b/core/imageboard/image.php @@ -576,20 +576,27 @@ class Image $this->delete_tags_from_image(); $database->execute("DELETE FROM images WHERE id=:id", ["id" => $this->id]); log_info("core_image", 'Deleted Post #'.$this->id.' ('.$this->hash.')'); - - unlink($this->get_image_filename()); - unlink($this->get_thumb_filename()); + $this->remove_image_only(quiet: true); } /** * This function removes an image (and thumbnail) from the DISK ONLY. * It DOES NOT remove anything from the database. */ - public function remove_image_only(): void + public function remove_image_only(bool $quiet=false): void { - log_info("core_image", 'Removed Post File ('.$this->hash.')'); - @unlink($this->get_image_filename()); - @unlink($this->get_thumb_filename()); + $img_del = @unlink($this->get_image_filename()); + $thumb_del = @unlink($this->get_thumb_filename()); + if($img_del && $thumb_del) { + if(!$quiet) { + log_info("core_image", "Deleted files for Post #{$this->id} ({$this->hash})"); + } + } + else { + $img = $img_del ? '' : ' image'; + $thumb = $thumb_del ? '' : ' thumbnail'; + log_error('core_image', "Failed to delete files for Post #{$this->id}{$img}{$thumb}"); + } } public function parse_link_template(string $tmpl, int $n = 0): string diff --git a/ext/image/main.php b/ext/image/main.php index 4579f8aa..b6a89ded 100644 --- a/ext/image/main.php +++ b/ext/image/main.php @@ -116,10 +116,6 @@ class ImageIO extends Extension if ($user->can(Permissions::DELETE_IMAGE)) { $event->add_part($this->theme->get_deleter_html($event->image->id)); } - /* In the future, could perhaps allow users to replace images that they own as well... */ - if ($user->can(Permissions::REPLACE_IMAGE)) { - $event->add_part($this->theme->get_replace_html($event->image->id)); - } } public function onCommand(CommandEvent $event) @@ -141,43 +137,6 @@ class ImageIO extends Extension log_info("image", "Uploaded >>{$event->image->id} ({$event->image->hash})"); } - public function onImageReplace(ImageReplaceEvent $event) - { - $image = $event->image; - - try { - $duplicate = Image::by_hash($event->new_hash); - if (!is_null($duplicate) && $duplicate->id != $image->id) { - throw new ImageReplaceException("A different post >>{$duplicate->id} already has hash {$duplicate->hash}"); - } - - $image->remove_image_only(); // Actually delete the old image file from disk - - $target = warehouse_path(Image::IMAGE_DIR, $event->new_hash); - if (!@copy($event->tmp_filename, $target)) { - $errors = error_get_last(); - throw new UploadException( - "Failed to copy file from uploads ({$event->tmp_filename}) to archive ($target): ". - "{$errors['type']} / {$errors['message']}" - ); - } - unlink($event->tmp_filename); - - // update metadata and save metadata to DB - $event->image->hash = $event->new_hash; - $event->image->filesize = filesize($target); - $event->image->set_mime(MimeType::get_for_file($target)); - send_event(new MediaCheckPropertiesEvent($image)); - $image->save_to_db(); - - send_event(new ThumbnailGenerationEvent($image)); - - log_info("image", "Replaced >>{$image->id} {$event->original_hash} with {$event->new_hash}"); - } catch (ImageReplaceException $e) { - throw new UploadException($e->error); - } - } - public function onImageDeletion(ImageDeletionEvent $event) { $event->image->delete(); diff --git a/ext/image/theme.php b/ext/image/theme.php index 9ee25327..8b4a99cb 100644 --- a/ext/image/theme.php +++ b/ext/image/theme.php @@ -20,14 +20,4 @@ class ImageIOTheme extends Themelet INPUT(["type" => 'submit', "value" => 'Delete', "onclick" => 'return confirm("Delete the image?");', "id" => "image_delete_button"]), ).""; } - - /** - * Display link to replace the image - */ - public function get_replace_html(int $image_id): string - { - $form = SHM_FORM("replace/$image_id", "GET"); - $form->appendChild(INPUT(["type" => 'submit', "value" => 'Replace'])); - return (string)$form; - } } diff --git a/ext/replace_file/info.php b/ext/replace_file/info.php new file mode 100644 index 00000000..86430f7a --- /dev/null +++ b/ext/replace_file/info.php @@ -0,0 +1,20 @@ +page_matches("replace")) { + if (!$user->can(Permissions::REPLACE_IMAGE)) { + $this->theme->display_error(403, "Error", "{$user->name} doesn't have permission to replace images"); + return; + } + + $image_id = int_escape($event->get_arg(0)); + $image = Image::by_id($image_id); + if (is_null($image)) { + throw new UploadException("Can not replace Post: No post with ID $image_id"); + } + + if($event->method == "GET") { + $this->theme->display_replace_page($page, $image_id); + } elseif($event->method == "POST") { + if (!empty($_POST["url"])) { + $tmp_filename = tempnam(ini_get('upload_tmp_dir'), "shimmie_transload"); + fetch_url($_POST["url"], $tmp_filename); + send_event(new ImageReplaceEvent($image, $tmp_filename)); + } elseif (count($_FILES) > 0) { + send_event(new ImageReplaceEvent($image, $_FILES["data"]['tmp_name'])); + } + if(!empty($_POST["source"])) { + send_event(new SourceSetEvent($image, $_POST["source"])); + } + $cache->delete("thumb-block:{$image_id}"); + $page->set_mode(PageMode::REDIRECT); + $page->set_redirect(make_link("post/view/$image_id")); + } + } + } + + public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event) + { + global $user; + + /* In the future, could perhaps allow users to replace images that they own as well... */ + if ($user->can(Permissions::REPLACE_IMAGE)) { + $event->add_part($this->theme->get_replace_html($event->image->id)); + } + } + + public function onImageReplace(ImageReplaceEvent $event) + { + $image = $event->image; + + $duplicate = Image::by_hash($event->new_hash); + if (!is_null($duplicate) && $duplicate->id != $image->id) { + throw new ImageReplaceException("A different post >>{$duplicate->id} already has hash {$duplicate->hash}"); + } + + $image->remove_image_only(); // Actually delete the old image file from disk + + $target = warehouse_path(Image::IMAGE_DIR, $event->new_hash); + if (!@copy($event->tmp_filename, $target)) { + $errors = error_get_last(); + throw new UploadException( + "Failed to copy file from uploads ({$event->tmp_filename}) to archive ($target): ". + "{$errors['type']} / {$errors['message']}" + ); + } + unlink($event->tmp_filename); + + // update metadata and save metadata to DB + $event->image->hash = $event->new_hash; + $event->image->filesize = filesize($target); + $event->image->set_mime(MimeType::get_for_file($target)); + send_event(new MediaCheckPropertiesEvent($image)); + $image->save_to_db(); + + send_event(new ThumbnailGenerationEvent($image)); + + log_info("image", "Replaced >>{$image->id} {$event->original_hash} with {$event->new_hash}"); + } +} diff --git a/ext/replace_file/test.php b/ext/replace_file/test.php new file mode 100644 index 00000000..2b537dce --- /dev/null +++ b/ext/replace_file/test.php @@ -0,0 +1,68 @@ +log_in_as_admin(); + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + $this->get_page("replace/$image_id"); + $this->assert_title("Replace File"); + } + public function testReplace() + { + global $database; + $this->log_in_as_admin(); + + // upload an image + $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); + + // check that the image is original + $image = Image::by_id($image_id); + $old_hash = md5_file("tests/pbx_screenshot.jpg"); + //$this->assertEquals("pbx_screenshot.jpg", $image->filename); + $this->assertEquals("image/jpeg", $image->get_mime()); + $this->assertEquals(19774, $image->filesize); + $this->assertEquals(640, $image->width); + $this->assertEquals($old_hash, $image->hash); + + // replace it + // create a copy because the file is deleted after upload + $tmpfile = tempnam(sys_get_temp_dir(), "shimmie_test"); + copy("tests/favicon.png", $tmpfile); + $new_hash = md5_file($tmpfile); + $_FILES = [ + 'data' => [ + 'name' => 'favicon.png', + 'type' => 'image/png', + 'tmp_name' => $tmpfile, + 'error' => 0, + 'size' => 246, + ] + ]; + $page = $this->post_page("replace/$image_id"); + $this->assert_response(302); + $this->assertEquals("/test/post/view/$image_id", $page->redirect); + + // check that there's still one image + $this->assertEquals(1, $database->get_one("SELECT COUNT(*) FROM images")); + + // check that the image was replaced + $image = Image::by_id($image_id); + // $this->assertEquals("favicon.png", $image->filename); // TODO should we update filename? + $this->assertEquals("image/png", $image->get_mime()); + $this->assertEquals(246, $image->filesize); + $this->assertEquals(16, $image->width); + $this->assertEquals(md5_file("tests/favicon.png"), $image->hash); + + // check that new files exist and old files don't + $this->assertFalse(file_exists(warehouse_path(Image::IMAGE_DIR, $old_hash))); + $this->assertFalse(file_exists(warehouse_path(Image::THUMBNAIL_DIR, $old_hash))); + $this->assertTrue(file_exists(warehouse_path(Image::IMAGE_DIR, $new_hash))); + $this->assertTrue(file_exists(warehouse_path(Image::THUMBNAIL_DIR, $new_hash))); + } +} diff --git a/ext/replace_file/theme.php b/ext/replace_file/theme.php new file mode 100644 index 00000000..133145dd --- /dev/null +++ b/ext/replace_file/theme.php @@ -0,0 +1,80 @@ +get_string(UploadConfig::TRANSLOAD_ENGINE, "none") != "none"); + $accept = $this->get_accept(); + + $max_size = $config->get_int(UploadConfig::SIZE); + $max_kb = to_shorthand_int($max_size); + + $image = Image::by_id($image_id); + $thumbnail = $this->build_thumb_html($image); + + $form = SHM_FORM("replace/".$image_id, "POST", true); + $form->appendChild(emptyHTML( + TABLE( + ["id" => "large_upload_form", "class" => "form"], + TR( + TD("File"), + TD(INPUT(["name" => "data", "type" => "file", "accept" => $accept])) + ), + $tl_enabled ? TR( + TD("or URL"), + TD(INPUT(["name" => "url", "type" => "text", "value" => @$_GET['url']])) + ) : null, + TR(TD("Source"), TD(["colspan" => 3], INPUT(["name" => "source", "type" => "text"]))), + TR(TD(["colspan" => 4], INPUT(["id" => "uploadbutton", "type" => "submit", "value" => "Post"]))), + ) + )); + + $html = emptyHTML( + P( + "Replacing Post ID $image_id", + BR(), + "Please note: You will have to refresh the post page, or empty your browser cache." + ), + $thumbnail, + BR(), + $form, + $max_size > 0 ? SMALL("(Max file size is $max_kb)") : null, + ); + + $page->set_title("Replace File"); + $page->set_heading("Replace File"); + $page->add_block(new NavBlock()); + $page->add_block(new Block("Upload Replacement File", $html, "main", 20)); + } + + /** + * Display link to replace the image + */ + public function get_replace_html(int $image_id): string + { + $form = SHM_FORM("replace/$image_id", "GET"); + $form->appendChild(INPUT(["type" => 'submit', "value" => 'Replace'])); + return (string)$form; + } + + protected function get_accept(): string + { + return ".".join(",.", DataHandlerExtension::get_all_supported_exts()); + } +} diff --git a/ext/tag_edit/main.php b/ext/tag_edit/main.php index 4088a48f..6f0e3f2e 100644 --- a/ext/tag_edit/main.php +++ b/ext/tag_edit/main.php @@ -182,13 +182,6 @@ class TagEdit extends Extension } } - public function onImageReplace(ImageReplaceEvent $event) - { - if(!empty($_POST['source'])) { - send_event(new SourceSetEvent($event->image, $_POST['source'])); - } - } - public function onImageInfoSet(ImageInfoSetEvent $event) { global $page, $user; diff --git a/ext/upload/main.php b/ext/upload/main.php index 16abf16b..f10900ed 100644 --- a/ext/upload/main.php +++ b/ext/upload/main.php @@ -27,7 +27,6 @@ class DataUploadEvent extends Event public function __construct( public string $tmpname, public array $metadata, - public ?int $replace_id = null ) { parent::__construct(); @@ -212,35 +211,7 @@ class Upload extends Extension } } - if ($event->page_matches("replace")) { - if (!$user->can(Permissions::REPLACE_IMAGE)) { - $this->theme->display_error(403, "Error", "{$user->name} doesn't have permission to replace images"); - return; - } - if ($this->is_full) { - $this->theme->display_error(507, "Error", "Can't replace images: disk nearly full"); - return; - } - - $image_id = int_escape($event->get_arg(0)); - $image_old = Image::by_id($image_id); - if (is_null($image_old)) { - throw new UploadException("Can not replace Post: No post with ID $image_id"); - } - - if($event->method == "GET") { - $this->theme->display_replace_page($page, $image_id); - } elseif($event->method == "POST") { - $results = []; - if (!empty($_POST["url"])) { - $results = $this->try_transload($_POST["url"], [], $_POST['source'] ?? null, $image_id); - } elseif (count($_FILES) > 0) { - $results = $this->try_upload($_FILES["data"], [], $_POST['source'] ?? null, $image_id); - } - $cache->delete("thumb-block:{$image_id}"); - $this->theme->display_upload_status($page, $results); - } - } elseif ($event->page_matches("upload")) { + if ($event->page_matches("upload")) { if (!$user->can(Permissions::CREATE_IMAGE)) { $this->theme->display_error(403, "Error", "{$user->name} doesn't have permission to upload images"); return; @@ -342,7 +313,7 @@ class Upload extends Extension * @param string[] $tags * @return UploadResult[] */ - private function try_upload(array $file, array $tags, ?string $source = null, ?int $replace_id = null): array + private function try_upload(array $file, array $tags, ?string $source = null): array { global $page, $config; @@ -376,7 +347,7 @@ class Upload extends Extension $metadata['tags'] = $tags; $metadata['source'] = $source; - $event = new DataUploadEvent($tmp_name, $metadata, $replace_id); + $event = new DataUploadEvent($tmp_name, $metadata); send_event($event); if (count($event->images) == 0) { throw new UploadException("MIME type not supported: " . $event->mime); @@ -395,7 +366,7 @@ class Upload extends Extension /** * @return UploadResult[] */ - private function try_transload(string $url, array $tags, string $source = null, ?int $replace_id = null): array + private function try_transload(string $url, array $tags, string $source = null): array { global $page, $config, $user; @@ -431,7 +402,7 @@ class Upload extends Extension } // Upload file - $event = new DataUploadEvent($tmp_filename, $metadata, $replace_id); + $event = new DataUploadEvent($tmp_filename, $metadata); send_event($event); if (count($event->images) == 0) { throw new UploadException("File type not supported: " . $event->mime); diff --git a/ext/upload/test.php b/ext/upload/test.php index daf256f2..0f0ff1df 100644 --- a/ext/upload/test.php +++ b/ext/upload/test.php @@ -50,42 +50,6 @@ class UploadTest extends ShimmiePHPUnitTestCase $this->assertEquals(4, $database->get_one("SELECT COUNT(*) FROM images")); } - public function testRawReplace() - { - global $database; - - $this->log_in_as_admin(); - $image_id = $this->post_image("tests/pbx_screenshot.jpg", "pbx computer screenshot"); - $original_posted = $database->get_one("SELECT posted FROM images WHERE id = $image_id"); - - sleep(1); // make sure the timestamp changes (see bug #903) - - // create a copy because the file is deleted after upload - $tmpfile = tempnam(sys_get_temp_dir(), "shimmie_test"); - copy("tests/bedroom_workshop.jpg", $tmpfile); - - $_FILES = [ - 'data' => [ - 'name' => ['puppy-hugs.jpg'], - 'type' => ['image/jpeg'], - 'tmp_name' => [$tmpfile], - 'error' => [0], - 'size' => [271386], - ] - ]; - - $page = $this->post_page("replace/$image_id"); - $this->assert_response(302); - $this->assertEquals("/test/post/view/$image_id", $page->redirect); - $new_posted = $database->get_one("SELECT posted FROM images WHERE id = $image_id"); - - $this->assertEquals(1, $database->get_one("SELECT COUNT(*) FROM images")); - - // check that the original timestamp is left alone, despite the - // file being replaced (see bug #903) - $this->assertEquals($original_posted, $new_posted); - } - public function testUpload() { $this->log_in_as_user(); diff --git a/ext/upload/theme.php b/ext/upload/theme.php index abf41d64..ccd20d0c 100644 --- a/ext/upload/theme.php +++ b/ext/upload/theme.php @@ -209,64 +209,6 @@ class UploadTheme extends Themelet return emptyHTML($html1, $html2); } - /** - * Only allows 1 file to be uploaded - for replacing another image file. - */ - public function display_replace_page(Page $page, int $image_id) - { - global $config, $page; - $tl_enabled = ($config->get_string(UploadConfig::TRANSLOAD_ENGINE, "none") != "none"); - $accept = $this->get_accept(); - - $upload_list = emptyHTML( - TR( - TD("File"), - TD(INPUT(["name" => "data[]", "type" => "file", "accept" => $accept])) - ) - ); - if ($tl_enabled) { - $upload_list->appendChild( - TR( - TD("or URL"), - TD(INPUT(["name" => "url", "type" => "text", "value" => @$_GET['url']])) - ) - ); - } - - $max_size = $config->get_int(UploadConfig::SIZE); - $max_kb = to_shorthand_int($max_size); - - $image = Image::by_id($image_id); - $thumbnail = $this->build_thumb_html($image); - - $form = SHM_FORM("replace/".$image_id, "POST", true); - $form->appendChild(emptyHTML( - TABLE( - ["id" => "large_upload_form", "class" => "form"], - $upload_list, - TR(TD("Source"), TD(["colspan" => 3], INPUT(["name" => "source", "type" => "text"]))), - TR(TD(["colspan" => 4], INPUT(["id" => "uploadbutton", "type" => "submit", "value" => "Post"]))), - ) - )); - - $html = emptyHTML( - P( - "Replacing Post ID $image_id", - BR(), - "Please note: You will have to refresh the post page, or empty your browser cache." - ), - $thumbnail, - BR(), - $form, - $max_size > 0 ? SMALL("(Max file size is $max_kb)") : null, - ); - - $page->set_title("Replace Post"); - $page->set_heading("Replace Post"); - $page->add_block(new NavBlock()); - $page->add_block(new Block("Upload Replacement Post", $html, "main", 20)); - } - /** * @param UploadResult[] $results */ @@ -337,7 +279,6 @@ class UploadTheme extends Themelet NOSCRIPT(BR(), A(["href" => make_link("upload")], "Larger Form")) ); } - protected function get_accept(): string { return ".".join(",.", DataHandlerExtension::get_all_supported_exts());