# frozen_string_literal: true RSpec.describe OptimizedImage do let(:upload) { build(:upload) } before { upload.id = 42 } describe ".crop" do it "should produce cropped images (requires ImageMagick 7)" do tmp_path = "/tmp/cropped.png" desired_width = 5 desired_height = 5 begin OptimizedImage.crop( "#{Rails.root}/spec/fixtures/images/logo.png", tmp_path, desired_width, desired_height, ) w, h = FastImage.size(tmp_path) expect(w).to eq(desired_width) expect(h).to eq(desired_height) cropped_size = File.size(tmp_path) expect(cropped_size).to be < 200 expect(cropped_size).to be > 50 ensure File.delete(tmp_path) if File.exist?(tmp_path) end end it "should correctly crop images vertically" do tmp_path = "/tmp/cropped.png" desired_width = 100 desired_height = 66 begin OptimizedImage.crop( "#{Rails.root}/spec/fixtures/images/logo.png", # 244x66px tmp_path, desired_width, desired_height, ) w, h = FastImage.size(tmp_path) expect(w).to eq(desired_width) expect(h).to eq(desired_height) ensure File.delete(tmp_path) if File.exist?(tmp_path) end end it "should correctly crop images horizontally" do tmp_path = "/tmp/cropped.png" desired_width = 244 desired_height = 500 begin OptimizedImage.crop( "#{Rails.root}/spec/fixtures/images/logo.png", # 244x66px tmp_path, desired_width, desired_height, ) w, h = FastImage.size(tmp_path) expect(w).to eq(desired_width) expect(h).to eq(desired_height) ensure File.delete(tmp_path) if File.exist?(tmp_path) end end describe ".resize_instructions" do let(:image) { "#{Rails.root}/spec/fixtures/images/logo.png" } it "doesn't return any color options by default" do instructions = described_class.resize_instructions(image, image, "50x50") expect(instructions).to_not include("-colors") end it "supports an optional color option" do instructions = described_class.resize_instructions(image, image, "50x50", colors: 12) expect(instructions).to include("-colors") end end describe ".resize" do it "should work correctly when extension is bad" do original_path = Dir::Tmpname.create(%w[origin .bin]) { nil } begin FileUtils.cp "#{Rails.root}/spec/fixtures/images/logo.png", original_path # we use "filename" to get the correct extension here, it is more important # then any other param orig_size = File.size(original_path) OptimizedImage.resize(original_path, original_path, 5, 5, filename: "test.png") new_size = File.size(original_path) expect(orig_size).to be > new_size expect(new_size).not_to eq(0) ensure File.delete(original_path) if File.exist?(original_path) end end it "should work correctly" do file = File.open("#{Rails.root}/spec/fixtures/images/resized.png") upload = UploadCreator.new(file, "test.bin").create_for(-1) expect(upload.filesize).to eq(199) expect(upload.width).to eq(5) expect(upload.height).to eq(5) upload.create_thumbnail!(10, 10) thumb = upload.thumbnail(10, 10) expect(thumb.width).to eq(10) expect(thumb.height).to eq(10) # very image magic specific so fudge here expect(thumb.filesize).to be > 200 # this size is based off original upload # it is the size we render, by default, in the post expect(upload.thumbnail_width).to eq(5) expect(upload.thumbnail_height).to eq(5) # lets ensure we can rebuild the filesize thumb.update_columns(filesize: nil) thumb = OptimizedImage.find(thumb.id) # attempts to auto correct expect(thumb.filesize).to be > 200 end describe "when an svg with a href is masked as a png" do it "should not trigger the external request" do tmp_path = "/tmp/resized.png" begin expect do OptimizedImage.resize( "#{Rails.root}/spec/fixtures/images/svg.png", tmp_path, 5, 5, raise_on_error: true, ) end.to raise_error(RuntimeError, /improper image header/) ensure File.delete(tmp_path) if File.exist?(tmp_path) end end end end describe ".downsize" do it "should downsize logo (requires ImageMagick 7)" do tmp_path = "/tmp/downsized.png" begin OptimizedImage.downsize( "#{Rails.root}/spec/fixtures/images/logo.png", tmp_path, "100x100\>", ) info = FastImage.new(tmp_path) expect(info.size).to eq([100, 27]) expect(File.size(tmp_path)).to be < 2300 ensure File.delete(tmp_path) if File.exist?(tmp_path) end end end end describe ".safe_path?" do it "correctly detects unsafe paths" do expect(OptimizedImage.safe_path?("/path/A-AA/22_00.JPG")).to eq(true) expect(OptimizedImage.safe_path?("/path/AAA/2200.JPG")).to eq(true) expect(OptimizedImage.safe_path?("/tmp/a.png")).to eq(true) expect(OptimizedImage.safe_path?("../a.png")).to eq(false) expect(OptimizedImage.safe_path?("/tmp/a.png\\test")).to eq(false) expect(OptimizedImage.safe_path?("/tmp/a.png\\test")).to eq(false) expect(OptimizedImage.safe_path?("/path/\u1000.png")).to eq(false) expect(OptimizedImage.safe_path?("/path/x.png\n")).to eq(false) expect(OptimizedImage.safe_path?("/path/x.png\ny.png")).to eq(false) expect(OptimizedImage.safe_path?("/path/x.png y.png")).to eq(false) expect(OptimizedImage.safe_path?(nil)).to eq(false) end end describe "ensure_safe_paths!" do it "raises nothing on safe paths" do expect { OptimizedImage.ensure_safe_paths!("/a.png", "/b.png") }.not_to raise_error end it "raises InvalidAccess error on paths" do expect { OptimizedImage.ensure_safe_paths!("/a.png", "/b.png", "c.png") }.to raise_error( Discourse::InvalidAccess, ) end end describe ".local?" do def local(url) OptimizedImage.new(url: url).local? end it "correctly detects local vs remote" do expect(local("//hello")).to eq(false) expect(local("http://hello")).to eq(false) expect(local("https://hello")).to eq(false) expect(local("https://hello")).to eq(false) expect(local("/hello")).to eq(true) end end describe ".create_for" do context "with versioning" do let(:filename) { "logo.png" } let(:file) { file_from_fixtures(filename) } it "is able to update optimized images on version change" do upload = UploadCreator.new(file, filename).create_for(Discourse.system_user.id) optimized = OptimizedImage.create_for(upload, 10, 10) expect(optimized.version).to eq(OptimizedImage::VERSION) optimized_again = OptimizedImage.create_for(upload, 10, 10) expect(optimized_again.id).to eq(optimized.id) optimized.update_columns(version: nil) old_id = optimized.id optimized_new = OptimizedImage.create_for(upload, 10, 10) expect(optimized_new.id).not_to eq(old_id) # cleanup (which transaction rollback may miss) optimized_new.destroy upload.destroy end end it "is able to 'optimize' an svg" do # we don't really optimize anything, we simply copy # but at least this confirms this actually works SiteSetting.authorized_extensions = "svg" svg = file_from_fixtures("image.svg") upload = UploadCreator.new(svg, "image.svg").create_for(Discourse.system_user.id) resized = upload.get_optimized_image(50, 50, {}) # we perform some basic svg mangling but expect the string Discourse to be there expect(File.read(Discourse.store.path_for(resized))).to include("Discourse") expect(File.read(Discourse.store.path_for(resized))).to eq( File.read(Discourse.store.path_for(upload)), ) end context "when using an internal store" do let(:store) { FakeInternalStore.new } before { Discourse.stubs(:store).returns(store) } context "when an error happened while generating the thumbnail" do it "returns nil" do OptimizedImage.expects(:resize).returns(false) expect(OptimizedImage.create_for(upload, 100, 200)).to eq(nil) end end context "when the thumbnail is properly generated" do before { OptimizedImage.expects(:resize).returns(true) } it "does not download a copy of the original image" do store.expects(:download).never OptimizedImage.create_for(upload, 100, 200) end it "closes and removes the tempfile" do Tempfile.any_instance.expects(:close!) OptimizedImage.create_for(upload, 100, 200) end it "works" do oi = OptimizedImage.create_for(upload, 100, 200) expect(oi.sha1).to eq("da39a3ee5e6b4b0d3255bfef95601890afd80709") expect(oi.extension).to eq(".png") expect(oi.width).to eq(100) expect(oi.height).to eq(200) expect(oi.url).to eq("/internally/stored/optimized/image.png") end it "is able to change the format" do oi = OptimizedImage.create_for(upload, 100, 200, format: "gif") expect(oi.url).to eq("/internally/stored/optimized/image.gif") end end end describe "external store" do before { setup_s3 } context "when we have a bad file returned" do it "returns nil" do s3_upload = Fabricate(:upload_s3) stub_request(:head, "http://#{s3_upload.url}").to_return(status: 200) stub_request(:get, "http://#{s3_upload.url}").to_return(status: 200) expect(OptimizedImage.create_for(s3_upload, 100, 200)).to eq(nil) end end context "when the thumbnail is properly generated" do context "with secure uploads disabled" do let(:s3_upload) { Fabricate(:upload_s3) } let(:optimized_path) { %r{/optimized/\d+X.*/#{s3_upload.sha1}_2_100x200\.png} } before do stub_request(:head, "http://#{s3_upload.url}").to_return(status: 200) stub_request(:get, "http://#{s3_upload.url}").to_return( status: 200, body: file_from_fixtures("logo.png"), ) stub_request( :put, %r{https://#{SiteSetting.s3_upload_bucket}\.s3\.dualstack\.#{SiteSetting.s3_region}\.amazonaws.com#{optimized_path}}, ).to_return(status: 200, headers: { "ETag" => "someetag" }) end it "downloads a copy of the original image" do oi = OptimizedImage.create_for(s3_upload, 100, 200) expect(oi.sha1).to_not be_nil expect(oi.extension).to eq(".png") expect(oi.width).to eq(100) expect(oi.height).to eq(200) expect(oi.url).to match( %r{//#{SiteSetting.s3_upload_bucket}\.s3\.dualstack\.us-west-1\.amazonaws\.com#{optimized_path}}, ) expect(oi.filesize).to be > 0 oi.filesize = nil stub_request( :get, %r{http://#{SiteSetting.s3_upload_bucket}\.s3\.dualstack\.us-west-1\.amazonaws\.com#{optimized_path}}, ).to_return(status: 200, body: file_from_fixtures("resized.png")) expect(oi.filesize).to be > 0 end end end end end describe "#destroy" do describe "when upload_id is no longer valid" do it "should still destroy the record" do image = Fabricate(:optimized_image) image.upload.delete image.reload.destroy expect(OptimizedImage.exists?(id: image.id)).to eq(false) end end end end class FakeInternalStore def external? false end def path_for(upload) upload.url end def store_optimized_image(file, optimized_image, content_type = nil, secure: false) "/internally/stored/optimized/image#{optimized_image.extension}" end end