actionpack 8.0.2 → 8.1.0.beta1

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +252 -137
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/asset_paths.rb +4 -2
  5. data/lib/abstract_controller/base.rb +11 -14
  6. data/lib/abstract_controller/caching.rb +6 -3
  7. data/lib/abstract_controller/collector.rb +1 -1
  8. data/lib/abstract_controller/logger.rb +2 -1
  9. data/lib/action_controller/base.rb +1 -1
  10. data/lib/action_controller/caching.rb +1 -2
  11. data/lib/action_controller/form_builder.rb +1 -1
  12. data/lib/action_controller/log_subscriber.rb +7 -0
  13. data/lib/action_controller/metal/allow_browser.rb +1 -1
  14. data/lib/action_controller/metal/conditional_get.rb +25 -0
  15. data/lib/action_controller/metal/data_streaming.rb +1 -3
  16. data/lib/action_controller/metal/exceptions.rb +5 -0
  17. data/lib/action_controller/metal/flash.rb +1 -4
  18. data/lib/action_controller/metal/head.rb +3 -1
  19. data/lib/action_controller/metal/live.rb +0 -6
  20. data/lib/action_controller/metal/permissions_policy.rb +9 -0
  21. data/lib/action_controller/metal/rate_limiting.rb +22 -7
  22. data/lib/action_controller/metal/redirecting.rb +63 -7
  23. data/lib/action_controller/metal/renderers.rb +27 -6
  24. data/lib/action_controller/metal/rendering.rb +8 -2
  25. data/lib/action_controller/metal/request_forgery_protection.rb +18 -10
  26. data/lib/action_controller/metal/rescue.rb +9 -0
  27. data/lib/action_controller/railtie.rb +2 -6
  28. data/lib/action_controller/renderer.rb +0 -1
  29. data/lib/action_dispatch/constants.rb +6 -0
  30. data/lib/action_dispatch/http/cache.rb +111 -1
  31. data/lib/action_dispatch/http/content_security_policy.rb +13 -1
  32. data/lib/action_dispatch/http/filter_parameters.rb +5 -3
  33. data/lib/action_dispatch/http/mime_negotiation.rb +8 -3
  34. data/lib/action_dispatch/http/mime_types.rb +1 -0
  35. data/lib/action_dispatch/http/param_builder.rb +28 -27
  36. data/lib/action_dispatch/http/parameters.rb +3 -3
  37. data/lib/action_dispatch/http/permissions_policy.rb +4 -0
  38. data/lib/action_dispatch/http/query_parser.rb +12 -10
  39. data/lib/action_dispatch/http/request.rb +10 -5
  40. data/lib/action_dispatch/http/response.rb +65 -17
  41. data/lib/action_dispatch/http/url.rb +101 -5
  42. data/lib/action_dispatch/journey/gtg/simulator.rb +33 -12
  43. data/lib/action_dispatch/journey/gtg/transition_table.rb +29 -39
  44. data/lib/action_dispatch/journey/nodes/node.rb +2 -1
  45. data/lib/action_dispatch/journey/route.rb +45 -31
  46. data/lib/action_dispatch/journey/router/utils.rb +8 -14
  47. data/lib/action_dispatch/journey/router.rb +59 -81
  48. data/lib/action_dispatch/journey/routes.rb +7 -0
  49. data/lib/action_dispatch/journey/visitors.rb +55 -23
  50. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  51. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  52. data/lib/action_dispatch/middleware/debug_exceptions.rb +10 -2
  53. data/lib/action_dispatch/middleware/debug_view.rb +11 -0
  54. data/lib/action_dispatch/middleware/exception_wrapper.rb +14 -8
  55. data/lib/action_dispatch/middleware/executor.rb +12 -2
  56. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -6
  57. data/lib/action_dispatch/middleware/session/cache_store.rb +17 -0
  58. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  59. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -2
  60. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  61. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  62. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  63. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +1 -0
  64. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  65. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  66. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  67. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  68. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  69. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  70. data/lib/action_dispatch/railtie.rb +10 -2
  71. data/lib/action_dispatch/routing/inspector.rb +4 -1
  72. data/lib/action_dispatch/routing/mapper.rb +323 -173
  73. data/lib/action_dispatch/routing/route_set.rb +3 -6
  74. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +2 -2
  75. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  76. data/lib/action_dispatch/testing/assertions/response.rb +14 -0
  77. data/lib/action_dispatch/testing/assertions/routing.rb +11 -3
  78. data/lib/action_pack/gem_version.rb +3 -3
  79. metadata +13 -12
