* feat: enhance image upload validation across the application
* Patch improvements.
* refactor: streamline avatar upload error handling in updateProfile handler
* refactor: extract image validation logic into a separate module for reuse in BannerUploader and ImageUploader
* fix: reset file input value on validation failure in image uploaders
* fix: update localization key for image file upload error message
* fix: update HTML content validation in image uploader to check for additional byte
* Code Quality improvements
* add : unit tests.
* fix : fixing some more issues.
* fix : some import fixes
* fix : fixing type-check issues
* fix : some more import fixes.
* fix : removing Duplicate max-file-size constant
* refactor: enhance base64 validation with regex pattern
* refactor: centralize MAX_IMAGE_FILE_SIZE and fix SVG checks.
* fix : some toast fixes.
* fixing the size of the banner.
* fix: update accepted image formats in BannerUploader and ImageUploader components
* fix
* coderabbit suggestions addressed.
* addressed coderrabit comment
* refactor: implement generic i18n error messages with interpolation
- Add generic 'unsupported_file_type' translation key with {{type}} interpolation
- Replace hardcoded file type error messages with reusable i18n pattern
- Update imageValidation interfaces to support errorKey and errorParams
- Refactor server-side and client-side validation to use new pattern
- Remove individual translation keys for each file type (PDF, HTML, Script, ZIP, Executable)
- Add new translation keys for other validation errors (SVG, base64, empty data, etc.)
- Type-safe error handling with proper interpolation support
Benefits:
- Single reusable translation pattern reduces duplication
- Easier to maintain and localize
- Consistent error messaging across file types
* feat: add MAX_BANNER_SIZE constant and update file size validation
- Add MAX_BANNER_SIZE constant (5MB) to shared constants file
- Update BannerUploader to use shared constant instead of inline value
- Update ImageUploader to handle new errorKey/errorParams pattern
- Maintain backward compatibility with existing error handling
Note: Pre-existing linting warnings in constants.ts and BannerUploader.tsx
are unrelated to these changes and were present before this refactor.
* adressed volnei's suggestions.
* test: update image validation tests for new errorKey/errorParams pattern
- Update all dangerous file type tests to expect errorKey and errorParams instead of hardcoded error messages
- Add comprehensive tests for new error format validation pattern
- Add tests for backward compatibility with error field
- Add tests for MAX_BANNER_SIZE constant integration
- Verify that unsupported file types now return generic 'unsupported_file_type' key with type interpolation
- Ensure all existing functionality continues to work with enhanced error handling
All 42 tests passing ✅
* test: update server-side image validation tests for new error format
- Update all dangerous file type tests to expect errorKey and errorParams
- Change hardcoded error messages to i18n keys (unsupported_file_type, svg_contains_dangerous_content, etc.)
- Update edge case tests to use new error keys (empty_image_data, invalid_base64_format, unrecognized_image_format)
- Add comprehensive tests for new error format validation pattern
- Add tests for backward compatibility with error field
- Verify that unsupported file types return generic 'unsupported_file_type' key with type interpolation
All 26 server-side tests passing ✅
Complements client-side test updates for complete test coverage
---------
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
132 lines
4.2 KiB
TypeScript
132 lines
4.2 KiB
TypeScript
import { MAX_BANNER_SIZE } from "@calcom/lib/constants";
|
|
import {
|
|
FILE_SIGNATURES,
|
|
matchesSignature,
|
|
containsDangerousSVGContent,
|
|
} from "@calcom/lib/imageValidationConstants";
|
|
|
|
export const MAX_IMAGE_FILE_SIZE = MAX_BANNER_SIZE;
|
|
|
|
const stripBomAndWhitespace = (data: Uint8Array): Uint8Array => {
|
|
let start = 0;
|
|
|
|
if (data.length >= 3 && data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf) {
|
|
start = 3;
|
|
}
|
|
|
|
while (start < data.length) {
|
|
const byte = data[start];
|
|
if (byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d || byte === 0x0c || byte === 0x0b) {
|
|
start++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return data.slice(start);
|
|
};
|
|
|
|
const checkTextBasedDangerousSignature = (
|
|
data: Uint8Array
|
|
): { key: string; params: Record<string, string | number> } | null => {
|
|
const text = new TextDecoder("utf-8", { fatal: false }).decode(data).toLowerCase();
|
|
|
|
if (text.includes("<!doctype") || text.includes("<html") || matchesSignature(data, FILE_SIGNATURES.HTML)) {
|
|
return { key: "unsupported_file_type", params: { type: "HTML" } };
|
|
}
|
|
|
|
if (text.includes("<script") || matchesSignature(data, FILE_SIGNATURES.SCRIPT_TAG)) {
|
|
return { key: "unsupported_file_type", params: { type: "Script" } };
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const createTrimmedTextHead = (text: string, maxLength = 512): Uint8Array => {
|
|
let trimmedText = text.replace(/^[\uFEFF\s]+/, "");
|
|
|
|
if (trimmedText.length > maxLength) {
|
|
trimmedText = trimmedText.substring(0, maxLength);
|
|
}
|
|
|
|
return new TextEncoder().encode(trimmedText);
|
|
};
|
|
|
|
export const validateImageFile = async (
|
|
file: File,
|
|
maxFileSize: number = MAX_IMAGE_FILE_SIZE
|
|
): Promise<{
|
|
isValid: boolean;
|
|
error?: string;
|
|
errorKey?: string;
|
|
errorParams?: Record<string, string | number>;
|
|
}> => {
|
|
if (file.size > maxFileSize) {
|
|
return { isValid: false, error: "image_size_limit_exceed" };
|
|
}
|
|
|
|
try {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
const uint8Array = new Uint8Array(arrayBuffer);
|
|
|
|
if (uint8Array.length === 0) {
|
|
return { isValid: false, error: "invalid_image_file_format" };
|
|
}
|
|
|
|
const strippedData = stripBomAndWhitespace(uint8Array);
|
|
|
|
if (strippedData.length === 0) {
|
|
return { isValid: false, error: "invalid_image_file_format" };
|
|
}
|
|
|
|
const trimmedHead = strippedData.slice(0, 512);
|
|
|
|
const textBasedError = checkTextBasedDangerousSignature(trimmedHead);
|
|
if (textBasedError) {
|
|
return { isValid: false, errorKey: textBasedError.key, errorParams: textBasedError.params };
|
|
}
|
|
|
|
if (matchesSignature(trimmedHead, FILE_SIGNATURES.PDF)) {
|
|
return { isValid: false, errorKey: "unsupported_file_type", errorParams: { type: "PDF" } };
|
|
}
|
|
|
|
if (matchesSignature(trimmedHead, FILE_SIGNATURES.ZIP)) {
|
|
return { isValid: false, errorKey: "unsupported_file_type", errorParams: { type: "ZIP" } };
|
|
}
|
|
|
|
if (matchesSignature(trimmedHead, FILE_SIGNATURES.EXECUTABLE)) {
|
|
return { isValid: false, errorKey: "unsupported_file_type", errorParams: { type: "Executable" } };
|
|
}
|
|
|
|
const isValidImage =
|
|
matchesSignature(trimmedHead, FILE_SIGNATURES.PNG) ||
|
|
matchesSignature(trimmedHead, FILE_SIGNATURES.JPEG_FF_D8_FF) ||
|
|
matchesSignature(trimmedHead, FILE_SIGNATURES.GIF87a) ||
|
|
matchesSignature(trimmedHead, FILE_SIGNATURES.GIF89a) ||
|
|
matchesSignature(trimmedHead, FILE_SIGNATURES.BMP) ||
|
|
matchesSignature(trimmedHead, FILE_SIGNATURES.ICO) ||
|
|
(matchesSignature(trimmedHead, FILE_SIGNATURES.WEBP) &&
|
|
trimmedHead.length >= 12 &&
|
|
matchesSignature(trimmedHead.slice(8), FILE_SIGNATURES.WEBP_SIGNATURE)) ||
|
|
(() => {
|
|
const fullText = new TextDecoder("utf-8", { fatal: false }).decode(strippedData);
|
|
|
|
const svgTrimmedHead = createTrimmedTextHead(fullText);
|
|
|
|
return (
|
|
(matchesSignature(svgTrimmedHead, FILE_SIGNATURES.SVG) ||
|
|
matchesSignature(svgTrimmedHead, FILE_SIGNATURES.SVG_DIRECT)) &&
|
|
!containsDangerousSVGContent(fullText)
|
|
);
|
|
})();
|
|
|
|
if (!isValidImage) {
|
|
return { isValid: false, error: "invalid_image_file_format" };
|
|
}
|
|
|
|
return { isValid: true };
|
|
} catch (error) {
|
|
return { isValid: false, error: "failed_to_validate_image_file" };
|
|
}
|
|
};
|