FIX: Various watched words improvements

- Client-side censoring fixed for non-chrome browsers. (Regular expression rewritten to avoid lookback)
- Regex generation is now done on the server, to reduce repeated logic, and make it easier to extend in plugins
- Censor tests are moved to ruby, to ensure everything works end-to-end
- If "watched words regular expressions" is enabled, warn the admin when the generated regex is invalid
This commit is contained in:
David Taylor
2019-07-31 18:33:49 +01:00
parent 4c6a0313f2
commit 39e0442de9
13 changed files with 134 additions and 178 deletions
@@ -13,7 +13,7 @@ function getOpts(opts) {
{
getURL: Discourse.getURLWithCDN,
currentUser: Discourse.__container__.lookup("current-user:main"),
censoredWords: site.censored_words,
censoredRegexp: site.censored_regexp,
siteSettings,
formatUsername
},
@@ -97,7 +97,7 @@ const Topic = RestModel.extend({
fancyTitle(title) {
let fancyTitle = censor(
emojiUnescape(title || ""),
Discourse.Site.currentProp("censored_words")
Discourse.Site.currentProp("censored_regexp")
);
if (Discourse.SiteSettings.support_mixed_text_direction) {
@@ -1,75 +1,19 @@
function escapeRegexp(text) {
return text.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&").replace(/\*/g, "S*");
}
export function censorFn(regexpString, replacementLetter) {
if (regexpString) {
let censorRegexp = new RegExp(regexpString, "ig");
replacementLetter = replacementLetter || "■";
function createCensorRegexp(patterns) {
return new RegExp(`((?<!\\w)(?:${patterns.join("|")}))(?!\\w)`, "ig");
}
export function censorFn(
censoredWords,
replacementLetter,
watchedWordsRegularExpressions
) {
let patterns = [];
replacementLetter = replacementLetter || "&#9632;";
if (censoredWords && censoredWords.length) {
patterns = censoredWords.split("|");
if (!watchedWordsRegularExpressions) {
patterns = patterns.map(t => `(${escapeRegexp(t)})`);
}
}
if (patterns.length) {
let censorRegexp;
try {
if (watchedWordsRegularExpressions) {
censorRegexp = new RegExp(
"((?:" + patterns.join("|") + "))(?![^\\(]*\\))",
"ig"
return function(text) {
text = text.replace(censorRegexp, (fullMatch, ...groupMatches) => {
const stringMatch = groupMatches.find(g => typeof g === "string");
return fullMatch.replace(
stringMatch,
new Array(stringMatch.length + 1).join(replacementLetter)
);
} else {
censorRegexp = createCensorRegexp(patterns);
}
});
if (censorRegexp) {
return function(text) {
let original = text;
try {
let m = censorRegexp.exec(text);
const fourCharReplacement = new Array(5).join(replacementLetter);
while (m && m[0]) {
if (m[0].length > original.length) {
return original;
} // regex is dangerous
if (watchedWordsRegularExpressions) {
text = text.replace(censorRegexp, fourCharReplacement);
} else {
const replacement = new Array(m[0].length + 1).join(
replacementLetter
);
text = text.replace(
createCensorRegexp([escapeRegexp(m[0])]),
replacement
);
}
m = censorRegexp.exec(text);
}
return text;
} catch (e) {
return original;
}
};
}
} catch (e) {
// fall through
}
return text;
};
}
return function(t) {
@@ -77,6 +21,6 @@ export function censorFn(
};
}
export function censor(text, censoredWords, replacementLetter) {
return censorFn(censoredWords, replacementLetter)(text);
export function censor(text, censoredRegexp, replacementLetter) {
return censorFn(censoredRegexp, replacementLetter)(text);
}
@@ -29,15 +29,11 @@ export function setup(helper) {
});
helper.registerPlugin(md => {
const words = md.options.discourse.censoredWords;
const censoredRegexp = md.options.discourse.censoredRegexp;
if (words && words.length > 0) {
if (censoredRegexp) {
const replacement = String.fromCharCode(9632);
const censor = censorFn(
words,
replacement,
md.options.discourse.watchedWordsRegularExpressions
);
const censor = censorFn(censoredRegexp, replacement);
md.core.ruler.push("censored", state => censorTree(state, censor));
}
});
@@ -29,7 +29,7 @@ export function buildOptions(state) {
lookupUploadUrls,
previewing,
linkify,
censoredWords,
censoredRegexp,
disableEmojis
} = state;
@@ -67,7 +67,7 @@ export function buildOptions(state) {
formatUsername,
emojiUnicodeReplacer,
lookupUploadUrls,
censoredWords,
censoredRegexp,
allowedHrefSchemes: siteSettings.allowed_href_schemes
? siteSettings.allowed_href_schemes.split("|")
: null,