nitro 0.23.0 → 0.24.0

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 (76) hide show
  1. data/CHANGELOG +350 -0
  2. data/INSTALL +2 -2
  3. data/ProjectInfo +61 -0
  4. data/README +5 -4
  5. data/Rakefile +5 -4
  6. data/bin/nitrogen +3 -1
  7. data/doc/AUTHORS +27 -3
  8. data/doc/RELEASES +193 -0
  9. data/doc/lhttpd.txt +4 -0
  10. data/lib/nitro.rb +1 -1
  11. data/lib/nitro/adapter/cgi.rb +6 -321
  12. data/lib/nitro/adapter/fastcgi.rb +2 -14
  13. data/lib/nitro/adapter/scgi.rb +237 -71
  14. data/lib/nitro/adapter/webrick.rb +25 -7
  15. data/lib/nitro/caching.rb +1 -0
  16. data/lib/nitro/cgi.rb +296 -0
  17. data/lib/nitro/{cookie.rb → cgi/cookie.rb} +0 -0
  18. data/lib/nitro/cgi/http.rb +62 -0
  19. data/lib/nitro/{request.rb → cgi/request.rb} +4 -1
  20. data/lib/nitro/{response.rb → cgi/response.rb} +0 -0
  21. data/lib/nitro/cgi/stream.rb +43 -0
  22. data/lib/nitro/cgi/utils.rb +38 -0
  23. data/lib/nitro/compiler.rb +23 -11
  24. data/lib/nitro/compiler/css.rb +8 -0
  25. data/lib/nitro/compiler/morphing.rb +66 -0
  26. data/lib/nitro/context.rb +21 -30
  27. data/lib/nitro/controller.rb +23 -100
  28. data/lib/nitro/dispatcher.rb +18 -8
  29. data/lib/nitro/element.rb +6 -2
  30. data/lib/nitro/flash.rb +2 -2
  31. data/lib/nitro/mixin/buffer.rb +2 -2
  32. data/lib/nitro/mixin/form.rb +204 -93
  33. data/lib/nitro/mixin/javascript.rb +170 -11
  34. data/lib/nitro/mixin/markup.rb +1 -0
  35. data/lib/nitro/mixin/pager.rb +7 -4
  36. data/lib/nitro/mixin/rss.rb +2 -0
  37. data/lib/nitro/mixin/table.rb +23 -6
  38. data/lib/nitro/mixin/xhtml.rb +2 -2
  39. data/lib/nitro/render.rb +19 -5
  40. data/lib/nitro/scaffold.rb +12 -6
  41. data/lib/nitro/server.rb +4 -6
  42. data/lib/nitro/server/runner.rb +2 -2
  43. data/lib/nitro/session.rb +8 -1
  44. data/lib/nitro/session/file.rb +40 -0
  45. data/lib/part/admin.rb +2 -0
  46. data/lib/part/admin/controller.rb +7 -3
  47. data/lib/part/admin/skin.rb +8 -1
  48. data/lib/part/admin/template/index.xhtml +39 -1
  49. data/proto/public/error.xhtml +5 -3
  50. data/proto/public/js/behaviour.js +254 -254
  51. data/proto/public/js/controls.js +427 -165
  52. data/proto/public/js/dragdrop.js +255 -276
  53. data/proto/public/js/effects.js +476 -277
  54. data/proto/public/js/prototype.js +561 -127
  55. data/proto/public/js/scaffold.js +74 -0
  56. data/proto/public/js/scriptaculous.js +44 -0
  57. data/proto/public/js/util.js +548 -0
  58. data/proto/public/scaffold/list.xhtml +4 -1
  59. data/proto/scgi.rb +333 -0
  60. data/script/scgi_ctl +221 -0
  61. data/script/scgi_service +120 -0
  62. data/test/nitro/adapter/raw_post1.bin +0 -0
  63. data/test/nitro/{tc_cookie.rb → cgi/tc_cookie.rb} +1 -1
  64. data/test/nitro/{tc_request.rb → cgi/tc_request.rb} +1 -1
  65. data/test/nitro/mixin/tc_xhtml.rb +1 -1
  66. data/test/nitro/{adapter/tc_cgi.rb → tc_cgi.rb} +12 -12
  67. data/test/nitro/tc_controller.rb +9 -5
  68. metadata +159 -169
  69. data/benchmark/bench.rb +0 -5
  70. data/benchmark/simple-webrick-n-200.txt +0 -44
  71. data/benchmark/static-webrick-n-200.txt +0 -43
  72. data/benchmark/tiny-lhttpd-n-200-c-5.txt +0 -43
  73. data/benchmark/tiny-webrick-n-200-c-5.txt +0 -44
  74. data/benchmark/tiny-webrick-n-200.txt +0 -44
  75. data/benchmark/tiny2-webrick-n-200.txt +0 -44
  76. data/examples/README +0 -7
