better_errors 2.8.1 → 2.10.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +142 -0
  3. data/.github/workflows/release.yml +64 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +6 -1
  6. data/README.md +2 -2
  7. data/gemfiles/pry010.gemfile +2 -1
  8. data/gemfiles/pry011.gemfile +2 -1
  9. data/gemfiles/pry09.gemfile +2 -1
  10. data/gemfiles/rack.gemfile +2 -1
  11. data/gemfiles/rack_boc.gemfile +2 -1
  12. data/gemfiles/rails42.gemfile +2 -1
  13. data/gemfiles/rails42_boc.gemfile +2 -1
  14. data/gemfiles/rails42_haml.gemfile +2 -1
  15. data/gemfiles/rails50.gemfile +2 -1
  16. data/gemfiles/rails50_boc.gemfile +2 -1
  17. data/gemfiles/rails50_haml.gemfile +2 -1
  18. data/gemfiles/rails51.gemfile +2 -1
  19. data/gemfiles/rails51_boc.gemfile +2 -1
  20. data/gemfiles/rails51_haml.gemfile +2 -1
  21. data/gemfiles/rails52.gemfile +2 -1
  22. data/gemfiles/rails52_boc.gemfile +2 -1
  23. data/gemfiles/rails52_haml.gemfile +2 -1
  24. data/gemfiles/rails60.gemfile +2 -1
  25. data/gemfiles/rails60_boc.gemfile +2 -1
  26. data/gemfiles/rails60_haml.gemfile +2 -1
  27. data/gemfiles/rails61.gemfile +8 -0
  28. data/gemfiles/rails61_boc.gemfile +9 -0
  29. data/gemfiles/rails61_haml.gemfile +9 -0
  30. data/lib/better_errors.rb +15 -34
  31. data/lib/better_errors/editor.rb +103 -0
  32. data/lib/better_errors/error_page.rb +30 -14
  33. data/lib/better_errors/exception_hint.rb +29 -0
  34. data/lib/better_errors/middleware.rb +21 -5
  35. data/lib/better_errors/raised_exception.rb +8 -1
  36. data/lib/better_errors/templates/main.erb +111 -28
  37. data/lib/better_errors/templates/text.erb +6 -3
  38. data/lib/better_errors/templates/variable_info.erb +18 -15
  39. data/lib/better_errors/version.rb +1 -1
  40. metadata +13 -6
  41. data/.travis.yml +0 -111
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 6.1.0rc"
4
+ gem "binding_of_caller"
5
+
6
+ gem 'simplecov', require: false
7
+ gem 'simplecov-lcov', require: false
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 6.1.0rc"
4
+ gem "haml"
5
+
6
+ gem 'simplecov', require: false
7
+ gem 'simplecov-lcov', require: false
8
+
9
+ gemspec path: "../"
@@ -3,6 +3,7 @@ require "erubi"
3
3
  require "coderay"
4
4
  require "uri"
5
5
 
6
+ require "better_errors/version"
6
7
  require "better_errors/code_formatter"
7
8
  require "better_errors/inspectable_value"
8
9
  require "better_errors/error_page"
@@ -10,21 +11,9 @@ require "better_errors/middleware"
10
11
  require "better_errors/raised_exception"
11
12
  require "better_errors/repl"
12
13
  require "better_errors/stack_frame"
13
- require "better_errors/version"
14
+ require "better_errors/editor"
14
15
 
15
16
  module BetterErrors