@@ -2,15 +2,12 @@
2
2
 
3
3
  # :markup: markdown
4
4
 
5
+ require "cgi/escape"
6
+ require "cgi/util" if RUBY_VERSION < "3.5"
5
7
  require "action_dispatch/journey/router/utils"
6
8
  require "action_dispatch/journey/routes"
7
9
  require "action_dispatch/journey/formatter"
8
-
9
- before = $-w
10
- $-w = false
11
10
  require "action_dispatch/journey/parser"
12
- $-w = before
13
-
14
11
  require "action_dispatch/journey/route"
15
12
  require "action_dispatch/journey/path/pattern"
16
13
 
@@ -31,71 +28,78 @@ module ActionDispatch
31
28
  end
32
29
 
33
30
  def serve(req)
34
- find_routes(req) do |match, parameters, route|
35
- set_params = req.path_parameters
36
- path_info = req.path_info
37
- script_name = req.script_name
38
-
39
- unless route.path.anchored
40
- req.script_name = (script_name.to_s + match.to_s).chomp("/")
41
- req.path_info = match.post_match
42
- req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
43
- end
44
-
45
- tmp_params = set_params.merge route.defaults
46
- parameters.each_pair { |key, val|
47
- tmp_params[key] = val.force_encoding(::Encoding::UTF_8)
48
- }
49
-
50
- req.path_parameters = tmp_params
51
- req.route_uri_pattern = route.path.spec.to_s
31
+ recognize(req) do |route, parameters|
32
+ req.path_parameters = parameters
33
+ req.route = route
52
34
 
53
35
  _, headers, _ = response = route.app.serve(req)
54
36
 
55
- if "pass" == headers[Constants::X_CASCADE]
56
- req.script_name = script_name
57
- req.path_info = path_info
58
- req.path_parameters = set_params
59
- next
60
- end
61
-
62
- return response
37
+ return response unless headers[Constants::X_CASCADE] == "pass"
63
38
  end
64
39
 
65
40
  [404, { Constants::X_CASCADE => "pass" }, ["Not Found"]]
66
41
  end
67
42
 
68
- def recognize(rails_req)
69
- find_routes(rails_req) do |match, parameters, route|
70
- unless route.path.anchored
71
- rails_req.script_name = match.to_s
72
- rails_req.path_info = match.post_match
73
- rails_req.path_info = "/" + rails_req.path_info unless rails_req.path_info.start_with? "/"
74
- end
43
+ def recognize(req, &block)
44
+ req_params = req.path_parameters
45
+ path_info = req.path_info
46
+ script_name = req.script_name
75
47
 
76
- parameters = route.defaults.merge parameters
77
- yield(route, parameters)
78
- end
79
- end
48
+ routes = filter_routes(path_info)
80
49
 
81
- def visualizer
82
- tt = GTG::Builder.new(ast).transition_table
83
- groups = partitioned_routes.first.map(&:ast).group_by(&:to_s)
84
- asts = groups.values.map(&:first)
85
- tt.visualizer(asts)
86
- end
50
+ custom_routes.each { |r|
51
+ routes << r if r.path.match?(path_info)
52
+ }
87
53
 
88
- private
89
- def partitioned_routes
90
- routes.partition { |r|
91
- r.path.anchored && r.path.requirements_anchored?
92
- }
54
+ if req.head?
55
+ routes = match_head_routes(routes, req)
56
+ else
57
+ routes.select! { |r| r.matches?(req) }
93
58
  end
94
59
 
95
- def ast
96
- routes.ast
60
+ if routes.size > 1
61
+ routes.sort! do |a, b|
62
+ a.precedence <=> b.precedence
63
+ end
97
64
  end
98
65
 
66
+ routes.each do |r|
67
+ match_data = r.path.match(path_info)
68
+
69
+ path_parameters = req_params.merge r.defaults
70
+
71
+ index = 1
72
+ match_data.names.each do |name|
73
+ if val = match_data[index]
74
+ val = if val.include?("%")
75
+ CGI.unescapeURIComponent(val)
76
+ else
77
+ val
78
+ end
79
+ val.force_encoding(::Encoding::UTF_8)
80
+ path_parameters[name.to_sym] = val
81
+ end
82
+ index += 1
83
+ end
84
+
85
+ if r.path.anchored
86
+ yield(r, path_parameters)
87
+ else
88
+ req.script_name = (script_name.to_s + match_data.to_s).chomp("/")
89
+ req.path_info = match_data.post_match
90
+ req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
91
+
92
+ yield(r, path_parameters)
93
+
94
+ req.script_name = script_name
95
+ req.path_info = path_info
96
+ end
97
+
98
+ req.path_parameters = req_params
99
+ end
100
+ end
101
+
102
+ private
99
103
  def simulator
