Compare commits

...

4 Commits

  1. 3
      controllers/forms/addfilter.js
  2. 3
      controllers/forms/editfilter.js
  3. 3
      db/filters.js
  4. 10
      lib/post/checkfilters.js
  5. 29
      lib/post/filteractions.js
  6. 1
      models/forms/addfilter.js
  7. 3
      models/forms/editfilter.js
  8. 32
      models/forms/editpost.js
  9. 38
      models/forms/makepost.js
  10. 3
      views/custompages/faq.pug
  11. 4
      views/includes/filtereditform.pug
  12. 4
      views/includes/filternewform.pug
  13. 4
      views/mixins/filter.pug
  14. 1
      views/pages/globalmanagefilters.pug
  15. 1
      views/pages/managefilters.pug

@ -20,8 +20,9 @@ module.exports = {
const errors = await checkSchema([
{ result: lengthBody(req.body.filters, 0, 50000), expected: false, error: __('Filter text cannot exceed 50000 characters') },
{ result: numberBody(req.body.filter_mode, 0, 2), expected: true, error: __('Filter mode must be a number from 0-2') },
{ result: numberBody(req.body.filter_mode, 0, 3), expected: true, error: __('Filter mode must be a number from 0-3') },
{ result: numberBody(req.body.filter_ban_duration), expected: true, error: __('Invalid filter auto ban duration') },
{ result: lengthBody(req.body.replace_text, 0, 500), expected: false, error: __('Replace text cannot exceed 500 characters') },
]);
if (errors.length > 0) {

@ -21,8 +21,9 @@ module.exports = {
const errors = await checkSchema([
{ result: existsBody(req.body.filter_id), expected: true, error: __('Missing filter id') },
{ result: lengthBody(req.body.filters, 0, 50000), expected: false, error: __('Filter text cannot exceed 50000 characters') },
{ result: numberBody(req.body.filter_mode, 0, 2), expected: true, error: __('Filter mode must be a number from 0-2') },
{ result: numberBody(req.body.filter_mode, 0, 3), expected: true, error: __('Filter mode must be a number from 0-3') },
{ result: numberBody(req.body.filter_ban_duration), expected: true, error: __('Invalid filter auto ban duration') },
{ result: lengthBody(req.body.replace_text, 0, 500), expected: false, error: __('Replace text cannot exceed 500 characters') },
]);
if (errors.length > 0) {

@ -42,7 +42,7 @@ module.exports = {
});
},
updateOne: async (board=null, id, filters, strictFiltering, filterMode, filterMessage, filterBanDuration, filterBanAppealable) => {
updateOne: async (board=null, id, filters, strictFiltering, filterMode, filterMessage, filterBanDuration, filterBanAppealable, replaceText) => {
const updatedFilter = await db.updateOne({
'_id': id,
'board': board,
@ -54,6 +54,7 @@ module.exports = {
'filterMessage': filterMessage,
'filterBanDuration': filterBanDuration,
'filterBanAppealable': filterBanAppealable,
'replaceText': replaceText,
}
});
await cache.del(`filters:${board}`);

@ -2,15 +2,21 @@
module.exports = (filters, combinedString, strictCombinedString) => {
let filterHits = [];
for (const filter of filters) {
if (filter.filterMode === 0) { continue; } //skip "Do nothing" mode filters
const string = filter.strictFiltering ? strictCombinedString : combinedString;
const hitFilter = filter.filters.find(match => string.includes(match.toLowerCase()) );
if (hitFilter) {
return [ hitFilter, filter.filterMode, filter.filterMessage, filter.filterBanDuration, filter.filterBanAppealable ];
//if either of these are hit, we can stop checking
if (filter.filterMode === 1 || filter.filterMode === 2) {
return [{ h: hitFilter, f: filter }];
} else {
filterHits.push({ h: hitFilter, f: filter });
}
}
}
return false;
return filterHits.length ? filterHits : false;
};

@ -1,26 +1,25 @@
'use strict';
const { Bans } = require(__dirname+'/../../db/')
, dynamicResponse = require(__dirname+'/../misc/dynamic.js');
, dynamicResponse = require(__dirname+'/../misc/dynamic.js')
, FIELDS_TO_REPLACE = ['email', 'subject', 'message'];
//ehhh, kinda too many args
module.exports = async (req, res, globalFilter, hitFilter, filterMode,
filterMessage, filterBanDuration, filterBanAppealable, redirect) => {
module.exports = async (req, res, globalFilter, hit, filter, redirect) => {
const { __ } = res.locals;
const blockMessage = __('Your post was blocked by a word filter') + (filterMessage ? ': ' + filterMessage : '');
const blockMessage = __('Your post was blocked by a word filter') + (filter.filterMessage ? ': ' + filter.filterMessage : '');
if (filterMode === 1) {
if (filter.filterMode === 1) {
return dynamicResponse(req, res, 400, 'message', {
'title': __('Bad request'),
'message': blockMessage,
'redirect': redirect
});
} else {
} else if (filter.filterMode === 2) {
const banBoard = globalFilter ? null : res.locals.board._id;
const banDate = new Date();
const banExpiry = new Date(filterBanDuration + banDate.getTime());
const banExpiry = new Date(filter.filterBanDuration + banDate.getTime());
const ban = {
'ip': {
'cloak': res.locals.ip.cloak,
@ -28,15 +27,15 @@ module.exports = async (req, res, globalFilter, hitFilter, filterMode,
'type': res.locals.ip.type,
},
'range': 0,
'reason': __(`${globalFilter ? 'global ' :''}word filter auto ban`) + (filterMessage ? ': ' + filterMessage : ''),
'reason': __(`${globalFilter ? 'global ' :''}word filter auto ban`) + (filter.filterMessage ? ': ' + filter.filterMessage : ''),
'board': banBoard,
'posts': null,
'issuer': 'system', //todo: make a "system" property instead?
'date': banDate,
'expireAt': banExpiry,
'allowAppeal': filterBanAppealable,
'allowAppeal': filter.filterBanAppealable,
'showUser': true,
'note': __(`${globalFilter ? 'global ' :''}filter hit: "%s"`, (hitFilter)),
'note': __(`${globalFilter ? 'global ' :''}filter hit: "%s"`, (hit)),
'seen': true,
};
const insertedResult = await Bans.insertOne(ban);
@ -47,6 +46,14 @@ module.exports = async (req, res, globalFilter, hitFilter, filterMode,
return dynamicResponse(req, res, 403, 'ban', {
bans: [ban]
});
} else {
//the filter could have hit any part of the combinedstring
for (const field of FIELDS_TO_REPLACE) {
if (req.body[field]) {
req.body[field] = req.body[field].replaceAll(hit, filter.replaceText);
}
}
return;
}
};

@ -29,6 +29,7 @@ module.exports = async (req, res) => {
'filterMessage': req.body.filter_message,
'filterBanDuration': req.body.filter_ban_duration,
'filterBanAppealable': req.body.filter_ban_appealable ? true : false,
'replaceText': req.body.replace_text,
};
await Filters.insertOne(filter);

@ -15,7 +15,8 @@ module.exports = async (req, res) => {
req.body.filter_mode,
req.body.filter_message,
req.body.filter_ban_duration,
req.body.filter_ban_appealable ? true : false
req.body.filter_ban_appealable ? true : false,
req.body.replace_text
).then(r => r.matchedCount);
if (updated === 0) {

@ -15,7 +15,8 @@ const { Posts, Modlogs, Filters } = require(__dirname+'/../../db/')
, buildQueue = require(__dirname+'/../../lib/build/queue.js')
, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
, Socketio = require(__dirname+'/../../lib/misc/socketio.js')
, { buildThread } = require(__dirname+'/../../lib/build/tasks.js');
, { buildThread } = require(__dirname+'/../../lib/build/tasks.js')
, FIELDS_TO_REPLACE = ['email', 'subject', 'message'];
module.exports = async (req, res) => {
@ -26,20 +27,39 @@ todo: handle some more situations
*/
const { __ } = res.locals;
const { previewReplies } = config.get;
const { globalLimits, previewReplies } = config.get;
const { board, post } = res.locals;
//filters
if (!res.locals.permissions.get(Permissions.BYPASS_FILTERS)) {
//only global filters are checked, because anybody who could edit bypasses board filters
const globalFilters = await Filters.findForBoard(null);
let hitFilter = false;
let hitFilters = false;
let { combinedString, strictCombinedString } = getFilterStrings(req, res);
hitFilter = checkFilters(globalFilters, combinedString, strictCombinedString);
if (hitFilter) {
return filterActions(req, res, true, hitFilter[0], hitFilter[1], hitFilter[2], hitFilter[3], hitFilter[4], null);
hitFilters = checkFilters(globalFilters, combinedString, strictCombinedString);
if (hitFilters) {
//if block or ban matched, only it is returned
if (hitFilters[0].f.filterMode === 1 || hitFilters[0].f.filterMode === 2) {
return filterActions(req, res, true, hitFilters[0].h, hitFilters[0].f, null);
} else {
for (const o of hitFilters) {
await filterActions(req, res, true, o.h, o.f, null);
}
for (const field of FIELDS_TO_REPLACE) {
//check filters haven't pushed a field past its limit
if (req.body[field] && (req.body[field].length > globalLimits.fieldLength[field])) {
return dynamicResponse(req, res, 400, 'message', {
'title': __('Bad request'),
'message': __(`After applying filters, ${field} exceeds maximum length of %s`, globalLimits.fieldLength[field]),
'redirect': null
});
}
}
}
}
}

@ -33,7 +33,8 @@ const { createHash, randomBytes } = require('crypto')
, { postPasswordSecret } = require(__dirname+'/../../configs/secrets.js')
, buildQueue = require(__dirname+'/../../lib/build/queue.js')
, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
, { buildThread } = require(__dirname+'/../../lib/build/tasks.js');
, { buildThread } = require(__dirname+'/../../lib/build/tasks.js')
, FIELDS_TO_REPLACE = ['email', 'subject', 'message'];
module.exports = async (req, res) => {
@ -120,23 +121,42 @@ module.exports = async (req, res) => {
Filters.findForBoard(res.locals.board._id)
]);
let hitFilter = false
let hitFilters = false
, globalFilter = false;
let { combinedString, strictCombinedString } = getFilterStrings(req, res);
//compare to global filters
hitFilter = checkFilters(globalFilters, combinedString, strictCombinedString);
if (hitFilter) {
hitFilters = checkFilters(globalFilters, combinedString, strictCombinedString);
if (hitFilters) {
globalFilter = true;
}
//if none matched, check local filters
if (!hitFilter) {
hitFilter = checkFilters(localFilters, combinedString, strictCombinedString);
if (!hitFilters) {
hitFilters = checkFilters(localFilters, combinedString, strictCombinedString);
}
if (hitFilter) {
await deleteTempFiles(req).catch(console.error);
return filterActions(req, res, globalFilter, hitFilter[0], hitFilter[1], hitFilter[2], hitFilter[3], hitFilter[4], redirect);
if (hitFilters) {
//if block or ban matched, only it is returned
if (hitFilters[0].f.filterMode === 1 || hitFilters[0].f.filterMode === 2) {
await deleteTempFiles(req).catch(console.error);
return filterActions(req, res, globalFilter, hitFilters[0].h, hitFilters[0].f, redirect);
} else {
for (const o of hitFilters) {
await filterActions(req, res, globalFilter, o.h, o.f, redirect);
}
for (const field of FIELDS_TO_REPLACE) {
//check filters haven't pushed a field past its limit
if (req.body[field] && (req.body[field].length > globalLimits.fieldLength[field])) {
await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': __('Bad request'),
'message': __(`After applying filters, ${field} exceeds maximum length of %s`, globalLimits.fieldLength[field]),
'redirect': redirect
});
}
}
}
}
}

@ -272,7 +272,10 @@ block content
p Filters: Newline separated list of words or phrases to match in posts. Checks name, message, email, subject, and filenames.
p Strict Filtering: More aggressively match filters, by normalising the input compared against the filters.
p Filter Mode: What to do when a post matches a filter.
p Block/Ban Message: The message returned if a post is blocked, or the ban reason if a poster is banned.
p Filter Auto Ban Duration: How long to automatically ban for when filter mode is set to ban. Input the duration in time format described in the #[a(href='#moderation') moderation section].
p Filter Bans Appealable: Whether or not a ban given by this filter can be appealed. This can be useful if the ban hits an innocent user by mistake.
p Replace Text: Text to substitute in for a replace filter. This is how word filters are added.
.table-container.flex-center.mv-5
.anchor#archive-reverse-url-format
table

@ -9,6 +9,7 @@ input(type='hidden' name='filter_id' value=filter._id)
th #{__('Block/Ban Message')}
th #{__('Filter Auto Ban Duration')}
th #{__('Filter Bans Appealable')}
th #{__('Replace Text')}
tr
td
textarea(name='filters' rows='10' placeholder=__('Newline separated') required) #{filter.filters.join('\n')}
@ -19,10 +20,13 @@ input(type='hidden' name='filter_id' value=filter._id)
option(value='0', selected=filter.filterMode === 0) #{__('Do nothing')}
option(value='1', selected=filter.filterMode === 1) #{__('Block post')}
option(value='2', selected=filter.filterMode === 2) #{__('Ban')}
option(value='3', selected=filter.filterMode === 3) #{__('Replace')}
td
input(type='text' name='filter_message' placeholder=__('e.g. Rule 1: No ad spam') value=filter.filterMessage)
td
input(type='text' name='filter_ban_duration' placeholder=__('e.g. 3d') value=filter.filterBanDuration)
td.text-center
input(type='checkbox', name='filter_ban_appealable', value='true', checked=filter.filterBanAppealable)
td
input(type='text' name='replace_text' value=filter.replaceText)
input(type='submit', value=__('Save'))

@ -8,6 +8,7 @@ input(type='hidden' name='_csrf' value=csrf)
th #{__('Block/Ban Message')}
th #{__('Filter Auto Ban Duration')}
th #{__('Filter Bans Appealable')}
th #{__('Replace Text')}
tr
td
textarea(name='filters' rows='3' placeholder=__('Newline separated') required)
@ -19,6 +20,7 @@ input(type='hidden' name='_csrf' value=csrf)
option(value='0', selected=true) #{__('Do nothing')}
option(value='1') #{__('Block post')}
option(value='2') #{__('Ban')}
option(value='3') #{__('Replace')}
td
input(type='text' name='filter_message' placeholder=__('e.g. Rule 1: No ad spam'))
td
@ -26,4 +28,6 @@ input(type='hidden' name='_csrf' value=csrf)
td.text-center
label
input(type='checkbox', name='filter_ban_appealable', value='true', checked='checked')
td
input(type='text' name='replace_text')
input(type='submit', value=__('Submit'))

@ -15,6 +15,8 @@ mixin filter(f, globalmanage=false)
p.no-m-p.nowrap #{__('Block post')}
else if f.filterMode === 2
p.no-m-p.nowrap #{__('Ban')}
else if f.filterMode === 3
p.no-m-p.nowrap #{__('Replace')}
else
p.no-m-p.nowrap ?
td
@ -24,6 +26,8 @@ mixin filter(f, globalmanage=false)
td.text-center
label
input(type='checkbox', name='filter_ban_appealable', value='true', checked=f.filterBanAppealable, disabled='true')
td
p.no-m-p #{f.replaceText}
td
if globalmanage
a.ml-5(href=`/globalmanage/editfilter/${f._id}.html`, style='overflow-wrap: break-word;') [#{__('Edit')}]

@ -34,6 +34,7 @@ block content
th #{__('Block/Ban Message')}
th #{__('Filter Auto Ban Duration')}
th #{__('Filter Bans Appealable')}
th #{__('Replace Text')}
th #{__('Edit')}
each f in filters
+filter(f, true)

@ -35,6 +35,7 @@ block content
th #{__('Block/Ban Message')}
th #{__('Filter Auto Ban Duration')}
th #{__('Filter Bans Appealable')}
th #{__('Replace Text')}
th #{__('Edit')}
each f in filters
+filter(f)

Loading…
Cancel
Save