From b3e67d8cca809dd7fc349619e35c2413e9999ac6 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sat, 18 Jul 2026 11:55:09 +0900 Subject: [PATCH] #399 --- backend/app/models/post.rb | 32 +++++++++++++++++-- backend/app/services/post_create_plan.rb | 26 +++++++++++++++ .../post_thumbnail_upload_validator.rb | 11 +++++-- .../app/services/preview/thumbnail_fetcher.rb | 8 +++-- 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/backend/app/models/post.rb b/backend/app/models/post.rb index 3f6f708..c8c209b 100644 --- a/backend/app/models/post.rb +++ b/backend/app/models/post.rb @@ -17,6 +17,7 @@ class Post < ApplicationRecord MAX_SVG_DIMENSION = 4_096 MAX_SVG_PIXELS = 16_777_216 THUMBNAIL_PROCESS_TIMEOUT = 5.seconds + RASTER_THUMBNAIL_FORMATS = ['jpeg', 'png', 'gif', 'webp'].freeze def self.resized_thumbnail_attachment(upload, content_type: nil) upload.rewind @@ -225,9 +226,23 @@ class Post < ApplicationRecord end def self.image_for_thumbnail_upload(bytes, content_type: nil) - return decode_raster_thumbnail(bytes) unless svg_content_type?(content_type) || svg_document_bytes?(bytes) + if svg_content_type?(content_type) || svg_document_bytes?(bytes) + return decode_svg_thumbnail(bytes) + end - decode_svg_thumbnail(bytes) + raise MiniMagick::Error, 'サムネイル画像の形式が不正です.' unless raster_thumbnail_bytes?(bytes) + + decode_raster_thumbnail(bytes) + end + + def self.raster_thumbnail_bytes?(bytes) + raster_thumbnail_format(bytes).present? + end + + def self.remote_thumbnail_image_bytes?(bytes, content_type: nil) + return true if svg_content_type?(content_type) || svg_document_bytes?(bytes) + + raster_thumbnail_bytes?(bytes) end def self.svg_content_type?(content_type) @@ -254,6 +269,18 @@ class Post < ApplicationRecord MiniMagick::Image.read(sanitised_svg_bytes(bytes)) end + def self.raster_thumbnail_format(bytes) + binary = bytes.to_s.b + return 'jpeg' if binary.start_with?("\xFF\xD8\xFF".b) + return 'png' if binary.start_with?("\x89PNG\r\n\x1A\n".b) + return 'gif' if binary.start_with?('GIF87a'.b) || binary.start_with?('GIF89a'.b) + return 'webp' if binary.bytesize >= 12 && + binary.start_with?('RIFF'.b) && + binary.byteslice(8, 4) == 'WEBP' + + nil + end + def self.sanitised_svg_bytes(bytes) parse_options = Nokogiri::XML::ParseOptions::STRICT | @@ -354,6 +381,7 @@ class Post < ApplicationRecord :svg_content_type?, :decode_raster_thumbnail, :decode_svg_thumbnail, + :raster_thumbnail_format, :sanitised_svg_bytes, :svg_uses_disallowed_features?, :external_svg_reference?, diff --git a/backend/app/services/post_create_plan.rb b/backend/app/services/post_create_plan.rb index 8cd09bc..4126765 100644 --- a/backend/app/services/post_create_plan.rb +++ b/backend/app/services/post_create_plan.rb @@ -12,6 +12,7 @@ class PostCreatePlan direct_tag_specs, tag_sections = parse_direct_tag_specs default_tag_specs = build_default_tag_specs(direct_tag_specs) snapshot_tag_specs = merge_tag_specs(direct_tag_specs + default_tag_specs) + validate_new_tag_specs!(snapshot_tag_specs) post_tag_specs = expand_parent_tag_specs(snapshot_tag_specs) video_ms = normalise_video_ms(snapshot_tag_specs) validate_video_sections!(video_ms, tag_sections) @@ -103,6 +104,31 @@ class PostCreatePlan default_tag_specs end + def validate_new_tag_specs! specs + Array(specs).each do |spec| + next if existing_tags_by_name.key?(spec[:name]) + + validate_new_tag_spec!(spec) + end + end + + def validate_new_tag_spec! spec + tag_name = TagName.new(name: spec[:name]) + tag = Tag.new(category: spec[:category], tag_name:) + return if tag_name.valid? && tag.valid? + + post = Post.new + tag_name.errors[:name].each do |message| + post.errors.add :tags, "#{ spec[:name] }: #{ message }" + end + tag.errors.each do |error| + next if error.attribute == :tag_name + + post.errors.add :tags, "#{ spec[:name] }: #{ error.message }" + end + raise ActiveRecord::RecordInvalid, post + end + def expand_parent_tag_specs snapshot_tag_specs existing_snapshot_tags = snapshot_tag_specs.filter_map { existing_tags_by_name[_1[:name]] } expanded_parent_specs = diff --git a/backend/app/services/post_thumbnail_upload_validator.rb b/backend/app/services/post_thumbnail_upload_validator.rb index b5b44e6..ccbab3a 100644 --- a/backend/app/services/post_thumbnail_upload_validator.rb +++ b/backend/app/services/post_thumbnail_upload_validator.rb @@ -11,10 +11,15 @@ class PostThumbnailUploadValidator end raise InvalidUpload, 'thumbnail file size が大きすぎます.' if thumbnail.size > MAX_THUMBNAIL_BYTES raise InvalidUpload, 'サムネイル画像の形式が不正です.' unless allowed_content_type?(thumbnail.content_type) - raise InvalidUpload, 'サムネイル画像の形式が不正です.' if Post.svg_document_bytes?(thumbnail.read) - thumbnail.rewind + bytes = thumbnail.read + raise InvalidUpload, 'サムネイル画像の形式が不正です.' if Post.svg_document_bytes?(bytes) + unless Post.raster_thumbnail_bytes?(bytes) + raise InvalidUpload, 'サムネイル画像の形式が不正です.' + end - attachment = Post.resized_thumbnail_attachment(thumbnail, content_type: thumbnail.content_type) + attachment = Post.resized_thumbnail_attachment( + StringIO.new(bytes), + content_type: thumbnail.content_type) attachment[:io].close if attachment[:io].respond_to?(:close) rescue MiniMagick::Error, Timeout::Error raise InvalidUpload, 'サムネイル画像の変換に失敗しました.' diff --git a/backend/app/services/preview/thumbnail_fetcher.rb b/backend/app/services/preview/thumbnail_fetcher.rb index 57ff16e..efde3ab 100644 --- a/backend/app/services/preview/thumbnail_fetcher.rb +++ b/backend/app/services/preview/thumbnail_fetcher.rb @@ -32,7 +32,9 @@ module Preview def self.fetch_image_response(raw_url) uri, = UrlSafety.validate(raw_url) response = HttpFetcher.fetch(uri.to_s) - unless allowed_remote_image_content_type?(response.content_type) || Post.svg_document_bytes?(response.body) + unless Post.remote_thumbnail_image_bytes?( + response.body, + content_type: response.content_type) raise GenerationFailed, 'サムネール画像が見つかりませんでした.' end @@ -55,7 +57,9 @@ module Preview return nil if url.blank? response = HttpFetcher.fetch(url) - return nil unless allowed_remote_image_content_type?(response.content_type) + return nil unless Post.remote_thumbnail_image_bytes?( + response.body, + content_type: response.content_type) response.body rescue HttpFetcher::FetchTimeout