actionpack 3.2.19 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +850 -401
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +5 -288
  5. data/lib/abstract_controller/asset_paths.rb +2 -2
  6. data/lib/abstract_controller/base.rb +39 -37
  7. data/lib/abstract_controller/callbacks.rb +101 -82
  8. data/lib/abstract_controller/collector.rb +7 -3
  9. data/lib/abstract_controller/helpers.rb +25 -13
  10. data/lib/abstract_controller/layouts.rb +74 -74
  11. data/lib/abstract_controller/logger.rb +1 -2
  12. data/lib/abstract_controller/rendering.rb +30 -13
  13. data/lib/abstract_controller/translation.rb +16 -1
  14. data/lib/abstract_controller/url_for.rb +6 -6
  15. data/lib/abstract_controller/view_paths.rb +1 -1
  16. data/lib/abstract_controller.rb +1 -8
  17. data/lib/action_controller/base.rb +46 -22
  18. data/lib/action_controller/caching/fragments.rb +23 -53
  19. data/lib/action_controller/caching.rb +46 -33
  20. data/lib/action_controller/deprecated/integration_test.rb +3 -0
  21. data/lib/action_controller/deprecated.rb +5 -1
  22. data/lib/action_controller/log_subscriber.rb +16 -8
  23. data/lib/action_controller/metal/conditional_get.rb +76 -32
  24. data/lib/action_controller/metal/data_streaming.rb +20 -26
  25. data/lib/action_controller/metal/exceptions.rb +19 -6
  26. data/lib/action_controller/metal/flash.rb +24 -9
  27. data/lib/action_controller/metal/force_ssl.rb +70 -12
  28. data/lib/action_controller/metal/head.rb +25 -4
  29. data/lib/action_controller/metal/helpers.rb +5 -9
  30. data/lib/action_controller/metal/hide_actions.rb +0 -1
  31. data/lib/action_controller/metal/http_authentication.rb +107 -83
  32. data/lib/action_controller/metal/implicit_render.rb +1 -1
  33. data/lib/action_controller/metal/instrumentation.rb +2 -1
  34. data/lib/action_controller/metal/live.rb +175 -0
  35. data/lib/action_controller/metal/mime_responds.rb +161 -47
  36. data/lib/action_controller/metal/params_wrapper.rb +112 -74
  37. data/lib/action_controller/metal/rack_delegation.rb +9 -3
  38. data/lib/action_controller/metal/redirecting.rb +15 -20
  39. data/lib/action_controller/metal/renderers.rb +11 -9
  40. data/lib/action_controller/metal/rendering.rb +9 -1
  41. data/lib/action_controller/metal/request_forgery_protection.rb +112 -19
  42. data/lib/action_controller/metal/responder.rb +20 -19
  43. data/lib/action_controller/metal/streaming.rb +12 -18
  44. data/lib/action_controller/metal/strong_parameters.rb +520 -0
  45. data/lib/action_controller/metal/testing.rb +13 -18
  46. data/lib/action_controller/metal/url_for.rb +28 -25
  47. data/lib/action_controller/metal.rb +17 -32
  48. data/lib/action_controller/model_naming.rb +12 -0
  49. data/lib/action_controller/railtie.rb +33 -17
  50. data/lib/action_controller/railties/helpers.rb +22 -0
  51. data/lib/action_controller/record_identifier.rb +18 -72
  52. data/lib/action_controller/test_case.rb +251 -131
  53. data/lib/action_controller/vendor/html-scanner.rb +4 -19
  54. data/lib/action_controller.rb +15 -6
  55. data/lib/action_dispatch/http/cache.rb +63 -11
  56. data/lib/action_dispatch/http/filter_parameters.rb +18 -8
  57. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  58. data/lib/action_dispatch/http/headers.rb +49 -17
  59. data/lib/action_dispatch/http/mime_negotiation.rb +24 -1
  60. data/lib/action_dispatch/http/mime_type.rb +154 -100
  61. data/lib/action_dispatch/http/mime_types.rb +1 -1
  62. data/lib/action_dispatch/http/parameter_filter.rb +44 -46
  63. data/lib/action_dispatch/http/parameters.rb +28 -28
  64. data/lib/action_dispatch/http/rack_cache.rb +2 -3
  65. data/lib/action_dispatch/http/request.rb +64 -18
  66. data/lib/action_dispatch/http/response.rb +130 -35
  67. data/lib/action_dispatch/http/upload.rb +63 -20
  68. data/lib/action_dispatch/http/url.rb +98 -35
  69. data/lib/action_dispatch/journey/backwards.rb +5 -0
  70. data/lib/action_dispatch/journey/formatter.rb +146 -0
  71. data/lib/action_dispatch/journey/gtg/builder.rb +162 -0
  72. data/lib/action_dispatch/journey/gtg/simulator.rb +44 -0
  73. data/lib/action_dispatch/journey/gtg/transition_table.rb +156 -0
  74. data/lib/action_dispatch/journey/nfa/builder.rb +76 -0
  75. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  76. data/lib/action_dispatch/journey/nfa/simulator.rb +47 -0
  77. data/lib/action_dispatch/journey/nfa/transition_table.rb +163 -0
  78. data/lib/action_dispatch/journey/nodes/node.rb +124 -0
  79. data/lib/action_dispatch/journey/parser.rb +206 -0
  80. data/lib/action_dispatch/journey/parser.y +47 -0
  81. data/lib/action_dispatch/journey/parser_extras.rb +23 -0
  82. data/lib/action_dispatch/journey/path/pattern.rb +196 -0
  83. data/lib/action_dispatch/journey/route.rb +124 -0
  84. data/lib/action_dispatch/journey/router/strexp.rb +24 -0
  85. data/lib/action_dispatch/journey/router/utils.rb +54 -0
  86. data/lib/action_dispatch/journey/router.rb +166 -0
  87. data/lib/action_dispatch/journey/routes.rb +75 -0
  88. data/lib/action_dispatch/journey/scanner.rb +61 -0
  89. data/lib/action_dispatch/journey/visitors.rb +197 -0
  90. data/lib/action_dispatch/journey/visualizer/fsm.css +34 -0
  91. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  92. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  93. data/lib/action_dispatch/journey.rb +5 -0
  94. data/lib/action_dispatch/middleware/callbacks.rb +9 -4
  95. data/lib/action_dispatch/middleware/cookies.rb +259 -114
  96. data/lib/action_dispatch/middleware/debug_exceptions.rb +26 -17
  97. data/lib/action_dispatch/middleware/exception_wrapper.rb +29 -3
  98. data/lib/action_dispatch/middleware/flash.rb +58 -58
  99. data/lib/action_dispatch/middleware/params_parser.rb +14 -29
  100. data/lib/action_dispatch/middleware/public_exceptions.rb +30 -14
  101. data/lib/action_dispatch/middleware/reloader.rb +6 -6
  102. data/lib/action_dispatch/middleware/remote_ip.rb +145 -39
  103. data/lib/action_dispatch/middleware/request_id.rb +2 -6
  104. data/lib/action_dispatch/middleware/session/abstract_store.rb +22 -20
  105. data/lib/action_dispatch/middleware/session/cookie_store.rb +82 -28
  106. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +8 -3
  107. data/lib/action_dispatch/middleware/show_exceptions.rb +12 -45
  108. data/lib/action_dispatch/middleware/ssl.rb +70 -0
  109. data/lib/action_dispatch/middleware/stack.rb +6 -1
  110. data/lib/action_dispatch/middleware/static.rb +2 -1
  111. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +14 -11
  112. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +25 -0
  113. data/lib/action_dispatch/middleware/templates/rescues/_trace.erb +7 -9
  114. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb +15 -9
  115. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +127 -5
  116. data/lib/action_dispatch/middleware/templates/rescues/missing_template.erb +7 -2
  117. data/lib/action_dispatch/middleware/templates/rescues/routing_error.erb +30 -15
  118. data/lib/action_dispatch/middleware/templates/rescues/template_error.erb +39 -13
  119. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb +6 -2
  120. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  121. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +144 -0
  122. data/lib/action_dispatch/railtie.rb +16 -6
  123. data/lib/action_dispatch/request/session.rb +181 -0
  124. data/lib/action_dispatch/routing/inspector.rb +240 -0
  125. data/lib/action_dispatch/routing/mapper.rb +540 -291
  126. data/lib/action_dispatch/routing/polymorphic_routes.rb +16 -20
  127. data/lib/action_dispatch/routing/redirection.rb +46 -29
  128. data/lib/action_dispatch/routing/route_set.rb +207 -164
  129. data/lib/action_dispatch/routing/routes_proxy.rb +2 -0
  130. data/lib/action_dispatch/routing/url_for.rb +48 -33
  131. data/lib/action_dispatch/routing.rb +48 -83
  132. data/lib/action_dispatch/testing/assertions/dom.rb +3 -13
  133. data/lib/action_dispatch/testing/assertions/response.rb +32 -40
  134. data/lib/action_dispatch/testing/assertions/routing.rb +42 -41
  135. data/lib/action_dispatch/testing/assertions/selector.rb +17 -22
  136. data/lib/action_dispatch/testing/assertions/tag.rb +20 -23
  137. data/lib/action_dispatch/testing/integration.rb +65 -51
  138. data/lib/action_dispatch/testing/test_process.rb +9 -6
  139. data/lib/action_dispatch/testing/test_request.rb +7 -3
  140. data/lib/action_dispatch.rb +21 -15
  141. data/lib/action_pack/version.rb +7 -6
  142. data/lib/action_pack.rb +1 -1
  143. data/lib/action_view/base.rb +15 -34
  144. data/lib/action_view/buffers.rb +7 -1
  145. data/lib/action_view/context.rb +4 -4
  146. data/lib/action_view/dependency_tracker.rb +93 -0
  147. data/lib/action_view/digestor.rb +85 -0
  148. data/lib/action_view/flows.rb +1 -4
  149. data/lib/action_view/helpers/active_model_helper.rb +3 -4
  150. data/lib/action_view/helpers/asset_tag_helper.rb +215 -352
  151. data/lib/action_view/helpers/asset_url_helper.rb +355 -0
  152. data/lib/action_view/helpers/atom_feed_helper.rb +13 -10
  153. data/lib/action_view/helpers/cache_helper.rb +150 -18
  154. data/lib/action_view/helpers/capture_helper.rb +44 -31
  155. data/lib/action_view/helpers/csrf_helper.rb +0 -2
  156. data/lib/action_view/helpers/date_helper.rb +269 -248
  157. data/lib/action_view/helpers/debug_helper.rb +10 -11
  158. data/lib/action_view/helpers/form_helper.rb +931 -537
  159. data/lib/action_view/helpers/form_options_helper.rb +341 -166
  160. data/lib/action_view/helpers/form_tag_helper.rb +190 -90
  161. data/lib/action_view/helpers/javascript_helper.rb +23 -16
  162. data/lib/action_view/helpers/number_helper.rb +148 -329
  163. data/lib/action_view/helpers/output_safety_helper.rb +3 -3
  164. data/lib/action_view/helpers/record_tag_helper.rb +17 -22
  165. data/lib/action_view/helpers/rendering_helper.rb +2 -2
  166. data/lib/action_view/helpers/sanitize_helper.rb +3 -6
  167. data/lib/action_view/helpers/tag_helper.rb +46 -33
  168. data/lib/action_view/helpers/tags/base.rb +147 -0
  169. data/lib/action_view/helpers/tags/check_box.rb +64 -0
  170. data/lib/action_view/helpers/tags/checkable.rb +16 -0
  171. data/lib/action_view/helpers/tags/collection_check_boxes.rb +43 -0
  172. data/lib/action_view/helpers/tags/collection_helpers.rb +83 -0
  173. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +36 -0
  174. data/lib/action_view/helpers/tags/collection_select.rb +28 -0
  175. data/lib/action_view/helpers/tags/color_field.rb +25 -0
  176. data/lib/action_view/helpers/tags/date_field.rb +13 -0
  177. data/lib/action_view/helpers/tags/date_select.rb +72 -0
  178. data/lib/action_view/helpers/tags/datetime_field.rb +22 -0
  179. data/lib/action_view/helpers/tags/datetime_local_field.rb +19 -0
  180. data/lib/action_view/helpers/tags/datetime_select.rb +8 -0
  181. data/lib/action_view/helpers/tags/email_field.rb +8 -0
  182. data/lib/action_view/helpers/tags/file_field.rb +8 -0
  183. data/lib/action_view/helpers/tags/grouped_collection_select.rb +29 -0
  184. data/lib/action_view/helpers/tags/hidden_field.rb +8 -0
  185. data/lib/action_view/helpers/tags/label.rb +65 -0
  186. data/lib/action_view/helpers/tags/month_field.rb +13 -0
  187. data/lib/action_view/helpers/tags/number_field.rb +18 -0
  188. data/lib/action_view/helpers/tags/password_field.rb +12 -0
  189. data/lib/action_view/helpers/tags/radio_button.rb +31 -0
  190. data/lib/action_view/helpers/tags/range_field.rb +8 -0
  191. data/lib/action_view/helpers/tags/search_field.rb +24 -0
  192. data/lib/action_view/helpers/tags/select.rb +40 -0
  193. data/lib/action_view/helpers/tags/tel_field.rb +8 -0
  194. data/lib/action_view/helpers/tags/text_area.rb +18 -0
  195. data/lib/action_view/helpers/tags/text_field.rb +29 -0
  196. data/lib/action_view/helpers/tags/time_field.rb +13 -0
  197. data/lib/action_view/helpers/tags/time_select.rb +8 -0
  198. data/lib/action_view/helpers/tags/time_zone_select.rb +20 -0
  199. data/lib/action_view/helpers/tags/url_field.rb +8 -0
  200. data/lib/action_view/helpers/tags/week_field.rb +13 -0
  201. data/lib/action_view/helpers/tags.rb +39 -0
  202. data/lib/action_view/helpers/text_helper.rb +130 -114
  203. data/lib/action_view/helpers/translation_helper.rb +32 -16
  204. data/lib/action_view/helpers/url_helper.rb +211 -270
  205. data/lib/action_view/helpers.rb +2 -4
  206. data/lib/action_view/locale/en.yml +1 -105
  207. data/lib/action_view/log_subscriber.rb +6 -4
  208. data/lib/action_view/lookup_context.rb +15 -28
  209. data/lib/action_view/model_naming.rb +12 -0
  210. data/lib/action_view/path_set.rb +8 -20
  211. data/lib/action_view/railtie.rb +6 -22
  212. data/lib/action_view/record_identifier.rb +84 -0
  213. data/lib/action_view/renderer/abstract_renderer.rb +25 -19
  214. data/lib/action_view/renderer/partial_renderer.rb +158 -81
  215. data/lib/action_view/renderer/renderer.rb +8 -12
  216. data/lib/action_view/renderer/streaming_template_renderer.rb +2 -5
  217. data/lib/action_view/renderer/template_renderer.rb +12 -10
  218. data/lib/action_view/routing_url_for.rb +107 -0
  219. data/lib/action_view/template/error.rb +22 -12
  220. data/lib/action_view/template/handlers/builder.rb +1 -1
  221. data/lib/action_view/template/handlers/erb.rb +40 -19
  222. data/lib/action_view/template/handlers/raw.rb +11 -0
  223. data/lib/action_view/template/handlers.rb +12 -9
  224. data/lib/action_view/template/resolver.rb +107 -53
  225. data/lib/action_view/template/text.rb +12 -8
  226. data/lib/action_view/template/types.rb +57 -0
  227. data/lib/action_view/template.rb +25 -23
  228. data/lib/action_view/test_case.rb +67 -42
  229. data/lib/{action_controller → action_view}/vendor/html-scanner/html/document.rb +0 -0
  230. data/lib/{action_controller → action_view}/vendor/html-scanner/html/node.rb +12 -12
  231. data/lib/{action_controller → action_view}/vendor/html-scanner/html/sanitizer.rb +13 -2
  232. data/lib/{action_controller → action_view}/vendor/html-scanner/html/selector.rb +9 -9
  233. data/lib/{action_controller → action_view}/vendor/html-scanner/html/tokenizer.rb +1 -1
  234. data/lib/{action_controller → action_view}/vendor/html-scanner/html/version.rb +0 -0
  235. data/lib/action_view/vendor/html-scanner.rb +20 -0
  236. data/lib/action_view.rb +17 -8
  237. metadata +184 -214
  238. data/lib/action_controller/caching/actions.rb +0 -185
  239. data/lib/action_controller/caching/pages.rb +0 -187
  240. data/lib/action_controller/caching/sweeping.rb +0 -97
  241. data/lib/action_controller/deprecated/performance_test.rb +0 -1
  242. data/lib/action_controller/metal/compatibility.rb +0 -65
  243. data/lib/action_controller/metal/session_management.rb +0 -14
  244. data/lib/action_controller/railties/paths.rb +0 -25
  245. data/lib/action_dispatch/middleware/best_standards_support.rb +0 -30
  246. data/lib/action_dispatch/middleware/body_proxy.rb +0 -30
  247. data/lib/action_dispatch/middleware/head.rb +0 -18
  248. data/lib/action_dispatch/middleware/rescue.rb +0 -26
  249. data/lib/action_dispatch/testing/performance_test.rb +0 -10
  250. data/lib/action_view/asset_paths.rb +0 -142
  251. data/lib/action_view/helpers/asset_paths.rb +0 -7
  252. data/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb +0 -146
  253. data/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb +0 -93
  254. data/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb +0 -193
  255. data/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb +0 -148
  256. data/lib/sprockets/assets.rake +0 -99
  257. data/lib/sprockets/bootstrap.rb +0 -37
  258. data/lib/sprockets/compressors.rb +0 -83
  259. data/lib/sprockets/helpers/isolated_helper.rb +0 -13
  260. data/lib/sprockets/helpers/rails_helper.rb +0 -182
  261. data/lib/sprockets/helpers.rb +0 -6
  262. data/lib/sprockets/railtie.rb +0 -62
  263. data/lib/sprockets/static_compiler.rb +0 -56
