actionpack 1.12.5 → 1.13.0

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 (179) hide show
  1. data/CHANGELOG +517 -15
  2. data/MIT-LICENSE +1 -1
  3. data/README +18 -20
  4. data/Rakefile +7 -4
  5. data/examples/address_book_controller.rb +3 -3
  6. data/examples/blog_controller.cgi +3 -3
  7. data/examples/debate_controller.cgi +5 -5
  8. data/lib/action_controller.rb +2 -2
  9. data/lib/action_controller/assertions.rb +73 -311
  10. data/lib/action_controller/{deprecated_assertions.rb → assertions/deprecated_assertions.rb} +32 -8
  11. data/lib/action_controller/assertions/dom_assertions.rb +25 -0
  12. data/lib/action_controller/assertions/model_assertions.rb +12 -0
  13. data/lib/action_controller/assertions/response_assertions.rb +140 -0
  14. data/lib/action_controller/assertions/routing_assertions.rb +82 -0
  15. data/lib/action_controller/assertions/selector_assertions.rb +571 -0
  16. data/lib/action_controller/assertions/tag_assertions.rb +117 -0
  17. data/lib/action_controller/base.rb +334 -163
  18. data/lib/action_controller/benchmarking.rb +3 -6
  19. data/lib/action_controller/caching.rb +83 -22
  20. data/lib/action_controller/cgi_ext/cgi_ext.rb +0 -7
  21. data/lib/action_controller/cgi_ext/cgi_methods.rb +167 -173
  22. data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +43 -22
  23. data/lib/action_controller/cgi_process.rb +50 -27
  24. data/lib/action_controller/components.rb +21 -25
  25. data/lib/action_controller/cookies.rb +10 -9
  26. data/lib/action_controller/{dependencies.rb → deprecated_dependencies.rb} +9 -27
  27. data/lib/action_controller/filters.rb +448 -225
  28. data/lib/action_controller/flash.rb +24 -20
  29. data/lib/action_controller/helpers.rb +2 -5
  30. data/lib/action_controller/integration.rb +40 -16
  31. data/lib/action_controller/layout.rb +11 -8
  32. data/lib/action_controller/macros/auto_complete.rb +3 -2
  33. data/lib/action_controller/macros/in_place_editing.rb +3 -2
  34. data/lib/action_controller/mime_responds.rb +41 -29
  35. data/lib/action_controller/mime_type.rb +68 -10
  36. data/lib/action_controller/pagination.rb +4 -3
  37. data/lib/action_controller/request.rb +22 -14
  38. data/lib/action_controller/rescue.rb +25 -22
  39. data/lib/action_controller/resources.rb +302 -0
  40. data/lib/action_controller/response.rb +20 -2
  41. data/lib/action_controller/response.rb.rej +17 -0
  42. data/lib/action_controller/routing.rb +1165 -567
  43. data/lib/action_controller/scaffolding.rb +30 -31
  44. data/lib/action_controller/session/active_record_store.rb +2 -0
  45. data/lib/action_controller/session/drb_store.rb +4 -0
  46. data/lib/action_controller/session/mem_cache_store.rb +4 -0
  47. data/lib/action_controller/session_management.rb +6 -9
  48. data/lib/action_controller/status_codes.rb +89 -0
  49. data/lib/action_controller/streaming.rb +6 -15
  50. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +5 -5
  51. data/lib/action_controller/templates/rescues/diagnostics.rhtml +2 -2
  52. data/lib/action_controller/templates/rescues/routing_error.rhtml +4 -4
  53. data/lib/action_controller/templates/rescues/template_error.rhtml +1 -1
  54. data/lib/action_controller/templates/scaffolds/list.rhtml +1 -1
  55. data/lib/action_controller/test_process.rb +52 -30
  56. data/lib/action_controller/url_rewriter.rb +63 -29
  57. data/lib/action_controller/vendor/html-scanner/html/document.rb +1 -0
  58. data/lib/action_controller/vendor/html-scanner/html/node.rb +3 -4
  59. data/lib/action_controller/vendor/html-scanner/html/selector.rb +822 -0
  60. data/lib/action_controller/verification.rb +22 -11
  61. data/lib/action_pack.rb +1 -1
  62. data/lib/action_pack/version.rb +2 -2
  63. data/lib/action_view.rb +1 -1
  64. data/lib/action_view/base.rb +46 -43
  65. data/lib/action_view/compiled_templates.rb +1 -1
  66. data/lib/action_view/helpers/active_record_helper.rb +54 -17
  67. data/lib/action_view/helpers/asset_tag_helper.rb +97 -46
  68. data/lib/action_view/helpers/capture_helper.rb +1 -1
  69. data/lib/action_view/helpers/date_helper.rb +258 -136
  70. data/lib/action_view/helpers/debug_helper.rb +1 -1
  71. data/lib/action_view/helpers/deprecated_helper.rb +34 -0
  72. data/lib/action_view/helpers/form_helper.rb +75 -35
  73. data/lib/action_view/helpers/form_options_helper.rb +7 -5
  74. data/lib/action_view/helpers/form_tag_helper.rb +44 -6
  75. data/lib/action_view/helpers/java_script_macros_helper.rb +59 -46
  76. data/lib/action_view/helpers/javascript_helper.rb +71 -10
  77. data/lib/action_view/helpers/javascripts/controls.js +41 -23
  78. data/lib/action_view/helpers/javascripts/dragdrop.js +105 -76
  79. data/lib/action_view/helpers/javascripts/effects.js +293 -163
  80. data/lib/action_view/helpers/javascripts/prototype.js +897 -389
  81. data/lib/action_view/helpers/javascripts/prototype.js.rej +561 -0
  82. data/lib/action_view/helpers/number_helper.rb +111 -65
  83. data/lib/action_view/helpers/prototype_helper.rb +84 -109
  84. data/lib/action_view/helpers/scriptaculous_helper.rb +5 -0
  85. data/lib/action_view/helpers/tag_helper.rb +69 -16
  86. data/lib/action_view/helpers/text_helper.rb +149 -112
  87. data/lib/action_view/helpers/url_helper.rb +200 -107
  88. data/lib/action_view/template_error.rb +66 -42
  89. data/test/abstract_unit.rb +4 -2
  90. data/test/active_record_unit.rb +84 -56
  91. data/test/activerecord/active_record_assertions_test.rb +26 -18
  92. data/test/activerecord/active_record_store_test.rb +4 -36
  93. data/test/activerecord/pagination_test.rb +1 -6
  94. data/test/controller/action_pack_assertions_test.rb +230 -113
  95. data/test/controller/addresses_render_test.rb +2 -6
  96. data/test/controller/assert_select_test.rb +576 -0
  97. data/test/controller/base_test.rb +73 -3
  98. data/test/controller/caching_test.rb +228 -0
  99. data/test/controller/capture_test.rb +12 -10
  100. data/test/controller/cgi_test.rb +89 -12
  101. data/test/controller/components_test.rb +24 -2
  102. data/test/controller/content_type_test.rb +139 -0
  103. data/test/controller/controller_fixtures/app/controllers/admin/user_controller.rb +0 -0
  104. data/test/controller/controller_fixtures/app/controllers/user_controller.rb +0 -0
  105. data/test/controller/controller_fixtures/vendor/plugins/bad_plugin/lib/plugin_controller.rb +0 -0
  106. data/test/controller/cookie_test.rb +33 -25
  107. data/test/controller/deprecated_instance_variables_test.rb +48 -0
  108. data/test/controller/deprecation/deprecated_base_methods_test.rb +60 -0
  109. data/test/controller/fake_controllers.rb +0 -1
  110. data/test/controller/filters_test.rb +301 -16
  111. data/test/controller/flash_test.rb +19 -2
  112. data/test/controller/helper_test.rb +2 -2
  113. data/test/controller/integration_test.rb +154 -0
  114. data/test/controller/layout_test.rb +115 -1
  115. data/test/controller/mime_responds_test.rb +94 -0
  116. data/test/controller/mime_type_test.rb +9 -0
  117. data/test/controller/new_render_test.rb +161 -11
  118. data/test/controller/raw_post_test.rb +52 -15
  119. data/test/controller/redirect_test.rb +27 -14
  120. data/test/controller/render_test.rb +76 -29
  121. data/test/controller/request_test.rb +55 -4
  122. data/test/controller/resources_test.rb +274 -0
  123. data/test/controller/routing_test.rb +1533 -824
  124. data/test/controller/selector_test.rb +628 -0
  125. data/test/controller/send_file_test.rb +9 -1
  126. data/test/controller/session_management_test.rb +51 -0
  127. data/test/controller/test_test.rb +113 -29
  128. data/test/controller/url_rewriter_test.rb +86 -17
  129. data/test/controller/verification_test.rb +19 -17
  130. data/test/controller/webservice_test.rb +0 -7
  131. data/test/fixtures/content_type/render_default_content_types_for_respond_to.rhtml +1 -0
  132. data/test/fixtures/content_type/render_default_for_rhtml.rhtml +1 -0
  133. data/test/fixtures/content_type/render_default_for_rjs.rjs +1 -0
  134. data/test/fixtures/content_type/render_default_for_rxml.rxml +1 -0
  135. data/test/fixtures/deprecated_instance_variables/_cookies_ivar.rhtml +1 -0
  136. data/test/fixtures/deprecated_instance_variables/_cookies_method.rhtml +1 -0
  137. data/test/fixtures/deprecated_instance_variables/_flash_ivar.rhtml +1 -0
  138. data/test/fixtures/deprecated_instance_variables/_flash_method.rhtml +1 -0
  139. data/test/fixtures/deprecated_instance_variables/_headers_ivar.rhtml +1 -0
  140. data/test/fixtures/deprecated_instance_variables/_headers_method.rhtml +1 -0
  141. data/test/fixtures/deprecated_instance_variables/_params_ivar.rhtml +1 -0
  142. data/test/fixtures/deprecated_instance_variables/_params_method.rhtml +1 -0
  143. data/test/fixtures/deprecated_instance_variables/_request_ivar.rhtml +1 -0
  144. data/test/fixtures/deprecated_instance_variables/_request_method.rhtml +1 -0
  145. data/test/fixtures/deprecated_instance_variables/_response_ivar.rhtml +1 -0
  146. data/test/fixtures/deprecated_instance_variables/_response_method.rhtml +1 -0
  147. data/test/fixtures/deprecated_instance_variables/_session_ivar.rhtml +1 -0
  148. data/test/fixtures/deprecated_instance_variables/_session_method.rhtml +1 -0
  149. data/test/fixtures/multipart/binary_file +0 -0
  150. data/test/fixtures/public/javascripts/application.js +1 -0
  151. data/test/fixtures/test/_hello.rxml +1 -0
  152. data/test/fixtures/test/hello_world_container.rxml +3 -0
  153. data/test/fixtures/topic.rb +2 -2
  154. data/test/template/active_record_helper_test.rb +83 -12
  155. data/test/template/asset_tag_helper_test.rb +75 -95
  156. data/test/template/compiled_templates_test.rb +1 -0
  157. data/test/template/date_helper_test.rb +873 -181
  158. data/test/template/deprecated_helper_test.rb +36 -0
  159. data/test/template/deprecated_instance_variables_test.rb +43 -0
  160. data/test/template/form_helper_test.rb +77 -1
  161. data/test/template/form_options_helper_test.rb +4 -0
  162. data/test/template/form_tag_helper_test.rb +66 -2
  163. data/test/template/java_script_macros_helper_test.rb +4 -1
  164. data/test/template/javascript_helper_test.rb +29 -0
  165. data/test/template/number_helper_test.rb +63 -27
  166. data/test/template/prototype_helper_test.rb +77 -34
  167. data/test/template/tag_helper_test.rb +34 -6
  168. data/test/template/text_helper_test.rb +69 -34
  169. data/test/template/url_helper_test.rb +168 -16
  170. data/test/testing_sandbox.rb +7 -22
  171. metadata +66 -20
  172. data/filler.txt +0 -50
  173. data/lib/action_controller/code_generation.rb +0 -235
  174. data/lib/action_controller/vendor/xml_simple.rb +0 -1019
  175. data/test/controller/caching_filestore.rb +0 -74
  176. data/test/fixtures/application_root/app/controllers/a_class_that_contains_a_controller/poorly_placed_controller.rb +0 -7
  177. data/test/fixtures/application_root/app/controllers/module_that_holds_controllers/nested_controller.rb +0 -3
  178. data/test/fixtures/application_root/app/models/a_class_that_contains_a_controller.rb +0 -7
  179. data/test/fixtures/dont_load.rb +0 -3
