egalite 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. data/.gitignore +17 -0
  2. data/README.md +91 -0
  3. data/auth/basic.rb +32 -0
  4. data/blank.rb +53 -0
  5. data/egalite.rb +742 -0
  6. data/errorconsole.rb +77 -0
  7. data/examples/simple/example.rb +39 -0
  8. data/examples/simple/pages/test.html +15 -0
  9. data/examples/simple_db/example_db.rb +103 -0
  10. data/examples/simple_db/pages/edit.html +6 -0
  11. data/helper.rb +251 -0
  12. data/keitai/keitai.rb +107 -0
  13. data/keitai/ketai.rb +11 -0
  14. data/keitai/rack/ketai/carrier/abstract.rb +131 -0
  15. data/keitai/rack/ketai/carrier/au.rb +78 -0
  16. data/keitai/rack/ketai/carrier/docomo.rb +80 -0
  17. data/keitai/rack/ketai/carrier/emoji/ausjisstrtoemojiid.rb +1391 -0
  18. data/keitai/rack/ketai/carrier/emoji/docomosjisstrtoemojiid.rb +759 -0
  19. data/keitai/rack/ketai/carrier/emoji/emojidata.rb +836 -0
  20. data/keitai/rack/ketai/carrier/emoji/softbankutf8strtoemojiid.rb +1119 -0
  21. data/keitai/rack/ketai/carrier/emoji/softbankwebcodetoutf8str.rb +499 -0
  22. data/keitai/rack/ketai/carrier/iphone.rb +8 -0
  23. data/keitai/rack/ketai/carrier/softbank.rb +82 -0
  24. data/keitai/rack/ketai/carrier.rb +17 -0
  25. data/keitai/rack/ketai/middleware.rb +24 -0
  26. data/m17n.rb +193 -0
  27. data/rack/auth/abstract/handler.rb +37 -0
  28. data/rack/auth/abstract/request.rb +37 -0
  29. data/rack/auth/basic.rb +58 -0
  30. data/rack/auth/digest/md5.rb +124 -0
  31. data/rack/auth/digest/nonce.rb +51 -0
  32. data/rack/auth/digest/params.rb +55 -0
  33. data/rack/auth/digest/request.rb +40 -0
  34. data/rack/builder.rb +80 -0
  35. data/rack/cascade.rb +41 -0
  36. data/rack/chunked.rb +49 -0
  37. data/rack/commonlogger.rb +49 -0
  38. data/rack/conditionalget.rb +47 -0
  39. data/rack/config.rb +15 -0
  40. data/rack/content_length.rb +29 -0
  41. data/rack/content_type.rb +23 -0
  42. data/rack/deflater.rb +96 -0
  43. data/rack/directory.rb +157 -0
  44. data/rack/etag.rb +32 -0
  45. data/rack/file.rb +92 -0
  46. data/rack/handler/cgi.rb +62 -0
  47. data/rack/handler/evented_mongrel.rb +8 -0
  48. data/rack/handler/fastcgi.rb +89 -0
  49. data/rack/handler/lsws.rb +63 -0
  50. data/rack/handler/mongrel.rb +90 -0
  51. data/rack/handler/scgi.rb +59 -0
  52. data/rack/handler/swiftiplied_mongrel.rb +8 -0
  53. data/rack/handler/thin.rb +18 -0
  54. data/rack/handler/webrick.rb +73 -0
  55. data/rack/handler.rb +88 -0
  56. data/rack/head.rb +19 -0
  57. data/rack/lint.rb +567 -0
  58. data/rack/lobster.rb +65 -0
  59. data/rack/lock.rb +16 -0
  60. data/rack/logger.rb +20 -0
  61. data/rack/methodoverride.rb +27 -0
  62. data/rack/mime.rb +208 -0
  63. data/rack/mock.rb +190 -0
  64. data/rack/nulllogger.rb +18 -0
  65. data/rack/recursive.rb +61 -0
  66. data/rack/reloader.rb +109 -0
  67. data/rack/request.rb +273 -0
  68. data/rack/response.rb +150 -0
  69. data/rack/rewindable_input.rb +103 -0
  70. data/rack/runtime.rb +27 -0
  71. data/rack/sendfile.rb +144 -0
  72. data/rack/server.rb +271 -0
  73. data/rack/session/abstract/id.rb +140 -0
  74. data/rack/session/cookie.rb +90 -0
  75. data/rack/session/memcache.rb +119 -0
  76. data/rack/session/pool.rb +100 -0
  77. data/rack/showexceptions.rb +349 -0
  78. data/rack/showstatus.rb +106 -0
  79. data/rack/static.rb +38 -0
  80. data/rack/urlmap.rb +55 -0
  81. data/rack/utils.rb +662 -0
  82. data/rack.rb +81 -0
  83. data/route.rb +231 -0
  84. data/sendmail.rb +222 -0
  85. data/sequel_helper.rb +20 -0
  86. data/session.rb +132 -0
  87. data/stringify_hash.rb +63 -0
  88. data/support.rb +35 -0
  89. data/template.rb +287 -0
  90. data/test/french.html +13 -0
  91. data/test/french_msg.html +3 -0
  92. data/test/m17n.txt +30 -0
  93. data/test/mobile.html +15 -0
  94. data/test/setup.rb +8 -0
  95. data/test/static/test.txt +1 -0
  96. data/test/template.html +58 -0
  97. data/test/template_inner.html +1 -0
  98. data/test/template_innerparam.html +1 -0
  99. data/test/test_auth.rb +43 -0
  100. data/test/test_blank.rb +44 -0
  101. data/test/test_csrf.rb +87 -0
  102. data/test/test_errorconsole.rb +91 -0
  103. data/test/test_handler.rb +155 -0
  104. data/test/test_helper.rb +296 -0
  105. data/test/test_keitai.rb +107 -0
  106. data/test/test_m17n.rb +129 -0
  107. data/test/test_route.rb +192 -0
  108. data/test/test_sendmail.rb +146 -0
  109. data/test/test_session.rb +83 -0
  110. data/test/test_stringify_hash.rb +67 -0
  111. data/test/test_template.rb +114 -0
  112. data/test.bat +2 -0
  113. metadata +240 -0