@@ -1,9 +1,11 @@
1
- require 'active_support/core_ext/object/blank'
2
1
  require 'active_support/core_ext/hash/keys'
3
2
  require 'active_support/core_ext/module/attribute_accessors'
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'active_support/key_generator'
5
+ require 'active_support/message_verifier'
4
6
 
5
7
  module ActionDispatch
6
- class Request
8
+ class Request < Rack::Request
7
9
  def cookie_jar
8
10
  env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self)
9
11
  end
@@ -15,7 +17,7 @@ module ActionDispatch
15
17
  # being written will be sent out with the response. Reading a cookie does not get
16
18
  # the cookie object itself back, just the value it holds.
17
19
  #
18
- # Examples for writing:
20
+ # Examples of writing:
19
21
  #
20
22
  # # Sets a simple session cookie.
21
23
  # # This cookie will be deleted when the user's browser is closed.
@@ -25,11 +27,11 @@ module ActionDispatch
25
27
  # cookies[:lat_lon] = [47.68, -122.37]
26
28
  #
27
29
  # # Sets a cookie that expires in 1 hour.
28
- # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }
30
+ # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now }
29
31
  #
30
- # # Sets a signed cookie, which prevents a user from tampering with its value.
31
- # # The cookie is signed by your app's <tt>config.secret_token</tt> value.
32
- # # Rails generates this value by default when you create a new Rails app.
32
+ # # Sets a signed cookie, which prevents users from tampering with its value.
33
+ # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value.
34
+ # # It can be read using the signed method <tt>cookies.signed[:name]</tt>
33
35
  # cookies.signed[:user_id] = current_user.id
