Skip to content

Commit c1a08e7

Browse files
committed
generate safe file urls relative to the shard of the files domain
allows it to be a different shard than where the file is. I had to remove type casting from dynamic finders that don't know how to deal with non-integral global ids. also cache s3 urls on the same shard as the attachment test plan: * have multiple shards and S3 storage * have a single safe files domain * you should be able to upload and download files in all shards * verify that it's going against the files domain, not the normal domain Change-Id: I2b498fc1df20d5b43bf20f702580451621eeaf6a Reviewed-on: https://gerrit.instructure.com/15158 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Brian Palmer <brianp@instructure.com>
1 parent 92b914c commit c1a08e7

9 files changed

Lines changed: 69 additions & 77 deletions

File tree

app/controllers/application_controller.rb

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,40 +1052,47 @@ def conversations_path(params={})
10521052

10531053
# escape everything but slashes, see http://code.google.com/p/phusion-passenger/issues/detail?id=113
10541054
FILE_PATH_ESCAPE_PATTERN = Regexp.new("[^#{URI::PATTERN::UNRESERVED}/]")
1055-
def safe_domain_file_url(attachment, host=nil, verifier = nil, download = false) # TODO: generalize this
1056-
res = "#{request.protocol}#{host || HostUrl.file_host(@domain_root_account || Account.default, request.host_with_port)}"
1057-
ts, sig = @current_user && @current_user.access_verifier
1058-
1059-
# add parameters so that the other domain can create a session that
1060-
# will authorize file access but not full app access. We need this in
1061-
# case there are relative URLs in the file that point to other pieces
1062-
# of content.
1063-
opts = { :user_id => @current_user.try(:id), :ts => ts, :sf_verifier => sig }
1064-
opts[:verifier] = verifier if verifier.present?
1065-
1066-
if download
1067-
# download "for realz, dude" (see later comments about :download)
1068-
opts[:download_frd] = 1
1069-
else
1070-
# don't set :download here, because file_download_url won't like it. see
1071-
# comment below for why we'd want to set :download
1072-
opts[:inline] = 1
1055+
def safe_domain_file_url(attachment, host_and_shard=nil, verifier = nil, download = false) # TODO: generalize this
1056+
if !host_and_shard
1057+
host_and_shard = HostUrl.file_host_with_shard(@domain_root_account || Account.default, request.host_with_port)
10731058
end
1059+
host, shard = host_and_shard
1060+
res = "#{request.protocol}#{host}"
1061+
1062+
shard.activate do
1063+
ts, sig = @current_user && @current_user.access_verifier
1064+
1065+
# add parameters so that the other domain can create a session that
1066+
# will authorize file access but not full app access. We need this in
1067+
# case there are relative URLs in the file that point to other pieces
1068+
# of content.
1069+
opts = { :user_id => @current_user.try(:id), :ts => ts, :sf_verifier => sig }
1070+
opts[:verifier] = verifier if verifier.present?
1071+
1072+
if download
1073+
# download "for realz, dude" (see later comments about :download)
1074+
opts[:download_frd] = 1
1075+
else
1076+
# don't set :download here, because file_download_url won't like it. see
1077+
# comment below for why we'd want to set :download
1078+
opts[:inline] = 1
1079+
end
10741080

1075-
if @context && Attachment.relative_context?(@context.class.base_ar_class) && @context == attachment.context
1076-
# so yeah, this is right. :inline=>1 wants :download=>1 to go along with
1077-
# it, so we're setting :download=>1 *because* we want to display inline.
1078-
opts[:download] = 1 unless download
1079-
1080-
# if the context is one that supports relative paths (which requires extra
1081-
# routes and stuff), then we'll build an actual named_context_url with the
1082-
# params for show_relative
1083-
res += named_context_url(@context, :context_file_url, attachment)
1084-
res += '/' + URI.escape(attachment.full_display_path, FILE_PATH_ESCAPE_PATTERN)
1085-
res += '?' + opts.to_query
1086-
else
1087-
# otherwise, just redirect to /files/:id
1088-
res += file_download_url(attachment, opts.merge(:only_path => true))
1081+
if @context && Attachment.relative_context?(@context.class.base_ar_class) && @context == attachment.context
1082+
# so yeah, this is right. :inline=>1 wants :download=>1 to go along with
1083+
# it, so we're setting :download=>1 *because* we want to display inline.
1084+
opts[:download] = 1 unless download
1085+
1086+
# if the context is one that supports relative paths (which requires extra
1087+
# routes and stuff), then we'll build an actual named_context_url with the
1088+
# params for show_relative
1089+
res += named_context_url(@context, :context_file_url, attachment)
1090+
res += '/' + URI.escape(attachment.full_display_path, FILE_PATH_ESCAPE_PATTERN)
1091+
res += '?' + opts.to_query
1092+
else
1093+
# otherwise, just redirect to /files/:id
1094+
res += file_download_url(attachment, opts.merge(:only_path => true))
1095+
end
10891096
end
10901097

