Add replace filter mode

merge-requests/341/head
disco 4 months ago
parent d928f4b6b4
commit f327179385
  1. 3
      controllers/forms/addfilter.js
  2. 3
      controllers/forms/editfilter.js
  3. 3
      db/filters.js
  4. 10
      lib/post/checkfilters.js
  5. 27
      lib/post/filteractions.js
  6. 1
      models/forms/addfilter.js
  7. 3
      models/forms/editfilter.js
  8. 29
      models/forms/editpost.js
  9. 36
      models/forms/makepost.js
  10. 1
      views/custompages/faq.pug
  11. 4
      views/includes/filtereditform.pug
  12. 6
      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;
};

@ -4,23 +4,22 @@ const { Bans } = require(__dirname+'/../../db/')
, dynamicResponse = require(__dirname+'/../misc/dynamic.js');
//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,16 @@ 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
//name field omitted to prevent malicious filter from revealing tripcodes
const fields = ['email','subject','message'];
for (const field of fields) {
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) {

@ -26,7 +26,7 @@ todo: handle some more situations
*/
const { __ } = res.locals;
const { previewReplies } = config.get;
const { globalLimits, previewReplies } = config.get;
const { board, post } = res.locals;
//filters
@ -34,12 +34,31 @@ todo: handle some more situations
//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);
}
const fields = ['name','email','subject','message'];
for (const field of fields) {
//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
});
}
}
}
}
}

@ -120,23 +120,43 @@ 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);
}
const fields = ['name','email','subject','message'];
for (const field of fields) {
//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
});
}
}
}
}
}

@ -275,6 +275,7 @@ block content
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,11 +20,14 @@ 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
input(type='text' name='filter_ban_duration' placeholder=__('e.g. 3d'))
td.text-center
label
input(type='checkbox', name='filter_ban_appealable', value='true')
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