diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware')
14 files changed, 359 insertions, 427 deletions
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index e4ae480bfb..1bb2ad7f67 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -1,30 +1,14 @@ +require 'active_support/core_ext/module/delegation' + module ActionDispatch # Provide callbacks to be executed before and after the request dispatch. - # - # It also provides a to_prepare callback, which is performed in all requests - # in development by only once in production and notification callback for async - # operations. - # class Callbacks include ActiveSupport::Callbacks define_callbacks :call, :rescuable => true - define_callbacks :prepare, :scope => :name - # Add a preparation callback. Preparation callbacks are run before every - # request in development mode, and before the first request in production mode. - # - # If a symbol with a block is given, the symbol is used as an identifier. - # That allows to_prepare to be called again with the same identifier to - # replace the existing callback. Passing an identifier is a suggested - # practice if the code adding a preparation block may be reloaded. - def self.to_prepare(*args, &block) - if args.first.is_a?(Symbol) && block_given? - define_method :"__#{args.first}", &block - set_callback(:prepare, :"__#{args.first}") - else - set_callback(:prepare, *args, &block) - end + class << self + delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader" end def self.before(*args, &block) @@ -35,14 +19,13 @@ module ActionDispatch set_callback(:call, :after, *args, &block) end - def initialize(app, prepare_each_request = false) - @app, @prepare_each_request = app, prepare_each_request - _run_prepare_callbacks + def initialize(app, unused = nil) + ActiveSupport::Deprecation.warn "Passing a second argument to ActionDispatch::Callbacks.new is deprecated." unless unused.nil? + @app = app end def call(env) - _run_call_callbacks do - _run_prepare_callbacks if @prepare_each_request + run_callbacks :call do @app.call(env) end end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 4d33cd3b0c..7ac608f0a8 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -7,7 +7,7 @@ module ActionDispatch end end - # Cookies are read and written through ActionController#cookies. + # \Cookies are read and written through ActionController#cookies. # # The cookies being read are the ones received along with the request, the cookies # being written will be sent out with the response. Reading a cookie does not get @@ -16,15 +16,31 @@ module ActionDispatch # Examples for writing: # # # Sets a simple session cookie. + # # This cookie will be deleted when the user's browser is closed. # cookies[:user_name] = "david" # + # # Assign an array of values to a cookie. + # cookies[:lat_lon] = [47.68, -122.37] + # # # Sets a cookie that expires in 1 hour. # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } # + # # Sets a signed cookie, which prevents a user from tampering with its value. + # # The cookie is signed by your app's <tt>config.secret_token</tt> value. + # # Rails generates this value by default when you create a new Rails app. + # cookies.signed[:user_id] = current_user.id + # + # # Sets a "permanent" cookie (which expires in 20 years from now). + # cookies.permanent[:login] = "XJ-122" + # + # # You can also chain these methods: + # cookies.permanent.signed[:login] = "XJ-122" + # # Examples for reading: # # cookies[:user_name] # => "david" # cookies.size # => 2 + # cookies[:lat_lon] # => [47.68, -122.37] # # Example for deleting: # @@ -55,7 +71,7 @@ module ActionDispatch # :domain => :all # Allow the cookie for the top most level # domain and subdomains. # - # * <tt>:expires</tt> - The time at which this cookie expires, as a Time object. + # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. # * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers. # Default is +false+. # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or @@ -69,27 +85,36 @@ module ActionDispatch class CookieJar < Hash #:nodoc: - # This regular expression is used to split the levels of a domain - # So www.example.co.uk gives: - # $1 => www. - # $2 => example - # $3 => co.uk - DOMAIN_REGEXP = /^(.*\.)*(.*)\.(...|...\...|....|..\...|..)$/ + # This regular expression is used to split the levels of a domain. + # The top level domain can be any string without a period or + # **.**, ***.** style TLDs like co.uk or com.au + # + # www.example.co.uk gives: + # $& => example.co.uk + # + # example.com gives: + # $& => example.com + # + # lots.of.subdomains.example.local gives: + # $& => example.local + DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ def self.build(request) secret = request.env[TOKEN_KEY] - host = request.env["HTTP_HOST"] + host = request.host + secure = request.ssl? - new(secret, host).tap do |hash| + new(secret, host, secure).tap do |hash| hash.update(request.cookies) end end - def initialize(secret = nil, host = nil) + def initialize(secret = nil, host = nil, secure = false) @secret = secret @set_cookies = {} @delete_cookies = {} @host = host + @secure = secure super() end @@ -103,8 +128,17 @@ module ActionDispatch options[:path] ||= "/" if options[:domain] == :all - @host =~ DOMAIN_REGEXP - options[:domain] = ".#{$2}.#{$3}" + # if there is a provided tld length then we use it otherwise default domain regexp + domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP + + # if host is not ip and matches domain regexp + # (ip confirms to domain regexp so we explicitly check for ip) + options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp) + ".#{$&}" + end + elsif options[:domain].is_a? Array + # if host matches one of the supplied domains without a dot in front of it + options[:domain] = options[:domain].find {|domain| @host.include? domain[/^\.?(.*)$/, 1] } end end @@ -174,9 +208,15 @@ module ActionDispatch end def write(headers) - @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) } + @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } end + + private + + def write_cookie?(cookie) + @secure || !cookie[:secure] || defined?(Rails.env) && Rails.env.development? + end end class PermanentCookieJar < CookieJar #:nodoc: @@ -248,7 +288,7 @@ module ActionDispatch "integrity hash for cookie session data. Use " + "config.secret_token = \"some secret phrase of at " + "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/application.rb" + "in config/initializers/secret_token.rb" end if secret.length < SECRET_MIN_LENGTH diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index bfa30cf5af..21aeeb217a 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -10,13 +10,13 @@ module ActionDispatch # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create - # action that sets <tt>flash[:notice] = "Successfully created"</tt> before redirecting to a display action that can + # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can # then expose the flash to its template. Actually, that exposure is automatically done. Example: # # class PostsController < ActionController::Base # def create # # save post - # flash[:notice] = "Successfully created post" + # flash[:notice] = "Post successfully created" # redirect_to posts_path(@post) # end # @@ -30,6 +30,11 @@ module ActionDispatch # <div class="notice"><%= flash[:notice] %></div> # <% end %> # + # Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available: + # + # flash.alert = "You must be logged in" + # flash.notice = "Post successfully created" + # # This example just places a string in the flash, but you can put any object in there. And of course, you can put as # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed. # diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb new file mode 100644 index 0000000000..29289a76b4 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -0,0 +1,76 @@ +module ActionDispatch + # ActionDispatch::Reloader provides prepare and cleanup callbacks, + # intended to assist with code reloading during development. + # + # Prepare callbacks are run before each request, and cleanup callbacks + # after each request. In this respect they are analogs of ActionDispatch::Callback's + # before and after callbacks. However, cleanup callbacks are not called until the + # request is fully complete -- that is, after #close has been called on + # the response body. This is important for streaming responses such as the + # following: + # + # self.response_body = lambda { |response, output| + # # code here which refers to application models + # } + # + # Cleanup callbacks will not be called until after the response_body lambda + # is evaluated, ensuring that it can refer to application models and other + # classes before they are unloaded. + # + # By default, ActionDispatch::Reloader is included in the middleware stack + # only in the development environment; specifically, when config.cache_classes + # is false. Callbacks may be registered even when it is not included in the + # middleware stack, but are executed only when +ActionDispatch::Reloader.prepare!+ + # or +ActionDispatch::Reloader.cleanup!+ are called manually. + # + class Reloader + include ActiveSupport::Callbacks + + define_callbacks :prepare, :scope => :name + define_callbacks :cleanup, :scope => :name + + # Add a prepare callback. Prepare callbacks are run before each request, prior + # to ActionDispatch::Callback's before callbacks. + def self.to_prepare(*args, &block) + set_callback(:prepare, *args, &block) + end + + # Add a cleanup callback. Cleanup callbacks are run after each request is + # complete (after #close is called on the response body). + def self.to_cleanup(*args, &block) + set_callback(:cleanup, *args, &block) + end + + # Execute all prepare callbacks. + def self.prepare! + new(nil).run_callbacks :prepare + end + + # Execute all cleanup callbacks. + def self.cleanup! + new(nil).run_callbacks :cleanup + end + + def initialize(app) + @app = app + end + + module CleanupOnClose + def close + super if defined?(super) + ensure + ActionDispatch::Reloader.cleanup! + end + end + + def call(env) + run_callbacks :prepare + response = @app.call(env) + response[2].extend(CleanupOnClose) + response + rescue Exception + run_callbacks :cleanup + raise + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index dd82294644..64d3a87fd0 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -1,5 +1,6 @@ require 'rack/utils' require 'rack/request' +require 'rack/session/abstract/id' require 'action_dispatch/middleware/cookies' require 'active_support/core_ext/object/blank' @@ -8,249 +9,76 @@ module ActionDispatch class SessionRestoreError < StandardError #:nodoc: end - class AbstractStore - ENV_SESSION_KEY = 'rack.session'.freeze - ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze - - # thin wrapper around Hash that allows us to lazily - # load session id into session_options - class OptionsHash < Hash - def initialize(by, env, default_options) - @by = by - @env = env - @session_id_loaded = false - merge!(default_options) - end - - def [](key) - if key == :id - load_session_id! unless key?(:id) || has_session_id? - end - super - end - - private - - def has_session_id? - @session_id_loaded - end - - def load_session_id! - self[:id] = @by.send(:extract_session_id, @env) - @session_id_loaded = true - end - end - - class SessionHash < Hash - def initialize(by, env) - super() - @by = by - @env = env - @loaded = false - end - - def [](key) - load_for_read! - super(key.to_s) - end - - def has_key?(key) - load_for_read! - super(key.to_s) - end - - def []=(key, value) - load_for_write! - super(key.to_s, value) - end - - def clear - load_for_write! - super - end - - def to_hash - load_for_read! - h = {}.replace(self) - h.delete_if { |k,v| v.nil? } - h - end - - def update(hash) - load_for_write! - super(hash.stringify_keys) - end - - def delete(key) - load_for_write! - super(key.to_s) - end - - def inspect - load_for_read! - super - end - - def exists? - return @exists if instance_variable_defined?(:@exists) - @exists = @by.send(:exists?, @env) - end - - def loaded? - @loaded - end - - def destroy - clear - @by.send(:destroy, @env) if @by - @env[ENV_SESSION_OPTIONS_KEY][:id] = nil if @env && @env[ENV_SESSION_OPTIONS_KEY] - @loaded = false - end - - private - - def load_for_read! - load! if !loaded? && exists? - end - - def load_for_write! - load! unless loaded? - end - - def load! - id, session = @by.send(:load_session, @env) - @env[ENV_SESSION_OPTIONS_KEY][:id] = id - replace(session.stringify_keys) - @loaded = true - end - + module DestroyableSession + def destroy + clear + options = @env[Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY] if @env + options ||= {} + @by.send(:destroy_session, @env, options[:id], options) if @by + options[:id] = nil + @loaded = false end + end - DEFAULT_OPTIONS = { - :key => '_session_id', - :path => '/', - :domain => nil, - :expire_after => nil, - :secure => false, - :httponly => true, - :cookie_only => true - } + ::Rack::Session::Abstract::SessionHash.send :include, DestroyableSession + module Compatibility def initialize(app, options = {}) - @app = app - @default_options = DEFAULT_OPTIONS.merge(options) - @key = @default_options.delete(:key).freeze - @cookie_only = @default_options.delete(:cookie_only) - ensure_session_key! + options[:key] ||= '_session_id' + super end - def call(env) - prepare!(env) - response = @app.call(env) - - session_data = env[ENV_SESSION_KEY] - options = env[ENV_SESSION_OPTIONS_KEY] - - if !session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after] - session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded? - - sid = options[:id] || generate_sid - session_data = session_data.to_hash - - value = set_session(env, sid, session_data) - return response unless value - - cookie = { :value => value } - unless options[:expire_after].nil? - cookie[:expires] = Time.now + options.delete(:expire_after) - end - - request = ActionDispatch::Request.new(env) - set_cookie(request, cookie.merge!(options)) - end - - response + def generate_sid + ActiveSupport::SecureRandom.hex(16) end - private - - def prepare!(env) - env[ENV_SESSION_KEY] = SessionHash.new(self, env) - env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options) - end - - def generate_sid - ActiveSupport::SecureRandom.hex(16) - end - - def set_cookie(request, options) - if request.cookie_jar[@key] != options[:value] || !options[:expires].nil? - request.cookie_jar[@key] = options - end - end - - def load_session(env) - stale_session_check! do - sid = current_session_id(env) - sid, session = get_session(env, sid) - [sid, session] - end - end + protected - def extract_session_id(env) - stale_session_check! do - request = ActionDispatch::Request.new(env) - sid = request.cookies[@key] - sid ||= request.params[@key] unless @cookie_only - sid - end - end + def initialize_sid + @default_options.delete(:sidbits) + @default_options.delete(:secure_random) + end + end - def current_session_id(env) - env[ENV_SESSION_OPTIONS_KEY][:id] - end + module StaleSessionCheck + def load_session(env) + stale_session_check! { super } + end - def ensure_session_key! - if @key.blank? - raise ArgumentError, 'A key is required to write a ' + - 'cookie containing the session data. Use ' + - 'config.session_store SESSION_STORE, { :key => ' + - '"_myapp_session" } in config/application.rb' - end - end + def extract_session_id(env) + stale_session_check! { super } + 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 ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" - end - retry - else - raise + 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 ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" end + retry + else + raise end + end + end - def exists?(env) - current_session_id(env).present? - end - - def get_session(env, sid) - raise '#get_session needs to be implemented.' - end + class AbstractStore < Rack::Session::Abstract::ID + include Compatibility + include StaleSessionCheck - def set_session(env, sid, session_data) - raise '#set_session needs to be implemented and should return ' << - 'the value to be stored in the cookie (usually the sid)' - end + def destroy_session(env, sid, options) + ActiveSupport::Deprecation.warn "Implementing #destroy in session stores is deprecated. " << + "Please implement destroy_session(env, session_id, options) instead." + destroy(env) + end - def destroy(env) - raise '#destroy needs to be implemented.' - end + def destroy(env) + raise '#destroy needs to be implemented.' + end end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index ca1494425f..9c9ccc62f5 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -1,5 +1,7 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/blank' +require 'action_dispatch/middleware/session/abstract_store' +require 'rack/session/cookie' module ActionDispatch module Session @@ -38,58 +40,32 @@ module ActionDispatch # "rake secret" and set the key in config/initializers/secret_token.rb. # # Note that changing digest or secret invalidates all existing sessions! - class CookieStore < AbstractStore - - def initialize(app, options = {}) - super(app, options.merge!(:cookie_only => true)) - freeze - end + class CookieStore < Rack::Session::Cookie + include Compatibility + include StaleSessionCheck private - def load_session(env) - data = unpacked_cookie_data(env) - data = persistent_session_id!(data) - [data["session_id"], data] - end - - def extract_session_id(env) - if data = unpacked_cookie_data(env) - data["session_id"] - else - nil - end - end - - def unpacked_cookie_data(env) - env["action_dispatch.request.unsigned_session_cookie"] ||= begin - stale_session_check! do - request = ActionDispatch::Request.new(env) - if data = request.cookie_jar.signed[@key] - data.stringify_keys! - end - data || {} + def unpacked_cookie_data(env) + env["action_dispatch.request.unsigned_session_cookie"] ||= begin + stale_session_check! do + request = ActionDispatch::Request.new(env) + if data = request.cookie_jar.signed[@key] + data.stringify_keys! end + data || {} end end + end - def set_cookie(request, options) - request.cookie_jar.signed[@key] = options - end - - def set_session(env, sid, session_data) - persistent_session_id!(session_data, sid) - end - - def destroy(env) - # session data is stored on client; nothing to do here - end + def set_session(env, sid, session_data, options) + persistent_session_id!(session_data, sid) + end - def persistent_session_id!(data, sid=nil) - data ||= {} - data["session_id"] ||= sid || generate_sid - data - end + def set_cookie(env, session_id, cookie) + request = ActionDispatch::Request.new(env) + request.cookie_jar.signed[@key] = cookie + end end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index 28e3dbd732..4dd9a946c2 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -1,56 +1,17 @@ +require 'action_dispatch/middleware/session/abstract_store' +require 'rack/session/memcache' + module ActionDispatch module Session - class MemCacheStore < AbstractStore + class MemCacheStore < Rack::Session::Memcache + include Compatibility + include StaleSessionCheck + def initialize(app, options = {}) require 'memcache' - - # Support old :expires option options[:expire_after] ||= options[:expires] - - super - - @default_options = { - :namespace => 'rack:session', - :memcache_server => 'localhost:11211' - }.merge(@default_options) - - @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 - 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) - sid - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - false - end - - def destroy(env) - if sid = current_session_id(env) - @pool.delete(sid) - end - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - false - end - end end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index e095b51342..dbe3206808 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -6,8 +6,6 @@ module ActionDispatch # This middleware rescues any exception returned by the application and renders # nice exception pages if it's being rescued locally. class ShowExceptions - LOCALHOST = [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/].freeze - RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') cattr_accessor :rescue_responses @@ -45,28 +43,29 @@ module ActionDispatch end def call(env) - status, headers, body = @app.call(env) - - # Only this middleware cares about RoutingError. So, let's just raise - # it here. - # TODO: refactor this middleware to handle the X-Cascade scenario without - # having to raise an exception. - if headers['X-Cascade'] == 'pass' - raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}" + begin + status, headers, body = @app.call(env) + exception = nil + + # Only this middleware cares about RoutingError. So, let's just raise + # it here. + if headers['X-Cascade'] == 'pass' + raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}" + end + rescue Exception => exception + raise exception if env['action_dispatch.show_exceptions'] == false end - [status, headers, body] - rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) + exception ? render_exception(env, exception) : [status, headers, body] end private def render_exception(env, exception) log_error(exception) + exception = original_exception(exception) request = Request.new(env) - if @consider_all_requests_local || local_request?(request) + if @consider_all_requests_local || request.local? rescue_action_locally(request, exception) else rescue_action_in_public(exception) @@ -112,11 +111,6 @@ module ActionDispatch end end - # True if the request came from localhost, 127.0.0.1. - def local_request?(request) - LOCALHOST.any? { |local_ip| local_ip === request.remote_addr && local_ip === request.remote_ip } - end - def status_code(exception) Rack::Utils.status_code(@@rescue_responses[exception.class.name]) end @@ -134,7 +128,7 @@ module ActionDispatch ActiveSupport::Deprecation.silence do message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code if exception.respond_to?(:annoted_source_code) + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) message << " " << application_trace(exception).join("\n ") logger.fatal("#{message}\n\n") end @@ -161,5 +155,17 @@ module ActionDispatch def logger defined?(Rails.logger) ? Rails.logger : Logger.new($stderr) end + + def original_exception(exception) + if registered_original_exception?(exception) + exception.original_exception + else + exception + end + end + + def registered_original_exception?(exception) + exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name) + end end end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 41078eced7..a4308f528c 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -1,17 +1,27 @@ require "active_support/inflector/methods" +require "active_support/dependencies" module ActionDispatch - class MiddlewareStack < Array + class MiddlewareStack class Middleware - attr_reader :args, :block + attr_reader :args, :block, :name, :classcache def initialize(klass_or_name, *args, &block) - @ref = ActiveSupport::Dependencies::Reference.new(klass_or_name) + @klass = nil + + if klass_or_name.respond_to?(:name) + @klass = klass_or_name + @name = @klass.name + else + @name = klass_or_name.to_s + end + + @classcache = ActiveSupport::Dependencies::Reference @args, @block = args, block end def klass - @ref.get + @klass || classcache[@name] end def ==(middleware) @@ -21,7 +31,7 @@ module ActionDispatch when Class klass == middleware else - normalize(@ref.name) == normalize(middleware) + normalize(@name) == normalize(middleware) end end @@ -40,15 +50,39 @@ module ActionDispatch end end - def initialize(*args, &block) - super(*args) - block.call(self) if block_given? + include Enumerable + + attr_accessor :middlewares + + def initialize(*args) + @middlewares = [] + yield(self) if block_given? + end + + def each + @middlewares.each { |x| yield x } + end + + def size + middlewares.size + end + + def last + middlewares.last + end + + def [](i) + middlewares[i] + end + + def initialize_copy(other) + self.middlewares = other.middlewares.dup end def insert(index, *args, &block) index = assert_index(index, :before) middleware = self.class::Middleware.new(*args, &block) - super(index, middleware) + middlewares.insert(index, middleware) end alias_method :insert_before, :insert @@ -63,26 +97,25 @@ module ActionDispatch delete(target) end - def use(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - push(middleware) + def delete(target) + middlewares.delete target end - def active - ActiveSupport::Deprecation.warn "All middlewares in the chain are active since the laziness " << - "was removed from the middleware stack", caller + def use(*args, &block) + middleware = self.class::Middleware.new(*args, &block) + middlewares.push(middleware) end def build(app = nil, &block) app ||= block raise "MiddlewareStack#build requires an app" unless app - reverse.inject(app) { |a, e| e.build(a) } + middlewares.reverse.inject(app) { |a, e| e.build(a) } end protected def assert_index(index, where) - i = index.is_a?(Integer) ? index : self.index(index) + i = index.is_a?(Integer) ? index : middlewares.index(index) raise "No such middleware to insert #{where}: #{index.inspect}" unless i i end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index d7e88a54e4..c57f694c4d 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -1,12 +1,47 @@ require 'rack/utils' module ActionDispatch + class FileHandler + def initialize(at, root) + @at, @root = at.chomp('/'), root.chomp('/') + @compiled_at = @at.blank? ? nil : /^#{Regexp.escape(at)}/ + @compiled_root = /^#{Regexp.escape(root)}/ + @file_server = ::Rack::File.new(@root) + end + + def match?(path) + path = path.dup + if !@compiled_at || path.sub!(@compiled_at, '') + full_path = path.empty? ? @root : File.join(@root, ::Rack::Utils.unescape(path)) + paths = "#{full_path}#{ext}" + + matches = Dir[paths] + match = matches.detect { |m| File.file?(m) } + if match + match.sub!(@compiled_root, '') + match + end + end + end + + def call(env) + @file_server.call(env) + end + + def ext + @ext ||= begin + ext = ::ActionController::Base.page_cache_extension + "{,#{ext},/index#{ext}}" + end + end + end + class Static FILE_METHODS = %w(GET HEAD).freeze - def initialize(app, root) + def initialize(app, roots) @app = app - @file_server = ::Rack::File.new(root) + @file_handlers = create_file_handlers(roots) end def call(env) @@ -14,15 +49,10 @@ module ActionDispatch method = env['REQUEST_METHOD'] if FILE_METHODS.include?(method) - if file_exist?(path) - return @file_server.call(env) - else - cached_path = directory_exist?(path) ? "#{path}/index" : path - cached_path += ::ActionController::Base.page_cache_extension - - if file_exist?(cached_path) - env['PATH_INFO'] = cached_path - return @file_server.call(env) + @file_handlers.each do |file_handler| + if match = file_handler.match?(path) + env["PATH_INFO"] = match + return file_handler.call(env) end end end @@ -31,14 +61,12 @@ module ActionDispatch end private - def file_exist?(path) - full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path)) - File.file?(full_path) && File.readable?(full_path) - end + def create_file_handlers(roots) + roots = { '' => roots } unless roots.is_a?(Hash) - def directory_exist?(path) - full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path)) - File.directory?(full_path) && File.readable?(full_path) + roots.map do |at, root| + FileHandler.new(at, root) if File.exist?(root) + end.compact end end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb index e963b04524..97f7cf0bbe 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb @@ -14,7 +14,7 @@ def debug_hash(hash) hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") - end + end unless self.class.method_defined?(:debug_hash) %> <h2 style="margin-top: 30px">Request</h2> @@ -28,4 +28,4 @@ <h2 style="margin-top: 30px">Response</h2> -<p><b>Headers</b>: <pre><%=h @response ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p> +<p><b>Headers</b>: <pre><%=h defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb index d18b162a93..8771b5fd6d 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb @@ -12,14 +12,14 @@ <div id="traces"> <% names.each do |name| %> <% - show = "document.getElementById('#{name.gsub /\s/, '-'}').style.display='block';" - hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub /\s/, '-'}').style.display='none';"} + show = "document.getElementById('#{name.gsub(/\s/, '-')}').style.display='block';" + hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub(/\s/, '-')}').style.display='none';"} %> <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %> <% end %> <% traces.each do |name, trace| %> - <div id="<%= name.gsub /\s/, '-' %>" style="display: <%= name == "Application Trace" ? 'block' : 'none' %>;"> + <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;"> <pre><code><%=h trace.join "\n" %></code></pre> </div> <% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb index bd6ffbab5d..50d8ca9484 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb @@ -6,5 +6,5 @@ </h1> <pre><%=h @exception.message %></pre> -<%= render :file => "rescues/_trace.erb" %> -<%= render :file => "rescues/_request_and_response.erb" %> +<%= render :template => "rescues/_trace" %> +<%= render :template => "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb index 02fa18211d..c658559be9 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb @@ -13,9 +13,5 @@ <p><%=h @exception.sub_template_message %></p> -<% @real_exception = @exception - @exception = @exception.original_exception || @exception %> -<%= render :file => "rescues/_trace.erb" %> -<% @exception = @real_exception %> - -<%= render :file => "rescues/_request_and_response.erb" %> +<%= render :template => "rescues/_trace" %> +<%= render :template => "rescues/_request_and_response" %> |