@@ -3,7 +3,7 @@ require 'fcgi'
3
3
 
4
4
  require 'nitro/context'
5
5
  require 'nitro/dispatcher'
6
- require 'nitro/adapter/cgi'
6
+ require 'nitro/cgi'
7
7
 
8
8
  require 'glue/flexob'
9
9
 
@@ -25,19 +25,7 @@ class FastCGI
25
25
  def self.start(server)
26
26
  FCGI.each do |cgi|
27
27
  begin
28
- context = Context.new(server)
29
-
30
- context.in = cgi.in
31
- context.headers = cgi.env
32
-
33
- CgiUtils.parse_params(context)
34
- CgiUtils.parse_cookies(context)
35
-
36
- context.render(context.path)
37
-
38
- cgi.out.print(CgiUtils.response_headers(context))
39
- cgi.out.print(context.out)
40
-
28
+ Cgi.process(server, cgi, cgi.in, cgi.out)
41
29
  cgi.finish
42
30
  end
43
31
  end
@@ -1,80 +1,246 @@
1
- # WARNING: Does not work yet!
1
+ #!/usr/bin/env ruby
2
2
 
3
- require 'cgi'
4
- require 'scgi'
5
3
  require 'stringio'
6
-
7
- require 'nitro/context'
8
- require 'nitro/dispatcher'
9
- require 'nitro/adapter/cgi'
10
-
11
- # Speeds things up, more comaptible with OSX.
12
-
13
- Socket.do_not_reverse_lookup = true
14
-
15
- module Nitro
16
-
17
- # SCGI Adaptor.
18
- # No need for connection pooling, SCGI uses processes.
19
-
20
- class Scgi
21
-
22
- # A special modification so that we can make the socket and then pass it in.
23
- # Should generalize this since it will be pretty common.
24
-
25
- class Listener < Myriad::Listener
26
- def initialize(socket, server_class, *params)
27
- @socket = socket
28
- @server_class = server_class
29
- @params = params
30
- @tick = [3,0] # this is just here until I can figure out the signals crap
31
- Event::set(self, @socket.fileno, Event::READ | Event::TIMEOUT)
32
- Event::add(self, @tick)
4
+ require 'yaml'
5
+ require 'digest/sha1'
6
+ require 'socket'
7
+ require 'cgi'
8
+ require 'monitor'
9
+ require 'singleton'
10
+
11
+ module SCGI
12
+
13
+ class LogFactory < Monitor
14
+ include Singleton
15
+
16
+ def initialize
17
+ super()
18
+ @@logs = {}
19
+ end
20
+
21
+ def create(file)
22
+ result = nil
23
+ synchronize do
24
+ result = @@logs[file]
25
+ if not result
26
+ result = Log.new(file)
27
+ @@logs[file] = result
28
+ end
29
+ end
30
+ return result
31
+ end
33
32
  end
