Rails 2.3.3.1

Update to latest Rails.
A little bit of jiggery-pokery is involved, since they
neglected to re-include vendored Rack in this release.
This commit is contained in:
Jacques Distler 2009-08-04 10:16:03 -05:00
parent 329fafafce
commit 664552ac02
257 changed files with 4346 additions and 1682 deletions

View file

@ -63,7 +63,10 @@ module ActionController
# Support partial arguments for hash redirections
if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
return true if options.all? {|(key, value)| @response.redirected_to[key] == value}
if options.all? {|(key, value)| @response.redirected_to[key] == value}
::ActiveSupport::Deprecation.warn("Using assert_redirected_to with partial hash arguments is deprecated. Specify the full set arguments instead", caller)
return true
end
end
redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to)
@ -82,6 +85,9 @@ module ActionController
# # assert that the "new" view template was rendered
# assert_template "new"
#
# # assert that the "new" view template was rendered with Symbol
# assert_template :new
#
# # assert that the "_customer" partial was rendered twice
# assert_template :partial => '_customer', :count => 2
#
@ -91,7 +97,7 @@ module ActionController
def assert_template(options = {}, message = nil)
clean_backtrace do
case options
when NilClass, String
when NilClass, String, Symbol
rendered = @response.rendered[:template].to_s
msg = build_message(message,
"expecting <?> but rendering with <?>",
@ -100,7 +106,7 @@ module ActionController
if options.nil?
@response.rendered[:template].blank?
else
rendered.to_s.match(options)
rendered.to_s.match(options.to_s)
end
end
when Hash
@ -123,6 +129,8 @@ module ActionController
assert @response.rendered[:partials].empty?,
"Expected no partials to be rendered"
end
else
raise ArgumentError
end
end
end

View file

@ -491,6 +491,10 @@ module ActionController #:nodoc:
filtered_parameters[key] = '[FILTERED]'
elsif value.is_a?(Hash)
filtered_parameters[key] = filter_parameters(value)
elsif value.is_a?(Array)
filtered_parameters[key] = value.collect do |item|
filter_parameters(item)
end
elsif block_given?
key = key.dup
value = value.dup if value
@ -950,8 +954,9 @@ module ActionController #:nodoc:
response.content_type ||= Mime::JS
render_for_text(js, options[:status])
elsif json = options[:json]
json = json.to_json unless json.is_a?(String)
elsif options.include?(:json)
json = options[:json]
json = ActiveSupport::JSON.encode(json) unless json.is_a?(String)
json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
response.content_type ||= Mime::JSON
render_for_text(json, options[:status])

View file

@ -27,7 +27,7 @@ module ActionController #:nodoc:
autoload :Actions, 'action_controller/caching/actions'
autoload :Fragments, 'action_controller/caching/fragments'
autoload :Pages, 'action_controller/caching/pages'
autoload :Sweeper, 'action_controller/caching/sweeping'
autoload :Sweeper, 'action_controller/caching/sweeper'
autoload :Sweeping, 'action_controller/caching/sweeping'
def self.included(base) #:nodoc:

View file

@ -61,7 +61,9 @@ module ActionController #:nodoc:
filter_options = { :only => actions, :if => options.delete(:if), :unless => options.delete(:unless) }
cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options)
around_filter(cache_filter, filter_options)
around_filter(filter_options) do |controller, action|
cache_filter.filter(controller, action)
end
end
end
@ -83,6 +85,12 @@ module ActionController #:nodoc:
@options = options
end
def filter(controller, action)
should_continue = before(controller)
action.call if should_continue
after(controller)
end
def before(controller)
cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path)))
if cache = controller.read_fragment(cache_path.path, @options[:store_options])

View file

@ -0,0 +1,45 @@
require 'active_record'
module ActionController #:nodoc:
module Caching
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
def before(controller)
self.controller = controller
callback(:before) if controller.perform_caching
end
def after(controller)
callback(:after) if controller.perform_caching
# Clean up, so that the controller can be collected after this request
self.controller = nil
end
protected
# gets the action cache path for the given options.
def action_path_for(options)
ActionController::Caching::Actions::ActionCachePath.path_for(controller, options)
end
# Retrieve instance variables set in the controller.
def assigns(key)
controller.instance_variable_get("@#{key}")
end
private
def callback(timing)
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
__send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
end
def method_missing(method, *arguments, &block)
return if @controller.nil?
@controller.__send__(method, *arguments, &block)
end
end
end
end

View file

@ -51,47 +51,5 @@ module ActionController #:nodoc:
end
end
end
if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
def before(controller)
self.controller = controller
callback(:before) if controller.perform_caching
end
def after(controller)
callback(:after) if controller.perform_caching
# Clean up, so that the controller can be collected after this request
self.controller = nil
end
protected
# gets the action cache path for the given options.
def action_path_for(options)
ActionController::Caching::Actions::ActionCachePath.path_for(controller, options)
end
# Retrieve instance variables set in the controller.
def assigns(key)
controller.instance_variable_get("@#{key}")
end
private
def callback(timing)
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
__send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
end
def method_missing(method, *arguments, &block)
return if @controller.nil?
@controller.__send__(method, *arguments, &block)
end
end
end
end
end

View file

