actionpack 4.2.10 → 5.0.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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +553 -401
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -3
  5. data/lib/abstract_controller/base.rb +28 -38
  6. data/lib/{action_controller → abstract_controller}/caching/fragments.rb +51 -11
  7. data/lib/abstract_controller/caching.rb +62 -0
  8. data/lib/abstract_controller/callbacks.rb +52 -19
  9. data/lib/abstract_controller/collector.rb +4 -9
  10. data/lib/abstract_controller/error.rb +4 -0
  11. data/lib/abstract_controller/helpers.rb +4 -3
  12. data/lib/abstract_controller/railties/routes_helpers.rb +2 -2
  13. data/lib/abstract_controller/rendering.rb +28 -18
  14. data/lib/abstract_controller/translation.rb +8 -7
  15. data/lib/abstract_controller.rb +6 -2
  16. data/lib/action_controller/api/api_rendering.rb +14 -0
  17. data/lib/action_controller/api.rb +147 -0
  18. data/lib/action_controller/base.rb +10 -13
  19. data/lib/action_controller/caching.rb +13 -58
  20. data/lib/action_controller/form_builder.rb +48 -0
  21. data/lib/action_controller/log_subscriber.rb +3 -10
  22. data/lib/action_controller/metal/basic_implicit_render.rb +11 -0
  23. data/lib/action_controller/metal/conditional_get.rb +106 -34
  24. data/lib/action_controller/metal/cookies.rb +1 -3
  25. data/lib/action_controller/metal/data_streaming.rb +11 -32
  26. data/lib/action_controller/metal/etag_with_template_digest.rb +1 -1
  27. data/lib/action_controller/metal/exceptions.rb +11 -6
  28. data/lib/action_controller/metal/force_ssl.rb +10 -10
  29. data/lib/action_controller/metal/head.rb +14 -8
  30. data/lib/action_controller/metal/helpers.rb +15 -6
  31. data/lib/action_controller/metal/http_authentication.rb +44 -35
  32. data/lib/action_controller/metal/implicit_render.rb +61 -6
  33. data/lib/action_controller/metal/instrumentation.rb +5 -5
  34. data/lib/action_controller/metal/live.rb +66 -88
  35. data/lib/action_controller/metal/mime_responds.rb +27 -42
  36. data/lib/action_controller/metal/params_wrapper.rb +8 -8
  37. data/lib/action_controller/metal/redirecting.rb +32 -9
  38. data/lib/action_controller/metal/renderers.rb +85 -40
  39. data/lib/action_controller/metal/rendering.rb +38 -6
  40. data/lib/action_controller/metal/request_forgery_protection.rb +126 -48
  41. data/lib/action_controller/metal/rescue.rb +3 -12
  42. data/lib/action_controller/metal/streaming.rb +4 -4
  43. data/lib/action_controller/metal/strong_parameters.rb +293 -90
  44. data/lib/action_controller/metal/testing.rb +1 -12
  45. data/lib/action_controller/metal/url_for.rb +12 -5
  46. data/lib/action_controller/metal.rb +88 -63
  47. data/lib/action_controller/renderer.rb +111 -0
  48. data/lib/action_controller/template_assertions.rb +9 -0
  49. data/lib/action_controller/test_case.rb +288 -368
  50. data/lib/action_controller.rb +12 -9
  51. data/lib/action_dispatch/http/cache.rb +73 -34
  52. data/lib/action_dispatch/http/filter_parameters.rb +15 -11
  53. data/lib/action_dispatch/http/filter_redirect.rb +7 -8
  54. data/lib/action_dispatch/http/headers.rb +44 -13
  55. data/lib/action_dispatch/http/mime_negotiation.rb +41 -23
  56. data/lib/action_dispatch/http/mime_type.rb +126 -90
  57. data/lib/action_dispatch/http/mime_types.rb +3 -4
  58. data/lib/action_dispatch/http/parameter_filter.rb +18 -8
  59. data/lib/action_dispatch/http/parameters.rb +54 -41
  60. data/lib/action_dispatch/http/request.rb +149 -82
  61. data/lib/action_dispatch/http/response.rb +206 -102
  62. data/lib/action_dispatch/http/url.rb +117 -8
  63. data/lib/action_dispatch/journey/formatter.rb +39 -28
  64. data/lib/action_dispatch/journey/gtg/transition_table.rb +1 -1
  65. data/lib/action_dispatch/journey/nfa/dot.rb +0 -2
  66. data/lib/action_dispatch/journey/nfa/transition_table.rb +1 -46
  67. data/lib/action_dispatch/journey/nodes/node.rb +14 -4
  68. data/lib/action_dispatch/journey/parser_extras.rb +4 -0
  69. data/lib/action_dispatch/journey/path/pattern.rb +38 -42
  70. data/lib/action_dispatch/journey/route.rb +74 -19
  71. data/lib/action_dispatch/journey/router/utils.rb +5 -5
  72. data/lib/action_dispatch/journey/router.rb +5 -9
  73. data/lib/action_dispatch/journey/routes.rb +14 -15
  74. data/lib/action_dispatch/journey/visitors.rb +86 -43
  75. data/lib/action_dispatch/middleware/callbacks.rb +10 -1
  76. data/lib/action_dispatch/middleware/cookies.rb +189 -135
  77. data/lib/action_dispatch/middleware/debug_exceptions.rb +124 -49
  78. data/lib/action_dispatch/middleware/exception_wrapper.rb +21 -21
  79. data/lib/action_dispatch/middleware/executor.rb +19 -0
  80. data/lib/action_dispatch/middleware/flash.rb +66 -45
  81. data/lib/action_dispatch/middleware/params_parser.rb +32 -46
  82. data/lib/action_dispatch/middleware/public_exceptions.rb +2 -2
  83. data/lib/action_dispatch/middleware/reloader.rb +14 -58
  84. data/lib/action_dispatch/middleware/remote_ip.rb +29 -19
  85. data/lib/action_dispatch/middleware/request_id.rb +11 -6
  86. data/lib/action_dispatch/middleware/session/abstract_store.rb +23 -11
  87. data/lib/action_dispatch/middleware/session/cache_store.rb +9 -6
  88. data/lib/action_dispatch/middleware/session/cookie_store.rb +30 -24
  89. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +4 -0
  90. data/lib/action_dispatch/middleware/show_exceptions.rb +11 -9
  91. data/lib/action_dispatch/middleware/ssl.rb +115 -36
  92. data/lib/action_dispatch/middleware/stack.rb +44 -40
  93. data/lib/action_dispatch/middleware/static.rb +51 -35
  94. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +2 -14
  95. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  96. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -1
  97. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  98. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +4 -4
  99. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +59 -63
  100. data/lib/action_dispatch/railtie.rb +2 -2
  101. data/lib/action_dispatch/request/session.rb +69 -33
  102. data/lib/action_dispatch/request/utils.rb +51 -19
  103. data/lib/action_dispatch/routing/inspector.rb +32 -43
  104. data/lib/action_dispatch/routing/mapper.rb +491 -338
  105. data/lib/action_dispatch/routing/polymorphic_routes.rb +8 -14
  106. data/lib/action_dispatch/routing/redirection.rb +3 -3
  107. data/lib/action_dispatch/routing/route_set.rb +145 -238
  108. data/lib/action_dispatch/routing/url_for.rb +27 -10
  109. data/lib/action_dispatch/routing.rb +17 -13
  110. data/lib/action_dispatch/testing/assertion_response.rb +45 -0
  111. data/lib/action_dispatch/testing/assertions/response.rb +38 -20
  112. data/lib/action_dispatch/testing/assertions/routing.rb +11 -10
  113. data/lib/action_dispatch/testing/assertions.rb +1 -1
  114. data/lib/action_dispatch/testing/integration.rb +368 -97
  115. data/lib/action_dispatch/testing/test_process.rb +5 -6
  116. data/lib/action_dispatch/testing/test_request.rb +22 -31
  117. data/lib/action_dispatch/testing/test_response.rb +7 -4
  118. data/lib/action_dispatch.rb +3 -1
  119. data/lib/action_pack/gem_version.rb +3 -3
  120. data/lib/action_pack.rb +1 -1
  121. metadata +30 -34
  122. data/lib/action_controller/metal/hide_actions.rb +0 -40
  123. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  124. data/lib/action_controller/middleware.rb +0 -39
  125. data/lib/action_controller/model_naming.rb +0 -12
  126. data/lib/action_dispatch/journey/backwards.rb +0 -5
  127. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  128. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  129. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  130. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
  131. /data/lib/action_dispatch/middleware/templates/rescues/{_source.erb → _source.html.erb} +0 -0
