actionpack 1.13.3 → 1.13.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (47) hide show
  1. data/CHANGELOG +44 -2
  2. data/Rakefile +1 -1
  3. data/lib/action_controller/assertions/dom_assertions.rb +2 -2
  4. data/lib/action_controller/assertions/model_assertions.rb +1 -1
  5. data/lib/action_controller/assertions/response_assertions.rb +2 -0
  6. data/lib/action_controller/assertions/routing_assertions.rb +1 -0
  7. data/lib/action_controller/base.rb +7 -1
  8. data/lib/action_controller/caching.rb +39 -38
  9. data/lib/action_controller/cgi_ext/pstore_performance_fix.rb +30 -0
  10. data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +1 -1
  11. data/lib/action_controller/cgi_process.rb +13 -4
  12. data/lib/action_controller/cookies.rb +5 -3
  13. data/lib/action_controller/filters.rb +176 -77
  14. data/lib/action_controller/integration.rb +31 -21
  15. data/lib/action_controller/pagination.rb +7 -1
  16. data/lib/action_controller/resources.rb +117 -32
  17. data/lib/action_controller/routing.rb +41 -1
  18. data/lib/action_controller/test_process.rb +5 -2
  19. data/lib/action_controller/url_rewriter.rb +4 -1
  20. data/lib/action_controller/verification.rb +1 -0
  21. data/lib/action_pack/version.rb +1 -1
  22. data/lib/action_view/base.rb +25 -19
  23. data/lib/action_view/compiled_templates.rb +2 -2
  24. data/lib/action_view/helpers/active_record_helper.rb +18 -18
  25. data/lib/action_view/helpers/debug_helper.rb +10 -0
  26. data/lib/action_view/helpers/deprecated_helper.rb +3 -0
  27. data/lib/action_view/helpers/prototype_helper.rb +33 -17
  28. data/test/activerecord/pagination_test.rb +9 -0
  29. data/test/controller/addresses_render_test.rb +4 -1
  30. data/test/controller/base_test.rb +1 -1
  31. data/test/controller/caching_test.rb +3 -2
  32. data/test/controller/cookie_test.rb +11 -0
  33. data/test/controller/deprecation/deprecated_base_methods_test.rb +18 -0
  34. data/test/controller/filter_params_test.rb +1 -0
  35. data/test/controller/filters_test.rb +149 -26
  36. data/test/controller/integration_test.rb +93 -8
  37. data/test/controller/resources_test.rb +215 -36
  38. data/test/controller/routing_test.rb +1 -1
  39. data/test/controller/test_test.rb +16 -0
  40. data/test/controller/url_rewriter_test.rb +13 -1
  41. data/test/controller/verification_test.rb +15 -0
  42. data/test/fixtures/test/hello_world.rxml +2 -1
  43. data/test/template/asset_tag_helper_test.rb +5 -0
  44. data/test/template/compiled_templates_test.rb +29 -17
  45. data/test/template/number_helper_test.rb +1 -1
  46. data/test/template/prototype_helper_test.rb +2 -2
  47. metadata +84 -83
data/CHANGELOG CHANGED
@@ -1,6 +1,48 @@
1
+ *1.13.4* (October 4th, 2007)
2
+
3
+ * Only accept session ids from cookies, prevents session fixation attacks. [bradediger]
4
+
5
+ * Change the resource seperator from ; to / change the generated routes to use the new-style named routes. e.g. new_group_user_path(@group) instead of group_new_user_path(@group). [pixeltrix]
6
+
7
+ * Integration tests: introduce methods for other HTTP methods. #6353 [caboose]
8
+
9
+ * Improve performance of action caching. Closes #8231 [skaes]
10
+
11
+ * Fix errors with around_filters which do not yield, restore 1.1 behaviour with after filters. Closes #8891 [skaes]
12
+
13
+ After filters will *no longer* be run if an around_filter fails to yield, users relying on
14
+ this behaviour are advised to put the code in question after a yield statement in an around filter.
15
+
16
+ * Allow you to delete cookies with options. Closes #3685 [josh, Chris Wanstrath]
17
+
18
+ * Deprecate pagination. Install the classic_pagination plugin for forward compatibility, or move to the superior will_paginate plugin. #8157 [Mislav Marohnic]
19
+
20
+ * Fix filtered parameter logging with nil parameter values. #8422 [choonkeat]
21
+
22
+ * Integration tests: alias xhr to xml_http_request and add a request_method argument instead of always using POST. #7124 [Nik Wakelin, Francois Beausoleil, Wizard]
23
+
24
+ * Document caches_action. #5419 [Jarkko Laine]
25
+
26
+ * observe_form always sends the serialized form. #5271 [manfred, normelton@gmail.com]
27
+
28
+ * Update UrlWriter to accept :anchor parameter. Closes #6771. [octopod]
29
+
30
+ * Replace the current block/continuation filter chain handling by an implementation based on a simple loop. Closes #8226 [Stefan Kaes]
31
+
32
+ * Return the string representation from an Xml Builder when rendering a partial. #5044 [tpope]
33
+
34
+ * Cleaned up, corrected, and mildly expanded ActionPack documentation. Closes #7190 [jeremymcanally]
35
+
36
+ * Small collection of ActionController documentation cleanups. Closes #7319 [jeremymcanally]
37
+
38
+ * Performance: patch cgi/session/pstore to require digest/md5 once rather than per #initialize. #7583 [Stefan Kaes]
39
+
40
+ * Deprecation: verification with :redirect_to => :named_route shouldn't be deprecated. #7525 [Justin French]
41
+
42
+
1
43
  *1.13.3* (March 12th, 2007)
