mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
refactor: improve error handling and validation for PDF field metadata parsing
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { type TFieldAndMeta, ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { logger } from '@documenso/lib/utils/logger';
|
||||
import { PDF, rgb } from '@libpdf/core';
|
||||
import type { FieldType, Recipient } from '@prisma/client';
|
||||
|
||||
@@ -114,14 +116,49 @@ export const extractPlaceholdersFromPDF = async (pdf: Buffer): Promise<Placehold
|
||||
|
||||
const recipient = recipientOrMeta;
|
||||
|
||||
const rawFieldMeta = parseRawFieldMetaFromPlaceholder(fieldMetaData);
|
||||
/*
|
||||
Parse and validate the field metadata. A malformed selection placeholder
|
||||
(e.g. an unknown validation rule or a default value that doesn't match an
|
||||
option) is skipped like an invalid field type rather than aborting the whole
|
||||
upload, which may contain other valid placeholders and files.
|
||||
*/
|
||||
let fieldAndMeta: TFieldAndMeta;
|
||||
|
||||
const parsedFieldMeta = parseFieldMetaFromPlaceholder(rawFieldMeta, fieldType);
|
||||
try {
|
||||
const rawFieldMeta = parseRawFieldMetaFromPlaceholder(fieldMetaData);
|
||||
const parsedFieldMeta = parseFieldMetaFromPlaceholder(rawFieldMeta, fieldType);
|
||||
|
||||
const fieldAndMeta: TFieldAndMeta = ZEnvelopeFieldAndMetaSchema.parse({
|
||||
type: fieldType,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
});
|
||||
const parsedFieldAndMeta = ZEnvelopeFieldAndMetaSchema.safeParse({
|
||||
type: fieldType,
|
||||
fieldMeta: parsedFieldMeta,
|
||||
});
|
||||
|
||||
/*
|
||||
Surface schema failures as INVALID_BODY (400) instead of letting the raw
|
||||
ZodError bubble up to the caller as an INTERNAL_SERVER_ERROR (500).
|
||||
*/
|
||||
if (!parsedFieldAndMeta.success) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: `Invalid field metadata for placeholder "${placeholder}": ${parsedFieldAndMeta.error.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
fieldAndMeta = parsedFieldAndMeta.data;
|
||||
} catch (error) {
|
||||
const appError = AppError.parseError(error);
|
||||
|
||||
logger.warn(
|
||||
{
|
||||
placeholder,
|
||||
page: page.index + 1,
|
||||
code: appError.code,
|
||||
message: appError.message,
|
||||
},
|
||||
'Skipping placeholder with invalid field metadata',
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
LibPDF returns bbox in points with bottom-left origin.
|
||||
|
||||
@@ -198,12 +198,6 @@ const getDefaultFieldMetaValue = (rawFieldMeta: Record<string, string>) => {
|
||||
return defaultValue ? normalizePlaceholderSelectionValue(defaultValue) : undefined;
|
||||
};
|
||||
|
||||
const getCheckedFieldMetaValues = (rawFieldMeta: Record<string, string>) => {
|
||||
const checkedValue = rawFieldMeta.checked;
|
||||
|
||||
return checkedValue ? parsePlaceholderOptions(checkedValue) : [];
|
||||
};
|
||||
|
||||
const parseCheckboxValidationRule = (value: string): string => {
|
||||
const validationRule = CHECKBOX_VALIDATION_RULE_BY_ALIAS[value];
|
||||
|
||||
@@ -268,7 +262,7 @@ const applyRadioFieldOptions = (parsedFieldMeta: Record<string, unknown>, rawFie
|
||||
|
||||
const applyCheckboxFieldOptions = (parsedFieldMeta: Record<string, unknown>, rawFieldMeta: Record<string, string>) => {
|
||||
const options = parseSelectionFieldOptions(rawFieldMeta, FieldType.CHECKBOX);
|
||||
const checkedValues = getCheckedFieldMetaValues(rawFieldMeta);
|
||||
const checkedValues = rawFieldMeta.checked ? parsePlaceholderOptions(rawFieldMeta.checked) : [];
|
||||
|
||||
if (!options && checkedValues.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
@@ -328,21 +322,37 @@ const applyDropdownFieldOptions = (parsedFieldMeta: Record<string, unknown>, raw
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
Generic field metadata properties are simple properties consisting of a key and a value.
|
||||
E.g. 'required=true', 'fontSize=12', 'textAlign=left'
|
||||
They don't require special handling.
|
||||
|
||||
Special field metadata properties are complex properties consisting of a key and a value with multiple parts.
|
||||
E.g. 'options=Card/Check|Bank Transfer', 'checked=Card|Check', 'selected=Bank Transfer'
|
||||
They require special handling.
|
||||
*/
|
||||
const shouldSkipGenericFieldMetaParsing = (property: string, fieldType: FieldType): boolean => {
|
||||
if (property === 'options' || property === 'default' || property === 'selected') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fieldType !== FieldType.CHECKBOX && fieldType !== FieldType.RADIO && fieldType !== FieldType.DROPDOWN) {
|
||||
const isSelectionField =
|
||||
fieldType === FieldType.CHECKBOX || fieldType === FieldType.RADIO || fieldType === FieldType.DROPDOWN;
|
||||
|
||||
if (!isSelectionField) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
if (
|
||||
property === 'label' ||
|
||||
property === 'placeholder' ||
|
||||
property === 'defaultValue' ||
|
||||
(property === 'checked' && fieldType === FieldType.CHECKBOX)
|
||||
);
|
||||
(fieldType === FieldType.CHECKBOX && property === 'checked')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -404,17 +414,11 @@ export const parseFieldMetaFromPlaceholder = (
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.RADIO) {
|
||||
applyRadioFieldOptions(parsedFieldMeta, rawFieldMeta);
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.CHECKBOX) {
|
||||
applyCheckboxFieldOptions(parsedFieldMeta, rawFieldMeta);
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.DROPDOWN) {
|
||||
applyDropdownFieldOptions(parsedFieldMeta, rawFieldMeta);
|
||||
}
|
||||
match(fieldType)
|
||||
.with(FieldType.RADIO, () => applyRadioFieldOptions(parsedFieldMeta, rawFieldMeta))
|
||||
.with(FieldType.CHECKBOX, () => applyCheckboxFieldOptions(parsedFieldMeta, rawFieldMeta))
|
||||
.with(FieldType.DROPDOWN, () => applyDropdownFieldOptions(parsedFieldMeta, rawFieldMeta))
|
||||
.otherwise(() => undefined);
|
||||
|
||||
return parsedFieldMeta;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user