10911098
res

app/controllers/files_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ def show_relative
342342
# won't be able to access or update data with AJAX requests.
343343
def safer_domain_available?
344344
if !@files_domain && request.host_with_port != HostUrl.file_host(@domain_root_account, request.host_with_port)
345-
@safer_domain_host = HostUrl.file_host(@domain_root_account, request.host_with_port)
345+
@safer_domain_host = HostUrl.file_host_with_shard(@domain_root_account, request.host_with_port)
346346
end
347347
!!@safer_domain_host
348348
end

app/models/attachment.rb

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -963,24 +963,26 @@ def cacheable_s3_inline_url
963963
end
964964

965965
def cacheable_s3_urls
966-
Rails.cache.fetch(['cacheable_s3_urls', self].cache_key, :expires_in => 24.hours) do
967-
ascii_filename = Iconv.conv("ASCII//TRANSLIT//IGNORE", "UTF-8", display_name)
968-
969-
# response-content-disposition will be url encoded in the depths of
970-
# aws-s3, doesn't need to happen here. we'll be nice and ghetto http
971-
# quote the filename string, though.
972-
quoted_ascii = ascii_filename.gsub(/([\x00-\x1f"\x7f])/, '\\\\\\1')
973-
974-
# awesome browsers will use the filename* and get the proper unicode filename,
975-
# everyone else will get the sanitized ascii version of the filename
976-
quoted_unicode = "UTF-8''#{URI.escape(display_name, /[^A-Za-z0-9.]/)}"
977-
filename = %(filename="#{quoted_ascii}"; filename*=#{quoted_unicode})
978-
979-
# we need to have versions of the url for each content-disposition
980-
{
981-
'inline' => authenticated_s3_url(:expires_in => 6.days, "response-content-disposition" => "inline; " + filename),
982-
'attachment' => authenticated_s3_url(:expires_in => 6.days, "response-content-disposition" => "attachment; " + filename)
983-
}
966+
self.shard.activate do
967+
Rails.cache.fetch(['cacheable_s3_urls', self].cache_key, :expires_in => 24.hours) do
968+
ascii_filename = Iconv.conv("ASCII//TRANSLIT//IGNORE", "UTF-8", display_name)
969+
970+
# response-content-disposition will be url encoded in the depths of
971+
# aws-s3, doesn't need to happen here. we'll be nice and ghetto http
972+
# quote the filename string, though.
973+
quoted_ascii = ascii_filename.gsub(/([\x00-\x1f"\x7f])/, '\\\\\\1')
974+
975+
# awesome browsers will use the filename* and get the proper unicode filename,
976+
# everyone else will get the sanitized ascii version of the filename
977+
quoted_unicode = "UTF-8''#{URI.escape(display_name, /[^A-Za-z0-9.]/)}"
978+
filename = %(filename="#{quoted_ascii}"; filename*=#{quoted_unicode})
979+
980+
# we need to have versions of the url for each content-disposition
981+
{
982+
'inline' => authenticated_s3_url(:expires_in => 6.days, "response-content-disposition" => "inline; " + filename),
983+
'attachment' => authenticated_s3_url(:expires_in => 6.days, "response-content-disposition" => "attachment; " + filename)
984+
}
985+
end
984986
end
985987
end
986988
protected :cacheable_s3_urls

config/environments/development.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
# Raise an exception on finder type mismatch or nil arguments. Helps us catch
2828
# these bugs before they hit.
2929
Canvas.dynamic_finder_nil_arguments_error = :raise
30-
Canvas.dynamic_finder_type_cast_error = :raise
3130
end
3231

3332
# initialize cache store

config/environments/test.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
# Raise an exception on finder type mismatch or nil arguments. Helps us catch
3737
# these bugs before they hit.
3838
Canvas.dynamic_finder_nil_arguments_error = :raise
39-
Canvas.dynamic_finder_type_cast_error = :raise
4039

4140
# eval <env>-local.rb if it exists
4241
Dir[File.dirname(__FILE__) + "/" + File.basename(__FILE__, ".rb") + "-*.rb"].each { |localfile| eval(File.new(localfile).read) }

config/initializers/active_record.rb

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -285,16 +285,7 @@ class DynamicFinderTypeError < Exception; end
285285
class << self
286286
def construct_attributes_from_arguments_with_type_cast(attribute_names, arguments)
287287
log_dynamic_finder_nil_arguments(attribute_names) if current_scoped_methods.nil? && arguments.flatten.compact.empty?
288-
attributes = construct_attributes_from_arguments_without_type_cast(attribute_names, arguments)
289-
attributes.each_pair do |attribute, value|
290-
next unless column = columns.detect{ |col| col.name == attribute.to_s }
291-
next if [value].flatten.compact.empty?
292-
cast_value = [value].flatten.map{ |v| v.respond_to?(:quoted_id) ? v : column.type_cast(v) }
293-
cast_value = cast_value.first unless value.is_a?(Array)
294-
next if [value].flatten.map(&:to_s) == [cast_value].flatten.map(&:to_s)
295-
log_dynamic_finder_type_cast(value, column)
296-
attributes[attribute] = cast_value
297-
end
288+
construct_attributes_from_arguments_without_type_cast(attribute_names, arguments)
298289
end
299290
alias_method_chain :construct_attributes_from_arguments, :type_cast
300291

@@ -303,12 +294,6 @@ def log_dynamic_finder_nil_arguments(attribute_names)
303294
raise DynamicFinderTypeError, error if Canvas.dynamic_finder_nil_arguments_error == :raise
304295
logger.debug "WARNING: " + error
305296
end
306-
307-
def log_dynamic_finder_type_cast(value, column)
308-
error = "Cannot cleanly cast #{value.inspect} to #{column.type} (#{self.base_class}\##{column.name})"
309-
raise DynamicFinderTypeError, error if Canvas.dynamic_finder_type_cast_error == :raise
310-
logger.debug "WARNING: " + error
311-
end
312297
end
313298

314299
def self.merge_includes(first, second)

lib/canvas.rb

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,6 @@ module Canvas
44
# this to :raise to raise an exception.
55
mattr_accessor :protected_attribute_error
66

7-
# defines the behavior around casting arguments passed into dynamic finders.
8-
# Arguments are coerced to the appropriate type (if the column exists), so
9-
# things like find_by_id('123') become find_by_id(123). The default (:log)
10-
# logs a warning if the cast isn't clean (e.g. '123a' -> 123 or '' -> 0).
11-
# Set this to :raise to raise an error on unclean casts.
12-
mattr_accessor :dynamic_finder_type_cast_error
13-
147
# defines the behavior when nil or empty array arguments passed into dynamic
158
# finders. The default (:log) logs a warning if the finder is not scoped and
169
# if *all* arguments are nil/[], e.g.

lib/host_url.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ def file_host(account, current_host = nil)
7979
res ||= @@file_host = default_host
8080
end
8181

82+
def file_host_with_shard(account, current_host = nil)
83+
[file_host(account, current_host), Shard.default]
84+
end
85+
8286
def cdn_host
8387
# by default only set it for development. useful so that gravatar can
8488
# proxy our fallback urls

spec/controllers/application_controller_spec.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@
8989
@controller.expects(:file_download_url).
9090
with(@attachment, @common_params.merge(:inline => 1)).
9191
returns('')
92-
@controller.send(:safe_domain_file_url, @attachment)
92+
HostUrl.expects(:file_host_with_shard).with(42, '').returns(['myfiles', Shard.default])
93+
@controller.instance_variable_set(:@domain_root_account, 42)
94+
url = @controller.send(:safe_domain_file_url, @attachment)
95+
url.should match /myfiles/
9396
end
9497

9598
it "should include :download=>1 in inline urls for relative contexts" do

0 commit comments

Comments
 (0)