diff options
Diffstat (limited to 'actionpack/lib/action_controller')
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 |