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