From 5ad62833319828cd908a3486b894edd46ca9f9fb Mon Sep 17 00:00:00 2001 From: Linus Groh Date: Sun, 25 Sep 2022 19:30:24 +0100 Subject: [PATCH] LibWeb: Implement '5.3. Body mixin' from the Fetch API :^) This will be used to share functionality between Request and Response. --- Userland/Libraries/LibWeb/CMakeLists.txt | 1 + Userland/Libraries/LibWeb/Fetch/Body.cpp | 174 +++++++++++++++++++++++ Userland/Libraries/LibWeb/Fetch/Body.h | 47 ++++++ Userland/Libraries/LibWeb/Fetch/Body.idl | 12 ++ Userland/Libraries/LibWeb/Forward.h | 1 + 5 files changed, 235 insertions(+) create mode 100644 Userland/Libraries/LibWeb/Fetch/Body.cpp create mode 100644 Userland/Libraries/LibWeb/Fetch/Body.h create mode 100644 Userland/Libraries/LibWeb/Fetch/Body.idl diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index bd46f8b1e39..fece527fc90 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -118,6 +118,7 @@ set(SOURCES Dump.cpp Encoding/TextDecoder.cpp Encoding/TextEncoder.cpp + Fetch/Body.cpp Fetch/BodyInit.cpp Fetch/Headers.cpp Fetch/HeadersIterator.cpp diff --git a/Userland/Libraries/LibWeb/Fetch/Body.cpp b/Userland/Libraries/LibWeb/Fetch/Body.cpp new file mode 100644 index 00000000000..a3e4359c351 --- /dev/null +++ b/Userland/Libraries/LibWeb/Fetch/Body.cpp @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2022, Linus Groh + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::Fetch { + +BodyMixin::~BodyMixin() = default; + +// https://fetch.spec.whatwg.org/#body-unusable +bool BodyMixin::is_unusable() const +{ + // 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. + auto const& body = body_impl(); + return body.has_value() && (body->stream()->is_disturbed() || body->stream()->is_locked()); +} + +// https://fetch.spec.whatwg.org/#dom-body-body +JS::GCPtr BodyMixin::body() const +{ + // The body getter steps are to return null if this’s body is null; otherwise this’s body’s stream. + auto const& body = body_impl(); + return body.has_value() ? body->stream().ptr() : nullptr; +} + +// https://fetch.spec.whatwg.org/#dom-body-bodyused +bool BodyMixin::body_used() const +{ + // 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. + auto const& body = body_impl(); + return body.has_value() && body->stream()->is_disturbed(); +} + +// https://fetch.spec.whatwg.org/#dom-body-arraybuffer +JS::NonnullGCPtr BodyMixin::array_buffer() const +{ + auto& vm = Bindings::main_thread_vm(); + auto& realm = *vm.current_realm(); + + // The arrayBuffer() method steps are to return the result of running consume body with this and ArrayBuffer. + return consume_body(realm, *this, PackageDataType::ArrayBuffer); +} + +// https://fetch.spec.whatwg.org/#dom-body-blob +JS::NonnullGCPtr BodyMixin::blob() const +{ + auto& vm = Bindings::main_thread_vm(); + auto& realm = *vm.current_realm(); + + // The blob() method steps are to return the result of running consume body with this and Blob. + return consume_body(realm, *this, PackageDataType::Blob); +} + +// https://fetch.spec.whatwg.org/#dom-body-formdata +JS::NonnullGCPtr BodyMixin::form_data() const +{ + auto& vm = Bindings::main_thread_vm(); + auto& realm = *vm.current_realm(); + + // The formData() method steps are to return the result of running consume body with this and FormData. + return consume_body(realm, *this, PackageDataType::FormData); +} + +// https://fetch.spec.whatwg.org/#dom-body-json +JS::NonnullGCPtr BodyMixin::json() const +{ + auto& vm = Bindings::main_thread_vm(); + auto& realm = *vm.current_realm(); + + // The json() method steps are to return the result of running consume body with this and JSON. + return consume_body(realm, *this, PackageDataType::JSON); +} + +// https://fetch.spec.whatwg.org/#dom-body-text +JS::NonnullGCPtr BodyMixin::text() const +{ + auto& vm = Bindings::main_thread_vm(); + auto& realm = *vm.current_realm(); + + // The text() method steps are to return the result of running consume body with this and text. + return consume_body(realm, *this, PackageDataType::Text); +} + +// https://fetch.spec.whatwg.org/#concept-body-package-data +WebIDL::ExceptionOr package_data(JS::Realm& realm, ByteBuffer bytes, PackageDataType type, Optional const& mime_type) +{ + auto& vm = realm.vm(); + auto& window = verify_cast(realm.global_object()); + + switch (type) { + case PackageDataType::ArrayBuffer: + // Return a new ArrayBuffer whose contents are bytes. + return JS::ArrayBuffer::create(realm, move(bytes)); + case PackageDataType::Blob: { + // Return a Blob whose contents are bytes and type attribute is mimeType. + // NOTE: If extracting the mime type returns failure, other browsers set it to an empty string - not sure if that's spec'd. + auto mime_type_string = mime_type.has_value() ? mime_type->serialized() : String::empty(); + return FileAPI::Blob::create(window, move(bytes), move(mime_type_string)); + } + case PackageDataType::FormData: + // If mimeType’s essence is "multipart/form-data", then: + if (mime_type.has_value() && mime_type->essence() == "multipart/form-data"sv) { + // 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] + // FIXME: 2. If that fails for some reason, then throw a TypeError. + // FIXME: 3. Return a new FormData object, appending each entry, resulting from the parsing operation, to its entry list. + return JS::js_null(); + } + // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then: + else if (mime_type.has_value() && mime_type->essence() == "application/x-www-form-urlencoded"sv) { + // FIXME: 1. Let entries be the result of parsing bytes. + // FIXME: 2. If entries is failure, then throw a TypeError. + // FIXME: 3. Return a new FormData object whose entry list is entries. + return JS::js_null(); + } + // Otherwise, throw a TypeError. + else { + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Mime type must be 'multipart/form-data' or 'application/x-www-form-urlencoded'"sv }; + } + case PackageDataType::JSON: + // Return the result of running parse JSON from bytes on bytes. + return Infra::parse_json_bytes_to_javascript_value(vm, bytes); + case PackageDataType::Text: + // Return the result of running UTF-8 decode on bytes. + return JS::js_string(vm, String::copy(bytes)); + default: + VERIFY_NOT_REACHED(); + } +} + +// https://fetch.spec.whatwg.org/#concept-body-consume-body +JS::NonnullGCPtr consume_body(JS::Realm& realm, BodyMixin const& object, PackageDataType type) +{ + auto& vm = realm.vm(); + + // 1. If object is unusable, then return a promise rejected with a TypeError. + if (object.is_unusable()) { + auto promise_capability = WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "Body is unusable"sv)); + return static_cast(*promise_capability.promise); + } + + // 2. Let promise be a promise resolved with an empty byte sequence. + auto promise = WebIDL::create_resolved_promise(realm, JS::js_string(vm, String::empty())); + + // 3. If object’s body is non-null, then set promise to the result of fully reading body as promise given object’s body. + auto const& body = object.body_impl(); + if (body.has_value()) + promise = body->fully_read_as_promise(); + + // 4. Let steps be to return the result of package data with the first argument given, type, and object’s MIME type. + auto steps = [&realm, &object, type](JS::Value value) -> WebIDL::ExceptionOr { + VERIFY(value.is_string()); + auto bytes = TRY_OR_RETURN_OOM(realm.global_object(), ByteBuffer::copy(value.as_string().string().bytes())); + return package_data(realm, move(bytes), type, object.mime_type_impl()); + }; + + // 5. Return the result of upon fulfillment of promise given steps. + return WebIDL::upon_fulfillment(promise, move(steps)); +} + +} diff --git a/Userland/Libraries/LibWeb/Fetch/Body.h b/Userland/Libraries/LibWeb/Fetch/Body.h new file mode 100644 index 00000000000..993caa2d854 --- /dev/null +++ b/Userland/Libraries/LibWeb/Fetch/Body.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022, Linus Groh + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::Fetch { + +enum class PackageDataType { + ArrayBuffer, + Blob, + FormData, + JSON, + Text, +}; + +// https://fetch.spec.whatwg.org/#body-mixin +class BodyMixin { +public: + virtual ~BodyMixin(); + + virtual Optional mime_type_impl() const = 0; + virtual Optional body_impl() = 0; + virtual Optional body_impl() const = 0; + + [[nodiscard]] bool is_unusable() const; + [[nodiscard]] JS::GCPtr body() const; + [[nodiscard]] bool body_used() const; + + // JS API functions + [[nodiscard]] JS::NonnullGCPtr array_buffer() const; + [[nodiscard]] JS::NonnullGCPtr blob() const; + [[nodiscard]] JS::NonnullGCPtr form_data() const; + [[nodiscard]] JS::NonnullGCPtr json() const; + [[nodiscard]] JS::NonnullGCPtr text() const; +}; + +[[nodiscard]] WebIDL::ExceptionOr package_data(JS::Realm&, ByteBuffer, PackageDataType, Optional const&); +[[nodiscard]] JS::NonnullGCPtr consume_body(JS::Realm&, BodyMixin const&, PackageDataType); + +} diff --git a/Userland/Libraries/LibWeb/Fetch/Body.idl b/Userland/Libraries/LibWeb/Fetch/Body.idl new file mode 100644 index 00000000000..80085c43111 --- /dev/null +++ b/Userland/Libraries/LibWeb/Fetch/Body.idl @@ -0,0 +1,12 @@ +#import + +// https://fetch.spec.whatwg.org/#body +interface mixin Body { + readonly attribute ReadableStream? body; + readonly attribute boolean bodyUsed; + [NewObject] Promise arrayBuffer(); + [NewObject] Promise blob(); + [NewObject] Promise formData(); + [NewObject] Promise json(); + [NewObject] Promise text(); +}; diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 8971d486f66..27a27848214 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -180,6 +180,7 @@ class TextEncoder; } namespace Web::Fetch { +class BodyMixin; class Headers; class HeadersIterator; }