From 098e1b7fff05dfc934020707899c17a19fe93764 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 20 Nov 2024 09:46:09 -0600 Subject: [PATCH] Add a custom parseFormData helper to simulate Mastodon's (Rails) formdata parser --- src/utils/api.ts | 8 +++++-- src/utils/formdata.test.ts | 30 ++++++++++++++++++++++++++ src/utils/formdata.ts | 43 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/utils/formdata.test.ts create mode 100644 src/utils/formdata.ts diff --git a/src/utils/api.ts b/src/utils/api.ts index e7766979..829a2cce 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -2,7 +2,6 @@ import { type Context } from '@hono/hono'; import { HTTPException } from '@hono/hono/http-exception'; import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; -import { parseFormData } from 'formdata-helper'; import { EventTemplate } from 'nostr-tools'; import * as TypeFest from 'type-fest'; @@ -13,6 +12,7 @@ import { RelayError } from '@/RelayError.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts'; import { Storages } from '@/storages.ts'; import { nostrNow } from '@/utils.ts'; +import { parseFormData } from '@/utils/formdata.ts'; import { purifyEvent } from '@/utils/purify.ts'; const debug = Debug('ditto:api'); @@ -182,7 +182,11 @@ async function parseBody(req: Request): Promise { switch (req.headers.get('content-type')?.split(';')[0]) { case 'multipart/form-data': case 'application/x-www-form-urlencoded': - return parseFormData(await req.formData()); + try { + return parseFormData(await req.formData()); + } catch { + throw new HTTPException(400, { message: 'Invalid form data' }); + } case 'application/json': return req.json(); } diff --git a/src/utils/formdata.test.ts b/src/utils/formdata.test.ts new file mode 100644 index 00000000..3eaf02aa --- /dev/null +++ b/src/utils/formdata.test.ts @@ -0,0 +1,30 @@ +import { assertEquals, assertThrows } from '@std/assert'; + +import { parseFormData } from '@/utils/formdata.ts'; + +Deno.test('parseFormData', () => { + const formData = new FormData(); + + formData.append('foo', 'bar'); + formData.append('fields_attributes[0][name]', 'baz'); + formData.append('fields_attributes[0][value]', 'qux'); + formData.append('fields_attributes[1][name]', 'quux'); + formData.append('fields_attributes[1][value]', 'corge'); + + const result = parseFormData(formData); + + assertEquals(result, { + foo: 'bar', + fields_attributes: [ + { name: 'baz', value: 'qux' }, + { name: 'quux', value: 'corge' }, + ], + }); + + assertThrows(() => { + const formData = new FormData(); + formData.append('fields_attributes[1]', 'unexpected'); + formData.append('fields_attributes[1][extra]', 'extra_value'); + parseFormData(formData); + }); +}); diff --git a/src/utils/formdata.ts b/src/utils/formdata.ts new file mode 100644 index 00000000..6d5d997b --- /dev/null +++ b/src/utils/formdata.ts @@ -0,0 +1,43 @@ +import { parseFormData as _parseFormData } from 'formdata-helper'; + +/** Parse formData into JSON, simulating the way Mastodon does it. */ +export function parseFormData(formData: FormData): unknown { + const json = _parseFormData(formData); + + const parsed: Record = {}; + + for (const [key, value] of Object.entries(json)) { + deepSet(parsed, key, value); + } + + return parsed; +} + +/** Deeply sets a value in an object based on a Rails-style nested key. */ +function deepSet( + /** The target object to modify. */ + target: Record, + /** The Rails-style key (e.g., "fields_attributes[0][name]"). */ + key: string, + /** The value to set. */ + value: any, +): void { + const keys = key.match(/[^[\]]+/g); // Extract keys like ["fields_attributes", "0", "name"] + if (!keys) return; + + let current = target; + + keys.forEach((k, index) => { + const isLast = index === keys.length - 1; + + if (isLast) { + current[k] = value; // Set the value at the final key + } else { + if (!current[k]) { + // Determine if the next key is numeric, then create an array; otherwise, an object + current[k] = /^\d+$/.test(keys[index + 1]) ? [] : {}; + } + current = current[k]; + } + }); +}