From a4fed0ea6b2c5b54cebbe93dba22166499ebcf00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Huchet?= Date: Fri, 25 Mar 2022 10:19:50 +0100 Subject: [PATCH] fix(dashboard): sanitize imports (#530) --- .../scenes/data-import-export/ImportData.js | 25 ++--- .../data-import-export/importSanitizer.js | 72 ++++++++++++++ dashboard/src/utils.js | 99 ++----------------- 3 files changed, 92 insertions(+), 104 deletions(-) create mode 100644 dashboard/src/scenes/data-import-export/importSanitizer.js diff --git a/dashboard/src/scenes/data-import-export/ImportData.js b/dashboard/src/scenes/data-import-export/ImportData.js index 32bc3288c..bbf140e0c 100644 --- a/dashboard/src/scenes/data-import-export/ImportData.js +++ b/dashboard/src/scenes/data-import-export/ImportData.js @@ -4,7 +4,6 @@ import XLSX from 'xlsx'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { toastr } from 'react-redux-toastr'; import { Modal, ModalBody, ModalHeader } from 'reactstrap'; - import ButtonCustom from '../../components/ButtonCustom'; import { customFieldsPersonsMedicalSelector, @@ -14,9 +13,10 @@ import { preparePersonForEncryption, } from '../../recoil/persons'; import { teamsState, userState } from '../../recoil/auth'; -import { isNullOrUndefined, typeOptions } from '../../utils'; +import { isNullOrUndefined } from '../../utils'; import useApi, { encryptItem, hashedOrgEncryptionKey } from '../../services/api'; import { formatDateWithFullMonth, now } from '../../services/date'; +import { sanitizeFieldValue } from './importSanitizer'; const ImportData = () => { const user = useRecoilValue(userState); @@ -82,29 +82,24 @@ const ImportData = () => { setIgnoredFields(fieldsToIgnore); const headersCellsToImport = headerCells.filter((headerKey) => importableLabels.includes(personsSheet[headerKey].v?.trim())); - const headerColumnsAndFieldname = headersCellsToImport.map((cell) => { + const headerColumnsAndField = headersCellsToImport.map((cell) => { const column = cell.replace('1', ''); // ['A', 'B'...] const field = importableFields.find((f) => f.label === personsSheet[cell].v?.trim()); // { name: type: label: importable: options: } - const fieldname = field.name; // 'name', 'gender', ... - const type = typeOptions.find((typeOption) => typeOption.value === field.type); // { value: label: validator: } - const validator = field.options ? type.validator(field.options) : type.validator; - return [column, fieldname, validator]; - }); // [['C', 'name], ['D', birthdate]] + return [column, field]; + }); setImportedFields(headersCellsToImport.map((headerKey) => personsSheet[headerKey].v?.trim())); - - // .replace(/[^a-zA-Z]+/g, '') const lastRow = parseInt(personsSheet['!ref'].split(':')[1].replace(/\D+/g, ''), 10); const persons = []; for (let i = 2; i <= lastRow; i++) { const person = {}; - for (const [column, fieldname, validator] of headerColumnsAndFieldname) { + for (const [column, field] of headerColumnsAndField) { if (!personsSheet[`${column}${i}`]) continue; - const value = validator(personsSheet[`${column}${i}`]); + const value = sanitizeFieldValue(field, personsSheet[`${column}${i}`]); if (!isNullOrUndefined(value)) { - person[fieldname] = value; - if (fieldname === 'assignedTeams' && value.length > 0) { - person[fieldname] = value.map((teamName) => teams.find((team) => team.name === teamName)?._id).filter((a) => a); + person[field.name] = value; + if (field.name === 'assignedTeams' && value.length > 0) { + person[field.name] = value.map((teamName) => teams.find((team) => team.name === teamName)?._id).filter((a) => a); } } } diff --git a/dashboard/src/scenes/data-import-export/importSanitizer.js b/dashboard/src/scenes/data-import-export/importSanitizer.js new file mode 100644 index 000000000..a07ac4056 --- /dev/null +++ b/dashboard/src/scenes/data-import-export/importSanitizer.js @@ -0,0 +1,72 @@ +import { dayjsInstance } from '../../services/date'; + +const sanitizeString = (value) => { + if (!value) return null; + if (typeof value === 'string') return value; + if (typeof `${value}` === 'string') return `${value}`; + return null; +}; + +const sanitizeNumber = (value) => { + if (!isNaN(value)) return value; + if (!isNaN(parseInt(value, 10))) return parseInt(value, 10); + return null; +}; + +const sanitizeDate = (value) => { + // https://stackoverflow.com/a/643827/5225096 + if (typeof value?.getMonth === 'function' || value instanceof dayjsInstance) return value; + if (!isNaN(new Date(value).getMonth())) return new Date(value); + return null; +}; + +const sanitizeYesNo = (value) => { + value = sanitizeString(value); + if (!value) return null; + if (['Oui', 'Non'].includes(value)) return value; + if (value === 'No') return 'Non'; + if (value === 'Yes') return 'Oui'; + return null; +}; + +const sanitizeEnum = (value, possibleValues = []) => { + value = sanitizeString(value); + if (!value) return null; + if (possibleValues.includes(value)) return value; + return null; +}; + +const sanitizeMultiChoice = (value, possibleValues = []) => { + // value is either string or array + if (!Array.isArray(value)) { + value = sanitizeString(value); + if (!value) return null; + value = value.split(','); + } + value = value.filter((value) => possibleValues.includes(value)); + if (value.length) return value; + return null; +}; + +const sanitizeBoolean = (value) => { + if (typeof value === 'undefined') return null; + // We have to handle the case where value is a string (cf: import XLSX users). + if (typeof value === 'string') { + if (['true', 'oui', 'yes'].includes(value.toLowerCase())) return true; + if (['false', 'non', 'no'].includes(value.toLowerCase())) return false; + } + return Boolean(value); +}; + +export function sanitizeFieldValue(field, { v: rawValue, w: formattedText }) { + if (field.type === 'text') return sanitizeString(rawValue); + if (field.type === 'textarea') return sanitizeString(rawValue); + if (field.type === 'number') return sanitizeNumber(rawValue); + if (field.type === 'date') return sanitizeDate(formattedText); + if (field.type === 'date-with-time') return sanitizeDate(formattedText); + if (field.type === 'yes-no') return sanitizeYesNo(rawValue); + if (field.type === 'enum') return sanitizeEnum(rawValue, field.options); + if (field.type === 'multi-choice') return sanitizeMultiChoice(rawValue, field.options); + if (field.type === 'boolean') return sanitizeBoolean(rawValue); + return rawValue; +} diff --git a/dashboard/src/utils.js b/dashboard/src/utils.js index ca7a8528f..5beb6930c 100644 --- a/dashboard/src/utils.js +++ b/dashboard/src/utils.js @@ -1,87 +1,19 @@ -import { dayjsInstance } from './services/date'; - const isNullOrUndefined = (value) => { if (typeof value === 'undefined') return true; if (value === null) return true; return false; }; -// These validators work only for Excel import. -// Todo: move this out out of utils and explain their scope. -const validateString = ({ v: value }) => { - if (!value) return null; - if (typeof value === 'string') return value; - if (typeof `${value}` === 'string') return `${value}`; - return null; -}; - -const validateNumber = ({ v: value }) => { - if (!isNaN(value)) return value; - if (!isNaN(parseInt(value, 10))) return parseInt(value, 10); - return null; -}; - -const validateDate = ({ w: value }) => { - // https://stackoverflow.com/a/643827/5225096 - if (typeof value?.getMonth === 'function' || value instanceof dayjsInstance) return value; - if (!isNaN(new Date(value).getMonth())) return new Date(value); - return null; -}; - -const validateYesNo = - (possibleValues = ['Oui', 'Non']) => - ({ v: value }) => { - value = validateString({ v: value }); - if (!value) return null; - if (possibleValues.includes(value)) return value; - if (value === 'No') return 'Non'; - if (value === 'Yes') return 'Oui'; - return null; - }; - -const validateEnum = - (possibleValues = []) => - ({ v: value }) => { - value = validateString({ v: value }); - if (!value) return null; - if (possibleValues.includes(value)) return value; - return null; - }; - -const validateMultiChoice = - (possibleValues = []) => - ({ v: value }) => { - // value is either string or array - if (!Array.isArray(value)) { - value = validateString({ v: value }); - if (!value) return null; - value = value.split(','); - } - value = value.filter((value) => possibleValues.includes(value)); - if (value.length) return value; - return null; - }; - -const validateBoolean = ({ v: value }) => { - if (typeof value === 'undefined') return null; - // We have to handle the case where value is a string (cf: import XLSX users). - if (typeof value === 'string') { - if (['true', 'oui', 'yes'].includes(value.toLowerCase())) return true; - if (['false', 'non', 'no'].includes(value.toLowerCase())) return false; - } - return Boolean(value); -}; - const typeOptions = [ - { value: 'text', label: 'Texte', validator: validateString }, - { value: 'textarea', label: 'Zone de texte multi-lignes', validator: validateString }, - { value: 'number', label: 'Nombre', validator: validateNumber }, - { value: 'date', label: 'Date sans heure', validator: validateDate }, - { value: 'date-with-time', label: 'Date avec heure', validator: validateDate }, - { value: 'yes-no', label: 'Oui/Non', validator: validateYesNo }, - { value: 'enum', label: 'Choix dans une liste', validator: validateEnum }, - { value: 'multi-choice', label: 'Choix multiple dans une liste', validator: validateMultiChoice }, - { value: 'boolean', label: 'Case à cocher', validator: validateBoolean }, + { value: 'text', label: 'Texte' }, + { value: 'textarea', label: 'Zone de texte multi-lignes' }, + { value: 'number', label: 'Nombre' }, + { value: 'date', label: 'Date sans heure' }, + { value: 'date-with-time', label: 'Date avec heure' }, + { value: 'yes-no', label: 'Oui/Non' }, + { value: 'enum', label: 'Choix dans une liste' }, + { value: 'multi-choice', label: 'Choix multiple dans une liste' }, + { value: 'boolean', label: 'Case à cocher' }, ]; // Download a file in browser. @@ -99,15 +31,4 @@ function download(file, fileName) { } } -export { - download, - typeOptions, - isNullOrUndefined, - validateString, - validateNumber, - validateDate, - validateYesNo, - validateEnum, - validateMultiChoice, - validateBoolean, -}; +export { download, typeOptions, isNullOrUndefined };