2
44
 
3
- * Apply [5709] to stable.
45
+ * Fix a bug in Routing where a parameter taken from the path of the current request could not be used as a query parameter for the next. #6752 [Nicholas Seckar]
4
46
 
5
47
  * session_enabled? works with session :off. #6680 [Catfish]
6
48
 
@@ -440,7 +482,7 @@
440
482
 
441
483
  * Avoid naming collision among compiled view methods. [Jeremy Kemper]
442
484
 
443
- * Fix CGI extensions when they expect string but get nil in Windows. Closes #5276 [mislav@nippur.irb.hr]
485
+ * Fix CGI extensions when they expect string but get nil in Windows. Closes #5276 [Mislav Marohnic]
444
486
 
445
487
  * Determine the correct template_root for deeply nested components. #2841 [s.brink@web.de]
446
488
 
data/Rakefile CHANGED
@@ -75,7 +75,7 @@ spec = Gem::Specification.new do |s|
75
75
  s.has_rdoc = true
76
76
  s.requirements << 'none'
77
77
 
78
- s.add_dependency('activesupport', '= 1.4.2' + PKG_BUILD)
78
+ s.add_dependency('activesupport', '= 1.4.3' + PKG_BUILD)
79
79
 
80
80
  s.require_path = 'lib'
81
81
  s.autorequire = 'action_controller'
@@ -1,7 +1,7 @@
1
1
  module ActionController
2
2
  module Assertions
3
3
  module DomAssertions
4
- # test 2 html strings to be equivalent, i.e. identical up to reordering of attributes
4
+ # Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
5
5
  def assert_dom_equal(expected, actual, message="")
6
6
  clean_backtrace do
7
7
  expected_dom = HTML::Document.new(expected).root
@@ -11,7 +11,7 @@ module ActionController
11
11
  end
12
12
  end
13
13
 
14
- # negated form of +assert_dom_equivalent+
14
+ # The negated form of +assert_dom_equivalent+.
15
15
  def assert_dom_not_equal(expected, actual, message="")
16
16
  clean_backtrace do
17
17
  expected_dom = HTML::Document.new(expected).root
@@ -1,7 +1,7 @@
1
1
  module ActionController
2
2
  module Assertions
3
3
  module ModelAssertions
4
- # ensures that the passed record is valid by active record standards. returns the error messages if not
4
+ # Ensures that the passed record is valid by ActiveRecord standards and returns any error messages if it is not.
5
5
  def assert_valid(record)
6
6
  clean_backtrace do
7
7
  assert record.valid?, record.errors.full_messages.join("\n")
@@ -120,6 +120,7 @@ module ActionController
120
120
  end
121
121
 
122
122
  private
123
+ # Recognizes the route for a given path.
123
124
  def recognized_request_for(path, request_method = nil)
124
125
  path = "/#{path}" unless path.first == '/'
125
126
 
@@ -132,6 +133,7 @@ module ActionController
132
133
  request
133
134
  end
134
135
 
136
+ # Proxy to to_param if the object will respond to it.
135
137
  def parameterize(value)
136
138
  value.respond_to?(:to_param) ? value.to_param : value
137
139
  end
@@ -82,6 +82,7 @@ module ActionController
82
82
  end
83
83
 
84
84
  private
85
+ # Recognizes the route for a given path.
85
86
  def recognized_request_for(path, request_method = nil)
86
87
  path = "/#{path}" unless path.first == '/'
87
88
 
