dragonfly 1.1.4 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +14 -6
  3. data/History.md +336 -309
  4. data/README.md +1 -1
  5. data/dev/rails_template.rb +35 -33
  6. data/dragonfly.gemspec +10 -16
  7. data/lib/dragonfly.rb +3 -1
  8. data/lib/dragonfly/content.rb +21 -22
  9. data/lib/dragonfly/image_magick/commands.rb +35 -0
  10. data/lib/dragonfly/image_magick/generators/plain.rb +13 -7
  11. data/lib/dragonfly/image_magick/generators/plasma.rb +10 -6
  12. data/lib/dragonfly/image_magick/generators/text.rb +67 -58
  13. data/lib/dragonfly/image_magick/plugin.rb +26 -25
  14. data/lib/dragonfly/image_magick/processors/encode.rb +16 -5
  15. data/lib/dragonfly/image_magick/processors/thumb.rb +37 -31
  16. data/lib/dragonfly/job/fetch_url.rb +1 -1
  17. data/lib/dragonfly/model/class_methods.rb +6 -1
  18. data/lib/dragonfly/param_validators.rb +37 -0
  19. data/lib/dragonfly/response.rb +2 -2
  20. data/lib/dragonfly/shell.rb +19 -13
  21. data/lib/dragonfly/utils.rb +1 -1
  22. data/lib/dragonfly/version.rb +1 -1
  23. data/samples/white pixel.png b/data/samples/mevs' white → pixel.png +0 -0
  24. data/spec/dragonfly/content_spec.rb +3 -3
  25. data/spec/dragonfly/cookie_monster_spec.rb +2 -2
  26. data/spec/dragonfly/image_magick/commands_spec.rb +98 -0
  27. data/spec/dragonfly/image_magick/generators/plain_spec.rb +39 -13
  28. data/spec/dragonfly/image_magick/generators/plasma_spec.rb +28 -9
  29. data/spec/dragonfly/image_magick/generators/text_spec.rb +51 -20
  30. data/spec/dragonfly/image_magick/plugin_spec.rb +45 -28
  31. data/spec/dragonfly/image_magick/processors/encode_spec.rb +30 -0
  32. data/spec/dragonfly/image_magick/processors/thumb_spec.rb +46 -45
  33. data/spec/dragonfly/model/active_record_spec.rb +62 -0
  34. data/spec/dragonfly/param_validators_spec.rb +89 -0
  35. data/spec/dragonfly/shell_spec.rb +12 -10
  36. data/spec/dragonfly_spec.rb +37 -13
  37. data/spec/functional/shell_commands_spec.rb +6 -9
  38. data/spec/spec_helper.rb +12 -14
  39. metadata +45 -14
  40. data/lib/dragonfly/image_magick/generators/convert.rb +0 -19
  41. data/lib/dragonfly/image_magick/processors/convert.rb +0 -33
  42. data/spec/dragonfly/image_magick/generators/convert_spec.rb +0 -19
  43. data/spec/dragonfly/image_magick/processors/convert_spec.rb +0 -88
@@ -1,11 +1,11 @@
1
- require 'dragonfly/image_magick/analysers/image_properties'
2
- require 'dragonfly/image_magick/generators/convert'
3
- require 'dragonfly/image_magick/generators/plain'
4
- require 'dragonfly/image_magick/generators/plasma'
5
- require 'dragonfly/image_magick/generators/text'
6
- require 'dragonfly/image_magick/processors/convert'
7
- require 'dragonfly/image_magick/processors/encode'
8
- require 'dragonfly/image_magick/processors/thumb'
1
+ require "dragonfly/image_magick/analysers/image_properties"
2
+ require "dragonfly/image_magick/generators/plain"
3
+ require "dragonfly/image_magick/generators/plasma"
4
+ require "dragonfly/image_magick/generators/text"
5
+ require "dragonfly/image_magick/processors/encode"
6
+ require "dragonfly/image_magick/processors/thumb"
7
+ require "dragonfly/image_magick/commands"
8
+ require "dragonfly/param_validators"
9
9
 
10
10
  module Dragonfly
11
11
  module ImageMagick
