Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Properly remove f.name usage in send_file #1988

Merged
merged 3 commits into from
Aug 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ Version 0.12
------------

- the cli command now responds to `--version`.
- Mimetype guessing for ``send_file`` has been removed, as per issue ``#104``.
See pull request ``#1849``.
- Mimetype guessing and ETag generation for file-like objects in ``send_file``
has been removed, as per issue ``#104``. See pull request ``#1849``.
- Mimetype guessing in ``send_file`` now fails loudly and doesn't fall back to
``application/octet-stream``. See pull request ``#1988``.
- Make ``flask.safe_join`` able to join multiple paths like ``os.path.join``
(pull request ``#1730``).

Expand Down
40 changes: 39 additions & 1 deletion docs/upgrading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,45 @@ providing the ``--upgrade`` parameter::

$ pip install --upgrade Flask

.. _upgrading-to-10:
.. _upgrading-to-012:

Version 0.12
------------

Changes to send_file
````````````````````

The ``filename`` is no longer automatically inferred from file-like objects.
This means that the following code will no longer automatically have
``X-Sendfile`` support, etag generation or MIME-type guessing::

response = send_file(open('/path/to/file.txt'))

Any of the following is functionally equivalent::

fname = '/path/to/file.txt'

# Just pass the filepath directly
response = send_file(fname)

# Set the MIME-type and ETag explicitly
response = send_file(open(fname), mimetype='text/plain')
response.set_etag(...)

# Set `attachment_filename` for MIME-type guessing
# ETag still needs to be manually set
response = send_file(open(fname), attachment_filename=fname)
response.set_etag(...)

The reason for this is that some file-like objects have a invalid or even
misleading ``name`` attribute. Silently swallowing errors in such cases was not
a satisfying solution.

Additionally the default of falling back to ``application/octet-stream`` has
been removed. If Flask can't guess one or the user didn't provide one, the
function fails.

.. _upgrading-to-011:

Version 0.11
------------
Expand Down
56 changes: 39 additions & 17 deletions flask/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,14 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
to ``True`` to directly emit an ``X-Sendfile`` header. This however
requires support of the underlying webserver for ``X-Sendfile``.

You must explicitly provide the mimetype for the filename or file object.
By default it will try to guess the mimetype for you, but you can
also explicitly provide one. For extra security you probably want
to send certain files as attachment (HTML for instance). The mimetype
guessing requires a `filename` or an `attachment_filename` to be
provided.

ETags will also be attached automatically if a `filename` is provided. You
can turn this off by setting `add_etags=False`.

Please never pass filenames to this function from user sources;
you should use :func:`send_from_directory` instead.
Expand All @@ -458,9 +465,13 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
cache_timeout pulls its default from application config, when None.

.. versionchanged:: 0.12
mimetype guessing and etag support removed for file objects.
If no mimetype or attachment_filename is provided, application/octet-stream
will be used.
The filename is no longer automatically inferred from file objects. If
you want to use automatic mimetype and etag support, pass a filepath via
`filename_or_fp` or `attachment_filename`.

.. versionchanged:: 0.12
The `attachment_filename` is preferred over `filename` for MIME-type
detection.

:param filename_or_fp: the filename of the file to send in `latin-1`.
This is relative to the :attr:`~Flask.root_path`
Expand All @@ -470,8 +481,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
back to the traditional method. Make sure that the
file pointer is positioned at the start of data to
send before calling :func:`send_file`.
:param mimetype: the mimetype of the file if provided, otherwise
auto detection happens.
:param mimetype: the mimetype of the file if provided. If a file path is
given, auto detection happens as fallback, otherwise an
error will be raised.
:param as_attachment: set to ``True`` if you want to send this file with
a ``Content-Disposition: attachment`` header.
:param attachment_filename: the filename for the attachment if it
Expand All @@ -490,26 +502,36 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
mtime = None
if isinstance(filename_or_fp, string_types):
filename = filename_or_fp
if not os.path.isabs(filename):
filename = os.path.join(current_app.root_path, filename)
file = None
if attachment_filename is None:
attachment_filename = os.path.basename(filename)
else:
file = filename_or_fp
filename = getattr(file, 'name', None)
filename = None

if filename is not None:
if not os.path.isabs(filename):
filename = os.path.join(current_app.root_path, filename)
if mimetype is None and (filename or attachment_filename):
mimetype = mimetypes.guess_type(filename or attachment_filename)[0]
if mimetype is None:
mimetype = 'application/octet-stream'
if attachment_filename is not None:
mimetype = mimetypes.guess_type(attachment_filename)[0]

if mimetype is None:
if attachment_filename is not None:
raise ValueError(
'Unable to infer MIME-type from filename {!r}, please '
'pass one explicitly.'.format(mimetype_filename)
)
raise ValueError(
'Unable to infer MIME-type because no filename is available. '
'Please set either `attachment_filename`, pass a filepath to '
'`filename_or_fp` or set your own MIME-type via `mimetype`.'
)

headers = Headers()
if as_attachment:
if attachment_filename is None:
if filename is None:
raise TypeError('filename unavailable, required for '
'sending as attachment')
attachment_filename = os.path.basename(filename)
raise TypeError('filename unavailable, required for '
'sending as attachment')
headers.add('Content-Disposition', 'attachment',
filename=attachment_filename)

Expand Down
31 changes: 21 additions & 10 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,18 +355,30 @@ def test_send_file_last_modified(self):

@app.route('/')
def index():
return flask.send_file(StringIO("party like it's"), last_modified=last_modified)
return flask.send_file(StringIO("party like it's"),
last_modified=last_modified,
mimetype='text/plain')

c = app.test_client()
rv = c.get('/')
assert rv.last_modified == last_modified

def test_send_file_object_without_mimetype(self):
app = flask.Flask(__name__)

with app.test_request_context():
with pytest.raises(ValueError) as excinfo:
flask.send_file(StringIO("LOL"))

assert 'Unable to infer MIME-type' in str(excinfo)
assert 'no filename is available' in str(excinfo)

def test_send_file_object(self):
app = flask.Flask(__name__)

with app.test_request_context():
with open(os.path.join(app.root_path, 'static/index.html'), mode='rb') as f:
rv = flask.send_file(f)
rv = flask.send_file(f, mimetype='text/html')
rv.direct_passthrough = False
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()
Expand All @@ -377,17 +389,15 @@ def test_send_file_object(self):

with app.test_request_context():
with open(os.path.join(app.root_path, 'static/index.html')) as f:
rv = flask.send_file(f)
rv = flask.send_file(f, mimetype='text/html')
assert rv.mimetype == 'text/html'
assert 'x-sendfile' in rv.headers
assert rv.headers['x-sendfile'] == \
os.path.join(app.root_path, 'static/index.html')
assert 'x-sendfile' not in rv.headers
rv.close()

app.use_x_sendfile = False
with app.test_request_context():
f = StringIO('Test')
rv = flask.send_file(f)
rv = flask.send_file(f, mimetype='application/octet-stream')
rv.direct_passthrough = False
assert rv.data == b'Test'
assert rv.mimetype == 'application/octet-stream'
Expand All @@ -400,7 +410,7 @@ def __getattr__(self, name):
return getattr(self._io, name)
f = PyStringIO('Test')
f.name = 'test.txt'
rv = flask.send_file(f)
rv = flask.send_file(f, attachment_filename=f.name)
rv.direct_passthrough = False
assert rv.data == b'Test'
assert rv.mimetype == 'text/plain'
Expand All @@ -417,15 +427,16 @@ def __getattr__(self, name):

with app.test_request_context():
f = StringIO('Test')
rv = flask.send_file(f)
rv = flask.send_file(f, mimetype='text/html')
assert 'x-sendfile' not in rv.headers
rv.close()

def test_attachment(self):
app = flask.Flask(__name__)
with app.test_request_context():
with open(os.path.join(app.root_path, 'static/index.html')) as f:
rv = flask.send_file(f, as_attachment=True)
rv = flask.send_file(f, as_attachment=True,
attachment_filename='index.html')
value, options = \
parse_options_header(rv.headers['Content-Disposition'])
assert value == 'attachment'
Expand Down