@@ -8,11 +8,8 @@ module ActionController #:nodoc:
8
8
  base.extend(ClassMethods)
9
9
 
10
10
  base.class_eval do
11
- alias_method :perform_action_without_benchmark, :perform_action
12
- alias_method :perform_action, :perform_action_with_benchmark
13
-
14
- alias_method :render_without_benchmark, :render
15
- alias_method :render, :render_with_benchmark
11
+ alias_method_chain :perform_action, :benchmark
12
+ alias_method_chain :render, :benchmark
16
13
  end
17
14
  end
18
15
 
@@ -68,7 +65,7 @@ module ActionController #:nodoc:
68
65
  else
69
66
  runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max
70
67
  log_message = "Completed in #{sprintf("%.5f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
71
- log_message << rendering_runtime(runtime) if @rendering_runtime
68
+ log_message << rendering_runtime(runtime) if defined?(@rendering_runtime)
72
69
  log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
73
70
  log_message << " | #{headers["Status"]}"
74
71
  log_message << " [#{complete_request_uri rescue "unknown"}]"
@@ -1,4 +1,5 @@
1
1
  require 'fileutils'
2
+ require 'uri'
2
3
 
3
4
  module ActionController #:nodoc:
4
5
  # Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls
@@ -117,24 +118,24 @@ module ActionController #:nodoc:
117
118
  return unless perform_caching