@@ -13,37 +13,36 @@ module Dragonfly
13
13
  # The ImageMagick Plugin registers an app with generators, analysers and processors.
14
14
  # Look at the source code for #call to see exactly how it configures the app.
15
15
  class Plugin
16
-
17
- def call(app, opts={})
16
+ def call(app, opts = {})
18
17
  # ENV
19
- app.env[:convert_command] = opts[:convert_command] || 'convert'
20
- app.env[:identify_command] = opts[:identify_command] || 'identify'
18
+ app.env[:convert_command] = opts[:convert_command] || "convert"
19
+ app.env[:identify_command] = opts[:identify_command] || "identify"
21
20
 
22
21
  # Analysers
23
22
  app.add_analyser :image_properties, ImageMagick::Analysers::ImageProperties.new
24
23
  app.add_analyser :width do |content|
25
- content.analyse(:image_properties)['width']
24
+ content.analyse(:image_properties)["width"]
26
25
  end
27
26
  app.add_analyser :height do |content|
28
- content.analyse(:image_properties)['height']
27
+ content.analyse(:image_properties)["height"]
29
28
  end
30
29
  app.add_analyser :format do |content|
31
- content.analyse(:image_properties)['format']
30
+ content.analyse(:image_properties)["format"]
32
31
  end
33
32
  app.add_analyser :aspect_ratio do |content|
34
33
  attrs = content.analyse(:image_properties)
35
- attrs['width'].to_f / attrs['height']
34
+ attrs["width"].to_f / attrs["height"]
36
35
  end
37
36
  app.add_analyser :portrait do |content|
38
37
  attrs = content.analyse(:image_properties)
39
- attrs['width'] <= attrs['height']
38
+ attrs["width"] <= attrs["height"]
40
39
  end
41
40
  app.add_analyser :landscape do |content|
42
41
  !content.analyse(:portrait)
43
42
  end
44
43
  app.add_analyser :image do |content|
45
44
  begin
46
- content.analyse(:image_properties)['format'] != 'pdf'
45
+ content.analyse(:image_properties)["format"] != "pdf"
47
46
  rescue Shell::CommandFailed
48
47
  false
49
48
  end
@@ -55,29 +54,31 @@ module Dragonfly
55
54
  app.define(:image?) { image }
56
55
 
57
56
  # Generators
58
- app.add_generator :convert, ImageMagick::Generators::Convert.new
59
57
  app.add_generator :plain, ImageMagick::Generators::Plain.new
60
58
  app.add_generator :plasma, ImageMagick::Generators::Plasma.new
61
59
  app.add_generator :text, ImageMagick::Generators::Text.new
60
+ app.add_generator :convert do
61
+ raise "The convert generator is deprecated for better security - use Dragonfly::ImageMagick::Commands.generate(content, args, format) instead."
62
+ end
62
63
 
63
64
  # Processors
64
- app.add_processor :convert, Processors::Convert.new
65
65
  app.add_processor :encode, Processors::Encode.new
66
66
  app.add_processor :thumb, Processors::Thumb.new
67
67
  app.add_processor :rotate do |content, amount|
68
- content.process!(:convert, "-rotate #{amount}")
68
+ ParamValidators.validate!(amount, &ParamValidators.is_number)
69
+ Commands.convert(content, "-rotate #{amount}")
70
+ end
71
+ app.add_processor :convert do
72
+ raise "The convert processor is deprecated for better security - use Dragonfly::ImageMagick::Commands.convert(content, args, opts) instead."
69
73
  end
70
74
 
71
75
  # Extra methods
72
- app.define :identify do |cli_args=nil|
76
+ app.define :identify do |cli_args = nil|
73
77
  shell_eval do |path|
74
78
  "#{app.env[:identify_command]} #{cli_args} #{path}"
75
79
  end
76
80
  end
77
-
78
81
  end
79
-
80
82
  end
81
83
  end
82
84
  end
83
-
@@ -1,18 +1,29 @@
1
+ require "dragonfly/image_magick/commands"
2
+
1
3
  module Dragonfly
2
4
  module ImageMagick
3
5
  module Processors
4
6
  class Encode
