Commit 59280e61 authored by Pierre-Yves David's avatar Pierre-Yves David
Browse files

[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/

--HG--
branch : stable
parent 1a87ccdf12a3
...@@ -619,15 +619,21 @@ class CubicWebRequestBase(DBAPIRequest): ...@@ -619,15 +619,21 @@ class CubicWebRequestBase(DBAPIRequest):
self.set_header('content-type', content_type) self.set_header('content-type', content_type)
if filename: if filename:
header = ['attachment'] header = ['attachment']
unicode_filename = None
try: try:
filename = filename.encode('ascii') ascii_filename = filename.encode('ascii')
header.append('filename=' + filename)
except UnicodeEncodeError: except UnicodeEncodeError:
# fallback filename for very old browser # 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 # encoded filename according RFC5987
filename = urllib.quote(filename.encode('utf-8'), '') urlquoted_filename = urllib.quote(unicode_filename.encode('utf-8'), '')
header.append("filename*=utf-8''" + filename) header.append("filename*=utf-8''" + urlquoted_filename)
self.set_header('content-disposition', ';'.join(header)) self.set_header('content-disposition', ';'.join(header))
# high level methods for HTML headers management ########################## # high level methods for HTML headers management ##########################
......
...@@ -58,12 +58,44 @@ class IDownloadableTC(CubicWebTC): ...@@ -58,12 +58,44 @@ class IDownloadableTC(CubicWebTC):
req.form['eid'] = str(req.user.eid) req.form['eid'] = str(req.user.eid)
data = self.ctrl_publish(req,'view') data = self.ctrl_publish(req,'view')
get = req.headers_out.getRawHeaders get = req.headers_out.getRawHeaders
self.assertEqual(['attachment;filename=admin.txt'], self.assertEqual(['attachment;filename="admin.txt"'],
get('content-disposition')) get('content-disposition'))
self.assertEqual(['text/plain;charset=ascii'], self.assertEqual(['text/plain;charset=ascii'],
get('content-type')) get('content-type'))
self.assertEqual('Babar is not dead!', data) 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): def test_header_unicode_filename(self):
req = self.request() req = self.request()
self.create_user(req, login=u'cécilia', password='babar') self.create_user(req, login=u'cécilia', password='babar')
...@@ -74,7 +106,7 @@ class IDownloadableTC(CubicWebTC): ...@@ -74,7 +106,7 @@ class IDownloadableTC(CubicWebTC):
req.form['eid'] = str(req.user.eid) req.form['eid'] = str(req.user.eid)
self.ctrl_publish(req,'view') self.ctrl_publish(req,'view')
get = req.headers_out.getRawHeaders 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')) get('content-disposition'))
def test_header_unicode_long_filename(self): def test_header_unicode_long_filename(self):
...@@ -88,7 +120,7 @@ class IDownloadableTC(CubicWebTC): ...@@ -88,7 +120,7 @@ class IDownloadableTC(CubicWebTC):
req.form['eid'] = str(req.user.eid) req.form['eid'] = str(req.user.eid)
self.ctrl_publish(req,'view') self.ctrl_publish(req,'view')
get = req.headers_out.getRawHeaders 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')) get('content-disposition'))
if __name__ == '__main__': if __name__ == '__main__':
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment