Merged branch master into admin-apollo

This commit is contained in:
Riley Davis
2017-02-08 13:42:37 -07:00
11 changed files with 285 additions and 26 deletions
@@ -11,6 +11,7 @@ export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
export const fetchSettings = () => dispatch => {
dispatch({type: SETTINGS_LOADING});
@@ -33,6 +34,10 @@ export const updateWordlist = (listName, list) => {
return {type: WORDLIST_UPDATED, listName, list};
};
export const updateDomainlist = (listName, list) => {
return {type: DOMAINLIST_UPDATED, listName, list};
};
export const saveSettingsToServer = () => (dispatch, getState) => {
let settings = getState().settings.toJS().settings;
if (settings.charCount) {
@@ -5,6 +5,7 @@ import {
updateSettings,
saveSettingsToServer,
updateWordlist,
updateDomainlist
} from '../../actions/settings';
import {Button, List, Item} from 'coral-ui';
@@ -14,6 +15,7 @@ import translations from '../../translations.json';
import EmbedLink from './EmbedLink';
import CommentSettings from './CommentSettings';
import Wordlist from './Wordlist';
import Domainlist from './Domainlist';
import has from 'lodash/has';
class Configure extends Component {
@@ -47,6 +49,11 @@ class Configure extends Component {
this.props.dispatch(updateWordlist(listName, list));
}
onChangeDomainlist = (listName, list) => {
this.setState({changed: true});
this.props.dispatch(updateDomainlist(listName, list));
}
onSettingUpdate = (setting) => {
this.setState({changed: true});
this.props.dispatch(updateSettings(setting));
@@ -73,7 +80,14 @@ class Configure extends Component {
errors={this.state.errors}
settingsError={this.onSettingError}/>;
case 'embed':
return <EmbedLink title={pageTitle} />;
return has(this, 'props.settings.domains.whitelist')
? <div>
<Domainlist
domains={this.props.settings.domains.whitelist}
onChangeDomainlist={this.onChangeDomainlist}/>
<EmbedLink title={pageTitle} />
</div>
: <EmbedLink title={pageTitle} />;
case 'wordlist':
return has(this, 'props.settings.wordlist')
? <Wordlist
@@ -0,0 +1,26 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import TagsInput from 'react-tagsinput';
import styles from './Configure.css';
import {Card} from 'coral-ui';
const Domainlist = ({domains, onChangeDomainlist}) => (
<div>
<h3>{lang.t('configure.domain-list-title')}</h3>
<Card id={styles.domainlist}>
<p className={styles.domainlistDesc}>{lang.t('configure.domain-list-text')}</p>
<TagsInput
value={domains}
inputProps={{placeholder: 'URL'}}
addOnPaste={true}
pasteSplit={data => data.split(',').map(d => d.trim())}
onChange={tags => onChangeDomainlist('whitelist', tags)}
/>
</Card>
</div>
);
export default Domainlist;
const lang = new I18n(translations);
@@ -6,6 +6,9 @@ const initialState = Map({
wordlist: Map({
banned: List(),
suspect: List()
}),
domains: Map({
whitelist: List()
})
}),
saveSettingsError: null,
@@ -24,6 +27,7 @@ export default (state = initialState, action) => {
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);
case types.DOMAINLIST_UPDATED: return updateDomainlist(state, action);
default: return state;
}
};
@@ -40,6 +44,10 @@ const updateWordlist = (state, action) => {
return state.setIn(['settings', 'wordlist', action.listName], action.list);
};
const updateDomainlist = (state, action) => {
return state.setIn(['settings', 'domains', action.listName], action.list);
};
const saveComplete = (state, action) => {
const s = state.set('fetchingSettings', false).set('saveSettingsError', null);
const settings = s.get('settings').merge(action.settings);
+9 -2
View File
@@ -79,7 +79,9 @@
"comment-count-header": "Limit Comment Length",
"comment-count-text-pre": "Comments will be limited to ",
"comment-count-text-post": " characters.",
"comment-count-error": "Please enter a valid number."
"comment-count-error": "Please enter a valid number.",
"domain-list-title": "Domain Whitelist",
"domain-list-text": "Some instructions on how to type the urls."
},
"bandialog": {
"ban_user": "Ban User?",
@@ -187,7 +189,12 @@
"comment-count-header": "Limitar el largo del comentario",
"comment-count-text-pre": "El largo de comentarios será ",
"comment-count-text-post": " caracteres",
"comment-count-error": "Por favor escribe un número válido."
"comment-count-error": "Por favor escribe un número válido.",
"domain-list-title": "Lista de Dominios Permitidos",
"domain-list-text": "Instrucciones de como ingresar las URLs."
},
"embedlink": {
"copy": "Copiar"
},
"bandialog": {
"ban_user": "Quieres suspender el Usuario?",
+6
View File
@@ -62,6 +62,12 @@ const SettingSchema = new Schema({
requireEmailConfirmation: {
type: Boolean,
default: false
},
domains: {
whitelist: {
type: Array,
default: ['localhost']
}
}
}, {
timestamps: {
+17 -7
View File
@@ -1,5 +1,7 @@
const AssetModel = require('../models/asset');
const SettingsService = require('./settings');
const domainlist = require('./domainlist');
const errors = require('../errors');
module.exports = class AssetsService {
@@ -53,16 +55,24 @@ module.exports = class AssetsService {
* @return {Promise}
*/
static findOrCreateByUrl(url) {
return AssetModel.findOneAndUpdate({url}, {url}, {
// Ensure that if it's new, we return the new object created.
new: true,
// Check the URL to confirm that is in the domain whitelist
return domainlist.urlCheck(url).then((whitelisted) => {
if (!whitelisted) {
return Promise.reject(errors.ErrInvalidAssetURL);
} else {
return AssetModel.findOneAndUpdate({url}, {url}, {
// Perform an upsert in the event that this doesn't exist.
upsert: true,
// Ensure that if it's new, we return the new object created.
new: true,
// Set the default values if not provided based on the mongoose models.
setDefaultsOnInsert: true
// Perform an upsert in the event that this doesn't exist.
upsert: true,
// Set the default values if not provided based on the mongoose models.
setDefaultsOnInsert: true
});
}
});
}
+110
View File
@@ -0,0 +1,110 @@
const debug = require('debug')('talk:services:domainlist');
const _ = require('lodash');
const SettingsService = require('./settings');
/**
* The root domainlist object.
* @type {Object}
*/
class Domainlist {
constructor() {
this.lists = {
whitelist: [],
};
}
/**
* Loads domains white list in from the database
*/
load() {
return SettingsService
.retrieve()
.then((settings) => {
// Insert the settings domains whitelist.
this.upsert(settings.domains);
});
}
/**
* Inserts the domains whitelist data
* @param {Array} list list of domains to be set to the whitelist
*/
upsert(lists) {
// Add the domains to this array and also be sure are all unique domains
if (!('whitelist' in lists)) {
return;
}
this.lists['whitelist'] = Domainlist.parseList(lists['whitelist']);
debug(`Added ${lists['whitelist'].length} domains to the whitelist.`);
return Promise.resolve(this);
}
/**
* Tests the url to see if it contains any of the whitelisted domains.
* @param {String} url value to match.
* @return {Boolean} true if the url contains any of the domains, false otherwise.
*/
match(list, url) {
const domainToMatch = Domainlist.parseURL(url);
// This will return true in the event that at least one blockword is found
// in the phrase.
for (let i = 0; i < list.length; i++) {
if (list[i] === domainToMatch) {
return true;
}
}
// We've walked over all the whitelisted domains, and haven't had a
// mismatch... It is not an allowed domain!
return false;
}
/**
* Parses the list content.
* @param {Array} list array of domains to parse for a list.
* @return {Array} the parsed list
*/
static parseList(list) {
return _.uniq(list.map((domain) => Domainlist.parseURL(domain)));
}
/**
* Parses the URL.
* @param {String} url url to parse for a domain.
* @return {String} the domain
*/
static parseURL(url){
let domain;
// removes protocol and get domain
if (url.indexOf('://') > -1) {
domain = url.split('/')[2];
} else {
domain = url.split('/')[0];
}
// remove port number
domain = domain.split(':')[0];
return domain.toLowerCase();
}
static urlCheck(url) {
const dl = new Domainlist();
return dl.load()
.then(() => {
return dl.match(dl.lists['whitelist'], url);
});
}
}
module.exports = Domainlist;
+19 -14
View File
@@ -10,24 +10,29 @@ chai.use(require('chai-http'));
const AssetModel = require('../../../../models/asset');
const AssetsService = require('../../../../services/assets');
const SettingsService = require('../../../../services/settings');
describe('/api/v1/assets', () => {
beforeEach(() => {
return AssetModel.create([
{
url: 'https://coralproject.net/news/asset1',
title: 'Asset 1',
description: 'term1',
closedAt: Date.now()
},
{
url: 'https://coralproject.net/news/asset2',
title: 'Asset 2',
description: 'term2',
closedAt: null
}
]);
const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['test.com']}};
return SettingsService.init(settings).then(() => {
return AssetModel.create([
{
url: 'https://coralproject.net/news/asset1',
title: 'Asset 1',
description: 'term1',
closedAt: Date.now()
},
{
url: 'https://coralproject.net/news/asset2',
title: 'Asset 2',
description: 'term2',
closedAt: null
}
]);
});
});
describe('#get', () => {
+18 -2
View File
@@ -1,5 +1,6 @@
const AssetModel = require('../../models/asset');
const AssetsService = require('../../services/assets');
const SettingsService = require('../../services/settings');
const chai = require('chai');
const expect = chai.expect;
@@ -10,8 +11,12 @@ chai.should();
describe('services.AssetsService', () => {
beforeEach(() => {
const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['new.test.com', 'test.com', 'override.test.com']}};
const defaults = {url:'http://test.com'};
return AssetModel.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
return SettingsService.init(settings).then(() => {
return AssetModel.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
});
});
describe('#findById', ()=> {
@@ -54,7 +59,7 @@ describe('services.AssetsService', () => {
});
});
it('should return a new asset when the url does not exist', () => {
it('should return a new asset when the url does not exist and its domain is whitelisted', () => {
return AssetsService
.findOrCreateByUrl('http://new.test.com')
.then((asset) => {
@@ -62,6 +67,17 @@ describe('services.AssetsService', () => {
.and.to.not.equal(1);
});
});
it('should return an error when the url does not exist and its domain is not whitelisted', () => {
return AssetsService
.findOrCreateByUrl('http://bad.test.com')
.then((asset) => {
expect(asset).to.be.null;
})
.catch((error) => {
expect(error).to.not.be.null;
});
});
});
describe('#overrideSettings', () => {
+52
View File
@@ -0,0 +1,52 @@
const expect = require('chai').expect;
const Domainlist = require('../../services/domainlist');
const SettingsService = require('../../services/settings');
describe('services.Domainlist', () => {
const domainlists = {
whitelist: [
'nytimes.com',
'wapo.com'
]
};
let domainlist = new Domainlist();
const settings = {id: '1', moderation: 'PRE', domainlist: {whitelist: ['nytimes.com', 'wapo.com']}};
beforeEach(() => SettingsService.init(settings));
describe('#init', () => {
before(() => domainlist.upsert(domainlists));
it('has entries', () => {
expect(domainlist.lists.whitelist).to.not.be.empty;
});
});
describe('#match', () => {
const whiteList = Domainlist.parseList(domainlists['whitelist']);
it('does match on an included domain', () => {
[
'wapo.com',
'nytimes.com'
].forEach((domain) => {
expect(domainlist.match(whiteList, domain)).to.be.true;
});
});
it('does not match on a not included domain', () => {
[
'badsite.com',
'www.badsite.com',
'otherexample.com'
].forEach((domain) => {
expect(domainlist.match(whiteList, domain)).to.be.false;
});
});
});
});