diff options
-rw-r--r-- | Userland/Libraries/LibWeb/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Forward.h | 1 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Infra/ByteSequences.cpp | 29 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/Infra/ByteSequences.h | 2 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/XHR/XMLHttpRequest.cpp | 828 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/XHR/XMLHttpRequest.h | 61 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/XHR/XMLHttpRequest.idl | 7 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.cpp | 28 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.h | 25 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.idl | 6 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/idl_files.cmake | 1 |
11 files changed, 784 insertions, 205 deletions
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index e468862db4..0cd6b4d481 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -479,6 +479,7 @@ set(SOURCES XHR/ProgressEvent.cpp XHR/XMLHttpRequest.cpp XHR/XMLHttpRequestEventTarget.cpp + XHR/XMLHttpRequestUpload.cpp XML/XMLDocumentBuilder.cpp ) diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 243c34e27f..fd68b197a6 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -491,6 +491,7 @@ class FormData; class ProgressEvent; class XMLHttpRequest; class XMLHttpRequestEventTarget; +class XMLHttpRequestUpload; } namespace Web::UIEvents { diff --git a/Userland/Libraries/LibWeb/Infra/ByteSequences.cpp b/Userland/Libraries/LibWeb/Infra/ByteSequences.cpp index f54008c0db..7009df471c 100644 --- a/Userland/Libraries/LibWeb/Infra/ByteSequences.cpp +++ b/Userland/Libraries/LibWeb/Infra/ByteSequences.cpp @@ -25,4 +25,33 @@ void byte_uppercase(ByteBuffer& bytes) bytes[i] = to_ascii_uppercase(bytes[i]); } +// https://infra.spec.whatwg.org/#byte-sequence-starts-with +bool is_prefix_of(ReadonlyBytes potential_prefix, ReadonlyBytes input) +{ + // "input starts with potentialPrefix" can be used as a synonym for "potentialPrefix is a prefix of input". + return input.starts_with(potential_prefix); +} + +// https://infra.spec.whatwg.org/#byte-less-than +bool is_byte_less_than(ReadonlyBytes a, ReadonlyBytes b) +{ + // 1. If b is a prefix of a, then return false. + if (is_prefix_of(b, a)) + return false; + + // 2. If a is a prefix of b, then return true. + if (is_prefix_of(a, b)) + return true; + + // 3. Let n be the smallest index such that the nth byte of a is different from the nth byte of b. + // (There has to be such an index, since neither byte sequence is a prefix of the other.) + // 4. If the nth byte of a is less than the nth byte of b, then return true. + // 5. Return false. + for (size_t i = 0; i < a.size(); ++i) { + if (a[i] != b[i]) + return a[i] < b[i]; + } + VERIFY_NOT_REACHED(); +} + } diff --git a/Userland/Libraries/LibWeb/Infra/ByteSequences.h b/Userland/Libraries/LibWeb/Infra/ByteSequences.h index aac82e314e..50c67269bb 100644 --- a/Userland/Libraries/LibWeb/Infra/ByteSequences.h +++ b/Userland/Libraries/LibWeb/Infra/ByteSequences.h @@ -12,5 +12,7 @@ namespace Web::Infra { void byte_lowercase(ByteBuffer&); void byte_uppercase(ByteBuffer&); +bool is_prefix_of(ReadonlyBytes potential_prefix, ReadonlyBytes input); +bool is_byte_less_than(ReadonlyBytes a, ReadonlyBytes b); } diff --git a/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.cpp b/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.cpp index 9b18be22eb..35d3f335ea 100644 --- a/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.cpp +++ b/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.cpp @@ -1,7 +1,7 @@ /* * Copyright (c) 2020, Andreas Kling <kling@serenityos.org> * Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org> - * Copyright (c) 2022, Luke Wilde <lukew@serenityos.org> + * Copyright (c) 2022-2023, Luke Wilde <lukew@serenityos.org> * Copyright (c) 2022, Ali Mohammad Pur <mpfard@serenityos.org> * Copyright (c) 2022-2023, Kenneth Myhra <kennethmyhra@serenityos.org> * @@ -22,37 +22,49 @@ #include <LibWeb/DOM/EventDispatcher.h> #include <LibWeb/DOM/IDLEventListener.h> #include <LibWeb/Fetch/BodyInit.h> +#include <LibWeb/Fetch/Fetching/Fetching.h> +#include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h> +#include <LibWeb/Fetch/Infrastructure/FetchController.h> #include <LibWeb/Fetch/Infrastructure/HTTP.h> -#include <LibWeb/Fetch/Infrastructure/HTTP/Bodies.h> #include <LibWeb/Fetch/Infrastructure/HTTP/Methods.h> +#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h> +#include <LibWeb/Fetch/Infrastructure/HTTP/Responses.h> #include <LibWeb/FileAPI/Blob.h> #include <LibWeb/HTML/EventHandler.h> #include <LibWeb/HTML/EventNames.h> #include <LibWeb/HTML/Origin.h> #include <LibWeb/HTML/Window.h> +#include <LibWeb/Infra/ByteSequences.h> #include <LibWeb/Infra/JSON.h> +#include <LibWeb/Infra/Strings.h> #include <LibWeb/Loader/ResourceLoader.h> #include <LibWeb/Page/Page.h> +#include <LibWeb/Platform/EventLoopPlugin.h> #include <LibWeb/WebIDL/DOMException.h> #include <LibWeb/WebIDL/ExceptionOr.h> #include <LibWeb/XHR/EventNames.h> #include <LibWeb/XHR/ProgressEvent.h> #include <LibWeb/XHR/XMLHttpRequest.h> +#include <LibWeb/XHR/XMLHttpRequestUpload.h> namespace Web::XHR { WebIDL::ExceptionOr<JS::NonnullGCPtr<XMLHttpRequest>> XMLHttpRequest::construct_impl(JS::Realm& realm) { - auto& window = verify_cast<HTML::Window>(realm.global_object()); + auto upload_object = MUST_OR_THROW_OOM(realm.heap().allocate<XMLHttpRequestUpload>(realm, realm)); auto author_request_headers = Fetch::Infrastructure::HeaderList::create(realm.vm()); - return MUST_OR_THROW_OOM(realm.heap().allocate<XMLHttpRequest>(realm, window, *author_request_headers)); + auto response = Fetch::Infrastructure::Response::network_error(realm.vm(), "Not sent yet"sv); + auto fetch_controller = Fetch::Infrastructure::FetchController::create(realm.vm()); + return MUST_OR_THROW_OOM(realm.heap().allocate<XMLHttpRequest>(realm, realm, *upload_object, *author_request_headers, *response, *fetch_controller)); } -XMLHttpRequest::XMLHttpRequest(HTML::Window& window, Fetch::Infrastructure::HeaderList& author_request_headers) - : XMLHttpRequestEventTarget(window.realm()) - , m_window(window) +XMLHttpRequest::XMLHttpRequest(JS::Realm& realm, XMLHttpRequestUpload& upload_object, Fetch::Infrastructure::HeaderList& author_request_headers, Fetch::Infrastructure::Response& response, Fetch::Infrastructure::FetchController& fetch_controller) + : XMLHttpRequestEventTarget(realm) + , m_upload_object(upload_object) , m_author_request_headers(author_request_headers) + , m_response(response) , m_response_type(Bindings::XMLHttpRequestResponseType::Empty) + , m_fetch_controller(fetch_controller) { set_overrides_must_survive_garbage_collection(true); } @@ -70,24 +82,31 @@ JS::ThrowCompletionOr<void> XMLHttpRequest::initialize(JS::Realm& realm) void XMLHttpRequest::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); - visitor.visit(m_window.ptr()); + visitor.visit(m_upload_object); visitor.visit(m_author_request_headers); + visitor.visit(m_response); + visitor.visit(m_fetch_controller); if (auto* value = m_response_object.get_pointer<JS::Value>()) visitor.visit(*value); } -void XMLHttpRequest::fire_progress_event(DeprecatedString const& event_name, u64 transmitted, u64 length) +// https://xhr.spec.whatwg.org/#concept-event-fire-progress +static void fire_progress_event(XMLHttpRequestEventTarget& target, DeprecatedString const& event_name, u64 transmitted, u64 length) { + // To fire a progress event named e at target, given transmitted and length, means to fire an event named e at target, using ProgressEvent, + // with the loaded attribute initialized to transmitted, and if length is not 0, with the lengthComputable attribute initialized to true + // and the total attribute initialized to length. ProgressEventInit event_init {}; event_init.length_computable = true; event_init.loaded = transmitted; event_init.total = length; - dispatch_event(*ProgressEvent::create(realm(), String::from_deprecated_string(event_name).release_value_but_fixme_should_propagate_errors(), event_init).release_value_but_fixme_should_propagate_errors()); + // FIXME: If we're in an async context, this will propagate to a callback context which can't propagate it anywhere else and does not expect this to fail. + target.dispatch_event(*ProgressEvent::create(target.realm(), String::from_deprecated_string(event_name).release_value_but_fixme_should_propagate_errors(), event_init).release_value_but_fixme_should_propagate_errors()); } // https://xhr.spec.whatwg.org/#dom-xmlhttprequest-responsetext -WebIDL::ExceptionOr<DeprecatedString> XMLHttpRequest::response_text() const +WebIDL::ExceptionOr<String> XMLHttpRequest::response_text() const { // 1. If this’s response type is not the empty string or "text", then throw an "InvalidStateError" DOMException. if (m_response_type != Bindings::XMLHttpRequestResponseType::Empty && m_response_type != Bindings::XMLHttpRequestResponseType::Text) @@ -95,7 +114,7 @@ WebIDL::ExceptionOr<DeprecatedString> XMLHttpRequest::response_text() const // 2. If this’s state is not loading or done, then return the empty string. if (m_state != State::Loading && m_state != State::Done) - return DeprecatedString::empty(); + return String {}; return get_text_response(); } @@ -177,8 +196,7 @@ WebIDL::ExceptionOr<JS::Value> XMLHttpRequest::response() // Note: Automatically done by the layers above us. // 2. If this’s response’s body is null, then return null. - // FIXME: Implement this once we have 'Response'. - if (m_received_bytes.is_empty()) + if (!m_response->body().has_value()) return JS::js_null(); // 3. Let jsonObject be the result of running parse JSON from bytes on this’s received bytes. If that threw an exception, then return null. @@ -195,9 +213,11 @@ WebIDL::ExceptionOr<JS::Value> XMLHttpRequest::response() } // https://xhr.spec.whatwg.org/#text-response -DeprecatedString XMLHttpRequest::get_text_response() const +String XMLHttpRequest::get_text_response() const { - // FIXME: 1. If xhr’s response’s body is null, then return the empty string. + // 1. If xhr’s response’s body is null, then return the empty string. + if (!m_response->body().has_value()) + return String {}; // 2. Let charset be the result of get a final encoding for xhr. auto charset = get_final_encoding().release_value_but_fixme_should_propagate_errors(); @@ -225,7 +245,7 @@ DeprecatedString XMLHttpRequest::get_text_response() const // If we don't support the decoder yet, let's crash instead of attempting to return something, as the result would be incorrect and create obscure bugs. VERIFY(decoder.has_value()); - return TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(*decoder, m_received_bytes).release_value_but_fixme_should_propagate_errors().to_deprecated_string(); + return TextCodec::convert_input_to_utf8_using_given_decoder_unless_there_is_a_byte_order_mark(*decoder, m_received_bytes).release_value_but_fixme_should_propagate_errors(); } // https://xhr.spec.whatwg.org/#final-mime-type @@ -242,17 +262,8 @@ ErrorOr<MimeSniff::MimeType> XMLHttpRequest::get_final_mime_type() const // https://xhr.spec.whatwg.org/#response-mime-type ErrorOr<MimeSniff::MimeType> XMLHttpRequest::get_response_mime_type() const { - auto& vm = this->vm(); - - // FIXME: Use an actual HeaderList for XHR headers. - auto header_list = Fetch::Infrastructure::HeaderList::create(vm); - for (auto const& entry : m_response_headers) { - auto header = TRY(Fetch::Infrastructure::Header::from_string_pair(entry.key, entry.value)); - TRY(header_list->append(move(header))); - } - // 1. Let mimeType be the result of extracting a MIME type from xhr’s response’s header list. - auto mime_type = TRY(header_list->extract_mime_type()); + auto mime_type = TRY(m_response->header_list()->extract_mime_type()); // 2. If mimeType is failure, then set mimeType to text/xml. if (!mime_type.has_value()) @@ -296,13 +307,13 @@ ErrorOr<Optional<StringView>> XMLHttpRequest::get_final_encoding() const } // https://xhr.spec.whatwg.org/#dom-xmlhttprequest-setrequestheader -WebIDL::ExceptionOr<void> XMLHttpRequest::set_request_header(DeprecatedString const& name_string, DeprecatedString const& value_string) +WebIDL::ExceptionOr<void> XMLHttpRequest::set_request_header(String const& name_string, String const& value_string) { auto& realm = this->realm(); auto& vm = realm.vm(); - auto name = name_string.to_byte_buffer(); - auto value = value_string.to_byte_buffer(); + auto name = name_string.bytes(); + auto value = value_string.bytes(); // 1. If this’s state is not opened, then throw an "InvalidStateError" DOMException. if (m_state != State::Opened) @@ -313,7 +324,7 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::set_request_header(DeprecatedString co return WebIDL::InvalidStateError::create(realm, "XHR send() flag is already set"); // 3. Normalize value. - value = MUST(Fetch::Infrastructure::normalize_header_value(value)); + auto normalized_value = TRY_OR_THROW_OOM(vm, Fetch::Infrastructure::normalize_header_value(value)); // 4. If name is not a header name or value is not a header value, then throw a "SyntaxError" DOMException. if (!Fetch::Infrastructure::is_header_name(name)) @@ -322,8 +333,8 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::set_request_header(DeprecatedString co return WebIDL::SyntaxError::create(realm, "Header value contains invalid characters."); auto header = Fetch::Infrastructure::Header { - .name = move(name), - .value = move(value), + .name = TRY_OR_THROW_OOM(vm, ByteBuffer::copy(name)), + .value = move(normalized_value), }; // 5. If (name, value) is a forbidden request-header, then return. @@ -337,84 +348,89 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::set_request_header(DeprecatedString co } // https://xhr.spec.whatwg.org/#dom-xmlhttprequest-open -WebIDL::ExceptionOr<void> XMLHttpRequest::open(DeprecatedString const& method_string, DeprecatedString const& url) +WebIDL::ExceptionOr<void> XMLHttpRequest::open(String const& method_string, String const& url) { - // 8. If the async argument is omitted, set async to true, and set username and password to null. - return open(method_string, url, true, {}, {}); + // 7. If the async argument is omitted, set async to true, and set username and password to null. + return open(method_string, url, true, Optional<String> {}, Optional<String> {}); } -WebIDL::ExceptionOr<void> XMLHttpRequest::open(DeprecatedString const& method_string, DeprecatedString const& url, bool async, DeprecatedString const& username, DeprecatedString const& password) +WebIDL::ExceptionOr<void> XMLHttpRequest::open(String const& method_string, String const& url, bool async, Optional<String> const& username, Optional<String> const& password) { - auto method = method_string.to_byte_buffer(); + auto method = method_string.bytes(); - // 1. Let settingsObject be this’s relevant settings object. - auto& settings_object = m_window->associated_document().relevant_settings_object(); - - // 2. If settingsObject has a responsible document and it is not fully active, then throw an "InvalidStateError" DOMException. - if (settings_object.responsible_document() && !settings_object.responsible_document()->is_active()) - return WebIDL::InvalidStateError::create(realm(), "Invalid state: Responsible document is not fully active."); + // 1. If this’s relevant global object is a Window object and its associated Document is not fully active, then throw an "InvalidStateError" DOMException. + if (is<HTML::Window>(HTML::relevant_global_object(*this))) { + auto const& window = static_cast<HTML::Window const&>(HTML::relevant_global_object(*this)); + if (!window.associated_document().is_fully_active()) + return WebIDL::InvalidStateError::create(realm(), "Invalid state: Window's associated document is not fully active."); + } - // 3. If method is not a method, then throw a "SyntaxError" DOMException. + // 2. If method is not a method, then throw a "SyntaxError" DOMException. if (!Fetch::Infrastructure::is_method(method)) return WebIDL::SyntaxError::create(realm(), "An invalid or illegal string was specified."); - // 4. If method is a forbidden method, then throw a "SecurityError" DOMException. + // 3. If method is a forbidden method, then throw a "SecurityError" DOMException. if (Fetch::Infrastructure::is_forbidden_method(method)) return WebIDL::SecurityError::create(realm(), "Forbidden method, must not be 'CONNECT', 'TRACE', or 'TRACK'"); - // 5. Normalize method. - method = MUST(Fetch::Infrastructure::normalize_method(method)); + // 4. Normalize method. + auto normalized_method = TRY_OR_THROW_OOM(vm(), Fetch::Infrastructure::normalize_method(method)); - // 6. Let parsedURL be the result of parsing url with settingsObject’s API base URL and settingsObject’s API URL character encoding. - auto parsed_url = settings_object.api_base_url().complete_url(url); + // 5. Let parsedURL be the result of parsing url with this’s relevant settings object’s API base URL and this’s relevant settings object’s API URL character encoding. + // FIXME: Pass in this’s relevant settings object’s API URL character encoding. + auto parsed_url = HTML::relevant_settings_object(*this).api_base_url().complete_url(url); - // 7. If parsedURL is failure, then throw a "SyntaxError" DOMException. + // 6. If parsedURL is failure, then throw a "SyntaxError" DOMException. if (!parsed_url.is_valid()) return WebIDL::SyntaxError::create(realm(), "Invalid URL"); - // 8. If the async argument is omitted, set async to true, and set username and password to null. + // 7. If the async argument is omitted, set async to true, and set username and password to null. // NOTE: This is handled in the overload lacking the async argument. - // 9. If parsedURL’s host is non-null, then: + // 8. If parsedURL’s host is non-null, then: if (!parsed_url.host().is_null()) { // 1. If the username argument is not null, set the username given parsedURL and username. - if (!username.is_null()) - parsed_url.set_username(username); + if (username.has_value()) + parsed_url.set_username(username.value().to_deprecated_string()); // 2. If the password argument is not null, set the password given parsedURL and password. - if (!password.is_null()) - parsed_url.set_password(password); + if (password.has_value()) + parsed_url.set_password(password.value().to_deprecated_string()); } - // 10. If async is false, the current global object is a Window object, and either this’s timeout is + // 9. If async is false, the current global object is a Window object, and either this’s timeout is // not 0 or this’s response type is not the empty string, then throw an "InvalidAccessError" DOMException. if (!async && is<HTML::Window>(HTML::current_global_object()) && (m_timeout != 0 || m_response_type != Bindings::XMLHttpRequestResponseType::Empty)) { - return WebIDL::InvalidAccessError::create(realm(), "synchronous XMLHttpRequests do not support timeout and responseType"); + return WebIDL::InvalidAccessError::create(realm(), "Synchronous XMLHttpRequests in a Window context do not support timeout or a non-empty responseType"); } - // FIXME: 11. Terminate the ongoing fetch operated by the XMLHttpRequest object. + // 10. Terminate this’s fetch controller. + // Spec Note: A fetch can be ongoing at this point. + m_fetch_controller->terminate(); - // 12. Set variables associated with the object as follows: + // 11. Set variables associated with the object as follows: // Unset this’s send() flag. m_send = false; // Unset this’s upload listener flag. m_upload_listener = false; // Set this’s request method to method. - m_request_method = move(method); + m_request_method = move(normalized_method); // Set this’s request URL to parsedURL. m_request_url = parsed_url; // Set this’s synchronous flag if async is false; otherwise unset this’s synchronous flag. m_synchronous = !async; // Empty this’s author request headers. m_author_request_headers->clear(); - // FIXME: Set this’s response to a network error. + // Set this’s response to a network error. + m_response = Fetch::Infrastructure::Response::network_error(realm().vm(), "Not yet sent"sv); // Set this’s received bytes to the empty byte sequence. m_received_bytes = {}; // Set this’s response object to null. m_response_object = {}; + // Spec Note: Override MIME type is not overridden here as the overrideMimeType() method can be invoked before the open() method. - // 13. If this’s state is not opened, then: + // 12. If this’s state is not opened, then: if (m_state != State::Opened) { // 1. Set this’s state to opened. m_state = State::Opened; @@ -432,144 +448,397 @@ WebIDL::ExceptionOr<void> XMLHttpRequest::send(Optional<DocumentOrXMLHttpRequest auto& vm = this->vm(); auto& realm = *vm.current_realm(); + // 1. If this’s state is not opened, then throw an "InvalidStateError" DOMException. if (m_state != State::Opened) return WebIDL::InvalidStateError::create(realm, "XHR readyState is not OPENED"); + // 2. If this’s send() flag is set, then throw an "InvalidStateError" DOMException. if (m_send) return WebIDL::InvalidStateError::create(realm, "XHR send() flag is already set"); - // If this’s request method is `GET` or `HEAD`, then set body to null. + // 3. If this’s request method is `GET` or `HEAD`, then set body to null. if (m_request_method.is_one_of("GET"sv, "HEAD"sv)) body = {}; - Optional<Fetch::Infrastructure::BodyWithType> body_with_type {}; - Optional<DeprecatedString> serialized_document {}; + // 4. If body is not null, then: if (body.has_value()) { - if (body->has<JS::Handle<DOM::Document>>()) - serialized_document = TRY(body->get<JS::Handle<DOM::Document>>().cell()->serialize_fragment(DOMParsing::RequireWellFormed::No)); - else - body_with_type = TRY(Fetch::extract_body(realm, body->downcast<Fetch::BodyInitOrReadableBytes>())); + // 1. Let extractedContentType be null. + Optional<ByteBuffer> extracted_content_type; + + // 2. If body is a Document, then set this’s request body to body, serialized, converted, and UTF-8 encoded. + if (body->has<JS::Handle<DOM::Document>>()) { + // FIXME: Perform USVString conversion and UTF-8 encoding. + auto string_serialized_document = TRY(body->get<JS::Handle<DOM::Document>>().cell()->serialize_fragment(DOMParsing::RequireWellFormed::No)); + m_request_body = TRY(Fetch::Infrastructure::byte_sequence_as_body(realm, string_serialized_document.bytes())); + } + // 3. Otherwise: + else { + // 1. Let bodyWithType be the result of safely extracting body. + auto body_with_type = TRY(Fetch::safely_extract_body(realm, body->downcast<Fetch::BodyInitOrReadableBytes>())); + + // 2. Set this’s request body to bodyWithType’s body. + m_request_body = move(body_with_type.body); + + // 3. Set extractedContentType to bodyWithType’s type. + extracted_content_type = move(body_with_type.type); + } + + // 4. Let originalAuthorContentType be the result of getting `Content-Type` from this’s author request headers. + auto original_author_content_type = TRY_OR_THROW_OOM(vm, m_author_request_headers->get("Content-Type"sv.bytes())); + + // 5. If originalAuthorContentType is non-null, then: + if (original_author_content_type.has_value()) { + // 1. If body is a Document or a USVString, then: + if (body->has<JS::Handle<DOM::Document>>() || body->has<String>()) { + // 1. Let contentTypeRecord be the result of parsing originalAuthorContentType. + auto content_type_record = TRY_OR_THROW_OOM(vm, MimeSniff::MimeType::parse(original_author_content_type.value())); + + // 2. If contentTypeRecord is not failure, contentTypeRecord’s parameters["charset"] exists, and parameters["charset"] is not an ASCII case-insensitive match for "UTF-8", then: + if (content_type_record.has_value()) { + auto charset_parameter_iterator = content_type_record->parameters().find("charset"sv); + if (charset_parameter_iterator != content_type_record->parameters().end() && !Infra::is_ascii_case_insensitive_match(charset_parameter_iterator->value, "UTF-8"sv)) { + // 1. Set contentTypeRecord’s parameters["charset"] to "UTF-8". + TRY_OR_THROW_OOM(vm, content_type_record->set_parameter(TRY_OR_THROW_OOM(vm, "charset"_string), TRY_OR_THROW_OOM(vm, "UTF-8"_string))); + + // 2. Let newContentTypeSerialized be the result of serializing contentTypeRecord. + auto new_content_type_serialized = TRY_OR_THROW_OOM(vm, content_type_record->serialized()); + + // 3. Set (`Content-Type`, newContentTypeSerialized) in this’s author request headers. + auto header = TRY_OR_THROW_OOM(vm, Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, new_content_type_serialized)); + TRY_OR_THROW_OOM(vm, m_author_request_headers->set(move(header))); + } + } + } + } + // 6. Otherwise: + else { + if (body->has<JS::Handle<DOM::Document>>()) { + auto document = body->get<JS::Handle<DOM::Document>>(); + + // NOTE: A document can only be an HTML document or XML document. + // 1. If body is an HTML document, then set (`Content-Type`, `text/html;charset=UTF-8`) in this’s author request headers. + if (document->is_html_document()) { + auto header = TRY_OR_THROW_OOM(vm, Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, "text/html;charset=UTF-8"sv)); + TRY_OR_THROW_OOM(vm, m_author_request_headers->set(move(header))); + } + // 2. Otherwise, if body is an XML document, set (`Content-Type`, `application/xml;charset=UTF-8`) in this’s author request headers. + else if (document->is_xml_document()) { + auto header = TRY_OR_THROW_OOM(vm, Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, "application/xml;charset=UTF-8"sv)); + TRY_OR_THROW_OOM(vm, m_author_request_headers->set(move(header))); + } else { + VERIFY_NOT_REACHED(); + } + } + // 3. Otherwise, if extractedContentType is not null, set (`Content-Type`, extractedContentType) in this’s author request headers. + else if (extracted_content_type.has_value()) { + auto header = TRY_OR_THROW_OOM(vm, Fetch::Infrastructure::Header::from_string_pair("Content-Type"sv, extracted_content_type.value())); + TRY_OR_THROW_OOM(vm, m_author_request_headers->set(move(header))); + } + } } - AK::URL request_url = m_window->associated_document().parse_url(m_request_url.to_deprecated_string()); - dbgln("XHR send from {} to {}", m_window->associated_document().url(), request_url); + // 5. If one or more event listeners are registered on this’s upload object, then set this’s upload listener flag. + m_upload_listener = m_upload_object->has_event_listeners(); - // TODO: Add support for preflight requests to support CORS requests - auto request_url_origin = HTML::Origin(request_url.scheme(), request_url.host(), request_url.port_or_default()); + // 6. Let req be a new request, initialized as follows: + auto request = Fetch::Infrastructure::Request::create(vm); - bool should_enforce_same_origin_policy = true; - if (auto* page = m_window->page()) - should_enforce_same_origin_policy = page->is_same_origin_policy_enabled(); + // method + // This’s request method. + request->set_method(TRY_OR_THROW_OOM(vm, ByteBuffer::copy(m_request_method.bytes()))); - if (should_enforce_same_origin_policy && !m_window->associated_document().origin().is_same_origin(request_url_origin)) { - dbgln("XHR failed to load: Same-Origin Policy violation: {} may not load {}", m_window->associated_document().url(), request_url); - m_state = State::Done; - dispatch_event(TRY(DOM::Event::create(realm, EventNames::readystatechange))); - dispatch_event(TRY(DOM::Event::create(realm, HTML::EventNames::error))); - return {}; - } + // URL + // This’s request URL. + request->set_url(m_request_url); - auto request = LoadRequest::create_for_url_on_page(request_url, m_window->page()); - request.set_method(m_request_method); - if (serialized_document.has_value()) { - request.set_body(serialized_document->to_byte_buffer()); - } else if (body_with_type.has_value()) { - TRY(body_with_type->body.source().visit( - [&](ByteBuffer const& buffer) -> WebIDL::ExceptionOr<void> { - auto byte_buffer = TRY_OR_THROW_OOM(vm, ByteBuffer::copy(buffer)); - request.set_body(move(byte_buffer)); - return {}; - }, - [&](JS::Handle<FileAPI::Blob> const& blob) -> WebIDL::ExceptionOr<void> { - auto byte_buffer = TRY_OR_THROW_OOM(vm, ByteBuffer::copy(blob->bytes())); - request.set_body(move(byte_buffer)); - return {}; - }, - [](auto&) -> WebIDL::ExceptionOr<void> { - return {}; - })); - } + // header list + // This’s author request headers. + request->set_header_list(m_author_request_headers); - // If this’s headers’s header list does not contain `Content-Type`, then append (`Content-Type`, type) to this’s headers. - if (!m_author_request_headers->contains("Content-Type"sv.bytes())) { - if (body_with_type.has_value() && body_with_type->type.has_value()) { - request.set_header("Content-Type", DeprecatedString { body_with_type->type->span() }); - } else if (body.has_value() && body->has<JS::Handle<DOM::Document>>()) { - request.set_header("Content-Type", "text/html;charset=UTF-8"); - } - } - for (auto& it : *m_author_request_headers) - request.set_header(DeprecatedString::copy(it.name), DeprecatedString::copy(it.value)); + // unsafe-request flag + // Set. + request->set_unsafe_request(true); + + // body + // This’s request body. + if (m_request_body.has_value()) + request->set_body(m_request_body.value()); + + // client + // This’s relevant settings object. + request->set_client(&HTML::relevant_settings_object(*this)); + + // mode + // "cors". + request->set_mode(Fetch::Infrastructure::Request::Mode::CORS); + + // use-CORS-preflight flag + // Set if this’s upload listener flag is set. + request->set_use_cors_preflight(m_upload_listener); + + // credentials mode + // If this’s cross-origin credentials is true, then "include"; otherwise "same-origin". + request->set_credentials_mode(m_cross_origin_credentials ? Fetch::Infrastructure::Request::CredentialsMode::Include : Fetch::Infrastructure::Request::CredentialsMode::SameOrigin); + + // use-URL-credentials flag + // Set if this’s request URL includes credentials. + request->set_use_url_credentials(m_request_url.includes_credentials()); + // initiator type + // "xmlhttprequest". + request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::XMLHttpRequest); + + // 7. Unset this’s upload complete flag. m_upload_complete = false; + + // 8. Unset this’s timed out flag. m_timed_out = false; - // FIXME: If req’s body is null (which it always is currently) - m_upload_complete = true; + // 9. If req’s body is null, then set this’s upload complete flag. + // NOTE: req's body is always m_request_body here, see step 6. + if (!m_request_body.has_value()) + m_upload_complete = true; + // 10. Set this’s send() flag. m_send = true; + dbgln("{}XHR send from {} to {}", m_synchronous ? "\033[33;1mSynchronous\033[0m " : "", HTML::relevant_settings_object(*this).creation_url, m_request_url); + + // 11. If this’s synchronous flag is unset, then: if (!m_synchronous) { - fire_progress_event(EventNames::loadstart, 0, 0); + // 1. Fire a progress event named loadstart at this with 0 and 0. + fire_progress_event(*this, EventNames::loadstart, 0, 0); + + // 2. Let requestBodyTransmitted be 0. + // NOTE: This is kept on the XHR object itself instead of the stack, as we cannot capture references to stack variables in an async context. + m_request_body_transmitted = 0; - // FIXME: If this’s upload complete flag is unset and this’s upload listener flag is set, - // then fire a progress event named loadstart at this’s upload object with 0 and req’s body’s total bytes. + // 3. Let requestBodyLength be req’s body’s length, if req’s body is non-null; otherwise 0. + // NOTE: req's body is always m_request_body here, see step 6. + // 4. Assert: requestBodyLength is an integer. + // NOTE: This is done to provide a better assertion failure message, whereas below the message would be "m_has_value" + if (m_request_body.has_value()) + VERIFY(m_request_body->length().has_value()); + // NOTE: This is const to allow the callback functions to take a copy of it and know it won't change. + auto const request_body_length = m_request_body.has_value() ? m_request_body->length().value() : 0; + + // 5. If this’s upload complete flag is unset and this’s upload listener flag is set, then fire a progress event named loadstart at this’s upload object with requestBodyTransmitted and requestBodyLength. + if (!m_upload_complete && m_upload_listener) + fire_progress_event(m_upload_object, EventNames::loadstart, m_request_body_transmitted, request_body_length); + + // 6. If this’s state is not opened or this’s send() flag is unset, then return. if (m_state != State::Opened || !m_send) return {}; - // FIXME: in order to properly set State::HeadersReceived and State::Loading, - // we need to make ResourceLoader give us more detailed updates than just "done" and "error". - // FIXME: In the Fetch spec, which XHR gets its definition of `status` from, the status code is 0-999. - // We could clamp, wrap around (current browser behavior!), or error out. - // See: https://github.com/whatwg/fetch/issues/1142 - ResourceLoader::the().load( + // 7. Let processRequestBodyChunkLength, given a bytesLength, be these steps: + // NOTE: request_body_length is captured by copy as to not UAF it when we leave `send()` and the callback gets called. + // NOTE: `this` is kept alive by FetchAlgorithms using JS::SafeFunction. + auto process_request_body_chunk_length = [this, request_body_length](u64 bytes_length) { + // 1. Increase requestBodyTransmitted by bytesLength. + m_request_body_transmitted += bytes_length; + + // FIXME: 2. If not roughly 50ms have passed since these steps were last invoked, then return. + + // 3. If this’s upload listener flag is set, then fire a progress event named progress at this’s upload object with requestBodyTransmitted and requestBodyLength. + if (m_upload_listener) + fire_progress_event(m_upload_object, EventNames::progress, m_request_body_transmitted, request_body_length); + }; + + // 8. Let processRequestEndOfBody be these steps: + // NOTE: request_body_length is captured by copy as to not UAF it when we leave `send()` and the callback gets called. + // NOTE: `this` is kept alive by FetchAlgorithms using JS::SafeFunction. + auto process_request_end_of_body = [this, request_body_length]() { + // 1. Set this’s upload complete flag. + m_upload_complete = true; + + // 2. If this’s upload listener flag is unset, then return. + if (!m_upload_listener) + return; + + // 3. Fire a progress event named progress at this’s upload object with requestBodyTransmitted and requestBodyLength. + fire_progress_event(m_upload_object, EventNames::progress, m_request_body_transmitted, request_body_length); + + // 4. Fire a progress event named load at this’s upload object with requestBodyTransmitted and requestBodyLength. + fire_progress_event(m_upload_object, EventNames::load, m_request_body_transmitted, request_body_length); + + // 5. Fire a progress event named loadend at this’s upload object with requestBodyTransmitted and requestBodyLength. + fire_progress_event(m_upload_object, EventNames::loadend, m_request_body_transmitted, request_body_length); + }; + + // 9. Let processResponse, given a response, be these steps: + // NOTE: `this` is kept alive by FetchAlgorithms using JS::SafeFunction. + auto process_response = [this](JS::NonnullGCPtr<Fetch::Infrastructure::Response> response) { + // 1. Set this’s response to response. + m_response = response; + + // 2. Handle errors for this. + // NOTE: This cannot throw, as `handle_errors` only throws in a synchronous context. + // FIXME: However, we can receive allocation failures, but we can't propagate them anywhere currently. + handle_errors().release_value_but_fixme_should_propagate_errors(); + + // 3. If this’s response is a network error, then return. + if (m_response->is_network_error()) + return; + + // 4. Set this’s state to headers received. + m_state = State::HeadersReceived; + + // 5. Fire an event named readystatechange at this. + // FIXME: We're in an async context, so we can't propagate the error anywhere. + dispatch_event(*DOM::Event::create(this->realm(), EventNames::readystatechange).release_value_but_fixme_should_propagate_errors()); + + // 6. If this’s state is not headers received, then return. + if (m_state != State::HeadersReceived) + return; + + // 7. If this’s response’s body is null, then run handle response end-of-body for this and return. + if (!m_response->body().has_value()) { + // NOTE: This cannot throw, as `handle_response_end_of_body` only throws in a synchronous context. + // FIXME: However, we can receive allocation failures, but we can't propagate them anywhere currently. + handle_response_end_of_body().release_value_but_fixme_should_propagate_errors(); + return; + } + + // 8. Let length be the result of extracting a length from this’s response’s header list. + // FIXME: We're in an async context, so we can't propagate the error anywhere. + auto length = m_response->header_list()->extract_length().release_value_but_fixme_should_propagate_errors(); + + // 9. If length is not an integer, then set it to 0. + if (!length.has<u64>()) + length = 0; + + // FIXME: We can't implement these steps yet, as we don't fully implement the Streams standard. + + // 10. Let processBodyChunk given bytes be these steps: + // 1. Append bytes to this’s received bytes. + // 2. If not roughly 50ms have passed since these steps were last invoked, then return. + // 3. If this’s state is headers received, then set this’s state to loading. + // 4. Fire an event named readystatechange at this. + // Spec Note: Web compatibility is the reason readystatechange fires more often than this’s state changes. + // 5. Fire a progress event named progress at this with this’s received bytes’s length and length. + + // 11. Let processEndOfBody be this step: run handle response end-of-body for this. + + // 12. Let processBodyError be these steps: + // 1. Set this’s response to a network error. + // 2. Run handle errors for this. + + // 13. Incrementally read this’s response’s body, given processBodyChunk, processEndOfBody, processBodyError, and this’s relevant global object. + }; + + // FIXME: Remove this once we implement the Streams standard. See above. + // NOTE: `this` is kept alive by FetchAlgorithms using JS::SafeFunction. + auto process_response_consume_body = [this](JS::NonnullGCPtr<Fetch::Infrastructure::Response>, Variant<Empty, Fetch::Infrastructure::FetchAlgorithms::ConsumeBodyFailureTag, ByteBuffer> null_or_failure_or_bytes) { + // NOTE: `response` is not used here as `process_response` is called before `process_response_consume_body` and thus `m_response` is already set up. + if (null_or_failure_or_bytes.has<ByteBuffer>()) { + // NOTE: We are not in a context where we can throw if this fails due to OOM. + m_received_bytes.append(null_or_failure_or_bytes.get<ByteBuffer>()); + } + + // NOTE: This cannot throw, as `handle_response_end_of_body` only throws in a synchronous context. + // FIXME: However, we can receive allocation failures, but we can't propagate them anywhere currently. + handle_response_end_of_body().release_value_but_fixme_should_propagate_errors(); + }; + + // 10. Set this’s fetch controller to the result of fetching req with processRequestBodyChunkLength set to processRequestBodyChunkLength, processRequestEndOfBody set to processRequestEndOfBody, and processResponse set to processResponse. + m_fetch_controller = TRY(Fetch::Fetching::fetch( + realm, request, - [weak_this = make_weak_ptr<XMLHttpRequest>()](auto data, auto& response_headers, auto status_code) { - JS::GCPtr<XMLHttpRequest> strong_this = weak_this.ptr(); - if (!strong_this) - return; - auto& xhr = const_cast<XMLHttpRequest&>(*weak_this); - // FIXME: Handle OOM failure. - auto response_data = ByteBuffer::copy(data).release_value_but_fixme_should_propagate_errors(); - // FIXME: There's currently no difference between transmitted and length. - u64 transmitted = response_data.size(); - u64 length = response_data.size(); - - if (!xhr.m_synchronous) { - xhr.m_received_bytes = response_data; - xhr.fire_progress_event(EventNames::progress, transmitted, length); + Fetch::Infrastructure::FetchAlgorithms::create(vm, + { + .process_request_body_chunk_length = move(process_request_body_chunk_length), + .process_request_end_of_body = move(process_request_end_of_body), + .process_early_hints_response = {}, + .process_response = move(process_response), + .process_response_end_of_body = {}, + .process_response_consume_body = move(process_response_consume_body), // FIXME: Set this to null once we implement the Streams standard. See above. + }))); + + // 11. Let now be the present time. + // 12. Run these steps in parallel: + // 1. Wait until either req’s done flag is set or this’s timeout is not 0 and this’s timeout milliseconds have passed since now. + // 2. If req’s done flag is unset, then set this’s timed out flag and terminate this’s fetch controller. + if (m_timeout != 0) { + auto timer = Platform::Timer::create_single_shot(m_timeout, nullptr); + + // NOTE: `timer` is kept alive by copying the NNRP into the lambda, incrementing its ref-count. + // NOTE: `this` and `request` is kept alive by Platform::Timer using JS::SafeFunction. + timer->on_timeout = [this, request, timer]() { + if (!request->done()) { + m_timed_out = true; + m_fetch_controller->terminate(); } + }; - xhr.m_state = State::Done; - xhr.m_status = status_code.value_or(0); - xhr.m_response_headers = move(response_headers); - xhr.m_send = false; - xhr.dispatch_event(DOM::Event::create(xhr.realm(), EventNames::readystatechange).release_value_but_fixme_should_propagate_errors()); - xhr.fire_progress_event(EventNames::load, transmitted, length); - xhr.fire_progress_event(EventNames::loadend, transmitted, length); - }, - [weak_this = make_weak_ptr<XMLHttpRequest>()](auto& error, auto status_code) { - dbgln("XHR failed to load: {}", error); - JS::GCPtr<XMLHttpRequest> strong_this = weak_this.ptr(); - if (!strong_this) - return; - auto& xhr = const_cast<XMLHttpRequest&>(*strong_this); - xhr.m_state = State::Done; - xhr.set_status(status_code.value_or(0)); - xhr.dispatch_event(DOM::Event::create(xhr.realm(), EventNames::readystatechange).release_value_but_fixme_should_propagate_errors()); - xhr.dispatch_event(DOM::Event::create(xhr.realm(), HTML::EventNames::error).release_value_but_fixme_should_propagate_errors()); - }, - m_timeout, - [weak_this = make_weak_ptr<XMLHttpRequest>()] { - JS::GCPtr<XMLHttpRequest> strong_this = weak_this.ptr(); - if (!strong_this) - return; - auto& xhr = const_cast<XMLHttpRequest&>(*strong_this); - xhr.dispatch_event(DOM::Event::create(xhr.realm(), EventNames::timeout).release_value_but_fixme_should_propagate_errors()); - }); + timer->start(); + } } else { - TODO(); + // 1. Let processedResponse be false. + bool processed_response = false; + + // 2. Let processResponseConsumeBody, given a response and nullOrFailureOrBytes, be these steps: + auto process_response_consume_body = [this, &processed_response](JS::NonnullGCPtr<Fetch::Infrastructure::Response> response, Variant<Empty, Fetch::Infrastructure::FetchAlgorithms::ConsumeBodyFailureTag, ByteBuffer> null_or_failure_or_bytes) { + // 1. If nullOrFailureOrBytes is not failure, then set this’s response to response. + if (!null_or_failure_or_bytes.has<Fetch::Infrastructure::FetchAlgorithms::ConsumeBodyFailureTag>()) + m_response = response; + + // 2. If nullOrFailureOrBytes is a byte sequence, then append nullOrFailureOrBytes to this’s received bytes. + if (null_or_failure_or_bytes.has<ByteBuffer>()) { + // NOTE: We are not in a context where we can throw if this fails due to OOM. + m_received_bytes.append(null_or_failure_or_bytes.get<ByteBuffer>()); + } + + // 3. Set processedResponse to true. + processed_response = true; + }; + + // 3. Set this’s fetch controller to the result of fetching req with processResponseConsumeBody set to processResponseConsumeBody and useParallelQueue set to true. + m_fetch_controller = TRY(Fetch::Fetching::fetch( + realm, + request, + Fetch::Infrastructure::FetchAlgorithms::create(vm, + { + .process_request_body_chunk_length = {}, + .process_request_end_of_body = {}, + .process_early_hints_response = {}, + .process_response = {}, + .process_response_end_of_body = {}, + .process_response_consume_body = move(process_response_consume_body), + }), + Fetch::Fetching::UseParallelQueue::Yes)); + + // 4. Let now be the present time. + // 5. Pause until either processedResponse is true or this’s timeout is not 0 and this’s timeout milliseconds have passed since now. + bool did_time_out = false; + + if (m_timeout != 0) { + auto timer = Platform::Timer::create_single_shot(m_timeout, nullptr); + + // NOTE: `timer` is kept alive by copying the NNRP into the lambda, incrementing its ref-count. + timer->on_timeout = [timer, &did_time_out]() { + did_time_out = true; + }; + + timer->start(); + } + + // FIXME: This is not exactly correct, as it allows the HTML event loop to continue executing tasks. + Platform::EventLoopPlugin::the().spin_until([&]() { + return processed_response || did_time_out; + }); + + // 6. If processedResponse is false, then set this’s timed out flag and terminate this’s fetch controller. + if (!processed_response) { + m_timed_out = true; + m_fetch_controller->terminate(); + } + + // FIXME: 7. Report timing for this’s fetch controller given the current global object. + // We cannot do this for responses that have a body yet, as we do not setup the stream that then calls processResponseEndOfBody in `fetch_response_handover`. + + // 8. Run handle response end-of-body for this. + TRY(handle_response_end_of_body()); } return {}; } @@ -584,26 +853,66 @@ void XMLHttpRequest::set_onreadystatechange(WebIDL::CallbackType* value) set_event_handler_attribute(Web::XHR::EventNames::readystatechange, value); } -// https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method -DeprecatedString XMLHttpRequest::get_all_response_headers() const +// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getresponseheader +WebIDL::ExceptionOr<Optional<String>> XMLHttpRequest::get_response_header(String const& name) const { - // FIXME: Implement the spec-compliant sort order. + auto& vm = this->vm(); - StringBuilder builder; - auto keys = m_response_headers.keys(); - quick_sort(keys); + // The getResponseHeader(name) method steps are to return the result of getting name from this’s response’s header list. + auto header_bytes = TRY_OR_THROW_OOM(vm, m_response->header_list()->get(name.bytes())); + return header_bytes.has_value() ? TRY_OR_THROW_OOM(vm, String::from_utf8(*header_bytes)) : Optional<String> {}; +} + +// https://xhr.spec.whatwg.org/#legacy-uppercased-byte-less-than +static ErrorOr<bool> is_legacy_uppercased_byte_less_than(ReadonlyBytes a, ReadonlyBytes b) +{ + // 1. Let A be a, byte-uppercased. + auto uppercased_a = TRY(ByteBuffer::copy(a)); + Infra::byte_uppercase(uppercased_a); + + // 2. Let B be b, byte-uppercased. + auto uppercased_b = TRY(ByteBuffer::copy(b)); + Infra::byte_uppercase(uppercased_b); + + // 3. Return A is byte less than B. + return Infra::is_byte_less_than(uppercased_a, uppercased_b); +} + +// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getallresponseheaders +WebIDL::ExceptionOr<String> XMLHttpRequest::get_all_response_headers() const +{ + auto& vm = this->vm(); - for (auto& key : keys) { - builder.append(key); - builder.append(": "sv); - builder.append(m_response_headers.get(key).value()); - builder.append("\r\n"sv); + // 1. Let output be an empty byte sequence. + ByteBuffer output; + + // 2. Let initialHeaders be the result of running sort and combine with this’s response’s header list. + auto initial_headers = TRY_OR_THROW_OOM(vm, m_response->header_list()->sort_and_combine()); + + // 3. Let headers be the result of sorting initialHeaders in ascending order, with a being less than b if a’s name is legacy-uppercased-byte less than b’s name. + // Spec Note: Unfortunately, this is needed for compatibility with deployed content. + // NOTE: quick_sort mutates the collection instead of returning a sorted copy. + quick_sort(initial_headers, [](Fetch::Infrastructure::Header const& a, Fetch::Infrastructure::Header const& b) { + // FIXME: We are not in a context where we can throw from OOM. + return is_legacy_uppercased_byte_less_than(a.name, b.name).release_value_but_fixme_should_propagate_errors(); + }); + + // 4. For each header in headers, append header’s name, followed by a 0x3A 0x20 byte pair, followed by header’s value, followed by a 0x0D 0x0A byte pair, to output. + for (auto const& header : initial_headers) { + TRY_OR_THROW_OOM(vm, output.try_append(header.name)); + TRY_OR_THROW_OOM(vm, output.try_append(0x3A)); // ':' + TRY_OR_THROW_OOM(vm, output.try_append(0x20)); // ' ' + TRY_OR_THROW_OOM(vm, output.try_append(header.value)); + TRY_OR_THROW_OOM(vm, output.try_append(0x0D)); // '\r' + TRY_OR_THROW_OOM(vm, output.try_append(0x0A)); // '\n' } - return builder.to_deprecated_string(); + + // 5. Return output. + return TRY_OR_THROW_OOM(vm, String::from_utf8(output)); } // https://xhr.spec.whatwg.org/#dom-xmlhttprequest-overridemimetype -WebIDL::ExceptionOr<void> XMLHttpRequest::override_mime_type(DeprecatedString const& mime) +WebIDL::ExceptionOr<void> XMLHttpRequest::override_mime_type(String const& mime) { auto& vm = this->vm(); @@ -697,9 +1006,164 @@ bool XMLHttpRequest::must_survive_garbage_collection() const return false; } +// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-abort void XMLHttpRequest::abort() { - dbgln("(STUBBED) XMLHttpRequest::abort()"); + // 1. Abort this’s fetch controller. + m_fetch_controller->abort(realm(), {}); + + // 2. If this’s state is opened with this’s send() flag set, headers received, or loading, then run the request error steps for this and abort. + if ((m_state == State::Opened || m_state == State::HeadersReceived || m_state == State::Loading) && m_send) { + // NOTE: This cannot throw as we don't pass in an exception. XHR::abort cannot be reached in a synchronous context where the state matches above. + // This is because it pauses inside XHR::send until the request is done or times out and then immediately calls `handle_response_end_of_body` + // which will always set `m_state` to `Done`. + MUST(request_error_steps(EventNames::abort)); + } + + // 3. If this’s state is done, then set this’s state to unsent and this’s response to a network error. + // Spec Note: No readystatechange event is dispatched. + if (m_state == State::Done) { + m_state = State::Unsent; + m_response = Fetch::Infrastructure::Response::network_error(vm(), "Not yet sent"sv); + } +} + +// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-upload +JS::NonnullGCPtr<XMLHttpRequestUpload> XMLHttpRequest::upload() const +{ + // The upload getter steps are to return this’s upload object. + return m_upload_object; +} + +// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-status +Fetch::Infrastructure::Status XMLHttpRequest::status() const +{ + // The status getter steps are to return this’s response’s status. + return m_response->status(); +} + +// https://xhr.spec.whatwg.org/#dom-xmlhttprequest-statustext +WebIDL::ExceptionOr<String> XMLHttpRequest::status_text() const +{ + auto& vm = this->vm(); + + // The statusText getter steps are to return this’s response’s status message. + return TRY_OR_THROW_OOM(vm, String::from_utf8(m_response->status_message())); +} + +// https://xhr.spec.whatwg.org/#handle-response-end-of-body +WebIDL::ExceptionOr<void> XMLHttpRequest::handle_response_end_of_body() +{ + auto& vm = this->vm(); + auto& realm = this->realm(); + + // 1. Handle errors for xhr. + TRY(handle_errors()); + + // 2. If xhr’s response is a network error, then return. + if (m_response->is_network_error()) + return {}; + + // 3. Let transmitted be xhr’s received bytes’s length. + auto transmitted = m_received_bytes.size(); + + // 4. Let length be the result of extracting a length from this’s response’s header list. + auto maybe_length = TRY_OR_THROW_OOM(vm, m_response->header_list()->extract_length()); + + // 5. If length is not an integer, then set it to 0. + if (!maybe_length.has<u64>()) + maybe_length = 0; + + auto length = maybe_length.get<u64>(); + + // 6. If xhr’s synchronous flag is unset, then fire a progress event named progress at xhr with transmitted and length. + if (!m_synchronous) + fire_progress_event(*this, EventNames::progress, transmitted, length); + + // 7. Set xhr’s state to done. + m_state = State::Done; + + // 8. Unset xhr’s send() flag. + m_send = false; + + // 9. Fire an event named readystatechange at xhr. + // FIXME: If we're in an async context, this will propagate to a callback context which can't propagate it anywhere else and does not expect this to fail. + dispatch_event(*DOM::Event::create(realm, EventNames::readystatechange).release_value_but_fixme_should_propagate_errors()); + + // 10. Fire a progress event named load at xhr with transmitted and length. + fire_progress_event(*this, EventNames::load, transmitted, length); + + // 11. Fire a progress event named loadend at xhr with transmitted and length. + fire_progress_event(*this, EventNames::loadend, transmitted, length); + + return {}; +} + +// https://xhr.spec.whatwg.org/#handle-errors +WebIDL::ExceptionOr<void> XMLHttpRequest::handle_errors() +{ + // 1. If xhr’s send() flag is unset, then return. + if (!m_send) + return {}; + + // 2. If xhr’s timed out flag is set, then run the request error steps for xhr, timeout, and "TimeoutError" DOMException. + if (m_timed_out) + return TRY(request_error_steps(EventNames::timeout, WebIDL::TimeoutError::create(realm(), "Timed out"sv))); + + // 3. Otherwise, if xhr’s response’s aborted flag is set, run the request error steps for xhr, abort, and "AbortError" DOMException. + if (m_response->aborted()) + return TRY(request_error_steps(EventNames::abort, WebIDL::TimeoutError::create(realm(), "Aborted"sv))); + + // 4. Otherwise, if xhr’s response is a network error, then run the request error steps for xhr, error, and "NetworkError" DOMException. + if (m_response->is_network_error()) + return TRY(request_error_steps(EventNames::error, WebIDL::TimeoutError::create(realm(), "Network error"sv))); + + return {}; +} + +JS::ThrowCompletionOr<void> XMLHttpRequest::request_error_steps(DeprecatedFlyString const& event_name, JS::GCPtr<WebIDL::DOMException> exception) +{ + // 1. Set xhr’s state to done. + m_state = State::Done; + + // 2. Unset xhr’s send() flag. + m_send = false; + + // 3. Set xhr’s response to a network error. + m_response = Fetch::Infrastructure::Response::network_error(realm().vm(), "Failed to load"sv); + + // 4. If xhr’s synchronous flag is set, then throw exception. + if (m_synchronous) { + VERIFY(exception); + return JS::throw_completion(exception.ptr()); + } + + // 5. Fire an event named readystatechange at xhr. + // FIXME: Since we're in an async context, this will propagate to a callback context which can't propagate it anywhere else and does not expect this to fail. + dispatch_event(*DOM::Event::create(realm(), EventNames::readystatechange).release_value_but_fixme_should_propagate_errors()); + + // 6. If xhr’s upload complete flag is unset, then: + if (!m_upload_complete) { + // 1. Set xhr’s upload complete flag. + m_upload_complete = true; + + // 2. If xhr’s upload listener flag is set, then: + if (m_upload_listener) { + // 1. Fire a progress event named event at xhr’s upload object with 0 and 0. + fire_progress_event(m_upload_object, event_name, 0, 0); + + // 2. Fire a progress event named loadend at xhr’s upload object with 0 and 0. + fire_progress_event(m_upload_object, EventNames::loadend, 0, 0); + } + } + + // 7. Fire a progress event named event at xhr with 0 and 0. + fire_progress_event(*this, event_name, 0, 0); + + // 8. Fire a progress event named loadend at xhr with 0 and 0. + fire_progress_event(*this, EventNames::loadend, 0, 0); + + return {}; } } diff --git a/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.h b/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.h index 8a8aa779d3..bcb8a48865 100644 --- a/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.h +++ b/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.h @@ -1,6 +1,7 @@ /* * Copyright (c) 2020-2021, Andreas Kling <kling@serenityos.org> * Copyright (c) 2022, Kenneth Myhra <kennethmyhra@serenityos.org> + * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org> * * SPDX-License-Identifier: BSD-2-Clause */ @@ -13,6 +14,7 @@ #include <AK/Weakable.h> #include <LibWeb/DOM/EventTarget.h> #include <LibWeb/Fetch/BodyInit.h> +#include <LibWeb/Fetch/Infrastructure/HTTP/Bodies.h> #include <LibWeb/Fetch/Infrastructure/HTTP/Headers.h> #include <LibWeb/Fetch/Infrastructure/HTTP/Statuses.h> #include <LibWeb/HTML/Window.h> @@ -24,7 +26,7 @@ namespace Web::XHR { // https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit -using DocumentOrXMLHttpRequestBodyInit = Variant<JS::Handle<Web::DOM::Document>, JS::Handle<Web::FileAPI::Blob>, JS::Handle<JS::Object>, JS::Handle<Web::URL::URLSearchParams>, AK::DeprecatedString>; +using DocumentOrXMLHttpRequestBodyInit = Variant<JS::Handle<Web::DOM::Document>, JS::Handle<Web::FileAPI::Blob>, JS::Handle<JS::Object>, JS::Handle<Web::URL::URLSearchParams>, AK::String>; class XMLHttpRequest final : public XMLHttpRequestEventTarget { WEB_PLATFORM_OBJECT(XMLHttpRequest, XMLHttpRequestEventTarget); @@ -43,25 +45,26 @@ public: virtual ~XMLHttpRequest() override; State ready_state() const { return m_state; }; - Fetch::Infrastructure::Status status() const { return m_status; }; - WebIDL::ExceptionOr<DeprecatedString> response_text() const; + Fetch::Infrastructure::Status status() const; + WebIDL::ExceptionOr<String> status_text() const; + WebIDL::ExceptionOr<String> response_text() const; WebIDL::ExceptionOr<JS::Value> response(); Bindings::XMLHttpRequestResponseType response_type() const { return m_response_type; } - WebIDL::ExceptionOr<void> open(DeprecatedString const& method, DeprecatedString const& url); - WebIDL::ExceptionOr<void> open(DeprecatedString const& method, DeprecatedString const& url, bool async, DeprecatedString const& username = {}, DeprecatedString const& password = {}); + WebIDL::ExceptionOr<void> open(String const& method, String const& url); + WebIDL::ExceptionOr<void> open(String const& method, String const& url, bool async, Optional<String> const& username = Optional<String> {}, Optional<String> const& password = Optional<String> {}); WebIDL::ExceptionOr<void> send(Optional<DocumentOrXMLHttpRequestBodyInit> body); - WebIDL::ExceptionOr<void> set_request_header(DeprecatedString const& header, DeprecatedString const& value); + WebIDL::ExceptionOr<void> set_request_header(String const& header, String const& value); WebIDL::ExceptionOr<void> set_response_type(Bindings::XMLHttpRequestResponseType); - DeprecatedString get_response_header(DeprecatedString const& name) { return m_response_headers.get(name).value_or({}); } - DeprecatedString get_all_response_headers() const; + WebIDL::ExceptionOr<Optional<String>> get_response_header(String const& name) const; + WebIDL::ExceptionOr<String> get_all_response_headers() const; WebIDL::CallbackType* onreadystatechange(); void set_onreadystatechange(WebIDL::CallbackType*); - WebIDL::ExceptionOr<void> override_mime_type(DeprecatedString const& mime); + WebIDL::ExceptionOr<void> override_mime_type(String const& mime); u32 timeout() const; WebIDL::ExceptionOr<void> set_timeout(u32 timeout); @@ -71,26 +74,29 @@ public: void abort(); + JS::NonnullGCPtr<XMLHttpRequestUpload> upload() const; + private: virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; virtual bool must_survive_garbage_collection() const override; - void set_status(Fetch::Infrastructure::Status status) { m_status = status; } - void fire_progress_event(DeprecatedString const&, u64, u64); - ErrorOr<MimeSniff::MimeType> get_response_mime_type() const; ErrorOr<Optional<StringView>> get_final_encoding() const; ErrorOr<MimeSniff::MimeType> get_final_mime_type() const; - DeprecatedString get_text_response() const; + String get_text_response() const; - XMLHttpRequest(HTML::Window&, Fetch::Infrastructure::HeaderList&); + WebIDL::ExceptionOr<void> handle_response_end_of_body(); + WebIDL::ExceptionOr<void> handle_errors(); + JS::ThrowCompletionOr<void> request_error_steps(DeprecatedFlyString const& event_name, JS::GCPtr<WebIDL::DOMException> exception = nullptr); - // Non-standard - JS::NonnullGCPtr<HTML::Window> m_window; - Fetch::Infrastructure::Status m_status { 0 }; - HashMap<DeprecatedString, DeprecatedString, CaseInsensitiveStringTraits> m_response_headers; + XMLHttpRequest(JS::Realm&, XMLHttpRequestUpload&, Fetch::Infrastructure::HeaderList&, Fetch::Infrastructure::Response&, Fetch::Infrastructure::FetchController&); + + // https://xhr.spec.whatwg.org/#upload-object + // upload object + // An XMLHttpRequestUpload object. + JS::NonnullGCPtr<XMLHttpRequestUpload> m_upload_object; // https://xhr.spec.whatwg.org/#concept-xmlhttprequest-state // state @@ -127,7 +133,10 @@ private: // A header list, initially empty. JS::NonnullGCPtr<Fetch::Infrastructure::HeaderList> m_author_request_headers; - // FIXME: https://xhr.spec.whatwg.org/#request-body + // https://xhr.spec.whatwg.org/#request-body + // request body + // Initially null. + Optional<Fetch::Infrastructure::Body> m_request_body; // https://xhr.spec.whatwg.org/#synchronous-flag // synchronous flag @@ -149,7 +158,10 @@ private: // A flag, initially unset. bool m_timed_out { false }; - // FIXME: https://xhr.spec.whatwg.org/#response + // https://xhr.spec.whatwg.org/#response + // response + // A response, initially a network error. + JS::NonnullGCPtr<Fetch::Infrastructure::Response> m_response; // https://xhr.spec.whatwg.org/#received-bytes // received bytes @@ -171,13 +183,20 @@ private: // NOTE: This needs to be a JS::Value as the JSON response might not actually be an object. Variant<JS::Value, Failure, Empty> m_response_object; - // FIXME: https://xhr.spec.whatwg.org/#xmlhttprequest-fetch-controller + // https://xhr.spec.whatwg.org/#xmlhttprequest-fetch-controller + // fetch controller + // A fetch controller, initially a new fetch controller. + // NOTE: The send() method sets it to a useful fetch controller, but for simplicity it always holds a fetch controller. + JS::NonnullGCPtr<Fetch::Infrastructure::FetchController> m_fetch_controller; // https://xhr.spec.whatwg.org/#override-mime-type // override MIME type // A MIME type or null, initially null. // NOTE: Can get a value when overrideMimeType() is invoked. Optional<MimeSniff::MimeType> m_override_mime_type; + + // Non-standard, see async path in `send()` + u64 m_request_body_transmitted { 0 }; }; } diff --git a/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.idl b/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.idl index 7e9660e76a..910917a203 100644 --- a/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.idl +++ b/Userland/Libraries/LibWeb/XHR/XMLHttpRequest.idl @@ -2,6 +2,7 @@ #import <DOM/EventHandler.idl> #import <Fetch/BodyInit.idl> #import <XHR/XMLHttpRequestEventTarget.idl> +#import <XHR/XMLHttpRequestUpload.idl> enum XMLHttpRequestResponseType { "", @@ -13,7 +14,7 @@ enum XMLHttpRequestResponseType { }; // https://xhr.spec.whatwg.org/#xmlhttprequest -[Exposed=(Window,DedicatedWorker,SharedWorker)] +[Exposed=(Window,DedicatedWorker,SharedWorker), UseNewAKString] interface XMLHttpRequest : XMLHttpRequestEventTarget { constructor(); @@ -26,14 +27,16 @@ interface XMLHttpRequest : XMLHttpRequestEventTarget { readonly attribute unsigned short readyState; readonly attribute unsigned short status; + readonly attribute ByteString statusText; readonly attribute DOMString responseText; readonly attribute any response; attribute XMLHttpRequestResponseType responseType; attribute unsigned long timeout; attribute boolean withCredentials; + [SameObject] readonly attribute XMLHttpRequestUpload upload; undefined open(DOMString method, DOMString url); - undefined open(ByteString method, USVString url, boolean async, optional USVString? username = {}, optional USVString? password = {}); + undefined open(ByteString method, USVString url, boolean async, optional USVString? username = null, optional USVString? password = null); undefined setRequestHeader(DOMString name, DOMString value); undefined send(optional (Document or XMLHttpRequestBodyInit)? body = null); undefined abort(); diff --git a/Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.cpp b/Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.cpp new file mode 100644 index 0000000000..1c54f4a4db --- /dev/null +++ b/Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <LibWeb/Bindings/Intrinsics.h> +#include <LibWeb/Bindings/XMLHttpRequestUploadPrototype.h> +#include <LibWeb/XHR/XMLHttpRequestUpload.h> + +namespace Web::XHR { + +XMLHttpRequestUpload::XMLHttpRequestUpload(JS::Realm& realm) + : XMLHttpRequestEventTarget(realm) +{ +} + +XMLHttpRequestUpload::~XMLHttpRequestUpload() = default; + +JS::ThrowCompletionOr<void> XMLHttpRequestUpload::initialize(JS::Realm& realm) +{ + MUST_OR_THROW_OOM(Base::initialize(realm)); + set_prototype(&Bindings::ensure_web_prototype<Bindings::XMLHttpRequestUploadPrototype>(realm, "XMLHttpRequestUpload")); + + return {}; +} + +} diff --git a/Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.h b/Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.h new file mode 100644 index 0000000000..d2132fe217 --- /dev/null +++ b/Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include <LibWeb/XHR/XMLHttpRequestEventTarget.h> + +namespace Web::XHR { + +class XMLHttpRequestUpload : public XMLHttpRequestEventTarget { + WEB_PLATFORM_OBJECT(XMLHttpRequestUpload, XMLHttpRequestEventTarget); + +public: + virtual ~XMLHttpRequestUpload() override; + +private: + XMLHttpRequestUpload(JS::Realm&); + + virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override; +}; + +} diff --git a/Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.idl b/Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.idl new file mode 100644 index 0000000000..4c679b7eba --- /dev/null +++ b/Userland/Libraries/LibWeb/XHR/XMLHttpRequestUpload.idl @@ -0,0 +1,6 @@ +#import <XHR/XMLHttpRequestEventTarget.idl> + +// https://xhr.spec.whatwg.org/#xmlhttprequestupload +[Exposed=(Window,DedicatedWorker,SharedWorker)] +interface XMLHttpRequestUpload : XMLHttpRequestEventTarget { +}; diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index e394258a7d..2ea24eeb42 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -206,3 +206,4 @@ libweb_js_bindings(XHR/FormData) libweb_js_bindings(XHR/ProgressEvent) libweb_js_bindings(XHR/XMLHttpRequest) libweb_js_bindings(XHR/XMLHttpRequestEventTarget) +libweb_js_bindings(XHR/XMLHttpRequestUpload) |