summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibWeb/Fetch/Body.cpp
blob: 50a5f3bb9a6af8cada573ce739da38fb1ce1866f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
/*
 * Copyright (c) 2022-2023, Linus Groh <linusg@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/TypeCasts.h>
#include <LibJS/Runtime/ArrayBuffer.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Error.h>
#include <LibJS/Runtime/PromiseCapability.h>
#include <LibTextCodec/Decoder.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/HostDefined.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Fetch/Body.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Bodies.h>
#include <LibWeb/FileAPI/Blob.h>
#include <LibWeb/Infra/JSON.h>
#include <LibWeb/MimeSniff/MimeType.h>
#include <LibWeb/Streams/ReadableStream.h>
#include <LibWeb/WebIDL/Promise.h>

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<Streams::ReadableStream> 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
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> 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
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> 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
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> 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
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> 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
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> 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<JS::Value> package_data(JS::Realm& realm, ByteBuffer bytes, PackageDataType type, Optional<MimeSniff::MimeType> const& mime_type)
{
    auto& vm = realm.vm();

    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() ? TRY_OR_THROW_OOM(vm, mime_type->serialized()) : String {};
        return TRY(FileAPI::Blob::create(realm, 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(realm, bytes);
    case PackageDataType::Text: {
        // Return the result of running UTF-8 decode on bytes.
        auto decoder = TextCodec::decoder_for("UTF-8"sv);
        VERIFY(decoder.has_value());

        auto utf8_text = TRY_OR_THROW_OOM(vm, TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(*decoder, bytes));
        return JS::PrimitiveString::create(vm, move(utf8_text));
    }
    default:
        VERIFY_NOT_REACHED();
    }
}

// https://fetch.spec.whatwg.org/#concept-body-consume-body
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> consume_body(JS::Realm& realm, BodyMixin const& object, PackageDataType type)
{
    // 1. If object is unusable, then return a promise rejected with a TypeError.
    if (object.is_unusable()) {
        auto exception = MUST_OR_THROW_OOM(JS::TypeError::create(realm, "Body is unusable"sv));
        auto promise_capability = WebIDL::create_rejected_promise(realm, exception);
        return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise_capability->promise().ptr()) };
    }

    // 2. Let promise be a new promise.
    auto promise = WebIDL::create_promise(realm);

    // 3. Let errorSteps given error be to reject promise with error.
    // NOTE: `promise` and `realm` is protected by JS::SafeFunction.
    auto error_steps = [promise, &realm](JS::GCPtr<WebIDL::DOMException> error) {
        // NOTE: Not part of the spec, but we need to have an execution context on the stack to call native functions.
        //       (In this case, Promise's reject function)
        auto& environment_settings_object = Bindings::host_defined_environment_settings_object(realm);
        environment_settings_object.prepare_to_run_script();

        WebIDL::reject_promise(realm, promise, error);

        // See above NOTE.
        environment_settings_object.clean_up_after_running_script();
    };

    // 4. Let successSteps given a byte sequence data be to resolve promise with the result of running convertBytesToJSValue
    //    with data. If that threw an exception, then run errorSteps with that exception.
    // NOTE: `promise`, `realm` and `object` is protected by JS::SafeFunction.
    // FIXME: Refactor this to the new version of the spec introduced with https://github.com/whatwg/fetch/commit/464326e8eb6a602122c030cd40042480a3c0e265
    auto success_steps = [promise, &realm, &object, type](ByteBuffer const& data) {
        auto& vm = realm.vm();

        // NOTE: Not part of the spec, but we need to have an execution context on the stack to call native functions.
        //       (In this case, Promise's reject function and JSON.parse)
        auto& environment_settings_object = Bindings::host_defined_environment_settings_object(realm);
        environment_settings_object.prepare_to_run_script();

        ScopeGuard guard = [&]() {
            // See above NOTE.
            environment_settings_object.clean_up_after_running_script();
        };

        auto value_or_error = Bindings::throw_dom_exception_if_needed(vm, [&]() -> WebIDL::ExceptionOr<JS::Value> {
            return package_data(realm, data, type, TRY_OR_THROW_OOM(vm, object.mime_type_impl()));
        });

        if (value_or_error.is_error()) {
            // We can't call error_steps here without moving it into success_steps, causing a double move when we pause error_steps
            // to fully_read, so just reject the promise like error_steps does.
            WebIDL::reject_promise(realm, promise, value_or_error.release_error().value().value());
            return;
        }

        WebIDL::resolve_promise(realm, promise, value_or_error.release_value());
    };

    // 5. If object’s body is null, then run successSteps with an empty byte sequence.
    auto const& body = object.body_impl();
    if (!body.has_value()) {
        success_steps(ByteBuffer {});
    }
    // 6. Otherwise, fully read object’s body given successSteps, errorSteps, and object’s relevant global object.
    else {
        TRY(body->fully_read(realm, move(success_steps), move(error_steps), JS::NonnullGCPtr { HTML::relevant_global_object(object.as_platform_object()) }));
    }

    // 7. Return promise.
    return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise().ptr()) };
}

}