1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | |
---|
4 | """ |
---|
5 | | This file is part of the web2py Web Framework |
---|
6 | | Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> |
---|
7 | | License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) |
---|
8 | |
---|
9 | HTTP statuses helpers |
---|
10 | -------------------------------------------- |
---|
11 | """ |
---|
12 | |
---|
13 | import re |
---|
14 | from gluon._compat import iteritems, unicodeT, to_bytes |
---|
15 | |
---|
16 | __all__ = ['HTTP', 'redirect'] |
---|
17 | |
---|
18 | defined_status = { |
---|
19 | 200: 'OK', |
---|
20 | 201: 'CREATED', |
---|
21 | 202: 'ACCEPTED', |
---|
22 | 203: 'NON-AUTHORITATIVE INFORMATION', |
---|
23 | 204: 'NO CONTENT', |
---|
24 | 205: 'RESET CONTENT', |
---|
25 | 206: 'PARTIAL CONTENT', |
---|
26 | 301: 'MOVED PERMANENTLY', |
---|
27 | 302: 'FOUND', |
---|
28 | 303: 'SEE OTHER', |
---|
29 | 304: 'NOT MODIFIED', |
---|
30 | 305: 'USE PROXY', |
---|
31 | 307: 'TEMPORARY REDIRECT', |
---|
32 | 400: 'BAD REQUEST', |
---|
33 | 401: 'UNAUTHORIZED', |
---|
34 | 402: 'PAYMENT REQUIRED', |
---|
35 | 403: 'FORBIDDEN', |
---|
36 | 404: 'NOT FOUND', |
---|
37 | 405: 'METHOD NOT ALLOWED', |
---|
38 | 406: 'NOT ACCEPTABLE', |
---|
39 | 407: 'PROXY AUTHENTICATION REQUIRED', |
---|
40 | 408: 'REQUEST TIMEOUT', |
---|
41 | 409: 'CONFLICT', |
---|
42 | 410: 'GONE', |
---|
43 | 411: 'LENGTH REQUIRED', |
---|
44 | 412: 'PRECONDITION FAILED', |
---|
45 | 413: 'REQUEST ENTITY TOO LARGE', |
---|
46 | 414: 'REQUEST-URI TOO LONG', |
---|
47 | 415: 'UNSUPPORTED MEDIA TYPE', |
---|
48 | 416: 'REQUESTED RANGE NOT SATISFIABLE', |
---|
49 | 417: 'EXPECTATION FAILED', |
---|
50 | 422: 'UNPROCESSABLE ENTITY', |
---|
51 | 429: 'TOO MANY REQUESTS', |
---|
52 | 451: 'UNAVAILABLE FOR LEGAL REASONS', # http://www.451unavailable.org/ |
---|
53 | 500: 'INTERNAL SERVER ERROR', |
---|
54 | 501: 'NOT IMPLEMENTED', |
---|
55 | 502: 'BAD GATEWAY', |
---|
56 | 503: 'SERVICE UNAVAILABLE', |
---|
57 | 504: 'GATEWAY TIMEOUT', |
---|
58 | 505: 'HTTP VERSION NOT SUPPORTED', |
---|
59 | 509: 'BANDWIDTH LIMIT EXCEEDED', |
---|
60 | } |
---|
61 | |
---|
62 | regex_status = re.compile('^\d{3} [0-9A-Z ]+$') |
---|
63 | |
---|
64 | |
---|
65 | class HTTP(Exception): |
---|
66 | """Raises an HTTP response |
---|
67 | |
---|
68 | Args: |
---|
69 | status: usually an integer. If it's a well known status code, the ERROR |
---|
70 | message will be automatically added. A string can also be passed |
---|
71 | as `510 Foo Bar` and in that case the status code and the error |
---|
72 | message will be parsed accordingly |
---|
73 | body: what to return as body. If left as is, will return the error code |
---|
74 | and the status message in the body itself |
---|
75 | cookies: pass cookies along (usually not needed) |
---|
76 | headers: pass headers as usual dict mapping |
---|
77 | """ |
---|
78 | |
---|
79 | def __init__( |
---|
80 | self, |
---|
81 | status, |
---|
82 | body='', |
---|
83 | cookies=None, |
---|
84 | **headers |
---|
85 | ): |
---|
86 | self.status = status |
---|
87 | self.body = body |
---|
88 | self.headers = headers |
---|
89 | self.cookies2headers(cookies) |
---|
90 | |
---|
91 | def cookies2headers(self, cookies): |
---|
92 | if cookies and len(cookies) > 0: |
---|
93 | self.headers['Set-Cookie'] = [ |
---|
94 | str(cookie)[11:] for cookie in cookies.values()] |
---|
95 | |
---|
96 | def to(self, responder, env=None): |
---|
97 | env = env or {} |
---|
98 | status = self.status |
---|
99 | headers = self.headers |
---|
100 | if status in defined_status: |
---|
101 | status = '%d %s' % (status, defined_status[status]) |
---|
102 | elif isinstance(status, int): |
---|
103 | status = '%d UNKNOWN ERROR' % status |
---|
104 | else: |
---|
105 | status = str(status) |
---|
106 | if not regex_status.match(status): |
---|
107 | status = '500 %s' % (defined_status[500]) |
---|
108 | headers.setdefault('Content-Type', 'text/html; charset=UTF-8') |
---|
109 | body = self.body |
---|
110 | if status[:1] == '4': |
---|
111 | if not body: |
---|
112 | body = status |
---|
113 | if isinstance(body, (str, bytes, bytearray)): |
---|
114 | if isinstance(body, unicodeT): |
---|
115 | body = to_bytes(body) # This must be done before len |
---|
116 | headers['Content-Length'] = len(body) |
---|
117 | rheaders = [] |
---|
118 | for k, v in iteritems(headers): |
---|
119 | if isinstance(v, list): |
---|
120 | rheaders += [(k, str(item)) for item in v] |
---|
121 | elif v is not None: |
---|
122 | rheaders.append((k, str(v))) |
---|
123 | responder(status, rheaders) |
---|
124 | if env.get('request_method', '') == 'HEAD': |
---|
125 | return [''] |
---|
126 | elif isinstance(body, (str, bytes, bytearray)): |
---|
127 | if isinstance(body, unicodeT): |
---|
128 | body = to_bytes(body) |
---|
129 | return [body] |
---|
130 | elif hasattr(body, '__iter__'): |
---|
131 | return body |
---|
132 | else: |
---|
133 | body = str(body) |
---|
134 | if isinstance(body, unicodeT): |
---|
135 | body = to_bytes(body) |
---|
136 | return [body] |
---|
137 | |
---|
138 | @property |
---|
139 | def message(self): |
---|
140 | """ |
---|
141 | compose a message describing this exception |
---|
142 | |
---|
143 | "status defined_status [web2py_error]" |
---|
144 | |
---|
145 | message elements that are not defined are omitted |
---|
146 | """ |
---|
147 | msg = '%(status)s' |
---|
148 | if self.status in defined_status: |
---|
149 | msg = '%(status)s %(defined_status)s' |
---|
150 | if 'web2py_error' in self.headers: |
---|
151 | msg += ' [%(web2py_error)s]' |
---|
152 | return msg % dict( |
---|
153 | status=self.status, |
---|
154 | defined_status=defined_status.get(self.status), |
---|
155 | web2py_error=self.headers.get('web2py_error')) |
---|
156 | |
---|
157 | def __str__(self): |
---|
158 | """stringify me""" |
---|
159 | return self.message |
---|
160 | |
---|
161 | |
---|
162 | def redirect(location='', how=303, client_side=False, headers=None): |
---|
163 | """Raises a redirect (303) |
---|
164 | |
---|
165 | Args: |
---|
166 | location: the url where to redirect |
---|
167 | how: what HTTP status code to use when redirecting |
---|
168 | client_side: if set to True, it triggers a reload of the entire page |
---|
169 | when the fragment has been loaded as a component |
---|
170 | """ |
---|
171 | headers = headers or {} |
---|
172 | if location: |
---|
173 | from gluon.globals import current |
---|
174 | loc = location.replace('\r', '%0D').replace('\n', '%0A') |
---|
175 | if client_side and current.request.ajax: |
---|
176 | headers['web2py-redirect-location'] = loc |
---|
177 | raise HTTP(200, **headers) |
---|
178 | else: |
---|
179 | headers['Location'] = loc |
---|
180 | raise HTTP(how, |
---|
181 | 'You are being redirected <a href="%s">here</a>' % loc, |
---|
182 | **headers) |
---|
183 | else: |
---|
184 | from gluon.globals import current |
---|
185 | if client_side and current.request.ajax: |
---|
186 | headers['web2py-component-command'] = 'window.location.reload(true)' |
---|
187 | raise HTTP(200, **headers) |
---|