2019-05-02 17:17:27 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2014-05-22 02:37:02 -05:00
|
|
|
class UserAvatar < ActiveRecord::Base
|
|
|
|
belongs_to :user
|
2023-01-09 06:20:10 -06:00
|
|
|
belongs_to :gravatar_upload, class_name: "Upload"
|
|
|
|
belongs_to :custom_upload, class_name: "Upload"
|
2022-06-08 18:24:30 -05:00
|
|
|
has_many :upload_references, as: :target, dependent: :destroy
|
|
|
|
|
|
|
|
after_save do
|
|
|
|
if saved_change_to_custom_upload_id? || saved_change_to_gravatar_upload_id?
|
|
|
|
upload_ids = [self.custom_upload_id, self.gravatar_upload_id]
|
|
|
|
UploadReference.ensure_exist!(upload_ids: upload_ids, target: self)
|
|
|
|
end
|
|
|
|
end
|
2014-05-22 02:37:02 -05:00
|
|
|
|
2020-05-22 23:56:13 -05:00
|
|
|
@@custom_user_gravatar_email_hash = {
|
2023-01-09 06:20:10 -06:00
|
|
|
Discourse::SYSTEM_USER_ID => User.email_hash("info@discourse.org"),
|
2020-05-22 23:56:13 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
def self.register_custom_user_gravatar_email_hash(user_id, email)
|
|
|
|
@@custom_user_gravatar_email_hash[user_id] = User.email_hash(email)
|
|
|
|
end
|
|
|
|
|
2014-05-22 02:37:02 -05:00
|
|
|
def contains_upload?(id)
|
2014-05-29 23:17:35 -05:00
|
|
|
gravatar_upload_id == id || custom_upload_id == id
|
2014-05-22 02:37:02 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def update_gravatar!
|
2015-09-17 12:42:44 -05:00
|
|
|
DistributedMutex.synchronize("update_gravatar_#{user_id}") do
|
2015-05-06 18:00:13 -05:00
|
|
|
begin
|
2020-05-22 23:56:13 -05:00
|
|
|
self.update!(last_gravatar_download_attempt: Time.zone.now)
|
2015-05-06 18:00:13 -05:00
|
|
|
|
2015-05-29 11:09:47 -05:00
|
|
|
max = Discourse.avatar_sizes.max
|
2019-03-08 12:39:56 -06:00
|
|
|
|
|
|
|
# The user could be deleted before this executes
|
|
|
|
return if user.blank? || user.primary_email.blank?
|
|
|
|
|
2020-05-22 23:56:13 -05:00
|
|
|
email_hash = @@custom_user_gravatar_email_hash[user_id] || user.email_hash
|
2023-01-09 06:20:10 -06:00
|
|
|
gravatar_url =
|
|
|
|
"https://#{SiteSetting.gravatar_base_url}/avatar/#{email_hash}.png?s=#{max}&d=404&reset_cache=#{SecureRandom.urlsafe_base64(5)}"
|
2017-09-28 01:35:27 -05:00
|
|
|
|
2021-06-04 07:13:58 -05:00
|
|
|
if SiteSetting.verbose_upload_logging
|
|
|
|
Rails.logger.warn("Verbose Upload Logging: Downloading gravatar from #{gravatar_url}")
|
|
|
|
end
|
|
|
|
|
2017-09-28 01:35:27 -05:00
|
|
|
# follow redirects in case gravatar change rules on us
|
2023-01-09 06:20:10 -06:00
|
|
|
tempfile =
|
|
|
|
FileHelper.download(
|
|
|
|
gravatar_url,
|
|
|
|
max_file_size: SiteSetting.max_image_size_kb.kilobytes,
|
|
|
|
tmp_file_name: "gravatar",
|
|
|
|
skip_rate_limit: true,
|
|
|
|
verbose: false,
|
|
|
|
follow_redirect: true,
|
|
|
|
)
|
2017-09-28 01:35:27 -05:00
|
|
|
|
2017-05-24 12:29:50 -05:00
|
|
|
if tempfile
|
2018-07-29 21:48:44 -05:00
|
|
|
ext = File.extname(tempfile)
|
2023-01-09 06:20:10 -06:00
|
|
|
ext = ".png" if ext.blank?
|
2018-07-29 21:48:44 -05:00
|
|
|
|
2023-01-09 06:20:10 -06:00
|
|
|
upload =
|
|
|
|
UploadCreator.new(
|
|
|
|
tempfile,
|
|
|
|
"gravatar#{ext}",
|
|
|
|
origin: gravatar_url,
|
|
|
|
type: "avatar",
|
|
|
|
for_gravatar: true,
|
|
|
|
).create_for(user_id)
|
2017-05-24 12:29:50 -05:00
|
|
|
|
2018-10-18 04:02:54 -05:00
|
|
|
if gravatar_upload_id != upload.id
|
2018-08-05 21:42:03 -05:00
|
|
|
User.transaction do
|
|
|
|
if gravatar_upload_id && user.uploaded_avatar_id == gravatar_upload_id
|
2018-10-18 04:02:54 -05:00
|
|
|
user.update!(uploaded_avatar_id: upload.id)
|
2018-08-05 21:42:03 -05:00
|
|
|
end
|
|
|
|
|
2018-10-18 20:51:34 -05:00
|
|
|
self.update!(gravatar_upload: upload)
|
2018-08-05 21:42:03 -05:00
|
|
|
end
|
2017-05-24 12:29:50 -05:00
|
|
|
end
|
2015-05-06 18:00:13 -05:00
|
|
|
end
|
2018-10-22 19:43:14 -05:00
|
|
|
rescue OpenURI::HTTPError => e
|
2023-12-06 16:25:00 -06:00
|
|
|
raise e if e.io&.status&.[](0).to_i != 404
|
2015-05-06 18:00:13 -05:00
|
|
|
ensure
|
2018-10-18 20:51:34 -05:00
|
|
|
tempfile&.close!
|
2015-05-06 18:00:13 -05:00
|
|
|
end
|
2014-05-26 04:46:43 -05:00
|
|
|
end
|
2014-05-22 02:37:02 -05:00
|
|
|
end
|
|
|
|
|
2015-05-29 11:51:17 -05:00
|
|
|
def self.local_avatar_url(hostname, username, upload_id, size)
|
2015-09-11 08:18:17 -05:00
|
|
|
self.local_avatar_template(hostname, username, upload_id).gsub("{size}", size.to_s)
|
2015-05-29 11:51:17 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.local_avatar_template(hostname, username, upload_id)
|
|
|
|
version = self.version(upload_id)
|
2020-10-09 06:51:24 -05:00
|
|
|
"#{Discourse.base_path}/user_avatar/#{hostname}/#{username}/{size}/#{version}.png"
|
2015-05-29 11:51:17 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.external_avatar_url(user_id, upload_id, size)
|
2015-09-11 08:18:17 -05:00
|
|
|
self.external_avatar_template(user_id, upload_id).gsub("{size}", size.to_s)
|
2015-05-29 11:51:17 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.external_avatar_template(user_id, upload_id)
|
|
|
|
version = self.version(upload_id)
|
|
|
|
"#{Discourse.store.absolute_base_url}/avatars/#{user_id}/{size}/#{version}.png"
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.version(upload_id)
|
|
|
|
"#{upload_id}_#{OptimizedImage::VERSION}"
|
|
|
|
end
|
|
|
|
|
2017-07-27 20:20:09 -05:00
|
|
|
def self.import_url_for_user(avatar_url, user, options = nil)
|
2021-06-04 07:13:58 -05:00
|
|
|
if SiteSetting.verbose_upload_logging
|
|
|
|
Rails.logger.warn("Verbose Upload Logging: Downloading sso-avatar from #{avatar_url}")
|
|
|
|
end
|
|
|
|
|
2023-01-09 06:20:10 -06:00
|
|
|
tempfile =
|
|
|
|
FileHelper.download(
|
|
|
|
avatar_url,
|
|
|
|
max_file_size: SiteSetting.max_image_size_kb.kilobytes,
|
|
|
|
tmp_file_name: "sso-avatar",
|
|
|
|
follow_redirect: true,
|
2023-08-07 05:23:58 -05:00
|
|
|
skip_rate_limit: !!options&.fetch(:skip_rate_limit, false),
|
2023-01-09 06:20:10 -06:00
|
|
|
)
|
2015-06-23 14:59:50 -05:00
|
|
|
|
2017-05-25 22:33:12 -05:00
|
|
|
return unless tempfile
|
|
|
|
|
2020-05-06 10:28:02 -05:00
|
|
|
ext = FastImage.type(tempfile).to_s
|
|
|
|
tempfile.rewind
|
2015-06-23 14:59:50 -05:00
|
|
|
|
2023-01-09 06:20:10 -06:00
|
|
|
upload =
|
|
|
|
UploadCreator.new(
|
|
|
|
tempfile,
|
|
|
|
"external-avatar." + ext,
|
|
|
|
origin: avatar_url,
|
|
|
|
type: "avatar",
|
|
|
|
).create_for(user.id)
|
2015-06-23 14:59:50 -05:00
|
|
|
|
2018-08-23 02:36:34 -05:00
|
|
|
user.create_user_avatar! unless user.user_avatar
|
2015-06-23 14:59:50 -05:00
|
|
|
|
|
|
|
if !user.user_avatar.contains_upload?(upload.id)
|
2018-08-23 02:36:34 -05:00
|
|
|
user.user_avatar.update!(custom_upload_id: upload.id)
|
2020-05-06 10:28:02 -05:00
|
|
|
override_gravatar = !options || options[:override_gravatar]
|
2016-09-19 00:10:02 -05:00
|
|
|
|
|
|
|
if user.uploaded_avatar_id.nil? ||
|
2023-01-09 06:20:10 -06:00
|
|
|
!user.user_avatar.contains_upload?(user.uploaded_avatar_id) || override_gravatar
|
2018-08-23 02:36:34 -05:00
|
|
|
user.update!(uploaded_avatar_id: upload.id)
|
2016-09-19 00:10:02 -05:00
|
|
|
end
|
2015-06-23 14:59:50 -05:00
|
|
|
end
|
2023-05-12 02:32:02 -05:00
|
|
|
rescue Net::ReadTimeout, OpenURI::HTTPError, FinalDestination::SSRFError
|
|
|
|
# Skip saving. We are not connected to the net, or SSRF checks failed.
|
2020-05-06 10:28:02 -05:00
|
|
|
ensure
|
|
|
|
tempfile.close! if tempfile && tempfile.respond_to?(:close!)
|
2015-06-23 14:59:50 -05:00
|
|
|
end
|
|
|
|
|
2023-05-31 19:00:01 -05:00
|
|
|
def self.ensure_consistency!(max_optimized_avatars_to_remove: 20_000)
|
2018-08-30 23:46:22 -05:00
|
|
|
DB.exec <<~SQL
|
|
|
|
UPDATE user_avatars
|
|
|
|
SET gravatar_upload_id = NULL
|
|
|
|
WHERE gravatar_upload_id IN (
|
|
|
|
SELECT u1.gravatar_upload_id FROM user_avatars u1
|
|
|
|
LEFT JOIN uploads up
|
|
|
|
ON u1.gravatar_upload_id = up.id
|
|
|
|
WHERE u1.gravatar_upload_id IS NOT NULL AND
|
|
|
|
up.id IS NULL
|
|
|
|
)
|
|
|
|
SQL
|
|
|
|
|
|
|
|
DB.exec <<~SQL
|
|
|
|
UPDATE user_avatars
|
|
|
|
SET custom_upload_id = NULL
|
|
|
|
WHERE custom_upload_id IN (
|
|
|
|
SELECT u1.custom_upload_id FROM user_avatars u1
|
|
|
|
LEFT JOIN uploads up
|
|
|
|
ON u1.custom_upload_id = up.id
|
|
|
|
WHERE u1.custom_upload_id IS NOT NULL AND
|
|
|
|
up.id IS NULL
|
|
|
|
)
|
|
|
|
SQL
|
2023-05-31 19:00:01 -05:00
|
|
|
|
|
|
|
ids =
|
|
|
|
DB.query_single(<<~SQL, sizes: Discourse.avatar_sizes, limit: max_optimized_avatars_to_remove)
|
2024-01-22 11:33:39 -06:00
|
|
|
SELECT oi.id FROM (
|
|
|
|
SELECT custom_upload_id FROM user_avatars
|
|
|
|
EXCEPT
|
|
|
|
SELECT upload_id FROM upload_references WHERE target_type <> 'UserAvatar'
|
|
|
|
AND upload_id IS NOT NULL
|
|
|
|
) AS a
|
2023-05-31 19:00:01 -05:00
|
|
|
JOIN optimized_images oi ON oi.upload_id = a.custom_upload_id
|
2024-01-22 11:33:39 -06:00
|
|
|
WHERE oi.width not in (:sizes) AND oi.height not in (:sizes)
|
2023-05-31 19:00:01 -05:00
|
|
|
LIMIT :limit
|
|
|
|
SQL
|
|
|
|
|
|
|
|
warnings_reported = 0
|
|
|
|
|
|
|
|
ids.each do |id|
|
|
|
|
begin
|
|
|
|
OptimizedImage.find(id).destroy!
|
|
|
|
rescue ActiveRecord::RecordNotFound
|
|
|
|
rescue => e
|
|
|
|
if warnings_reported < 10
|
|
|
|
Discourse.warn_exception(e, message: "Failed to remove optimized image")
|
|
|
|
warnings_reported += 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2018-08-30 23:46:22 -05:00
|
|
|
end
|
2014-05-22 02:37:02 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: user_avatars
|
|
|
|
#
|
|
|
|
# id :integer not null, primary key
|
|
|
|
# user_id :integer not null
|
|
|
|
# custom_upload_id :integer
|
|
|
|
# gravatar_upload_id :integer
|
|
|
|
# last_gravatar_download_attempt :datetime
|
2014-08-27 00:19:25 -05:00
|
|
|
# created_at :datetime not null
|
|
|
|
# updated_at :datetime not null
|
2014-05-27 20:50:49 -05:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
2016-11-23 20:13:03 -06:00
|
|
|
# index_user_avatars_on_custom_upload_id (custom_upload_id)
|
|
|
|
# index_user_avatars_on_gravatar_upload_id (gravatar_upload_id)
|
|
|
|
# index_user_avatars_on_user_id (user_id)
|
2014-05-22 02:37:02 -05:00
|
|
|
#
|