@@ -7,13 +7,18 @@ require 'action_controller/metal/strong_parameters'
7
7
  module ActionController
8
8
  extend ActiveSupport::Autoload
9
9
 
10
+ autoload :API
10
11
  autoload :Base
11
- autoload :Caching
12
12
  autoload :Metal
13
13
  autoload :Middleware
14
+ autoload :Renderer
15
+ autoload :FormBuilder
16
+
17
+ eager_autoload do
18
+ autoload :Caching
19
+ end
14
20
 
15
21
  autoload_under "metal" do
16
- autoload :Compatibility
17
22
  autoload :ConditionalGet
18
23
  autoload :Cookies
19
24
  autoload :DataStreaming
@@ -22,13 +27,12 @@ module ActionController
22
27
  autoload :ForceSSL
23
28
  autoload :Head
24
29
  autoload :Helpers
25
- autoload :HideActions
26
30
  autoload :HttpAuthentication
31
+ autoload :BasicImplicitRender
27
32
  autoload :ImplicitRender
28
33
  autoload :Instrumentation
29
34
  autoload :MimeResponds
30
35
  autoload :ParamsWrapper
31
- autoload :RackDelegation
32
36
  autoload :Redirecting
33
37
  autoload :Renderers
34
38
  autoload :Rendering
