VYPR
Critical severityNVD Advisory· Published May 29, 2021· Updated Aug 3, 2024

CVE-2021-33564

CVE-2021-33564

Description

An argument injection vulnerability in the Dragonfly gem before 1.4.0 for Ruby allows remote attackers to read and write to arbitrary files via a crafted URL when the verify_url option is disabled. This may lead to code execution. The problem occurs because the generate and process features mishandle use of the ImageMagick convert utility.

AI Insight

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

Argument injection in Dragonfly gem before 1.4.0 allows remote attackers to read/write arbitrary files via crafted URL when verify_url disabled, potentially leading to code execution.

Vulnerability

The Dragonfly gem for Ruby versions before 1.4.0 contains an argument injection vulnerability in the generate and process features, which mishandle the ImageMagick convert utility [1]. When the verify_url option is disabled, a remote attacker can inject arbitrary arguments into the convert command by crafting a malicious URL [4]. This allows unintended file read/write operations [1].

Exploitation

An attacker can send a crafted URL to an exposed Dragonfly server with verify_url disabled. The injected arguments (e.g., -write or -read) allow reading arbitrary files (e.g., /etc/passwd) or writing arbitrary content to files [3]. No authentication or user interaction is required [1]. Proof-of-concept code is publicly available [3].

Impact

Successful exploitation enables arbitrary file read and write on the server, potentially leading to remote code execution (RCE) by writing a malicious file (e.g., a Ruby script or web shell) [1][3]. The attacker gains the privileges of the Dragonfly process, typically the web server user, which can lead to full server compromise [1].

Mitigation

Upgrade to Dragonfly gem version 1.4.0 or later, which fixes the issue [2]. As a workaround, ensure verify_url is enabled (the default is true) to prevent injection [4]. If upgrading is not possible, restrict allowed URLs and disable the generate/process features if not required [1]. The CVE is not listed in the Known Exploited Vulnerabilities (KEV) catalog.

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

9

News mentions

0

No linked articles in our index yet.