34
36
  #
35
37
  # # Sets a "permanent" cookie (which expires in 20 years from now).
@@ -38,11 +40,12 @@ module ActionDispatch
38
40
  # # You can also chain these methods:
39
41
  # cookies.permanent.signed[:login] = "XJ-122"
40
42
  #
41
- # Examples for reading:
43
+ # Examples of reading:
42
44
  #
43
- # cookies[:user_name] # => "david"
44
- # cookies.size # => 2
45
- # cookies[:lat_lon] # => [47.68, -122.37]
45
+ # cookies[:user_name] # => "david"
46
+ # cookies.size # => 2
47
+ # cookies[:lat_lon] # => [47.68, -122.37]
48
+ # cookies.signed[:login] # => "XJ-122"
46
49
  #
47
50
  # Example for deleting:
48
51
  #
@@ -50,13 +53,13 @@ module ActionDispatch
50
53
  #
51
54
  # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
52
55
  #
53
- # cookies[:key] = {
54
- # :value => 'a yummy cookie',
55
- # :expires => 1.year.from_now,
56
- # :domain => 'domain.com'
56
+ # cookies[:name] = {
57
+ # value: 'a yummy cookie',
58
+ # expires: 1.year.from_now,
59
+ # domain: 'domain.com'
57
60
  # }