@@ -292,6 +292,10 @@ module ActionController #:nodoc:
292
292
  # Turn on +ignore_missing_templates+ if you want to unit test actions without making the associated templates.
293
293
  cattr_accessor :ignore_missing_templates
294
294
 
295
+ # Controls the resource action separator
296
+ @@resource_action_separator = "/"
297
+ cattr_accessor :resource_action_separator
298
+
295
299
  # Holds the request object that's primarily used to get environment variables through access like
296
300
  # <tt>request.env["REQUEST_URI"]</tt>.
297
301
  attr_internal :request
@@ -393,7 +397,8 @@ module ActionController #:nodoc:
393
397
  elsif value.is_a?(Hash)
394
398
  filtered_parameters[key] = filter_parameters(value)
395
399
  elsif block_given?
396
- key, value = key.dup, value.dup
400
+ key = key.dup
401
+ value = value.dup if value
397
402
  yield key, value
398
403
  filtered_parameters[key] = value
399
404
  else
@@ -538,6 +543,7 @@ module ActionController #:nodoc:
538
543
  self.class.controller_path
539
544
  end
540
545
 
546
+ # Test whether the session is enabled for this request.
541
547
  def session_enabled?
542
548
  request.session_options && request.session_options[:disabled] != false
543
549
  end
@@ -1,5 +1,6 @@
1
1
  require 'fileutils'
2
2
  require 'uri'
3
+ require 'set'
3
4
 
4
5
  module ActionController #:nodoc:
5
6
  # Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls
@@ -163,13 +164,24 @@ module ActionController #:nodoc:
163
164
  module Actions
164
165
  def self.included(base) #:nodoc:
165
166
  base.extend(ClassMethods)
166
- base.send(:attr_accessor, :rendered_action_cache)
167
+ base.class_eval do
168
+ attr_accessor :rendered_action_cache, :action_cache_path
169
+ alias_method_chain :protected_instance_variables, :action_caching
170
+ end
167
171
  end
168
172
 
169
- module ClassMethods #:nodoc:
173
+ def protected_instance_variables_with_action_caching
174
+ protected_instance_variables_without_action_caching + %w(@action_cache_path)
175
+ end
176
+
177
+ module ClassMethods
178
+ # Declares that +actions+ should be cached.
179
+ # See ActionController::Caching::Actions for details.
170
180
  def caches_action(*actions)
171
181
  return unless perform_caching
172
- around_filter(ActionCacheFilter.new(*actions))
182
+ action_cache_filter = ActionCacheFilter.new(*actions)
183
+ before_filter action_cache_filter
184
+ after_filter action_cache_filter
173
185
  end
174
186
  end
175
187
 
@@ -185,70 +197,59 @@ module ActionController #:nodoc:
185
197
  end
186
198
 
187
199
  class ActionCacheFilter #:nodoc:
188
- def initialize(*actions, &block)
189
- @actions = actions
200
+ def initialize(*actions)
201
+ @actions = Set.new actions
190
202
  end
191
203
 
192
204
  def before(controller)
193
- return unless @actions.include?(controller.action_name.intern)
194
- action_cache_path = ActionCachePath.new(controller)
195
- if cache = controller.read_fragment(action_cache_path.path)
205
+ return unless @actions.include?(controller.action_name.to_sym)
206
+ cache_path = ActionCachePath.new(controller, {})
207
+ if cache = controller.read_fragment(cache_path.path)
196
208
  controller.rendered_action_cache = true
197
- set_content_type!(action_cache_path)
209
+ set_content_type!(controller, cache_path.extension)
198
210
  controller.send(:render_text, cache)
199
211
  false
212
+ else
213
+ controller.action_cache_path = cache_path
200
214
  end
201
215
  end
202
216
 
203
217
  def after(controller)
204
- return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache
205
- controller.write_fragment(ActionCachePath.path_for(controller), controller.response.body)
218
+ return if !@actions.include?(controller.action_name.to_sym) || controller.rendered_action_cache
219
+ controller.write_fragment(controller.action_cache_path.path, controller.response.body)
206
220
  end
207
221
 
208
222
  private
209
-
210
- def set_content_type!(action_cache_path)
211
- if extention = action_cache_path.extension
212
- content_type = Mime::EXTENSION_LOOKUP[extention]
213
- action_cache_path.controller.response.content_type = content_type.to_s
214
- end
223
+ def set_content_type!(controller, extension)
224
+ controller.response.content_type = Mime::EXTENSION_LOOKUP[extension].to_s if extension
215
225
  end
216
226
 
217
227
  end
