[downloadable] fix filename HTTP header for simple name with space (closes #2535715) stable
authorPierre-Yves David <pierre-yves.david@logilab.fr>
Tue, 11 Dec 2012 17:17:40 +0100
branchstable
changeset 8610 b1145ad53999
parent 8608 1a87ccdf12a3
child 8611 51068fe1e39a
[downloadable] fix filename HTTP header for simple name with space (closes #2535715) Since d74addac92bb, we export simple ascii filename without any encoding in the `filename` parameter of the `Content-Disposition` header. If this name contains space this will fails, the parameter value will be truncated at the space position. (eg. `filename=jungle babar.txt` read as `jungle`) We need to quote the filename to prevent this (eg. `filename="jungle babar.txt"`). Then literal quote and backslash needs to be escaped too. The new escaping is correct according this extensive test case data base: http://greenbytes.de/tech/tc2231/
web/request.py
web/test/unittest_idownloadable.py
--- a/web/request.py	Fri Nov 30 21:19:36 2012 +0100
+++ b/web/request.py	Tue Dec 11 17:17:40 2012 +0100
@@ -619,15 +619,21 @@
         self.set_header('content-type', content_type)
         if filename:
             header = ['attachment']
+            unicode_filename = None
             try:
-                filename = filename.encode('ascii')
-                header.append('filename=' + filename)
+                ascii_filename = filename.encode('ascii')
             except UnicodeEncodeError:
                 # fallback filename for very old browser
-                header.append('filename=' + filename.encode('ascii', 'ignore'))
+                unicode_filename = filename
+                ascii_filename = filename.encode('ascii', 'ignore')
+            # escape " and \
+            # see http://greenbytes.de/tech/tc2231/#attwithfilenameandextparamescaped
+            ascii_filename = ascii_filename.replace('\x5c', r'\\').replace('"', r'\"')
+            header.append('filename="%s"' % ascii_filename)
+            if unicode_filename is not None:
                 # encoded filename according RFC5987
-                filename = urllib.quote(filename.encode('utf-8'), '')
-                header.append("filename*=utf-8''" + filename)
+                urlquoted_filename = urllib.quote(unicode_filename.encode('utf-8'), '')
+                header.append("filename*=utf-8''" + urlquoted_filename)
             self.set_header('content-disposition', ';'.join(header))
 
     # high level methods for HTML headers management ##########################
--- a/web/test/unittest_idownloadable.py	Fri Nov 30 21:19:36 2012 +0100
+++ b/web/test/unittest_idownloadable.py	Tue Dec 11 17:17:40 2012 +0100
@@ -58,12 +58,44 @@
         req.form['eid'] = str(req.user.eid)
         data = self.ctrl_publish(req,'view')
         get = req.headers_out.getRawHeaders
-        self.assertEqual(['attachment;filename=admin.txt'],
+        self.assertEqual(['attachment;filename="admin.txt"'],
                          get('content-disposition'))
         self.assertEqual(['text/plain;charset=ascii'],
                          get('content-type'))
         self.assertEqual('Babar is not dead!', data)
 
+    def test_header_with_space(self):
+        req = self.request()
+        self.create_user(req, login=u'c c l a', password='babar')
+        self.commit()
+        with self.login(u'c c l a', password='babar'):
+            req = self.request()
+            req.form['vid'] = 'download'
+            req.form['eid'] = str(req.user.eid)
+            data = self.ctrl_publish(req,'view')
+            get = req.headers_out.getRawHeaders
+            self.assertEqual(['attachment;filename="c c l a.txt"'],
+                             get('content-disposition'))
+            self.assertEqual(['text/plain;charset=ascii'],
+                             get('content-type'))
+            self.assertEqual('Babar is not dead!', data)
+
+    def test_header_with_space_and_comma(self):
+        req = self.request()
+        self.create_user(req, login=ur'c " l\ a', password='babar')
+        self.commit()
+        with self.login(ur'c " l\ a', password='babar'):
+            req = self.request()
+            req.form['vid'] = 'download'
+            req.form['eid'] = str(req.user.eid)
+            data = self.ctrl_publish(req,'view')
+            get = req.headers_out.getRawHeaders
+            self.assertEqual([r'attachment;filename="c \" l\\ a.txt"'],
+                             get('content-disposition'))
+            self.assertEqual(['text/plain;charset=ascii'],
+                             get('content-type'))
+            self.assertEqual('Babar is not dead!', data)
+
     def test_header_unicode_filename(self):
         req = self.request()
         self.create_user(req, login=u'cécilia', password='babar')
@@ -74,7 +106,7 @@
             req.form['eid'] = str(req.user.eid)
             self.ctrl_publish(req,'view')
             get = req.headers_out.getRawHeaders
-            self.assertEqual(["attachment;filename=ccilia.txt;filename*=utf-8''c%C3%A9cilia.txt"],
+            self.assertEqual(['''attachment;filename="ccilia.txt";filename*=utf-8''c%C3%A9cilia.txt'''],
                              get('content-disposition'))
 
     def test_header_unicode_long_filename(self):
@@ -88,7 +120,7 @@
             req.form['eid'] = str(req.user.eid)
             self.ctrl_publish(req,'view')
             get = req.headers_out.getRawHeaders
-            self.assertEqual(["attachment;filename=Brte_h_grand_nm_a_va_totallement_dborder_de_la_limite_l.txt;filename*=utf-8''B%C3%A8rte_h%C3%B4_grand_n%C3%B4m_%C3%A7a_va_totallement_d%C3%A9border_de_la_limite_l%C3%A0.txt"],
+            self.assertEqual(["""attachment;filename="Brte_h_grand_nm_a_va_totallement_dborder_de_la_limite_l.txt";filename*=utf-8''B%C3%A8rte_h%C3%B4_grand_n%C3%B4m_%C3%A7a_va_totallement_d%C3%A9border_de_la_limite_l%C3%A0.txt"""],
                              get('content-disposition'))
 
 if __name__ == '__main__':