58
61
  #
59
- # cookies.delete(:key, :domain => 'domain.com')
62
+ # cookies.delete(:name, domain: 'domain.com')
60
63
  #
61
64
  # The option symbols for setting cookies are:
62
65
  #
@@ -67,26 +70,125 @@ module ActionDispatch
67
70
  # restrict to the domain level. If you use a schema like www.example.com
68
71
  # and want to share session with user.example.com set <tt>:domain</tt>
69
72
  # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
70
- # <tt>:all</tt> again when deleting keys.
73
+ # <tt>:all</tt> again when deleting cookies.
71
74
  #
72
- # :domain => nil # Does not sets cookie domain. (default)
73
- # :domain => :all # Allow the cookie for the top most level
75
+ # domain: nil # Does not sets cookie domain. (default)
76
+ # domain: :all # Allow the cookie for the top most level
74
77
  # domain and subdomains.
75
78
  #
76
79
  # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
77
- # * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers.
80
+ # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
78
81
  # Default is +false+.
79
82
  # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
80
83
  # only HTTP. Defaults to +false+.
81
84
  class Cookies
82
- HTTP_HEADER = "Set-Cookie".freeze
83
- TOKEN_KEY = "action_dispatch.secret_token".freeze
85
+ HTTP_HEADER = "Set-Cookie".freeze
86
+ GENERATOR_KEY = "action_dispatch.key_generator".freeze
87
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
88
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
89
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
90
+ SECRET_TOKEN = "action_dispatch.secret_token".freeze
91
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
92
+
93
+ # Cookies can typically store 4096 bytes.
94
+ MAX_COOKIE_SIZE = 4096
84
95
 