218
228
 
219
229
  class ActionCachePath
220
- attr_reader :controller, :options
230
+ attr_reader :path, :extension
221
231
 
222
232
  class << self
223
- def path_for(*args, &block)
224
- new(*args).path
233
+ def path_for(controller, options)
234
+ new(controller, options).path
225
235
  end
226
236
  end
227
237
 
228
238
  def initialize(controller, options = {})
229
- @controller = controller
230
- @options = options
231
- end
232
-
233
- def path
234
- return @path if @path
235
- @path = controller.url_for(options).split('://').last
236
- normalize!
237
- add_extension!
238
- URI.unescape(@path)
239
- end
240
-
241
- def extension
242
- @extension ||= extract_extension(controller.request.path)
239
+ @extension = extract_extension(controller.request.path)
240
+ path = controller.url_for(options).split('://').last
241
+ normalize!(path)
242
+ add_extension!(path, @extension)
243
+ @path = URI.unescape(path)
243
244
  end
244
245
 
245
246
  private
246
- def normalize!
247
- @path << 'index' if @path.last == '/'
247
+ def normalize!(path)
248
+ path << 'index' if path[-1] == ?/
248
249
  end
249
250
 
250
- def add_extension!
251
- @path << ".#{extension}" if extension
251
+ def add_extension!(path, extension)
252
+ path << ".#{extension}" if extension
252
253
  end
253
254
 
254
255
  def extract_extension(file_path)
@@ -0,0 +1,30 @@
1
+ # CGI::Session::PStore.initialize requires 'digest/md5' on every call.
2
+ # This makes sense when spawning processes per request, but is
3
+ # unnecessarily expensive when serving requests from a long-lived
4
+ # process.
5
+ require 'cgi/session'
6
+ require 'cgi/session/pstore'
7
+ require 'digest/md5'
8
+
9
+ class CGI::Session::PStore #:nodoc:
10
+ def initialize(session, option={})
11
+ dir = option['tmpdir'] || Dir::tmpdir
12
+ prefix = option['prefix'] || ''
13
+ id = session.session_id
14
+ md5 = Digest::MD5.hexdigest(id)[0,16]
15
+ path = dir+"/"+prefix+md5
16
+ path.untaint
17
+ if File::exist?(path)
18
+ @hash = nil
19
+ else
20
+ unless session.new_session
21
+ raise CGI::Session::NoSession, "uninitialized session"
22
+ end
23
+ @hash = {}
24
+ end
25
+ @p = ::PStore.new(path)
26
+ @p.transaction do |p|
27
+ File.chmod(0600, p.path)
28
+ end
29
+ end
30
+ end
@@ -65,7 +65,7 @@ class CGI #:nodoc:
65
65
  if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank?
66
66
  uri.split('?', 2)[1] || ''
67
67
  else
68
- env_qs
68
+ env_qs || ''
69
69
  end
70
70
  end
71
71
  end
@@ -2,6 +2,7 @@ require 'action_controller/cgi_ext/cgi_ext'
2
2
  require 'action_controller/cgi_ext/cookie_performance_fix'
3
3
  require 'action_controller/cgi_ext/raw_post_data_fix'
4
4
  require 'action_controller/cgi_ext/session_performance_fix'
5
+ require 'action_controller/cgi_ext/pstore_performance_fix'
5
6
 
6
7
  module ActionController #:nodoc:
7
8
  class Base
@@ -12,8 +13,8 @@ module ActionController #:nodoc:
12
13
  # (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
13
14
  # lib/action_controller/session.
14
15
  # * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'.
15
- # * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ parameter
16
- # of the request, or automatically generated for a new session.
16
+ # * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ cookie, or
17
+ # automatically generated for a new session.
17
18
  # * <tt>:new_session</tt> - if true, force creation of a new session. If not set, a new session is only created if none currently
18
19
  # exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set,
19
20
  # an ArgumentError is raised.
@@ -23,6 +24,8 @@ module ActionController #:nodoc:
23
24
  # server.
24
25
  # * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS.
25
26
  # * <tt>:session_path</tt> - the path for which this session applies. Defaults to the directory of the CGI script.
27
+ # * <tt>:cookie_only</tt> - if +true+ (the default), session IDs will only be accepted from cookies and not from
28
+ # the query string or POST parameters. This protects against session fixation attacks.
26
29
  def self.process_cgi(cgi = CGI.new, session_options = {})
27
30
  new.process_cgi(cgi, session_options)
28
31
  end
@@ -33,18 +36,21 @@ module ActionController #:nodoc:
33
36
  end
