jr-paperclip 7.3.0 → 8.0.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/{test.yml → tests.yml} +19 -9
- data/.rubocop.yml +2 -1
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +1 -0
- data/NEWS +16 -1
- data/README.md +119 -8
- data/UPGRADING +5 -0
- data/VIPS_MIGRATION_GUIDE.md +131 -0
- data/features/basic_integration.feature +27 -0
- data/features/step_definitions/attachment_steps.rb +17 -0
- data/gemfiles/7.0.gemfile +1 -0
- data/gemfiles/7.1.gemfile +1 -0
- data/gemfiles/7.2.gemfile +1 -0
- data/gemfiles/8.0.gemfile +1 -0
- data/gemfiles/8.1.gemfile +1 -0
- data/lib/paperclip/attachment.rb +3 -2
- data/lib/paperclip/errors.rb +4 -5
- data/lib/paperclip/geometry.rb +3 -3
- data/lib/paperclip/geometry_detector_factory.rb +52 -12
- data/lib/paperclip/helpers.rb +18 -0
- data/lib/paperclip/processor.rb +36 -4
- data/lib/paperclip/thumbnail.rb +568 -62
- data/lib/paperclip/version.rb +1 -1
- data/lib/paperclip.rb +26 -9
- data/paperclip.gemspec +3 -2
- data/spec/paperclip/attachment_definitions_spec.rb +300 -0
- data/spec/paperclip/attachment_spec.rb +1 -1
- data/spec/paperclip/geometry_detector_spec.rb +81 -32
- data/spec/paperclip/geometry_spec.rb +8 -5
- data/spec/paperclip/helpers_spec.rb +49 -0
- data/spec/paperclip/lazy_thumbnail_compatibility_spec.rb +266 -0
- data/spec/paperclip/processor_spec.rb +35 -1
- data/spec/paperclip/style_spec.rb +58 -0
- data/spec/paperclip/thumbnail_custom_options_spec.rb +173 -0
- data/spec/paperclip/thumbnail_loader_options_spec.rb +53 -0
- data/spec/paperclip/thumbnail_security_spec.rb +42 -0
- data/spec/paperclip/thumbnail_spec.rb +1127 -172
- metadata +36 -4
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
# This spec tests compatibility with Mastodon's LazyThumbnail pattern.
|
|
4
|
+
# LazyThumbnail is a subclass of Paperclip::Thumbnail that conditionally
|
|
5
|
+
# skips processing when the image doesn't need transformation.
|
|
6
|
+
# See: https://github.com/mastodon/mastodon/blob/main/lib/paperclip/lazy_thumbnail.rb
|
|
7
|
+
|
|
8
|
+
module Paperclip
|
|
9
|
+
class LazyThumbnail < Paperclip::Thumbnail
|
|
10
|
+
def make
|
|
11
|
+
return File.open(@file.path) unless needs_convert?
|
|
12
|
+
|
|
13
|
+
if options[:geometry]
|
|
14
|
+
min_side = [@current_geometry.width, @current_geometry.height].min.to_i
|
|
15
|
+
options[:geometry] = "#{min_side}x#{min_side}#" if @target_geometry.square? && min_side < @target_geometry.width
|
|
16
|
+
elsif options[:pixels]
|
|
17
|
+
width = Math.sqrt(options[:pixels] * (@current_geometry.width.to_f / @current_geometry.height)).round.to_i
|
|
18
|
+
height = Math.sqrt(options[:pixels] * (@current_geometry.height.to_f / @current_geometry.width)).round.to_i
|
|
19
|
+
options[:geometry] = "#{width}x#{height}>"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Paperclip::Thumbnail.make(file, options, attachment)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def needs_convert?
|
|
28
|
+
needs_different_geometry? || needs_different_format? || needs_metadata_stripping?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def needs_different_geometry?
|
|
32
|
+
(options[:geometry] && @current_geometry.width != @target_geometry.width && @current_geometry.height != @target_geometry.height) ||
|
|
33
|
+
(options[:pixels] && @current_geometry.width * @current_geometry.height > options[:pixels])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def needs_different_format?
|
|
37
|
+
@format.present? && @current_format != @format
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def needs_metadata_stripping?
|
|
41
|
+
@attachment.respond_to?(:instance) && @attachment.instance.respond_to?(:local?) && @attachment.instance.local?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe Paperclip::LazyThumbnail do
|
|
47
|
+
let(:file) { File.new(fixture_file("5k.png"), "rb") }
|
|
48
|
+
let(:rotated_file) { File.new(fixture_file("rotated.jpg"), "rb") }
|
|
49
|
+
let(:attachment) { double("Attachment", options: {}, instance: double(local?: false)) }
|
|
50
|
+
|
|
51
|
+
after do
|
|
52
|
+
file.close
|
|
53
|
+
rotated_file.close rescue nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
describe "basic compatibility" do
|
|
57
|
+
it "inherits from Paperclip::Thumbnail" do
|
|
58
|
+
expect(Paperclip::LazyThumbnail.superclass).to eq(Paperclip::Thumbnail)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "can access current_geometry" do
|
|
62
|
+
processor = described_class.new(file, { geometry: "100x100" }, attachment)
|
|
63
|
+
expect(processor.current_geometry).to be_a(Paperclip::Geometry)
|
|
64
|
+
expect(processor.current_geometry.width).to be > 0
|
|
65
|
+
expect(processor.current_geometry.height).to be > 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "can access target_geometry" do
|
|
69
|
+
processor = described_class.new(file, { geometry: "100x100#" }, attachment)
|
|
70
|
+
expect(processor.target_geometry).to be_a(Paperclip::Geometry)
|
|
71
|
+
expect(processor.target_geometry.width).to eq(100)
|
|
72
|
+
expect(processor.target_geometry.height).to eq(100)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "target_geometry responds to square?" do
|
|
76
|
+
processor = described_class.new(file, { geometry: "100x100#" }, attachment)
|
|
77
|
+
expect(processor.target_geometry).to respond_to(:square?)
|
|
78
|
+
expect(processor.target_geometry.square?).to be true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "can access format and current_format" do
|
|
82
|
+
processor = described_class.new(file, { geometry: "100x100", format: :jpg }, attachment)
|
|
83
|
+
expect(processor.format).to eq(:jpg)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
describe "with ImageMagick backend" do
|
|
88
|
+
let(:attachment) { double("Attachment", options: { backend: :image_magick }, instance: double(local?: false)) }
|
|
89
|
+
|
|
90
|
+
it "processes images when geometry change is needed" do
|
|
91
|
+
processor = described_class.new(file, { geometry: "50x50#" }, attachment)
|
|
92
|
+
result = processor.make
|
|
93
|
+
|
|
94
|
+
expect(File.exist?(result.path)).to be true
|
|
95
|
+
dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
|
|
96
|
+
expect(dimensions).to eq("50x50")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "processes images when format change is needed" do
|
|
100
|
+
processor = described_class.new(file, { geometry: "100x100", format: :jpg }, attachment)
|
|
101
|
+
result = processor.make
|
|
102
|
+
|
|
103
|
+
expect(File.exist?(result.path)).to be true
|
|
104
|
+
expect(result.path).to end_with(".jpg")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "skips processing when no changes needed" do
|
|
108
|
+
# Create a processor where geometry matches source
|
|
109
|
+
processor = described_class.new(file, { geometry: "434x66" }, attachment)
|
|
110
|
+
|
|
111
|
+
# Since the source is 434x66 (5k.png dimensions), and target is same,
|
|
112
|
+
# needs_different_geometry? returns false
|
|
113
|
+
result = processor.make
|
|
114
|
+
|
|
115
|
+
# Result should be the original file (File.open(@file.path))
|
|
116
|
+
expect(File.exist?(result.path)).to be true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "handles square geometry optimization" do
|
|
120
|
+
# Source is 434x66, min_side is 66
|
|
121
|
+
# Requesting 100x100# square, but min_side (66) < target width (100)
|
|
122
|
+
# So geometry should be adjusted to "66x66#"
|
|
123
|
+
processor = described_class.new(file, { geometry: "100x100#" }, attachment)
|
|
124
|
+
result = processor.make
|
|
125
|
+
|
|
126
|
+
expect(File.exist?(result.path)).to be true
|
|
127
|
+
dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
|
|
128
|
+
expect(dimensions).to eq("66x66")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe "with libvips backend" do
|
|
133
|
+
let(:attachment) { double("Attachment", options: { backend: :vips }, instance: double(local?: false)) }
|
|
134
|
+
|
|
135
|
+
before do
|
|
136
|
+
begin
|
|
137
|
+
require "vips"
|
|
138
|
+
rescue LoadError
|
|
139
|
+
skip "libvips not installed"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "processes images when geometry change is needed" do
|
|
144
|
+
processor = described_class.new(file, { geometry: "50x50#" }, attachment)
|
|
145
|
+
result = processor.make
|
|
146
|
+
|
|
147
|
+
expect(File.exist?(result.path)).to be true
|
|
148
|
+
dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
|
|
149
|
+
expect(dimensions).to eq("50x50")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "processes images when format change is needed" do
|
|
153
|
+
processor = described_class.new(file, { geometry: "100x100", format: :jpg }, attachment)
|
|
154
|
+
result = processor.make
|
|
155
|
+
|
|
156
|
+
expect(File.exist?(result.path)).to be true
|
|
157
|
+
expect(result.path).to end_with(".jpg")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "skips processing when no changes needed" do
|
|
161
|
+
processor = described_class.new(file, { geometry: "434x66" }, attachment)
|
|
162
|
+
result = processor.make
|
|
163
|
+
expect(File.exist?(result.path)).to be true
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "handles square geometry optimization" do
|
|
167
|
+
processor = described_class.new(file, { geometry: "100x100#" }, attachment)
|
|
168
|
+
result = processor.make
|
|
169
|
+
|
|
170
|
+
expect(File.exist?(result.path)).to be true
|
|
171
|
+
dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
|
|
172
|
+
expect(dimensions).to eq("66x66")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
describe "pixels-based resizing (Mastodon-specific)" do
|
|
177
|
+
let(:attachment) { double("Attachment", options: {}, instance: double(local?: false)) }
|
|
178
|
+
|
|
179
|
+
context "with ImageMagick backend" do
|
|
180
|
+
let(:attachment) { double("Attachment", options: { backend: :image_magick }, instance: double(local?: false)) }
|
|
181
|
+
|
|
182
|
+
it "resizes based on maximum pixel count" do
|
|
183
|
+
# Source is 434x66 = 28,644 pixels
|
|
184
|
+
# Request max 10,000 pixels
|
|
185
|
+
processor = described_class.new(file, { pixels: 10_000 }, attachment)
|
|
186
|
+
result = processor.make
|
|
187
|
+
|
|
188
|
+
expect(File.exist?(result.path)).to be true
|
|
189
|
+
dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
|
|
190
|
+
width, height = dimensions.split("x").map(&:to_i)
|
|
191
|
+
expect(width * height).to be <= 10_000
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it "skips processing when image is already small enough" do
|
|
195
|
+
# Source is 434x66 = 28,644 pixels
|
|
196
|
+
# Request max 50,000 pixels - no resize needed
|
|
197
|
+
processor = described_class.new(file, { pixels: 50_000 }, attachment)
|
|
198
|
+
result = processor.make
|
|
199
|
+
|
|
200
|
+
expect(File.exist?(result.path)).to be true
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
context "with libvips backend" do
|
|
205
|
+
let(:attachment) { double("Attachment", options: { backend: :vips }, instance: double(local?: false)) }
|
|
206
|
+
|
|
207
|
+
before do
|
|
208
|
+
begin
|
|
209
|
+
require "vips"
|
|
210
|
+
rescue LoadError
|
|
211
|
+
skip "libvips not installed"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "resizes based on maximum pixel count" do
|
|
216
|
+
processor = described_class.new(file, { pixels: 10_000 }, attachment)
|
|
217
|
+
result = processor.make
|
|
218
|
+
|
|
219
|
+
expect(File.exist?(result.path)).to be true
|
|
220
|
+
dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
|
|
221
|
+
width, height = dimensions.split("x").map(&:to_i)
|
|
222
|
+
expect(width * height).to be <= 10_000
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
describe "metadata stripping trigger" do
|
|
228
|
+
it "processes when attachment instance is local" do
|
|
229
|
+
local_instance = double(local?: true)
|
|
230
|
+
local_attachment = double("Attachment", options: {}, instance: local_instance)
|
|
231
|
+
|
|
232
|
+
processor = described_class.new(file, { geometry: "434x66" }, local_attachment)
|
|
233
|
+
|
|
234
|
+
# Even with matching geometry, should process because local? is true
|
|
235
|
+
# This is verified by the fact that make doesn't just return the original file
|
|
236
|
+
result = processor.make
|
|
237
|
+
expect(File.exist?(result.path)).to be true
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
describe "Thumbnail.make class method" do
|
|
242
|
+
it "is available and works correctly" do
|
|
243
|
+
expect(Paperclip::Thumbnail).to respond_to(:make)
|
|
244
|
+
|
|
245
|
+
result = Paperclip::Thumbnail.make(file, { geometry: "50x50#" }, attachment)
|
|
246
|
+
expect(File.exist?(result.path)).to be true
|
|
247
|
+
|
|
248
|
+
dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
|
|
249
|
+
expect(dimensions).to eq("50x50")
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it "respects backend option" do
|
|
253
|
+
begin
|
|
254
|
+
require "vips"
|
|
255
|
+
rescue LoadError
|
|
256
|
+
skip "libvips not installed"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
result = Paperclip::Thumbnail.make(file, { geometry: "50x50#", backend: :vips }, attachment)
|
|
260
|
+
expect(File.exist?(result.path)).to be true
|
|
261
|
+
|
|
262
|
+
dimensions = `identify -format "%wx%h" "#{result.path}"`.strip
|
|
263
|
+
expect(dimensions).to eq("50x50")
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -11,16 +11,50 @@ describe Paperclip::Processor do
|
|
|
11
11
|
context "Calling #convert" do
|
|
12
12
|
it "runs the convert command with Terrapin" do
|
|
13
13
|
Paperclip.options[:log_command] = false
|
|
14
|
+
allow(Paperclip).to receive(:imagemagick7?).and_return(false)
|
|
15
|
+
Paperclip.options[:is_windows] = false
|
|
14
16
|
expect(Terrapin::CommandLine).to receive(:new).with("convert", "stuff", {}).and_return(double(run: nil))
|
|
15
17
|
Paperclip::Processor.new("filename").convert("stuff")
|
|
16
18
|
end
|
|
19
|
+
|
|
20
|
+
it "runs the magick convert command when ImageMagick 7 is present" do
|
|
21
|
+
Paperclip.options[:log_command] = false
|
|
22
|
+
allow(Paperclip).to receive(:imagemagick7?).and_return(true)
|
|
23
|
+
expect(Terrapin::CommandLine).to receive(:new).with("magick", "stuff", {}).and_return(double(run: nil))
|
|
24
|
+
Paperclip::Processor.new("filename").convert("stuff")
|
|
25
|
+
end
|
|
17
26
|
end
|
|
18
27
|
|
|
19
28
|
context "Calling #identify" do
|
|
20
|
-
it "runs the identify command
|
|
29
|
+
it "runs the identify command" do
|
|
21
30
|
Paperclip.options[:log_command] = false
|
|
31
|
+
allow(Paperclip).to receive(:imagemagick7?).and_return(false)
|
|
32
|
+
Paperclip.options[:is_windows] = false
|
|
22
33
|
expect(Terrapin::CommandLine).to receive(:new).with("identify", "stuff", {}).and_return(double(run: nil))
|
|
23
34
|
Paperclip::Processor.new("filename").identify("stuff")
|
|
24
35
|
end
|
|
36
|
+
|
|
37
|
+
it "runs the magick identify command when ImageMagick 7 is present" do
|
|
38
|
+
Paperclip.options[:log_command] = false
|
|
39
|
+
allow(Paperclip).to receive(:imagemagick7?).and_return(true)
|
|
40
|
+
expect(Terrapin::CommandLine).to receive(:new).with("magick identify", "stuff", {}).and_return(double(run: nil))
|
|
41
|
+
Paperclip::Processor.new("filename").identify("stuff")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
context "Calling #vips" do
|
|
46
|
+
it "runs the vips command with Paperclip.run" do
|
|
47
|
+
Paperclip.options[:log_command] = false
|
|
48
|
+
expect(Paperclip).to receive(:run).with("vips", "stuff", { :some => "args" })
|
|
49
|
+
Paperclip::Processor.new("filename").vips("stuff", { :some => "args" })
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context "Calling #vipsheader" do
|
|
54
|
+
it "runs the vipsheader command with Paperclip.run" do
|
|
55
|
+
Paperclip.options[:log_command] = false
|
|
56
|
+
expect(Paperclip).to receive(:run).with("vipsheader", "stuff", { :some => "args" })
|
|
57
|
+
Paperclip::Processor.new("filename").vipsheader("stuff", { :some => "args" })
|
|
58
|
+
end
|
|
25
59
|
end
|
|
26
60
|
end
|
|
@@ -212,6 +212,64 @@ describe Paperclip::Style do
|
|
|
212
212
|
end
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
+
context "A style rule with per-style backend selection" do
|
|
216
|
+
before do
|
|
217
|
+
@attachment = attachment path: ":basename.:extension",
|
|
218
|
+
styles: {
|
|
219
|
+
vips_style: {
|
|
220
|
+
geometry: "800x800>",
|
|
221
|
+
backend: :vips
|
|
222
|
+
},
|
|
223
|
+
magick_style: {
|
|
224
|
+
geometry: "100x100#",
|
|
225
|
+
backend: :image_magick
|
|
226
|
+
},
|
|
227
|
+
default_style: {
|
|
228
|
+
geometry: "200x200"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it "passes backend option through to processor_options for vips style" do
|
|
234
|
+
assert_equal :vips, @attachment.styles[:vips_style].processor_options[:backend]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "passes backend option through to processor_options for image_magick style" do
|
|
238
|
+
assert_equal :image_magick, @attachment.styles[:magick_style].processor_options[:backend]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it "does not include backend in processor_options when not specified" do
|
|
242
|
+
expect(@attachment.styles[:default_style].processor_options).not_to have_key(:backend)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it "includes geometry correctly for each style" do
|
|
246
|
+
assert_equal "800x800>", @attachment.styles[:vips_style].geometry
|
|
247
|
+
assert_equal "100x100#", @attachment.styles[:magick_style].geometry
|
|
248
|
+
assert_equal "200x200", @attachment.styles[:default_style].geometry
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it "allows accessing backend via hash notation" do
|
|
252
|
+
assert_equal :vips, @attachment.styles[:vips_style][:backend]
|
|
253
|
+
assert_equal :image_magick, @attachment.styles[:magick_style][:backend]
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
context "A style rule with backend as a proc" do
|
|
258
|
+
before do
|
|
259
|
+
@attachment = attachment path: ":basename.:extension",
|
|
260
|
+
styles: {
|
|
261
|
+
dynamic_backend: {
|
|
262
|
+
geometry: "500x500",
|
|
263
|
+
backend: lambda { |_a| :vips }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it "evaluates proc when processor_options are requested" do
|
|
269
|
+
assert_equal :vips, @attachment.styles[:dynamic_backend].processor_options[:backend]
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
215
273
|
context "A style rule supplied with default format" do
|
|
216
274
|
before do
|
|
217
275
|
@attachment = attachment default_format: :png,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Paperclip::Thumbnail do
|
|
4
|
+
context "with ImageMagick specific + options" do
|
|
5
|
+
before do
|
|
6
|
+
@file = File.new(fixture_file("5k.png"), "rb")
|
|
7
|
+
@attachment = double("Attachment", options: {})
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
after { @file.close }
|
|
11
|
+
|
|
12
|
+
it "correctly applies options starting with +" do
|
|
13
|
+
# The user's specific options - with Shellwords, quoted values are parsed correctly
|
|
14
|
+
convert_options = '-coalesce +profile "!icc,*" +set date:modify +set date:create +set date:timestamp'
|
|
15
|
+
|
|
16
|
+
thumb = Paperclip::Thumbnail.new(@file, {
|
|
17
|
+
geometry: "100x100",
|
|
18
|
+
convert_options: convert_options,
|
|
19
|
+
backend: :image_magick,
|
|
20
|
+
}, @attachment)
|
|
21
|
+
|
|
22
|
+
# Spy on the pipeline to verify the correct methods/arguments are called on it.
|
|
23
|
+
allow(thumb).to receive(:apply_single_option).and_call_original
|
|
24
|
+
expect { thumb.make }.not_to raise_error
|
|
25
|
+
|
|
26
|
+
# With Shellwords parsing, the outer quotes are stripped from "!icc,*" -> !icc,*
|
|
27
|
+
expect(thumb).to have_received(:apply_single_option).with(anything, "coalesce", nil, "-")
|
|
28
|
+
expect(thumb).to have_received(:apply_single_option).with(anything, "profile", "!icc,*", "+")
|
|
29
|
+
expect(thumb).to have_received(:apply_single_option).with(anything, "set", "date:modify", "+")
|
|
30
|
+
expect(thumb).to have_received(:apply_single_option).with(anything, "set", "date:create", "+")
|
|
31
|
+
expect(thumb).to have_received(:apply_single_option).with(anything, "set", "date:timestamp", "+")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
context "with convert_options having multiple arguments for a flag" do
|
|
36
|
+
before do
|
|
37
|
+
@file = File.new(fixture_file("5k.png"), "rb")
|
|
38
|
+
@attachment = double("Attachment", options: {})
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
after { @file.close }
|
|
42
|
+
|
|
43
|
+
it "handles -set with two arguments correctly" do
|
|
44
|
+
# User provided example
|
|
45
|
+
convert_options = "-coalesce -set my_prop 123123"
|
|
46
|
+
|
|
47
|
+
thumb = Paperclip::Thumbnail.new(@file, {
|
|
48
|
+
geometry: "100x100",
|
|
49
|
+
convert_options: convert_options,
|
|
50
|
+
backend: :image_magick,
|
|
51
|
+
}, @attachment)
|
|
52
|
+
|
|
53
|
+
result = nil
|
|
54
|
+
expect { result = thumb.make }.not_to raise_error
|
|
55
|
+
|
|
56
|
+
# Verify property is set
|
|
57
|
+
require "shellwords"
|
|
58
|
+
output = `identify -verbose #{Shellwords.escape(result.path)}`
|
|
59
|
+
expect(output).to include("my_prop: 123123")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
context "with convert_options having multiple arguments followed by other options" do
|
|
64
|
+
before do
|
|
65
|
+
@file = File.new(fixture_file("5k.png"), "rb")
|
|
66
|
+
@attachment = double("Attachment", options: {})
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
after { @file.close }
|
|
70
|
+
|
|
71
|
+
it "handles -set with two arguments followed by another option" do
|
|
72
|
+
# User provided example scenario
|
|
73
|
+
convert_options = "-set my_prop 123123 -auto-orient"
|
|
74
|
+
|
|
75
|
+
thumb = Paperclip::Thumbnail.new(@file, {
|
|
76
|
+
geometry: "100x100",
|
|
77
|
+
convert_options: convert_options,
|
|
78
|
+
backend: :image_magick,
|
|
79
|
+
}, @attachment)
|
|
80
|
+
|
|
81
|
+
result = nil
|
|
82
|
+
expect { result = thumb.make }.not_to raise_error
|
|
83
|
+
|
|
84
|
+
require "shellwords"
|
|
85
|
+
output = `identify -verbose #{Shellwords.escape(result.path)}`
|
|
86
|
+
expect(output).to include("my_prop: 123123")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
context "with quoted values in convert_options (Shellwords parsing)" do
|
|
91
|
+
before do
|
|
92
|
+
@file = File.new(fixture_file("5k.png"), "rb")
|
|
93
|
+
@attachment = double("Attachment", options: {})
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
after { @file.close }
|
|
97
|
+
|
|
98
|
+
it "handles single-quoted values correctly" do
|
|
99
|
+
# Shellwords should parse 'Hello World' as a single token
|
|
100
|
+
convert_options = "-set my_comment 'Hello World'"
|
|
101
|
+
|
|
102
|
+
thumb = Paperclip::Thumbnail.new(@file, {
|
|
103
|
+
geometry: "100x100",
|
|
104
|
+
convert_options: convert_options,
|
|
105
|
+
backend: :image_magick,
|
|
106
|
+
}, @attachment)
|
|
107
|
+
|
|
108
|
+
allow(thumb).to receive(:apply_single_option).and_call_original
|
|
109
|
+
result = nil
|
|
110
|
+
expect { result = thumb.make }.not_to raise_error
|
|
111
|
+
|
|
112
|
+
# Verify that 'Hello World' was passed as a single value (without quotes)
|
|
113
|
+
expect(thumb).to have_received(:apply_single_option).with(anything, "set", "my_comment", "-")
|
|
114
|
+
|
|
115
|
+
# Check output includes the set property
|
|
116
|
+
require "shellwords"
|
|
117
|
+
output = `identify -verbose #{Shellwords.escape(result.path)}`
|
|
118
|
+
expect(output).to include("my_comment: Hello World")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "handles double-quoted values correctly" do
|
|
122
|
+
convert_options = '-set my_comment "Hello World"'
|
|
123
|
+
|
|
124
|
+
thumb = Paperclip::Thumbnail.new(@file, {
|
|
125
|
+
geometry: "100x100",
|
|
126
|
+
convert_options: convert_options,
|
|
127
|
+
backend: :image_magick,
|
|
128
|
+
}, @attachment)
|
|
129
|
+
|
|
130
|
+
result = nil
|
|
131
|
+
expect { result = thumb.make }.not_to raise_error
|
|
132
|
+
|
|
133
|
+
require "shellwords"
|
|
134
|
+
output = `identify -verbose #{Shellwords.escape(result.path)}`
|
|
135
|
+
expect(output).to include("my_comment: Hello World")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it "handles escaped spaces correctly" do
|
|
139
|
+
convert_options = '-set my_comment Hello\ World'
|
|
140
|
+
|
|
141
|
+
thumb = Paperclip::Thumbnail.new(@file, {
|
|
142
|
+
geometry: "100x100",
|
|
143
|
+
convert_options: convert_options,
|
|
144
|
+
backend: :image_magick,
|
|
145
|
+
}, @attachment)
|
|
146
|
+
|
|
147
|
+
result = nil
|
|
148
|
+
expect { result = thumb.make }.not_to raise_error
|
|
149
|
+
|
|
150
|
+
require "shellwords"
|
|
151
|
+
output = `identify -verbose #{Shellwords.escape(result.path)}`
|
|
152
|
+
expect(output).to include("my_comment: Hello World")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "handles multiple quoted options correctly" do
|
|
156
|
+
convert_options = "-set first_prop 'First Value' -set second_prop 'Second Value'"
|
|
157
|
+
|
|
158
|
+
thumb = Paperclip::Thumbnail.new(@file, {
|
|
159
|
+
geometry: "100x100",
|
|
160
|
+
convert_options: convert_options,
|
|
161
|
+
backend: :image_magick,
|
|
162
|
+
}, @attachment)
|
|
163
|
+
|
|
164
|
+
result = nil
|
|
165
|
+
expect { result = thumb.make }.not_to raise_error
|
|
166
|
+
|
|
167
|
+
require "shellwords"
|
|
168
|
+
output = `identify -verbose #{Shellwords.escape(result.path)}`
|
|
169
|
+
expect(output).to include("first_prop: First Value")
|
|
170
|
+
expect(output).to include("second_prop: Second Value")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Paperclip::Thumbnail do
|
|
4
|
+
describe "#parse_loader_options" do
|
|
5
|
+
let(:file) { File.new(fixture_file("5k.png"), "rb") }
|
|
6
|
+
let(:thumb) { Paperclip::Thumbnail.new(file, geometry: "50x50") }
|
|
7
|
+
|
|
8
|
+
after { file.close }
|
|
9
|
+
|
|
10
|
+
it "correctly parses positive numeric values" do
|
|
11
|
+
options = "-density 300"
|
|
12
|
+
result = thumb.send(:parse_loader_options, options)
|
|
13
|
+
expect(result).to eq({ density: "300" })
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "correctly parses negative numeric values" do
|
|
17
|
+
# Some hypothetical loader option that might take a negative value
|
|
18
|
+
options = "-something -90"
|
|
19
|
+
result = thumb.send(:parse_loader_options, options)
|
|
20
|
+
expect(result).to eq({ something: "-90" })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "still treats non-numeric tokens starting with - as new options" do
|
|
24
|
+
options = "-density 300 -strip"
|
|
25
|
+
result = thumb.send(:parse_loader_options, options)
|
|
26
|
+
expect(result).to eq({ density: "300", strip: true })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "handles single-quoted values correctly" do
|
|
30
|
+
options = "-comment 'Hello World'"
|
|
31
|
+
result = thumb.send(:parse_loader_options, options)
|
|
32
|
+
expect(result).to eq({ comment: "Hello World" })
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "handles double-quoted values correctly" do
|
|
36
|
+
options = '-path "/some/path with spaces"'
|
|
37
|
+
result = thumb.send(:parse_loader_options, options)
|
|
38
|
+
expect(result).to eq({ path: "/some/path with spaces" })
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "handles escaped spaces correctly" do
|
|
42
|
+
options = '-label Hello\ World'
|
|
43
|
+
result = thumb.send(:parse_loader_options, options)
|
|
44
|
+
expect(result).to eq({ label: "Hello World" })
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "handles multiple options with quoted values" do
|
|
48
|
+
options = "-first 'Value One' -second 'Value Two'"
|
|
49
|
+
result = thumb.send(:parse_loader_options, options)
|
|
50
|
+
expect(result).to eq({ first: "Value One", second: "Value Two" })
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe Paperclip::Thumbnail do
|
|
4
|
+
context "Security" do
|
|
5
|
+
old_backend = Paperclip.options[:backend]
|
|
6
|
+
|
|
7
|
+
before do
|
|
8
|
+
@file = File.new(fixture_file("5k.png"), "rb")
|
|
9
|
+
@attachment = double("Attachment", options: {})
|
|
10
|
+
Paperclip.options[:backend] = :image_magick
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
after do
|
|
14
|
+
@file.close
|
|
15
|
+
Paperclip.options[:backend] = old_backend
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "allows safe convert options" do
|
|
19
|
+
thumb = Paperclip::Thumbnail.new(@file, { geometry: "100x100", convert_options: "-strip" }, @attachment)
|
|
20
|
+
|
|
21
|
+
expect(Paperclip).to_not receive(:log).with(/Warning: Option strip is not allowed/)
|
|
22
|
+
thumb.make
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "blocks unsafe convert options" do
|
|
26
|
+
# -write is not in the ALLOWED_IMAGEMAGICK_OPTIONS list
|
|
27
|
+
thumb = Paperclip::Thumbnail.new(@file, { geometry: "100x100", convert_options: "-write /tmp/hacked.png" },
|
|
28
|
+
@attachment)
|
|
29
|
+
|
|
30
|
+
expect(Paperclip).to receive(:log).with("Warning: Option write is not allowed.")
|
|
31
|
+
thumb.make
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "allows options with underscores in the whitelist when passed with hyphens" do
|
|
35
|
+
# 'auto_orient' is in the list. User passes '-auto-orient'.
|
|
36
|
+
thumb = Paperclip::Thumbnail.new(@file, { geometry: "100x100", convert_options: "-auto-orient" }, @attachment)
|
|
37
|
+
|
|
38
|
+
expect(Paperclip).to_not receive(:log).with(/Warning: Option auto-orient is not allowed/)
|
|
39
|
+
thumb.make
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|