FIX: Make replace watched words work with wildcard (#13084)

Watched words are always regular expressions, despite watched_words_
_regular_expressions being enabled or not. Internally, wildcard
characters are replaced with a regular expression that matches any non
whitespace character.
This commit is contained in:
Bianca Nenciu
2021-05-18 12:09:47 +03:00
committed by GitHub
parent a21700a444
commit c1dfd76658
5 changed files with 35 additions and 48 deletions

View File

@ -6,50 +6,30 @@ function isLinkClose(str) {
return /^<\/a\s*>/i.test(str); return /^<\/a\s*>/i.test(str);
} }
function findAllMatches(text, matchers, useRegExp) { function findAllMatches(text, matchers) {
const matches = []; const matches = [];
if (useRegExp) { const maxMatches = 100;
const maxMatches = 100; let count = 0;
let count = 0;
matchers.forEach((matcher) => { matchers.forEach((matcher) => {
let match; let match;
while ( while (
(match = matcher.pattern.exec(text)) !== null && (match = matcher.pattern.exec(text)) !== null &&
count++ < maxMatches count++ < maxMatches
) { ) {
matches.push({ matches.push({
index: match.index, index: match.index,
text: match[0], text: match[0],
replacement: matcher.replacement, replacement: matcher.replacement,
}); });
} }
}); });
} else {
const lowerText = text.toLowerCase();
matchers.forEach((matcher) => {
const lowerPattern = matcher.pattern.toLowerCase();
let index = -1;
while ((index = lowerText.indexOf(lowerPattern, index + 1)) !== -1) {
matches.push({
index,
text: text.substr(index, lowerPattern.length),
replacement: matcher.replacement,
});
}
});
}
return matches.sort((a, b) => a.index - b.index); return matches.sort((a, b) => a.index - b.index);
} }
export function setup(helper) { export function setup(helper) {
helper.registerOptions((opts, siteSettings) => {
opts.watchedWordsRegularExpressions =
siteSettings.watched_words_regular_expressions;
});
helper.registerPlugin((md) => { helper.registerPlugin((md) => {
const replacements = md.options.discourse.watchedWordsReplacements; const replacements = md.options.discourse.watchedWordsReplacements;
if (!replacements) { if (!replacements) {
@ -57,9 +37,7 @@ export function setup(helper) {
} }
const matchers = Object.keys(replacements).map((word) => ({ const matchers = Object.keys(replacements).map((word) => ({
pattern: md.options.discourse.watchedWordsRegularExpressions pattern: new RegExp(word, "gi"),
? new RegExp(word, "gi")
: word,
replacement: replacements[word], replacement: replacements[word],
})); }));
@ -110,12 +88,7 @@ export function setup(helper) {
if (currentToken.type === "text") { if (currentToken.type === "text") {
const text = currentToken.content; const text = currentToken.content;
const matches = (cache[text] = const matches = (cache[text] =
cache[text] || cache[text] || findAllMatches(text, matchers));
findAllMatches(
text,
matchers,
md.options.discourse.watchedWordsRegularExpressions
));
// Now split string to nodes // Now split string to nodes
const nodes = []; const nodes = [];

View File

@ -187,7 +187,7 @@ class SiteSerializer < ApplicationSerializer
end end
def watched_words_replace def watched_words_replace
WordWatcher.get_cached_words(:replace) WordWatcher.word_matcher_regexps(:replace)
end end
private private

View File

@ -51,6 +51,12 @@ class WordWatcher
nil # Admin will be alerted via admin_dashboard_data.rb nil # Admin will be alerted via admin_dashboard_data.rb
end end
def self.word_matcher_regexps(action)
if words = get_cached_words(action)
words.map { |w, r| [word_to_regexp(w), r] }.to_h
end
end
def self.word_to_regexp(word) def self.word_to_regexp(word)
if SiteSetting.watched_words_regular_expressions? if SiteSetting.watched_words_regular_expressions?
# Strip ruby regexp format if present, we're going to make the whole thing # Strip ruby regexp format if present, we're going to make the whole thing

View File

@ -173,7 +173,7 @@ module PrettyText
__optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer; __optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer;
__optInput.lookupUploadUrls = __lookupUploadUrls; __optInput.lookupUploadUrls = __lookupUploadUrls;
__optInput.censoredRegexp = #{WordWatcher.word_matcher_regexp(:censor)&.source.to_json}; __optInput.censoredRegexp = #{WordWatcher.word_matcher_regexp(:censor)&.source.to_json};
__optInput.watchedWordsReplacements = #{WordWatcher.get_cached_words(:replace).to_json}; __optInput.watchedWordsReplacements = #{WordWatcher.word_matcher_regexps(:replace).to_json};
JS JS
if opts[:topicId] if opts[:topicId]

View File

@ -1401,11 +1401,19 @@ HTML
after(:all) { Discourse.redis.flushdb } after(:all) { Discourse.redis.flushdb }
it "replaces words with other words" do it "replaces words with other words" do
Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "dolor sit", replacement: "something else") Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "dolor sit*", replacement: "something else")
expect(PrettyText.cook("Lorem ipsum dolor sit amet")).to match_html(<<~HTML) expect(PrettyText.cook("Lorem ipsum dolor sit amet")).to match_html(<<~HTML)
<p>Lorem ipsum something else amet</p> <p>Lorem ipsum something else amet</p>
HTML HTML
expect(PrettyText.cook("Lorem ipsum dolor sits amet")).to match_html(<<~HTML)
<p>Lorem ipsum something else amet</p>
HTML
expect(PrettyText.cook("Lorem ipsum dolor sittt amet")).to match_html(<<~HTML)
<p>Lorem ipsum something else amet</p>
HTML
end end
it "replaces words with links" do it "replaces words with links" do