@@ -40,13 +44,12 @@ module ActionController
40
44
  autoload :UrlFor
41
45
  end
42
46
 
47
+ autoload_under "api" do
48
+ autoload :ApiRendering
49
+ end
50
+
43
51
  autoload :TestCase, 'action_controller/test_case'
44
52
  autoload :TemplateAssertions, 'action_controller/test_case'
45
-
46
- def self.eager_load!
47
- super
48
- ActionController::Caching.eager_load!
49
- end
50
53
  end
51
54
 
52
55
  # Common Active Support usage in Action Controller
@@ -1,4 +1,3 @@
1
-
2
1
  module ActionDispatch
3
2
  module Http
4
3
  module Cache
@@ -8,19 +7,17 @@ module ActionDispatch
8
7
  HTTP_IF_NONE_MATCH = 'HTTP_IF_NONE_MATCH'.freeze
9
8
 
10
9
  def if_modified_since
11
- if since = env[HTTP_IF_MODIFIED_SINCE]
10
+ if since = get_header(HTTP_IF_MODIFIED_SINCE)
12
11
  Time.rfc2822(since) rescue nil
13
12
  end
14
13
  end
15
14
 
16
15
  def if_none_match
17
- env[HTTP_IF_NONE_MATCH]
16
+ get_header HTTP_IF_NONE_MATCH
18
17
  end
19
18
 
20
19
  def if_none_match_etags