118
119
  if options[:action].is_a?(Array)
119
120
  options[:action].dup.each do |action|
120
- self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true, :action => action })))
121
+ self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :action => action)))
121
122
  end
122
123
  else
123
- self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })))
124
+ self.class.expire_page(url_for(options.merge(:only_path => true, :skip_relative_url_root => true)))
124
125
  end
125
126
  end
126
127
 
127
- # Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of @response.body is used
128
+ # Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of response.body is used
128
129
  # If no options are provided, the current +options+ for this action is used. Example:
129
130
  # cache_page "I'm the cached content", :controller => "lists", :action => "show"
130
131
  def cache_page(content = nil, options = {})
131
132
  return unless perform_caching && caching_allowed
132
- self.class.cache_page(content || @response.body, url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })))
133
+ self.class.cache_page(content || response.body, url_for(options.merge(:only_path => true, :skip_relative_url_root => true, :format => params[:format])))
133
134
  end
134
135
 
135
136
  private
136
137
  def caching_allowed
137
- !@request.post? && @response.headers['Status'] && @response.headers['Status'].to_i < 400
138
+ request.get? && response.headers['Status'].to_i == 200
138
139
  end
139
140
  end
140
141
 
@@ -155,9 +156,12 @@ module ActionController #:nodoc:
155
156
  # the current host and the path. So a page that is accessed at http://david.somewhere.com/lists/show/1 will result in a fragment named
