2019-04-29 20:27:42 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2015-10-11 05:41:23 -04:00
|
|
|
require 'rails_helper'
|
2013-06-16 04:39:48 -04:00
|
|
|
|
|
|
|
describe OptimizedImage do
|
2013-11-05 13:04:47 -05:00
|
|
|
let(:upload) { build(:upload) }
|
|
|
|
before { upload.id = 42 }
|
2013-06-16 20:46:42 -04:00
|
|
|
|
2018-07-17 05:11:05 -04:00
|
|
|
unless ENV["TRAVIS"]
|
|
|
|
describe '.crop' do
|
2019-01-02 11:43:13 -05:00
|
|
|
it 'should produce cropped images (requires ImageMagick 7)' do
|
2018-07-17 05:11:05 -04:00
|
|
|
tmp_path = "/tmp/cropped.png"
|
2018-07-17 03:48:59 -04:00
|
|
|
|
2018-07-17 05:11:05 -04:00
|
|
|
begin
|
|
|
|
OptimizedImage.crop(
|
|
|
|
"#{Rails.root}/spec/fixtures/images/logo.png",
|
|
|
|
tmp_path,
|
|
|
|
5,
|
|
|
|
5
|
|
|
|
)
|
2018-07-17 03:48:59 -04:00
|
|
|
|
2019-01-02 01:19:52 -05:00
|
|
|
# we don't want to deal with something new here every time image magick
|
|
|
|
# is upgraded or pngquant is upgraded, lets just test the basics ...
|
|
|
|
# cropped image should be less than 120 bytes
|
2018-11-05 04:12:52 -05:00
|
|
|
|
2019-01-02 01:19:52 -05:00
|
|
|
cropped_size = File.size(tmp_path)
|
|
|
|
|
|
|
|
expect(cropped_size).to be < 120
|
|
|
|
expect(cropped_size).to be > 50
|
2018-11-05 04:12:52 -05:00
|
|
|
|
2018-07-17 05:11:05 -04:00
|
|
|
ensure
|
|
|
|
File.delete(tmp_path) if File.exists?(tmp_path)
|
|
|
|
end
|
2018-07-17 03:48:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-12-14 17:44:38 -05:00
|
|
|
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
|
|
|
|
|
2018-07-17 05:11:05 -04:00
|
|
|
describe '.resize' do
|
2018-08-19 22:18:49 -04:00
|
|
|
it 'should work correctly when extension is bad' do
|
|
|
|
|
|
|
|
original_path = Dir::Tmpname.create(['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
|
|
|
|
|
2018-10-22 17:45:06 -04:00
|
|
|
orig_size = File.size(original_path)
|
|
|
|
|
2018-08-19 22:18:49 -04:00
|
|
|
OptimizedImage.resize(
|
|
|
|
original_path,
|
|
|
|
original_path,
|
|
|
|
5,
|
|
|
|
5,
|
|
|
|
filename: "test.png"
|
|
|
|
)
|
|
|
|
|
2018-10-22 17:50:07 -04:00
|
|
|
new_size = File.size(original_path)
|
|
|
|
expect(orig_size).to be > new_size
|
|
|
|
expect(new_size).not_to eq(0)
|
2018-10-22 17:45:06 -04:00
|
|
|
|
2018-08-19 22:18:49 -04:00
|
|
|
ensure
|
|
|
|
File.delete(original_path) if File.exists?(original_path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-07-17 05:11:05 -04:00
|
|
|
it 'should work correctly' do
|
2018-07-17 03:48:59 -04:00
|
|
|
|
2018-08-27 22:48:43 -04:00
|
|
|
file = File.open("#{Rails.root}/spec/fixtures/images/resized.png")
|
|
|
|
upload = UploadCreator.new(file, "test.bin").create_for(-1)
|
2018-07-17 03:48:59 -04:00
|
|
|
|
2018-08-27 22:48:43 -04:00
|
|
|
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
|
2018-07-17 03:48:59 -04:00
|
|
|
end
|
2018-07-25 21:16:14 -04:00
|
|
|
|
|
|
|
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,
|
2018-07-25 22:17:38 -04:00
|
|
|
5,
|
|
|
|
raise_on_error: true
|
2018-07-25 21:16:14 -04:00
|
|
|
)
|
|
|
|
end.to raise_error(RuntimeError, /improper image header/)
|
|
|
|
ensure
|
|
|
|
File.delete(tmp_path) if File.exists?(tmp_path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2018-07-17 03:48:59 -04:00
|
|
|
end
|
|
|
|
|
2018-07-17 05:11:05 -04:00
|
|
|
describe '.downsize' do
|
2019-01-02 11:43:13 -05:00
|
|
|
it 'should downsize logo (requires ImageMagick 7)' do
|
2018-07-17 05:11:05 -04:00
|
|
|
tmp_path = "/tmp/downsized.png"
|
2018-07-17 03:48:59 -04:00
|
|
|
|
2018-07-17 05:11:05 -04:00
|
|
|
begin
|
|
|
|
OptimizedImage.downsize(
|
|
|
|
"#{Rails.root}/spec/fixtures/images/logo.png",
|
|
|
|
tmp_path,
|
|
|
|
"100x100\>"
|
|
|
|
)
|
2018-07-17 03:48:59 -04:00
|
|
|
|
2019-01-02 01:19:52 -05:00
|
|
|
info = FastImage.new(tmp_path)
|
|
|
|
expect(info.size).to eq([100, 27])
|
|
|
|
expect(File.size(tmp_path)).to be < 2300
|
2018-11-05 04:12:52 -05:00
|
|
|
|
2018-07-17 05:11:05 -04:00
|
|
|
ensure
|
|
|
|
File.delete(tmp_path) if File.exists?(tmp_path)
|
|
|
|
end
|
2018-07-17 03:48:59 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-12-18 18:16:18 -05:00
|
|
|
describe ".safe_path?" do
|
|
|
|
|
|
|
|
it "correctly detects unsafe paths" do
|
2019-02-11 00:28:43 -05:00
|
|
|
expect(OptimizedImage.safe_path?("/path/A-AA/22_00.JPG")).to eq(true)
|
|
|
|
expect(OptimizedImage.safe_path?("/path/AAA/2200.JPG")).to eq(true)
|
2016-12-18 18:16:18 -05:00
|
|
|
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
|
|
|
|
|
2018-07-25 16:00:04 -04:00
|
|
|
it "raises InvalidAccess error on paths" do
|
2016-12-18 18:16:18 -05:00
|
|
|
expect {
|
|
|
|
OptimizedImage.ensure_safe_paths!("/a.png", "/b.png", "c.png")
|
|
|
|
}.to raise_error(Discourse::InvalidAccess)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2015-05-25 22:32:52 -04:00
|
|
|
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
|
|
|
|
|
2013-06-16 20:46:42 -04:00
|
|
|
describe ".create_for" do
|
|
|
|
|
2019-01-03 01:07:30 -05:00
|
|
|
context "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
|
|
|
|
|
2018-11-06 23:29:14 -05:00
|
|
|
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
|
|
|
|
|
2013-11-05 13:04:47 -05:00
|
|
|
context "when using an internal store" do
|
|
|
|
|
|
|
|
let(:store) { FakeInternalStore.new }
|
|
|
|
before { Discourse.stubs(:store).returns(store) }
|
|
|
|
|
2014-05-26 05:46:43 -04:00
|
|
|
context "when an error happened while generating the thumbnail" do
|
2013-11-05 13:04:47 -05:00
|
|
|
|
|
|
|
it "returns nil" do
|
2014-05-26 05:46:43 -04:00
|
|
|
OptimizedImage.expects(:resize).returns(false)
|
2015-01-05 11:04:23 -05:00
|
|
|
expect(OptimizedImage.create_for(upload, 100, 200)).to eq(nil)
|
2013-11-05 13:04:47 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
context "when the thumbnail is properly generated" do
|
|
|
|
|
2014-05-26 05:46:43 -04:00
|
|
|
before do
|
|
|
|
OptimizedImage.expects(:resize).returns(true)
|
|
|
|
end
|
2013-11-05 13:04:47 -05:00
|
|
|
|
|
|
|
it "does not download a copy of the original image" do
|
|
|
|
store.expects(:download).never
|
|
|
|
OptimizedImage.create_for(upload, 100, 200)
|
|
|
|
end
|
2013-07-31 17:26:34 -04:00
|
|
|
|
2013-11-05 13:04:47 -05:00
|
|
|
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)
|
2015-01-05 11:04:23 -05:00
|
|
|
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")
|
2013-11-05 13:04:47 -05:00
|
|
|
end
|
2013-07-31 17:26:34 -04:00
|
|
|
|
2018-12-14 17:44:38 -05:00
|
|
|
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
|
|
|
|
|
2013-07-31 17:26:34 -04:00
|
|
|
end
|
|
|
|
|
2013-06-16 20:46:42 -04:00
|
|
|
end
|
|
|
|
|
2013-07-31 17:26:34 -04:00
|
|
|
describe "external store" do
|
2019-05-29 08:10:46 -04:00
|
|
|
let(:s3_upload) { Fabricate(:upload_s3) }
|
|
|
|
|
|
|
|
before do
|
|
|
|
SiteSetting.enable_s3_uploads = true
|
|
|
|
SiteSetting.s3_upload_bucket = "s3-upload-bucket"
|
|
|
|
SiteSetting.s3_access_key_id = "some key"
|
|
|
|
SiteSetting.s3_secret_access_key = "some secret key"
|
2019-12-11 09:13:17 -05:00
|
|
|
SiteSetting.s3_region = "us-east-1"
|
2019-05-29 08:10:46 -04:00
|
|
|
|
|
|
|
tempfile = Tempfile.new(["discourse-external", ".png"])
|
|
|
|
|
|
|
|
%i{head get}.each do |method|
|
|
|
|
stub_request(method, "http://#{s3_upload.url}")
|
|
|
|
.to_return(
|
|
|
|
status: 200,
|
|
|
|
body: tempfile.read
|
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
2013-11-05 13:04:47 -05:00
|
|
|
|
|
|
|
context "when an error happened while generatign the thumbnail" do
|
|
|
|
it "returns nil" do
|
2014-05-26 05:46:43 -04:00
|
|
|
OptimizedImage.expects(:resize).returns(false)
|
2019-05-29 08:10:46 -04:00
|
|
|
expect(OptimizedImage.create_for(s3_upload, 100, 200)).to eq(nil)
|
2013-11-05 13:04:47 -05:00
|
|
|
end
|
2013-07-31 17:26:34 -04:00
|
|
|
end
|
|
|
|
|
2013-11-05 13:04:47 -05:00
|
|
|
context "when the thumbnail is properly generated" do
|
2014-05-26 05:46:43 -04:00
|
|
|
before do
|
|
|
|
OptimizedImage.expects(:resize).returns(true)
|
|
|
|
end
|
2013-11-05 13:04:47 -05:00
|
|
|
|
|
|
|
it "downloads a copy of the original image" do
|
2019-05-29 08:10:46 -04:00
|
|
|
optimized_path = "/optimized/1X/#{s3_upload.sha1}_2_100x200.png"
|
|
|
|
|
|
|
|
stub_request(
|
|
|
|
:head,
|
2019-12-11 14:26:52 -05:00
|
|
|
"https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com/"
|
2019-05-29 08:10:46 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
stub_request(
|
|
|
|
:put,
|
2019-12-11 14:26:52 -05:00
|
|
|
"https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com#{optimized_path}"
|
2019-05-29 08:10:46 -04:00
|
|
|
).to_return(
|
|
|
|
status: 200,
|
|
|
|
headers: { "ETag" => "someetag" }
|
|
|
|
)
|
|
|
|
|
|
|
|
oi = OptimizedImage.create_for(s3_upload, 100, 200)
|
2013-11-05 13:04:47 -05:00
|
|
|
|
2015-01-05 11:04:23 -05:00
|
|
|
expect(oi.sha1).to eq("da39a3ee5e6b4b0d3255bfef95601890afd80709")
|
|
|
|
expect(oi.extension).to eq(".png")
|
|
|
|
expect(oi.width).to eq(100)
|
|
|
|
expect(oi.height).to eq(200)
|
2019-05-29 08:10:46 -04:00
|
|
|
expect(oi.url).to eq("//#{SiteSetting.s3_upload_bucket}.s3.dualstack.us-east-1.amazonaws.com#{optimized_path}")
|
2013-11-05 13:04:47 -05:00
|
|
|
end
|
|
|
|
|
2013-07-31 17:26:34 -04:00
|
|
|
end
|
|
|
|
|
2013-06-16 20:46:42 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
2019-03-22 04:46:13 -04:00
|
|
|
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
|
|
|
|
|
2013-06-16 04:39:48 -04:00
|
|
|
end
|
2013-11-05 13:04:47 -05:00
|
|
|
|
|
|
|
class FakeInternalStore
|
|
|
|
|
|
|
|
def external?
|
2015-05-29 12:39:47 -04:00
|
|
|
false
|
2013-11-05 13:04:47 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def path_for(upload)
|
|
|
|
upload.url
|
|
|
|
end
|
|
|
|
|
2020-01-15 22:50:27 -05:00
|
|
|
def store_optimized_image(file, optimized_image, content_type = nil, secure: false)
|
2013-11-05 13:04:47 -05:00
|
|
|
"/internally/stored/optimized/image#{optimized_image.extension}"
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|