@ -1,4 +1,19 @@
require 'erb'
module ActionController
# The Failsafe middleware is usually the top-most middleware in the Rack
# middleware chain. It returns the underlying middleware's response, but if
# the underlying middle raises an exception then Failsafe will log the
# exception into the Rails log file, and will attempt to return an error
# message response.
#
# Failsafe is a last resort for logging errors and for telling the HTTP
# client that something went wrong. Do not confuse this with the
# ActionController::Rescue module, which is responsible for catching
# exceptions at deeper levels. Unlike Failsafe, which is as simple as
# possible, Rescue provides features that allow developers to hook into
# the error handling logic, and can customize the error message response
# based on the HTTP client's IP.
class Failsafe
cattr_accessor :error_file_path
self.error_file_path = Rails.public_path if defined?(Rails.public_path)
@ -11,7 +26,7 @@ module ActionController
@app.call(env)
rescue Exception => exception
# Reraise exception in test environment
if env["rack.test"]
if defined?(Rails) && Rails.env.test?
raise exception
else
failsafe_response(exception)
@ -21,18 +36,37 @@ module ActionController
private
def failsafe_response(exception)
log_failsafe_exception(exception)
[500, {'Content-Type' => 'text/html'}, failsafe_response_body]
[500, {'Content-Type' => 'text/html'}, [failsafe_response_body]]
rescue Exception => failsafe_error # Logger or IO errors
$stderr.puts "Error during failsafe response: #{failsafe_error}"
end
def failsafe_response_body
error_path = "#{self.class.error_file_path}/500.html"
if File.exist?(error_path)
File.read(error_path)
error_template_path = "#{self.class.error_file_path}/500.html"
if File.exist?(error_template_path)
begin
result = render_template(error_template_path)
rescue Exception
result = nil
end
else
"<html><body><h1>500 Internal Server Error</h1></body></html>"
result = nil
end
if result.nil?
result = "<html><body><h1>500 Internal Server Error</h1>" <<
"If you are the administrator of this website, then please read this web " <<
"application's log file to find out what went wrong.</body></html>"
end
result
end
# The default 500.html uses the h() method.
def h(text) # :nodoc:
ERB::Util.h(text)
end
def render_template(filename)
ERB.new(File.read(filename)).result(binding)
end
def log_failsafe_exception(exception)

View file

@ -120,6 +120,11 @@ module ActionController #:nodoc:
(@used.keys - keys).each{ |k| @used.delete(k) }
end
def store(session, key = "flash")
return if self.empty?
session[key] = self
end
private
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
# use() # marks the entire flash as used
@ -139,7 +144,10 @@ module ActionController #:nodoc:
protected
def perform_action_with_flash
perform_action_without_flash
remove_instance_variable(:@_flash) if defined? @_flash
if defined? @_flash
@_flash.store(session)
remove_instance_variable(:@_flash)
end
end
def reset_session_with_flash
@ -151,8 +159,8 @@ module ActionController #:nodoc:
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one.
def flash #:doc:
unless defined? @_flash
@_flash = session["flash"] ||= FlashHash.new
if !defined?(@_flash)
@_flash = session["flash"] || FlashHash.new
@_flash.sweep
end

View file

@ -183,7 +183,7 @@ module ActionController
request.env['REDIRECT_X_HTTP_AUTHORIZATION']
end
# Raises error unless the request credentials response value matches the expected value.
# Returns false unless the request credentials response value matches the expected value.
# First try the password as a ha1 digest password. If this fails, then try it as a plain
# text password.
def validate_digest_response(request, realm, &password_procedure)
@ -192,9 +192,12 @@ module ActionController
if valid_nonce && realm == credentials[:realm] && opaque == credentials[:opaque]
password = password_procedure.call(credentials[:username])
return false unless password
method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD']
[true, false].any? do |password_is_ha1|
expected = expected_response(request.env['REQUEST_METHOD'], request.env['REQUEST_URI'], credentials, password, password_is_ha1)
expected = expected_response(method, request.env['REQUEST_URI'], credentials, password, password_is_ha1)
expected == credentials[:response]
end
end

View file

@ -292,9 +292,7 @@ module ActionController
"rack.errors" => StringIO.new,
"rack.multithread" => true,
"rack.multiprocess" => true,
"rack.run_once" => false,
"rack.test" => true
"rack.run_once" => false
)
(headers || {}).each do |key, value|
@ -311,12 +309,7 @@ module ActionController
ActionController::Base.clear_last_instantiation!
app = @application
# Rack::Lint doesn't accept String headers or bodies in Ruby 1.9
unless RUBY_VERSION >= '1.9.0' && Rack.release <= '0.9.0'
app = Rack::Lint.new(app)
end
app = Rack::Lint.new(@application)
status, headers, body = app.call(env)
@request_count += 1
@ -333,7 +326,7 @@ module ActionController
end
@body = ""
if body.is_a?(String)
if body.respond_to?(:to_str)
@body << body
else
body.each { |part| @body << part }
@ -416,7 +409,7 @@ module ActionController
def multipart_requestify(params, first=true)
returning Hash.new do |p|
params.each do |key, value|
k = first ? CGI.escape(key.to_s) : "[#{CGI.escape(key.to_s)}]"
k = first ? key.to_s : "[#{key.to_s}]"
if Hash === value
multipart_requestify(value, false).each do |subkey, subvalue|
p[k + subkey] = subvalue

View file

@ -7,7 +7,6 @@ use "ActionController::Failsafe"
use lambda { ActionController::Base.session_store },
lambda { ActionController::Base.session_options }
use "ActionController::RewindableInput"
use "ActionController::ParamsParser"
use "Rack::MethodOverride"
use "Rack::Head"

View file

@ -1,14 +1,45 @@
module ActionController
class Reloader
class BodyWrapper
def initialize(body)
@body = body
end
def close
@body.close if @body.respond_to?(:close)
ensure
Dispatcher.cleanup_application
end
def method_missing(*args, &block)
@body.send(*args, &block)
end
def respond_to?(symbol, include_private = false)
symbol == :close || @body.respond_to?(symbol, include_private)
end
end
def initialize(app)
@app = app
end
def call(env)
Dispatcher.reload_application
@app.call(env)
ensure
Dispatcher.cleanup_application
status, headers, body = @app.call(env)
# We do not want to call 'cleanup_application' in an ensure block
# because the returned Rack response body may lazily generate its data. This
# is for example the case if one calls
#
# render :text => lambda { ... code here which refers to application models ... }
#
# in an ActionController.
#
# Instead, we will want to cleanup the application code after the request is
# completely finished. So we wrap the body in a BodyWrapper class so that
# when the Rack handler calls #close during the end of the request, we get to
# run our cleanup code.
[status, headers, BodyWrapper.new(body)]
end
end
end

View file

@ -95,6 +95,10 @@ module ActionController
end
end
def media_type
content_type.to_s
end
# Returns the accepted MIME type for the request.
def accepts
@accepts ||= begin
@ -383,7 +387,7 @@ EOM
alias_method :params, :parameters
def path_parameters=(parameters) #:nodoc:
@env["rack.routing_args"] = parameters
@env["action_controller.request.path_parameters"] = parameters
@symbolized_path_parameters = @parameters = nil
end
@ -399,7 +403,7 @@ EOM
#
# See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters
@env["rack.routing_args"] ||= {}
@env["action_controller.request.path_parameters"] ||= {}
end
# The request body is an IO input stream. If the RAW_POST_DATA environment

