From a1a382b44a33d8112996f15c1dd573bd2b1a039b Mon Sep 17 00:00:00 2001
From: Alex Vandiver <alex@chmrr.net>
Date: Wed, 30 Nov 2022 15:09:49 -0500
Subject: [PATCH] Extract function for generating a proper Content-Disposition
header.
---
django/http/response.py | 20 ++++----------------
django/utils/http.py | 22 ++++++++++++++++++++++
tests/utils_tests/test_http.py | 16 ++++++++++++++++
3 files changed, 42 insertions(+), 16 deletions(-)
diff --git django/http/response.py django/http/response.py
index bb94e81263..242a146aa2 100644
|
|
import sys
|
8 | 8 | import time |
9 | 9 | from email.header import Header |
10 | 10 | from http.client import responses |
11 | | from urllib.parse import quote, urlparse |
| 11 | from urllib.parse import urlparse |
12 | 12 | |
13 | 13 | from django.conf import settings |
14 | 14 | from django.core import signals, signing |
… |
… |
from django.http.cookie import SimpleCookie
|
18 | 18 | from django.utils import timezone |
19 | 19 | from django.utils.datastructures import CaseInsensitiveMapping |
20 | 20 | from django.utils.encoding import iri_to_uri |
21 | | from django.utils.http import http_date |
| 21 | from django.utils.http import content_disposition_header, http_date |
22 | 22 | from django.utils.regex_helper import _lazy_re_compile |
23 | 23 | |
24 | 24 | _charset_from_content_type_re = _lazy_re_compile( |
… |
… |
class FileResponse(StreamingHttpResponse):
|
569 | 569 | else: |
570 | 570 | self.headers["Content-Type"] = "application/octet-stream" |
571 | 571 | |
572 | | if filename: |
573 | | disposition = "attachment" if self.as_attachment else "inline" |
574 | | try: |
575 | | filename.encode("ascii") |
576 | | file_expr = 'filename="{}"'.format( |
577 | | filename.replace("\\", "\\\\").replace('"', r"\"") |
578 | | ) |
579 | | except UnicodeEncodeError: |
580 | | file_expr = "filename*=utf-8''{}".format(quote(filename)) |
581 | | self.headers["Content-Disposition"] = "{}; {}".format( |
582 | | disposition, file_expr |
583 | | ) |
584 | | elif self.as_attachment: |
585 | | self.headers["Content-Disposition"] = "attachment" |
| 572 | if self.as_attachment or filename: |
| 573 | self.headers["Content-Disposition"] = content_disposition_header(self.as_attachment, filename) |
586 | 574 | |
587 | 575 | |
588 | 576 | class HttpResponseRedirectBase(HttpResponse): |
diff --git django/utils/http.py django/utils/http.py
index db4dee2f27..c5f2870458 100644
|
|
from urllib.parse import (
|
10 | 10 | _coerce_args, |
11 | 11 | _splitnetloc, |
12 | 12 | _splitparams, |
| 13 | quote, |
13 | 14 | scheme_chars, |
14 | 15 | unquote, |
15 | 16 | ) |
… |
… |
def parse_header_parameters(line):
|
425 | 426 | value = unquote(value, encoding=encoding) |
426 | 427 | pdict[name] = value |
427 | 428 | return key, pdict |
| 429 | |
| 430 | def content_disposition_header(as_attachment, filename): |
| 431 | """ |
| 432 | Construct a Content-Disposition header value. |
| 433 | """ |
| 434 | if filename: |
| 435 | disposition = "attachment" if as_attachment else "inline" |
| 436 | try: |
| 437 | filename.encode("ascii") |
| 438 | file_expr = 'filename="{}"'.format( |
| 439 | filename.replace("\\", "\\\\").replace('"', r"\"") |
| 440 | ) |
| 441 | except UnicodeEncodeError: |
| 442 | file_expr = "filename*=utf-8''{}".format(quote(filename)) |
| 443 | return "{}; {}".format( |
| 444 | disposition, file_expr |
| 445 | ) |
| 446 | elif as_attachment: |
| 447 | return "attachment" |
| 448 | else: |
| 449 | return None |
diff --git tests/utils_tests/test_http.py tests/utils_tests/test_http.py
index add9625685..e028a90594 100644
|
|
from django.test import SimpleTestCase
|
7 | 7 | from django.utils.datastructures import MultiValueDict |
8 | 8 | from django.utils.http import ( |
9 | 9 | base36_to_int, |
| 10 | content_disposition_header, |
10 | 11 | escape_leading_slashes, |
11 | 12 | http_date, |
12 | 13 | int_to_base36, |
… |
… |
class ParseHeaderParameterTests(unittest.TestCase):
|
511 | 512 | for raw_line, expected_title in test_data: |
512 | 513 | parsed = parse_header_parameters(raw_line) |
513 | 514 | self.assertEqual(parsed[1]["title"], expected_title) |
| 515 | |
| 516 | class ContentDispositionHeaderTests(unittest.TestCase): |
| 517 | def test(self): |
| 518 | tests = ( |
| 519 | ((False, None), None), |
| 520 | ((False, "example"), 'inline; filename="example"'), |
| 521 | ((True, None), "attachment"), |
| 522 | ((True, "example"), 'attachment; filename="example"'), |
| 523 | ((True, '"example" file\\name'), 'attachment; filename="\\\"example\\\" file\\\\name"'), |
| 524 | ((True, "espécimen"), 'attachment; filename*=utf-8\'\'esp%C3%A9cimen'), |
| 525 | ((True, '"espécimen" filename'), 'attachment; filename*=utf-8\'\'%22esp%C3%A9cimen%22%20filename'), |
| 526 | ) |
| 527 | for (is_attachment, filename), expected in tests: |
| 528 | with self.subTest(is_attachment=is_attachment, filename=filename): |
| 529 | self.assertEqual(content_disposition_header(is_attachment, filename), expected) |