From 06b941765095b19760bd6f0e72834bfca66d0f92 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Mon, 25 Sep 2023 12:51:15 +0200 Subject: [PATCH] Channel configuration UI: form validation. --- client/@types/global.d.ts | 2 + .../configuration/templates/logic/channel.ts | 79 ++++++++++++++++++- languages/en.yml | 4 +- server/lib/configuration/channel/sanitize.ts | 7 +- 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/client/@types/global.d.ts b/client/@types/global.d.ts index 6b382490..d0f60662 100644 --- a/client/@types/global.d.ts +++ b/client/@types/global.d.ts @@ -69,3 +69,5 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_QUOTE_DELAY_DESC: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BANNED_JIDS_LABEL: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_BOT_NICKNAME: string declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_FOR_MORE_INFO: string + +declare const LOC_INVALID_VALUE: string diff --git a/client/common/configuration/templates/logic/channel.ts b/client/common/configuration/templates/logic/channel.ts index b8f9a172..26479eec 100644 --- a/client/common/configuration/templates/logic/channel.ts +++ b/client/common/configuration/templates/logic/channel.ts @@ -132,8 +132,9 @@ async function vivifyConfigurationChannel ( ): Promise { const form = rootEl.querySelector('form[livechat-configuration-channel-options]') as HTMLFormElement if (!form) { return } - const labelSaved = await clientOptions.peertubeHelpers.translate(LOC_SUCCESSFULLY_SAVED) - const labelError = await clientOptions.peertubeHelpers.translate(LOC_ERROR) + const translate = clientOptions.peertubeHelpers.translate + const labelSaved = await translate(LOC_SUCCESSFULLY_SAVED) + const labelError = await translate(LOC_ERROR) const enableBotCB = form.querySelector('input[name=bot]') as HTMLInputElement const botEnabledEl = form.querySelectorAll('[livechat-configuration-channel-options-bot-enabled]') @@ -147,8 +148,70 @@ async function vivifyConfigurationChannel ( }) } + const removeDisplayedErrors = (): void => { + form.querySelectorAll('.form-error').forEach(el => el.remove()) + } + + const displayError = async (fieldSelector: string, message: string): Promise => { + form.querySelectorAll(fieldSelector).forEach(el => { + const erEl = document.createElement('div') + erEl.classList.add('form-error') + erEl.textContent = message + el.after(erEl) + }) + } + + const validateData: Function = async (channelConfigurationOptions: ChannelConfigurationOptions): Promise => { + const botConf = channelConfigurationOptions.bot + const errorFieldSelectors = [] + if (/[^\p{L}\p{N}\p{Z}_-]/u.test(botConf.nickname ?? '')) { + const selector = '#peertube-livechat-bot-nickname' + errorFieldSelectors.push(selector) + await displayError(selector, await translate(LOC_INVALID_VALUE)) + } + + for (let iFw = 0; iFw < botConf.forbiddenWords.length; iFw++) { + const fw = botConf.forbiddenWords[iFw] + if (fw.regexp) { + for (const v of fw.entries) { + if (v === '' || /^\s+$/.test(v)) { continue } + try { + // eslint-disable-next-line no-new + new RegExp(v) + } catch (err) { + const selector = '#peertube-livechat-forbidden-words-' + iFw.toString() + errorFieldSelectors.push(selector) + let message = await translate(LOC_INVALID_VALUE) + message += ` "${v}": ${err as string}` + await displayError(selector, message) + } + } + } + } + + for (let iCd = 0; iCd < botConf.commands.length; iCd++) { + const cd = botConf.commands[iCd] + if (/\s+/.test(cd.command)) { + const selector = '#peertube-livechat-command-' + iCd.toString() + errorFieldSelectors.push(selector) + const message = await translate(LOC_INVALID_VALUE) + await displayError(selector, message) + } + } + + if (errorFieldSelectors.length) { + // Set the focus to the first in-error field: + const el: HTMLInputElement | HTMLTextAreaElement | null = document.querySelector(errorFieldSelectors[0]) + el?.focus() + return false + } + + return true + } + const submitForm: Function = async () => { const data = new FormData(form) + removeDisplayedErrors() const channelConfigurationOptions: ChannelConfigurationOptions = { bot: { enabled: data.get('bot') === '1', @@ -160,8 +223,7 @@ async function vivifyConfigurationChannel ( } } - // TODO: handle form errors. - + // Note: but data in order, because validateData assume index are okay to find associated fields. for (let i = 0; data.has('forbidden_words_' + i.toString()); i++) { const entries = (data.get('forbidden_words_' + i.toString())?.toString() ?? '') .split(/\r?\n|\r|\n/g) @@ -180,6 +242,7 @@ async function vivifyConfigurationChannel ( channelConfigurationOptions.bot.forbiddenWords.push(fw) } + // Note: but data in order, because validateData assume index are okay to find associated fields. for (let i = 0; data.has('quote_' + i.toString()); i++) { const messages = (data.get('quote_' + i.toString())?.toString() ?? '') .split(/\r?\n|\r|\n/g) @@ -196,6 +259,7 @@ async function vivifyConfigurationChannel ( channelConfigurationOptions.bot.quotes.push(q) } + // Note: but data in order, because validateData assume index are okay to find associated fields. for (let i = 0; data.has('command_' + i.toString()); i++) { const command = (data.get('command_' + i.toString())?.toString() ?? '') const message = (data.get('command_message_' + i.toString())?.toString() ?? '') @@ -206,6 +270,10 @@ async function vivifyConfigurationChannel ( channelConfigurationOptions.bot.commands.push(c) } + if (!await validateData(channelConfigurationOptions)) { + throw new Error('Invalid form data') + } + const headers: any = clientOptions.peertubeHelpers.getAuthHeader() ?? {} headers['content-type'] = 'application/json;charset=UTF-8' @@ -235,6 +303,9 @@ async function vivifyConfigurationChannel ( enableBotCB.onclick = () => refresh() form.onsubmit = () => { toggleSubmit(true) + if (!form.checkValidity()) { + return false + } submitForm().then( () => { clientOptions.peertubeHelpers.notifier.success(labelSaved) diff --git a/languages/en.yml b/languages/en.yml index 1827edd4..2acfeb83 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -360,4 +360,6 @@ livechat_configuration_channel_command_message_desc: | livechat_configuration_channel_for_more_info: | For more information about how to configure this feature, please refer to the documentation by clicking on the help button. livechat_configuration_channel_banned_jids_label: "Banned users and patterns" -livechat_configuration_channel_bot_nickname: "Bot nickname" \ No newline at end of file +livechat_configuration_channel_bot_nickname: "Bot nickname" + +invalid_value: "Invalid value." diff --git a/server/lib/configuration/channel/sanitize.ts b/server/lib/configuration/channel/sanitize.ts index 4bc3e3c7..86920a96 100644 --- a/server/lib/configuration/channel/sanitize.ts +++ b/server/lib/configuration/channel/sanitize.ts @@ -64,7 +64,7 @@ function _readInteger (data: any, f: string, min: number, max: number): number { return v } -function _readSimpleInput (data: any, f: string, strict?: boolean): string { +function _readSimpleInput (data: any, f: string, strict?: boolean, noSpace?: boolean): string { if (!(f in data)) { return '' } @@ -78,6 +78,9 @@ function _readSimpleInput (data: any, f: string, strict?: boolean): string { // Replacing all invalid characters, no need to throw an error.. s = s.replace(/[^\p{L}\p{N}\p{Z}_-]$/gu, '') } + if (noSpace) { + s = s.replace(/\s+/g, '') + } return s } @@ -185,7 +188,7 @@ function _readCommands (botData: any): ChannelConfigurationOptions['bot']['comma const result: ChannelConfigurationOptions['bot']['commands'] = [] for (const cs of botData.commands) { const message = _readSimpleInput(cs, 'message') - const command = _readSimpleInput(cs, 'command') + const command = _readSimpleInput(cs, 'command', false, true) result.push({ message,