aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_controller')
-rw-r--r--actionpack/lib/action_controller/assertions/response_assertions.rb56
-rw-r--r--actionpack/lib/action_controller/assertions/routing_assertions.rb2
-rw-r--r--actionpack/lib/action_controller/assertions/selector_assertions.rb3
-rw-r--r--actionpack/lib/action_controller/base.rb111
-rw-r--r--actionpack/lib/action_controller/benchmarking.rb18
-rw-r--r--actionpack/lib/action_controller/caching.rb3
-rw-r--r--actionpack/lib/action_controller/caching/actions.rb2
-rw-r--r--actionpack/lib/action_controller/caching/pages.rb2
-rw-r--r--actionpack/lib/action_controller/caching/sql_cache.rb18
-rw-r--r--actionpack/lib/action_controller/cgi_ext.rb1
-rw-r--r--actionpack/lib/action_controller/cgi_ext/session.rb53
-rw-r--r--actionpack/lib/action_controller/cgi_process.rb2
-rw-r--r--actionpack/lib/action_controller/cookies.rb36
-rw-r--r--actionpack/lib/action_controller/dispatcher.rb55
-rw-r--r--actionpack/lib/action_controller/failsafe.rb6
-rw-r--r--actionpack/lib/action_controller/flash.rb73
-rw-r--r--actionpack/lib/action_controller/helpers.rb6
-rw-r--r--actionpack/lib/action_controller/http_authentication.rb191
-rw-r--r--actionpack/lib/action_controller/integration.rb147
-rw-r--r--actionpack/lib/action_controller/layout.rb22
-rw-r--r--actionpack/lib/action_controller/lock.rb16
-rw-r--r--actionpack/lib/action_controller/middleware_stack.rb74
-rw-r--r--actionpack/lib/action_controller/middlewares.rb21
-rw-r--r--actionpack/lib/action_controller/mime_responds.rb23
-rw-r--r--actionpack/lib/action_controller/mime_type.rb8
-rw-r--r--actionpack/lib/action_controller/polymorphic_routes.rb18
-rw-r--r--actionpack/lib/action_controller/rack_process.rb295
-rwxr-xr-xactionpack/lib/action_controller/request.rb535
-rw-r--r--actionpack/lib/action_controller/request_parser.rb315
-rw-r--r--actionpack/lib/action_controller/request_profiler.rb12
-rw-r--r--actionpack/lib/action_controller/rescue.rb12
-rw-r--r--actionpack/lib/action_controller/response.rb179
-rw-r--r--actionpack/lib/action_controller/routing/recognition_optimisation.rb2
-rw-r--r--actionpack/lib/action_controller/routing/route_set.rb67
-rw-r--r--actionpack/lib/action_controller/session/abstract_store.rb166
-rw-r--r--actionpack/lib/action_controller/session/active_record_store.rb350
-rw-r--r--actionpack/lib/action_controller/session/cookie_store.rb377
-rwxr-xr-xactionpack/lib/action_controller/session/drb_server.rb32
-rw-r--r--actionpack/lib/action_controller/session/drb_store.rb35
-rw-r--r--actionpack/lib/action_controller/session/mem_cache_store.rb119
-rw-r--r--actionpack/lib/action_controller/session_management.rb151
-rw-r--r--actionpack/lib/action_controller/streaming.rb15
-rw-r--r--actionpack/lib/action_controller/test_case.rb27
-rw-r--r--actionpack/lib/action_controller/test_process.rb155
-rw-r--r--actionpack/lib/action_controller/uploaded_file.rb37
-rw-r--r--actionpack/lib/action_controller/url_encoded_pair_parser.rb94
-rw-r--r--actionpack/lib/action_controller/verb_piggybacking.rb24
47 files changed, 1926 insertions, 2040 deletions
diff --git a/actionpack/lib/action_controller/assertions/response_assertions.rb b/actionpack/lib/action_controller/assertions/response_assertions.rb
index 7ab24389b8..5976090273 100644
--- a/actionpack/lib/action_controller/assertions/response_assertions.rb
+++ b/actionpack/lib/action_controller/assertions/response_assertions.rb
@@ -16,7 +16,7 @@ module ActionController
# ==== Examples
#
# # assert that the response was a redirection
- # assert_response :redirect
+ # assert_response :redirect
#
# # assert that the response code was status code 401 (unauthorized)
# assert_response 401
@@ -41,7 +41,7 @@ module ActionController
end
end
- # Assert that the redirection options passed in match those of the redirect called in the latest action.
+ # Assert that the redirection options passed in match those of the redirect called in the latest action.
# This match can be partial, such that assert_redirected_to(:controller => "weblog") will also
# match the redirection of redirect_to(:controller => "weblog", :action => "show") and so on.
#
@@ -60,12 +60,12 @@ module ActionController
clean_backtrace do
assert_response(:redirect, message)
return true if options == @response.redirected_to
-
+
# Support partial arguments for hash redirections
if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
return true if options.all? {|(key, value)| @response.redirected_to[key] == value}
end
-
+
redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to)
options_after_normalisation = normalize_argument_to_redirection(options)
@@ -75,29 +75,59 @@ module ActionController
end
end
- # Asserts that the request was rendered with the appropriate template file.
+ # Asserts that the request was rendered with the appropriate template file or partials
#
# ==== Examples
#
# # assert that the "new" view template was rendered
# assert_template "new"
#
- def assert_template(expected = nil, message=nil)
+ # # assert that the "_customer" partial was rendered twice
+ # assert_template :partial => '_customer', :count => 2
+ #
+ # # assert that no partials were rendered
+ # assert_template :partial => false
+ #
+ def assert_template(options = {}, message = nil)
clean_backtrace do
- rendered = @response.rendered_template.to_s
- msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered)
- assert_block(msg) do
- if expected.nil?
- @response.rendered_template.blank?
+ case options
+ when NilClass, String
+ rendered = @response.rendered[:template].to_s
+ msg = build_message(message,
+ "expecting <?> but rendering with <?>",
+ options, rendered)
+ assert_block(msg) do
+ if options.nil?
+ @response.rendered[:template].blank?
+ else
+ rendered.to_s.match(options)
+ end
+ end
+ when Hash
+ if expected_partial = options[:partial]
+ partials = @response.rendered[:partials]
+ if expected_count = options[:count]
+ found = partials.detect { |p, _| p.to_s.match(expected_partial) }
+ actual_count = found.nil? ? 0 : found.second
+ msg = build_message(message,
+ "expecting ? to be rendered ? time(s) but rendered ? time(s)",
+ expected_partial, expected_count, actual_count)
+ assert(actual_count == expected_count.to_i, msg)
+ else
+ msg = build_message(message,
+ "expecting partial <?> but action rendered <?>",
+ options[:partial], partials.keys)
+ assert(partials.keys.any? { |p| p.to_s.match(expected_partial) }, msg)
+ end
else
- rendered.to_s.match(expected)
+ assert @response.rendered[:partials].empty?,
+ "Expected no partials to be rendered"
end
end
end
end
private
-
# Proxy to to_param if the object will respond to it.
def parameterize(value)
value.respond_to?(:to_param) ? value.to_param : value
diff --git a/actionpack/lib/action_controller/assertions/routing_assertions.rb b/actionpack/lib/action_controller/assertions/routing_assertions.rb
index 8a837c592c..5101751cea 100644
--- a/actionpack/lib/action_controller/assertions/routing_assertions.rb
+++ b/actionpack/lib/action_controller/assertions/routing_assertions.rb
@@ -134,7 +134,7 @@ module ActionController
path = "/#{path}" unless path.first == '/'
# Assume given controller
- request = ActionController::TestRequest.new({}, {}, nil)
+ request = ActionController::TestRequest.new
request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method
request.path = path
diff --git a/actionpack/lib/action_controller/assertions/selector_assertions.rb b/actionpack/lib/action_controller/assertions/selector_assertions.rb
index e03fed7abb..7f8fe9ab19 100644
--- a/actionpack/lib/action_controller/assertions/selector_assertions.rb
+++ b/actionpack/lib/action_controller/assertions/selector_assertions.rb
@@ -402,6 +402,7 @@ module ActionController
if rjs_type
if rjs_type == :insert
position = args.shift
+ id = args.shift
insertion = "insert_#{position}".to_sym
raise ArgumentError, "Unknown RJS insertion type #{position}" unless RJS_STATEMENTS[insertion]
statement = "(#{RJS_STATEMENTS[insertion]})"
@@ -587,7 +588,7 @@ module ActionController
def response_from_page_or_rjs()
content_type = @response.content_type
- if content_type && content_type =~ /text\/javascript/
+ if content_type && Mime::JS =~ content_type
body = @response.body.dup
root = HTML::Node.new(nil)
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index c2f0c1c4f6..e22114195c 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -164,8 +164,8 @@ module ActionController #:nodoc:
#
# Other options for session storage are:
#
- # * ActiveRecordStore - Sessions are stored in your database, which works better than PStore with multiple app servers and,
- # unlike CookieStore, hides your session contents from the user. To use ActiveRecordStore, set
+ # * ActiveRecord::SessionStore - Sessions are stored in your database, which works better than PStore with multiple app servers and,
+ # unlike CookieStore, hides your session contents from the user. To use ActiveRecord::SessionStore, set
#
# config.action_controller.session_store = :active_record_store
#
@@ -254,7 +254,7 @@ module ActionController #:nodoc:
cattr_reader :protected_instance_variables
# Controller specific instance variables which will not be accessible inside views.
@@protected_instance_variables = %w(@assigns @performed_redirect @performed_render @variables_added @request_origin @url @parent_controller
- @action_name @before_filter_chain_aborted @action_cache_path @_session @_cookies @_headers @_params
+ @action_name @before_filter_chain_aborted @action_cache_path @_session @_headers @_params
@_flash @_response)
# Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets,
@@ -382,6 +382,13 @@ module ActionController #:nodoc:
attr_accessor :action_name
class << self
+ def call(env)
+ # HACK: For global rescue to have access to the original request and response
+ request = env["action_controller.rescue.request"] ||= Request.new(env)
+ response = env["action_controller.rescue.response"] ||= Response.new
+ process(request, response)
+ end
+
# Factory for the standard create, process loop where the controller is discarded after processing.
def process(request, response) #:nodoc:
new.process(request, response)
@@ -502,7 +509,7 @@ module ActionController #:nodoc:
protected :filter_parameters
end
- delegate :exempt_from_layout, :to => 'ActionView::Base'
+ delegate :exempt_from_layout, :to => 'ActionView::Template'
end
public
@@ -859,16 +866,23 @@ module ActionController #:nodoc:
def render(options = nil, extra_options = {}, &block) #:doc:
raise DoubleRenderError, "Can only render or redirect once per action" if performed?
+ validate_render_arguments(options, extra_options, block_given?)
+
if options.nil?
- return render(:file => default_template_name, :layout => true)
- elsif !extra_options.is_a?(Hash)
- raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}"
- else
- if options == :update
- options = extra_options.merge({ :update => true })
- elsif !options.is_a?(Hash)
- raise RenderError, "You called render with invalid options : #{options.inspect}"
+ options = { :template => default_template, :layout => true }
+ elsif options == :update
+ options = extra_options.merge({ :update => true })
+ elsif options.is_a?(String) || options.is_a?(Symbol)
+ case options.to_s.index('/')
+ when 0
+ extra_options[:file] = options
+ when nil
+ extra_options[:action] = options
+ else
+ extra_options[:template] = options
end
+
+ options = extra_options
end
layout = pick_layout(options)
@@ -898,7 +912,7 @@ module ActionController #:nodoc:
render_for_text(@template.render(options.merge(:layout => layout)), options[:status])
elsif action_name = options[:action]
- render_for_file(default_template_name(action_name.to_s), options[:status], layout)
+ render_for_file(default_template(action_name.to_s), options[:status], layout)
elsif xml = options[:xml]
response.content_type ||= Mime::XML
@@ -933,7 +947,7 @@ module ActionController #:nodoc:
render_for_text(nil, options[:status])
else
- render_for_file(default_template_name, options[:status], layout)
+ render_for_file(default_template, options[:status], layout)
end
end
end
@@ -990,7 +1004,7 @@ module ActionController #:nodoc:
@performed_redirect = false
response.redirected_to = nil
response.redirected_to_method_params = nil
- response.headers['Status'] = DEFAULT_RENDER_STATUS_CODE
+ response.status = DEFAULT_RENDER_STATUS_CODE
response.headers.delete('Location')
end
@@ -1111,7 +1125,7 @@ module ActionController #:nodoc:
end
# Sets the etag, last_modified, or both on the response and renders a
- # "304 Not Modified" response if the request is already fresh.
+ # "304 Not Modified" response if the request is already fresh.
#
# Example:
#
@@ -1119,8 +1133,8 @@ module ActionController #:nodoc:
# @article = Article.find(params[:id])
# fresh_when(:etag => @article, :last_modified => @article.created_at.utc)
# end
- #
- # This will render the show template if the request isn't sending a matching etag or
+ #
+ # This will render the show template if the request isn't sending a matching etag or
# If-Modified-Since header and just a "304 Not Modified" response if there's a match.
def fresh_when(options)
options.assert_valid_keys(:etag, :last_modified)
@@ -1160,20 +1174,19 @@ module ActionController #:nodoc:
def reset_session #:doc:
request.reset_session
@_session = request.session
- response.session = @_session
end
-
private
def render_for_file(template_path, status = nil, layout = nil, locals = {}) #:nodoc:
- logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger
+ path = template_path.respond_to?(:path_without_format_and_extension) ? template_path.path_without_format_and_extension : template_path
+ logger.info("Rendering #{path}" + (status ? " (#{status})" : '')) if logger
render_for_text @template.render(:file => template_path, :locals => locals, :layout => layout), status
end
def render_for_text(text = nil, status = nil, append_response = false) #:nodoc:
@performed_render = true
- response.headers['Status'] = interpret_status(status || DEFAULT_RENDER_STATUS_CODE)
+ response.status = interpret_status(status || DEFAULT_RENDER_STATUS_CODE)
if append_response
response.body ||= ''
@@ -1187,6 +1200,16 @@ module ActionController #:nodoc:
end
end
+ def validate_render_arguments(options, extra_options, has_block)
+ if options && (has_block && options != :update) && !options.is_a?(String) && !options.is_a?(Hash) && !options.is_a?(Symbol)
+ raise RenderError, "You called render with invalid options : #{options.inspect}"
+ end
+
+ if !extra_options.is_a?(Hash)
+ raise RenderError, "You called render with invalid options : #{options.inspect}, #{extra_options.inspect}"
+ end
+ end
+
def initialize_template_class(response)
response.template = ActionView::Base.new(self.class.view_paths, {}, self)
response.template.helpers.send :include, self.class.master_helper_module
@@ -1195,7 +1218,7 @@ module ActionController #:nodoc:
end
def assign_shortcuts(request, response)
- @_request, @_params, @_cookies = request, request.parameters, request.cookies
+ @_request, @_params = request, request.parameters
@_response = response
@_response.session = request.session
@@ -1213,11 +1236,10 @@ module ActionController #:nodoc:
def log_processing
if logger && logger.info?
log_processing_for_request_id
- log_processing_for_session_id
log_processing_for_parameters
end
end
-
+
def log_processing_for_request_id
request_id = "\n\nProcessing #{self.class.name}\##{action_name} "
request_id << "to #{params[:format]} " if params[:format]
@@ -1226,17 +1248,10 @@ module ActionController #:nodoc:
logger.info(request_id)
end
- def log_processing_for_session_id
- if @_session && @_session.respond_to?(:session_id) && @_session.respond_to?(:dbman) &&
- !@_session.dbman.is_a?(CGI::Session::CookieStore)
- logger.info " Session ID: #{@_session.session_id}"
- end
- end
-
def log_processing_for_parameters
parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup
parameters = parameters.except!(:controller, :action, :format, :_method)
-
+
logger.info " Parameters: #{parameters.inspect}" unless parameters.empty?
end
@@ -1251,10 +1266,17 @@ module ActionController #:nodoc:
elsif respond_to? :method_missing
method_missing action_name
default_render unless performed?
- elsif template_exists?
- default_render
else
- raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller
+ begin
+ default_render
+ rescue ActionView::MissingTemplate => e
+ # Was the implicit template missing, or was it another template?
+ if e.path == default_template_name
+ raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller
+ else
+ raise e
+ end
+ end
end
end
@@ -1300,10 +1322,8 @@ module ActionController #:nodoc:
@_session.close if @_session && @_session.respond_to?(:close)
end
- def template_exists?(template_name = default_template_name)
- @template.send(:_pick_template, template_name) ? true : false
- rescue ActionView::MissingTemplate
- false
+ def default_template(action_name = self.action_name)
+ self.view_paths.find_template(default_template_name(action_name), default_template_format)
end
def default_template_name(action_name = self.action_name)
@@ -1330,9 +1350,12 @@ module ActionController #:nodoc:
end
Base.class_eval do
- include Flash, Filters, Layout, Benchmarking, Rescue, MimeResponds, Helpers
- include Cookies, Caching, Verification, Streaming
- include SessionManagement, HttpAuthentication::Basic::ControllerMethods
- include RecordIdentifier, RequestForgeryProtection, Translation
+ [ Filters, Layout, Benchmarking, Rescue, Flash, MimeResponds, Helpers,
+ Cookies, Caching, Verification, Streaming, SessionManagement,
+ HttpAuthentication::Basic::ControllerMethods, RecordIdentifier,
+ RequestForgeryProtection, Translation
+ ].each do |mod|
+ include mod
+ end
end
end
diff --git a/actionpack/lib/action_controller/benchmarking.rb b/actionpack/lib/action_controller/benchmarking.rb
index fa572ebf3d..47377e5fa9 100644
--- a/actionpack/lib/action_controller/benchmarking.rb
+++ b/actionpack/lib/action_controller/benchmarking.rb
@@ -23,8 +23,8 @@ module ActionController #:nodoc:
def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
if logger && logger.level == log_level
result = nil
- seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
- logger.add(log_level, "#{title} (#{('%.1f' % (seconds * 1000))}ms)")
+ ms = Benchmark.ms { result = use_silence ? silence { yield } : yield }
+ logger.add(log_level, "#{title} (#{('%.1f' % ms)}ms)")
result
else
yield
@@ -48,7 +48,7 @@ module ActionController #:nodoc:
end
render_output = nil
- @view_runtime = Benchmark::realtime { render_output = render_without_benchmark(options, extra_options, &block) }
+ @view_runtime = Benchmark.ms { render_output = render_without_benchmark(options, extra_options, &block) }
if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
@db_rt_before_render = db_runtime
@@ -65,11 +65,11 @@ module ActionController #:nodoc:
private
def perform_action_with_benchmark
if logger
- seconds = [ Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001 ].max
+ ms = [Benchmark.ms { perform_action_without_benchmark }, 0.01].max
logging_view = defined?(@view_runtime)
logging_active_record = Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
- log_message = "Completed in #{sprintf("%.0f", seconds * 1000)}ms"
+ log_message = 'Completed in %.0fms' % ms
if logging_view || logging_active_record
log_message << " ("
@@ -83,25 +83,25 @@ module ActionController #:nodoc:
end
end
- log_message << " | #{headers["Status"]}"
+ log_message << " | #{response.status}"
log_message << " [#{complete_request_uri rescue "unknown"}]"
logger.info(log_message)
- response.headers["X-Runtime"] = "#{sprintf("%.0f", seconds * 1000)}ms"
+ response.headers["X-Runtime"] = "%.0f" % ms
else
perform_action_without_benchmark
end
end
def view_runtime
- "View: %.0f" % (@view_runtime * 1000)
+ "View: %.0f" % @view_runtime
end
def active_record_runtime
db_runtime = ActiveRecord::Base.connection.reset_runtime
db_runtime += @db_rt_before_render if @db_rt_before_render
db_runtime += @db_rt_after_render if @db_rt_after_render
- "DB: %.0f" % (db_runtime * 1000)
+ "DB: %.0f" % db_runtime
end
end
end
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index b4d251eb3c..1d14df0052 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -27,7 +27,6 @@ module ActionController #:nodoc:
autoload :Actions, 'action_controller/caching/actions'
autoload :Fragments, 'action_controller/caching/fragments'
autoload :Pages, 'action_controller/caching/pages'
- autoload :SqlCache, 'action_controller/caching/sql_cache'
autoload :Sweeping, 'action_controller/caching/sweeping'
def self.included(base) #:nodoc:
@@ -41,7 +40,7 @@ module ActionController #:nodoc:
end
include Pages, Actions, Fragments
- include Sweeping, SqlCache if defined?(ActiveRecord)
+ include Sweeping if defined?(ActiveRecord)
@@perform_caching = true
cattr_accessor :perform_caching
diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb
index 7c803a9830..34e1c3527f 100644
--- a/actionpack/lib/action_controller/caching/actions.rb
+++ b/actionpack/lib/action_controller/caching/actions.rb
@@ -113,7 +113,7 @@ module ActionController #:nodoc:
end
def caching_allowed(controller)
- controller.request.get? && controller.response.headers['Status'].to_i == 200
+ controller.request.get? && controller.response.status.to_i == 200
end
def cache_layout?
diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb
index 22e4fbec43..bd3b5a5875 100644
--- a/actionpack/lib/action_controller/caching/pages.rb
+++ b/actionpack/lib/action_controller/caching/pages.rb
@@ -145,7 +145,7 @@ module ActionController #:nodoc:
private
def caching_allowed
- request.get? && response.headers['Status'].to_i == 200
+ request.get? && response.status.to_i == 200
end
end
end
diff --git a/actionpack/lib/action_controller/caching/sql_cache.rb b/actionpack/lib/action_controller/caching/sql_cache.rb
deleted file mode 100644
index 139be6100d..0000000000
--- a/actionpack/lib/action_controller/caching/sql_cache.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module ActionController #:nodoc:
- module Caching
- module SqlCache
- def self.included(base) #:nodoc:
- if defined?(ActiveRecord) && ActiveRecord::Base.respond_to?(:cache)
- base.alias_method_chain :perform_action, :caching
- end
- end
-
- protected
- def perform_action_with_caching
- ActiveRecord::Base.cache do
- perform_action_without_caching
- end
- end
- end
- end
-end \ No newline at end of file
diff --git a/actionpack/lib/action_controller/cgi_ext.rb b/actionpack/lib/action_controller/cgi_ext.rb
index f3b8c08d8f..406b6f06d6 100644
--- a/actionpack/lib/action_controller/cgi_ext.rb
+++ b/actionpack/lib/action_controller/cgi_ext.rb
@@ -1,7 +1,6 @@
require 'action_controller/cgi_ext/stdinput'
require 'action_controller/cgi_ext/query_extension'
require 'action_controller/cgi_ext/cookie'
-require 'action_controller/cgi_ext/session'
class CGI #:nodoc:
include ActionController::CgiExt::Stdinput
diff --git a/actionpack/lib/action_controller/cgi_ext/session.rb b/actionpack/lib/action_controller/cgi_ext/session.rb
deleted file mode 100644
index d3f85e3705..0000000000
--- a/actionpack/lib/action_controller/cgi_ext/session.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require 'digest/md5'
-require 'cgi/session'
-require 'cgi/session/pstore'
-
-class CGI #:nodoc:
- # * Expose the CGI instance to session stores.
- # * Don't require 'digest/md5' whenever a new session id is generated.
- class Session #:nodoc:
- def self.generate_unique_id(constant = nil)
- ActiveSupport::SecureRandom.hex(16)
- end
-
- # Make the CGI instance available to session stores.
- attr_reader :cgi
- attr_reader :dbman
- alias_method :initialize_without_cgi_reader, :initialize
- def initialize(cgi, options = {})
- @cgi = cgi
- initialize_without_cgi_reader(cgi, options)
- end
-
- private
- # Create a new session id.
- def create_new_id
- @new_session = true
- self.class.generate_unique_id
- end
-
- # * Don't require 'digest/md5' whenever a new session is started.
- class PStore #:nodoc:
- def initialize(session, option={})
- dir = option['tmpdir'] || Dir::tmpdir
- prefix = option['prefix'] || ''
- id = session.session_id
- md5 = Digest::MD5.hexdigest(id)[0,16]
- path = dir+"/"+prefix+md5
- path.untaint
- if File::exist?(path)
- @hash = nil
- else
- unless session.new_session
- raise CGI::Session::NoSession, "uninitialized session"
- end
- @hash = {}
- end
- @p = ::PStore.new(path)
- @p.transaction do |p|
- File.chmod(0600, p.path)
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/cgi_process.rb b/actionpack/lib/action_controller/cgi_process.rb
index 5d6988e1b1..7e5e95e135 100644
--- a/actionpack/lib/action_controller/cgi_process.rb
+++ b/actionpack/lib/action_controller/cgi_process.rb
@@ -61,7 +61,7 @@ module ActionController #:nodoc:
class CgiRequest #:nodoc:
DEFAULT_SESSION_OPTIONS = {
- :database_manager => CGI::Session::CookieStore,
+ :database_manager => nil,
:prefix => "ruby_sess.",
:session_path => "/",
:session_key => "_session_id",
diff --git a/actionpack/lib/action_controller/cookies.rb b/actionpack/lib/action_controller/cookies.rb
index 0428f2a23d..840ceb5abd 100644
--- a/actionpack/lib/action_controller/cookies.rb
+++ b/actionpack/lib/action_controller/cookies.rb
@@ -64,43 +64,31 @@ module ActionController #:nodoc:
# Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
def [](name)
- cookie = @cookies[name.to_s]
- if cookie && cookie.respond_to?(:value)
- cookie.size > 1 ? cookie.value : cookie.value[0]
- end
+ super(name.to_s)
end
# Sets the cookie named +name+. The second argument may be the very cookie
# value, or a hash of options as documented above.
- def []=(name, options)
+ def []=(key, options)
if options.is_a?(Hash)
- options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options }
- options["name"] = name.to_s
+ options.symbolize_keys!
else
- options = { "name" => name.to_s, "value" => options }
+ options = { :value => options }
end
- set_cookie(options)
+ options[:path] = "/" unless options.has_key?(:path)
+ super(key.to_s, options[:value])
+ @controller.response.set_cookie(key, options)
end
# Removes the cookie on the client machine by setting the value to an empty string
# and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
# an options hash to delete cookies with extra data such as a <tt>:path</tt>.
- def delete(name, options = {})
- options.stringify_keys!
- set_cookie(options.merge("name" => name.to_s, "value" => "", "expires" => Time.at(0)))
+ def delete(key, options = {})
+ options.symbolize_keys!
+ options[:path] = "/" unless options.has_key?(:path)
+ super(key.to_s)
+ @controller.response.delete_cookie(key, options)
end
-
- private
- # Builds a CGI::Cookie object and adds the cookie to the response headers.
- #
- # The path of the cookie defaults to "/" if there's none in +options+, and
- # everything is passed to the CGI::Cookie constructor.
- def set_cookie(options) #:doc:
- options["path"] = "/" unless options["path"]
- cookie = CGI::Cookie.new(options)
- @controller.logger.info "Cookie set: #{cookie}" unless @controller.logger.nil?
- @controller.response.headers["cookie"] << cookie
- end
end
end
diff --git a/actionpack/lib/action_controller/dispatcher.rb b/actionpack/lib/action_controller/dispatcher.rb
index 203f6b1683..781bc48887 100644
--- a/actionpack/lib/action_controller/dispatcher.rb
+++ b/actionpack/lib/action_controller/dispatcher.rb
@@ -2,14 +2,14 @@ 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
- @@guard = Mutex.new
-
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)
@@ -45,39 +45,35 @@ module ActionController
end
cattr_accessor :middleware
- self.middleware = MiddlewareStack.new
- self.middleware.use "ActionController::Failsafe"
+ self.middleware = MiddlewareStack.new do |middleware|
+ middlewares = File.join(File.dirname(__FILE__), "middlewares.rb")
+ middleware.instance_eval(File.read(middlewares))
+ end
include ActiveSupport::Callbacks
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch
- # DEPRECATE: Remove arguments
+ # DEPRECATE: Remove arguments, since they are only used by CGI
def initialize(output = $stdout, request = nil, response = nil)
- @output, @request, @response = output, request, response
+ @output = output
@app = @@middleware.build(lambda { |env| self.dup._call(env) })
end
- def dispatch_unlocked
+ def dispatch
begin
run_callbacks :before_dispatch
- handle_request
+ Routing::Routes.call(@env)
rescue Exception => exception
- failsafe_rescue 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
- def dispatch
- if ActionController::Base.allow_concurrency
- dispatch_unlocked
- else
- @@guard.synchronize do
- dispatch_unlocked
- end
- end
- end
-
# DEPRECATE: Remove CGI support
def dispatch_cgi(cgi, session_options)
CGIHandler.dispatch_cgi(self, cgi, @output)
@@ -88,8 +84,7 @@ module ActionController
end
def _call(env)
- @request = RackRequest.new(env)
- @response = RackResponse.new(@request)
+ @env = env
dispatch
end
@@ -98,7 +93,6 @@ module ActionController
run_callbacks :prepare_dispatch
Routing::Routes.reload
- ActionView::Helpers::AssetTagHelper::AssetTag::Cache.clear
end
# Cleanup the application by clearing out loaded classes so they can
@@ -115,23 +109,8 @@ module ActionController
def checkin_connections
# Don't return connection (and peform implicit rollback) if this request is a part of integration test
- # TODO: This callback should have direct access to env
- return if @request.key?("action_controller.test")
+ return if @env.key?("rack.test")
ActiveRecord::Base.clear_active_connections!
end
-
- protected
- def handle_request
- @controller = Routing::Routes.recognize(@request)
- @controller.process(@request, @response).out
- end
-
- def failsafe_rescue(exception)
- if @controller ||= (::ApplicationController rescue Base)
- @controller.process_with_exception(@request, @response, exception).out
- else
- raise exception
- end
- end
end
end
diff --git a/actionpack/lib/action_controller/failsafe.rb b/actionpack/lib/action_controller/failsafe.rb
index bb6ef39470..567581142c 100644
--- a/actionpack/lib/action_controller/failsafe.rb
+++ b/actionpack/lib/action_controller/failsafe.rb
@@ -11,7 +11,7 @@ module ActionController
@app.call(env)
rescue Exception => exception
# Reraise exception in test environment
- if env["action_controller.test"]
+ if env["rack.test"]
raise exception
else
failsafe_response(exception)
@@ -42,8 +42,8 @@ module ActionController
end
def failsafe_logger
- if defined?(::RAILS_DEFAULT_LOGGER) && !::RAILS_DEFAULT_LOGGER.nil?
- ::RAILS_DEFAULT_LOGGER
+ if defined?(Rails) && Rails.logger
+ Rails.logger
else
Logger.new($stderr)
end
diff --git a/actionpack/lib/action_controller/flash.rb b/actionpack/lib/action_controller/flash.rb
index 62fa381a6f..9856dbed2a 100644
--- a/actionpack/lib/action_controller/flash.rb
+++ b/actionpack/lib/action_controller/flash.rb
@@ -27,55 +27,54 @@ module ActionController #:nodoc:
def self.included(base)
base.class_eval do
include InstanceMethods
- alias_method_chain :assign_shortcuts, :flash
- alias_method_chain :reset_session, :flash
+ alias_method_chain :perform_action, :flash
+ alias_method_chain :reset_session, :flash
end
end
-
-
+
class FlashNow #:nodoc:
def initialize(flash)
@flash = flash
end
-
+
def []=(k, v)
@flash[k] = v
@flash.discard(k)
v
end
-
+
def [](k)
@flash[k]
end
end
-
+
class FlashHash < Hash
def initialize #:nodoc:
super
@used = {}
end
-
+
def []=(k, v) #:nodoc:
keep(k)
super
end
-
+
def update(h) #:nodoc:
h.keys.each { |k| keep(k) }
super
end
-
+
alias :merge! :update
-
+
def replace(h) #:nodoc:
@used = {}
super
end
-
+
# Sets a flash that will not be available to the next action, only to the current.
#
# flash.now[:message] = "Hello current action"
- #
+ #
# This method enables you to use the flash as a central messaging system in your app.
# When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
# When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
@@ -85,7 +84,7 @@ module ActionController #:nodoc:
def now
FlashNow.new(self)
end
-
+
# Keeps either the entire current flash or a specific flash entry available for the next action:
#
# flash.keep # keeps the entire flash
@@ -93,7 +92,7 @@ module ActionController #:nodoc:
def keep(k = nil)
use(k, false)
end
-
+
# Marks the entire flash or a single flash entry to be discarded by the end of the current action:
#
# flash.discard # discard the entire flash at the end of the current action
@@ -101,12 +100,12 @@ module ActionController #:nodoc:
def discard(k = nil)
use(k)
end
-
+
# Mark for removal entries that were kept, and delete unkept ones.
#
# This method is called automatically by filters, so you generally don't need to care about it.
def sweep #:nodoc:
- keys.each do |k|
+ keys.each do |k|
unless @used[k]
use(k)
else
@@ -118,7 +117,7 @@ module ActionController #:nodoc:
# clean up after keys that could have been left over by calling reject! or shift on the flash
(@used.keys - keys).each{ |k| @used.delete(k) }
end
-
+
private
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
# use() # marks the entire flash as used
@@ -136,37 +135,27 @@ module ActionController #:nodoc:
module InstanceMethods #:nodoc:
protected
+ def perform_action_with_flash
+ perform_action_without_flash
+ remove_instance_variable(:@_flash) if defined? @_flash
+ end
+
def reset_session_with_flash
reset_session_without_flash
- remove_instance_variable(:@_flash)
- flash(:refresh)
+ remove_instance_variable(:@_flash) if defined? @_flash
end
-
- # Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or
- # <tt>flash["notice"] = "hello"</tt> to put a new one.
- # Note that if sessions are disabled only flash.now will work.
- def flash(refresh = false) #:doc:
- if !defined?(@_flash) || refresh
- @_flash =
- if session.is_a?(Hash)
- # don't put flash in session if disabled
- FlashHash.new
- else
- # otherwise, session is a CGI::Session or a TestSession
- # so make sure it gets retrieved from/saved to session storage after request processing
- session["flash"] ||= FlashHash.new
- end
+
+ # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
+ # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
+ # to put a new one.
+ def flash #:doc:
+ unless defined? @_flash
+ @_flash = session["flash"] ||= FlashHash.new
+ @_flash.sweep
end
@_flash
end
-
- private
- def assign_shortcuts_with_flash(request, response) #:nodoc:
- assign_shortcuts_without_flash(request, response)
- flash(:refresh)
- flash.sweep if @_session
- end
end
end
end
diff --git a/actionpack/lib/action_controller/helpers.rb b/actionpack/lib/action_controller/helpers.rb
index 402750c57d..ba65032f6a 100644
--- a/actionpack/lib/action_controller/helpers.rb
+++ b/actionpack/lib/action_controller/helpers.rb
@@ -163,9 +163,9 @@ module ActionController #:nodoc:
def helper_method(*methods)
methods.flatten.each do |method|
master_helper_module.module_eval <<-end_eval
- def #{method}(*args, &block)
- controller.send(%(#{method}), *args, &block)
- end
+ def #{method}(*args, &block) # def current_user(*args, &block)
+ controller.send(%(#{method}), *args, &block) # controller.send(%(current_user), *args, &block)
+ end # end
end_eval
end
end
diff --git a/actionpack/lib/action_controller/http_authentication.rb b/actionpack/lib/action_controller/http_authentication.rb
index 2ed810db7d..3cb5829eca 100644
--- a/actionpack/lib/action_controller/http_authentication.rb
+++ b/actionpack/lib/action_controller/http_authentication.rb
@@ -55,7 +55,31 @@ module ActionController
# end
# end
#
- #
+ # Simple Digest example. Note the block must return the user's password so the framework
+ # can appropriately hash it to check the user's credentials. Returning nil will cause authentication to fail.
+ #
+ # class PostsController < ApplicationController
+ # Users = {"dhh" => "secret"}
+ #
+ # before_filter :authenticate, :except => [ :index ]
+ #
+ # def index
+ # render :text => "Everyone can see me!"
+ # end
+ #
+ # def edit
+ # render :text => "I'm only accessible if you know the password"
+ # end
+ #
+ # private
+ # def authenticate
+ # authenticate_or_request_with_http_digest(realm) do |user_name|
+ # Users[user_name]
+ # end
+ # end
+ # end
+ #
+ #
# In your integration tests, you can do something like this:
#
# def test_access_granted_from_xml
@@ -108,7 +132,10 @@ module ActionController
end
def decode_credentials(request)
- ActiveSupport::Base64.decode64(authorization(request).split.last || '')
+ # Properly decode credentials spanning a new-line
+ auth = authorization(request)
+ auth.slice!('Basic ')
+ ActiveSupport::Base64.decode64(auth || '')
end
def encode_credentials(user_name, password)
@@ -120,5 +147,165 @@ module ActionController
controller.__send__ :render, :text => "HTTP Basic: Access denied.\n", :status => :unauthorized
end
end
+
+ module Digest
+ extend self
+
+ module ControllerMethods
+ def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure)
+ begin
+ authenticate_with_http_digest!(realm, &password_procedure)
+ rescue ActionController::HttpAuthentication::Error => e
+ msg = e.message
+ msg = "#{msg} expected '#{e.expected}' was '#{e.was}'" unless e.expected.nil?
+ raise msg if e.fatal?
+ request_http_digest_authentication(realm, msg)
+ end
+ end
+
+ # Authenticate using HTTP Digest, throwing ActionController::HttpAuthentication::Error on failure.
+ # This allows more detailed analysis of authentication failures
+ # to be relayed to the client.
+ def authenticate_with_http_digest!(realm = "Application", &login_procedure)
+ HttpAuthentication::Digest.authenticate(self, realm, &login_procedure)
+ end
+
+ # Authenticate with HTTP Digest, returns true or false
+ def authenticate_with_http_digest(realm = "Application", &login_procedure)
+ HttpAuthentication::Digest.authenticate(self, realm, &login_procedure) rescue false
+ end
+
+ # Render output including the HTTP Digest authentication header
+ def request_http_digest_authentication(realm = "Application", message = nil)
+ HttpAuthentication::Digest.authentication_request(self, realm, message)
+ end
+
+ # Add HTTP Digest authentication header to result headers
+ def http_digest_authentication_header(realm = "Application")
+ HttpAuthentication::Digest.authentication_header(self, realm)
+ end
+ end
+
+ # Raises error unless authentictaion succeeds, returns true otherwise
+ def authenticate(controller, realm, &password_procedure)
+ raise Error.new(false), "No authorization header found" unless authorization(controller.request)
+ validate_digest_response(controller, realm, &password_procedure)
+ true
+ end
+
+ def authorization(request)
+ request.env['HTTP_AUTHORIZATION'] ||
+ request.env['X-HTTP_AUTHORIZATION'] ||
+ request.env['X_HTTP_AUTHORIZATION'] ||
+ request.env['REDIRECT_X_HTTP_AUTHORIZATION']
+ end
+
+ # Raises error unless the request credentials response value matches the expected value.
+ def validate_digest_response(controller, realm, &password_procedure)
+ credentials = decode_credentials(controller.request)
+
+ # Check the nonce, opaque and realm.
+ # Ignore nc, as we have no way to validate the number of times this nonce has been used
+ validate_nonce(controller.request, credentials[:nonce])
+ raise Error.new(false, realm, credentials[:realm]), "Realm doesn't match" unless realm == credentials[:realm]
+ raise Error.new(true, opaque(controller.request), credentials[:opaque]),"Opaque doesn't match" unless opaque(controller.request) == credentials[:opaque]
+
+ password = password_procedure.call(credentials[:username])
+ raise Error.new(false), "No password" if password.nil?
+ expected = expected_response(controller.request.env['REQUEST_METHOD'], controller.request.url, credentials, password)
+ raise Error.new(false, expected, credentials[:response]), "Invalid response" unless expected == credentials[:response]
+ end
+
+ # Returns the expected response for a request of +http_method+ to +uri+ with the decoded +credentials+ and the expected +password+
+ def expected_response(http_method, uri, credentials, password)
+ ha1 = ::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(':'))
+ ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase,uri].join(':'))
+ ::Digest::MD5.hexdigest([ha1,credentials[:nonce], credentials[:nc], credentials[:cnonce],credentials[:qop],ha2].join(':'))
+ end
+
+ def encode_credentials(http_method, credentials, password)
+ credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password)
+ "Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ')
+ end
+
+ def decode_credentials(request)
+ authorization(request).to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
+ key, value = pair.split('=', 2)
+ hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
+ hash
+ end
+ end
+
+ def authentication_header(controller, realm)
+ controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce(controller.request)}", opaque="#{opaque(controller.request)}")
+ end
+
+ def authentication_request(controller, realm, message = "HTTP Digest: Access denied")
+ authentication_header(controller, realm)
+ controller.send! :render, :text => message, :status => :unauthorized
+ end
+
+ # Uses an MD5 digest based on time to generate a value to be used only once.
+ #
+ # A server-specified data string which should be uniquely generated each time a 401 response is made.
+ # It is recommended that this string be base64 or hexadecimal data.
+ # Specifically, since the string is passed in the header lines as a quoted string, the double-quote character is not allowed.
+ #
+ # The contents of the nonce are implementation dependent.
+ # The quality of the implementation depends on a good choice.
+ # A nonce might, for example, be constructed as the base 64 encoding of
+ #
+ # => time-stamp H(time-stamp ":" ETag ":" private-key)
+ #
+ # where time-stamp is a server-generated time or other non-repeating value,
+ # ETag is the value of the HTTP ETag header associated with the requested entity,
+ # and private-key is data known only to the server.
+ # With a nonce of this form a server would recalculate the hash portion after receiving the client authentication header and
+ # reject the request if it did not match the nonce from that header or
+ # if the time-stamp value is not recent enough. In this way the server can limit the time of the nonce's validity.
+ # The inclusion of the ETag prevents a replay request for an updated version of the resource.
+ # (Note: including the IP address of the client in the nonce would appear to offer the server the ability
+ # to limit the reuse of the nonce to the same client that originally got it.
+ # However, that would break proxy farms, where requests from a single user often go through different proxies in the farm.
+ # Also, IP address spoofing is not that hard.)
+ #
+ # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to
+ # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for
+ # POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4
+ # of this document.
+ #
+ # The nonce is opaque to the client.
+ def nonce(request, time = Time.now)
+ session_id = request.is_a?(String) ? request : request.session.session_id
+ t = time.to_i
+ hashed = [t, session_id]
+ digest = ::Digest::MD5.hexdigest(hashed.join(":"))
+ Base64.encode64("#{t}:#{digest}").gsub("\n", '')
+ end
+
+ def validate_nonce(request, value)
+ t = Base64.decode64(value).split(":").first.to_i
+ raise Error.new(true), "Stale Nonce" if (t - Time.now.to_i).abs > 10 * 60
+ n = nonce(request, t)
+ raise Error.new(true, value, n), "Bad Nonce" unless n == value
+ end
+
+ # Opaque based on digest of session_id
+ def opaque(request)
+ session_id = request.is_a?(String) ? request : request.session.session_id
+ @opaque ||= Base64.encode64(::Digest::MD5::hexdigest(session_id)).gsub("\n", '')
+ end
+ end
+
+ class Error < RuntimeError
+ attr_accessor :expected, :was
+ def initialize(fatal = false, expected = nil, was = nil)
+ @fatal = fatal
+ @expected = expected
+ @was = was
+ end
+
+ def fatal?; @fatal; end
+ end
end
end
diff --git a/actionpack/lib/action_controller/integration.rb b/actionpack/lib/action_controller/integration.rb
index eeabe5b845..ded72a71fb 100644
--- a/actionpack/lib/action_controller/integration.rb
+++ b/actionpack/lib/action_controller/integration.rb
@@ -2,6 +2,17 @@ require 'stringio'
require 'uri'
require 'active_support/test_case'
+# Monkey patch Rack::Lint to support rewind
+module Rack
+ class Lint
+ class InputWrapper
+ def rewind
+ @input.rewind
+ end
+ end
+ end
+end
+
module ActionController
module Integration #:nodoc:
# An integration Session instance represents a set of requests and responses
@@ -57,12 +68,21 @@ module ActionController
# A running counter of the number of requests processed.
attr_accessor :request_count
+ # Nonce value for Digest Authentication, implicitly set on response with WWW-Authentication
+ attr_accessor :nonce
+
+ # Opaque value for Digest Authentication, implicitly set on response with WWW-Authentication
+ attr_accessor :opaque
+
+ # Opaque value for Authentication, implicitly set on response with WWW-Authentication
+ attr_accessor :realm
+
class MultiPartNeededException < Exception
end
# Create and initialize a new Session instance.
- def initialize(app)
- @application = app
+ def initialize(app = nil)
+ @application = app || ActionController::Dispatcher.new
reset!
end
@@ -126,7 +146,7 @@ module ActionController
# performed on the location header.
def follow_redirect!
raise "not a redirect! #{@status} #{@status_message}" unless redirect?
- get(interpret_uri(headers['location'].first))
+ get(interpret_uri(headers['location']))
status
end
@@ -181,7 +201,7 @@ module ActionController
# - +headers+: Additional HTTP headers to pass, as a Hash. The keys will
# automatically be upcased, with the prefix 'HTTP_' added if needed.
#
- # This method returns an AbstractResponse object, which one can use to
+ # This method returns an Response object, which one can use to
# inspect the details of the response. Furthermore, if this method was
# called from an ActionController::IntegrationTest object, then that
# object's <tt>@response</tt> instance variable will point to the same
@@ -227,13 +247,58 @@ module ActionController
def xml_http_request(request_method, path, parameters = nil, headers = nil)
headers ||= {}
headers['X-Requested-With'] = 'XMLHttpRequest'
- headers['Accept'] ||= 'text/javascript, text/html, application/xml, ' +
- 'text/xml, */*'
-
+ headers['Accept'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
process(request_method, path, parameters, headers)
end
alias xhr :xml_http_request
+ def request_with_noauth(http_method, uri, parameters, headers)
+ process_with_auth http_method, uri, parameters, headers
+ end
+
+ # Performs a request with the given http_method and parameters, including HTTP Basic authorization headers.
+ # See get() for more details on paramters and headers.
+ #
+ # You can perform GET, POST, PUT, DELETE, and HEAD requests with #get_with_basic, #post_with_basic,
+ # #put_with_basic, #delete_with_basic, and #head_with_basic.
+ def request_with_basic(http_method, uri, parameters, headers, user_name, password)
+ process_with_auth http_method, uri, parameters, headers.merge(:authorization => ActionController::HttpAuthentication::Basic.encode_credentials(user_name, password))
+ end
+
+ # Performs a request with the given http_method and parameters, including HTTP Digest authorization headers.
+ # See get() for more details on paramters and headers.
+ #
+ # You can perform GET, POST, PUT, DELETE, and HEAD requests with #get_with_digest, #post_with_digest,
+ # #put_with_digest, #delete_with_digest, and #head_with_digest.
+ def request_with_digest(http_method, uri, parameters, headers, user_name, password)
+ # Realm, Nonce, and Opaque taken from previoius 401 response
+
+ credentials = {
+ :username => user_name,
+ :realm => @realm,
+ :nonce => @nonce,
+ :qop => "auth",
+ :nc => "00000001",
+ :cnonce => "0a4f113b",
+ :opaque => @opaque,
+ :uri => uri
+ }
+
+ raise "Digest request without previous 401 response" if @opaque.nil?
+
+ process_with_auth http_method, uri, parameters, headers.merge(:authorization => ActionController::HttpAuthentication::Digest.encode_credentials(http_method, credentials, password))
+ end
+
+ # def get_with_basic, def post_with_basic, def put_with_basic, def delete_with_basic, def head_with_basic
+ # def get_with_digest, def post_with_digest, def put_with_digest, def delete_with_digest, def head_with_digest
+ [:get, :post, :put, :delete, :head].each do |method|
+ [:noauth, :basic, :digest].each do |auth_type|
+ define_method("#{method}_with_#{auth_type}") do |uri, parameters, headers, *auth|
+ send("request_with_#{auth_type}", method, uri, parameters, headers, *auth)
+ end
+ end
+ end
+
# Returns the URL for the given options, according to the rules specified
# in the application's routes.
def url_for(options)
@@ -278,6 +343,7 @@ module ActionController
"SCRIPT_NAME" => "",
"REQUEST_URI" => path,
+ "PATH_INFO" => path,
"HTTP_HOST" => host,
"REMOTE_ADDR" => remote_addr,
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
@@ -292,7 +358,7 @@ module ActionController
"rack.multiprocess" => true,
"rack.run_once" => false,
- "action_controller.test" => true
+ "rack.test" => true
)
(headers || {}).each do |key, value|
@@ -301,8 +367,10 @@ module ActionController
env[key] = value
end
- unless ActionController::Base.respond_to?(:clear_last_instantiation!)
- ActionController::Base.module_eval { include ControllerCapture }
+ [ControllerCapture, ActionController::ProcessWithTest].each do |mod|
+ unless ActionController::Base < mod
+ ActionController::Base.class_eval { include mod }
+ end
end
ActionController::Base.clear_last_instantiation!
@@ -312,16 +380,6 @@ module ActionController
status, headers, body = app.call(env)
@request_count += 1
- if @controller = ActionController::Base.last_instantiation
- @request = @controller.request
- @response = @controller.response
-
- # Decorate the response with the standard behavior of the
- # TestResponse so that things like assert_response can be
- # used in integration tests.
- @response.extend(TestResponseBehavior)
- end
-
@html_document = nil
@status = status.to_i
@@ -337,6 +395,24 @@ module ActionController
@body = ""
body.each { |part| @body << part }
+ if @controller = ActionController::Base.last_instantiation
+ @request = @controller.request
+ @response = @controller.response
+ @controller.send(:set_test_assigns)
+ else
+ # Decorate responses from Rack Middleware and Rails Metal
+ # as an Response for the purposes of integration testing
+ @response = Response.new
+ @response.status = status.to_s
+ @response.headers.replace(@headers)
+ @response.body = @body
+ end
+
+ # Decorate the response with the standard behavior of the
+ # TestResponse so that things like assert_response can be
+ # used in integration tests.
+ @response.extend(TestResponseBehavior)
+
return @status
rescue MultiPartNeededException
boundary = "----------XnJLe9ZIbbGUYtzPQJ16u1"
@@ -347,6 +423,32 @@ module ActionController
return status
end
+ # Same as process, but handles authentication returns to perform
+ # Basic or Digest authentication
+ def process_with_auth(method, path, parameters = nil, headers = nil)
+ status = process(method, path, parameters, headers)
+
+ if status == 401
+ # Extract authentication information from response
+ auth_data = @response.headers['WWW-Authenticate']
+ if /^Basic /.match(auth_data)
+ # extract realm, to be used in subsequent request
+ @realm = auth_header.split(' ')[1]
+ elsif /^Digest/.match(auth_data)
+ creds = auth_data.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair|
+ key, value = pair.split('=', 2)
+ hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')
+ hash
+ end
+ @realm = creds[:realm]
+ @nonce = creds[:nonce]
+ @opaque = creds[:opaque]
+ end
+ end
+
+ return status
+ end
+
# Encode the cookies hash in a format suitable for passing to a
# request.
def encode_cookies
@@ -365,7 +467,7 @@ module ActionController
"SERVER_PORT" => https? ? "443" : "80",
"HTTPS" => https? ? "on" : "off"
}
- UrlRewriter.new(RackRequest.new(env), {})
+ UrlRewriter.new(Request.new(env), {})
end
def name_with_prefix(prefix, name)
@@ -491,8 +593,7 @@ EOF
# By default, a single session is automatically created for you, but you
# can use this method to open multiple sessions that ought to be tested
# simultaneously.
- def open_session
- application = ActionController::Dispatcher.new
+ def open_session(application = nil)
session = Integration::Session.new(application)
# delegate the fixture accessors back to the test instance
diff --git a/actionpack/lib/action_controller/layout.rb b/actionpack/lib/action_controller/layout.rb
index 54108df06d..159c5c7326 100644
--- a/actionpack/lib/action_controller/layout.rb
+++ b/actionpack/lib/action_controller/layout.rb
@@ -178,9 +178,15 @@ module ActionController #:nodoc:
find_layout(layout, format)
end
+ def layout_list #:nodoc:
+ Array(view_paths).sum([]) { |path| Dir["#{path}/layouts/**/*"] }
+ end
+
def find_layout(layout, *formats) #:nodoc:
return layout if layout.respond_to?(:render)
view_paths.find_template(layout.to_s =~ /layouts\// ? layout : "layouts/#{layout}", *formats)
+ rescue ActionView::MissingTemplate
+ nil
end
private
@@ -188,7 +194,7 @@ module ActionController #:nodoc:
inherited_without_layout(child)
unless child.name.blank?
layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '')
- child.layout(layout_match, {}, true) if child.find_layout(layout_match, :all)
+ child.layout(layout_match, {}, true) unless child.layout_list.grep(%r{layouts/#{layout_match}(\.[a-z][0-9a-z]*)+$}).empty?
end
end
@@ -225,8 +231,16 @@ module ActionController #:nodoc:
private
def candidate_for_layout?(options)
- options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty? &&
- !@template.__send__(:_exempt_from_layout?, options[:template] || default_template_name(options[:action]))
+ template = options[:template] || default_template(options[:action])
+ if options.values_at(:text, :xml, :json, :file, :inline, :partial, :nothing, :update).compact.empty?
+ begin
+ !self.view_paths.find_template(template, default_template_format).exempt_from_layout?
+ rescue ActionView::MissingTemplate
+ true
+ end
+ end
+ rescue ActionView::MissingTemplate
+ false
end
def pick_layout(options)
@@ -235,7 +249,7 @@ module ActionController #:nodoc:
when FalseClass
nil
when NilClass, TrueClass
- active_layout if action_has_layout? && !@template.__send__(:_exempt_from_layout?, default_template_name)
+ active_layout if action_has_layout? && candidate_for_layout?(:template => default_template_name)
else
active_layout(layout)
end
diff --git a/actionpack/lib/action_controller/lock.rb b/actionpack/lib/action_controller/lock.rb
new file mode 100644
index 0000000000..c50762216e
--- /dev/null
+++ b/actionpack/lib/action_controller/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/middleware_stack.rb b/actionpack/lib/action_controller/middleware_stack.rb
index 1864bed23a..2bccba2ba1 100644
--- a/actionpack/lib/action_controller/middleware_stack.rb
+++ b/actionpack/lib/action_controller/middleware_stack.rb
@@ -1,14 +1,47 @@
module ActionController
class MiddlewareStack < Array
class Middleware
- attr_reader :klass, :args, :block
+ 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.is_a?(Class) ? klass : klass.to_s.constantize
+ @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
+ end
+
+ def active?
+ if @conditional.respond_to?(:call)
+ @conditional.call
+ else
+ @conditional
+ end
+ end
+
def ==(middleware)
case middleware
when Middleware
@@ -21,22 +54,49 @@ module ActionController
end
def inspect
- str = @klass.to_s
- @args.each { |arg| str += ", #{arg.inspect}" }
+ str = klass.to_s
+ args.each { |arg| str += ", #{arg.inspect}" }
str
end
def build(app)
- klass.new(app, *args, &block)
+ 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, *objs)
+ index = self.index(index) unless index.is_a?(Integer)
+ objs = objs.map { |obj| Middleware.new(obj) }
+ super(index, *objs)
+ end
+
+ alias_method :insert_before, :insert
+
+ def insert_after(index, *objs)
+ index = self.index(index) unless index.is_a?(Integer)
+ insert(index + 1, *objs)
+ end
+
def use(*args, &block)
- push(Middleware.new(*args, &block))
+ middleware = Middleware.new(*args, &block)
+ push(middleware)
+ end
+
+ def active
+ find_all { |middleware| middleware.active? }
end
def build(app)
- reverse.inject(app) { |a, e| e.build(a) }
+ active.reverse.inject(app) { |a, e| e.build(a) }
end
end
end
diff --git a/actionpack/lib/action_controller/middlewares.rb b/actionpack/lib/action_controller/middlewares.rb
new file mode 100644
index 0000000000..793739723f
--- /dev/null
+++ b/actionpack/lib/action_controller/middlewares.rb
@@ -0,0 +1,21 @@
+use "ActionController::Lock", :if => lambda {
+ !ActionController::Base.allow_concurrency
+}
+
+use "ActionController::Failsafe"
+
+use "ActiveRecord::QueryCache", :if => lambda { defined?(ActiveRecord) }
+
+["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::VerbPiggybacking
diff --git a/actionpack/lib/action_controller/mime_responds.rb b/actionpack/lib/action_controller/mime_responds.rb
index 29294476f7..b755363873 100644
--- a/actionpack/lib/action_controller/mime_responds.rb
+++ b/actionpack/lib/action_controller/mime_responds.rb
@@ -143,12 +143,27 @@ module ActionController #:nodoc:
custom(@mime_type_priority.first, &block)
end
end
+
+ def self.generate_method_for_mime(mime)
+ sym = mime.is_a?(Symbol) ? mime : mime.to_sym
+ const = sym.to_s.upcase
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{sym}(&block) # def html(&block)
+ custom(Mime::#{const}, &block) # custom(Mime::HTML, &block)
+ end # end
+ RUBY
+ end
- def method_missing(symbol, &block)
- mime_constant = symbol.to_s.upcase
+ Mime::SET.each do |mime|
+ generate_method_for_mime(mime)
+ end
- if Mime::SET.include?(Mime.const_get(mime_constant))
- custom(Mime.const_get(mime_constant), &block)
+ def method_missing(symbol, &block)
+ mime_constant = Mime.const_get(symbol.to_s.upcase)
+
+ if Mime::SET.include?(mime_constant)
+ self.class.generate_method_for_mime(mime_constant)
+ send(symbol, &block)
else
super
end
diff --git a/actionpack/lib/action_controller/mime_type.rb b/actionpack/lib/action_controller/mime_type.rb
index 6923a13f3f..017626ba27 100644
--- a/actionpack/lib/action_controller/mime_type.rb
+++ b/actionpack/lib/action_controller/mime_type.rb
@@ -176,6 +176,14 @@ module Mime
end
end
+ def =~(mime_type)
+ return false if mime_type.blank?
+ regexp = Regexp.new(Regexp.quote(mime_type.to_s))
+ (@synonyms + [ self ]).any? do |synonym|
+ synonym.to_s =~ regexp
+ end
+ end
+
# Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See
# ActionController::RequestForgeryProtection.
def verify_request?
diff --git a/actionpack/lib/action_controller/polymorphic_routes.rb b/actionpack/lib/action_controller/polymorphic_routes.rb
index dce50c6c3b..924d1aa6bd 100644
--- a/actionpack/lib/action_controller/polymorphic_routes.rb
+++ b/actionpack/lib/action_controller/polymorphic_routes.rb
@@ -118,13 +118,17 @@ module ActionController
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__
- def #{action}_polymorphic_url(record_or_hash, options = {})
- polymorphic_url(record_or_hash, options.merge(:action => "#{action}"))
- end
-
- def #{action}_polymorphic_path(record_or_hash, options = {})
- polymorphic_url(record_or_hash, options.merge(:action => "#{action}", :routing_type => :path))
- end
+ def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
+ polymorphic_url( # polymorphic_url(
+ record_or_hash, # record_or_hash,
+ options.merge(:action => "#{action}")) # options.merge(:action => "edit"))
+ end # end
+ #
+ def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {})
+ polymorphic_url( # polymorphic_url(
+ record_or_hash, # record_or_hash,
+ options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path))
+ end # end
EOT
end
diff --git a/actionpack/lib/action_controller/rack_process.rb b/actionpack/lib/action_controller/rack_process.rb
deleted file mode 100644
index 568f893c6c..0000000000
--- a/actionpack/lib/action_controller/rack_process.rb
+++ /dev/null
@@ -1,295 +0,0 @@
-require 'action_controller/cgi_ext'
-
-module ActionController #:nodoc:
- class RackRequest < AbstractRequest #:nodoc:
- attr_accessor :session_options
- attr_reader :cgi
-
- class SessionFixationAttempt < StandardError #:nodoc:
- end
-
- DEFAULT_SESSION_OPTIONS = {
- :database_manager => CGI::Session::CookieStore, # store data in cookie
- :prefix => "ruby_sess.", # prefix session file names
- :session_path => "/", # available to all paths in app
- :session_key => "_session_id",
- :cookie_only => true,
- :session_http_only=> true
- }
-
- def initialize(env, session_options = DEFAULT_SESSION_OPTIONS)
- @session_options = session_options
- @env = env
- @cgi = CGIWrapper.new(self)
- super()
- end
-
- %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_INFO
- PATH_TRANSLATED REMOTE_HOST
- REMOTE_IDENT REMOTE_USER SCRIPT_NAME
- 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 query_string
- qs = super
- if !qs.blank?
- qs
- else
- @env['QUERY_STRING']
- end
- end
-
- def body_stream #:nodoc:
- @env['rack.input']
- end
-
- def key?(key)
- @env.key?(key)
- end
-
- def cookies
- Rack::Request.new(@env).cookies
- end
-
- def server_port
- @env['SERVER_PORT'].to_i
- end
-
- def server_software
- @env['SERVER_SOFTWARE'].split("/").first
- end
-
- def session
- unless defined?(@session)
- if @session_options == false
- @session = Hash.new
- else
- stale_session_check! do
- if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
- raise SessionFixationAttempt
- end
- case value = session_options_with_string_keys['new_session']
- when true
- @session = new_session
- when false
- begin
- @session = CGI::Session.new(@cgi, session_options_with_string_keys)
- # CGI::Session raises ArgumentError if 'new_session' == false
- # and no session cookie or query param is present.
- rescue ArgumentError
- @session = Hash.new
- end
- when nil
- @session = CGI::Session.new(@cgi, session_options_with_string_keys)
- else
- raise ArgumentError, "Invalid new_session option: #{value}"
- end
- @session['__valid_session']
- end
- end
- end
- @session
- end
-
- def reset_session
- @session.delete if defined?(@session) && @session.is_a?(CGI::Session)
- @session = new_session
- end
-
- private
- # Delete an old session if it exists then create a new one.
- def new_session
- if @session_options == false
- Hash.new
- else
- CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
- CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
- end
- end
-
- def cookie_only?
- session_options_with_string_keys['cookie_only']
- end
-
- def stale_session_check!
- yield
- rescue ArgumentError => argument_error
- if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
- begin
- # Note that the regexp does not allow $1 to end with a ':'
- $1.constantize
- rescue LoadError, NameError => const_error
- raise ActionController::SessionRestoreError, <<-end_msg
-Session contains objects whose class definition isn\'t available.
-Remember to require the classes for all objects kept in the session.
-(Original exception: #{const_error.message} [#{const_error.class}])
-end_msg
- end
-
- retry
- else
- raise
- end
- end
-
- def session_options_with_string_keys
- @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
- end
- end
-
- class RackResponse < AbstractResponse #:nodoc:
- def initialize(request)
- @cgi = request.cgi
- @writer = lambda { |x| @body << x }
- @block = nil
- super()
- end
-
- # Retrieve status from instance variable if has already been delete
- def status
- @status || super
- end
-
- def out(&block)
- # Nasty hack because CGI sessions are closed after the normal
- # prepare! statement
- set_cookies!
-
- @block = block
- @status = headers.delete("Status")
- if [204, 304].include?(status.to_i)
- headers.delete("Content-Type")
- [status, headers.to_hash, []]
- else
- [status, headers.to_hash, self]
- end
- end
- alias to_a out
-
- 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
-
- def close
- @body.close if @body.respond_to?(:close)
- end
-
- def empty?
- @block == nil && @body.empty?
- end
-
- def prepare!
- super
-
- convert_language!
- convert_expires!
- set_status!
- # set_cookies!
- end
-
- private
- 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_content_type!
- super
- headers['Content-Type'] = headers.delete('type') || "text/html"
- headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
- end
-
- def set_content_length!
- super
- headers["Content-Length"] = headers["Content-Length"].to_s if headers["Content-Length"]
- end
-
- def set_status!
- self.status ||= "200 OK"
- end
-
- def set_cookies!
- # Convert 'cookie' header to 'Set-Cookie' headers.
- # Because Set-Cookie header can appear more the once in the response body,
- # we store it in a line break separated string that will be translated to
- # multiple Set-Cookie header by the handler.
- if cookie = headers.delete('cookie')
- cookies = []
-
- case cookie
- when Array then cookie.each { |c| cookies << c.to_s }
- when Hash then cookie.each { |_, c| cookies << c.to_s }
- else cookies << cookie.to_s
- end
-
- @cgi.output_cookies.each { |c| cookies << c.to_s } if @cgi.output_cookies
-
- headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact
- end
- end
- end
-
- class CGIWrapper < ::CGI
- attr_reader :output_cookies
-
- def initialize(request, *args)
- @request = request
- @args = *args
- @input = request.body
-
- super *args
- end
-
- def params
- @params ||= @request.params
- end
-
- def cookies
- @request.cookies
- end
-
- def query_string
- @request.query_string
- end
-
- # Used to wrap the normal args variable used inside CGI.
- def args
- @args
- end
-
- # Used to wrap the normal env_table variable used inside CGI.
- def env_table
- @request.env
- end
-
- # Used to wrap the normal stdinput variable used inside CGI.
- def stdinput
- @input
- end
- end
-end
diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb
index 087fffe87d..b4ab1ccda1 100755
--- a/actionpack/lib/action_controller/request.rb
+++ b/actionpack/lib/action_controller/request.rb
@@ -3,33 +3,41 @@ require 'stringio'
require 'strscan'
require 'active_support/memoizable'
+require 'action_controller/cgi_ext'
module ActionController
- # CgiRequest and TestRequest provide concrete implementations.
- class AbstractRequest
+ class Request < Rack::Request
extend ActiveSupport::Memoizable
- def self.relative_url_root=(relative_url_root)
- ActiveSupport::Deprecation.warn(
- "ActionController::AbstractRequest.relative_url_root= has been renamed." +
- "You can now set it with config.action_controller.relative_url_root=", caller)
- ActionController::Base.relative_url_root=relative_url_root
+ def initialize(env)
+ super
+ @parser = ActionController::RequestParser.new(env)
+ end
+
+ %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 }
- # The hash of environment variables for this request,
- # such as { 'RAILS_ENV' => 'production' }.
- attr_reader :env
-
# The true HTTP request \method as a lowercase symbol, such as <tt>:get</tt>.
# UnknownHttpMethod is raised for invalid methods not listed in ACCEPTED_HTTP_METHODS.
def request_method
- method = @env['REQUEST_METHOD']
- method = parameters[:_method] if method == 'POST' && !parameters[:_method].blank?
-
- HTTP_METHOD_LOOKUP[method] || raise(UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
+ HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}")
end
memoize :request_method
@@ -76,16 +84,15 @@ module ActionController
# Returns the content length of the request as an integer.
def content_length
- @env['CONTENT_LENGTH'].to_i
+ super.to_i
end
- memoize :content_length
# 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
- Mime::Type.lookup(content_type_without_parameters)
+ Mime::Type.lookup(@parser.content_type_without_parameters)
end
memoize :content_type
@@ -125,15 +132,15 @@ module ActionController
# 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
+ 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
# Returns the Mime type for the \format used in the request.
@@ -248,7 +255,6 @@ EOM
end
memoize :server_software
-
# Returns the complete URL used for this request.
def url
protocol + host_with_port + request_uri
@@ -271,7 +277,7 @@ EOM
if forwarded = env["HTTP_X_FORWARDED_HOST"]
forwarded.split(/,\s?/).last
else
- env['HTTP_HOST'] || env['SERVER_NAME'] || "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
+ env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
end
end
@@ -332,11 +338,7 @@ EOM
# Returns the query string, accounting for server idiosyncrasies.
def query_string
- if uri = @env['REQUEST_URI']
- uri.split('?', 2)[1] || ''
- else
- @env['QUERY_STRING'] || ''
- end
+ @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
end
memoize :query_string
@@ -378,20 +380,17 @@ EOM
# 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(content_length)
- body.rewind if body.respond_to?(:rewind)
- end
- env['RAW_POST_DATA']
+ @parser.raw_post
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:
- @path_parameters = parameters
+ @env["rack.routing_args"] = parameters
@symbolized_path_parameters = @parameters = nil
end
@@ -407,464 +406,56 @@ EOM
#
# See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters
- @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
- body_stream
- end
- end
-
- def remote_addr
- @env['REMOTE_ADDR']
- end
-
- def referrer
- @env['HTTP_REFERER']
+ @parser.body
end
- alias referer referrer
-
- def query_parameters
- @query_parameters ||= self.class.parse_query_parameters(query_string)
+ # Override Rack's GET method to support nested query strings
+ def GET
+ @parser.query_parameters
end
+ alias_method :query_parameters, :GET
- def request_parameters
- @request_parameters ||= parse_formatted_request_parameters
+ # Override Rack's POST method to support nested query strings
+ def POST
+ @parser.request_parameters
end
-
-
- #--
- # Must be implemented in the concrete request
- #++
+ alias_method :request_parameters, :POST
def body_stream #:nodoc:
+ @env['rack.input']
end
- def cookies #:nodoc:
- end
-
- def session #:nodoc:
+ def session
+ @env['rack.session'] ||= {}
end
def session=(session) #:nodoc:
- @session = session
+ @env['rack.session'] = session
end
- def reset_session #:nodoc:
+ def reset_session
+ @env['rack.session'] = {}
end
- protected
- # 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
-
- # 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
- memoize :content_type_without_parameters
-
- private
- 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
-
- 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 named_host?(host)
- !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
- 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
+ def session_options
+ @env['rack.session.options'] ||= {}
end
- end
-
- class UrlEncodedPairParser < StringScanner #:nodoc:
- attr_reader :top, :parent, :result
- def initialize(pairs = [])
- super('')
- @result = {}
- pairs.each { |key, value| parse(key, value) }
+ def session_options=(options)
+ @env['rack.session.options'] = options
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)
+ def server_port
+ @env['SERVER_PORT'].to_i
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
- push top.last
- value = top[key]
- end
- else
- top << 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
-
- return value
- 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
-
- module UploadedFile
- def self.included(base)
- base.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
+ def named_host?(host)
+ !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
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/request_parser.rb b/actionpack/lib/action_controller/request_parser.rb
new file mode 100644
index 0000000000..d1739ef4d0
--- /dev/null
+++ b/actionpack/lib/action_controller/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/request_profiler.rb b/actionpack/lib/action_controller/request_profiler.rb
index 70bb77e7ac..80cd55334f 100644
--- a/actionpack/lib/action_controller/request_profiler.rb
+++ b/actionpack/lib/action_controller/request_profiler.rb
@@ -20,7 +20,7 @@ module ActionController
@quiet = true
print ' '
- result = Benchmark.realtime do
+ ms = Benchmark.ms do
n.times do |i|
run(profiling)
print_progress(i)
@@ -28,7 +28,7 @@ module ActionController
end
puts
- result
+ ms
ensure
@quiet = false
end
@@ -88,7 +88,7 @@ module ActionController
puts 'Warming up once'
elapsed = warmup(sandbox)
- puts '%.2f sec, %d requests, %d req/sec' % [elapsed, sandbox.request_count, sandbox.request_count / elapsed]
+ puts '%.0f ms, %d requests, %d req/sec' % [elapsed, sandbox.request_count, 1000 * sandbox.request_count / elapsed]
puts "\n#{options[:benchmark] ? 'Benchmarking' : 'Profiling'} #{options[:n]}x"
options[:benchmark] ? benchmark(sandbox) : profile(sandbox)
@@ -106,13 +106,13 @@ module ActionController
def benchmark(sandbox, profiling = false)
sandbox.request_count = 0
- elapsed = sandbox.benchmark(options[:n], profiling).to_f
+ elapsed = sandbox.benchmark(options[:n], profiling)
count = sandbox.request_count.to_i
- puts '%.2f sec, %d requests, %d req/sec' % [elapsed, count, count / elapsed]
+ puts '%.0f ms, %d requests, %d req/sec' % [elapsed, count, 1000 * count / elapsed]
end
def warmup(sandbox)
- Benchmark.realtime { sandbox.run(false) }
+ Benchmark.ms { sandbox.run(false) }
end
def default_options
diff --git a/actionpack/lib/action_controller/rescue.rb b/actionpack/lib/action_controller/rescue.rb
index d7b0e96c93..4b7d1e32fd 100644
--- a/actionpack/lib/action_controller/rescue.rb
+++ b/actionpack/lib/action_controller/rescue.rb
@@ -38,8 +38,8 @@ module ActionController #:nodoc:
'ActionView::TemplateError' => 'template_error'
}
- RESCUES_TEMPLATE_PATH = ActionView::PathSet::Path.new(
- "#{File.dirname(__FILE__)}/templates", true)
+ RESCUES_TEMPLATE_PATH = ActionView::Template::EagerPath.new(
+ File.join(File.dirname(__FILE__), "templates"))
def self.included(base) #:nodoc:
base.cattr_accessor :rescue_responses
@@ -59,7 +59,9 @@ module ActionController #:nodoc:
end
module ClassMethods
- def process_with_exception(request, response, exception) #:nodoc:
+ def call_with_exception(env, exception) #:nodoc:
+ request = env["action_controller.rescue.request"] ||= Request.new(env)
+ response = env["action_controller.rescue.response"] ||= Response.new
new.process(request, response, :rescue_action, exception)
end
end
@@ -102,9 +104,9 @@ module ActionController #:nodoc:
# 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[0,3]}.html"
+ path = "#{Rails.public_path}/#{status.to_s[0,3]}.html"
if File.exist?(path)
- render :file => path, :status => status
+ render :file => path, :status => status, :content_type => Mime::HTML
else
head status
end
diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb
index 559c38efd0..27860a6207 100644
--- a/actionpack/lib/action_controller/response.rb
+++ b/actionpack/lib/action_controller/response.rb
@@ -1,24 +1,25 @@
require 'digest/md5'
module ActionController # :nodoc:
- # Represents an HTTP response generated by a controller action. One can use an
- # ActionController::AbstractResponse object to retrieve the current state of the
- # response, or customize the response. An AbstractResponse 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.
+ # 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.
#
- # AbstractResponse 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
- # AbstractResponse#headers.
+ # 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 AbstractResponse 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 AbstractResponse).
+ # 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:
@@ -29,25 +30,25 @@ module ActionController # :nodoc:
# puts @response.body
# end
# end
- class AbstractResponse
+ class Response < Rack::Response
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :request
- # The body content (e.g. HTML) of the response, as a String.
- attr_accessor :body
- # The headers of the response, as a Hash. It maps header names to header values.
- attr_accessor :headers
- attr_accessor :session, :cookies, :assigns, :template, :layout
+ attr_accessor :session, :assigns, :template, :layout
attr_accessor :redirected_to, :redirected_to_method_params
delegate :default_charset, :to => 'ActionController::Base'
def initialize
- @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
- end
+ @status = 200
+ @header = DEFAULT_HEADERS.dup
+
+ @writer = lambda { |x| @body << x }
+ @block = nil
- def status; headers['Status'] end
- def status=(status) headers['Status'] = status end
+ @body = "",
+ @session, @assigns = [], []
+ end
def location; headers['Location'] end
def location=(url) headers['Location'] = url end
@@ -109,13 +110,17 @@ module ActionController # :nodoc:
def etag
headers['ETag']
end
-
+
def etag?
headers.include?('ETag')
end
-
+
def etag=(etag)
- headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(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)
@@ -138,26 +143,77 @@ module ActionController # :nodoc:
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
+ 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[0..2] == '200'
+ ok = !status || status.to_s[0..2] == '200'
ok && body.is_a?(String) && !body.empty?
end
@@ -168,23 +224,32 @@ module ActionController # :nodoc:
end
def convert_content_type!
- if content_type = headers.delete("Content-Type")
- self.headers["type"] = content_type
- end
- if content_type = headers.delete("Content-type")
- self.headers["type"] = content_type
- end
- if content_type = headers.delete("content-type")
- self.headers["type"] = content_type
- end
+ 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.
+
+ # 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!
- unless body.respond_to?(:call) || (status && status[0..2] == '304')
- self.headers["Content-Length"] ||= body.size
+ 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/routing/recognition_optimisation.rb b/actionpack/lib/action_controller/routing/recognition_optimisation.rb
index 3b98b16683..ebc553512f 100644
--- a/actionpack/lib/action_controller/routing/recognition_optimisation.rb
+++ b/actionpack/lib/action_controller/routing/recognition_optimisation.rb
@@ -56,7 +56,7 @@ module ActionController
result = recognize_optimized(path, environment) and return result
# Route was not recognized. Try to find out why (maybe wrong verb).
- allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, :method => verb) } }
+ allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, environment.merge(:method => verb)) } }
if environment[:method] && !HTTP_METHODS.include?(environment[:method])
raise NotImplemented.new(*allows)
diff --git a/actionpack/lib/action_controller/routing/route_set.rb b/actionpack/lib/action_controller/routing/route_set.rb
index 13646aef61..044ace7de1 100644
--- a/actionpack/lib/action_controller/routing/route_set.rb
+++ b/actionpack/lib/action_controller/routing/route_set.rb
@@ -145,10 +145,10 @@ module ActionController
def define_hash_access(route, name, kind, options)
selector = hash_access_name(name, kind)
named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
- def #{selector}(options = nil)
- options ? #{options.inspect}.merge(options) : #{options.inspect}
- end
- protected :#{selector}
+ def #{selector}(options = nil) # def hash_for_users_url(options = nil)
+ options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false}
+ end # end
+ protected :#{selector} # protected :hash_for_users_url
end_eval
helpers << selector
end
@@ -173,32 +173,33 @@ module ActionController
# foo_url(bar, baz, bang, :sort_by => 'baz')
#
named_helper_module_eval <<-end_eval # We use module_eval to avoid leaks
- def #{selector}(*args)
-
- #{generate_optimisation_block(route, kind)}
-
- opts = if args.empty? || Hash === args.first
- args.first || {}
- else
- options = args.extract_options!
- args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)|
- h[k] = v
- h
- end
- options.merge(args)
- end
-
- url_for(#{hash_access_method}(opts))
-
- end
- #Add an alias to support the now deprecated formatted_* URL.
- def formatted_#{selector}(*args)
- ActiveSupport::Deprecation.warn(
- "formatted_#{selector}() has been deprecated. please pass format to the standard" +
- "#{selector}() method instead.", caller)
- #{selector}(*args)
- end
- protected :#{selector}
+ def #{selector}(*args) # def users_url(*args)
+ #
+ #{generate_optimisation_block(route, kind)} # #{generate_optimisation_block(route, kind)}
+ #
+ opts = if args.empty? || Hash === args.first # opts = if args.empty? || Hash === args.first
+ args.first || {} # args.first || {}
+ else # else
+ options = args.extract_options! # options = args.extract_options!
+ args = args.zip(#{route.segment_keys.inspect}).inject({}) do |h, (v, k)| # args = args.zip([]).inject({}) do |h, (v, k)|
+ h[k] = v # h[k] = v
+ h # h
+ end # end
+ options.merge(args) # options.merge(args)
+ end # end
+ #
+ url_for(#{hash_access_method}(opts)) # url_for(hash_for_users_url(opts))
+ #
+ end # end
+ #Add an alias to support the now deprecated formatted_* URL. # #Add an alias to support the now deprecated formatted_* URL.
+ def formatted_#{selector}(*args) # def formatted_users_url(*args)
+ ActiveSupport::Deprecation.warn( # ActiveSupport::Deprecation.warn(
+ "formatted_#{selector}() has been deprecated. " + # "formatted_users_url() has been deprecated. " +
+ "Please pass format to the standard " + # "Please pass format to the standard " +
+ "#{selector} method instead.", caller) # "users_url method instead.", caller)
+ #{selector}(*args) # users_url(*args)
+ end # end
+ protected :#{selector} # protected :users_url
end_eval
helpers << selector
end
@@ -426,6 +427,12 @@ module ActionController
end
end
+ def call(env)
+ request = Request.new(env)
+ app = Routing::Routes.recognize(request)
+ app.call(env).to_a
+ end
+
def recognize(request)
params = recognize_path(request.path, extract_request_environment(request))
request.path_parameters = params.with_indifferent_access
diff --git a/actionpack/lib/action_controller/session/abstract_store.rb b/actionpack/lib/action_controller/session/abstract_store.rb
new file mode 100644
index 0000000000..bf09fd33c5
--- /dev/null
+++ b/actionpack/lib/action_controller/session/abstract_store.rb
@@ -0,0 +1,166 @@
+require 'rack/utils'
+
+module ActionController
+ module Session
+ class AbstractStore
+ ENV_SESSION_KEY = 'rack.session'.freeze
+ ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
+
+ HTTP_COOKIE = 'HTTP_COOKIE'.freeze
+ SET_COOKIE = 'Set-Cookie'.freeze
+
+ class SessionHash < Hash
+ def initialize(by, env)
+ super()
+ @by = by
+ @env = env
+ @loaded = false
+ end
+
+ def id
+ load! unless @loaded
+ @id
+ end
+
+ def session_id
+ ActiveSupport::Deprecation.warn(
+ "ActionController::Session::AbstractStore::SessionHash#session_id" +
+ "has been deprecated.Please use #id instead.", caller)
+ id
+ end
+
+ def [](key)
+ load! unless @loaded
+ super
+ end
+
+ def []=(key, value)
+ load! unless @loaded
+ super
+ end
+
+ def to_hash
+ h = {}.replace(self)
+ h.delete_if { |k,v| v.nil? }
+ h
+ end
+
+ def data
+ ActiveSupport::Deprecation.warn(
+ "ActionController::Session::AbstractStore::SessionHash#data" +
+ "has been deprecated.Please use #to_hash instead.", caller)
+ to_hash
+ end
+
+ private
+ def loaded?
+ @loaded
+ end
+
+ def load!
+ @id, session = @by.send(:load_session, @env)
+ replace(session)
+ @loaded = true
+ end
+ end
+
+ DEFAULT_OPTIONS = {
+ :key => '_session_id',
+ :path => '/',
+ :domain => nil,
+ :expire_after => nil,
+ :secure => false,
+ :httponly => true,
+ :cookie_only => true
+ }
+
+ def initialize(app, options = {})
+ # Process legacy CGI options
+ options = options.symbolize_keys
+ if options.has_key?(:session_path)
+ options[:path] = options.delete(:session_path)
+ end
+ if options.has_key?(:session_key)
+ options[:key] = options.delete(:session_key)
+ end
+ if options.has_key?(:session_http_only)
+ options[:httponly] = options.delete(:session_http_only)
+ end
+
+ @app = app
+ @default_options = DEFAULT_OPTIONS.merge(options)
+ @key = @default_options[:key]
+ @cookie_only = @default_options[:cookie_only]
+ end
+
+ def call(env)
+ session = SessionHash.new(self, env)
+
+ env[ENV_SESSION_KEY] = session
+ env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup
+
+ response = @app.call(env)
+
+ session_data = env[ENV_SESSION_KEY]
+ if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?)
+ options = env[ENV_SESSION_OPTIONS_KEY]
+
+ if session_data.is_a?(AbstractStore::SessionHash)
+ sid = session_data.id
+ else
+ sid = generate_sid
+ end
+
+ unless set_session(env, sid, session_data.to_hash)
+ return response
+ end
+
+ cookie = Rack::Utils.escape(@key) + '=' + Rack::Utils.escape(sid)
+ cookie << "; domain=#{options[:domain]}" if options[:domain]
+ cookie << "; path=#{options[:path]}" if options[:path]
+ if options[:expire_after]
+ expiry = Time.now + options[:expire_after]
+ cookie << "; expires=#{expiry.httpdate}"
+ end
+ cookie << "; Secure" if options[:secure]
+ cookie << "; HttpOnly" if options[:httponly]
+
+ headers = response[1]
+ case a = headers[SET_COOKIE]
+ when Array
+ a << cookie
+ when String
+ headers[SET_COOKIE] = [a, cookie]
+ when nil
+ headers[SET_COOKIE] = cookie
+ end
+ end
+
+ response
+ end
+
+ private
+ def generate_sid
+ ActiveSupport::SecureRandom.hex(16)
+ end
+
+ def load_session(env)
+ request = Rack::Request.new(env)
+ sid = request.cookies[@key]
+ unless @cookie_only
+ sid ||= request.params[@key]
+ end
+ sid, session = get_session(env, sid)
+ [sid, session]
+ end
+
+ def get_session(env, sid)
+ raise '#get_session needs to be implemented.'
+ end
+
+ def set_session(env, sid, session_data)
+ raise '#set_session needs to be implemented.'
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/session/active_record_store.rb b/actionpack/lib/action_controller/session/active_record_store.rb
deleted file mode 100644
index fadf2a6b32..0000000000
--- a/actionpack/lib/action_controller/session/active_record_store.rb
+++ /dev/null
@@ -1,350 +0,0 @@
-require 'cgi'
-require 'cgi/session'
-require 'digest/md5'
-
-class CGI
- class Session
- attr_reader :data
-
- # Return this session's underlying Session instance. Useful for the DB-backed session stores.
- def model
- @dbman.model if @dbman
- end
-
-
- # A session store backed by an Active Record class. A default class is
- # provided, but any object duck-typing to an Active Record Session class
- # with text +session_id+ and +data+ attributes is sufficient.
- #
- # The default assumes a +sessions+ tables with columns:
- # +id+ (numeric primary key),
- # +session_id+ (text, or longtext if your session data exceeds 65K), and
- # +data+ (text or longtext; careful if your session data exceeds 65KB).
- # The +session_id+ column should always be indexed for speedy lookups.
- # Session data is marshaled to the +data+ column in Base64 format.
- # If the data you write is larger than the column's size limit,
- # ActionController::SessionOverflowError will be raised.
- #
- # You may configure the table name, primary key, and data column.
- # For example, at the end of <tt>config/environment.rb</tt>:
- # CGI::Session::ActiveRecordStore::Session.table_name = 'legacy_session_table'
- # CGI::Session::ActiveRecordStore::Session.primary_key = 'session_id'
- # CGI::Session::ActiveRecordStore::Session.data_column_name = 'legacy_session_data'
- # Note that setting the primary key to the +session_id+ frees you from
- # having a separate +id+ column if you don't want it. However, you must
- # set <tt>session.model.id = session.session_id</tt> by hand! A before filter
- # on ApplicationController is a good place.
- #
- # Since the default class is a simple Active Record, you get timestamps
- # for free if you add +created_at+ and +updated_at+ datetime columns to
- # the +sessions+ table, making periodic session expiration a snap.
- #
- # You may provide your own session class implementation, whether a
- # feature-packed Active Record or a bare-metal high-performance SQL
- # store, by setting
- # CGI::Session::ActiveRecordStore.session_class = MySessionClass
- # You must implement these methods:
- # self.find_by_session_id(session_id)
- # initialize(hash_of_session_id_and_data)
- # attr_reader :session_id
- # attr_accessor :data
- # save
- # destroy
- #
- # The example SqlBypass class is a generic SQL session store. You may
- # use it as a basis for high-performance database-specific stores.
- class ActiveRecordStore
- # The default Active Record class.
- class Session < ActiveRecord::Base
- ##
- # :singleton-method:
- # Customizable data column name. Defaults to 'data'.
- cattr_accessor :data_column_name
- self.data_column_name = 'data'
-
- before_save :marshal_data!
- before_save :raise_on_session_data_overflow!
-
- class << self
- # Don't try to reload ARStore::Session in dev mode.
- def reloadable? #:nodoc:
- false
- end
-
- def data_column_size_limit
- @data_column_size_limit ||= columns_hash[@@data_column_name].limit
- end
-
- # Hook to set up sessid compatibility.
- def find_by_session_id(session_id)
- setup_sessid_compatibility!
- find_by_session_id(session_id)
- end
-
- def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
- def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
-
- def create_table!
- connection.execute <<-end_sql
- CREATE TABLE #{table_name} (
- id INTEGER PRIMARY KEY,
- #{connection.quote_column_name('session_id')} TEXT UNIQUE,
- #{connection.quote_column_name(@@data_column_name)} TEXT(255)
- )
- end_sql
- end
-
- def drop_table!
- connection.execute "DROP TABLE #{table_name}"
- end
-
- private
- # Compatibility with tables using sessid instead of session_id.
- def setup_sessid_compatibility!
- # Reset column info since it may be stale.
- reset_column_information
- if columns_hash['sessid']
- def self.find_by_session_id(*args)
- find_by_sessid(*args)
- end
-
- define_method(:session_id) { sessid }
- define_method(:session_id=) { |session_id| self.sessid = session_id }
- else
- def self.find_by_session_id(session_id)
- find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id]
- end
- end
- end
- end
-
- # Lazy-unmarshal session state.
- def data
- @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {}
- end
-
- attr_writer :data
-
- # Has the session been loaded yet?
- def loaded?
- !! @data
- end
-
- private
-
- def marshal_data!
- return false if !loaded?
- write_attribute(@@data_column_name, self.class.marshal(self.data))
- end
-
- # Ensures that the data about to be stored in the database is not
- # larger than the data storage column. Raises
- # ActionController::SessionOverflowError.
- def raise_on_session_data_overflow!
- return false if !loaded?
- limit = self.class.data_column_size_limit
- if loaded? and limit and read_attribute(@@data_column_name).size > limit
- raise ActionController::SessionOverflowError
- end
- end
- end
-
- # A barebones session store which duck-types with the default session
- # store but bypasses Active Record and issues SQL directly. This is
- # an example session model class meant as a basis for your own classes.
- #
- # The database connection, table name, and session id and data columns
- # are configurable class attributes. Marshaling and unmarshaling
- # are implemented as class methods that you may override. By default,
- # marshaling data is
- #
- # ActiveSupport::Base64.encode64(Marshal.dump(data))
- #
- # and unmarshaling data is
- #
- # Marshal.load(ActiveSupport::Base64.decode64(data))
- #
- # This marshaling behavior is intended to store the widest range of
- # binary session data in a +text+ column. For higher performance,
- # store in a +blob+ column instead and forgo the Base64 encoding.
- class SqlBypass
- ##
- # :singleton-method:
- # Use the ActiveRecord::Base.connection by default.
- cattr_accessor :connection
-
- ##
- # :singleton-method:
- # The table name defaults to 'sessions'.
- cattr_accessor :table_name
- @@table_name = 'sessions'
-
- ##
- # :singleton-method:
- # The session id field defaults to 'session_id'.
- cattr_accessor :session_id_column
- @@session_id_column = 'session_id'
-
- ##
- # :singleton-method:
- # The data field defaults to 'data'.
- cattr_accessor :data_column
- @@data_column = 'data'
-
- class << self
-
- def connection
- @@connection ||= ActiveRecord::Base.connection
- end
-
- # Look up a session by id and unmarshal its data if found.
- def find_by_session_id(session_id)
- if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}")
- new(:session_id => session_id, :marshaled_data => record['data'])
- end
- end
-
- def marshal(data) ActiveSupport::Base64.encode64(Marshal.dump(data)) if data end
- def unmarshal(data) Marshal.load(ActiveSupport::Base64.decode64(data)) if data end
-
- def create_table!
- @@connection.execute <<-end_sql
- CREATE TABLE #{table_name} (
- id INTEGER PRIMARY KEY,
- #{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE,
- #{@@connection.quote_column_name(data_column)} TEXT
- )
- end_sql
- end
-
- def drop_table!
- @@connection.execute "DROP TABLE #{table_name}"
- end
- end
-
- attr_reader :session_id
- attr_writer :data
-
- # Look for normal and marshaled data, self.find_by_session_id's way of
- # telling us to postpone unmarshaling until the data is requested.
- # We need to handle a normal data attribute in case of a new record.
- def initialize(attributes)
- @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data]
- @new_record = @marshaled_data.nil?
- end
-
- def new_record?
- @new_record
- end
-
- # Lazy-unmarshal session state.
- def data
- unless @data
- if @marshaled_data
- @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil
- else
- @data = {}
- end
- end
- @data
- end
-
- def loaded?
- !! @data
- end
-
- def save
- return false if !loaded?
- marshaled_data = self.class.marshal(data)
-
- if @new_record
- @new_record = false
- @@connection.update <<-end_sql, 'Create session'
- INSERT INTO #{@@table_name} (
- #{@@connection.quote_column_name(@@session_id_column)},
- #{@@connection.quote_column_name(@@data_column)} )
- VALUES (
- #{@@connection.quote(session_id)},
- #{@@connection.quote(marshaled_data)} )
- end_sql
- else
- @@connection.update <<-end_sql, 'Update session'
- UPDATE #{@@table_name}
- SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)}
- WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
- end_sql
- end
- end
-
- def destroy
- unless @new_record
- @@connection.delete <<-end_sql, 'Destroy session'
- DELETE FROM #{@@table_name}
- WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)}
- end_sql
- end
- end
- end
-
-
- # The class used for session storage. Defaults to
- # CGI::Session::ActiveRecordStore::Session.
- cattr_accessor :session_class
- self.session_class = Session
-
- # Find or instantiate a session given a CGI::Session.
- def initialize(session, option = nil)
- session_id = session.session_id
- unless @session = ActiveRecord::Base.silence { @@session_class.find_by_session_id(session_id) }
- unless session.new_session
- raise CGI::Session::NoSession, 'uninitialized session'
- end
- @session = @@session_class.new(:session_id => session_id, :data => {})
- # session saving can be lazy again, because of improved component implementation
- # therefore next line gets commented out:
- # @session.save
- end
- end
-
- # Access the underlying session model.
- def model
- @session
- end
-
- # Restore session state. The session model handles unmarshaling.
- def restore
- if @session
- @session.data
- end
- end
-
- # Save session store.
- def update
- if @session
- ActiveRecord::Base.silence { @session.save }
- end
- end
-
- # Save and close the session store.
- def close
- if @session
- update
- @session = nil
- end
- end
-
- # Delete and close the session store.
- def delete
- if @session
- ActiveRecord::Base.silence { @session.destroy }
- @session = nil
- end
- end
-
- protected
- def logger
- ActionController::Base.logger rescue nil
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/session/cookie_store.rb b/actionpack/lib/action_controller/session/cookie_store.rb
index ea0ea4f841..135bedaf50 100644
--- a/actionpack/lib/action_controller/session/cookie_store.rb
+++ b/actionpack/lib/action_controller/session/cookie_store.rb
@@ -1,163 +1,224 @@
-require 'cgi'
-require 'cgi/session'
-
-# This cookie-based session store is the Rails default. Sessions typically
-# contain at most a user_id and flash message; both fit within the 4K cookie
-# size limit. Cookie-based sessions are dramatically faster than the
-# alternatives.
-#
-# If you have more than 4K of session data or don't want your data to be
-# visible to the user, pick another session store.
-#
-# CookieOverflow is raised if you attempt to store more than 4K of data.
-# TamperedWithCookie is raised if the data integrity check fails.
-#
-# A message digest is included with the cookie to ensure data integrity:
-# a user cannot alter his +user_id+ without knowing the secret key included in
-# the hash. New apps are generated with a pregenerated secret in
-# config/environment.rb. Set your own for old apps you're upgrading.
-#
-# Session options:
-#
-# * <tt>:secret</tt>: An application-wide key string or block returning a string
-# called per generated digest. The block is called with the CGI::Session
-# instance as an argument. It's important that the secret is not vulnerable to
-# a dictionary attack. Therefore, you should choose a secret consisting of
-# random numbers and letters and more than 30 characters. Examples:
-#
-# :secret => '449fe2e7daee471bffae2fd8dc02313d'
-# :secret => Proc.new { User.current_user.secret_key }
-#
-# * <tt>:digest</tt>: The message digest algorithm used to verify session
-# integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
-# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
-#
-# To generate a secret key for an existing application, run
-# "rake secret" and set the key in config/environment.rb.
-#
-# Note that changing digest or secret invalidates all existing sessions!
-class CGI::Session::CookieStore
- # Cookies can typically store 4096 bytes.
- MAX = 4096
- SECRET_MIN_LENGTH = 30 # characters
-
- # Raised when storing more than 4K of session data.
- class CookieOverflow < StandardError; end
-
- # Raised when the cookie fails its integrity check.
- class TamperedWithCookie < StandardError; end
-
- # Called from CGI::Session only.
- def initialize(session, options = {})
- # The session_key option is required.
- if options['session_key'].blank?
- raise ArgumentError, 'A session_key is required to write a cookie containing the session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb'
- end
-
- # The secret option is required.
- ensure_secret_secure(options['secret'])
-
- # Keep the session and its secret on hand so we can read and write cookies.
- @session, @secret = session, options['secret']
-
- # Message digest defaults to SHA1.
- @digest = options['digest'] || 'SHA1'
-
- # Default cookie options derived from session settings.
- @cookie_options = {
- 'name' => options['session_key'],
- 'path' => options['session_path'],
- 'domain' => options['session_domain'],
- 'expires' => options['session_expires'],
- 'secure' => options['session_secure'],
- 'http_only' => options['session_http_only']
- }
-
- # Set no_hidden and no_cookies since the session id is unused and we
- # set our own data cookie.
- options['no_hidden'] = true
- options['no_cookies'] = true
- end
-
- # To prevent users from using something insecure like "Password" we make sure that the
- # secret they've provided is at least 30 characters in length.
- def ensure_secret_secure(secret)
- # There's no way we can do this check if they've provided a proc for the
- # secret.
- return true if secret.is_a?(Proc)
-
- if secret.blank?
- raise ArgumentError, %Q{A secret is required to generate an integrity hash for cookie session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase of at least #{SECRET_MIN_LENGTH} characters" } in config/environment.rb}
- end
-
- if secret.length < SECRET_MIN_LENGTH
- raise ArgumentError, %Q{Secret should be something secure, like "#{CGI::Session.generate_unique_id}". The value you provided, "#{secret}", is shorter than the minimum length of #{SECRET_MIN_LENGTH} characters}
- end
- end
-
- # Restore session data from the cookie.
- def restore
- @original = read_cookie
- @data = unmarshal(@original) || {}
- end
-
- # Wait until close to write the session data cookie.
- def update; end
-
- # Write the session data cookie if it was loaded and has changed.
- def close
- if defined?(@data) && !@data.blank?
- updated = marshal(@data)
- raise CookieOverflow if updated.size > MAX
- write_cookie('value' => updated) unless updated == @original
- end
- end
-
- # Delete the session data by setting an expired cookie with no data.
- def delete
- @data = nil
- clear_old_cookie_value
- write_cookie('value' => nil, 'expires' => 1.year.ago)
- end
-
- private
- # Marshal a session hash into safe cookie data. Include an integrity hash.
- def marshal(session)
- verifier.generate(session)
- end
-
- # Unmarshal cookie data to a hash and verify its integrity.
- def unmarshal(cookie)
- if cookie
- verifier.verify(cookie)
+module ActionController
+ module Session
+ # This cookie-based session store is the Rails default. Sessions typically
+ # contain at most a user_id and flash message; both fit within the 4K cookie
+ # size limit. Cookie-based sessions are dramatically faster than the
+ # alternatives.
+ #
+ # If you have more than 4K of session data or don't want your data to be
+ # visible to the user, pick another session store.
+ #
+ # CookieOverflow is raised if you attempt to store more than 4K of data.
+ #
+ # A message digest is included with the cookie to ensure data integrity:
+ # a user cannot alter his +user_id+ without knowing the secret key
+ # included in the hash. New apps are generated with a pregenerated secret
+ # in config/environment.rb. Set your own for old apps you're upgrading.
+ #
+ # Session options:
+ #
+ # * <tt>:secret</tt>: An application-wide key string or block returning a
+ # string called per generated digest. The block is called with the
+ # CGI::Session instance as an argument. It's important that the secret
+ # is not vulnerable to a dictionary attack. Therefore, you should choose
+ # a secret consisting of random numbers and letters and more than 30
+ # characters. Examples:
+ #
+ # :secret => '449fe2e7daee471bffae2fd8dc02313d'
+ # :secret => Proc.new { User.current_user.secret_key }
+ #
+ # * <tt>:digest</tt>: The message digest algorithm used to verify session
+ # integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
+ # such as 'MD5', 'RIPEMD160', 'SHA256', etc.
+ #
+ # To generate a secret key for an existing application, run
+ # "rake secret" and set the key in config/environment.rb.
+ #
+ # Note that changing digest or secret invalidates all existing sessions!
+ class CookieStore
+ # Cookies can typically store 4096 bytes.
+ MAX = 4096
+ SECRET_MIN_LENGTH = 30 # characters
+
+ DEFAULT_OPTIONS = {
+ :key => '_session_id',
+ :domain => nil,
+ :path => "/",
+ :expire_after => nil,
+ :httponly => false
+ }.freeze
+
+ ENV_SESSION_KEY = "rack.session".freeze
+ ENV_SESSION_OPTIONS_KEY = "rack.session.options".freeze
+ HTTP_SET_COOKIE = "Set-Cookie".freeze
+
+ # Raised when storing more than 4K of session data.
+ class CookieOverflow < StandardError; end
+
+ def initialize(app, options = {})
+ options = options.dup
+
+ # Process legacy CGI options
+ options = options.symbolize_keys
+ if options.has_key?(:session_path)
+ options[:path] = options.delete(:session_path)
+ end
+ if options.has_key?(:session_key)
+ options[:key] = options.delete(:session_key)
+ end
+ if options.has_key?(:session_http_only)
+ options[:httponly] = options.delete(:session_http_only)
+ end
+
+ @app = app
+
+ # The session_key option is required.
+ ensure_session_key(options[:key])
+ @key = options.delete(:key).freeze
+
+ # The secret option is required.
+ ensure_secret_secure(options[:secret])
+ @secret = options.delete(:secret).freeze
+
+ @digest = options.delete(:digest) || 'SHA1'
+ @verifier = verifier_for(@secret, @digest)
+
+ @default_options = DEFAULT_OPTIONS.merge(options).freeze
+
+ freeze
end
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- delete
- raise TamperedWithCookie
- end
- # Read the session data cookie.
- def read_cookie
- @session.cgi.cookies[@cookie_options['name']].first
- end
-
- # CGI likes to make you hack.
- def write_cookie(options)
- cookie = CGI::Cookie.new(@cookie_options.merge(options))
- @session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
- end
-
- # Clear cookie value so subsequent new_session doesn't reload old data.
- def clear_old_cookie_value
- @session.cgi.cookies[@cookie_options['name']].clear
- end
-
- def verifier
- if @secret.respond_to?(:call)
- key = @secret.call
- else
- key = @secret
+ def call(env)
+ env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
+ env[ENV_SESSION_OPTIONS_KEY] = @default_options
+
+ status, headers, body = @app.call(env)
+
+ session_data = env[ENV_SESSION_KEY]
+ if !session_data.is_a?(AbstractStore::SessionHash) || session_data.send(:loaded?)
+ session_data = marshal(session_data.to_hash)
+
+ raise CookieOverflow if session_data.size > MAX
+
+ options = env[ENV_SESSION_OPTIONS_KEY]
+ cookie = Hash.new
+ cookie[:value] = session_data
+ unless options[:expire_after].nil?
+ cookie[:expires] = Time.now + options[:expire_after]
+ end
+
+ cookie = build_cookie(@key, cookie.merge(options))
+ case headers[HTTP_SET_COOKIE]
+ when Array
+ headers[HTTP_SET_COOKIE] << cookie
+ when String
+ headers[HTTP_SET_COOKIE] = [headers[HTTP_SET_COOKIE], cookie]
+ when nil
+ headers[HTTP_SET_COOKIE] = cookie
+ end
+ end
+
+ [status, headers, body]
end
- ActiveSupport::MessageVerifier.new(key, @digest)
+
+ private
+ # Should be in Rack::Utils soon
+ def build_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[:httponly]
+ 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}"
+ end
+
+ def load_session(env)
+ request = Rack::Request.new(env)
+ session_data = request.cookies[@key]
+ data = unmarshal(session_data) || persistent_session_id!({})
+ [data[:session_id], data]
+ end
+
+ # Marshal a session hash into safe cookie data. Include an integrity hash.
+ def marshal(session)
+ @verifier.generate(persistent_session_id!(session))
+ end
+
+ # Unmarshal cookie data to a hash and verify its integrity.
+ def unmarshal(cookie)
+ persistent_session_id!(@verifier.verify(cookie)) if cookie
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
+ nil
+ end
+
+ def ensure_session_key(key)
+ if key.blank?
+ raise ArgumentError, 'A session_key is required to write a ' +
+ 'cookie containing the session data. Use ' +
+ 'config.action_controller.session = { :session_key => ' +
+ '"_myapp_session", :secret => "some secret phrase" } in ' +
+ 'config/environment.rb'
+ end
+ end
+
+ # To prevent users from using something insecure like "Password" we make sure that the
+ # secret they've provided is at least 30 characters in length.
+ def ensure_secret_secure(secret)
+ # There's no way we can do this check if they've provided a proc for the
+ # secret.
+ return true if secret.is_a?(Proc)
+
+ if secret.blank?
+ raise ArgumentError, "A secret is required to generate an " +
+ "integrity hash for cookie session data. Use " +
+ "config.action_controller.session = { :session_key => " +
+ "\"_myapp_session\", :secret => \"some secret phrase of at " +
+ "least #{SECRET_MIN_LENGTH} characters\" } " +
+ "in config/environment.rb"
+ end
+
+ if secret.length < SECRET_MIN_LENGTH
+ raise ArgumentError, "Secret should be something secure, " +
+ "like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
+ "provided, \"#{secret}\", is shorter than the minimum length " +
+ "of #{SECRET_MIN_LENGTH} characters"
+ end
+ end
+
+ def verifier_for(secret, digest)
+ key = secret.respond_to?(:call) ? secret.call : secret
+ ActiveSupport::MessageVerifier.new(key, digest)
+ end
+
+ def generate_sid
+ ActiveSupport::SecureRandom.hex(16)
+ end
+
+ def persistent_session_id!(data)
+ (data ||= {}).merge!(inject_persistent_session_id(data))
+ end
+
+ def inject_persistent_session_id(data)
+ requires_session_id?(data) ? { :session_id => generate_sid } : {}
+ end
+
+ def requires_session_id?(data)
+ if data
+ data.respond_to?(:key?) && !data.key?(:session_id)
+ else
+ true
+ end
+ end
end
+ end
end
diff --git a/actionpack/lib/action_controller/session/drb_server.rb b/actionpack/lib/action_controller/session/drb_server.rb
deleted file mode 100755
index 2caa27f62a..0000000000
--- a/actionpack/lib/action_controller/session/drb_server.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env ruby
-
-# This is a really simple session storage daemon, basically just a hash,
-# which is enabled for DRb access.
-
-require 'drb'
-
-session_hash = Hash.new
-session_hash.instance_eval { @mutex = Mutex.new }
-
-class <<session_hash
- def []=(key, value)
- @mutex.synchronize do
- super(key, value)
- end
- end
-
- def [](key)
- @mutex.synchronize do
- super(key)
- end
- end
-
- def delete(key)
- @mutex.synchronize do
- super(key)
- end
- end
-end
-
-DRb.start_service('druby://127.0.0.1:9192', session_hash)
-DRb.thread.join
diff --git a/actionpack/lib/action_controller/session/drb_store.rb b/actionpack/lib/action_controller/session/drb_store.rb
deleted file mode 100644
index 4feb2636e7..0000000000
--- a/actionpack/lib/action_controller/session/drb_store.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'cgi'
-require 'cgi/session'
-require 'drb'
-
-class CGI #:nodoc:all
- class Session
- class DRbStore
- @@session_data = DRbObject.new(nil, 'druby://localhost:9192')
-
- def initialize(session, option=nil)
- @session_id = session.session_id
- end
-
- def restore
- @h = @@session_data[@session_id] || {}
- end
-
- def update
- @@session_data[@session_id] = @h
- end
-
- def close
- update
- end
-
- def delete
- @@session_data.delete(@session_id)
- end
-
- def data
- @@session_data[@session_id]
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/session/mem_cache_store.rb b/actionpack/lib/action_controller/session/mem_cache_store.rb
index 2f08af663d..f745715a97 100644
--- a/actionpack/lib/action_controller/session/mem_cache_store.rb
+++ b/actionpack/lib/action_controller/session/mem_cache_store.rb
@@ -1,95 +1,48 @@
-# cgi/session/memcached.rb - persistent storage of marshalled session data
-#
-# == Overview
-#
-# This file provides the CGI::Session::MemCache class, which builds
-# persistence of storage data on top of the MemCache library. See
-# cgi/session.rb for more details on session storage managers.
-#
-
begin
- require 'cgi/session'
require_library_or_gem 'memcache'
- class CGI
- class Session
- # MemCache-based session storage class.
- #
- # This builds upon the top-level MemCache class provided by the
- # library file memcache.rb. Session data is marshalled and stored
- # in a memcached cache.
- class MemCacheStore
- def check_id(id) #:nodoc:#
- /[^0-9a-zA-Z]+/ =~ id.to_s ? false : true
- end
+ module ActionController
+ module Session
+ class MemCacheStore < AbstractStore
+ def initialize(app, options = {})
+ # Support old :expires option
+ options[:expire_after] ||= options[:expires]
- # Create a new CGI::Session::MemCache instance
- #
- # This constructor is used internally by CGI::Session. The
- # user does not generally need to call it directly.
- #
- # +session+ is the session for which this instance is being
- # created. The session id must only contain alphanumeric
- # characters; automatically generated session ids observe
- # this requirement.
- #
- # +options+ is a hash of options for the initializer. The
- # following options are recognized:
- #
- # cache:: an instance of a MemCache client to use as the
- # session cache.
- #
- # expires:: an expiry time value to use for session entries in
- # the session cache. +expires+ is interpreted in seconds
- # relative to the current time if it’s less than 60*60*24*30
- # (30 days), or as an absolute Unix time (e.g., Time#to_i) if
- # greater. If +expires+ is +0+, or not passed on +options+,
- # the entry will never expire.
- #
- # This session's memcache entry will be created if it does
- # not exist, or retrieved if it does.
- def initialize(session, options = {})
- id = session.session_id
- unless check_id(id)
- raise ArgumentError, "session_id '%s' is invalid" % id
- end
- @cache = options['cache'] || MemCache.new('localhost')
- @expires = options['expires'] || 0
- @session_key = "session:#{id}"
- @session_data = {}
- # Add this key to the store if haven't done so yet
- unless @cache.get(@session_key)
- @cache.add(@session_key, @session_data, @expires)
- end
- end
+ super
- # Restore session state from the session's memcache entry.
- #
- # Returns the session state as a hash.
- def restore
- @session_data = @cache[@session_key] || {}
- end
+ @default_options = {
+ :namespace => 'rack:session',
+ :memcache_server => 'localhost:11211'
+ }.merge(@default_options)
- # Save session state to the session's memcache entry.
- def update
- @cache.set(@session_key, @session_data, @expires)
- end
-
- # Update and close the session's memcache entry.
- def close
- update
- end
+ @pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options)
+ unless @pool.servers.any? { |s| s.alive? }
+ raise "#{self} unable to find server during initialization."
+ end
+ @mutex = Mutex.new
- # Delete the session's memcache entry.
- def delete
- @cache.delete(@session_key)
- @session_data = {}
- end
-
- def data
- @session_data
+ super
end
+ private
+ def get_session(env, sid)
+ sid ||= generate_sid
+ begin
+ session = @pool.get(sid) || {}
+ rescue MemCache::MemCacheError, Errno::ECONNREFUSED
+ session = {}
+ end
+ [sid, session]
+ end
+
+ def set_session(env, sid, session_data)
+ options = env['rack.session.options']
+ expiry = options[:expire_after] || 0
+ @pool.set(sid, session_data, expiry)
+ return true
+ rescue MemCache::MemCacheError, Errno::ECONNREFUSED
+ return false
+ end
end
end
end
diff --git a/actionpack/lib/action_controller/session_management.rb b/actionpack/lib/action_controller/session_management.rb
index 60a9aec39c..f06a0da75c 100644
--- a/actionpack/lib/action_controller/session_management.rb
+++ b/actionpack/lib/action_controller/session_management.rb
@@ -3,8 +3,6 @@ module ActionController #:nodoc:
def self.included(base)
base.class_eval do
extend ClassMethods
- alias_method_chain :process, :session_management_support
- alias_method_chain :process_cleanup, :session_management_support
end
end
@@ -12,144 +10,45 @@ module ActionController #:nodoc:
# Set the session store to be used for keeping the session data between requests.
# By default, sessions are stored in browser cookies (<tt>:cookie_store</tt>),
# but you can also specify one of the other included stores (<tt>:active_record_store</tt>,
- # <tt>:p_store</tt>, <tt>:drb_store</tt>, <tt>:mem_cache_store</tt>, or
- # <tt>:memory_store</tt>) or your own custom class.
+ # <tt>:mem_cache_store</tt>, or your own custom class.
def session_store=(store)
- ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager] =
- store.is_a?(Symbol) ? CGI::Session.const_get(store == :drb_store ? "DRbStore" : store.to_s.camelize) : store
+ if store == :active_record_store
+ self.session_store = ActiveRecord::SessionStore
+ else
+ @@session_store = store.is_a?(Symbol) ?
+ Session.const_get(store.to_s.camelize) :
+ store
+ end
end
# Returns the session store class currently used.
def session_store
- ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager]
+ if defined? @@session_store
+ @@session_store
+ else
+ Session::CookieStore
+ end
+ end
+
+ def session=(options = {})
+ self.session_store = nil if options.delete(:disabled)
+ session_options.merge!(options)
end
# Returns the hash used to configure the session. Example use:
#
# ActionController::Base.session_options[:session_secure] = true # session only available over HTTPS
def session_options
- ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS
- end
-
- # Specify how sessions ought to be managed for a subset of the actions on
- # the controller. Like filters, you can specify <tt>:only</tt> and
- # <tt>:except</tt> clauses to restrict the subset, otherwise options
- # apply to all actions on this controller.
- #
- # The session options are inheritable, as well, so if you specify them in
- # a parent controller, they apply to controllers that extend the parent.
- #
- # Usage:
- #
- # # turn off session management for all actions.
- # session :off
- #
- # # turn off session management for all actions _except_ foo and bar.
- # session :off, :except => %w(foo bar)
- #
- # # turn off session management for only the foo and bar actions.
- # session :off, :only => %w(foo bar)
- #
- # # the session will only work over HTTPS, but only for the foo action
- # session :only => :foo, :session_secure => true
- #
- # # the session by default uses HttpOnly sessions for security reasons.
- # # this can be switched off.
- # session :only => :foo, :session_http_only => false
- #
- # # the session will only be disabled for 'foo', and only if it is
- # # requested as a web service
- # session :off, :only => :foo,
- # :if => Proc.new { |req| req.parameters[:ws] }
- #
- # # the session will be disabled for non html/ajax requests
- # session :off,
- # :if => Proc.new { |req| !(req.format.html? || req.format.js?) }
- #
- # # turn the session back on, useful when it was turned off in the
- # # application controller, and you need it on in another controller
- # session :on
- #
- # All session options described for ActionController::Base.process_cgi
- # are valid arguments.
- def session(*args)
- options = args.extract_options!
-
- options[:disabled] = false if args.delete(:on)
- options[:disabled] = true if !args.empty?
- options[:only] = [*options[:only]].map { |o| o.to_s } if options[:only]
- options[:except] = [*options[:except]].map { |o| o.to_s } if options[:except]
- if options[:only] && options[:except]
- raise ArgumentError, "only one of either :only or :except are allowed"
- end
-
- write_inheritable_array(:session_options, [options])
- end
-
- # So we can declare session options in the Rails initializer.
- alias_method :session=, :session
-
- def cached_session_options #:nodoc:
- @session_options ||= read_inheritable_attribute(:session_options) || []
+ @session_options ||= {}
end
- def session_options_for(request, action) #:nodoc:
- if (session_options = cached_session_options).empty?
- {}
- else
- options = {}
-
- action = action.to_s
- session_options.each do |opts|
- next if opts[:if] && !opts[:if].call(request)
- if opts[:only] && opts[:only].include?(action)
- options.merge!(opts)
- elsif opts[:except] && !opts[:except].include?(action)
- options.merge!(opts)
- elsif !opts[:only] && !opts[:except]
- options.merge!(opts)
- end
- end
-
- if options.empty? then options
- else
- options.delete :only
- options.delete :except
- options.delete :if
- options[:disabled] ? false : options
- end
- end
+ def session(*args)
+ ActiveSupport::Deprecation.warn(
+ "Disabling sessions for a single controller has been deprecated. " +
+ "Sessions are now lazy loaded. So if you don't access them, " +
+ "consider them off. You can still modify the session cookie " +
+ "options with request.session_options.", caller)
end
end
-
- def process_with_session_management_support(request, response, method = :perform_action, *arguments) #:nodoc:
- set_session_options(request)
- process_without_session_management_support(request, response, method, *arguments)
- end
-
- private
- def set_session_options(request)
- request.session_options = self.class.session_options_for(request, request.parameters["action"] || "index")
- end
-
- def process_cleanup_with_session_management_support
- clear_persistent_model_associations
- process_cleanup_without_session_management_support
- end
-
- # Clear cached associations in session data so they don't overflow
- # the database field. Only applies to ActiveRecordStore since there
- # is not a standard way to iterate over session data.
- def clear_persistent_model_associations #:doc:
- if defined?(@_session) && @_session.respond_to?(:data)
- session_data = @_session.data
-
- if session_data && session_data.respond_to?(:each_value)
- session_data.each_value do |obj|
- obj.clear_association_cache if obj.respond_to?(:clear_association_cache)
- end
- end
- end
- end
end
end
diff --git a/actionpack/lib/action_controller/streaming.rb b/actionpack/lib/action_controller/streaming.rb
index 333fb61b45..e1786913a7 100644
--- a/actionpack/lib/action_controller/streaming.rb
+++ b/actionpack/lib/action_controller/streaming.rb
@@ -24,7 +24,8 @@ module ActionController #:nodoc:
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# Defaults to <tt>File.basename(path)</tt>.
- # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
+ # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
+ # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
# * <tt>:length</tt> - used to manually override the length (in bytes) of the content that
# is going to be sent to the client. Defaults to <tt>File.size(path)</tt>.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
@@ -107,7 +108,8 @@ module ActionController #:nodoc:
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
- # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'.
+ # * <tt>:type</tt> - specifies an HTTP content type. Defaults to 'application/octet-stream'. You can specify
+ # either a string or a symbol for a registered type register with <tt>Mime::Type.register</tt>, for example :json
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:status</tt> - specifies the status code to send with the response. Defaults to '200 OK'.
@@ -143,9 +145,16 @@ module ActionController #:nodoc:
disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
+ content_type = options[:type]
+ if content_type.is_a?(Symbol)
+ raise ArgumentError, "Unknown MIME type #{options[:type]}" unless Mime::EXTENSION_LOOKUP.has_key?(content_type.to_s)
+ content_type = Mime::Type.lookup_by_extension(content_type.to_s)
+ end
+ content_type = content_type.to_s.strip # fixes a problem with extra '\r' with some browsers
+
headers.update(
'Content-Length' => options[:length],
- 'Content-Type' => options[:type].to_s.strip, # fixes a problem with extra '\r' with some browsers
+ 'Content-Type' => content_type,
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary'
)
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 79a8e1364d..0b0d0c799b 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -1,4 +1,5 @@
require 'active_support/test_case'
+require 'action_controller/test_process'
module ActionController
# Superclass for ActionController functional tests. Functional tests allow you to
@@ -93,10 +94,7 @@ module ActionController
# and cookies, though. For sessions, you just do:
#
# @request.session[:key] = "value"
- #
- # For cookies, you need to manually create the cookie, like this:
- #
- # @request.cookies["key"] = CGI::Cookie.new("key", "value")
+ # @request.cookies["key"] = "value"
#
# == Testing named routes
#
@@ -105,6 +103,8 @@ module ActionController
#
# assert_redirected_to page_url(:title => 'foo')
class TestCase < ActiveSupport::TestCase
+ include TestProcess
+
module Assertions
%w(response selector tag dom routing model).each do |kind|
include ActionController::Assertions.const_get("#{kind.camelize}Assertions")
@@ -127,17 +127,18 @@ module ActionController
#
# The exception is stored in the exception accessor for further inspection.
module RaiseActionExceptions
- attr_accessor :exception
+ protected
+ attr_accessor :exception
- def rescue_action_without_handler(e)
- self.exception = e
-
- if request.remote_addr == "0.0.0.0"
- raise(e)
- else
- super(e)
+ def rescue_action_without_handler(e)
+ self.exception = e
+
+ if request.remote_addr == "0.0.0.0"
+ raise(e)
+ else
+ super(e)
+ end
end
- end
end
setup :setup_controller_request_and_response
diff --git a/actionpack/lib/action_controller/test_process.rb b/actionpack/lib/action_controller/test_process.rb
index cd3914f011..8180d4ee93 100644
--- a/actionpack/lib/action_controller/test_process.rb
+++ b/actionpack/lib/action_controller/test_process.rb
@@ -1,46 +1,17 @@
-require 'action_controller/test_case'
-
module ActionController #:nodoc:
- class Base
- attr_reader :assigns
-
- # Process a test request called with a TestRequest object.
- def self.process_test(request)
- new.process_test(request)
- end
-
- def process_test(request) #:nodoc:
- process(request, TestResponse.new)
- end
-
- def process_with_test(*args)
- returning process_without_test(*args) do
- @assigns = {}
- (instance_variable_names - @@protected_instance_variables).each do |var|
- value = instance_variable_get(var)
- @assigns[var[1..-1]] = value
- response.template.assigns[var[1..-1]] = value if response
- end
- end
- end
-
- alias_method_chain :process, :test
- end
-
- class TestRequest < AbstractRequest #:nodoc:
+ class TestRequest < Request #:nodoc:
attr_accessor :cookies, :session_options
- attr_accessor :query_parameters, :request_parameters, :path, :session
- attr_accessor :host, :user_agent
+ attr_accessor :query_parameters, :path, :session
+ attr_accessor :host
- def initialize(query_parameters = nil, request_parameters = nil, session = nil)
- @query_parameters = query_parameters || {}
- @request_parameters = request_parameters || {}
- @session = session || TestSession.new
+ def initialize
+ super(Rack::MockRequest.env_for("/"))
- initialize_containers
- initialize_default_values
+ @query_parameters = {}
+ @session = TestSession.new
- super()
+ initialize_default_values
+ initialize_containers
end
def reset_session
@@ -55,7 +26,11 @@ module ActionController #:nodoc:
# Either the RAW_POST_DATA environment variable or the URL-encoded request
# parameters.
def raw_post
- env['RAW_POST_DATA'] ||= returning(url_encoded_request_parameters) { |b| b.force_encoding(Encoding::BINARY) if b.respond_to?(:force_encoding) }
+ @env['RAW_POST_DATA'] ||= begin
+ data = url_encoded_request_parameters
+ data.force_encoding(Encoding::BINARY) if data.respond_to?(:force_encoding)
+ data
+ end
end
def port=(number)
@@ -125,26 +100,30 @@ module ActionController #:nodoc:
path_parameters[key.to_s] = value
end
end
+ raw_post # populate env['RAW_POST_DATA']
@parameters = nil # reset TestRequest#parameters to use the new path_parameters
end
def recycle!
- self.request_parameters = {}
self.query_parameters = {}
self.path_parameters = {}
unmemoize_all
end
+ def user_agent=(user_agent)
+ @env['HTTP_USER_AGENT'] = user_agent
+ end
+
private
def initialize_containers
- @env, @cookies = {}, {}
+ @cookies = {}
end
def initialize_default_values
@host = "test.host"
@request_uri = "/"
- @user_agent = "Rails Testing"
- self.remote_addr = "0.0.0.0"
+ @env['HTTP_USER_AGENT'] = "Rails Testing"
+ @env['REMOTE_ADDR'] = "0.0.0.0"
@env["SERVER_PORT"] = 80
@env['REQUEST_METHOD'] = "GET"
end
@@ -166,7 +145,7 @@ module ActionController #:nodoc:
module TestResponseBehavior #:nodoc:
# The response code of the request
def response_code
- status[0,3].to_i rescue 0
+ status.to_s[0,3].to_i rescue 0
end
# Returns a String to ensure compatibility with Net::HTTPResponse
@@ -221,8 +200,8 @@ module ActionController #:nodoc:
# Returns the template of the file which was used to
# render this response (or nil)
- def rendered_template
- template.instance_variable_get(:@_first_render)
+ def rendered
+ template.instance_variable_get(:@_rendered)
end
# A shortcut to the flash. Returns an empty hash if no session flash exists.
@@ -232,7 +211,7 @@ module ActionController #:nodoc:
# Do we have a flash?
def has_flash?
- !session['flash'].empty?
+ !flash.empty?
end
# Do we have a flash that has contents?
@@ -260,11 +239,16 @@ module ActionController #:nodoc:
!template_objects[name].nil?
end
- # Returns the response cookies, converted to a Hash of (name => CGI::Cookie) pairs
+ # Returns the response cookies, converted to a Hash of (name => value) pairs
#
- # assert_equal ['AuthorOfNewPage'], r.cookies['author'].value
+ # assert_equal 'AuthorOfNewPage', r.cookies['author']
def cookies
- headers['cookie'].inject({}) { |hash, cookie| hash[cookie.name] = cookie; hash }
+ cookies = {}
+ Array(headers['Set-Cookie']).each do |cookie|
+ key, value = cookie.split(";").first.split("=")
+ cookies[key] = value
+ end
+ cookies
end
# Returns binary content (downloadable file), converted to a String
@@ -285,8 +269,8 @@ module ActionController #:nodoc:
# TestResponse, which represent the HTTP response results of the requested
# controller actions.
#
- # See AbstractResponse for more information on controller response objects.
- class TestResponse < AbstractResponse
+ # See Response for more information on controller response objects.
+ class TestResponse < Response
include TestResponseBehavior
def recycle!
@@ -372,20 +356,33 @@ module ActionController #:nodoc:
module TestProcess
def self.included(base)
- # execute the request simulating a specific HTTP method and set/volley the response
- # TODO: this should be un-DRY'ed for the sake of API documentation.
- %w( get post put delete head ).each do |method|
- base.class_eval <<-EOV, __FILE__, __LINE__
- def #{method}(action, parameters = nil, session = nil, flash = nil)
- @request.env['REQUEST_METHOD'] = "#{method.upcase}" if defined?(@request)
- process(action, parameters, session, flash)
- end
- EOV
+ # Executes a request simulating GET HTTP method and set/volley the response
+ def get(action, parameters = nil, session = nil, flash = nil)
+ process(action, parameters, session, flash, "GET")
+ end
+
+ # Executes a request simulating POST HTTP method and set/volley the response
+ def post(action, parameters = nil, session = nil, flash = nil)
+ process(action, parameters, session, flash, "POST")
+ end
+
+ # Executes a request simulating PUT HTTP method and set/volley the response
+ def put(action, parameters = nil, session = nil, flash = nil)
+ process(action, parameters, session, flash, "PUT")
+ end
+
+ # Executes a request simulating DELETE HTTP method and set/volley the response
+ def delete(action, parameters = nil, session = nil, flash = nil)
+ process(action, parameters, session, flash, "DELETE")
+ end
+
+ # Executes a request simulating HEAD HTTP method and set/volley the response
+ def head(action, parameters = nil, session = nil, flash = nil)
+ process(action, parameters, session, flash, "HEAD")
end
end
- # execute the request and set/volley the response
- def process(action, parameters = nil, session = nil, flash = nil)
+ def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
# Sanity check for required instance variables so we can give an
# understandable error message.
%w(@controller @request @response).each do |iv_name|
@@ -398,7 +395,7 @@ module ActionController #:nodoc:
@response.recycle!
@html_document = nil
- @request.env['REQUEST_METHOD'] ||= "GET"
+ @request.env['REQUEST_METHOD'] = http_method
@request.action = action.to_s
@@ -408,12 +405,14 @@ module ActionController #:nodoc:
@request.session = ActionController::TestSession.new(session) unless session.nil?
@request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash
build_request_uri(action, parameters)
- @controller.process(@request, @response)
+
+ Base.class_eval { include ProcessWithTest } unless Base < ProcessWithTest
+ @controller.process_with_test(@request, @response)
end
def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
@request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- @request.env['HTTP_ACCEPT'] = 'text/javascript, text/html, application/xml, text/xml, */*'
+ @request.env['HTTP_ACCEPT'] = [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
returning __send__(request_method, action, parameters, session, flash) do
@request.env.delete 'HTTP_X_REQUESTED_WITH'
@request.env.delete 'HTTP_ACCEPT'
@@ -430,7 +429,7 @@ module ActionController #:nodoc:
end
def session
- @response.session
+ @request.session
end
def flash
@@ -520,12 +519,24 @@ module ActionController #:nodoc:
ActionController::Routing.const_set(:Routes, real_routes) if real_routes
end
end
-end
-module Test
- module Unit
- class TestCase #:nodoc:
- include ActionController::TestProcess
+ module ProcessWithTest #:nodoc:
+ def self.included(base)
+ base.class_eval { attr_reader :assigns }
+ end
+
+ def process_with_test(*args)
+ process(*args).tap { set_test_assigns }
end
+
+ private
+ def set_test_assigns
+ @assigns = {}
+ (instance_variable_names - self.class.protected_instance_variables).each do |var|
+ name, value = var[1..-1], instance_variable_get(var)
+ @assigns[name] = value
+ response.template.assigns[name] = value if response
+ end
+ end
end
end
diff --git a/actionpack/lib/action_controller/uploaded_file.rb b/actionpack/lib/action_controller/uploaded_file.rb
new file mode 100644
index 0000000000..ea4845c68f
--- /dev/null
+++ b/actionpack/lib/action_controller/uploaded_file.rb
@@ -0,0 +1,37 @@
+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
+
+ # 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/url_encoded_pair_parser.rb b/actionpack/lib/action_controller/url_encoded_pair_parser.rb
new file mode 100644
index 0000000000..9883ad0d85
--- /dev/null
+++ b/actionpack/lib/action_controller/url_encoded_pair_parser.rb
@@ -0,0 +1,94 @@
+module ActionController
+ class UrlEncodedPairParser < StringScanner #:nodoc:
+ 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
diff --git a/actionpack/lib/action_controller/verb_piggybacking.rb b/actionpack/lib/action_controller/verb_piggybacking.rb
new file mode 100644
index 0000000000..86cde304a0
--- /dev/null
+++ b/actionpack/lib/action_controller/verb_piggybacking.rb
@@ -0,0 +1,24 @@
+module ActionController
+ # TODO: Use Rack::MethodOverride when it is released
+ class VerbPiggybacking
+ HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS)
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ if env["REQUEST_METHOD"] == "POST"
+ req = Request.new(env)
+ if method = (req.parameters[:_method] || env["HTTP_X_HTTP_METHOD_OVERRIDE"])
+ method = method.to_s.upcase
+ if HTTP_METHODS.include?(method)
+ env["REQUEST_METHOD"] = method
+ end
+ end
+ end
+
+ @app.call(env)
+ end
+ end
+end