34
37
 
35
38
  class CgiRequest < AbstractRequest #:nodoc:
36
- attr_accessor :cgi, :session_options
39
+ attr_accessor :cgi, :session_options, :cookie_only
40
+ class SessionFixationAttempt < StandardError; end #:nodoc:
37
41
 
38
42
  DEFAULT_SESSION_OPTIONS = {
39
43
  :database_manager => CGI::Session::PStore,
40
44
  :prefix => "ruby_sess.",
41
- :session_path => "/"
45
+ :session_path => "/",
46
+ :cookie_only => true
42
47
  } unless const_defined?(:DEFAULT_SESSION_OPTIONS)
43
48
 
44
49
  def initialize(cgi, session_options = {})
45
50
  @cgi = cgi
46
51
  @session_options = session_options
47
52
  @env = @cgi.send(:env_table)
53
+ @cookie_only = session_options.delete :cookie_only
48
54
  super()
49
55
  end
50
56
 
@@ -108,6 +114,9 @@ module ActionController #:nodoc:
108
114
  @session = Hash.new
109
115
  else
110
116
  stale_session_check! do
117
+ if @cookie_only && request_parameters[session_options_with_string_keys['session_key']]
118
+ raise SessionFixationAttempt
119
+ end
111
120
  case value = session_options_with_string_keys['new_session']
112
121
  when true
113
122
  @session = new_session
@@ -62,9 +62,11 @@ module ActionController #:nodoc:
62
62
  end
63
63
 
64
64
  # Removes the cookie on the client machine by setting the value to an empty string
65
- # and setting its expiration date into the past
66
- def delete(name)
67
- set_cookie("name" => name.to_s, "value" => "", "expires" => Time.at(0))
65
+ # and setting its expiration date into the past. Like []=, you can pass in an options
66
+ # hash to delete cookies with extra data such as a +path+.
67
+ def delete(name, options = {})
68
+ options.stringify_keys!
69
+ set_cookie(options.merge("name" => name.to_s, "value" => "", "expires" => Time.at(0)))
68
70
  end
69
71
 
70
72
  private
@@ -214,9 +214,10 @@ module ActionController #:nodoc:
214
214
  # == Filter Chain Halting
215
215
  #
216
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
217
+ # before a controller action is run. This is useful, for example, to deny
218
218
  # access to unauthenticated users or to redirect from http to https.
219
219
  # Simply return false from the filter or call render or redirect.
220
+ # After filters will not be executed if the filter chain is halted.
220
221
  #
221
222
  # Around filters halt the request unless the action block is called.
222
223
  # Given these filters
@@ -238,12 +239,12 @@ module ActionController #:nodoc:
238
239
  # . . /
239
240
  # . #around (code after yield)
240
241
  # . /
241
- # #after (actual filter code is run)
242
+ # #after (actual filter code is run, unless the around filter does not yield)
242
243
  #
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.
244
+ # If #around returns before yielding, #after will still not be run. The #before
245
+ # filter and controller action will not be run. If #before returns false,
246
+ # the second half of #around and will still run but #after and the
247
+ # action will not. If #around does not yield, #after will not be run.
247
248
  module ClassMethods
248
249
  # The passed <tt>filters</tt> will be appended to the filter_chain and
249
250
  # will execute before the action on this controller is performed.
@@ -263,13 +264,13 @@ module ActionController #:nodoc:
263
264
  # The passed <tt>filters</tt> will be appended to the array of filters
264
265
  # that run _after_ actions on this controller are performed.
265
266
  def append_after_filter(*filters, &block)
266
- prepend_filter_to_chain(filters, :after, &block)
267
+ append_filter_to_chain(filters, :after, &block)
267
268
  end
268
269
 
269
270
  # The passed <tt>filters</tt> will be prepended to the array of filters
270
271
  # that run _after_ actions on this controller are performed.
271
272
  def prepend_after_filter(*filters, &block)
272
- append_filter_to_chain(filters, :after, &block)
273
+ prepend_filter_to_chain(filters, :after, &block)
273
274
  end
274
275
 
275
276
  # Shorthand for append_after_filter since it's the most common.
@@ -362,12 +363,12 @@ module ActionController #:nodoc:
362
363
 
363
364
  # Returns a mapping between filters and the actions that may run them.
364
365
  def included_actions #:nodoc:
365
- read_inheritable_attribute("included_actions") || {}
366
+ @included_actions ||= read_inheritable_attribute("included_actions") || {}
366
367
  end
367
368
 
368
369
  # Returns a mapping between filters and actions that may not run them.
