Browse Source

Fixed #34194 -- Added django.utils.http.content_disposition_header().

Alex Vandiver 2 years ago
parent
commit
cbce427c17
5 changed files with 66 additions and 16 deletions
  1. 6 16
      django/http/response.py
  2. 22 0
      django/utils/http.py
  3. 9 0
      docs/ref/utils.txt
  4. 3 0
      docs/releases/4.2.txt
  5. 26 0
      tests/utils_tests/test_http.py

+ 6 - 16
django/http/response.py

@@ -8,7 +8,7 @@ import sys
 import time
 from email.header import Header
 from http.client import responses
-from urllib.parse import quote, urlparse
+from urllib.parse import urlparse
 
 from django.conf import settings
 from django.core import signals, signing
@@ -18,7 +18,7 @@ from django.http.cookie import SimpleCookie
 from django.utils import timezone
 from django.utils.datastructures import CaseInsensitiveMapping
 from django.utils.encoding import iri_to_uri
-from django.utils.http import http_date
+from django.utils.http import content_disposition_header, http_date
 from django.utils.regex_helper import _lazy_re_compile
 
 _charset_from_content_type_re = _lazy_re_compile(
@@ -569,20 +569,10 @@ class FileResponse(StreamingHttpResponse):
             else:
                 self.headers["Content-Type"] = "application/octet-stream"
 
-        if filename:
-            disposition = "attachment" if self.as_attachment else "inline"
-            try:
-                filename.encode("ascii")
-                file_expr = 'filename="{}"'.format(
-                    filename.replace("\\", "\\\\").replace('"', r"\"")
-                )
-            except UnicodeEncodeError:
-                file_expr = "filename*=utf-8''{}".format(quote(filename))
-            self.headers["Content-Disposition"] = "{}; {}".format(
-                disposition, file_expr
-            )
-        elif self.as_attachment:
-            self.headers["Content-Disposition"] = "attachment"
+        if content_disposition := content_disposition_header(
+            self.as_attachment, filename
+        ):
+            self.headers["Content-Disposition"] = content_disposition
 
 
 class HttpResponseRedirectBase(HttpResponse):

+ 22 - 0
django/utils/http.py

@@ -10,6 +10,7 @@ from urllib.parse import (
     _coerce_args,
     _splitnetloc,
     _splitparams,
+    quote,
     scheme_chars,
     unquote,
 )
@@ -425,3 +426,24 @@ def parse_header_parameters(line):
                 value = unquote(value, encoding=encoding)
             pdict[name] = value
     return key, pdict
+
+
+def content_disposition_header(as_attachment, filename):
+    """
+    Construct a Content-Disposition HTTP header value from the given filename
+    as specified by RFC 6266.
+    """
+    if filename:
+        disposition = "attachment" if as_attachment else "inline"
+        try:
+            filename.encode("ascii")
+            file_expr = 'filename="{}"'.format(
+                filename.replace("\\", "\\\\").replace('"', r"\"")
+            )
+        except UnicodeEncodeError:
+            file_expr = "filename*=utf-8''{}".format(quote(filename))
+        return f"{disposition}; {file_expr}"
+    elif as_attachment:
+        return "attachment"
+    else:
+        return None

+ 9 - 0
docs/ref/utils.txt

@@ -729,6 +729,15 @@ escaping HTML.
 
     Outputs a string in the format ``Wdy, DD Mon YYYY HH:MM:SS GMT``.
 
+.. function:: content_disposition_header(as_attachment, filename)
+
+    .. versionadded:: 4.2
+
+    Constructs a ``Content-Disposition`` HTTP header value from the given
+    ``filename`` as specified by :rfc:`6266`. Returns ``None`` if
+    ``as_attachment`` is ``False`` and ``filename`` is ``None``, otherwise
+    returns a string suitable for the ``Content-Disposition`` HTTP header.
+
 .. function:: base36_to_int(s)
 
     Converts a base 36 string to an integer.

+ 3 - 0
docs/releases/4.2.txt

@@ -321,6 +321,9 @@ Utilities
   documented functions for handling URL redirects. The Django functions were
   not affected.
 
+* The new :func:`django.utils.http.content_disposition_header` function returns
+  a ``Content-Disposition`` HTTP header value as specified by :rfc:`6266`.
+
 Validators
 ~~~~~~~~~~
 

+ 26 - 0
tests/utils_tests/test_http.py

@@ -7,6 +7,7 @@ from django.test import SimpleTestCase
 from django.utils.datastructures import MultiValueDict
 from django.utils.http import (
     base36_to_int,
+    content_disposition_header,
     escape_leading_slashes,
     http_date,
     int_to_base36,
@@ -511,3 +512,28 @@ class ParseHeaderParameterTests(unittest.TestCase):
         for raw_line, expected_title in test_data:
             parsed = parse_header_parameters(raw_line)
             self.assertEqual(parsed[1]["title"], expected_title)
+
+
+class ContentDispositionHeaderTests(unittest.TestCase):
+    def test_basic(self):
+        tests = (
+            ((False, None), None),
+            ((False, "example"), 'inline; filename="example"'),
+            ((True, None), "attachment"),
+            ((True, "example"), 'attachment; filename="example"'),
+            (
+                (True, '"example" file\\name'),
+                'attachment; filename="\\"example\\" file\\\\name"',
+            ),
+            ((True, "espécimen"), "attachment; filename*=utf-8''esp%C3%A9cimen"),
+            (
+                (True, '"espécimen" filename'),
+                "attachment; filename*=utf-8''%22esp%C3%A9cimen%22%20filename",
+            ),
+        )
+
+        for (is_attachment, filename), expected in tests:
+            with self.subTest(is_attachment=is_attachment, filename=filename):
+                self.assertEqual(
+                    content_disposition_header(is_attachment, filename), expected
+                )