kjvarga-rack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. data/COPYING +18 -0
  2. data/KNOWN-ISSUES.rdoc +18 -0
  3. data/Manifest +117 -0
  4. data/README.rdoc +357 -0
  5. data/Rakefile +164 -0
  6. data/bin/rackup +176 -0
  7. data/contrib/rack_logo.svg +111 -0
  8. data/lib/rack.rb +90 -0
  9. data/lib/rack/adapter/camping.rb +22 -0
  10. data/lib/rack/auth/abstract/handler.rb +37 -0
  11. data/lib/rack/auth/abstract/request.rb +37 -0
  12. data/lib/rack/auth/basic.rb +58 -0
  13. data/lib/rack/auth/digest/md5.rb +124 -0
  14. data/lib/rack/auth/digest/nonce.rb +51 -0
  15. data/lib/rack/auth/digest/params.rb +55 -0
  16. data/lib/rack/auth/digest/request.rb +40 -0
  17. data/lib/rack/auth/openid.rb +487 -0
  18. data/lib/rack/builder.rb +63 -0
  19. data/lib/rack/cascade.rb +41 -0
  20. data/lib/rack/chunked.rb +49 -0
  21. data/lib/rack/commonlogger.rb +52 -0
  22. data/lib/rack/conditionalget.rb +47 -0
  23. data/lib/rack/content_length.rb +29 -0
  24. data/lib/rack/content_type.rb +23 -0
  25. data/lib/rack/deflater.rb +96 -0
  26. data/lib/rack/directory.rb +153 -0
  27. data/lib/rack/file.rb +88 -0
  28. data/lib/rack/handler.rb +69 -0
  29. data/lib/rack/handler/cgi.rb +61 -0
  30. data/lib/rack/handler/evented_mongrel.rb +8 -0
  31. data/lib/rack/handler/fastcgi.rb +88 -0
  32. data/lib/rack/handler/lsws.rb +60 -0
  33. data/lib/rack/handler/mongrel.rb +87 -0
  34. data/lib/rack/handler/scgi.rb +62 -0
  35. data/lib/rack/handler/swiftiplied_mongrel.rb +8 -0
  36. data/lib/rack/handler/thin.rb +18 -0
  37. data/lib/rack/handler/webrick.rb +71 -0
  38. data/lib/rack/head.rb +19 -0
  39. data/lib/rack/lint.rb +546 -0
  40. data/lib/rack/lobster.rb +65 -0
  41. data/lib/rack/lock.rb +16 -0
  42. data/lib/rack/methodoverride.rb +27 -0
  43. data/lib/rack/mime.rb +205 -0
  44. data/lib/rack/mock.rb +187 -0
  45. data/lib/rack/recursive.rb +57 -0
  46. data/lib/rack/reloader.rb +109 -0
  47. data/lib/rack/request.rb +248 -0
  48. data/lib/rack/response.rb +183 -0
  49. data/lib/rack/rewindable_input.rb +100 -0
  50. data/lib/rack/session/abstract/id.rb +142 -0
  51. data/lib/rack/session/cookie.rb +91 -0
  52. data/lib/rack/session/memcache.rb +109 -0
  53. data/lib/rack/session/pool.rb +100 -0
  54. data/lib/rack/showexceptions.rb +349 -0
  55. data/lib/rack/showstatus.rb +106 -0
  56. data/lib/rack/static.rb +38 -0
  57. data/lib/rack/urlmap.rb +55 -0
  58. data/lib/rack/utils.rb +528 -0
  59. data/rack.gemspec +140 -0
  60. data/test/cgi/lighttpd.conf +20 -0
  61. data/test/cgi/test +9 -0
  62. data/test/cgi/test.fcgi +8 -0
  63. data/test/cgi/test.ru +7 -0
  64. data/test/multipart/binary +0 -0
  65. data/test/multipart/empty +10 -0
  66. data/test/multipart/file1.txt +1 -0
  67. data/test/multipart/ie +6 -0
  68. data/test/multipart/nested +10 -0
  69. data/test/multipart/none +9 -0
  70. data/test/multipart/text +10 -0
  71. data/test/spec_rack_auth_basic.rb +73 -0
  72. data/test/spec_rack_auth_digest.rb +226 -0
  73. data/test/spec_rack_auth_openid.rb +84 -0
  74. data/test/spec_rack_builder.rb +84 -0
  75. data/test/spec_rack_camping.rb +51 -0
  76. data/test/spec_rack_cascade.rb +48 -0
  77. data/test/spec_rack_cgi.rb +89 -0
  78. data/test/spec_rack_chunked.rb +62 -0
  79. data/test/spec_rack_commonlogger.rb +61 -0
  80. data/test/spec_rack_conditionalget.rb +41 -0
  81. data/test/spec_rack_content_length.rb +43 -0
  82. data/test/spec_rack_content_type.rb +30 -0
  83. data/test/spec_rack_deflater.rb +127 -0
  84. data/test/spec_rack_directory.rb +61 -0
  85. data/test/spec_rack_fastcgi.rb +89 -0
  86. data/test/spec_rack_file.rb +75 -0
  87. data/test/spec_rack_handler.rb +43 -0
  88. data/test/spec_rack_head.rb +30 -0
  89. data/test/spec_rack_lint.rb +521 -0
  90. data/test/spec_rack_lobster.rb +45 -0
  91. data/test/spec_rack_lock.rb +38 -0
  92. data/test/spec_rack_methodoverride.rb +60 -0
  93. data/test/spec_rack_mock.rb +243 -0
  94. data/test/spec_rack_mongrel.rb +189 -0
  95. data/test/spec_rack_recursive.rb +77 -0
  96. data/test/spec_rack_request.rb +504 -0
  97. data/test/spec_rack_response.rb +218 -0
  98. data/test/spec_rack_rewindable_input.rb +118 -0
  99. data/test/spec_rack_session_cookie.rb +82 -0
  100. data/test/spec_rack_session_memcache.rb +250 -0
  101. data/test/spec_rack_session_pool.rb +172 -0
  102. data/test/spec_rack_showexceptions.rb +21 -0
  103. data/test/spec_rack_showstatus.rb +72 -0
  104. data/test/spec_rack_static.rb +37 -0
  105. data/test/spec_rack_thin.rb +91 -0
  106. data/test/spec_rack_urlmap.rb +185 -0
  107. data/test/spec_rack_utils.rb +467 -0
  108. data/test/spec_rack_webrick.rb +130 -0
  109. data/test/testrequest.rb +57 -0
  110. data/test/unregistered_handler/rack/handler/unregistered.rb +7 -0
  111. data/test/unregistered_handler/rack/handler/unregistered_long_one.rb +7 -0
  112. metadata +175 -0
