From 9e2ec2dc5c23bf770b63ee38771803117359c3ee Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 13 Jun 2026 16:05:47 +0200 Subject: [PATCH] LibWeb: Add session history serialization support Add structured helpers for the history data mirrored by LibWebView. Cover POST resources, history state, navigation API state, scroll positions, and entry metadata. Keep this below the UI model so browser-side history code can move data through typed objects instead of ad-hoc strings. --- Libraries/LibWeb/CMakeLists.txt | 1 + Libraries/LibWeb/HTML/POSTResource.cpp | 49 +++++++ Libraries/LibWeb/HTML/POSTResource.h | 25 +++- Libraries/LibWeb/HTML/SessionHistoryEntry.cpp | 126 ++++++++++++++++++ Libraries/LibWeb/HTML/SessionHistoryEntry.h | 92 ++++++++++++- Libraries/LibWeb/PixelUnits.cpp | 16 +++ Libraries/LibWeb/PixelUnits.h | 5 + 7 files changed, 311 insertions(+), 3 deletions(-) create mode 100644 Libraries/LibWeb/HTML/POSTResource.cpp diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index d38c2f793f..ecc57417e5 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -656,6 +656,7 @@ set(SOURCES HTML/Plugin.cpp HTML/PluginArray.cpp HTML/PolicyContainers.cpp + HTML/POSTResource.cpp HTML/PopoverTargetAttributes.cpp HTML/PopStateEvent.cpp HTML/PotentialCORSRequest.cpp diff --git a/Libraries/LibWeb/HTML/POSTResource.cpp b/Libraries/LibWeb/HTML/POSTResource.cpp new file mode 100644 index 0000000000..0e6bee13c6 --- /dev/null +++ b/Libraries/LibWeb/HTML/POSTResource.cpp @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace IPC { + +template<> +ErrorOr encode(Encoder& encoder, Web::HTML::POSTResource::Directive const& directive) +{ + TRY(encoder.encode(directive.type)); + TRY(encoder.encode(directive.value)); + return {}; +} + +template<> +ErrorOr decode(Decoder& decoder) +{ + return Web::HTML::POSTResource::Directive { + .type = TRY(decoder.decode()), + .value = TRY(decoder.decode()), + }; +} + +template<> +ErrorOr encode(Encoder& encoder, Web::HTML::POSTResource const& post_resource) +{ + TRY(encoder.encode(post_resource.request_body)); + TRY(encoder.encode(post_resource.request_content_type)); + TRY(encoder.encode(post_resource.request_content_type_directives)); + return {}; +} + +template<> +ErrorOr decode(Decoder& decoder) +{ + return Web::HTML::POSTResource { + .request_body = TRY(decoder.decode>()), + .request_content_type = TRY(decoder.decode()), + .request_content_type_directives = TRY(decoder.decode>()), + }; +} + +} diff --git a/Libraries/LibWeb/HTML/POSTResource.h b/Libraries/LibWeb/HTML/POSTResource.h index 059c8b67f6..5ea4b1cd43 100644 --- a/Libraries/LibWeb/HTML/POSTResource.h +++ b/Libraries/LibWeb/HTML/POSTResource.h @@ -8,8 +8,9 @@ #include #include -#include #include +#include +#include namespace Web::HTML { @@ -31,10 +32,30 @@ struct POSTResource { RequestContentType request_content_type {}; struct Directive { - StringView type; + String type; String value; + + bool operator==(Directive const&) const = default; }; Vector request_content_type_directives {}; + + bool operator==(POSTResource const&) const = default; }; } + +namespace IPC { + +template<> +WEB_API ErrorOr encode(Encoder&, Web::HTML::POSTResource::Directive const&); + +template<> +WEB_API ErrorOr decode(Decoder&); + +template<> +WEB_API ErrorOr encode(Encoder&, Web::HTML::POSTResource const&); + +template<> +WEB_API ErrorOr decode(Decoder&); + +} diff --git a/Libraries/LibWeb/HTML/SessionHistoryEntry.cpp b/Libraries/LibWeb/HTML/SessionHistoryEntry.cpp index 4698cdbd82..dc37d608dd 100644 --- a/Libraries/LibWeb/HTML/SessionHistoryEntry.cpp +++ b/Libraries/LibWeb/HTML/SessionHistoryEntry.cpp @@ -4,6 +4,8 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include +#include #include #include #include @@ -31,3 +33,127 @@ SessionHistoryEntry::SessionHistoryEntry() } } + +template<> +ErrorOr IPC::encode(Encoder& encoder, Web::HTML::SessionHistoryEntryDescriptor const& entry) +{ + TRY(encoder.encode(entry.step)); + TRY(encoder.encode(entry.url)); + TRY(encoder.encode(entry.document_state)); + TRY(encoder.encode(entry.classic_history_api_state)); + TRY(encoder.encode(entry.navigation_api_state)); + TRY(encoder.encode(entry.navigation_api_key)); + TRY(encoder.encode(entry.navigation_api_id)); + TRY(encoder.encode(entry.scroll_restoration_mode)); + TRY(encoder.encode(entry.scroll_position_data)); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto step = TRY(decoder.decode()); + auto url = TRY(decoder.decode()); + auto document_state = TRY(decoder.decode()); + auto classic_history_api_state = TRY(decoder.decode()); + auto navigation_api_state = TRY(decoder.decode()); + auto navigation_api_key = TRY(decoder.decode()); + auto navigation_api_id = TRY(decoder.decode()); + auto scroll_restoration_mode = TRY(decoder.decode()); + auto scroll_position_data = TRY(decoder.decode()); + + return Web::HTML::SessionHistoryEntryDescriptor { + .step = step, + .url = move(url), + .document_state = move(document_state), + .classic_history_api_state = move(classic_history_api_state), + .navigation_api_state = move(navigation_api_state), + .navigation_api_key = move(navigation_api_key), + .navigation_api_id = move(navigation_api_id), + .scroll_restoration_mode = scroll_restoration_mode, + .scroll_position_data = move(scroll_position_data), + }; +} + +template<> +ErrorOr IPC::encode(Encoder& encoder, Web::HTML::SessionHistoryEntryScrollPositionData const& scroll_position_data) +{ + TRY(encoder.encode(scroll_position_data.viewport_scroll_position)); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto viewport_scroll_position = TRY(decoder.decode>()); + return Web::HTML::SessionHistoryEntryScrollPositionData { + .viewport_scroll_position = move(viewport_scroll_position), + }; +} + +template<> +ErrorOr IPC::encode(Encoder& encoder, Web::HTML::SessionHistoryDocumentStateDescriptor const& document_state) +{ + TRY(encoder.encode(document_state.id)); + TRY(encoder.encode(document_state.history_policy_container)); + TRY(encoder.encode(document_state.request_referrer)); + TRY(encoder.encode(document_state.request_referrer_policy)); + TRY(encoder.encode(document_state.initiator_origin)); + TRY(encoder.encode(document_state.origin)); + TRY(encoder.encode(document_state.about_base_url)); + TRY(encoder.encode(document_state.resource)); + TRY(encoder.encode(document_state.reload_pending)); + TRY(encoder.encode(document_state.ever_populated)); + TRY(encoder.encode(document_state.navigable_target_name)); + TRY(encoder.encode(document_state.nested_histories)); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto id = TRY(decoder.decode()); + auto history_policy_container = TRY(decoder.decode>()); + auto request_referrer = TRY(decoder.decode()); + auto request_referrer_policy = TRY(decoder.decode()); + auto initiator_origin = TRY(decoder.decode>()); + auto origin = TRY(decoder.decode>()); + auto about_base_url = TRY(decoder.decode>()); + auto resource = TRY(decoder.decode>()); + auto reload_pending = TRY(decoder.decode()); + auto ever_populated = TRY(decoder.decode()); + auto navigable_target_name = TRY(decoder.decode()); + auto nested_histories = TRY(decoder.decode>()); + + return Web::HTML::SessionHistoryDocumentStateDescriptor { + .id = id, + .history_policy_container = move(history_policy_container), + .request_referrer = move(request_referrer), + .request_referrer_policy = request_referrer_policy, + .initiator_origin = move(initiator_origin), + .origin = move(origin), + .about_base_url = move(about_base_url), + .resource = move(resource), + .reload_pending = reload_pending, + .ever_populated = ever_populated, + .navigable_target_name = move(navigable_target_name), + .nested_histories = move(nested_histories), + }; +} + +template<> +ErrorOr IPC::encode(Encoder& encoder, Web::HTML::SessionHistoryNestedHistoryDescriptor const& nested_history) +{ + TRY(encoder.encode(nested_history.id)); + TRY(encoder.encode(nested_history.entries)); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto id = TRY(decoder.decode()); + auto entries = TRY(decoder.decode>()); + + return Web::HTML::SessionHistoryNestedHistoryDescriptor { move(id), move(entries) }; +} diff --git a/Libraries/LibWeb/HTML/SessionHistoryEntry.h b/Libraries/LibWeb/HTML/SessionHistoryEntry.h index 6ed9b5f4bd..85ed387531 100644 --- a/Libraries/LibWeb/HTML/SessionHistoryEntry.h +++ b/Libraries/LibWeb/HTML/SessionHistoryEntry.h @@ -8,9 +8,16 @@ #include #include +#include +#include +#include #include +#include #include +#include #include +#include +#include namespace Web::HTML { @@ -25,6 +32,57 @@ enum class ScrollRestorationMode { Manual, }; +struct SessionHistoryNestedHistoryDescriptor; + +// IPC-friendly descriptors for the parts of session history entries and document states that can survive +// WebContent process swaps. +// +// https://html.spec.whatwg.org/multipage/browsing-the-web.html#session-history-entry +// https://html.spec.whatwg.org/multipage/browsing-the-web.html#document-state +struct SessionHistoryDocumentStateDescriptor { + // AD-HOC: The spec models shared document state by object identity. The UI-process mirror uses a stable + // descriptor ID so entries that share a document state can be reconstructed after IPC. + u64 id { 0 }; + Variant history_policy_container { DocumentState::Client::Tag }; + Fetch::Infrastructure::Request::ReferrerType request_referrer { Fetch::Infrastructure::Request::Referrer::Client }; + ReferrerPolicy::ReferrerPolicy request_referrer_policy { ReferrerPolicy::DEFAULT_REFERRER_POLICY }; + Optional initiator_origin; + Optional origin; + Optional about_base_url; + Variant resource; + bool reload_pending { false }; + bool ever_populated { false }; + String navigable_target_name; + Vector nested_histories; +}; + +// https://html.spec.whatwg.org/multipage/browsing-the-web.html#she-scroll-position +struct SessionHistoryEntryScrollPositionData { + // FIXME: Track all restorable scrollable regions. Currently only the viewport is persisted. + Optional viewport_scroll_position; + + bool operator==(SessionHistoryEntryScrollPositionData const&) const = default; +}; + +// https://html.spec.whatwg.org/multipage/browsing-the-web.html#session-history-entry +struct SessionHistoryEntryDescriptor { + i32 step { 0 }; + URL::URL url; + SessionHistoryDocumentStateDescriptor document_state; + SerializationRecord classic_history_api_state; + SerializationRecord navigation_api_state; + String navigation_api_key; + String navigation_api_id; + ScrollRestorationMode scroll_restoration_mode { ScrollRestorationMode::Auto }; + SessionHistoryEntryScrollPositionData scroll_position_data; +}; + +// https://html.spec.whatwg.org/multipage/browsing-the-web.html#nested-history +struct SessionHistoryNestedHistoryDescriptor { + String id; + Vector entries; +}; + // https://html.spec.whatwg.org/multipage/history.html#session-history-entry class SessionHistoryEntry final : public RefCounted { public: @@ -61,6 +119,9 @@ public: [[nodiscard]] ScrollRestorationMode scroll_restoration_mode() const { return m_scroll_restoration_mode; } void set_scroll_restoration_mode(ScrollRestorationMode scroll_restoration_mode) { m_scroll_restoration_mode = scroll_restoration_mode; } + [[nodiscard]] SessionHistoryEntryScrollPositionData const& scroll_position_data() const { return m_scroll_position_data; } + void set_scroll_position_data(SessionHistoryEntryScrollPositionData scroll_position_data) { m_scroll_position_data = move(scroll_position_data); } + private: // https://html.spec.whatwg.org/multipage/browsing-the-web.html#she-step // step, a non-negative integer or "pending", initially "pending". @@ -94,7 +155,8 @@ private: ScrollRestorationMode m_scroll_restoration_mode { ScrollRestorationMode::Auto }; // https://html.spec.whatwg.org/multipage/browsing-the-web.html#she-scroll-position - // FIXME: scroll position data, which is scroll position data for the document's restorable scrollable regions + // scroll position data, which is scroll position data for the document's restorable scrollable regions + SessionHistoryEntryScrollPositionData m_scroll_position_data; // https://html.spec.whatwg.org/multipage/browsing-the-web.html#she-other // FIXME: persisted user state, which is implementation-defined, initially null @@ -102,3 +164,31 @@ private: }; } + +namespace IPC { + +template<> +WEB_API ErrorOr encode(Encoder&, Web::HTML::SessionHistoryEntryDescriptor const&); + +template<> +WEB_API ErrorOr decode(Decoder&); + +template<> +WEB_API ErrorOr encode(Encoder&, Web::HTML::SessionHistoryEntryScrollPositionData const&); + +template<> +WEB_API ErrorOr decode(Decoder&); + +template<> +WEB_API ErrorOr encode(Encoder&, Web::HTML::SessionHistoryDocumentStateDescriptor const&); + +template<> +WEB_API ErrorOr decode(Decoder&); + +template<> +WEB_API ErrorOr encode(Encoder&, Web::HTML::SessionHistoryNestedHistoryDescriptor const&); + +template<> +WEB_API ErrorOr decode(Decoder&); + +} diff --git a/Libraries/LibWeb/PixelUnits.cpp b/Libraries/LibWeb/PixelUnits.cpp index 77d25c22d5..3893a776b6 100644 --- a/Libraries/LibWeb/PixelUnits.cpp +++ b/Libraries/LibWeb/PixelUnits.cpp @@ -15,6 +15,22 @@ namespace Web { namespace IPC { +template<> +ErrorOr encode(Encoder& encoder, Web::CSSPixelPoint const& value) +{ + TRY(encoder.encode(value.x().raw_value())); + TRY(encoder.encode(value.y().raw_value())); + return {}; +} + +template<> +ErrorOr decode(Decoder& decoder) +{ + auto x = TRY(decoder.decode()); + auto y = TRY(decoder.decode()); + return Web::CSSPixelPoint { Web::CSSPixels::from_raw(x), Web::CSSPixels::from_raw(y) }; +} + template<> ErrorOr encode(Encoder& encoder, Web::DevicePixelPoint const& value) { diff --git a/Libraries/LibWeb/PixelUnits.h b/Libraries/LibWeb/PixelUnits.h index dee398288e..d0e90e15c4 100644 --- a/Libraries/LibWeb/PixelUnits.h +++ b/Libraries/LibWeb/PixelUnits.h @@ -536,6 +536,11 @@ struct Formatter : Formatter { namespace IPC { +template<> +WEB_API ErrorOr encode(Encoder& encoder, Web::CSSPixelPoint const& value); +template<> +WEB_API ErrorOr decode(Decoder& decoder); + template<> WEB_API ErrorOr encode(Encoder& encoder, Web::DevicePixelPoint const& value); template<>