21
- (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag|
22
- etag.gsub(/^\"|\"$/, "")
23
- end
20
+ if_none_match ? if_none_match.split(/\s*,\s*/) : []
24
21
  end
25
22
 
26
23
  def not_modified?(modified_at)
@@ -29,8 +26,8 @@ module ActionDispatch
29
26
 
30
27
  def etag_matches?(etag)
31
28
  if etag
32
- etag = etag.gsub(/^\"|\"$/, "")
33
- if_none_match_etags.include?(etag)
29
+ validators = if_none_match_etags
30
+ validators.include?(etag) || validators.include?('*')
34
31
  end
35
32
  end
36
33
 
@@ -51,52 +48,95 @@ module ActionDispatch
51
48
  end
52
49
 
53
50
  module Response
54
- attr_reader :cache_control, :etag
55
- alias :etag? :etag
51
+ attr_reader :cache_control
56
52
 
57
53
  def last_modified
58
- if last = headers[LAST_MODIFIED]
54
+ if last = get_header(LAST_MODIFIED)
59
55
  Time.httpdate(last)
60
56
  end
61
57
  end
62
58
 
63
59
  def last_modified?
64
- headers.include?(LAST_MODIFIED)
60
+ has_header? LAST_MODIFIED
65
61
  end
66
62
 
67
63
  def last_modified=(utc_time)
68
- headers[LAST_MODIFIED] = utc_time.httpdate
64
+ set_header LAST_MODIFIED, utc_time.httpdate
69
65
  end
70
66
 
71
67
  def date
72
- if date_header = headers[DATE]
68
+ if date_header = get_header(DATE)
73
69
  Time.httpdate(date_header)
74
70
  end
75
71
  end
76
72
 
77
73
  def date?
78
- headers.include?(DATE)
74
+ has_header? DATE
79
75
  end
80
76
 
81
77
  def date=(utc_time)
82
- headers[DATE] = utc_time.httpdate
78
+ set_header DATE, utc_time.httpdate
79
+ end
80
+
81
+ # This method sets a weak ETag validator on the response so browsers
82
+ # and proxies may cache the response, keyed on the ETag. On subsequent
83
+ # requests, the If-None-Match header is set to the cached ETag. If it
84
+ # matches the current ETag, we can return a 304 Not Modified response
85
+ # with no body, letting the browser or proxy know that their cache is
86
+ # current. Big savings in request time and network bandwidth.
87
+ #
88
+ # Weak ETags are considered to be semantically equivalent but not
89
+ # byte-for-byte identical. This is perfect for browser caching of HTML
90
+ # pages where we don't care about exact equality, just what the user
91
+ # is viewing.
92
+ #
93
+ # Strong ETags are considered byte-for-byte identical. They allow a
94
+ # browser or proxy cache to support Range requests, useful for paging
95
+ # through a PDF file or scrubbing through a video. Some CDNs only
96
+ # support strong ETags and will ignore weak ETags entirely.
97
+ #
98
+ # Weak ETags are what we almost always need, so they're the default.
99
+ # Check out `#strong_etag=` to provide a strong ETag validator.
100
+ def etag=(weak_validators)
101
+ self.weak_etag = weak_validators
83
102
  end
84
103
 
85
- def etag=(etag)
86
- key = ActiveSupport::Cache.expand_cache_key(etag)
87
- @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}")
104
+ def weak_etag=(weak_validators)
105
+ set_header 'ETag', generate_weak_etag(weak_validators)
106
+ end
107
+
108
+ def strong_etag=(strong_validators)
109
+ set_header 'ETag', generate_strong_etag(strong_validators)
110
+ end
111
+
112
+ def etag?; etag; end
113
+
114
+ # True if an ETag is set and it's a weak validator (preceded with W/)
115
+ def weak_etag?
116
+ etag? && etag.starts_with?('W/"')
117
+ end
118
+
119
+ # True if an ETag is set and it isn't a weak validator (not preceded with W/)
120
+ def strong_etag?
121
+ etag? && !weak_etag?
88
122
  end
89
123
 
90
124
  private
91
125
 
92
126
  DATE = 'Date'.freeze
93
127
  LAST_MODIFIED = "Last-Modified".freeze
94
- ETAG = "ETag".freeze
95
- CACHE_CONTROL = "Cache-Control".freeze
96
- SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
128
+ SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
129
+
130
+ def generate_weak_etag(validators)
131
+ "W/#{generate_strong_etag(validators)}"
132
+ end
133
+
134
+ def generate_strong_etag(validators)
135
+ %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
136
+ end
97
137
 
98
138
  def cache_control_segments
99
- if cache_control = self[CACHE_CONTROL]
139
+ if cache_control = _cache_control
100
140
  cache_control.delete(' ').split(',')
101
141
  else
102
142
  []
@@ -123,12 +163,11 @@ module ActionDispatch
123
163
 
124
164
  def prepare_cache_control!
125
165
  @cache_control = cache_control_headers
126
- @etag = self[ETAG]
127
166
  end
128
167
 
129
168
  def handle_conditional_get!
130
169
  if etag? || last_modified? || !@cache_control.empty?
131
- set_conditional_cache_control!
170
+ set_conditional_cache_control!(@cache_control)
132
171
  end
133
172
  end
134
173
 
@@ -138,24 +177,24 @@ module ActionDispatch
138
177
  PRIVATE = "private".freeze
139
178
  MUST_REVALIDATE = "must-revalidate".freeze
140
179
 
141
- def set_conditional_cache_control!
180
+ def set_conditional_cache_control!(cache_control)
142
181
  control = {}
143
182
  cc_headers = cache_control_headers
144
183
  if extras = cc_headers.delete(:extras)
145
- @cache_control[:extras] ||= []
146
- @cache_control[:extras] += extras
147
- @cache_control[:extras].uniq!
184
+ cache_control[:extras] ||= []
185
+ cache_control[:extras] += extras
186
+ cache_control[:extras].uniq!
148
187
  end
149
188
 
150
189
  control.merge! cc_headers
151
- control.merge! @cache_control
190
+ control.merge! cache_control
152
191
 
153
192
  if control.empty?
154
- headers[CACHE_CONTROL] = DEFAULT_CACHE_CONTROL
193
+ self._cache_control = DEFAULT_CACHE_CONTROL
155
194
  elsif control[:no_cache]
156
- headers[CACHE_CONTROL] = NO_CACHE
195
+ self._cache_control = NO_CACHE
157
196
  if control[:extras]
158
- headers[CACHE_CONTROL] += ", #{control[:extras].join(', ')}"
197
+ self._cache_control = _cache_control + ", #{control[:extras].join(', ')}"
159
198
  end
160
199
  else
161
200
  extras = control[:extras]
@@ -167,7 +206,7 @@ module ActionDispatch
167
206
  options << MUST_REVALIDATE if control[:must_revalidate]
168
207
  options.concat(extras) if extras
169
208
 
170
- headers[CACHE_CONTROL] = options.join(", ")
209
+ self._cache_control = options.join(", ")
171
210
  end
172
211
  end
173
212
  end
@@ -1,14 +1,14 @@
1
- require 'active_support/core_ext/hash/keys'
2
- require 'active_support/core_ext/object/duplicable'
3
1
  require 'action_dispatch/http/parameter_filter'
4
2
 
5
3
  module ActionDispatch
6
4
  module Http
7
5
  # Allows you to specify sensitive parameters which will be replaced from
8
6
  # the request log by looking in the query string of the request and all
9
- # sub-hashes of the params hash to filter. If a block is given, each key and
10
- # value of the params hash and all sub-hashes is passed to it, the value
11
- # or key can be replaced using String#replace or similar method.
7
+ # sub-hashes of the params hash to filter. Filtering only certain sub-keys
8
+ # from a hash is possible by using the dot notation: 'credit_card.number'.
9
+ # If a block is given, each key and value of the params hash and all
10
+ # sub-hashes is passed to it, the value or key can be replaced using
11
+ # String#replace or similar method.
12
12
  #
13
13
  # env["action_dispatch.parameter_filter"] = [:password]
14
14
  # => replaces the value to all keys matching /password/i with "[FILTERED]"
@@ -16,7 +16,11 @@ module ActionDispatch
16
16
  # env["action_dispatch.parameter_filter"] = [:foo, "bar"]
17
17
  # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
18
18
  #
19
- # env["action_dispatch.parameter_filter"] = lambda do |k,v|
19
+ # env["action_dispatch.parameter_filter"] = [ "credit_card.code" ]
20
+ # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
21
+ # change { file: { code: "xxxx"} }
22
+ #
23
+ # env["action_dispatch.parameter_filter"] = -> (k, v) do
20
24
  # v.reverse! if k =~ /secret/i
21
25
  # end
22
26
  # => reverses the value to all keys matching /secret/i
@@ -25,19 +29,19 @@ module ActionDispatch
25
29
  NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
26
30
  NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
27
31
 
28
- def initialize(env)
32
+ def initialize
29
33
  super
30
34
  @filtered_parameters = nil
31
35
  @filtered_env = nil
32
36
  @filtered_path = nil
33
37
  end
34
38
 
35
- # Return a hash of parameters with all sensitive data replaced.
39
+ # Returns a hash of parameters with all sensitive data replaced.
36
40
  def filtered_parameters
37
41
  @filtered_parameters ||= parameter_filter.filter(parameters)
38
42
  end
39
43
 
40
- # Return a hash of request.env with all sensitive data replaced.
44
+ # Returns a hash of request.env with all sensitive data replaced.
41
45
  def filtered_env
42
46
  @filtered_env ||= env_filter.filter(@env)
43
47
  end
@@ -50,13 +54,13 @@ module ActionDispatch
50
54
  protected
51
55
 
52
56
  def parameter_filter
53
- parameter_filter_for @env.fetch("action_dispatch.parameter_filter") {
57
+ parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
54
58
  return NULL_PARAM_FILTER
55
59
  }
56
60
  end
57
61
 
58
62
  def env_filter
59
- user_key = @env.fetch("action_dispatch.parameter_filter") {
63
+ user_key = fetch_header("action_dispatch.parameter_filter") {
60
64
  return NULL_ENV_FILTER
61
65
  }
62
66
  parameter_filter_for(Array(user_key) + ENV_MATCH)
@@ -4,9 +4,8 @@ module ActionDispatch
4
4
 
5
5
  FILTERED = '[FILTERED]'.freeze # :nodoc:
6
6
 
7
- def filtered_location
8
- filters = location_filter
9
- if !filters.empty? && location_filter_match?(filters)
7
+ def filtered_location # :nodoc:
8
+ if location_filter_match?
10
9
  FILTERED
11
10
  else
12
11
  location
@@ -15,20 +14,20 @@ module ActionDispatch
15
14
 
16
15
  private
17
16
 
18
- def location_filter
17
+ def location_filters
19
18
  if request
20
- request.env['action_dispatch.redirect_filter'] || []
19
+ request.get_header('action_dispatch.redirect_filter') || []
21
20
  else
22
21
  []
23
22
  end
24
23
  end
25
24
 
26
- def location_filter_match?(filters)
27
- filters.any? do |filter|
25
+ def location_filter_match?
26
+ location_filters.any? do |filter|
28
27
  if String === filter
29
28
  location.include?(filter)
30
29
  elsif Regexp === filter
31
- location.match(filter)
30
+ location =~ filter
32
31
  end
33
32
  end
34
33
  end
@@ -2,9 +2,23 @@ module ActionDispatch
2
2
  module Http
3
3
  # Provides access to the request's HTTP headers from the environment.
4
4
  #
5
- # env = { "CONTENT_TYPE" => "text/plain" }
5
+ # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
6
6
  # headers = ActionDispatch::Http::Headers.new(env)
7
7
  # headers["Content-Type"] # => "text/plain"
8
+ # headers["User-Agent"] # => "curl/7.43.0"
9
+ #
10
+ # Also note that when headers are mapped to CGI-like variables by the Rack
11
+ # server, both dashes and underscores are converted to underscores. This
12
+ # ambiguity cannot be resolved at this stage anymore. Both underscores and
13
+ # dashes have to be interpreted as if they were originally sent as dashes.
14
+ #
15
+ # # GET / HTTP/1.1
16
+ # # ...
17
+ # # User-Agent: curl/7.43.0
18
+ # # X_Custom_Header: token
19
+ #
20
+ # headers["X_Custom_Header"] # => nil
21
+ # headers["X-Custom-Header"] # => "token"
8
22
  class Headers
9
23
  CGI_VARIABLES = Set.new(%W[
10
24
  AUTH_TYPE
@@ -30,27 +44,37 @@ module ActionDispatch
30
44
  HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
31
45
 
32
46
  include Enumerable
33
- attr_reader :env
34
47
 
35
- def initialize(env = {}) # :nodoc:
36
- @env = env
48
+ def self.from_hash(hash)
49
+ new ActionDispatch::Request.new hash
50
+ end
51
+
52
+ def initialize(request) # :nodoc:
53
+ @req = request
37
54
  end
38
55
 
39
56
  # Returns the value for the given key mapped to @env.
40
57
  def [](key)
41
- @env[env_name(key)]
58
+ @req.get_header env_name(key)
42
59
  end
43
60
 
44
61
  # Sets the given value for the key mapped to @env.
45
62
  def []=(key, value)
46
- @env[env_name(key)] = value
63
+ @req.set_header env_name(key), value
64
+ end
65
+
66
+ # Add a value to a multivalued header like Vary or Accept-Encoding.
67
+ def add(key, value)
68
+ @req.add_header env_name(key), value
47
69
  end
48
70
 
49
71
  def key?(key)
50
- @env.key? env_name(key)
72
+ @req.has_header? env_name(key)
51
73
  end
52
74
  alias :include? :key?
53
75
 
76
+ DEFAULT = Object.new # :nodoc:
77
+
54
78
  # Returns the value for the given key mapped to @env.
55
79
  #
56
80
  # If the key is not found and an optional code block is not provided,
@@ -58,18 +82,22 @@ module ActionDispatch
58
82
  #
59
83
  # If the code block is provided, then it will be run and
60
84
  # its result returned.
61
- def fetch(key, *args, &block)
62
- @env.fetch env_name(key), *args, &block
85
+ def fetch(key, default = DEFAULT)
86
+ @req.fetch_header(env_name(key)) do
87
+ return default unless default == DEFAULT
88
+ return yield if block_given?
89
+ raise NameError, key
90
+ end
63
91
  end
64
92
 
65
93
  def each(&block)
66
- @env.each(&block)
94
+ @req.each_header(&block)
67
95
  end
68
96
 
69
97
  # Returns a new Http::Headers instance containing the contents of
70
98
  # <tt>headers_or_env</tt> and the original instance.
71
99
  def merge(headers_or_env)
72
- headers = Http::Headers.new(env.dup)
100
+ headers = @req.dup.headers
73
101
  headers.merge!(headers_or_env)
74
102
  headers
75
103
  end
@@ -79,12 +107,15 @@ module ActionDispatch
79
107
  # <tt>headers_or_env</tt>.
80
108
  def merge!(headers_or_env)
81
109
  headers_or_env.each do |key, value|
82
- self[env_name(key)] = value
110
+ @req.set_header env_name(key), value
83
111
  end
84
112
  end
85
113
 
114
+ def env; @req.env.dup; end
115
+
86
116
  private
87
- # Converts a HTTP header name to an environment variable name if it is
117
+
118
+ # Converts an HTTP header name to an environment variable name if it is
88
119
  # not contained within the headers hash.
89
120
  def env_name(key)
90
121
  key = key.to_s
@@ -10,19 +10,18 @@ module ActionDispatch
10
10
  self.ignore_accept_header = false
11
11
  end
12
12
 
13
- attr_reader :variant
14
-
15
- # The MIME type of the HTTP request, such as Mime::XML.
13
+ # The MIME type of the HTTP request, such as Mime[:xml].
16
14
  #
17
15
  # For backward compatibility, the post \format is extracted from the
18
16
  # X-Post-Data-Format HTTP header if present.
19
17
  def content_mime_type
20
- @env["action_dispatch.request.content_type"] ||= begin
21
- if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
18
+ fetch_header("action_dispatch.request.content_type") do |k|
19
+ v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/
22
20
  Mime::Type.lookup($1.strip.downcase)
23
21
  else
24
22
  nil
25
23
  end
24
+ set_header k, v
26
25
  end
27
26
  end
28
27
 
@@ -30,63 +29,75 @@ module ActionDispatch
30
29
  content_mime_type && content_mime_type.to_s
31
30
  end
32
31
 
32
+ def has_content_type?
33
+ has_header? 'CONTENT_TYPE'
34
+ end
35
+
33
36
  # Returns the accepted MIME type for the request.
34
37
  def accepts
35
- @env["action_dispatch.request.accepts"] ||= begin
36
- header = @env['HTTP_ACCEPT'].to_s.strip
38
+ fetch_header("action_dispatch.request.accepts") do |k|
39
+ header = get_header('HTTP_ACCEPT').to_s.strip
37
40
 
38
- if header.empty?
41
+ v = if header.empty?
39
42
  [content_mime_type]
40
43
  else
41
44
  Mime::Type.parse(header)
42
45
  end
46
+ set_header k, v
43
47
  end
44
48
  end
45
49
 
46
50
  # Returns the MIME type for the \format used in the request.
47
51
  #
48
- # GET /posts/5.xml | request.format => Mime::XML
49
- # GET /posts/5.xhtml | request.format => Mime::HTML
50
- # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first
52
+ # GET /posts/5.xml | request.format => Mime[:xml]
53
+ # GET /posts/5.xhtml | request.format => Mime[:html]
54
+ # GET /posts/5 | request.format => Mime[:html] or Mime[:js], or request.accepts.first
51
55
  #
52
56
  def format(view_path = [])
53
57
  formats.first || Mime::NullType.instance
54
58
  end
55
59
 
56
60
  def formats
57
- @env["action_dispatch.request.formats"] ||= begin
61
+ fetch_header("action_dispatch.request.formats") do |k|
58
62
  params_readable = begin
59
63
  parameters[:format]
60
64
  rescue ActionController::BadRequest
61
65
  false
62
66
  end
63
67
 
64
- if params_readable
68
+ v = if params_readable
65
69
  Array(Mime[parameters[:format]])
66
70
  elsif use_accept_header && valid_accept_header
67
71
  accepts
72
+ elsif extension_format = format_from_path_extension
73
+ [extension_format]
68
74
  elsif xhr?
69
- [Mime::JS]
75
+ [Mime[:js]]
70
76
  else
71
- [Mime::HTML]
77
+ [Mime[:html]]
72
78
  end
79
+ set_header k, v
73
80
  end
74
81
  end
75
82
 
76
83
  # Sets the \variant for template.
77
84
  def variant=(variant)
78
- if variant.is_a?(Symbol)
79
- @variant = [variant]
80
- elsif variant.nil? || variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) }
81
- @variant = variant
85
+ variant = Array(variant)
86
+
87
+ if variant.all? { |v| v.is_a?(Symbol) }
88
+ @variant = ActiveSupport::ArrayInquirer.new(variant)
82
89
  else
83
- raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \
90
+ raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols. " \
84
91
  "For security reasons, never directly set the variant to a user-provided value, " \
85
92
  "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \
86
93
  "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'"
87
94
  end
88
95
  end
89
96
 
97
+ def variant
98
+ @variant ||= ActiveSupport::ArrayInquirer.new
99
+ end
100
+
90
101
  # Sets the \format by string extension, which can be used to force custom formats
91
102
  # that are not controlled by the extension.
92
103
  #
@@ -100,7 +111,7 @@ module ActionDispatch
100
111
  # end
101
112
  def format=(extension)
102
113
  parameters[:format] = extension.to_s
103
- @env["action_dispatch.request.formats"] = [Mime::Type.lookup_by_extension(parameters[:format])]
114
+ set_header "action_dispatch.request.formats", [Mime::Type.lookup_by_extension(parameters[:format])]
104
115
  end
105
116
 
106
117
  # Sets the \formats by string extensions. This differs from #format= by allowing you
@@ -119,9 +130,9 @@ module ActionDispatch
119
130
  # end
120
131
  def formats=(extensions)
121
132
  parameters[:format] = extensions.first.to_s
122
- @env["action_dispatch.request.formats"] = extensions.collect do |extension|
133
+ set_header "action_dispatch.request.formats", extensions.collect { |extension|
123
134
  Mime::Type.lookup_by_extension(extension)
124
- end
135
+ }
125
136
  end
126
137
 
127
138
  # Receives an array of mimes and return the first user sent mime that
@@ -151,6 +162,13 @@ module ActionDispatch
151
162
  def use_accept_header
152
163
  !self.class.ignore_accept_header
153
164
  end
165
+
166
+ def format_from_path_extension
167
+ path = get_header('action_dispatch.original_path') || get_header('PATH_INFO')
168
+ if match = path && path.match(/\.(\w+)\z/)
169
+ Mime[match.captures.first]
170
+ end
171
+ end
154
172
  end
155
173
  end
156
174
  end