Repository: pallets/werkzeug

Issue:
RFC2231 filename continuations of Content-Disposition header.
This patch allows you to appropriately deal with filename continuations in Content-Disposition header.
The params continualtion is defined in RFC 2231(http://tools.ietf.org/html/rfc2231).
When specifying a long filename as a attachment file, a string of the filename will be sometimes splitted into several pieces, like 'filename_0_=' and  'filename_1_='.

Requirements: email.utils



Discussion:

Resolving patch:
diff --git a/werkzeug/http.py b/werkzeug/http.py
--- a/werkzeug/http.py
+++ b/werkzeug/http.py
@@ -64,18 +64,21 @@
 _etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)')
 _unsafe_header_chars = set('()<>@,;:\"/[]?={} \t')
 _option_header_piece_re = re.compile(r'''
-    ;\s*
+    ;\s*,?\s*  # newlines were replaced with commas
     (?P<key>
         "[^"\\]*(?:\\.[^"\\]*)*"  # quoted string
     |
         [^\s;,=*]+  # token
     )
+    (?:\*(?P<count>\d+))?  # *1, optional continuation index
     \s*
     (?:  # optionally followed by =value
         (?:  # equals sign, possibly with encoding
             \*\s*=\s*  # * indicates extended notation
-            (?P<encoding>[^\s]+?)
-            '(?P<language>[^\s]*?)'
+            (?:  # optional encoding
+                (?P<encoding>[^\s]+?)
+                '(?P<language>[^\s]*?)'
+            )?
         |
             =\s*  # basic notation
         )
@@ -355,6 +358,9 @@ def parse_options_header(value, multiple=False):
     a slightly different format.  For these headers use the
     :func:`parse_dict_header` function.
 
+    .. versionchanged:: 0.15
+        :rfc:`2231` parameter continuations are handled.
+
     .. versionadded:: 0.5
 
     :param value: the header to parse.
@@ -376,11 +382,22 @@ def parse_options_header(value, multiple=False):
         options = {}
         # Parse options
         rest = match.group(2)
+        continued_encoding = None
         while rest:
             optmatch = _option_header_piece_re.match(rest)
             if not optmatch:
                 break
-            option, encoding, _, option_value = optmatch.groups()
+            option, count, encoding, language, option_value = optmatch.groups()
+            # Continuations don't have to supply the encoding after the
+            # first line. If we're in a continuation, track the current
+            # encoding to use for subsequent lines. Reset it when the
+            # continuation ends.
+            if not count:
+                continued_encoding = None
+            else:
+                if not encoding:
+                    encoding = continued_encoding
+                continued_encoding = encoding
             option = unquote_header_value(option)
             if option_value is not None:
                 option_value = unquote_header_value(
@@ -388,7 +405,13 @@ def parse_options_header(value, multiple=False):
                     option == 'filename')
                 if encoding is not None:
                     option_value = _unquote(option_value).decode(encoding)
-            options[option] = option_value
+            if count:
+                # Continuations append to the existing value. For
+                # simplicity, this ignores the possibility of
+                # out-of-order indices, which shouldn't happen anyway.
+                options[option] = options.get(option, "") + option_value
+            else:
+                options[option] = option_value
             rest = rest[optmatch.end():]
         result.append(options)
         if multiple is False:


Test patch:
diff --git a/tests/test_formparser.py b/tests/test_formparser.py
--- a/tests/test_formparser.py
+++ b/tests/test_formparser.py
@@ -441,6 +441,25 @@ def stream_factory():
         assert parser.stream_factory is stream_factory
         assert parser.cls is dict
 
+    def test_file_rfc2231_filename_continuations(self):
+        data = (
+            b"--foo\r\n"
+            b"Content-Type: text/plain; charset=utf-8\r\n"
+            b'Content-Disposition: form-data; name=rfc2231;\r\n'
+            b"	filename*0*=ascii''a%20b%20;\r\n"
+            b"	filename*1*=c%20d%20;\r\n"
+            b'	filename*2="e f.txt"\r\n\r\n'
+            b"file contents\r\n--foo--"
+        )
+        request = Request.from_values(
+            input_stream=BytesIO(data),
+            content_length=len(data),
+            content_type="multipart/form-data; boundary=foo",
+            method="POST"
+        )
+        assert request.files["rfc2231"].filename == "a b c d e f.txt"
+        assert request.files["rfc2231"].read() == b"file contents"
+
 
 class TestInternalFunctions(object):