7
+ include ParamValidators
8
+
9
+ WHITELISTED_ARGS = %w(quality)
10
+
11
+ IS_IN_WHITELISTED_ARGS = ->(args_string) {
12
+ args_string.scan(/-\w+/).all? { |arg|
13
+ WHITELISTED_ARGS.include?(arg.sub("-", ""))
14
+ }
15
+ }
5
16
 
6
- def update_url(attrs, format, args="")
17
+ def update_url(attrs, format, args = "")
7
18
  attrs.ext = format.to_s
8
19
  end
9
20
 
10
- def call(content, format, args="")
11
- content.process!(:convert, args, 'format' => format)
21
+ def call(content, format, args = "")
22
+ validate!(format, &is_word)
23
+ validate!(args, &IS_IN_WHITELISTED_ARGS)
24
+ Commands.convert(content, args, "format" => format)
12
25
  end
13
-
14
26
  end
15
27
  end
16
28
  end
17
29
  end
18
-
@@ -1,32 +1,40 @@
1
+ require "dragonfly/image_magick/commands"
2
+
1
3
  module Dragonfly
2
4
  module ImageMagick
3
5
  module Processors
4
6
  class Thumb
7
+ include ParamValidators
5
8
 
6
9
  GRAVITIES = {
7
- 'nw' => 'NorthWest',
8
- 'n' => 'North',
9
- 'ne' => 'NorthEast',
10
- 'w' => 'West',
11
- 'c' => 'Center',
12
- 'e' => 'East',
13
- 'sw' => 'SouthWest',
14
- 's' => 'South',
15
- 'se' => 'SouthEast'
10
+ "nw" => "NorthWest",
11
+ "n" => "North",
12
+ "ne" => "NorthEast",
13
+ "w" => "West",
14
+ "c" => "Center",
15
+ "e" => "East",
16
+ "sw" => "SouthWest",
17
+ "s" => "South",
18
+ "se" => "SouthEast",
16
19
  }
17
20
 
18
21
  # Geometry string patterns
19
- RESIZE_GEOMETRY = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!'
22
+ RESIZE_GEOMETRY = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!'
20
23
  CROPPED_RESIZE_GEOMETRY = /\A(\d+)x(\d+)#(\w{1,2})?\z/ # e.g. '20x50#ne'
21
- CROP_GEOMETRY = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10'
24
+ CROP_GEOMETRY = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10'
22
25
 
23
- def update_url(url_attributes, geometry, opts={})
24
- format = opts['format']
26
+ def update_url(url_attributes, geometry, opts = {})
27
+ format = opts["format"]
25
28
  url_attributes.ext = format if format
26
29
  end
27
30
 
28
- def call(content, geometry, opts={})
29
- content.process!(:convert, args_for_geometry(geometry), opts)
31
+ def call(content, geometry, opts = {})
32
+ validate!(opts["format"], &is_word)
33
+ validate!(opts["frame"], &is_number)
34
+ Commands.convert(content, args_for_geometry(geometry), {
35
+ "format" => opts["format"],
36
+ "frame" => opts["frame"],
37
+ })
30
38
  end
31
39
 
32
40
  def args_for_geometry(geometry)
@@ -37,11 +45,11 @@ module Dragonfly
37
45
  resize_and_crop_args($1, $2, $3)
38
46
  when CROP_GEOMETRY
39
47
  crop_args(
40
- 'width' => $1,
41
- 'height' => $2,
42
- 'x' => $3,
43
- 'y' => $4,
44
- 'gravity' => $5
48
+ "width" => $1,
49
+ "height" => $2,
50
+ "x" => $3,
51
+ "y" => $4,
52
+ "gravity" => $5,
45
53
  )
46
54
  else raise ArgumentError, "Didn't recognise the geometry string #{geometry}"
47
55
  end
@@ -54,26 +62,24 @@ module Dragonfly
54
62
  end
55
63
 
56
64
  def crop_args(opts)
57
- raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts['x'] && opts['gravity']
65
+ raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts["x"] && opts["gravity"]
58
66
 