369
370
  def excluded_actions #:nodoc:
370
- read_inheritable_attribute("excluded_actions") || {}
371
+ @excluded_actions ||= read_inheritable_attribute("excluded_actions") || {}
371
372
  end
372
373
 
373
374
  # Find a filter in the filter_chain where the filter method matches the _filter_ param
@@ -381,10 +382,11 @@ module ActionController #:nodoc:
381
382
 
382
383
  # Returns true if the filter is excluded from the given action
383
384
  def filter_excluded_from_action?(filter,action) #:nodoc:
384
- if (ia = included_actions[filter]) && !ia.empty?
385
+ case
386
+ when ia = included_actions[filter]
385
387
  !ia.include?(action)
386
- else
387
- (excluded_actions[filter] || []).include?(action)
388
+ when ea = excluded_actions[filter]
389
+ ea.include?(action)
388
390
  end
389
391
  end
390
392
 
@@ -397,20 +399,28 @@ module ActionController #:nodoc:
397
399
  @filter = filter
398
400
  end
399
401
 
402
+ def type
403
+ :around
404
+ end
405
+
400
406
  def before?
401
- false
407
+ type == :before
402
408
  end
403
409
 
404
410
  def after?
405
- false
411
+ type == :after
406
412
  end
407
413
 
408
414
  def around?
409
- true
415
+ type == :around
416
+ end
417
+
418
+ def run(controller)
419
+ raise ActionControllerError, 'No filter type: Nothing to do here.'
410
420
  end
411
421
 
412
422
  def call(controller, &block)
413
- raise(ActionControllerError, 'No filter type: Nothing to do here.')
423
+ run(controller)
414
424
  end
415
425
  end
416
426
 
@@ -420,35 +430,38 @@ module ActionController #:nodoc:
420
430
  def filter
421
431
  @filter.filter
422
432
  end
423
-
424
- def around?
425
- false
426
- end
427
433
  end
428
434
 
429
435
  class BeforeFilterProxy < FilterProxy #:nodoc:
430
- def before?
431
- true
436
+ def type
437
+ :before
432
438
  end
433
439
 
434
- def call(controller, &block)
435
- if false == @filter.call(controller) # must only stop if equal to false. only filters returning false are halted.
436
- controller.halt_filter_chain(@filter, :returned_false)
437
- else
438
- yield
440
+ def run(controller)
441
+ # only filters returning false are halted.
442
+ if false == @filter.call(controller)
443
+ controller.send :halt_filter_chain, @filter, :returned_false
439
444
  end
440
445
  end
446
+
447
+ def call(controller)
448
+ yield unless run(controller)
449
+ end
441
450
  end
442
451
 
443
452
  class AfterFilterProxy < FilterProxy #:nodoc:
444
- def after?
445
- true
453
+ def type
454
+ :after
446
455
  end
447
456
 
448
- def call(controller, &block)
449
- yield
457
+ def run(controller)
450
458
  @filter.call(controller)
451
459
  end
460
+
461
+ def call(controller)
462
+ yield
463
+ run(controller)
464
+ end
452
465
  end
453
466
 
454
467
  class SymbolFilter < Filter #:nodoc:
@@ -485,29 +498,72 @@ module ActionController #:nodoc:
485
498
  end
486
499
  end
487
500
 
501
+ class ClassBeforeFilter < Filter #:nodoc:
502
+ def call(controller, &block)
503
+ @filter.before(controller)
504
+ end
505
+ end
506
+
507
+ class ClassAfterFilter < Filter #:nodoc:
508
+ def call(controller, &block)
509
+ @filter.after(controller)
510
+ end
511
+ end
512
+
488
513
  protected
489
- def append_filter_to_chain(filters, position = :around, &block)
490
- write_inheritable_array('filter_chain', create_filters(filters, position, &block) )
514
+ def append_filter_to_chain(filters, filter_type = :around, &block)
515
+ pos = find_filter_append_position(filters, filter_type)
516
+ update_filter_chain(filters, filter_type, pos, &block)
491
517
  end
492
518
 
493
- def prepend_filter_to_chain(filters, position = :around, &block)
494
- write_inheritable_attribute('filter_chain', create_filters(filters, position, &block) + filter_chain)
519
+ def prepend_filter_to_chain(filters, filter_type = :around, &block)
520
+ pos = find_filter_prepend_position(filters, filter_type)
521
+ update_filter_chain(filters, filter_type, pos, &block)
495
522
  end
496
523
 
