FIX: Don't cook user fields to apply watched words (#17590)

The previous method for reused the PrettyText logic which applied the
watched word logic, but had the unwanted effect of cooking the text too.
This meant that regular text values were converted to HTML.

Follow up to commit 5a4c35f627.
This commit is contained in:
Bianca Nenciu 2022-07-26 18:15:42 +03:00 committed by GitHub
parent 171789f47a
commit 5f13ca5e54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 101 additions and 51 deletions

View File

@ -1283,7 +1283,7 @@ class User < ActiveRecord::Base
def apply_watched_words def apply_watched_words
validatable_user_fields.each do |id, value| validatable_user_fields.each do |id, value|
set_user_field(id, PrettyText.cook(value).gsub(/^<p>(.*)<\/p>$/, "\\1")) set_user_field(id, WordWatcher.apply_to_text(value))
end end
end end

View File

@ -164,7 +164,7 @@ class UserProfile < ActiveRecord::Base
end end
def apply_watched_words def apply_watched_words
self.location = PrettyText.cook(location).gsub(/^<p>(.*)<\/p>$/, "\\1") self.location = WordWatcher.apply_to_text(location)
end end
def website_domain_validator def website_domain_validator

View File

@ -99,7 +99,7 @@ class WordWatcher
end end
def self.censor(html) def self.censor(html)
regexp = WordWatcher.word_matcher_regexp(:censor) regexp = word_matcher_regexp(:censor)
return html if regexp.blank? return html if regexp.blank?
doc = Nokogiri::HTML5::fragment(html) doc = Nokogiri::HTML5::fragment(html)
@ -110,12 +110,24 @@ class WordWatcher
end end
def self.censor_text(text) def self.censor_text(text)
regexp = WordWatcher.word_matcher_regexp(:censor) regexp = word_matcher_regexp(:censor)
return text if regexp.blank? return text if regexp.blank?
censor_text_with_regexp(text, regexp) censor_text_with_regexp(text, regexp)
end end
def self.apply_to_text(text)
if regexp = word_matcher_regexp(:censor)
text = censor_text_with_regexp(text, regexp)
end
%i[replace link]
.flat_map { |type| word_matcher_regexps(type).to_a }
.reduce(text) do |t, (word_regexp, replacement)|
t.gsub(Regexp.new(word_regexp)) { |match| "#{match[0]}#{replacement}" }
end
end
def self.clear_cache! def self.clear_cache!
WatchedWord.actions.each do |a, i| WatchedWord.actions.each do |a, i|
Discourse.cache.delete word_matcher_regexp_key(a) Discourse.cache.delete word_matcher_regexp_key(a)

View File

@ -45,6 +45,11 @@ RSpec.describe UserProfile do
context "when it does not contain watched words" do context "when it does not contain watched words" do
it { is_expected.to be_valid } it { is_expected.to be_valid }
end end
it "is not cooked" do
profile.location = "https://discourse.org"
expect { profile.save! }.not_to change { profile.location }
end
end end
describe "#bio_raw" do describe "#bio_raw" do

View File

@ -224,6 +224,16 @@ RSpec.describe User do
it { is_expected.to be_valid } it { is_expected.to be_valid }
end end
context "when user fields contain URL" do
let(:value) { "https://discourse.org" }
let(:user_field_value) { user.reload.user_fields[user_field.id.to_s] }
it "is not cooked" do
user.save!
expect(user_field_value).to eq "https://discourse.org"
end
end
context "with a multiselect user field" do context "with a multiselect user field" do
fab!(:user_field) do fab!(:user_field) do
Fabricate(:user_field, field_type: 'multiselect', show_on_profile: true) do Fabricate(:user_field, field_type: 'multiselect', show_on_profile: true) do

View File

