aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorDavid Heinemeier Hansson <david@loudthinking.com>2017-07-03 20:14:41 +0200
committerDavid Heinemeier Hansson <david@loudthinking.com>2017-07-03 20:14:41 +0200
commitdde68d4a8b6db22054cb218871b320eddbb3c546 (patch)
tree6dff533f9e5c0dbecf115c1fab5a8ec78eeccc97 /lib
parentd2ff19c39c097aa17d16e33c8de981f43cd1ffa0 (diff)
downloadrails-dde68d4a8b6db22054cb218871b320eddbb3c546.tar.gz
rails-dde68d4a8b6db22054cb218871b320eddbb3c546.tar.bz2
rails-dde68d4a8b6db22054cb218871b320eddbb3c546.zip
Download extract from BC3
Diffstat (limited to 'lib')
-rw-r--r--lib/active_file/download.rb90
1 files changed, 90 insertions, 0 deletions
diff --git a/lib/active_file/download.rb b/lib/active_file/download.rb
new file mode 100644
index 0000000000..74f69a9dfc
--- /dev/null
+++ b/lib/active_file/download.rb
@@ -0,0 +1,90 @@
+class ActiveFile::Download
+ # Sending .ai files as application/postscript to Safari opens them in a blank, grey screen.
+ # Downloading .ai as application/postscript files in Safari appends .ps to the extension.
+ # Sending HTML, SVG, XML and SWF files as binary closes XSS vulnerabilities.
+ # Sending JS files as binary avoids InvalidCrossOriginRequest without compromising security.
+ CONTENT_TYPES_TO_RENDER_AS_BINARY = %w(
+ text/html
+ text/javascript
+ image/svg+xml
+ application/postscript
+ application/x-shockwave-flash
+ text/xml
+ application/xml
+ application/xhtml+xml
+ )
+
+ BINARY_CONTENT_TYPE = 'application/octet-stream'
+
+ def initialize(stored_file)
+ @stored_file = stored_file
+ end
+
+ def headers(force_attachment: false)
+ {
+ x_accel_redirect: '/reproxy',
+ x_reproxy_url: reproxy_url,
+ content_type: content_type,
+ content_disposition: content_disposition(force_attachment),
+ x_frame_options: 'SAMEORIGIN'
+ }
+ end
+
+ private
+ def reproxy_url
+ @stored_file.depot_location.paths.first
+ end
+
+ def content_type
+ if @stored_file.content_type.in? CONTENT_TYPES_TO_RENDER_AS_BINARY
+ BINARY_CONTENT_TYPE
+ else
+ @stored_file.content_type
+ end
+ end
+
+ def content_disposition(force_attachment = false)
+ if force_attachment || content_type == BINARY_CONTENT_TYPE
+ "attachment; #{escaped_filename}"
+ else
+ "inline; #{escaped_filename}"
+ end
+ end
+
+ # RFC2231 encoding for UTF-8 filenames, with an ASCII fallback
+ # first for unsupported browsers (IE < 9, perhaps others?).
+ # http://greenbytes.de/tech/tc2231/#encoding-2231-fb
+ def escaped_filename
+ filename = @stored_file.filename.sanitized
+ ascii_filename = encode_ascii_filename(filename)
+ utf8_filename = encode_utf8_filename(filename)
+ "#{ascii_filename}; #{utf8_filename}"
+ end
+
+ TRADITIONAL_PARAMETER_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/
+
+ def encode_ascii_filename(filename)
+ # There is no reliable way to escape special or non-Latin characters
+ # in a traditionally quoted Content-Disposition filename parameter.
+ # Settle for transliterating to ASCII, then percent-escaping special
+ # characters, excluding spaces.
+ filename = I18n.transliterate(filename)
+ filename = percent_escape(filename, TRADITIONAL_PARAMETER_ESCAPED_CHAR)
+ %(filename="#{filename}")
+ end
+
+ RFC5987_PARAMETER_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/
+
+ def encode_utf8_filename(filename)
+ # RFC2231 filename parameters can simply be percent-escaped according
+ # to RFC5987.
+ filename = percent_escape(filename, RFC5987_PARAMETER_ESCAPED_CHAR)
+ %(filename*=UTF-8''#{filename})
+ end
+
+ def percent_escape(string, pattern)
+ string.gsub(pattern) do |char|
+ char.bytes.map { |byte| "%%%02X" % byte }.join("")
+ end
+ end
+end