From 36330658dd4e0c4ecaa8f774602e09fbeaf1e519 Mon Sep 17 00:00:00 2001 From: Min RK Date: Wed, 16 Aug 2017 11:25:46 +0200 Subject: [PATCH] use RFC5987 encoding for filenames as described in RFC 6266 describing Content-Disposition --- notebook/base/handlers.py | 16 +++++++++++++++- notebook/bundler/tarball_bundler.py | 7 +++---- notebook/bundler/zip_bundler.py | 3 +-- notebook/files/handlers.py | 2 +- notebook/nbconvert/handlers.py | 6 ++---- notebook/tests/test_files.py | 2 +- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/notebook/base/handlers.py b/notebook/base/handlers.py index 4a88744ffd5..359cb300b5f 100755 --- a/notebook/base/handlers.py +++ b/notebook/base/handlers.py @@ -279,6 +279,20 @@ def set_default_headers(self): if self.allow_credentials: self.set_header("Access-Control-Allow-Credentials", 'true') + def set_attachment_header(self, filename): + """Set Content-Disposition: attachment header + + As a method to ensure handling of filename encoding + """ + escaped_filename = url_escape(filename) + self.set_header('Content-Disposition', + 'attachment;' + ' filename*= UTF-8''{utf8}' + .format( + utf8=escaped_filename, + ) + ) + def get_origin(self): # Handle WebSocket Origin naming convention differences # The difference between version 8 and 13 is that in 8 the @@ -478,7 +492,7 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler): def get(self, path): if os.path.splitext(path)[1] == '.ipynb' or self.get_argument("download", False): name = path.rsplit('/', 1)[-1] - self.set_header('Content-Disposition','attachment; filename="%s"' % name) + self.set_attachment_header(name) return web.StaticFileHandler.get(self, path) diff --git a/notebook/bundler/tarball_bundler.py b/notebook/bundler/tarball_bundler.py index e513dbf3bce..854ab678811 100644 --- a/notebook/bundler/tarball_bundler.py +++ b/notebook/bundler/tarball_bundler.py @@ -39,10 +39,9 @@ def bundle(handler, model): with io.BytesIO() as tar_buffer: with tarfile.open(tar_filename, "w:gz", fileobj=tar_buffer) as tar: tar.addfile(info, io.BytesIO(notebook_content)) - - handler.set_header('Content-Disposition', - 'attachment; filename="{}"'.format(tar_filename)) + + handler.set_attachment_header(tar_filename) handler.set_header('Content-Type', 'application/gzip') - + # Return the buffer value as the response handler.finish(tar_buffer.getvalue()) diff --git a/notebook/bundler/zip_bundler.py b/notebook/bundler/zip_bundler.py index 2fd228a0d5e..f7bd5cc7a6b 100644 --- a/notebook/bundler/zip_bundler.py +++ b/notebook/bundler/zip_bundler.py @@ -35,8 +35,7 @@ def bundle(handler, model): # Headers zip_filename = os.path.splitext(notebook_name)[0] + '.zip' - handler.set_header('Content-Disposition', - 'attachment; filename="%s"' % zip_filename) + handler.set_attachment_header(zip_filename) handler.set_header('Content-Type', 'application/zip') # Get associated files diff --git a/notebook/files/handlers.py b/notebook/files/handlers.py index 193eaa86041..7c6407be47c 100644 --- a/notebook/files/handlers.py +++ b/notebook/files/handlers.py @@ -46,7 +46,7 @@ def get(self, path, include_body=True): model = cm.get(path, type='file', content=include_body) if self.get_argument("download", False): - self.set_header('Content-Disposition','attachment; filename="%s"' % name) + self.set_attachment_header(name) # get mimetype from filename if name.endswith('.ipynb'): diff --git a/notebook/nbconvert/handlers.py b/notebook/nbconvert/handlers.py index c7da994d13d..2348b83eed6 100644 --- a/notebook/nbconvert/handlers.py +++ b/notebook/nbconvert/handlers.py @@ -38,8 +38,7 @@ def respond_zip(handler, name, output, resources): # Headers zip_filename = os.path.splitext(name)[0] + '.zip' - handler.set_header('Content-Disposition', - 'attachment; filename="%s"' % zip_filename) + handler.set_attachment_header(zip_filename) handler.set_header('Content-Type', 'application/zip') # Prepare the zip file @@ -114,8 +113,7 @@ def get(self, format, path): # Force download if requested if self.get_argument('download', 'false').lower() == 'true': filename = os.path.splitext(name)[0] + resources['output_extension'] - self.set_header('Content-Disposition', - 'attachment; filename="%s"' % filename) + self.set_attachment_header(filename) # MIME type if exporter.output_mimetype: diff --git a/notebook/tests/test_files.py b/notebook/tests/test_files.py index 8345f3c4675..41cd605170e 100644 --- a/notebook/tests/test_files.py +++ b/notebook/tests/test_files.py @@ -112,7 +112,7 @@ def test_download(self): r = self.request('GET', 'files/test.txt?download=1') disposition = r.headers.get('Content-Disposition', '') self.assertIn('attachment', disposition) - self.assertIn('filename="test.txt"', disposition) + self.assertIn('filename*= UTF-8''test.txt', disposition) def test_view_html(self): nbdir = self.notebook_dir