|
1 import os |
|
2 from Cookie import SimpleCookie, CookieError |
|
3 from pprint import pformat |
|
4 from urllib import urlencode |
|
5 from urlparse import urljoin |
|
6 try: |
|
7 # The mod_python version is more efficient, so try importing it first. |
|
8 from mod_python.util import parse_qsl |
|
9 except ImportError: |
|
10 from cgi import parse_qsl |
|
11 |
|
12 from django.utils.datastructures import MultiValueDict, FileDict |
|
13 from django.utils.encoding import smart_str, iri_to_uri, force_unicode |
|
14 |
|
15 from utils import * |
|
16 |
|
17 RESERVED_CHARS="!*'();:@&=+$,/?%#[]" |
|
18 |
|
19 |
|
20 class Http404(Exception): |
|
21 pass |
|
22 |
|
23 class HttpRequest(object): |
|
24 """A basic HTTP request.""" |
|
25 |
|
26 # The encoding used in GET/POST dicts. None means use default setting. |
|
27 _encoding = None |
|
28 |
|
29 def __init__(self): |
|
30 self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {} |
|
31 self.path = '' |
|
32 self.method = None |
|
33 |
|
34 def __repr__(self): |
|
35 return '<HttpRequest\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % \ |
|
36 (pformat(self.GET), pformat(self.POST), pformat(self.COOKIES), |
|
37 pformat(self.META)) |
|
38 |
|
39 def __getitem__(self, key): |
|
40 for d in (self.POST, self.GET): |
|
41 if key in d: |
|
42 return d[key] |
|
43 raise KeyError, "%s not found in either POST or GET" % key |
|
44 |
|
45 def has_key(self, key): |
|
46 return key in self.GET or key in self.POST |
|
47 |
|
48 __contains__ = has_key |
|
49 |
|
50 def get_host(self): |
|
51 """Returns the HTTP host using the environment or request headers.""" |
|
52 # We try three options, in order of decreasing preference. |
|
53 if 'HTTP_X_FORWARDED_HOST' in self.META: |
|
54 host = self.META['HTTP_X_FORWARDED_HOST'] |
|
55 elif 'HTTP_HOST' in self.META: |
|
56 host = self.META['HTTP_HOST'] |
|
57 else: |
|
58 # Reconstruct the host using the algorithm from PEP 333. |
|
59 host = self.META['SERVER_NAME'] |
|
60 server_port = self.META['SERVER_PORT'] |
|
61 if server_port != (self.is_secure() and 443 or 80): |
|
62 host = '%s:%s' % (host, server_port) |
|
63 return host |
|
64 |
|
65 def get_full_path(self): |
|
66 return '' |
|
67 |
|
68 def build_absolute_uri(self, location=None): |
|
69 """ |
|
70 Builds an absolute URI from the location and the variables available in |
|
71 this request. If no location is specified, the absolute URI is built on |
|
72 ``request.get_full_path()``. |
|
73 """ |
|
74 if not location: |
|
75 location = self.get_full_path() |
|
76 if not ':' in location: |
|
77 current_uri = '%s://%s%s' % (self.is_secure() and 'https' or 'http', |
|
78 self.get_host(), self.path) |
|
79 location = urljoin(current_uri, location) |
|
80 return location |
|
81 |
|
82 def is_secure(self): |
|
83 return os.environ.get("HTTPS") == "on" |
|
84 |
|
85 def is_ajax(self): |
|
86 return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' |
|
87 |
|
88 def _set_encoding(self, val): |
|
89 """ |
|
90 Sets the encoding used for GET/POST accesses. If the GET or POST |
|
91 dictionary has already been created, it is removed and recreated on the |
|
92 next access (so that it is decoded correctly). |
|
93 """ |
|
94 self._encoding = val |
|
95 if hasattr(self, '_get'): |
|
96 del self._get |
|
97 if hasattr(self, '_post'): |
|
98 del self._post |
|
99 |
|
100 def _get_encoding(self): |
|
101 return self._encoding |
|
102 |
|
103 encoding = property(_get_encoding, _set_encoding) |
|
104 |
|
105 def parse_file_upload(header_dict, post_data): |
|
106 """Returns a tuple of (POST QueryDict, FILES MultiValueDict).""" |
|
107 import email, email.Message |
|
108 from cgi import parse_header |
|
109 raw_message = '\r\n'.join(['%s:%s' % pair for pair in header_dict.items()]) |
|
110 raw_message += '\r\n\r\n' + post_data |
|
111 msg = email.message_from_string(raw_message) |
|
112 POST = QueryDict('', mutable=True) |
|
113 FILES = MultiValueDict() |
|
114 for submessage in msg.get_payload(): |
|
115 if submessage and isinstance(submessage, email.Message.Message): |
|
116 name_dict = parse_header(submessage['Content-Disposition'])[1] |
|
117 # name_dict is something like {'name': 'file', 'filename': 'test.txt'} for file uploads |
|
118 # or {'name': 'blah'} for POST fields |
|
119 # We assume all uploaded files have a 'filename' set. |
|
120 if 'filename' in name_dict: |
|
121 assert type([]) != type(submessage.get_payload()), "Nested MIME messages are not supported" |
|
122 if not name_dict['filename'].strip(): |
|
123 continue |
|
124 # IE submits the full path, so trim everything but the basename. |
|
125 # (We can't use os.path.basename because that uses the server's |
|
126 # directory separator, which may not be the same as the |
|
127 # client's one.) |
|
128 filename = name_dict['filename'][name_dict['filename'].rfind("\\")+1:] |
|
129 FILES.appendlist(name_dict['name'], FileDict({ |
|
130 'filename': filename, |
|
131 'content-type': 'Content-Type' in submessage and submessage['Content-Type'] or None, |
|
132 'content': submessage.get_payload(), |
|
133 })) |
|
134 else: |
|
135 POST.appendlist(name_dict['name'], submessage.get_payload()) |
|
136 return POST, FILES |
|
137 |
|
138 |
|
139 class QueryDict(MultiValueDict): |
|
140 """ |
|
141 A specialized MultiValueDict that takes a query string when initialized. |
|
142 This is immutable unless you create a copy of it. |
|
143 |
|
144 Values retrieved from this class are converted from the given encoding |
|
145 (DEFAULT_CHARSET by default) to unicode. |
|
146 """ |
|
147 def __init__(self, query_string, mutable=False, encoding=None): |
|
148 MultiValueDict.__init__(self) |
|
149 if not encoding: |
|
150 # *Important*: do not import settings any earlier because of note |
|
151 # in core.handlers.modpython. |
|
152 from django.conf import settings |
|
153 encoding = settings.DEFAULT_CHARSET |
|
154 self.encoding = encoding |
|
155 self._mutable = True |
|
156 for key, value in parse_qsl((query_string or ''), True): # keep_blank_values=True |
|
157 self.appendlist(force_unicode(key, encoding, errors='replace'), |
|
158 force_unicode(value, encoding, errors='replace')) |
|
159 self._mutable = mutable |
|
160 |
|
161 def _assert_mutable(self): |
|
162 if not self._mutable: |
|
163 raise AttributeError("This QueryDict instance is immutable") |
|
164 |
|
165 def __setitem__(self, key, value): |
|
166 self._assert_mutable() |
|
167 key = str_to_unicode(key, self.encoding) |
|
168 value = str_to_unicode(value, self.encoding) |
|
169 MultiValueDict.__setitem__(self, key, value) |
|
170 |
|
171 def __delitem__(self, key): |
|
172 self._assert_mutable() |
|
173 super(QueryDict, self).__delitem__(key) |
|
174 |
|
175 def __copy__(self): |
|
176 result = self.__class__('', mutable=True) |
|
177 for key, value in dict.items(self): |
|
178 dict.__setitem__(result, key, value) |
|
179 return result |
|
180 |
|
181 def __deepcopy__(self, memo): |
|
182 import copy |
|
183 result = self.__class__('', mutable=True) |
|
184 memo[id(self)] = result |
|
185 for key, value in dict.items(self): |
|
186 dict.__setitem__(result, copy.deepcopy(key, memo), copy.deepcopy(value, memo)) |
|
187 return result |
|
188 |
|
189 def setlist(self, key, list_): |
|
190 self._assert_mutable() |
|
191 key = str_to_unicode(key, self.encoding) |
|
192 list_ = [str_to_unicode(elt, self.encoding) for elt in list_] |
|
193 MultiValueDict.setlist(self, key, list_) |
|
194 |
|
195 def setlistdefault(self, key, default_list=()): |
|
196 self._assert_mutable() |
|
197 if key not in self: |
|
198 self.setlist(key, default_list) |
|
199 return MultiValueDict.getlist(self, key) |
|
200 |
|
201 def appendlist(self, key, value): |
|
202 self._assert_mutable() |
|
203 key = str_to_unicode(key, self.encoding) |
|
204 value = str_to_unicode(value, self.encoding) |
|
205 MultiValueDict.appendlist(self, key, value) |
|
206 |
|
207 def update(self, other_dict): |
|
208 self._assert_mutable() |
|
209 f = lambda s: str_to_unicode(s, self.encoding) |
|
210 d = dict([(f(k), f(v)) for k, v in other_dict.items()]) |
|
211 MultiValueDict.update(self, d) |
|
212 |
|
213 def pop(self, key, *args): |
|
214 self._assert_mutable() |
|
215 return MultiValueDict.pop(self, key, *args) |
|
216 |
|
217 def popitem(self): |
|
218 self._assert_mutable() |
|
219 return MultiValueDict.popitem(self) |
|
220 |
|
221 def clear(self): |
|
222 self._assert_mutable() |
|
223 MultiValueDict.clear(self) |
|
224 |
|
225 def setdefault(self, key, default=None): |
|
226 self._assert_mutable() |
|
227 key = str_to_unicode(key, self.encoding) |
|
228 default = str_to_unicode(default, self.encoding) |
|
229 return MultiValueDict.setdefault(self, key, default) |
|
230 |
|
231 def copy(self): |
|
232 """Returns a mutable copy of this object.""" |
|
233 return self.__deepcopy__({}) |
|
234 |
|
235 def urlencode(self): |
|
236 output = [] |
|
237 for k, list_ in self.lists(): |
|
238 k = smart_str(k, self.encoding) |
|
239 output.extend([urlencode({k: smart_str(v, self.encoding)}) for v in list_]) |
|
240 return '&'.join(output) |
|
241 |
|
242 def parse_cookie(cookie): |
|
243 if cookie == '': |
|
244 return {} |
|
245 try: |
|
246 c = SimpleCookie() |
|
247 c.load(cookie) |
|
248 except CookieError: |
|
249 # Invalid cookie |
|
250 return {} |
|
251 |
|
252 cookiedict = {} |
|
253 for key in c.keys(): |
|
254 cookiedict[key] = c.get(key).value |
|
255 return cookiedict |
|
256 |
|
257 class HttpResponse(object): |
|
258 """A basic HTTP response, with content and dictionary-accessed headers.""" |
|
259 |
|
260 status_code = 200 |
|
261 |
|
262 def __init__(self, content='', mimetype=None, status=None, |
|
263 content_type=None): |
|
264 from django.conf import settings |
|
265 self._charset = settings.DEFAULT_CHARSET |
|
266 if mimetype: |
|
267 content_type = mimetype # For backwards compatibility |
|
268 if not content_type: |
|
269 content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, |
|
270 settings.DEFAULT_CHARSET) |
|
271 if not isinstance(content, basestring) and hasattr(content, '__iter__'): |
|
272 self._container = content |
|
273 self._is_string = False |
|
274 else: |
|
275 self._container = [content] |
|
276 self._is_string = True |
|
277 self.cookies = SimpleCookie() |
|
278 if status: |
|
279 self.status_code = status |
|
280 |
|
281 # _headers is a mapping of the lower-case name to the original case of |
|
282 # the header (required for working with legacy systems) and the header |
|
283 # value. |
|
284 self._headers = {'content-type': ('Content-Type', content_type)} |
|
285 |
|
286 def __str__(self): |
|
287 """Full HTTP message, including headers.""" |
|
288 return '\n'.join(['%s: %s' % (key, value) |
|
289 for key, value in self._headers.values()]) \ |
|
290 + '\n\n' + self.content |
|
291 |
|
292 def _convert_to_ascii(self, *values): |
|
293 """Converts all values to ascii strings.""" |
|
294 for value in values: |
|
295 if isinstance(value, unicode): |
|
296 try: |
|
297 yield value.encode('us-ascii') |
|
298 except UnicodeError, e: |
|
299 e.reason += ', HTTP response headers must be in US-ASCII format' |
|
300 raise |
|
301 else: |
|
302 yield str(value) |
|
303 |
|
304 def __setitem__(self, header, value): |
|
305 header, value = self._convert_to_ascii(header, value) |
|
306 self._headers[header.lower()] = (header, value) |
|
307 |
|
308 def __delitem__(self, header): |
|
309 try: |
|
310 del self._headers[header.lower()] |
|
311 except KeyError: |
|
312 pass |
|
313 |
|
314 def __getitem__(self, header): |
|
315 return self._headers[header.lower()][1] |
|
316 |
|
317 def has_header(self, header): |
|
318 """Case-insensitive check for a header.""" |
|
319 return self._headers.has_key(header.lower()) |
|
320 |
|
321 __contains__ = has_header |
|
322 |
|
323 def items(self): |
|
324 return self._headers.values() |
|
325 |
|
326 def get(self, header, alternate): |
|
327 return self._headers.get(header.lower(), (None, alternate))[1] |
|
328 |
|
329 def set_cookie(self, key, value='', max_age=None, expires=None, path='/', |
|
330 domain=None, secure=False): |
|
331 self.cookies[key] = value |
|
332 if max_age is not None: |
|
333 self.cookies[key]['max-age'] = max_age |
|
334 if expires is not None: |
|
335 self.cookies[key]['expires'] = expires |
|
336 if path is not None: |
|
337 self.cookies[key]['path'] = path |
|
338 if domain is not None: |
|
339 self.cookies[key]['domain'] = domain |
|
340 if secure: |
|
341 self.cookies[key]['secure'] = True |
|
342 |
|
343 def delete_cookie(self, key, path='/', domain=None): |
|
344 self.set_cookie(key, max_age=0, path=path, domain=domain, |
|
345 expires='Thu, 01-Jan-1970 00:00:00 GMT') |
|
346 |
|
347 def _get_content(self): |
|
348 if self.has_header('Content-Encoding'): |
|
349 return ''.join(self._container) |
|
350 return smart_str(''.join(self._container), self._charset) |
|
351 |
|
352 def _set_content(self, value): |
|
353 self._container = [value] |
|
354 self._is_string = True |
|
355 |
|
356 content = property(_get_content, _set_content) |
|
357 |
|
358 def __iter__(self): |
|
359 self._iterator = iter(self._container) |
|
360 return self |
|
361 |
|
362 def next(self): |
|
363 chunk = self._iterator.next() |
|
364 if isinstance(chunk, unicode): |
|
365 chunk = chunk.encode(self._charset) |
|
366 return str(chunk) |
|
367 |
|
368 def close(self): |
|
369 if hasattr(self._container, 'close'): |
|
370 self._container.close() |
|
371 |
|
372 # The remaining methods partially implement the file-like object interface. |
|
373 # See http://docs.python.org/lib/bltin-file-objects.html |
|
374 def write(self, content): |
|
375 if not self._is_string: |
|
376 raise Exception("This %s instance is not writable" % self.__class__) |
|
377 self._container.append(content) |
|
378 |
|
379 def flush(self): |
|
380 pass |
|
381 |
|
382 def tell(self): |
|
383 if not self._is_string: |
|
384 raise Exception("This %s instance cannot tell its position" % self.__class__) |
|
385 return sum([len(chunk) for chunk in self._container]) |
|
386 |
|
387 class HttpResponseRedirect(HttpResponse): |
|
388 status_code = 302 |
|
389 |
|
390 def __init__(self, redirect_to): |
|
391 HttpResponse.__init__(self) |
|
392 self['Location'] = iri_to_uri(redirect_to) |
|
393 |
|
394 class HttpResponsePermanentRedirect(HttpResponse): |
|
395 status_code = 301 |
|
396 |
|
397 def __init__(self, redirect_to): |
|
398 HttpResponse.__init__(self) |
|
399 self['Location'] = iri_to_uri(redirect_to) |
|
400 |
|
401 class HttpResponseNotModified(HttpResponse): |
|
402 status_code = 304 |
|
403 |
|
404 class HttpResponseBadRequest(HttpResponse): |
|
405 status_code = 400 |
|
406 |
|
407 class HttpResponseNotFound(HttpResponse): |
|
408 status_code = 404 |
|
409 |
|
410 class HttpResponseForbidden(HttpResponse): |
|
411 status_code = 403 |
|
412 |
|
413 class HttpResponseNotAllowed(HttpResponse): |
|
414 status_code = 405 |
|
415 |
|
416 def __init__(self, permitted_methods): |
|
417 HttpResponse.__init__(self) |
|
418 self['Allow'] = ', '.join(permitted_methods) |
|
419 |
|
420 class HttpResponseGone(HttpResponse): |
|
421 status_code = 410 |
|
422 |
|
423 def __init__(self, *args, **kwargs): |
|
424 HttpResponse.__init__(self, *args, **kwargs) |
|
425 |
|
426 class HttpResponseServerError(HttpResponse): |
|
427 status_code = 500 |
|
428 |
|
429 def __init__(self, *args, **kwargs): |
|
430 HttpResponse.__init__(self, *args, **kwargs) |
|
431 |
|
432 # A backwards compatible alias for HttpRequest.get_host. |
|
433 def get_host(request): |
|
434 return request.get_host() |
|
435 |
|
436 # It's neither necessary nor appropriate to use |
|
437 # django.utils.encoding.smart_unicode for parsing URLs and form inputs. Thus, |
|
438 # this slightly more restricted function. |
|
439 def str_to_unicode(s, encoding): |
|
440 """ |
|
441 Converts basestring objects to unicode, using the given encoding. Illegally |
|
442 encoded input characters are replaced with Unicode "unknown" codepoint |
|
443 (\ufffd). |
|
444 |
|
445 Returns any non-basestring objects without change. |
|
446 """ |
|
447 if isinstance(s, str): |
|
448 return unicode(s, encoding, 'replace') |
|
449 else: |
|
450 return s |