1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Filters overhaul including meantime filter support for around filters. Closes #5949.

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@5163 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
Jeremy Kemper 2006-09-22 03:41:03 +00:00
parent 79821e8bb4
commit a0c677c8e6
4 changed files with 679 additions and 233 deletions

View file

@ -1,5 +1,7 @@
*SVN* *SVN*
* Filters overhaul including meantime filter support using around filters + blocks. #5949 [Martin Emde, Roman Le Negrate, Stefan Kaes, Jeremy Kemper]
* Update RJS render tests. [sam] * Update RJS render tests. [sam]
* Update CGI process to allow sessions to contain namespaced models. Closes #4638. [dfelstead@site5.com] * Update CGI process to allow sessions to contain namespaced models. Closes #4638. [dfelstead@site5.com]

View file

@ -5,19 +5,14 @@ module ActionController #:nodoc:
base.send(:include, ActionController::Filters::InstanceMethods) base.send(:include, ActionController::Filters::InstanceMethods)
end end
# Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do # Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
# authentication, caching, or auditing before the intended action is performed. Or to do localization or output # authentication, caching, or auditing before the intended action is performed. Or to do localization or output
# compression after the action has been performed. # compression after the action has been performed. Filters have access to the request, response, and all the instance
# # variables set by other filters in the chain or by the action (in the case of after filters).
# Filters have access to the request, response, and all the instance variables set by other filters in the chain
# or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <tt>before_filter</tt>
# to halt the processing before the intended action is processed by returning false or performing a redirect or render.
# This is especially useful for filters like authentication where you're not interested in allowing the action to be
# performed if the proper credentials are not in order.
# #
# == Filter inheritance # == Filter inheritance
# #
# Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without # Controller inheritance hierarchies share filters downwards, but subclasses can also add or skip filters without
# affecting the superclass. For example: # affecting the superclass. For example:
# #
# class BankController < ActionController::Base # class BankController < ActionController::Base
@ -39,7 +34,7 @@ module ActionController #:nodoc:
# end # end
# #
# Now any actions performed on the BankController will have the audit method called before. On the VaultController, # Now any actions performed on the BankController will have the audit method called before. On the VaultController,
# first the audit method is called, then the verify_credentials method. If the audit method returns false, then # first the audit method is called, then the verify_credentials method. If the audit method returns false, then
# verify_credentials and the intended action are never called. # verify_credentials and the intended action are never called.
# #
# == Filter types # == Filter types
@ -64,7 +59,7 @@ module ActionController #:nodoc:
# The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can # The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can
# manipulate them as it sees fit. # manipulate them as it sees fit.
# #
# The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation. # The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation.
# Or just as a quick test. It works like this: # Or just as a quick test. It works like this:
# #
# class WeblogController < ActionController::Base # class WeblogController < ActionController::Base
@ -76,6 +71,9 @@ module ActionController #:nodoc:
# session, template, and assigns. Note: The inline method doesn't strictly have to be a block; any object that responds to call # session, template, and assigns. Note: The inline method doesn't strictly have to be a block; any object that responds to call
# and returns 1 or -1 on arity will do (such as a Proc or an Method object). # and returns 1 or -1 on arity will do (such as a Proc or an Method object).
# #
# Please note that around_filters function a little differently than the normal before and after filters with regard to filter
# types. Please see the section dedicated to around_filters below.
#
# == Filter chain ordering # == Filter chain ordering
# #
# Using <tt>before_filter</tt> and <tt>after_filter</tt> appends the specified filters to the existing chain. That's usually # Using <tt>before_filter</tt> and <tt>after_filter</tt> appends the specified filters to the existing chain. That's usually
@ -90,7 +88,7 @@ module ActionController #:nodoc:
# prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock # prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock
# #
# The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt> # The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt>
# <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop # <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop
# is open or not. # is open or not.
# #
# You may pass multiple filter arguments of each type as well as a filter block. # You may pass multiple filter arguments of each type as well as a filter block.
@ -98,253 +96,507 @@ module ActionController #:nodoc:
# #
# == Around filters # == Around filters
# #
# In addition to the individual before and after filters, it's also possible to specify that a single object should handle # Around filters wrap an action, executing code both before and after.
# both the before and after call. That's especially useful when you need to keep state active between the before and after, # They may be declared as method references, blocks, or objects responding
# such as the example of a benchmark filter below: # to #filter or to both #before and #after.
# #
# class WeblogController < ActionController::Base # To use a method as an around_filter, pass a symbol naming the Ruby method.
# around_filter BenchmarkingFilter.new # Yield (or block.call) within the method to run the action.
# #
# # Before this action is performed, BenchmarkingFilter#before(controller) is executed # around_filter :catch_exceptions
# def index #
# private
# def catch_exceptions
# yield
# rescue => exception
# logger.debug "Caught exception! #{exception}"
# raise
# end # end
# # After this action has been performed, BenchmarkingFilter#after(controller) is executed #
# To use a block as an around_filter, pass a block taking as args both
# the controller and the action block. You can't call yield directly from
# an around_filter block; explicitly call the action block instead:
#
# around_filter do |controller, action|
# logger.debug "before #{controller.action_name}"
# action.call
# logger.debug "after #{controller.action_name}"
# end # end
# #
# To use a filter object with around_filter, pass an object responding
# to :filter or both :before and :after. With a filter method, yield to
# the block as above:
#
# around_filter BenchmarkingFilter
#
# class BenchmarkingFilter # class BenchmarkingFilter
# def initialize # def self.filter(controller, &block)
# @runtime # Benchmark.measure(&block)
# end
#
# def before
# start_timer
# end
#
# def after
# stop_timer
# report_result
# end # end
# end # end
# #
# With before and after methods:
#
# around_filter Authorizer.new
#
# class Authorizer
# # This will run before the action. Returning false aborts the action.
# def before(controller)
# if user.authorized?
# return true
# else
# redirect_to login_url
# return false
# end
# end
#
# # This will run after the action if and only if before returned true.
# def after(controller)
# end
# end
#
# If the filter has before and after methods, the before method will be
# called before the action. If before returns false, the filter chain is
# halted and after will not be run. See Filter Chain Halting below for
# an example.
#
# == Filter chain skipping # == Filter chain skipping
# #
# Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the # Declaring a filter on a base class conveniently applies to its subclasses,
# subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters # but sometimes a subclass should skip some of its superclass' filters:
# they would like to be relieved of. Examples
# #
# class ApplicationController < ActionController::Base # class ApplicationController < ActionController::Base
# before_filter :authenticate # before_filter :authenticate
# around_filter :catch_exceptions
# end # end
# #
# class WeblogController < ApplicationController # class WeblogController < ApplicationController
# # will run the :authenticate filter # # Will run the :authenticate and :catch_exceptions filters.
# end # end
# #
# class SignupController < ApplicationController # class SignupController < ApplicationController
# # will not run the :authenticate filter # # Skip :authenticate, run :catch_exceptions.
# skip_before_filter :authenticate # skip_before_filter :authenticate
# end # end
# #
# class ProjectsController < ApplicationController
# # Skip :catch_exceptions, run :authenticate.
# skip_filter :catch_exceptions
# end
#
# class ClientsController < ApplicationController
# # Skip :catch_exceptions and :authenticate unless action is index.
# skip_filter :catch_exceptions, :authenticate, :except => :index
# end
#
# == Filter conditions # == Filter conditions
# #
# Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to # Filters may be limited to specific actions by declaring the actions to
# exclude or the actions to include when executing the filter. Available conditions are +:only+ or +:except+, both # include or exclude. Both options accept single actions (:only => :index)
# of which accept an arbitrary number of method references. For example: # or arrays of actions (:except => [:foo, :bar]).
# #
# class Journal < ActionController::Base # class Journal < ActionController::Base
# # only require authentication if the current action is edit or delete # # Require authentication for edit and delete.
# before_filter :authorize, :only => [ :edit, :delete ] # before_filter :authorize, :only => [:edit, :delete]
# #
# # Passing options to a filter with a block.
# around_filter(:except => :index) do |controller, action_block|
# results = Profiler.run(&action_block)
# controller.response.sub! "</body>", "#{results}</body>"
# end
#
# private # private
# def authorize # def authorize
# # redirect to login unless authenticated # # Redirect to login unless authenticated.
# end # end
# end # end
#
# When setting conditions on inline method (proc) filters the condition must come first and be placed in parentheses.
# #
# class UserPreferences < ActionController::Base # == Filter Chain Halting
# before_filter(:except => :new) { # some proc ... }
# # ...
# end
# #
# <tt>before_filter</tt> and <tt>around_filter</tt> may halt the request
# before controller action is run. This is useful, for example, to deny
# access to unauthenticated users or to redirect from http to https.
# Simply return false from the filter or call render or redirect.
#
# Around filters halt the request unless the action block is called.
# Given these filters
# after_filter :after
# around_filter :around
# before_filter :before
#
# The filter chain will look like:
#
# ...
# . \
# . #around (code before yield)
# . . \
# . . #before (actual filter code is run)
# . . . \
# . . . execute controller action
# . . . /
# . . ...
# . . /
# . #around (code after yield)
# . /
# #after (actual filter code is run)
#
# If #around returns before yielding, only #after will be run. The #before
# filter and controller action will not be run. If #before returns false,
# the second half of #around and all of #after will still run but the
# action will not.
module ClassMethods module ClassMethods
# The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions # The passed <tt>filters</tt> will be appended to the filter_chain and
# on this controller are performed. # will execute before the action on this controller is performed.
def append_before_filter(*filters, &block) def append_before_filter(*filters, &block)
conditions = extract_conditions!(filters) append_filter_to_chain(filters, :before, &block)
filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain('before', filters)
end end
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions # The passed <tt>filters</tt> will be prepended to the filter_chain and
# on this controller are performed. # will execute before the action on this controller is performed.
def prepend_before_filter(*filters, &block) def prepend_before_filter(*filters, &block)
conditions = extract_conditions!(filters) prepend_filter_to_chain(filters, :before, &block)
filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain('before', filters)
end end
# Short-hand for append_before_filter since that's the most common of the two. # Shorthand for append_before_filter since it's the most common.
alias :before_filter :append_before_filter alias :before_filter :append_before_filter
# The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions # The passed <tt>filters</tt> will be appended to the array of filters
# on this controller are performed. # that run _after_ actions on this controller are performed.
def append_after_filter(*filters, &block) def append_after_filter(*filters, &block)
conditions = extract_conditions!(filters) prepend_filter_to_chain(filters, :after, &block)
filters << block if block_given?
add_action_conditions(filters, conditions)
append_filter_to_chain('after', filters)
end end
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions # The passed <tt>filters</tt> will be prepended to the array of filters
# on this controller are performed. # that run _after_ actions on this controller are performed.
def prepend_after_filter(*filters, &block) def prepend_after_filter(*filters, &block)
conditions = extract_conditions!(filters) append_filter_to_chain(filters, :after, &block)
filters << block if block_given?
add_action_conditions(filters, conditions)
prepend_filter_to_chain("after", filters)
end end
# Short-hand for append_after_filter since that's the most common of the two. # Shorthand for append_after_filter since it's the most common.
alias :after_filter :append_after_filter alias :after_filter :append_after_filter
# The passed <tt>filters</tt> will have their +before+ method appended to the array of filters that's run both before actions
# on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all # If you append_around_filter A.new, B.new, the filter chain looks like
# respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like:
# #
# B#before # B#before
# A#before # A#before
# # run the action
# A#after # A#after
# B#after # B#after
def append_around_filter(*filters) #
conditions = extract_conditions!(filters) # With around filters which yield to the action block, #before and #after
for filter in filters.flatten # are the code before and after the yield.
ensure_filter_responds_to_before_and_after(filter) def append_around_filter(*filters, &block)
append_before_filter(conditions || {}) { |c| filter.before(c) } filters, conditions = extract_conditions(filters, &block)
prepend_after_filter(conditions || {}) { |c| filter.after(c) } filters.map { |f| proxy_before_and_after_filter(f) }.each do |filter|
append_filter_to_chain([filter, conditions])
end end
end end
# The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions # If you prepend_around_filter A.new, B.new, the filter chain looks like:
# on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all
# respond to both +before+ and +after+. So if you do prepend_around_filter A.new, B.new, the callstack will look like:
# #
# A#before # A#before
# B#before # B#before
# # run the action
# B#after # B#after
# A#after # A#after
def prepend_around_filter(*filters) #
for filter in filters.flatten # With around filters which yield to the action block, #before and #after
ensure_filter_responds_to_before_and_after(filter) # are the code before and after the yield.
prepend_before_filter { |c| filter.before(c) } def prepend_around_filter(*filters, &block)
append_after_filter { |c| filter.after(c) } filters, conditions = extract_conditions(filters, &block)
filters.map { |f| proxy_before_and_after_filter(f) }.each do |filter|
prepend_filter_to_chain([filter, conditions])
end end
end end
# Short-hand for append_around_filter since that's the most common of the two. # Shorthand for append_around_filter since it's the most common.
alias :around_filter :append_around_filter alias :around_filter :append_around_filter
# Removes the specified filters from the +before+ filter chain. Note that this only works for skipping method-reference # Removes the specified filters from the +before+ filter chain. Note that this only works for skipping method-reference
# filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out # filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out
# of many sub-controllers need a different hierarchy. # of many sub-controllers need a different hierarchy.
# #
# You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options, # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,
# just like when you apply the filters. # just like when you apply the filters.
def skip_before_filter(*filters) def skip_before_filter(*filters)
if conditions = extract_conditions!(filters) skip_filter_in_chain(*filters, &:before?)
remove_contradicting_conditions!(filters, conditions)
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
add_action_conditions(filters, conditions)
else
for filter in filters.flatten
write_inheritable_attribute("before_filters", read_inheritable_attribute("before_filters") - [ filter ])
end
end
end end
# Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference # Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference
# filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out # filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out
# of many sub-controllers need a different hierarchy. # of many sub-controllers need a different hierarchy.
# #
# You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options, # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,
# just like when you apply the filters. # just like when you apply the filters.
def skip_after_filter(*filters) def skip_after_filter(*filters)
if conditions = extract_conditions!(filters) skip_filter_in_chain(*filters, &:after?)
remove_contradicting_conditions!(filters, conditions)
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
add_action_conditions(filters, conditions)
else
for filter in filters.flatten
write_inheritable_attribute("after_filters", read_inheritable_attribute("after_filters") - [ filter ])
end
end
end end
# Removes the specified filters from the filter chain. This only works for method reference (symbol)
# filters, not procs. This method is different from skip_after_filter and skip_before_filter in that
# it will match any before, after or yielding around filter.
#
# You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,
# just like when you apply the filters.
def skip_filter(*filters)
skip_filter_in_chain(*filters)
end
# Returns an array of Filter objects for this controller.
def filter_chain
read_inheritable_attribute("filter_chain") || []
end
# Returns all the before filters for this class and all its ancestors. # Returns all the before filters for this class and all its ancestors.
# This method returns the actual filter that was assigned in the controller to maintain existing functionality.
def before_filters #:nodoc: def before_filters #:nodoc:
@before_filters ||= read_inheritable_attribute("before_filters") || [] filter_chain.select(&:before?).map(&:filter)
end end
# Returns all the after filters for this class and all its ancestors. # Returns all the after filters for this class and all its ancestors.
# This method returns the actual filter that was assigned in the controller to maintain existing functionality.
def after_filters #:nodoc: def after_filters #:nodoc:
@after_filters ||= read_inheritable_attribute("after_filters") || [] filter_chain.select(&:after?).map(&:filter)
end end
# Returns a mapping between filters and the actions that may run them. # Returns a mapping between filters and the actions that may run them.
def included_actions #:nodoc: def included_actions #:nodoc:
@included_actions ||= read_inheritable_attribute("included_actions") || {} filter_chain.inject({}) { |h,f| h[f.filter] = f.included_actions; h }
end end
# Returns a mapping between filters and actions that may not run them. # Returns a mapping between filters and actions that may not run them.
def excluded_actions #:nodoc: def excluded_actions #:nodoc:
@excluded_actions ||= read_inheritable_attribute("excluded_actions") || {} filter_chain.inject({}) { |h,f| h[f.filter] = f.excluded_actions; h }
end end
private # Find a filter in the filter_chain where the filter method matches the _filter_ param
def append_filter_to_chain(condition, filters) # and (optionally) the passed block evaluates to true (mostly used for testing before?
write_inheritable_array("#{condition}_filters", filters) # and after? on the filter). Useful for symbol filters.
#
# The object of type Filter is passed to the block when yielded, not the filter itself.
def find_filter(filter, &block) #:nodoc:
filter_chain.select { |f| f.filter == filter && (!block_given? || yield(f)) }.first
end
# Filter class is an abstract base class for all filters. Handles all of the included/excluded actions but
# contains no logic for calling the actual filters.
class Filter #:nodoc:
attr_reader :filter, :included_actions, :excluded_actions
def initialize(filter, options={})
@filter = filter
@position = options[:position]
update_conditions(options)
end end
def prepend_filter_to_chain(condition, filters) def update_conditions(conditions)
old_filters = read_inheritable_attribute("#{condition}_filters") || [] if conditions[:only]
write_inheritable_attribute("#{condition}_filters", filters + old_filters) @included_actions = [conditions[:only]].flatten.map(&:to_s)
else
@excluded_actions = [conditions[:except]].flatten.compact.map(&:to_s)
end
build_excluded_actions_hash
end end
def ensure_filter_responds_to_before_and_after(filter) def remove_actions_from_included_actions!(*actions)
unless filter.respond_to?(:before) && filter.respond_to?(:after) if @included_actions
raise ActionControllerError, "Filter object must respond to both before and after" actions.flatten.map(&:to_s).each { |action| @included_actions.delete(action) }
build_excluded_actions_hash
end end
end end
def extract_conditions!(filters) def before?
return nil unless filters.last.is_a? Hash false
filters.pop
end end
def add_action_conditions(filters, conditions) def after?
return unless conditions false
included, excluded = conditions[:only], conditions[:except]
write_inheritable_hash('included_actions', condition_hash(filters, included)) && return if included
write_inheritable_hash('excluded_actions', condition_hash(filters, excluded)) if excluded
end end
def condition_hash(filters, *actions) def excluded_from?(action)
filters.inject({}) {|hash, filter| hash.merge(filter => actions.flatten.map {|action| action.to_s})} @excluded_actions_hash[action]
end end
def remove_contradicting_conditions!(filters, conditions) def call(controller, &block)
return unless conditions[:only] raise(ActionControllerError, 'No filter type: Nothing to do here.')
filters.each do |filter| end
next unless included_actions_for_filter = (read_inheritable_attribute('included_actions') || {})[filter]
[*conditions[:only]].each do |conditional_action| private
conditional_action = conditional_action.to_s def build_excluded_actions_hash
included_actions_for_filter.delete(conditional_action) if included_actions_for_filter.include?(conditional_action) @excluded_actions_hash =
if @included_actions
@included_actions.inject(Hash.new(true)){|h, a| h[a] = false; h}
else
@excluded_actions.inject(Hash.new(false)){|h, a| h[a] = true; h}
end
end
end
# Abstract base class for filter proxies. FilterProxy objects are meant to mimic the behaviour of the old
# before_filter and after_filter by moving the logic into the filter itself.
class FilterProxy < Filter #:nodoc:
def filter
@filter.filter
end
end
class BeforeFilterProxy < FilterProxy #:nodoc:
def before?
true
end
def call(controller, &block)
if false == @filter.call(controller) # must only stop if equal to false. only filters returning false are halted.
controller.halt_filter_chain(@filter, :returned_false)
else
yield
end
end
end
class AfterFilterProxy < FilterProxy #:nodoc:
def after?
true
end
def call(controller, &block)
yield
@filter.call(controller)
end
end
class SymbolFilter < Filter #:nodoc:
def call(controller, &block)
controller.send(@filter, &block)
end
end
class ProcFilter < Filter #:nodoc:
def call(controller)
@filter.call(controller)
rescue LocalJumpError # a yield from a proc... no no bad dog.
raise(ActionControllerError, 'Cannot yield from a Proc type filter. The Proc must take two arguments and execute #call on the second argument.')
end
end
class ProcWithCallFilter < Filter #:nodoc:
def call(controller, &block)
@filter.call(controller, block)
rescue LocalJumpError # a yield from a proc... no no bad dog.
raise(ActionControllerError, 'Cannot yield from a Proc type filter. The Proc must take two arguments and execute #call on the second argument.')
end
end
class MethodFilter < Filter #:nodoc:
def call(controller, &block)
@filter.call(controller, &block)
end
end
class ClassFilter < Filter #:nodoc:
def call(controller, &block)
@filter.filter(controller, &block)
end
end
protected
def append_filter_to_chain(filters, position = :around, &block)
write_inheritable_array('filter_chain', create_filters(filters, position, &block) )
end
def prepend_filter_to_chain(filters, position = :around, &block)
write_inheritable_attribute('filter_chain', create_filters(filters, position, &block) + filter_chain)
end
def create_filters(filters, position, &block) #:nodoc:
filters, conditions = extract_conditions(filters, &block)
conditions.merge!(:position => position)
# conditions should be blank for a filter behind a filter proxy since the proxy will take care of all conditions.
proxy_conditions, conditions = conditions, {} unless :around == position
filters.map do |filter|
# change all the filters into instances of Filter derived classes
class_for_filter(filter).new(filter, conditions)
end.collect do |filter|
# apply proxy to filter if necessary
case position
when :before
BeforeFilterProxy.new(filter, proxy_conditions)
when :after
AfterFilterProxy.new(filter, proxy_conditions)
else
filter
end end
end end
end end
# The determination of the filter type was once done at run time.
# This method is here to extract as much logic from the filter run time as possible
def class_for_filter(filter) #:nodoc:
case
when filter.is_a?(Symbol)
SymbolFilter
when filter.respond_to?(:call)
if filter.is_a?(Method)
MethodFilter
elsif filter.arity == 1
ProcFilter
else
ProcWithCallFilter
end
when filter.respond_to?(:filter)
ClassFilter
else
raise(ActionControllerError, 'A filters must be a Symbol, Proc, Method, or object responding to filter.')
end
end
def filter_responds_to_before_and_after(filter) #:nodoc:
filter.respond_to?(:before) && filter.respond_to?(:after)
end
def proxy_before_and_after_filter(filter) #:nodoc:
return filter unless filter_responds_to_before_and_after(filter)
Proc.new do |controller, action|
unless filter.before(controller) == false
begin
action.call
ensure
filter.after(controller)
end
end
end
end
def extract_conditions(*filters, &block) #:nodoc:
filters.flatten!
conditions = filters.last.is_a?(Hash) ? filters.pop : {}
filters << block if block_given?
return filters.flatten, conditions
end
def skip_filter_in_chain(*filters, &test) #:nodoc:
filters, conditions = extract_conditions(filters)
finder_proc = block_given? ?
proc { |f| find_filter(f, &test) } :
proc { |f| find_filter(f) }
filters.map(&finder_proc).reject(&:nil?).each do |filter|
if conditions.empty?
delete_filter_in_chain(filter)
else
filter.remove_actions_from_included_actions!(conditions[:only] || [])
conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
filter.update_conditions(conditions)
end
end
end
def delete_filter_in_chain(filter) #:nodoc:
write_inheritable_attribute('filter_chain', filter_chain.reject { |f| f == filter })
end
end end
module InstanceMethods # :nodoc: module InstanceMethods # :nodoc:
@ -357,14 +609,10 @@ module ActionController #:nodoc:
end end
def perform_action_with_filters def perform_action_with_filters
before_action_result = before_action #result = perform_filters do
# perform_action_without_filters unless performed?
unless before_action_result == false || performed? #end
perform_action_without_filters @before_filter_chain_aborted = (call_filter(self.class.filter_chain, 0) == false)
after_action
end
@before_filter_chain_aborted = (before_action_result == false)
end end
def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc: def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc:
@ -372,61 +620,37 @@ module ActionController #:nodoc:
process_without_filters(request, response, method, *arguments) process_without_filters(request, response, method, *arguments)
end end
# Calls all the defined before-filter filters, which are added by using "before_filter :method". def filter_chain
# If any of the filters return false, no more filters will be executed and the action is aborted. self.class.filter_chain
def before_action #:doc:
call_filters(self.class.before_filters)
end end
# Calls all the defined after-filter filters, which are added by using "after_filter :method". def call_filter(chain, index)
# If any of the filters return false, no more filters will be executed. return (performed? || perform_action_without_filters) if index >= chain.size
def after_action #:doc: filter = chain[index]
call_filters(self.class.after_filters) return call_filter(chain, index.next) if filter.excluded_from?(action_name)
called = false
filter.call(self) do
call_filter(chain, index.next)
called = true
end
halt_filter_chain(filter.filter, :no_yield) if called == false
called
end end
def halt_filter_chain(filter, reason)
if logger
case reason
when :no_yield
logger.info "Filter chain halted as [#{filter.inspect}] did not yield."
when :returned_false
logger.info "Filter chain halted as [#{filter.inspect}] returned false."
end
end
return false
end
private private
def call_filters(filters)
filters.each do |filter|
next if action_exempted?(filter)
filter_result = case
when filter.is_a?(Symbol)
self.send(filter)
when filter_block?(filter)
filter.call(self)
when filter_class?(filter)
filter.filter(self)
else
raise(
ActionControllerError,
'Filters need to be either a symbol, proc/method, or class implementing a static filter method'
)
end
if filter_result == false
logger.info "Filter chain halted as [#{filter}] returned false" if logger
return false
end
end
end
def filter_block?(filter)
filter.respond_to?('call') && (filter.arity == 1 || filter.arity == -1)
end
def filter_class?(filter)
filter.respond_to?('filter')
end
def action_exempted?(filter)
case
when ia = self.class.included_actions[filter]
!ia.include?(action_name)
when ea = self.class.excluded_actions[filter]
ea.include?(action_name)
end
end
def process_cleanup_with_filters def process_cleanup_with_filters
if @before_filter_chain_aborted if @before_filter_chain_aborted
close_session close_session