59
- width = opts['width']
60
- height = opts['height']
61
- gravity = GRAVITIES[opts['gravity']]
62
- x = "#{opts['x'] || 0}"
63
- x = '+' + x unless x[/\A[+-]/]
64
- y = "#{opts['y'] || 0}"
65
- y = '+' + y unless y[/\A[+-]/]
67
+ width = opts["width"]
68
+ height = opts["height"]
69
+ gravity = GRAVITIES[opts["gravity"]]
70
+ x = "#{opts["x"] || 0}"
71
+ x = "+" + x unless x[/\A[+-]/]
72
+ y = "#{opts["y"] || 0}"
73
+ y = "+" + y unless y[/\A[+-]/]
66
74
 
67
75
  "#{"-gravity #{gravity} " if gravity}-crop #{width}x#{height}#{x}#{y} +repage"
68
76
  end
69
77
 
70
78
  def resize_and_crop_args(width, height, gravity)
71
- gravity = GRAVITIES[gravity || 'c']
79
+ gravity = GRAVITIES[gravity || "c"]
72
80
  "-resize #{width}x#{height}^^ -gravity #{gravity} -crop #{width}x#{height}+0+0 +repage"
73
81
  end
74
-
75
82
  end
76
83
  end
77
84
  end
78
85
  end
79
-
@@ -91,7 +91,7 @@ module Dragonfly
91
91
  end
92
92
 
93
93
  def parse_url(url)
94
- URI.parse(url)
94
+ URI.parse(url.to_s)
95
95
  rescue URI::InvalidURIError
96
96
  begin
97
97
  encoded_uri = Addressable::URI.parse(url).normalize.to_s
@@ -30,7 +30,12 @@ module Dragonfly
30
30
 
31
31
  # Add callbacks
32
32
  before_save :save_dragonfly_attachments if respond_to?(:before_save)
33
- before_destroy :destroy_dragonfly_attachments if respond_to?(:before_destroy)
33
+ case
34
+ when respond_to?(:after_commit)
35
+ after_commit :destroy_dragonfly_attachments, on: :destroy
36
+ when respond_to?(:after_destroy)
37
+ after_destroy :destroy_dragonfly_attachments
38
+ end
34
39
 
35
40
  # Register the new attribute
36
41
  dragonfly_attachment_classes << new_dragonfly_attachment_class(attribute, app, config_block)
@@ -0,0 +1,37 @@
1
+ module Dragonfly
2
+ module ParamValidators
3
+ class InvalidParameter < RuntimeError; end
4
+
5
+ module_function
6
+
7
+ IS_NUMBER = ->(param) {
8
+ param.is_a?(Numeric) || /\A[\d\.]+\z/ === param
9
+ }
10
+
11
+ IS_WORD = ->(param) {
12
+ /\A\w+\z/ === param
13
+ }
14
+
15
+ IS_WORDS = ->(param) {
16
+ /\A[\w ]+\z/ === param
17
+ }
18
+
19
+ def is_number; IS_NUMBER; end
20
+ def is_word; IS_WORD; end
21
+ def is_words; IS_WORDS; end
22
+
23
+ def validate!(parameter, &validator)
24
+ return if parameter.nil?
25
+ raise InvalidParameter unless validator.(parameter)
26
+ end
27
+
28
+ def validate_all!(parameters, &validator)
29
+ parameters.each { |p| validate!(p, &validator) }
30
+ end
31
+
32
+ def validate_all_keys!(obj, keys, &validator)
33
+ parameters = keys.map { |key| obj[key] }
34
+ validate_all!(parameters, &validator)
35
+ end
36
+ end
37
+ end
@@ -1,4 +1,4 @@
1
- require 'uri'
1
+ require 'cgi'
2
2
  require 'rack'
3
3
 
4
4
  module Dragonfly
@@ -99,7 +99,7 @@ module Dragonfly
99
99
 
100
100
  def filename_string
101
101
  return unless job.name
102
- filename = request_from_msie? ? URI.encode(job.name) : job.name
102
+ filename = request_from_msie? ? CGI.escape(job.name) : job.name
103
103
  %(filename="#{filename}")
104
104
  end
105
105
 
@@ -15,14 +15,11 @@ module Dragonfly
15
15
  end
16
16
 
17
17
  def escape_args(args)
