# frozen-string-literal: true
#
class Roda
module RodaPlugins
# The exception_page plugin provides an exception_page method that is designed
# to be called inside the error handler to provide a page to the developer
# with debugging information. It should only be used in developer environments
# with trusted clients, as it can leak source code and other information that
# may be useful for attackers if used in other environments.
#
# Example:
#
# plugin :exception_page
# plugin :error_handler do |e|
# next exception_page(e) if ENV['RACK_ENV'] == 'development'
# # ...
# end
#
# The exception_page plugin is based on Rack::ShowExceptions, with the following
# differences:
#
# * Not a middleware, so it doesn't handle exceptions itself, and has no effect
# on the callstack unless the exception_page method is called.
# * Supports external javascript and stylesheets, allowing context toggling to
# work in applications that use a content security policy to restrict inline
# javascript and stylesheets (:assets, :css_file, and :js_file options).
# * Has fewer dependencies (does not require ostruct and erb).
# * Sets the Content-Type for the response, and returns the body string, but does
# not modify other headers or the response status.
# * Supports a configurable amount of context lines in backtraces (:context option).
# * Supports optional JSON formatted output, if used with the json plugin (:json option).
#
# To use the external javascript and stylesheets, you can call +r.exception_page_assets+
# in your routing tree:
#
# route do |r|
# # ...
#
# # serve GET /exception_page.{css,js} requests
# # Use with assets: true +exception_page+ option
# r.exception_page_assets
#
# r.on "static" do
# # serve GET /static/exception_page.{css,js} requests
# # Use with assets: '/static' +exception_page+ option
# r.exception_page_assets
# end
# end
#
# It's also possible to store the asset information in static files and serve those,
# you can get the current assets by calling:
#
# Roda::RodaPlugins::ExceptionPage.css
# Roda::RodaPlugins::ExceptionPage.js
#
# As the exception_page plugin is based on Rack::ShowExceptions, it is also under
# rack's license:
#
# Copyright (C) 2007-2018 Christian Neukirchen
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# The HTML template used in Rack::ShowExceptions was based on Django's
# template and is under the following license:
#
# adapted from Django
# Copyright (c) Django Software Foundation and individual contributors.
# Used under the modified BSD license:
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
module ExceptionPage
def self.load_dependencies(app)
app.plugin :h
end
# Stylesheet used by the HTML exception page
def self.css
<div { border-bottom:1px solid #ddd; }
h1 { font-weight:normal; }
h2 { margin-bottom:.8em; }
h2 span { font-size:80%; color:#666; font-weight:normal; }
h3 { margin:1em 0 .5em 0; }
h4 { margin:0 0 .5em 0; font-weight: normal; }
table {
border:1px solid #ccc; border-collapse: collapse; background:white; }
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
thead th {
padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
font-weight:normal; font-size:11px; border:1px solid #ddd; }
tbody th { text-align:right; color:#666; padding-right:.5em; }
table.vars { margin:5px 0 2px 40px; }
table.vars td, table.req td { font-family:monospace; }
table td.code { width:100%;}
table td.code div { overflow:hidden; }
table.source th { color:#666; }
table.source td {
font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
ul.traceback { list-style-type:none; }
ul.traceback li.frame { margin-bottom:1em; }
div.context { margin: 10px 0; }
div.context ol {
padding-left:30px; margin:0 10px; list-style-position: inside; }
div.context ol li {
font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
div.context ol.context-line li { color:black; background-color:#ccc; }
div.context ol.context-line li span { float: right; }
div.commands { margin-left: 40px; }
div.commands a { color:black; text-decoration:none; }
#summary { background: #ffc; }
#summary h2 { font-weight: normal; color: #666; font-family: monospace; white-space: pre-wrap;}
#summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
#summary ul#quicklinks li { float: left; padding: 0 1em; }
#summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
#explanation { background:#eee; }
#traceback { background:#eee; }
#requestinfo { background:#f6f6f6; padding-left:120px; }
#summary table { border:none; background:transparent; }
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
#requestinfo h3 { margin-bottom:-1em; }
.error { background: #ffc; }
.specific { color:#cc3300; font-weight:bold; }
END
end
# Javascript used by the HTML exception page for context toggling
def self.js
<{
"class"=>exception.class.to_s,
"message"=>message,
"backtrace"=>exception.backtrace.map(&:to_s)
}
}
elsif env['HTTP_ACCEPT'] =~ /text\/html/
@_response['Content-Type'] = "text/html"
context = opts[:context] || 7
css_file = opts[:css_file]
js_file = opts[:js_file]
case prefix = opts[:assets]
when false
css_file = false if css_file.nil?
js_file = false if js_file.nil?
when nil
# nothing
else
prefix = '' if prefix == true
css_file ||= "#{prefix}/exception_page.css"
js_file ||= "#{prefix}/exception_page.js"
end
css = case css_file
when nil
""
when false
# :nothing
else
""
end
js = case js_file
when nil
""
when false
# :nothing
else
""
end
frames = exception.backtrace.map.with_index do |line, i|
frame = {:id=>i}
if line =~ /\A(.*?):(\d+)(?::in `(.*)')?\Z/
filename = frame[:filename] = $1
lineno = frame[:lineno] = $2.to_i
frame[:function] = $3
begin
lineno -= 1
lines = ::File.readlines(filename)
if line = lines[lineno]
pre_lineno = [lineno-context, 0].max
if (pre_context = lines[pre_lineno...lineno]) && !pre_context.empty?
frame[:pre_context_lineno] = pre_lineno
frame[:pre_context] = pre_context
end
post_lineno = [lineno+context, lines.size].min
if (post_context = lines[lineno+1..post_lineno]) && !post_context.empty?
frame[:post_context_lineno] = post_lineno
frame[:post_context] = post_context
end
frame[:context_line] = line.chomp
end
rescue
end
frame
end
end.compact
r = @_request
begin
post_data = r.POST
missing_post = "No POST data"
rescue
missing_post = "Invalid POST data"
end
info = lambda do |title, id, var, none|
<#{title}
#{(var && !var.empty?) ? (<#{none}
"
Variable
Value
#{var.sort_by{|k, _| k.to_s}.map{|key, val| (<
#{h key}
#{h val.inspect}
END2
}
END1
}
END
end
<#{h exception.class} at #{h r.path}
#{css}
#{h exception.class} at #{h r.path}
#{h message}
Ruby
#{(first = frames.first) ? "#{h first[:filename]}: in #{h first[:function]}, line #{first[:lineno]}" : "unknown location"}
You're seeing this error because you use the Roda exception_page plugin.
#{js}
END
else
@_response['Content-Type'] = "text/plain"
"#{exception.class}: #{message}\n#{exception.backtrace.map{|l| "\t#{l}"}.join("\n")}"
end
end
private
# :nocov:
if RUBY_VERSION >= '3.2'
def exception_page_exception_message(exception)
exception.detailed_message(highlight: false).to_s
end
# :nocov:
else
# Return message to use for exception.
def exception_page_exception_message(exception)
exception.message.to_s
end
end
end
module RequestMethods
# Serve exception page assets
def exception_page_assets
get 'exception_page.css' do
response['Content-Type'] = "text/css"
ExceptionPage.css
end
get 'exception_page.js' do
response['Content-Type'] = "application/javascript"
ExceptionPage.js
end
end
end
end
register_plugin(:exception_page, ExceptionPage)
end
end