@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
describe WordWatcher do describe WordWatcher do
let(:raw) { "Do you like liquorice?\n\nI really like them. One could even say that I am *addicted* to liquorice. And if\nyou can mix it up with some anise, then I'm in heaven ;)" } let(:raw) { "Do you like liquorice?\n\nI really like them. One could even say that I am *addicted* to liquorice. And if\nyou can mix it up with some anise, then I'm in heaven ;)" }
after do after do
@ -15,67 +14,69 @@ describe WordWatcher do
context 'format of the result regexp' do context 'format of the result regexp' do
it "is correct when watched_words_regular_expressions = true" do it "is correct when watched_words_regular_expressions = true" do
SiteSetting.watched_words_regular_expressions = true SiteSetting.watched_words_regular_expressions = true
regexp = WordWatcher.word_matcher_regexp(:block) regexp = described_class.word_matcher_regexp(:block)
expect(regexp.inspect).to eq("/(#{word1})|(#{word2})/i") expect(regexp.inspect).to eq("/(#{word1})|(#{word2})/i")
end end
it "is correct when watched_words_regular_expressions = false" do it "is correct when watched_words_regular_expressions = false" do
SiteSetting.watched_words_regular_expressions = false SiteSetting.watched_words_regular_expressions = false
regexp = WordWatcher.word_matcher_regexp(:block) regexp = described_class.word_matcher_regexp(:block)
expect(regexp.inspect).to eq("/(?:\\W|^)(#{word1}|#{word2})(?=\\W|$)/i") expect(regexp.inspect).to eq("/(?:\\W|^)(#{word1}|#{word2})(?=\\W|$)/i")
end end
end end
end end
describe "word_matches_for_action?" do describe "#word_matches_for_action?" do
it "is falsey when there are no watched words" do it "is falsey when there are no watched words" do
expect(WordWatcher.new(raw).word_matches_for_action?(:require_approval)).to be_falsey expect(described_class.new(raw).word_matches_for_action?(:require_approval)).to be_falsey
end end
context "with watched words" do context "with watched words" do
fab!(:anise) { Fabricate(:watched_word, word: "anise", action: WatchedWord.actions[:require_approval]) } fab!(:anise) { Fabricate(:watched_word, word: "anise", action: WatchedWord.actions[:require_approval]) }
it "is falsey without a match" do it "is falsey without a match" do
expect(WordWatcher.new("No liquorice for me, thanks...").word_matches_for_action?(:require_approval)).to be_falsey expect(described_class.new("No liquorice for me, thanks...").word_matches_for_action?(:require_approval)).to be_falsey
end end
it "is returns matched words if there's a match" do it "is returns matched words if there's a match" do
m = WordWatcher.new(raw).word_matches_for_action?(:require_approval) matches = described_class.new(raw).word_matches_for_action?(:require_approval)
expect(m).to be_truthy expect(matches).to be_truthy
expect(m[1]).to eq(anise.word) expect(matches[1]).to eq(anise.word)
end end
it "finds at start of string" do it "finds at start of string" do
m = WordWatcher.new("#{anise.word} is garbage").word_matches_for_action?(:require_approval) matches = described_class.new("#{anise.word} is garbage").word_matches_for_action?(:require_approval)
expect(m[1]).to eq(anise.word) expect(matches[1]).to eq(anise.word)
end end
it "finds at end of string" do it "finds at end of string" do
m = WordWatcher.new("who likes #{anise.word}").word_matches_for_action?(:require_approval) matches = described_class.new("who likes #{anise.word}").word_matches_for_action?(:require_approval)
expect(m[1]).to eq(anise.word) expect(matches[1]).to eq(anise.word)
end end
it "finds non-letters in place of letters" do it "finds non-letters in place of letters" do
Fabricate(:watched_word, word: "co(onut", action: WatchedWord.actions[:require_approval]) Fabricate(:watched_word, word: "co(onut", action: WatchedWord.actions[:require_approval])
m = WordWatcher.new("This co(onut is delicious.").word_matches_for_action?(:require_approval)
expect(m[1]).to eq("co(onut") matches = described_class.new("This co(onut is delicious.").word_matches_for_action?(:require_approval)
expect(matches[1]).to eq("co(onut")
end end
it "handles * for wildcards" do it "handles * for wildcards" do
Fabricate(:watched_word, word: "a**le*", action: WatchedWord.actions[:require_approval]) Fabricate(:watched_word, word: "a**le*", action: WatchedWord.actions[:require_approval])
m = WordWatcher.new("I acknowledge you.").word_matches_for_action?(:require_approval)
expect(m[1]).to eq("acknowledge") matches = described_class.new("I acknowledge you.").word_matches_for_action?(:require_approval)
expect(matches[1]).to eq("acknowledge")
end end
context "word boundary" do context "word boundary" do
it "handles word boundary" do it "handles word boundary" do
Fabricate(:watched_word, word: "love", action: WatchedWord.actions[:require_approval]) Fabricate(:watched_word, word: "love", action: WatchedWord.actions[:require_approval])
expect(WordWatcher.new("I Love, bananas.").word_matches_for_action?(:require_approval)[1]).to eq("Love") expect(described_class.new("I Love, bananas.").word_matches_for_action?(:require_approval)[1]).to eq("Love")
expect(WordWatcher.new("I LOVE; apples.").word_matches_for_action?(:require_approval)[1]).to eq("LOVE") expect(described_class.new("I LOVE; apples.").word_matches_for_action?(:require_approval)[1]).to eq("LOVE")
expect(WordWatcher.new("love: is a thing.").word_matches_for_action?(:require_approval)[1]).to eq("love") expect(described_class.new("love: is a thing.").word_matches_for_action?(:require_approval)[1]).to eq("love")
expect(WordWatcher.new("I love. oranges").word_matches_for_action?(:require_approval)[1]).to eq("love") expect(described_class.new("I love. oranges").word_matches_for_action?(:require_approval)[1]).to eq("love")
expect(WordWatcher.new("I :love. pineapples").word_matches_for_action?(:require_approval)[1]).to eq("love") expect(described_class.new("I :love. pineapples").word_matches_for_action?(:require_approval)[1]).to eq("love")
expect(WordWatcher.new("peace ,love and understanding.").word_matches_for_action?(:require_approval)[1]).to eq("love") expect(described_class.new("peace ,love and understanding.").word_matches_for_action?(:require_approval)[1]).to eq("love")
end end
end end
@ -85,9 +86,11 @@ describe WordWatcher do
%w{bananas hate hates}.each do |word| %w{bananas hate hates}.each do |word|
Fabricate(:watched_word, word: word, action: WatchedWord.actions[:block]) Fabricate(:watched_word, word: word, action: WatchedWord.actions[:block])
end end
matches = WordWatcher.new("I hate bananas").word_matches_for_action?(:block, all_matches: true)
matches = described_class.new("I hate bananas").word_matches_for_action?(:block, all_matches: true)
expect(matches).to contain_exactly('hate', 'bananas') expect(matches).to contain_exactly('hate', 'bananas')
matches = WordWatcher.new("She hates bananas too").word_matches_for_action?(:block, all_matches: true)
matches = described_class.new("She hates bananas too").word_matches_for_action?(:block, all_matches: true)
expect(matches).to contain_exactly('hates', 'bananas') expect(matches).to contain_exactly('hates', 'bananas')
end end
end end
@ -101,10 +104,10 @@ describe WordWatcher do
Fabricate(:watched_word, word: "(pine)?apples", action: WatchedWord.actions[:block]) Fabricate(:watched_word, word: "(pine)?apples", action: WatchedWord.actions[:block])
Fabricate(:watched_word, word: "((move|store)(d)?)|((watch|listen)(ed|ing)?)", action: WatchedWord.actions[:block]) Fabricate(:watched_word, word: "((move|store)(d)?)|((watch|listen)(ed|ing)?)", action: WatchedWord.actions[:block])
matches = WordWatcher.new("pine pineapples apples").word_matches_for_action?(:block, all_matches: true) matches = described_class.new("pine pineapples apples").word_matches_for_action?(:block, all_matches: true)
expect(matches).to contain_exactly('pineapples', 'apples') expect(matches).to contain_exactly('pineapples', 'apples')
matches = WordWatcher.new("go watched watch ed ing move d moveed moved moving").word_matches_for_action?(:block, all_matches: true) matches = described_class.new("go watched watch ed ing move d moveed moved moving").word_matches_for_action?(:block, all_matches: true)
expect(matches).to contain_exactly(*%w{watched watch move moved}) expect(matches).to contain_exactly(*%w{watched watch move moved})
end end
end end
@ -113,20 +116,23 @@ describe WordWatcher do
context "emojis" do context "emojis" do
it "handles emoji" do it "handles emoji" do
Fabricate(:watched_word, word: ":joy:", action: WatchedWord.actions[:require_approval]) Fabricate(:watched_word, word: ":joy:", action: WatchedWord.actions[:require_approval])
m = WordWatcher.new("Lots of emojis here :joy:").word_matches_for_action?(:require_approval)
expect(m[1]).to eq(":joy:") matches = described_class.new("Lots of emojis here :joy:").word_matches_for_action?(:require_approval)
expect(matches[1]).to eq(":joy:")
end end
it "handles unicode emoji" do it "handles unicode emoji" do
Fabricate(:watched_word, word: "🎃", action: WatchedWord.actions[:require_approval]) Fabricate(:watched_word, word: "🎃", action: WatchedWord.actions[:require_approval])
m = WordWatcher.new("Halloween party! 🎃").word_matches_for_action?(:require_approval)
expect(m[1]).to eq("🎃") matches = described_class.new("Halloween party! 🎃").word_matches_for_action?(:require_approval)
expect(matches[1]).to eq("🎃")
end end
it "handles emoji skin tone" do it "handles emoji skin tone" do
Fabricate(:watched_word, word: ":woman:t5:", action: WatchedWord.actions[:require_approval]) Fabricate(:watched_word, word: ":woman:t5:", action: WatchedWord.actions[:require_approval])
m = WordWatcher.new("To Infinity and beyond! 🚀 :woman:t5:").word_matches_for_action?(:require_approval)
expect(m[1]).to eq(":woman:t5:") matches = described_class.new("To Infinity and beyond! 🚀 :woman:t5:").word_matches_for_action?(:require_approval)
expect(matches[1]).to eq(":woman:t5:")
end end
end end
@ -141,8 +147,9 @@ describe WordWatcher do
word: /\btest\b/, word: /\btest\b/,
action: WatchedWord.actions[:block] action: WatchedWord.actions[:block]
) )
m = WordWatcher.new("this is not a test.").word_matches_for_action?(:block)
expect(m[0]).to eq("test") matches = described_class.new("this is not a test.").word_matches_for_action?(:block)
expect(matches[0]).to eq("test")
end end
it "supports regular expressions as a site setting" do it "supports regular expressions as a site setting" do
@ -151,12 +158,15 @@ describe WordWatcher do
word: /tro[uo]+t/, word: /tro[uo]+t/,
action: WatchedWord.actions[:require_approval] action: WatchedWord.actions[:require_approval]
) )
m = WordWatcher.new("Evil Trout is cool").word_matches_for_action?(:require_approval)
expect(m[0]).to eq("Trout") matches = described_class.new("Evil Trout is cool").word_matches_for_action?(:require_approval)
m = WordWatcher.new("Evil Troot is cool").word_matches_for_action?(:require_approval) expect(matches[0]).to eq("Trout")
expect(m[0]).to eq("Troot")
m = WordWatcher.new("trooooooooot").word_matches_for_action?(:require_approval) matches = described_class.new("Evil Troot is cool").word_matches_for_action?(:require_approval)
expect(m[0]).to eq("trooooooooot") expect(matches[0]).to eq("Troot")
matches = described_class.new("trooooooooot").word_matches_for_action?(:require_approval)
expect(matches[0]).to eq("trooooooooot")
end end
it "support uppercase" do it "support uppercase" do
@ -166,16 +176,29 @@ describe WordWatcher do
action: WatchedWord.actions[:require_approval] action: WatchedWord.actions[:require_approval]
) )
m = WordWatcher.new('Amazing place').word_matches_for_action?(:require_approval) matches = described_class.new('Amazing place').word_matches_for_action?(:require_approval)
expect(m).to be_nil expect(matches).to be_nil
m = WordWatcher.new('Amazing applesauce').word_matches_for_action?(:require_approval)
expect(m[0]).to eq('applesauce') matches = described_class.new('Amazing applesauce').word_matches_for_action?(:require_approval)
m = WordWatcher.new('Amazing AppleSauce').word_matches_for_action?(:require_approval) expect(matches[0]).to eq('applesauce')
expect(m[0]).to eq('AppleSauce')
matches = described_class.new('Amazing AppleSauce').word_matches_for_action?(:require_approval)
expect(matches[0]).to eq('AppleSauce')
end end
end end
end end
end end
describe ".apply_to_text" do
fab!(:censored_word) { Fabricate(:watched_word, word: "censored", action: WatchedWord.actions[:censor]) }
fab!(:replaced_word) { Fabricate(:watched_word, word: "to replace", replacement: "replaced", action: WatchedWord.actions[:replace]) }
fab!(:link_word) { Fabricate(:watched_word, word: "https://notdiscourse.org", replacement: "https://discourse.org", action: WatchedWord.actions[:link]) }
it "replaces all types of words" do
text = "hello censored world to replace https://notdiscourse.org"
expected = "hello #{described_class::REPLACEMENT_LETTER * 8} world replaced https://discourse.org"
expect(described_class.apply_to_text(text)).to eq(expected)
end
end
end end