data/rack/utils.rb ADDED
@@ -0,0 +1,662 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ require 'fileutils'
4
+ require 'set'
5
+ require 'tempfile'
6
+
7
+ module Rack
8
+ # Rack::Utils contains a grab-bag of useful methods for writing web
9
+ # applications adopted from all kinds of Ruby libraries.
10
+
11
+ module Utils
12
+ # Performs URI escaping so that you can construct proper
13
+ # query strings faster. Use this rather than the cgi.rb
14
+ # version since it's faster. (Stolen from Camping).
15
+ def escape(s)
16
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
17
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
18
+ }.tr(' ', '+')
19
+ end
20
+ module_function :escape
21
+
22
+ # Unescapes a URI escaped string. (Stolen from Camping).
23
+ def unescape(s)
24
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
25
+ [$1.delete('%')].pack('H*')
26
+ }
27
+ end
28
+ module_function :unescape
29
+
30
+ DEFAULT_SEP = /[&;] */n
31
+
32
+ # Stolen from Mongrel, with some small modifications:
33
+ # Parses a query string by breaking it up at the '&'
34
+ # and ';' characters. You can also use this to parse
35
+ # cookies by changing the characters used in the second
36
+ # parameter (which defaults to '&;').
37
+ def parse_query(qs, d = nil)
38
+ params = {}
39
+
40
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
41
+ k, v = p.split('=', 2).map { |x| unescape(x) }
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
+ def set_cookie_header!(header, key, value)
172
+ case value
173
+ when Hash
174
+ domain = "; domain=" + value[:domain] if value[:domain]
175
+ path = "; path=" + value[:path] if value[:path]
176
+ # According to RFC 2109, we need dashes here.
177
+ # N.B.: cgi.rb uses spaces...
178
+ expires = "; expires=" +
179
+ rfc2822(value[:expires].clone.gmtime) if value[:expires]
180
+ secure = "; secure" if value[:secure]
181
+ httponly = "; HttpOnly" if value[:httponly]
182
+ value = value[:value]
183
+ end
184
+ value = [value] unless Array === value
185
+ cookie = escape(key) + "=" +
186
+ value.map { |v| escape v }.join("&") +
187
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
188
+
189
+ case header["Set-Cookie"]
190
+ when nil, ''
191
+ header["Set-Cookie"] = cookie
192
+ when String
193
+ header["Set-Cookie"] = [header["Set-Cookie"], cookie].join("\n")
194
+ when Array
195
+ header["Set-Cookie"] = (header["Set-Cookie"] + [cookie]).join("\n")
196
+ end
197
+
198
+ nil
199
+ end
200
+ module_function :set_cookie_header!
201
+
202
+ def delete_cookie_header!(header, key, value = {})
203
+ case header["Set-Cookie"]
204
+ when nil, ''
205
+ cookies = []
206
+ when String
207
+ cookies = header["Set-Cookie"].split("\n")
208
+ when Array
209
+ cookies = header["Set-Cookie"]
210
+ end
211
+
212
+ cookies.reject! { |cookie|
213
+ if value[:domain]
214
+ cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
215
+ else
216
+ cookie =~ /\A#{escape(key)}=/
217
+ end
218
+ }
219
+
220
+ header["Set-Cookie"] = cookies.join("\n")
221
+
222
+ set_cookie_header!(header, key,
223
+ {:value => '', :path => nil, :domain => nil,
224
+ :expires => Time.at(0) }.merge(value))
225
+
226
+ nil
227
+ end
228
+ module_function :delete_cookie_header!
229
+
230
+ # Return the bytesize of String; uses String#length under Ruby 1.8 and
231
+ # String#bytesize under 1.9.
232
+ if ''.respond_to?(:bytesize)
233
+ def bytesize(string)
234
+ string.bytesize
235
+ end
236
+ else
237
+ def bytesize(string)
238
+ string.size
239
+ end
240
+ end
241
+ module_function :bytesize
242
+
243
+ # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
244
+ # of '% %b %Y'.
245
+ # It assumes that the time is in GMT to comply to the RFC 2109.
246
+ #
247
+ # NOTE: I'm not sure the RFC says it requires GMT, but is ambigous enough
248
+ # that I'm certain someone implemented only that option.
249
+ # Do not use %a and %b from Time.strptime, it would use localized names for
250
+ # weekday and month.
251
+ #
252
+ def rfc2822(time)
253
+ wday = Time::RFC2822_DAY_NAME[time.wday]
254
+ mon = Time::RFC2822_MONTH_NAME[time.mon - 1]
255
+ time.strftime("#{wday}, %d-#{mon}-%Y %T GMT")
256
+ end
257
+ module_function :rfc2822
258
+
259
+ # Context allows the use of a compatible middleware at different points
260
+ # in a request handling stack. A compatible middleware must define
261
+ # #context which should take the arguments env and app. The first of which
262
+ # would be the request environment. The second of which would be the rack
263
+ # application that the request would be forwarded to.
264
+ class Context
265
+ attr_reader :for, :app
266
+
267
+ def initialize(app_f, app_r)
268
+ raise 'running context does not respond to #context' unless app_f.respond_to? :context
269
+ @for, @app = app_f, app_r
270
+ end
271
+
272
+ def call(env)
273
+ @for.context(env, @app)
274
+ end
275
+
276
+ def recontext(app)
277
+ self.class.new(@for, app)
278
+ end
279
+
280
+ def context(env, app=@app)
281
+ recontext(app).call(env)
282
+ end
283
+ end
284
+
285
+ # A case-insensitive Hash that preserves the original case of a
286
+ # header when set.
287
+ class HeaderHash < Hash
288
+ def self.new(hash={})
289
+ HeaderHash === hash ? hash : super(hash)
290
+ end
291
+
292
+ def initialize(hash={})
293
+ super()
294
+ @names = {}
295
+ hash.each { |k, v| self[k] = v }
296
+ end
297
+
298
+ def each
299
+ super do |k, v|
300
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
301
+ end
302
+ end
303
+
304
+ def to_hash
305
+ inject({}) do |hash, (k,v)|
306
+ if v.respond_to? :to_ary
307
+ hash[k] = v.to_ary.join("\n")
308
+ else
309
+ hash[k] = v
310
+ end
311
+ hash
312
+ end
313
+ end
314
+
315
+ def [](k)
316
+ super(@names[k]) if @names[k]
317
+ super(@names[k.downcase])
318
+ end
319
+
320
+ def []=(k, v)
321
+ delete k
322
+ @names[k] = @names[k.downcase] = k
323
+ super k, v
324
+ end
325
+
326
+ def delete(k)
327
+ canonical = k.downcase
328
+ result = super @names.delete(canonical)
329
+ @names.delete_if { |name,| name.downcase == canonical }
330
+ result
331
+ end
332
+
333
+ def include?(k)
334
+ @names.include?(k) || @names.include?(k.downcase)
335
+ end
336
+
337
+ alias_method :has_key?, :include?
338
+ alias_method :member?, :include?
339
+ alias_method :key?, :include?
340
+
341
+ def merge!(other)
342
+ other.each { |k, v| self[k] = v }
343
+ self
344
+ end
345
+
346
+ def merge(other)
347
+ hash = dup
348
+ hash.merge! other
349
+ end
350
+
351
+ def replace(other)
352
+ clear
353
+ other.each { |k, v| self[k] = v }
354
+ self
355
+ end
356
+ end
357
+
358
+ # Every standard HTTP code mapped to the appropriate message.
359
+ # Generated with:
360
+ # curl -s http://www.iana.org/assignments/http-status-codes | \
361
+ # ruby -ane 'm = /^(\d{3}) +(\S[^\[(]+)/.match($_) and
362
+ # puts " #{m[1]} => \x27#{m[2].strip}x27,"'
363
+ HTTP_STATUS_CODES = {
364
+ 100 => 'Continue',
365
+ 101 => 'Switching Protocols',
366
+ 102 => 'Processing',
367
+ 200 => 'OK',
368
+ 201 => 'Created',
369
+ 202 => 'Accepted',
370
+ 203 => 'Non-Authoritative Information',
371
+ 204 => 'No Content',
372
+ 205 => 'Reset Content',
373
+ 206 => 'Partial Content',
374
+ 207 => 'Multi-Status',
375
+ 226 => 'IM Used',
376
+ 300 => 'Multiple Choices',
377
+ 301 => 'Moved Permanently',
378
+ 302 => 'Found',
379
+ 303 => 'See Other',
380
+ 304 => 'Not Modified',
381
+ 305 => 'Use Proxy',
382
+ 306 => 'Reserved',
383
+ 307 => 'Temporary Redirect',
384
+ 400 => 'Bad Request',
385
+ 401 => 'Unauthorized',
386
+ 402 => 'Payment Required',
387
+ 403 => 'Forbidden',
388
+ 404 => 'Not Found',
389
+ 405 => 'Method Not Allowed',
390
+ 406 => 'Not Acceptable',
391
+ 407 => 'Proxy Authentication Required',
392
+ 408 => 'Request Timeout',
393
+ 409 => 'Conflict',
394
+ 410 => 'Gone',
395
+ 411 => 'Length Required',
396
+ 412 => 'Precondition Failed',
397
+ 413 => 'Request Entity Too Large',
398
+ 414 => 'Request-URI Too Long',
399
+ 415 => 'Unsupported Media Type',
400
+ 416 => 'Requested Range Not Satisfiable',
401
+ 417 => 'Expectation Failed',
402
+ 422 => 'Unprocessable Entity',
403
+ 423 => 'Locked',
404
+ 424 => 'Failed Dependency',
405
+ 426 => 'Upgrade Required',
406
+ 500 => 'Internal Server Error',
407
+ 501 => 'Not Implemented',
408
+ 502 => 'Bad Gateway',
409
+ 503 => 'Service Unavailable',
410
+ 504 => 'Gateway Timeout',
411
+ 505 => 'HTTP Version Not Supported',
412
+ 506 => 'Variant Also Negotiates',
413
+ 507 => 'Insufficient Storage',
414
+ 510 => 'Not Extended',
415
+ }
416
+
417
+ # Responses with HTTP status codes that should not have an entity body
418
+ STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
419
+
420
+ SYMBOL_TO_STATUS_CODE = HTTP_STATUS_CODES.inject({}) { |hash, (code, message)|
421
+ hash[message.downcase.gsub(/\s|-/, '_').to_sym] = code
422
+ hash
423
+ }
424
+
425
+ def status_code(status)
426
+ if status.is_a?(Symbol)
427
+ SYMBOL_TO_STATUS_CODE[status] || 500
428
+ else
429
+ status.to_i
430
+ end
431
+ end
432
+ module_function :status_code
433
+
434
+ # A multipart form data parser, adapted from IOWA.
435
+ #
436
+ # Usually, Rack::Request#POST takes care of calling this.
437
+
438
+ module Multipart
439
+ class UploadedFile
440
+ # The filename, *not* including the path, of the "uploaded" file
441
+ attr_reader :original_filename
442
+
443
+ # The content type of the "uploaded" file
444
+ attr_accessor :content_type
445
+
446
+ def initialize(path, content_type = "text/plain", binary = false)
447
+ raise "#{path} file does not exist" unless ::File.exist?(path)
448
+ @content_type = content_type
449
+ @original_filename = ::File.basename(path)
450
+ @tempfile = Tempfile.new(@original_filename)
451
+ @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
452
+ @tempfile.binmode if binary
453
+ FileUtils.copy_file(path, @tempfile.path)
454
+ end
455
+
456
+ def path
457
+ @tempfile.path
458
+ end
459
+ alias_method :local_path, :path
460
+
461
+ def method_missing(method_name, *args, &block) #:nodoc:
462
+ @tempfile.__send__(method_name, *args, &block)
463
+ end
464
+ end
465
+
466
+ EOL = "\r\n"
467
+ MULTIPART_BOUNDARY = "AaB03x"
468
+
469
+ def self.parse_multipart(env)
470
+ unless env['CONTENT_TYPE'] =~
471
+ %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
472
+ nil
473
+ else
474
+ boundary = "--#{$1}"
475
+
476
+ params = {}
477
+ buf = ""
478
+ content_length = env['CONTENT_LENGTH'].to_i
479
+ input = env['rack.input']
480
+ input.rewind
481
+
482
+ boundary_size = Utils.bytesize(boundary) + EOL.size
483
+ bufsize = 16384
484
+
485
+ content_length -= boundary_size
486
+
487
+ read_buffer = ''
488
+
489
+ status = input.read(boundary_size, read_buffer)
490
+ raise EOFError, "bad content body" unless status == boundary + EOL
491
+
492
+ rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
493
+
494
+ loop {
495
+ head = nil
496
+ body = ''
497
+ filename = content_type = name = nil
498
+
499
+ until head && buf =~ rx
500
+ if !head && i = buf.index(EOL+EOL)
501
+ head = buf.slice!(0, i+2) # First \r\n
502
+ buf.slice!(0, 2) # Second \r\n
503
+
504
+ token = /[^\s()<>,;:\\"\/\[\]?=]+/
505
+ condisp = /Content-Disposition:\s*#{token}\s*/i
506
+ dispparm = /;\s*(#{token})=("(?:\\"|[^"])*"|#{token})*/
507
+
508
+ rfc2183 = /^#{condisp}(#{dispparm})+$/i
509
+ broken_quoted = /^#{condisp}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{token}=)/i
510
+ broken_unquoted = /^#{condisp}.*;\sfilename=(#{token})/i
511
+
512
+ if head =~ rfc2183
513
+ filename = Hash[head.scan(dispparm)]['filename']
514
+ filename = $1 if filename and filename =~ /^"(.*)"$/
515
+ elsif head =~ broken_quoted
516
+ filename = $1
517
+ elsif head =~ broken_unquoted
518
+ filename = $1
519
+ end
520
+
521
+ if filename && filename !~ /\\[^\\"]/
522
+ filename = Utils.unescape(filename).gsub(/\\(.)/, '\1')
523
+ end
524
+
525
+ content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
526
+ name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
527
+
528
+ if filename
529
+ body = Tempfile.new("RackMultipart")
530
+ body.binmode if body.respond_to?(:binmode)
531
+ end
532
+
533
+ next
534
+ end
535
+
536
+ # Save the read body part.
537
+ if head && (boundary_size+4 < buf.size)
538
+ body << buf.slice!(0, buf.size - (boundary_size+4))
539
+ end
540
+
541
+ c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
542
+ raise EOFError, "bad content body" if c.nil? || c.empty?
543
+ buf << c
544
+ content_length -= c.size
545
+ end
546
+
547
+ # Save the rest.
548
+ if i = buf.index(rx)
549
+ body << buf.slice!(0, i)
550
+ buf.slice!(0, boundary_size+2)
551
+
552
+ content_length = -1 if $1 == "--"
553
+ end
554
+
555
+ if filename == ""
556
+ # filename is blank which means no file has been selected
557
+ data = nil
558
+ elsif filename
559
+ body.rewind
560
+
561
+ # Take the basename of the upload's original filename.
562
+ # This handles the full Windows paths given by Internet Explorer
563
+ # (and perhaps other broken user agents) without affecting
564
+ # those which give the lone filename.
565
+ filename = filename.split(/[\/\\]/).last
566
+
567
+ data = {:filename => filename, :type => content_type,
568
+ :name => name, :tempfile => body, :head => head}
569
+ elsif !filename && content_type
570
+ body.rewind
571
+
572
+ # Generic multipart cases, not coming from a form
573
+ data = {:type => content_type,
574
+ :name => name, :tempfile => body, :head => head}
575
+ else
576
+ data = body
577
+ end
578
+
579
+ Utils.normalize_params(params, name, data) unless data.nil?
580
+
581
+ # break if we're at the end of a buffer, but not if it is the end of a field
582
+ break if (buf.empty? && $1 != EOL) || content_length == -1
583
+ }
584
+
585
+ input.rewind
586
+
587
+ params
588
+ end
589
+ end
590
+
591
+ def self.build_multipart(params, first = true)
592
+ if first
593
+ unless params.is_a?(Hash)
594
+ raise ArgumentError, "value must be a Hash"
595
+ end
596
+
597
+ multipart = false
598
+ query = lambda { |value|
599
+ case value
600
+ when Array
601
+ value.each(&query)
602
+ when Hash
603
+ value.values.each(&query)
604
+ when UploadedFile
605
+ multipart = true
606
+ end
607
+ }
608
+ params.values.each(&query)
609
+ return nil unless multipart
610
+ end
611
+
612
+ flattened_params = Hash.new
613
+
614
+ params.each do |key, value|
615
+ k = first ? key.to_s : "[#{key}]"
616
+
617
+ case value
618
+ when Array
619
+ value.map { |v|
620
+ build_multipart(v, false).each { |subkey, subvalue|
621
+ flattened_params["#{k}[]#{subkey}"] = subvalue
622
+ }
623
+ }
624
+ when Hash
625
+ build_multipart(value, false).each { |subkey, subvalue|
626
+ flattened_params[k + subkey] = subvalue
627
+ }
628
+ else
629
+ flattened_params[k] = value
630
+ end
631
+ end
632
+
633
+ if first
634
+ flattened_params.map { |name, file|
635
+ if file.respond_to?(:original_filename)
636
+ ::File.open(file.path, "rb") do |f|
637
+ f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
638
+ <<-EOF
639
+ --#{MULTIPART_BOUNDARY}\r
640
+ Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
641
+ Content-Type: #{file.content_type}\r
642
+ Content-Length: #{::File.stat(file.path).size}\r
643
+ \r
644
+ #{f.read}\r
645
+ EOF
646
+ end
647
+ else
648
+ <<-EOF
649
+ --#{MULTIPART_BOUNDARY}\r
650
+ Content-Disposition: form-data; name="#{name}"\r
651
+ \r
652
+ #{file}\r
653
+ EOF
654
+ end
655
+ }.join + "--#{MULTIPART_BOUNDARY}--\r"
656
+ else
657
+ flattened_params
658
+ end
659
+ end
660
+ end
661
+ end
662
+ end