85
96
  # Raised when storing more than 4K of session data.
86
- class CookieOverflow < StandardError; end
97
+ CookieOverflow = Class.new StandardError
98
+
99
+ # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed
100
+ module ChainedCookieJars
101
+ # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
102
+ #
103
+ # cookies.permanent[:prefers_open_id] = true
104
+ # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
105
+ #
106
+ # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
107
+ #
108
+ # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
109
+ #
110
+ # cookies.permanent.signed[:remember_me] = current_user.id
111
+ # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
112
+ def permanent
113
+ @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
114
+ end
115
+
116
+ # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
117
+ # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
118
+ # cookie was tampered with by the user (or a 3rd party), nil will be returned.
119
+ #
120
+ # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
121
+ # legacy cookies signed with the old key generator will be transparently upgraded.
122
+ #
123
+ # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
124
+ #
125
+ # Example:
126
+ #
127
+ # cookies.signed[:discount] = 45
128
+ # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
129
+ #
130
+ # cookies.signed[:discount] # => 45
131
+ def signed
132
+ @signed ||=
133
+ if @options[:upgrade_legacy_signed_cookies]
134
+ UpgradeLegacySignedCookieJar.new(self, @key_generator, @options)
135
+ else
136
+ SignedCookieJar.new(self, @key_generator, @options)
137
+ end
138
+ end
139
+
140
+ # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
141
+ # If the cookie was tampered with by the user (or a 3rd party), nil will be returned.
142
+ #
143
+ # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
144
+ # legacy cookies signed with the old key generator will be transparently upgraded.
145
+ #
146
+ # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
147
+ #
148
+ # Example:
149
+ #
150
+ # cookies.encrypted[:discount] = 45
151
+ # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
152
+ #
153
+ # cookies.encrypted[:discount] # => 45
154
+ def encrypted
155
+ @encrypted ||=
156
+ if @options[:upgrade_legacy_signed_cookies]
157
+ UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options)
158
+ else
159
+ EncryptedCookieJar.new(self, @key_generator, @options)
160
+ end
161
+ end
162
+
163
+ # Returns the +signed+ or +encrypted jar, preferring +encrypted+ if +secret_key_base+ is set.
164
+ # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
165
+ def signed_or_encrypted
166
+ @signed_or_encrypted ||=
167
+ if @options[:secret_key_base].present?
168
+ encrypted
169
+ else
170
+ signed
171
+ end
172
+ end
173
+ end
174
+
175
+ module VerifyAndUpgradeLegacySignedMessage
176
+ def initialize(*args)
177
+ super
178
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token])
179
+ end
180
+
181
+ def verify_and_upgrade_legacy_signed_message(name, signed_message)
182
+ @legacy_verifier.verify(signed_message).tap do |value|
183
+ self[name] = value
184
+ end
185
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
186
+ nil
187
+ end
188
+ end
87
189
 
88
190
  class CookieJar #:nodoc:
89
- include Enumerable
191
+ include Enumerable, ChainedCookieJars
90
192
 
91
193
  # This regular expression is used to split the levels of a domain.
92
194
  # The top level domain can be any string without a period or
@@ -102,23 +204,36 @@ module ActionDispatch
102
204
  # $& => example.local
103
205
  DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
104
206
 
207
+ def self.options_for_env(env) #:nodoc:
208
+ { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '',
209
+ encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '',
210
+ encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
211
+ secret_token: env[SECRET_TOKEN],
212
+ secret_key_base: env[SECRET_KEY_BASE],
213
+ upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?
214
+ }
215
+ end
216
+
105
217
  def self.build(request)
106
- secret = request.env[TOKEN_KEY]
218
+ env = request.env
219
+ key_generator = env[GENERATOR_KEY]
220
+ options = options_for_env env
221
+
107
222
  host = request.host
108
223
  secure = request.ssl?
109
224
 
110
- new(secret, host, secure).tap do |hash|
225
+ new(key_generator, host, secure, options).tap do |hash|
111
226
  hash.update(request.cookies)
