From b4066d7d2149782cd694b98d1a6ed5c7ee885f55 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 21 Oct 2012 11:12:59 -0700 Subject: [PATCH] Cleaned up the the http module. Moved all of the code from __init__.py to request.py, response.py and utils.py --- django/http/__init__.py | 943 +--------------------------------------- django/http/cookie.py | 83 ++++ django/http/request.py | 453 +++++++++++++++++++ django/http/response.py | 423 ++++++++++++++++++ django/http/utils.py | 5 +- 5 files changed, 973 insertions(+), 934 deletions(-) create mode 100644 django/http/cookie.py create mode 100644 django/http/request.py create mode 100644 django/http/response.py diff --git a/django/http/__init__.py b/django/http/__init__.py index 49acd57af3746..46afa34ee7376 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -1,933 +1,10 @@ -from __future__ import absolute_import, unicode_literals - -import copy -import datetime -from email.header import Header -import os -import re -import sys -import time -import warnings - -from io import BytesIO -from pprint import pformat -try: - from urllib.parse import quote, parse_qsl, urlencode, urljoin, urlparse -except ImportError: # Python 2 - from urllib import quote, urlencode - from urlparse import parse_qsl, urljoin, urlparse - -from django.utils.six.moves import http_cookies -# Some versions of Python 2.7 and later won't need this encoding bug fix: -_cookie_encodes_correctly = http_cookies.SimpleCookie().value_encode(';') == (';', '"\\073"') -# See ticket #13007, http://bugs.python.org/issue2193 and http://trac.edgewall.org/ticket/2256 -_tc = http_cookies.SimpleCookie() -try: - _tc.load(str('foo:bar=1')) - _cookie_allows_colon_in_names = True -except http_cookies.CookieError: - _cookie_allows_colon_in_names = False - -if _cookie_encodes_correctly and _cookie_allows_colon_in_names: - SimpleCookie = http_cookies.SimpleCookie -else: - Morsel = http_cookies.Morsel - - class SimpleCookie(http_cookies.SimpleCookie): - if not _cookie_encodes_correctly: - def value_encode(self, val): - # Some browsers do not support quoted-string from RFC 2109, - # including some versions of Safari and Internet Explorer. - # These browsers split on ';', and some versions of Safari - # are known to split on ', '. Therefore, we encode ';' and ',' - - # SimpleCookie already does the hard work of encoding and decoding. - # It uses octal sequences like '\\012' for newline etc. - # and non-ASCII chars. We just make use of this mechanism, to - # avoid introducing two encoding schemes which would be confusing - # and especially awkward for javascript. - - # NB, contrary to Python docs, value_encode returns a tuple containing - # (real val, encoded_val) - val, encoded = super(SimpleCookie, self).value_encode(val) - - encoded = encoded.replace(";", "\\073").replace(",","\\054") - # If encoded now contains any quoted chars, we need double quotes - # around the whole string. - if "\\" in encoded and not encoded.startswith('"'): - encoded = '"' + encoded + '"' - - return val, encoded - - if not _cookie_allows_colon_in_names: - def load(self, rawdata): - self.bad_cookies = set() - super(SimpleCookie, self).load(force_str(rawdata)) - for key in self.bad_cookies: - del self[key] - - # override private __set() method: - # (needed for using our Morsel, and for laxness with CookieError - def _BaseCookie__set(self, key, real_value, coded_value): - key = force_str(key) - try: - M = self.get(key, Morsel()) - M.set(key, real_value, coded_value) - dict.__setitem__(self, key, M) - except http_cookies.CookieError: - self.bad_cookies.add(key) - dict.__setitem__(self, key, http_cookies.Morsel()) - - -from django.conf import settings -from django.core import signing -from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation -from django.core.files import uploadhandler -from django.http.multipartparser import MultiPartParser -from django.http.utils import * -from django.utils.datastructures import MultiValueDict, ImmutableList -from django.utils.encoding import force_bytes, force_str, force_text, iri_to_uri -from django.utils.http import cookie_date -from django.utils import six -from django.utils import timezone - -RESERVED_CHARS="!*'();:@&=+$,/?%#[]" - -absolute_http_url_re = re.compile(r"^https?://", re.I) - -class Http404(Exception): - pass - -RAISE_ERROR = object() - - -def build_request_repr(request, path_override=None, GET_override=None, - POST_override=None, COOKIES_override=None, - META_override=None): - """ - Builds and returns the request's representation string. The request's - attributes may be overridden by pre-processed values. - """ - # Since this is called as part of error handling, we need to be very - # robust against potentially malformed input. - try: - get = (pformat(GET_override) - if GET_override is not None - else pformat(request.GET)) - except Exception: - get = '' - if request._post_parse_error: - post = '' - else: - try: - post = (pformat(POST_override) - if POST_override is not None - else pformat(request.POST)) - except Exception: - post = '' - try: - cookies = (pformat(COOKIES_override) - if COOKIES_override is not None - else pformat(request.COOKIES)) - except Exception: - cookies = '' - try: - meta = (pformat(META_override) - if META_override is not None - else pformat(request.META)) - except Exception: - meta = '' - path = path_override if path_override is not None else request.path - return force_str('<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % - (request.__class__.__name__, - path, - six.text_type(get), - six.text_type(post), - six.text_type(cookies), - six.text_type(meta))) - -class UnreadablePostError(IOError): - pass - -class HttpRequest(object): - """A basic HTTP request.""" - - # The encoding used in GET/POST dicts. None means use default setting. - _encoding = None - _upload_handlers = [] - - def __init__(self): - self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {} - self.path = '' - self.path_info = '' - self.method = None - self._post_parse_error = False - - def __repr__(self): - return build_request_repr(self) - - def get_host(self): - """Returns the HTTP host using the environment or request headers.""" - # We try three options, in order of decreasing preference. - if settings.USE_X_FORWARDED_HOST and ( - 'HTTP_X_FORWARDED_HOST' in self.META): - host = self.META['HTTP_X_FORWARDED_HOST'] - elif 'HTTP_HOST' in self.META: - host = self.META['HTTP_HOST'] - else: - # Reconstruct the host using the algorithm from PEP 333. - host = self.META['SERVER_NAME'] - server_port = str(self.META['SERVER_PORT']) - if server_port != ('443' if self.is_secure() else '80'): - host = '%s:%s' % (host, server_port) - - # Disallow potentially poisoned hostnames. - if set(';/?@&=+$,').intersection(host): - raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host) - - return host - - def get_full_path(self): - # RFC 3986 requires query string arguments to be in the ASCII range. - # Rather than crash if this doesn't happen, we encode defensively. - return '%s%s' % (self.path, ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) if self.META.get('QUERY_STRING', '') else '') - - def get_signed_cookie(self, key, default=RAISE_ERROR, salt='', max_age=None): - """ - Attempts to return a signed cookie. If the signature fails or the - cookie has expired, raises an exception... unless you provide the - default argument in which case that value will be returned instead. - """ - try: - cookie_value = self.COOKIES[key] - except KeyError: - if default is not RAISE_ERROR: - return default - else: - raise - try: - value = signing.get_cookie_signer(salt=key + salt).unsign( - cookie_value, max_age=max_age) - except signing.BadSignature: - if default is not RAISE_ERROR: - return default - else: - raise - return value - - def build_absolute_uri(self, location=None): - """ - Builds an absolute URI from the location and the variables available in - this request. If no location is specified, the absolute URI is built on - ``request.get_full_path()``. - """ - if not location: - location = self.get_full_path() - if not absolute_http_url_re.match(location): - current_uri = '%s://%s%s' % ('https' if self.is_secure() else 'http', - self.get_host(), self.path) - location = urljoin(current_uri, location) - return iri_to_uri(location) - - def _is_secure(self): - return os.environ.get("HTTPS") == "on" - - def is_secure(self): - # First, check the SECURE_PROXY_SSL_HEADER setting. - if settings.SECURE_PROXY_SSL_HEADER: - try: - header, value = settings.SECURE_PROXY_SSL_HEADER - except ValueError: - raise ImproperlyConfigured('The SECURE_PROXY_SSL_HEADER setting must be a tuple containing two values.') - if self.META.get(header, None) == value: - return True - - # Failing that, fall back to _is_secure(), which is a hook for - # subclasses to implement. - return self._is_secure() - - def is_ajax(self): - return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' - - @property - def encoding(self): - return self._encoding - - @encoding.setter - def encoding(self, val): - """ - Sets the encoding used for GET/POST accesses. If the GET or POST - dictionary has already been created, it is removed and recreated on the - next access (so that it is decoded correctly). - """ - self._encoding = val - if hasattr(self, '_get'): - del self._get - if hasattr(self, '_post'): - del self._post - - def _initialize_handlers(self): - self._upload_handlers = [uploadhandler.load_handler(handler, self) - for handler in settings.FILE_UPLOAD_HANDLERS] - - @property - def upload_handlers(self): - if not self._upload_handlers: - # If there are no upload handlers defined, initialize them from settings. - self._initialize_handlers() - return self._upload_handlers - - @upload_handlers.setter - def upload_handlers(self, upload_handlers): - if hasattr(self, '_files'): - raise AttributeError("You cannot set the upload handlers after the upload has been processed.") - self._upload_handlers = upload_handlers - - def parse_file_upload(self, META, post_data): - """Returns a tuple of (POST QueryDict, FILES MultiValueDict).""" - self.upload_handlers = ImmutableList( - self.upload_handlers, - warning="You cannot alter upload handlers after the upload has been processed." - ) - parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding) - return parser.parse() - - @property - def body(self): - if not hasattr(self, '_body'): - if self._read_started: - raise Exception("You cannot access body after reading from request's data stream") - try: - self._body = self.read() - except IOError as e: - six.reraise(UnreadablePostError, UnreadablePostError(*e.args), sys.exc_info()[2]) - self._stream = BytesIO(self._body) - return self._body - - @property - def raw_post_data(self): - warnings.warn('HttpRequest.raw_post_data has been deprecated. Use HttpRequest.body instead.', DeprecationWarning) - return self.body - - def _mark_post_parse_error(self): - self._post = QueryDict('') - self._files = MultiValueDict() - self._post_parse_error = True - - def _load_post_and_files(self): - """Populate self._post and self._files if the content-type is a form type""" - if self.method != 'POST': - self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict() - return - if self._read_started and not hasattr(self, '_body'): - self._mark_post_parse_error() - return - - if self.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'): - if hasattr(self, '_body'): - # Use already read data - data = BytesIO(self._body) - else: - data = self - try: - self._post, self._files = self.parse_file_upload(self.META, data) - except: - # An error occured while parsing POST data. Since when - # formatting the error the request handler might access - # self.POST, set self._post and self._file to prevent - # attempts to parse POST data again. - # Mark that an error occured. This allows self.__repr__ to - # be explicit about it instead of simply representing an - # empty POST - self._mark_post_parse_error() - raise - elif self.META.get('CONTENT_TYPE', '').startswith('application/x-www-form-urlencoded'): - self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict() - else: - self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict() - - ## File-like and iterator interface. - ## - ## Expects self._stream to be set to an appropriate source of bytes by - ## a corresponding request subclass (e.g. WSGIRequest). - ## Also when request data has already been read by request.POST or - ## request.body, self._stream points to a BytesIO instance - ## containing that data. - - def read(self, *args, **kwargs): - self._read_started = True - return self._stream.read(*args, **kwargs) - - def readline(self, *args, **kwargs): - self._read_started = True - return self._stream.readline(*args, **kwargs) - - def xreadlines(self): - while True: - buf = self.readline() - if not buf: - break - yield buf - - __iter__ = xreadlines - - def readlines(self): - return list(iter(self)) - - -class QueryDict(MultiValueDict): - """ - A specialized MultiValueDict that takes a query string when initialized. - This is immutable unless you create a copy of it. - - Values retrieved from this class are converted from the given encoding - (DEFAULT_CHARSET by default) to unicode. - """ - # These are both reset in __init__, but is specified here at the class - # level so that unpickling will have valid values - _mutable = True - _encoding = None - - def __init__(self, query_string, mutable=False, encoding=None): - super(QueryDict, self).__init__() - if not encoding: - encoding = settings.DEFAULT_CHARSET - self.encoding = encoding - if six.PY3: - for key, value in parse_qsl(query_string or '', - keep_blank_values=True, - encoding=encoding): - self.appendlist(key, value) - else: - for key, value in parse_qsl(query_string or '', - keep_blank_values=True): - self.appendlist(force_text(key, encoding, errors='replace'), - force_text(value, encoding, errors='replace')) - self._mutable = mutable - - @property - def encoding(self): - if self._encoding is None: - self._encoding = settings.DEFAULT_CHARSET - return self._encoding - - @encoding.setter - def encoding(self, value): - self._encoding = value - - def _assert_mutable(self): - if not self._mutable: - raise AttributeError("This QueryDict instance is immutable") - - def __setitem__(self, key, value): - self._assert_mutable() - key = bytes_to_text(key, self.encoding) - value = bytes_to_text(value, self.encoding) - super(QueryDict, self).__setitem__(key, value) - - def __delitem__(self, key): - self._assert_mutable() - super(QueryDict, self).__delitem__(key) - - def __copy__(self): - result = self.__class__('', mutable=True, encoding=self.encoding) - for key, value in six.iterlists(self): - result.setlist(key, value) - return result - - def __deepcopy__(self, memo): - result = self.__class__('', mutable=True, encoding=self.encoding) - memo[id(self)] = result - for key, value in six.iterlists(self): - result.setlist(copy.deepcopy(key, memo), copy.deepcopy(value, memo)) - return result - - def setlist(self, key, list_): - self._assert_mutable() - key = bytes_to_text(key, self.encoding) - list_ = [bytes_to_text(elt, self.encoding) for elt in list_] - super(QueryDict, self).setlist(key, list_) - - def setlistdefault(self, key, default_list=None): - self._assert_mutable() - return super(QueryDict, self).setlistdefault(key, default_list) - - def appendlist(self, key, value): - self._assert_mutable() - key = bytes_to_text(key, self.encoding) - value = bytes_to_text(value, self.encoding) - super(QueryDict, self).appendlist(key, value) - - def pop(self, key, *args): - self._assert_mutable() - return super(QueryDict, self).pop(key, *args) - - def popitem(self): - self._assert_mutable() - return super(QueryDict, self).popitem() - - def clear(self): - self._assert_mutable() - super(QueryDict, self).clear() - - def setdefault(self, key, default=None): - self._assert_mutable() - key = bytes_to_text(key, self.encoding) - default = bytes_to_text(default, self.encoding) - return super(QueryDict, self).setdefault(key, default) - - def copy(self): - """Returns a mutable copy of this object.""" - return self.__deepcopy__({}) - - def urlencode(self, safe=None): - """ - Returns an encoded string of all query string arguments. - - :arg safe: Used to specify characters which do not require quoting, for - example:: - - >>> q = QueryDict('', mutable=True) - >>> q['next'] = '/a&b/' - >>> q.urlencode() - 'next=%2Fa%26b%2F' - >>> q.urlencode(safe='/') - 'next=/a%26b/' - - """ - output = [] - if safe: - safe = force_bytes(safe, self.encoding) - encode = lambda k, v: '%s=%s' % ((quote(k, safe), quote(v, safe))) - else: - encode = lambda k, v: urlencode({k: v}) - for k, list_ in self.lists(): - k = force_bytes(k, self.encoding) - output.extend([encode(k, force_bytes(v, self.encoding)) - for v in list_]) - return '&'.join(output) - - -def parse_cookie(cookie): - if cookie == '': - return {} - if not isinstance(cookie, http_cookies.BaseCookie): - try: - c = SimpleCookie() - c.load(cookie) - except http_cookies.CookieError: - # Invalid cookie - return {} - else: - c = cookie - cookiedict = {} - for key in c.keys(): - cookiedict[key] = c.get(key).value - return cookiedict - -class BadHeaderError(ValueError): - pass - -class HttpResponseBase(object): - """ - An HTTP response base class with dictionary-accessed headers. - - This class doesn't handle content. It should not be used directly. - Use the HttpResponse and StreamingHttpResponse subclasses instead. - """ - - status_code = 200 - - def __init__(self, content_type=None, status=None, mimetype=None): - # _headers is a mapping of the lower-case name to the original case of - # the header (required for working with legacy systems) and the header - # value. Both the name of the header and its value are ASCII strings. - self._headers = {} - self._charset = settings.DEFAULT_CHARSET - self._closable_objects = [] - if mimetype: - warnings.warn("Using mimetype keyword argument is deprecated, use" - " content_type instead", PendingDeprecationWarning) - content_type = mimetype - if not content_type: - content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, - self._charset) - self.cookies = SimpleCookie() - if status: - self.status_code = status - - self['Content-Type'] = content_type - - def serialize_headers(self): - """HTTP headers as a bytestring.""" - headers = [ - ('%s: %s' % (key, value)).encode('us-ascii') - for key, value in self._headers.values() - ] - return b'\r\n'.join(headers) - - if six.PY3: - __bytes__ = serialize_headers - else: - __str__ = serialize_headers - - def _convert_to_charset(self, value, charset, mime_encode=False): - """Converts headers key/value to ascii/latin1 native strings. - - `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and - `value` value can't be represented in the given charset, MIME-encoding - is applied. - """ - if not isinstance(value, (bytes, six.text_type)): - value = str(value) - try: - if six.PY3: - if isinstance(value, str): - # Ensure string is valid in given charset - value.encode(charset) - else: - # Convert bytestring using given charset - value = value.decode(charset) - else: - if isinstance(value, str): - # Ensure string is valid in given charset - value.decode(charset) - else: - # Convert unicode string to given charset - value = value.encode(charset) - except UnicodeError as e: - if mime_encode: - # Wrapping in str() is a workaround for #12422 under Python 2. - value = str(Header(value, 'utf-8').encode()) - else: - e.reason += ', HTTP response headers must be in %s format' % charset - raise - if str('\n') in value or str('\r') in value: - raise BadHeaderError("Header values can't contain newlines (got %r)" % value) - return value - - def __setitem__(self, header, value): - header = self._convert_to_charset(header, 'ascii') - value = self._convert_to_charset(value, 'latin1', mime_encode=True) - self._headers[header.lower()] = (header, value) - - def __delitem__(self, header): - try: - del self._headers[header.lower()] - except KeyError: - pass - - def __getitem__(self, header): - return self._headers[header.lower()][1] - - def __getstate__(self): - # SimpleCookie is not pickeable with pickle.HIGHEST_PROTOCOL, so we - # serialise to a string instead - state = self.__dict__.copy() - state['cookies'] = str(state['cookies']) - return state - - def __setstate__(self, state): - self.__dict__.update(state) - self.cookies = SimpleCookie(self.cookies) - - def has_header(self, header): - """Case-insensitive check for a header.""" - return header.lower() in self._headers - - __contains__ = has_header - - def items(self): - return self._headers.values() - - def get(self, header, alternate=None): - return self._headers.get(header.lower(), (None, alternate))[1] - - def set_cookie(self, key, value='', max_age=None, expires=None, path='/', - domain=None, secure=False, httponly=False): - """ - Sets a cookie. - - ``expires`` can be: - - a string in the correct format, - - a naive ``datetime.datetime`` object in UTC, - - an aware ``datetime.datetime`` object in any time zone. - If it is a ``datetime.datetime`` object then ``max_age`` will be calculated. - - """ - self.cookies[key] = value - if expires is not None: - if isinstance(expires, datetime.datetime): - if timezone.is_aware(expires): - expires = timezone.make_naive(expires, timezone.utc) - delta = expires - expires.utcnow() - # Add one second so the date matches exactly (a fraction of - # time gets lost between converting to a timedelta and - # then the date string). - delta = delta + datetime.timedelta(seconds=1) - # Just set max_age - the max_age logic will set expires. - expires = None - max_age = max(0, delta.days * 86400 + delta.seconds) - else: - self.cookies[key]['expires'] = expires - if max_age is not None: - self.cookies[key]['max-age'] = max_age - # IE requires expires, so set it if hasn't been already. - if not expires: - self.cookies[key]['expires'] = cookie_date(time.time() + - max_age) - if path is not None: - self.cookies[key]['path'] = path - if domain is not None: - self.cookies[key]['domain'] = domain - if secure: - self.cookies[key]['secure'] = True - if httponly: - self.cookies[key]['httponly'] = True - - def set_signed_cookie(self, key, value, salt='', **kwargs): - value = signing.get_cookie_signer(salt=key + salt).sign(value) - return self.set_cookie(key, value, **kwargs) - - def delete_cookie(self, key, path='/', domain=None): - self.set_cookie(key, max_age=0, path=path, domain=domain, - expires='Thu, 01-Jan-1970 00:00:00 GMT') - - # Common methods used by subclasses - - def make_bytes(self, value): - """Turn a value into a bytestring encoded in the output charset.""" - # For backwards compatibility, this method supports values that are - # unlikely to occur in real applications. It has grown complex and - # should be refactored. It also overlaps __next__. See #18796. - if self.has_header('Content-Encoding'): - if isinstance(value, int): - value = six.text_type(value) - if isinstance(value, six.text_type): - value = value.encode('ascii') - # force conversion to bytes in case chunk is a subclass - return bytes(value) - else: - return force_bytes(value, self._charset) - - # These methods partially implement the file-like object interface. - # See http://docs.python.org/lib/bltin-file-objects.html - - # The WSGI server must call this method upon completion of the request. - # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html - def close(self): - for closable in self._closable_objects: - closable.close() - - def write(self, content): - raise Exception("This %s instance is not writable" % self.__class__.__name__) - - def flush(self): - pass - - def tell(self): - raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) - -class HttpResponse(HttpResponseBase): - """ - An HTTP response class with a string as content. - - This content that can be read, appended to or replaced. - """ - - streaming = False - - def __init__(self, content='', *args, **kwargs): - super(HttpResponse, self).__init__(*args, **kwargs) - # Content is a bytestring. See the `content` property methods. - self.content = content - - def serialize(self): - """Full HTTP message, including headers, as a bytestring.""" - return self.serialize_headers() + b'\r\n\r\n' + self.content - - if six.PY3: - __bytes__ = serialize - else: - __str__ = serialize - - @property - def content(self): - return b''.join(self.make_bytes(e) for e in self._container) - - @content.setter - def content(self, value): - if hasattr(value, '__iter__') and not isinstance(value, (bytes, six.string_types)): - self._container = value - self._base_content_is_iter = True - if hasattr(value, 'close'): - self._closable_objects.append(value) - else: - self._container = [value] - self._base_content_is_iter = False - - def __iter__(self): - self._iterator = iter(self._container) - return self - - def __next__(self): - chunk = next(self._iterator) - if isinstance(chunk, int): - chunk = six.text_type(chunk) - if isinstance(chunk, six.text_type): - chunk = chunk.encode(self._charset) - # force conversion to bytes in case chunk is a subclass - return bytes(chunk) - - next = __next__ # Python 2 compatibility - - def write(self, content): - if self._base_content_is_iter: - raise Exception("This %s instance is not writable" % self.__class__.__name__) - self._container.append(content) - - def tell(self): - if self._base_content_is_iter: - raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) - return sum([len(chunk) for chunk in self]) - -class StreamingHttpResponse(HttpResponseBase): - """ - A streaming HTTP response class with an iterator as content. - - This should only be iterated once, when the response is streamed to the - client. However, it can be appended to or replaced with a new iterator - that wraps the original content (or yields entirely new content). - """ - - streaming = True - - def __init__(self, streaming_content=(), *args, **kwargs): - super(StreamingHttpResponse, self).__init__(*args, **kwargs) - # `streaming_content` should be an iterable of bytestrings. - # See the `streaming_content` property methods. - self.streaming_content = streaming_content - - @property - def content(self): - raise AttributeError("This %s instance has no `content` attribute. " - "Use `streaming_content` instead." % self.__class__.__name__) - - @property - def streaming_content(self): - return self._iterator - - @streaming_content.setter - def streaming_content(self, value): - # Ensure we can never iterate on "value" more than once. - self._iterator = iter(value) - if hasattr(value, 'close'): - self._closable_objects.append(value) - - def __iter__(self): - return self - - def __next__(self): - return self.make_bytes(next(self._iterator)) - - next = __next__ # Python 2 compatibility - -class CompatibleStreamingHttpResponse(StreamingHttpResponse): - """ - This class maintains compatibility with middleware that doesn't know how - to handle the content of a streaming response by exposing a `content` - attribute that will consume and cache the content iterator when accessed. - - These responses will stream only if no middleware attempts to access the - `content` attribute. Otherwise, they will behave like a regular response, - and raise a `PendingDeprecationWarning`. - """ - @property - def content(self): - warnings.warn( - 'Accessing the `content` attribute on a streaming response is ' - 'deprecated. Use the `streaming_content` attribute instead.', - PendingDeprecationWarning) - content = b''.join(self) - self.streaming_content = [content] - return content - - @content.setter - def content(self, content): - warnings.warn( - 'Accessing the `content` attribute on a streaming response is ' - 'deprecated. Use the `streaming_content` attribute instead.', - PendingDeprecationWarning) - self.streaming_content = [content] - -class HttpResponseRedirectBase(HttpResponse): - allowed_schemes = ['http', 'https', 'ftp'] - - def __init__(self, redirect_to, *args, **kwargs): - parsed = urlparse(redirect_to) - if parsed.scheme and parsed.scheme not in self.allowed_schemes: - raise SuspiciousOperation("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) - super(HttpResponseRedirectBase, self).__init__(*args, **kwargs) - self['Location'] = iri_to_uri(redirect_to) - -class HttpResponseRedirect(HttpResponseRedirectBase): - status_code = 302 - -class HttpResponsePermanentRedirect(HttpResponseRedirectBase): - status_code = 301 - -class HttpResponseNotModified(HttpResponse): - status_code = 304 - - def __init__(self, *args, **kwargs): - super(HttpResponseNotModified, self).__init__(*args, **kwargs) - del self['content-type'] - - @HttpResponse.content.setter - def content(self, value): - if value: - raise AttributeError("You cannot set content to a 304 (Not Modified) response") - self._container = [] - -class HttpResponseBadRequest(HttpResponse): - status_code = 400 - -class HttpResponseNotFound(HttpResponse): - status_code = 404 - -class HttpResponseForbidden(HttpResponse): - status_code = 403 - -class HttpResponseNotAllowed(HttpResponse): - status_code = 405 - - def __init__(self, permitted_methods, *args, **kwargs): - super(HttpResponseNotAllowed, self).__init__(*args, **kwargs) - self['Allow'] = ', '.join(permitted_methods) - -class HttpResponseGone(HttpResponse): - status_code = 410 - -class HttpResponseServerError(HttpResponse): - status_code = 500 - -# A backwards compatible alias for HttpRequest.get_host. -def get_host(request): - return request.get_host() - -# It's neither necessary nor appropriate to use -# django.utils.encoding.smart_text for parsing URLs and form inputs. Thus, -# this slightly more restricted function, used by QueryDict. -def bytes_to_text(s, encoding): - """ - Converts basestring objects to unicode, using the given encoding. Illegally - encoded input characters are replaced with Unicode "unknown" codepoint - (\ufffd). - - Returns any non-basestring objects without change. - """ - if isinstance(s, bytes): - return six.text_type(s, encoding, 'replace') - else: - return s - +from django.http.cookie import SimpleCookie, parse_cookie +from django.http.request import (HttpRequest, QueryDict, UnreadablePostError, + build_request_repr) +from django.http.response import (HttpResponse, StreamingHttpResponse, + CompatibleStreamingHttpResponse, HttpResponsePermanentRedirect, + HttpResponseRedirect, HttpResponseNotModified, HttpResponseBadRequest, + HttpResponseForbidden, HttpResponseNotFound, HttpResponseNotAllowed, + HttpResponseGone, HttpResponseServerError, Http404, BadHeaderError) +from django.http.utils import (fix_location_header, conditional_content_removal, + fix_IE_for_attach, fix_IE_for_vary) diff --git a/django/http/cookie.py b/django/http/cookie.py new file mode 100644 index 0000000000000..78adb09ce81a0 --- /dev/null +++ b/django/http/cookie.py @@ -0,0 +1,83 @@ +from __future__ import absolute_import, unicode_literals + +from django.utils.encoding import force_str +from django.utils.six.moves import http_cookies + + +# Some versions of Python 2.7 and later won't need this encoding bug fix: +_cookie_encodes_correctly = http_cookies.SimpleCookie().value_encode(';') == (';', '"\\073"') +# See ticket #13007, http://bugs.python.org/issue2193 and http://trac.edgewall.org/ticket/2256 +_tc = http_cookies.SimpleCookie() +try: + _tc.load(str('foo:bar=1')) + _cookie_allows_colon_in_names = True +except http_cookies.CookieError: + _cookie_allows_colon_in_names = False + +if _cookie_encodes_correctly and _cookie_allows_colon_in_names: + SimpleCookie = http_cookies.SimpleCookie +else: + Morsel = http_cookies.Morsel + + class SimpleCookie(http_cookies.SimpleCookie): + if not _cookie_encodes_correctly: + def value_encode(self, val): + # Some browsers do not support quoted-string from RFC 2109, + # including some versions of Safari and Internet Explorer. + # These browsers split on ';', and some versions of Safari + # are known to split on ', '. Therefore, we encode ';' and ',' + + # SimpleCookie already does the hard work of encoding and decoding. + # It uses octal sequences like '\\012' for newline etc. + # and non-ASCII chars. We just make use of this mechanism, to + # avoid introducing two encoding schemes which would be confusing + # and especially awkward for javascript. + + # NB, contrary to Python docs, value_encode returns a tuple containing + # (real val, encoded_val) + val, encoded = super(SimpleCookie, self).value_encode(val) + + encoded = encoded.replace(";", "\\073").replace(",","\\054") + # If encoded now contains any quoted chars, we need double quotes + # around the whole string. + if "\\" in encoded and not encoded.startswith('"'): + encoded = '"' + encoded + '"' + + return val, encoded + + if not _cookie_allows_colon_in_names: + def load(self, rawdata): + self.bad_cookies = set() + super(SimpleCookie, self).load(force_str(rawdata)) + for key in self.bad_cookies: + del self[key] + + # override private __set() method: + # (needed for using our Morsel, and for laxness with CookieError + def _BaseCookie__set(self, key, real_value, coded_value): + key = force_str(key) + try: + M = self.get(key, Morsel()) + M.set(key, real_value, coded_value) + dict.__setitem__(self, key, M) + except http_cookies.CookieError: + self.bad_cookies.add(key) + dict.__setitem__(self, key, http_cookies.Morsel()) + + +def parse_cookie(cookie): + if cookie == '': + return {} + if not isinstance(cookie, http_cookies.BaseCookie): + try: + c = SimpleCookie() + c.load(cookie) + except http_cookies.CookieError: + # Invalid cookie + return {} + else: + c = cookie + cookiedict = {} + for key in c.keys(): + cookiedict[key] = c.get(key).value + return cookiedict diff --git a/django/http/request.py b/django/http/request.py new file mode 100644 index 0000000000000..96c7606c86d94 --- /dev/null +++ b/django/http/request.py @@ -0,0 +1,453 @@ +from __future__ import absolute_import, unicode_literals + +import copy +import os +import re +import sys +import warnings +from io import BytesIO +from pprint import pformat +try: + from urllib.parse import parse_qsl, urlencode, quote, urljoin +except ImportError: + from urllib import urlencode, quote + from urlparse import parse_qsl, urljoin + +from django.conf import settings +from django.core import signing +from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured +from django.core.files import uploadhandler +from django.http.multipartparser import MultiPartParser +from django.utils import six +from django.utils.datastructures import MultiValueDict, ImmutableList +from django.utils.encoding import force_bytes, force_text, force_str, iri_to_uri + + +RAISE_ERROR = object() +absolute_http_url_re = re.compile(r"^https?://", re.I) + + +class UnreadablePostError(IOError): + pass + + +class HttpRequest(object): + """A basic HTTP request.""" + + # The encoding used in GET/POST dicts. None means use default setting. + _encoding = None + _upload_handlers = [] + + def __init__(self): + self.GET, self.POST, self.COOKIES, self.META, self.FILES = {}, {}, {}, {}, {} + self.path = '' + self.path_info = '' + self.method = None + self._post_parse_error = False + + def __repr__(self): + return build_request_repr(self) + + def get_host(self): + """Returns the HTTP host using the environment or request headers.""" + # We try three options, in order of decreasing preference. + if settings.USE_X_FORWARDED_HOST and ( + 'HTTP_X_FORWARDED_HOST' in self.META): + host = self.META['HTTP_X_FORWARDED_HOST'] + elif 'HTTP_HOST' in self.META: + host = self.META['HTTP_HOST'] + else: + # Reconstruct the host using the algorithm from PEP 333. + host = self.META['SERVER_NAME'] + server_port = str(self.META['SERVER_PORT']) + if server_port != ('443' if self.is_secure() else '80'): + host = '%s:%s' % (host, server_port) + + # Disallow potentially poisoned hostnames. + if set(';/?@&=+$,').intersection(host): + raise SuspiciousOperation('Invalid HTTP_HOST header: %s' % host) + + return host + + def get_full_path(self): + # RFC 3986 requires query string arguments to be in the ASCII range. + # Rather than crash if this doesn't happen, we encode defensively. + return '%s%s' % (self.path, ('?' + iri_to_uri(self.META.get('QUERY_STRING', ''))) if self.META.get('QUERY_STRING', '') else '') + + def get_signed_cookie(self, key, default=RAISE_ERROR, salt='', max_age=None): + """ + Attempts to return a signed cookie. If the signature fails or the + cookie has expired, raises an exception... unless you provide the + default argument in which case that value will be returned instead. + """ + try: + cookie_value = self.COOKIES[key] + except KeyError: + if default is not RAISE_ERROR: + return default + else: + raise + try: + value = signing.get_cookie_signer(salt=key + salt).unsign( + cookie_value, max_age=max_age) + except signing.BadSignature: + if default is not RAISE_ERROR: + return default + else: + raise + return value + + def build_absolute_uri(self, location=None): + """ + Builds an absolute URI from the location and the variables available in + this request. If no location is specified, the absolute URI is built on + ``request.get_full_path()``. + """ + if not location: + location = self.get_full_path() + if not absolute_http_url_re.match(location): + current_uri = '%s://%s%s' % ('https' if self.is_secure() else 'http', + self.get_host(), self.path) + location = urljoin(current_uri, location) + return iri_to_uri(location) + + def _is_secure(self): + return os.environ.get("HTTPS") == "on" + + def is_secure(self): + # First, check the SECURE_PROXY_SSL_HEADER setting. + if settings.SECURE_PROXY_SSL_HEADER: + try: + header, value = settings.SECURE_PROXY_SSL_HEADER + except ValueError: + raise ImproperlyConfigured('The SECURE_PROXY_SSL_HEADER setting must be a tuple containing two values.') + if self.META.get(header, None) == value: + return True + + # Failing that, fall back to _is_secure(), which is a hook for + # subclasses to implement. + return self._is_secure() + + def is_ajax(self): + return self.META.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest' + + @property + def encoding(self): + return self._encoding + + @encoding.setter + def encoding(self, val): + """ + Sets the encoding used for GET/POST accesses. If the GET or POST + dictionary has already been created, it is removed and recreated on the + next access (so that it is decoded correctly). + """ + self._encoding = val + if hasattr(self, '_get'): + del self._get + if hasattr(self, '_post'): + del self._post + + def _initialize_handlers(self): + self._upload_handlers = [uploadhandler.load_handler(handler, self) + for handler in settings.FILE_UPLOAD_HANDLERS] + + @property + def upload_handlers(self): + if not self._upload_handlers: + # If there are no upload handlers defined, initialize them from settings. + self._initialize_handlers() + return self._upload_handlers + + @upload_handlers.setter + def upload_handlers(self, upload_handlers): + if hasattr(self, '_files'): + raise AttributeError("You cannot set the upload handlers after the upload has been processed.") + self._upload_handlers = upload_handlers + + def parse_file_upload(self, META, post_data): + """Returns a tuple of (POST QueryDict, FILES MultiValueDict).""" + self.upload_handlers = ImmutableList( + self.upload_handlers, + warning="You cannot alter upload handlers after the upload has been processed." + ) + parser = MultiPartParser(META, post_data, self.upload_handlers, self.encoding) + return parser.parse() + + @property + def body(self): + if not hasattr(self, '_body'): + if self._read_started: + raise Exception("You cannot access body after reading from request's data stream") + try: + self._body = self.read() + except IOError as e: + six.reraise(UnreadablePostError, UnreadablePostError(*e.args), sys.exc_info()[2]) + self._stream = BytesIO(self._body) + return self._body + + @property + def raw_post_data(self): + warnings.warn('HttpRequest.raw_post_data has been deprecated. Use HttpRequest.body instead.', DeprecationWarning) + return self.body + + def _mark_post_parse_error(self): + self._post = QueryDict('') + self._files = MultiValueDict() + self._post_parse_error = True + + def _load_post_and_files(self): + """Populate self._post and self._files if the content-type is a form type""" + if self.method != 'POST': + self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict() + return + if self._read_started and not hasattr(self, '_body'): + self._mark_post_parse_error() + return + + if self.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'): + if hasattr(self, '_body'): + # Use already read data + data = BytesIO(self._body) + else: + data = self + try: + self._post, self._files = self.parse_file_upload(self.META, data) + except: + # An error occured while parsing POST data. Since when + # formatting the error the request handler might access + # self.POST, set self._post and self._file to prevent + # attempts to parse POST data again. + # Mark that an error occured. This allows self.__repr__ to + # be explicit about it instead of simply representing an + # empty POST + self._mark_post_parse_error() + raise + elif self.META.get('CONTENT_TYPE', '').startswith('application/x-www-form-urlencoded'): + self._post, self._files = QueryDict(self.body, encoding=self._encoding), MultiValueDict() + else: + self._post, self._files = QueryDict('', encoding=self._encoding), MultiValueDict() + + ## File-like and iterator interface. + ## + ## Expects self._stream to be set to an appropriate source of bytes by + ## a corresponding request subclass (e.g. WSGIRequest). + ## Also when request data has already been read by request.POST or + ## request.body, self._stream points to a BytesIO instance + ## containing that data. + + def read(self, *args, **kwargs): + self._read_started = True + return self._stream.read(*args, **kwargs) + + def readline(self, *args, **kwargs): + self._read_started = True + return self._stream.readline(*args, **kwargs) + + def xreadlines(self): + while True: + buf = self.readline() + if not buf: + break + yield buf + + __iter__ = xreadlines + + def readlines(self): + return list(iter(self)) + + +class QueryDict(MultiValueDict): + """ + A specialized MultiValueDict that takes a query string when initialized. + This is immutable unless you create a copy of it. + + Values retrieved from this class are converted from the given encoding + (DEFAULT_CHARSET by default) to unicode. + """ + # These are both reset in __init__, but is specified here at the class + # level so that unpickling will have valid values + _mutable = True + _encoding = None + + def __init__(self, query_string, mutable=False, encoding=None): + super(QueryDict, self).__init__() + if not encoding: + encoding = settings.DEFAULT_CHARSET + self.encoding = encoding + if six.PY3: + for key, value in parse_qsl(query_string or '', + keep_blank_values=True, + encoding=encoding): + self.appendlist(key, value) + else: + for key, value in parse_qsl(query_string or '', + keep_blank_values=True): + self.appendlist(force_text(key, encoding, errors='replace'), + force_text(value, encoding, errors='replace')) + self._mutable = mutable + + @property + def encoding(self): + if self._encoding is None: + self._encoding = settings.DEFAULT_CHARSET + return self._encoding + + @encoding.setter + def encoding(self, value): + self._encoding = value + + def _assert_mutable(self): + if not self._mutable: + raise AttributeError("This QueryDict instance is immutable") + + def __setitem__(self, key, value): + self._assert_mutable() + key = bytes_to_text(key, self.encoding) + value = bytes_to_text(value, self.encoding) + super(QueryDict, self).__setitem__(key, value) + + def __delitem__(self, key): + self._assert_mutable() + super(QueryDict, self).__delitem__(key) + + def __copy__(self): + result = self.__class__('', mutable=True, encoding=self.encoding) + for key, value in six.iterlists(self): + result.setlist(key, value) + return result + + def __deepcopy__(self, memo): + result = self.__class__('', mutable=True, encoding=self.encoding) + memo[id(self)] = result + for key, value in six.iterlists(self): + result.setlist(copy.deepcopy(key, memo), copy.deepcopy(value, memo)) + return result + + def setlist(self, key, list_): + self._assert_mutable() + key = bytes_to_text(key, self.encoding) + list_ = [bytes_to_text(elt, self.encoding) for elt in list_] + super(QueryDict, self).setlist(key, list_) + + def setlistdefault(self, key, default_list=None): + self._assert_mutable() + return super(QueryDict, self).setlistdefault(key, default_list) + + def appendlist(self, key, value): + self._assert_mutable() + key = bytes_to_text(key, self.encoding) + value = bytes_to_text(value, self.encoding) + super(QueryDict, self).appendlist(key, value) + + def pop(self, key, *args): + self._assert_mutable() + return super(QueryDict, self).pop(key, *args) + + def popitem(self): + self._assert_mutable() + return super(QueryDict, self).popitem() + + def clear(self): + self._assert_mutable() + super(QueryDict, self).clear() + + def setdefault(self, key, default=None): + self._assert_mutable() + key = bytes_to_text(key, self.encoding) + default = bytes_to_text(default, self.encoding) + return super(QueryDict, self).setdefault(key, default) + + def copy(self): + """Returns a mutable copy of this object.""" + return self.__deepcopy__({}) + + def urlencode(self, safe=None): + """ + Returns an encoded string of all query string arguments. + + :arg safe: Used to specify characters which do not require quoting, for + example:: + + >>> q = QueryDict('', mutable=True) + >>> q['next'] = '/a&b/' + >>> q.urlencode() + 'next=%2Fa%26b%2F' + >>> q.urlencode(safe='/') + 'next=/a%26b/' + + """ + output = [] + if safe: + safe = force_bytes(safe, self.encoding) + encode = lambda k, v: '%s=%s' % ((quote(k, safe), quote(v, safe))) + else: + encode = lambda k, v: urlencode({k: v}) + for k, list_ in self.lists(): + k = force_bytes(k, self.encoding) + output.extend([encode(k, force_bytes(v, self.encoding)) + for v in list_]) + return '&'.join(output) + + +def build_request_repr(request, path_override=None, GET_override=None, + POST_override=None, COOKIES_override=None, + META_override=None): + """ + Builds and returns the request's representation string. The request's + attributes may be overridden by pre-processed values. + """ + # Since this is called as part of error handling, we need to be very + # robust against potentially malformed input. + try: + get = (pformat(GET_override) + if GET_override is not None + else pformat(request.GET)) + except Exception: + get = '' + if request._post_parse_error: + post = '' + else: + try: + post = (pformat(POST_override) + if POST_override is not None + else pformat(request.POST)) + except Exception: + post = '' + try: + cookies = (pformat(COOKIES_override) + if COOKIES_override is not None + else pformat(request.COOKIES)) + except Exception: + cookies = '' + try: + meta = (pformat(META_override) + if META_override is not None + else pformat(request.META)) + except Exception: + meta = '' + path = path_override if path_override is not None else request.path + return force_str('<%s\npath:%s,\nGET:%s,\nPOST:%s,\nCOOKIES:%s,\nMETA:%s>' % + (request.__class__.__name__, + path, + six.text_type(get), + six.text_type(post), + six.text_type(cookies), + six.text_type(meta))) + + +# It's neither necessary nor appropriate to use +# django.utils.encoding.smart_text for parsing URLs and form inputs. Thus, +# this slightly more restricted function, used by QueryDict. +def bytes_to_text(s, encoding): + """ + Converts basestring objects to unicode, using the given encoding. Illegally + encoded input characters are replaced with Unicode "unknown" codepoint + (\ufffd). + + Returns any non-basestring objects without change. + """ + if isinstance(s, bytes): + return six.text_type(s, encoding, 'replace') + else: + return s diff --git a/django/http/response.py b/django/http/response.py new file mode 100644 index 0000000000000..4a5c479419334 --- /dev/null +++ b/django/http/response.py @@ -0,0 +1,423 @@ +from __future__ import absolute_import, unicode_literals + +import datetime +import time +import warnings +from email.header import Header +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + +from django.conf import settings +from django.core import signing +from django.core.exceptions import SuspiciousOperation +from django.http.cookie import SimpleCookie +from django.utils import six, timezone +from django.utils.encoding import force_bytes, iri_to_uri +from django.utils.http import cookie_date + + +class BadHeaderError(ValueError): + pass + + +class HttpResponseBase(object): + """ + An HTTP response base class with dictionary-accessed headers. + + This class doesn't handle content. It should not be used directly. + Use the HttpResponse and StreamingHttpResponse subclasses instead. + """ + + status_code = 200 + + def __init__(self, content_type=None, status=None, mimetype=None): + # _headers is a mapping of the lower-case name to the original case of + # the header (required for working with legacy systems) and the header + # value. Both the name of the header and its value are ASCII strings. + self._headers = {} + self._charset = settings.DEFAULT_CHARSET + self._closable_objects = [] + if mimetype: + warnings.warn("Using mimetype keyword argument is deprecated, use" + " content_type instead", PendingDeprecationWarning) + content_type = mimetype + if not content_type: + content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, + self._charset) + self.cookies = SimpleCookie() + if status: + self.status_code = status + + self['Content-Type'] = content_type + + def serialize_headers(self): + """HTTP headers as a bytestring.""" + headers = [ + ('%s: %s' % (key, value)).encode('us-ascii') + for key, value in self._headers.values() + ] + return b'\r\n'.join(headers) + + if six.PY3: + __bytes__ = serialize_headers + else: + __str__ = serialize_headers + + def _convert_to_charset(self, value, charset, mime_encode=False): + """Converts headers key/value to ascii/latin1 native strings. + + `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and + `value` value can't be represented in the given charset, MIME-encoding + is applied. + """ + if not isinstance(value, (bytes, six.text_type)): + value = str(value) + try: + if six.PY3: + if isinstance(value, str): + # Ensure string is valid in given charset + value.encode(charset) + else: + # Convert bytestring using given charset + value = value.decode(charset) + else: + if isinstance(value, str): + # Ensure string is valid in given charset + value.decode(charset) + else: + # Convert unicode string to given charset + value = value.encode(charset) + except UnicodeError as e: + if mime_encode: + # Wrapping in str() is a workaround for #12422 under Python 2. + value = str(Header(value, 'utf-8').encode()) + else: + e.reason += ', HTTP response headers must be in %s format' % charset + raise + if str('\n') in value or str('\r') in value: + raise BadHeaderError("Header values can't contain newlines (got %r)" % value) + return value + + def __setitem__(self, header, value): + header = self._convert_to_charset(header, 'ascii') + value = self._convert_to_charset(value, 'latin1', mime_encode=True) + self._headers[header.lower()] = (header, value) + + def __delitem__(self, header): + try: + del self._headers[header.lower()] + except KeyError: + pass + + def __getitem__(self, header): + return self._headers[header.lower()][1] + + def __getstate__(self): + # SimpleCookie is not pickeable with pickle.HIGHEST_PROTOCOL, so we + # serialise to a string instead + state = self.__dict__.copy() + state['cookies'] = str(state['cookies']) + return state + + def __setstate__(self, state): + self.__dict__.update(state) + self.cookies = SimpleCookie(self.cookies) + + def has_header(self, header): + """Case-insensitive check for a header.""" + return header.lower() in self._headers + + __contains__ = has_header + + def items(self): + return self._headers.values() + + def get(self, header, alternate=None): + return self._headers.get(header.lower(), (None, alternate))[1] + + def set_cookie(self, key, value='', max_age=None, expires=None, path='/', + domain=None, secure=False, httponly=False): + """ + Sets a cookie. + + ``expires`` can be: + - a string in the correct format, + - a naive ``datetime.datetime`` object in UTC, + - an aware ``datetime.datetime`` object in any time zone. + If it is a ``datetime.datetime`` object then ``max_age`` will be calculated. + + """ + self.cookies[key] = value + if expires is not None: + if isinstance(expires, datetime.datetime): + if timezone.is_aware(expires): + expires = timezone.make_naive(expires, timezone.utc) + delta = expires - expires.utcnow() + # Add one second so the date matches exactly (a fraction of + # time gets lost between converting to a timedelta and + # then the date string). + delta = delta + datetime.timedelta(seconds=1) + # Just set max_age - the max_age logic will set expires. + expires = None + max_age = max(0, delta.days * 86400 + delta.seconds) + else: + self.cookies[key]['expires'] = expires + if max_age is not None: + self.cookies[key]['max-age'] = max_age + # IE requires expires, so set it if hasn't been already. + if not expires: + self.cookies[key]['expires'] = cookie_date(time.time() + + max_age) + if path is not None: + self.cookies[key]['path'] = path + if domain is not None: + self.cookies[key]['domain'] = domain + if secure: + self.cookies[key]['secure'] = True + if httponly: + self.cookies[key]['httponly'] = True + + def set_signed_cookie(self, key, value, salt='', **kwargs): + value = signing.get_cookie_signer(salt=key + salt).sign(value) + return self.set_cookie(key, value, **kwargs) + + def delete_cookie(self, key, path='/', domain=None): + self.set_cookie(key, max_age=0, path=path, domain=domain, + expires='Thu, 01-Jan-1970 00:00:00 GMT') + + # Common methods used by subclasses + + def make_bytes(self, value): + """Turn a value into a bytestring encoded in the output charset.""" + # For backwards compatibility, this method supports values that are + # unlikely to occur in real applications. It has grown complex and + # should be refactored. It also overlaps __next__. See #18796. + if self.has_header('Content-Encoding'): + if isinstance(value, int): + value = six.text_type(value) + if isinstance(value, six.text_type): + value = value.encode('ascii') + # force conversion to bytes in case chunk is a subclass + return bytes(value) + else: + return force_bytes(value, self._charset) + + # These methods partially implement the file-like object interface. + # See http://docs.python.org/lib/bltin-file-objects.html + + # The WSGI server must call this method upon completion of the request. + # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html + def close(self): + for closable in self._closable_objects: + closable.close() + + def write(self, content): + raise Exception("This %s instance is not writable" % self.__class__.__name__) + + def flush(self): + pass + + def tell(self): + raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) + + +class HttpResponse(HttpResponseBase): + """ + An HTTP response class with a string as content. + + This content that can be read, appended to or replaced. + """ + + streaming = False + + def __init__(self, content='', *args, **kwargs): + super(HttpResponse, self).__init__(*args, **kwargs) + # Content is a bytestring. See the `content` property methods. + self.content = content + + def serialize(self): + """Full HTTP message, including headers, as a bytestring.""" + return self.serialize_headers() + b'\r\n\r\n' + self.content + + if six.PY3: + __bytes__ = serialize + else: + __str__ = serialize + + @property + def content(self): + return b''.join(self.make_bytes(e) for e in self._container) + + @content.setter + def content(self, value): + if hasattr(value, '__iter__') and not isinstance(value, (bytes, six.string_types)): + self._container = value + self._base_content_is_iter = True + if hasattr(value, 'close'): + self._closable_objects.append(value) + else: + self._container = [value] + self._base_content_is_iter = False + + def __iter__(self): + self._iterator = iter(self._container) + return self + + def __next__(self): + chunk = next(self._iterator) + if isinstance(chunk, int): + chunk = six.text_type(chunk) + if isinstance(chunk, six.text_type): + chunk = chunk.encode(self._charset) + # force conversion to bytes in case chunk is a subclass + return bytes(chunk) + + next = __next__ # Python 2 compatibility + + def write(self, content): + if self._base_content_is_iter: + raise Exception("This %s instance is not writable" % self.__class__.__name__) + self._container.append(content) + + def tell(self): + if self._base_content_is_iter: + raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) + return sum([len(chunk) for chunk in self]) + + +class StreamingHttpResponse(HttpResponseBase): + """ + A streaming HTTP response class with an iterator as content. + + This should only be iterated once, when the response is streamed to the + client. However, it can be appended to or replaced with a new iterator + that wraps the original content (or yields entirely new content). + """ + + streaming = True + + def __init__(self, streaming_content=(), *args, **kwargs): + super(StreamingHttpResponse, self).__init__(*args, **kwargs) + # `streaming_content` should be an iterable of bytestrings. + # See the `streaming_content` property methods. + self.streaming_content = streaming_content + + @property + def content(self): + raise AttributeError("This %s instance has no `content` attribute. " + "Use `streaming_content` instead." % self.__class__.__name__) + + @property + def streaming_content(self): + return self._iterator + + @streaming_content.setter + def streaming_content(self, value): + # Ensure we can never iterate on "value" more than once. + self._iterator = iter(value) + if hasattr(value, 'close'): + self._closable_objects.append(value) + + def __iter__(self): + return self + + def __next__(self): + return self.make_bytes(next(self._iterator)) + + next = __next__ # Python 2 compatibility + + +class CompatibleStreamingHttpResponse(StreamingHttpResponse): + """ + This class maintains compatibility with middleware that doesn't know how + to handle the content of a streaming response by exposing a `content` + attribute that will consume and cache the content iterator when accessed. + + These responses will stream only if no middleware attempts to access the + `content` attribute. Otherwise, they will behave like a regular response, + and raise a `PendingDeprecationWarning`. + """ + @property + def content(self): + warnings.warn( + 'Accessing the `content` attribute on a streaming response is ' + 'deprecated. Use the `streaming_content` attribute instead.', + PendingDeprecationWarning) + content = b''.join(self) + self.streaming_content = [content] + return content + + @content.setter + def content(self, content): + warnings.warn( + 'Accessing the `content` attribute on a streaming response is ' + 'deprecated. Use the `streaming_content` attribute instead.', + PendingDeprecationWarning) + self.streaming_content = [content] + + +class HttpResponseRedirectBase(HttpResponse): + allowed_schemes = ['http', 'https', 'ftp'] + + def __init__(self, redirect_to, *args, **kwargs): + parsed = urlparse(redirect_to) + if parsed.scheme and parsed.scheme not in self.allowed_schemes: + raise SuspiciousOperation("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) + super(HttpResponseRedirectBase, self).__init__(*args, **kwargs) + self['Location'] = iri_to_uri(redirect_to) + + +class HttpResponseRedirect(HttpResponseRedirectBase): + status_code = 302 + + +class HttpResponsePermanentRedirect(HttpResponseRedirectBase): + status_code = 301 + + +class HttpResponseNotModified(HttpResponse): + status_code = 304 + + def __init__(self, *args, **kwargs): + super(HttpResponseNotModified, self).__init__(*args, **kwargs) + del self['content-type'] + + @HttpResponse.content.setter + def content(self, value): + if value: + raise AttributeError("You cannot set content to a 304 (Not Modified) response") + self._container = [] + + +class HttpResponseBadRequest(HttpResponse): + status_code = 400 + + +class HttpResponseNotFound(HttpResponse): + status_code = 404 + + +class HttpResponseForbidden(HttpResponse): + status_code = 403 + + +class HttpResponseNotAllowed(HttpResponse): + status_code = 405 + + def __init__(self, permitted_methods, *args, **kwargs): + super(HttpResponseNotAllowed, self).__init__(*args, **kwargs) + self['Allow'] = ', '.join(permitted_methods) + + +class HttpResponseGone(HttpResponse): + status_code = 410 + + +class HttpResponseServerError(HttpResponse): + status_code = 500 + + +class Http404(Exception): + pass diff --git a/django/http/utils.py b/django/http/utils.py index f7ff477f09a25..fcb3fecb6ca5f 100644 --- a/django/http/utils.py +++ b/django/http/utils.py @@ -8,6 +8,7 @@ # it's a little fiddly to override this behavior, so they should be truly # universally applicable. + def fix_location_header(request, response): """ Ensures that we always use an absolute URI in any location header in the @@ -20,6 +21,7 @@ def fix_location_header(request, response): response['Location'] = request.build_absolute_uri(response['Location']) return response + def conditional_content_removal(request, response): """ Removes the content of responses for HEAD requests, 1xx, 204 and 304 @@ -38,6 +40,7 @@ def conditional_content_removal(request, response): response.content = '' return response + def fix_IE_for_attach(request, response): """ This function will prevent Django from serving a Content-Disposition header @@ -66,6 +69,7 @@ def fix_IE_for_attach(request, response): return response + def fix_IE_for_vary(request, response): """ This function will fix the bug reported at @@ -90,4 +94,3 @@ def fix_IE_for_vary(request, response): pass return response -