18
- args.shellsplit.map do |arg|
19
- quote arg.gsub(/\\?'/, %q('\\\\''))
20
- end.join(' ')
18
+ args.shellsplit.map{|arg| escape(arg) }.join(' ')
21
19
  end
22
20
 
23
- def quote(string)
24
- q = Dragonfly.running_on_windows? ? '"' : "'"
25
- q + string + q
21
+ def escape(string)
22
+ Shellwords.escape(string)
26
23
  end
27
24
 
28
25
  private
@@ -36,22 +33,31 @@ module Dragonfly
36
33
  def run_command(command)
37
34
  result = `#{command}`
38
35
  status = $?
39
- raise CommandFailed, "Command failed (#{command}) with exit status #{status.exitstatus}" unless status.success?
36
+ raise_command_failed!(command, status.exitstatus) unless status.success?
40
37
  result
38
+ rescue Errno::ENOENT => e
39
+ raise_command_failed!(command, nil, e.message)
41
40
  end
42
41
 
43
42
  else
44
43
 
45
44
  def run_command(command)
46
- Open3.popen3 command do |stdin, stdout, stderr, wait_thread|
47
- stdin.close_write # make sure it doesn't hang
48
- status = wait_thread.value
49
- raise CommandFailed, "Command failed (#{command}) with exit status #{status.exitstatus} and stderr #{stderr.read}" unless status.success?
50
- stdout.read
51
- end
45
+ stdout_str, stderr_str, status = Open3.capture3(command)
46
+ raise_command_failed!(command, status.exitstatus, stderr_str) unless status.success?
47
+ stdout_str
48
+ rescue Errno::ENOENT => e
49
+ raise_command_failed!(command, nil, e.message)
52
50
  end
53
51
 
54
52
  end
55
53
 
54
+ def raise_command_failed!(command, status=nil, error=nil)
55
+ raise CommandFailed, [
56
+ "Command failed: #{command}",
57
+ ("exit status: #{status}" if status),
58
+ ("error: #{error}" if error),
59
+ ].join(', ')
60
+ end
61
+
56
62
  end
57
63
  end
@@ -38,7 +38,7 @@ module Dragonfly
38
38
  end
39
39
 
40
40
  def uri_unescape(string)
41
- URI.unescape(string)
41
+ URI::DEFAULT_PARSER.unescape(string)
42
42
  end
43
43
 
44
44
  end
@@ -1,3 +1,3 @@
1
1
  module Dragonfly
2
- VERSION = '1.1.4'
2
+ VERSION = "1.4.0"
3
3
  end
@@ -232,7 +232,7 @@ describe Dragonfly::Content do
232
232
  path = p
233
233
  "cat #{path}"
234
234
  end.should == "big\nstuff"
235
- path.should == app.shell.quote(content.path)
235
+ path.should == app.shell.escape(content.path)
236
236
  end
237
237
 
238
238
  it "allows evaluating without escaping" do
@@ -253,8 +253,8 @@ describe Dragonfly::Content do
253
253
  new_path = n
254
254
  "cp #{o} #{n}"
255
255
  end.should == content
256
- old_path.should == app.shell.quote(original_path)
257
- new_path.should == app.shell.quote(content.path)
256
+ old_path.should == app.shell.escape(original_path)
257
+ new_path.should == app.shell.escape(content.path)
258
258
  content.data.should == "big\nstuff"
259
259
  end
260
260
 
@@ -7,7 +7,7 @@ describe Dragonfly::CookieMonster do
7
7
  def app(extra_env={})
8
8
  Rack::Builder.new do
9
9
  use Dragonfly::CookieMonster
10
- run proc{|env| env.merge!(extra_env); [200, {"Set-Cookie" => "blah", "Something" => "else"}, ["body here"]] }
10
+ run proc{|env| env.merge!(extra_env); [200, {"Set-Cookie" => "blah=thing", "Something" => "else"}, ["body here"]] }
11
11
  end
12
12
  end
13
13
 
@@ -15,7 +15,7 @@ describe Dragonfly::CookieMonster do
15
15
  response = Rack::MockRequest.new(app).get('')
16
16
  response.status.should == 200
