From fc42c75a0ce06f6c3ce1552da084af73ad253599 Mon Sep 17 00:00:00 2001 From: Luke Wilde Date: Mon, 6 Nov 2023 20:22:56 +0000 Subject: [PATCH] LibWeb: Make Window.postMessage closer to the spec The main issues are using Structured{Serialize,Deserailize} instead of Structured{Serialize,Deserialize}WithTransfer and the temporary execution context usage for StructuredDeserialize. Allows Discord to load once again, as it uses a postMessage scheduler to render components, including the main App component. The callback checked the (previously) non-existent source attribute of the MessageEvent and returned if it was not the main window. Fixes the Twitch cookie consent banner saying "failed integrity check" for unknown reasons, but presumably related to the source and origin attributes. --- .../Text/expected/HTML/Window-postMessage.txt | 100 +++++++++++++ .../Text/input/HTML/Window-postMessage.html | 137 ++++++++++++++++++ Userland/Libraries/LibWeb/HTML/Window.cpp | 118 +++++++++++++-- Userland/Libraries/LibWeb/HTML/Window.h | 10 +- Userland/Libraries/LibWeb/HTML/Window.idl | 9 +- 5 files changed, 361 insertions(+), 13 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/HTML/Window-postMessage.txt create mode 100644 Tests/LibWeb/Text/input/HTML/Window-postMessage.html diff --git a/Tests/LibWeb/Text/expected/HTML/Window-postMessage.txt b/Tests/LibWeb/Text/expected/HTML/Window-postMessage.txt new file mode 100644 index 0000000000..a3890c7d9f --- /dev/null +++ b/Tests/LibWeb/Text/expected/HTML/Window-postMessage.txt @@ -0,0 +1,100 @@ + originError instanceof DOMException: true +originError.name: SyntaxError +originError.message: Invalid URL for targetOrigin: 'aaaa' +originError.constructor === window.DOMException: true +originParsedBeforeSerializeError instanceof DOMException: true +originParsedBeforeSerializeError.name: SyntaxError +originParsedBeforeSerializeError.message: Invalid URL for targetOrigin: 'aaaa' +originParsedBeforeSerializeError.constructor === window.DOMException: true +serializeError instanceof DOMException: true +serializeError.name: DataCloneError +serializeError.message: Unsupported type +serializeError.constructor === window.DOMException: true +originIframeError instanceof DOMException: false +originIframeError instanceof iframe.contentWindow.DOMException: true +originIframeError.name: SyntaxError +originIframeError.message: Invalid URL for targetOrigin: 'aaaa' +originIframeError.constructor === DOMException: false +originIframeError.constructor === iframe.contentWindow.DOMException: true +originParsedBeforeSerializeIframeError instanceof DOMException: false +originParsedBeforeSerializeIframeError instanceof iframe.contentWindow.DOMException: true +originParsedBeforeSerializeIframeError.name: SyntaxError +originParsedBeforeSerializeIframeError.message: Invalid URL for targetOrigin: 'aaaa' +originParsedBeforeSerializeIframeError.constructor === DOMException: false +originParsedBeforeSerializeIframeError.constructor === iframe.contentWindow.DOMException: true +serializeIframeError instanceof DOMException: false +serializeIframeError instanceof iframe.contentWindow.DOMException: true +serializeIframeError.name: DataCloneError +serializeIframeError.message: Unsupported type +serializeIframeError.constructor === DOMException: false +serializeIframeError.constructor === iframe.contentWindow.DOMException: true +Message 1 data: undefined +Message 1 origin: file:// +Message 1 lastEventId: +Message 1 source: [object Window] +Message 1 source === window: true +Message 1 source === iframe.contentWindow: false +Message 1 source === blobIframe.contentWindow: false +Message 2 data: null +Message 2 origin: file:// +Message 2 lastEventId: +Message 2 source: [object Window] +Message 2 source === window: true +Message 2 source === iframe.contentWindow: false +Message 2 source === blobIframe.contentWindow: false +Message 3 data: true +Message 3 origin: file:// +Message 3 lastEventId: +Message 3 source: [object Window] +Message 3 source === window: true +Message 3 source === iframe.contentWindow: false +Message 3 source === blobIframe.contentWindow: false +Message 4 data: false +Message 4 origin: file:// +Message 4 lastEventId: +Message 4 source: [object Window] +Message 4 source === window: true +Message 4 source === iframe.contentWindow: false +Message 4 source === blobIframe.contentWindow: false +Message 5 data: 123 +Message 5 origin: file:// +Message 5 lastEventId: +Message 5 source: [object Window] +Message 5 source === window: true +Message 5 source === iframe.contentWindow: false +Message 5 source === blobIframe.contentWindow: false +Message 6 data: 123.456 +Message 6 origin: file:// +Message 6 lastEventId: +Message 6 source: [object Window] +Message 6 source === window: true +Message 6 source === iframe.contentWindow: false +Message 6 source === blobIframe.contentWindow: false +Message 7 data: 9007199254740991 +Message 7 origin: file:// +Message 7 lastEventId: +Message 7 source: [object Window] +Message 7 source === window: true +Message 7 source === iframe.contentWindow: false +Message 7 source === blobIframe.contentWindow: false +Message 8 data: This is a string +Message 8 origin: file:// +Message 8 lastEventId: +Message 8 source: [object Window] +Message 8 source === window: true +Message 8 source === iframe.contentWindow: false +Message 8 source === blobIframe.contentWindow: false +Message 9 data: I am from another ~planet~ iframe +Message 9 origin: file:// +Message 9 lastEventId: +Message 9 source: [object Window] +Message 9 source === window: false +Message 9 source === iframe.contentWindow: true +Message 9 source === blobIframe.contentWindow: false +Message 10 data: All done :^) +Message 10 origin: file:// +Message 10 lastEventId: +Message 10 source: [object Window] +Message 10 source === window: false +Message 10 source === iframe.contentWindow: false +Message 10 source === blobIframe.contentWindow: true \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/HTML/Window-postMessage.html b/Tests/LibWeb/Text/input/HTML/Window-postMessage.html new file mode 100644 index 0000000000..fa2ee130f7 --- /dev/null +++ b/Tests/LibWeb/Text/input/HTML/Window-postMessage.html @@ -0,0 +1,137 @@ + + + + + diff --git a/Userland/Libraries/LibWeb/HTML/Window.cpp b/Userland/Libraries/LibWeb/HTML/Window.cpp index d7ba5e6101..b27ac83dea 100644 --- a/Userland/Libraries/LibWeb/HTML/Window.cpp +++ b/Userland/Libraries/LibWeb/HTML/Window.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -64,6 +65,7 @@ #include #include #include +#include #include namespace Web::HTML { @@ -997,17 +999,115 @@ Optional Window::prompt(Optional const& message, Optional Window::window_post_message_steps(JS::Value message, WindowPostMessageOptions const& options) { - // FIXME: This is an ad-hoc hack implementation instead, since we don't currently - // have serialization and deserialization of messages. - queue_global_task(Task::Source::PostedMessage, *this, [this, message] { - MessageEventInit event_init {}; - event_init.data = message; - event_init.origin = ""_string; - dispatch_event(MessageEvent::create(realm(), EventNames::message, event_init)); + // 1. Let targetRealm be targetWindow's realm. + auto& target_realm = this->realm(); + + // 2. Let incumbentSettings be the incumbent settings object. + auto& incumbent_settings = incumbent_settings_object(); + + // 3. Let targetOrigin be options["targetOrigin"]. + Variant target_origin = options.target_origin; + + // 4. If targetOrigin is a single U+002F SOLIDUS character (/), then set targetOrigin to incumbentSettings's origin. + if (options.target_origin == "/"sv) { + target_origin = incumbent_settings.origin(); + } + // 5. Otherwise, if targetOrigin is not a single U+002A ASTERISK character (*), then: + else if (options.target_origin != "*"sv) { + // 1. Let parsedURL be the result of running the URL parser on targetOrigin. + auto parsed_url = URL::parse(options.target_origin); + + // 2. If parsedURL is failure, then throw a "SyntaxError" DOMException. + if (!parsed_url.is_valid()) + return WebIDL::SyntaxError::create(target_realm, MUST(String::formatted("Invalid URL for targetOrigin: '{}'", options.target_origin))); + + // 3. Set targetOrigin to parsedURL's origin. + target_origin = URL::url_origin(parsed_url); + } + + // 6. Let transfer be options["transfer"]. + // FIXME: This is currently unused. + + // 7. Let serializeWithTransferResult be StructuredSerializeWithTransfer(message, transfer). Rethrow any exceptions. + // FIXME: Use StructuredSerializeWithTransfer instead of StructuredSerialize + auto serialize_with_transfer_result = TRY(structured_serialize(target_realm.vm(), message)); + + // 8. Queue a global task on the posted message task source given targetWindow to run the following steps: + queue_global_task(Task::Source::PostedMessage, *this, [this, serialize_with_transfer_result = move(serialize_with_transfer_result), target_origin = move(target_origin), &incumbent_settings, &target_realm]() { + // 1. If the targetOrigin argument is not a single literal U+002A ASTERISK character (*) and targetWindow's + // associated Document's origin is not same origin with targetOrigin, then return. + // NOTE: Due to step 4 and 5 above, the only time it's not '*' is if target_origin contains an Origin. + if (!target_origin.has()) { + auto const& actual_target_origin = target_origin.get(); + if (!document()->origin().is_same_origin(actual_target_origin)) + return; + } + + // 2. Let origin be the serialization of incumbentSettings's origin. + auto origin = incumbent_settings.origin().serialize(); + + // 3. Let source be the WindowProxy object corresponding to incumbentSettings's global object (a Window object). + auto& source = verify_cast(incumbent_settings.realm().global_environment().global_this_value()); + + // 4. Let deserializeRecord be StructuredDeserializeWithTransfer(serializeWithTransferResult, targetRealm). + // FIXME: Use StructuredDeserializeWithTransfer instead of StructuredDeserialize + // FIXME: Don't use a temporary execution context here. + auto& settings_object = Bindings::host_defined_environment_settings_object(target_realm); + auto temporary_execution_context = TemporaryExecutionContext { settings_object }; + auto deserialize_record_or_error = structured_deserialize(vm(), serialize_with_transfer_result, target_realm, Optional {}); + + // If this throws an exception, catch it, fire an event named messageerror at targetWindow, using MessageEvent, + // with the origin attribute initialized to origin and the source attribute initialized to source, and then return. + if (deserialize_record_or_error.is_exception()) { + MessageEventInit message_event_init {}; + message_event_init.origin = MUST(String::from_deprecated_string(origin)); + message_event_init.source = JS::make_handle(source); + + auto message_error_event = MessageEvent::create(target_realm, EventNames::messageerror, message_event_init); + dispatch_event(message_error_event); + return; + } + + // 5. Let messageClone be deserializeRecord.[[Deserialized]]. + // FIXME: Get this from deserializeRecord.[[Deserialized]] once it uses StructuredDeserializeWithTransfer instead of StructuredDeserialize. + auto message_clone = deserialize_record_or_error.release_value(); + + // FIXME: 6. Let newPorts be a new frozen array consisting of all MessagePort objects in deserializeRecord.[[TransferredValues]], + // if any, maintaining their relative order. + + // 7. Fire an event named message at targetWindow, using MessageEvent, with the origin attribute initialized to origin, + // the source attribute initialized to source, the data attribute initialized to messageClone, and the ports attribute + // initialized to newPorts. + // FIXME: Set the ports attribute to newPorts. + MessageEventInit message_event_init {}; + message_event_init.origin = MUST(String::from_deprecated_string(origin)); + message_event_init.source = JS::make_handle(source); + message_event_init.data = message_clone; + + auto message_event = MessageEvent::create(target_realm, EventNames::message, message_event_init); + dispatch_event(message_event); }); + + return {}; +} + +// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-window-postmessage-options +WebIDL::ExceptionOr Window::post_message(JS::Value message, WindowPostMessageOptions const& options) +{ + // The Window interface's postMessage(message, options) method steps are to run the window post message steps given + // this, message, and options. + return window_post_message_steps(message, options); +} + +// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-window-postmessage +WebIDL::ExceptionOr Window::post_message(JS::Value message, String const& target_origin, Vector> const& transfer) +{ + // The Window interface's postMessage(message, targetOrigin, transfer) method steps are to run the window post message + // steps given this, message, and «[ "targetOrigin" → targetOrigin, "transfer" → transfer ]». + return window_post_message_steps(message, WindowPostMessageOptions { { .transfer = transfer }, target_origin }); } // https://dom.spec.whatwg.org/#dom-window-event diff --git a/Userland/Libraries/LibWeb/HTML/Window.h b/Userland/Libraries/LibWeb/HTML/Window.h index 9eeaa87335..1305165f37 100644 --- a/Userland/Libraries/LibWeb/HTML/Window.h +++ b/Userland/Libraries/LibWeb/HTML/Window.h @@ -38,6 +38,11 @@ struct ScrollToOptions : public ScrollOptions { Optional top; }; +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#windowpostmessageoptions +struct WindowPostMessageOptions : public StructuredSerializeOptions { + String target_origin { "/"_string }; +}; + class Window final : public DOM::EventTarget , public GlobalEventHandlers @@ -146,7 +151,8 @@ public: bool confirm(Optional const& message); Optional prompt(Optional const& message, Optional const& default_); - void post_message(JS::Value message, String const&); + WebIDL::ExceptionOr post_message(JS::Value message, String const&, Vector> const&); + WebIDL::ExceptionOr post_message(JS::Value message, WindowPostMessageOptions const&); Variant, JS::Value> event() const; @@ -215,6 +221,8 @@ private: }; NamedObjects named_objects(StringView name); + WebIDL::ExceptionOr window_post_message_steps(JS::Value, WindowPostMessageOptions const&); + // https://html.spec.whatwg.org/multipage/window-object.html#concept-document-window JS::GCPtr m_associated_document; diff --git a/Userland/Libraries/LibWeb/HTML/Window.idl b/Userland/Libraries/LibWeb/HTML/Window.idl index ca19c0697a..eb5ccdd295 100644 --- a/Userland/Libraries/LibWeb/HTML/Window.idl +++ b/Userland/Libraries/LibWeb/HTML/Window.idl @@ -52,9 +52,8 @@ interface Window : EventTarget { boolean confirm(optional DOMString message = ""); DOMString? prompt(optional DOMString message = "", optional DOMString default = ""); - undefined postMessage(any message, USVString targetOrigin); - // FIXME: undefined postMessage(any message, USVString targetOrigin, optional sequence transfer = []); - // FIXME: undefined postMessage(any message, optional WindowPostMessageOptions options = {}); + undefined postMessage(any message, USVString targetOrigin, optional sequence transfer = []); + undefined postMessage(any message, optional WindowPostMessageOptions options = {}); // https://dom.spec.whatwg.org/#interface-window-extensions [Replaceable] readonly attribute (Event or undefined) event; // legacy @@ -122,3 +121,7 @@ dictionary ScrollToOptions : ScrollOptions { unrestricted double left; unrestricted double top; }; + +dictionary WindowPostMessageOptions : StructuredSerializeOptions { + USVString targetOrigin = "/"; +};