From 0af03978e5398b94440bd0860ebb43ee8aa50a82 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Mon, 10 Aug 2020 01:31:32 +0300 Subject: [PATCH] Fixes to RegExpRunner usage in !search and !bansearch, validate that input regex is valid --- backend/src/plugins/Utility/search.ts | 47 ++++++++++++++++------- backend/src/utils/async.ts | 54 +++++++++++++++++++++++++++ backend/src/validatorUtils.ts | 8 +++- 3 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 backend/src/utils/async.ts diff --git a/backend/src/plugins/Utility/search.ts b/backend/src/plugins/Utility/search.ts index 89109b70..491b27c7 100644 --- a/backend/src/plugins/Utility/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -11,6 +11,8 @@ import { UtilityPluginType } from "./types"; import { refreshMembersIfNeeded } from "./refreshMembers"; import { getUserInfoEmbed } from "./functions/getUserInfoEmbed"; import { allowTimeout } from "../../RegExpRunner"; +import { inputPatternToRegExp, InvalidRegexError } from "../../validatorUtils"; +import { asyncFilter } from "../../utils/async"; const SEARCH_RESULTS_PER_PAGE = 15; const SEARCH_ID_RESULTS_PER_PAGE = 50; @@ -83,6 +85,10 @@ export async function displaySearch( return sendErrorMessage(pluginData, msg.channel, e.message); } + if (e instanceof InvalidRegexError) { + return sendErrorMessage(pluginData, msg.channel, e.message); + } + throw e; } @@ -199,6 +205,10 @@ export async function archiveSearch( return sendErrorMessage(pluginData, msg.channel, e.message); } + if (e instanceof InvalidRegexError) { + return sendErrorMessage(pluginData, msg.channel, e.message); + } + throw e; } @@ -257,28 +267,33 @@ async function performMemberSearch( if (args.query) { let queryRegex: RegExp; if (args.regex) { - queryRegex = new RegExp(args.query.trimStart(), args["case-sensitive"] ? "" : "i"); + const flags = args["case-sensitive"] ? "" : "i"; + queryRegex = inputPatternToRegExp(args.query.trimStart()); + queryRegex = new RegExp(queryRegex.source, flags); } else { queryRegex = new RegExp(escapeStringRegexp(args.query.trimStart()), args["case-sensitive"] ? "" : "i"); } if (args["status-search"]) { - matchingMembers = matchingMembers.filter(member => { + matchingMembers = await asyncFilter(matchingMembers, async member => { if (member.game) { - if (member.game.name && pluginData.state.regexRunner.exec(queryRegex, member.game.name).catch(allowTimeout)) { + if ( + member.game.name && + (await pluginData.state.regexRunner.exec(queryRegex, member.game.name).catch(allowTimeout)) + ) { return true; } if ( member.game.state && - pluginData.state.regexRunner.exec(queryRegex, member.game.state).catch(allowTimeout) + (await pluginData.state.regexRunner.exec(queryRegex, member.game.state).catch(allowTimeout)) ) { return true; } if ( member.game.details && - pluginData.state.regexRunner.exec(queryRegex, member.game.details).catch(allowTimeout) + (await pluginData.state.regexRunner.exec(queryRegex, member.game.details).catch(allowTimeout)) ) { return true; } @@ -286,14 +301,14 @@ async function performMemberSearch( if (member.game.assets) { if ( member.game.assets.small_text && - pluginData.state.regexRunner.exec(queryRegex, member.game.assets.small_text).catch(allowTimeout) + (await pluginData.state.regexRunner.exec(queryRegex, member.game.assets.small_text).catch(allowTimeout)) ) { return true; } if ( member.game.assets.large_text && - pluginData.state.regexRunner.exec(queryRegex, member.game.assets.large_text).catch(allowTimeout) + (await pluginData.state.regexRunner.exec(queryRegex, member.game.assets.large_text).catch(allowTimeout)) ) { return true; } @@ -301,7 +316,7 @@ async function performMemberSearch( if ( member.game.emoji && - pluginData.state.regexRunner.exec(queryRegex, member.game.emoji.name).catch(allowTimeout) + (await pluginData.state.regexRunner.exec(queryRegex, member.game.emoji.name).catch(allowTimeout)) ) { return true; } @@ -309,11 +324,12 @@ async function performMemberSearch( return false; }); } else { - matchingMembers = matchingMembers.filter(member => { - if (member.nick && pluginData.state.regexRunner.exec(queryRegex, member.nick).catch(allowTimeout)) return true; + matchingMembers = await asyncFilter(matchingMembers, async member => { + if (member.nick && (await pluginData.state.regexRunner.exec(queryRegex, member.nick).catch(allowTimeout))) + return true; const fullUsername = `${member.user.username}#${member.user.discriminator}`; - if (pluginData.state.regexRunner.exec(queryRegex, fullUsername).catch(allowTimeout)) return true; + if (await pluginData.state.regexRunner.exec(queryRegex, fullUsername).catch(allowTimeout)) return true; return false; }); @@ -363,14 +379,17 @@ async function performBanSearch( if (args.query) { let queryRegex: RegExp; if (args.regex) { - queryRegex = new RegExp(args.query.trimStart(), args["case-sensitive"] ? "" : "i"); + const flags = args["case-sensitive"] ? "" : "i"; + queryRegex = inputPatternToRegExp(args.query.trimStart()); + queryRegex = new RegExp(queryRegex.source, flags); } else { queryRegex = new RegExp(escapeStringRegexp(args.query.trimStart()), args["case-sensitive"] ? "" : "i"); } - matchingBans = matchingBans.filter(user => { + matchingBans = await asyncFilter(matchingBans, async user => { const fullUsername = `${user.username}#${user.discriminator}`; - if (pluginData.state.regexRunner.exec(queryRegex, fullUsername).catch(allowTimeout)) return true; + if (await pluginData.state.regexRunner.exec(queryRegex, fullUsername).catch(allowTimeout)) return true; + return false; }); } diff --git a/backend/src/utils/async.ts b/backend/src/utils/async.ts new file mode 100644 index 00000000..ee877c1a --- /dev/null +++ b/backend/src/utils/async.ts @@ -0,0 +1,54 @@ +import { Awaitable } from "knub/dist/utils"; + +export async function asyncReduce( + arr: T[], + callback: (accumulator: V, currentValue: T, index: number, array: T[]) => Awaitable, + initialValue?: V, +): Promise { + let accumulator; + let arrayToIterate; + if (initialValue !== undefined) { + accumulator = initialValue; + arrayToIterate = arr; + } else { + accumulator = arr[0]; + arrayToIterate = arr.slice(1); + } + + for (const [i, currentValue] of arr.entries()) { + accumulator = await callback(accumulator, currentValue, i, arr); + } + + return accumulator; +} + +export function asyncFilter( + arr: T[], + callback: (element: T, index: number, array: T[]) => Awaitable, +): Promise { + return asyncReduce( + arr, + async (newArray, element, i, _arr) => { + if (await callback(element, i, _arr)) { + newArray.push(element); + } + + return newArray; + }, + [], + ); +} + +export function asyncMap( + arr: T[], + callback: (currentValue: T, index: number, array: T[]) => Awaitable, +): Promise { + return asyncReduce( + arr, + async (newArray, element, i, _arr) => { + newArray.push(await callback(element, i, _arr)); + return newArray; + }, + [], + ); +} diff --git a/backend/src/validatorUtils.ts b/backend/src/validatorUtils.ts index 9c469bb9..8c1e1615 100644 --- a/backend/src/validatorUtils.ts +++ b/backend/src/validatorUtils.ts @@ -7,13 +7,19 @@ import safeRegex from "safe-regex"; const regexWithFlags = /^\/(.*?)\/([i]*)$/; +export class InvalidRegexError extends Error {} + /** * This function supports two input syntaxes for regexes: // and just */ export function inputPatternToRegExp(pattern: string) { const advancedSyntaxMatch = pattern.match(regexWithFlags); const [finalPattern, flags] = advancedSyntaxMatch ? [advancedSyntaxMatch[1], advancedSyntaxMatch[2]] : [pattern, ""]; - return new RegExp(finalPattern, flags); + try { + return new RegExp(finalPattern, flags); + } catch (e) { + throw new InvalidRegexError(e.message); + } } export const TRegex = new t.Type(