156
157
  # "david.somewhere.com/lists/show/1". This allows the cacher to differentiate between "david.somewhere.com/lists/" and
157
158
  # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key pattern.
159
+ #
160
+ # Different representations of the same resource, e.g. <tt>http://david.somewhere.com/lists</tt> and <tt>http://david.somewhere.com/lists.xml</tt>
161
+ # are treated like separate requests and so are cached separately. Keep in mind when expiring an action cache that <tt>:action => 'lists'</tt> is not the same
162
+ # as <tt>:action => 'list', :format => :xml</tt>.
158
163
  module Actions
159
- def self.append_features(base) #:nodoc:
160
- super
164
+ def self.included(base) #:nodoc:
161
165
  base.extend(ClassMethods)
162
166
  base.send(:attr_accessor, :rendered_action_cache)
163
167
  end
@@ -173,22 +177,24 @@ module ActionController #:nodoc:
173
177
  return unless perform_caching
174
178
  if options[:action].is_a?(Array)
175
179
  options[:action].dup.each do |action|
176
- expire_fragment(url_for(options.merge({ :action => action })).split("://").last)
180
+ expire_fragment(ActionCachePath.path_for(self, options.merge({ :action => action })))
177
181
  end
178
182
  else
179
- expire_fragment(url_for(options).split("://").last)
183
+ expire_fragment(ActionCachePath.path_for(self, options))
180
184
  end
181
185
  end
182
186
 
183
187
  class ActionCacheFilter #:nodoc:
184
- def initialize(*actions)
188
+ def initialize(*actions, &block)
185
189
  @actions = actions
186
190
  end
187
191
 
188
192
  def before(controller)
189
193
  return unless @actions.include?(controller.action_name.intern)
190
- if cache = controller.read_fragment(controller.url_for.split("://").last)
194
+ action_cache_path = ActionCachePath.new(controller)
195
+ if cache = controller.read_fragment(action_cache_path.path)
191
196
  controller.rendered_action_cache = true
197
+ set_content_type!(action_cache_path)
192
198
  controller.send(:render_text, cache)
193
199
  false
194
200
  end
@@ -196,8 +202,60 @@ module ActionController #:nodoc:
196
202
 
197
203
  def after(controller)
198
204
  return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache
199
- controller.write_fragment(controller.url_for.split("://").last, controller.response.body)
205
+ controller.write_fragment(ActionCachePath.path_for(controller), controller.response.body)
200
206
  end
