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