Skip to content

Commit

Permalink
Fix: properly encode UTF-8 filenames in query results request (#4498)
Browse files Browse the repository at this point in the history
* 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 <omer@rauchy.net>

Co-authored-by: Omer Lachish <omer@rauchy.net>
  • Loading branch information
arikfr and Omer Lachish authored Dec 30, 2019
1 parent d0fb377 commit ff34ded
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 3 deletions.
26 changes: 23 additions & 3 deletions redash/handlers/query_results.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions tests/handlers/test_query_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit ff34ded

Please sign in to comment.