From ff34dedf46ad83f9e0a6c1dff17157257b278a57 Mon Sep 17 00:00:00 2001 From: Arik Fraimovich Date: Mon, 30 Dec 2019 11:52:18 +0200 Subject: [PATCH] Fix: properly encode UTF-8 filenames in query results request (#4498) * Fix: properly encode UTF-8 filenames in query results request Ended up copying the implementation from Flask's send_file helper function, because send_file doesn't really fit our use case. * Update tests/handlers/test_query_results.py Co-Authored-By: Omer Lachish Co-authored-by: Omer Lachish --- redash/handlers/query_results.py | 26 +++++++++++++++++++++++--- tests/handlers/test_query_results.py | 13 +++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/redash/handlers/query_results.py b/redash/handlers/query_results.py index 9a045f1982..2056e7a5ff 100644 --- a/redash/handlers/query_results.py +++ b/redash/handlers/query_results.py @@ -1,9 +1,11 @@ import logging import time +import unicodedata from flask import make_response, request from flask_login import current_user from flask_restful import abort +from werkzeug.urls import url_quote from redash import models, settings from redash.handlers.base import BaseResource, get_object_or_404, record_event from redash.permissions import ( @@ -129,6 +131,25 @@ def get_download_filename(query_result, query, filetype): return "{}_{}.{}".format(filename, retrieved_at, filetype) +def content_disposition_filenames(attachment_filename): + if not isinstance(attachment_filename, str): + attachment_filename = attachment_filename.decode("utf-8") + + try: + attachment_filename = attachment_filename.encode("ascii") + except UnicodeEncodeError: + filenames = { + "filename": unicodedata.normalize("NFKD", attachment_filename).encode( + "ascii", "ignore" + ), + "filename*": "UTF-8''%s" % url_quote(attachment_filename, safe=b""), + } + else: + filenames = {"filename": attachment_filename} + + return filenames + + class QueryResultListResource(BaseResource): @require_permission("execute_query") def post(self): @@ -383,9 +404,8 @@ def get(self, query_id=None, query_result_id=None, filetype="json"): filename = get_download_filename(query_result, query, filetype) - response.headers.add_header( - "Content-Disposition", 'attachment; filename="{}"'.format(filename) - ) + filenames = content_disposition_filenames(filename) + response.headers.add("Content-Disposition", "attachment", **filenames) return response diff --git a/tests/handlers/test_query_results.py b/tests/handlers/test_query_results.py index 1016e33066..57a9e3dbc7 100644 --- a/tests/handlers/test_query_results.py +++ b/tests/handlers/test_query_results.py @@ -29,6 +29,19 @@ def test_returns_404_if_no_cached_result_found(self): self.assertEqual(404, rv.status_code) +class TestQueryResultsContentDispositionHeaders(BaseTestCase): + def test_supports_unicode(self): + query_result = self.factory.create_query_result() + query = self.factory.create_query(name="עברית", latest_query_data=query_result) + + rv = self.make_request("get", "/api/queries/{}/results.json".format(query.id)) + # This is what gunicorn will do with it + try: + rv.headers['Content-Disposition'].encode('ascii') + except Exception as e: + self.fail(repr(e)) + + class TestQueryResultListAPI(BaseTestCase): def test_get_existing_result(self): query_result = self.factory.create_query_result()