discourse/spec/models/optimized_image_spec.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

410 lines
12 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
RSpec.describe OptimizedImage do
2013-11-06 02:04:47 +08:00
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)
2018-07-17 15:48:59 +08:00
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
2018-07-17 17:11:05 +08:00
describe '.resize' do
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
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
2018-07-17 17:11:05 +08:00
it 'should work correctly' do
2018-07-17 15:48:59 +08:00
file = File.open("#{Rails.root}/spec/fixtures/images/resized.png")
upload = UploadCreator.new(file, "test.bin").create_for(-1)
2018-07-17 15:48:59 +08: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 15:48:59 +08:00
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,
2018-07-26 10:17:38 +08:00
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
2018-07-17 15:48:59 +08:00
end
2018-07-17 17:11:05 +08:00
describe '.downsize' do
it 'should downsize logo (requires ImageMagick 7)' do
2018-07-17 17:11:05 +08:00
tmp_path = "/tmp/downsized.png"
2018-07-17 15:48:59 +08:00
2018-07-17 17:11:05 +08:00
begin
OptimizedImage.downsize(
"#{Rails.root}/spec/fixtures/images/logo.png",
tmp_path,
"100x100\>"
)
2018-07-17 15:48:59 +08:00
info = FastImage.new(tmp_path)
expect(info.size).to eq([100, 27])
expect(File.size(tmp_path)).to be < 2300
2018-07-17 17:11:05 +08:00
ensure
File.delete(tmp_path) if File.exist?(tmp_path)
2018-07-17 17:11:05 +08:00
end
2018-07-17 15:48:59 +08:00
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
2013-11-06 02:04:47 +08:00
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
2013-11-06 02:04:47 +08:00
it "returns nil" do
OptimizedImage.expects(:resize).returns(false)
2015-01-06 00:04:23 +08:00
expect(OptimizedImage.create_for(upload, 100, 200)).to eq(nil)
2013-11-06 02:04:47 +08:00
end
end
context "when the thumbnail is properly generated" do
before do
OptimizedImage.expects(:resize).returns(true)
end
2013-11-06 02:04:47 +08:00
it "does not download a copy of the original image" do
store.expects(:download).never
OptimizedImage.create_for(upload, 100, 200)
end
2013-11-06 02:04:47 +08: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-06 00:04:23 +08: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-06 02:04:47 +08:00
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 do
setup_s3
end
2013-11-06 02:04:47 +08:00
context "when we have a bad file returned" do
2013-11-06 02:04:47 +08:00
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)
2013-11-06 02:04:47 +08:00
end
end
2013-11-06 02:04:47 +08:00
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\.#{SiteSetting.s3_region}\.amazonaws.com#{optimized_path}})
.to_return(status: 200, headers: { "ETag" => "someetag" })
end
2013-11-06 02:04:47 +08:00
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"))
2013-11-06 02:04:47 +08:00
expect(oi.filesize).to be > 0
end
2013-11-06 02:04:47 +08:00
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
2013-06-16 16:39:48 +08:00
end
2013-11-06 02:04:47 +08:00
class FakeInternalStore
def external?
2015-05-30 00:39:47 +08:00
false
2013-11-06 02:04:47 +08:00
end
def path_for(upload)
upload.url
end
FEATURE: Secure media allowing duplicated uploads with category-level privacy and post-based access rules (#8664) ### General Changes and Duplication * We now consider a post `with_secure_media?` if it is in a read-restricted category. * When uploading we now set an upload's secure status straight away. * When uploading if `SiteSetting.secure_media` is enabled, we do not check to see if the upload already exists using the `sha1` digest of the upload. The `sha1` column of the upload is filled with a `SecureRandom.hex(20)` value which is the same length as `Upload::SHA1_LENGTH`. The `original_sha1` column is filled with the _real_ sha1 digest of the file. * Whether an upload `should_be_secure?` is now determined by whether the `access_control_post` is `with_secure_media?` (if there is no access control post then we leave the secure status as is). * When serializing the upload, we now cook the URL if the upload is secure. This is so it shows up correctly in the composer preview, because we set secure status on upload. ### Viewing Secure Media * The secure-media-upload URL will take the post that the upload is attached to into account via `Guardian.can_see?` for access permissions * If there is no `access_control_post` then we just deliver the media. This should be a rare occurrance and shouldn't cause issues as the `access_control_post` is set when `link_post_uploads` is called via `CookedPostProcessor` ### Removed We no longer do any of these because we do not reuse uploads by sha1 if secure media is enabled. * We no longer have a way to prevent cross-posting of a secure upload from a private context to a public context. * We no longer have to set `secure: false` for uploads when uploading for a theme component.
2020-01-16 11:50:27 +08:00
def store_optimized_image(file, optimized_image, content_type = nil, secure: false)
2013-11-06 02:04:47 +08:00
"/internally/stored/optimized/image#{optimized_image.extension}"
end
end