View file

@ -106,6 +106,7 @@ class ActiveRecordTestCase < Test::Unit::TestCase
private private
# If things go wrong, we don't want to run our test cases. We'll just define them to test nothing. # If things go wrong, we don't want to run our test cases. We'll just define them to test nothing.
def abort_tests def abort_tests
$stderr.puts 'No Active Record connection, aborting tests.'
self.class.public_instance_methods.grep(/^test./).each do |method| self.class.public_instance_methods.grep(/^test./).each do |method|
self.class.class_eval { define_method(method.to_sym){} } self.class.class_eval { define_method(method.to_sym){} }
end end

View file

@ -198,15 +198,6 @@ class FilterTest < Test::Unit::TestCase
end end
end end
class BadFilterController < ActionController::Base
before_filter 2
def show() "show" end
protected
def rescue_action(e) raise(e) end
end
class AroundFilterController < PrependingController class AroundFilterController < PrependingController
around_filter AroundFilter.new around_filter AroundFilter.new
end end
@ -336,19 +327,20 @@ class FilterTest < Test::Unit::TestCase
assert_equal %w( ensure_login clean_up_tmp), test_process(BeforeAndAfterConditionController).template.assigns["ran_filter"] assert_equal %w( ensure_login clean_up_tmp), test_process(BeforeAndAfterConditionController).template.assigns["ran_filter"]
assert_equal nil, test_process(BeforeAndAfterConditionController, "show_without_filter").template.assigns["ran_filter"] assert_equal nil, test_process(BeforeAndAfterConditionController, "show_without_filter").template.assigns["ran_filter"]
end end
def test_bad_filter def test_bad_filter
assert_raises(ActionController::ActionControllerError) { bad_filter_controller = Class.new(ActionController::Base)
test_process(BadFilterController) assert_raises(ActionController::ActionControllerError) do
} bad_filter_controller.before_filter 2
end
end end
def test_around_filter def test_around_filter
controller = test_process(AroundFilterController) controller = test_process(AroundFilterController)
assert controller.template.assigns["before_ran"] assert controller.template.assigns["before_ran"]
assert controller.template.assigns["after_ran"] assert controller.template.assigns["after_ran"]
end end
def test_having_properties_in_around_filter def test_having_properties_in_around_filter
controller = test_process(AroundFilterController) controller = test_process(AroundFilterController)
assert_equal "before and after", controller.template.assigns["execution_log"] assert_equal "before and after", controller.template.assigns["execution_log"]
@ -408,3 +400,230 @@ class FilterTest < Test::Unit::TestCase
controller.process(request, ActionController::TestResponse.new) controller.process(request, ActionController::TestResponse.new)
end end
end end
class PostsController < ActionController::Base
def rescue_action(e); raise e; end
module AroundExceptions
class Error < StandardError ; end
class Before < Error ; end
class After < Error ; end
end
include AroundExceptions
class DefaultFilter
include AroundExceptions
end
module_eval %w(raises_before raises_after raises_both no_raise no_filter).map { |action| "def #{action}; default_action end" }.join("\n")
private
def default_action
render :inline => "#{action_name} called"
end
end
class ControllerWithSymbolAsFilter < PostsController
around_filter :raise_before, :only => :raises_before
around_filter :raise_after, :only => :raises_after
around_filter :without_exception, :only => :no_raise
private
def raise_before
raise Before
yield
end
def raise_after
yield
raise After
end
def without_exception
# Do stuff...
1 + 1
yield
# Do stuff...
1 + 1
end
end
class ControllerWithFilterClass < PostsController
class YieldingFilter < DefaultFilter
def self.filter(controller)
yield
raise After
end
end
around_filter YieldingFilter, :only => :raises_after
end
class ControllerWithFilterInstance < PostsController
class YieldingFilter < DefaultFilter
def filter(controller)
yield
raise After
end
end
around_filter YieldingFilter.new, :only => :raises_after
end
class ControllerWithFilterMethod < PostsController
class YieldingFilter < DefaultFilter
def filter(controller)
yield
raise After
end
end
around_filter YieldingFilter.new.method(:filter), :only => :raises_after
end
class ControllerWithProcFilter < PostsController
around_filter(:only => :no_raise) do |c,b|
c.assigns['before'] = true
b.call
c.assigns['after'] = true
end
end
class ControllerWithWrongFilterType < PostsController
around_filter lambda { yield }, :only => :no_raise
end
class ControllerWithNestedFilters < ControllerWithSymbolAsFilter
around_filter :raise_before, :raise_after, :without_exception, :only => :raises_both
end
class ControllerWithAllTypesOfFilters < PostsController
before_filter :before
around_filter :around
after_filter :after
around_filter :around_again
private
def before
@ran_filter ||= []
@ran_filter << 'before'
end
def around
@ran_filter << 'around (before yield)'
yield
@ran_filter << 'around (after yield)'
end
def after
@ran_filter << 'after'
end
def around_again
@ran_filter << 'around_again (before yield)'
yield
@ran_filter << 'around_again (after yield)'
end
end
class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters
skip_filter :around_again
skip_filter :after
end
class YieldingAroundFiltersTest < Test::Unit::TestCase
include PostsController::AroundExceptions
def test_filters_registering
assert_equal 1, ControllerWithFilterMethod.filter_chain.size
assert_equal 1, ControllerWithFilterClass.filter_chain.size
assert_equal 1, ControllerWithFilterInstance.filter_chain.size
assert_equal 3, ControllerWithSymbolAsFilter.filter_chain.size
assert_equal 1, ControllerWithWrongFilterType.filter_chain.size
assert_equal 6, ControllerWithNestedFilters.filter_chain.size
assert_equal 4, ControllerWithAllTypesOfFilters.filter_chain.size
end
def test_wrong_filter_type
assert_raise(ActionController::ActionControllerError) do
test_process(ControllerWithWrongFilterType,'no_raise')
end
end
def test_base
controller = PostsController
assert_nothing_raised { test_process(controller,'no_raise') }
assert_nothing_raised { test_process(controller,'raises_before') }
assert_nothing_raised { test_process(controller,'raises_after') }
assert_nothing_raised { test_process(controller,'no_filter') }
end
def test_with_symbol
controller = ControllerWithSymbolAsFilter
assert_nothing_raised { test_process(controller,'no_raise') }
assert_raise(Before) { test_process(controller,'raises_before') }
assert_raise(After) { test_process(controller,'raises_after') }
assert_nothing_raised { test_process(controller,'no_raise') }
end
def test_with_class
controller = ControllerWithFilterClass
assert_nothing_raised { test_process(controller,'no_raise') }
assert_raise(After) { test_process(controller,'raises_after') }
end
def test_with_instance
controller = ControllerWithFilterInstance
assert_nothing_raised { test_process(controller,'no_raise') }
assert_raise(After) { test_process(controller,'raises_after') }
end
def test_with_method
controller = ControllerWithFilterMethod
assert_nothing_raised { test_process(controller,'no_raise') }
assert_raise(After) { test_process(controller,'raises_after') }
end
def test_with_proc
controller = test_process(ControllerWithProcFilter,'no_raise')
assert controller.template.assigns['before']
assert controller.template.assigns['after']
end
def test_nested_filters
controller = ControllerWithNestedFilters
assert_nothing_raised do
begin
test_process(controller,'raises_both')
rescue Before, After
end
end
assert_raise Before do
begin
test_process(controller,'raises_both')
rescue After
end
end
end
def test_filter_order_with_all_filter_types
controller = test_process(ControllerWithAllTypesOfFilters,'no_raise')
assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) around (after yield) after',controller.template.assigns['ran_filter'].join(' ')
end
def test_filter_order_with_skip_filter_method
controller = test_process(ControllerWithTwoLessFilters,'no_raise')
assert_equal 'before around (before yield) around (after yield)',controller.template.assigns['ran_filter'].join(' ')
end
protected
def test_process(controller, action = "show")
request = ActionController::TestRequest.new
request.action = action
controller.process(request, ActionController::TestResponse.new)
end
end