aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/CHANGELOG2
-rw-r--r--actionpack/lib/action_controller/filters.rb654
-rw-r--r--actionpack/test/active_record_unit.rb1
-rw-r--r--actionpack/test/controller/filters_test.rb249
4 files changed, 676 insertions, 230 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG
index fb03e5b505..f8710fc893 100644
--- a/actionpack/CHANGELOG
+++ b/actionpack/CHANGELOG
@@ -1,5 +1,7 @@
*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 CGI process to allow sessions to contain namespaced models. Closes #4638. [dfelstead@site5.com]
diff --git a/actionpack/lib/action_controller/filters.rb b/actionpack/lib/action_controller/filters.rb
index 624124ac01..58868a8371 100644
--- a/actionpack/lib/action_controller/filters.rb
+++ b/actionpack/lib/action_controller/filters.rb
@@ -5,19 +5,14 @@ module ActionController #:nodoc:
base.send(:include, ActionController::Filters::InstanceMethods)
end
- # 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
- # 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). 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.
+ # 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
+ # 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).
#
# == 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:
#
# class BankController < ActionController::Base
@@ -39,7 +34,7 @@ module ActionController #:nodoc:
# end
#
# 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.
#
# == 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
# 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:
#
# 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
# 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
#
# 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
#
# 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.
#
# You may pass multiple filter arguments of each type as well as a filter block.
@@ -98,253 +96,507 @@ module ActionController #:nodoc:
#
# == Around filters
#
- # In addition to the individual before and after filters, it's also possible to specify that a single object should handle
- # both the before and after call. That's especially useful when you need to keep state active between the before and after,
- # such as the example of a benchmark filter below:
- #
- # class WeblogController < ActionController::Base
- # around_filter BenchmarkingFilter.new
- #
- # # Before this action is performed, BenchmarkingFilter#before(controller) is executed
- # def index
+ # Around filters wrap an action, executing code both before and after.
+ # They may be declared as method references, blocks, or objects responding
+ # to #filter or to both #before and #after.
+ #
+ # To use a method as an around_filter, pass a symbol naming the Ruby method.
+ # Yield (or block.call) within the method to run the action.
+ #
+ # around_filter :catch_exceptions
+ #
+ # private
+ # def catch_exceptions
+ # yield
+ # rescue => exception
+ # logger.debug "Caught exception! #{exception}"
+ # raise
# 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
#
+ # 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
- # def initialize
- # @runtime
+ # def self.filter(controller, &block)
+ # Benchmark.measure(&block)
# end
- #
- # def before
- # start_timer
+ # 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
- #
- # def after
- # stop_timer
- # report_result
+ #
+ # # 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
#
- # Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the
- # subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters
- # they would like to be relieved of. Examples
+ # Declaring a filter on a base class conveniently applies to its subclasses,
+ # but sometimes a subclass should skip some of its superclass' filters:
#
# class ApplicationController < ActionController::Base
# before_filter :authenticate
+ # around_filter :catch_exceptions
# end
#
# class WeblogController < ApplicationController
- # # will run the :authenticate filter
+ # # Will run the :authenticate and :catch_exceptions filters.
# end
#
# class SignupController < ApplicationController
- # # will not run the :authenticate filter
+ # # Skip :authenticate, run :catch_exceptions.
# skip_before_filter :authenticate
# 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
#
- # Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to
- # exclude or the actions to include when executing the filter. Available conditions are +:only+ or +:except+, both
- # of which accept an arbitrary number of method references. For example:
+ # Filters may be limited to specific actions by declaring the actions to
+ # include or exclude. Both options accept single actions (:only => :index)
+ # or arrays of actions (:except => [:foo, :bar]).
#
# class Journal < ActionController::Base
- # # only require authentication if the current action is edit or delete
- # before_filter :authorize, :only => [ :edit, :delete ]
- #
+ # # Require authentication for edit and 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
# def authorize
- # # redirect to login unless authenticated
+ # # Redirect to login unless authenticated.
# end
# end
- #
- # When setting conditions on inline method (proc) filters the condition must come first and be placed in parentheses.
- #
- # class UserPreferences < ActionController::Base
- # before_filter(:except => :new) { # some proc ... }
- # # ...
- # end
#
+ # == Filter Chain Halting
+ #
+ # <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
- # The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions
- # on this controller are performed.
+ # The passed <tt>filters</tt> will be appended to the filter_chain and
+ # will execute before the action on this controller is performed.
def append_before_filter(*filters, &block)
- conditions = extract_conditions!(filters)
- filters << block if block_given?
- add_action_conditions(filters, conditions)
- append_filter_to_chain('before', filters)
+ append_filter_to_chain(filters, :before, &block)
end
- # The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions
- # on this controller are performed.
+ # The passed <tt>filters</tt> will be prepended to the filter_chain and
+ # will execute before the action on this controller is performed.
def prepend_before_filter(*filters, &block)
- conditions = extract_conditions!(filters)
- filters << block if block_given?
- add_action_conditions(filters, conditions)
- prepend_filter_to_chain('before', filters)
+ prepend_filter_to_chain(filters, :before, &block)
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
-
- # The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions
- # on this controller are performed.
+
+ # The passed <tt>filters</tt> will be appended to the array of filters
+ # that run _after_ actions on this controller are performed.
def append_after_filter(*filters, &block)
- conditions = extract_conditions!(filters)
- filters << block if block_given?
- add_action_conditions(filters, conditions)
- append_filter_to_chain('after', filters)
+ prepend_filter_to_chain(filters, :after, &block)
end
- # The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions
- # on this controller are performed.
+ # The passed <tt>filters</tt> will be prepended to the array of filters
+ # that run _after_ actions on this controller are performed.
def prepend_after_filter(*filters, &block)
- conditions = extract_conditions!(filters)
- filters << block if block_given?
- add_action_conditions(filters, conditions)
- prepend_filter_to_chain("after", filters)
+ append_filter_to_chain(filters, :after, &block)
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
-
- # 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
- # respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like:
+
+
+ # If you append_around_filter A.new, B.new, the filter chain looks like
#
# B#before
# A#before
+ # # run the action
# A#after
# B#after
- def append_around_filter(*filters)
- conditions = extract_conditions!(filters)
- for filter in filters.flatten
- ensure_filter_responds_to_before_and_after(filter)
- append_before_filter(conditions || {}) { |c| filter.before(c) }
- prepend_after_filter(conditions || {}) { |c| filter.after(c) }
- end
- end
-
- # The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions
- # 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:
+ #
+ # With around filters which yield to the action block, #before and #after
+ # are the code before and after the yield.
+ def append_around_filter(*filters, &block)
+ filters, conditions = extract_conditions(filters, &block)
+ filters.map { |f| proxy_before_and_after_filter(f) }.each do |filter|
+ append_filter_to_chain([filter, conditions])
+ end
+ end
+
+ # If you prepend_around_filter A.new, B.new, the filter chain looks like:
#
# A#before
# B#before
+ # # run the action
# B#after
# A#after
- def prepend_around_filter(*filters)
- for filter in filters.flatten
- ensure_filter_responds_to_before_and_after(filter)
- prepend_before_filter { |c| filter.before(c) }
- append_after_filter { |c| filter.after(c) }
+ #
+ # With around filters which yield to the action block, #before and #after
+ # are the code before and after the yield.
+ def prepend_around_filter(*filters, &block)
+ 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
- # 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
-
- # 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
# 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.
def skip_before_filter(*filters)
- if conditions = extract_conditions!(filters)
- 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
+ skip_filter_in_chain(*filters, &:before?)
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
# 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.
def skip_after_filter(*filters)
- if conditions = extract_conditions!(filters)
- 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
+ skip_filter_in_chain(*filters, &:after?)
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.
+ # This method returns the actual filter that was assigned in the controller to maintain existing functionality.
def before_filters #:nodoc:
- @before_filters ||= read_inheritable_attribute("before_filters") || []
+ filter_chain.select(&:before?).map(&:filter)
end
-
+
# 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:
- @after_filters ||= read_inheritable_attribute("after_filters") || []
+ filter_chain.select(&:after?).map(&:filter)
end
-
+
# Returns a mapping between filters and the actions that may run them.
def included_actions #:nodoc:
- @included_actions ||= read_inheritable_attribute("included_actions") || {}
+ filter_chain.inject({}) { |h,f| h[f.filter] = f.included_actions; h }
end
-
+
# Returns a mapping between filters and actions that may not run them.
def excluded_actions #:nodoc:
- @excluded_actions ||= read_inheritable_attribute("excluded_actions") || {}
+ filter_chain.inject({}) { |h,f| h[f.filter] = f.excluded_actions; h }
end
-
- private
- def append_filter_to_chain(condition, filters)
- write_inheritable_array("#{condition}_filters", filters)
+
+ # Find a filter in the filter_chain where the filter method matches the _filter_ param
+ # and (optionally) the passed block evaluates to true (mostly used for testing before?
+ # 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
- def prepend_filter_to_chain(condition, filters)
- old_filters = read_inheritable_attribute("#{condition}_filters") || []
- write_inheritable_attribute("#{condition}_filters", filters + old_filters)
+ def update_conditions(conditions)
+ if conditions[:only]
+ @included_actions = [conditions[:only]].flatten.map(&:to_s)
+ else
+ @excluded_actions = [conditions[:except]].flatten.compact.map(&:to_s)
+ end
+ build_excluded_actions_hash
end
- def ensure_filter_responds_to_before_and_after(filter)
- unless filter.respond_to?(:before) && filter.respond_to?(:after)
- raise ActionControllerError, "Filter object must respond to both before and after"
+ def remove_actions_from_included_actions!(*actions)
+ if @included_actions
+ actions.flatten.map(&:to_s).each { |action| @included_actions.delete(action) }
+ build_excluded_actions_hash
end
end
- def extract_conditions!(filters)
- return nil unless filters.last.is_a? Hash
- filters.pop
+ def before?
+ false
+ end
+
+ def after?
+ false
end
- def add_action_conditions(filters, conditions)
- return unless conditions
- 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
+ def excluded_from?(action)
+ @excluded_actions_hash[action]
end
- def condition_hash(filters, *actions)
- filters.inject({}) {|hash, filter| hash.merge(filter => actions.flatten.map {|action| action.to_s})}
+ def call(controller, &block)
+ raise(ActionControllerError, 'No filter type: Nothing to do here.')
+ end
+
+ private
+ def build_excluded_actions_hash
+ @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 remove_contradicting_conditions!(filters, conditions)
- return unless conditions[:only]
- filters.each do |filter|
- next unless included_actions_for_filter = (read_inheritable_attribute('included_actions') || {})[filter]
- [*conditions[:only]].each do |conditional_action|
- conditional_action = conditional_action.to_s
- included_actions_for_filter.delete(conditional_action) if included_actions_for_filter.include?(conditional_action)
+
+ 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
+
+ # 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
module InstanceMethods # :nodoc:
@@ -357,14 +609,10 @@ module ActionController #:nodoc:
end
def perform_action_with_filters
- before_action_result = before_action
-
- unless before_action_result == false || performed?
- perform_action_without_filters
- after_action
- end
-
- @before_filter_chain_aborted = (before_action_result == false)
+ #result = perform_filters do
+ # perform_action_without_filters unless performed?
+ #end
+ @before_filter_chain_aborted = (call_filter(self.class.filter_chain, 0) == false)
end
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)
end
- # Calls all the defined before-filter filters, which are added by using "before_filter :method".
- # If any of the filters return false, no more filters will be executed and the action is aborted.
- def before_action #:doc:
- call_filters(self.class.before_filters)
+ def filter_chain
+ self.class.filter_chain
end
- # Calls all the defined after-filter filters, which are added by using "after_filter :method".
- # If any of the filters return false, no more filters will be executed.
- def after_action #:doc:
- call_filters(self.class.after_filters)
- end
-
- 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
+ def call_filter(chain, index)
+ return (performed? || perform_action_without_filters) if index >= chain.size
+ filter = chain[index]
+ return call_filter(chain, index.next) if filter.excluded_from?(action_name)
- 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')
+ 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
- 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)
+ 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
def process_cleanup_with_filters
if @before_filter_chain_aborted
close_session
diff --git a/actionpack/test/active_record_unit.rb b/actionpack/test/active_record_unit.rb
index 2555eb7c1a..02cf3f707d 100644
--- a/actionpack/test/active_record_unit.rb
+++ b/actionpack/test/active_record_unit.rb
@@ -106,6 +106,7 @@ class ActiveRecordTestCase < Test::Unit::TestCase
private
# If things go wrong, we don't want to run our test cases. We'll just define them to test nothing.
def abort_tests
+ $stderr.puts 'No Active Record connection, aborting tests.'
self.class.public_instance_methods.grep(/^test./).each do |method|
self.class.class_eval { define_method(method.to_sym){} }
end
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
index d92143e41d..a8bb11f7eb 100644
--- a/actionpack/test/controller/filters_test.rb
+++ b/actionpack/test/controller/filters_test.rb
@@ -198,15 +198,6 @@ class FilterTest < Test::Unit::TestCase
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
around_filter AroundFilter.new
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 nil, test_process(BeforeAndAfterConditionController, "show_without_filter").template.assigns["ran_filter"]
end
-
+
def test_bad_filter
- assert_raises(ActionController::ActionControllerError) {
- test_process(BadFilterController)
- }
+ bad_filter_controller = Class.new(ActionController::Base)
+ assert_raises(ActionController::ActionControllerError) do
+ bad_filter_controller.before_filter 2
+ end
end
-
+
def test_around_filter
controller = test_process(AroundFilterController)
assert controller.template.assigns["before_ran"]
assert controller.template.assigns["after_ran"]
end
-
+
def test_having_properties_in_around_filter
controller = test_process(AroundFilterController)
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)
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