34
- end
35
-
36
- class SCGINitro < ::SCGI::SCGIServer
37
- def process_request(data)
38
- context = Context.new(Nitro::Server.new)
39
-
40
- context.in = SCGIFixed.new(@request, data)
41
- context.headers = cgi.env
42
-
43
- CgiUtils.parse_params(context)
44
- CgiUtils.parse_cookies(context)
45
-
46
- context.render(context.path)
47
-
48
- body = ''
49
- body << CgiUtils.response_headers(context)
50
- body << context.out
51
-
52
- @trans.write(body)
53
-
54
- super(data)
33
+
34
+ class Log < Monitor
35
+ def initialize(file)
36
+ super()
37
+ @out = open(file, "a+")
38
+ @out.sync = true
39
+ @pid = Process.pid
40
+ @info = "[INF][#{Process.pid}] "
41
+ @error = "[ERR][#{Process.pid}] "
42
+ end
43
+
44
+ def info(msg)
45
+ synchronize do
46
+ @out.print @info, msg,"\n"
47
+ end
48
+ end
49
+
50
+ def error(msg, exc=nil)
51
+ if exc
52
+ synchronize do
53
+ @out.print @error, "#{msg}: #{exc}\n"
54
+ @out.print @error, exc.backtrace.join("\n"), "\n"
55
+ end
56
+ else
57
+ synchronize do
58
+ @out.print @error, msg,"\n"
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ # Modifies CGI so that we can use it.
65
+ class SCGIFixed < ::CGI
66
+ public :env_table
67
+
68
+ def initialize(params, data, out, *args)
69
+ @env_table = params
70
+ @args = *args
71
+ @input = StringIO.new(data)
72
+ @out = out
73
+ super(*args)
74
+ end
75
+ def args
76
+ @args
77
+ end
78
+ def env_table
79
+ @env_table
80
+ end
81
+ def stdinput
82
+ @input
83
+ end
84
+ def stdoutput
85
+ @out
86
+ end
55
87
  end
56
- end
57
-
58
- def self.start(server)
59
-
60
- puts '-------------1'
61
- children = "5"
62
- host = "127.0.0.0"
63
- port = "8888"
64
88
 
65
- socket = TCPServer.new(host, port)
66
- socket.fcntl(Fcntl::F_SETFL, socket.fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK)
67
89
 
