Serenity Operating System
1/*
2 * Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/TypeCasts.h>
8#include <LibJS/Runtime/ArrayBuffer.h>
9#include <LibJS/Runtime/Completion.h>
10#include <LibJS/Runtime/Error.h>
11#include <LibJS/Runtime/PromiseCapability.h>
12#include <LibWeb/Bindings/ExceptionOrUtils.h>
13#include <LibWeb/Bindings/HostDefined.h>
14#include <LibWeb/Bindings/MainThreadVM.h>
15#include <LibWeb/Fetch/Body.h>
16#include <LibWeb/Fetch/Infrastructure/HTTP/Bodies.h>
17#include <LibWeb/FileAPI/Blob.h>
18#include <LibWeb/Infra/JSON.h>
19#include <LibWeb/MimeSniff/MimeType.h>
20#include <LibWeb/Streams/ReadableStream.h>
21#include <LibWeb/WebIDL/Promise.h>
22
23namespace Web::Fetch {
24
25BodyMixin::~BodyMixin() = default;
26
27// https://fetch.spec.whatwg.org/#body-unusable
28bool BodyMixin::is_unusable() const
29{
30 // An object including the Body interface mixin is said to be unusable if its body is non-null and its body’s stream is disturbed or locked.
31 auto const& body = body_impl();
32 return body.has_value() && (body->stream()->is_disturbed() || body->stream()->is_locked());
33}
34
35// https://fetch.spec.whatwg.org/#dom-body-body
36JS::GCPtr<Streams::ReadableStream> BodyMixin::body() const
37{
38 // The body getter steps are to return null if this’s body is null; otherwise this’s body’s stream.
39 auto const& body = body_impl();
40 return body.has_value() ? body->stream().ptr() : nullptr;
41}
42
43// https://fetch.spec.whatwg.org/#dom-body-bodyused
44bool BodyMixin::body_used() const
45{
46 // The bodyUsed getter steps are to return true if this’s body is non-null and this’s body’s stream is disturbed; otherwise false.
47 auto const& body = body_impl();
48 return body.has_value() && body->stream()->is_disturbed();
49}
50
51// https://fetch.spec.whatwg.org/#dom-body-arraybuffer
52WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> BodyMixin::array_buffer() const
53{
54 auto& vm = Bindings::main_thread_vm();
55 auto& realm = *vm.current_realm();
56
57 // The arrayBuffer() method steps are to return the result of running consume body with this and ArrayBuffer.
58 return consume_body(realm, *this, PackageDataType::ArrayBuffer);
59}
60
61// https://fetch.spec.whatwg.org/#dom-body-blob
62WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> BodyMixin::blob() const
63{
64 auto& vm = Bindings::main_thread_vm();
65 auto& realm = *vm.current_realm();
66
67 // The blob() method steps are to return the result of running consume body with this and Blob.
68 return consume_body(realm, *this, PackageDataType::Blob);
69}
70
71// https://fetch.spec.whatwg.org/#dom-body-formdata
72WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> BodyMixin::form_data() const
73{
74 auto& vm = Bindings::main_thread_vm();
75 auto& realm = *vm.current_realm();
76
77 // The formData() method steps are to return the result of running consume body with this and FormData.
78 return consume_body(realm, *this, PackageDataType::FormData);
79}
80
81// https://fetch.spec.whatwg.org/#dom-body-json
82WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> BodyMixin::json() const
83{
84 auto& vm = Bindings::main_thread_vm();
85 auto& realm = *vm.current_realm();
86
87 // The json() method steps are to return the result of running consume body with this and JSON.
88 return consume_body(realm, *this, PackageDataType::JSON);
89}
90
91// https://fetch.spec.whatwg.org/#dom-body-text
92WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> BodyMixin::text() const
93{
94 auto& vm = Bindings::main_thread_vm();
95 auto& realm = *vm.current_realm();
96
97 // The text() method steps are to return the result of running consume body with this and text.
98 return consume_body(realm, *this, PackageDataType::Text);
99}
100
101// https://fetch.spec.whatwg.org/#concept-body-package-data
102WebIDL::ExceptionOr<JS::Value> package_data(JS::Realm& realm, ByteBuffer bytes, PackageDataType type, Optional<MimeSniff::MimeType> const& mime_type)
103{
104 auto& vm = realm.vm();
105
106 switch (type) {
107 case PackageDataType::ArrayBuffer:
108 // Return a new ArrayBuffer whose contents are bytes.
109 return JS::ArrayBuffer::create(realm, move(bytes));
110 case PackageDataType::Blob: {
111 // Return a Blob whose contents are bytes and type attribute is mimeType.
112 // NOTE: If extracting the mime type returns failure, other browsers set it to an empty string - not sure if that's spec'd.
113 auto mime_type_string = mime_type.has_value() ? TRY_OR_THROW_OOM(vm, mime_type->serialized()) : String {};
114 return TRY(FileAPI::Blob::create(realm, move(bytes), move(mime_type_string)));
115 }
116 case PackageDataType::FormData:
117 // If mimeType’s essence is "multipart/form-data", then:
118 if (mime_type.has_value() && mime_type->essence() == "multipart/form-data"sv) {
119 // FIXME: 1. Parse bytes, using the value of the `boundary` parameter from mimeType, per the rules set forth in Returning Values from Forms: multipart/form-data. [RFC7578]
120 // FIXME: 2. If that fails for some reason, then throw a TypeError.
121 // FIXME: 3. Return a new FormData object, appending each entry, resulting from the parsing operation, to its entry list.
122 return JS::js_null();
123 }
124 // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:
125 else if (mime_type.has_value() && mime_type->essence() == "application/x-www-form-urlencoded"sv) {
126 // FIXME: 1. Let entries be the result of parsing bytes.
127 // FIXME: 2. If entries is failure, then throw a TypeError.
128 // FIXME: 3. Return a new FormData object whose entry list is entries.
129 return JS::js_null();
130 }
131 // Otherwise, throw a TypeError.
132 else {
133 return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Mime type must be 'multipart/form-data' or 'application/x-www-form-urlencoded'"sv };
134 }
135 case PackageDataType::JSON:
136 // Return the result of running parse JSON from bytes on bytes.
137 return Infra::parse_json_bytes_to_javascript_value(realm, bytes);
138 case PackageDataType::Text:
139 // Return the result of running UTF-8 decode on bytes.
140 return JS::PrimitiveString::create(vm, TRY_OR_THROW_OOM(vm, String::from_utf8(bytes)));
141 default:
142 VERIFY_NOT_REACHED();
143 }
144}
145
146// https://fetch.spec.whatwg.org/#concept-body-consume-body
147WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> consume_body(JS::Realm& realm, BodyMixin const& object, PackageDataType type)
148{
149 // 1. If object is unusable, then return a promise rejected with a TypeError.
150 if (object.is_unusable()) {
151 auto promise_capability = WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "Body is unusable"sv).release_allocated_value_but_fixme_should_propagate_errors());
152 return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise_capability->promise().ptr()) };
153 }
154
155 // 2. Let promise be a new promise.
156 auto promise = WebIDL::create_promise(realm);
157
158 // 3. Let errorSteps given error be to reject promise with error.
159 // NOTE: `promise` and `realm` is protected by JS::SafeFunction.
160 auto error_steps = [promise, &realm](JS::Object& error) {
161 // NOTE: Not part of the spec, but we need to have an execution context on the stack to call native functions.
162 // (In this case, Promise's reject function)
163 auto& environment_settings_object = Bindings::host_defined_environment_settings_object(realm);
164 environment_settings_object.prepare_to_run_script();
165
166 WebIDL::reject_promise(realm, promise, &error);
167
168 // See above NOTE.
169 environment_settings_object.clean_up_after_running_script();
170 };
171
172 // 4. Let successSteps given a byte sequence data be to resolve promise with the result of running convertBytesToJSValue
173 // with data. If that threw an exception, then run errorSteps with that exception.
174 // NOTE: `promise`, `realm` and `object` is protected by JS::SafeFunction.
175 // FIXME: Refactor this to the new version of the spec introduced with https://github.com/whatwg/fetch/commit/464326e8eb6a602122c030cd40042480a3c0e265
176 auto success_steps = [promise, &realm, &object, type](ByteBuffer const& data) {
177 auto& vm = realm.vm();
178
179 // NOTE: Not part of the spec, but we need to have an execution context on the stack to call native functions.
180 // (In this case, Promise's reject function and JSON.parse)
181 auto& environment_settings_object = Bindings::host_defined_environment_settings_object(realm);
182 environment_settings_object.prepare_to_run_script();
183
184 ScopeGuard guard = [&]() {
185 // See above NOTE.
186 environment_settings_object.clean_up_after_running_script();
187 };
188
189 auto value_or_error = Bindings::throw_dom_exception_if_needed(vm, [&]() -> WebIDL::ExceptionOr<JS::Value> {
190 return package_data(realm, data, type, TRY_OR_THROW_OOM(vm, object.mime_type_impl()));
191 });
192
193 if (value_or_error.is_error()) {
194 // We can't call error_steps here without moving it into success_steps, causing a double move when we pause error_steps
195 // to fully_read, so just reject the promise like error_steps does.
196 WebIDL::reject_promise(realm, promise, value_or_error.release_error().value().value());
197 return;
198 }
199
200 WebIDL::resolve_promise(realm, promise, value_or_error.release_value());
201 };
202
203 // 5. If object’s body is null, then run successSteps with an empty byte sequence.
204 auto const& body = object.body_impl();
205 if (!body.has_value()) {
206 success_steps(ByteBuffer {});
207 }
208 // 6. Otherwise, fully read object’s body given successSteps, errorSteps, and object’s relevant global object.
209 else {
210 TRY(body->fully_read(realm, move(success_steps), move(error_steps), JS::NonnullGCPtr { HTML::relevant_global_object(object.as_platform_object()) }));
211 }
212
213 // 7. Return promise.
214 return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise().ptr()) };
215}
216
217}