diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 83e4a492ed..88832a6f2a 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,11 @@ *SVN* +* Fix errors with around_filters which do not yield, restore 1.1 behaviour with after filters. Closes #8891 [skaes] + + After filters will *no longer* be run if an around_filter fails to yield, users relying on + this behaviour are advised to put the code in question after a yield statement in an around filter. + + * Allow you to delete cookies with options. Closes #3685 [josh, Chris Wanstrath] * Allow you to render views with periods in the name. Closes #8076 [norbert] diff --git a/actionpack/lib/action_controller/filters.rb b/actionpack/lib/action_controller/filters.rb index 5705960eb5..c2f2c2a432 100644 --- a/actionpack/lib/action_controller/filters.rb +++ b/actionpack/lib/action_controller/filters.rb @@ -214,9 +214,10 @@ module ActionController #:nodoc: # == Filter Chain Halting # # before_filter and around_filter may halt the request - # before controller action is run. This is useful, for example, to deny + # before a 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. + # After filters will not be executed if the filter chain is halted. # # Around filters halt the request unless the action block is called. # Given these filters @@ -238,12 +239,12 @@ module ActionController #:nodoc: # . . / # . #around (code after yield) # . / - # #after (actual filter code is run) + # #after (actual filter code is run, unless the around filter does not yield) # - # 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. + # If #around returns before yielding, #after will still not be run. The #before + # filter and controller action will not be run. If #before returns false, + # the second half of #around and will still run but #after and the + # action will not. If #around fails to yield, #after will not be run. module ClassMethods # The passed filters will be appended to the filter_chain and # will execute before the action on this controller is performed. @@ -439,7 +440,7 @@ module ActionController #:nodoc: def run(controller) # only filters returning false are halted. if false == @filter.call(controller) - controller.halt_filter_chain(@filter) + controller.send :halt_filter_chain, @filter, :returned_false end end @@ -528,7 +529,7 @@ module ActionController #:nodoc: def find_filter_append_position(filters, filter_type) # appending an after filter puts it at the end of the call chain - # before and around filters goe before the first after filter in the chain + # before and around filters go before the first after filter in the chain unless filter_type == :after filter_chain.each_with_index do |f,i| return i if f.after? @@ -655,7 +656,7 @@ module ActionController #:nodoc: return filter unless filter_responds_to_before_and_after(filter) Proc.new do |controller, action| if filter.before(controller) == false - controller.send :halt_filter_chain, filter + controller.send :halt_filter_chain, filter, :returned_false else begin action.call @@ -675,8 +676,25 @@ module ActionController #:nodoc: end end - def filter_chain - self.class.filter_chain + protected + + def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc: + @before_filter_chain_aborted = false + process_without_filters(request, response, method, *arguments) + end + + def perform_action_with_filters + call_filters(self.class.filter_chain, 0, 0) + end + + private + + def call_filters(chain, index, nesting) + index = run_before_filters(chain, index, nesting) + aborted = @before_filter_chain_aborted + perform_action_without_filters unless performed? || aborted + return index if nesting != 0 || aborted + run_after_filters(chain, index) end def skip_excluded_filters(chain, index) @@ -686,72 +704,54 @@ module ActionController #:nodoc: [filter, index] end - def call_filters(chain, index, nesting) - # run before filters until we find an after filter or around filter + def run_before_filters(chain, index, nesting) while chain[index] filter, index = skip_excluded_filters(chain, index) break unless filter # end of call chain reached case filter.type when :before - # invoke before filter - filter.run(self) + filter.run(self) # invoke before filter index = index.next break if @before_filter_chain_aborted when :around + yielded = false filter.call(self) do + yielded = true # all remaining before and around filters will be run in this call index = call_filters(chain, index.next, nesting.next) end + halt_filter_chain(filter, :did_not_yield) unless yielded break else - # no before or around filters left - break + break # no before or around filters left end end + index + end - aborted = @before_filter_chain_aborted - perform_action_without_filters unless performed? || aborted - return index if aborted || nesting != 0 - - # if an around filter catches an exception during rendering and handles it, e.g. - # by rendering an error page, we might have left over around filters in the filter - # chain. so skip over these. - index = index.next while (filter = chain[index]) && filter.type == :around - - # run after filters, if any + def run_after_filters(chain, index) + seen_after_filter = false while chain[index] filter, index = skip_excluded_filters(chain, index) - break unless filter + break unless filter # end of call chain reached case filter.type when :after - filter.run(self) - index = index.next + seen_after_filter = true + filter.run(self) # invoke after filter else - raise ActionControllerError, "filter #{filter.inspect} was in the wrong place!" + # implementation error or someone has mucked with the filter chain + raise ActionControllerError, "filter #{filter.inspect} was in the wrong place!" if seen_after_filter end + index = index.next end - index.next end - def halt_filter_chain(filter) - logger.info "Filter chain halted as [#{filter.inspect}] returned false." if logger + def halt_filter_chain(filter, reason) @before_filter_chain_aborted = true + logger.info "Filter chain halted as [#{filter.inspect}] #{reason}." if logger false end - - protected - - def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc: - @before_filter_chain_aborted = false - process_without_filters(request, response, method, *arguments) - end - - private - def perform_action_with_filters - call_filters(self.class.filter_chain, 0, 0) - end - end end end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index a38e55fb52..8adee4c12f 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -324,9 +324,9 @@ class FilterTest < Test::Unit::TestCase render :text => 'hello' end end - + class ErrorToRescue < Exception; end - + class RescuingAroundFilterWithBlock def filter(controller) begin @@ -336,20 +336,86 @@ class FilterTest < Test::Unit::TestCase end end end - + class RescuedController < ActionController::Base around_filter RescuingAroundFilterWithBlock.new - + def show raise ErrorToRescue.new("Something made the bad noise.") end - + private def rescue_action(exception) raise exception end end + class NonYieldingAroundFilterController < ActionController::Base + + before_filter :filter_one + around_filter :non_yielding_filter + before_filter :filter_two + after_filter :filter_three + + def index + render :inline => "index" + end + + #make sure the controller complains + def rescue_action(e); raise e; end + + private + + def filter_one + @filters ||= [] + @filters << "filter_one" + end + + def filter_two + @filters << "filter_two" + end + + def non_yielding_filter + @filters << "zomg it didn't yield" + @filter_return_value + end + + def filter_three + @filters << "filter_three" + end + + end + + def test_non_yielding_around_filters_not_returning_false_do_not_raise + controller = NonYieldingAroundFilterController.new + controller.instance_variable_set "@filter_return_value", true + assert_nothing_raised do + test_process(controller, "index") + end + end + + def test_non_yielding_around_filters_returning_false_do_not_raise + controller = NonYieldingAroundFilterController.new + controller.instance_variable_set "@filter_return_value", false + assert_nothing_raised do + test_process(controller, "index") + end + end + + def test_after_filters_are_not_run_if_around_filter_returns_false + controller = NonYieldingAroundFilterController.new + controller.instance_variable_set "@filter_return_value", false + test_process(controller, "index") + assert_equal ["filter_one", "zomg it didn't yield"], controller.assigns['filters'] + end + + def test_after_filters_are_not_run_if_around_filter_does_not_yield + controller = NonYieldingAroundFilterController.new + controller.instance_variable_set "@filter_return_value", true + test_process(controller, "index") + assert_equal ["filter_one", "zomg it didn't yield"], controller.assigns['filters'] + end + def test_empty_filter_chain assert_equal 0, EmptyFilterChainController.filter_chain.size assert test_process(EmptyFilterChainController).template.assigns['action_executed'] @@ -516,13 +582,13 @@ class FilterTest < Test::Unit::TestCase def test_changing_the_requirements assert_equal nil, test_process(ChangingTheRequirementsController, "go_wild").template.assigns['ran_filter'] end - + def test_a_rescuing_around_filter response = nil assert_nothing_raised do response = test_process(RescuedController) end - + assert response.success? assert_equal("I rescued this: #", response.body) end