View file

@ -151,8 +151,8 @@ module ActionController # :nodoc:
if @body.respond_to?(:call)
@writer = lambda { |x| callback.call(x) }
@body.call(self, self)
elsif @body.is_a?(String)
@body.each_line(&callback)
elsif @body.respond_to?(:to_str)
yield @body
else
@body.each(&callback)
end

View file

@ -1,28 +0,0 @@
module ActionController
class RewindableInput
class RewindableIO < ActiveSupport::BasicObject
def initialize(io)
@io = io
@rewindable = io.is_a?(::StringIO)
end
def method_missing(method, *args, &block)
unless @rewindable
@io = ::StringIO.new(@io.read)
@rewindable = true
end
@io.__send__(method, *args, &block)
end
end
def initialize(app)
@app = app
end
def call(env)
env['rack.input'] = RewindableIO.new(env['rack.input'])
@app.call(env)
end
end
end

View file

@ -305,6 +305,7 @@ module ActionController
end
def add_route(path, options = {})
options.each { |k, v| options[k] = v.to_s if [:controller, :action].include?(k) && v.is_a?(Symbol) }
route = builder.build(path, options)
routes << route
route
@ -436,7 +437,7 @@ module ActionController
def recognize(request)
params = recognize_path(request.path, extract_request_environment(request))
request.path_parameters = params.with_indifferent_access
"#{params[:controller].camelize}Controller".constantize
"#{params[:controller].to_s.camelize}Controller".constantize
end
def recognize_path(path, environment={})

View file

