Source code for gavo.web.weberrors

"""
Default error displays for the data center and error helper code.

Everything in here must render synchronuosly.

You probably should not construct anything in this module directly
but rather just raise the appropriate exceptions from svcs.
"""

#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


import urllib.parse

from twisted.python import failure
from twisted.web import server
from twisted.web import template
from twisted.web.template import tags as T

from gavo import base
from gavo import svcs
from gavo import utils
from gavo.formal import nevowc
from gavo.web import common


class _Silence(Exception):
	"""A sentinel used by Silence.
	"""

[docs]def Silence(): """returns a failure.Failure that cases this module to not touch request at all. You want to return this from errbacks that already have spit out their error message. """ return failure.Failure(exc_value=_Silence("Keep quiet"))
[docs]class ErrorPage(nevowc.TemplatedPage, common.CommonRenderers): """A base for error handling pages. The idea is that you set the "handles" class attribute to the exception you handle. The exception has to match exactly, i.e., no isinstancing is done. You also must set status to the HTTP status code the error should return. All error pages have a failure attribute that's a twisted failure with all the related mess (e.g., tracebacks). You have the status and message data methods. In custom exceptions, you can attach stan in htmlMessage, and ErrorPages will show that as the error message. """ handles = None status = 500 titleMessage = "Unspecified Error" beforeMessage = "We're sorry, but something didn't work out:" afterMessage = T.p["This generic text shouldn't be here. The" " child class should override afterMessage."] _footer = "delete this when done" def __init__(self, error): self.flr = error
[docs] def data_status(self, request, tag): return str(self.status)
[docs] @template.renderer def beforemessage(self, request, tag): return tag[self.beforeMessage]
[docs] @template.renderer def aftermessage(self, request, tag): return tag[self.afterMessage]
[docs] @template.renderer def message(self, request, tag): if hasattr(self.flr.value, "htmlMessage"): return tag(class_="errmsg")[self.flr.value.htmlMessage] else: return tag(class_="errmsg")[self.flr.getErrorMessage()]
[docs] @template.renderer def hint(self, request, tag): if (hasattr(self, "flr") and hasattr(self.flr.value, "hint") and self.flr.value.hint): return tag[T.strong["Hint: "], self.flr.value.hint] return ""
[docs] @template.renderer def titlemessage(self, request, tag): return tag["%s -- %s"%( base.getConfig("web", "sitename"), self.titleMessage)]
[docs] @template.renderer def footer(self, request, tag): contactMail = str(base.getGlobalMeta("contact.email")) return tag[ T.hr, T.address[T.a(href="mailto:%s"%contactMail)[contactMail]]]
[docs] def render(self, request): request.setResponseCode(self.status) try: request.write(nevowc.flattenSync(self._getDoc(request))) except Exception as msg: base.ui.notifyError("Error while writing error page: %s"%msg) # something went wrong while serialising the error page request.write(b"Ouch. Error while writing the error page. Sorry.\r\n" b"\nPlease complain.\r\n") request.finish() return server.NOT_DONE_YET
loader = common.doctypedStan(T.html[ T.head(render="commonhead")[ T.title(render="titlemessage")], T.body[ T.img(src="/static/img/logo_medium.png", class_="headlinelogo", style="position:absolute;right:5pt"), T.h1[ T.transparent(render="titlemessage"), " (", nevowc.addNevowAttributes( T.transparent(render="string"), data="status"), ")"], T.p(render="beforemessage"), T.div(class_="errors", render="message"), T.div(render="aftermessage"), T.div(class_="hint", render="hint")[ T.p("The error message also has a hint, which may in particular" " be useful to the service operator. Be sure to include it" " with your report:")], T.transparent(render="footer")]])
[docs]class NotFoundPage(ErrorPage): handles = svcs.UnknownURI status = 404 titleMessage = "Not Found" beforeMessage = ("We're sorry, but the resource you" " requested could not be located.") afterMessage = [ T.p["If this message resulted from following a link from ", T.strong["within the data center"], ", you have discovered a bug, and we would be" " extremely grateful if you could notify us."], T.p["If you got here following an ", T.strong["external link"], ", we would be" " grateful for a notification as well. We will ask the" " external operators to fix their links or provide" " redirects as appropriate."], T.p["In either case, you may find whatever you were looking" " for by inspecting our ", T.a(href="/")["list of published services"], "."], T.p(render="rdlink")]
[docs]class NotFoundPageWithFancyMessage(NotFoundPage): """A NotFoundPage with a message that's taken from a piece of stan. """ handles = None def __init__(self, message): self.fancyMessage = message
[docs] @template.renderer def message(self, request, tag): return tag[self.fancyMessage]
[docs]class OtherNotFoundPage(NotFoundPage): handles = base.NotFoundError
[docs]class RDNotFoundPage(NotFoundPage): handles = base.RDNotFound
[docs]class ForbiddenPage(ErrorPage): handles = svcs.ForbiddenURI status = 403 titleMessage = "Forbidden" beforeMessage = "We're sorry, but the resource you requested is forbidden." afterMessage = T.div[ T.p["This usually means you tried to use a renderer on a service" " that does not support it. If you did not come up with the" " URL in question yourself, complain fiercely to the staff of ", T.transparent(render="getconfig")["[web]sitename"], "."], T.p(render="rdlink")]
[docs]class RedirectBase(ErrorPage):
[docs] def render(self, request): # add request arguments if they are not already included in the # URL we're redirecting to: self.destURL = utils.bytify(self.flr.value.dest) if b'?' not in self.destURL: args = urllib.parse.urlparse(request.uri).query if args: self.destURL = self.destURL+b"?"+utils.bytify(args) request.setHeader("location", self.destURL) return ErrorPage.render(self, request)
afterMessage = T.p["You should not see this page -- either your browser or" " our site is broken. Complain."]
[docs]class RedirectPage(RedirectBase): handles = svcs.WebRedirect status = 301 titleMessage = "Moved Permanently" beforeMessage = ["The resource you requested is available from a ", T.a(render="destLink")[ "different URL"], "."]
[docs]class FoundPage(RedirectBase): handles = svcs.Found status = 302 titleMessage = "Found" beforeMessage = ["The resource you requested can be found at ", T.a(render="destLink")[ T.transparent(render="destLink")], "."]
[docs]class SeeOtherPage(RedirectBase): handles = svcs.SeeOther status = 303 titleMessage = "See Other" beforeMessage = ["Please turn to a ", T.a(render="destLink")[ "different URL"], " to go on."]
[docs]class AuthenticatePage(ErrorPage): handles = svcs.Authenticate status = 401 titleMessage = "Authentication Required"
[docs] def render(self, request): request.setHeader('WWW-Authenticate', 'Basic realm="%s"'%str(self.flr.value.realm)) return ErrorPage.render(self, request)
loader = svcs.loadSystemTemplate("unauth.html")
[docs]class BadMethodPage(ErrorPage): handles = svcs.BadMethod status = 405 titleMessage = "Bad Method" beforeMessage = ( "You just tried to use some HTTP method to access this resource" " that this resource does not support. This probably means that" " this resource is for exclusive use for specialized clients.") afterMessage = T.p["You may find whatever you were really looking" " for by inspecting our ", T.a(href="/")["list of published services"], "."]
[docs]class UploadTooLargePage(ErrorPage): handles = svcs.RequestEntityTooLarge status = 413 titleMessage = "Upload too big"
[docs]class NotAcceptable(ErrorPage): handles = base.DataError status = 406 titleMessage = "Not Acceptable" beforeMessage = ("The server cannot generate the data you requested." " The associated message is:") afterMessage = ""
[docs]class ErrorDisplay(ErrorPage): handles = base.ReportableError status = 500 titleMessage = "Error" beforeMessage = ("A piece of code failed:") afterMessage = [T.p["Problems of this sort usually mean we considered" " the possibility of something like this happening; if the above" " doesn't give you sufficient hints to fix the problem, please" " complain to the address given below."], T.p(render="hint")]
[docs]class NullPage(ErrorPage): handles = _Silence
[docs] def render(self, request): pass
# HTML mess for last-resort type error handling. errorTemplate = ( '<body><div style="position:fixed;left:4px;top:4px;' 'visibility:visible;overflow:visible !important;' 'max-width:600px !important;z-index:500">' '<div style="border:2px solid red;' 'width:400px !important;background:white">' '%s' '</div></div></body></html>') def _formatFailure(flr): res = errorTemplate%( "<h1>Internal Error</h1><p>A(n)" " %s exception occurred. The" " accompanying message is: '%s'</p>" "<p>If you are seeing this, it is always a bug in our code" " or the data descriptions, and we would be extremely grateful" " for a report at" " %s</p>"%(flr.value.__class__.__name__, common.escapeForHTML(flr.getErrorMessage()), str(base.getGlobalMeta("contact.email")))) return res.encode("ascii", "ignore")
[docs]class InternalServerErrorPage(ErrorPage): """A catch-all page served when no other error page seemed responsible. """ handles = base.Error # meaningless, no isinstance done here status = 500 titleMessage = "Uncaught Exception" beforeMessage = T.p["Your action has caused a(n) ", nevowc.addNevowAttributes( T.span(render="unicode"), data="excname"), " exception to occur. As additional info, the failing code" " gave:"], afterMessage = T.p["This is always a bug in our software, and we would really" " be grateful for a report to the contact address below," " preferably with a description of what you were trying to do," " including any data pieces if applicable. Thanks."]
[docs] def data_excname(self, request, tag): return self.flr.value.__class__.__name__
[docs] def renderInnerException(self, request): """called when rendering already has started. We don't know where we're sitting, so we try to break out as well as we can. """ request.setResponseCode(500) # probably too late, but log still profits. data = _formatFailure(self.flr) if isinstance(data, str): data = data.encode("utf-8", "ignore") request.write(data) request.finish() return server.NOT_DONE_YET
[docs] def render(self, request): base.ui.notifyFailure(self.flr, "Returning an internal server error!") base.ui.notifyInfo("Arguments of failed request: %s"% repr(request.args)[:2000]) if getattr(self.flr.value, "hint", None): base.ui.notifyDebug("Exception hint: %s"%self.flr.value.hint) # Hack to work around excessive chattiness of t.w.FlattenerError # (for something in an error page) if hasattr(self.flr.value, "_roots"): self.flr.value._roots = '' self.flr.value._traceback = '' if getattr(request, "startedWriting", False): # exception happened while rendering a page. return self.renderInnerException(request) else: return ErrorPage.render(self, request)
getErrorPage = utils.buildClassResolver( baseClass=ErrorPage, objects=list(globals().values()), instances=False, key=lambda obj: obj.handles, default=InternalServerErrorPage)
[docs]def getDCErrorPage(flr): """returns a t.w resource for a twisted failure. """ if flr is None: flr = failure.Failure() return getErrorPage(flr.value.__class__)(flr)
[docs]def renderDCErrorPage(flr, request): """renders a resource a twisted failure. This finishes request itself. It returns t.w.server.NOT_DONE_YET because of that, so you can write return renderDCErrorPage from a render method (or similar). """ if getattr(request, "channel", "just testing") is None: # the remote end has hung up; let's forget things return res = getDCErrorPage(flr) return res.render(request)
common.produceErrorDocument = renderDCErrorPage