Skip to content

Commit

Permalink
fix(dashboard): sanitize imports (#530)
Browse files Browse the repository at this point in the history
  • Loading branch information
rap2hpoutre authored Mar 25, 2022
1 parent 3b2c47d commit a4fed0e
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 104 deletions.
25 changes: 10 additions & 15 deletions dashboard/src/scenes/data-import-export/ImportData.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
72 changes: 72 additions & 0 deletions dashboard/src/scenes/data-import-export/importSanitizer.js
Original file line number Diff line number Diff line change
@@ -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;
}
99 changes: 10 additions & 89 deletions dashboard/src/utils.js
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -99,15 +31,4 @@ function download(file, fileName) {
}
}

export {
download,
typeOptions,
isNullOrUndefined,
validateString,
validateNumber,
validateDate,
validateYesNo,
validateEnum,
validateMultiChoice,
validateBoolean,
};
export { download, typeOptions, isNullOrUndefined };

0 comments on commit a4fed0e

Please sign in to comment.