discourse/app/controllers/user_avatars_controller.rb

222 lines
6.3 KiB
Ruby

# frozen_string_literal: true
class UserAvatarsController < ApplicationController
skip_before_action :preload_json,
:redirect_to_login_if_required,
:check_xhr,
:verify_authenticity_token,
only: %i[show show_letter show_proxy_letter]
before_action :apply_cdn_headers, only: %i[show show_letter show_proxy_letter]
def refresh_gravatar
user = User.find_by(username_lower: params[:username].downcase)
guardian.ensure_can_edit!(user)
if user
hijack do
user.create_user_avatar(user_id: user.id) unless user.user_avatar
user.user_avatar.update_gravatar!
gravatar =
if user.user_avatar.gravatar_upload_id
{
gravatar_upload_id: user.user_avatar.gravatar_upload_id,
gravatar_avatar_template:
User.avatar_template(user.username, user.user_avatar.gravatar_upload_id),
}
else
{ gravatar_upload_id: nil, gravatar_avatar_template: nil }
end
render json: gravatar
end
else
raise Discourse::NotFound
end
end
def show_proxy_letter
is_asset_path
if SiteSetting.external_system_avatars_url !~ %r{\A/letter_avatar_proxy}
raise Discourse::NotFound
end
params.require(:letter)
params.require(:color)
params.require(:version)
params.require(:size)
hijack do
begin
proxy_avatar(
"https://avatars.discourse-cdn.com/#{params[:version]}/letter/#{params[:letter]}/#{params[:color]}/#{params[:size]}.png",
Time.new(1990, 01, 01),
)
rescue OpenURI::HTTPError
render_blank
end
end
end
def show_letter
is_asset_path
params.require(:username)
params.require(:version)
params.require(:size)
no_cookies
return render_blank if params[:version] != LetterAvatar.version
hijack do
image = LetterAvatar.generate(params[:username].to_s, params[:size].to_i)
response.headers["Last-Modified"] = File.ctime(image).httpdate
response.headers["Content-Length"] = File.size(image).to_s
immutable_for(1.year)
send_file image, disposition: nil
end
end
def show
is_asset_path
# we need multisite support to keep a single origin pull for CDNs
RailsMultisite::ConnectionManagement.with_hostname(params[:hostname]) do
hijack { show_in_site(params[:hostname]) }
end
end
protected
def show_in_site(hostname)
username = params[:username].to_s
return render_blank unless user = User.find_by(username_lower: username.downcase)
upload_id, version = params[:version].split("_")
version = (version || OptimizedImage::VERSION).to_i
# old versions simply get new avatar
return render_blank if version > OptimizedImage::VERSION
upload_id = upload_id.to_i
return render_blank if upload_id <= 0
size = params[:size].to_i
return render_blank if size < 8 || size > 1000
if !Discourse.avatar_sizes.include?(size) && Discourse.store.external?
closest = Discourse.avatar_sizes.to_a.min { |a, b| (size - a).abs <=> (size - b).abs }
avatar_url =
UserAvatar.local_avatar_url(
hostname,
user.encoded_username(lower: true),
upload_id,
closest,
)
return redirect_to cdn_path(avatar_url), allow_other_host: true
end
upload = Upload.find_by(id: upload_id) if user&.user_avatar&.contains_upload?(upload_id)
upload ||= user.uploaded_avatar if user.uploaded_avatar_id == upload_id
if user.uploaded_avatar && !upload
avatar_url =
UserAvatar.local_avatar_url(
hostname,
user.encoded_username(lower: true),
user.uploaded_avatar_id,
size,
)
return redirect_to cdn_path(avatar_url), allow_other_host: true
elsif upload && optimized = get_optimized_image(upload, size)
if optimized.local?
optimized_path = Discourse.store.path_for(optimized)
image = optimized_path if File.exist?(optimized_path)
elsif GlobalSetting.redirect_avatar_requests
return redirect_s3_avatar(Discourse.store.cdn_url(optimized.url))
else
return proxy_avatar(Discourse.store.cdn_url(optimized.url), upload.created_at)
end
end
if image
response.headers["Last-Modified"] = File.ctime(image).httpdate
response.headers["Content-Length"] = File.size(image).to_s
immutable_for 1.year
send_file image, disposition: nil
else
render_blank
end
rescue OpenURI::HTTPError
render_blank
end
# Allow plugins to overwrite max file size value
def max_file_size
1.megabyte
end
PROXY_PATH = Rails.root + "tmp/avatar_proxy"
def proxy_avatar(url, last_modified)
url = (SiteSetting.force_https ? "https:" : "http:") + url if url[0..1] == "//"
sha = Digest::SHA1.hexdigest(url)
filename = "#{sha}#{File.extname(url)}"
path = "#{PROXY_PATH}/#{filename}"
unless File.exist? path
FileUtils.mkdir_p PROXY_PATH
tmp =
FileHelper.download(
url,
max_file_size: max_file_size,
tmp_file_name: filename,
follow_redirect: true,
read_timeout: 10,
)
return render_blank if tmp.nil?
FileUtils.mv tmp.path, path
end
response.headers["Last-Modified"] = last_modified.httpdate
response.headers["Content-Length"] = File.size(path).to_s
immutable_for(1.year)
send_file path, disposition: nil
end
def redirect_s3_avatar(url)
response.cache_control[:max_age] = 1.hour.to_i
response.cache_control[:public] = true
response.cache_control[:extras] = ["immutable", "stale-while-revalidate=#{1.day.to_i}"]
redirect_to url, allow_other_host: true
end
# this protects us from a DoS
def render_blank
path = Rails.root + "public/images/avatar.png"
expires_in 10.minutes, public: true
response.headers["Last-Modified"] = Time.new(1990, 01, 01).httpdate
response.headers["Content-Length"] = File.size(path).to_s
send_file path, disposition: nil
end
protected
# consider removal of hacks some time in 2019
def get_optimized_image(upload, size)
return if !upload
return upload if upload.extension == "svg"
upload.get_optimized_image(size, size)
# TODO decide if we want to detach here
end
end