File
Blob: src/workerd/api/form-data.h
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #pragma once |
| 6 | |
| 7 | #include "blob.h" |
| 8 | #include <kj/vector.h> |
| 9 | #include <workerd/jsg/jsg.h> |
| 10 | #include <workerd/io/compatibility-date.capnp.h> |
| 11 | |
| 12 | namespace workerd::api { |
| 13 | |
| 14 | // Implements the FormData interface as prescribed by: |
| 15 | // https://xhr.spec.whatwg.org/#interface-formdata |
| 16 | class FormData: public jsg::Object { |
| 17 | private: |
| 18 | using EntryType = kj::OneOf<jsg::Ref<File>, kj::String>; |
| 19 | using EntryIteratorType = kj::Array<EntryType>; |
| 20 | using KeyIteratorType = kj::String; |
| 21 | using ValueIteratorType = EntryType; |
| 22 | |
| 23 | struct IteratorState final { |
| 24 | jsg::Ref<FormData> parent; |
| 25 | uint index = 0; |
| 26 | |
| 27 | void visitForGc(jsg::GcVisitor& visitor) { |
| 28 | visitor.visit(parent); |
| 29 | } |
| 30 | |
| 31 | JSG_MEMORY_INFO(IteratorState) { |
| 32 | tracker.trackField("parent", parent); |
| 33 | } |
| 34 | }; |
| 35 | |
| 36 | public: |
| 37 | |
| 38 | using ParseCallback = kj::FunctionParam<void(kj::StringPtr name, |
| 39 | kj::Maybe<kj::StringPtr> filename, |
| 40 | kj::Maybe<kj::StringPtr> type, |
| 41 | kj::ArrayPtr<const kj::byte> data)>; |
| 42 | |
| 43 | static void parseFormDataImpl(kj::ArrayPtr<const char> rawText, |
| 44 | kj::StringPtr boundary, |
| 45 | ParseCallback callback); |
| 46 | struct EntryWithoutLock { |
| 47 | kj::String name; |
| 48 | kj::Maybe<kj::String> filename; |
| 49 | kj::Maybe<kj::String> type; |
| 50 | kj::OneOf<kj::Array<kj::byte>, kj::String> value; |
| 51 | }; |
| 52 | |
| 53 | // Parse `rawText`, storing the results in this FormData object. `contentType` must be either |
| 54 | // multipart/form-data or application/x-www-form-urlencoded. |
| 55 | // |
| 56 | // `convertFilesToStrings` is for backwards-compatibility. The first implementation of this |
| 57 | // class in Workers incorrectly represented files as strings (of their content). Changing this |
| 58 | // could break deployed code, so this has to be controlled by a compatibility flag. |
| 59 | // |
| 60 | // Parsing may or may not pass a jsg::Lock. If a lock is passed, any File objects created will |
| 61 | // track their internal allocated memory in the associated isolate. If a lock is not passed, |
| 62 | // the internal allocated memory will not be tracked. |
| 63 | void parse(jsg::Lock& js, |
| 64 | kj::ArrayPtr<const char> rawText, |
| 65 | kj::StringPtr contentType, |
| 66 | bool convertFilesToStrings); |
| 67 | |
| 68 | // Given a delimiter string `boundary`, serialize all fields in this form data to an array of |
| 69 | // bytes suitable for use as an HTTP message body. |
| 70 | kj::Array<kj::byte> serialize(kj::ArrayPtr<const char> boundary); |
| 71 | |
| 72 | struct Entry { |
| 73 | kj::String name; |
| 74 | kj::OneOf<jsg::Ref<File>, kj::String> value; |
| 75 | |
| 76 | JSG_MEMORY_INFO(Entry) { |
| 77 | tracker.trackField("name", name); |
| 78 | KJ_SWITCH_ONEOF(value) { |
| 79 | KJ_CASE_ONEOF(file, jsg::Ref<File>) { |
| 80 | tracker.trackField("value", file); |
| 81 | } |
| 82 | KJ_CASE_ONEOF(str, kj::String) { |
| 83 | tracker.trackField("value", str); |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | }; |
| 88 | |
| 89 | kj::ArrayPtr<const Entry> getData() { return data; } |
| 90 | |
| 91 | // JS API |
| 92 | |
| 93 | // The spec allows a FormData to be constructed from a <form> HTML element. We don't support that, |
| 94 | // for obvious reasons, so this constructor doesn't take any parameters. If someone tries to use |
| 95 | // FormData to represent a <form> element we probably don't have to worry about making the error |
| 96 | // message they receive too pretty: they won't get farther than `document.getElementById()`. |
| 97 | static jsg::Ref<FormData> constructor(jsg::Lock& js); |
| 98 | |
| 99 | void append(jsg::Lock& js, kj::String name, |
| 100 | kj::OneOf<jsg::Ref<File>, jsg::Ref<Blob>, kj::String> value, |
| 101 | jsg::Optional<kj::String> filename); |
| 102 | |
| 103 | void delete_(kj::String name); |
| 104 | |
| 105 | kj::Maybe<kj::OneOf<jsg::Ref<File>, kj::String>> get(jsg::Lock& js, kj::String name); |
| 106 | |
| 107 | kj::Array<kj::OneOf<jsg::Ref<File>, kj::String>> getAll(jsg::Lock& js, kj::String name); |
| 108 | |
| 109 | bool has(kj::String name); |
| 110 | |
| 111 | void set(jsg::Lock& js, kj::String name, |
| 112 | kj::OneOf<jsg::Ref<File>, jsg::Ref<Blob>, kj::String> value, |
| 113 | jsg::Optional<kj::String> filename); |
| 114 | |
| 115 | JSG_ITERATOR(EntryIterator, entries, |
| 116 | EntryIteratorType, |
| 117 | IteratorState, |
| 118 | iteratorNext<EntryIteratorType>); |
| 119 | JSG_ITERATOR(KeyIterator, keys, |
| 120 | KeyIteratorType, |
| 121 | IteratorState, |
| 122 | iteratorNext<KeyIteratorType>); |
| 123 | JSG_ITERATOR(ValueIterator, |
| 124 | values, |
| 125 | ValueIteratorType, |
| 126 | IteratorState, |
| 127 | iteratorNext<ValueIteratorType>); |
| 128 | |
| 129 | void forEach( |
| 130 | jsg::Lock& js, |
| 131 | jsg::Function<void(EntryType, kj::StringPtr, jsg::Ref<FormData>)> callback, |
| 132 | jsg::Optional<jsg::Value> thisArg); |
| 133 | |
| 134 | JSG_RESOURCE_TYPE(FormData, CompatibilityFlags::Reader flags) { |
| 135 | JSG_METHOD(append); |
| 136 | JSG_METHOD_NAMED(delete, delete_); |
| 137 | JSG_METHOD(get); |
| 138 | JSG_METHOD(getAll); |
| 139 | JSG_METHOD(has); |
| 140 | JSG_METHOD(set); |
| 141 | JSG_METHOD(entries); |
| 142 | JSG_METHOD(keys); |
| 143 | JSG_METHOD(values); |
| 144 | |
| 145 | JSG_METHOD(forEach); |
| 146 | JSG_ITERABLE(entries); |
| 147 | |
| 148 | if (flags.getFormDataParserSupportsFiles()) { |
| 149 | JSG_TS_OVERRIDE({ |
| 150 | append(name: string, value: string | Blob): void; |
| 151 | append(name: string, value: string): void; |
| 152 | append(name: string, value: Blob, filename?: string): void; |
| 153 | |
| 154 | set(name: string, value: string | Blob): void; |
| 155 | set(name: string, value: string): void; |
| 156 | set(name: string, value: Blob, filename?: string): void; |
| 157 | |
| 158 | entries(): IterableIterator<[key: string, value: File | string]>; |
| 159 | [Symbol.iterator](): IterableIterator<[key: string, value: File | string]>; |
| 160 | |
| 161 | forEach<This = unknown>(callback: (this: This, value: File | string, key: string, parent: FormData) => void, thisArg?: This): void; |
| 162 | }); |
| 163 | } else { |
| 164 | JSG_TS_OVERRIDE({ |
| 165 | get(name: string): string | null; |
| 166 | getAll(name: string): string[]; |
| 167 | |
| 168 | append(name: string, value: string | Blob): void; |
| 169 | append(name: string, value: string): void; |
| 170 | append(name: string, value: Blob, filename?: string): void; |
| 171 | |
| 172 | set(name: string, value: string | Blob): void; |
| 173 | set(name: string, value: string): void; |
| 174 | set(name: string, value: Blob, filename?: string): void; |
| 175 | |
| 176 | entries(): IterableIterator<[key: string, value: string]>; |
| 177 | [Symbol.iterator](): IterableIterator<[key: string, value: string]>; |
| 178 | |
| 179 | forEach<This = unknown>(callback: (this: This, value: string, key: string, parent: FormData) => void, thisArg?: This): void; |
| 180 | }); |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { |
| 185 | tracker.trackField("data", data.asPtr()); |
| 186 | } |
| 187 | |
| 188 | private: |
| 189 | kj::Vector<Entry> data; |
| 190 | |
| 191 | static EntryType clone(jsg::Lock& js, EntryType& value); |
| 192 | |
| 193 | template <typename Type> |
| 194 | static kj::Maybe<Type> iteratorNext(jsg::Lock& js, IteratorState& state) { |
| 195 | if (state.index >= state.parent->data.size()) { |
| 196 | return kj::none; |
| 197 | } |
| 198 | auto& [key, value] = state.parent->data[state.index++]; |
| 199 | if constexpr (kj::isSameType<Type, EntryIteratorType>()) { |
| 200 | return kj::arr<EntryType>(kj::str(key), clone(js, value)); |
| 201 | } else if constexpr (kj::isSameType<Type, KeyIteratorType>()) { |
| 202 | return kj::str(key); |
| 203 | } else if constexpr (kj::isSameType<Type, ValueIteratorType>()) { |
| 204 | return clone(js, value); |
| 205 | } else { |
| 206 | KJ_UNREACHABLE; |
| 207 | } |
| 208 | } |
| 209 | }; |
| 210 | |
| 211 | #define EW_FORMDATA_ISOLATE_TYPES \ |
| 212 | api::FormData, \ |
| 213 | api::FormData::EntryIterator, \ |
| 214 | api::FormData::EntryIterator::Next, \ |
| 215 | api::FormData::KeyIterator, \ |
| 216 | api::FormData::KeyIterator::Next, \ |
| 217 | api::FormData::ValueIterator, \ |
| 218 | api::FormData::ValueIterator::Next |
| 219 | |
| 220 | } // namespace workerd::api |