Skip to content

Commit

Permalink
Fixed #34194 -- Added django.utils.http.content_disposition_header().
Browse files Browse the repository at this point in the history
  • Loading branch information
alexmv authored and felixxm committed Dec 5, 2022
1 parent 3d3e955 commit cbce427
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 16 deletions.
22 changes: 6 additions & 16 deletions django/http/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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
Expand All @@ -18,7 +18,7 @@
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(
Expand Down Expand Up @@ -569,20 +569,10 @@ def set_headers(self, filelike):
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):
Expand Down
22 changes: 22 additions & 0 deletions django/utils/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
_coerce_args,
_splitnetloc,
_splitparams,
quote,
scheme_chars,
unquote,
)
Expand Down Expand Up @@ -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 changes: 9 additions & 0 deletions docs/ref/utils.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/4.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~

Expand Down
26 changes: 26 additions & 0 deletions tests/utils_tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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,
Expand Down Expand Up @@ -511,3 +512,28 @@ def test_rfc2231_wrong_title(self):
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
)

0 comments on commit cbce427

Please sign in to comment.