Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Changeset 5163

Show
Ignore:
Timestamp:
09/22/06 03:41:03 (3 years ago)
Author:
bitsweat
Message:

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

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/actionpack/CHANGELOG

    r5155 r5163  
    11*SVN* 
     2 
     3* Filters overhaul including meantime filter support using around filters + blocks.  #5949 [Martin Emde, Roman Le Negrate, Stefan Kaes, Jeremy Kemper] 
    24 
    35* Update RJS render tests. [sam] 
  • trunk/actionpack/lib/action_controller/filters.rb

    r4312 r5163  
    66    end 
    77 
    8     # Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do  
    9     # authentication, caching, or auditing before the intended action is performed. Or to do localization or output  
    10     # compression after the action has been performed. 
    11     # 
    12     # Filters have access to the request, response, and all the instance variables set by other filters in the chain 
    13     # or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <tt>before_filter</tt> 
    14     # to halt the processing before the intended action is processed by returning false or performing a redirect or render.  
    15     # This is especially useful for filters like authentication where you're not interested in allowing the action to be  
    16     # performed if the proper credentials are not in order. 
     8    # Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do 
     9    # authentication, caching, or auditing before the intended action is performed. Or to do localization or output 
     10    # compression after the action has been performed. Filters have access to the request, response, and all the instance 
     11    # variables set by other filters in the chain or by the action (in the case of after filters). 
    1712    # 
    1813    # == Filter inheritance 
    1914    # 
    20     # Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without 
     15    # Controller inheritance hierarchies share filters downwards, but subclasses can also add or skip filters without 
    2116    # affecting the superclass. For example: 
    2217    # 
     
    4035    # 
    4136    # Now any actions performed on the BankController will have the audit method called before. On the VaultController, 
    42     # first the audit method is called, then the verify_credentials method. If the audit method returns false, then  
     37    # first the audit method is called, then the verify_credentials method. If the audit method returns false, then 
    4338    # verify_credentials and the intended action are never called. 
    4439    # 
     
    6560    # manipulate them as it sees fit. 
    6661    # 
    67     # The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation.  
     62    # The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation. 
    6863    # Or just as a quick test. It works like this: 
    6964    # 
     
    7772    # and returns 1 or -1 on arity will do (such as a Proc or an Method object). 
    7873    # 
     74    # Please note that around_filters function a little differently than the normal before and after filters with regard to filter 
     75    # types. Please see the section dedicated to around_filters below. 
     76    # 
    7977    # == Filter chain ordering 
    8078    # 
     
    9189    # 
    9290    # The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt> 
    93     # <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop  
     91    # <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop 
    9492    # is open or not. 
    9593    # 
     
    9997    # == Around filters 
    10098    # 
    101     # In addition to the individual before and after filters, it's also possible to specify that a single object should handle 
    102     # both the before and after call. That's especially useful when you need to keep state active between the before and after, 
    103     # such as the example of a benchmark filter below: 
    104     #  
    105     #   class WeblogController < ActionController::Base 
    106     #     around_filter BenchmarkingFilter.new 
    107     #      
    108     #     # Before this action is performed, BenchmarkingFilter#before(controller) is executed 
    109     #     def index 
     99    # Around filters wrap an action, executing code both before and after. 
     100    # They may be declared as method references, blocks, or objects responding 
     101    # to #filter or to both #before and #after. 
     102    # 
     103    # To use a method as an around_filter, pass a symbol naming the Ruby method. 
     104    # Yield (or block.call) within the method to run the action. 
     105    # 
     106    #   around_filter :catch_exceptions 
     107    # 
     108    #   private 
     109    #     def catch_exceptions 
     110    #       yield 
     111    #     rescue => exception 
     112    #       logger.debug "Caught exception! #{exception}" 
     113    #       raise 
    110114    #     end 
    111     #     # After this action has been performed, BenchmarkingFilter#after(controller) is executed 
    112     #   end 
     115    # 
     116    # To use a block as an around_filter, pass a block taking as args both 
     117    # the controller and the action block. You can't call yield directly from 
     118    # an around_filter block; explicitly call the action block instead: 
     119    # 
     120    #   around_filter do |controller, action| 
     121    #     logger.debug "before #{controller.action_name}" 
     122    #     action.call 
     123    #     logger.debug "after #{controller.action_name}" 
     124    #   end 
     125    # 
     126    # To use a filter object with around_filter, pass an object responding 
     127    # to :filter or both :before and :after. With a filter method, yield to 
     128    # the block as above: 
     129    # 
     130    #   around_filter BenchmarkingFilter 
    113131    # 
    114132    #   class BenchmarkingFilter 
    115     #     def initialize 
    116     #       @runtime 
     133    #     def self.filter(controller, &block) 
     134    #       Benchmark.measure(&block) 
    117135    #     end 
    118     #      
    119     #     def before 
    120     #       start_timer 
     136    #   end 
     137    # 
     138    # With before and after methods: 
     139    # 
     140    #   around_filter Authorizer.new 
     141    # 
     142    #   class Authorizer 
     143    #     # This will run before the action. Returning false aborts the action. 
     144    #     def before(controller) 
     145    #       if user.authorized? 
     146    #         return true 
     147    #       else 
     148    #         redirect_to login_url 
     149    #         return false 
     150    #       end 
    121151    #     end 
    122     #      
    123     #     def after 
    124     #       stop_timer 
    125     #       report_result 
     152    # 
     153    #     # This will run after the action if and only if before returned true. 
     154    #     def after(controller) 
    126155    #     end 
    127156    #   end 
    128157    # 
     158    # If the filter has before and after methods, the before method will be 
     159    # called before the action. If before returns false, the filter chain is 
     160    # halted and after will not be run. See Filter Chain Halting below for 
     161    # an example. 
     162    # 
    129163    # == Filter chain skipping 
    130164    # 
    131     # Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the  
    132     # subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters 
    133     # they would like to be relieved of. Examples 
     165    # Declaring a filter on a base class conveniently applies to its subclasses, 
     166    # but sometimes a subclass should skip some of its superclass' filters: 
    134167    # 
    135168    #   class ApplicationController < ActionController::Base 
    136169    #     before_filter :authenticate 
     170    #     around_filter :catch_exceptions 
    137171    #   end 
    138172    # 
    139173    #   class WeblogController < ApplicationController 
    140     #     # will run the :authenticate filter 
     174    #     # Will run the :authenticate and :catch_exceptions filters. 
    141175    #   end 
    142176    # 
    143177    #   class SignupController < ApplicationController 
    144     #     # will not run the :authenticate filter 
     178    #     # Skip :authenticate, run :catch_exceptions. 
    145179    #     skip_before_filter :authenticate 
    146180    #   end 
    147181    # 
     182    #   class ProjectsController < ApplicationController 
     183    #     # Skip :catch_exceptions, run :authenticate. 
     184    #     skip_filter :catch_exceptions 
     185    #   end 
     186    # 
     187    #   class ClientsController < ApplicationController 
     188    #     # Skip :catch_exceptions and :authenticate unless action is index. 
     189    #     skip_filter :catch_exceptions, :authenticate, :except => :index 
     190    #   end 
     191    # 
    148192    # == Filter conditions 
    149193    # 
    150     # Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to 
    151     # exclude or the actions to include when executing the filter. Available conditions are +:only+ or +:except+, both  
    152     # of which accept an arbitrary number of method references. For example: 
     194    # Filters may be limited to specific actions by declaring the actions to 
     195    # include or exclude. Both options accept single actions (:only => :index) 
     196    # or arrays of actions (:except => [:foo, :bar]). 
    153197    # 
    154198    #   class Journal < ActionController::Base 
    155     #     # only require authentication if the current action is edit or delete 
    156     #     before_filter :authorize, :only => [ :edit, :delete ] 
    157     #     
     199    #     # Require authentication for edit and delete. 
     200    #     before_filter :authorize, :only => [:edit, :delete] 
     201    # 
     202    #     # Passing options to a filter with a block. 
     203    #     around_filter(:except => :index) do |controller, action_block| 
     204    #       results = Profiler.run(&action_block) 
     205    #       controller.response.sub! "</body>", "#{results}</body>" 
     206    #     end 
     207    # 
    158208    #     private 
    159209    #       def authorize 
    160     #         # redirect to login unless authenticated 
     210    #         # Redirect to login unless authenticated. 
    161211    #       end 
    162212    #   end 
    163     #  
    164     # When setting conditions on inline method (proc) filters the condition must come first and be placed in parentheses. 
    165     # 
    166     #   class UserPreferences < ActionController::Base 
    167     #     before_filter(:except => :new) { # some proc ... } 
    168     #     # ... 
    169     #   end 
    170     # 
     213    # 
     214    # == Filter Chain Halting 
     215    # 
     216    # <tt>before_filter</tt> and <tt>around_filter</tt> may halt the request 
     217    # before controller action is run. This is useful, for example, to deny 
     218    # access to unauthenticated users or to redirect from http to https. 
     219    # Simply return false from the filter or call render or redirect. 
     220    # 
     221    # Around filters halt the request unless the action block is called. 
     222    # Given these filters 
     223    #   after_filter :after 
     224    #   around_filter :around 
     225    #   before_filter :before 
     226    # 
     227    # The filter chain will look like: 
     228    # 
     229    #   ... 
     230    #   . \ 
     231    #   .  #around (code before yield) 
     232    #   .  .  \ 
     233    #   .  .  #before (actual filter code is run) 
     234    #   .  .  .  \ 
     235    #   .  .  .  execute controller action 
     236    #   .  .  .  / 
     237    #   .  .  ... 
     238    #   .  .  / 
     239    #   .  #around (code after yield) 
     240    #   . / 
     241    #   #after (actual filter code is run) 
     242    # 
     243    # If #around returns before yielding, only #after will be run. The #before 
     244    # filter and controller action will not be run.  If #before returns false, 
     245    # the second half of #around and all of #after will still run but the 
     246    # action will not. 
    171247    module ClassMethods 
    172       # The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions 
    173       # on this controller are performed. 
     248      # The passed <tt>filters</tt> will be appended to the filter_chain and 
     249      # will execute before the action on this controller is performed. 
    174250      def append_before_filter(*filters, &block) 
    175         conditions = extract_conditions!(filters) 
    176         filters << block if block_given? 
    177         add_action_conditions(filters, conditions) 
    178         append_filter_to_chain('before', filters) 
    179       end 
    180  
    181       # The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions 
    182       # on this controller are performed. 
     251        append_filter_to_chain(filters, :before, &block) 
     252      end 
     253 
     254      # The passed <tt>filters</tt> will be prepended to the filter_chain and 
     255      # will execute before the action on this controller is performed. 
    183256      def prepend_before_filter(*filters, &block) 
    184         conditions = extract_conditions!(filters)  
    185         filters << block if block_given? 
    186         add_action_conditions(filters, conditions) 
    187         prepend_filter_to_chain('before', filters) 
    188       end 
    189  
    190       # Short-hand for append_before_filter since that's the most common of the two. 
     257        prepend_filter_to_chain(filters, :before, &block) 
     258      end 
     259 
     260      # Shorthand for append_before_filter since it's the most common. 
    191261      alias :before_filter :append_before_filter 
    192        
    193       # The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions 
    194       # on this controller are performed. 
     262 
     263      # The passed <tt>filters</tt> will be appended to the array of filters 
     264      # that run _after_ actions on this controller are performed. 
    195265      def append_after_filter(*filters, &block) 
    196         conditions = extract_conditions!(filters)  
    197         filters << block if block_given? 
    198         add_action_conditions(filters, conditions) 
    199         append_filter_to_chain('after', filters) 
    200       end 
    201  
    202       # The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions 
    203       # on this controller are performed. 
     266        prepend_filter_to_chain(filters, :after, &block) 
     267      end 
     268 
     269      # The passed <tt>filters</tt> will be prepended to the array of filters 
     270      # that run _after_ actions on this controller are performed. 
    204271      def prepend_after_filter(*filters, &block) 
    205         conditions = extract_conditions!(filters)  
    206         filters << block if block_given? 
    207         add_action_conditions(filters, conditions) 
    208         prepend_filter_to_chain("after", filters) 
    209       end 
    210  
    211       # Short-hand for append_after_filter since that's the most common of the two. 
     272        append_filter_to_chain(filters, :after, &block) 
     273      end 
     274 
     275      # Shorthand for append_after_filter since it's the most common. 
    212276      alias :after_filter :append_after_filter 
    213        
    214       # The passed <tt>filters</tt> will have their +before+ method appended to the array of filters that's run both before actions 
    215       # on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all  
    216       # respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like: 
     277 
     278 
     279      # If you append_around_filter A.new, B.new, the filter chain looks like 
    217280      # 
    218281      #   B#before 
    219282      #     A#before 
     283      #       # run the action 
    220284      #     A#after 
    221285      #   B#after 
    222       def append_around_filter(*filters) 
    223         conditions = extract_conditions!(filters)  
    224         for filter in filters.flatten 
    225           ensure_filter_responds_to_before_and_after(filter) 
    226           append_before_filter(conditions || {}) { |c| filter.before(c) } 
    227           prepend_after_filter(conditions || {}) { |c| filter.after(c) } 
    228         end 
    229       end         
    230  
    231       # The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions 
    232       # on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all  
    233       # respond to both +before+ and +after+. So if you do prepend_around_filter A.new, B.new, the callstack will look like: 
     286      # 
     287      # With around filters which yield to the action block, #before and #after 
     288      # are the code before and after the yield. 
     289      def append_around_filter(*filters, &block) 
     290        filters, conditions = extract_conditions(filters, &block) 
     291        filters.map { |f| proxy_before_and_after_filter(f) }.each do |filter| 
     292          append_filter_to_chain([filter, conditions]) 
     293        end 
     294      end 
     295 
     296      # If you prepend_around_filter A.new, B.new, the filter chain looks like: 
    234297      # 
    235298      #   A#before 
    236299      #     B#before 
     300      #       # run the action 
    237301      #     B#after 
    238302      #   A#after 
    239       def prepend_around_filter(*filters) 
    240         for filter in filters.flatten 
    241           ensure_filter_responds_to_before_and_after(filter) 
    242           prepend_before_filter { |c| filter.before(c) } 
    243           append_after_filter   { |c| filter.after(c) } 
    244         end 
    245       end      
    246  
    247       # Short-hand for append_around_filter since that's the most common of the two. 
     303      # 
     304      # With around filters which yield to the action block, #before and #after 
     305      # are the code before and after the yield. 
     306      def prepend_around_filter(*filters, &block) 
     307        filters, conditions = extract_conditions(filters, &block) 
     308        filters.map { |f| proxy_before_and_after_filter(f) }.each do |filter| 
     309          prepend_filter_to_chain([filter, conditions]) 
     310        end 
     311      end 
     312 
     313      # Shorthand for append_around_filter since it's the most common. 
    248314      alias :around_filter :append_around_filter 
    249        
    250       # Removes the specified filters from the +before+ filter chain. Note that this only works for skipping method-reference  
     315 
     316      # Removes the specified filters from the +before+ filter chain. Note that this only works for skipping method-reference 
    251317      # filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out 
    252318      # of many sub-controllers need a different hierarchy. 
    253319      # 
    254       # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,  
     320      # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options, 
    255321      # just like when you apply the filters. 
    256322      def skip_before_filter(*filters) 
    257         if conditions = extract_conditions!(filters) 
    258           remove_contradicting_conditions!(filters, conditions) 
    259           conditions[:only], conditions[:except] = conditions[:except], conditions[:only] 
    260           add_action_conditions(filters, conditions) 
    261         else 
    262           for filter in filters.flatten 
    263             write_inheritable_attribute("before_filters", read_inheritable_attribute("before_filters") - [ filter ]) 
    264           end 
    265         end 
    266       end 
    267  
    268       # Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference  
     323        skip_filter_in_chain(*filters, &:before?) 
     324      end 
     325 
     326      # Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference 
    269327      # filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out 
    270328      # of many sub-controllers need a different hierarchy. 
    271329      # 
    272       # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,  
     330      # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options, 
    273331      # just like when you apply the filters. 
    274332      def skip_after_filter(*filters) 
    275         if conditions = extract_conditions!(filters) 
    276           remove_contradicting_conditions!(filters, conditions) 
    277           conditions[:only], conditions[:except] = conditions[:except], conditions[:only] 
    278           add_action_conditions(filters, conditions) 
    279         else 
    280           for filter in filters.flatten 
    281             write_inheritable_attribute("after_filters", read_inheritable_attribute("after_filters") - [ filter ]) 
    282           end 
    283         end 
    284       end 
    285        
     333        skip_filter_in_chain(*filters, &:after?) 
     334      end 
     335 
     336      # Removes the specified filters from the filter chain. This only works for method reference (symbol) 
     337      # filters, not procs. This method is different from skip_after_filter and skip_before_filter in that 
     338      # it will match any before, after or yielding around filter. 
     339      # 
     340      # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options, 
     341      # just like when you apply the filters. 
     342      def skip_filter(*filters) 
     343        skip_filter_in_chain(*filters) 
     344      end 
     345 
     346      # Returns an array of Filter objects for this controller. 
     347      def filter_chain 
     348        read_inheritable_attribute("filter_chain") || [] 
     349      end 
     350 
    286351      # Returns all the before filters for this class and all its ancestors. 
     352      # This method returns the actual filter that was assigned in the controller to maintain existing functionality. 
    287353      def before_filters #:nodoc: 
    288         @before_filters ||= read_inheritable_attribute("before_filters") || [] 
    289       end 
    290        
     354        filter_chain.select(&:before?).map(&:filter) 
     355      end 
     356 
    291357      # Returns all the after filters for this class and all its ancestors. 
     358      # This method returns the actual filter that was assigned in the controller to maintain existing functionality. 
    292359      def after_filters #:nodoc: 
    293         @after_filters ||= read_inheritable_attribute("after_filters") || [] 
    294       end 
    295        
     360        filter_chain.select(&:after?).map(&:filter) 
     361      end 
     362 
    296363      # Returns a mapping between filters and the actions that may run them. 
    297364      def included_actions #:nodoc: 
    298         @included_actions ||= read_inheritable_attribute("included_actions") || {
    299       end 
    300        
     365        filter_chain.inject({}) { |h,f| h[f.filter] = f.included_actions; h
     366      end 
     367 
    301368      # Returns a mapping between filters and actions that may not run them. 
    302369      def excluded_actions #:nodoc: 
    303         @excluded_actions ||= read_inheritable_attribute("excluded_actions") || {} 
    304       end 
    305        
    306       private 
    307         def append_filter_to_chain(condition, filters) 
    308           write_inheritable_array("#{condition}_filters", filters) 
    309         end 
    310  
    311         def prepend_filter_to_chain(condition, filters) 
    312           old_filters = read_inheritable_attribute("#{condition}_filters") || [] 
    313           write_inheritable_attribute("#{condition}_filters", filters + old_filters) 
    314         end 
    315  
    316         def ensure_filter_responds_to_before_and_after(filter) 
    317           unless filter.respond_to?(:before) && filter.respond_to?(:after) 
    318             raise ActionControllerError, "Filter object must respond to both before and after" 
    319           end 
    320         end 
    321  
    322         def extract_conditions!(filters) 
    323           return nil unless filters.last.is_a? Hash 
    324           filters.pop 
    325         end 
    326  
    327         def add_action_conditions(filters, conditions) 
    328           return unless conditions 
    329           included, excluded = conditions[:only], conditions[:except] 
    330           write_inheritable_hash('included_actions', condition_hash(filters, included)) && return if included 
    331           write_inheritable_hash('excluded_actions', condition_hash(filters, excluded)) if excluded 
    332         end 
    333  
    334         def condition_hash(filters, *actions) 
    335           filters.inject({}) {|hash, filter| hash.merge(filter => actions.flatten.map {|action| action.to_s})} 
    336         end 
    337          
    338         def remove_contradicting_conditions!(filters, conditions) 
    339           return unless conditions[:only] 
    340           filters.each do |filter| 
    341             next unless included_actions_for_filter = (read_inheritable_attribute('included_actions') || {})[filter] 
    342             [*conditions[:only]].each do |conditional_action| 
    343               conditional_action = conditional_action.to_s 
    344               included_actions_for_filter.delete(conditional_action) if included_actions_for_filter.include?(conditional_action) 
     370        filter_chain.inject({}) { |h,f| h[f.filter] = f.excluded_actions; h } 
     371      end 
     372 
     373      # Find a filter in the filter_chain where the filter method matches the _filter_ param 
     374      # and (optionally) the passed block evaluates to true (mostly used for testing before? 
     375      # and after? on the filter). Useful for symbol filters. 
     376      # 
     377      # The object of type Filter is passed to the block when yielded, not the filter itself. 
     378      def find_filter(filter, &block) #:nodoc: 
     379        filter_chain.select { |f| f.filter == filter && (!block_given? || yield(f)) }.first 
     380      end 
     381 
     382      # Filter class is an abstract base class for all filters. Handles all of the included/excluded actions but 
     383      # contains no logic for calling the actual filters. 
     384      class Filter #:nodoc: 
     385        attr_reader :filter, :included_actions, :excluded_actions 
     386 
     387        def initialize(filter, options={}) 
     388          @filter = filter 
     389          @position = options[:position] 
     390          update_conditions(options) 
     391        end 
     392 
     393        def update_conditions(conditions) 
     394          if conditions[:only] 
     395            @included_actions = [conditions[:only]].flatten.map(&:to_s) 
     396          else 
     397            @excluded_actions = [conditions[:except]].flatten.compact.map(&:to_s) 
     398          end 
     399          build_excluded_actions_hash 
     400        end 
     401 
     402        def remove_actions_from_included_actions!(*actions) 
     403          if @included_actions 
     404            actions.flatten.map(&:to_s).each { |action| @included_actions.delete(action) } 
     405            build_excluded_actions_hash 
     406          end 
     407        end 
     408 
     409        def before? 
     410          false 
     411        end 
     412 
     413        def after? 
     414          false 
     415        end 
     416 
     417        def excluded_from?(action) 
     418          @excluded_actions_hash[action] 
     419        end 
     420 
     421        def call(controller, &block) 
     422          raise(ActionControllerError, 'No filter type: Nothing to do here.') 
     423        end 
     424 
     425        private 
     426        def build_excluded_actions_hash 
     427          @excluded_actions_hash = 
     428                   if @included_actions 
     429                     @included_actions.inject(Hash.new(true)){|h, a| h[a] = false; h} 
     430                   else 
     431                     @excluded_actions.inject(Hash.new(false)){|h, a| h[a] = true; h} 
     432                   end 
     433        end 
     434      end 
     435 
     436      # Abstract base class for filter proxies. FilterProxy objects are meant to mimic the behaviour of the old 
     437      # before_filter and after_filter by moving the logic into the filter itself. 
     438      class FilterProxy < Filter #:nodoc: 
     439        def filter 
     440          @filter.filter 
     441        end 
     442      end 
     443 
     444      class BeforeFilterProxy < FilterProxy #:nodoc: 
     445        def before? 
     446          true 
     447        end 
     448 
     449        def call(controller, &block) 
     450          if false == @filter.call(controller) # must only stop if equal to false. only filters returning false are halted. 
     451            controller.halt_filter_chain(@filter, :returned_false) 
     452          else 
     453            yield 
     454          end 
     455        end 
     456      end 
     457 
     458      class AfterFilterProxy < FilterProxy #:nodoc: 
     459        def after? 
     460          true 
     461        end 
     462 
     463        def call(controller, &block) 
     464          yield 
     465          @filter.call(controller) 
     466        end 
     467      end 
     468 
     469      class SymbolFilter < Filter #:nodoc: 
     470        def call(controller, &block) 
     471          controller.send(@filter, &block) 
     472        end 
     473      end 
     474 
     475      class ProcFilter < Filter #:nodoc: 
     476        def call(controller) 
     477          @filter.call(controller) 
     478        rescue LocalJumpError # a yield from a proc... no no bad dog. 
     479          raise(ActionControllerError, 'Cannot yield from a Proc type filter. The Proc must take two arguments and execute #call on the second argument.') 
     480        end 
     481      end 
     482 
     483      class ProcWithCallFilter < Filter #:nodoc: 
     484        def call(controller, &block) 
     485          @filter.call(controller, block) 
     486        rescue LocalJumpError # a yield from a proc... no no bad dog. 
     487          raise(ActionControllerError, 'Cannot yield from a Proc type filter. The Proc must take two arguments and execute #call on the second argument.') 
     488        end 
     489      end 
     490 
     491      class MethodFilter < Filter #:nodoc: 
     492        def call(controller, &block) 
     493          @filter.call(controller, &block) 
     494        end 
     495      end 
     496 
     497      class ClassFilter < Filter #:nodoc: 
     498        def call(controller, &block) 
     499          @filter.filter(controller, &block) 
     500        end 
     501      end 
     502 
     503      protected 
     504        def append_filter_to_chain(filters, position = :around, &block) 
     505          write_inheritable_array('filter_chain', create_filters(filters, position, &block) ) 
     506        end 
     507 
     508        def prepend_filter_to_chain(filters, position = :around, &block) 
     509          write_inheritable_attribute('filter_chain', create_filters(filters, position, &block) + filter_chain) 
     510        end 
     511 
     512        def create_filters(filters, position, &block) #:nodoc: 
     513          filters, conditions = extract_conditions(filters, &block) 
     514          conditions.merge!(:position => position) 
     515 
     516          # conditions should be blank for a filter behind a filter proxy since the proxy will take care of all conditions. 
     517          proxy_conditions, conditions = conditions, {} unless :around == position 
     518 
     519          filters.map do |filter| 
     520            # change all the filters into instances of Filter derived classes 
     521            class_for_filter(filter).new(filter, conditions) 
     522          end.collect do |filter| 
     523            # apply proxy to filter if necessary 
     524            case position 
     525            when :before 
     526              BeforeFilterProxy.new(filter, proxy_conditions) 
     527            when :after 
     528              AfterFilterProxy.new(filter, proxy_conditions) 
     529            else 
     530              filter 
    345531            end 
    346532          end 
     533        end 
     534 
     535        # The determination of the filter type was once done at run time. 
     536        # This method is here to extract as much logic from the filter run time as possible 
     537        def class_for_filter(filter) #:nodoc: 
     538          case 
     539          when filter.is_a?(Symbol) 
     540            SymbolFilter 
     541          when filter.respond_to?(:call) 
     542            if filter.is_a?(Method) 
     543              MethodFilter 
     544            elsif filter.arity == 1 
     545              ProcFilter 
     546            else 
     547              ProcWithCallFilter 
     548            end 
     549          when filter.respond_to?(:filter) 
     550            ClassFilter 
     551          else 
     552            raise(ActionControllerError, 'A filters must be a Symbol, Proc, Method, or object responding to filter.') 
     553          end 
     554        end 
     555 
     556        def filter_responds_to_before_and_after(filter) #:nodoc: 
     557          filter.respond_to?(:before) && filter.respond_to?(:after) 
     558        end 
     559 
     560        def proxy_before_and_after_filter(filter) #:nodoc: 
     561          return filter unless filter_responds_to_before_and_after(filter) 
     562          Proc.new do |controller, action| 
     563            unless filter.before(controller) == false 
     564              begin 
     565                action.call 
     566              ensure 
     567                filter.after(controller) 
     568              end 
     569            end 
     570          end 
     571        end 
     572 
     573        def extract_conditions(*filters, &block) #:nodoc: 
     574          filters.flatten! 
     575          conditions = filters.last.is_a?(Hash) ? filters.pop : {} 
     576          filters << block if block_given? 
     577          return filters.flatten, conditions 
     578        end 
     579 
     580        def skip_filter_in_chain(*filters, &test) #:nodoc: 
     581          filters, conditions = extract_conditions(filters) 
     582          finder_proc = block_given? ? 
     583            proc { |f| find_filter(f, &test) } : 
     584            proc { |f| find_filter(f) } 
     585 
     586          filters.map(&finder_proc).reject(&:nil?).each do |filter| 
     587            if conditions.empty? 
     588              delete_filter_in_chain(filter) 
     589            else 
     590              filter.remove_actions_from_included_actions!(conditions[:only] || []) 
     591              conditions[:only], conditions[:except] = conditions[:except], conditions[:only] 
     592              filter.update_conditions(conditions) 
     593            end 
     594          end 
     595        end 
     596 
     597        def delete_filter_in_chain(filter) #:nodoc: 
     598          write_inheritable_attribute('filter_chain', filter_chain.reject { |f| f == filter }) 
    347599        end 
    348600    end 
     
    358610 
    359611      def perform_action_with_filters 
    360         before_action_result = before_action 
    361  
    362         unless before_action_result == false || performed? 
    363           perform_action_without_filters 
    364           after_action 
    365         end 
    366  
    367         @before_filter_chain_aborted = (before_action_result == false) 
     612        #result = perform_filters do 
     613        #  perform_action_without_filters unless performed? 
     614        #end 
     615        @before_filter_chain_aborted = (call_filter(self.class.filter_chain, 0) == false) 
    368616      end 
    369617 
     
    373621      end 
    374622 
    375       # Calls all the defined before-filter filters, which are added by using "before_filter :method". 
    376       # If any of the filters return false, no more filters will be executed and the action is aborted. 
    377       def before_action #:doc: 
    378         call_filters(self.class.before_filters) 
    379       end 
    380  
    381       # Calls all the defined after-filter filters, which are added by using "after_filter :method". 
    382       # If any of the filters return false, no more filters will be executed. 
    383       def after_action #:doc: 
    384         call_filters(self.class.after_filters) 
    385       end 
    386        
     623      def filter_chain 
     624        self.class.filter_chain 
     625      end 
     626 
     627      def call_filter(chain, index) 
     628        return (performed? || perform_action_without_filters) if index >= chain.size 
     629        filter = chain[index] 
     630        return call_filter(chain, index.next) if filter.excluded_from?(action_name) 
     631 
     632        called = false 
     633        filter.call(self) do 
     634          call_filter(chain, index.next) 
     635          called = true 
     636        end 
     637        halt_filter_chain(filter.filter, :no_yield) if called == false 
     638        called 
     639      end 
     640 
     641      def halt_filter_chain(filter, reason) 
     642        if logger 
     643          case reason 
     644          when :no_yield 
     645            logger.info "Filter chain halted as [#{filter.inspect}] did not yield." 
     646          when :returned_false 
     647            logger.info "Filter chain halted as [#{filter.inspect}] returned false." 
     648          end 
     649        end 
     650        return false 
     651      end 
     652 
    387653      private 
    388         def call_filters(filters) 
    389           filters.each do |filter|  
    390             next if action_exempted?(filter) 
    391  
    392             filter_result = case 
    393               when filter.is_a?(Symbol) 
    394                 self.send(filter) 
    395               when filter_block?(filter) 
    396                 filter.call(self) 
    397               when filter_class?(filter) 
    398                 filter.filter(self) 
    399               else 
    400                 raise( 
    401                   ActionControllerError,  
    402                   'Filters need to be either a symbol, proc/method, or class implementing a static filter method' 
    403                 ) 
    404             end 
    405  
    406             if filter_result == false 
    407               logger.info "Filter chain halted as [#{filter}] returned false" if logger 
    408               return false  
    409             end 
    410           end 
    411         end 
    412          
    413         def filter_block?(filter) 
    414           filter.respond_to?('call') && (filter.arity == 1 || filter.arity == -1) 
    415         end 
    416          
    417         def filter_class?(filter) 
    418           filter.respond_to?('filter') 
    419         end 
    420  
    421         def action_exempted?(filter) 
    422           case 
    423             when ia = self.class.included_actions[filter] 
    424               !ia.include?(action_name) 
    425             when ea = self.class.excluded_actions[filter]  
    426               ea.include?(action_name) 
    427           end 
    428         end 
    429  
    430654        def process_cleanup_with_filters 
    431655          if @before_filter_chain_aborted 
  • trunk/actionpack/test/active_record_unit.rb

    r4885 r5163  
    107107    # If things go wrong, we don't want to run our test cases. We'll just define them to test nothing. 
    108108    def abort_tests 
     109      $stderr.puts 'No Active Record connection, aborting tests.' 
    109110      self.class.public_instance_methods.grep(/^test./).each do |method| 
    110111        self.class.class_eval { define_method(method.to_sym){} } 
  • trunk/actionpack/test/controller/filters_test.rb

    r4178 r5163  
    197197      render_text "hello" 
    198198    end 
    199   end 
    200  
    201   class BadFilterController < ActionController::Base 
    202     before_filter 2 
    203      
    204     def show() "show" end 
    205      
    206     protected 
    207       def rescue_action(e) raise(e) end 
    208199  end 
    209200 
     
    337328    assert_equal nil, test_process(BeforeAndAfterConditionController, "show_without_filter").template.assigns["ran_filter"] 
    338329  end 
    339    
     330 
    340331  def test_bad_filter 
    341     assert_raises(ActionController::ActionControllerError) {  
    342       test_process(BadFilterController) 
    343     } 
    344   end 
    345    
     332    bad_filter_controller = Class.new(ActionController::Base) 
     333    assert_raises(ActionController::ActionControllerError) do 
     334      bad_filter_controller.before_filter 2 
     335    end 
     336  end 
     337 
    346338  def test_around_filter 
    347339    controller = test_process(AroundFilterController) 
     
    349341    assert controller.template.assigns["after_ran"] 
    350342  end 
    351   
     343 
    352344  def test_having_properties_in_around_filter 
    353345    controller = test_process(AroundFilterController) 
     
    409401    end 
    410402end 
     403 
     404 
     405 
     406class PostsController < ActionController::Base 
     407  def rescue_action(e); raise e; end 
     408 
     409  module AroundExceptions 
     410    class Error < StandardError ; end 
     411    class Before < Error ; end 
     412    class After < Error ; end 
     413  end 
     414  include AroundExceptions 
     415 
     416  class DefaultFilter 
     417    include AroundExceptions 
     418  end 
     419 
     420  module_eval %w(raises_before raises_after raises_both no_raise no_filter).map { |action| "def #{action}; default_action end" }.join("\n") 
     421 
     422  private 
     423    def default_action 
     424      render :inline => "#{action_name} called" 
     425    end 
     426end 
     427 
     428class ControllerWithSymbolAsFilter < PostsController 
     429  around_filter :raise_before, :only => :raises_before 
     430  around_filter :raise_after, :only => :raises_after 
     431  around_filter :without_exception, :only => :no_raise 
     432 
     433  private 
     434    def raise_before 
     435      raise Before 
     436      yield 
     437    end 
     438 
     439    def raise_after 
     440      yield 
     441      raise After 
     442    end 
     443 
     444    def without_exception 
     445      # Do stuff... 
     446      1 + 1 
     447 
     448      yield 
     449 
     450      # Do stuff... 
     451      1 + 1 
     452    end 
     453end 
     454 
     455class ControllerWithFilterClass < PostsController 
     456  class YieldingFilter < DefaultFilter 
     457    def self.filter(controller) 
     458      yield 
     459      raise After 
     460    end 
     461  end 
     462 
     463  around_filter YieldingFilter, :only => :raises_after 
     464end 
     465 
     466class ControllerWithFilterInstance < PostsController 
     467  class YieldingFilter < DefaultFilter 
     468    def filter(controller) 
     469      yield 
     470      raise After 
     471    end 
     472  end 
     473 
     474  around_filter YieldingFilter.new, :only => :raises_after 
     475end 
     476 
     477class ControllerWithFilterMethod < PostsController 
     478  class YieldingFilter < DefaultFilter 
     479    def filter(controller) 
     480      yield 
     481      raise After 
     482    end 
     483  end 
     484 
     485  around_filter YieldingFilter.new.method(:filter), :only => :raises_after 
     486end 
     487 
     488class ControllerWithProcFilter < PostsController 
     489  around_filter(:only => :no_raise) do |c,b| 
     490    c.assigns['before'] = true 
     491    b.call 
     492    c.assigns['after'] = true 
     493  end 
     494end 
     495 
     496class ControllerWithWrongFilterType < PostsController 
     497  around_filter lambda { yield }, :only => :no_raise 
     498end 
     499 
     500class ControllerWithNestedFilters < ControllerWithSymbolAsFilter 
     501  around_filter :raise_before, :raise_after, :without_exception, :only => :raises_both 
     502end 
     503 
     504class ControllerWithAllTypesOfFilters < PostsController 
     505  before_filter :before 
     506  around_filter :around 
     507  after_filter :after 
     508  around_filter :around_again 
     509 
     510  private 
     511  def before 
     512    @ran_filter ||= [] 
     513    @ran_filter << 'before' 
     514  end 
     515 
     516  def around 
     517    @ran_filter << 'around (before yield)' 
     518    yield 
     519    @ran_filter << 'around (after yield)' 
     520  end 
     521 
     522  def after 
     523    @ran_filter << 'after' 
     524  end 
     525 
     526  def around_again 
     527    @ran_filter << 'around_again (before yield)' 
     528    yield 
     529    @ran_filter << 'around_again (after yield)' 
     530  end 
     531end 
     532 
     533class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters 
     534  skip_filter :around_again 
     535  skip_filter :after 
     536end 
     537 
     538class YieldingAroundFiltersTest < Test::Unit::TestCase 
     539  include PostsController::AroundExceptions 
     540 
     541  def test_filters_registering 
     542    assert_equal 1, ControllerWithFilterMethod.filter_chain.size 
     543    assert_equal 1, ControllerWithFilterClass.filter_chain.size 
     544    assert_equal 1, ControllerWithFilterInstance.filter_chain.size 
     545    assert_equal 3, ControllerWithSymbolAsFilter.filter_chain.size 
     546    assert_equal 1, ControllerWithWrongFilterType.filter_chain.size 
     547    assert_equal 6, ControllerWithNestedFilters.filter_chain.size 
     548    assert_equal 4, ControllerWithAllTypesOfFilters.filter_chain.size 
     549  end 
     550 
     551  def test_wrong_filter_type 
     552    assert_raise(ActionController::ActionControllerError) do 
     553      test_process(ControllerWithWrongFilterType,'no_raise') 
     554    end 
     555  end 
     556 
     557  def test_base 
     558    controller = PostsController 
     559    assert_nothing_raised { test_process(controller,'no_raise') } 
     560    assert_nothing_raised { test_process(controller,'raises_before') } 
     561    assert_nothing_raised { test_process(controller,'raises_after') } 
     562    assert_nothing_raised { test_process(controller,'no_filter') } 
     563  end 
     564 
     565  def test_with_symbol 
     566    controller = ControllerWithSymbolAsFilter 
     567    assert_nothing_raised { test_process(controller,'no_raise') } 
     568    assert_raise(Before) { test_process(controller,'raises_before') } 
     569    assert_raise(After) { test_process(controller,'raises_after') } 
     570    assert_nothing_raised { test_process(controller,'no_raise') } 
     571  end 
     572 
     573  def test_with_class 
     574    controller = ControllerWithFilterClass 
     575    assert_nothing_raised { test_process(controller,'no_raise') } 
     576    assert_raise(After) { test_process(controller,'raises_after') } 
     577  end 
     578 
     579  def test_with_instance 
     580    controller = ControllerWithFilterInstance 
     581    assert_nothing_raised { test_process(controller,'no_raise') } 
     582    assert_raise(After) { test_process(controller,'raises_after') } 
     583  end 
     584 
     585  def test_with_method 
     586    controller = ControllerWithFilterMethod 
     587    assert_nothing_raised { test_process(controller,'no_raise') } 
     588    assert_raise(After) { test_process(controller,'raises_after') } 
     589  end 
     590 
     591  def test_with_proc 
     592    controller = test_process(ControllerWithProcFilter,'no_raise') 
     593    assert controller.template.assigns['before'] 
     594    assert controller.template.assigns['after'] 
     595  end 
     596 
     597  def test_nested_filters 
     598    controller = ControllerWithNestedFilters 
     599    assert_nothing_raised do 
     600      begin 
     601        test_process(controller,'raises_both') 
     602      rescue Before, After 
     603      end 
     604    end 
     605    assert_raise Before do 
     606      begin 
     607        test_process(controller,'raises_both') 
     608      rescue After 
     609      end 
     610    end 
     611  end 
     612 
     613  def test_filter_order_with_all_filter_types 
     614    controller = test_process(ControllerWithAllTypesOfFilters,'no_raise') 
     615    assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) around (after yield) after',controller.template.assigns['ran_filter'].join(' ') 
     616  end 
     617 
     618  def test_filter_order_with_skip_filter_method 
     619    controller = test_process(ControllerWithTwoLessFilters,'no_raise') 
     620    assert_equal 'before around (before yield) around (after yield)',controller.template.assigns['ran_filter'].join(' ') 
     621  end 
     622 
     623  protected 
     624    def test_process(controller, action = "show") 
     625      request = ActionController::TestRequest.new 
     626      request.action = action 
     627      controller.process(request, ActionController::TestResponse.new) 
     628    end 
     629end