From db045dbbf60b53dbe013ef25554fd013baf88134 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 24 Nov 2004 01:04:44 +0000 Subject: Initial git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionpack/lib/action_controller/filters.rb | 279 ++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 actionpack/lib/action_controller/filters.rb (limited to 'actionpack/lib/action_controller/filters.rb') diff --git a/actionpack/lib/action_controller/filters.rb b/actionpack/lib/action_controller/filters.rb new file mode 100644 index 0000000000..bd5c545dfb --- /dev/null +++ b/actionpack/lib/action_controller/filters.rb @@ -0,0 +1,279 @@ +module ActionController #:nodoc: + module Filters #:nodoc: + def self.append_features(base) + super + base.extend(ClassMethods) + base.class_eval { 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 before_filter + # to halt the processing before the intended action is processed by returning false. 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 + # + # Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without + # affecting the superclass. For example: + # + # class BankController < ActionController::Base + # before_filter :audit + # + # private + # def audit + # # record the action and parameters in an audit log + # end + # end + # + # class VaultController < BankController + # before_filter :verify_credentials + # + # private + # def verify_credentials + # # make sure the user is allowed into the vault + # end + # 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 + # verify_credentials and the intended action is never called. + # + # == Filter types + # + # A filter can take one of three forms: method reference (symbol), external class, or inline method (proc). The first + # is the most common and works by referencing a protected or private method somewhere in the inheritance hierarchy of + # the controller by use of a symbol. In the bank example above, both BankController and VaultController use this form. + # + # Using an external class makes for more easily reused generic filters, such as output compression. External filter classes + # are implemented by having a static +filter+ method on any class and then passing this class to the filter method. Example: + # + # class OutputCompressionFilter + # def self.filter(controller) + # controller.response.body = compress(controller.response.body) + # end + # end + # + # class NewspaperController < ActionController::Base + # after_filter OutputCompressionFilter + # end + # + # 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. + # Or just as a quick test. It works like this: + # + # class WeblogController < ActionController::Base + # before_filter { |controller| return false if controller.params["stop_action"] } + # end + # + # As you can see, the block expects to be passed the controller after it has assigned the request to the internal variables. + # This means that the block has access to both the request and response objects complete with convenience methods for params, + # session, template, and assigns. Note: The inline method doesn't strictly has 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). + # + # == Filter chain ordering + # + # Using before_filter and after_filter appends the specified filters to the existing chain. That's usually + # just fine, but some times you care more about the order in which the filters are executed. When that's the case, you + # can use prepend_before_filter and prepend_after_filter. Filters added by these methods will be put at the + # beginning of their respective chain and executed before the rest. For example: + # + # class ShoppingController + # before_filter :verify_open_shop + # + # class CheckoutController + # prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock + # + # The filter chain for the CheckoutController is now :ensure_items_in_cart, :ensure_items_in_stock, + # :verify_open_shop. 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. + # If a block is given, it is treated as the last argument. + # + # == 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 usefuly 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 + # end + # # After this action has been performed, BenchmarkingFilter#after(controller) is executed + # end + # + # class BenchmarkingFilter + # def initialize + # @runtime + # end + # + # def before + # start_timer + # end + # + # def after + # stop_timer + # report_result + # end + # end + module ClassMethods + # The passed filters will be appended to the array of filters that's run _before_ actions + # on this controller are performed. + def append_before_filter(*filters, &block) + filters << block if block_given? + append_filter_to_chain("before", filters) + end + + # The passed filters will be prepended to the array of filters that's run _before_ actions + # on this controller are performed. + def prepend_before_filter(*filters, &block) + filters << block if block_given? + prepend_filter_to_chain("before", filters) + end + + # Short-hand for append_before_filter since that's the most common of the two. + alias :before_filter :append_before_filter + + # The passed filters will be appended to the array of filters that's run _after_ actions + # on this controller are performed. + def append_after_filter(*filters, &block) + filters << block if block_given? + append_filter_to_chain("after", filters) + end + + # The passed filters will be prepended to the array of filters that's run _after_ actions + # on this controller are performed. + def prepend_after_filter(*filters, &block) + filters << block if block_given? + prepend_filter_to_chain("after", filters) + end + + # Short-hand for append_after_filter since that's the most common of the two. + alias :after_filter :append_after_filter + + # The passed filters 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: + # + # B#before + # A#before + # A#after + # B#after + def append_around_filter(filters) + for filter in [filters].flatten + ensure_filter_responds_to_before_and_after(filter) + append_before_filter { |c| filter.before(c) } + prepend_after_filter { |c| filter.after(c) } + end + end + + # The passed filters 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: + # + # A#before + # B#before + # 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) } + end + end + + # Short-hand for append_around_filter since that's the most common of the two. + alias :around_filter :append_around_filter + + # Returns all the before filters for this class and all its ancestors. + def before_filters #:nodoc: + read_inheritable_attribute("before_filters") + end + + # Returns all the after filters for this class and all its ancestors. + def after_filters #:nodoc: + read_inheritable_attribute("after_filters") + end + + private + def append_filter_to_chain(condition, filters) + write_inheritable_array("#{condition}_filters", filters) + end + + def prepend_filter_to_chain(condition, filters) + write_inheritable_attribute("#{condition}_filters", filters + read_inheritable_attribute("#{condition}_filters")) + 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" + end + end + end + + module InstanceMethods # :nodoc: + def self.append_features(base) + super + base.class_eval { + alias_method :perform_action_without_filters, :perform_action + alias_method :perform_action, :perform_action_with_filters + } + end + + def perform_action_with_filters + return if before_action == false + perform_action_without_filters + after_action + 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) + 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| + if Symbol === filter + if self.send(filter) == false then return false end + elsif filter_block?(filter) + if filter.call(self) == false then return false end + elsif filter_class?(filter) + if filter.filter(self) == false then return false end + else + raise( + ActionControllerError, + "Filters need to be either a symbol, proc/method, or class implementing a static filter method" + ) + 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 + end + end +end -- cgit v1.2.3