112
227
  end
113
228
  end
114
229
 
115
- def initialize(secret = nil, host = nil, secure = false)
116
- @secret = secret
230
+ def initialize(key_generator, host = nil, secure = false, options = {})
231
+ @key_generator = key_generator
117
232
  @set_cookies = {}
118
233
  @delete_cookies = {}
119
234
  @host = host
120
235
  @secure = secure
121
- @closed = false
236
+ @options = options
122
237
  @cookies = {}
123
238
  end
124
239
 
@@ -131,6 +246,10 @@ module ActionDispatch
131
246
  @cookies[name.to_s]
132
247
  end
133
248
 
249
+ def fetch(name, *args, &block)
250
+ @cookies.fetch(name.to_s, *args, &block)
251
+ end
252
+
134
253
  def key?(name)
135
254
  @cookies.key?(name.to_s)
136
255
  end
@@ -155,13 +274,13 @@ module ActionDispatch
155
274
  end
156
275
  elsif options[:domain].is_a? Array
157
276
  # if host matches one of the supplied domains without a dot in front of it
158
- options[:domain] = options[:domain].find {|domain| @host.include? domain[/^\.?(.*)$/, 1] }
277
+ options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') }
159
278
  end
160
279
  end
161
280
 
162
281
  # Sets the cookie named +name+. The second argument may be the very cookie
163
282
  # value, or a hash of options as documented above.
164
- def []=(key, options)
283
+ def []=(name, options)
165
284
  if options.is_a?(Hash)
166
285
  options.symbolize_keys!
167
286
  value = options[:value]
@@ -172,65 +291,43 @@ module ActionDispatch
172
291
 
173
292
  handle_options(options)
174
293
 
175
- if @cookies[key.to_s] != value or options[:expires]
176
- @cookies[key.to_s] = value
177
- @set_cookies[key.to_s] = options
178
- @delete_cookies.delete(key.to_s)
294
+ if @cookies[name.to_s] != value or options[:expires]
295
+ @cookies[name.to_s] = value
296
+ @set_cookies[name.to_s] = options
297
+ @delete_cookies.delete(name.to_s)
179
298
  end
180
299
 
181
300
  value
182
301
  end
183
302
 
184
303
  # Removes the cookie on the client machine by setting the value to an empty string
185
- # and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
304
+ # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
186
305
  # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
187
- def delete(key, options = {})
188
- options.symbolize_keys!
306
+ def delete(name, options = {})
307
+ return unless @cookies.has_key? name.to_s
189
308
 
309
+ options.symbolize_keys!
190
310
  handle_options(options)
191
311
 
192
- value = @cookies.delete(key.to_s)
193
- @delete_cookies[key.to_s] = options
312
+ value = @cookies.delete(name.to_s)
313
+ @delete_cookies[name.to_s] = options
194
314
  value
195
315
  end
196
316
 
317
+ # Whether the given cookie is to be deleted by this CookieJar.
318
+ # Like <tt>[]=</tt>, you can pass in an options hash to test if a
319
+ # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
320
+ def deleted?(name, options = {})
321
+ options.symbolize_keys!
322
+ handle_options(options)
323
+ @delete_cookies[name.to_s] == options
324
+ end
325
+
197
326
  # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
198
327
  def clear(options = {})
199
328
  @cookies.each_key{ |k| delete(k, options) }
200
329
  end
201
330
 
202
- # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
203
- #
204
- # cookies.permanent[:prefers_open_id] = true
205
- # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
206
- #
207
- # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
208
- #
209
- # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
210
- #
211
- # cookies.permanent.signed[:remember_me] = current_user.id
212
- # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
213
- def permanent
214
- @permanent ||= PermanentCookieJar.new(self, @secret)
215
- end
216
-
217
- # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
218
- # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
219
- # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
220
- # be raised.
221
- #
222
- # This jar requires that you set a suitable secret for the verification on your app's config.secret_token.
223
- #
224
- # Example:
225
- #
226
- # cookies.signed[:discount] = 45
227
- # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
228
- #
229
- # cookies.signed[:discount] # => 45
230
- def signed
231
- @signed ||= SignedCookieJar.new(self, @secret)
232
- end
233
-
234
331
  def write(headers)
235
332
  @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) }
236
333
  @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
@@ -245,18 +342,25 @@ module ActionDispatch
245
342
  self.always_write_cookie = false
246
343
 
247
344
  private
248
-
249
345
  def write_cookie?(cookie)
250
346
  @secure || !cookie[:secure] || always_write_cookie
251
347
  end
252
348
  end
253
349
 
254
- class PermanentCookieJar < CookieJar #:nodoc:
255
- def initialize(parent_jar, secret)
256
- @parent_jar, @secret = parent_jar, secret
350
+ class PermanentCookieJar #:nodoc:
351
+ include ChainedCookieJars
352
+
353
+ def initialize(parent_jar, key_generator, options = {})
354
+ @parent_jar = parent_jar
355
+ @key_generator = key_generator
356
+ @options = options
257
357
  end
258
358
 
259
- def []=(key, options)
359
+ def [](name)
360
+ @parent_jar[name.to_s]
361
+ end
362
+
363
+ def []=(name, options)
260
364
  if options.is_a?(Hash)
261
365
  options.symbolize_keys!
262
366
  else
@@ -264,37 +368,27 @@ module ActionDispatch
264
368
  end