100
104
  routes.simulator
101
105
  end
@@ -105,35 +109,9 @@ module ActionDispatch
105
109
  end
106
110
 
107
111
  def filter_routes(path)
108
- return [] unless ast
109
112
  simulator.memos(path) { [] }
110
113
  end
111
114
 
112
- def find_routes(req)
113
- path_info = req.path_info
114
- routes = filter_routes(path_info).concat custom_routes.find_all { |r|
115
- r.path.match?(path_info)
116
- }
117
-
118
- if req.head?
119
- routes = match_head_routes(routes, req)
120
- else
121
- routes.select! { |r| r.matches?(req) }
122
- end
123
-
124
- routes.sort_by!(&:precedence)
125
-
126
- routes.each { |r|
127
- match_data = r.path.match(path_info)
128
- path_parameters = {}
129
- match_data.names.each_with_index { |name, i|
130
- val = match_data[i + 1]
131
- path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
132
- }
133
- yield [match_data, path_parameters, r]
134
- }
135
- end
136
-
137
115
  def match_head_routes(routes, req)
138
116
  head_routes = routes.select { |r| r.requires_matching_verb? && r.matches?(req) }
139
117
  return head_routes unless head_routes.empty?
@@ -72,6 +72,13 @@ module ActionDispatch
72
72
  route
73
73
  end
74
74
 
75
+ def visualizer
76
+ tt = GTG::Builder.new(ast).transition_table
77
+ groups = anchored_routes.map(&:ast).group_by(&:to_s)
78
+ asts = groups.values.map(&:first)
79
+ tt.visualizer(asts)
80
+ end
81
+
75
82
  private
76
83
  def clear_cache!
77
84
  @ast = nil
@@ -128,8 +128,8 @@ module ActionDispatch
128
128
  def visit_DOT(n, seed); terminal(n, seed); end
129
129
 
130
130
  instance_methods(false).each do |pim|
131
- next unless pim =~ /^visit_(.*)$/
132
- DISPATCH_CACHE[$1.to_sym] = pim
131
+ next unless pim.start_with?("visit_")
132
+ DISPATCH_CACHE[pim.name.delete_prefix("visit_").to_sym] = pim
133
133
  end
134
134
  end
135
135
 
@@ -167,32 +167,64 @@ module ActionDispatch
167
167
  INSTANCE = new
168
168
  end
169
169
 
170
- class String < FunctionalVisitor # :nodoc:
171
- private
172
- def binary(node, seed)
173
- visit(node.right, visit(node.left, seed))
174
- end
175
-
176
- def nary(node, seed)
170
+ class String # :nodoc:
171
+ def accept(node, seed)
172
+ case node.type
173
+ when :DOT
174
+ seed << node.left
175
+ when :LITERAL
176
+ seed << node.left
177
+ when :SYMBOL
178
+ seed << node.left
179
+ when :SLASH
180
+ seed << node.left
181
+ when :CAT
182
+ accept(node.right, accept(node.left, seed))
183
+ when :STAR
184
+ accept(node.left, seed)
185
+ when :OR
177
186
  last_child = node.children.last
178
- node.children.inject(seed) { |s, c|
179
- string = visit(c, s)
180
- string << "|" unless last_child == c
181
- string
182
- }
183
- end
184
-
185
- def terminal(node, seed)
186
- seed + node.left
187
- end
188
-
189
- def visit_GROUP(node, seed)
190
- visit(node.left, seed.dup << "(") << ")"
187
+ node.children.each do |c|
188
+ accept(c, seed)
189
+ seed << "|" unless last_child == c
190
+ end
191
+ seed
192
+ when :GROUP
193
+ accept(node.left, seed << "(") << ")"
194
+ else
195
+ raise "Unknown node type: #{node.type}"
191
196
  end
197
+ end
192
198
 
193
- INSTANCE = new
199
+ INSTANCE = new
194
200
  end
195
201
 
