From 41c3941c4cf0caa13fe02c1d61cf7dd059b10d44 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 27 Sep 2017 15:48:57 -0400 Subject: [PATCH] FEATURE: Support regular expressions for watched words --- .../admin/components/watched-word-form.js.es6 | 8 +++++++- .../admin/controllers/admin-watched-words.js.es6 | 1 + .../javascripts/admin/models/watched-word.js.es6 | 12 +++++++++--- .../admin/routes/admin-watched-words.js.es6 | 7 +++++++ .../templates/components/watched-word-form.hbs | 2 +- .../admin/templates/watched-words-action.hbs | 5 ++++- app/serializers/watched_word_list_serializer.rb | 8 +++++++- app/services/word_watcher.rb | 7 ++++++- config/locales/client.en.yml | 1 + config/locales/server.en.yml | 1 + config/site_settings.yml | 2 ++ spec/services/word_watcher_spec.rb | 15 +++++++++++++++ 12 files changed, 61 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/admin/components/watched-word-form.js.es6 b/app/assets/javascripts/admin/components/watched-word-form.js.es6 index 95f859abf4b..1e187bcd538 100644 --- a/app/assets/javascripts/admin/components/watched-word-form.js.es6 +++ b/app/assets/javascripts/admin/components/watched-word-form.js.es6 @@ -1,5 +1,5 @@ import WatchedWord from 'admin/models/watched-word'; -import { on, observes } from 'ember-addons/ember-computed-decorators'; +import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; export default Ember.Component.extend({ classNames: ['watched-word-form'], @@ -7,6 +7,12 @@ export default Ember.Component.extend({ actionKey: null, showSuccessMessage: false, + @computed('regularExpressions') + placeholderKey(regularExpressions) { + return "admin.watched_words.form.placeholder" + + (regularExpressions ? "_regexp" : ""); + }, + @observes('word') removeSuccessMessage() { if (this.get('showSuccessMessage') && !Ember.isEmpty(this.get('word'))) { diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 index 07d53fd2301..dc4428fc723 100644 --- a/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-watched-words.js.es6 @@ -5,6 +5,7 @@ export default Ember.Controller.extend({ filtered: false, showWords: false, disableShowWords: Ember.computed.alias('filtered'), + regularExpressions: null, filterContentNow() { diff --git a/app/assets/javascripts/admin/models/watched-word.js.es6 b/app/assets/javascripts/admin/models/watched-word.js.es6 index c0418aef978..f2b1ad8a05e 100644 --- a/app/assets/javascripts/admin/models/watched-word.js.es6 +++ b/app/assets/javascripts/admin/models/watched-word.js.es6 @@ -16,7 +16,7 @@ const WatchedWord = Discourse.Model.extend({ WatchedWord.reopenClass({ findAll() { - return ajax("/admin/logs/watched_words").then(function (list) { + return ajax("/admin/logs/watched_words").then(list => { const actions = {}; list.words.forEach(s => { if (!actions[s.action]) { actions[s.action] = []; } @@ -27,8 +27,14 @@ WatchedWord.reopenClass({ if (!actions[a]) { actions[a] = []; } }); - return Object.keys(actions).map(function(n) { - return Ember.Object.create({nameKey: n, name: I18n.t('admin.watched_words.actions.' + n), words: actions[n], count: actions[n].length}); + return Object.keys(actions).map(n => { + return Ember.Object.create({ + nameKey: n, + name: I18n.t('admin.watched_words.actions.' + n), + words: actions[n], + count: actions[n].length, + regularExpressions: list.regular_expressions + }); }); }); } diff --git a/app/assets/javascripts/admin/routes/admin-watched-words.js.es6 b/app/assets/javascripts/admin/routes/admin-watched-words.js.es6 index 77f9082b576..308dfd65443 100644 --- a/app/assets/javascripts/admin/routes/admin-watched-words.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-watched-words.js.es6 @@ -9,6 +9,13 @@ export default Discourse.Route.extend({ return WatchedWord.findAll(); }, + setupController(controller, model) { + controller.set('model', model); + if (model && model.length) { + controller.set('regularExpressions', model[0].get('regularExpressions')); + } + }, + afterModel(watchedWordsList) { this.controllerFor('adminWatchedWords').set('allWatchedWords', watchedWordsList); } diff --git a/app/assets/javascripts/admin/templates/components/watched-word-form.hbs b/app/assets/javascripts/admin/templates/components/watched-word-form.hbs index 61ce0fbe76f..7d2b460aa07 100644 --- a/app/assets/javascripts/admin/templates/components/watched-word-form.hbs +++ b/app/assets/javascripts/admin/templates/components/watched-word-form.hbs @@ -1,5 +1,5 @@ {{i18n 'admin.watched_words.form.label'}} -{{text-field value=word disabled=formSubmitted class="watched-word-input" autocorrect="off" autocapitalize="off" placeholderKey="admin.watched_words.form.placeholder"}} +{{text-field value=word disabled=formSubmitted class="watched-word-input" autocorrect="off" autocapitalize="off" placeholderKey=placeholderKey}} {{d-button action="submit" disabled=formSubmitted label="admin.watched_words.form.add"}} {{#if showSuccessMessage}} diff --git a/app/assets/javascripts/admin/templates/watched-words-action.hbs b/app/assets/javascripts/admin/templates/watched-words-action.hbs index 3a7351d9ed4..8377c28bd50 100644 --- a/app/assets/javascripts/admin/templates/watched-words-action.hbs +++ b/app/assets/javascripts/admin/templates/watched-words-action.hbs @@ -2,7 +2,10 @@

{{actionDescription}}

-{{watched-word-form actionKey=actionNameKey action="recordAdded"}} +{{watched-word-form + actionKey=actionNameKey + action="recordAdded" + regularExpressions=adminWatchedWords.regularExpressions}} {{watched-word-uploader uploading=uploading actionKey=actionNameKey done="uploadComplete"}} diff --git a/app/serializers/watched_word_list_serializer.rb b/app/serializers/watched_word_list_serializer.rb index e38d56365ca..16fba3ea503 100644 --- a/app/serializers/watched_word_list_serializer.rb +++ b/app/serializers/watched_word_list_serializer.rb @@ -1,5 +1,5 @@ class WatchedWordListSerializer < ApplicationSerializer - attributes :actions, :words + attributes :actions, :words, :regular_expressions def actions WatchedWord.actions.keys @@ -10,4 +10,10 @@ class WatchedWordListSerializer < ApplicationSerializer WatchedWordSerializer.new(word, root: false) end end + + # No point making this site setting `client: true` when it's only used + # in the admin section + def regular_expressions + SiteSetting.watched_words_regular_expressions? + end end diff --git a/app/services/word_watcher.rb b/app/services/word_watcher.rb index 11d61f0f522..5dbf7a732d1 100644 --- a/app/services/word_watcher.rb +++ b/app/services/word_watcher.rb @@ -15,12 +15,17 @@ class WordWatcher def self.word_matcher_regexp(action) s = Discourse.cache.fetch(word_matcher_regexp_key(action), expires_in: 1.day) do words = words_for_action(action) - words.empty? ? nil : '\b(' + words.map { |w| Regexp.escape(w).gsub("\\*", '\S*') }.join('|'.freeze) + ')\b' + words.empty? ? nil : '\b(' + words.map { |w| word_to_regexp(w) }.join('|'.freeze) + ')\b' end s.present? ? Regexp.new(s, Regexp::IGNORECASE) : nil end + def self.word_to_regexp(word) + return word if SiteSetting.watched_words_regular_expressions? + Regexp.escape(word).gsub("\\*", '\S*') + end + def self.word_matcher_regexp_key(action) "watched-words-regexp:#{action}" end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d19c2dbef05..80ffb7603c2 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3203,6 +3203,7 @@ en: form: label: 'New Word:' placeholder: 'full word or * as wildcard' + placeholder_regexp: "regular expression" add: 'Add' success: 'Success' upload: "Upload" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 11c191568ae..e289dd0208d 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1521,6 +1521,7 @@ en: code_formatting_style: "Code button in composer will default to this code formatting style" max_allowed_message_recipients: "Maximum recipients allowed in a message." + watched_words_regular_expressions: "Watched words are regular expressions." default_email_digest_frequency: "How often users receive summary emails by default." default_include_tl0_in_digests: "Include posts from new users in summary emails by default. Users can change this in their preferences." diff --git a/config/site_settings.yml b/config/site_settings.yml index ab296981597..982d5cef5cc 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -625,6 +625,8 @@ posting: max_allowed_message_recipients: default: 30 min: 1 + watched_words_regular_expressions: + default: false email: email_time_window_mins: diff --git a/spec/services/word_watcher_spec.rb b/spec/services/word_watcher_spec.rb index 98c2776d8ee..ba42f79bc6b 100644 --- a/spec/services/word_watcher_spec.rb +++ b/spec/services/word_watcher_spec.rb @@ -47,6 +47,21 @@ describe WordWatcher do m = WordWatcher.new("I acknowledge you.").word_matches_for_action?(:require_approval) expect(m[1]).to eq("acknowledge") end + + it "supports regular expressions as a site setting" do + SiteSetting.watched_words_regular_expressions = true + Fabricate( + :watched_word, + word: "tro[uo]+t", + action: WatchedWord.actions[:require_approval] + ) + m = WordWatcher.new("Evil Trout is cool").word_matches_for_action?(:require_approval) + expect(m[1]).to eq("Trout") + m = WordWatcher.new("Evil Troot is cool").word_matches_for_action?(:require_approval) + expect(m[1]).to eq("Troot") + m = WordWatcher.new("trooooooooot").word_matches_for_action?(:require_approval) + expect(m[1]).to eq("trooooooooot") + end end end