497
- def create_filters(filters, position, &block) #:nodoc:
524
+ def update_filter_chain(filters, filter_type, pos, &block)
525
+ new_filters = create_filters(filters, filter_type, &block)
526
+ new_chain = filter_chain.insert(pos, new_filters).flatten
527
+ write_inheritable_attribute('filter_chain', new_chain)
528
+ end
529
+
530
+ def find_filter_append_position(filters, filter_type)
531
+ # appending an after filter puts it at the end of the call chain
532
+ # before and around filters go before the first after filter in the chain
533
+ unless filter_type == :after
534
+ filter_chain.each_with_index do |f,i|
535
+ return i if f.after?
536
+ end
537
+ end
538
+ return -1
539
+ end
540
+
541
+ def find_filter_prepend_position(filters, filter_type)
542
+ # prepending a before or around filter puts it at the front of the call chain
543
+ # after filters go before the first after filter in the chain
544
+ if filter_type == :after
545
+ filter_chain.each_with_index do |f,i|
546
+ return i if f.after?
547
+ end
548
+ return -1
549
+ end
550
+ return 0
551
+ end
552
+
553
+ def create_filters(filters, filter_type, &block) #:nodoc:
498
554
  filters, conditions = extract_conditions(filters, &block)
499
- filters.map! { |filter| find_or_create_filter(filter,position) }
555
+ filters.map! { |filter| find_or_create_filter(filter, filter_type) }
500
556
  update_conditions(filters, conditions)
501
557
  filters
502
558
  end
503
559
 
504
- def find_or_create_filter(filter,position)
505
- if found_filter = find_filter(filter) { |f| f.send("#{position}?") }
560
+ def find_or_create_filter(filter, filter_type)
561
+ if found_filter = find_filter(filter) { |f| f.type == filter_type }
506
562
  found_filter
507
563
  else
508
- f = class_for_filter(filter).new(filter)
564
+ f = class_for_filter(filter, filter_type).new(filter)
509
565
  # apply proxy to filter if necessary
510
- case position
566
+ case filter_type
511
567
  when :before
512
568
  BeforeFilterProxy.new(f)
513
569
  when :after
@@ -520,7 +576,7 @@ module ActionController #:nodoc:
520
576
 
521
577
  # The determination of the filter type was once done at run time.
522
578
  # This method is here to extract as much logic from the filter run time as possible
523
- def class_for_filter(filter) #:nodoc:
579
+ def class_for_filter(filter, filter_type) #:nodoc:
524
580
  case
525
581
  when filter.is_a?(Symbol)
526
582
  SymbolFilter
@@ -534,8 +590,12 @@ module ActionController #:nodoc:
534
590
  end
535
591
  when filter.respond_to?(:filter)
536
592
  ClassFilter
593
+ when filter.respond_to?(:before) && filter_type == :before
594
+ ClassBeforeFilter
595
+ when filter.respond_to?(:after) && filter_type == :after
596
+ ClassAfterFilter
537
597
  else
538
- raise(ActionControllerError, 'A filters must be a Symbol, Proc, Method, or object responding to filter.')
598
+ raise(ActionControllerError, 'A filter must be a Symbol, Proc, Method, or object responding to filter, after or before.')
539
599
  end
540
600
  end
541
601
 
@@ -550,8 +610,8 @@ module ActionController #:nodoc:
550
610
  return if conditions.empty?
551
611
  if conditions[:only]
552
612
  write_inheritable_hash('included_actions', condition_hash(filters, conditions[:only]))
553
- else
554
- write_inheritable_hash('excluded_actions', condition_hash(filters, conditions[:except])) if conditions[:except]
613
+ elsif conditions[:except]
614
+ write_inheritable_hash('excluded_actions', condition_hash(filters, conditions[:except]))
555
615
  end
556
616
  end
557
617
 
@@ -576,9 +636,9 @@ module ActionController #:nodoc:
576
636
 
577
637
  def remove_actions_from_included_actions!(filters,*actions)
578
638
  actions = actions.flatten.map(&:to_s)
579
- updated_hash = filters.inject(included_actions) do |hash,filter|
639
+ updated_hash = filters.inject(read_inheritable_attribute('included_actions')||{}) do |hash,filter|
580
640
  ia = (hash[filter] || []) - actions
581
- ia.blank? ? hash.delete(filter) : hash[filter] = ia
641
+ ia.empty? ? hash.delete(filter) : hash[filter] = ia
582
642
  hash
583
643
  end
584
644
  write_inheritable_attribute('included_actions', updated_hash)
@@ -595,7 +655,9 @@ module ActionController #:nodoc:
595
655
  def proxy_before_and_after_filter(filter) #:nodoc:
