diff options
author | Fumiaki MATSUSHIMA <mtsmfm@gmail.com> | 2018-09-09 16:35:48 +0900 |
---|---|---|
committer | Fumiaki MATSUSHIMA <mtsmfm@gmail.com> | 2018-09-13 21:38:46 +0900 |
commit | 890485cfce4c361c03a41ec23b0ba187007818cc (patch) | |
tree | 1bcf2bccc7cdb710511929dbcb4b68dd602df7f3 /actionpack | |
parent | 823f9e0a89707561b54196bf4aabe20c5edb88c1 (diff) | |
download | rails-890485cfce4c361c03a41ec23b0ba187007818cc.tar.gz rails-890485cfce4c361c03a41ec23b0ba187007818cc.tar.bz2 rails-890485cfce4c361c03a41ec23b0ba187007818cc.zip |
Encode Content-Disposition filenames on send_data and send_file
Diffstat (limited to 'actionpack')
-rw-r--r-- | actionpack/CHANGELOG.md | 12 | ||||
-rw-r--r-- | actionpack/lib/action_controller/metal/data_streaming.rb | 7 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/content_disposition.rb | 45 | ||||
-rw-r--r-- | actionpack/test/controller/send_file_test.rb | 4 | ||||
-rw-r--r-- | actionpack/test/dispatch/content_disposition_test.rb | 37 |
5 files changed, 99 insertions, 6 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 7781980cab..dfe6e00865 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,15 @@ +* Encode Content-Disposition filenames on `send_data` and `send_file`. + Previously, `send_data 'data', filename: "\u{3042}.txt"` sends + `"filename=\"\u{3042}.txt\""` as Content-Disposition and it can be + garbled. + Now it follows [RFC 2231](https://tools.ietf.org/html/rfc2231) and + [RFC 5987](https://tools.ietf.org/html/rfc5987) and sends + `"filename=\"%3F.txt\"; filename*=UTF-8''%E3%81%82.txt"`. + Most browsers can find filename correctly and old browsers fallback to ASCII + converted name. + + *Fumiaki Matsushima* + * Expose `ActionController::Parameters#each_key` which allows iterating over keys without allocating an array. diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 5a82ccf668..5140a667de 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "action_controller/metal/exceptions" +require "action_dispatch/http/content_disposition" module ActionController #:nodoc: # Methods for sending arbitrary data and for streaming files to the browser, @@ -132,10 +133,8 @@ module ActionController #:nodoc: end disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION) - unless disposition.nil? - disposition = disposition.to_s - disposition += %(; filename="#{options[:filename]}") if options[:filename] - headers["Content-Disposition"] = disposition + if disposition + headers["Content-Disposition"] = ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: options[:filename]) end headers["Content-Transfer-Encoding"] = "binary" diff --git a/actionpack/lib/action_dispatch/http/content_disposition.rb b/actionpack/lib/action_dispatch/http/content_disposition.rb new file mode 100644 index 0000000000..58164c1522 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/content_disposition.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module ActionDispatch + module Http + class ContentDisposition # :nodoc: + def self.format(disposition:, filename:) + new(disposition: disposition, filename: filename).to_s + end + + attr_reader :disposition, :filename + + def initialize(disposition:, filename:) + @disposition = disposition + @filename = filename + end + + TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/ + + def ascii_filename + 'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"' + end + + RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/ + + def utf8_filename + "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR) + end + + def to_s + if filename + "#{disposition}; #{ascii_filename}; #{utf8_filename}" + else + "#{disposition}" + end + end + + private + def percent_escape(string, pattern) + string.gsub(pattern) do |char| + char.bytes.map { |byte| "%%%02X" % byte }.join + end + end + end + end +end diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb index 7b1a52b277..c917cdf761 100644 --- a/actionpack/test/controller/send_file_test.rb +++ b/actionpack/test/controller/send_file_test.rb @@ -144,7 +144,7 @@ class SendFileTest < ActionController::TestCase get :test_send_file_headers_bang assert_equal "image/png", response.content_type - assert_equal 'disposition; filename="filename"', response.get_header("Content-Disposition") + assert_equal %(disposition; filename="filename"; filename*=UTF-8''filename), response.get_header("Content-Disposition") assert_equal "binary", response.get_header("Content-Transfer-Encoding") assert_equal "private", response.get_header("Cache-Control") end @@ -153,7 +153,7 @@ class SendFileTest < ActionController::TestCase def test_send_file_headers_with_disposition_as_a_symbol get :test_send_file_headers_with_disposition_as_a_symbol - assert_equal 'disposition; filename="filename"', response.get_header("Content-Disposition") + assert_equal %(disposition; filename="filename"; filename*=UTF-8''filename), response.get_header("Content-Disposition") end def test_send_file_headers_with_mime_lookup_with_symbol diff --git a/actionpack/test/dispatch/content_disposition_test.rb b/actionpack/test/dispatch/content_disposition_test.rb new file mode 100644 index 0000000000..3f5959da6e --- /dev/null +++ b/actionpack/test/dispatch/content_disposition_test.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module ActionDispatch + class ContentDispositionTest < ActiveSupport::TestCase + test "encoding a Latin filename" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: "racecar.jpg") + + assert_equal %(filename="racecar.jpg"), disposition.ascii_filename + assert_equal "filename*=UTF-8''racecar.jpg", disposition.utf8_filename + assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s + end + + test "encoding a Latin filename with accented characters" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: "råcëçâr.jpg") + + assert_equal %(filename="racecar.jpg"), disposition.ascii_filename + assert_equal "filename*=UTF-8''r%C3%A5c%C3%AB%C3%A7%C3%A2r.jpg", disposition.utf8_filename + assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s + end + + test "encoding a non-Latin filename" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: "автомобиль.jpg") + + assert_equal %(filename="%3F%3F%3F%3F%3F%3F%3F%3F%3F%3F.jpg"), disposition.ascii_filename + assert_equal "filename*=UTF-8''%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%BE%D0%B1%D0%B8%D0%BB%D1%8C.jpg", disposition.utf8_filename + assert_equal "inline; #{disposition.ascii_filename}; #{disposition.utf8_filename}", disposition.to_s + end + + test "without filename" do + disposition = Http::ContentDisposition.new(disposition: :inline, filename: nil) + + assert_equal "inline", disposition.to_s + end + end +end |