FIX: Split link watched words from replace (#13196)
It was not clear that replace watched words can be used to replace text with URLs. This introduces a new watched word type that makes it easier to understand.
This commit is contained in:
parent
eea9fead63
commit
d9484db718
|
@ -9,6 +9,7 @@ export default Component.extend({
|
||||||
|
|
||||||
isReplace: equal("actionKey", "replace"),
|
isReplace: equal("actionKey", "replace"),
|
||||||
isTag: equal("actionKey", "tag"),
|
isTag: equal("actionKey", "tag"),
|
||||||
|
isLink: equal("actionKey", "link"),
|
||||||
|
|
||||||
@discourseComputed("word.replacement")
|
@discourseComputed("word.replacement")
|
||||||
tags(replacement) {
|
tags(replacement) {
|
||||||
|
|
|
@ -15,9 +15,16 @@ export default Component.extend({
|
||||||
formSubmitted: false,
|
formSubmitted: false,
|
||||||
actionKey: null,
|
actionKey: null,
|
||||||
showMessage: false,
|
showMessage: false,
|
||||||
|
selectedTags: null,
|
||||||
|
|
||||||
canReplace: equal("actionKey", "replace"),
|
canReplace: equal("actionKey", "replace"),
|
||||||
canTag: equal("actionKey", "tag"),
|
canTag: equal("actionKey", "tag"),
|
||||||
|
canLink: equal("actionKey", "link"),
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.set("selectedTags", []);
|
||||||
|
},
|
||||||
|
|
||||||
@discourseComputed("siteSettings.watched_words_regular_expressions")
|
@discourseComputed("siteSettings.watched_words_regular_expressions")
|
||||||
placeholderKey(watchedWordsRegularExpressions) {
|
placeholderKey(watchedWordsRegularExpressions) {
|
||||||
|
@ -47,6 +54,13 @@ export default Component.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
changeSelectedTags(tags) {
|
||||||
|
this.setProperties({
|
||||||
|
selectedTags: tags,
|
||||||
|
replacement: tags.join(","),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
if (!this.isUniqueWord) {
|
if (!this.isUniqueWord) {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
|
@ -61,7 +75,10 @@ export default Component.extend({
|
||||||
|
|
||||||
const watchedWord = WatchedWord.create({
|
const watchedWord = WatchedWord.create({
|
||||||
word: this.word,
|
word: this.word,
|
||||||
replacement: this.canReplace || this.canTag ? this.replacement : null,
|
replacement:
|
||||||
|
this.canReplace || this.canTag || this.canLink
|
||||||
|
? this.replacement
|
||||||
|
: null,
|
||||||
action: this.actionKey,
|
action: this.actionKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,15 +6,17 @@ import { equal } from "@ember/object/computed";
|
||||||
export default Controller.extend(ModalFunctionality, {
|
export default Controller.extend(ModalFunctionality, {
|
||||||
isReplace: equal("model.nameKey", "replace"),
|
isReplace: equal("model.nameKey", "replace"),
|
||||||
isTag: equal("model.nameKey", "tag"),
|
isTag: equal("model.nameKey", "tag"),
|
||||||
|
isLink: equal("model.nameKey", "link"),
|
||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"value",
|
"value",
|
||||||
"model.compiledRegularExpression",
|
"model.compiledRegularExpression",
|
||||||
"model.words",
|
"model.words",
|
||||||
"isReplace",
|
"isReplace",
|
||||||
"isTag"
|
"isTag",
|
||||||
|
"isLink"
|
||||||
)
|
)
|
||||||
matches(value, regexpString, words, isReplace, isTag) {
|
matches(value, regexpString, words, isReplace, isTag, isLink) {
|
||||||
if (!value || !regexpString) {
|
if (!value || !regexpString) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +24,7 @@ export default Controller.extend(ModalFunctionality, {
|
||||||
const regexp = new RegExp(regexpString, "ig");
|
const regexp = new RegExp(regexpString, "ig");
|
||||||
const matches = value.match(regexp) || [];
|
const matches = value.match(regexp) || [];
|
||||||
|
|
||||||
if (isReplace) {
|
if (isReplace || isLink) {
|
||||||
return matches.map((match) => ({
|
return matches.map((match) => ({
|
||||||
match,
|
match,
|
||||||
replacement: words.find((word) =>
|
replacement: words.find((word) =>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{d-icon "times"}} {{word.word}}
|
{{d-icon "times"}} {{word.word}}
|
||||||
{{#if isReplace}}
|
{{#if (or isReplace isLink)}}
|
||||||
→ <span class="replacement">{{word.replacement}}</span>
|
→ <span class="replacement">{{word.replacement}}</span>
|
||||||
{{else if isTag}}
|
{{else if isTag}}
|
||||||
→
|
→
|
||||||
|
|
|
@ -5,15 +5,29 @@
|
||||||
|
|
||||||
{{#if canReplace}}
|
{{#if canReplace}}
|
||||||
<div class="watched-word-input">
|
<div class="watched-word-input">
|
||||||
<label for="watched-replacement">{{i18n "admin.watched_words.form.replacement_label"}}</label>
|
<label for="watched-replacement">{{i18n "admin.watched_words.form.replace_label"}}</label>
|
||||||
{{text-field id="watched-replacement" value=replacement disabled=formSubmitted class="watched-word-input-field" autocorrect="off" autocapitalize="off" placeholderKey="admin.watched_words.form.replacement_placeholder"}}
|
{{text-field id="watched-replacement" value=replacement disabled=formSubmitted class="watched-word-input-field" autocorrect="off" autocapitalize="off" placeholderKey="admin.watched_words.form.replace_placeholder"}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if canTag}}
|
{{#if canTag}}
|
||||||
<div class="watched-word-input">
|
<div class="watched-word-input">
|
||||||
<label for="watched-tag">{{i18n "admin.watched_words.form.tag_label"}}</label>
|
<label for="watched-tag">{{i18n "admin.watched_words.form.tag_label"}}</label>
|
||||||
{{text-field id="watched-tag" value=replacement disabled=formSubmitted class="watched-word-input-field" autocorrect="off" autocapitalize="off" placeholderKey="admin.watched_words.form.tag_placeholder"}}
|
{{tag-chooser
|
||||||
|
id="watched-tag"
|
||||||
|
class="watched-word-input-field"
|
||||||
|
allowCreate=true
|
||||||
|
disabled=formSubmitted
|
||||||
|
tags=selectedTags
|
||||||
|
onChange=(action "changeSelectedTags")
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if canLink}}
|
||||||
|
<div class="watched-word-input">
|
||||||
|
<label for="watched-replacement">{{i18n "admin.watched_words.form.link_label"}}</label>
|
||||||
|
{{text-field id="watched-replacement" value=replacement disabled=formSubmitted class="watched-word-input-field" autocorrect="off" autocapitalize="off" placeholderKey="admin.watched_words.form.link_placeholder"}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<p>
|
<p>
|
||||||
{{i18n "admin.watched_words.test.found_matches"}}
|
{{i18n "admin.watched_words.test.found_matches"}}
|
||||||
<ul>
|
<ul>
|
||||||
{{#if isReplace}}
|
{{#if (or isReplace isLink)}}
|
||||||
{{#each matches as |match|}}
|
{{#each matches as |match|}}
|
||||||
<li>
|
<li>
|
||||||
<span class="match">{{match.match}}</span>
|
<span class="match">{{match.match}}</span>
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if showWordsList}}
|
{{#if showWordsList}}
|
||||||
<div class="watched-words-list">
|
<div class="watched-words-list watched-words-{{actionNameKey}}">
|
||||||
{{#each currentAction.words as |word| }}
|
{{#each currentAction.words as |word| }}
|
||||||
<div class="watched-word-box">{{admin-watched-word actionKey=actionNameKey word=word action=(action "recordRemoved")}}</div>
|
<div class="watched-word-box">{{admin-watched-word actionKey=actionNameKey word=word action=(action "recordRemoved")}}</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
|
@ -21,7 +21,8 @@ function getOpts(opts) {
|
||||||
customEmojiTranslation: context.site.custom_emoji_translation,
|
customEmojiTranslation: context.site.custom_emoji_translation,
|
||||||
siteSettings: context.siteSettings,
|
siteSettings: context.siteSettings,
|
||||||
formatUsername,
|
formatUsername,
|
||||||
watchedWordsReplacements: context.site.watched_words_replace,
|
watchedWordsReplace: context.site.watched_words_replace,
|
||||||
|
watchedWordsLink: context.site.watched_words_link,
|
||||||
},
|
},
|
||||||
opts
|
opts
|
||||||
);
|
);
|
||||||
|
|
|
@ -1675,17 +1675,29 @@ var bar = 'bar';
|
||||||
|
|
||||||
test("watched words replace", function (assert) {
|
test("watched words replace", function (assert) {
|
||||||
const opts = {
|
const opts = {
|
||||||
watchedWordsReplacements: { fun: "times" },
|
watchedWordsReplace: { fun: "times" },
|
||||||
};
|
};
|
||||||
|
|
||||||
assert.cookedOptions("test fun", opts, "<p>test times</p>");
|
assert.cookedOptions("test fun", opts, "<p>test times</p>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("watched words link", function (assert) {
|
||||||
|
const opts = {
|
||||||
|
watchedWordsLink: { fun: "https://discourse.org" },
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.cookedOptions(
|
||||||
|
"test fun",
|
||||||
|
opts,
|
||||||
|
'<p>test <a href="https://discourse.org">fun</a></p>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("watched words replace with bad regex", function (assert) {
|
test("watched words replace with bad regex", function (assert) {
|
||||||
const maxMatches = 100; // same limit as MD watched-words-replace plugin
|
const maxMatches = 100; // same limit as MD watched-words-replace plugin
|
||||||
const opts = {
|
const opts = {
|
||||||
siteSettings: { watched_words_regular_expressions: true },
|
siteSettings: { watched_words_regular_expressions: true },
|
||||||
watchedWordsReplacements: { "\\bu?\\b": "you" },
|
watchedWordsReplace: { "\\bu?\\b": "you" },
|
||||||
};
|
};
|
||||||
|
|
||||||
assert.cookedOptions(
|
assert.cookedOptions(
|
||||||
|
|
|
@ -33,7 +33,8 @@ export function buildOptions(state) {
|
||||||
censoredRegexp,
|
censoredRegexp,
|
||||||
disableEmojis,
|
disableEmojis,
|
||||||
customEmojiTranslation,
|
customEmojiTranslation,
|
||||||
watchedWordsReplacements,
|
watchedWordsReplace,
|
||||||
|
watchedWordsLink,
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
let features = {
|
let features = {
|
||||||
|
@ -83,7 +84,8 @@ export function buildOptions(state) {
|
||||||
siteSettings.enable_advanced_editor_preview_sync,
|
siteSettings.enable_advanced_editor_preview_sync,
|
||||||
previewing,
|
previewing,
|
||||||
disableEmojis,
|
disableEmojis,
|
||||||
watchedWordsReplacements,
|
watchedWordsReplace,
|
||||||
|
watchedWordsLink,
|
||||||
};
|
};
|
||||||
|
|
||||||
// note, this will mutate options due to the way the API is designed
|
// note, this will mutate options due to the way the API is designed
|
||||||
|
|
|
@ -23,6 +23,7 @@ function findAllMatches(text, matchers) {
|
||||||
index: match.index,
|
index: match.index,
|
||||||
text: match[0],
|
text: match[0],
|
||||||
replacement: matcher.replacement,
|
replacement: matcher.replacement,
|
||||||
|
link: matcher.link,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -32,19 +33,39 @@ function findAllMatches(text, matchers) {
|
||||||
|
|
||||||
export function setup(helper) {
|
export function setup(helper) {
|
||||||
helper.registerPlugin((md) => {
|
helper.registerPlugin((md) => {
|
||||||
const replacements = md.options.discourse.watchedWordsReplacements;
|
const matchers = [];
|
||||||
if (!replacements) {
|
|
||||||
|
if (md.options.discourse.watchedWordsReplace) {
|
||||||
|
Object.entries(md.options.discourse.watchedWordsReplace).map(
|
||||||
|
([word, replacement]) => {
|
||||||
|
matchers.push({
|
||||||
|
pattern: new RegExp(word, "gi"),
|
||||||
|
replacement,
|
||||||
|
link: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (md.options.discourse.watchedWordsLink) {
|
||||||
|
Object.entries(md.options.discourse.watchedWordsLink).map(
|
||||||
|
([word, replacement]) => {
|
||||||
|
matchers.push({
|
||||||
|
pattern: new RegExp(word, "gi"),
|
||||||
|
replacement,
|
||||||
|
link: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchers.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchers = Object.keys(replacements).map((word) => ({
|
|
||||||
pattern: new RegExp(word, "gi"),
|
|
||||||
replacement: replacements[word],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cache = {};
|
const cache = {};
|
||||||
|
|
||||||
md.core.ruler.push("watched-words-replace", (state) => {
|
md.core.ruler.push("watched-words", (state) => {
|
||||||
for (let j = 0, l = state.tokens.length; j < l; j++) {
|
for (let j = 0, l = state.tokens.length; j < l; j++) {
|
||||||
if (state.tokens[j].type !== "inline") {
|
if (state.tokens[j].type !== "inline") {
|
||||||
continue;
|
continue;
|
||||||
|
@ -82,10 +103,6 @@ export function setup(helper) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (htmlLinkLevel > 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentToken.type === "text") {
|
if (currentToken.type === "text") {
|
||||||
const text = currentToken.content;
|
const text = currentToken.content;
|
||||||
const matches = (cache[text] =
|
const matches = (cache[text] =
|
||||||
|
@ -109,25 +126,27 @@ export function setup(helper) {
|
||||||
nodes.push(token);
|
nodes.push(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = state.md.normalizeLink(matches[ln].replacement);
|
if (matches[ln].link) {
|
||||||
if (state.md.validateLink(url) && /^https?/.test(url)) {
|
const url = state.md.normalizeLink(matches[ln].replacement);
|
||||||
token = new state.Token("link_open", "a", 1);
|
if (htmlLinkLevel === 0 && state.md.validateLink(url)) {
|
||||||
token.attrs = [["href", url]];
|
token = new state.Token("link_open", "a", 1);
|
||||||
token.level = level++;
|
token.attrs = [["href", url]];
|
||||||
token.markup = "linkify";
|
token.level = level++;
|
||||||
token.info = "auto";
|
token.markup = "linkify";
|
||||||
nodes.push(token);
|
token.info = "auto";
|
||||||
|
nodes.push(token);
|
||||||
|
|
||||||
token = new state.Token("text", "", 0);
|
token = new state.Token("text", "", 0);
|
||||||
token.content = matches[ln].text;
|
token.content = matches[ln].text;
|
||||||
token.level = level;
|
token.level = level;
|
||||||
nodes.push(token);
|
nodes.push(token);
|
||||||
|
|
||||||
token = new state.Token("link_close", "a", -1);
|
token = new state.Token("link_close", "a", -1);
|
||||||
token.level = --level;
|
token.level = --level;
|
||||||
token.markup = "linkify";
|
token.markup = "linkify";
|
||||||
token.info = "auto";
|
token.info = "auto";
|
||||||
nodes.push(token);
|
nodes.push(token);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
token = new state.Token("text", "", 0);
|
token = new state.Token("text", "", 0);
|
||||||
token.content = matches[ln].replacement;
|
token.content = matches[ln].replacement;
|
|
@ -332,6 +332,19 @@ table.screened-ip-addresses {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.watched-words-link {
|
||||||
|
.watched-word-box {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.watched-words-replace,
|
||||||
|
.watched-words-tag {
|
||||||
|
.watched-word-box {
|
||||||
|
min-width: calc(50% - 5px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.watched-word-box,
|
.watched-word-box,
|
||||||
.watched-words-test-modal {
|
.watched-words-test-modal {
|
||||||
.replacement {
|
.replacement {
|
||||||
|
@ -354,6 +367,7 @@ table.screened-ip-addresses {
|
||||||
.watched-words-list {
|
.watched-words-list {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.watched-word {
|
.watched-word {
|
||||||
|
@ -396,6 +410,9 @@ table.screened-ip-addresses {
|
||||||
input.watched-word-input-field {
|
input.watched-word-input-field {
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
.select-kit.multi-select.watched-word-input-field {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search logs
|
// Search logs
|
||||||
|
|
|
@ -8,9 +8,10 @@ class WatchedWord < ActiveRecord::Base
|
||||||
censor: 2,
|
censor: 2,
|
||||||
require_approval: 3,
|
require_approval: 3,
|
||||||
flag: 4,
|
flag: 4,
|
||||||
|
link: 8,
|
||||||
replace: 5,
|
replace: 5,
|
||||||
tag: 6,
|
tag: 6,
|
||||||
silence: 7
|
silence: 7,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -18,10 +19,16 @@ class WatchedWord < ActiveRecord::Base
|
||||||
|
|
||||||
before_validation do
|
before_validation do
|
||||||
self.word = self.class.normalize_word(self.word)
|
self.word = self.class.normalize_word(self.word)
|
||||||
|
if self.action == WatchedWord.actions[:link] && !(self.replacement =~ /^https?:\/\//)
|
||||||
|
self.replacement = "#{Discourse.base_url}#{self.replacement.starts_with?("/") ? "" : "/"}#{self.replacement}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
validates :word, presence: true, uniqueness: true, length: { maximum: 100 }
|
validates :word, presence: true, uniqueness: true, length: { maximum: 100 }
|
||||||
validates :action, presence: true
|
validates :action, presence: true
|
||||||
|
|
||||||
|
validate :replacement_is_url, if: -> { action == WatchedWord.actions[:link] }
|
||||||
|
|
||||||
validates_each :word do |record, attr, val|
|
validates_each :word do |record, attr, val|
|
||||||
if WatchedWord.where(action: record.action).count >= MAX_WORDS_PER_ACTION
|
if WatchedWord.where(action: record.action).count >= MAX_WORDS_PER_ACTION
|
||||||
record.errors.add(:word, :too_many)
|
record.errors.add(:word, :too_many)
|
||||||
|
@ -37,6 +44,12 @@ class WatchedWord < ActiveRecord::Base
|
||||||
w.strip.squeeze('*')
|
w.strip.squeeze('*')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def replacement_is_url
|
||||||
|
if !(replacement =~ URI::regexp)
|
||||||
|
errors.add(:base, :invalid_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.create_or_update_word(params)
|
def self.create_or_update_word(params)
|
||||||
new_word = normalize_word(params[:word])
|
new_word = normalize_word(params[:word])
|
||||||
w = WatchedWord.where("word ILIKE ?", new_word).first || WatchedWord.new(word: new_word)
|
w = WatchedWord.where("word ILIKE ?", new_word).first || WatchedWord.new(word: new_word)
|
||||||
|
@ -48,7 +61,7 @@ class WatchedWord < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.has_replacement?(action)
|
def self.has_replacement?(action)
|
||||||
action == :replace || action == :tag
|
action == :replace || action == :tag || action == :link
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_key=(arg)
|
def action_key=(arg)
|
||||||
|
|
|
@ -29,7 +29,8 @@ class SiteSerializer < ApplicationSerializer
|
||||||
:censored_regexp,
|
:censored_regexp,
|
||||||
:shared_drafts_category_id,
|
:shared_drafts_category_id,
|
||||||
:custom_emoji_translation,
|
:custom_emoji_translation,
|
||||||
:watched_words_replace
|
:watched_words_replace,
|
||||||
|
:watched_words_link
|
||||||
)
|
)
|
||||||
|
|
||||||
has_many :categories, serializer: SiteCategorySerializer, embed: :objects
|
has_many :categories, serializer: SiteCategorySerializer, embed: :objects
|
||||||
|
@ -185,6 +186,10 @@ class SiteSerializer < ApplicationSerializer
|
||||||
WordWatcher.word_matcher_regexps(:replace)
|
WordWatcher.word_matcher_regexps(:replace)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def watched_words_link
|
||||||
|
WordWatcher.word_matcher_regexps(:link)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def ordered_flags(flags)
|
def ordered_flags(flags)
|
||||||
|
|
|
@ -8,7 +8,7 @@ class WordWatcher
|
||||||
|
|
||||||
def self.words_for_action(action)
|
def self.words_for_action(action)
|
||||||
words = WatchedWord.where(action: WatchedWord.actions[action.to_sym]).limit(1000)
|
words = WatchedWord.where(action: WatchedWord.actions[action.to_sym]).limit(1000)
|
||||||
if action.to_sym == :replace || action.to_sym == :tag
|
if WatchedWord.has_replacement?(action.to_sym)
|
||||||
words.pluck(:word, :replacement).to_h
|
words.pluck(:word, :replacement).to_h
|
||||||
else
|
else
|
||||||
words.pluck(:word)
|
words.pluck(:word)
|
||||||
|
@ -31,7 +31,7 @@ class WordWatcher
|
||||||
def self.word_matcher_regexp(action, raise_errors: false)
|
def self.word_matcher_regexp(action, raise_errors: false)
|
||||||
words = get_cached_words(action)
|
words = get_cached_words(action)
|
||||||
if words
|
if words
|
||||||
if action.to_sym == :replace || action.to_sym == :tag
|
if WatchedWord.has_replacement?(action.to_sym)
|
||||||
words = words.keys
|
words = words.keys
|
||||||
end
|
end
|
||||||
words = words.map do |w|
|
words = words.map do |w|
|
||||||
|
|
|
@ -4741,22 +4741,25 @@ en:
|
||||||
replace: "Replace"
|
replace: "Replace"
|
||||||
tag: "Tag"
|
tag: "Tag"
|
||||||
silence: "Silence"
|
silence: "Silence"
|
||||||
|
link: "Link"
|
||||||
action_descriptions:
|
action_descriptions:
|
||||||
block: "Prevent posts containing these words from being posted. The user will see an error message when they try to submit their post."
|
block: "Prevent posts containing these words from being posted. The user will see an error message when they try to submit their post."
|
||||||
censor: "Allow posts containing these words, but replace them with characters that hide the censored words."
|
censor: "Allow posts containing these words, but replace them with characters that hide the censored words."
|
||||||
require_approval: "Posts containing these words will require approval by staff before they can be seen."
|
require_approval: "Posts containing these words will require approval by staff before they can be seen."
|
||||||
flag: "Allow posts containing these words, but flag them as inappropriate so moderators can review them."
|
flag: "Allow posts containing these words, but flag them as inappropriate so moderators can review them."
|
||||||
replace: "Replace words in posts with other words or links"
|
replace: "Replace words in posts with other words"
|
||||||
tag: "Automatically tag topics based on first post"
|
tag: "Automatically tag topics based on first post"
|
||||||
silence: "First posts of users containing these words will require approval by staff before they can be seen and the user will be automatically silenced."
|
silence: "First posts of users containing these words will require approval by staff before they can be seen and the user will be automatically silenced."
|
||||||
|
link: "Replace words in posts with links"
|
||||||
form:
|
form:
|
||||||
label: "Has word or phrase"
|
label: "Has word or phrase"
|
||||||
placeholder: "Enter word or phrase (* is a wildcard)"
|
placeholder: "Enter word or phrase (* is a wildcard)"
|
||||||
placeholder_regexp: "regular expression"
|
placeholder_regexp: "regular expression"
|
||||||
replacement_label: "Replacement"
|
replace_label: "Replacement"
|
||||||
replacement_placeholder: "example or https://example.com"
|
replace_placeholder: "example"
|
||||||
tag_label: "Tag"
|
tag_label: "Tag"
|
||||||
tag_placeholder: "tag1,tag2,tag3"
|
link_label: "Link"
|
||||||
|
link_placeholder: "https://example.com"
|
||||||
add: "Add"
|
add: "Add"
|
||||||
success: "Success"
|
success: "Success"
|
||||||
exists: "Already exists"
|
exists: "Already exists"
|
||||||
|
|
|
@ -637,6 +637,8 @@ en:
|
||||||
attributes:
|
attributes:
|
||||||
word:
|
word:
|
||||||
too_many: "Too many words for that action"
|
too_many: "Too many words for that action"
|
||||||
|
base:
|
||||||
|
invalid_url: "Replacement URL is invalid"
|
||||||
|
|
||||||
uncategorized_category_name: "Uncategorized"
|
uncategorized_category_name: "Uncategorized"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class MigrateWatchedWordsFromReplaceToLink < ActiveRecord::Migration[6.1]
|
||||||
|
def up
|
||||||
|
execute <<~SQL
|
||||||
|
UPDATE watched_words
|
||||||
|
SET action = 8
|
||||||
|
WHERE action = 5 AND replacement ILIKE 'http%'
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
execute("UPDATE watched_words SET action = 5 WHERE action = 8")
|
||||||
|
end
|
||||||
|
end
|
|
@ -173,7 +173,8 @@ 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.word_matcher_regexps(:replace).to_json};
|
__optInput.watchedWordsReplace = #{WordWatcher.word_matcher_regexps(:replace).to_json};
|
||||||
|
__optInput.watchedWordsLink = #{WordWatcher.word_matcher_regexps(:link).to_json};
|
||||||
JS
|
JS
|
||||||
|
|
||||||
if opts[:topicId]
|
if opts[:topicId]
|
||||||
|
|
|
@ -1403,7 +1403,7 @@ HTML
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "watched words - replace" do
|
describe "watched words - replace & link" do
|
||||||
after(:all) { Discourse.redis.flushdb }
|
after(:all) { Discourse.redis.flushdb }
|
||||||
|
|
||||||
it "replaces words with other words" do
|
it "replaces words with other words" do
|
||||||
|
@ -1423,7 +1423,7 @@ HTML
|
||||||
end
|
end
|
||||||
|
|
||||||
it "replaces words with links" do
|
it "replaces words with links" do
|
||||||
Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "meta", replacement: "https://meta.discourse.org")
|
Fabricate(:watched_word, action: WatchedWord.actions[:link], word: "meta", replacement: "https://meta.discourse.org")
|
||||||
|
|
||||||
expect(PrettyText.cook("Meta is a Discourse forum")).to match_html(<<~HTML)
|
expect(PrettyText.cook("Meta is a Discourse forum")).to match_html(<<~HTML)
|
||||||
<p>
|
<p>
|
||||||
|
@ -1446,14 +1446,14 @@ HTML
|
||||||
end
|
end
|
||||||
|
|
||||||
it "supports overlapping words" do
|
it "supports overlapping words" do
|
||||||
Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "discourse", replacement: "https://discourse.org")
|
Fabricate(:watched_word, action: WatchedWord.actions[:link], word: "meta", replacement: "https://meta.discourse.org")
|
||||||
Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "is", replacement: "https://example.com")
|
Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "iz", replacement: "is")
|
||||||
|
Fabricate(:watched_word, action: WatchedWord.actions[:link], word: "discourse", replacement: "https://discourse.org")
|
||||||
|
|
||||||
expect(PrettyText.cook("Meta is a Discourse forum")).to match_html(<<~HTML)
|
expect(PrettyText.cook("Meta iz a Discourse forum")).to match_html(<<~HTML)
|
||||||
<p>
|
<p>
|
||||||
Meta
|
<a href="https://meta.discourse.org" rel="noopener nofollow ugc">Meta</a>
|
||||||
<a href="https://example.com" rel="noopener nofollow ugc">is</a>
|
is a
|
||||||
a
|
|
||||||
<a href="https://discourse.org" rel="noopener nofollow ugc">Discourse</a>
|
<a href="https://discourse.org" rel="noopener nofollow ugc">Discourse</a>
|
||||||
forum
|
forum
|
||||||
</p>
|
</p>
|
||||||
|
|
Loading…
Reference in New Issue