VYPR
Critical severityNVD Advisory· Published Jun 2, 2022· Updated Aug 3, 2024

CVE-2021-33473

CVE-2021-33473

Description

An argument injection vulnerability in Dragonfly Ruby Gem v1.3.0 allows attackers to read and write arbitrary files when the verify_url option is disabled. This vulnerability is exploited via a crafted URL.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Argument injection in Dragonfly gem v1.3.0 allows arbitrary file read/write when verify_urls is disabled via crafted URL.

Vulnerability

An argument injection vulnerability exists in Dragonfly Ruby Gem v1.3.0 when the verify_urls option is disabled [1][2]. The bug allows an attacker to inject additional arguments into commands executed by the gem by providing a specially crafted URL [1]. This affects any application using Dragonfly with verify_urls set to false [2].

Exploitation

An attacker needs network access to a service that uses Dragonfly with verify_urls disabled [1]. The attacker sends a crafted URL containing malicious arguments that are passed to underlying system commands [2]. No authentication is required if the endpoint is publicly accessible [1].

Impact

Successful exploitation allows an attacker to read and write arbitrary files on the server [1][2]. This could lead to disclosure of sensitive information, modification of application data, or potentially remote code execution depending on the file written [1].

Mitigation

Upgrade to Dragonfly gem version 1.4.0 or later, which disables the vulnerable verify_urls option by default and includes additional security hardening [3][4]. If upgrading is not immediately possible, ensure verify_urls is enabled (set to true) in the configuration [2]. A fix was committed in commit 25399297bb457f7fcf8e3f91e85945b255b111b5 [3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
dragonflyRubyGems
< 1.4.01.4.0

Affected products

2

Patches

1
25399297bb45

Merge branch 'better-security'

https://github.com/markevans/dragonflyMark EvansMay 19, 2021via ghsa
23 files changed · +683 448
  • lib/dragonfly/content.rb+17 18 modified
    @@ -1,8 +1,8 @@
    -require 'base64'
    -require 'forwardable'
    -require 'dragonfly/has_filename'
    -require 'dragonfly/temp_object'
    -require 'dragonfly/utils'
    +require "base64"
    +require "forwardable"
    +require "dragonfly/has_filename"
    +require "dragonfly/temp_object"
    +require "dragonfly/utils"
     
     module Dragonfly
     
    @@ -16,11 +16,10 @@ module Dragonfly
       # It is acted upon in generator, processor, analyser and datastore methods and provides a standard interface for updating content,
       # no matter how that content first got there (whether in the form of a String/Pathname/File/etc.)
       class Content
    -
         include HasFilename
         extend Forwardable
     
    -    def initialize(app, obj="", meta=nil)
    +    def initialize(app, obj = "", meta = nil)
           @app = app
           @meta = {}
           @previous_temp_objects = []
    @@ -79,7 +78,7 @@ def name=(name)
         # @example "image/jpeg"
         # @return [String]
         def mime_type
    -      meta['mime_type'] || app.mime_type_for(ext)
    +      meta["mime_type"] || app.mime_type_for(ext)
         end
     
         # Set the content using a pre-registered generator
    @@ -93,7 +92,7 @@ def generate!(name, *args)
     
         # Update the content using a pre-registered processor
         # @example
    -    #   content.process!(:convert, "-resize 300x300")
    +    #   content.process!(:thumb, "300x300")
         # @return [Content] self
         def process!(name, *args)
           app.get_processor(name).call(self, *args)
    @@ -111,10 +110,10 @@ def analyse(name)
         # @param obj [String, Pathname, Tempfile, File, Content, TempObject] can be any of these types
         # @param meta [Hash] - should be json-like, i.e. contain no types other than String, Number, Boolean
         # @return [Content] self
    -    def update(obj, meta=nil)
    +    def update(obj, meta = nil)
           meta ||= {}
    -      self.temp_object = TempObject.new(obj, meta['name'])
    -      self.meta['name'] ||= temp_object.name if temp_object.name
    +      self.temp_object = TempObject.new(obj, meta["name"])
    +      self.meta["name"] ||= temp_object.name if temp_object.name
           clear_analyser_cache
           add_meta(obj.meta) if obj.respond_to?(:meta)
           add_meta(meta)
    @@ -135,7 +134,7 @@ def add_meta(meta)
         #     "file --mime-type #{path}"
         #   end
         #   # ===> "beach.jpg: image/jpeg"
    -    def shell_eval(opts={})
    +    def shell_eval(opts = {})
           should_escape = opts[:escape] != false
           command = yield(should_escape ? shell.escape(path) : path)
           run command, :escape => should_escape
    @@ -148,7 +147,7 @@ def shell_eval(opts={})
         #     "/usr/local/bin/generate_text gumfry -o #{path}"
         #   end
         # @return [Content] self
    -    def shell_generate(opts={})
    +    def shell_generate(opts = {})
           ext = opts[:ext] || self.ext
           should_escape = opts[:escape] != false
           tempfile = Utils.new_tempfile(ext)
    @@ -165,7 +164,7 @@ def shell_generate(opts={})
         #     "convert -resize 20x10 #{old_path} #{new_path}"
         #   end
         # @return [Content] self
    -    def shell_update(opts={})
    +    def shell_update(opts = {})
           ext = opts[:ext] || self.ext
           should_escape = opts[:escape] != false
           tempfile = Utils.new_tempfile(ext)
    @@ -176,7 +175,7 @@ def shell_update(opts={})
           update(tempfile)
         end
     
    -    def store(opts={})
    +    def store(opts = {})
           datastore.write(self, opts)
         end
     
    @@ -188,7 +187,7 @@ def b64_data
         end
     
         def close
    -      previous_temp_objects.each{|temp_object| temp_object.close }
    +      previous_temp_objects.each { |temp_object| temp_object.close }
           temp_object.close
         end
     
    @@ -199,6 +198,7 @@ def inspect
         private
     
         attr_reader :previous_temp_objects
    +
         def temp_object=(temp_object)
           previous_temp_objects.push(@temp_object) if @temp_object
           @temp_object = temp_object
    @@ -215,6 +215,5 @@ def clear_analyser_cache
         def run(command, opts)
           shell.run(command, opts)
         end
    -
       end
     end
    
  • lib/dragonfly/image_magick/commands.rb+35 0 added
    @@ -0,0 +1,35 @@
    +module Dragonfly
    +  module ImageMagick
    +    module Commands
    +      module_function
    +
    +      def convert(content, args = "", opts = {})
    +        convert_command = content.env[:convert_command] || "convert"
    +        format = opts["format"]
    +
    +        input_args = opts["input_args"] if opts["input_args"]
    +        delegate_string = "#{opts["delegate"]}:" if opts["delegate"]
    +        frame_string = "[#{opts["frame"]}]" if opts["frame"]
    +
    +        content.shell_update :ext => format do |old_path, new_path|
    +          "#{convert_command} #{input_args} #{delegate_string}#{old_path}#{frame_string} #{args} #{new_path}"
    +        end
    +
    +        if format
    +          content.meta["format"] = format.to_s
    +          content.ext = format
    +          content.meta["mime_type"] = nil # don't need it as we have ext now
    +        end
    +      end
    +
    +      def generate(content, args, format)
    +        format = format.to_s
    +        convert_command = content.env[:convert_command] || "convert"
    +        content.shell_generate :ext => format do |path|
    +          "#{convert_command} #{args} #{path}"
    +        end
    +        content.add_meta("format" => format)
    +      end
    +    end
    +  end
    +end
    
  • lib/dragonfly/image_magick/generators/convert.rb+0 19 removed
    @@ -1,19 +0,0 @@
    -module Dragonfly
    -  module ImageMagick
    -    module Generators
    -      class Convert
    -
    -        def call(content, args, format)
    -          format = format.to_s
    -          convert_command = content.env[:convert_command] || 'convert'
    -          content.shell_generate :ext => format do |path|
    -            "#{convert_command} #{args} #{path}"
    -          end
    -          content.add_meta('format' => format)
    -        end
    -
    -      end
    -    end
    -  end
    -end
    -
    
  • lib/dragonfly/image_magick/generators/plain.rb+13 7 modified
    @@ -1,25 +1,31 @@
    +require "dragonfly/image_magick/commands"
    +require "dragonfly/param_validators"
    +
     module Dragonfly
       module ImageMagick
         module Generators
           class Plain
    +        include ParamValidators
     
    -        def call(content, width, height, opts={})
    +        def call(content, width, height, opts = {})
    +          validate_all!([width, height], &is_number)
    +          validate_all_keys!(opts, %w(colour color format), &is_word)
               format = extract_format(opts)
    -          colour = opts['colour'] || opts['color'] || 'white'
    -          content.generate!(:convert, "-size #{width}x#{height} xc:#{colour}", format)
    -          content.add_meta('format' => format, 'name' => "plain.#{format}")
    +
    +          colour = opts["colour"] || opts["color"] || "white"
    +          Commands.generate(content, "-size #{width}x#{height} xc:#{colour}", format)
    +          content.add_meta("format" => format, "name" => "plain.#{format}")
             end
     
    -        def update_url(url_attributes, width, height, opts={})
    +        def update_url(url_attributes, width, height, opts = {})
               url_attributes.name = "plain.#{extract_format(opts)}"
             end
     
             private
     
             def extract_format(opts)
    -          opts['format'] || 'png'
    +          opts["format"] || "png"
             end
    -
           end
         end
       end
    
  • lib/dragonfly/image_magick/generators/plasma.rb+10 6 modified
    @@ -1,24 +1,28 @@
    +require "dragonfly/image_magick/commands"
    +
     module Dragonfly
       module ImageMagick
         module Generators
           class Plasma
    +        include ParamValidators
     
    -        def call(content, width, height, opts={})
    +        def call(content, width, height, opts = {})
    +          validate_all!([width, height], &is_number)
    +          validate!(opts["format"], &is_word)
               format = extract_format(opts)
    -          content.generate!(:convert, "-size #{width}x#{height} plasma:fractal", format)
    -          content.add_meta('format' => format, 'name' => "plasma.#{format}")
    +          Commands.generate(content, "-size #{width}x#{height} plasma:fractal", format)
    +          content.add_meta("format" => format, "name" => "plasma.#{format}")
             end
     
    -        def update_url(url_attributes, width, height, opts={})
    +        def update_url(url_attributes, width, height, opts = {})
               url_attributes.name = "plasma.#{extract_format(opts)}"
             end
     
             private
     
             def extract_format(opts)
    -          opts['format'] || 'png'
    +          opts["format"] || "png"
             end
    -
           end
         end
       end
    
  • lib/dragonfly/image_magick/generators/text.rb+67 58 modified
    @@ -1,100 +1,111 @@
    -require 'dragonfly/hash_with_css_style_keys'
    +require "dragonfly/hash_with_css_style_keys"
    +require "dragonfly/image_magick/commands"
    +require "dragonfly/param_validators"
     
     module Dragonfly
       module ImageMagick
         module Generators
           class Text
    +        include ParamValidators
     
             FONT_STYLES = {
    -          'normal'  => 'normal',
    -          'italic'  => 'italic',
    -          'oblique' => 'oblique'
    +          "normal" => "normal",
    +          "italic" => "italic",
    +          "oblique" => "oblique",
             }
     
             FONT_STRETCHES = {
    -          'normal'          => 'normal',
    -          'semi-condensed'  => 'semi-condensed',
    -          'condensed'       => 'condensed',
    -          'extra-condensed' => 'extra-condensed',
    -          'ultra-condensed' => 'ultra-condensed',
    -          'semi-expanded'   => 'semi-expanded',
    -          'expanded'        => 'expanded',
    -          'extra-expanded'  => 'extra-expanded',
    -          'ultra-expanded'  => 'ultra-expanded'
    +          "normal" => "normal",
    +          "semi-condensed" => "semi-condensed",
    +          "condensed" => "condensed",
    +          "extra-condensed" => "extra-condensed",
    +          "ultra-condensed" => "ultra-condensed",
    +          "semi-expanded" => "semi-expanded",
    +          "expanded" => "expanded",
    +          "extra-expanded" => "extra-expanded",
    +          "ultra-expanded" => "ultra-expanded",
             }
     
             FONT_WEIGHTS = {
    -          'normal'  => 'normal',
    -          'bold'    => 'bold',
    -          'bolder'  => 'bolder',
    -          'lighter' => 'lighter',
    -          '100'     => 100,
    -          '200'     => 200,
    -          '300'     => 300,
    -          '400'     => 400,
    -          '500'     => 500,
    -          '600'     => 600,
    -          '700'     => 700,
    -          '800'     => 800,
    -          '900'     => 900
    +          "normal" => "normal",
    +          "bold" => "bold",
    +          "bolder" => "bolder",
    +          "lighter" => "lighter",
    +          "100" => 100,
    +          "200" => 200,
    +          "300" => 300,
    +          "400" => 400,
    +          "500" => 500,
    +          "600" => 600,
    +          "700" => 700,
    +          "800" => 800,
    +          "900" => 900,
             }
     
    -        def update_url(url_attributes, string, opts={})
    +        IS_COLOUR = ->(param) {
    +          /\A(#\w+|rgba?\([\d\.,]+\)|\w+)\z/ === param
    +        }
    +
    +        def update_url(url_attributes, string, opts = {})
               url_attributes.name = "text.#{extract_format(opts)}"
             end
     
    -        def call(content, string, opts={})
    +        def call(content, string, opts = {})
    +          validate_all_keys!(opts, %w(font font_family), &is_words)
    +          validate_all_keys!(opts, %w(color background_color stroke_color), &IS_COLOUR)
    +          validate!(opts["format"], &is_word)
    +
               opts = HashWithCssStyleKeys[opts]
               args = []
               format = extract_format(opts)
    -          background = opts['background_color'] || 'none'
    -          font_size = (opts['font_size'] || 12).to_i
    +          background = opts["background_color"] || "none"
    +          font_size = (opts["font_size"] || 12).to_i
    +          font_family = opts["font_family"] || opts["font"]
               escaped_string = "\"#{string.gsub(/"/, '\"')}\""
     
               # Settings
               args.push("-gravity NorthWest")
               args.push("-antialias")
               args.push("-pointsize #{font_size}")
    -          args.push("-font \"#{opts['font']}\"") if opts['font']
    -          args.push("-family '#{opts['font_family']}'") if opts['font_family']
    -          args.push("-fill #{opts['color']}") if opts['color']
    -          args.push("-stroke #{opts['stroke_color']}") if opts['stroke_color']
    -          args.push("-style #{FONT_STYLES[opts['font_style']]}") if opts['font_style']
    -          args.push("-stretch #{FONT_STRETCHES[opts['font_stretch']]}") if opts['font_stretch']
    -          args.push("-weight #{FONT_WEIGHTS[opts['font_weight']]}") if opts['font_weight']
    +          args.push("-family '#{font_family}'") if font_family
    +          args.push("-fill #{opts["color"]}") if opts["color"]
    +          args.push("-stroke #{opts["stroke_color"]}") if opts["stroke_color"]
    +          args.push("-style #{FONT_STYLES[opts["font_style"]]}") if opts["font_style"]
    +          args.push("-stretch #{FONT_STRETCHES[opts["font_stretch"]]}") if opts["font_stretch"]
    +          args.push("-weight #{FONT_WEIGHTS[opts["font_weight"]]}") if opts["font_weight"]
               args.push("-background #{background}")
               args.push("label:#{escaped_string}")
     
               # Padding
    -          pt, pr, pb, pl = parse_padding_string(opts['padding']) if opts['padding']
    -          padding_top    = (opts['padding_top']    || pt || 0)
    -          padding_right  = (opts['padding_right']  || pr || 0)
    -          padding_bottom = (opts['padding_bottom'] || pb || 0)
    -          padding_left   = (opts['padding_left']   || pl || 0)
    +          pt, pr, pb, pl = parse_padding_string(opts["padding"]) if opts["padding"]
    +          padding_top = (opts["padding_top"] || pt).to_i
    +          padding_right = (opts["padding_right"] || pr).to_i
    +          padding_bottom = (opts["padding_bottom"] || pb).to_i
    +          padding_left = (opts["padding_left"] || pl).to_i
     
    -          content.generate!(:convert, args.join(' '), format)
    +          Commands.generate(content, args.join(" "), format)
     
               if (padding_top || padding_right || padding_bottom || padding_left)
                 dimensions = content.analyse(:image_properties)
    -            text_width  = dimensions['width']
    -            text_height = dimensions['height']
    -            width  = padding_left + text_width  + padding_right
    -            height = padding_top  + text_height + padding_bottom
    +            text_width = dimensions["width"]
    +            text_height = dimensions["height"]
    +            width = padding_left + text_width + padding_right
    +            height = padding_top + text_height + padding_bottom
     
                 args = args.slice(0, args.length - 2)
                 args.push("-size #{width}x#{height}")
                 args.push("xc:#{background}")
                 args.push("-annotate 0x0+#{padding_left}+#{padding_top} #{escaped_string}")
    -            content.generate!(:convert, args.join(' '), format)
    +            Commands.generate(content, args.join(" "), format)
               end
     
    -          content.add_meta('format' => format, 'name' => "text.#{format}")
    +          content.add_meta("format" => format, "name" => "text.#{format}")
             end
     
             private
     
             def extract_format(opts)
    -          opts['format'] || 'png'
    +          opts["format"] || "png"
             end
     
             # Use css-style padding declaration, i.e.
    @@ -103,25 +114,23 @@ def extract_format(opts)
             # 10 5 10   (top, left/right, bottom)
             # 10 5 10 5 (top, right, bottom, left)
             def parse_padding_string(str)
    -          padding_parts = str.gsub('px','').split(/\s+/).map{|px| px.to_i}
    +          padding_parts = str.gsub("px", "").split(/\s+/).map { |px| px.to_i }
               case padding_parts.size
               when 1
                 p = padding_parts.first
    -            [p,p,p,p]
    +            [p, p, p, p]
               when 2
    -            p,q = padding_parts
    -            [p,q,p,q]
    +            p, q = padding_parts
    +            [p, q, p, q]
               when 3
    -            p,q,r = padding_parts
    -            [p,q,r,q]
    +            p, q, r = padding_parts
    +            [p, q, r, q]
               when 4
                 padding_parts
               else raise ArgumentError, "Couldn't parse padding string '#{str}' - should be a css-style string"
               end
             end
           end
    -
         end
       end
     end
    -
    
  • lib/dragonfly/image_magick/plugin.rb+20 25 modified
    @@ -1,49 +1,48 @@
    -require 'dragonfly/image_magick/analysers/image_properties'
    -require 'dragonfly/image_magick/generators/convert'
    -require 'dragonfly/image_magick/generators/plain'
    -require 'dragonfly/image_magick/generators/plasma'
    -require 'dragonfly/image_magick/generators/text'
    -require 'dragonfly/image_magick/processors/convert'
    -require 'dragonfly/image_magick/processors/encode'
    -require 'dragonfly/image_magick/processors/thumb'
    +require "dragonfly/image_magick/analysers/image_properties"
    +require "dragonfly/image_magick/generators/plain"
    +require "dragonfly/image_magick/generators/plasma"
    +require "dragonfly/image_magick/generators/text"
    +require "dragonfly/image_magick/processors/encode"
    +require "dragonfly/image_magick/processors/thumb"
    +require "dragonfly/image_magick/commands"
    +require "dragonfly/param_validators"
     
     module Dragonfly
       module ImageMagick
     
         # The ImageMagick Plugin registers an app with generators, analysers and processors.
         # Look at the source code for #call to see exactly how it configures the app.
         class Plugin
    -
    -      def call(app, opts={})
    +      def call(app, opts = {})
             # ENV
    -        app.env[:convert_command] = opts[:convert_command] || 'convert'
    -        app.env[:identify_command] = opts[:identify_command] || 'identify'
    +        app.env[:convert_command] = opts[:convert_command] || "convert"
    +        app.env[:identify_command] = opts[:identify_command] || "identify"
     
             # Analysers
             app.add_analyser :image_properties, ImageMagick::Analysers::ImageProperties.new
             app.add_analyser :width do |content|
    -          content.analyse(:image_properties)['width']
    +          content.analyse(:image_properties)["width"]
             end
             app.add_analyser :height do |content|
    -          content.analyse(:image_properties)['height']
    +          content.analyse(:image_properties)["height"]
             end
             app.add_analyser :format do |content|
    -          content.analyse(:image_properties)['format']
    +          content.analyse(:image_properties)["format"]
             end
             app.add_analyser :aspect_ratio do |content|
               attrs = content.analyse(:image_properties)
    -          attrs['width'].to_f / attrs['height']
    +          attrs["width"].to_f / attrs["height"]
             end
             app.add_analyser :portrait do |content|
               attrs = content.analyse(:image_properties)
    -          attrs['width'] <= attrs['height']
    +          attrs["width"] <= attrs["height"]
             end
             app.add_analyser :landscape do |content|
               !content.analyse(:portrait)
             end
             app.add_analyser :image do |content|
               begin
    -            content.analyse(:image_properties)['format'] != 'pdf'
    +            content.analyse(:image_properties)["format"] != "pdf"
               rescue Shell::CommandFailed
                 false
               end
    @@ -55,29 +54,25 @@ def call(app, opts={})
             app.define(:image?) { image }
     
             # Generators
    -        app.add_generator :convert, ImageMagick::Generators::Convert.new
             app.add_generator :plain, ImageMagick::Generators::Plain.new
             app.add_generator :plasma, ImageMagick::Generators::Plasma.new
             app.add_generator :text, ImageMagick::Generators::Text.new
     
             # Processors
    -        app.add_processor :convert, Processors::Convert.new
             app.add_processor :encode, Processors::Encode.new
             app.add_processor :thumb, Processors::Thumb.new
             app.add_processor :rotate do |content, amount|
    -          content.process!(:convert, "-rotate #{amount}")
    +          ParamValidators.validate!(amount, &ParamValidators.is_number)
    +          Commands.convert(content, "-rotate #{amount}")
             end
     
             # Extra methods
    -        app.define :identify do |cli_args=nil|
    +        app.define :identify do |cli_args = nil|
               shell_eval do |path|
                 "#{app.env[:identify_command]} #{cli_args} #{path}"
               end
             end
    -
           end
    -
         end
       end
     end
    -
    
  • lib/dragonfly/image_magick/processors/convert.rb+0 33 removed
    @@ -1,33 +0,0 @@
    -module Dragonfly
    -  module ImageMagick
    -    module Processors
    -      class Convert
    -
    -        def call(content, args='', opts={})
    -          convert_command = content.env[:convert_command] || 'convert'
    -          format = opts['format']
    -
    -          input_args = opts['input_args'] if opts['input_args']
    -          delegate_string = "#{opts['delegate']}:" if opts['delegate']
    -          frame_string = "[#{opts['frame']}]" if opts['frame']
    -
    -          content.shell_update :ext => format do |old_path, new_path|
    -            "#{convert_command} #{input_args} #{delegate_string}#{old_path}#{frame_string} #{args} #{new_path}"
    -          end
    -
    -          if format
    -            content.meta['format'] = format.to_s
    -            content.ext = format
    -            content.meta['mime_type'] = nil # don't need it as we have ext now
    -          end
    -        end
    -
    -        def update_url(attrs, args='', opts={})
    -          format = opts['format']
    -          attrs.ext = format if format
    -        end
    -
    -      end
    -    end
    -  end
    -end
    
  • lib/dragonfly/image_magick/processors/encode.rb+16 5 modified
    @@ -1,18 +1,29 @@
    +require "dragonfly/image_magick/commands"
    +
     module Dragonfly
       module ImageMagick
         module Processors
           class Encode
    +        include ParamValidators
    +
    +        WHITELISTED_ARGS = %w(quality)
    +
    +        IS_IN_WHITELISTED_ARGS = ->(args_string) {
    +          args_string.scan(/-\w+/).all? { |arg|
    +            WHITELISTED_ARGS.include?(arg.sub("-", ""))
    +          }
    +        }
     
    -        def update_url(attrs, format, args="")
    +        def update_url(attrs, format, args = "")
               attrs.ext = format.to_s
             end
     
    -        def call(content, format, args="")
    -          content.process!(:convert, args, 'format' => format)
    +        def call(content, format, args = "")
    +          validate!(format, &is_word)
    +          validate!(args, &IS_IN_WHITELISTED_ARGS)
    +          Commands.convert(content, args, "format" => format)
             end
    -
           end
         end
       end
     end
    -
    
  • lib/dragonfly/image_magick/processors/thumb.rb+37 31 modified
    @@ -1,32 +1,40 @@
    +require "dragonfly/image_magick/commands"
    +
     module Dragonfly
       module ImageMagick
         module Processors
           class Thumb
    +        include ParamValidators
     
             GRAVITIES = {
    -          'nw' => 'NorthWest',
    -          'n'  => 'North',
    -          'ne' => 'NorthEast',
    -          'w'  => 'West',
    -          'c'  => 'Center',
    -          'e'  => 'East',
    -          'sw' => 'SouthWest',
    -          's'  => 'South',
    -          'se' => 'SouthEast'
    +          "nw" => "NorthWest",
    +          "n" => "North",
    +          "ne" => "NorthEast",
    +          "w" => "West",
    +          "c" => "Center",
    +          "e" => "East",
    +          "sw" => "SouthWest",
    +          "s" => "South",
    +          "se" => "SouthEast",
             }
     
             # Geometry string patterns
    -        RESIZE_GEOMETRY         = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!'
    +        RESIZE_GEOMETRY = /\A\d*x\d*[><%^!]?\z|\A\d+@\z/ # e.g. '300x200!'
             CROPPED_RESIZE_GEOMETRY = /\A(\d+)x(\d+)#(\w{1,2})?\z/ # e.g. '20x50#ne'
    -        CROP_GEOMETRY           = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10'
    +        CROP_GEOMETRY = /\A(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?\z/ # e.g. '30x30+10+10'
     
    -        def update_url(url_attributes, geometry, opts={})
    -          format = opts['format']
    +        def update_url(url_attributes, geometry, opts = {})
    +          format = opts["format"]
               url_attributes.ext = format if format
             end
     
    -        def call(content, geometry, opts={})
    -          content.process!(:convert, args_for_geometry(geometry), opts)
    +        def call(content, geometry, opts = {})
    +          validate!(opts["format"], &is_word)
    +          validate!(opts["frame"], &is_number)
    +          Commands.convert(content, args_for_geometry(geometry), {
    +            "format" => opts["format"],
    +            "frame" => opts["frame"],
    +          })
             end
     
             def args_for_geometry(geometry)
    @@ -37,11 +45,11 @@ def args_for_geometry(geometry)
                 resize_and_crop_args($1, $2, $3)
               when CROP_GEOMETRY
                 crop_args(
    -              'width' => $1,
    -              'height' => $2,
    -              'x' => $3,
    -              'y' => $4,
    -              'gravity' => $5
    +              "width" => $1,
    +              "height" => $2,
    +              "x" => $3,
    +              "y" => $4,
    +              "gravity" => $5,
                 )
               else raise ArgumentError, "Didn't recognise the geometry string #{geometry}"
               end
    @@ -54,26 +62,24 @@ def resize_args(geometry)
             end
     
             def crop_args(opts)
    -          raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts['x'] && opts['gravity']
    +          raise ArgumentError, "you can't give a crop offset and gravity at the same time" if opts["x"] && opts["gravity"]
     
    -          width   = opts['width']
    -          height  = opts['height']
    -          gravity = GRAVITIES[opts['gravity']]
    -          x       = "#{opts['x'] || 0}"
    -          x = '+' + x unless x[/\A[+-]/]
    -          y       = "#{opts['y'] || 0}"
    -          y = '+' + y unless y[/\A[+-]/]
    +          width = opts["width"]
    +          height = opts["height"]
    +          gravity = GRAVITIES[opts["gravity"]]
    +          x = "#{opts["x"] || 0}"
    +          x = "+" + x unless x[/\A[+-]/]
    +          y = "#{opts["y"] || 0}"
    +          y = "+" + y unless y[/\A[+-]/]
     
               "#{"-gravity #{gravity} " if gravity}-crop #{width}x#{height}#{x}#{y} +repage"
             end
     
             def resize_and_crop_args(width, height, gravity)
    -          gravity = GRAVITIES[gravity || 'c']
    +          gravity = GRAVITIES[gravity || "c"]
               "-resize #{width}x#{height}^^ -gravity #{gravity} -crop #{width}x#{height}+0+0 +repage"
             end
    -
           end
         end
       end
     end
    -
    
  • lib/dragonfly/param_validators.rb+37 0 added
    @@ -0,0 +1,37 @@
    +module Dragonfly
    +  module ParamValidators
    +    class InvalidParameter < RuntimeError; end
    +
    +    module_function
    +
    +    IS_NUMBER = ->(param) {
    +      param.is_a?(Numeric) || /\A[\d\.]+\z/ === param
    +    }
    +
    +    IS_WORD = ->(param) {
    +      /\A\w+\z/ === param
    +    }
    +
    +    IS_WORDS = ->(param) {
    +      /\A[\w ]+\z/ === param
    +    }
    +
    +    def is_number; IS_NUMBER; end
    +    def is_word; IS_WORD; end
    +    def is_words; IS_WORDS; end
    +
    +    def validate!(parameter, &validator)
    +      return if parameter.nil?
    +      raise InvalidParameter unless validator.(parameter)
    +    end
    +
    +    def validate_all!(parameters, &validator)
    +      parameters.each { |p| validate!(p, &validator) }
    +    end
    +
    +    def validate_all_keys!(obj, keys, &validator)
    +      parameters = keys.map { |key| obj[key] }
    +      validate_all!(parameters, &validator)
    +    end
    +  end
    +end
    
  • spec/dragonfly/image_magick/commands_spec.rb+98 0 added
    @@ -0,0 +1,98 @@
    +require "spec_helper"
    +require "dragonfly/image_magick/commands"
    +
    +describe Dragonfly::ImageMagick::Commands do
    +  include Dragonfly::ImageMagick::Commands
    +
    +  let(:app) { test_app }
    +
    +  def sample_content(name)
    +    Dragonfly::Content.new(app, SAMPLES_DIR.join(name))
    +  end
    +
    +  describe "convert" do
    +    let(:image) { sample_content("beach.png") } # 280x355
    +
    +    it "should allow for general convert commands" do
    +      convert(image, "-scale 56x71")
    +      image.should have_width(56)
    +      image.should have_height(71)
    +    end
    +
    +    it "should allow for general convert commands with added format" do
    +      convert(image, "-scale 56x71", "format" => "gif")
    +      image.should have_width(56)
    +      image.should have_height(71)
    +      image.should have_format("gif")
    +      image.meta["format"].should == "gif"
    +    end
    +
    +    it "should work for commands with parenthesis" do
    +      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")
    +      image.should have_width(280)
    +    end
    +
    +    it "should work for files with spaces/apostrophes in the name" do
    +      image = Dragonfly::Content.new(app, SAMPLES_DIR.join("mevs' white pixel.png"))
    +      convert(image, "-resize 2x2!")
    +      image.should have_width(2)
    +    end
    +
    +    it "allows converting specific frames" do
    +      gif = sample_content("gif.gif")
    +      convert(gif, "-resize 50x50")
    +      all_frames_size = gif.size
    +
    +      gif = sample_content("gif.gif")
    +      convert(gif, "-resize 50x50", "frame" => 0)
    +      one_frame_size = gif.size
    +
    +      one_frame_size.should < all_frames_size
    +    end
    +
    +    it "accepts input arguments for convert commands" do
    +      image2 = image.clone
    +      convert(image, "")
    +      convert(image2, "", "input_args" => "-extract 50x50+10+10")
    +
    +      image.should_not equal_image(image2)
    +      image2.should have_width(50)
    +    end
    +
    +    it "allows converting using specific delegates" do
    +      expect {
    +        convert(image, "", "format" => "jpg", "delegate" => "png")
    +      }.to call_command(app.shell, %r{convert png:/[^']+?/beach\.png /[^']+?\.jpg})
    +    end
    +
    +    it "maintains the mime_type meta if it exists already" do
    +      convert(image, "-resize 10x")
    +      image.meta["mime_type"].should be_nil
    +
    +      image.add_meta("mime_type" => "image/png")
    +      convert(image, "-resize 5x")
    +      image.meta["mime_type"].should == "image/png"
    +      image.mime_type.should == "image/png" # sanity check
    +    end
    +
    +    it "doesn't maintain the mime_type meta on format change" do
    +      image.add_meta("mime_type" => "image/png")
    +      convert(image, "", "format" => "gif")
    +      image.meta["mime_type"].should be_nil
    +      image.mime_type.should == "image/gif" # sanity check
    +    end
    +  end
    +
    +  describe "generate" do
    +    let (:image) { Dragonfly::Content.new(app) }
    +
    +    before(:each) do
    +      generate(image, "-size 1x1 xc:white", "png")
    +    end
    +
    +    it { image.should have_width(1) }
    +    it { image.should have_height(1) }
    +    it { image.should have_format("png") }
    +    it { image.meta.should == { "format" => "png" } }
    +  end
    +end
    
  • spec/dragonfly/image_magick/generators/convert_spec.rb+0 19 removed
    @@ -1,19 +0,0 @@
    -require 'spec_helper'
    -
    -describe Dragonfly::ImageMagick::Generators::Convert do
    -  let (:generator) { Dragonfly::ImageMagick::Generators::Convert.new }
    -  let (:app) { test_app }
    -  let (:image) { Dragonfly::Content.new(app) }
    -
    -  describe "calling convert" do
    -    before(:each) do
    -      generator.call(image, "-size 1x1 xc:white", 'png')
    -    end
    -    it {image.should have_width(1)}
    -    it {image.should have_height(1)}
    -    it {image.should have_format('png')}
    -    it {image.meta.should == {'format' => 'png'}}
    -  end
    -
    -end
    -
    
  • spec/dragonfly/image_magick/generators/plain_spec.rb+39 13 modified
    @@ -1,4 +1,5 @@
    -require 'spec_helper'
    +require "spec_helper"
    +require "dragonfly/param_validators"
     
     describe Dragonfly::ImageMagick::Generators::Plain do
       let (:generator) { Dragonfly::ImageMagick::Generators::Plain.new }
    @@ -9,42 +10,67 @@
         before(:each) do
           generator.call(image, 3, 2)
         end
    -    it {image.should have_width(3)}
    -    it {image.should have_height(2)}
    -    it {image.should have_format('png')}
    -    it {image.meta.should == {'format' => 'png', 'name' => 'plain.png'}}
    +    it { image.should have_width(3) }
    +    it { image.should have_height(2) }
    +    it { image.should have_format("png") }
    +    it { image.meta.should == { "format" => "png", "name" => "plain.png" } }
       end
     
       describe "specifying the format" do
         before(:each) do
    -      generator.call(image, 1, 1, 'format'=> 'gif')
    +      generator.call(image, 1, 1, "format" => "gif")
         end
    -    it {image.should have_format('gif')}
    -    it {image.meta.should == {'format' => 'gif', 'name' => 'plain.gif'}}
    +    it { image.should have_format("gif") }
    +    it { image.meta.should == { "format" => "gif", "name" => "plain.gif" } }
       end
     
       describe "specifying the colour" do
         it "works with English spelling" do
    -      generator.call(image, 1, 1, 'colour' => 'red')
    +      generator.call(image, 1, 1, "colour" => "red")
         end
     
         it "works with American spelling" do
    -      generator.call(image, 1, 1, 'color' => 'red')
    +      generator.call(image, 1, 1, "color" => "red")
         end
     
         it "blows up with a bad colour" do
           expect {
    -        generator.call(image, 1, 1, 'colour' => 'lardoin')
    +        generator.call(image, 1, 1, "colour" => "lardoin")
           }.to raise_error(Dragonfly::Shell::CommandFailed)
         end
       end
     
       describe "urls" do
         it "updates the url" do
           url_attributes = Dragonfly::UrlAttributes.new
    -      generator.update_url(url_attributes, 1, 1, 'format' => 'gif')
    -      url_attributes.name.should == 'plain.gif'
    +      generator.update_url(url_attributes, 1, 1, "format" => "gif")
    +      url_attributes.name.should == "plain.gif"
         end
       end
     
    +  describe "param validations" do
    +    {
    +      "color" => "white -write bad.png",
    +      "colour" => "white -write bad.png",
    +      "format" => "png -write bad.png",
    +    }.each do |opt, value|
    +      it "validates bad opts like #{opt} = '#{value}'" do
    +        expect {
    +          generator.call(image, 1, 1, opt => value)
    +        }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +      end
    +    end
    +
    +    it "validates width" do
    +      expect {
    +        generator.call(image, "1 -write bad.png", 1)
    +      }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +    end
    +
    +    it "validates height" do
    +      expect {
    +        generator.call(image, 1, "1 -write bad.png")
    +      }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +    end
    +  end
     end
    
  • spec/dragonfly/image_magick/generators/plasma_spec.rb+28 9 modified
    @@ -1,4 +1,4 @@
    -require 'spec_helper'
    +require "spec_helper"
     
     describe Dragonfly::ImageMagick::Generators::Plasma do
       let (:generator) { Dragonfly::ImageMagick::Generators::Plasma.new }
    @@ -10,23 +10,42 @@
           generator.call(image, 5, 3)
           image.should have_width(5)
           image.should have_height(3)
    -      image.should have_format('png')
    -      image.meta.should == {'format' => 'png', 'name' => 'plasma.png'}
    +      image.should have_format("png")
    +      image.meta.should == { "format" => "png", "name" => "plasma.png" }
         end
     
         it "allows changing the format" do
    -      generator.call(image, 1, 1, 'format' => 'jpg')
    -      image.should have_format('jpeg')
    -      image.meta.should == {'format' => 'jpg', 'name' => 'plasma.jpg'}
    +      generator.call(image, 1, 1, "format" => "jpg")
    +      image.should have_format("jpeg")
    +      image.meta.should == { "format" => "jpg", "name" => "plasma.jpg" }
         end
       end
     
       describe "urls" do
         it "updates the url" do
           url_attributes = Dragonfly::UrlAttributes.new
    -      generator.update_url(url_attributes, 1, 1, 'format' => 'jpg')
    -      url_attributes.name.should == 'plasma.jpg'
    +      generator.update_url(url_attributes, 1, 1, "format" => "jpg")
    +      url_attributes.name.should == "plasma.jpg"
         end
       end
    -end
     
    +  describe "param validations" do
    +    it "validates format" do
    +      expect {
    +        generator.call(image, 1, 1, "format" => "png -write bad.png")
    +      }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +    end
    +
    +    it "validates width" do
    +      expect {
    +        generator.call(image, "1 -write bad.png", 1)
    +      }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +    end
    +
    +    it "validates height" do
    +      expect {
    +        generator.call(image, 1, "1 -write bad.png")
    +      }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +    end
    +  end
    +end
    
  • spec/dragonfly/image_magick/generators/text_spec.rb+51 20 modified
    @@ -1,4 +1,4 @@
    -require 'spec_helper'
    +require "spec_helper"
     
     describe Dragonfly::ImageMagick::Generators::Text do
       let (:generator) { Dragonfly::ImageMagick::Generators::Text.new }
    @@ -7,71 +7,102 @@
     
       describe "creating a text image" do
         before(:each) do
    -      generator.call(image, "mmm", 'font_size' => 12)
    +      generator.call(image, "mmm", "font_size" => 12)
         end
    -    it {image.should have_width(20..40)} # approximate
    -    it {image.should have_height(10..20)}
    -    it {image.should have_format('png')}
    -    it {image.meta.should == {'format' => 'png', 'name' => 'text.png'}}
    +    it { image.should have_width(20..40) } # approximate
    +    it { image.should have_height(10..20) }
    +    it { image.should have_format("png") }
    +    it { image.meta.should == { "format" => "png", "name" => "text.png" } }
       end
     
       describe "specifying the format" do
         before(:each) do
    -      generator.call(image, "mmm", 'format' => 'gif')
    +      generator.call(image, "mmm", "format" => "gif")
         end
    -    it {image.should have_format('gif')}
    -    it {image.meta.should == {'format' => 'gif', 'name' => 'text.gif'}}
    +    it { image.should have_format("gif") }
    +    it { image.meta.should == { "format" => "gif", "name" => "text.gif" } }
       end
     
       describe "padding" do
         before(:each) do
           image_without_padding = image.clone
    -      generator.call(image_without_padding, "mmm", 'font_size' => 12)
    +      generator.call(image_without_padding, "mmm", "font_size" => 12)
           @width = image_properties(image_without_padding)[:width].to_i
           @height = image_properties(image_without_padding)[:height].to_i
         end
         it "1 number shortcut" do
    -      generator.call(image, "mmm", 'padding' => '10')
    +      generator.call(image, "mmm", "padding" => "10")
           image.should have_width(@width + 20)
           image.should have_height(@height + 20)
         end
         it "2 numbers shortcut" do
    -      generator.call(image, "mmm", 'padding' => '10 5')
    +      generator.call(image, "mmm", "padding" => "10 5")
           image.should have_width(@width + 10)
           image.should have_height(@height + 20)
         end
         it "3 numbers shortcut" do
    -      generator.call(image, "mmm", 'padding' => '10 5 8')
    +      generator.call(image, "mmm", "padding" => "10 5 8")
           image.should have_width(@width + 10)
           image.should have_height(@height + 18)
         end
         it "4 numbers shortcut" do
    -      generator.call(image, "mmm", 'padding' => '1 2 3 4')
    +      generator.call(image, "mmm", "padding" => "1 2 3 4")
           image.should have_width(@width + 6)
           image.should have_height(@height + 4)
         end
         it "should override the general padding declaration with the specific one (e.g. 'padding-left')" do
    -      generator.call(image, "mmm", 'padding' => '10', 'padding-left' => 9)
    +      generator.call(image, "mmm", "padding" => "10", "padding-left" => 9)
           image.should have_width(@width + 19)
           image.should have_height(@height + 20)
         end
         it "should ignore 'px' suffixes" do
    -      generator.call(image, "mmm", 'padding' => '1px 2px 3px 4px')
    +      generator.call(image, "mmm", "padding" => "1px 2px 3px 4px")
           image.should have_width(@width + 6)
           image.should have_height(@height + 4)
         end
         it "bad padding string" do
    -      lambda{
    -        generator.call(image, "mmm", 'padding' => '1 2 3 4 5')
    +      lambda {
    +        generator.call(image, "mmm", "padding" => "1 2 3 4 5")
           }.should raise_error(ArgumentError)
         end
       end
     
       describe "urls" do
         it "updates the url" do
           url_attributes = Dragonfly::UrlAttributes.new
    -      generator.update_url(url_attributes, "mmm", 'format' => 'gif')
    -      url_attributes.name.should == 'text.gif'
    +      generator.update_url(url_attributes, "mmm", "format" => "gif")
    +      url_attributes.name.should == "text.gif"
    +    end
    +  end
    +
    +  describe "param validations" do
    +    {
    +      "font" => "Times New Roman -write bad.png",
    +      "font_family" => "Times New Roman -write bad.png",
    +      "color" => "rgb(255, 34, 55) -write bad.png",
    +      "background_color" => "rgb(255, 52, 55) -write bad.png",
    +      "stroke_color" => "rgb(255, 52, 55) -write bad.png",
    +      "format" => "png -write bad.png",
    +    }.each do |opt, value|
    +      it "validates bad opts like #{opt} = '#{value}'" do
    +        expect {
    +          generator.call(image, "some text", opt => value)
    +        }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +      end
    +    end
    +
    +    ["rgb(33,33,33)", "rgba(33,33,33,0.5)", "rgb(33.5,33.5,33.5)", "#fff", "#efefef", "blue"].each do |colour|
    +      it "allows #{colour.inspect} as a colour specification" do
    +        generator.call(image, "mmm", "color" => colour)
    +      end
    +    end
    +
    +    ["rgb(33, 33, 33)", "something else", "blue:", "f#ff"].each do |colour|
    +      it "disallows #{colour.inspect} as a colour specification" do
    +        expect {
    +          generator.call(image, "mmm", "color" => colour)
    +        }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +      end
         end
       end
     end
    
  • spec/dragonfly/image_magick/plugin_spec.rb+32 29 modified
    @@ -1,25 +1,24 @@
    -require 'spec_helper'
    +require "spec_helper"
     
     describe "a configured imagemagick app" do
    -
    -  let(:app){ test_app.configure_with(:imagemagick) }
    -  let(:image){ app.fetch_file(SAMPLES_DIR.join('beach.png')) }
    +  let(:app) { test_app.configure_with(:imagemagick) }
    +  let(:image) { app.fetch_file(SAMPLES_DIR.join("beach.png")) }
     
       describe "env variables" do
    -    let(:app){ test_app }
    +    let(:app) { test_app }
     
         it "allows setting the convert command" do
           app.configure do
    -        plugin :imagemagick, :convert_command => '/bin/convert'
    +        plugin :imagemagick, :convert_command => "/bin/convert"
           end
    -      app.env[:convert_command].should == '/bin/convert'
    +      app.env[:convert_command].should == "/bin/convert"
         end
     
         it "allows setting the identify command" do
           app.configure do
    -        plugin :imagemagick, :identify_command => '/bin/identify'
    +        plugin :imagemagick, :identify_command => "/bin/identify"
           end
    -      app.env[:identify_command].should == '/bin/identify'
    +      app.env[:identify_command].should == "/bin/identify"
         end
       end
     
    @@ -33,7 +32,7 @@
         end
     
         it "should return the aspect ratio" do
    -      image.aspect_ratio.should == (280.0/355.0)
    +      image.aspect_ratio.should == (280.0 / 355.0)
         end
     
         it "should say if it's portrait" do
    @@ -60,53 +59,53 @@
         end
     
         it "should return false for pdfs" do
    -      image.encode('pdf').image?.should be_falsey
    -    end unless ENV['SKIP_FLAKY_TESTS']
    +      image.encode("pdf").image?.should be_falsey
    +    end unless ENV["SKIP_FLAKY_TESTS"]
       end
     
       describe "processors that change the url" do
         before do
    -      app.configure{ url_format '/:name' }
    +      app.configure { url_format "/:name" }
         end
     
    -    describe "convert" do
    +    describe "thumb" do
           it "sanity check with format" do
    -        thumb = image.convert('-resize 1x1!', 'format' => 'jpg')
    +        thumb = image.thumb("1x1!", "format" => "jpg")
             thumb.url.should =~ /^\/beach\.jpg\?.*job=\w+/
             thumb.width.should == 1
    -        thumb.format.should == 'jpeg'
    -        thumb.meta['format'].should == 'jpg'
    +        thumb.format.should == "jpeg"
    +        thumb.meta["format"].should == "jpg"
           end
     
           it "sanity check without format" do
    -        thumb = image.convert('-resize 1x1!')
    +        thumb = image.thumb("1x1!")
             thumb.url.should =~ /^\/beach\.png\?.*job=\w+/
             thumb.width.should == 1
    -        thumb.format.should == 'png'
    -        thumb.meta['format'].should be_nil
    +        thumb.format.should == "png"
    +        thumb.meta["format"].should be_nil
           end
         end
     
         describe "encode" do
           it "sanity check" do
    -        thumb = image.encode('jpg')
    +        thumb = image.encode("jpg")
             thumb.url.should =~ /^\/beach\.jpg\?.*job=\w+/
    -        thumb.format.should == 'jpeg'
    -        thumb.meta['format'].should == 'jpg'
    +        thumb.format.should == "jpeg"
    +        thumb.meta["format"].should == "jpg"
           end
         end
       end
     
       describe "other processors" do
         describe "encode" do
           it "should encode the image to the correct format" do
    -        image.encode!('gif')
    -        image.format.should == 'gif'
    +        image.encode!("gif")
    +        image.format.should == "gif"
           end
     
           it "should allow for extra args" do
    -        image.encode!('jpg', '-quality 1')
    -        image.format.should == 'jpeg'
    +        image.encode!("jpg", "-quality 1")
    +        image.format.should == "jpeg"
             image.size.should < 2000
           end
         end
    @@ -117,8 +116,13 @@
             image.width.should == 355
             image.height.should == 280
           end
    -    end
     
    +      it "disallows bad parameters" do
    +        expect {
    +          image.rotate!("90 -write bad.png").apply
    +        }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +      end
    +    end
       end
     
       describe "identify" do
    @@ -127,5 +131,4 @@
           image.identify("-format %h").chomp.should == "355"
         end
       end
    -
     end
    
  • spec/dragonfly/image_magick/processors/convert_spec.rb+0 88 removed
    @@ -1,88 +0,0 @@
    -require 'spec_helper'
    -
    -describe Dragonfly::ImageMagick::Processors::Convert do
    -
    -  def sample_content(name)
    -    Dragonfly::Content.new(app, SAMPLES_DIR.join(name))
    -  end
    -
    -  let(:app){ test_app }
    -  let(:image){ sample_content('beach.png') } # 280x355
    -  let(:processor){ Dragonfly::ImageMagick::Processors::Convert.new }
    -
    -  it "should allow for general convert commands" do
    -    processor.call(image, '-scale 56x71')
    -    image.should have_width(56)
    -    image.should have_height(71)
    -  end
    -
    -  it "should allow for general convert commands with added format" do
    -    processor.call(image, '-scale 56x71', 'format' => 'gif')
    -    image.should have_width(56)
    -    image.should have_height(71)
    -    image.should have_format('gif')
    -    image.meta['format'].should == 'gif'
    -  end
    -
    -  it "should work for commands with parenthesis" do
    -    processor.call(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")
    -    image.should have_width(280)
    -  end
    -
    -  it "should work for files with spaces/apostrophes in the name" do
    -    image = Dragonfly::Content.new(app, SAMPLES_DIR.join("mevs' white pixel.png"))
    -    processor.call(image, "-resize 2x2!")
    -    image.should have_width(2)
    -  end
    -
    -  it "updates the url with format if given" do
    -    url_attributes = Dragonfly::UrlAttributes.new
    -    processor.update_url(url_attributes, '-scale 56x71', 'format' => 'gif')
    -    url_attributes.ext.should == 'gif'
    -  end
    -
    -  it "allows converting specific frames" do
    -    gif = sample_content('gif.gif')
    -    processor.call(gif, '-resize 50x50')
    -    all_frames_size = gif.size
    -
    -    gif = sample_content('gif.gif')
    -    processor.call(gif, '-resize 50x50', 'frame' => 0)
    -    one_frame_size = gif.size
    -
    -    one_frame_size.should < all_frames_size
    -  end
    -
    -  it "accepts input arguments for convert commands" do
    -    image2 = image.clone
    -    processor.call(image, '')
    -    processor.call(image2, '', 'input_args' => '-extract 50x50+10+10')
    -
    -    image.should_not equal_image(image2)
    -    image2.should have_width(50)
    -  end
    -
    -  it "allows converting using specific delegates" do
    -    expect {
    -      processor.call(image, '', 'format' => 'jpg', 'delegate' => 'png')
    -    }.to call_command(app.shell, %r{convert png:/[^']+?/beach\.png /[^']+?\.jpg})
    -  end
    -
    -  it "maintains the mime_type meta if it exists already" do
    -    processor.call(image, '-resize 10x')
    -    image.meta['mime_type'].should be_nil
    -
    -    image.add_meta('mime_type' => 'image/png')
    -    processor.call(image, '-resize 5x')
    -    image.meta['mime_type'].should == 'image/png'
    -    image.mime_type.should == 'image/png' # sanity check
    -  end
    -
    -  it "doesn't maintain the mime_type meta on format change" do
    -    image.add_meta('mime_type' => 'image/png')
    -    processor.call(image, '', 'format' => 'gif')
    -    image.meta['mime_type'].should be_nil
    -    image.mime_type.should == 'image/gif' # sanity check
    -  end
    -
    -end
    
  • spec/dragonfly/image_magick/processors/encode_spec.rb+30 0 added
    @@ -0,0 +1,30 @@
    +require "spec_helper"
    +
    +describe Dragonfly::ImageMagick::Processors::Encode do
    +  let (:app) { test_imagemagick_app }
    +  let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("beach.png")) } # 280x355
    +  let (:processor) { Dragonfly::ImageMagick::Processors::Encode.new }
    +
    +  it "encodes to a different format" do
    +    processor.call(image, "jpeg")
    +    image.should have_format("jpeg")
    +  end
    +
    +  describe "param validations" do
    +    it "validates the format param" do
    +      expect {
    +        processor.call(image, "jpeg -write bad.png")
    +      }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +    end
    +
    +    it "allows good args" do
    +      processor.call(image, "jpeg", "-quality 10")
    +    end
    +
    +    it "disallows bad args" do
    +      expect {
    +        processor.call(image, "jpeg", "-write bad.png")
    +      }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +    end
    +  end
    +end
    
  • spec/dragonfly/image_magick/processors/thumb_spec.rb+46 45 modified
    @@ -1,100 +1,92 @@
    -require 'spec_helper'
    -require 'ostruct'
    +require "spec_helper"
    +require "ostruct"
     
     describe Dragonfly::ImageMagick::Processors::Thumb do
    -
       let (:app) { test_imagemagick_app }
    -  let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join('beach.png')) } # 280x355
    +  let (:image) { Dragonfly::Content.new(app, SAMPLES_DIR.join("beach.png")) } # 280x355
       let (:processor) { Dragonfly::ImageMagick::Processors::Thumb.new }
     
       it "raises an error if an unrecognized string is given" do
    -    expect{
    -      processor.call(image, '30x40#ne!')
    +    expect {
    +      processor.call(image, "30x40#ne!")
         }.to raise_error(ArgumentError)
       end
     
       describe "resizing" do
    -
         it "works with xNN" do
    -      processor.call(image, 'x30')
    +      processor.call(image, "x30")
           image.should have_width(24)
           image.should have_height(30)
         end
     
         it "works with NNx" do
    -      processor.call(image, '30x')
    +      processor.call(image, "30x")
           image.should have_width(30)
           image.should have_height(38)
         end
     
         it "works with NNxNN" do
    -      processor.call(image, '30x30')
    +      processor.call(image, "30x30")
           image.should have_width(24)
           image.should have_height(30)
         end
     
         it "works with NNxNN!" do
    -      processor.call(image, '30x30!')
    +      processor.call(image, "30x30!")
           image.should have_width(30)
           image.should have_height(30)
         end
     
         it "works with NNxNN%" do
    -      processor.call(image, '25x50%')
    +      processor.call(image, "25x50%")
           image.should have_width(70)
           image.should have_height(178)
         end
     
         describe "NNxNN>" do
    -
           it "doesn't resize if the image is smaller than specified" do
    -        processor.call(image, '1000x1000>')
    +        processor.call(image, "1000x1000>")
             image.should have_width(280)
             image.should have_height(355)
           end
     
           it "resizes if the image is larger than specified" do
    -        processor.call(image, '30x30>')
    +        processor.call(image, "30x30>")
             image.should have_width(24)
             image.should have_height(30)
           end
    -
         end
     
         describe "NNxNN<" do
    -
           it "doesn't resize if the image is larger than specified" do
    -        processor.call(image, '10x10<')
    +        processor.call(image, "10x10<")
             image.should have_width(280)
             image.should have_height(355)
           end
     
           it "resizes if the image is smaller than specified" do
    -        processor.call(image, '400x400<')
    +        processor.call(image, "400x400<")
             image.should have_width(315)
             image.should have_height(400)
           end
    -
         end
    -
       end
     
       describe "cropping" do # Difficult to test here other than dimensions
    -
         it "crops" do
    -      processor.call(image, '10x20+30+30')
    +      processor.call(image, "10x20+30+30")
           image.should have_width(10)
           image.should have_height(20)
         end
     
         it "crops with gravity" do
           image2 = image.clone
     
    -      processor.call(image, '10x8nw')
    +      processor.call(image, "10x8nw")
           image.should have_width(10)
           image.should have_height(8)
     
    -      processor.call(image2, '10x8se')
    +      processor.call(image2, "10x8se")
           image2.should have_width(10)
           image2.should have_height(8)
     
    @@ -103,77 +95,86 @@
     
         it "raises if given both gravity and offset" do
           expect {
    -        processor.call(image, '100x100+10+10se')
    +        processor.call(image, "100x100+10+10se")
           }.to raise_error(ArgumentError)
         end
     
         it "works when the crop area is outside the image" do
    -      processor.call(image, '100x100+250+300')
    +      processor.call(image, "100x100+250+300")
           image.should have_width(30)
           image.should have_height(55)
         end
     
         it "crops twice in a row correctly" do
    -      processor.call(image, '100x100+10+10')
    -      processor.call(image, '50x50+0+0')
    +      processor.call(image, "100x100+10+10")
    +      processor.call(image, "50x50+0+0")
           image.should have_width(50)
           image.should have_height(50)
         end
    -
       end
     
       describe "resize_and_crop" do
    -
         it "crops to the correct dimensions" do
    -      processor.call(image, '100x100#')
    +      processor.call(image, "100x100#")
           image.should have_width(100)
           image.should have_height(100)
         end
     
         it "resizes before cropping" do
           image2 = image.clone
    -      processor.call(image, '100x100#')
    -      processor.call(image2, '100x100c')
    +      processor.call(image, "100x100#")
    +      processor.call(image2, "100x100c")
           image2.should_not equal_image(image)
         end
     
         it "works with gravity" do
           image2 = image.clone
    -      processor.call(image, '10x10#nw')
    -      processor.call(image, '10x10#se')
    +      processor.call(image, "10x10#nw")
    +      processor.call(image, "10x10#se")
           image2.should_not equal_image(image)
         end
    -
       end
     
       describe "format" do
         let (:url_attributes) { OpenStruct.new }
     
         it "changes the format if passed in" do
    -      processor.call(image, '2x2', 'format' => 'jpeg')
    -      image.should have_format('jpeg')
    +      processor.call(image, "2x2", "format" => "jpeg")
    +      image.should have_format("jpeg")
         end
     
         it "doesn't change the format if not passed in" do
    -      processor.call(image, '2x2')
    -      image.should have_format('png')
    +      processor.call(image, "2x2")
    +      image.should have_format("png")
         end
     
         it "updates the url ext if passed in" do
    -      processor.update_url(url_attributes, '2x2', 'format' => 'png')
    -      url_attributes.ext.should == 'png'
    +      processor.update_url(url_attributes, "2x2", "format" => "png")
    +      url_attributes.ext.should == "png"
         end
     
         it "doesn't update the url ext if not passed in" do
    -      processor.update_url(url_attributes, '2x2')
    +      processor.update_url(url_attributes, "2x2")
           url_attributes.ext.should be_nil
         end
       end
     
       describe "args_for_geometry" do
         it "returns the convert arguments used for a given geometry" do
    -      expect(processor.args_for_geometry('30x40')).to eq('-resize 30x40')
    +      expect(processor.args_for_geometry("30x40")).to eq("-resize 30x40")
         end
       end
     
    +  describe "param validations" do
    +    {
    +      "format" => "png -write bad.png",
    +      "frame" => "0] -write bad.png [",
    +    }.each do |opt, value|
    +      it "validates bad opts like #{opt} = '#{value}'" do
    +        expect {
    +          processor.call(image, "30x30", opt => value)
    +        }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +      end
    +    end
    +  end
     end
    
  • spec/dragonfly/param_validators_spec.rb+89 0 added
    @@ -0,0 +1,89 @@
    +require "spec_helper"
    +require "dragonfly/param_validators"
    +
    +describe Dragonfly::ParamValidators do
    +  include Dragonfly::ParamValidators
    +
    +  describe "validate!" do
    +    it "does nothing if the parameter meets the condition" do
    +      validate!("thing") { |t| t === "thing" }
    +    end
    +
    +    it "raises if the parameter doesn't meet the condition" do
    +      expect {
    +        validate!("thing") { |t| t === "ting" }
    +      }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +    end
    +
    +    it "does nothing if the parameter is nil" do
    +      validate!(nil) { |t| t === "thing" }
    +    end
    +  end
    +
    +  describe "validate_all!" do
    +    it "allows passing an array of parameters to validate" do
    +      validate_all!(["a", "b"]) { |p| /\w/ === p }
    +      expect {
    +        validate_all!(["a", " "]) { |p| /\w/ === p }
    +      }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +    end
    +  end
    +
    +  describe "validate_all_keys!" do
    +    it "allows passing an array of parameters to validate" do
    +      obj = { "a" => "A", "b" => "B" }
    +      validate_all_keys!(obj, ["a", "b"]) { |p| /\w/ === p }
    +      expect {
    +        validate_all_keys!(obj, ["a", "b"]) { |p| /[a-z]/ === p }
    +      }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +    end
    +  end
    +
    +  describe "is_number" do
    +    [3, 3.14, "3", "3.2"].each do |val|
    +      it "validates #{val.inspect}" do
    +        validate!(val, &is_number)
    +      end
    +    end
    +
    +    ["", "3 2", "hello4", {}, []].each do |val|
    +      it "validates #{val.inspect}" do
    +        expect {
    +          validate!(val, &is_number)
    +        }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +      end
    +    end
    +  end
    +
    +  describe "is_word" do
    +    ["hello", "helLo", "HELLO"].each do |val|
    +      it "validates #{val.inspect}" do
    +        validate!(val, &is_word)
    +      end
    +    end
    +
    +    ["", "hel%$lo", "hel lo", "hel-lo", {}, []].each do |val|
    +      it "validates #{val.inspect}" do
    +        expect {
    +          validate!(val, &is_word)
    +        }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +      end
    +    end
    +  end
    +
    +  describe "is_words" do
    +    ["hello there", "Hi", "  What is Up "].each do |val|
    +      it "validates #{val.inspect}" do
    +        validate!(val, &is_words)
    +      end
    +    end
    +
    +    ["", "hel%$lo", "What's up", "hel-lo", {}, []].each do |val|
    +      it "validates #{val.inspect}" do
    +        expect {
    +          validate!(val, &is_words)
    +        }.to raise_error(Dragonfly::ParamValidators::InvalidParameter)
    +      end
    +    end
    +  end
    +end
    
  • spec/functional/shell_commands_spec.rb+6 9 modified
    @@ -1,33 +1,30 @@
    -require 'spec_helper'
    +require "spec_helper"
     
     describe "using the shell" do
    -
       let (:app) { test_app }
     
       describe "shell injection" do
         it "should not allow it!" do
           app.configure_with(:imagemagick)
           begin
    -        app.generate(:plain, 10, 10, 'white').convert("-resize 5x5 ; touch tmp/stuff").apply
    +        app.generate(:plain, 10, 10, "white; touch tmp/stuff").apply
           rescue Dragonfly::Shell::CommandFailed
           end
    -      File.exist?('tmp/stuff').should be_falsey
    +      File.exist?("tmp/stuff").should be_falsey
         end
       end
     
       describe "env variables with imagemagick" do
         it "allows configuring the convert path" do
    -      app.configure_with(:imagemagick, :convert_command => '/bin/convert')
    +      app.configure_with(:imagemagick, :convert_command => "/bin/convert")
           app.shell.should_receive(:run).with(%r[/bin/convert], hash_including)
    -      app.create("").thumb('30x30').apply
    +      app.create("").thumb("30x30").apply
         end
     
         it "allows configuring the identify path" do
    -      app.configure_with(:imagemagick, :identify_command => '/bin/identify')
    +      app.configure_with(:imagemagick, :identify_command => "/bin/identify")
           app.shell.should_receive(:run).with(%r[/bin/identify], hash_including).and_return("JPG 1 1")
           app.create("").width
         end
       end
    -
     end
    -
    
  • spec/spec_helper.rb+12 14 modified
    @@ -2,19 +2,19 @@
     require "bundler"
     Bundler.setup(:default, :test)
     
    -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
    +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
     $LOAD_PATH.unshift(File.dirname(__FILE__))
    -require 'rspec'
    -require 'dragonfly'
    -require 'fileutils'
    -require 'tempfile'
    -require 'webmock/rspec'
    -require 'pry'
    +require "rspec"
    +require "dragonfly"
    +require "fileutils"
    +require "tempfile"
    +require "webmock/rspec"
    +require "pry"
     
     # Requires supporting files with custom matchers and macros, etc,
    -Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
    +Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
     
    -SAMPLES_DIR = Pathname.new(File.expand_path('../../samples', __FILE__))
    +SAMPLES_DIR = Pathname.new(File.expand_path("../../samples", __FILE__))
     
     RSpec.configure do |c|
       c.include ModelHelpers
    @@ -25,8 +25,8 @@ def todo
       raise "TODO"
     end
     
    -require 'logger'
    -LOG_FILE = 'tmp/test.log'
    +require "logger"
    +LOG_FILE = "tmp/test.log"
     FileUtils.rm_rf(LOG_FILE)
     Dragonfly.logger = Logger.new(LOG_FILE)
     
    @@ -36,7 +36,7 @@ def todo
       end
     end
     
    -def test_app(name=nil)
    +def test_app(name = nil)
       app = Dragonfly::App.instance(name)
       app.datastore = Dragonfly::MemoryDataStore.new
       app.secret = "test secret"
    @@ -45,8 +45,6 @@ def test_app(name=nil)
     
     def test_imagemagick_app
       test_app.configure do
    -    generator :convert, Dragonfly::ImageMagick::Generators::Convert.new
    -    processor :convert, Dragonfly::ImageMagick::Processors::Convert.new
         analyser :image_properties, Dragonfly::ImageMagick::Analysers::ImageProperties.new
       end
     end
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.