265
369
 
266
370
  options[:expires] = 20.years.from_now
267
- @parent_jar[key] = options
268
- end
269
-
270
- def signed
271
- @signed ||= SignedCookieJar.new(self, @secret)
272
- end
273
-
274
- def method_missing(method, *arguments, &block)
275
- @parent_jar.send(method, *arguments, &block)
371
+ @parent_jar[name] = options
276
372
  end
277
373
  end
278
374
 
279
- class SignedCookieJar < CookieJar #:nodoc:
280
- MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes.
281
- SECRET_MIN_LENGTH = 30 # Characters
375
+ class SignedCookieJar #:nodoc:
376
+ include ChainedCookieJars
282
377
 
283
- def initialize(parent_jar, secret)
284
- ensure_secret_secure(secret)
378
+ def initialize(parent_jar, key_generator, options = {})
285
379
  @parent_jar = parent_jar
380
+ @options = options
381
+ secret = key_generator.generate_key(@options[:signed_cookie_salt])
286
382
  @verifier = ActiveSupport::MessageVerifier.new(secret)
287
383
  end
288
384
 
289
385
  def [](name)
290
386
  if signed_message = @parent_jar[name]
291
- @verifier.verify(signed_message)
387
+ verify(signed_message)
292
388
  end
293
- rescue ActiveSupport::MessageVerifier::InvalidSignature
294
- nil
295
389
  end
296
390
 
297
- def []=(key, options)
391
+ def []=(name, options)
298
392
  if options.is_a?(Hash)
299
393
  options.symbolize_keys!
300
394
  options[:value] = @verifier.generate(options[:value])
@@ -303,31 +397,83 @@ module ActionDispatch
303
397
  end
304
398
 
305
399
  raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
306
- @parent_jar[key] = options
400
+ @parent_jar[name] = options
401
+ end
402
+
403
+ private
404
+ def verify(signed_message)
405
+ @verifier.verify(signed_message)
406
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
407
+ nil
408
+ end
409
+ end
410
+
411
+ # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
412
+ # config.secret_token and config.secret_key_base are both set. It reads
413
+ # legacy cookies signed with the old dummy key generator and re-saves
414
+ # them using the new key generator to provide a smooth upgrade path.
415
+ class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
416
+ include VerifyAndUpgradeLegacySignedMessage
417
+
418
+ def [](name)
419
+ if signed_message = @parent_jar[name]
420
+ verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message)
421
+ end
422
+ end
423
+ end
424
+
425
+ class EncryptedCookieJar #:nodoc:
426
+ include ChainedCookieJars
427
+
428
+ def initialize(parent_jar, key_generator, options = {})
429
+ if ActiveSupport::LegacyKeyGenerator === key_generator
430
+ raise "You didn't set config.secret_key_base, which is required for this cookie jar. " +
431
+ "Read the upgrade documentation to learn more about this new config option."
432
+ end
433
+
434
+ @parent_jar = parent_jar
435
+ @options = options
436
+ secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
437
+ sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
438
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
307
439
  end
308
440
 
309
- def method_missing(method, *arguments, &block)
310
- @parent_jar.send(method, *arguments, &block)
441
+ def [](name)
442
+ if encrypted_message = @parent_jar[name]
443
+ decrypt_and_verify(encrypted_message)
444
+ end
311
445
  end
312
446
 
313
- protected
447
+ def []=(name, options)
448
+ if options.is_a?(Hash)
449
+ options.symbolize_keys!
450
+ else
451
+ options = { :value => options }
452
+ end
453
+ options[:value] = @encryptor.encrypt_and_sign(options[:value])
454
+
455
+ raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
456
+ @parent_jar[name] = options
457
+ end
314
458
 
315
- # To prevent users from using something insecure like "Password" we make sure that the
316
- # secret they've provided is at least 30 characters in length.
317
- def ensure_secret_secure(secret)
318
- if secret.blank?
319
- raise ArgumentError, "A secret is required to generate an " +
320
- "integrity hash for cookie session data. Use " +
321
- "config.secret_token = \"some secret phrase of at " +
322
- "least #{SECRET_MIN_LENGTH} characters\"" +
323
- "in config/initializers/secret_token.rb"
459
+ private
460
+ def decrypt_and_verify(encrypted_message)
461
+ @encryptor.decrypt_and_verify(encrypted_message)
462
+ rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
463
+ nil
324
464
  end
465
+ end
325
466
 
326
- if secret.length < SECRET_MIN_LENGTH
327
- raise ArgumentError, "Secret should be something secure, " +
328
- "like \"#{SecureRandom.hex(16)}\". The value you " +
329
- "provided, \"#{secret}\", is shorter than the minimum length " +
330
- "of #{SECRET_MIN_LENGTH} characters"
467
+ # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
468
+ # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base
469
+ # are both set. It reads legacy cookies signed with the old dummy key generator and
470
+ # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
471
+ class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
472
+ include VerifyAndUpgradeLegacySignedMessage
473
+
474
+ def [](name)
475
+ if encrypted_or_signed_message = @parent_jar[name]
476
+ decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
331
477
  end
332
478
  end
333
479
  end
@@ -337,7 +483,6 @@ module ActionDispatch
337
483
  end
338
484
 
339
485
  def call(env)
340
- cookie_jar = nil
341
486
  status, headers, body = @app.call(env)
342
487
 
