PageRenderTime 36ms CodeModel.GetById 10ms app.highlight 21ms RepoModel.GetById 1ms app.codeStats 0ms

/circuits/web/errors.py

https://bitbucket.org/prologic/circuits/
Python | 208 lines | 172 code | 12 blank | 24 comment | 8 complexity | 584bbcf4e6b94d7ca57264e1ba17780d MD5 | raw file
  1# Module:   errors
  2# Date:     11th February 2009
  3# Author:   James Mills, prologic at shortcircuit dot net dot au
  4
  5"""Errors
  6
  7This module implements a set of standard HTTP Errors.
  8"""
  9
 10from cgi import escape
 11
 12try:
 13    from urllib.parse import urljoin as _urljoin
 14except ImportError:
 15    from urlparse import urljoin as _urljoin  # NOQA
 16
 17from circuits import Event
 18
 19from ..six import string_types
 20from .constants import SERVER_URL, SERVER_VERSION
 21from .constants import DEFAULT_ERROR_MESSAGE, HTTP_STATUS_CODES
 22
 23
 24class httperror(Event):
 25    """An event for signaling an HTTP error"""
 26
 27    code = 500
 28    description = ""
 29    contenttype = "text/html"
 30
 31    def __init__(self, request, response, code=None, **kwargs):
 32        """
 33        The constructor creates a new instance and modifies the *response*
 34        argument to reflect the error.
 35        """
 36        super(httperror, self).__init__(request, response, code, **kwargs)
 37
 38        # Override HTTPError subclasses
 39        self.name = "httperror"
 40
 41        self.request = request
 42        self.response = response
 43
 44        if code is not None:
 45            self.code = code
 46
 47        self.error = kwargs.get("error", None)
 48
 49        self.description = kwargs.get(
 50            "description", getattr(self.__class__, "description", "")
 51        )
 52
 53        if self.error is not None:
 54            self.traceback = "ERROR: (%s) %s\n%s" % (
 55                self.error[0], self.error[1], "".join(self.error[2])
 56            )
 57        else:
 58            self.traceback = ""
 59
 60        self.response.close = True
 61        self.response.status = self.code
 62
 63        self.response.headers["Content-Type"] = self.contenttype
 64
 65        self.data = {
 66            "code": self.code,
 67            "name": HTTP_STATUS_CODES.get(self.code, "???"),
 68            "description": self.description,
 69            "traceback": escape(self.traceback),
 70            "url": SERVER_URL,
 71            "version": SERVER_VERSION
 72        }
 73
 74    def sanitize(self):
 75        if self.code != 201 and not (299 < self.code < 400):
 76            if "Location" in self.response.headers:
 77                del self.response.headers["Location"]
 78
 79    def __str__(self):
 80        self.sanitize()
 81        return DEFAULT_ERROR_MESSAGE % self.data
 82
 83    def __repr__(self):
 84        return "<%s %d %s>" % (
 85            self.__class__.__name__, self.code, HTTP_STATUS_CODES.get(
 86                self.code, "???"
 87            )
 88        )
 89
 90
 91class forbidden(httperror):
 92    """An event for signaling the HTTP Forbidden error"""
 93
 94    code = 403
 95
 96
 97class unauthorized(httperror):
 98    """An event for signaling the HTTP Unauthorized error"""
 99
100    code = 401
101
102
103class notfound(httperror):
104    """An event for signaling the HTTP Not Fouond error"""
105
106    code = 404
107
108
109class redirect(httperror):
110    """An event for signaling the HTTP Redirect response"""
111
112    def __init__(self, request, response, urls, code=None):
113        """
114        The constructor creates a new instance and modifies the
115        *response* argument to reflect a redirect response to the
116        given *url*.
117        """
118
119        if isinstance(urls, string_types):
120            urls = [urls]
121
122        abs_urls = []
123        for url in urls:
124            # Note that urljoin will "do the right thing" whether url is:
125            #  1. a complete URL with host (e.g. "http://www.example.com/test")
126            #  2. a URL relative to root (e.g. "/dummy")
127            #  3. a URL relative to the current path
128            # Note that any query string in request is discarded.
129            url = request.uri.relative(url).unicode()
130            abs_urls.append(url)
131        self.urls = urls = abs_urls
132
133        # RFC 2616 indicates a 301 response code fits our goal; however,
134        # browser support for 301 is quite messy. Do 302/303 instead. See
135        # http://ppewww.ph.gla.ac.uk/~flavell/www/post-redirect.html
136        if code is None:
137            if request.protocol >= (1, 1):
138                code = 303
139            else:
140                code = 302
141        else:
142            if code < 300 or code > 399:
143                raise ValueError("status code must be between 300 and 399.")
144
145        super(redirect, self).__init__(request, response, code)
146
147        if code in (300, 301, 302, 303, 307):
148            response.headers["Content-Type"] = "text/html"
149            # "The ... URI SHOULD be given by the Location field
150            # in the response."
151            response.headers["Location"] = urls[0]
152
153            # "Unless the request method was HEAD, the entity of the response
154            # SHOULD contain a short hypertext note with a hyperlink to the
155            # new URI(s)."
156            msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
157                   301: ("This resource has permanently moved to "
158                         "<a href='%s'>%s</a>."),
159                   302: ("This resource resides temporarily at "
160                         "<a href='%s'>%s</a>."),
161                   303: ("This resource can be found at "
162                         "<a href='%s'>%s</a>."),
163                   307: ("This resource has moved temporarily to "
164                         "<a href='%s'>%s</a>."),
165                   }[code]
166            response.body = "<br />\n".join([msg % (u, u) for u in urls])
167            # Previous code may have set C-L, so we have to reset it
168            # (allow finalize to set it).
169            response.headers.pop("Content-Length", None)
170        elif code == 304:
171            # Not Modified.
172            # "The response MUST include the following header fields:
173            # Date, unless its omission is required by section 14.18.1"
174            # The "Date" header should have been set in Response.__init__
175
176            # "...the response SHOULD NOT include other entity-headers."
177            for key in ("Allow", "Content-Encoding", "Content-Language",
178                        "Content-Length", "Content-Location", "Content-MD5",
179                        "Content-Range", "Content-Type", "Expires",
180                        "Last-Modified"):
181                if key in response.headers:
182                    del response.headers[key]
183
184            # "The 304 response MUST NOT contain a message-body."
185            response.body = None
186            # Previous code may have set C-L, so we have to reset it.
187            response.headers.pop("Content-Length", None)
188        elif code == 305:
189            # Use Proxy.
190            # urls[0] should be the URI of the proxy.
191            response.headers["Location"] = urls[0]
192            response.body = None
193            # Previous code may have set C-L, so we have to reset it.
194            response.headers.pop("Content-Length", None)
195        else:
196            raise ValueError("The %s status code is unknown." % code)
197
198    def __repr__(self):
199        if len(self.channels) > 1:
200            channels = repr(self.channels)
201        elif len(self.channels) == 1:
202            channels = str(self.channels[0])
203        else:
204            channels = ""
205        return "<%s %d[%s.%s] %s>" % (
206            self.__class__.__name__, self.code, channels, self.name,
207            " ".join(self.urls)
208        )