596
656
  return filter unless filter_responds_to_before_and_after(filter)
597
657
  Proc.new do |controller, action|
598
- unless filter.before(controller) == false
658
+ if filter.before(controller) == false
659
+ controller.send :halt_filter_chain, filter, :returned_false
660
+ else
599
661
  begin
600
662
  action.call
601
663
  ensure
@@ -615,53 +677,90 @@ module ActionController #:nodoc:
615
677
  end
616
678
  end
617
679
 
618
- def perform_action_with_filters
619
- call_filter(self.class.filter_chain, 0)
620
- end
680
+ protected
621
681
 
622
682
  def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc:
623
683
  @before_filter_chain_aborted = false
624
684
  process_without_filters(request, response, method, *arguments)
625
685
  end
626
686
 
627
- def filter_chain
628
- self.class.filter_chain
687
+ def perform_action_with_filters
688
+ call_filters(self.class.filter_chain, 0, 0)
629
689
  end
630
690
 
631
- def call_filter(chain, index)
632
- return (performed? || perform_action_without_filters) if index >= chain.size
633
- filter = chain[index]
634
- return call_filter(chain, index.next) if self.class.filter_excluded_from_action?(filter,action_name)
691
+ private
635
692
 
636
- halted = false
637
- filter.call(self) do
638
- halted = call_filter(chain, index.next)
693
+ def call_filters(chain, index, nesting)
694
+ index = run_before_filters(chain, index, nesting)
695
+ aborted = @before_filter_chain_aborted
696
+ perform_action_without_filters unless performed? || aborted
697
+ return index if nesting != 0 || aborted
698
+ run_after_filters(chain, index)
699
+ end
700
+
701
+ def skip_excluded_filters(chain, index)
702
+ while (filter = chain[index]) && self.class.filter_excluded_from_action?(filter, action_name)
703
+ index = index.next
639
704
  end
640
- halt_filter_chain(filter.filter, :no_yield) if halted == false unless @before_filter_chain_aborted
641
- halted
705
+ [filter, index]
642
706
  end
643
707
 
644
- def halt_filter_chain(filter, reason)
645
- if logger
646
- case reason
647
- when :no_yield
648
- logger.info "Filter chain halted as [#{filter.inspect}] did not yield."
649
- when :returned_false
650
- logger.info "Filter chain halted as [#{filter.inspect}] returned false."
708
+ def run_before_filters(chain, index, nesting)
709
+ while chain[index]
710
+ filter, index = skip_excluded_filters(chain, index)
711
+ break unless filter # end of call chain reached
712
+ case filter.type
713
+ when :before
714
+ filter.run(self) # invoke before filter
715
+ index = index.next
716
+ break if @before_filter_chain_aborted
717
+ when :around
718
+ yielded = false
719
+ filter.call(self) do
720
+ yielded = true
721
+ # all remaining before and around filters will be run in this call
722
+ index = call_filters(chain, index.next, nesting.next)
723
+ end
724
+ halt_filter_chain(filter, :did_not_yield) unless yielded
725
+ break
726
+ else
727
+ break # no before or around filters left
651
728
  end
652
729
  end
653
- @before_filter_chain_aborted = true
654
- return false
730
+ index
655
731
  end
656
732
 
657
- private
658
- def process_cleanup_with_filters
659
- if @before_filter_chain_aborted
660
- close_session
733
+ def run_after_filters(chain, index)
734
+ seen_after_filter = false
735
+ while chain[index]
736
+ filter, index = skip_excluded_filters(chain, index)
737
+ break unless filter # end of call chain reached
738
+ case filter.type
739
+ when :after
740
+ seen_after_filter = true
741
+ filter.run(self) # invoke after filter
661
742
  else
662
- process_cleanup_without_filters
743
+ # implementation error or someone has mucked with the filter chain
744
+ raise ActionControllerError, "filter #{filter.inspect} was in the wrong place!" if seen_after_filter
663
745
  end
746
+ index = index.next
747
+ end
748
+ index.next
749
+ end
750
+
751
+ def halt_filter_chain(filter, reason)
752
+ @before_filter_chain_aborted = true
753
+ logger.info "Filter chain halted as [#{filter.inspect}] #{reason}." if logger
754
+ false
755
+ end
756
+
757
+ def process_cleanup_with_filters
758
+ if @before_filter_chain_aborted
759
+ close_session
760
+ else
761
+ process_cleanup_without_filters
664
762
  end
763
+ end
665
764
  end
666
765
  end
667
766
  end