filters.rb 14.7 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2 3 4 5
module ActionController #:nodoc:
  module Filters #:nodoc:
    def self.append_features(base)
      super
      base.extend(ClassMethods)
6
      base.send(:include, ActionController::Filters::InstanceMethods)
D
Initial  
David Heinemeier Hansson 已提交
7 8 9 10 11 12 13 14
    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>
15 16 17
    # 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.
D
Initial  
David Heinemeier Hansson 已提交
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
    #
    # == 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
D
David Heinemeier Hansson 已提交
72
    #     before_filter { |controller| false if controller.params["stop_action"] }
D
Initial  
David Heinemeier Hansson 已提交
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
    #   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 <tt>before_filter</tt> and <tt>after_filter</tt> 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 <tt>prepend_before_filter</tt> and <tt>prepend_after_filter</tt>. 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 <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 
    # 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
103
    # both the before and after call. That's especially useful when you need to keep state active between the before and after,
D
Initial  
David Heinemeier Hansson 已提交
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
    # 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
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
    #
    # == 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:
    #
    #   class Journal < ActionController::Base
    #     # only require authentication if the current action is edit or delete
    #     before_filter :authorize, :only => [ :edit, :delete ]
    #    
    #     private
    #       def authorize
    #         # redirect to login unless authenticated
    #       end
    #   end
    # 
    # When setting conditions on inline method (proc) filters the condition must come first and be placed in parenthesis.
    #
    #   class UserPreferences < ActionController::Base
    #     before_filter(:except => :new) { # some proc ... }
    #     # ...
    #   end
    #
D
Initial  
David Heinemeier Hansson 已提交
153 154 155 156
    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.
      def append_before_filter(*filters, &block)
157
        conditions = extract_conditions!(filters)
158
        filters << block if block_given?
159
        add_action_conditions(filters, conditions)
160
        append_filter_to_chain("before", filters)
D
Initial  
David Heinemeier Hansson 已提交
161 162 163 164 165
      end

      # The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions
      # on this controller are performed.
      def prepend_before_filter(*filters, &block)
166
        conditions = extract_conditions!(filters) 
167
        filters << block if block_given?
168
        add_action_conditions(filters, conditions)
169
        prepend_filter_to_chain("before", filters)
D
Initial  
David Heinemeier Hansson 已提交
170 171 172 173 174 175 176 177
      end

      # Short-hand for append_before_filter since that's the most common of the two.
      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.
      def append_after_filter(*filters, &block)
178
        conditions = extract_conditions!(filters) 
179
        filters << block if block_given?
180
        add_action_conditions(filters, conditions)
181
        append_filter_to_chain("after", filters)
D
Initial  
David Heinemeier Hansson 已提交
182 183 184 185 186
      end

      # The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions
      # on this controller are performed.
      def prepend_after_filter(*filters, &block)
187
        conditions = extract_conditions!(filters) 
188
        filters << block if block_given?
189
        add_action_conditions(filters, conditions)
190
        prepend_filter_to_chain("after", filters)
D
Initial  
David Heinemeier Hansson 已提交
191 192 193 194 195 196 197 198 199 200 201 202 203
      end

      # Short-hand for append_after_filter since that's the most common of the two.
      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:
      #
      #   B#before
      #     A#before
      #     A#after
      #   B#after
204
      def append_around_filter(*filters)
205
        conditions = extract_conditions!(filters) 
206
        for filter in filters.flatten
D
Initial  
David Heinemeier Hansson 已提交
207
          ensure_filter_responds_to_before_and_after(filter)
208 209
          append_before_filter(conditions || {}) { |c| filter.before(c) }
          prepend_after_filter(conditions || {}) { |c| filter.after(c) }
D
Initial  
David Heinemeier Hansson 已提交
210 211 212 213 214 215 216 217 218 219 220
        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:
      #
      #   A#before
      #     B#before
      #     B#after
      #   A#after
221 222
      def prepend_around_filter(*filters)
        for filter in filters.flatten
D
Initial  
David Heinemeier Hansson 已提交
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
          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
      
242 243 244 245 246 247 248 249 250 251
      # Returns a mapping between filters and the actions that may run them.
      def included_actions #:nodoc:
        read_inheritable_attribute("included_actions") || {}
      end
      
      # Returns a mapping between filters and actions that may not run them.
      def excluded_actions #:nodoc:
        read_inheritable_attribute("excluded_actions") || {}
      end
      
D
Initial  
David Heinemeier Hansson 已提交
252 253 254 255 256 257 258 259 260 261 262 263 264 265
      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
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281

        def extract_conditions!(filters)
          return nil unless filters.last.is_a? Hash
          filters.pop
        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
        end

        def condition_hash(filters, *actions)
          filters.inject({}) {|hash, filter| hash.merge(filter => actions.flatten.map {|action| action.to_s})}
        end
D
Initial  
David Heinemeier Hansson 已提交
282 283 284 285 286 287 288 289 290 291 292 293
    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
294
        return if before_action == false || performed?
D
Initial  
David Heinemeier Hansson 已提交
295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
        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| 
314 315 316 317 318 319 320 321 322 323 324 325 326
            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"
                )
D
Initial  
David Heinemeier Hansson 已提交
327
            end
328
            return false if filter_result == false
D
Initial  
David Heinemeier Hansson 已提交
329 330 331 332 333 334 335 336 337 338
          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
339 340 341

        def action_exempted?(filter)
          case
342 343 344 345
            when ia = self.class.included_actions[filter]
              !ia.include?(action_name)
            when ea = self.class.excluded_actions[filter] 
              ea.include?(action_name)
346 347
          end
        end
D
Initial  
David Heinemeier Hansson 已提交
348 349 350
    end
  end
end