# frozen_string_literal: true class UserProfile < ActiveRecord::Base # TODO Remove `badge_granted_title` after 2023-09-01 self.ignored_columns = ["badge_granted_title"] BAKED_VERSION = 1 belongs_to :user, inverse_of: :user_profile belongs_to :card_background_upload, class_name: "Upload" belongs_to :profile_background_upload, class_name: "Upload" belongs_to :granted_title_badge, class_name: "Badge" belongs_to :featured_topic, class_name: "Topic" has_many :upload_references, as: :target, dependent: :destroy has_many :user_profile_views, dependent: :destroy validates :bio_raw, length: { maximum: 3000 }, watched_words: true validates :website, url: true, length: { maximum: 3000, }, allow_blank: true, if: :validate_website? validates :location, length: { maximum: 3000 }, watched_words: true validates :user, presence: true validate :website_domain_validator, if: :validate_website? before_save :cook before_save :apply_watched_words, if: :location? after_save :trigger_badges after_save :pull_hotlinked_image after_save do if saved_change_to_profile_background_upload_id? || saved_change_to_card_background_upload_id? || saved_change_to_bio_raw? upload_ids = [self.profile_background_upload_id, self.card_background_upload_id] + Upload.extract_upload_ids(self.bio_raw) UploadReference.ensure_exist!(upload_ids: upload_ids, target: self) end end attr_accessor :skip_pull_hotlinked_image def bio_excerpt(length = 350, opts = {}) return nil if bio_cooked.blank? excerpt = PrettyText.excerpt(bio_cooked, length, opts).sub(/
\z/, "") return excerpt if excerpt.blank? || (user.has_trust_level?(TrustLevel[1]) && !user.suspended?) PrettyText.strip_links(excerpt) end def bio_processed if bio_cooked.blank? || (user.has_trust_level?(TrustLevel[1]) && !user.suspended?) return bio_cooked end PrettyText.strip_links(bio_cooked) end def bio_summary bio_excerpt(500, strip_links: true, text_entities: true) end def recook_bio self.bio_raw_will_change! cook end def upload_card_background(upload) self.update!(card_background_upload: upload) end def clear_card_background self.update!(card_background_upload: nil) end def upload_profile_background(upload) self.update!(profile_background_upload: upload) end def clear_profile_background self.update!(profile_background_upload: nil) end def self.rebake_old(limit) problems = [] UserProfile .where("bio_cooked_version IS NULL OR bio_cooked_version < ?", BAKED_VERSION) .limit(limit) .each do |p| begin p.rebake! rescue => e problems << { profile: p, ex: e } end end problems end def rebake! update_columns(bio_cooked: cooked, bio_cooked_version: BAKED_VERSION) end def self.import_url_for_user(background_url, user, options = nil) if SiteSetting.verbose_upload_logging Rails.logger.warn( "Verbose Upload Logging: Downloading profile background from #{background_url}", ) end tempfile = FileHelper.download( background_url, max_file_size: SiteSetting.max_image_size_kb.kilobytes, tmp_file_name: "sso-profile-background", follow_redirect: true, skip_rate_limit: true, ) return unless tempfile ext = FastImage.type(tempfile).to_s tempfile.rewind is_card_background = !options || options[:is_card_background] type = is_card_background ? "card_background" : "profile_background" upload = UploadCreator.new( tempfile, "external-profile-background." + ext, origin: background_url, type: type, ).create_for(user.id) if (is_card_background) user.user_profile.upload_card_background(upload) else user.user_profile.upload_profile_background(upload) end rescue Net::ReadTimeout, OpenURI::HTTPError # skip saving, we are not connected to the net ensure tempfile.close! if tempfile && tempfile.respond_to?(:close!) end protected def trigger_badges BadgeGranter.queue_badge_grant(Badge::Trigger::UserChange, user: self) end def pull_hotlinked_image if !skip_pull_hotlinked_image && saved_change_to_bio_raw? Jobs.enqueue_in( SiteSetting.editing_grace_period, :pull_user_profile_hotlinked_images, user_id: self.user_id, ) end end private def self.remove_featured_topic_from_all_profiles(topic) where(featured_topic_id: topic.id).update_all(featured_topic_id: nil) end def cooked if self.bio_raw.present? PrettyText.cook( self.bio_raw, omit_nofollow: user.has_trust_level?(TrustLevel[3]) && !SiteSetting.tl3_links_no_follow, ) else nil end end def cook if self.bio_raw.present? if bio_raw_changed? self.bio_cooked = cooked self.bio_cooked_version = BAKED_VERSION end else self.bio_cooked = nil end end def apply_watched_words self.location = WordWatcher.apply_to_text(location) end def website_domain_validator allowed_domains = SiteSetting.allowed_user_website_domains return if (allowed_domains.blank? || self.website.blank?) domain = begin URI.parse(self.website).host rescue URI::Error end unless allowed_domains.split("|").include?(domain) self.errors.add :base, ( I18n.t( "user.website.domain_not_allowed", domains: allowed_domains.split("|").join(", "), ) ) end end def validate_website? new_record? || website_changed? end end # == Schema Information # # Table name: user_profiles # # user_id :integer not null, primary key # location :string(3000) # website :string(3000) # bio_raw :text # bio_cooked :text # dismissed_banner_key :integer # bio_cooked_version :integer # views :integer default(0), not null # profile_background_upload_id :integer # card_background_upload_id :integer # granted_title_badge_id :bigint # featured_topic_id :integer # # Indexes # # index_user_profiles_on_bio_cooked_version (bio_cooked_version) # index_user_profiles_on_card_background_upload_id (card_background_upload_id) # index_user_profiles_on_granted_title_badge_id (granted_title_badge_id) # index_user_profiles_on_profile_background_upload_id (profile_background_upload_id) # # Foreign Keys # # fk_rails_... (card_background_upload_id => uploads.id) # fk_rails_... (granted_title_badge_id => badges.id) # fk_rails_... (profile_background_upload_id => uploads.id) #