diff options
| -rw-r--r-- | app/models/concerns/file_attachment.rb | 54 | ||||
| -rw-r--r-- | config/locales/de.yml | 2 | ||||
| -rw-r--r-- | config/locales/en.yml | 2 | ||||
| -rw-r--r-- | test/models/asset_test.rb | 23 |
4 files changed, 76 insertions, 5 deletions
diff --git a/app/models/concerns/file_attachment.rb b/app/models/concerns/file_attachment.rb index fd0cabed..38b0b706 100644 --- a/app/models/concerns/file_attachment.rb +++ b/app/models/concerns/file_attachment.rb | |||
| @@ -37,6 +37,20 @@ module FileAttachment | |||
| 37 | "MAGICK_CONFIGURE_PATH" => Rails.root.join("config", "imagemagick").to_s | 37 | "MAGICK_CONFIGURE_PATH" => Rails.root.join("config", "imagemagick").to_s |
| 38 | }.freeze | 38 | }.freeze |
| 39 | 39 | ||
| 40 | # ImageMagick picks its decoder from the file, not from the multipart | ||
| 41 | # header. Where the two disagree, believe ImageMagick: otherwise | ||
| 42 | # generate_all_variants dispatches on a claim and hands a file to a | ||
| 43 | # pipeline built for something else. | ||
| 44 | MAGICK_FORMAT_CONTENT_TYPES = { | ||
| 45 | "JPEG" => "image/jpeg", | ||
| 46 | "PNG" => "image/png", | ||
| 47 | "GIF" => "image/gif", | ||
| 48 | "WEBP" => "image/webp", | ||
| 49 | "SVG" => "image/svg+xml", | ||
| 50 | "MSVG" => "image/svg+xml", | ||
| 51 | "PDF" => "application/pdf" | ||
| 52 | }.freeze | ||
| 53 | |||
| 40 | IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze | 54 | IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze |
| 41 | VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze | 55 | VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze |
| 42 | RASTERIZED_CONTENT_TYPES = %w[application/pdf].freeze | 56 | RASTERIZED_CONTENT_TYPES = %w[application/pdf].freeze |
| @@ -88,6 +102,7 @@ module FileAttachment | |||
| 88 | after_initialize :build_upload_proxy | 102 | after_initialize :build_upload_proxy |
| 89 | after_save :process_upload | 103 | after_save :process_upload |
| 90 | before_destroy :delete_upload_files | 104 | before_destroy :delete_upload_files |
| 105 | validate :upload_must_be_readable_when_it_claims_to_be_an_image | ||
| 91 | end | 106 | end |
| 92 | 107 | ||
| 93 | def upload=(uploaded_file) | 108 | def upload=(uploaded_file) |
| @@ -95,7 +110,10 @@ module FileAttachment | |||
| 95 | @pending_upload = uploaded_file | 110 | @pending_upload = uploaded_file |
| 96 | # Populate the database columns immediately so validations can use them | 111 | # Populate the database columns immediately so validations can use them |
| 97 | self.upload_file_name = sanitize_filename(uploaded_file.original_filename) | 112 | self.upload_file_name = sanitize_filename(uploaded_file.original_filename) |
| 98 | self.upload_content_type = uploaded_file.content_type.to_s.split(';').first.strip | 113 | detected = detected_content_type(uploaded_file) |
| 114 | @upload_unreadable = detected.nil? | ||
| 115 | self.upload_content_type = detected || | ||
| 116 | uploaded_file.content_type.to_s.split(';').first.strip | ||
| 99 | self.upload_file_size = uploaded_file.size | 117 | self.upload_file_size = uploaded_file.size |
| 100 | self.upload_updated_at = Time.current | 118 | self.upload_updated_at = Time.current |
| 101 | build_upload_proxy | 119 | build_upload_proxy |
| @@ -160,7 +178,9 @@ module FileAttachment | |||
| 160 | STYLES.each do |style, options| | 178 | STYLES.each do |style, options| |
| 161 | dest_path = file_path(style) | 179 | dest_path = file_path(style) |
| 162 | FileUtils.mkdir_p(File.dirname(dest_path)) | 180 | FileUtils.mkdir_p(File.dirname(dest_path)) |
| 163 | system(MAGICK_ENV, "magick", original_path, *extra_args, *options[:args], dest_path) | 181 | if !system(MAGICK_ENV, "magick", original_path, *extra_args, *options[:args], dest_path) |
| 182 | Rails.logger.warn("Asset##{id}: magick failed for #{style} of #{upload_file_name}") | ||
| 183 | end | ||
| 164 | end | 184 | end |
| 165 | end | 185 | end |
| 166 | 186 | ||
| @@ -180,7 +200,7 @@ module FileAttachment | |||
| 180 | dest_path = file_path(:og) | 200 | dest_path = file_path(:og) |
| 181 | FileUtils.mkdir_p(File.dirname(dest_path)) | 201 | FileUtils.mkdir_p(File.dirname(dest_path)) |
| 182 | 202 | ||
| 183 | if og_full_bleed?(original_path) | 203 | ok = if og_full_bleed?(original_path) |
| 184 | system(MAGICK_ENV, "magick", "#{original_path}[0]", | 204 | system(MAGICK_ENV, "magick", "#{original_path}[0]", |
| 185 | "-resize", "#{OG_WIDTH}x#{OG_HEIGHT}^", | 205 | "-resize", "#{OG_WIDTH}x#{OG_HEIGHT}^", |
| 186 | "-gravity", "center", "-extent", "#{OG_WIDTH}x#{OG_HEIGHT}", | 206 | "-gravity", "center", "-extent", "#{OG_WIDTH}x#{OG_HEIGHT}", |
| @@ -189,6 +209,9 @@ module FileAttachment | |||
| 189 | else | 209 | else |
| 190 | system(MAGICK_ENV, *og_template_command(dest_path)) | 210 | system(MAGICK_ENV, *og_template_command(dest_path)) |
| 191 | end | 211 | end |
| 212 | |||
| 213 | Rails.logger.warn("Asset##{id}: magick failed for og of #{upload_file_name}") unless ok | ||
| 214 | ok | ||
| 192 | end | 215 | end |
| 193 | 216 | ||
| 194 | def generate_all_variants(original_path) | 217 | def generate_all_variants(original_path) |
| @@ -214,8 +237,11 @@ module FileAttachment | |||
| 214 | end | 237 | end |
| 215 | 238 | ||
| 216 | def source_dimensions(path) | 239 | def source_dimensions(path) |
| 217 | out, status = Open3.capture2(MAGICK_ENV, "magick", "identify", "-format", "%w %h", "#{path}[0]") | 240 | out, err, status = Open3.capture3(MAGICK_ENV, "magick", "identify", "-format", "%w %h", "#{path}[0]") |
| 218 | return nil unless status.success? | 241 | unless status.success? |
| 242 | Rails.logger.warn("Asset##{id}: magick identify failed for #{path}: #{err.lines.first&.strip}") | ||
| 243 | return nil | ||
| 244 | end | ||
| 219 | 245 | ||
| 220 | width, height = out.split.map(&:to_i) | 246 | width, height = out.split.map(&:to_i) |
| 221 | (width.positive? && height.positive?) ? [width, height] : nil | 247 | (width.positive? && height.positive?) ? [width, height] : nil |
| @@ -288,12 +314,30 @@ module FileAttachment | |||
| 288 | end | 314 | end |
| 289 | end | 315 | end |
| 290 | 316 | ||
| 317 | def detected_content_type(uploaded_file) | ||
| 318 | path = uploaded_file.try(:tempfile).try(:path) || uploaded_file.try(:path) | ||
| 319 | return nil if path.blank? || !File.exist?(path) | ||
| 320 | |||
| 321 | out, _err, status = Open3.capture3(MAGICK_ENV, "magick", "identify", "-format", "%m", "#{path}[0]") | ||
| 322 | return nil unless status.success? | ||
| 323 | |||
| 324 | MAGICK_FORMAT_CONTENT_TYPES[out.strip.upcase] | ||
| 325 | rescue StandardError | ||
| 326 | nil | ||
| 327 | end | ||
| 291 | 328 | ||
| 292 | def delete_upload_files | 329 | def delete_upload_files |
| 293 | dir = upload_root.join(id.to_s) | 330 | dir = upload_root.join(id.to_s) |
| 294 | FileUtils.rm_rf(dir) if Dir.exist?(dir) | 331 | FileUtils.rm_rf(dir) if Dir.exist?(dir) |
| 295 | end | 332 | end |
| 296 | 333 | ||
| 334 | def upload_must_be_readable_when_it_claims_to_be_an_image | ||
| 335 | return unless @upload_unreadable | ||
| 336 | return unless IMAGE_CONTENT_TYPES.include?(upload_content_type) | ||
| 337 | |||
| 338 | errors.add(:upload, :unreadable_image) | ||
| 339 | end | ||
| 340 | |||
| 297 | def file_path(style) | 341 | def file_path(style) |
| 298 | upload_root.join(id.to_s, style.to_s, variant_filename(style)).to_s | 342 | upload_root.join(id.to_s, style.to_s, variant_filename(style)).to_s |
| 299 | end | 343 | end |
diff --git a/config/locales/de.yml b/config/locales/de.yml index 027fcd44..b3319e61 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -159,6 +159,8 @@ de: | |||
| 159 | attributes: | 159 | attributes: |
| 160 | base: | 160 | base: |
| 161 | not_permitted: "Dieses Asset ist an geschützte Seiten angehängt; nur die Redaktion darf es löschen" | 161 | not_permitted: "Dieses Asset ist an geschützte Seiten angehängt; nur die Redaktion darf es löschen" |
| 162 | upload: | ||
| 163 | unreadable_image: "konnte nicht als Bild gelesen werden. Möglicherweise ist sie beschädigt oder hat ein anderes Format, als der Name vermuten lässt." | ||
| 162 | 164 | ||
| 163 | tags: | 165 | tags: |
| 164 | index: | 166 | index: |
diff --git a/config/locales/en.yml b/config/locales/en.yml index 256c8540..ccca0ad0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -105,6 +105,8 @@ en: | |||
| 105 | attributes: | 105 | attributes: |
| 106 | base: | 106 | base: |
| 107 | not_permitted: "Only Redaktion members may change assets related to nodes in this section" | 107 | not_permitted: "Only Redaktion members may change assets related to nodes in this section" |
| 108 | upload: | ||
| 109 | unreadable_image: "could not be read as an image. It may be damaged, or not the format its name suggests." | ||
| 108 | 110 | ||
| 109 | tags: | 111 | tags: |
| 110 | index: | 112 | index: |
diff --git a/test/models/asset_test.rb b/test/models/asset_test.rb index ab1cc5df..2677681b 100644 --- a/test/models/asset_test.rb +++ b/test/models/asset_test.rb | |||
| @@ -1,4 +1,5 @@ | |||
| 1 | require 'test_helper' | 1 | require 'test_helper' |
| 2 | require "rack/test/uploaded_file" | ||
| 2 | 3 | ||
| 3 | class AssetTest < ActiveSupport::TestCase | 4 | class AssetTest < ActiveSupport::TestCase |
| 4 | 5 | ||
| @@ -53,4 +54,26 @@ class AssetTest < ActiveSupport::TestCase | |||
| 53 | assert asset.has_credit? | 54 | assert asset.has_credit? |
| 54 | assert_not asset.show_credit? | 55 | assert_not asset.show_credit? |
| 55 | end | 56 | end |
| 57 | |||
| 58 | test "an upload that claims to be an image but is not is refused" do | ||
| 59 | Tempfile.create(["fake", ".jpg"]) do |f| | ||
| 60 | f.write("this is not a jpeg") | ||
| 61 | f.flush | ||
| 62 | |||
| 63 | asset = Asset.new(:name => "fake") | ||
| 64 | asset.upload = Rack::Test::UploadedFile.new(f.path, "image/jpeg") | ||
| 65 | |||
| 66 | assert_not asset.valid? | ||
| 67 | assert_includes asset.errors.full_messages.to_sentence, | ||
| 68 | I18n.t("activerecord.errors.models.asset.attributes.upload.unreadable_image") | ||
| 69 | end | ||
| 70 | end | ||
| 71 | |||
| 72 | test "a real image is accepted and its type comes from the file" do | ||
| 73 | asset = Asset.new(:name => "real") | ||
| 74 | asset.upload = Rack::Test::UploadedFile.new(file_fixture("test_image.png"), "image/jpeg") | ||
| 75 | |||
| 76 | assert asset.valid?, asset.errors.full_messages.to_sentence | ||
| 77 | assert_equal "image/png", asset.upload_content_type | ||
| 78 | end | ||
| 56 | end | 79 | end |
