diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index b8635f2e1d..7ff1645335 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -675,14 +675,18 @@ function parseHeaderParameters(parameters: string[]): Record { return parameters.reduce( (acc, param) => { const [key, value] = param.split('='); - acc[key.toLowerCase().trim()] = decodeURIComponent(value); + let decodedValue = decodeURIComponent(value).trim(); + if (decodedValue.startsWith('"') && decodedValue.endsWith('"')) { + decodedValue = decodedValue.slice(1, -1); + } + acc[key.toLowerCase().trim()] = decodedValue; return acc; }, {} as Record, ); } -function parseContentType(contentType?: string): IContentType | null { +export function parseContentType(contentType?: string): IContentType | null { if (!contentType) { return null; } @@ -695,22 +699,7 @@ function parseContentType(contentType?: string): IContentType | null { }; } -function parseFileName(filename?: string): string | undefined { - if (filename?.startsWith('"') && filename?.endsWith('"')) { - return filename.slice(1, -1); - } - - return filename; -} - -// https://datatracker.ietf.org/doc/html/rfc5987 -function parseFileNameStar(filename?: string): string | undefined { - const [_encoding, _locale, content] = parseFileName(filename)?.split("'") ?? []; - - return content; -} - -function parseContentDisposition(contentDisposition?: string): IContentDisposition | null { +export function parseContentDisposition(contentDisposition?: string): IContentDisposition | null { if (!contentDisposition) { return null; } @@ -725,11 +714,15 @@ function parseContentDisposition(contentDisposition?: string): IContentDispositi const parsedParameters = parseHeaderParameters(parameters); - return { - type, - filename: - parseFileNameStar(parsedParameters['filename*']) ?? parseFileName(parsedParameters.filename), - }; + let { filename } = parsedParameters; + const wildcard = parsedParameters['filename*']; + if (wildcard) { + // https://datatracker.ietf.org/doc/html/rfc5987 + const [_encoding, _locale, content] = wildcard?.split("'") ?? []; + filename = content; + } + + return { type, filename }; } export function parseIncomingMessage(message: IncomingMessage) { diff --git a/packages/core/test/NodeExecuteFunctions.test.ts b/packages/core/test/NodeExecuteFunctions.test.ts index bf985fe729..f754abec58 100644 --- a/packages/core/test/NodeExecuteFunctions.test.ts +++ b/packages/core/test/NodeExecuteFunctions.test.ts @@ -27,6 +27,8 @@ import { copyInputItems, getBinaryDataBuffer, isFilePathBlocked, + parseContentDisposition, + parseContentType, parseIncomingMessage, parseRequestObject, proxyRequestToAxios, @@ -150,6 +152,152 @@ describe('NodeExecuteFunctions', () => { }); }); + describe('parseContentType', () => { + const testCases = [ + { + input: 'text/plain', + expected: { + type: 'text/plain', + parameters: { + charset: 'utf-8', + }, + }, + description: 'should parse basic content type', + }, + { + input: 'TEXT/PLAIN', + expected: { + type: 'text/plain', + parameters: { + charset: 'utf-8', + }, + }, + description: 'should convert type to lowercase', + }, + { + input: 'text/html; charset=iso-8859-1', + expected: { + type: 'text/html', + parameters: { + charset: 'iso-8859-1', + }, + }, + description: 'should parse content type with charset', + }, + { + input: 'application/json; charset=utf-8; boundary=---123', + expected: { + type: 'application/json', + parameters: { + charset: 'utf-8', + boundary: '---123', + }, + }, + description: 'should parse content type with multiple parameters', + }, + { + input: 'text/plain; charset="utf-8"; filename="test.txt"', + expected: { + type: 'text/plain', + parameters: { + charset: 'utf-8', + filename: 'test.txt', + }, + }, + description: 'should handle quoted parameter values', + }, + { + input: 'text/plain; filename=%22test%20file.txt%22', + expected: { + type: 'text/plain', + parameters: { + charset: 'utf-8', + filename: 'test file.txt', + }, + }, + description: 'should handle encoded parameter values', + }, + { + input: undefined, + expected: null, + description: 'should return null for undefined input', + }, + { + input: '', + expected: null, + description: 'should return null for empty string', + }, + ]; + + test.each(testCases)('$description', ({ input, expected }) => { + expect(parseContentType(input)).toEqual(expected); + }); + }); + + describe('parseContentDisposition', () => { + const testCases = [ + { + input: 'attachment; filename="file.txt"', + expected: { type: 'attachment', filename: 'file.txt' }, + description: 'should parse basic content disposition', + }, + { + input: 'attachment; filename=file.txt', + expected: { type: 'attachment', filename: 'file.txt' }, + description: 'should parse filename without quotes', + }, + { + input: 'inline; filename="image.jpg"', + expected: { type: 'inline', filename: 'image.jpg' }, + description: 'should parse inline disposition', + }, + { + input: 'attachment; filename="my file.pdf"', + expected: { type: 'attachment', filename: 'my file.pdf' }, + description: 'should parse filename with spaces', + }, + { + input: "attachment; filename*=UTF-8''my%20file.txt", + expected: { type: 'attachment', filename: 'my file.txt' }, + description: 'should parse filename* parameter (RFC 5987)', + }, + { + input: 'filename="test.txt"', + expected: { type: 'attachment', filename: 'test.txt' }, + description: 'should handle invalid syntax but with filename', + }, + { + input: 'filename=test.txt', + expected: { type: 'attachment', filename: 'test.txt' }, + description: 'should handle invalid syntax with only filename parameter', + }, + { + input: undefined, + expected: null, + description: 'should return null for undefined input', + }, + { + input: '', + expected: null, + description: 'should return null for empty string', + }, + { + input: 'attachment; filename="%F0%9F%98%80.txt"', + expected: { type: 'attachment', filename: '😀.txt' }, + description: 'should handle encoded filenames', + }, + { + input: 'attachment; size=123; filename="test.txt"; creation-date="Thu, 1 Jan 2020"', + expected: { type: 'attachment', filename: 'test.txt' }, + description: 'should handle multiple parameters', + }, + ]; + + test.each(testCases)('$description', ({ input, expected }) => { + expect(parseContentDisposition(input)).toEqual(expected); + }); + }); + describe('parseIncomingMessage', () => { it('parses valid content-type header', () => { const message = mock({ @@ -170,6 +318,20 @@ describe('NodeExecuteFunctions', () => { parseIncomingMessage(message); expect(message.contentType).toEqual('application/json'); + expect(message.encoding).toEqual('utf-8'); + }); + + it('parses valid content-type header with encoding wrapped in quotes', () => { + const message = mock({ + headers: { + 'content-type': 'application/json; charset="utf-8"', + 'content-disposition': undefined, + }, + }); + parseIncomingMessage(message); + + expect(message.contentType).toEqual('application/json'); + expect(message.encoding).toEqual('utf-8'); }); it('parses valid content-disposition header with filename*', () => {