16
- POSSIBLE_EDITOR_PRESETS = [
17
- { symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
18
- { symbols: [:macvim, :mvim], sniff: /vim/i, url: proc { |file, line| "mvim://open?url=file://#{file}&line=#{line}" } },
19
- { symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
20
- { symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
21
- { symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
22
- { symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
23
- { symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
24
- { symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" },
25
- { symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
26
- ]
27
-
28
17
  class << self
29
18
  # The path to the root of the application. Better Errors uses this property
30
19
  # to determine if a file in a backtrace should be considered an application
@@ -64,17 +53,18 @@ module BetterErrors
64
53
  @maximum_variable_inspect_size = 100_000
65
54
  @ignored_classes = ['ActionDispatch::Request', 'ActionDispatch::Response']
66
55
 
67
- # Returns a proc, which when called with a filename and line number argument,
56
+ # Returns an object which responds to #url, which when called with
57
+ # a filename and line number argument,
68
58
  # returns a URL to open the filename and line in the selected editor.
69
59
  #
70
60
  # Generates TextMate URLs by default.
71
61
  #
72
- # BetterErrors.editor["/some/file", 123]
62
+ # BetterErrors.editor.url("/some/file", 123)
73
63
  # # => txmt://open?url=file:///some/file&line=123
74
64
  #
75
65
  # @return [Proc]
76
66
  def self.editor
77
- @editor
67
+ @editor ||= default_editor
78
68
  end
79
69
 
80
70
  # Configures how Better Errors generates open-in-editor URLs.
@@ -115,20 +105,15 @@ module BetterErrors
115
105
  # @param [Proc] proc
116
106
  #
117
107
  def self.editor=(editor)
118
- POSSIBLE_EDITOR_PRESETS.each do |config|
119
- if config[:symbols].include?(editor)
120
- return self.editor = config[:url]
121
- end
122
- end
123
-
124
- if editor.is_a? String
125
- self.editor = proc { |file, line| editor % { file: URI.encode_www_form_component(file), line: line } }
108
+ if editor.is_a? Symbol
109
+ @editor = Editor.editor_from_symbol(editor)
110
+ raise(ArgumentError, "Symbol #{editor} is not a symbol in the list of supported errors.") unless editor
111
+ elsif editor.is_a? String
112
+ @editor = Editor.for_formatting_string(editor)
113
+ elsif editor.respond_to? :call
114
+ @editor = Editor.for_proc(editor)
126
115
  else
127
- if editor.respond_to? :call
128
- @editor = editor
129
- else
130
- raise TypeError, "Expected editor to be a valid editor key, a format string or a callable."
131
- end
116
+ raise ArgumentError, "Expected editor to be a valid editor key, a format string or a callable."
132
117
  end
133
118
  end
134
119
 
@@ -145,12 +130,8 @@ module BetterErrors
145
130
  #
146
131
  # @return [Symbol]
147
132
  def self.default_editor
148
- POSSIBLE_EDITOR_PRESETS.detect(-> { {} }) { |config|
149
- ENV["EDITOR"] =~ config[:sniff]
150
- }[:url] || :textmate
133
+ Editor.default_editor
151
134
  end
152
-
153
- BetterErrors.editor = default_editor
154
135
  end
155
136
 
156
137
  begin
@@ -0,0 +1,103 @@
1
+ require "uri"
2
+
3
+ module BetterErrors
4
+ class Editor
5
+ KNOWN_EDITORS = [
6
+ { symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
7
+ { symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
8
+ { symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
9
+ { symbols: [:macvim, :mvim], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" },
10
+ { symbols: [:rubymine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
11
+ { symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
12
+ { symbols: [:textmate, :txmt, :tm], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
13
+ { symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
14
+ { symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" },
15
+ ]
16
+
17
+ def self.for_formatting_string(formatting_string)
18
+ new proc { |file, line|
19
+ formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line }
20
+ }
21
+ end
22
+
23
+ def self.for_proc(url_proc)
24
+ new url_proc
25
+ end
26
+
27
+ # Automatically sniffs a default editor preset based on
28
+ # environment variables.
29
+ #
30
+ # @return [Symbol]
31
+ def self.default_editor
32
+ editor_from_environment_formatting_string ||
33
+ editor_from_environment_editor ||
34
+ editor_from_symbol(:textmate)
35
+ end
36
+
37
+ def self.editor_from_environment_editor
38
+ if ENV["BETTER_ERRORS_EDITOR"]
39
+ editor = editor_from_command(ENV["BETTER_ERRORS_EDITOR"])
40
+ return editor if editor
41
+ puts "BETTER_ERRORS_EDITOR environment variable is not recognized as a supported Better Errors editor."
42
+ end
43
+ if ENV["EDITOR"]
44
+ editor = editor_from_command(ENV["EDITOR"])
45
+ return editor if editor
46
+ puts "EDITOR environment variable is not recognized as a supported Better Errors editor. Using TextMate by default."
47
+ else
48
+ puts "Since there is no EDITOR or BETTER_ERRORS_EDITOR environment variable, using Textmate by default."
49
+ end
50
+ end
51
+
52
+ def self.editor_from_command(editor_command)
53
+ env_preset = KNOWN_EDITORS.find { |preset| editor_command =~ preset[:sniff] }
54
+ for_formatting_string(env_preset[:url]) if env_preset
55
+ end
56
+
57
+ def self.editor_from_environment_formatting_string
58
+ return unless ENV['BETTER_ERRORS_EDITOR_URL']
59
+
60
+ for_formatting_string(ENV['BETTER_ERRORS_EDITOR_URL'])
61
+ end
62
+
63
+ def self.editor_from_symbol(symbol)
64
+ KNOWN_EDITORS.each do |preset|
65
+ return for_formatting_string(preset[:url]) if preset[:symbols].include?(symbol)
66
+ end
67
+ end
68
+
69
+ def initialize(url_proc)
70
+ @url_proc = url_proc
71
+ end
72
+
73
+ def url(raw_path, line)
74
+ if virtual_path && raw_path.start_with?(virtual_path)
75
+ if host_path
76
+ file = raw_path.sub(%r{\A#{virtual_path}}, host_path)
77
+ else
78
+ file = raw_path.sub(%r{\A#{virtual_path}/}, '')
79
+ end
80
+ else
81
+ file = raw_path
82
+ end
83
+
84
+ url_proc.call(file, line)
85
+ end
86
+
87
+ def scheme
88
+ url('/fake', 42).sub(/:.*/, ':')
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :url_proc
94
+
95
+ def virtual_path
96
+ @virtual_path ||= ENV['BETTER_ERRORS_VIRTUAL_PATH']
97
+ end
98
+
99
+ def host_path
100
+ @host_path ||= ENV['BETTER_ERRORS_HOST_PATH']
101
+ end
102
+ end
103
+ end
@@ -5,6 +5,8 @@ require "securerandom"
5
5
  module BetterErrors
6
6
  # @private
7
7
  class ErrorPage
8
+ VariableInfo = Struct.new(:frame, :editor_url, :rails_params, :rack_session, :start_time)
9
+
8
10
  def self.template_path(template_name)
9
11
  File.expand_path("../templates/#{template_name}.erb", __FILE__)
10
12
  end
@@ -13,6 +15,15 @@ module BetterErrors
13
15
  Erubi::Engine.new(File.read(template_path(template_name)), escape: true)
14
16
  end
15
17
 
18
+ def self.render_template(template_name, locals)
19
+ locals.send(:eval, self.template(template_name).src)
20
+ rescue => e
21
+ # Fix the backtrace, which doesn't identify the template that failed (within Better Errors).
22
+ # We don't know the line number, so just injecting the template path has to be enough.
23
+ e.backtrace.unshift "#{self.template_path(template_name)}:0"
24
+ raise
25
+ end
26
+
16
27
  attr_reader :exception, :env, :repls
17
28
 
18
29
  def initialize(exception, env)
@@ -26,20 +37,21 @@ module BetterErrors
26
37
  @id ||= SecureRandom.hex(8)
27
38
  end
28
39
 
29
- def render(template_name = "main", csrf_token = nil)
30
- binding.eval(self.class.template(template_name).src)
31
- rescue => e
32
- # Fix the backtrace, which doesn't identify the template that failed (within Better Errors).
33
- # We don't know the line number, so just injecting the template path has to be enough.
34
- e.backtrace.unshift "#{self.class.template_path(template_name)}:0"
35
- raise
40
+ def render_main(csrf_token, csp_nonce)
41
+ frame = backtrace_frames[0]
42
+ first_frame_variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
43
+ self.class.render_template('main', binding)
44
+ end
45
+
46
+ def render_text
47
+ self.class.render_template('text', binding)
36
48
  end
37
49
 
38
50
  def do_variables(opts)
39
51
  index = opts["index"].to_i
40
- @frame = backtrace_frames[index]
41
- @var_start_time = Time.now.to_f
42
- { html: render("variable_info") }
52
+ frame = backtrace_frames[index]
53
+ variable_info = VariableInfo.new(frame, editor_url(frame), rails_params, rack_session, Time.now.to_f)
54
+ { html: self.class.render_template("variable_info", variable_info) }
43
55
  end
44
56
 
45
57
  def do_eval(opts)
@@ -67,6 +79,10 @@ module BetterErrors
67
79
  exception.message.strip.gsub(/(\r?\n\s*\r?\n)+/, "\n")
68
80
  end
69
81
 
82
+ def exception_hint
83
+ exception.hint
84
+ end
85
+
70
86
  def active_support_actions
71
87
  return [] unless defined?(ActiveSupport::ActionableError)
72
88
 
@@ -90,7 +106,7 @@ module BetterErrors
90
106
  private
91
107
 
92
108
  def editor_url(frame)
93
- BetterErrors.editor[frame.filename, frame.line]
109
+ BetterErrors.editor.url(frame.filename, frame.line)
94
110
  end
95
111
 
96
112
  def rack_session
@@ -109,11 +125,11 @@ module BetterErrors
109
125
  env["PATH_INFO"]
110
126
  end
111
127
 
112
- def html_formatted_code_block(frame)
128
+ def self.html_formatted_code_block(frame)
113
129
  CodeFormatter::HTML.new(frame.filename, frame.line).output
114
130
  end
115
131
 
116
- def text_formatted_code_block(frame)
132
+ def self.text_formatted_code_block(frame)
117
133
  CodeFormatter::Text.new(frame.filename, frame.line).output
118
134
  end
119
135
 
@@ -121,7 +137,7 @@ module BetterErrors
121
137
  str + "\n" + char*str.size
122
138
  end
123
139
 
124
- def inspect_value(obj)
140
+ def self.inspect_value(obj)
125
141
  if BetterErrors.ignored_classes.include? obj.class.name
126
142
  "<span class='unsupported'>(Instance of ignored class. "\
127
143
  "#{obj.class.name ? "Remove #{CGI.escapeHTML(obj.class.name)} from" : "Modify"}"\
@@ -0,0 +1,29 @@
1
+ module BetterErrors
2
+ class ExceptionHint
3
+ def initialize(exception)
4
+ @exception = exception
5
+ end
6
+
7
+ def hint
8
+ case exception
9
+ when NoMethodError
10
+ /\Aundefined method `(?<method>[^']+)' for (?<val>[^:]+):(?<klass>\w+)/.match(exception.message) do |match|
11
+ if match[:val] == "nil"
12
+ return "Something is `nil` when it probably shouldn't be."
13
+ elsif !match[:klass].start_with? '0x'
14
+ return "`#{match[:method]}` is being called on a `#{match[:klass]}` object, "\
15
+ "which might not be the type of object you were expecting."
16
+ end
17
+ end
18
+ when NameError
19
+ /\Aundefined local variable or method `(?<method>[^']+)' for/.match(exception.message) do |match|
20
+ return "`#{match[:method]}` is probably misspelled."
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :exception
28
+ end
29
+ end
@@ -40,7 +40,7 @@ module BetterErrors
40
40
  allow_ip! "127.0.0.0/8"
41
41
  allow_ip! "::1/128" rescue nil # windows ruby doesn't have ipv6 support
42
42
 
43
- CSRF_TOKEN_COOKIE_NAME = 'BetterErrors-CSRF-Token'
43
+ CSRF_TOKEN_COOKIE_NAME = "BetterErrors-#{BetterErrors::VERSION}-CSRF-Token"
44
44
 
45
45
  # A new instance of BetterErrors::Middleware
46
46
  #
@@ -94,12 +94,13 @@ module BetterErrors
94
94
  def show_error_page(env, exception=nil)
95
95
  request = Rack::Request.new(env)
96
96
  csrf_token = request.cookies[CSRF_TOKEN_COOKIE_NAME] || SecureRandom.uuid
97
+ csp_nonce = SecureRandom.base64(12)
97
98
 
98
99
  type, content = if @error_page
99
100
  if text?(env)
100
- [ 'plain', @error_page.render('text') ]
101
+ [ 'plain', @error_page.render_text ]
101
102
  else
102
- [ 'html', @error_page.render('main', csrf_token) ]
103
+ [ 'html', @error_page.render_main(csrf_token, csp_nonce) ]
103
104
  end
104
105
  else
105
106
  [ 'html', no_errors_page ]
@@ -110,10 +111,25 @@ module BetterErrors
110
111
  status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
111
112
  end
112
113
 
113
- response = Rack::Response.new(content, status_code, { "Content-Type" => "text/#{type}; charset=utf-8" })
114
+ headers = {
115
+ "Content-Type" => "text/#{type}; charset=utf-8",
116
+ "Content-Security-Policy" => [
117
+ "default-src 'none'",
118
+ # Specifying nonce makes a modern browser ignore 'unsafe-inline' which could still be set
119
+ # for older browsers without nonce support.
120
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
121
+ "script-src 'self' 'nonce-#{csp_nonce}' 'unsafe-inline'",
122
+ # Inline style is required by the syntax highlighter.
123
+ "style-src 'self' 'unsafe-inline'",
124
+ "connect-src 'self'",
125
+ "navigate-to 'self' #{BetterErrors.editor.scheme}",
126
+ ].join('; '),
127
+ }
128
+
129
+ response = Rack::Response.new(content, status_code, headers)
114
130
 
115
131
  unless request.cookies[CSRF_TOKEN_COOKIE_NAME]
116
- response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, httponly: true, same_site: :strict)
132
+ response.set_cookie(CSRF_TOKEN_COOKIE_NAME, value: csrf_token, path: "/", httponly: true, same_site: :strict)
117
133
  end
118
134
 
119
135
  # In older versions of Rack, the body returned here is actually a Rack::BodyProxy which seems to be a bug.
@@ -1,7 +1,9 @@
1
+ require 'better_errors/exception_hint'
2
+
1
3
  # @private
2
4
  module BetterErrors
3
5
  class RaisedException
4
- attr_reader :exception, :message, :backtrace
6
+ attr_reader :exception, :message, :backtrace, :hint
5
7
 
6
8
  def initialize(exception)
7
9
  if exception.class.name == "ActionView::Template::Error" && exception.respond_to?(:cause)
@@ -23,6 +25,7 @@ module BetterErrors
23
25
  @message = exception.message
24
26
 
25
27
  setup_backtrace
28
+ setup_hint
26
29
  massage_syntax_error
27
30
  end
28
31
 
@@ -78,5 +81,9 @@ module BetterErrors
78
81
  end
79
82
  end
80
83
  end
84
+
85
+ def setup_hint
86
+ @hint = ExceptionHint.new(exception).hint
87
+ end
81
88
  end
82
89
  end
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <title><%= exception_type %> at <%= request_path %></title>
5
5
  </head>
6
- <body>
6
+ <body class="better-errors-javascript-not-loaded">
7
7
  <%# Stylesheets are placed in the <body> for Turbolinks compatibility. %>
8
8
  <style>
9
9
  /* Basic reset */
@@ -90,7 +90,7 @@
90
90
  nav.sidebar,
91
91
  .frame_info {
92
92
  position: fixed;
93
- top: 95px;
93
+ top: 102px;
94
94
  bottom: 0;
95
95
 
96
96
  box-sizing: border-box;
@@ -102,11 +102,13 @@
102
102
  nav.sidebar {
103
103
  width: 40%;
104
104
  left: 20px;
105
- top: 115px;
105
+ top: 122px;
106
106
  bottom: 20px;
107
107
  }
108
108
 
109
109
  .frame_info {
110
+ display: none;
111
+
110
112
  right: 0;
111
113
  left: 40%;
112
114
 
@@ -114,6 +116,9 @@
114
116
  padding-left: 10px;
115
117
  margin-left: 30px;
116
118
  }
119
+ .frame_info.current {
120
+ display: block;
121
+ }
117
122
  }
118
123
 
119
124
  nav.sidebar {
@@ -131,7 +136,7 @@
131
136
  header.exception {
132
137
  padding: 18px 20px;
133
138
 
134
- height: 59px;
139
+ height: 66px;
135
140
  min-height: 59px;
136
141
 
137
142
  overflow: hidden;
@@ -227,6 +232,10 @@
227
232
  * Navigation
228
233
  * --------------------------------------------------------------------- */
229
234
 
235
+ .better-errors-javascript-not-loaded .backtrace .tabs {
236
+ display: none;
237
+ }
238
+
230
239
  nav.tabs {
231
240
  border-bottom: solid 1px #ddd;
232
241
 
@@ -411,6 +420,18 @@
411
420
  * Display area
412
421
  * --------------------------------------------------------------------- */
413
422
 
423
+ p.no-javascript-notice {
424
+ margin-bottom: 1em;
425
+ padding: 1em;
426
+ border: 2px solid #e00;
427
+ }
428
+ .better-errors-javascript-loaded .no-javascript-notice {
429
+ display: none;
430
+ }
431
+ .no-inline-style-notice {
432
+ display: none;
433
+ }
434
+
414
435
  .trace_info {
415
436
  background: #fff;
416
437
  padding: 6px;
@@ -468,6 +489,10 @@
468
489
  font-weight: 200;
469
490
  }
470
491
 
492
+ .better-errors-javascript-not-loaded .be-repl {
493
+ display: none;
494
+ }
495
+
471
496
  .code, .be-console, .unavailable {
472
497
  background: #fff;
473
498
  padding: 5px;
@@ -595,6 +620,12 @@
595
620
  color: #8080a0;
596
621
  padding-left: 20px;
597
622
  }
623
+ .console-has-been-used .live-console-hint {
624
+ display: none;
625
+ }
626
+ .better-errors-javascript-not-loaded .live-console-hint {
627
+ display: none;
628
+ }
598
629
 
599
630
  .hint:before {
600
631
  content: '\25b2';
@@ -611,17 +642,6 @@
611
642
  margin: 10px 0;
612
643
  }
613
644
 
614
- .sub:before {
615
- content: '';
616
- display: block;
617
- width: 100%;
618
- height: 4px;
619
-
620
- border-radius: 2px;
621
- background: rgba(0, 150, 200, 0.05);
622
- box-shadow: 1px 1px 0 rgba(255, 255, 255, 0.7), inset 0 0 0 1px rgba(0, 0, 0, 0.04), inset 2px 2px 2px rgba(0, 0, 0, 0.07);
623
- }
624
-
625
645
  .sub h3 {
626
646
  color: #39a;
627
647
  font-size: 1.1em;
@@ -709,7 +729,7 @@
709
729
  </style>
710
730
 
711
731
  <%# IE8 compatibility crap %>
712
- <script>
732
+ <script nonce="<%= csp_nonce %>">
713
733
  (function() {
714
734
  var elements = ["section", "nav", "header", "footer", "audio"];
715
735
  for (var i = 0; i < elements.length; i++) {
@@ -723,7 +743,7 @@
723
743
  rendered in the host app's layout. Let's empty out the styles of the
724
744
  host app.
725
745
  %>
726
- <script>
746
+ <script nonce="<%= csp_nonce %>">
727
747
  if (window.Turbolinks) {
728
748
  for(var i=0; i < document.styleSheets.length; i++) {
729
749
  if(document.styleSheets[i].href)
@@ -748,6 +768,15 @@
748
768
  }
749
769
  </script>
750
770
 
771
+ <p class='no-inline-style-notice'>
772
+ <strong>
773
+ Better Errors can't apply inline style<span class='no-javascript-notice'> (or run Javascript)</span>,
774
+ possibly because you have a Content Security Policy along with Turbolinks.
775
+ But you can
776
+ <a href='/__better_errors' target="_blank">open the interactive console in a new tab/window</a>.
777
+ </strong>
778
+ </p>
779
+
751
780
  <div class='top'>
752
781
  <header class="exception">
753
782
  <h2><strong><%= exception_type %></strong> <span>at <%= request_path %></span></h2>
@@ -764,6 +793,9 @@
764
793
  <% end %>
765
794
  </div>
766
795
  <% end %>
796
+ <% if exception_hint %>
797
+ <h2>Hint: <%= exception_hint %></h2>
798
+ <% end %>
767
799
  </header>
768
800
  </div>
769
801
 
@@ -791,21 +823,37 @@
791
823
  </ul>
792
824
  </nav>
793
825
 
794
- <% backtrace_frames.each_with_index do |frame, index| %>
795
- <div class="frame_info" id="frame_info_<%= index %>" style="display:none;"></div>
796
- <% end %>
826
+ <div class="frameInfos">
827
+ <div class="frame_info current" data-frame-idx="0">
828
+ <p class='no-javascript-notice'>
829
+ Better Errors can't run Javascript here<span class='no-inline-style-notice'> (or apply inline style)</span>,
830
+ possibly because you have a Content Security Policy along with Turbolinks.
831
+ But you can
832
+ <a href='/__better_errors' target="_blank">open the interactive console in a new tab/window</a>.
833
+ </p>
834
+ <!-- this is enough information to show something in case JS doesn't get to load -->
835
+ <%== ErrorPage.render_template('variable_info', first_frame_variable_info) %>
836
+ </div>
837
+ </div>
797
838
  </section>
798
839
  </body>
799
- <script>
840
+ <script nonce="<%= csp_nonce %>">
800
841
  (function() {
801
842
 
802
843
  var OID = "<%= id %>";
803
844
  var csrfToken = "<%= csrf_token %>";
804
845
 
805
846
  var previousFrame = null;
806
- var previousFrameInfo = null;
807
847
  var allFrames = document.querySelectorAll("ul.frames li");
808
- var allFrameInfos = document.querySelectorAll(".frame_info");
848
+ var frameInfos = document.querySelector(".frameInfos");
849
+
850
+ document.querySelector('body').classList.remove("better-errors-javascript-not-loaded");
851
+ document.querySelector('body').classList.add("better-errors-javascript-loaded");
852
+
853
+ var noJSNotices = document.querySelectorAll('.no-javascript-notice');
854
+ for(var i = 0; i < noJSNotices.length; i++) {
855
+ noJSNotices[i].remove();
856
+ }
809
857
 
810
858
  function apiCall(method, opts, cb) {
811
859
  var req = new XMLHttpRequest();
@@ -825,6 +873,28 @@
825
873
  return html.replace(/&/, "&amp;").replace(/</g, "&lt;");
826
874
  }
827
875
 
876
+ function hasConsoleBeenUsedPreviously() {
877
+ return !!document.cookie.split('; ').find(function(cookie) {
878
+ return cookie.startsWith('BetterErrors-has-used-console=');
879
+ });
880
+ }
881
+
882
+ var consoleHasBeenUsed = hasConsoleBeenUsedPreviously();
883
+
884
+ function consoleWasJustUsed() {
885
+ if (consoleHasBeenUsed) {
886
+ return;
887
+ }
888
+
889
+ hideConsoleHint();
890
+ consoleHasBeenUsed = true;
891
+ document.cookie = "BetterErrors-has-used-console=true;path=/;max-age=31536000;samesite"
892
+ }
893
+
894
+ function hideConsoleHint() {
895
+ document.querySelector('body').className += " console-has-been-used";
896
+ }
897
+
828
898
  function REPL(index) {
829
899
  this.index = index;
830
900
 
@@ -846,15 +916,20 @@
846
916
  this.inputElement = this.container.querySelector("input");
847
917
  this.outputElement = this.container.querySelector("pre");
848
918
 
919
+ if (consoleHasBeenUsed) {
920
+ hideConsoleHint();
921
+ }
922
+
849
923
  var self = this;
850
924
  this.inputElement.onkeydown = function(ev) {
851
925
  self.onKeyDown(ev);
926
+ consoleWasJustUsed();
852
927
  };
853
928
 
854
929
  this.setPrompt(">>");
855
930
 
856
931
  REPL.all[this.index] = this;
857
- }
932
+ };
858
933
 
859
934
  REPL.prototype.focus = function() {
860
935
  this.inputElement.focus();
@@ -952,17 +1027,25 @@
952
1027
  };
953
1028
 
954
1029
  function switchTo(el) {
955
- if(previousFrameInfo) previousFrameInfo.style.display = "none";
956
- previousFrameInfo = el;
1030
+ var currentFrameInfo = document.querySelectorAll('.frame_info.current');
1031
+ for(var i = 0; i < currentFrameInfo.length; i++) {
1032
+ currentFrameInfo[i].className = "frame_info";
1033
+ }
957
1034
 
958
- el.style.display = "block";
1035
+ el.className = "frame_info current";
959
1036
 
960
1037
  var replInput = el.querySelector('.be-console input');
961
1038
  if (replInput) replInput.focus();
962
1039
  }
963
1040
 
964
1041
  function selectFrameInfo(index) {
965
- var el = allFrameInfos[index];
1042
+ var el = document.querySelector(".frame_info[data-frame-idx='" + index + "']")
1043
+ if (!el) {
1044
+ el = document.createElement("div");
1045
+ el.className = "frame_info";
1046
+ el.setAttribute('data-frame-idx', index);
1047
+ frameInfos.appendChild(el);
1048
+ }
966
1049
  if(el) {
967
1050
  if (el.loaded) {
968
1051
  return switchTo(el);