diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2008-12-16 12:05:27 +0000 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2008-12-16 12:05:27 +0000 |
commit | ce0e2084107a20a773a587335cfe54bf70ade795 (patch) | |
tree | fded8e2f6a2d459bdd4676bb9646d3b54cc9f3fe /actionpack/lib | |
parent | 016fffff6d6e434ee7fa69531b08b07d99f48583 (diff) | |
parent | 9e2b4a10f7f091868b3c3701efb4c04048455706 (diff) | |
download | rails-ce0e2084107a20a773a587335cfe54bf70ade795.tar.gz rails-ce0e2084107a20a773a587335cfe54bf70ade795.tar.bz2 rails-ce0e2084107a20a773a587335cfe54bf70ade795.zip |
Merge commit 'mainstream/master'
Diffstat (limited to 'actionpack/lib')
29 files changed, 610 insertions, 1068 deletions
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index abc404afe7..c170e4dd2a 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -89,18 +89,15 @@ module ActionController autoload :Headers, 'action_controller/headers' end - # DEPRECATE: Remove CGI support - autoload :CgiRequest, 'action_controller/cgi_process' - autoload :CGIHandler, 'action_controller/cgi_process' -end - -class CGI - class Session - autoload :ActiveRecordStore, 'action_controller/session/active_record_store' + module Session + autoload :AbstractStore, 'action_controller/session/abstract_store' autoload :CookieStore, 'action_controller/session/cookie_store' - autoload :DRbStore, 'action_controller/session/drb_store' autoload :MemCacheStore, 'action_controller/session/mem_cache_store' end + + # DEPRECATE: Remove CGI support + autoload :CgiRequest, 'action_controller/cgi_process' + autoload :CGIHandler, 'action_controller/cgi_process' end autoload :Mime, 'action_controller/mime_type' 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/selector_assertions.rb b/actionpack/lib/action_controller/assertions/selector_assertions.rb index e03fed7abb..248ca85994 100644 --- a/actionpack/lib/action_controller/assertions/selector_assertions.rb +++ b/actionpack/lib/action_controller/assertions/selector_assertions.rb @@ -587,7 +587,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..0b32da55d5 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 # @@ -1160,6 +1160,9 @@ module ActionController #:nodoc: def reset_session #:doc: request.reset_session @_session = request.session + #http://rails.lighthouseapp.com/projects/8994/tickets/1558-memory-problem-on-reset_session-in-around_filter#ticket-1558-1 + #MRI appears to have a GC related memory leak to do with the finalizer that is defined on CGI::Session + ObjectSpace.undefine_finalizer(@_session) response.session = @_session end @@ -1213,7 +1216,6 @@ 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 @@ -1226,13 +1228,6 @@ 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) 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/dispatcher.rb b/actionpack/lib/action_controller/dispatcher.rb index 203f6b1683..c9a9264b6d 100644 --- a/actionpack/lib/action_controller/dispatcher.rb +++ b/actionpack/lib/action_controller/dispatcher.rb @@ -45,8 +45,10 @@ module ActionController end cattr_accessor :middleware - self.middleware = MiddlewareStack.new - self.middleware.use "ActionController::Failsafe" + self.middleware = MiddlewareStack.new do |middleware| + middleware.use "ActionController::Failsafe" + middleware.use "ActionController::SessionManagement::Middleware" + end include ActiveSupport::Callbacks define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch @@ -89,7 +91,7 @@ module ActionController def _call(env) @request = RackRequest.new(env) - @response = RackResponse.new(@request) + @response = RackResponse.new dispatch end diff --git a/actionpack/lib/action_controller/failsafe.rb b/actionpack/lib/action_controller/failsafe.rb index 1cd649b2e1..b1e9957b49 100644 --- a/actionpack/lib/action_controller/failsafe.rb +++ b/actionpack/lib/action_controller/failsafe.rb @@ -42,7 +42,7 @@ module ActionController end def failsafe_logger - if defined? Rails && Rails.logger + if defined?(Rails) && Rails.logger Rails.logger else Logger.new($stderr) 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/integration.rb b/actionpack/lib/action_controller/integration.rb index 0f0db03b6b..1b0543033b 100644 --- a/actionpack/lib/action_controller/integration.rb +++ b/actionpack/lib/action_controller/integration.rb @@ -227,9 +227,7 @@ 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 @@ -491,8 +489,8 @@ 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) + application ||= ActionController::Dispatcher.new session = Integration::Session.new(application) # delegate the fixture accessors back to the test instance diff --git a/actionpack/lib/action_controller/middleware_stack.rb b/actionpack/lib/action_controller/middleware_stack.rb index 1864bed23a..a6597a6fec 100644 --- a/actionpack/lib/action_controller/middleware_stack.rb +++ b/actionpack/lib/action_controller/middleware_stack.rb @@ -4,7 +4,12 @@ module ActionController attr_reader :klass, :args, :block def initialize(klass, *args, &block) - @klass = klass.is_a?(Class) ? klass : klass.to_s.constantize + if klass.is_a?(Class) + @klass = klass + else + @klass = klass.to_s.constantize + end + @args = args @block = block end @@ -21,18 +26,28 @@ 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 use(*args, &block) - push(Middleware.new(*args, &block)) + middleware = Middleware.new(*args, &block) + push(middleware) end def build(app) diff --git a/actionpack/lib/action_controller/mime_type.rb b/actionpack/lib/action_controller/mime_type.rb index 6923a13f3f..43b3da8d35 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(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/rack_process.rb b/actionpack/lib/action_controller/rack_process.rb index 568f893c6c..e783839f34 100644 --- a/actionpack/lib/action_controller/rack_process.rb +++ b/actionpack/lib/action_controller/rack_process.rb @@ -3,24 +3,12 @@ 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 + def initialize(env) @env = env - @cgi = CGIWrapper.new(self) super() end @@ -66,87 +54,25 @@ module ActionController #:nodoc: @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 + def session_options + @env['rack.session.options'] ||= {} end - def reset_session - @session.delete if defined?(@session) && @session.is_a?(CGI::Session) - @session = new_session + def session_options=(options) + @env['rack.session.options'] = options 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 + @env['rack.session'] ||= {} + end - def session_options_with_string_keys - @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys - end + def reset_session + @env['rack.session'] = {} + end end class RackResponse < AbstractResponse #:nodoc: - def initialize(request) - @cgi = request.cgi + def initialize @writer = lambda { |x| @body << x } @block = nil super() @@ -247,49 +173,8 @@ end_msg 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/rescue.rb b/actionpack/lib/action_controller/rescue.rb index d7b0e96c93..24ee160ee8 100644 --- a/actionpack/lib/action_controller/rescue.rb +++ b/actionpack/lib/action_controller/rescue.rb @@ -39,7 +39,7 @@ module ActionController #:nodoc: } RESCUES_TEMPLATE_PATH = ActionView::PathSet::Path.new( - "#{File.dirname(__FILE__)}/templates", true) + File.join(File.dirname(__FILE__), "templates"), true) def self.included(base) #:nodoc: base.cattr_accessor :rescue_responses diff --git a/actionpack/lib/action_controller/response.rb b/actionpack/lib/action_controller/response.rb index 559c38efd0..4c37f09215 100644 --- a/actionpack/lib/action_controller/response.rb +++ b/actionpack/lib/action_controller/response.rb @@ -115,7 +115,11 @@ module ActionController # :nodoc: 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) 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..c6dd865fad --- /dev/null +++ b/actionpack/lib/action_controller/session/abstract_store.rb @@ -0,0 +1,131 @@ +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) + @by = by + @env = env + @loaded = false + end + + def id + load! unless @loaded + @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 + + private + def load! + @id, session = @by.send(:load_session, @env) + replace(session) + @loaded = true + end + end + + DEFAULT_OPTIONS = { + :key => 'rack.session', + :path => '/', + :domain => nil, + :expire_after => nil, + :secure => false, + :httponly => true, + :cookie_only => true + } + + def initialize(app, options = {}) + @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) + original_session = session.dup + + env[ENV_SESSION_KEY] = session + env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup + + response = @app.call(env) + + session = env[ENV_SESSION_KEY] + unless session == original_session + options = env[ENV_SESSION_OPTIONS_KEY] + sid = session.id + + unless set_session(env, sid, session.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..f4089bfa8b 100644 --- a/actionpack/lib/action_controller/session/cookie_store.rb +++ b/actionpack/lib/action_controller/session/cookie_store.rb @@ -1,163 +1,200 @@ -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 +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 = { + :domain => nil, + :path => "/", + :expire_after => nil + }.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 + + @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 - # 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 + class SessionHash < AbstractStore::SessionHash + private + def load! + session = @by.send(:load_session, @env) + replace(session) + @loaded = true + 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) + def call(env) + session_data = SessionHash.new(self, env) + original_value = session_data.dup - 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 + env[ENV_SESSION_KEY] = session_data + env[ENV_SESSION_OPTIONS_KEY] = @default_options.dup - 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 + status, headers, body = @app.call(env) - # Restore session data from the cookie. - def restore - @original = read_cookie - @data = unmarshal(@original) || {} - end + unless env[ENV_SESSION_KEY] == original_value + session_data = marshal(env[ENV_SESSION_KEY].to_hash) - # Wait until close to write the session data cookie. - def update; end + raise CookieOverflow if session_data.size > MAX - # 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 + 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 - # 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 + 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 - 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) + [status, headers, body] 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 - 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] + unmarshal(session_data) || {} + end + + # 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) + @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 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..a9989d8198 100644 --- a/actionpack/lib/action_controller/session_management.rb +++ b/actionpack/lib/action_controller/session_management.rb @@ -3,8 +3,35 @@ 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 + + class Middleware + DEFAULT_OPTIONS = { + :path => "/", + :key => "_session_id", + :httponly => true, + }.freeze + + def self.new(app) + cgi_options = ActionController::Base.session_options + options = cgi_options.symbolize_keys + options = DEFAULT_OPTIONS.merge(options) + 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 + + if store = ActionController::Base.session_store + store.new(app, options) + else # Sessions disabled + lambda { |env| app.call(env) } + end end end @@ -12,144 +39,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 + @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) || [] - 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/test_process.rb b/actionpack/lib/action_controller/test_process.rb index cd3914f011..c613d6b862 100644 --- a/actionpack/lib/action_controller/test_process.rb +++ b/actionpack/lib/action_controller/test_process.rb @@ -221,8 +221,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 +232,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? @@ -413,7 +413,7 @@ module ActionController #:nodoc: 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' diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb index 9c8b8ade1e..623b9ff6b0 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/paths.rb @@ -57,6 +57,10 @@ module ActionView #:nodoc: end end + def to_str + path.to_str + end + def ==(path) to_str == path.to_str end diff --git a/actionpack/lib/action_view/renderable.rb b/actionpack/lib/action_view/renderable.rb index 7258ad04bf..7c0e62f1d7 100644 --- a/actionpack/lib/action_view/renderable.rb +++ b/actionpack/lib/action_view/renderable.rb @@ -28,11 +28,6 @@ module ActionView stack = view.instance_variable_get(:@_render_stack) stack.push(self) - # This is only used for TestResponse to set rendered_template - unless is_a?(InlineTemplate) || view.instance_variable_get(:@_first_render) - view.instance_variable_set(:@_first_render, self) - end - view.send(:_evaluate_assigns_and_ivars) view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type) diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index e32b7688d0..8f4ca433c0 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -98,14 +98,10 @@ module ActionView #:nodoc: end private - def valid_extension?(extension) - Template.template_handler_extensions.include?(extension) - end - def find_full_path(path, load_paths) load_paths = Array(load_paths) + [nil] load_paths.each do |load_path| - file = [load_path, path].compact.join('/') + file = load_path ? "#{load_path.to_str}/#{path}" : path return load_path, file if File.file?(file) end raise MissingTemplate.new(load_paths, path) @@ -115,11 +111,11 @@ module ActionView #:nodoc: # [base_path, name, format, extension] def split(file) if m = file.match(/^(.*\/)?([^\.]+)\.?(\w+)?\.?(\w+)?\.?(\w+)?$/) - if valid_extension?(m[5]) # Multipart formats + if Template.valid_extension?(m[5]) # Multipart formats [m[1], m[2], "#{m[3]}.#{m[4]}", m[5]] - elsif valid_extension?(m[4]) # Single format + elsif Template.valid_extension?(m[4]) # Single format [m[1], m[2], m[3], m[4]] - elsif valid_extension?(m[3]) # No format + elsif Template.valid_extension?(m[3]) # No format [m[1], m[2], nil, m[3]] else # No extension [m[1], m[2], m[3], nil] diff --git a/actionpack/lib/action_view/template_handlers.rb b/actionpack/lib/action_view/template_handlers.rb index d06ddd5fb5..c50a51b0d1 100644 --- a/actionpack/lib/action_view/template_handlers.rb +++ b/actionpack/lib/action_view/template_handlers.rb @@ -28,6 +28,10 @@ module ActionView #:nodoc: @@template_handlers[extension.to_sym] = klass end + def valid_extension?(extension) + template_handler_extensions.include?(extension) || init_path_for_extension(extension) + end + def template_handler_extensions @@template_handlers.keys.map(&:to_s).sort end @@ -38,7 +42,26 @@ module ActionView #:nodoc: end def handler_class_for_extension(extension) - (extension && @@template_handlers[extension.to_sym]) || @@default_template_handlers + (extension && @@template_handlers[extension.to_sym] || autoload_handler_class(extension)) || + @@default_template_handlers end + + private + def autoload_handler_class(extension) + return if Gem.loaded_specs[extension] + return unless init_path = init_path_for_extension(extension) + Gem.activate(extension) + load(init_path) + handler_class_for_extension(extension) + end + + # Returns the path to the rails/init.rb file for the given extension, + # or nil if no gem provides it. + def init_path_for_extension(extension) + return unless spec = Gem.searcher.find(extension.to_s) + returning File.join(spec.full_gem_path, 'rails', 'init.rb') do |path| + return unless File.file?(path) + end + end end end diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 4ab4ed233f..1a9ef983a5 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -1,4 +1,24 @@ module ActionView + class Base + alias_method :initialize_without_template_tracking, :initialize + def initialize(*args) + @_rendered = { :template => nil, :partials => Hash.new(0) } + initialize_without_template_tracking(*args) + end + end + + module Renderable + alias_method :render_without_template_tracking, :render + def render(view, local_assigns = {}) + if respond_to?(:path) && !is_a?(InlineTemplate) + rendered = view.instance_variable_get(:@_rendered) + rendered[:partials][self] += 1 if is_a?(RenderablePartial) + rendered[:template] ||= self + end + render_without_template_tracking(view, local_assigns) + end + end + class TestCase < ActiveSupport::TestCase include ActionController::TestCase::Assertions @@ -40,11 +60,14 @@ module ActionView end class TestController < ActionController::Base - attr_accessor :request, :response + attr_accessor :request, :response, :params def initialize @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new + + @params = {} + send(:initialize_current_url) end end |