202
+ # class String < FunctionalVisitor # :nodoc:
203
+ # private
204
+ # def binary(node, seed)
205
+ # visit(node.right, visit(node.left, seed))
206
+ # end
207
+ #
208
+ # def nary(node, seed)
209
+ # last_child = node.children.last
210
+ # node.children.inject(seed) { |s, c|
211
+ # string = visit(c, s)
212
+ # string << "|" unless last_child == c
213
+ # string
214
+ # }
215
+ # end
216
+ #
217
+ # def terminal(node, seed)
218
+ # seed + node.left
219
+ # end
220
+ #
221
+ # def visit_GROUP(node, seed)
222
+ # visit(node.left, seed.dup << "(") << ")"
223
+ # end
224
+ #
225
+ # INSTANCE = new
226
+ # end
227
+
196
228
  class Dot < FunctionalVisitor # :nodoc:
197
229
  def initialize
198
230
  @nodes = []
@@ -105,12 +105,10 @@ function match(input) {
105
105
  }
106
106
 
107
107
  if(stdparam_states[state] && default_re.test(token)) {
108
- for(var key in stdparam_states[state]) {
109
- var new_state = stdparam_states[state][key];
110
- highlight_edge(state, new_state);
111
- highlight_state(new_state);
112
- new_states.push([new_state, null]);
113
- }
108
+ var new_state = stdparam_states[state];
109
+ highlight_edge(state, new_state);
110
+ highlight_state(new_state);
111
+ new_states.push([new_state, null]);
114
112
  }
115
113
  }
116
114
 
@@ -610,8 +610,10 @@ module ActionDispatch
610
610
  end
611
611
 
612
612
  def check_for_overflow!(name, options)
613
- if options[:value].bytesize > MAX_COOKIE_SIZE
614
- raise CookieOverflow, "#{name} cookie overflowed with size #{options[:value].bytesize} bytes"
613
+ total_size = name.to_s.bytesize + options[:value].bytesize
614
+
615
+ if total_size > MAX_COOKIE_SIZE
616
+ raise CookieOverflow, "#{name} cookie overflowed with size #{total_size} bytes"
615
617
  end
616
618
  end
617
619
  end
@@ -65,7 +65,9 @@ module ActionDispatch
65
65
  content_type = Mime[:text]
66
66
  end
67
67
 
68
- if api_request?(content_type)
68
+ if request.head?
69
+ render(wrapper.status_code, "", content_type)
70
+ elsif api_request?(content_type)
69
71
  render_for_api_request(content_type, wrapper)
70
72
  else
71
73
  render_for_browser_request(request, wrapper)
@@ -125,6 +127,7 @@ module ActionDispatch
125
127
  trace_to_show: wrapper.trace_to_show,
126
128
  routes_inspector: routes_inspector(wrapper),
127
129
  source_extracts: wrapper.source_extracts,
130
+ exception_message_for_copy: compose_exception_message(wrapper).join("\n"),
128
131
  )
129
132
  end
130
133
 
@@ -138,6 +141,11 @@ module ActionDispatch
138
141
  return unless logger
139
142
  return if !log_rescued_responses?(request) && wrapper.rescue_response?
140
143
 
144
+ message = compose_exception_message(wrapper)
145
+ log_array(logger, message, request)
146
+ end
147
+
148
+ def compose_exception_message(wrapper)
141
149
  trace = wrapper.exception_trace
142
150
 
143
151
  message = []
@@ -166,7 +174,7 @@ module ActionDispatch
166
174
  end
167
175
  end
168
176
 
169
- log_array(logger, message, request)
177
+ message
170
178
  end
171
179
 
172
180
  def log_array(logger, lines, request)
@@ -55,6 +55,17 @@ module ActionDispatch
55
55
  end
56
56
  end
57
57
 
58
+ def editor_url(location, line: nil)
59
+ if editor = ActiveSupport::Editor.current
60
+ line ||= location&.lineno
61
+ absolute_path = location&.absolute_path
62
+
63
+ if absolute_path && line && File.exist?(absolute_path)
64
+ editor.url_for(absolute_path, line)
65
+ end
66
+ end
67
+ end
68
+
58
69
  def protect_against_forgery?
59
70
  false
60
71
  end
@@ -18,11 +18,12 @@ module ActionDispatch
18
18
  "ActionController::UnknownFormat" => :not_acceptable,
19
19
  "ActionDispatch::Http::MimeNegotiation::InvalidType" => :not_acceptable,
20
20
  "ActionController::MissingExactTemplate" => :not_acceptable,