207
+
208
+ 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
215
+ end
216
+
217
+ end
218
+
219
+ class ActionCachePath
220
+ attr_reader :controller, :options
221
+
222
+ class << self
223
+ def path_for(*args, &block)
224
+ new(*args).path
225
+ end
226
+ end
227
+
228
+ 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)
243
+ end
244
+
245
+ private
246
+ def normalize!
247
+ @path << 'index' if @path.last == '/'
248
+ end
249
+
250
+ def add_extension!
251
+ @path << ".#{extension}" if extension
252
+ end
253
+
254
+ def extract_extension(file_path)
255
+ # Don't want just what comes after the last '.' to accomodate multi part extensions
256
+ # such as tar.gz.
257
+ file_path[/^[^.]+\.(.+)$/, 1]
258
+ end
201
259
  end
202
260
  end
203
261
 
@@ -208,7 +266,7 @@ module ActionController #:nodoc:
208
266
  # <b>Hello <%= @name %></b>
209
267
  # <% cache do %>
210
268
  # All the topics in the system:
211
- # <%= render_collection_of_partials "topic", Topic.find_all %>
269
+ # <%= render :partial => "topic", :collection => Topic.find(:all) %>
212
270
  # <% end %>
213
271
  #
214
272
  # This cache will bind to the name of action that called it. So you would be able to invalidate it using
@@ -246,8 +304,7 @@ module ActionController #:nodoc:
246
304
  # ActionController::Base.fragment_cache_store = :mem_cache_store, "localhost"
247
305
  # ActionController::Base.fragment_cache_store = MyOwnStore.new("parameter")
248
306
  module Fragments
249
- def self.append_features(base) #:nodoc:
250
- super
307
+ def self.included(base) #:nodoc:
251
308
  base.class_eval do
252
309
  @@fragment_cache_store = MemoryStore.new
253
310
  cattr_reader :fragment_cache_store
@@ -306,7 +363,12 @@ module ActionController #:nodoc:
306
363
  # Name can take one of three forms:
307
364
  # * String: This would normally take the form of a path like "pages/45/notes"
308
365
  # * Hash: Is treated as an implicit call to url_for, like { :controller => "pages", :action => "notes", :id => 45 }
309
- # * Regexp: Will destroy all the matched fragments, example: %r{pages/\d*/notes} Ensure you do not specify start and finish in the regex (^$) because the actual filename matched looks like ./cache/filename/path.cache
366
+ # * Regexp: Will destroy all the matched fragments, example:
367
+ # %r{pages/\d*/notes}
368
+ # Ensure you do not specify start and finish in the regex (^$) because
369
+ # the actual filename matched looks like ./cache/filename/path.cache
370
+ # Regexp expiration is not supported on caches which can't iterate over
371
+ # all keys, such as memcached.
310
372
  def expire_fragment(name, options = nil)
311
373
  return unless perform_caching
312
374
 
@@ -327,6 +389,7 @@ module ActionController #:nodoc:
327
389
  def expire_matched_fragments(matcher = /.*/, options = nil) #:nodoc:
328
390
  expire_fragment(matcher, options)
329
391
  end
392
+ deprecate :expire_matched_fragments => :expire_fragment
330
393
 
331
394
 
332
395
  class UnthreadedMemoryStore #:nodoc:
@@ -430,7 +493,7 @@ module ActionController #:nodoc:
430
493
  if f =~ matcher
431
494
  begin
432
495
  File.delete(f)
433
- rescue Object => e
496
+ rescue SystemCallError => e
434
497
  # If there's no cache, then there's nothing to complain about
435
498
  end
436
499
  end
@@ -493,8 +556,7 @@ module ActionController #:nodoc:
493
556
  #
494
557
  # In the example above, four actions are cached and three actions are responsible for expiring those caches.
495
558
  module Sweeping
496
- def self.append_features(base) #:nodoc:
497
- super
559
+ def self.included(base) #:nodoc:
498
560
  base.extend(ClassMethods)
499
561
  end
500
562
 
@@ -503,8 +565,7 @@ module ActionController #:nodoc:
503
565
  return unless perform_caching
504
566
  configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {}
505
567
  sweepers.each do |sweeper|
506
- observer(sweeper)
507
-
568
+ ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base)
508
569
  sweeper_instance = Object.const_get(Inflector.classify(sweeper)).instance
509
570
 
510
571
  if sweeper_instance.is_a?(Sweeper)
@@ -523,7 +584,7 @@ module ActionController #:nodoc:
523
584
 
524
585
  # ActiveRecord::Observer will mark this class as reloadable even though it should not be.
