Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/http/response.py: 36%
383 statements
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
1import datetime
2import io
3import json
4import mimetypes
5import os
6import re
7import sys
8import time
9import warnings
10from email.header import Header
11from http.client import responses
12from urllib.parse import urlparse
14from asgiref.sync import async_to_sync, sync_to_async
16from django.conf import settings
17from django.core import signals, signing
18from django.core.exceptions import DisallowedRedirect
19from django.core.serializers.json import DjangoJSONEncoder
20from django.http.cookie import SimpleCookie
21from django.utils import timezone
22from django.utils.datastructures import CaseInsensitiveMapping
23from django.utils.encoding import iri_to_uri
24from django.utils.http import content_disposition_header, http_date
25from django.utils.regex_helper import _lazy_re_compile
27_charset_from_content_type_re = _lazy_re_compile(
28 r";\s*charset=(?P<charset>[^\s;]+)", re.I
29)
32class ResponseHeaders(CaseInsensitiveMapping):
33 def __init__(self, data):
34 """
35 Populate the initial data using __setitem__ to ensure values are
36 correctly encoded.
37 """
38 self._store = {}
39 if data:
40 for header, value in self._unpack_items(data):
41 self[header] = value
43 def _convert_to_charset(self, value, charset, mime_encode=False):
44 """
45 Convert headers key/value to ascii/latin-1 native strings.
46 `charset` must be 'ascii' or 'latin-1'. If `mime_encode` is True and
47 `value` can't be represented in the given charset, apply MIME-encoding.
48 """
49 try:
50 if isinstance(value, str):
51 # Ensure string is valid in given charset
52 value.encode(charset)
53 elif isinstance(value, bytes):
54 # Convert bytestring using given charset
55 value = value.decode(charset)
56 else:
57 value = str(value)
58 # Ensure string is valid in given charset.
59 value.encode(charset)
60 if "\n" in value or "\r" in value:
61 raise BadHeaderError(
62 f"Header values can't contain newlines (got {value!r})"
63 )
64 except UnicodeError as e:
65 # Encoding to a string of the specified charset failed, but we
66 # don't know what type that value was, or if it contains newlines,
67 # which we may need to check for before sending it to be
68 # encoded for multiple character sets.
69 if (isinstance(value, bytes) and (b"\n" in value or b"\r" in value)) or (
70 isinstance(value, str) and ("\n" in value or "\r" in value)
71 ):
72 raise BadHeaderError(
73 f"Header values can't contain newlines (got {value!r})"
74 ) from e
75 if mime_encode:
76 value = Header(value, "utf-8", maxlinelen=sys.maxsize).encode()
77 else:
78 e.reason += ", HTTP response headers must be in %s format" % charset
79 raise
80 return value
82 def __delitem__(self, key):
83 self.pop(key)
85 def __setitem__(self, key, value):
86 key = self._convert_to_charset(key, "ascii")
87 value = self._convert_to_charset(value, "latin-1", mime_encode=True)
88 self._store[key.lower()] = (key, value)
90 def pop(self, key, default=None):
91 return self._store.pop(key.lower(), default)
93 def setdefault(self, key, value):
94 if key not in self:
95 self[key] = value
98class BadHeaderError(ValueError):
99 pass
102class HttpResponseBase:
103 """
104 An HTTP response base class with dictionary-accessed headers.
106 This class doesn't handle content. It should not be used directly.
107 Use the HttpResponse and StreamingHttpResponse subclasses instead.
108 """
110 status_code = 200
112 def __init__(
113 self, content_type=None, status=None, reason=None, charset=None, headers=None
114 ):
115 self.headers = ResponseHeaders(headers)
116 self._charset = charset
117 if "Content-Type" not in self.headers:
118 if content_type is None:
119 content_type = f"text/html; charset={self.charset}"
120 self.headers["Content-Type"] = content_type
121 elif content_type:
122 raise ValueError(
123 "'headers' must not contain 'Content-Type' when the "
124 "'content_type' parameter is provided."
125 )
126 self._resource_closers = []
127 # This parameter is set by the handler. It's necessary to preserve the
128 # historical behavior of request_finished.
129 self._handler_class = None
130 self.cookies = SimpleCookie()
131 self.closed = False
132 if status is not None:
133 try:
134 self.status_code = int(status)
135 except (ValueError, TypeError):
136 raise TypeError("HTTP status code must be an integer.")
138 if not 100 <= self.status_code <= 599:
139 raise ValueError("HTTP status code must be an integer from 100 to 599.")
140 self._reason_phrase = reason
142 @property
143 def reason_phrase(self):
144 if self._reason_phrase is not None:
145 return self._reason_phrase
146 # Leave self._reason_phrase unset in order to use the default
147 # reason phrase for status code.
148 return responses.get(self.status_code, "Unknown Status Code")
150 @reason_phrase.setter
151 def reason_phrase(self, value):
152 self._reason_phrase = value
154 @property
155 def charset(self):
156 if self._charset is not None:
157 return self._charset
158 # The Content-Type header may not yet be set, because the charset is
159 # being inserted *into* it.
160 if content_type := self.headers.get("Content-Type"):
161 if matched := _charset_from_content_type_re.search(content_type):
162 # Extract the charset and strip its double quotes.
163 # Note that having parsed it from the Content-Type, we don't
164 # store it back into the _charset for later intentionally, to
165 # allow for the Content-Type to be switched again later.
166 return matched["charset"].replace('"', "")
167 return settings.DEFAULT_CHARSET
169 @charset.setter
170 def charset(self, value):
171 self._charset = value
173 def serialize_headers(self):
174 """HTTP headers as a bytestring."""
175 return b"\r\n".join(
176 [
177 key.encode("ascii") + b": " + value.encode("latin-1")
178 for key, value in self.headers.items()
179 ]
180 )
182 __bytes__ = serialize_headers
184 @property
185 def _content_type_for_repr(self):
186 return (
187 ', "%s"' % self.headers["Content-Type"]
188 if "Content-Type" in self.headers
189 else ""
190 )
192 def __setitem__(self, header, value):
193 self.headers[header] = value
195 def __delitem__(self, header):
196 del self.headers[header]
198 def __getitem__(self, header):
199 return self.headers[header]
201 def has_header(self, header):
202 """Case-insensitive check for a header."""
203 return header in self.headers
205 __contains__ = has_header
207 def items(self):
208 return self.headers.items()
210 def get(self, header, alternate=None):
211 return self.headers.get(header, alternate)
213 def set_cookie(
214 self,
215 key,
216 value="",
217 max_age=None,
218 expires=None,
219 path="/",
220 domain=None,
221 secure=False,
222 httponly=False,
223 samesite=None,
224 ):
225 """
226 Set a cookie.
228 ``expires`` can be:
229 - a string in the correct format,
230 - a naive ``datetime.datetime`` object in UTC,
231 - an aware ``datetime.datetime`` object in any time zone.
232 If it is a ``datetime.datetime`` object then calculate ``max_age``.
234 ``max_age`` can be:
235 - int/float specifying seconds,
236 - ``datetime.timedelta`` object.
237 """
238 self.cookies[key] = value
239 if expires is not None:
240 if isinstance(expires, datetime.datetime):
241 if timezone.is_naive(expires):
242 expires = timezone.make_aware(expires, datetime.timezone.utc)
243 delta = expires - datetime.datetime.now(tz=datetime.timezone.utc)
244 # Add one second so the date matches exactly (a fraction of
245 # time gets lost between converting to a timedelta and
246 # then the date string).
247 delta += datetime.timedelta(seconds=1)
248 # Just set max_age - the max_age logic will set expires.
249 expires = None
250 if max_age is not None:
251 raise ValueError("'expires' and 'max_age' can't be used together.")
252 max_age = max(0, delta.days * 86400 + delta.seconds)
253 else:
254 self.cookies[key]["expires"] = expires
255 else:
256 self.cookies[key]["expires"] = ""
257 if max_age is not None:
258 if isinstance(max_age, datetime.timedelta):
259 max_age = max_age.total_seconds()
260 self.cookies[key]["max-age"] = int(max_age)
261 # IE requires expires, so set it if hasn't been already.
262 if not expires:
263 self.cookies[key]["expires"] = http_date(time.time() + max_age)
264 if path is not None:
265 self.cookies[key]["path"] = path
266 if domain is not None:
267 self.cookies[key]["domain"] = domain
268 if secure:
269 self.cookies[key]["secure"] = True
270 if httponly:
271 self.cookies[key]["httponly"] = True
272 if samesite:
273 if samesite.lower() not in ("lax", "none", "strict"):
274 raise ValueError('samesite must be "lax", "none", or "strict".')
275 self.cookies[key]["samesite"] = samesite
277 def setdefault(self, key, value):
278 """Set a header unless it has already been set."""
279 self.headers.setdefault(key, value)
281 def set_signed_cookie(self, key, value, salt="", **kwargs):
282 value = signing.get_cookie_signer(salt=key + salt).sign(value)
283 return self.set_cookie(key, value, **kwargs)
285 def delete_cookie(self, key, path="/", domain=None, samesite=None):
286 # Browsers can ignore the Set-Cookie header if the cookie doesn't use
287 # the secure flag and:
288 # - the cookie name starts with "__Host-" or "__Secure-", or
289 # - the samesite is "none".
290 secure = key.startswith(("__Secure-", "__Host-")) or (
291 samesite and samesite.lower() == "none"
292 )
293 self.set_cookie(
294 key,
295 max_age=0,
296 path=path,
297 domain=domain,
298 secure=secure,
299 expires="Thu, 01 Jan 1970 00:00:00 GMT",
300 samesite=samesite,
301 )
303 # Common methods used by subclasses
305 def make_bytes(self, value):
306 """Turn a value into a bytestring encoded in the output charset."""
307 # Per PEP 3333, this response body must be bytes. To avoid returning
308 # an instance of a subclass, this function returns `bytes(value)`.
309 # This doesn't make a copy when `value` already contains bytes.
311 # Handle string types -- we can't rely on force_bytes here because:
312 # - Python attempts str conversion first
313 # - when self._charset != 'utf-8' it re-encodes the content
314 if isinstance(value, (bytes, memoryview)):
315 return bytes(value)
316 if isinstance(value, str):
317 return bytes(value.encode(self.charset))
318 # Handle non-string types.
319 return str(value).encode(self.charset)
321 # These methods partially implement the file-like object interface.
322 # See https://docs.python.org/library/io.html#io.IOBase
324 # The WSGI server must call this method upon completion of the request.
325 # See http://blog.dscpl.com.au/2012/10/obligations-for-calling-close-on.html
326 def close(self):
327 for closer in self._resource_closers:
328 try:
329 closer()
330 except Exception:
331 pass
332 # Free resources that were still referenced.
333 self._resource_closers.clear()
334 self.closed = True
335 signals.request_finished.send(sender=self._handler_class)
337 def write(self, content):
338 raise OSError("This %s instance is not writable" % self.__class__.__name__)
340 def flush(self):
341 pass
343 def tell(self):
344 raise OSError(
345 "This %s instance cannot tell its position" % self.__class__.__name__
346 )
348 # These methods partially implement a stream-like object interface.
349 # See https://docs.python.org/library/io.html#io.IOBase
351 def readable(self):
352 return False
354 def seekable(self):
355 return False
357 def writable(self):
358 return False
360 def writelines(self, lines):
361 raise OSError("This %s instance is not writable" % self.__class__.__name__)
364class HttpResponse(HttpResponseBase):
365 """
366 An HTTP response class with a string as content.
368 This content can be read, appended to, or replaced.
369 """
371 streaming = False
372 non_picklable_attrs = frozenset(
373 [
374 "resolver_match",
375 # Non-picklable attributes added by test clients.
376 "client",
377 "context",
378 "json",
379 "templates",
380 ]
381 )
383 def __init__(self, content=b"", *args, **kwargs):
384 super().__init__(*args, **kwargs)
385 # Content is a bytestring. See the `content` property methods.
386 self.content = content
388 def __getstate__(self):
389 obj_dict = self.__dict__.copy()
390 for attr in self.non_picklable_attrs:
391 if attr in obj_dict:
392 del obj_dict[attr]
393 return obj_dict
395 def __repr__(self):
396 return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
397 "cls": self.__class__.__name__,
398 "status_code": self.status_code,
399 "content_type": self._content_type_for_repr,
400 }
402 def serialize(self):
403 """Full HTTP message, including headers, as a bytestring."""
404 return self.serialize_headers() + b"\r\n\r\n" + self.content
406 __bytes__ = serialize
408 @property
409 def content(self):
410 return b"".join(self._container)
412 @content.setter
413 def content(self, value):
414 # Consume iterators upon assignment to allow repeated iteration.
415 if hasattr(value, "__iter__") and not isinstance(
416 value, (bytes, memoryview, str)
417 ):
418 content = b"".join(self.make_bytes(chunk) for chunk in value)
419 if hasattr(value, "close"):
420 try:
421 value.close()
422 except Exception:
423 pass
424 else:
425 content = self.make_bytes(value)
426 # Create a list of properly encoded bytestrings to support write().
427 self._container = [content]
429 def __iter__(self):
430 return iter(self._container)
432 def write(self, content):
433 self._container.append(self.make_bytes(content))
435 def tell(self):
436 return len(self.content)
438 def getvalue(self):
439 return self.content
441 def writable(self):
442 return True
444 def writelines(self, lines):
445 for line in lines:
446 self.write(line)
449class StreamingHttpResponse(HttpResponseBase):
450 """
451 A streaming HTTP response class with an iterator as content.
453 This should only be iterated once, when the response is streamed to the
454 client. However, it can be appended to or replaced with a new iterator
455 that wraps the original content (or yields entirely new content).
456 """
458 streaming = True
460 def __init__(self, streaming_content=(), *args, **kwargs):
461 super().__init__(*args, **kwargs)
462 # `streaming_content` should be an iterable of bytestrings.
463 # See the `streaming_content` property methods.
464 self.streaming_content = streaming_content
466 def __repr__(self):
467 return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % {
468 "cls": self.__class__.__qualname__,
469 "status_code": self.status_code,
470 "content_type": self._content_type_for_repr,
471 }
473 @property
474 def content(self):
475 raise AttributeError(
476 "This %s instance has no `content` attribute. Use "
477 "`streaming_content` instead." % self.__class__.__name__
478 )
480 @property
481 def streaming_content(self):
482 if self.is_async:
483 # pull to lexical scope to capture fixed reference in case
484 # streaming_content is set again later.
485 _iterator = self._iterator
487 async def awrapper():
488 async for part in _iterator:
489 yield self.make_bytes(part)
491 return awrapper()
492 else:
493 return map(self.make_bytes, self._iterator)
495 @streaming_content.setter
496 def streaming_content(self, value):
497 self._set_streaming_content(value)
499 def _set_streaming_content(self, value):
500 # Ensure we can never iterate on "value" more than once.
501 try:
502 self._iterator = iter(value)
503 self.is_async = False
504 except TypeError:
505 self._iterator = value.__aiter__()
506 self.is_async = True
507 if hasattr(value, "close"):
508 self._resource_closers.append(value.close)
510 def __iter__(self):
511 try:
512 return iter(self.streaming_content)
513 except TypeError:
514 warnings.warn(
515 "StreamingHttpResponse must consume asynchronous iterators in order to "
516 "serve them synchronously. Use a synchronous iterator instead.",
517 Warning,
518 )
520 # async iterator. Consume in async_to_sync and map back.
521 async def to_list(_iterator):
522 as_list = []
523 async for chunk in _iterator:
524 as_list.append(chunk)
525 return as_list
527 return map(self.make_bytes, iter(async_to_sync(to_list)(self._iterator)))
529 async def __aiter__(self):
530 try:
531 async for part in self.streaming_content:
532 yield part
533 except TypeError:
534 warnings.warn(
535 "StreamingHttpResponse must consume synchronous iterators in order to "
536 "serve them asynchronously. Use an asynchronous iterator instead.",
537 Warning,
538 )
539 # sync iterator. Consume via sync_to_async and yield via async
540 # generator.
541 for part in await sync_to_async(list)(self.streaming_content):
542 yield part
544 def getvalue(self):
545 return b"".join(self.streaming_content)
548class FileResponse(StreamingHttpResponse):
549 """
550 A streaming HTTP response class optimized for files.
551 """
553 block_size = 4096
555 def __init__(self, *args, as_attachment=False, filename="", **kwargs):
556 self.as_attachment = as_attachment
557 self.filename = filename
558 self._no_explicit_content_type = (
559 "content_type" not in kwargs or kwargs["content_type"] is None
560 )
561 super().__init__(*args, **kwargs)
563 def _set_streaming_content(self, value):
564 if not hasattr(value, "read"):
565 self.file_to_stream = None
566 return super()._set_streaming_content(value)
568 self.file_to_stream = filelike = value
569 if hasattr(filelike, "close"):
570 self._resource_closers.append(filelike.close)
571 value = iter(lambda: filelike.read(self.block_size), b"")
572 self.set_headers(filelike)
573 super()._set_streaming_content(value)
575 def set_headers(self, filelike):
576 """
577 Set some common response headers (Content-Length, Content-Type, and
578 Content-Disposition) based on the `filelike` response content.
579 """
580 filename = getattr(filelike, "name", "")
581 filename = filename if isinstance(filename, str) else ""
582 seekable = hasattr(filelike, "seek") and (
583 not hasattr(filelike, "seekable") or filelike.seekable()
584 )
585 if hasattr(filelike, "tell"):
586 if seekable:
587 initial_position = filelike.tell()
588 filelike.seek(0, io.SEEK_END)
589 self.headers["Content-Length"] = filelike.tell() - initial_position
590 filelike.seek(initial_position)
591 elif hasattr(filelike, "getbuffer"):
592 self.headers["Content-Length"] = (
593 filelike.getbuffer().nbytes - filelike.tell()
594 )
595 elif os.path.exists(filename):
596 self.headers["Content-Length"] = (
597 os.path.getsize(filename) - filelike.tell()
598 )
599 elif seekable:
600 self.headers["Content-Length"] = sum(
601 iter(lambda: len(filelike.read(self.block_size)), 0)
602 )
603 filelike.seek(-int(self.headers["Content-Length"]), io.SEEK_END)
605 filename = os.path.basename(self.filename or filename)
606 if self._no_explicit_content_type:
607 if filename:
608 content_type, encoding = mimetypes.guess_type(filename)
609 # Encoding isn't set to prevent browsers from automatically
610 # uncompressing files.
611 content_type = {
612 "bzip2": "application/x-bzip",
613 "gzip": "application/gzip",
614 "xz": "application/x-xz",
615 }.get(encoding, content_type)
616 self.headers["Content-Type"] = (
617 content_type or "application/octet-stream"
618 )
619 else:
620 self.headers["Content-Type"] = "application/octet-stream"
622 if content_disposition := content_disposition_header(
623 self.as_attachment, filename
624 ):
625 self.headers["Content-Disposition"] = content_disposition
628class HttpResponseRedirectBase(HttpResponse):
629 allowed_schemes = ["http", "https", "ftp"]
631 def __init__(self, redirect_to, *args, **kwargs):
632 super().__init__(*args, **kwargs)
633 self["Location"] = iri_to_uri(redirect_to)
634 parsed = urlparse(str(redirect_to))
635 if parsed.scheme and parsed.scheme not in self.allowed_schemes:
636 raise DisallowedRedirect(
637 "Unsafe redirect to URL with protocol '%s'" % parsed.scheme
638 )
640 url = property(lambda self: self["Location"])
642 def __repr__(self):
643 return (
644 '<%(cls)s status_code=%(status_code)d%(content_type)s, url="%(url)s">'
645 % {
646 "cls": self.__class__.__name__,
647 "status_code": self.status_code,
648 "content_type": self._content_type_for_repr,
649 "url": self.url,
650 }
651 )
654class HttpResponseRedirect(HttpResponseRedirectBase):
655 status_code = 302
658class HttpResponsePermanentRedirect(HttpResponseRedirectBase):
659 status_code = 301
662class HttpResponseNotModified(HttpResponse):
663 status_code = 304
665 def __init__(self, *args, **kwargs):
666 super().__init__(*args, **kwargs)
667 del self["content-type"]
669 @HttpResponse.content.setter
670 def content(self, value):
671 if value:
672 raise AttributeError(
673 "You cannot set content to a 304 (Not Modified) response"
674 )
675 self._container = []
678class HttpResponseBadRequest(HttpResponse):
679 status_code = 400
682class HttpResponseNotFound(HttpResponse):
683 status_code = 404
686class HttpResponseForbidden(HttpResponse):
687 status_code = 403
690class HttpResponseNotAllowed(HttpResponse):
691 status_code = 405
693 def __init__(self, permitted_methods, *args, **kwargs):
694 super().__init__(*args, **kwargs)
695 self["Allow"] = ", ".join(permitted_methods)
697 def __repr__(self):
698 return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % {
699 "cls": self.__class__.__name__,
700 "status_code": self.status_code,
701 "content_type": self._content_type_for_repr,
702 "methods": self["Allow"],
703 }
706class HttpResponseGone(HttpResponse):
707 status_code = 410
710class HttpResponseServerError(HttpResponse):
711 status_code = 500
714class Http404(Exception):
715 pass
718class JsonResponse(HttpResponse):
719 """
720 An HTTP response class that consumes data to be serialized to JSON.
722 :param data: Data to be dumped into json. By default only ``dict`` objects
723 are allowed to be passed due to a security flaw before ECMAScript 5. See
724 the ``safe`` parameter for more information.
725 :param encoder: Should be a json encoder class. Defaults to
726 ``django.core.serializers.json.DjangoJSONEncoder``.
727 :param safe: Controls if only ``dict`` objects may be serialized. Defaults
728 to ``True``.
729 :param json_dumps_params: A dictionary of kwargs passed to json.dumps().
730 """
732 def __init__(
733 self,
734 data,
735 encoder=DjangoJSONEncoder,
736 safe=True,
737 json_dumps_params=None,
738 **kwargs,
739 ):
740 if safe and not isinstance(data, dict):
741 raise TypeError(
742 "In order to allow non-dict objects to be serialized set the "
743 "safe parameter to False."
744 )
745 if json_dumps_params is None:
746 json_dumps_params = {}
747 kwargs.setdefault("content_type", "application/json")
748 data = json.dumps(data, cls=encoder, **json_dumps_params)
749 super().__init__(content=data, **kwargs)