66require "tempfile"
77
88require "protocol/http/request"
9- require "protocol/multipart/parser "
9+ require "protocol/multipart/form_data "
1010require "protocol/url/encoding"
1111
1212module Utopia
@@ -24,6 +24,12 @@ class Request
2424 # The maximum nesting depth accepted for structured arguments.
2525 MAXIMUM_ARGUMENT_DEPTH = 8
2626
27+ # The default maximum size of a URL-encoded form body.
28+ MAXIMUM_URL_ENCODED_SIZE = Protocol ::Multipart ::FormData ::MAXIMUM_FIELD_SIZE
29+
30+ FORM_DATA_UNDEFINED = Object . new . freeze
31+ private_constant :FORM_DATA_UNDEFINED
32+
2733 # A file uploaded as part of a multipart form.
2834 class Upload
2935 # Initialize an uploaded file.
@@ -52,7 +58,7 @@ def initialize(headers, filename, tempfile, size)
5258
5359 # The submitted content type, if present.
5460 def content_type
55- @headers [ "content-type" ]
61+ @headers [ "content-type" ] &. type
5662 end
5763 end
5864
@@ -119,18 +125,20 @@ def self.[](*arguments)
119125 # Initialize the request proxy.
120126 # @parameter delegate [Protocol::HTTP::Request] The underlying protocol request.
121127 # @parameter request_path [String | Nil] The original path before internal rewrites.
122- def initialize ( delegate , request_path : nil )
128+ # @parameter form_data_options [Hash | Nil] Default options for parsing form data.
129+ def initialize ( delegate , request_path : nil , form_data_options : nil )
123130 @delegate = delegate
124131 @request_path = request_path
132+ @form_data_options = form_data_options &.dup &.freeze || { } . freeze
125133 @session = nil
126134 @variables = nil
127135 @locale = nil
128136 @localization = nil
129137 @exception = nil
130138
131139 @query_arguments = nil
132- @form_arguments = nil
133- @arguments = nil
140+ @form_data = FORM_DATA_UNDEFINED
141+ @form_data_effective_options = nil
134142 @cookies = nil
135143 end
136144
@@ -144,8 +152,8 @@ def initialize_copy(other)
144152
145153 @delegate = other . delegate . dup
146154 @query_arguments = nil
147- @form_arguments = nil
148- @arguments = nil
155+ @form_data = FORM_DATA_UNDEFINED
156+ @form_data_effective_options = nil
149157 @cookies = nil
150158 end
151159
@@ -157,15 +165,14 @@ def path= value
157165
158166 @delegate . path = value
159167 @query_arguments = nil
160- @arguments = nil
161168 end
162169
163- # Assign the request body and clear any decoded form arguments .
170+ # Assign the request body and clear any decoded form data .
164171 # @parameter value [Protocol::HTTP::Body::Readable | Nil] The new request body.
165172 def body = value
166173 @delegate . body = value
167- @form_arguments = nil
168- @arguments = nil
174+ @form_data = FORM_DATA_UNDEFINED
175+ @form_data_effective_options = nil
169176 end
170177
171178 # Whether the request method is POST.
@@ -208,14 +215,26 @@ def query_arguments
208215 @query_arguments ||= decode_arguments ( self . query )
209216 end
210217
211- # Decoded form arguments, when the request has a supported form content type.
212- def form_arguments
213- @form_arguments ||= decode_form_arguments
214- end
215-
216- # Decoded query and form arguments. Form arguments take precedence on collision.
217- def arguments
218- @arguments ||= self . query_arguments . merge ( self . form_arguments )
218+ # Decode form data using the request defaults and any endpoint-specific overrides.
219+ #
220+ # @parameter options [Hash] Endpoint-specific form-data parsing options.
221+ # @returns [Hash] The decoded fields and uploads, or an empty hash for an unsupported content type.
222+ def form_data ( **options )
223+ effective_options = @form_data_options . merge ( options )
224+
225+ unless @form_data . equal? ( FORM_DATA_UNDEFINED )
226+ if options . any? and effective_options != @form_data_effective_options
227+ raise ArgumentError , "Form data has already been decoded with different options!"
228+ end
229+
230+ return @form_data
231+ end
232+
233+ form_data = decode_form_data ( effective_options )
234+ @form_data_effective_options = effective_options . freeze
235+ @form_data = form_data
236+
237+ return form_data
219238 end
220239
221240 # Decoded request cookies.
@@ -272,7 +291,7 @@ def with(method: self.method, path: self.path, path_info: nil)
272291 delegate = @delegate . dup
273292 delegate . method = method
274293
275- request = self . class . new ( delegate , request_path : self . request_path )
294+ request = self . class . new ( delegate , request_path : self . request_path , form_data_options : @form_data_options )
276295 request . session = @session
277296 request . variables = @variables
278297 request . locale = @locale
@@ -316,54 +335,55 @@ def decode_arguments(query)
316335 return Protocol ::URL ::Encoding . decode ( query . gsub ( "+" , "%20" ) , MAXIMUM_ARGUMENT_DEPTH )
317336 end
318337
319- def decode_form_arguments
320- content_type , parameters = parse_header ( self . headers [ "content-type" ] )
338+ def decode_form_data ( options )
339+ value = self . headers [ "content-type" ]
340+ return { } unless value
321341
322- case content_type
342+ content_type = Protocol ::Multipart ::Header ::ContentType . coerce ( value )
343+
344+ case content_type . type
323345 when FORM_URL_ENCODED
324- return decode_arguments ( read_body )
346+ maximum_size = options . fetch ( :maximum_total_size , MAXIMUM_URL_ENCODED_SIZE )
347+ return decode_arguments ( read_body ( maximum_size ) )
325348 when MULTIPART_FORM_DATA
326- boundary = parameters [ "boundary" ]
349+ boundary = content_type [ "boundary" ]
327350
328351 unless boundary
329352 raise ArgumentError , "Multipart form data is missing a boundary!"
330353 end
331354
332- return decode_multipart_form ( boundary )
355+ return decode_multipart_form ( boundary , ** options )
333356 else
334357 return { }
335358 end
336359 end
337360
338- def read_body
361+ def read_body ( maximum_size )
362+ content = String . new . b
363+ limit = Protocol ::Multipart ::ByteLimit . new ( maximum_size , name : :form_size )
364+
339365 if body = self . body
340- return body . join || String . new
366+ while chunk = body . read
367+ limit . consume ( chunk . bytesize )
368+ content << chunk
369+ end
341370 end
342371
343- return String . new
372+ return content
344373 end
345374
346- def decode_multipart_form ( boundary )
375+ def decode_multipart_form ( boundary , ** options )
347376 arguments = { }
348377 body = self . body
349378
350379 return arguments unless body
351380
352381 io = BodyIO . new ( body )
353- parser = Protocol ::Multipart ::Parser . new ( io , boundary )
354382
355383 begin
356- parser . each do |part |
357- disposition , parameters = parse_header ( part . headers [ "content-disposition" ] )
358-
359- unless disposition == "form-data" and name = parameters [ "name" ]
360- raise ArgumentError , "Multipart form part is missing a form-data name!"
361- end
362-
363- if filename = parameters [ "filename" ]
364- value = create_upload ( part , filename )
365- else
366- value = read_part ( part )
384+ Protocol ::Multipart ::FormData . parse ( io , boundary , **options ) do |name , value |
385+ if value . is_a? ( Protocol ::Multipart ::FormData ::Upload )
386+ value = create_upload ( value )
367387 end
368388
369389 assign_argument ( arguments , name , value )
@@ -375,24 +395,16 @@ def decode_multipart_form(boundary)
375395 return arguments
376396 end
377397
378- def read_part ( part )
379- content = String . new . b
380- part . each { |chunk | content << chunk }
381- return content
382- end
383-
384- def create_upload ( part , filename )
398+ def create_upload ( upload )
385399 tempfile = Tempfile . new ( "utopia-upload" , binmode : true )
386- size = 0
387400
388401 begin
389- part . each do |chunk |
402+ upload . each do |chunk |
390403 tempfile . write ( chunk )
391- size += chunk . bytesize
392404 end
393405
394406 tempfile . rewind
395- return Upload . new ( part . headers , filename , tempfile , size )
407+ return Upload . new ( upload . headers , upload . filename , tempfile , upload . size )
396408 rescue
397409 tempfile . close!
398410 raise
@@ -413,21 +425,6 @@ def assign_argument(arguments, name, value)
413425 Protocol ::URL ::Encoding . assign ( keys , value , arguments )
414426 end
415427
416- PARAMETER = /;\s *([!#$%&'*+\- .^_`|~0-9A-Za-z]+)\s *=\s *(?:"((?:\\ .|[^"])*)"|([^;\s ]*))/ . freeze
417-
418- def parse_header ( value )
419- return [ nil , { } ] unless value
420-
421- value = value . first if value . is_a? ( Array )
422- parameters = { }
423-
424- value . scan ( PARAMETER ) do |name , quoted , token |
425- parameters [ name . downcase ] = quoted ? quoted . gsub ( /\\ (.)/ , "\\ 1" ) : token
426- end
427-
428- return [ value . split ( ";" , 2 ) . first . strip . downcase , parameters ]
429- end
430-
431428 def parse_cookies ( cookie_header )
432429 cookies = { }
433430
0 commit comments