({
+ actions: state.actions.toJS(),
+ settings: state.settings.toJS(),
comments: state.comments.toJS(),
users: state.users.toJS()
});
diff --git a/client/coral-admin/src/reducers/actions.js b/client/coral-admin/src/reducers/actions.js
new file mode 100644
index 000000000..709de5257
--- /dev/null
+++ b/client/coral-admin/src/reducers/actions.js
@@ -0,0 +1,24 @@
+import {Map, Set} from 'immutable';
+import * as types from '../constants/actions';
+
+const initialState = Map({
+ ids: Set()
+});
+
+export default (state = initialState, action) => {
+ switch (action.type) {
+ case types.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS: return addActions(state, action);
+ default:
+ return state;
+ }
+};
+
+const addActions = (state, action) => {
+ const ids = action.actions.map(action => action.item_id);
+ const map = action.actions.reduce((memo, action) => {
+ memo[action.item_id] = action;
+ return memo;
+ }, {});
+ const newIds = state.get('ids').concat(ids);
+ return state.merge(map).set('ids', newIds);
+};
diff --git a/client/coral-admin/src/reducers/comments.js b/client/coral-admin/src/reducers/comments.js
index 27dc79177..6876f91dc 100644
--- a/client/coral-admin/src/reducers/comments.js
+++ b/client/coral-admin/src/reducers/comments.js
@@ -24,10 +24,10 @@ const initialState = Map({
// Handle the comment actions
export default (state = initialState, action) => {
switch (action.type) {
- case actions.COMMENTS_MODERATION_QUEUE_FETCH: return state.set('loading', true);
+ case actions.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST: return state.set('loading', true);
case actions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS: return replaceComments(action, state);
case actions.COMMENTS_MODERATION_QUEUE_FAILED: return state.set('loading', false);
- case actions.COMMENT_STATUS_UPDATE: return updateStatus(state, action);
+ case actions.COMMENT_STATUS_UPDATE_REQUEST: return updateStatus(state, action);
case actions.COMMENT_FLAG: return flag(state, action);
case actions.COMMENT_CREATE_SUCCESS: return addComment(state, action);
case actions.COMMENT_STREAM_FETCH_SUCCESS: return replaceComments(action, state);
diff --git a/client/coral-admin/src/reducers/index.js b/client/coral-admin/src/reducers/index.js
index fca4694d8..a19a5a087 100644
--- a/client/coral-admin/src/reducers/index.js
+++ b/client/coral-admin/src/reducers/index.js
@@ -4,6 +4,7 @@ import settings from 'reducers/settings';
import community from 'reducers/community';
import users from 'reducers/users';
import auth from 'reducers/auth';
+import actions from 'reducers/actions';
import assets from 'reducers/assets';
// Combine all reducers into a main one
@@ -12,6 +13,7 @@ export default combineReducers({
comments,
community,
auth,
- users,
- assets
+ actions,
+ assets,
+ users
});
diff --git a/client/coral-admin/src/reducers/settings.js b/client/coral-admin/src/reducers/settings.js
index 20cd024f5..b05418715 100644
--- a/client/coral-admin/src/reducers/settings.js
+++ b/client/coral-admin/src/reducers/settings.js
@@ -1,8 +1,13 @@
-import {Map} from 'immutable';
+import {Map, List} from 'immutable';
import * as types from '../actions/settings';
const initialState = Map({
- settings: Map(),
+ settings: Map({
+ wordlist: Map({
+ banned: List(),
+ suspect: List()
+ })
+ }),
saveSettingsError: null,
fetchSettingsError: null,
fetchingSettings: false
@@ -18,16 +23,23 @@ export default (state = initialState, action) => {
case types.SAVE_SETTINGS_LOADING: return state.set('fetchingSettings', true).set('saveSettingsError', null);
case types.SAVE_SETTINGS_SUCCESS: return saveComplete(state, action);
case types.SAVE_SETTINGS_FAILED: return settingsSaveFailed(state, action);
+ case types.WORDLIST_UPDATED: return updateWordlist(state, action);
default: return state;
}
};
+// only for updating top-level settings
const updateSettings = (state, action) => {
const s = state.set('fetchingSettings', false).set('fetchSettingsError', null);
const settings = s.get('settings').merge(action.settings);
return s.set('settings', settings);
};
+// any nested settings must have a specialized setter
+const updateWordlist = (state, action) => {
+ return state.setIn(['settings', 'wordlist', action.listName], action.wordlist);
+};
+
const saveComplete = (state, action) => {
const s = state.set('fetchingSettings', false).set('saveSettingsError', null);
const settings = s.get('settings').merge(action.settings);
diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json
index 11218552f..3397a521b 100644
--- a/client/coral-admin/src/translations.json
+++ b/client/coral-admin/src/translations.json
@@ -49,8 +49,12 @@
"comment-settings": "Comment Settings",
"embed-comment-stream": "Embed Comment Stream",
"banned-word-header": "Write the bannned words list",
- "banned-word-text": "Comments which contain these words or phrases, not separated by commas and not case sensitive, will be automatically removed from the comment stream.",
- "wordlist": "Banned words list",
+ "suspect-word-header": "Write the suspect words list",
+ "banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
+ "suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
+ "wordlist": "Banned & Suspect Words",
+ "banned-words-title": "Banned words list",
+ "suspect-words-title": "Suspect words list",
"save-changes": "Save Changes",
"copy-and-paste": "Copy and paste code below into your CMS to embed your comment box in your articles",
"moderate": "Moderate",
@@ -130,9 +134,13 @@
"include-text": "Incluir tu texto aqui.",
"comment-settings": "Configuración de Comentarios",
"embed-comment-stream": "Colocar Hilo de Comentarios",
- "wordlist": "Lista de palabras no permitidas",
+ "wordlist": "Palabras Suspendidas y Suspechosas",
"banned-word-header": "Escribir las palabras no permitidas",
+ "suspect-word-header": "Write the suspect words list",
"banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente separadas de los comentarios publicados.",
+ "suspect-word-text": "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.",
+ "banned-words-title": "Banned words list",
+ "suspect-words-title": "Suspect words list",
"save-changes": "Guardar Cambios",
"copy-and-paste": "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus articulos",
"moderate": "Moderar",
diff --git a/init.js b/init.js
index 768d02999..ec675d5a4 100644
--- a/init.js
+++ b/init.js
@@ -1,15 +1,7 @@
const Setting = require('./models/setting');
-const wordlist = require('./services/wordlist');
module.exports = () => Promise.all([
// Upsert the settings object.
- Setting
- .init({id: '1', moderation: 'pre'})
- .then(() => {
-
- // Load in the wordlist now that settings have been init'd.
- return wordlist.init();
- })
-
+ Setting.init({id: '1', moderation: 'pre'})
]);
diff --git a/models/action.js b/models/action.js
index 79eabdd98..ee698029d 100644
--- a/models/action.js
+++ b/models/action.js
@@ -38,8 +38,17 @@ ActionSchema.statics.findById = function(id) {
*/
ActionSchema.statics.insertUserAction = (action) => {
+ // Actions are made unique by using a query that can be reproducable, i.e.,
+ // not containing user inputable values.
+ let query = {
+ action_type: action.action_type,
+ item_type: action.item_type,
+ item_id: action.item_id,
+ user_id: action.user_id
+ };
+
// Create/Update the action.
- return Action.findOneAndUpdate(action, action, {
+ return Action.findOneAndUpdate(query, action, {
// Ensure that if it's new, we return the new object created.
new: true,
diff --git a/models/asset.js b/models/asset.js
index 3ca2b95cf..ac87e006f 100644
--- a/models/asset.js
+++ b/models/asset.js
@@ -25,10 +25,6 @@ const AssetSchema = new Schema({
type: Date,
default: null
},
- settings: {
- type: Schema.Types.Mixed,
- default: null
- },
closedAt: {
type: Date,
default: null
@@ -44,7 +40,15 @@ const AssetSchema = new Schema({
subsection: String,
author: String,
publication_date: Date,
- modified_date: Date
+ modified_date: Date,
+
+ // This object is used exclusivly for storing settings that are to override
+ // the base settings from the base Settings object. This is to be accessed
+ // always after running `rectifySettings` against it.
+ settings: {
+ type: Schema.Types.Mixed,
+ default: null
+ },
}, {
versionKey: false,
timestamps: {
diff --git a/models/setting.js b/models/setting.js
index 83fdcbe1d..e9e38f463 100644
--- a/models/setting.js
+++ b/models/setting.js
@@ -3,6 +3,13 @@ const Schema = mongoose.Schema;
const _ = require('lodash');
const cache = require('../services/cache');
+const WordlistSchema = new Schema({
+ banned: [String],
+ suspect: [String]
+}, {
+ _id: false
+});
+
/**
* SettingSchema manages application settings that get used on front and backend.
* @type {Schema}
@@ -38,7 +45,7 @@ const SettingSchema = new Schema({
type: String,
default: ''
},
- wordlist: [String],
+ wordlist: WordlistSchema,
charCount: {
type: Number,
default: 5000
diff --git a/package.json b/package.json
index 4d4aa01bb..bba80518c 100644
--- a/package.json
+++ b/package.json
@@ -129,12 +129,14 @@
"react": "15.3.2",
"react-addons-test-utils": "15.3.2",
"react-dom": "15.3.2",
+ "react-highlight-words": "^0.6.0",
"react-linkify": "^0.1.3",
"react-mdl": "^1.7.2",
"react-mdl-selectfield": "^0.2.0",
"react-onclickoutside": "^5.7.1",
"react-redux": "^4.4.5",
"react-router": "^3.0.0",
+ "react-tagsinput": "^3.14.0",
"redux": "^3.6.0",
"redux-mock-store": "^1.2.1",
"redux-thunk": "^2.1.0",
diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js
index 329cf54fd..39b4b02bc 100644
--- a/routes/api/comments/index.js
+++ b/routes/api/comments/index.js
@@ -96,7 +96,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
// premod, set it to `premod`.
let status;
- if (req.wordlist.matched) {
+ if (req.wordlist.banned) {
status = Promise.resolve('rejected');
} else {
status = Asset
@@ -134,6 +134,15 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
status,
author_id: req.user.id
}))
+ .then((comment) => {
+ if (req.wordlist.suspect) {
+ return Comment
+ .addAction(comment.id, null, 'flag', 'body', 'Matched suspect word filters.')
+ .then(() => comment);
+ }
+
+ return comment;
+ })
.then((comment) => {
// The comment was created! Send back the created comment.
diff --git a/routes/api/users/index.js b/routes/api/users/index.js
index 759701e52..1ce2e0e26 100644
--- a/routes/api/users/index.js
+++ b/routes/api/users/index.js
@@ -119,7 +119,7 @@ router.post('/request-password-reset', (req, res, next) => {
const options = {
subject: 'Password Reset Requested - Talk',
- from: 'noreply@coralproject.net',
+ from: process.env.TALK_SMTP_FROM_ADDRESS,
to: email,
html: resetEmailTemplate({
token,
diff --git a/services/mailer.js b/services/mailer.js
index fe07f063b..9ff7291d4 100644
--- a/services/mailer.js
+++ b/services/mailer.js
@@ -1,6 +1,7 @@
const nodemailer = require('nodemailer');
const smtpRequiredProps = [
+ 'TALK_SMTP_FROM_ADDRESS',
'TALK_SMTP_USERNAME',
'TALK_SMTP_PASSWORD',
'TALK_SMTP_HOST'
diff --git a/services/wordlist.js b/services/wordlist.js
index 59a842817..96e0c3b1f 100644
--- a/services/wordlist.js
+++ b/services/wordlist.js
@@ -8,157 +8,196 @@ const Setting = require('../models/setting');
* The root wordlist object.
* @type {Object}
*/
-const wordlist = {
- list: [],
- enabled: false
-};
+class Wordlist {
-/**
- * Loads wordlists in from the naughty-words package based on languages
- * selected.
- * @param {Array} languages language codes to add to the wordlist
- */
-wordlist.init = () => {
- return Setting
- .retrieve()
- .then((settings) => {
+ constructor() {
+ this.lists = {
+ banned: [],
+ suspect: []
+ };
+ }
- // Insert the settings wordlist.
- wordlist.insert(settings.wordlist);
+ /**
+ * Loads wordlists in from the database
+ */
+ load() {
+ return Setting
+ .retrieve()
+ .then((settings) => {
+
+ // Insert the settings wordlist.
+ this.upsert(settings.wordlist);
+ });
+ }
+
+ /**
+ * Inserts the wordlist data
+ * @param {Array} list list of words to be set to the wordlist
+ */
+ upsert(lists) {
+
+ // Add the words to this array, but also lowercase the words so that an
+ // easy comparison can take place.
+ ['banned', 'suspect'].forEach((k) => {
+ if (!(k in lists)) {
+ return;
+ }
+
+ this.lists[k] = Wordlist.parseList(lists[k]);
+
+ debug(`Added ${lists[k].length} words to the ${k} wordlist.`);
});
-};
-/**
- * Inserts the wordlist data and enables the wordlist.
- * @param {Array} list list of words to be added to the wordlist
- */
-wordlist.insert = (list) => {
+ return Promise.resolve(this);
+ }
- // Add the words to this array, but also lowercase the words so that an
- // easy comparison can take place.
- wordlist.list = _.uniq(wordlist.list.concat(list.map((word) => {
- return tokenizer.tokenize(word.toLowerCase());
- })));
+ /**
+ * Parses the list content.
+ * @param {Array} list array of words to parse for a list.
+ * @return {Array} the parsed list
+ */
+ static parseList(list) {
+ return _.uniq(list.map((word) => tokenizer.tokenize(word.toLowerCase())));
+ }
- debug(`Added ${list.length} words to the wordlist, now the wordlist is ${wordlist.list.length} entries long.`);
+ /**
+ * Tests the phrase to see if it contains any of the defined blockwords.
+ * @param {String} phrase value to check for blockwords.
+ * @return {Boolean} true if a blockword is found, false otherwise.
+ */
+ match(list, phrase) {
- // Enable the wordlist.
- wordlist.enabled = true;
+ // Lowercase the word to ensure that we don't miss a match due to
+ // capitalization.
+ let lowerPhraseWords = tokenizer.tokenize(phrase.toLowerCase());
- return Promise.resolve(wordlist);
-};
+ // This will return true in the event that at least one blockword is found
+ // in the phrase.
+ return list.some((blockphrase) => {
-/**
- * Tests the phrase to see if it contains any of the defined blockwords.
- * @param {String} phrase value to check for blockwords.
- * @return {Boolean} true if a blockword is found, false otherwise.
- */
-wordlist.match = (phrase) => {
+ // First, let's see if we can find the first word in the blockphrase in the
+ // source phrase.
+ let idx = lowerPhraseWords.indexOf(blockphrase[0]);
- // Lowercase the word to ensure that we don't miss a match due to
- // capitalization.
- let lowerPhraseWords = tokenizer.tokenize(phrase.toLowerCase());
+ if (idx === -1) {
- // This will return true in the event that at least one blockword is found
- // in the phrase.
- return wordlist.list.some((blockphrase) => {
-
- // First, let's see if we can find the first word in the blockphrase in the
- // source phrase.
- let idx = lowerPhraseWords.indexOf(blockphrase[0]);
-
- if (idx === -1) {
-
- // The first blockword in the blockphrase did not match the source phrase
- // anywhere.
- return false;
- }
-
- // Here we'll quick respond with true in the event that the blockphrase was
- // just a single word.
- if (blockphrase.length === 1) {
- return true;
- }
-
- // We found the first word in the source phrase! Lets ensure it matches the
- // rest of the blockphrase...
-
- // Check to see if it even has the length to support this word!
- if (lowerPhraseWords.length < idx + blockphrase.length - 1) {
-
- // We couldn't possibly have the entire phrase here because we don't have
- // enough entries!
- return false;
- }
-
- for (let i = 1; i < blockphrase.length; i++) {
-
- // Check to see if the next word also matches!
- if (lowerPhraseWords[idx + i] !== blockphrase[i]) {
+ // The first blockword in the blockphrase did not match the source phrase
+ // anywhere.
return false;
}
+
+ // Here we'll quick respond with true in the event that the blockphrase was
+ // just a single word.
+ if (blockphrase.length === 1) {
+ return true;
+ }
+
+ // We found the first word in the source phrase! Lets ensure it matches the
+ // rest of the blockphrase...
+
+ // Check to see if it even has the length to support this word!
+ if (lowerPhraseWords.length < idx + blockphrase.length - 1) {
+
+ // We couldn't possibly have the entire phrase here because we don't have
+ // enough entries!
+ return false;
+ }
+
+ for (let i = 1; i < blockphrase.length; i++) {
+
+ // Check to see if the next word also matches!
+ if (lowerPhraseWords[idx + i] !== blockphrase[i]) {
+ return false;
+ }
+ }
+
+ // We've walked over all the words of the blockphrase, and haven't had a
+ // mismatch... It does contain the whole word!
+ return true;
+ });
+ }
+
+ /**
+ * Perform the filtering based on the loaded wordlists.
+ */
+ filter(body, ...fields) {
+
+ // Start with the sensible default that the content does not contain
+ // profanity.
+ let errors = {};
+
+ // Loop over all the fields from the body that we want to check.
+ for (let i = 0; i < fields.length; i++) {
+ let field = fields[i];
+
+ let phrase = _.get(body, field, false);
+
+ // If the field doesn't exist in the body, then it can't be profane!
+ if (!phrase) {
+
+ // Return that there wasn't a profane word here.
+ continue;
+ }
+
+ // Check if the field contains a banned word.
+ if (this.match(this.lists.banned, phrase)) {
+ debug(`the field "${field}" contained a phrase "${phrase}" which contained a banned word/phrase`);
+
+ errors.banned = ErrContainsProfanity;
+
+ // Stop looping through the fields now, we discovered the worst possible
+ // situation (a banned word).
+ break;
+ }
+
+ // Check if the field contains a banned word.
+ if (this.match(this.lists.suspect, phrase)) {
+ debug(`the field "${field}" contained a phrase "${phrase}" which contained a suspected word/phrase`);
+
+ errors.suspect = ErrContainsProfanity;
+
+ // Continue looping through the fields now, we discovered a possible bad
+ // word (suspect).
+ continue;
+ }
}
- // We've walked over all the words of the blockphrase, and haven't had a
- // mismatch... It does contain the whole word!
- return true;
- });
-};
+ return errors;
+ }
+
+ /**
+ * Connect middleware for scanning request bodies for wordlisted words and
+ * attaching a ErrContainsProfanity to the req.wordlisted parameter, otherwise
+ * it will just set that parameter to false.
+ * @param {Array} fields selectors for the body to extract the fields to be
+ * tested
+ * @return {Function} the Connect middleware
+ */
+ static filter(...fields) {
+ return (req, res, next) => {
+
+ // Create a new instance of the Wordlist.
+ const wl = new Wordlist();
+
+ wl
+ .load()
+ .then(() => {
+
+ // Perform a filtering operation using the new instance of the
+ // Wordlist.
+ req.wordlist = wl.filter(req.body, ...fields);
+
+ // Call the next piece of middleware.
+ next();
+ });
+ };
+ }
+}
// ErrContainsProfanity is returned in the event that the middleware detects
// profanity/wordlisted words in the payload.
const ErrContainsProfanity = new Error('contains profanity');
ErrContainsProfanity.status = 400;
-/**
- * Connect middleware for scanning request bodies for wordlisted words and
- * attaching a ErrContainsProfanity to the req.wordlisted parameter, otherwise
- * it will just set that parameter to false.
- * @param {Array} fields selectors for the body to extract the fields to be
- * tested
- * @return {Function} the Connect middleware
- */
-wordlist.filter = (...fields) => (req, res, next) => {
-
- // Start with the sensible default that the content does not contain
- // profanity.
- req.wordlist = {
- matched: false
- };
-
- // If the wordlist isn't enabled, then don't actually perform checking and
- // forward the request!
- if (!wordlist.enabled) {
- return next();
- }
-
- // Loop over all the fields from the body that we want to check.
- const containsProfanity = fields.some((field) => {
- let phrase = _.get(req.body, field, false);
-
- // If the field doesn't exist in the body, then it can't be profane!
- if (!phrase) {
-
- // Return that there wasn't a profane word here.
- return false;
- }
-
- // Check if the field contains a profane word.
- if (wordlist.match(phrase)) {
- debug(`the field "${field}" contained a phrase "${phrase}" which contained a wordlisted word/phrase`);
- return true;
- }
-
- return false;
- });
-
- // The body could contain some profanity, address that here.
- if (containsProfanity) {
- req.wordlist.matched = ErrContainsProfanity;
- }
-
- next();
-};
-
-module.exports = wordlist;
+module.exports = Wordlist;
module.exports.ErrContainsProfanity = ErrContainsProfanity;
diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js
index 5e7045fb0..1c782e405 100644
--- a/tests/routes/api/comments/index.js
+++ b/tests/routes/api/comments/index.js
@@ -8,22 +8,18 @@ const expect = chai.expect;
chai.should();
chai.use(require('chai-http'));
-const wordlist = require('../../../../services/wordlist');
const Comment = require('../../../../models/comment');
const Asset = require('../../../../models/asset');
const Action = require('../../../../models/action');
const User = require('../../../../models/user');
const Setting = require('../../../../models/setting');
-const settings = {id: '1', moderation: 'pre'};
+const settings = {id: '1', moderation: 'pre', wordlist: {banned: ['bad words'], suspect: ['suspect words']}};
describe('/api/v1/comments', () => {
// Ensure that the settings are always available.
- beforeEach(() => Promise.all([
- wordlist.insert(['bad words']),
- Setting.init(settings)
- ]));
+ beforeEach(() => Setting.init(settings));
describe('#get', () => {
const comments = [{
@@ -168,12 +164,22 @@ describe('/api/v1/comments', () => {
describe('#post', () => {
let asset_id;
+ let postmod_asset_id;
- beforeEach(() => Asset.findOrCreateByUrl('https://coralproject.net/section/article-is-the-best').then((asset) => {
+ beforeEach(() => Promise.all([
+ Asset.findOrCreateByUrl('https://coralproject.net/section/article-is-the-best').then((asset) => {
- // Update the asset id.
- asset_id = asset.id;
- }));
+ // Update the asset id.
+ asset_id = asset.id;
+ }),
+ Asset.findOrCreateByUrl('https://coralproject.net/section/postmod-article-is-the-best').then((asset) => {
+
+ // Update the asset id.
+ postmod_asset_id = asset.id;
+
+ return Asset.overrideSettings(postmod_asset_id, {moderation: 'post'});
+ }),
+ ]));
it('should create a comment', () => {
return chai.request(app)
@@ -198,6 +204,32 @@ describe('/api/v1/comments', () => {
});
});
+ it('should create a comment with no status and a flag if it contains a suspected word', () => {
+ return chai.request(app)
+ .post('/api/v1/comments')
+ .set(passport.inject({roles: []}))
+ .send({'body': 'suspect words are the most suspicious', 'author_id': '123', 'asset_id': postmod_asset_id, 'parent_id': ''})
+ .then((res) => {
+ expect(res).to.have.status(201);
+ expect(res.body).to.have.property('id');
+ expect(res.body).to.have.property('status', null);
+
+ return Promise.all([
+ res.body,
+ Action.findByType('flag', 'comments')
+ ]);
+ })
+ .then(([comment, actions]) => {
+ expect(actions).to.have.length(1);
+
+ let action = actions[0];
+
+ expect(action).to.have.property('item_id', comment.id);
+ expect(action).to.have.property('field', 'body');
+ expect(action).to.have.property('detail', 'Matched suspect word filters.');
+ });
+ });
+
it('should create a comment with a premod status if it\'s asset is has pre-moderation enabled', () => {
return Asset
.findOrCreateByUrl('https://coralproject.net/article1')
diff --git a/tests/services/wordlist.js b/tests/services/wordlist.js
index 0ae76c176..4edfef0c7 100644
--- a/tests/services/wordlist.js
+++ b/tests/services/wordlist.js
@@ -1,54 +1,58 @@
const expect = require('chai').expect;
-const wordlist = require('../../services/wordlist');
+const Wordlist = require('../../services/wordlist');
describe('wordlist: services', () => {
- before(() => wordlist.insert([
- 'BAD',
- 'bad',
- 'how to murder',
- 'how to kill'
- ]));
+ const wordlists = {
+ banned: [
+ 'cookies',
+ 'how to do bad things',
+ 'how to do really bad things'
+ ],
+ suspect: [
+ 'do bad things'
+ ]
+ };
- beforeEach(() => {
- expect(wordlist.list).to.not.be.empty;
- expect(wordlist.enabled).to.be.true;
- });
+ let wordlist = new Wordlist();
describe('#init', () => {
+ before(() => wordlist.upsert(wordlists));
+
it('has entries', () => {
- expect(wordlist.list).to.not.be.empty;
- expect(wordlist.enabled).to.be.true;
+ expect(wordlist.lists.banned).to.not.be.empty;
+ expect(wordlist.lists.suspect).to.not.be.empty;
});
});
describe('#match', () => {
+ const bannedList = Wordlist.parseList(wordlists.banned);
+
it('does match on a bad word', () => {
[
- 'how to kill',
- 'what is bad',
- 'bad',
- 'BAD.',
- 'how to murder',
- 'How To mUrDer'
+ 'how to do really bad things',
+ 'what is cookies',
+ 'cookies',
+ 'COOKIES.',
+ 'how to do bad things',
+ 'How To do bad things!'
].forEach((word) => {
- expect(wordlist.match(word)).to.be.true;
+ expect(wordlist.match(bannedList, word)).to.be.true;
});
});
it('does not match on a good word', () => {
[
'how to',
- 'kill',
- 'bads',
+ 'cookie',
'how to be a great person?',
- 'how to not kill?'
+ 'how to not do really bad things?'
].forEach((word) => {
- expect(wordlist.match(word)).to.be.false;
+ expect(wordlist.match(bannedList, word)).to.be.false;
});
});
@@ -56,62 +60,31 @@ describe('wordlist: services', () => {
describe('#filter', () => {
- it('matches on bodies containing bad words', (done) => {
+ before(() => wordlist.upsert(wordlists));
- let req = {
- body: {
- content: 'how to kill?'
- }
- };
-
- wordlist.filter('content')(req, {}, (err) => {
- expect(err).to.be.undefined;
- expect(req).to.have.property('wordlist');
- expect(req.wordlist).to.have.property('matched');
- expect(req.wordlist.matched).to.be.equal(wordlist.ErrContainsProfanity);
-
- done();
- });
+ it('matches on bodies containing bad words', () => {
+ let errors = wordlist.filter({
+ content: 'how to do really bad things?'
+ }, 'content');
+ expect(errors).to.have.property('banned', Wordlist.ErrContainsProfanity);
});
- it('does not match on bodies not containing bad words', (done) => {
-
- let req = {
- body: {
- content: 'how to be a great person?'
- }
- };
-
- wordlist.filter('content')(req, {}, (err) => {
- expect(err).to.be.undefined;
- expect(req).to.have.property('wordlist');
- expect(req.wordlist).to.have.property('matched');
- expect(req.wordlist.matched).to.be.false;
-
- done();
- });
+ it('does not match on bodies not containing bad words', () => {
+ let errors = wordlist.filter({
+ content: 'how to not do really bad things?'
+ }, 'content');
+ expect(errors).to.not.have.property('banned');
});
- it('does not match on bodies not containing the bad word field', (done) => {
-
- let req = {
- body: {
- author: 'how to kill?',
- content: 'how to be a great person?'
- }
- };
-
- wordlist.filter('content')(req, {}, (err) => {
- expect(err).to.be.undefined;
- expect(req).to.have.property('wordlist');
- expect(req.wordlist).to.have.property('matched');
- expect(req.wordlist.matched).to.be.false;
-
- done();
- });
+ it('does not match on bodies not containing the bad word field', () => {
+ let errors = wordlist.filter({
+ author: 'how to do really bad things?',
+ content: 'how to be a great person?'
+ }, 'content');
+ expect(errors).to.not.have.property('banned');
});
});
diff --git a/views/admin.ejs b/views/admin.ejs
index 038fb58d7..49a921875 100644
--- a/views/admin.ejs
+++ b/views/admin.ejs
@@ -13,6 +13,57 @@
margin: 0;
background: #fff;
}
+ /* putting this here until I can get webpack to behave */
+ .react-tagsinput {
+ background-color: #fff;
+ border: 1px solid #ccc;
+ overflow: hidden;
+ padding-left: 5px;
+ padding-top: 5px;
+ }
+
+ .react-tagsinput--focused {
+ border-color: rgb(142, 76, 65);
+ }
+
+ .react-tagsinput-tag {
+ background-color: rgb(255, 220, 214);
+ border-radius: 2px;
+ border: 1px solid rgb(244, 126, 107);
+ color: rgb(244, 126, 107);
+ display: inline-block;
+ font-family: sans-serif;
+ font-size: 13px;
+ font-weight: 400;
+ margin-bottom: 5px;
+ margin-right: 5px;
+ padding: 5px;
+ }
+
+ .react-tagsinput-remove {
+ cursor: pointer;
+ font-weight: bold;
+ color: rgb(101, 24, 23);
+ }
+
+ .react-tagsinput-tag a::before {
+ content: " ×";
+ }
+
+ .react-tagsinput-input {
+ background: transparent;
+ border: 0;
+ color: #777;
+ font-family: sans-serif;
+ font-size: 13px;
+ font-weight: 400;
+ margin-bottom: 6px;
+ margin-top: 1px;
+ outline: none;
+ padding: 5px;
+ width: 90px;
+ }
+