// Copyright (c) 2017-2022 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #pragma once #include "blob.h" #include #include #include namespace workerd::api { // Implements the FormData interface as prescribed by: // https://xhr.spec.whatwg.org/#interface-formdata class FormData: public jsg::Object { private: using EntryType = kj::OneOf, kj::String>; using EntryIteratorType = kj::Array; using KeyIteratorType = kj::String; using ValueIteratorType = EntryType; struct IteratorState final { jsg::Ref parent; uint index = 0; void visitForGc(jsg::GcVisitor& visitor) { visitor.visit(parent); } JSG_MEMORY_INFO(IteratorState) { tracker.trackField("parent", parent); } }; public: using ParseCallback = kj::FunctionParam filename, kj::Maybe type, kj::ArrayPtr data)>; static void parseFormDataImpl(kj::ArrayPtr rawText, kj::StringPtr boundary, ParseCallback callback); struct EntryWithoutLock { kj::String name; kj::Maybe filename; kj::Maybe type; kj::OneOf, kj::String> value; }; // Parse `rawText`, storing the results in this FormData object. `contentType` must be either // multipart/form-data or application/x-www-form-urlencoded. // // `convertFilesToStrings` is for backwards-compatibility. The first implementation of this // class in Workers incorrectly represented files as strings (of their content). Changing this // could break deployed code, so this has to be controlled by a compatibility flag. // // Parsing may or may not pass a jsg::Lock. If a lock is passed, any File objects created will // track their internal allocated memory in the associated isolate. If a lock is not passed, // the internal allocated memory will not be tracked. void parse(jsg::Lock& js, kj::ArrayPtr rawText, kj::StringPtr contentType, bool convertFilesToStrings); // Given a delimiter string `boundary`, serialize all fields in this form data to an array of // bytes suitable for use as an HTTP message body. kj::Array serialize(kj::ArrayPtr boundary); struct Entry { kj::String name; kj::OneOf, kj::String> value; JSG_MEMORY_INFO(Entry) { tracker.trackField("name", name); KJ_SWITCH_ONEOF(value) { KJ_CASE_ONEOF(file, jsg::Ref) { tracker.trackField("value", file); } KJ_CASE_ONEOF(str, kj::String) { tracker.trackField("value", str); } } } }; kj::ArrayPtr getData() { return data; } // JS API // The spec allows a FormData to be constructed from a
HTML element. We don't support that, // for obvious reasons, so this constructor doesn't take any parameters. If someone tries to use // FormData to represent a element we probably don't have to worry about making the error // message they receive too pretty: they won't get farther than `document.getElementById()`. static jsg::Ref constructor(jsg::Lock& js); void append(jsg::Lock& js, kj::String name, kj::OneOf, jsg::Ref, kj::String> value, jsg::Optional filename); void delete_(kj::String name); kj::Maybe, kj::String>> get(jsg::Lock& js, kj::String name); kj::Array, kj::String>> getAll(jsg::Lock& js, kj::String name); bool has(kj::String name); void set(jsg::Lock& js, kj::String name, kj::OneOf, jsg::Ref, kj::String> value, jsg::Optional filename); JSG_ITERATOR(EntryIterator, entries, EntryIteratorType, IteratorState, iteratorNext); JSG_ITERATOR(KeyIterator, keys, KeyIteratorType, IteratorState, iteratorNext); JSG_ITERATOR(ValueIterator, values, ValueIteratorType, IteratorState, iteratorNext); void forEach( jsg::Lock& js, jsg::Function)> callback, jsg::Optional thisArg); JSG_RESOURCE_TYPE(FormData, CompatibilityFlags::Reader flags) { JSG_METHOD(append); JSG_METHOD_NAMED(delete, delete_); JSG_METHOD(get); JSG_METHOD(getAll); JSG_METHOD(has); JSG_METHOD(set); JSG_METHOD(entries); JSG_METHOD(keys); JSG_METHOD(values); JSG_METHOD(forEach); JSG_ITERABLE(entries); if (flags.getFormDataParserSupportsFiles()) { JSG_TS_OVERRIDE({ append(name: string, value: string | Blob): void; append(name: string, value: string): void; append(name: string, value: Blob, filename?: string): void; set(name: string, value: string | Blob): void; set(name: string, value: string): void; set(name: string, value: Blob, filename?: string): void; entries(): IterableIterator<[key: string, value: File | string]>; [Symbol.iterator](): IterableIterator<[key: string, value: File | string]>; forEach(callback: (this: This, value: File | string, key: string, parent: FormData) => void, thisArg?: This): void; }); } else { JSG_TS_OVERRIDE({ get(name: string): string | null; getAll(name: string): string[]; append(name: string, value: string | Blob): void; append(name: string, value: string): void; append(name: string, value: Blob, filename?: string): void; set(name: string, value: string | Blob): void; set(name: string, value: string): void; set(name: string, value: Blob, filename?: string): void; entries(): IterableIterator<[key: string, value: string]>; [Symbol.iterator](): IterableIterator<[key: string, value: string]>; forEach(callback: (this: This, value: string, key: string, parent: FormData) => void, thisArg?: This): void; }); } } void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { tracker.trackField("data", data.asPtr()); } private: kj::Vector data; static EntryType clone(jsg::Lock& js, EntryType& value); template static kj::Maybe iteratorNext(jsg::Lock& js, IteratorState& state) { if (state.index >= state.parent->data.size()) { return kj::none; } auto& [key, value] = state.parent->data[state.index++]; if constexpr (kj::isSameType()) { return kj::arr(kj::str(key), clone(js, value)); } else if constexpr (kj::isSameType()) { return kj::str(key); } else if constexpr (kj::isSameType()) { return clone(js, value); } else { KJ_UNREACHABLE; } } }; #define EW_FORMDATA_ISOLATE_TYPES \ api::FormData, \ api::FormData::EntryIterator, \ api::FormData::EntryIterator::Next, \ api::FormData::KeyIterator, \ api::FormData::KeyIterator::Next, \ api::FormData::ValueIterator, \ api::FormData::ValueIterator::Next } // namespace workerd::api