343
488
  if cookie_jar = env['action_dispatch.cookies']
@@ -1,30 +1,30 @@
1
1
  require 'action_dispatch/http/request'
2
2
  require 'action_dispatch/middleware/exception_wrapper'
3
+ require 'action_dispatch/routing/inspector'
3
4
 
4
5
  module ActionDispatch
5
6
  # This middleware is responsible for logging exceptions and
6
7
  # showing a debugging page in case the request is local.
7
8
  class DebugExceptions
8
- RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates')
9
+ RESCUES_TEMPLATE_PATH = File.expand_path('../templates', __FILE__)
9
10
 
10
- def initialize(app)
11
- @app = app
11
+ def initialize(app, routes_app = nil)
12
+ @app = app
13
+ @routes_app = routes_app
12
14
  end
13
15
 
14
16
  def call(env)
15
- begin
16
- response = @app.call(env)
17
-
18
- if response[1]['X-Cascade'] == 'pass'
19
- body = response[2]
20
- body.close if body.respond_to?(:close)
21
- raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
22
- end
23
- rescue Exception => exception
24
- raise exception if env['action_dispatch.show_exceptions'] == false
17
+ _, headers, body = response = @app.call(env)
18
+
19
+ if headers['X-Cascade'] == 'pass'
20
+ body.close if body.respond_to?(:close)
21
+ raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
25
22
  end
26
23
 
27
- exception ? render_exception(env, exception) : response
24
+ response
25
+ rescue Exception => exception
26
+ raise exception if env['action_dispatch.show_exceptions'] == false
27
+ render_exception(env, exception)
28
28
  end
29
29
 
30
30
  private
@@ -39,9 +39,12 @@ module ActionDispatch
39
39
  :exception => wrapper.exception,
40
40
  :application_trace => wrapper.application_trace,
41
41
  :framework_trace => wrapper.framework_trace,
42
- :full_trace => wrapper.full_trace
42
+ :full_trace => wrapper.full_trace,
43
+ :routes_inspector => routes_inspector(exception),
44
+ :source_extract => wrapper.source_extract,
45
+ :line_number => wrapper.line_number,
46
+ :file => wrapper.file
43
47
  )
44
-
45
48
  file = "rescues/#{wrapper.rescue_template}"
46
49
  body = template.render(:template => file, :layout => 'rescues/layout')
47
50
  render(wrapper.status_code, body)
@@ -76,7 +79,13 @@ module ActionDispatch
76
79
  end
77
80
 
78
81
  def stderr_logger
79
- @stderr_logger ||= Logger.new($stderr)
82
+ @stderr_logger ||= ActiveSupport::Logger.new($stderr)
83
+ end
84
+
85
+ def routes_inspector(exception)
86
+ if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
87
+ ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
88
+ end
80
89
  end
81
90
  end
82
91
  end
@@ -1,5 +1,5 @@
1
1
  require 'action_controller/metal/exceptions'
2
- require 'active_support/core_ext/exception'
2
+ require 'active_support/core_ext/class/attribute_accessors'
3
3
 
4
4
  module ActionDispatch
5
5
  class ExceptionWrapper
@@ -9,8 +9,13 @@ module ActionDispatch
9
9
  'ActionController::RoutingError' => :not_found,
10
10
  'AbstractController::ActionNotFound' => :not_found,
11
11
  'ActionController::MethodNotAllowed' => :method_not_allowed,
12
+ 'ActionController::UnknownHttpMethod' => :method_not_allowed,
12
13
  'ActionController::NotImplemented' => :not_implemented,
13
- 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity
14
+ 'ActionController::UnknownFormat' => :not_acceptable,
15
+ 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
16
+ 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
17
+ 'ActionController::BadRequest' => :bad_request,
18
+ 'ActionController::ParameterMissing' => :bad_request
14
19
  )
15
20
 
16
21
  cattr_accessor :rescue_templates
@@ -22,7 +27,7 @@ module ActionDispatch
22
27
  'ActionView::Template::Error' => 'template_error'
23
28
  )
24
29
 
25
- attr_reader :env, :exception
30
+ attr_reader :env, :exception, :line_number, :file
26
31
 
27
32
  def initialize(env, exception)
28
33
  @env = env
@@ -53,6 +58,15 @@ module ActionDispatch
53
58
  Rack::Utils.status_code(@@rescue_responses[class_name])
54
59
  end
55
60
 
61
+ def source_extract
62
+ if application_trace && trace = application_trace.first
63
+ file, line, _ = trace.split(":")
64
+ @file = file
65
+ @line_number = line.to_i
66
+ source_fragment(@file, @line_number)
67
+ end
68
+ end
69
+
56
70
  private
57
71
 
58
72
  def original_exception(exception)
@@ -78,5 +92,17 @@ module ActionDispatch
78
92
  def backtrace_cleaner
79
93
  @backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner']
80
94
  end
95
+
96
+ def source_fragment(path, line)
97
+ return unless Rails.respond_to?(:root) && Rails.root
98
+ full_path = Rails.root.join(path)
99
+ if File.exists?(full_path)
100
+ File.open(full_path, "r") do |file|
101
+ start = [line - 3, 0].max
102
+ lines = file.each_line.drop(start).take(6)
103
+ Hash[*(start+1..(lines.count+start)).zip(lines).flatten]
104
+ end
105
+ end
106
+ end
81
107
  end
82
108
  end