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.
This commit is contained in:
Andreas Kling 2026-06-13 16:05:47 +02:00 committed by Andreas Kling
parent 6c13a3f37d
commit 9e2ec2dc5c
7 changed files with 311 additions and 3 deletions

View file

@ -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

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibWeb/HTML/POSTResource.h>
namespace IPC {
template<>
ErrorOr<void> encode(Encoder& encoder, Web::HTML::POSTResource::Directive const& directive)
{
TRY(encoder.encode(directive.type));
TRY(encoder.encode(directive.value));
return {};
}
template<>
ErrorOr<Web::HTML::POSTResource::Directive> decode(Decoder& decoder)
{
return Web::HTML::POSTResource::Directive {
.type = TRY(decoder.decode<String>()),
.value = TRY(decoder.decode<String>()),
};
}
template<>
ErrorOr<void> 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<Web::HTML::POSTResource> decode(Decoder& decoder)
{
return Web::HTML::POSTResource {
.request_body = TRY(decoder.decode<Optional<ByteBuffer>>()),
.request_content_type = TRY(decoder.decode<Web::HTML::POSTResource::RequestContentType>()),
.request_content_type_directives = TRY(decoder.decode<Vector<Web::HTML::POSTResource::Directive>>()),
};
}
}

View file

@ -8,8 +8,9 @@
#include <AK/ByteBuffer.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Vector.h>
#include <LibIPC/Forward.h>
#include <LibWeb/Forward.h>
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<Directive> request_content_type_directives {};
bool operator==(POSTResource const&) const = default;
};
}
namespace IPC {
template<>
WEB_API ErrorOr<void> encode(Encoder&, Web::HTML::POSTResource::Directive const&);
template<>
WEB_API ErrorOr<Web::HTML::POSTResource::Directive> decode(Decoder&);
template<>
WEB_API ErrorOr<void> encode(Encoder&, Web::HTML::POSTResource const&);
template<>
WEB_API ErrorOr<Web::HTML::POSTResource> decode(Decoder&);
}

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibJS/Runtime/VM.h>
#include <LibWeb/Crypto/Crypto.h>
#include <LibWeb/HTML/DocumentState.h>
@ -31,3 +33,127 @@ SessionHistoryEntry::SessionHistoryEntry()
}
}
template<>
ErrorOr<void> 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<Web::HTML::SessionHistoryEntryDescriptor> IPC::decode(Decoder& decoder)
{
auto step = TRY(decoder.decode<i32>());
auto url = TRY(decoder.decode<URL::URL>());
auto document_state = TRY(decoder.decode<Web::HTML::SessionHistoryDocumentStateDescriptor>());
auto classic_history_api_state = TRY(decoder.decode<Web::HTML::SerializationRecord>());
auto navigation_api_state = TRY(decoder.decode<Web::HTML::SerializationRecord>());
auto navigation_api_key = TRY(decoder.decode<String>());
auto navigation_api_id = TRY(decoder.decode<String>());
auto scroll_restoration_mode = TRY(decoder.decode<Web::HTML::ScrollRestorationMode>());
auto scroll_position_data = TRY(decoder.decode<Web::HTML::SessionHistoryEntryScrollPositionData>());
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<void> IPC::encode(Encoder& encoder, Web::HTML::SessionHistoryEntryScrollPositionData const& scroll_position_data)
{
TRY(encoder.encode(scroll_position_data.viewport_scroll_position));
return {};
}
template<>
ErrorOr<Web::HTML::SessionHistoryEntryScrollPositionData> IPC::decode(Decoder& decoder)
{
auto viewport_scroll_position = TRY(decoder.decode<Optional<Web::CSSPixelPoint>>());
return Web::HTML::SessionHistoryEntryScrollPositionData {
.viewport_scroll_position = move(viewport_scroll_position),
};
}
template<>
ErrorOr<void> 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<Web::HTML::SessionHistoryDocumentStateDescriptor> IPC::decode(Decoder& decoder)
{
auto id = TRY(decoder.decode<u64>());
auto history_policy_container = TRY(decoder.decode<Variant<Web::HTML::SerializedPolicyContainer, Web::HTML::DocumentState::Client>>());
auto request_referrer = TRY(decoder.decode<Web::Fetch::Infrastructure::Request::ReferrerType>());
auto request_referrer_policy = TRY(decoder.decode<Web::ReferrerPolicy::ReferrerPolicy>());
auto initiator_origin = TRY(decoder.decode<Optional<URL::Origin>>());
auto origin = TRY(decoder.decode<Optional<URL::Origin>>());
auto about_base_url = TRY(decoder.decode<Optional<URL::URL>>());
auto resource = TRY(decoder.decode<Variant<Empty, String, Web::HTML::POSTResource>>());
auto reload_pending = TRY(decoder.decode<bool>());
auto ever_populated = TRY(decoder.decode<bool>());
auto navigable_target_name = TRY(decoder.decode<String>());
auto nested_histories = TRY(decoder.decode<Vector<Web::HTML::SessionHistoryNestedHistoryDescriptor>>());
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<void> 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<Web::HTML::SessionHistoryNestedHistoryDescriptor> IPC::decode(Decoder& decoder)
{
auto id = TRY(decoder.decode<String>());
auto entries = TRY(decoder.decode<Vector<Web::HTML::SessionHistoryEntryDescriptor>>());
return Web::HTML::SessionHistoryNestedHistoryDescriptor { move(id), move(entries) };
}

View file

@ -8,9 +8,16 @@
#include <AK/RefCounted.h>
#include <AK/RefPtr.h>
#include <AK/String.h>
#include <AK/Types.h>
#include <AK/Vector.h>
#include <LibURL/URL.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/DocumentState.h>
#include <LibWeb/HTML/StructuredSerializeTypes.h>
#include <LibWeb/PixelUnits.h>
#include <LibWeb/ReferrerPolicy/ReferrerPolicy.h>
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<SerializedPolicyContainer, DocumentState::Client> 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<URL::Origin> initiator_origin;
Optional<URL::Origin> origin;
Optional<URL::URL> about_base_url;
Variant<Empty, String, POSTResource> resource;
bool reload_pending { false };
bool ever_populated { false };
String navigable_target_name;
Vector<SessionHistoryNestedHistoryDescriptor> 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<CSSPixelPoint> 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<SessionHistoryEntryDescriptor> entries;
};
// https://html.spec.whatwg.org/multipage/history.html#session-history-entry
class SessionHistoryEntry final : public RefCounted<SessionHistoryEntry> {
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<void> encode(Encoder&, Web::HTML::SessionHistoryEntryDescriptor const&);
template<>
WEB_API ErrorOr<Web::HTML::SessionHistoryEntryDescriptor> decode(Decoder&);
template<>
WEB_API ErrorOr<void> encode(Encoder&, Web::HTML::SessionHistoryEntryScrollPositionData const&);
template<>
WEB_API ErrorOr<Web::HTML::SessionHistoryEntryScrollPositionData> decode(Decoder&);
template<>
WEB_API ErrorOr<void> encode(Encoder&, Web::HTML::SessionHistoryDocumentStateDescriptor const&);
template<>
WEB_API ErrorOr<Web::HTML::SessionHistoryDocumentStateDescriptor> decode(Decoder&);
template<>
WEB_API ErrorOr<void> encode(Encoder&, Web::HTML::SessionHistoryNestedHistoryDescriptor const&);
template<>
WEB_API ErrorOr<Web::HTML::SessionHistoryNestedHistoryDescriptor> decode(Decoder&);
}

View file

@ -15,6 +15,22 @@ namespace Web {
namespace IPC {
template<>
ErrorOr<void> encode(Encoder& encoder, Web::CSSPixelPoint const& value)
{
TRY(encoder.encode(value.x().raw_value()));
TRY(encoder.encode(value.y().raw_value()));
return {};
}
template<>
ErrorOr<Web::CSSPixelPoint> decode(Decoder& decoder)
{
auto x = TRY(decoder.decode<i32>());
auto y = TRY(decoder.decode<i32>());
return Web::CSSPixelPoint { Web::CSSPixels::from_raw(x), Web::CSSPixels::from_raw(y) };
}
template<>
ErrorOr<void> encode(Encoder& encoder, Web::DevicePixelPoint const& value)
{

View file

@ -536,6 +536,11 @@ struct Formatter<Web::DevicePixels> : Formatter<Web::DevicePixels::Type> {
namespace IPC {
template<>
WEB_API ErrorOr<void> encode(Encoder& encoder, Web::CSSPixelPoint const& value);
template<>
WEB_API ErrorOr<Web::CSSPixelPoint> decode(Decoder& decoder);
template<>
WEB_API ErrorOr<void> encode(Encoder& encoder, Web::DevicePixelPoint const& value);
template<>