aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/cgi_ext/cgi_methods.rb
blob: e58c743345953aeff07d552598399f88ac301cab (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
require 'cgi'
require 'action_controller/vendor/xml_node'
require 'strscan'

# Static methods for parsing the query and request parameters that can be used in
# a CGI extension class or testing in isolation.
class CGIMethods #:nodoc:
  class << self
    # DEPRECATED: Use parse_form_encoded_parameters
    def parse_query_parameters(query_string)
      parse_form_encoded_parameters(query_string)
    end

    # DEPRECATED: Use parse_form_encoded_parameters
    def parse_request_parameters(params)
      parsed_params = {}

      for key, value in params
        next unless key
        value = [value] if key =~ /.*\[\]$/
        unless key.include?('[')
          # much faster to test for the most common case first (GET)
          # and avoid the call to build_deep_hash
          parsed_params[key] = get_typed_value(value[0])
        else
          build_deep_hash(get_typed_value(value[0]), parsed_params, get_levels(key))
        end
      end
    
      parsed_params
    end
    
    # TODO: Docs
    def parse_form_encoded_parameters(form_encoded_string)
      FormEncodedStringScanner.decode(form_encoded_string)
    end

    def parse_formatted_request_parameters(mime_type, raw_post_data)
      case strategy = ActionController::Base.param_parsers[mime_type]
        when Proc
          strategy.call(raw_post_data)
        when :xml_simple
          raw_post_data.blank? ? {} : Hash.create_from_xml(raw_post_data)
        when :yaml
          YAML.load(raw_post_data)
        when :xml_node
          node = XmlNode.from_xml(raw_post_data)
          { node.node_name => node }
      end
    rescue Object => e
      { "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace, 
        "raw_post_data" => raw_post_data, "format" => mime_type }
    end

    private
      def get_typed_value(value)
        # test most frequent case first
        if value.is_a?(String)
          value
        elsif value.respond_to?(:content_type) && ! value.content_type.blank?
          # Uploaded file
          unless value.respond_to?(:full_original_filename)
            class << value
              alias_method :full_original_filename, :original_filename

              # Take the basename of the upload's original filename.
              # This handles the full Windows paths given by Internet Explorer
              # (and perhaps other broken user agents) without affecting
              # those which give the lone filename.
              # The Windows regexp is adapted from Perl's File::Basename.
              def original_filename
                if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
                  md.captures.first
                else
                  File.basename full_original_filename
                end
              end
            end
          end

          # Return the same value after overriding original_filename.
          value

        elsif value.respond_to?(:read)
          # Value as part of a multipart request
          result = value.read
          value.rewind
          result
        elsif value.class == Array
          value.collect { |v| get_typed_value(v) }
        else
          # other value (neither string nor a multipart request)
          value.to_s
        end
      end
  
      PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
      def get_levels(key)
        all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a
        if main.nil?
          []
        elsif trailing
          [key]
        elsif bracketed
          [main] + bracketed.slice(1...-1).split('][')
        else
          [main]
        end
      end

      def build_deep_hash(value, hash, levels)
        if levels.length == 0
          value
        elsif hash.nil?
          { levels.first => build_deep_hash(value, nil, levels[1..-1]) }
        else
          hash.update({ levels.first => build_deep_hash(value, hash[levels.first], levels[1..-1]) })
        end
      end
  end

  class FormEncodedStringScanner < StringScanner
    attr_reader :top, :parent, :result

    def self.decode(form_encoded_string)
      new(form_encoded_string).parse
    end

    def initialize(form_encoded_string)
      super(unescape_keys(form_encoded_string))
    end
    
    KEY_REGEXP = %r{([^\[\]=&]+)}
    BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
    
    
    def parse
      @result = {}

      until eos?
        # Parse each & delimited chunk
        @parent, @top = nil, result
        
        # First scan the bare key
        key = scan(KEY_REGEXP) or (skip_term and next)
        key = post_key_check(key)
        
        # Then scan as many nestings as present
        until check(/\=/) || eos? 
          r = scan(BRACKETED_KEY_REGEXP) or (skip_term and break)
          key = self[1]
          key = post_key_check(key)
        end
        
        # Scan the value if we see an =
        if scan %r{=}
          value = scan(/[^\&]+/) # scan_until doesn't handle \Z
          value = CGI.unescape(value) if value # May be nil when eos?
          bind(key, value)
        end

        scan(%r/\&+/) # Ignore multiple adjacent &'s
      end
      
      return result
    end

    private
      # Turn keys like person%5Bname%5D into person[name], so they can be processed as hashes
      def unescape_keys(query_string)
        query_string.split('&').collect do |fragment|
          key, value = fragment.split('=', 2)
          
          if key
            key = key.gsub(/%5D/, ']').gsub(/%5B/, '[')
            [ key, value ].join("=")
          else
            fragment
          end
        end.join('&')
      end

      # Skip over the current term by scanning past the next &, or to
      # then end of the string if there is no next &
      def skip_term
        scan_until(%r/\&+/) || scan(/.+/)
      end
    
      # After we see a key, we must look ahead to determine our next action. Cases:
      # 
      #   [] follows the key. Then the value must be an array.
      #   = follows the key. (A value comes next)
      #   & or the end of string follows the key. Then the key is a flag.
      #   otherwise, a hash follows the key. 
      def post_key_check(key)
        if eos? || check(/\&/) # a& or a\Z indicates a is a flag.
          bind key, nil # Curiously enough, the flag's value is nil
          nil
        elsif scan(/\[\]/) # a[b][] indicates that b is an array
          container key, Array
          nil
        elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
          container key, Hash
          nil
        else # Presumably an = sign is next.
          key
        end
      end
    
      # Add a container to the stack.
      # 
      def container(key, klass)
        raise TypeError if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
        value = bind(key, klass.new)
        raise TypeError unless value.is_a? klass
        push value
      end
    
      # Push a value onto the 'stack', which is actually only the top 2 items.
      def push(value)
        @parent, @top = @top, value
      end
    
      # Bind a key (which may be nil for items in an array) to the provided value.
      def bind(key, value)
        if top.is_a? Array
          if key
            if top[-1].is_a?(Hash) && ! top[-1].key?(key)
              top[-1][key] = value
            else
              top << {key => value}.with_indifferent_access
              push top.last
            end
          else
            top << value
          end
        elsif top.is_a? Hash
          key = CGI.unescape(key)
          if top.key?(key) && parent.is_a?(Array)
            parent << (@top = {})
          end
          return top[key] ||= value
        else
          # Do nothing?
        end
        return value
      end
    end
end