From a5fcdf03bae681c5b2e0de6b681d20efe8ebdd7f Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Tue, 11 May 2021 21:33:07 -0400 Subject: [PATCH] feat: add mimeTypes validation for uploads --- src/collections/config/sanitize.ts | 5 ++ src/errors/ValidationError.ts | 2 +- .../baseFields/mimeTypeValidator.spec.ts | 55 +++++++++++++++++++ src/fields/baseFields/mimeTypeValidator.ts | 9 +++ src/fields/config/types.ts | 2 +- 5 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/fields/baseFields/mimeTypeValidator.spec.ts create mode 100644 src/fields/baseFields/mimeTypeValidator.ts diff --git a/src/collections/config/sanitize.ts b/src/collections/config/sanitize.ts index 340fad1e73..fe7671c8ec 100644 --- a/src/collections/config/sanitize.ts +++ b/src/collections/config/sanitize.ts @@ -10,6 +10,7 @@ import baseUploadFields from '../../fields/baseFields/baseUploadFields'; import baseImageUploadFields from '../../fields/baseFields/baseImageUploadFields'; import { formatLabels } from '../../utilities/formatLabels'; import { defaults, authDefaults } from './defaults'; +import { mimeTypeValidator } from '../../fields/baseFields/mimeTypeValidator'; const mergeBaseFields = (fields, baseFields) => { const mergedFields = []; @@ -73,6 +74,10 @@ const sanitizeCollection = (collections: PayloadCollectionConfig[], collection: let uploadFields = baseUploadFields; + if (sanitized.upload.mimeTypes) { + uploadFields.find((f) => f.name === 'mimeType').validate = mimeTypeValidator(sanitized.upload.mimeTypes); + } + if (sanitized.upload.imageSizes && Array.isArray(sanitized.upload.imageSizes)) { uploadFields = uploadFields.concat(baseImageUploadFields(sanitized.upload.imageSizes)); } diff --git a/src/errors/ValidationError.ts b/src/errors/ValidationError.ts index 0b24af4b41..fa111eff76 100644 --- a/src/errors/ValidationError.ts +++ b/src/errors/ValidationError.ts @@ -3,7 +3,7 @@ import APIError from './APIError'; class ValidationError extends APIError { constructor(results: {message: string, field: string}[]) { - super(`Bad request with ${results.length} errors`, httpStatus.BAD_REQUEST, results); + super(`The following field${results.length === 1 ? ' is' : 's are'} invalid: ${results.map((f) => f.field).join(', ')}`, httpStatus.BAD_REQUEST, results); } } diff --git a/src/fields/baseFields/mimeTypeValidator.spec.ts b/src/fields/baseFields/mimeTypeValidator.spec.ts new file mode 100644 index 0000000000..b07677a97d --- /dev/null +++ b/src/fields/baseFields/mimeTypeValidator.spec.ts @@ -0,0 +1,55 @@ +import { mimeTypeValidator } from './mimeTypeValidator'; + +describe('mimeTypeValidator', () => { + it('should validate single mimeType', () => { + const mimeTypes = ['image/png']; + const validate = mimeTypeValidator(mimeTypes); + expect(validate('image/png')).toBe(true); + }); + + it('should validate multiple mimeTypes', () => { + const mimeTypes = ['image/png', 'application/pdf']; + const validate = mimeTypeValidator(mimeTypes); + expect(validate('image/png')).toBe(true); + expect(validate('application/pdf')).toBe(true); + }); + + it('should validate using wildcard', () => { + const mimeTypes = ['image/*']; + const validate = mimeTypeValidator(mimeTypes); + expect(validate('image/png')).toBe(true); + expect(validate('image/gif')).toBe(true); + }); + + it('should validate multiple wildcards', () => { + const mimeTypes = ['image/*', 'audio/*']; + const validate = mimeTypeValidator(mimeTypes); + expect(validate('image/png')).toBe(true); + expect(validate('audio/mpeg')).toBe(true); + }); + + it('should not validate when unmatched', () => { + const mimeTypes = ['image/png']; + const validate = mimeTypeValidator(mimeTypes); + expect(validate('audio/mpeg')).toBe('Invalid file type: \'audio/mpeg\''); + }); + + it('should not validate when unmatched - multiple mimeTypes', () => { + const mimeTypes = ['image/png', 'application/pdf']; + const validate = mimeTypeValidator(mimeTypes); + expect(validate('audio/mpeg')).toBe('Invalid file type: \'audio/mpeg\''); + }); + + it('should not validate using wildcard - unmatched', () => { + const mimeTypes = ['image/*']; + const validate = mimeTypeValidator(mimeTypes); + expect(validate('audio/mpeg')).toBe('Invalid file type: \'audio/mpeg\''); + }); + + it('should not validate multiple wildcards - unmatched', () => { + const mimeTypes = ['image/*', 'audio/*']; + const validate = mimeTypeValidator(mimeTypes); + expect(validate('video/mp4')).toBe('Invalid file type: \'video/mp4\''); + expect(validate('application/pdf')).toBe('Invalid file type: \'application/pdf\''); + }); +}); diff --git a/src/fields/baseFields/mimeTypeValidator.ts b/src/fields/baseFields/mimeTypeValidator.ts new file mode 100644 index 0000000000..7135458d4f --- /dev/null +++ b/src/fields/baseFields/mimeTypeValidator.ts @@ -0,0 +1,9 @@ +import { Validate } from '../config/types'; + +export const mimeTypeValidator = (mimeTypes: string[]): Validate => (val: string) => { + const cleanedMimeTypes = mimeTypes.map((v) => v.replace('*', '')); + + return !cleanedMimeTypes.some((v) => val.startsWith(v)) + ? `Invalid file type: '${val}'` + : true; +}; diff --git a/src/fields/config/types.ts b/src/fields/config/types.ts index 91133cdc80..1a4aec8b48 100644 --- a/src/fields/config/types.ts +++ b/src/fields/config/types.ts @@ -40,7 +40,7 @@ export type Labels = { plural: string; }; -export type Validate = (value: unknown, options?: any) => string | boolean | Promise; +export type Validate = (value: unknown, options?: any) => string | true | Promise; export type OptionObject = { label: string