aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/cgi_ext/parameters.rb
blob: a7f539327263433aecff77ca7c85cfe51f223c16 (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
require 'cgi'
require 'strscan'

module ActionController
  module CgiExt
    module Parameters
      def self.included(base)
        base.extend ClassMethods
      end

      # Merge POST and GET parameters from the request body and query string,
      # with GET parameters taking precedence.
      def parameters
        request_parameters.update(query_parameters)
      end

      def query_parameters
        self.class.parse_query_parameters(query_string)
      end

      def request_parameters
        self.class.parse_request_parameters(params, env_table)
      end

      module ClassMethods
        # DEPRECATED: Use parse_form_encoded_parameters
        def parse_query_parameters(query_string)
          pairs = query_string.split('&').collect do |chunk|
            next if chunk.empty?
            key, value = chunk.split('=', 2)
            next if key.empty?
            value = value.nil? ? nil : CGI.unescape(value)
            [ CGI.unescape(key), value ]
          end.compact

          FormEncodedPairParser.new(pairs).result
        end

        # DEPRECATED: Use parse_form_encoded_parameters
        def parse_request_parameters(params)
          parser = FormEncodedPairParser.new

          params = params.dup
          until params.empty?
            for key, value in params
              if key.blank?
                params.delete key
              elsif !key.include?('[')
                # much faster to test for the most common case first (GET)
                # and avoid the call to build_deep_hash
                parser.result[key] = get_typed_value(value[0])
                params.delete key
              elsif value.is_a?(Array)
                parser.parse(key, get_typed_value(value.shift))
                params.delete key if value.empty?
              else
                raise TypeError, "Expected array, found #{value.inspect}"
              end
            end
          end

          parser.result
        end

        def parse_formatted_request_parameters(mime_type, body)
          case strategy = ActionController::Base.param_parsers[mime_type]
            when Proc
              strategy.call(body)
            when :xml_simple, :xml_node
              body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
            when :yaml
              YAML.load(body)
          end
        rescue Exception => e # YAML, XML or Ruby code block errors
          { "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
            "body" => body, "format" => mime_type }
        end

        private
          def get_typed_value(value)
            case value
              when String
                value
              when NilClass
                ''
              when Array
                value.map { |v| get_typed_value(v) }
              else
                # Uploaded file provides content type and filename.
                if value.respond_to?(:content_type) &&
                      !value.content_type.blank? &&
                      !value.original_filename.blank?
                  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

                # Multipart values may have content type, but no filename.
                elsif value.respond_to?(:read)
                  result = value.read
                  value.rewind
                  result

                # Unknown value, neither string nor multipart.
                else
                  raise "Unknown form value: #{value.inspect}"
                end
            end
          end
      end

      class FormEncodedPairParser < StringScanner #:nodoc:
        attr_reader :top, :parent, :result

        def initialize(pairs = [])
          super('')
          @result = {}
          pairs.each { |key, value| parse(key, value) }
        end

        KEY_REGEXP = %r{([^\[\]=&]+)}
        BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}

        # Parse the query string
        def parse(key, value)
          self.string = key
          @top, @parent = result, nil

          # First scan the bare key
          key = scan(KEY_REGEXP) or return
          key = post_key_check(key)

          # Then scan as many nestings as present
          until eos?
            r = scan(BRACKETED_KEY_REGEXP) or return
            key = self[1]
            key = post_key_check(key)
          end

          bind(key, value)
        end

        private
          # 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 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 # End of key? We do nothing.
              key
            end
          end

          # Add a container to the stack.
          def container(key, klass)
            type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
            value = bind(key, klass.new)
            type_conflict! klass, value 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)
              parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
              return top[key] ||= value
            else
              raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
            end

            return value
          end

          def type_conflict!(klass, value)
            raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value."
          end
      end
    end
  end
end