From 5a4c35f62714d2d72bc0ee57a10e08116bdc476a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Guitaut?= Date: Tue, 14 Jun 2022 18:27:01 +0200 Subject: [PATCH] FIX: Apply all watched words rules to user fields Currently we only apply watched words of the `Block` type to custom user fields and user profile fields. This patch enables all rules to be applied such as `Censor` or `Replace`. --- app/models/user.rb | 15 +++++++- app/models/user_profile.rb | 28 +++++++++----- spec/models/user_profile_spec.rb | 34 +++++++++++++--- spec/models/user_spec.rb | 66 +++++++++++++++++++++++++++----- 4 files changed, 118 insertions(+), 25 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index abf0f69628d..76d7873bc76 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -146,6 +146,7 @@ class User < ActiveRecord::Base before_save :ensure_password_is_hashed before_save :match_primary_group_changes before_save :check_if_title_is_badged_granted + before_save :apply_watched_words, unless: :custom_fields_clean? after_save :expire_tokens_if_password_changed after_save :clear_global_notice_if_needed @@ -1273,14 +1274,24 @@ class User < ActiveRecord::Base end def public_user_field_values - @public_user_field_ids ||= UserField.public_fields.pluck(:id) - user_fields(@public_user_field_ids).values.join(" ") + public_user_fields.values.join(" ") end def set_user_field(field_id, value) custom_fields["#{USER_FIELD_PREFIX}#{field_id}"] = value end + def apply_watched_words + public_user_fields.each do |id, value| + set_user_field(id, PrettyText.cook(value).gsub(/^

(.*)<\/p>$/, "\\1")) + end + end + + def public_user_fields + @public_user_field_ids ||= UserField.public_fields.pluck(:id) + user_fields(@public_user_field_ids) + end + def number_of_deleted_posts Post.with_deleted .where(user_id: self.id) diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb index c5038edd6d5..bedb61a9aa7 100644 --- a/app/models/user_profile.rb +++ b/app/models/user_profile.rb @@ -1,19 +1,27 @@ # frozen_string_literal: true class UserProfile < ActiveRecord::Base + 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, allow_blank: true, if: Proc.new { |c| c.new_record? || c.website_changed? } + validates :website, url: true, allow_blank: true, if: :validate_website? validates :user, presence: true validates :location, watched_words: 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 @@ -24,12 +32,6 @@ class UserProfile < ActiveRecord::Base end end - validate :website_domain_validator, if: Proc.new { |c| c.new_record? || c.website_changed? } - - has_many :user_profile_views, dependent: :destroy - - BAKED_VERSION = 1 - attr_accessor :skip_pull_hotlinked_image def bio_excerpt(length = 350, opts = {}) @@ -138,6 +140,10 @@ class UserProfile < ActiveRecord::Base 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) @@ -157,6 +163,10 @@ class UserProfile < ActiveRecord::Base end end + def apply_watched_words + self.location = PrettyText.cook(location).gsub(/^

(.*)<\/p>$/, "\\1") + end + def website_domain_validator allowed_domains = SiteSetting.allowed_user_website_domains return if (allowed_domains.blank? || self.website.blank?) @@ -168,8 +178,8 @@ class UserProfile < ActiveRecord::Base self.errors.add :base, (I18n.t('user.website.domain_not_allowed', domains: allowed_domains.split('|').join(", "))) unless allowed_domains.split('|').include?(domain) end - def self.remove_featured_topic_from_all_profiles(topic) - where(featured_topic_id: topic.id).update_all(featured_topic_id: nil) + def validate_website? + new_record? || website_changed? end end diff --git a/spec/models/user_profile_spec.rb b/spec/models/user_profile_spec.rb index b88aefacd9a..dcbafb5ab8b 100644 --- a/spec/models/user_profile_spec.rb +++ b/spec/models/user_profile_spec.rb @@ -9,12 +9,36 @@ RSpec.describe UserProfile do describe "#location" do context "when it contains watched words" do - before { profile.location = "bad location" } + before { profile.location = location } - it "is not valid" do - profile.valid? - expect(profile.errors[:base].size).to eq(1) - expect(profile.errors.messages[:base]).to include(/you can't post the word/) + context "when watched words are of type 'Block'" do + let(:location) { "bad location" } + + it "is not valid" do + profile.valid? + expect(profile.errors[:base].size).to eq(1) + expect(profile.errors.messages[:base]).to include(/you can't post the word/) + end + end + + context "when watched words are of type 'Censor'" do + let!(:censored_word) { Fabricate(:watched_word, word: "censored", action: WatchedWord.actions[:censor]) } + let(:location) { "censored location" } + + it "censors the words upon saving" do + expect { profile.save! }.to change { profile.location }.to eq "■■■■■■■■ location" + end + end + + context "when watched words are of type 'Replace'" do + let(:location) { "word to replace" } + let!(:replace_word) do + Fabricate(:watched_word, word: "to replace", replacement: "replaced", action: WatchedWord.actions[:replace]) + end + + it "replaces the words upon saving" do + expect { profile.save! }.to change { profile.location }.to eq "word replaced" + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 27e4af6f78e..7be09aa1b3e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -153,20 +153,68 @@ RSpec.describe User do before { user.set_user_field(user_field.id, value) } context "when user fields contain watched words" do - let(:value) { "bad user field value" } + let(:user_field_value) { user.reload.user_fields[user_field.id.to_s] } - context "when user field is public" do - it "is not valid" do - user.valid? - expect(user.errors[:base].size).to eq(1) - expect(user.errors.messages[:base]).to include(/you can't post the word/) + context "when watched words are of type 'Block'" do + let(:value) { "bad user field value" } + + context "when user field is public" do + it "is not valid" do + user.valid? + expect(user.errors[:base].size).to eq(1) + expect(user.errors.messages[:base]).to include(/you can't post the word/) + end + end + + context "when user field is private" do + before { user_field.update(show_on_profile: false) } + + it { is_expected.to be_valid } end end - context "when user field is private" do - before { user_field.update(show_on_profile: false) } + context "when watched words are of type 'Censor'" do + let!(:censored_word) { Fabricate(:watched_word, word: "censored", action: WatchedWord.actions[:censor]) } + let(:value) { "censored word" } - it { is_expected.to be_valid } + context "when user field is public" do + it "censors the words upon saving" do + user.save! + expect(user_field_value).to eq "■■■■■■■■ word" + end + end + + context "when user field is private" do + before { user_field.update(show_on_profile: false) } + + it "does not censor anything" do + user.save! + expect(user_field_value).to eq "censored word" + end + end + end + + context "when watched words are of type 'Replace'" do + let(:value) { "word to replace" } + let!(:replace_word) do + Fabricate(:watched_word, word: "to replace", replacement: "replaced", action: WatchedWord.actions[:replace]) + end + + context "when user field is public" do + it "replaces the words upon saving" do + user.save! + expect(user_field_value).to eq "word replaced" + end + end + + context "when user field is private" do + before { user_field.update(show_on_profile: false) } + + it "does not replace anything" do + user.save! + expect(user_field_value).to eq "word to replace" + end + end end end