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

@ -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