aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/dispatch
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller/dispatch')
-rw-r--r--actionpack/lib/action_controller/dispatch/dispatcher.rb116
-rw-r--r--actionpack/lib/action_controller/dispatch/params_parser.rb71
-rw-r--r--actionpack/lib/action_controller/dispatch/rack/failsafe.rb52
-rw-r--r--actionpack/lib/action_controller/dispatch/rack/lock.rb16
-rw-r--r--actionpack/lib/action_controller/dispatch/rack/middleware_stack.rb109
-rw-r--r--actionpack/lib/action_controller/dispatch/rack/middlewares.rb21
-rwxr-xr-xactionpack/lib/action_controller/dispatch/request.rb492
-rw-r--r--actionpack/lib/action_controller/dispatch/request_parser.rb315
-rw-r--r--actionpack/lib/action_controller/dispatch/rescue.rb179
-rw-r--r--actionpack/lib/action_controller/dispatch/response.rb255
-rw-r--r--actionpack/lib/action_controller/dispatch/rewindable_input.rb28
-rw-r--r--actionpack/lib/action_controller/dispatch/status_codes.rb88
-rw-r--r--actionpack/lib/action_controller/dispatch/templates/rescues/_request_and_response.erb24
-rw-r--r--actionpack/lib/action_controller/dispatch/templates/rescues/_trace.erb26
-rw-r--r--actionpack/lib/action_controller/dispatch/templates/rescues/diagnostics.erb10
-rw-r--r--actionpack/lib/action_controller/dispatch/templates/rescues/layout.erb29
-rw-r--r--actionpack/lib/action_controller/dispatch/templates/rescues/missing_template.erb2
-rw-r--r--actionpack/lib/action_controller/dispatch/templates/rescues/routing_error.erb10
-rw-r--r--actionpack/lib/action_controller/dispatch/templates/rescues/template_error.erb21
-rw-r--r--actionpack/lib/action_controller/dispatch/templates/rescues/unknown_action.erb2
-rw-r--r--actionpack/lib/action_controller/dispatch/uploaded_file.rb44
-rw-r--r--actionpack/lib/action_controller/dispatch/url_encoded_pair_parser.rb155
22 files changed, 2065 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/dispatch/dispatcher.rb b/actionpack/lib/action_controller/dispatch/dispatcher.rb
new file mode 100644
index 0000000000..714e270781
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/dispatcher.rb
@@ -0,0 +1,116 @@
+module ActionController
+ # Dispatches requests to the appropriate controller and takes care of
+ # reloading the app after each request when Dependencies.load? is true.
+ class Dispatcher
+ class << self
+ def define_dispatcher_callbacks(cache_classes)
+ unless cache_classes
+ # Development mode callbacks
+ before_dispatch :reload_application
+ after_dispatch :cleanup_application
+
+ ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false
+ end
+
+ if defined?(ActiveRecord)
+ after_dispatch :checkin_connections
+ to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers }
+ end
+
+ after_dispatch :flush_logger if Base.logger && Base.logger.respond_to?(:flush)
+
+ to_prepare do
+ I18n.reload!
+ end
+ end
+
+ # DEPRECATE: Remove CGI support
+ def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
+ new(output).dispatch_cgi(cgi, session_options)
+ end
+
+ # Add a preparation callback. Preparation callbacks are run before every
+ # request in development mode, and before the first request in production
+ # mode.
+ #
+ # An optional identifier may be supplied for the callback. If provided,
+ # to_prepare may be called again with the same identifier to replace the
+ # existing callback. Passing an identifier is a suggested practice if the
+ # code adding a preparation block may be reloaded.
+ def to_prepare(identifier = nil, &block)
+ @prepare_dispatch_callbacks ||= ActiveSupport::Callbacks::CallbackChain.new
+ callback = ActiveSupport::Callbacks::Callback.new(:prepare_dispatch, block, :identifier => identifier)
+ @prepare_dispatch_callbacks.replace_or_append!(callback)
+ end
+ end
+
+ cattr_accessor :middleware
+ self.middleware = MiddlewareStack.new do |middleware|
+ middlewares = File.join(File.dirname(__FILE__), "rack", "middlewares.rb")
+ middleware.instance_eval(File.read(middlewares))
+ end
+
+ include ActiveSupport::Callbacks
+ define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch
+
+ # DEPRECATE: Remove arguments, since they are only used by CGI
+ def initialize(output = $stdout, request = nil, response = nil)
+ @output = output
+ @app = @@middleware.build(lambda { |env| self.dup._call(env) })
+ end
+
+ def dispatch
+ begin
+ run_callbacks :before_dispatch
+ Routing::Routes.call(@env)
+ rescue Exception => exception
+ if controller ||= (::ApplicationController rescue Base)
+ controller.call_with_exception(@env, exception).to_a
+ else
+ raise exception
+ end
+ ensure
+ run_callbacks :after_dispatch, :enumerator => :reverse_each
+ end
+ end
+
+ # DEPRECATE: Remove CGI support
+ def dispatch_cgi(cgi, session_options)
+ CGIHandler.dispatch_cgi(self, cgi, @output)
+ end
+
+ def call(env)
+ @app.call(env)
+ end
+
+ def _call(env)
+ @env = env
+ dispatch
+ end
+
+ def reload_application
+ # Run prepare callbacks before every request in development mode
+ run_callbacks :prepare_dispatch
+
+ Routing::Routes.reload
+ end
+
+ # Cleanup the application by clearing out loaded classes so they can
+ # be reloaded on the next request without restarting the server.
+ def cleanup_application
+ ActiveRecord::Base.reset_subclasses if defined?(ActiveRecord)
+ ActiveSupport::Dependencies.clear
+ ActiveRecord::Base.clear_reloadable_connections! if defined?(ActiveRecord)
+ end
+
+ def flush_logger
+ Base.logger.flush
+ end
+
+ def checkin_connections
+ # Don't return connection (and peform implicit rollback) if this request is a part of integration test
+ return if @env.key?("rack.test")
+ ActiveRecord::Base.clear_active_connections!
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/dispatch/params_parser.rb b/actionpack/lib/action_controller/dispatch/params_parser.rb
new file mode 100644
index 0000000000..d269fe07fa
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/params_parser.rb
@@ -0,0 +1,71 @@
+module ActionController
+ class ParamsParser
+ ActionController::Base.param_parsers[Mime::XML] = :xml_simple
+ ActionController::Base.param_parsers[Mime::JSON] = :json
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ if params = parse_formatted_parameters(env)
+ env["action_controller.request.request_parameters"] = params
+ end
+
+ @app.call(env)
+ end
+
+ private
+ def parse_formatted_parameters(env)
+ request = Request.new(env)
+
+ return false if request.content_length.zero?
+
+ mime_type = content_type_from_legacy_post_data_format_header(env) || request.content_type
+ strategy = ActionController::Base.param_parsers[mime_type]
+
+ return false unless strategy
+
+ case strategy
+ when Proc
+ strategy.call(request.raw_post)
+ when :xml_simple, :xml_node
+ body = request.raw_post
+ body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
+ when :yaml
+ YAML.load(request.raw_post)
+ when :json
+ body = request.raw_post
+ if body.blank?
+ {}
+ else
+ data = ActiveSupport::JSON.decode(body)
+ data = {:_json => data} unless data.is_a?(Hash)
+ data.with_indifferent_access
+ end
+ else
+ false
+ end
+ rescue Exception => e # YAML, XML or Ruby code block errors
+ raise
+ { "body" => request.raw_post,
+ "content_type" => request.content_type,
+ "content_length" => request.content_length,
+ "exception" => "#{e.message} (#{e.class})",
+ "backtrace" => e.backtrace }
+ end
+
+ def content_type_from_legacy_post_data_format_header(env)
+ if x_post_format = env['HTTP_X_POST_DATA_FORMAT']
+ case x_post_format.to_s.downcase
+ when 'yaml'
+ return Mime::YAML
+ when 'xml'
+ return Mime::XML
+ end
+ end
+
+ nil
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/dispatch/rack/failsafe.rb b/actionpack/lib/action_controller/dispatch/rack/failsafe.rb
new file mode 100644
index 0000000000..567581142c
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/rack/failsafe.rb
@@ -0,0 +1,52 @@
+module ActionController
+ class Failsafe
+ cattr_accessor :error_file_path
+ self.error_file_path = Rails.public_path if defined?(Rails.public_path)
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ @app.call(env)
+ rescue Exception => exception
+ # Reraise exception in test environment
+ if env["rack.test"]
+ raise exception
+ else
+ failsafe_response(exception)
+ end
+ end
+
+ private
+ def failsafe_response(exception)
+ log_failsafe_exception(exception)
+ [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)
+ else
+ "<html><body><h1>500 Internal Server Error</h1></body></html>"
+ end
+ end
+
+ def log_failsafe_exception(exception)
+ message = "/!\\ FAILSAFE /!\\ #{Time.now}\n Status: 500 Internal Server Error\n"
+ message << " #{exception}\n #{exception.backtrace.join("\n ")}" if exception
+ failsafe_logger.fatal(message)
+ end
+
+ def failsafe_logger
+ if defined?(Rails) && Rails.logger
+ Rails.logger
+ else
+ Logger.new($stderr)
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/dispatch/rack/lock.rb b/actionpack/lib/action_controller/dispatch/rack/lock.rb
new file mode 100644
index 0000000000..c50762216e
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/rack/lock.rb
@@ -0,0 +1,16 @@
+module ActionController
+ class Lock
+ FLAG = 'rack.multithread'.freeze
+
+ def initialize(app, lock = Mutex.new)
+ @app, @lock = app, lock
+ end
+
+ def call(env)
+ old, env[FLAG] = env[FLAG], false
+ @lock.synchronize { @app.call(env) }
+ ensure
+ env[FLAG] = old
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/dispatch/rack/middleware_stack.rb b/actionpack/lib/action_controller/dispatch/rack/middleware_stack.rb
new file mode 100644
index 0000000000..dbc2fda41e
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/rack/middleware_stack.rb
@@ -0,0 +1,109 @@
+module ActionController
+ class MiddlewareStack < Array
+ class Middleware
+ def self.new(klass, *args, &block)
+ if klass.is_a?(self)
+ klass
+ else
+ super
+ end
+ end
+
+ attr_reader :args, :block
+
+ def initialize(klass, *args, &block)
+ @klass = klass
+
+ options = args.extract_options!
+ if options.has_key?(:if)
+ @conditional = options.delete(:if)
+ else
+ @conditional = true
+ end
+ args << options unless options.empty?
+
+ @args = args
+ @block = block
+ end
+
+ def klass
+ if @klass.is_a?(Class)
+ @klass
+ else
+ @klass.to_s.constantize
+ end
+ rescue NameError
+ @klass
+ end
+
+ def active?
+ if @conditional.respond_to?(:call)
+ @conditional.call
+ else
+ @conditional
+ end
+ end
+
+ def ==(middleware)
+ case middleware
+ when Middleware
+ klass == middleware.klass
+ when Class
+ klass == middleware
+ else
+ klass == middleware.to_s.constantize
+ end
+ end
+
+ def inspect
+ str = klass.to_s
+ args.each { |arg| str += ", #{arg.inspect}" }
+ str
+ end
+
+ def build(app)
+ if block
+ klass.new(app, *args, &block)
+ else
+ klass.new(app, *args)
+ end
+ end
+ end
+
+ def initialize(*args, &block)
+ super(*args)
+ block.call(self) if block_given?
+ end
+
+ def insert(index, *args, &block)
+ index = self.index(index) unless index.is_a?(Integer)
+ middleware = Middleware.new(*args, &block)
+ super(index, middleware)
+ end
+
+ alias_method :insert_before, :insert
+
+ def insert_after(index, *args, &block)
+ index = self.index(index) unless index.is_a?(Integer)
+ insert(index + 1, *args, &block)
+ end
+
+ def swap(target, *args, &block)
+ insert_before(target, *args, &block)
+ delete(target)
+ end
+
+ def use(*args, &block)
+ middleware = Middleware.new(*args, &block)
+ push(middleware)
+ end
+
+ def active
+ find_all { |middleware| middleware.active? }
+ end
+
+ def build(app)
+ active.reverse.inject(app) { |a, e| e.build(a) }
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/dispatch/rack/middlewares.rb b/actionpack/lib/action_controller/dispatch/rack/middlewares.rb
new file mode 100644
index 0000000000..f9cfc2b18e
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/rack/middlewares.rb
@@ -0,0 +1,21 @@
+use "Rack::Lock", :if => lambda {
+ !ActionController::Base.allow_concurrency
+}
+
+use "ActionController::Failsafe"
+
+["ActionController::Session::CookieStore",
+ "ActionController::Session::MemCacheStore",
+ "ActiveRecord::SessionStore"].each do |store|
+ use(store, ActionController::Base.session_options,
+ :if => lambda {
+ if session_store = ActionController::Base.session_store
+ session_store.name == store
+ end
+ }
+ )
+end
+
+use "ActionController::RewindableInput"
+use "ActionController::ParamsParser"
+use "Rack::MethodOverride"
diff --git a/actionpack/lib/action_controller/dispatch/request.rb b/actionpack/lib/action_controller/dispatch/request.rb
new file mode 100755
index 0000000000..f8c77241b9
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/request.rb
@@ -0,0 +1,492 @@
+require 'tempfile'
+require 'stringio'
+require 'strscan'
+
+require 'active_support/memoizable'
+require 'action_controller/cgi_ext'
+
+module ActionController
+ class Request < Rack::Request
+ extend ActiveSupport::Memoizable
+
+ %w[ AUTH_TYPE GATEWAY_INTERFACE
+ PATH_TRANSLATED REMOTE_HOST
+ REMOTE_IDENT REMOTE_USER REMOTE_ADDR
+ SERVER_NAME SERVER_PROTOCOL
+
+ HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
+ HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
+ HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
+ define_method(env.sub(/^HTTP_/n, '').downcase) do
+ @env[env]
+ end
+ end
+
+ def key?(key)
+ @env.key?(key)
+ end
+
+ HTTP_METHODS = %w(get head put post delete options)
+ HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h }
+
+ # Returns the true HTTP request \method as a lowercase symbol, such as
+ # <tt>:get</tt>. If the request \method is not listed in the HTTP_METHODS
+ # constant above, an UnknownHttpMethod exception is raised.
+ def request_method
+ HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
+ end
+ memoize :request_method
+
+ # Returns the HTTP request \method used for action processing as a
+ # lowercase symbol, such as <tt>:post</tt>. (Unlike #request_method, this
+ # method returns <tt>:get</tt> for a HEAD request because the two are
+ # functionally equivalent from the application's perspective.)
+ def method
+ request_method == :head ? :get : request_method
+ end
+
+ # Is this a GET (or HEAD) request? Equivalent to <tt>request.method == :get</tt>.
+ def get?
+ method == :get
+ end
+
+ # Is this a POST request? Equivalent to <tt>request.method == :post</tt>.
+ def post?
+ request_method == :post
+ end
+
+ # Is this a PUT request? Equivalent to <tt>request.method == :put</tt>.
+ def put?
+ request_method == :put
+ end
+
+ # Is this a DELETE request? Equivalent to <tt>request.method == :delete</tt>.
+ def delete?
+ request_method == :delete
+ end
+
+ # Is this a HEAD request? Since <tt>request.method</tt> sees HEAD as <tt>:get</tt>,
+ # this \method checks the actual HTTP \method directly.
+ def head?
+ request_method == :head
+ end
+
+ # Provides access to the request's HTTP headers, for example:
+ #
+ # request.headers["Content-Type"] # => "text/plain"
+ def headers
+ ActionController::Http::Headers.new(@env)
+ end
+ memoize :headers
+
+ # Returns the content length of the request as an integer.
+ def content_length
+ super.to_i
+ end
+
+ # The MIME type of the HTTP request, such as Mime::XML.
+ #
+ # For backward compatibility, the post \format is extracted from the
+ # X-Post-Data-Format HTTP header if present.
+ def content_type
+ if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
+ Mime::Type.lookup($1.strip.downcase)
+ else
+ nil
+ end
+ end
+ memoize :content_type
+
+ # Returns the accepted MIME type for the request.
+ def accepts
+ header = @env['HTTP_ACCEPT'].to_s.strip
+
+ fallback = xhr? ? Mime::JS : Mime::HTML
+
+ if header.empty?
+ [content_type, fallback, Mime::ALL].compact
+ else
+ ret = Mime::Type.parse(header)
+ if ret.last == Mime::ALL
+ ret.insert(-2, fallback)
+ end
+ ret
+ end
+ end
+ memoize :accepts
+
+ def if_modified_since
+ if since = env['HTTP_IF_MODIFIED_SINCE']
+ Time.rfc2822(since) rescue nil
+ end
+ end
+ memoize :if_modified_since
+
+ def if_none_match
+ env['HTTP_IF_NONE_MATCH']
+ end
+
+ def not_modified?(modified_at)
+ if_modified_since && modified_at && if_modified_since >= modified_at
+ end
+
+ def etag_matches?(etag)
+ if_none_match && if_none_match == etag
+ end
+
+ # Check response freshness (Last-Modified and ETag) against request
+ # If-Modified-Since and If-None-Match conditions. If both headers are
+ # supplied, both must match, or the request is not considered fresh.
+ def fresh?(response)
+ case
+ when if_modified_since && if_none_match
+ not_modified?(response.last_modified) && etag_matches?(response.etag)
+ when if_modified_since
+ not_modified?(response.last_modified)
+ when if_none_match
+ etag_matches?(response.etag)
+ else
+ false
+ end
+ end
+
+ ONLY_ALL = [Mime::ALL].freeze
+
+ # Returns the Mime type for the \format used in the request.
+ #
+ # GET /posts/5.xml | request.format => Mime::XML
+ # GET /posts/5.xhtml | request.format => Mime::HTML
+ # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt>
+
+ def format(view_path = [])
+ @format ||=
+ if parameters[:format]
+ Mime[parameters[:format]]
+ elsif Base.use_accept_header && !(accepts == ONLY_ALL)
+ accepts.first
+ elsif xhr? then Mime::JS
+ else Mime::HTML
+ end
+ end
+
+ def formats
+ @formats =
+ if Base.use_accept_header
+ ret = Array(Mime[parameters[:format]] || accepts)
+ else
+ [format]
+ end
+ end
+
+ # Sets the \format by string extension, which can be used to force custom formats
+ # that are not controlled by the extension.
+ #
+ # class ApplicationController < ActionController::Base
+ # before_filter :adjust_format_for_iphone
+ #
+ # private
+ # def adjust_format_for_iphone
+ # request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
+ # end
+ # end
+ def format=(extension)
+ parameters[:format] = extension.to_s
+ @format = Mime::Type.lookup_by_extension(parameters[:format])
+ end
+
+ # Returns a symbolized version of the <tt>:format</tt> parameter of the request.
+ # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt>
+ # otherwise.
+ def template_format
+ parameter_format = parameters[:format]
+
+ if parameter_format
+ parameter_format
+ elsif xhr?
+ :js
+ else
+ :html
+ end
+ end
+
+ def cache_format
+ parameters[:format]
+ end
+
+ # Returns true if the request's "X-Requested-With" header contains
+ # "XMLHttpRequest". (The Prototype Javascript library sends this header with
+ # every Ajax request.)
+ def xml_http_request?
+ !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i)
+ end
+ alias xhr? :xml_http_request?
+
+ # Which IP addresses are "trusted proxies" that can be stripped from
+ # the right-hand-side of X-Forwarded-For
+ TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i
+
+ # Determines originating IP address. REMOTE_ADDR is the standard
+ # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
+ # HTTP_X_FORWARDED_FOR are set by proxies so check for these if
+ # REMOTE_ADDR is a proxy. HTTP_X_FORWARDED_FOR may be a comma-
+ # delimited list in the case of multiple chained proxies; the last
+ # address which is not trusted is the originating IP.
+ def remote_ip
+ remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].scan(/[^,\s]+/)
+
+ unless remote_addr_list.blank?
+ not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES}
+ return not_trusted_addrs.first unless not_trusted_addrs.empty?
+ end
+ remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')
+
+ if @env.include? 'HTTP_CLIENT_IP'
+ if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])
+ # We don't know which came from the proxy, and which from the user
+ raise ActionControllerError.new(<<EOM)
+IP spoofing attack?!
+HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
+HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
+EOM
+ end
+
+ return @env['HTTP_CLIENT_IP']
+ end
+
+ if remote_ips
+ while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
+ remote_ips.pop
+ end
+
+ return remote_ips.last.strip
+ end
+
+ @env['REMOTE_ADDR']
+ end
+ memoize :remote_ip
+
+ # Returns the lowercase name of the HTTP server software.
+ def server_software
+ (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
+ end
+ memoize :server_software
+
+ # Returns the complete URL used for this request.
+ def url
+ protocol + host_with_port + request_uri
+ end
+ memoize :url
+
+ # Returns 'https://' if this is an SSL request and 'http://' otherwise.
+ def protocol
+ ssl? ? 'https://' : 'http://'
+ end
+ memoize :protocol
+
+ # Is this an SSL request?
+ def ssl?
+ @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
+ end
+
+ # Returns the \host for this request, such as "example.com".
+ def raw_host_with_port
+ if forwarded = env["HTTP_X_FORWARDED_HOST"]
+ forwarded.split(/,\s?/).last
+ else
+ env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
+ end
+ end
+
+ # Returns the host for this request, such as example.com.
+ def host
+ raw_host_with_port.sub(/:\d+$/, '')
+ end
+ memoize :host
+
+ # Returns a \host:\port string for this request, such as "example.com" or
+ # "example.com:8080".
+ def host_with_port
+ "#{host}#{port_string}"
+ end
+ memoize :host_with_port
+
+ # Returns the port number of this request as an integer.
+ def port
+ if raw_host_with_port =~ /:(\d+)$/
+ $1.to_i
+ else
+ standard_port
+ end
+ end
+ memoize :port
+
+ # Returns the standard \port number for this request's protocol.
+ def standard_port
+ case protocol
+ when 'https://' then 443
+ else 80
+ end
+ end
+
+ # Returns a \port suffix like ":8080" if the \port number of this request
+ # is not the default HTTP \port 80 or HTTPS \port 443.
+ def port_string
+ port == standard_port ? '' : ":#{port}"
+ end
+
+ # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
+ # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
+ def domain(tld_length = 1)
+ return nil unless named_host?(host)
+
+ host.split('.').last(1 + tld_length).join('.')
+ end
+
+ # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
+ # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
+ # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
+ # in "www.rubyonrails.co.uk".
+ def subdomains(tld_length = 1)
+ return [] unless named_host?(host)
+ parts = host.split('.')
+ parts[0..-(tld_length+2)]
+ end
+
+ # Returns the query string, accounting for server idiosyncrasies.
+ def query_string
+ @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
+ end
+ memoize :query_string
+
+ # Returns the request URI, accounting for server idiosyncrasies.
+ # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
+ def request_uri
+ if uri = @env['REQUEST_URI']
+ # Remove domain, which webrick puts into the request_uri.
+ (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
+ else
+ # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
+ uri = @env['PATH_INFO'].to_s
+
+ if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
+ uri = uri.sub(/#{script_filename}\//, '')
+ end
+
+ env_qs = @env['QUERY_STRING'].to_s
+ uri += "?#{env_qs}" unless env_qs.empty?
+
+ if uri.blank?
+ @env.delete('REQUEST_URI')
+ else
+ @env['REQUEST_URI'] = uri
+ end
+ end
+ end
+ memoize :request_uri
+
+ # Returns the interpreted \path to requested resource after all the installation
+ # directory of this application was taken into account.
+ def path
+ path = request_uri.to_s[/\A[^\?]*/]
+ path.sub!(/\A#{ActionController::Base.relative_url_root}/, '')
+ path
+ end
+ memoize :path
+
+ # Read the request \body. This is useful for web services that need to
+ # work with raw requests directly.
+ def raw_post
+ unless @env.include? 'RAW_POST_DATA'
+ @env['RAW_POST_DATA'] = body.read(@env['CONTENT_LENGTH'].to_i)
+ body.rewind if body.respond_to?(:rewind)
+ end
+ @env['RAW_POST_DATA']
+ end
+
+ # Returns both GET and POST \parameters in a single hash.
+ def parameters
+ @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
+ end
+ alias_method :params, :parameters
+
+ def path_parameters=(parameters) #:nodoc:
+ @env["rack.routing_args"] = parameters
+ @symbolized_path_parameters = @parameters = nil
+ end
+
+ # The same as <tt>path_parameters</tt> with explicitly symbolized keys.
+ def symbolized_path_parameters
+ @symbolized_path_parameters ||= path_parameters.symbolize_keys
+ end
+
+ # Returns a hash with the \parameters used to form the \path of the request.
+ # Returned hash keys are strings:
+ #
+ # {'action' => 'my_action', 'controller' => 'my_controller'}
+ #
+ # See <tt>symbolized_path_parameters</tt> for symbolized keys.
+ def path_parameters
+ @env["rack.routing_args"] ||= {}
+ end
+
+ # The request body is an IO input stream. If the RAW_POST_DATA environment
+ # variable is already set, wrap it in a StringIO.
+ def body
+ if raw_post = @env['RAW_POST_DATA']
+ raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
+ StringIO.new(raw_post)
+ else
+ @env['rack.input']
+ end
+ end
+
+ def form_data?
+ FORM_DATA_MEDIA_TYPES.include?(content_type.to_s)
+ end
+
+ # Override Rack's GET method to support nested query strings
+ def GET
+ @env["action_controller.request.query_parameters"] ||= UrlEncodedPairParser.parse_query_parameters(query_string)
+ end
+ alias_method :query_parameters, :GET
+
+ # Override Rack's POST method to support nested query strings
+ def POST
+ @env["action_controller.request.request_parameters"] ||= UrlEncodedPairParser.parse_hash_parameters(super)
+ end
+ alias_method :request_parameters, :POST
+
+ def body_stream #:nodoc:
+ @env['rack.input']
+ end
+
+ def session
+ @env['rack.session'] ||= {}
+ end
+
+ def session=(session) #:nodoc:
+ @env['rack.session'] = session
+ end
+
+ def reset_session
+ @env['rack.session'] = {}
+ end
+
+ def session_options
+ @env['rack.session.options'] ||= {}
+ end
+
+ def session_options=(options)
+ @env['rack.session.options'] = options
+ end
+
+ def server_port
+ @env['SERVER_PORT'].to_i
+ end
+
+ private
+ def named_host?(host)
+ !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/dispatch/request_parser.rb b/actionpack/lib/action_controller/dispatch/request_parser.rb
new file mode 100644
index 0000000000..d1739ef4d0
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/request_parser.rb
@@ -0,0 +1,315 @@
+module ActionController
+ class RequestParser
+ def initialize(env)
+ @env = env
+ freeze
+ end
+
+ def request_parameters
+ @env["action_controller.request_parser.request_parameters"] ||= parse_formatted_request_parameters
+ end
+
+ def query_parameters
+ @env["action_controller.request_parser.query_parameters"] ||= self.class.parse_query_parameters(query_string)
+ end
+
+ # Returns the query string, accounting for server idiosyncrasies.
+ def query_string
+ @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
+ end
+
+ # The request body is an IO input stream. If the RAW_POST_DATA environment
+ # variable is already set, wrap it in a StringIO.
+ def body
+ if raw_post = @env['RAW_POST_DATA']
+ raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
+ StringIO.new(raw_post)
+ else
+ @env['rack.input']
+ end
+ end
+
+ # The raw content type string with its parameters stripped off.
+ def content_type_without_parameters
+ self.class.extract_content_type_without_parameters(content_type_with_parameters)
+ end
+
+ def raw_post
+ unless @env.include? 'RAW_POST_DATA'
+ @env['RAW_POST_DATA'] = body.read(content_length)
+ body.rewind if body.respond_to?(:rewind)
+ end
+ @env['RAW_POST_DATA']
+ end
+
+ private
+
+ def parse_formatted_request_parameters
+ return {} if content_length.zero?
+
+ content_type, boundary = self.class.extract_multipart_boundary(content_type_with_parameters)
+
+ # Don't parse params for unknown requests.
+ return {} if content_type.blank?
+
+ mime_type = Mime::Type.lookup(content_type)
+ strategy = ActionController::Base.param_parsers[mime_type]
+
+ # Only multipart form parsing expects a stream.
+ body = (strategy && strategy != :multipart_form) ? raw_post : self.body
+
+ case strategy
+ when Proc
+ strategy.call(body)
+ when :url_encoded_form
+ self.class.clean_up_ajax_request_body! body
+ self.class.parse_query_parameters(body)
+ when :multipart_form
+ self.class.parse_multipart_form_parameters(body, boundary, content_length, @env)
+ when :xml_simple, :xml_node
+ body.blank? ? {} : Hash.from_xml(body).with_indifferent_access
+ when :yaml
+ YAML.load(body)
+ when :json
+ if body.blank?
+ {}
+ else
+ data = ActiveSupport::JSON.decode(body)
+ data = {:_json => data} unless data.is_a?(Hash)
+ data.with_indifferent_access
+ end
+ else
+ {}
+ end
+ rescue Exception => e # YAML, XML or Ruby code block errors
+ raise
+ { "body" => body,
+ "content_type" => content_type_with_parameters,
+ "content_length" => content_length,
+ "exception" => "#{e.message} (#{e.class})",
+ "backtrace" => e.backtrace }
+ end
+
+ def content_length
+ @env['CONTENT_LENGTH'].to_i
+ end
+
+ # The raw content type string. Use when you need parameters such as
+ # charset or boundary which aren't included in the content_type MIME type.
+ # Overridden by the X-POST_DATA_FORMAT header for backward compatibility.
+ def content_type_with_parameters
+ content_type_from_legacy_post_data_format_header || @env['CONTENT_TYPE'].to_s
+ end
+
+ def content_type_from_legacy_post_data_format_header
+ if x_post_format = @env['HTTP_X_POST_DATA_FORMAT']
+ case x_post_format.to_s.downcase
+ when 'yaml'; 'application/x-yaml'
+ when 'xml'; 'application/xml'
+ end
+ end
+ end
+
+ class << self
+ def parse_query_parameters(query_string)
+ return {} if query_string.blank?
+
+ pairs = query_string.split('&').collect do |chunk|
+ next if chunk.empty?
+ key, value = chunk.split('=', 2)
+ next if key.empty?
+ value = value.nil? ? nil : CGI.unescape(value)
+ [ CGI.unescape(key), value ]
+ end.compact
+
+ UrlEncodedPairParser.new(pairs).result
+ end
+
+ def parse_request_parameters(params)
+ parser = UrlEncodedPairParser.new
+
+ params = params.dup
+ until params.empty?
+ for key, value in params
+ if key.blank?
+ params.delete key
+ elsif !key.include?('[')
+ # much faster to test for the most common case first (GET)
+ # and avoid the call to build_deep_hash
+ parser.result[key] = get_typed_value(value[0])
+ params.delete key
+ elsif value.is_a?(Array)
+ parser.parse(key, get_typed_value(value.shift))
+ params.delete key if value.empty?
+ else
+ raise TypeError, "Expected array, found #{value.inspect}"
+ end
+ end
+ end
+
+ parser.result
+ end
+
+ def parse_multipart_form_parameters(body, boundary, body_size, env)
+ parse_request_parameters(read_multipart(body, boundary, body_size, env))
+ end
+
+ def extract_multipart_boundary(content_type_with_parameters)
+ if content_type_with_parameters =~ MULTIPART_BOUNDARY
+ ['multipart/form-data', $1.dup]
+ else
+ extract_content_type_without_parameters(content_type_with_parameters)
+ end
+ end
+
+ def extract_content_type_without_parameters(content_type_with_parameters)
+ $1.strip.downcase if content_type_with_parameters =~ /^([^,\;]*)/
+ end
+
+ def clean_up_ajax_request_body!(body)
+ body.chop! if body[-1] == 0
+ body.gsub!(/&_=$/, '')
+ end
+
+
+ private
+ def get_typed_value(value)
+ case value
+ when String
+ value
+ when NilClass
+ ''
+ when Array
+ value.map { |v| get_typed_value(v) }
+ else
+ if value.respond_to? :original_filename
+ # Uploaded file
+ if value.original_filename
+ value
+ # Multipart param
+ else
+ result = value.read
+ value.rewind
+ result
+ end
+ # Unknown value, neither string nor multipart.
+ else
+ raise "Unknown form value: #{value.inspect}"
+ end
+ end
+ end
+
+ MULTIPART_BOUNDARY = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n
+
+ EOL = "\015\012"
+
+ def read_multipart(body, boundary, body_size, env)
+ params = Hash.new([])
+ boundary = "--" + boundary
+ quoted_boundary = Regexp.quote(boundary)
+ buf = ""
+ bufsize = 10 * 1024
+ boundary_end=""
+
+ # start multipart/form-data
+ body.binmode if defined? body.binmode
+ case body
+ when File
+ body.set_encoding(Encoding::BINARY) if body.respond_to?(:set_encoding)
+ when StringIO
+ body.string.force_encoding(Encoding::BINARY) if body.string.respond_to?(:force_encoding)
+ end
+ boundary_size = boundary.size + EOL.size
+ body_size -= boundary_size
+ status = body.read(boundary_size)
+ if nil == status
+ raise EOFError, "no content body"
+ elsif boundary + EOL != status
+ raise EOFError, "bad content body"
+ end
+
+ loop do
+ head = nil
+ content =
+ if 10240 < body_size
+ UploadedTempfile.new("CGI")
+ else
+ UploadedStringIO.new
+ end
+ content.binmode if defined? content.binmode
+
+ until head and /#{quoted_boundary}(?:#{EOL}|--)/n.match(buf)
+
+ if (not head) and /#{EOL}#{EOL}/n.match(buf)
+ buf = buf.sub(/\A((?:.|\n)*?#{EOL})#{EOL}/n) do
+ head = $1.dup
+ ""
+ end
+ next
+ end
+
+ if head and ( (EOL + boundary + EOL).size < buf.size )
+ content.print buf[0 ... (buf.size - (EOL + boundary + EOL).size)]
+ buf[0 ... (buf.size - (EOL + boundary + EOL).size)] = ""
+ end
+
+ c = if bufsize < body_size
+ body.read(bufsize)
+ else
+ body.read(body_size)
+ end
+ if c.nil? || c.empty?
+ raise EOFError, "bad content body"
+ end
+ buf.concat(c)
+ body_size -= c.size
+ end
+
+ buf = buf.sub(/\A((?:.|\n)*?)(?:[\r\n]{1,2})?#{quoted_boundary}([\r\n]{1,2}|--)/n) do
+ content.print $1
+ if "--" == $2
+ body_size = -1
+ end
+ boundary_end = $2.dup
+ ""
+ end
+
+ content.rewind
+
+ head =~ /Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;]*))/ni
+ if filename = $1 || $2
+ if /Mac/ni.match(env['HTTP_USER_AGENT']) and
+ /Mozilla/ni.match(env['HTTP_USER_AGENT']) and
+ (not /MSIE/ni.match(env['HTTP_USER_AGENT']))
+ filename = CGI.unescape(filename)
+ end
+ content.original_path = filename.dup
+ end
+
+ head =~ /Content-Type: ([^\r]*)/ni
+ content.content_type = $1.dup if $1
+
+ head =~ /Content-Disposition:.* name="?([^\";]*)"?/ni
+ name = $1.dup if $1
+
+ if params.has_key?(name)
+ params[name].push(content)
+ else
+ params[name] = [content]
+ end
+ break if body_size == -1
+ end
+ raise EOFError, "bad boundary end of body part" unless boundary_end=~/--/
+
+ begin
+ body.rewind if body.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
+
+ params
+ end
+ end # class << self
+ end
+end
diff --git a/actionpack/lib/action_controller/dispatch/rescue.rb b/actionpack/lib/action_controller/dispatch/rescue.rb
new file mode 100644
index 0000000000..0293e62fc7
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/rescue.rb
@@ -0,0 +1,179 @@
+module ActionController #:nodoc:
+ # Actions that fail to perform as expected throw exceptions. These
+ # exceptions can either be rescued for the public view (with a nice
+ # user-friendly explanation) or for the developers view (with tons of
+ # debugging information). The developers view is already implemented by
+ # the Action Controller, but the public view should be tailored to your
+ # specific application.
+ #
+ # The default behavior for public exceptions is to render a static html
+ # file with the name of the error code thrown. If no such file exists, an
+ # empty response is sent with the correct status code.
+ #
+ # You can override what constitutes a local request by overriding the
+ # <tt>local_request?</tt> method in your own controller. Custom rescue
+ # behavior is achieved by overriding the <tt>rescue_action_in_public</tt>
+ # and <tt>rescue_action_locally</tt> methods.
+ module Rescue
+ LOCALHOST = '127.0.0.1'.freeze
+
+ DEFAULT_RESCUE_RESPONSE = :internal_server_error
+ DEFAULT_RESCUE_RESPONSES = {
+ 'ActionController::RoutingError' => :not_found,
+ 'ActionController::UnknownAction' => :not_found,
+ 'ActiveRecord::RecordNotFound' => :not_found,
+ 'ActiveRecord::StaleObjectError' => :conflict,
+ 'ActiveRecord::RecordInvalid' => :unprocessable_entity,
+ 'ActiveRecord::RecordNotSaved' => :unprocessable_entity,
+ 'ActionController::MethodNotAllowed' => :method_not_allowed,
+ 'ActionController::NotImplemented' => :not_implemented,
+ 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity
+ }
+
+ DEFAULT_RESCUE_TEMPLATE = 'diagnostics'
+ DEFAULT_RESCUE_TEMPLATES = {
+ 'ActionView::MissingTemplate' => 'missing_template',
+ 'ActionController::RoutingError' => 'routing_error',
+ 'ActionController::UnknownAction' => 'unknown_action',
+ 'ActionView::TemplateError' => 'template_error'
+ }
+
+ RESCUES_TEMPLATE_PATH = ActionView::Template::EagerPath.new(
+ File.join(File.dirname(__FILE__), "templates"))
+
+ def self.included(base) #:nodoc:
+ base.cattr_accessor :rescue_responses
+ base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE)
+ base.rescue_responses.update DEFAULT_RESCUE_RESPONSES
+
+ base.cattr_accessor :rescue_templates
+ base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE)
+ base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES
+
+ base.extend(ClassMethods)
+ base.send :include, ActiveSupport::Rescuable
+
+ base.class_eval do
+ alias_method_chain :perform_action, :rescue
+ end
+ end
+
+ module ClassMethods
+ def call_with_exception(env, exception) #:nodoc:
+ request = env["action_controller.rescue.request"] ||= ActionController::Request.new(env)
+ response = env["action_controller.rescue.response"] ||= ActionController::Response.new
+ new.process(request, response, :rescue_action, exception)
+ end
+ end
+
+ protected
+ # Exception handler called when the performance of an action raises
+ # an exception.
+ def rescue_action(exception)
+ rescue_with_handler(exception) ||
+ rescue_action_without_handler(exception)
+ end
+
+ # Overwrite to implement custom logging of errors. By default
+ # logs as fatal.
+ def log_error(exception) #:doc:
+ ActiveSupport::Deprecation.silence do
+ if ActionView::TemplateError === exception
+ logger.fatal(exception.to_s)
+ else
+ logger.fatal(
+ "\n#{exception.class} (#{exception.message}):\n " +
+ clean_backtrace(exception).join("\n ") + "\n\n"
+ )
+ end
+ end
+ end
+
+ # Overwrite to implement public exception handling (for requests
+ # answering false to <tt>local_request?</tt>). By default will call
+ # render_optional_error_file. Override this method to provide more
+ # user friendly error messages.
+ def rescue_action_in_public(exception) #:doc:
+ render_optional_error_file response_code_for_rescue(exception)
+ end
+
+ # Attempts to render a static error page based on the
+ # <tt>status_code</tt> thrown, or just return headers if no such file
+ # exists. For example, if a 500 error is being handled Rails will first
+ # attempt to render the file at <tt>public/500.html</tt>. If the file
+ # doesn't exist, the body of the response will be left empty.
+ def render_optional_error_file(status_code)
+ status = interpret_status(status_code)
+ path = "#{Rails.public_path}/#{status.to_s[0,3]}.html"
+ if File.exist?(path)
+ render :file => path, :status => status, :content_type => Mime::HTML
+ else
+ head status
+ end
+ end
+
+ # True if the request came from localhost, 127.0.0.1. Override this
+ # method if you wish to redefine the meaning of a local request to
+ # include remote IP addresses or other criteria.
+ def local_request? #:doc:
+ request.remote_addr == LOCALHOST && request.remote_ip == LOCALHOST
+ end
+
+ # Render detailed diagnostics for unhandled exceptions rescued from
+ # a controller action.
+ def rescue_action_locally(exception)
+ @template.instance_variable_set("@exception", exception)
+ @template.instance_variable_set("@rescues_path", RESCUES_TEMPLATE_PATH)
+ @template.instance_variable_set("@contents",
+ @template._render_template(template_path_for_local_rescue(exception)))
+
+ response.content_type = Mime::HTML
+ response.status = interpret_status(response_code_for_rescue(exception))
+
+ content = @template._render_template(rescues_path("layout"))
+ render_for_text(content)
+ end
+
+ def rescue_action_without_handler(exception)
+ log_error(exception) if logger
+ erase_results if performed?
+
+ # Let the exception alter the response if it wants.
+ # For example, MethodNotAllowed sets the Allow header.
+ if exception.respond_to?(:handle_response!)
+ exception.handle_response!(response)
+ end
+
+ if consider_all_requests_local || local_request?
+ rescue_action_locally(exception)
+ else
+ rescue_action_in_public(exception)
+ end
+ end
+
+ private
+ def perform_action_with_rescue #:nodoc:
+ perform_action_without_rescue
+ rescue Exception => exception
+ rescue_action(exception)
+ end
+
+ def rescues_path(template_name)
+ RESCUES_TEMPLATE_PATH.find_template("rescues/#{template_name}.erb")
+ end
+
+ def template_path_for_local_rescue(exception)
+ rescues_path(rescue_templates[exception.class.name])
+ end
+
+ def response_code_for_rescue(exception)
+ rescue_responses[exception.class.name]
+ end
+
+ def clean_backtrace(exception)
+ defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ?
+ Rails.backtrace_cleaner.clean(exception.backtrace) :
+ exception.backtrace
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/dispatch/response.rb b/actionpack/lib/action_controller/dispatch/response.rb
new file mode 100644
index 0000000000..27860a6207
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/response.rb
@@ -0,0 +1,255 @@
+require 'digest/md5'
+
+module ActionController # :nodoc:
+ # Represents an HTTP response generated by a controller action. One can use
+ # an ActionController::Response object to retrieve the current state
+ # of the response, or customize the response. An Response object can
+ # either represent a "real" HTTP response (i.e. one that is meant to be sent
+ # back to the web browser) or a test response (i.e. one that is generated
+ # from integration tests). See CgiResponse and TestResponse, respectively.
+ #
+ # Response is mostly a Ruby on Rails framework implement detail, and
+ # should never be used directly in controllers. Controllers should use the
+ # methods defined in ActionController::Base instead. For example, if you want
+ # to set the HTTP response's content MIME type, then use
+ # ActionControllerBase#headers instead of Response#headers.
+ #
+ # Nevertheless, integration tests may want to inspect controller responses in
+ # more detail, and that's when Response can be useful for application
+ # developers. Integration test methods such as
+ # ActionController::Integration::Session#get and
+ # ActionController::Integration::Session#post return objects of type
+ # TestResponse (which are of course also of type Response).
+ #
+ # For example, the following demo integration "test" prints the body of the
+ # controller response to the console:
+ #
+ # class DemoControllerTest < ActionController::IntegrationTest
+ # def test_print_root_path_to_console
+ # get('/')
+ # puts @response.body
+ # end
+ # end
+ class Response < Rack::Response
+ DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
+ attr_accessor :request
+
+ attr_accessor :session, :assigns, :template, :layout
+ attr_accessor :redirected_to, :redirected_to_method_params
+
+ delegate :default_charset, :to => 'ActionController::Base'
+
+ def initialize
+ @status = 200
+ @header = DEFAULT_HEADERS.dup
+
+ @writer = lambda { |x| @body << x }
+ @block = nil
+
+ @body = "",
+ @session, @assigns = [], []
+ end
+
+ def location; headers['Location'] end
+ def location=(url) headers['Location'] = url end
+
+
+ # Sets the HTTP response's content MIME type. For example, in the controller
+ # you could write this:
+ #
+ # response.content_type = "text/plain"
+ #
+ # If a character set has been defined for this response (see charset=) then
+ # the character set information will also be included in the content type
+ # information.
+ def content_type=(mime_type)
+ self.headers["Content-Type"] =
+ if mime_type =~ /charset/ || (c = charset).nil?
+ mime_type.to_s
+ else
+ "#{mime_type}; charset=#{c}"
+ end
+ end
+
+ # Returns the response's content MIME type, or nil if content type has been set.
+ def content_type
+ content_type = String(headers["Content-Type"] || headers["type"]).split(";")[0]
+ content_type.blank? ? nil : content_type
+ end
+
+ # Set the charset of the Content-Type header. Set to nil to remove it.
+ # If no content type is set, it defaults to HTML.
+ def charset=(charset)
+ headers["Content-Type"] =
+ if charset
+ "#{content_type || Mime::HTML}; charset=#{charset}"
+ else
+ content_type || Mime::HTML.to_s
+ end
+ end
+
+ def charset
+ charset = String(headers["Content-Type"] || headers["type"]).split(";")[1]
+ charset.blank? ? nil : charset.strip.split("=")[1]
+ end
+
+ def last_modified
+ if last = headers['Last-Modified']
+ Time.httpdate(last)
+ end
+ end
+
+ def last_modified?
+ headers.include?('Last-Modified')
+ end
+
+ def last_modified=(utc_time)
+ headers['Last-Modified'] = utc_time.httpdate
+ end
+
+ def etag
+ headers['ETag']
+ end
+
+ def etag?
+ headers.include?('ETag')
+ end
+
+ def etag=(etag)
+ if etag.blank?
+ headers.delete('ETag')
+ else
+ headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
+ end
+ end
+
+ def redirect(url, status)
+ self.status = status
+ self.location = url.gsub(/[\r\n]/, '')
+ self.body = "<html><body>You are being <a href=\"#{CGI.escapeHTML(url)}\">redirected</a>.</body></html>"
+ end
+
+ def sending_file?
+ headers["Content-Transfer-Encoding"] == "binary"
+ end
+
+ def assign_default_content_type_and_charset!
+ self.content_type ||= Mime::HTML
+ self.charset ||= default_charset unless sending_file?
+ end
+
+ def prepare!
+ assign_default_content_type_and_charset!
+ handle_conditional_get!
+ set_content_length!
+ convert_content_type!
+ convert_language!
+ convert_expires!
+ convert_cookies!
+ end
+
+ def each(&callback)
+ if @body.respond_to?(:call)
+ @writer = lambda { |x| callback.call(x) }
+ @body.call(self, self)
+ elsif @body.is_a?(String)
+ @body.each_line(&callback)
+ else
+ @body.each(&callback)
+ end
+
+ @writer = callback
+ @block.call(self) if @block
+ end
+
+ def write(str)
+ @writer.call str.to_s
+ str
+ end
+
+ # Over Rack::Response#set_cookie to add HttpOnly option
+ def set_cookie(key, value)
+ case value
+ when Hash
+ domain = "; domain=" + value[:domain] if value[:domain]
+ path = "; path=" + value[:path] if value[:path]
+ # According to RFC 2109, we need dashes here.
+ # N.B.: cgi.rb uses spaces...
+ expires = "; expires=" + value[:expires].clone.gmtime.
+ strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
+ secure = "; secure" if value[:secure]
+ httponly = "; HttpOnly" if value[:http_only]
+ value = value[:value]
+ end
+ value = [value] unless Array === value
+ cookie = ::Rack::Utils.escape(key) + "=" +
+ value.map { |v| ::Rack::Utils.escape v }.join("&") +
+ "#{domain}#{path}#{expires}#{secure}#{httponly}"
+
+ case self["Set-Cookie"]
+ when Array
+ self["Set-Cookie"] << cookie
+ when String
+ self["Set-Cookie"] = [self["Set-Cookie"], cookie]
+ when nil
+ self["Set-Cookie"] = cookie
+ end
+ end
+
+ private
+ def handle_conditional_get!
+ if etag? || last_modified?
+ set_conditional_cache_control!
+ elsif nonempty_ok_response?
+ self.etag = body
+
+ if request && request.etag_matches?(etag)
+ self.status = '304 Not Modified'
+ self.body = ''
+ end
+
+ set_conditional_cache_control!
+ end
+ end
+
+ def nonempty_ok_response?
+ ok = !status || status.to_s[0..2] == '200'
+ ok && body.is_a?(String) && !body.empty?
+ end
+
+ def set_conditional_cache_control!
+ if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
+ headers['Cache-Control'] = 'private, max-age=0, must-revalidate'
+ end
+ end
+
+ def convert_content_type!
+ headers['Content-Type'] ||= "text/html"
+ headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
+ end
+
+ # Don't set the Content-Length for block-based bodies as that would mean
+ # reading it all into memory. Not nice for, say, a 2GB streaming file.
+ def set_content_length!
+ if status && status.to_s[0..2] == '204'
+ headers.delete('Content-Length')
+ elsif length = headers['Content-Length']
+ headers['Content-Length'] = length.to_s
+ elsif !body.respond_to?(:call) && (!status || status.to_s[0..2] != '304')
+ headers["Content-Length"] = body.size.to_s
+ end
+ end
+
+ def convert_language!
+ headers["Content-Language"] = headers.delete("language") if headers["language"]
+ end
+
+ def convert_expires!
+ headers["Expires"] = headers.delete("") if headers["expires"]
+ end
+
+ def convert_cookies!
+ headers['Set-Cookie'] = Array(headers['Set-Cookie']).compact
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/dispatch/rewindable_input.rb b/actionpack/lib/action_controller/dispatch/rewindable_input.rb
new file mode 100644
index 0000000000..36f655c51e
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/rewindable_input.rb
@@ -0,0 +1,28 @@
+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
diff --git a/actionpack/lib/action_controller/dispatch/status_codes.rb b/actionpack/lib/action_controller/dispatch/status_codes.rb
new file mode 100644
index 0000000000..4977c79491
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/status_codes.rb
@@ -0,0 +1,88 @@
+module ActionController
+ module StatusCodes #:nodoc:
+ # Defines the standard HTTP status codes, by integer, with their
+ # corresponding default message texts.
+ # Source: http://www.iana.org/assignments/http-status-codes
+ STATUS_CODES = {
+ 100 => "Continue",
+ 101 => "Switching Protocols",
+ 102 => "Processing",
+
+ 200 => "OK",
+ 201 => "Created",
+ 202 => "Accepted",
+ 203 => "Non-Authoritative Information",
+ 204 => "No Content",
+ 205 => "Reset Content",
+ 206 => "Partial Content",
+ 207 => "Multi-Status",
+ 226 => "IM Used",
+
+ 300 => "Multiple Choices",
+ 301 => "Moved Permanently",
+ 302 => "Found",
+ 303 => "See Other",
+ 304 => "Not Modified",
+ 305 => "Use Proxy",
+ 307 => "Temporary Redirect",
+
+ 400 => "Bad Request",
+ 401 => "Unauthorized",
+ 402 => "Payment Required",
+ 403 => "Forbidden",
+ 404 => "Not Found",
+ 405 => "Method Not Allowed",
+ 406 => "Not Acceptable",
+ 407 => "Proxy Authentication Required",
+ 408 => "Request Timeout",
+ 409 => "Conflict",
+ 410 => "Gone",
+ 411 => "Length Required",
+ 412 => "Precondition Failed",
+ 413 => "Request Entity Too Large",
+ 414 => "Request-URI Too Long",
+ 415 => "Unsupported Media Type",
+ 416 => "Requested Range Not Satisfiable",
+ 417 => "Expectation Failed",
+ 422 => "Unprocessable Entity",
+ 423 => "Locked",
+ 424 => "Failed Dependency",
+ 426 => "Upgrade Required",
+
+ 500 => "Internal Server Error",
+ 501 => "Not Implemented",
+ 502 => "Bad Gateway",
+ 503 => "Service Unavailable",
+ 504 => "Gateway Timeout",
+ 505 => "HTTP Version Not Supported",
+ 507 => "Insufficient Storage",
+ 510 => "Not Extended"
+ }
+
+ # Provides a symbol-to-fixnum lookup for converting a symbol (like
+ # :created or :not_implemented) into its corresponding HTTP status
+ # code (like 200 or 501).
+ SYMBOL_TO_STATUS_CODE = STATUS_CODES.inject({}) do |hash, (code, message)|
+ hash[message.gsub(/ /, "").underscore.to_sym] = code
+ hash
+ end
+
+ # Given a status parameter, determine whether it needs to be converted
+ # to a string. If it is a fixnum, use the STATUS_CODES hash to lookup
+ # the default message. If it is a symbol, use the SYMBOL_TO_STATUS_CODE
+ # hash to convert it.
+ def interpret_status(status)
+ case status
+ when Fixnum then
+ "#{status} #{STATUS_CODES[status]}".strip
+ when Symbol then
+ interpret_status(SYMBOL_TO_STATUS_CODE[status] ||
+ "500 Unknown Status #{status.inspect}")
+ else
+ status.to_s
+ end
+ end
+ private :interpret_status
+
+ end
+end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/_request_and_response.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/_request_and_response.erb
new file mode 100644
index 0000000000..64b34650b1
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/templates/rescues/_request_and_response.erb
@@ -0,0 +1,24 @@
+<% unless @exception.blamed_files.blank? %>
+ <% if (hide = @exception.blamed_files.length > 8) %>
+ <a href="#" onclick="document.getElementById('blame_trace').style.display='block'; return false;">Show blamed files</a>
+ <% end %>
+ <pre id="blame_trace" <%='style="display:none"' if hide %>><code><%=h @exception.describe_blame %></code></pre>
+<% end %>
+
+<%
+ clean_params = request.parameters.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
+%>
+
+<h2 style="margin-top: 30px">Request</h2>
+<p><b>Parameters</b>: <pre><%=h request_dump %></pre></p>
+
+<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p>
+<div id="session_dump" style="display:none"><%= debug(request.session.instance_variable_get("@data")) %></div>
+
+
+<h2 style="margin-top: 30px">Response</h2>
+<p><b>Headers</b>: <pre><%=h response ? response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p>
diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/_trace.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/_trace.erb
new file mode 100644
index 0000000000..bb2d8375bd
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/templates/rescues/_trace.erb
@@ -0,0 +1,26 @@
+<%
+ traces = [
+ ["Application Trace", @exception.application_backtrace],
+ ["Framework Trace", @exception.framework_backtrace],
+ ["Full Trace", @exception.clean_backtrace]
+ ]
+ names = traces.collect {|name, trace| name}
+%>
+
+<p><code>RAILS_ROOT: <%= defined?(RAILS_ROOT) ? RAILS_ROOT : "unset" %></code></p>
+
+<div id="traces">
+ <% names.each do |name| %>
+ <%
+ show = "document.getElementById('#{name.gsub /\s/, '-'}').style.display='block';"
+ hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub /\s/, '-'}').style.display='none';"}
+ %>
+ <a href="#" onclick="<%= hide %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
+ <% end %>
+
+ <% traces.each do |name, trace| %>
+ <div id="<%= name.gsub /\s/, '-' %>" style="display: <%= name == "Application Trace" ? 'block' : 'none' %>;">
+ <pre><code><%= trace.join "\n" %></code></pre>
+ </div>
+ <% end %>
+</div>
diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/diagnostics.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/diagnostics.erb
new file mode 100644
index 0000000000..95be64511d
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/templates/rescues/diagnostics.erb
@@ -0,0 +1,10 @@
+<h1>
+ <%=h @exception.class.to_s %>
+ <% if request.parameters['controller'] %>
+ in <%=h request.parameters['controller'].humanize %>Controller<% if request.parameters['action'] %>#<%=h request.parameters['action'] %><% end %>
+ <% end %>
+</h1>
+<pre><%=h @exception.clean_message %></pre>
+
+<%= @template._render_template(@rescues_path.find_template("rescues/_trace.erb")) %>
+<%= @template._render_template(@rescues_path.find_template("rescues/_request_and_response.erb")) %> \ No newline at end of file
diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/layout.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/layout.erb
new file mode 100644
index 0000000000..4a04742e40
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/templates/rescues/layout.erb
@@ -0,0 +1,29 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Action Controller: Exception caught</title>
+ <style>
+ body { background-color: #fff; color: #333; }
+
+ body, p, ol, ul, td {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+ }
+
+ pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+ }
+
+ a { color: #000; }
+ a:visited { color: #666; }
+ a:hover { color: #fff; background-color:#000; }
+ </style>
+</head>
+<body>
+
+<%= @contents %>
+
+</body>
+</html> \ No newline at end of file
diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/missing_template.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/missing_template.erb
new file mode 100644
index 0000000000..dbfdf76947
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/templates/rescues/missing_template.erb
@@ -0,0 +1,2 @@
+<h1>Template is missing</h1>
+<p><%=h @exception.message %></p>
diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/routing_error.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/routing_error.erb
new file mode 100644
index 0000000000..ccfa858cce
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/templates/rescues/routing_error.erb
@@ -0,0 +1,10 @@
+<h1>Routing Error</h1>
+<p><pre><%=h @exception.message %></pre></p>
+<% unless @exception.failures.empty? %><p>
+ <h2>Failure reasons:</h2>
+ <ol>
+ <% @exception.failures.each do |route, reason| %>
+ <li><code><%=h route.inspect.gsub('\\', '') %></code> failed because <%=h reason.downcase %></li>
+ <% end %>
+ </ol>
+</p><% end %>
diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/template_error.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/template_error.erb
new file mode 100644
index 0000000000..2e34e03bd5
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/templates/rescues/template_error.erb
@@ -0,0 +1,21 @@
+<h1>
+ <%=h @exception.original_exception.class.to_s %> in
+ <%=h request.parameters["controller"].capitalize if request.parameters["controller"]%>#<%=h request.parameters["action"] %>
+</h1>
+
+<p>
+ Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised:
+ <pre><code><%=h @exception.message %></code></pre>
+</p>
+
+<p>Extracted source (around line <b>#<%=h @exception.line_number %></b>):
+<pre><code><%=h @exception.source_extract %></code></pre></p>
+
+<p><%=h @exception.sub_template_message %></p>
+
+<% @real_exception = @exception
+ @exception = @exception.original_exception || @exception %>
+<%= render :file => @rescues_path["rescues/_trace.erb"] %>
+<% @exception = @real_exception %>
+
+<%= render :file => @rescues_path["rescues/_request_and_response.erb"] %>
diff --git a/actionpack/lib/action_controller/dispatch/templates/rescues/unknown_action.erb b/actionpack/lib/action_controller/dispatch/templates/rescues/unknown_action.erb
new file mode 100644
index 0000000000..683379da10
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/templates/rescues/unknown_action.erb
@@ -0,0 +1,2 @@
+<h1>Unknown action</h1>
+<p><%=h @exception.message %></p>
diff --git a/actionpack/lib/action_controller/dispatch/uploaded_file.rb b/actionpack/lib/action_controller/dispatch/uploaded_file.rb
new file mode 100644
index 0000000000..376ba3621a
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/uploaded_file.rb
@@ -0,0 +1,44 @@
+module ActionController
+ module UploadedFile
+ def self.included(base)
+ base.class_eval do
+ attr_accessor :original_path, :content_type
+ alias_method :local_path, :path
+ end
+ end
+
+ def self.extended(object)
+ object.class_eval do
+ attr_accessor :original_path, :content_type
+ alias_method :local_path, :path
+ end
+ end
+
+ # Take the basename of the upload's original filename.
+ # This handles the full Windows paths given by Internet Explorer
+ # (and perhaps other broken user agents) without affecting
+ # those which give the lone filename.
+ # The Windows regexp is adapted from Perl's File::Basename.
+ def original_filename
+ unless defined? @original_filename
+ @original_filename =
+ unless original_path.blank?
+ if original_path =~ /^(?:.*[:\\\/])?(.*)/m
+ $1
+ else
+ File.basename original_path
+ end
+ end
+ end
+ @original_filename
+ end
+ end
+
+ class UploadedStringIO < StringIO
+ include UploadedFile
+ end
+
+ class UploadedTempfile < Tempfile
+ include UploadedFile
+ end
+end
diff --git a/actionpack/lib/action_controller/dispatch/url_encoded_pair_parser.rb b/actionpack/lib/action_controller/dispatch/url_encoded_pair_parser.rb
new file mode 100644
index 0000000000..57594c4259
--- /dev/null
+++ b/actionpack/lib/action_controller/dispatch/url_encoded_pair_parser.rb
@@ -0,0 +1,155 @@
+module ActionController
+ class UrlEncodedPairParser < StringScanner #:nodoc:
+ class << self
+ def parse_query_parameters(query_string)
+ return {} if query_string.blank?
+
+ pairs = query_string.split('&').collect do |chunk|
+ next if chunk.empty?
+ key, value = chunk.split('=', 2)
+ next if key.empty?
+ value = value.nil? ? nil : CGI.unescape(value)
+ [ CGI.unescape(key), value ]
+ end.compact
+
+ new(pairs).result
+ end
+
+ def parse_hash_parameters(params)
+ parser = new
+
+ params = params.dup
+ until params.empty?
+ for key, value in params
+ if key.blank?
+ params.delete(key)
+ elsif value.is_a?(Array)
+ parser.parse(key, get_typed_value(value.shift))
+ params.delete(key) if value.empty?
+ else
+ parser.parse(key, get_typed_value(value))
+ params.delete(key)
+ end
+ end
+ end
+
+ parser.result
+ end
+
+ private
+ def get_typed_value(value)
+ case value
+ when String
+ value
+ when NilClass
+ ''
+ when Array
+ value.map { |v| get_typed_value(v) }
+ when Hash
+ if value.has_key?(:tempfile) && value[:filename].any?
+ upload = value[:tempfile]
+ upload.extend(UploadedFile)
+ upload.original_path = value[:filename]
+ upload.content_type = value[:type]
+ upload
+ else
+ nil
+ end
+ else
+ raise "Unknown form value: #{value.inspect}"
+ end
+ end
+ end
+
+ attr_reader :top, :parent, :result
+
+ def initialize(pairs = [])
+ super('')
+ @result = {}
+ pairs.each { |key, value| parse(key, value) }
+ end
+
+ KEY_REGEXP = %r{([^\[\]=&]+)}
+ BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
+
+ # Parse the query string
+ def parse(key, value)
+ self.string = key
+ @top, @parent = result, nil
+
+ # First scan the bare key
+ key = scan(KEY_REGEXP) or return
+ key = post_key_check(key)
+
+ # Then scan as many nestings as present
+ until eos?
+ r = scan(BRACKETED_KEY_REGEXP) or return
+ key = self[1]
+ key = post_key_check(key)
+ end
+
+ bind(key, value)
+ end
+
+ private
+ # After we see a key, we must look ahead to determine our next action. Cases:
+ #
+ # [] follows the key. Then the value must be an array.
+ # = follows the key. (A value comes next)
+ # & or the end of string follows the key. Then the key is a flag.
+ # otherwise, a hash follows the key.
+ def post_key_check(key)
+ if scan(/\[\]/) # a[b][] indicates that b is an array
+ container(key, Array)
+ nil
+ elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
+ container(key, Hash)
+ nil
+ else # End of key? We do nothing.
+ key
+ end
+ end
+
+ # Add a container to the stack.
+ def container(key, klass)
+ type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
+ value = bind(key, klass.new)
+ type_conflict! klass, value unless value.is_a?(klass)
+ push(value)
+ end
+
+ # Push a value onto the 'stack', which is actually only the top 2 items.
+ def push(value)
+ @parent, @top = @top, value
+ end
+
+ # Bind a key (which may be nil for items in an array) to the provided value.
+ def bind(key, value)
+ if top.is_a? Array
+ if key
+ if top[-1].is_a?(Hash) && ! top[-1].key?(key)
+ top[-1][key] = value
+ else
+ top << {key => value}.with_indifferent_access
+ end
+ push top.last
+ return top[key]
+ else
+ top << value
+ return value
+ end
+ elsif top.is_a? Hash
+ key = CGI.unescape(key)
+ parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
+ top[key] ||= value
+ return top[key]
+ else
+ raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
+ end
+ end
+
+ def type_conflict!(klass, value)
+ raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
+ end
+ end
+end