21
- "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
22
- "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
21
+ "ActionController::InvalidAuthenticityToken" => ActionDispatch::Constants::UNPROCESSABLE_CONTENT,
22
+ "ActionController::InvalidCrossOriginRequest" => ActionDispatch::Constants::UNPROCESSABLE_CONTENT,
23
23
  "ActionDispatch::Http::Parameters::ParseError" => :bad_request,
24
24
  "ActionController::BadRequest" => :bad_request,
25
25
  "ActionController::ParameterMissing" => :bad_request,
26
+ "ActionController::TooManyRequests" => :too_many_requests,
26
27
  "Rack::QueryParser::ParameterTypeError" => :bad_request,
27
28
  "Rack::QueryParser::InvalidParameterError" => :bad_request
28
29
  )
@@ -148,15 +149,20 @@ module ActionDispatch
148
149
  application_trace_with_ids = []
149
150
  framework_trace_with_ids = []
150
151
  full_trace_with_ids = []
152
+ application_traces = application_trace.map(&:to_s)
151
153
 
154
+ full_trace = backtrace_cleaner&.clean_locations(backtrace, :all).presence || backtrace
152
155
  full_trace.each_with_index do |trace, idx|
156
+ filtered_trace = backtrace_cleaner&.clean_frame(trace, :all) || trace
157
+
153
158
  trace_with_id = {
154
159
  exception_object_id: @exception.object_id,
155
160
  id: idx,
156
- trace: trace
161
+ trace: trace,
162
+ filtered_trace: filtered_trace,
157
163
  }
158
164
 
159
- if application_trace.include?(trace)
165
+ if application_traces.include?(filtered_trace.to_s)
160
166
  application_trace_with_ids << trace_with_id
161
167
  else
162
168
  framework_trace_with_ids << trace_with_id
@@ -173,7 +179,7 @@ module ActionDispatch
173
179
  end
174
180
 
175
181
  def self.status_code_for_exception(class_name)
176
- Rack::Utils.status_code(@@rescue_responses[class_name])
182
+ ActionDispatch::Response.rack_status_code(@@rescue_responses[class_name])
177
183
  end
178
184
 
179
185
  def show?(request)
@@ -197,7 +203,7 @@ module ActionDispatch
197
203
 
198
204
  def source_extracts
199
205
  backtrace.map do |trace|
200
- extract_source(trace)
206
+ extract_source(trace).merge(trace: trace)
201
207
  end
202
208
  end
203
209
 
@@ -261,13 +267,13 @@ module ActionDispatch
261
267
  end
262
268
 
263
269
  (@exception.backtrace_locations || []).map do |loc|
264
- if built_methods.key?(loc.label.to_s)
270
+ if built_methods.key?(loc.base_label)
265
271
  thread_backtrace_location = if loc.respond_to?(:__getobj__)
266
272
  loc.__getobj__
267
273
  else
268
274
  loc
269
275
  end
270
- SourceMapLocation.new(thread_backtrace_location, built_methods[loc.label.to_s])
276
+ SourceMapLocation.new(thread_backtrace_location, built_methods[loc.base_label])
271
277
  else
272
278
  loc
273
279
  end
@@ -12,6 +12,10 @@ module ActionDispatch
12
12
 
13
13
  def call(env)
14
14
  state = @executor.run!(reset: true)
15
+ if response_finished = env["rack.response_finished"]
16
+ response_finished << proc { state.complete! }
17
+ end
18
+
15
19
  begin
16
20
  response = @app.call(env)
17
21
 
@@ -20,7 +24,11 @@ module ActionDispatch
20
24
  @executor.error_reporter.report(error, handled: false, source: "application.action_dispatch")
21
25
  end
22
26
 
23
- returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
27
+ unless response_finished
28
+ response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
29
+ end
30
+ returned = true
31
+ response
24
32
  rescue Exception => error
25
33
  request = ActionDispatch::Request.new env
26
34
  backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
@@ -28,7 +36,9 @@ module ActionDispatch
28
36
  @executor.error_reporter.report(wrapper.unwrapped_exception, handled: false, source: "application.action_dispatch")
29
37
  raise
30
38
  ensure
31
- state.complete! unless returned
39
+ if !returned && !response_finished
40
+ state.complete!
41
+ end
32
42
  end
33
43
  end
34
44
  end
@@ -25,14 +25,14 @@ module ActionDispatch
25
25
  def call(env)
26
26
  request = ActionDispatch::Request.new(env)
27
27
  status = request.path_info[1..-1].to_i