@@ -0,0 +1,38 @@
1
+ module Rack
2
+
3
+ # The Rack::Static middleware intercepts requests for static files
4
+ # (javascript files, images, stylesheets, etc) based on the url prefixes
5
+ # passed in the options, and serves them using a Rack::File object. This
6
+ # allows a Rack stack to serve both static and dynamic content.
7
+ #
8
+ # Examples:
9
+ # use Rack::Static, :urls => ["/media"]
10
+ # will serve all requests beginning with /media from the "media" folder
11
+ # located in the current directory (ie media/*).
12
+ #
13
+ # use Rack::Static, :urls => ["/css", "/images"], :root => "public"
14
+ # will serve all requests beginning with /css or /images from the folder
15
+ # "public" in the current directory (ie public/css/* and public/images/*)
16
+
17
+ class Static
18
+
19
+ def initialize(app, options={})
20
+ @app = app
21
+ @urls = options[:urls] || ["/favicon.ico"]
22
+ root = options[:root] || Dir.pwd
23
+ @file_server = Rack::File.new(root)
24
+ end
25
+
26
+ def call(env)
27
+ path = env["PATH_INFO"]
28
+ can_serve = @urls.any? { |url| path.index(url) == 0 }
29
+
30
+ if can_serve
31
+ @file_server.call(env)
32
+ else
33
+ @app.call(env)
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,55 @@
1
+ module Rack
2
+ # Rack::URLMap takes a hash mapping urls or paths to apps, and
3
+ # dispatches accordingly. Support for HTTP/1.1 host names exists if
4
+ # the URLs start with <tt>http://</tt> or <tt>https://</tt>.
5
+ #
6
+ # URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part
7
+ # relevant for dispatch is in the SCRIPT_NAME, and the rest in the
8
+ # PATH_INFO. This should be taken care of when you need to
9
+ # reconstruct the URL in order to create links.
10
+ #
11
+ # URLMap dispatches in such a way that the longest paths are tried
12
+ # first, since they are most specific.
13
+
14
+ class URLMap
15
+ def initialize(map = {})
16
+ remap(map)
17
+ end
18
+
19
+ def remap(map)
20
+ @mapping = map.map { |location, app|
21
+ if location =~ %r{\Ahttps?://(.*?)(/.*)}
22
+ host, location = $1, $2
23
+ else
24
+ host = nil
25
+ end
26
+
27
+ unless location[0] == ?/
28
+ raise ArgumentError, "paths need to start with /"
29
+ end
30
+ location = location.chomp('/')
31
+
32
+ [host, location, app]
33
+ }.sort_by { |(h, l, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
34
+ end
35
+
36
+ def call(env)
37
+ path = env["PATH_INFO"].to_s.squeeze("/")
38
+ script_name = env['SCRIPT_NAME']
39
+ hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT')
40
+ @mapping.each { |host, location, app|
41
+ next unless (hHost == host || sName == host \
42
+ || (host.nil? && (hHost == sName || hHost == sName+':'+sPort)))
43
+ next unless location == path[0, location.size]
44
+ next unless path[location.size] == nil || path[location.size] == ?/
45
+
46
+ return app.call(
47
+ env.merge(
48
+ 'SCRIPT_NAME' => (script_name + location),
49
+ 'PATH_INFO' => path[location.size..-1]))
50
+ }
51
+ [404, {"Content-Type" => "text/plain"}, ["Not Found: #{path}"]]
52
+ end
53
+ end
54
+ end
55
+
data/lib/rack/utils.rb ADDED
@@ -0,0 +1,528 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ require 'set'
4
+ require 'tempfile'
5
+
6
+ module Rack
7
+ # Rack::Utils contains a grab-bag of useful methods for writing web
8
+ # applications adopted from all kinds of Ruby libraries.
9
+
10
+ module Utils
11
+ # Performs URI escaping so that you can construct proper
12
+ # query strings faster. Use this rather than the cgi.rb
13
+ # version since it's faster. (Stolen from Camping).
14
+ def escape(s)
15
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
16
+ '%'+$1.unpack('H2'*$1.size).join('%').upcase
17
+ }.tr(' ', '+')
18
+ end
19
+ module_function :escape
20
+
21
+ # Unescapes a URI escaped string. (Stolen from Camping).
22
+ def unescape(s)
23
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
24
+ [$1.delete('%')].pack('H*')
25
+ }
26
+ end
27
+ module_function :unescape
28
+
29
+ DEFAULT_SEP = /[&;] */n
30
+
31
+ # Stolen from Mongrel, with some small modifications:
32
+ # Parses a query string by breaking it up at the '&'
33
+ # and ';' characters. You can also use this to parse
34
+ # cookies by changing the characters used in the second
35
+ # parameter (which defaults to '&;').
36
+ def parse_query(qs, d = nil)
37
+ params = {}
38
+
39
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
40
+ k, v = p.split('=', 2).map { |x| unescape(x) }
41
+
42
+ if cur = params[k]
43
+ if cur.class == Array
44
+ params[k] << v
45
+ else
46
+ params[k] = [cur, v]
47
+ end
48
+ else
49
+ params[k] = v
50
+ end
51
+ end
52
+
53
+ return params
54
+ end
55
+ module_function :parse_query
56
+
57
+ def parse_nested_query(qs, d = nil)
58
+ params = {}
59
+
60
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
61
+ k, v = unescape(p).split('=', 2)
62
+ normalize_params(params, k, v)
63
+ end
64
+
65
+ return params
66
+ end
67
+ module_function :parse_nested_query
68
+
69
+ def normalize_params(params, name, v = nil)
70
+ name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
71
+ k = $1 || ''
72
+ after = $' || ''
73
+
74
+ return if k.empty?
75
+
76
+ if after == ""
77
+ params[k] = v
78
+ elsif after == "[]"
79
+ params[k] ||= []
80
+ raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
81
+ params[k] << v
82
+ elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
83
+ child_key = $1
84
+ params[k] ||= []
85
+ raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
86
+ if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
87
+ normalize_params(params[k].last, child_key, v)
88
+ else
89
+ params[k] << normalize_params({}, child_key, v)
90
+ end
91
+ else
92
+ params[k] ||= {}
93
+ raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
94
+ params[k] = normalize_params(params[k], after, v)
95
+ end
96
+
97
+ return params
98
+ end
99
+ module_function :normalize_params
100
+
101
+ def build_query(params)
102
+ params.map { |k, v|
103
+ if v.class == Array
104
+ build_query(v.map { |x| [k, x] })
105
+ else
106
+ "#{escape(k)}=#{escape(v)}"
107
+ end
108
+ }.join("&")
109
+ end
110
+ module_function :build_query
111
+
112
+ def build_nested_query(value, prefix = nil)
113
+ case value
114
+ when Array
115
+ value.map { |v|
116
+ build_nested_query(v, "#{prefix}[]")
117
+ }.join("&")
118
+ when Hash
119
+ value.map { |k, v|
120
+ build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
121
+ }.join("&")
122
+ when String
123
+ raise ArgumentError, "value must be a Hash" if prefix.nil?
124
+ "#{prefix}=#{escape(value)}"
125
+ else
126
+ prefix
127
+ end
128
+ end
129
+ module_function :build_nested_query
130
+
131
+ # Escape ampersands, brackets and quotes to their HTML/XML entities.
132
+ def escape_html(string)
133
+ string.to_s.gsub("&", "&amp;").
134
+ gsub("<", "&lt;").
135
+ gsub(">", "&gt;").
136
+ gsub("'", "&#39;").
137
+ gsub('"', "&quot;")
138
+ end
139
+ module_function :escape_html
140
+
141
+ def select_best_encoding(available_encodings, accept_encoding)
142
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
143
+
144
+ expanded_accept_encoding =
145
+ accept_encoding.map { |m, q|
146
+ if m == "*"
147
+ (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
148
+ else
149
+ [[m, q]]
150
+ end
151
+ }.inject([]) { |mem, list|
152
+ mem + list
153
+ }
154
+
155
+ encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
156
+
157
+ unless encoding_candidates.include?("identity")
158
+ encoding_candidates.push("identity")
159
+ end
160
+
161
+ expanded_accept_encoding.find_all { |m, q|
162
+ q == 0.0
163
+ }.each { |m, _|
164
+ encoding_candidates.delete(m)
165
+ }
166
+
167
+ return (encoding_candidates & available_encodings)[0]
168
+ end
169
+ module_function :select_best_encoding
170
+
171
+ # Return the bytesize of String; uses String#length under Ruby 1.8 and
172
+ # String#bytesize under 1.9.
173
+ if ''.respond_to?(:bytesize)
174
+ def bytesize(string)
175
+ string.bytesize
176
+ end
177
+ else
178
+ def bytesize(string)
179
+ string.size
180
+ end
181
+ end
182
+ module_function :bytesize
183
+
184
+ # Context allows the use of a compatible middleware at different points
185
+ # in a request handling stack. A compatible middleware must define
186
+ # #context which should take the arguments env and app. The first of which
187
+ # would be the request environment. The second of which would be the rack
188
+ # application that the request would be forwarded to.
189
+ class Context
190
+ attr_reader :for, :app
191
+
192
+ def initialize(app_f, app_r)
193
+ raise 'running context does not respond to #context' unless app_f.respond_to? :context
194
+ @for, @app = app_f, app_r
195
+ end
196
+
197
+ def call(env)
198
+ @for.context(env, @app)
199
+ end
200
+
201
+ def recontext(app)
202
+ self.class.new(@for, app)
203
+ end
204
+
205
+ def context(env, app=@app)
206
+ recontext(app).call(env)
207
+ end
208
+ end
209
+
210
+ # A case-insensitive Hash that preserves the original case of a
211
+ # header when set.
212
+ class HeaderHash < Hash
213
+ def initialize(hash={})
214
+ super()
215
+ @names = {}
216
+ hash.each { |k, v| self[k] = v }
217
+ end
218
+
219
+ def to_hash
220
+ inject({}) do |hash, (k,v)|
221
+ if v.respond_to? :to_ary
222
+ hash[k] = v.to_ary.join("\n")
223
+ else
224
+ hash[k] = v
225
+ end
226
+ hash
227
+ end
228
+ end
229
+
230
+ def [](k)
231
+ super(@names[k] ||= @names[k.downcase])
232
+ end
233
+
234
+ def []=(k, v)
235
+ delete k
236
+ @names[k] = @names[k.downcase] = k
237
+ super k, v
238
+ end
239
+
240
+ def delete(k)
241
+ canonical = k.downcase
242
+ result = super @names.delete(canonical)
243
+ @names.delete_if { |name,| name.downcase == canonical }
244
+ result
245
+ end
246
+
247
+ def include?(k)
248
+ @names.include?(k) || @names.include?(k.downcase)
249
+ end
250
+
251
+ alias_method :has_key?, :include?
252
+ alias_method :member?, :include?
253
+ alias_method :key?, :include?
254
+
255
+ def merge!(other)
256
+ other.each { |k, v| self[k] = v }
257
+ self
258
+ end
259
+
260
+ def merge(other)
261
+ hash = dup
262
+ hash.merge! other
263
+ end
264
+
265
+ def replace(other)
266
+ clear
267
+ other.each { |k, v| self[k] = v }
268
+ self
269
+ end
270
+ end
271
+
272
+ # Every standard HTTP code mapped to the appropriate message.
273
+ # Stolen from Mongrel.
274
+ HTTP_STATUS_CODES = {
275
+ 100 => 'Continue',
276
+ 101 => 'Switching Protocols',
277
+ 200 => 'OK',
278
+ 201 => 'Created',
279
+ 202 => 'Accepted',
280
+ 203 => 'Non-Authoritative Information',
281
+ 204 => 'No Content',
282
+ 205 => 'Reset Content',
283
+ 206 => 'Partial Content',
284
+ 300 => 'Multiple Choices',
285
+ 301 => 'Moved Permanently',
286
+ 302 => 'Found',
287
+ 303 => 'See Other',
288
+ 304 => 'Not Modified',
289
+ 305 => 'Use Proxy',
290
+ 307 => 'Temporary Redirect',
291
+ 400 => 'Bad Request',
292
+ 401 => 'Unauthorized',
293
+ 402 => 'Payment Required',
294
+ 403 => 'Forbidden',
295
+ 404 => 'Not Found',
296
+ 405 => 'Method Not Allowed',
297
+ 406 => 'Not Acceptable',
298
+ 407 => 'Proxy Authentication Required',
299
+ 408 => 'Request Timeout',
300
+ 409 => 'Conflict',
301
+ 410 => 'Gone',
302
+ 411 => 'Length Required',
303
+ 412 => 'Precondition Failed',
304
+ 413 => 'Request Entity Too Large',
305
+ 414 => 'Request-URI Too Large',
306
+ 415 => 'Unsupported Media Type',
307
+ 416 => 'Requested Range Not Satisfiable',
308
+ 417 => 'Expectation Failed',
309
+ 500 => 'Internal Server Error',
310
+ 501 => 'Not Implemented',
311
+ 502 => 'Bad Gateway',
312
+ 503 => 'Service Unavailable',
313
+ 504 => 'Gateway Timeout',
314
+ 505 => 'HTTP Version Not Supported'
315
+ }
316
+
317
+ # Responses with HTTP status codes that should not have an entity body
318
+ STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
319
+
320
+ # A multipart form data parser, adapted from IOWA.
321
+ #
322
+ # Usually, Rack::Request#POST takes care of calling this.
323
+
324
+ module Multipart
325
+ class UploadedFile
326
+ # The filename, *not* including the path, of the "uploaded" file
327
+ attr_reader :original_filename
328
+
329
+ # The content type of the "uploaded" file
330
+ attr_accessor :content_type
331
+
332
+ def initialize(path, content_type = "text/plain", binary = false)
333
+ raise "#{path} file does not exist" unless ::File.exist?(path)
334
+ @content_type = content_type
335
+ @original_filename = ::File.basename(path)
336
+ @tempfile = Tempfile.new(@original_filename)
337
+ @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
338
+ @tempfile.binmode if binary
339
+ FileUtils.copy_file(path, @tempfile.path)
340
+ end
341
+
342
+ def path
343
+ @tempfile.path
344
+ end
345
+ alias_method :local_path, :path
346
+
347
+ def method_missing(method_name, *args, &block) #:nodoc:
348
+ @tempfile.__send__(method_name, *args, &block)
349
+ end
350
+ end
351
+
352
+ EOL = "\r\n"
353
+ MULTIPART_BOUNDARY = "AaB03x"
354
+
355
+ def self.parse_multipart(env)
356
+ unless env['CONTENT_TYPE'] =~
357
+ %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
358
+ nil
359
+ else
360
+ boundary = "--#{$1}"
361
+
362
+ params = {}
363
+ buf = ""
364
+ content_length = env['CONTENT_LENGTH'].to_i
365
+ input = env['rack.input']
366
+ input.rewind
367
+
368
+ boundary_size = Utils.bytesize(boundary) + EOL.size
369
+ bufsize = 16384
370
+
371
+ content_length -= boundary_size
372
+
373
+ read_buffer = ''
374
+
375
+ status = input.read(boundary_size, read_buffer)
376
+ raise EOFError, "bad content body" unless status == boundary + EOL
377
+
378
+ rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
379
+
380
+ loop {
381
+ head = nil
382
+ body = ''
383
+ filename = content_type = name = nil
384
+
385
+ until head && buf =~ rx
386
+ if !head && i = buf.index(EOL+EOL)
387
+ head = buf.slice!(0, i+2) # First \r\n
388
+ buf.slice!(0, 2) # Second \r\n
389
+
390
+ filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1]
391
+ content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
392
+ name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
393
+
394
+ if content_type || filename
395
+ body = Tempfile.new("RackMultipart")
396
+ body.binmode if body.respond_to?(:binmode)
397
+ end
398
+
399
+ next
400
+ end
401
+
402
+ # Save the read body part.
403
+ if head && (boundary_size+4 < buf.size)
404
+ body << buf.slice!(0, buf.size - (boundary_size+4))
405
+ end
406
+
407
+ c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
408
+ raise EOFError, "bad content body" if c.nil? || c.empty?
409
+ buf << c
410
+ content_length -= c.size
411
+ end
412
+
413
+ # Save the rest.
414
+ if i = buf.index(rx)
415
+ body << buf.slice!(0, i)
416
+ buf.slice!(0, boundary_size+2)
417
+
418
+ content_length = -1 if $1 == "--"
419
+ end
420
+
421
+ if filename == ""
422
+ # filename is blank which means no file has been selected
423
+ data = nil
424
+ elsif filename
425
+ body.rewind
426
+
427
+ # Take the basename of the upload's original filename.
428
+ # This handles the full Windows paths given by Internet Explorer
429
+ # (and perhaps other broken user agents) without affecting
430
+ # those which give the lone filename.
431
+ filename =~ /^(?:.*[:\\\/])?(.*)/m
432
+ filename = $1
433
+
434
+ data = {:filename => filename, :type => content_type,
435
+ :name => name, :tempfile => body, :head => head}
436
+ elsif !filename && content_type
437
+ body.rewind
438
+
439
+ # Generic multipart cases, not coming from a form
440
+ data = {:type => content_type,
441
+ :name => name, :tempfile => body, :head => head}
442
+ else
443
+ data = body
444
+ end
445
+
446
+ Utils.normalize_params(params, name, data) unless data.nil?
447
+
448
+ break if buf.empty? || content_length == -1
449
+ }
450
+
451
+ input.rewind
452
+
453
+ params
454
+ end
455
+ end
456
+
457
+ def self.build_multipart(params, first = true)
458
+ if first
459
+ unless params.is_a?(Hash)
460
+ raise ArgumentError, "value must be a Hash"
461
+ end
462
+
463
+ multipart = false
464
+ query = lambda { |value|
465
+ case value
466
+ when Array
467
+ value.each(&query)
468
+ when Hash
469
+ value.values.each(&query)
470
+ when UploadedFile
471
+ multipart = true
472
+ end
473
+ }
474
+ params.values.each(&query)
475
+ return nil unless multipart
476
+ end
477
+
478
+ flattened_params = Hash.new
479
+
480
+ params.each do |key, value|
481
+ k = first ? key.to_s : "[#{key}]"
482
+
483
+ case value
484
+ when Array
485
+ value.map { |v|
486
+ build_multipart(v, false).each { |subkey, subvalue|
487
+ flattened_params["#{k}[]#{subkey}"] = subvalue
488
+ }
489
+ }
490
+ when Hash
491
+ build_multipart(value, false).each { |subkey, subvalue|
492
+ flattened_params[k + subkey] = subvalue
493
+ }
494
+ else
495
+ flattened_params[k] = value
496
+ end
497
+ end
498
+
499
+ if first
500
+ flattened_params.map { |name, file|
501
+ if file.respond_to?(:original_filename)
502
+ ::File.open(file.path, "rb") do |f|
503
+ f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
504
+ <<-EOF
505
+ --#{MULTIPART_BOUNDARY}\r
506
+ Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
507
+ Content-Type: #{file.content_type}\r
508
+ Content-Length: #{::File.stat(file.path).size}\r
509
+ \r
510
+ #{f.read}\r
511
+ EOF
512
+ end
513
+ else
514
+ <<-EOF
515
+ --#{MULTIPART_BOUNDARY}\r
516
+ Content-Disposition: form-data; name="#{name}"\r
517
+ \r
518
+ #{file}\r
519
+ EOF
520
+ end
521
+ }.join + "--#{MULTIPART_BOUNDARY}--\r"
522
+ else
523
+ flattened_params
524
+ end
525
+ end
526
+ end
527
+ end
528
+ end