68
- children.to_i.times do
69
- pid = fork do
70
- Event::init(10)
71
- server = Listener.new(socket, SCGINitro)
72
- Event::dispatch
73
- end
74
- Process.detach(pid)
90
+ class SCGIProcessor < Monitor
91
+ attr_reader :settings
92
+
93
+ def initialize(settings = {})
94
+ @conns = 0
95
+ @shutdown = false
96
+ @dead = false
97
+ super()
98
+
99
+ configure(settings)
100
+ end
101
+
102
+ def configure(settings)
103
+ @settings = settings
104
+ @log = LogFactory.instance.create(settings[:logfile] || "log/scgi.log")
105
+ @maxconns = settings[:maxconns] || 2**30-1
106
+ @started = Time.now
107
+
108
+ if settings[:socket]
109
+ @socket = settings[:socket]
110
+ else
111
+ @host = settings[:host] || "127.0.0.1"
112
+ @port = settings[:port] || "9999"
113
+ end
114
+
115
+ @throttle_sleep = 1.0/settings[:conns_second] if settings[:conns_second]
116
+ end
117
+
118
+ def listen
119
+ @socket = TCPServer.new(@host, @port)
120
+
121
+ begin
122
+ while true
123
+ handle_client(@socket.accept)
124
+ sleep @throttle_sleep if @throttle_sleep
125
+ break if @shutdown and @conns <= 0
126
+ end
127
+ rescue Interrupt
128
+ @log.info("Shutting down from SIGINT.")
129
+ rescue Object
130
+ @log.error("while listening for connections on #@host:#@port -- #{$!.class}", $!)
131
+ end
132
+
133
+ @socket.close if not @socket.closed?
134
+ @dead = true
135
+ @log.info("Exited accept loop. Shutdown complete.")
136
+ end
137
+
138
+
139
+ def handle_client(socket)
140
+ Thread.new do
141
+ begin
142
+ # remember if we were doing a shutdown so we can avoid increment later
143
+ in_shutdown = @shutdown
144
+ synchronize { @conns += 1 if not in_shutdown }
145
+
146
+ len = ""
147
+ # we only read 10 bytes of the length. any request longer than this is invalid
148
+ while len.length <= 10
149
+ c = socket.read(1)
150
+ if c == ':'
151
+ # found the terminal, len now has a length in it so read the payload
152
+ break
153
+ else
154
+ len << c
155
+ end
156
+ end
157
+
158
+ # we should now either have a payload length to get
159
+ payload = socket.read(len.to_i)
160
+ if (c = socket.read(1)) != ','
161
+ @log.error("Malformed request, does not end with ','")
162
+ else
163
+ read_header(socket, payload, @conns)
164
+ end
165
+ rescue IOError
166
+ @log.error("received IOError #$! when handling client. Your web server doesn't like me.")
167
+ rescue Object
168
+ @log.error("after accepting client #@host:#@port -- #{$!.class}", $!)
169
+ ensure
170
+ synchronize { @conns -= 1 if not in_shutdown}
171
+ socket.close if not socket.closed?
172
+ end
173
+ end
174
+ end
175
+
176
+
177
+ def read_header(socket, payload, conns)
178
+ return if socket.closed?
179
+ request = split_body(payload)
180
+ if request["CONTENT_LENGTH"]
181
+ length = request["CONTENT_LENGTH"].to_i
182
+ if length > 0
183
+ body = socket.read(length)
184
+ else
185
+ body = ""
186
+ end
187
+
188
+ if @shutdown or @conns > @maxconns
189
+ socket.write("Location: /busy.html\r\n")
190
+ socket.write("Cache-control: no-cache, must-revalidate\r\n")
191
+ socket.write("Expires: Mon, 26 Jul 1997 05:00:00 GMT\r\n")
192
+ socket.write("Status: 307 Temporary Redirect\r\n\r\n")
193
+ else
194
+ process_request(request, body, socket)
195
+ end
196
+ end
197
+ end
198
+
199
+
200
+ def process_request(request, body, socket)
201
+ raise "You must implement process_request"
202
+ end
203
+
204
+
205
+ def split_body(data)
206
+ result = {}
207
+ el = data.split("\0")
208
+ i = 0
209
+ len = el.length
210
+ while i < len
211
+ result[el[i]] = el[i+1]
212
+ i += 2
213
+ end
214
+
215
+ return result
216
+ end
217
+
218
+
219
+ def status
220
+ {
221
+ :time => Time.now, :pid => Process.pid, :settings => @settings,
222
+ :env => @settings[:env], :started => @started,
223
+ :max_conns => @maxconns, :conns => @conns, :systimes => Process.times,
224
+ :conns_second => @throttle, :shutdown => @shutdown, :dead => @dead
225
+ }
226
+ end
227
+
228
+ # Graceful shutdown is done by setting the maxconns to 0 so that all new requests
229
+ # get the 503 Service Unavailable status. The handler code checks if @shutdown is
230
+ # set and if so it will not increase the @conns count, but it will decrease.
231
+ # Once the @conns count is down to 0 it will exit the loop.
232
+ def shutdown(force = false)
233
+ @shutdown = true;
234
+
235
+ if force
236
+ @socket.close
237
+ @log.info("FORCED shutdown requested. Oh well.")
238
+ else
239
+ @log.info("Shutdown requested. Beginning graceful shutdown with #@conns connected.")
240
+ end
241
+ end
75
242
  end
76
- end
77
-
78
243
  end
79
244
 
80
- end
245
+ # * Zed A Shaw <zedshaw@zedshaw.com>
246
+ # * George Moschovitis <gm@navel.gr>
@@ -2,6 +2,7 @@ require 'webrick'
2
2
  require 'stringio'
3
3
 
4
4
  require 'glue/flexob'
5
+ require 'nitro/cgi'
5
6
  require 'nitro/context'
6
7
  require 'nitro/dispatcher'
7
8
 
@@ -18,6 +19,8 @@ class Webrick
18
19
  class << self
19
20
  attr_accessor :webrick
20
21
 
22
+ # Start the Webrick adapter.
23
+
21
24
  def start(server)
22
25
  if RUBY_PLATFORM !~ /mswin32/
23
26
  wblog = WEBrick::BasicLog::new('/dev/null')
@@ -41,11 +44,27 @@ class Webrick
41
44
  )
42
45
  @webrick = WEBrick::HTTPServer.new(webrick_options)