28
- begin
29
- content_type = request.formats.first
30
- rescue ActionDispatch::Http::MimeNegotiation::InvalidType
31
- content_type = Mime[:text]
32
- end
28
+ content_type = request.formats.first
33
29
  body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
34
30
 
35
- render(status, content_type, body)
31
+ if env["action_dispatch.original_request_method"] == "HEAD"
32
+ render_format(status, content_type, "")
33
+ else
34
+ render(status, content_type, body)
35
+ end
36
36
  end
37
37
 
38
38
  private
@@ -18,11 +18,16 @@ module ActionDispatch
18
18
  # * `expire_after` - The length of time a session will be stored before
19
19
  # automatically expiring. By default, the `:expires_in` option of the cache
20
20
  # is used.
21
+ # * `check_collisions` - Check if newly generated session ids aren't already in use.
22
+ # If for some reason 128 bits of randomness aren't considered secure enough to avoid
23
+ # collisions, this option can be enabled to ensure newly generated ids aren't in use.
24
+ # By default, it is set to `false` to avoid additional cache write operations.
21
25
  #
22
26
  class CacheStore < AbstractSecureStore
23
27
  def initialize(app, options = {})
24
28
  @cache = options[:cache] || Rails.cache
25
29
  options[:expire_after] ||= @cache.options[:expires_in]
30
+ @check_collisions = options[:check_collisions] || false
26
31
  super
27
32
  end
28
33
 
@@ -61,6 +66,18 @@ module ActionDispatch
61
66
  def get_session_with_fallback(sid)
62
67
  @cache.read(cache_key(sid.private_id)) || @cache.read(cache_key(sid.public_id))
63
68
  end
69
+
70
+ def generate_sid
71
+ if @check_collisions
72
+ loop do
73
+ sid = super
74
+ key = cache_key(sid.private_id)
75
+ break sid if @cache.write(key, {}, unless_exist: true, expires_in: default_options[:expire_after])
76
+ end
77
+ else
78
+ super
79
+ end
80
+ end
64
81
  end
65
82
  end
66
83
  end
@@ -0,0 +1 @@
1
+ <button onclick="copyAsText.bind(this)()">Copy as text</button>
@@ -11,8 +11,9 @@
11
11
  <tr>
12
12
  <td>
13
13
  <pre class="line_numbers">
14
- <% source_extract[:code].each_key do |line_number| %>
15
- <span><%= line_number -%></span>
14
+ <% source_extract[:code].each_key do |line| %>
15
+ <% file_url = editor_url(source_extract[:trace], line: line) %>
16
+ <span><%= link_to_if file_url, line, file_url -%></span>
16
17
  <% end %>
17
18
  </pre>
18
19
  </td>
@@ -13,13 +13,17 @@
13
13
  <% end %>
14
14
 
15
15
  <% traces.each do |name, trace| %>
16
- <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;">
16
+ <div id="<%= "#{name.gsub(/\s/, '-')}-#{error_index}" %>" class="trace-container" style="display: <%= (name == trace_to_show) ? 'block' : 'none' %>;">
17
17
  <code class="traces">
18
18
  <% trace.each do |frame| %>
19
- <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#">
20
- <%= frame[:trace] %>
21
- </a>
22
- <br>
19
+ <div class="trace">
20
+ <% file_url = editor_url(frame[:trace]) %>
21
+ <%= file_url && link_to("✏️", file_url, class: "edit-icon") %>
22
+ <a class="trace-frames trace-frames-<%= error_index %>" data-exception-object-id="<%= frame[:exception_object_id] %>" data-frame-id="<%= frame[:id] %>" href="#">
23
+ <%= frame[:trace] %>
24
+ </a>
25
+ <br>
26
+ </div>
23
27
  <% end %>
24
28
  </code>
25
29
  </div>
@@ -1,4 +1,5 @@
1
1
  <header>
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>Blocked hosts: <%= @hosts.join(", ") %></h1>
3
4
  </header>
4
5
  <main role="main" id="container">
@@ -1,4 +1,5 @@
1
1
  <header>
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>
3
4
  <%= @exception_wrapper.exception_class_name %>
4
5
  <% if params_valid? && @request.parameters['controller'] %>
@@ -1,4 +1,5 @@
1
1
  <header role="banner">
2
+ <%= render "rescues/copy_button" %>
2
3
  <h1>
3
4
  <%= @exception.class.to_s %>
4
5
  <% if @request.parameters['controller'] %>