17
17
  response.body.should == "body here"
18
- response.headers["Set-Cookie"].should == "blah"
18
+ response.headers["Set-Cookie"].should == "blah=thing"
19
19
  response.headers["Something"].should == "else"
20
20
  end
21
21
 
@@ -0,0 +1,98 @@
1
+ require "spec_helper"
2
+ require "dragonfly/image_magick/commands"
3
+
4
+ describe Dragonfly::ImageMagick::Commands do
5
+ include Dragonfly::ImageMagick::Commands
6
+
7
+ let(:app) { test_app }
8
+
9
+ def sample_content(name)
10
+ Dragonfly::Content.new(app, SAMPLES_DIR.join(name))
11
+ end
12
+
13
+ describe "convert" do
14
+ let(:image) { sample_content("beach.png") } # 280x355
15
+
16
+ it "should allow for general convert commands" do
17
+ convert(image, "-scale 56x71")
18
+ image.should have_width(56)
19
+ image.should have_height(71)
20
+ end
21
+
22
+ it "should allow for general convert commands with added format" do
23
+ convert(image, "-scale 56x71", "format" => "gif")
24
+ image.should have_width(56)
25
+ image.should have_height(71)
26
+ image.should have_format("gif")
27
+ image.meta["format"].should == "gif"
28
+ end
29
+
30
+ it "should work for commands with parenthesis" do
31
+ convert(image, "\\( +clone -sparse-color Barycentric '0,0 black 0,%[fx:h-1] white' -function polynomial 2,-2,0.5 \\) -compose Blur -set option:compose:args 15 -composite")
32
+ image.should have_width(280)
33
+ end
34
+
35
+ it "should work for files with spaces/apostrophes in the name" do
36
+ image = Dragonfly::Content.new(app, SAMPLES_DIR.join("mevs' white pixel.png"))
37
+ convert(image, "-resize 2x2!")
38
+ image.should have_width(2)
39
+ end
40
+
41
+ it "allows converting specific frames" do
42
+ gif = sample_content("gif.gif")
43
+ convert(gif, "-resize 50x50")
44
+ all_frames_size = gif.size
45
+
46
+ gif = sample_content("gif.gif")
47
+ convert(gif, "-resize 50x50", "frame" => 0)
48
+ one_frame_size = gif.size
49
+
50
+ one_frame_size.should < all_frames_size
51
+ end
52
+
53
+ it "accepts input arguments for convert commands" do
54
+ image2 = image.clone
55
+ convert(image, "")
56
+ convert(image2, "", "input_args" => "-extract 50x50+10+10")
57
+
58
+ image.should_not equal_image(image2)
59
+ image2.should have_width(50)
60
+ end
61
+
62
+ it "allows converting using specific delegates" do
63
+ expect {
64
+ convert(image, "", "format" => "jpg", "delegate" => "png")
65
+ }.to call_command(app.shell, %r{convert png:/[^']+?/beach\.png /[^']+?\.jpg})
66
+ end
67
+
68
+ it "maintains the mime_type meta if it exists already" do
69
+ convert(image, "-resize 10x")
70
+ image.meta["mime_type"].should be_nil
71
+
72
+ image.add_meta("mime_type" => "image/png")
73
+ convert(image, "-resize 5x")
74
+ image.meta["mime_type"].should == "image/png"
75
+ image.mime_type.should == "image/png" # sanity check
76
+ end
77
+
78
+ it "doesn't maintain the mime_type meta on format change" do
79
+ image.add_meta("mime_type" => "image/png")
80
+ convert(image, "", "format" => "gif")
81
+ image.meta["mime_type"].should be_nil
82
+ image.mime_type.should == "image/gif" # sanity check
83
+ end
84
+ end
85
+
86
+ describe "generate" do
87
+ let (:image) { Dragonfly::Content.new(app) }
88
+
89
+ before(:each) do
90
+ generate(image, "-size 1x1 xc:white", "png")
91
+ end
92
+
93
+ it { image.should have_width(1) }
94
+ it { image.should have_height(1) }
95
+ it { image.should have_format("png") }
96
+ it { image.meta.should == { "format" => "png" } }
97
+ end
98
+ end