43
46
 
44
- trap('INT') { @webrick.shutdown }
47
+ trap('INT') { stop }
45
48
 
46
49
  @webrick.mount('/', WebrickAdapter, server)
50
+
51
+ initialize_webrick(server)
52
+
47
53
  @webrick.start
48
54
  end
55
+
56
+ # Stop the Webrick adapter.
57
+
58
+ def stop
59
+ @webrick.shutdown
60
+ end
61
+
62
+ # Override this method to perform customized webrick
63
+ # initialization.
64
+
65
+ def initialize_webrick(server)
66
+ end
67
+
49
68
  end
50
69
 
51
70
  end
@@ -105,8 +124,6 @@ class WebrickAdapter < WEBrick::HTTPServlet::AbstractServlet
105
124
  begin
106
125
  path = req.request_uri.path
107
126
 
108
- # REQUEST_MUTEX.lock
109
-
110
127
  context = Context.new(@server)
111
128
 
112
129
  context.in = StringIO.new(req.body || "")
@@ -119,8 +136,8 @@ class WebrickAdapter < WEBrick::HTTPServlet::AbstractServlet
119
136
  context.headers['REQUEST_URI'].slice!(/http:\/\/(.*?)\//)
120
137
  context.headers['REQUEST_URI'] << '/'
121
138
 
122
- CgiUtils.parse_params(context)
123
- CgiUtils.parse_cookies(context)
139
+ Cgi.parse_params(context)
140
+ Cgi.parse_cookies(context)
124
141
 
125
142
  context.render(path)
126
143
 
@@ -128,10 +145,11 @@ class WebrickAdapter < WEBrick::HTTPServlet::AbstractServlet
128
145
  res.instance_variable_set(:@header, context.response_headers || {})
129
146
  res.instance_variable_set(:@cookies, context.response_cookies || {})
130
147
  res.body = context.out
131
-
148
+ res.chunked = true if context.out.is_a?(IO)
149
+
132
150
  context.close
133
151
  ensure
134
- Og.manager.put_store if defined?(Og) and Og.manager
152
+ Og.manager.put_store if defined?(Og) and Og.respond_to?(:manager)
135
153
  end
136
154
  end
137
155
  end
data/lib/nitro/caching.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'fileutils'
2
2
 
3
3
  require 'glue/attribute'
4
+ require 'glue/configuration'
4
5
 
5
6
  require 'nitro/caching/output'
6
7
  require 'nitro/caching/actions'
data/lib/nitro/cgi.rb ADDED
@@ -0,0 +1,296 @@
1
+ require 'glue/configuration'
2
+ require 'nitro/cgi/http'
3
+
4
+ module Nitro
5
+
6
+ class Cgi
7
+ include Http
8
+
9
+ # Maximum content length allowed in requests.
10
+
11
+ setting :max_content_length, :default => (2 * 1024 * 1024), :doc => 'Maximum content length allowed in requests'
12
+
13
+ # Multipart parsing buffer size.
14
+
15
+ setting :buffer_size, :default => (10 * 1024), :doc => 'Multipart parsing buffer size'
16
+
17
+ # Process a CGI request. This is a general method reused by
18
+ # many adapters.
19
+
20
+ def self.process(server, cgi, inp, out)
21
+ context = Context.new(server)
22
+
23
+ context.in = inp
24
+ context.headers = cgi.env
25
+
26
+ Cgi.parse_params(context)
27
+ Cgi.parse_cookies(context)
28
+ context.render(context.path)
29
+
30
+ out.print(Cgi.response_headers(context))
31
+
32
+ if context.out.is_a?(IO)
33
+ while buf = context.out.read(4096)
34
+ out.write(buf)
35
+ end
36
+ else
37
+ out.print(context.out)
38
+ end
39
+
40
+ context.close
41
+ end
42
+
43
+ # Returns a hash with the pairs from the query
44
+ # string. The implicit hash construction that is done
45
+ # in parse_request_params is not done here.
46
+ #
47
+ # Parameters in the form xxx[] are converted
48
+ # to arrays.
49
+ #
50
+ # Use the field.attr or field[attr] notation to pass
51
+ # compound objects.
52
+
53
+ def self.parse_query_string(query_string)
54
+ params = {}
55
+
56
+ # gmosx, THINK: better return nil here?
57
+ return params if (query_string.nil? or query_string.empty?)
58
+
59
+ query_string.split(/[&;]/).each do |p|
60
+ key, val = p.split('=')
61
+
62
+ key = CGI.unescape(key) unless key.nil?
63
+ val = CGI.unescape(val) unless val.nil?
64
+
65
+ if key =~ /(.*)\[\]$/
66
+ # Multiple values, for example a checkbox collection.
67
+ # Stored as an array.
68
+ if params.has_key?($1)
69
+ params[$1] << val
70
+ else
71
+ params[$1] = [val]
72
+ end
73
+ elsif key =~ /(.*)\[(.*)\]$/ or key =~ /(.*)\.(.*)$/
74
+ # A compound object with attributes.
75
+ # Stored as a Hash.
76
+ params[$1] ||= {}
77
+ params[$1][$2] = val
78
+ else
79
+ # Standard single valued parameter.
80
+ params[key] = val.nil? ? nil : val
81
+ end
82
+ end
83
+
84
+ return params
85
+ end
86
+
87
+ # Parse the HTTP_COOKIE header and returns the
88
+ # cookies as a key->value hash. For efficiency no
89
+ # cookie objects are created.
90
+ #
91
+ # [+context+]
92
+ # The context
93
+
94
+ def self.parse_cookies(context)
95
+ env = context.env
96
+
97
+ # FIXME: dont precreate?
98
+ context.cookies = {}
99
+
100
+ if env.has_key?('HTTP_COOKIE') or env.has_key?('COOKIE')
101
+ (env['HTTP_COOKIE'] or env['COOKIE']).split(/; /).each do |c|
102
+ key, val = c.split(/=/, 2)
103
+
104
+ key = CGI.unescape(key)
105
+ val = val.split(/&/).collect{|v| CGI::unescape(v)}.join("\0")
106
+
107
+ if context.cookies.include?(key)
108
+ context.cookies[key] += "\0" + val
109
+ else
110
+ context.cookies[key] = val
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ # Build the response headers for the context.
117
+ #
118
+ # [+context+]
119
+ # The context of the response.
120
+ #
121
+ # [+proto+]
122
+ # If true emmit the protocol line. Useful for MOD_RUBY.
123
+ #--
124
+ # FIXME: return the correct protocol from env.
125
+ # TODO: Perhaps I can optimize status calc.
126
+ #++
127
+
128
+ def self.response_headers(context, proto = false)
129
+ reason = STATUS_STRINGS[context.status]
130
+
131
+ if proto
132
+ buf = "HTTP/1.1 #{context.status} #{reason}#{EOL}Date: #{CGI::rfc1123_date(Time.now)}#{EOL}"
133
+ else
134
+ buf = "Status: #{context.status} #{reason}#{EOL}"
135
+ end
136
+
137
+ context.response_headers.each do |key, value|
138
+ tmp = key.gsub(/\bwww|^te$|\b\w/) { |s| s.upcase }
139
+ buf << "#{tmp}: #{value}" << EOL
140
+ end
141
+
142
+ context.response_cookies.each do |cookie|
143
+ buf << "Set-Cookie: " << cookie.to_s << EOL
144
+ end if context.response_cookies
145
+
146
+ buf << EOL
147
+
148
+ return buf
149
+ end
150
+
151
+ # Initialize the request params.
152
+ # Handles multipart forms (in particular, forms that involve
153
+ # file uploads). Reads query parameters in the @params field,
154
+ # and cookies into @cookies.
155
+
156
+ def self.parse_params(context)
157
+ method = context.method
158
+ if (:post == method) and
159
+ %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n.match(context.headers['CONTENT_TYPE'])
160
+ boundary = $1.dup
161
+ context.params = parse_multipart(context, boundary)
162
+
163
+ # Also include the URL parameters.
164
+ context.params.update(Cgi.parse_query_string(context.query_string))
165
+ else
166
+ case method
167
+ when :get, :head
168
+ context.params = Cgi.parse_query_string(context.query_string)
169
+ when :post
170
+ context.in.binmode # if defined?(context.in.binmode)
171
+ context.params = Cgi.parse_query_string(context.in.read(context.content_length) || '')
172
+ end
173
+ end
174
+ end
175
+
176
+ # Parse a multipart request.
177
+ # Adapted from Ruby's cgi.rb
178
+ #--
179
+ # TODO: optimize and rationalize this.
180
+ #++
181
+
182
+ def self.parse_multipart(context, boundary)
183
+ input = context.in
184
+ content_length = context.content_length
185
+ env_table = context.env
186
+
187
+ params = Hash.new([])
188
+ boundary = "--" + boundary
189
+ buf = ""
190
+
191
+ input.binmode if defined? input.binmode
192
+ boundary_size = boundary.size + EOL.size
193
+ content_length -= boundary_size
194
+ status = input.read(boundary_size)
195
+
196
+ if nil == status
197
+ raise EOFError, "no content body"
198
+ elsif boundary + EOL != status
199
+ raise EOFError, "bad content body"
200
+ end
201
+
202
+ loop do
203
+ head = nil
204
+
205
+ if 10240 < content_length
206
+ body = Tempfile.new("CGI")
207
+ else
208
+ begin
209
+ body = StringIO.new
210
+ rescue LoadError
211
+ body = Tempfile.new("CGI")
212
+ end
213
+ end
214
+ body.binmode if defined? body.binmode
215
+
216
+ until head and /#{boundary}(?:#{EOL}|--)/n.match(buf)
217
+
218
+ if (not head) and /#{EOL}#{EOL}/n.match(buf)
219
+ buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do
220
+ head = $1.dup
221
+ ""
222
+ end
223
+ next
224
+ end
225
+
226
+ if head and ( (EOL + boundary + EOL).size < buf.size )
227
+ body.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)]
228
+ buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = ""
229
+ end
230
+
231
+ c = if Cgi.buffer_size < content_length
232
+ input.read(Cgi.buffer_size)
233
+ else
234
+ input.read(content_length)
235
+ end
236
+ if c.nil?
237
+ raise EOFError, "bad content body"
238
+ end
239
+ buf.concat(c)
240
+ content_length -= c.size
241
+ end
242
+
243
+ buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{boundary}([\r\n]{1,2}|--)/n) do
244
+ body.print $1
245
+ if "--" == $2
246
+ content_length = -1
247
+ end
248
+ ""
249
+ end
250
+
251
+ body.rewind
252
+
253
+ /Content-Disposition:.* filename="?([^\";]*)"?/ni.match(head)
254
+
255
+ filename = ($1 or "")
256
+
257
+ if /Mac/ni.match(env_table['HTTP_USER_AGENT']) and
258
+ /Mozilla/ni.match(env_table['HTTP_USER_AGENT']) and
259
+ (not /MSIE/ni.match(env_table['HTTP_USER_AGENT']))
260
+ filename = CGI::unescape(filename)
261
+ end
262
+
263
+ /Content-Type: (.*)/ni.match(head)
264
+ content_type = ($1 or "")
265
+
266
+ (class << body; self; end).class_eval do
267
+ alias local_path path
268
+ define_method(:original_filename) { filename.dup.taint }
269
+ define_method(:content_type) { content_type.dup.taint }
270
+
271
+ # gmosx: this hides the performance hit!!
272
+ define_method(:to_s) { read }
273
+ end
274
+
275
+ /Content-Disposition:.* name="?([^\";]*)"?/ni.match(head)
276
+ name = $1.dup
277
+
278
+ if params.has_key?(name)
279
+ params[name] = [params[name]] << body
280
+ else
281
+ params[name] = body
282
+ end
283
+
284
+ break if buf.size == 0
285
+ break if content_length === -1
286
+ end
287
+
288
+ return params
289
+ end
290
+
291
+ end
292
+
293
+ end
294
+
295
+ # * George Moschovitis <gm@navel.gr>
296
+ # * Guillaume Pierronnet <guillaume.pierronnet@gmail.com>