525
586
  # However, subclasses of ActionController::Caching::Sweeper should be Reloadable
526
- include Reloadable::Subclasses
587
+ include Reloadable::Deprecated
527
588
 
528
589
  def before(controller)
529
590
  self.controller = controller
@@ -27,13 +27,6 @@ class CGI #:nodoc:
27
27
  def request_parameters
28
28
  CGIMethods.parse_request_parameters(params, env_table)
29
29
  end
30
-
31
- def redirect(where)
32
- header({
33
- "Status" => "302 Moved",
34
- "location" => "#{where}"
35
- })
36
- end
37
30
 
38
31
  def session(parameters = nil)
39
32
  parameters = {} if parameters.nil?
@@ -1,217 +1,211 @@
1
1
  require 'cgi'
2
- require 'action_controller/vendor/xml_simple'
3
2
  require 'action_controller/vendor/xml_node'
3
+ require 'strscan'
4
4
 
5
5
  # Static methods for parsing the query and request parameters that can be used in
6
6
  # a CGI extension class or testing in isolation.
7
7
  class CGIMethods #:nodoc:
8
- public
9
- # Returns a hash with the pairs from the query string. The implicit hash construction that is done in
10
- # parse_request_params is not done here.
11
- def CGIMethods.parse_query_parameters(query_string)
12
- parsed_params = {}
13
-
14
- query_string.split(/[&;]/).each { |p|
15
- # Ignore repeated delimiters.
16
- next if p.empty?
17
-
18
- k, v = p.split('=',2)
19
- v = nil if (v && v.empty?)
20
-
21
- k = CGI.unescape(k) if k
22
- v = CGI.unescape(v) if v
23
-
24
- unless k.include?(?[)
25
- parsed_params[k] = v
26
- else
27
- keys = split_key(k)
28
- last_key = keys.pop
29
- last_key = keys.pop if (use_array = last_key.empty?)
30
- parent = keys.inject(parsed_params) {|h, k| h[k] ||= {}}
31
-
32
- if use_array then (parent[last_key] ||= []) << v
33
- else parent[last_key] = v
34
- end
35
- end
36
- }
37
-
38
- parsed_params
8
+ class << self
9
+ # DEPRECATED: Use parse_form_encoded_parameters
10
+ def parse_query_parameters(query_string)
11
+ pairs = query_string.split('&').collect do |chunk|
12
+ next if chunk.empty?
13
+ key, value = chunk.split('=', 2)
14
+ next if key.empty?
15
+ value = (value.nil? || value.empty?) ? nil : CGI.unescape(value)
16
+ [ CGI.unescape(key), value ]
17
+ end.compact
18
+
19
+ FormEncodedPairParser.new(pairs).result
39
20
  end
40
21
 
41
- # Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" /
42
- # "Somewhere cool!" are translated into a full hash hierarchy, like
43
- # { "customer" => { "address" => { "street" => "Somewhere cool!" } } }
44
- def CGIMethods.parse_request_parameters(params)
45
- parsed_params = {}
46
-
47
- for key, value in params
48
- value = [value] if key =~ /.*\[\]$/
49
- unless key.include?('[')
50
- # much faster to test for the most common case first (GET)
51
- # and avoid the call to build_deep_hash
52
- parsed_params[key] = get_typed_value(value[0])
53
- else
54
- build_deep_hash(get_typed_value(value[0]), parsed_params, get_levels(key))
22
+ # DEPRECATED: Use parse_form_encoded_parameters
23
+ def parse_request_parameters(params)
24
+ parser = FormEncodedPairParser.new
25
+
26
+ params = params.dup
27
+ until params.empty?
28
+ for key, value in params
29
+ if key.blank?
30
+ params.delete key
31
+ elsif !key.include?('[')
32
+ # much faster to test for the most common case first (GET)
33
+ # and avoid the call to build_deep_hash
34
+ parser.result[key] = get_typed_value(value[0])
35
+ params.delete key
36
+ elsif value.is_a?(Array)
37
+ parser.parse(key, get_typed_value(value.shift))
38
+ params.delete key if value.empty?
39
+ else
40
+ raise TypeError, "Expected array, found #{value.inspect}"
41
+ end
55
42
  end
56
43
  end
57
44
 
58
- parsed_params
45
+ parser.result
59
46
  end
60
47
 
61
- def self.parse_formatted_request_parameters(mime_type, raw_post_data)
62
- params = case strategy = ActionController::Base.param_parsers[mime_type]
48
+ def parse_formatted_request_parameters(mime_type, raw_post_data)
49
+ case strategy = ActionController::Base.param_parsers[mime_type]
63
50
  when Proc
64
51
  strategy.call(raw_post_data)
65
52
  when :xml_simple
66
- raw_post_data.blank? ? nil :
67
- typecast_xml_value(XmlSimple.xml_in(raw_post_data,
68
- 'forcearray' => false,
69
- 'forcecontent' => true,
70
- 'keeproot' => true,
71
- 'contentkey' => '__content__'))
53
+ raw_post_data.blank? ? {} : Hash.from_xml(raw_post_data)
72
54
  when :yaml
73
55
  YAML.load(raw_post_data)
74
56
  when :xml_node
75
57
  node = XmlNode.from_xml(raw_post_data)
76
58
  { node.node_name => node }
77
59
  end
78
-
79
- dasherize_keys(params || {})
80
- rescue Object => e
60
+ rescue Exception => e # YAML, XML or Ruby code block errors
81
61
  { "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
82
62
  "raw_post_data" => raw_post_data, "format" => mime_type }
83
63
  end
84
64
 
85
- def self.typecast_xml_value(value)
86
- case value
87
- when Hash
88
- if value.has_key?("__content__")
89
- content = translate_xml_entities(value["__content__"])
90
- case value["type"]
91
- when "integer" then content.to_i
92
- when "boolean" then content == "true"
93
- when "datetime" then Time.parse(content)
94
- when "date" then Date.parse(content)
95
- else content
96
- end
97
- else
98
- value.empty? ? nil : value.inject({}) do |h,(k,v)|
99
- h[k] = typecast_xml_value(v)
100
- h
101
- end
102
- end
103
- when Array
104
- value.map! { |i| typecast_xml_value(i) }
105
- case value.length
106
- when 0 then nil
107
- when 1 then value.first
108
- else value
65
+ private
66
+ def get_typed_value(value)
67
+ case value
68
+ when String
69
+ value
70
+ when NilClass
71
+ ''
72
+ when Array
73
+ value.map { |v| get_typed_value(v) }
74
+ else
75
+ # Uploaded file provides content type and filename.
76
+ if value.respond_to?(:content_type) &&
77
+ !value.content_type.blank? &&
78
+ !value.original_filename.blank?
79
+ unless value.respond_to?(:full_original_filename)
80
+ class << value
81
+ alias_method :full_original_filename, :original_filename
82
+
83
+ # Take the basename of the upload's original filename.
84
+ # This handles the full Windows paths given by Internet Explorer
85
+ # (and perhaps other broken user agents) without affecting
86
+ # those which give the lone filename.
87
+ # The Windows regexp is adapted from Perl's File::Basename.
88
+ def original_filename
89
+ if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
90
+ md.captures.first
91
+ else
92
+ File.basename full_original_filename
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ # Return the same value after overriding original_filename.
99
+ value
100
+
101
+ # Multipart values may have content type, but no filename.
102
+ elsif value.respond_to?(:read)
103
+ result = value.read
104
+ value.rewind
105
+ result
106
+
107
+ # Unknown value, neither string nor multipart.
108
+ else
109
+ raise "Unknown form value: #{value.inspect}"
110
+ end
109
111
  end
110
- else
111
- raise "can't typecast #{value.inspect}"
112
112
  end
113
- end
113
+ end
114
114
 
115
- private
115
+ class FormEncodedPairParser < StringScanner
116
+ attr_reader :top, :parent, :result
116
117
 
117
- def self.translate_xml_entities(value)
118
- value.gsub(/&lt;/, "<").
119
- gsub(/&gt;/, ">").
120
- gsub(/&quot;/, '"').
121
- gsub(/&apos;/, "'").
122
- gsub(/&amp;/, "&")
118
+ def initialize(pairs = [])
119
+ super('')
120
+ @result = {}
121
+ pairs.each { |key, value| parse(key, value) }
123
122
  end
124
-
125
- def self.dasherize_keys(params)
126
- case params.class.to_s
127
- when "Hash"
128
- params.inject({}) do |h,(k,v)|
129
- h[k.to_s.tr("-", "_")] = dasherize_keys(v)
130
- h
131
- end
132
- when "Array"
133
- params.map { |v| dasherize_keys(v) }
134
- else
135
- params
123
+
124
+ KEY_REGEXP = %r{([^\[\]=&]+)}
125
+ BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
126
+
127
+ # Parse the query string
128
+ def parse(key, value)
129
+ self.string = key
130
+ @top, @parent = result, nil
131
+
132
+ # First scan the bare key
133
+ key = scan(KEY_REGEXP) or return
134
+ key = post_key_check(key)
135
+
136
+ # Then scan as many nestings as present
137
+ until eos?
138
+ r = scan(BRACKETED_KEY_REGEXP) or return
139
+ key = self[1]
140
+ key = post_key_check(key)
136
141
  end
142
+
143
+ bind(key, value)
137
144
  end
138
145
 
139
- # Splits the given key into several pieces. Example keys are 'name', 'person[name]',
140
- # 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned.
141
- # 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', '']
142
- def CGIMethods.split_key(key)
143
- if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key
144
- keys = [$1]
145
-
146
- keys.concat($2[1..-2].split(']['))
147
- keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings
148
-
149
- keys
150
- else
151
- [key]
146
+ private
147
+ # After we see a key, we must look ahead to determine our next action. Cases:
148
+ #
149
+ # [] follows the key. Then the value must be an array.
150
+ # = follows the key. (A value comes next)
151
+ # & or the end of string follows the key. Then the key is a flag.
152
+ # otherwise, a hash follows the key.
153
+ def post_key_check(key)
154
+ if scan(/\[\]/) # a[b][] indicates that b is an array
155
+ container(key, Array)
156
+ nil
157
+ elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
158
+ container(key, Hash)
159
+ nil
160
+ else # End of key? We do nothing.
161
+ key
162
+ end
152
163
  end
153
- end
154
164
 
155
- def CGIMethods.get_typed_value(value)
156
- # test most frequent case first
157
- if value.is_a?(String)
158
- value
159
- elsif value.respond_to?(:content_type) && ! value.content_type.blank?
160
- # Uploaded file
161
- unless value.respond_to?(:full_original_filename)
162
- class << value
163
- alias_method :full_original_filename, :original_filename
164
-
165
- # Take the basename of the upload's original filename.
166
- # This handles the full Windows paths given by Internet Explorer
167
- # (and perhaps other broken user agents) without affecting
168
- # those which give the lone filename.
169
- # The Windows regexp is adapted from Perl's File::Basename.
170
- def original_filename
171
- if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
172
- md.captures.first
173
- else
174
- File.basename full_original_filename
175
- end
165
+ # Add a container to the stack.
166
+ #
167
+ def container(key, klass)
168
+ type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
169
+ value = bind(key, klass.new)
170
+ type_conflict! klass, value unless value.is_a?(klass)
171
+ push(value)
172
+ end
173
+
174
+ # Push a value onto the 'stack', which is actually only the top 2 items.
175
+ def push(value)
176
+ @parent, @top = @top, value
177
+ end
178
+
179
+ # Bind a key (which may be nil for items in an array) to the provided value.
180
+ def bind(key, value)
181
+ if top.is_a? Array
182
+ if key
183
+ if top[-1].is_a?(Hash) && ! top[-1].key?(key)
184
+ top[-1][key] = value
185
+ else
186
+ top << {key => value}.with_indifferent_access
187
+ push top.last
176
188
  end
189
+ else
190
+ top << value
177
191
  end
192
+ elsif top.is_a? Hash
193
+ key = CGI.unescape(key)
194
+ parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
195
+ return top[key] ||= value
196
+ else
197
+ raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
178
198
  end
179
199
 
180
- # Return the same value after overriding original_filename.
181
- value
182
-
183
- elsif value.respond_to?(:read)
184
- # Value as part of a multipart request
185
- value.read
186
- elsif value.class == Array
187
- value.collect { |v| CGIMethods.get_typed_value(v) }
188
- else
189
- # other value (neither string nor a multipart request)
190
- value.to_s
191
- end
192
- end
193
-
194
- PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
195
- def CGIMethods.get_levels(key)
196
- all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a
197
- if main.nil?
198
- []
199
- elsif trailing
200
- [key]
201
- elsif bracketed
202
- [main] + bracketed.slice(1...-1).split('][')
203
- else
204
- [main]
200
+ return value
205
201
  end
206
- end
207
-
208
- def CGIMethods.build_deep_hash(value, hash, levels)
209
- if levels.length == 0
210
- value
211
- elsif hash.nil?
212
- { levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) }
213
- else
214
- hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) })
202
+
203
+ def type_conflict!(klass, value)
204
+ raise TypeError,
205
+ "Conflicting types for parameter containers. " +
206
+ "Expected an instance of #{klass}, but found an instance of #{value.class}. " +
207
+ "This can be caused by passing Array and Hash based paramters qs[]=value&qs[key]=value. "
215
208
  end
209
+
216
210
  end
217
211
  end