@ -161,7 +161,7 @@ module ActionController #:nodoc:
content_type = content_type.to_s.strip # fixes a problem with extra '\r' with some browsers
headers.merge!(
'Content-Length' => options[:length],
'Content-Length' => options[:length].to_s,
'Content-Type' => content_type,
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary'

View file

@ -1,3 +1,4 @@
require 'rack/session/abstract/id'
module ActionController #:nodoc:
class TestRequest < Request #:nodoc:
attr_accessor :cookies, :session_options
@ -13,6 +14,8 @@ module ActionController #:nodoc:
@query_parameters = {}
@session = TestSession.new
default_rack_options = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
@session_options ||= {:id => generate_sid(default_rack_options[:sidbits])}.merge(default_rack_options)
initialize_default_values
initialize_containers
@ -110,6 +113,7 @@ module ActionController #:nodoc:
end
def recycle!
@env["action_controller.request.request_parameters"] = {}
self.query_parameters = {}
self.path_parameters = {}
@headers, @request_method, @accepts, @content_type = nil, nil, nil, nil
@ -120,6 +124,10 @@ module ActionController #:nodoc:
end
private
def generate_sid(sidbits)
"%0#{sidbits / 4}x" % rand(2**sidbits - 1)
end
def initialize_containers
@cookies = {}
end
@ -250,7 +258,7 @@ module ActionController #:nodoc:
def cookies
cookies = {}
Array(headers['Set-Cookie']).each do |cookie|
key, value = cookie.split(";").first.split("=")
key, value = cookie.split(";").first.split("=").map {|val| Rack::Utils.unescape(val)}
cookies[key] = value
end
cookies

View file

@ -1,64 +0,0 @@
require 'thread'
module Rack
# Rack::Reloader checks on every request, but at most every +secs+
# seconds, if a file loaded changed, and reloads it, logging to
# rack.errors.
#
# It is recommended you use ShowExceptions to catch SyntaxErrors etc.
class Reloader
def initialize(app, secs=10)
@app = app
@secs = secs # reload every @secs seconds max
@last = Time.now
end
def call(env)
if Time.now > @last + @secs
Thread.exclusive {
reload!(env['rack.errors'])
@last = Time.now
}
end
@app.call(env)
end
def reload!(stderr=$stderr)
need_reload = $LOADED_FEATURES.find_all { |loaded|
begin
if loaded =~ /\A[.\/]/ # absolute filename or 1.9
abs = loaded
else
abs = $LOAD_PATH.map { |path| ::File.join(path, loaded) }.
find { |file| ::File.exist? file }
end
if abs
::File.mtime(abs) > @last - @secs rescue false
else
false
end
end
}
need_reload.each { |l|
$LOADED_FEATURES.delete l
}
need_reload.each { |to_load|
begin
if require to_load
stderr.puts "#{self.class}: reloaded `#{to_load}'"
end
rescue LoadError, SyntaxError => e
raise e # Possibly ShowExceptions
end
}
stderr.flush
need_reload
end
end
end

View file

@ -3,7 +3,8 @@
# Rack is freely distributable under the terms of an MIT-style license.
# See COPYING or http://www.opensource.org/licenses/mit-license.php.
$:.unshift(File.expand_path(File.dirname(__FILE__)))
path = File.expand_path(File.dirname(__FILE__))
$:.unshift(path) unless $:.include?(path)
# The Rack main module, serving as a namespace for all core Rack
@ -14,7 +15,7 @@ $:.unshift(File.expand_path(File.dirname(__FILE__)))
module Rack
# The Rack protocol version number implemented.
VERSION = [0,1]
VERSION = [1,0]
# Return the Rack protocol version as a dotted string.
def self.version
@ -23,7 +24,7 @@ module Rack
# Return the Rack release as a dotted string.
def self.release
"1.0 bundled"
"1.0"
end
autoload :Builder, "rack/builder"

View file

@ -26,6 +26,8 @@ module Rack
headers = Utils::HeaderHash.new(headers)
if etag_matches?(env, headers) || modified_since?(env, headers)
status = 304
headers.delete('Content-Type')
headers.delete('Content-Length')
body = []
end
[status, headers, body]

View file

@ -33,17 +33,15 @@ module Rack
case encoding
when "gzip"
headers['Content-Encoding'] = "gzip"
headers.delete('Content-Length')
mtime = headers.key?("Last-Modified") ?
Time.httpdate(headers["Last-Modified"]) : Time.now
body = self.class.gzip(body, mtime)
size = Rack::Utils.bytesize(body)
headers = headers.merge("Content-Encoding" => "gzip", "Content-Length" => size.to_s)
[status, headers, [body]]
[status, headers, GzipStream.new(body, mtime)]
when "deflate"
body = self.class.deflate(body)
size = Rack::Utils.bytesize(body)
headers = headers.merge("Content-Encoding" => "deflate", "Content-Length" => size.to_s)
[status, headers, [body]]
headers['Content-Encoding'] = "deflate"
headers.delete('Content-Length')
[status, headers, DeflateStream.new(body)]
when "identity"
[status, headers, body]
when nil
@ -52,34 +50,47 @@ module Rack
end
end
def self.gzip(body, mtime)
io = StringIO.new
gzip = Zlib::GzipWriter.new(io)
gzip.mtime = mtime
class GzipStream
def initialize(body, mtime)
@body = body
@mtime = mtime
end
# TODO: Add streaming
body.each { |part| gzip << part }
def each(&block)
@writer = block
gzip =::Zlib::GzipWriter.new(self)
gzip.mtime = @mtime
@body.each { |part| gzip << part }
@body.close if @body.respond_to?(:close)
gzip.close
@writer = nil
end
gzip.close
return io.string
def write(data)
@writer.call(data)
end
end
DEFLATE_ARGS = [
Zlib::DEFAULT_COMPRESSION,
# drop the zlib header which causes both Safari and IE to choke
-Zlib::MAX_WBITS,
Zlib::DEF_MEM_LEVEL,
Zlib::DEFAULT_STRATEGY
]
class DeflateStream
DEFLATE_ARGS = [
Zlib::DEFAULT_COMPRESSION,
# drop the zlib header which causes both Safari and IE to choke
-Zlib::MAX_WBITS,
Zlib::DEF_MEM_LEVEL,
Zlib::DEFAULT_STRATEGY
]
# Loosely based on Mongrel's Deflate handler
def self.deflate(body)
deflater = Zlib::Deflate.new(*DEFLATE_ARGS)
def initialize(body)
@body = body
end
# TODO: Add streaming
body.each { |part| deflater << part }
return deflater.finish
def each
deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
@body.each { |part| yield deflater.deflate(part) }
@body.close if @body.respond_to?(:close)
yield deflater.finish
nil
end
end
end
end

View file

@ -10,16 +10,37 @@ module Rack
module Handler
def self.get(server)
return unless server
server = server.to_s
if klass = @handlers[server]
obj = Object
klass.split("::").each { |x| obj = obj.const_get(x) }
obj
else
Rack::Handler.const_get(server.capitalize)
try_require('rack/handler', server)
const_get(server)
end
end
# Transforms server-name constants to their canonical form as filenames,
# then tries to require them but silences the LoadError if not found
#
# Naming convention:
#
# Foo # => 'foo'
# FooBar # => 'foo_bar.rb'
# FooBAR # => 'foobar.rb'
# FOObar # => 'foobar.rb'
# FOOBAR # => 'foobar.rb'
# FooBarBaz # => 'foo_bar_baz.rb'
def self.try_require(prefix, const_name)
file = const_name.gsub(/^[A-Z]+/) { |pre| pre.downcase }.
gsub(/[A-Z]+[^A-Z]/, '_\&').downcase
require(::File.join(prefix, file))
rescue LoadError
end
def self.register(server, klass)
@handlers ||= {}
@handlers[server] = klass

View file

@ -15,7 +15,7 @@ module Rack
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({"rack.version" => [0,1],
env.update({"rack.version" => [1,0],
"rack.input" => $stdin,
"rack.errors" => $stderr,

View file

@ -1,6 +1,17 @@
require 'fcgi'
require 'socket'
require 'rack/content_length'
require 'rack/rewindable_input'
class FCGI::Stream
alias _rack_read_without_buffer read
def read(n, buffer=nil)
buf = _rack_read_without_buffer n
buffer.replace(buf.to_s) if buffer
buf
end
end
module Rack
module Handler
@ -13,34 +24,18 @@ module Rack
}
end
module ProperStream # :nodoc:
def each # This is missing by default.
while line = gets
yield line
end
end
def read(*args)
if args.empty?
super || "" # Empty string on EOF.
else
super
end
end
end
def self.serve(request, app)
app = Rack::ContentLength.new(app)
env = request.env
env.delete "HTTP_CONTENT_LENGTH"
request.in.extend ProperStream
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({"rack.version" => [0,1],
"rack.input" => request.in,
rack_input = RewindableInput.new(request.in)
env.update({"rack.version" => [1,0],
"rack.input" => rack_input,
"rack.errors" => request.err,
"rack.multithread" => false,
@ -57,12 +52,16 @@ module Rack
env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == ""
env.delete "CONTENT_LENGTH" if env["CONTENT_LENGTH"] == ""
status, headers, body = app.call(env)
begin
send_headers request.out, status, headers
send_body request.out, body
status, headers, body = app.call(env)
begin
send_headers request.out, status, headers
send_body request.out, body
ensure
body.close if body.respond_to? :close
end
ensure
body.close if body.respond_to? :close
rack_input.close
request.finish
end
end

View file

@ -15,7 +15,7 @@ module Rack
env = ENV.to_hash
env.delete "HTTP_CONTENT_LENGTH"
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({"rack.version" => [0,1],
env.update({"rack.version" => [1,0],
"rack.input" => StringIO.new($stdin.read.to_s),
"rack.errors" => $stderr,
"rack.multithread" => false,

View file

@ -10,7 +10,7 @@ module Rack
server = ::Mongrel::HttpServer.new(options[:Host] || '0.0.0.0',
options[:Port] || 8080)
# Acts like Rack::URLMap, utilizing Mongrel's own path finding methods.
# Use is similar to #run, replacing the app argument with a hash of
# Use is similar to #run, replacing the app argument with a hash of
# { path=>app, ... } or an instance of Rack::URLMap.
if options[:map]
if app.is_a? Hash
@ -45,7 +45,7 @@ module Rack
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({"rack.version" => [0,1],
env.update({"rack.version" => [1,0],
"rack.input" => request.body || StringIO.new(""),
"rack.errors" => $stderr,

View file

@ -7,14 +7,14 @@ module Rack
module Handler
class SCGI < ::SCGI::Processor
attr_accessor :app
def self.run(app, options=nil)
new(options.merge(:app=>app,
:host=>options[:Host],
:port=>options[:Port],
:socket=>options[:Socket])).listen
end
def initialize(settings = {})
@app = Rack::Chunked.new(Rack::ContentLength.new(settings[:app]))
@log = Object.new
@ -22,7 +22,7 @@ module Rack
def @log.error(*args); end
super(settings)
end
def process_request(request, input_body, socket)
env = {}.replace(request)
env.delete "HTTP_CONTENT_TYPE"
@ -32,7 +32,7 @@ module Rack
env["PATH_INFO"] = env["REQUEST_PATH"]
env["QUERY_STRING"] ||= ""
env["SCRIPT_NAME"] = ""
env.update({"rack.version" => [0,1],
env.update({"rack.version" => [1,0],
"rack.input" => StringIO.new(input_body),
"rack.errors" => $stderr,

View file

@ -23,7 +23,7 @@ module Rack
env = req.meta_vars
env.delete_if { |k, v| v.nil? }
env.update({"rack.version" => [0,1],
env.update({"rack.version" => [1,0],
"rack.input" => StringIO.new(req.body.to_s),
"rack.errors" => $stderr,

View file

@ -88,9 +88,9 @@ module Rack
## within the application. This may be an
## empty string, if the request URL targets
## the application root and does not have a
## trailing slash. This information should be
## decoded by the server if it comes from a
## URL.
## trailing slash. This value may be
## percent-encoded when I originating from
## a URL.
## <tt>QUERY_STRING</tt>:: The portion of the request URL that
## follows the <tt>?</tt>, if any. May be
@ -111,19 +111,48 @@ module Rack
## In addition to this, the Rack environment must include these
## Rack-specific variables:
## <tt>rack.version</tt>:: The Array [0,1], representing this version of Rack.
## <tt>rack.version</tt>:: The Array [1,0], representing this version of Rack.
## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL.
## <tt>rack.input</tt>:: See below, the input stream.
## <tt>rack.errors</tt>:: See below, the error stream.
## <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
## <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
## <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar).
##
## Additional environment specifications have approved to
## standardized middleware APIs. None of these are required to
## be implemented by the server.
## <tt>rack.session</tt>:: A hash like interface for storing request session data.
## The store must implement:
if session = env['rack.session']
## store(key, value) (aliased as []=);
assert("session #{session.inspect} must respond to store and []=") {
session.respond_to?(:store) && session.respond_to?(:[]=)
}
## fetch(key, default = nil) (aliased as []);
assert("session #{session.inspect} must respond to fetch and []") {
session.respond_to?(:fetch) && session.respond_to?(:[])
}
## delete(key);
assert("session #{session.inspect} must respond to delete") {
session.respond_to?(:delete)
}
## clear;
assert("session #{session.inspect} must respond to clear") {
session.respond_to?(:clear)
}
end
## The server or the application can store their own data in the
## environment, too. The keys must contain at least one dot,
## and should be prefixed uniquely. The prefix <tt>rack.</tt>
## is reserved for use with the Rack core distribution and must
## not be used otherwise.
## is reserved for use with the Rack core distribution and other
## accepted specifications and must not be used otherwise.
##
%w[REQUEST_METHOD SERVER_NAME SERVER_PORT
@ -202,9 +231,12 @@ module Rack
end
## === The Input Stream
##
## The input stream is an IO-like object which contains the raw HTTP
## POST data. If it is a file then it must be opened in binary mode.
def check_input(input)
## The input stream must respond to +gets+, +each+ and +read+.
[:gets, :each, :read].each { |method|
## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
[:gets, :each, :read, :rewind].each { |method|
assert("rack.input #{input} does not respond to ##{method}") {
input.respond_to? method
}
@ -222,10 +254,6 @@ module Rack
@input.size
end
def rewind
@input.rewind
end
## * +gets+ must be called without arguments and return a string,
## or +nil+ on EOF.
def gets(*args)
@ -237,21 +265,44 @@ module Rack
v
end
## * +read+ must be called without or with one integer argument
## and return a string, or +nil+ on EOF.
## * +read+ behaves like IO#read. Its signature is <tt>read([length, [buffer]])</tt>.
## If given, +length+ must be an non-negative Integer (>= 0) or +nil+, and +buffer+ must
## be a String and may not be nil. If +length+ is given and not nil, then this method
## reads at most +length+ bytes from the input stream. If +length+ is not given or nil,
## then this method reads all data until EOF.
## When EOF is reached, this method returns nil if +length+ is given and not nil, or ""
## if +length+ is not given or is nil.
## If +buffer+ is given, then the read data will be placed into +buffer+ instead of a
## newly created String object.
def read(*args)
assert("rack.input#read called with too many arguments") {
args.size <= 1
args.size <= 2
}
if args.size == 1
assert("rack.input#read called with non-integer argument") {
args.first.kind_of? Integer
if args.size >= 1
assert("rack.input#read called with non-integer and non-nil length") {
args.first.kind_of?(Integer) || args.first.nil?
}
assert("rack.input#read called with a negative length") {
args.first.nil? || args.first >= 0
}
end
if args.size >= 2
assert("rack.input#read called with non-String buffer") {
args[1].kind_of?(String)
}
end
v = @input.read(*args)
assert("rack.input#read didn't return a String") {
assert("rack.input#read didn't return nil or a String") {
v.nil? or v.instance_of? String
}
if args[0].nil?
assert("rack.input#read(nil) returned nil on EOF") {
!v.nil?
}
end
v
end
@ -266,6 +317,23 @@ module Rack
}
end
## * +rewind+ must be called without arguments. It rewinds the input
## stream back to the beginning. It must not raise Errno::ESPIPE:
## that is, it may not be a pipe or a socket. Therefore, handler
## developers must buffer the input data into some rewindable object
## if the underlying input stream is not rewindable.
def rewind(*args)
assert("rack.input#rewind called with arguments") { args.size == 0 }
assert("rack.input#rewind raised Errno::ESPIPE") {
begin
@input.rewind
true
rescue Errno::ESPIPE
false
end
}
end
## * +close+ must never be called on the input stream.
def close(*args)
assert("rack.input#close must not be called") { false }
@ -316,13 +384,14 @@ module Rack
## === The Status
def check_status(status)
## The status, if parsed as integer (+to_i+), must be greater than or equal to 100.
## This is an HTTP status. When parsed as integer (+to_i+), it must be
## greater than or equal to 100.
assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
end
## === The Headers
def check_headers(header)
## The header must respond to each, and yield values of key and value.
## The header must respond to +each+, and yield values of key and value.
assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
header.respond_to? :each
}
@ -344,7 +413,8 @@ module Rack
## The values of the header must be Strings,
assert("a header value must be a String, but the value of " +
"'#{key}' is a #{value.class}") { value.kind_of? String }
## consisting of lines (for multiple header values) seperated by "\n".
## consisting of lines (for multiple header values, e.g. multiple
## <tt>Set-Cookie</tt> values) seperated by "\n".
value.split("\n").each { |item|
## The lines must not contain characters below 037.
assert("invalid header value #{key}: #{item.inspect}") {
@ -416,7 +486,7 @@ module Rack
## === The Body
def each
@closed = false
## The Body must respond to #each
## The Body must respond to +each+
@body.each { |part|
## and must only yield String values.
assert("Body yielded non-string value #{part.inspect}") {
@ -425,14 +495,19 @@ module Rack
yield part
}
##
## If the Body responds to #close, it will be called after iteration.
## The Body itself should not be an instance of String, as this will
## break in Ruby 1.9.
##
## If the Body responds to +close+, it will be called after iteration.
# XXX howto: assert("Body has not been closed") { @closed }
##
## If the Body responds to #to_path, it must return a String
## If the Body responds to +to_path+, it must return a String
## identifying the location of a file whose contents are identical
## to that produced by calling #each.
## to that produced by calling +each+; this may be used by the
## server as an alternative, possibly more efficient way to
## transport the response.
if @body.respond_to?(:to_path)
assert("The file identified by body.to_path does not exist") {

View file

@ -40,7 +40,7 @@ module Rack
end
DEFAULT_ENV = {
"rack.version" => [0,1],
"rack.version" => [1,0],
"rack.input" => StringIO.new,
"rack.errors" => StringIO.new,
"rack.multithread" => true,
@ -73,14 +73,17 @@ module Rack
# Return the Rack environment used for a request to +uri+.
def self.env_for(uri="", opts={})
uri = URI(uri)
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
env = DEFAULT_ENV.dup
env["REQUEST_METHOD"] = opts[:method] || "GET"
env["REQUEST_METHOD"] = opts[:method] ? opts[:method].to_s.upcase : "GET"
env["SERVER_NAME"] = uri.host || "example.org"
env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
env["QUERY_STRING"] = uri.query.to_s
env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
env["rack.url_scheme"] = uri.scheme || "http"
env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
env["SCRIPT_NAME"] = opts[:script_name] || ""
@ -90,6 +93,27 @@ module Rack
env["rack.errors"] = StringIO.new
end
if params = opts[:params]
if env["REQUEST_METHOD"] == "GET"
params = Utils.parse_nested_query(params) if params.is_a?(String)
params.update(Utils.parse_nested_query(env["QUERY_STRING"]))
env["QUERY_STRING"] = Utils.build_nested_query(params)
elsif !opts.has_key?(:input)
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
if params.is_a?(Hash)
if data = Utils::Multipart.build_multipart(params)
opts[:input] = data
opts["CONTENT_LENGTH"] ||= data.length.to_s
opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}"
else
opts[:input] = Utils.build_nested_query(params)
end
else
opts[:input] = params
end
end
end
opts[:input] ||= ""
if String === opts[:input]
env["rack.input"] = StringIO.new(opts[:input])
@ -125,7 +149,7 @@ module Rack
@body = ""
body.each { |part| @body << part }
@errors = errors.string
@errors = errors.string if errors.respond_to?(:string)
end
# Status

View file

@ -0,0 +1,106 @@
# Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
# All files in this distribution are subject to the terms of the Ruby license.
require 'pathname'
module Rack
# High performant source reloader
#
# This class acts as Rack middleware.
#
# What makes it especially suited for use in a production environment is that
# any file will only be checked once and there will only be made one system
# call stat(2).
#
# Please note that this will not reload files in the background, it does so
# only when actively called.
#
# It is performing a check/reload cycle at the start of every request, but
# also respects a cool down time, during which nothing will be done.
class Reloader
def initialize(app, cooldown = 10, backend = Stat)
@app = app
@cooldown = cooldown
@last = (Time.now - cooldown)
@cache = {}
@mtimes = {}
extend backend
end
def call(env)
if @cooldown and Time.now > @last + @cooldown
if Thread.list.size > 1
Thread.exclusive{ reload! }
else
reload!
end
@last = Time.now
end
@app.call(env)
end
def reload!(stderr = $stderr)
rotation do |file, mtime|
previous_mtime = @mtimes[file] ||= mtime
safe_load(file, mtime, stderr) if mtime > previous_mtime
end
end
# A safe Kernel::load, issuing the hooks depending on the results
def safe_load(file, mtime, stderr = $stderr)
load(file)
stderr.puts "#{self.class}: reloaded `#{file}'"
file
rescue LoadError, SyntaxError => ex
stderr.puts ex
ensure
@mtimes[file] = mtime
end
module Stat
def rotation
files = [$0, *$LOADED_FEATURES].uniq
paths = ['./', *$LOAD_PATH].uniq
files.map{|file|
next if file =~ /\.(so|bundle)$/ # cannot reload compiled files
found, stat = figure_path(file, paths)
next unless found and stat and mtime = stat.mtime
@cache[file] = found
yield(found, mtime)
}.compact
end
# Takes a relative or absolute +file+ name, a couple possible +paths+ that
# the +file+ might reside in. Returns the full path and File::Stat for the
# path.
def figure_path(file, paths)
found = @cache[file]
found = file if !found and Pathname.new(file).absolute?
found, stat = safe_stat(found)
return found, stat if found
paths.each do |possible_path|
path = ::File.join(possible_path, file)
found, stat = safe_stat(path)
return ::File.expand_path(found), stat if found
end
end
def safe_stat(file)
return unless file
stat = ::File.stat(file)
return file, stat if stat.file?
rescue Errno::ENOENT, Errno::ENOTDIR
@cache.delete(file) and false
end
end
end
end

View file

@ -17,7 +17,7 @@ module Rack
# The environment of the request.
attr_reader :env
def self.new(env)
def self.new(env, *args)
if self == Rack::Request
env["rack.request"] ||= super
else
@ -38,6 +38,8 @@ module Rack
def query_string; @env["QUERY_STRING"].to_s end
def content_length; @env['CONTENT_LENGTH'] end
def content_type; @env['CONTENT_TYPE'] end
def session; @env['rack.session'] ||= {} end
def session_options; @env['rack.session.options'] ||= {} end
# The media type (type/subtype) portion of the CONTENT_TYPE header
# without any media type parameters. e.g., when CONTENT_TYPE is
@ -46,7 +48,7 @@ module Rack
# For more information on the use of media types in HTTP, see:
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
def media_type
content_type && content_type.split(/\s*[;,]\s*/, 2)[0].downcase
content_type && content_type.split(/\s*[;,]\s*/, 2).first.downcase
end
# The media type parameters provided in CONTENT_TYPE as a Hash, or
@ -92,6 +94,14 @@ module Rack
'multipart/form-data'
]
# The set of media-types. Requests that do not indicate
# one of the media types presents in this list will not be eligible
# for param parsing like soap attachments or generic multiparts
PARSEABLE_DATA_MEDIA_TYPES = [
'multipart/related',
'multipart/mixed'
]
# Determine whether the request body contains form-data by checking
# the request media_type against registered form-data media-types:
# "application/x-www-form-urlencoded" and "multipart/form-data". The
@ -101,6 +111,12 @@ module Rack
FORM_DATA_MEDIA_TYPES.include?(media_type)
end
# Determine whether the request body contains data by checking
# the request media_type against registered parse-data media-types
def parseable_data?
PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
end
# Returns the data recieved in the query string.
def GET
if @env["rack.request.query_string"] == query_string
@ -119,7 +135,7 @@ module Rack
def POST
if @env["rack.request.form_input"].eql? @env["rack.input"]
@env["rack.request.form_hash"]
elsif form_data?
elsif form_data? || parseable_data?
@env["rack.request.form_input"] = @env["rack.input"]
unless @env["rack.request.form_hash"] =
Utils::Multipart.parse_multipart(env)
@ -131,12 +147,7 @@ module Rack
@env["rack.request.form_vars"] = form_vars
@env["rack.request.form_hash"] = Utils.parse_nested_query(form_vars)
begin
@env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)
rescue Errno::ESPIPE
# Handles exceptions raised by input streams that cannot be rewound
# such as when using plain CGI under Apache
end
@env["rack.input"].rewind
end
@env["rack.request.form_hash"]
else
@ -212,10 +223,12 @@ module Rack
url
end
def path
script_name + path_info
end
def fullpath
path = script_name + path_info
path << "?" << query_string unless query_string.empty?
path
query_string.empty? ? path : "#{path}?#{query_string}"
end
def accept_encoding

View file

@ -95,6 +95,10 @@ module Rack
:expires => Time.at(0) }.merge(value))
end
def redirect(target, status=302)
self.status = status
self["Location"] = target
end
def finish(&block)
@block = block
@ -120,7 +124,7 @@ module Rack
#
def write(str)
s = str.to_s
@length += s.size
@length += Rack::Utils.bytesize(s)
@writer.call s
header["Content-Length"] = @length.to_s

View file

@ -0,0 +1,98 @@
require 'tempfile'
module Rack
# Class which can make any IO object rewindable, including non-rewindable ones. It does
# this by buffering the data into a tempfile, which is rewindable.
#
# rack.input is required to be rewindable, so if your input stream IO is non-rewindable
# by nature (e.g. a pipe or a socket) then you can wrap it in an object of this class
# to easily make it rewindable.
#
# Don't forget to call #close when you're done. This frees up temporary resources that
# RewindableInput uses, though it does *not* close the original IO object.
class RewindableInput
def initialize(io)
@io = io
@rewindable_io = nil
@unlinked = false
end
def gets
make_rewindable unless @rewindable_io
@rewindable_io.gets
end
def read(*args)
make_rewindable unless @rewindable_io
@rewindable_io.read(*args)
end
def each(&block)
make_rewindable unless @rewindable_io
@rewindable_io.each(&block)
end
def rewind
make_rewindable unless @rewindable_io
@rewindable_io.rewind
end
# Closes this RewindableInput object without closing the originally
# wrapped IO oject. Cleans up any temporary resources that this RewindableInput
# has created.
#
# This method may be called multiple times. It does nothing on subsequent calls.
def close
if @rewindable_io
if @unlinked
@rewindable_io.close
else
@rewindable_io.close!
end
@rewindable_io = nil
end
end
private
# Ruby's Tempfile class has a bug. Subclass it and fix it.
class Tempfile < ::Tempfile
def _close
@tmpfile.close if @tmpfile
@data[1] = nil if @data
@tmpfile = nil
end
end
def make_rewindable
# Buffer all data into a tempfile. Since this tempfile is private to this
# RewindableInput object, we chmod it so that nobody else can read or write
# it. On POSIX filesystems we also unlink the file so that it doesn't
# even have a file entry on the filesystem anymore, though we can still
# access it because we have the file handle open.
@rewindable_io = Tempfile.new('RackRewindableInput')
@rewindable_io.chmod(0000)
if filesystem_has_posix_semantics?
@rewindable_io.unlink
@unlinked = true
end
buffer = ""
while @io.read(1024 * 4, buffer)
entire_buffer_written_out = false
while !entire_buffer_written_out
written = @rewindable_io.write(buffer)
entire_buffer_written_out = written == buffer.size
if !entire_buffer_written_out
buffer.slice!(0 .. written - 1)
end
end
end
@rewindable_io.rewind
end
def filesystem_has_posix_semantics?
RUBY_PLATFORM !~ /(mswin|mingw|cygwin|java)/
end
end
end

View file

@ -30,7 +30,7 @@ module Rack
location = location.chomp('/')
[host, location, app]
}.sort_by { |(h, l, a)| [-l.size, h.to_s.size] } # Longest path first
}.sort_by { |(h, l, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
end
def call(env)

View file

@ -1,3 +1,5 @@
# -*- encoding: binary -*-
require 'set'
require 'tempfile'
@ -63,7 +65,7 @@ module Rack
module_function :parse_nested_query
def normalize_params(params, name, v = nil)
name =~ %r([\[\]]*([^\[\]]+)\]*)
name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
k = $1 || ''
after = $' || ''
@ -73,12 +75,12 @@ module Rack
params[k] = v
elsif after == "[]"
params[k] ||= []
raise TypeError unless params[k].is_a?(Array)
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
params[k] << v
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
child_key = $1
params[k] ||= []
raise TypeError unless params[k].is_a?(Array)
raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
normalize_params(params[k].last, child_key, v)
else
@ -86,6 +88,7 @@ module Rack
end
else
params[k] ||= {}
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
params[k] = normalize_params(params[k], after, v)
end
@ -104,6 +107,25 @@ module Rack
end
module_function :build_query
def build_nested_query(value, prefix = nil)
case value
when Array
value.map { |v|
build_nested_query(v, "#{prefix}[]")
}.join("&")
when Hash
value.map { |k, v|
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
}.join("&")
when String
raise ArgumentError, "value must be a Hash" if prefix.nil?
"#{prefix}=#{escape(value)}"
else
prefix
end
end
module_function :build_nested_query
# Escape ampersands, brackets and quotes to their HTML/XML entities.
def escape_html(string)
string.to_s.gsub("&", "&amp;").
@ -288,11 +310,39 @@ module Rack
# Usually, Rack::Request#POST takes care of calling this.
module Multipart
class UploadedFile
# The filename, *not* including the path, of the "uploaded" file
attr_reader :original_filename
# The content type of the "uploaded" file
attr_accessor :content_type
def initialize(path, content_type = "text/plain", binary = false)
raise "#{path} file does not exist" unless ::File.exist?(path)
@content_type = content_type
@original_filename = ::File.basename(path)
@tempfile = Tempfile.new(@original_filename)
@tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
@tempfile.binmode if binary
FileUtils.copy_file(path, @tempfile.path)
end
def path
@tempfile.path
end
alias_method :local_path, :path
def method_missing(method_name, *args, &block) #:nodoc:
@tempfile.__send__(method_name, *args, &block)
end
end
EOL = "\r\n"
MULTIPART_BOUNDARY = "AaB03x"
def self.parse_multipart(env)
unless env['CONTENT_TYPE'] =~
%r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
%r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
nil
else
boundary = "--#{$1}"
@ -301,13 +351,16 @@ module Rack
buf = ""
content_length = env['CONTENT_LENGTH'].to_i
input = env['rack.input']
input.rewind
boundary_size = boundary.size + EOL.size
boundary_size = Utils.bytesize(boundary) + EOL.size
bufsize = 16384
content_length -= boundary_size
status = input.read(boundary_size)
read_buffer = ''
status = input.read(boundary_size, read_buffer)
raise EOFError, "bad content body" unless status == boundary + EOL
rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
@ -318,15 +371,15 @@ module Rack
filename = content_type = name = nil
until head && buf =~ rx
if !head && i = buf.index("\r\n\r\n")
if !head && i = buf.index(EOL+EOL)
head = buf.slice!(0, i+2) # First \r\n
buf.slice!(0, 2) # Second \r\n
filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1]
content_type = head[/Content-Type: (.*)\r\n/ni, 1]
name = head[/Content-Disposition:.* name="?([^\";]*)"?/ni, 1]
content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
if filename
if content_type || filename
body = Tempfile.new("RackMultipart")
body.binmode if body.respond_to?(:binmode)
end
@ -339,7 +392,7 @@ module Rack
body << buf.slice!(0, buf.size - (boundary_size+4))
end
c = input.read(bufsize < content_length ? bufsize : content_length)
c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
raise EOFError, "bad content body" if c.nil? || c.empty?
buf << c
content_length -= c.size
@ -368,6 +421,12 @@ module Rack
data = {:filename => filename, :type => content_type,
:name => name, :tempfile => body, :head => head}
elsif !filename && content_type
body.rewind
# Generic multipart cases, not coming from a form
data = {:type => content_type,
:name => name, :tempfile => body, :head => head}
else
data = body
end
@ -377,16 +436,81 @@ module Rack
break if buf.empty? || content_length == -1
}
begin
input.rewind if input.respond_to?(:rewind)
rescue Errno::ESPIPE
# Handles exceptions raised by input streams that cannot be rewound
# such as when using plain CGI under Apache
end
input.rewind
params
end
end
def self.build_multipart(params, first = true)
if first
unless params.is_a?(Hash)
raise ArgumentError, "value must be a Hash"
end
multipart = false
query = lambda { |value|
case value
when Array
value.each(&query)
when Hash
value.values.each(&query)
when UploadedFile
multipart = true
end
}
params.values.each(&query)
return nil unless multipart
end
flattened_params = Hash.new
params.each do |key, value|
k = first ? key.to_s : "[#{key}]"
case value
when Array
value.map { |v|
build_multipart(v, false).each { |subkey, subvalue|
flattened_params["#{k}[]#{subkey}"] = subvalue
}
}
when Hash
build_multipart(value, false).each { |subkey, subvalue|
flattened_params[k + subkey] = subvalue
}
else
flattened_params[k] = value
end
end
if first
flattened_params.map { |name, file|
if file.respond_to?(:original_filename)
::File.open(file.path, "rb") do |f|
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
<<-EOF
--#{MULTIPART_BOUNDARY}\r
Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
Content-Type: #{file.content_type}\r
Content-Length: #{::File.stat(file.path).size}\r
\r
#{f.read}\r
EOF
end
else
<<-EOF
--#{MULTIPART_BOUNDARY}\r
Content-Disposition: form-data; name="#{name}"\r
\r
#{file}\r
EOF
end
}.join + "--#{MULTIPART_BOUNDARY}--\r"
else
flattened_params
end
end
end
end
end