ladybird/Libraries/LibWeb/HTML/SessionHistoryEntry.cpp
Andreas Kling b06955277a LibWeb: Stabilize same-document history mirrors
Same-document navigations now commit synchronously in WebContent, while
the UI process mirror learns about them over asynchronous IPC. A stale
UI seed could be accepted back into a live non-initial document and
overwrite its latest entry, making queued traversals target unreachable
entries.

Share descriptor comparison helpers between LibWeb and LibWebView.
Reject stale top-level seeds against the active document latest entry,
and let the UI process adopt WebContent current snapshots when a seed is
rejected. Test-only session history dumps now first send WebContent
current state synchronously, so dumps observe the converged state.

Allow post-load UI seeds to carry UI-owned nested histories that the
freshly loaded top-level document has not reconstructed yet. Add unit
coverage for matching those seeds while still checking top-level state.
2026-06-16 00:00:38 +02:00

397 lines
18 KiB
C++

/*
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
*
* 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>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/HTML/StructuredSerialize.h>
namespace Web::HTML {
NonnullRefPtr<SessionHistoryEntry> SessionHistoryEntry::create()
{
return adopt_ref(*new SessionHistoryEntry());
}
SessionHistoryEntry::~SessionHistoryEntry() = default;
RefPtr<DocumentState> SessionHistoryEntry::document_state() const { return m_document_state; }
void SessionHistoryEntry::set_document_state(RefPtr<DocumentState> document_state) { m_document_state = move(document_state); }
SessionHistoryEntry::SessionHistoryEntry()
: m_classic_history_api_state(MUST(structured_serialize_for_storage(JS::VM::the(), JS::js_null())))
, m_navigation_api_state(MUST(structured_serialize_for_storage(JS::VM::the(), JS::js_undefined())))
, m_navigation_api_key(Crypto::generate_random_uuid())
, m_navigation_api_id(Crypto::generate_random_uuid())
{
}
static bool session_history_nested_history_descriptors_match(Vector<SessionHistoryNestedHistoryDescriptor> const& a, Vector<SessionHistoryNestedHistoryDescriptor> const& b);
static bool serialized_directives_match(Vector<ContentSecurityPolicy::Directives::SerializedDirective> const& a, Vector<ContentSecurityPolicy::Directives::SerializedDirective> const& b)
{
if (a.size() != b.size())
return false;
for (size_t i = 0; i < a.size(); ++i) {
if (a[i].name != b[i].name || a[i].value != b[i].value)
return false;
}
return true;
}
static bool serialized_policies_match(Vector<ContentSecurityPolicy::SerializedPolicy> const& a, Vector<ContentSecurityPolicy::SerializedPolicy> const& b)
{
if (a.size() != b.size())
return false;
for (size_t i = 0; i < a.size(); ++i) {
if (!serialized_directives_match(a[i].directives, b[i].directives)
|| a[i].disposition != b[i].disposition
|| a[i].source != b[i].source
|| a[i].self_origin != b[i].self_origin
|| a[i].pre_parsed_policy_string != b[i].pre_parsed_policy_string)
return false;
}
return true;
}
static bool embedder_policies_match(EmbedderPolicy const& a, EmbedderPolicy const& b)
{
return a.value == b.value
&& a.report_only_value == b.report_only_value
&& a.reporting_endpoint == b.reporting_endpoint
&& a.report_only_reporting_endpoint == b.report_only_reporting_endpoint;
}
static bool serialized_policy_containers_match(SerializedPolicyContainer const& a, SerializedPolicyContainer const& b)
{
return serialized_policies_match(a.csp_list, b.csp_list)
&& embedder_policies_match(a.embedder_policy, b.embedder_policy)
&& a.referrer_policy == b.referrer_policy;
}
static bool history_policy_containers_match(Variant<SerializedPolicyContainer, DocumentState::Client> const& a, Variant<SerializedPolicyContainer, DocumentState::Client> const& b)
{
if (auto const* a_serialized_policy_container = a.get_pointer<SerializedPolicyContainer>()) {
auto const* b_serialized_policy_container = b.get_pointer<SerializedPolicyContainer>();
return b_serialized_policy_container && serialized_policy_containers_match(*a_serialized_policy_container, *b_serialized_policy_container);
}
return a.has<DocumentState::Client>() && b.has<DocumentState::Client>();
}
static bool session_history_document_state_descriptors_match(SessionHistoryDocumentStateDescriptor const& a, SessionHistoryDocumentStateDescriptor const& b)
{
return a.id == b.id
&& history_policy_containers_match(a.history_policy_container, b.history_policy_container)
&& a.request_referrer == b.request_referrer
&& a.request_referrer_policy == b.request_referrer_policy
&& a.initiator_origin == b.initiator_origin
&& a.origin == b.origin
&& a.about_base_url == b.about_base_url
&& a.resource == b.resource
&& a.reload_pending == b.reload_pending
&& a.ever_populated == b.ever_populated
&& a.navigable_target_name == b.navigable_target_name
&& session_history_nested_history_descriptors_match(a.nested_histories, b.nested_histories);
}
static bool session_history_nested_history_descriptors_match_ignoring_document_state_ids(Vector<SessionHistoryNestedHistoryDescriptor> const&, Vector<SessionHistoryNestedHistoryDescriptor> const&);
static bool session_history_document_state_descriptors_match_ignoring_id(SessionHistoryDocumentStateDescriptor const& a, SessionHistoryDocumentStateDescriptor const& b, MatchNestedHistories match_nested_histories)
{
if (!(history_policy_containers_match(a.history_policy_container, b.history_policy_container)
&& a.request_referrer == b.request_referrer
&& a.request_referrer_policy == b.request_referrer_policy
&& a.initiator_origin == b.initiator_origin
&& a.origin == b.origin
&& a.about_base_url == b.about_base_url
&& a.resource == b.resource
&& a.reload_pending == b.reload_pending
&& a.ever_populated == b.ever_populated
&& a.navigable_target_name == b.navigable_target_name))
return false;
if (match_nested_histories == MatchNestedHistories::No)
return true;
return session_history_nested_history_descriptors_match_ignoring_document_state_ids(a.nested_histories, b.nested_histories);
}
bool session_history_entry_descriptors_match(SessionHistoryEntryDescriptor const& a, SessionHistoryEntryDescriptor const& b)
{
return a.step == b.step
&& a.url == b.url
&& session_history_document_state_descriptors_match(a.document_state, b.document_state)
&& a.classic_history_api_state == b.classic_history_api_state
&& a.navigation_api_state == b.navigation_api_state
&& a.navigation_api_key == b.navigation_api_key
&& a.navigation_api_id == b.navigation_api_id
&& a.scroll_restoration_mode == b.scroll_restoration_mode
&& a.scroll_position_data == b.scroll_position_data;
}
bool session_history_entry_descriptors_match_ignoring_document_state_id(SessionHistoryEntryDescriptor const& a, SessionHistoryEntryDescriptor const& b, MatchNestedHistories match_nested_histories)
{
if (a.step != b.step)
return false;
if (a.url != b.url)
return false;
if (a.document_state.id != 0 && !session_history_document_state_descriptors_match_ignoring_id(a.document_state, b.document_state, match_nested_histories))
return false;
if (a.classic_history_api_state != b.classic_history_api_state)
return false;
if (a.navigation_api_state != b.navigation_api_state || a.navigation_api_key != b.navigation_api_key || a.navigation_api_id != b.navigation_api_id)
return false;
if (a.scroll_restoration_mode != b.scroll_restoration_mode)
return false;
if (a.document_state.id != 0 && a.scroll_position_data != b.scroll_position_data)
return false;
return true;
}
static bool session_history_entry_descriptors_match(Vector<SessionHistoryEntryDescriptor> const& a, Vector<SessionHistoryEntryDescriptor> const& b)
{
if (a.size() != b.size())
return false;
for (size_t i = 0; i < a.size(); ++i) {
if (session_history_entry_descriptors_match(a[i], b[i]))
continue;
return false;
}
return true;
}
static bool session_history_entry_descriptors_match_ignoring_document_state_ids(Vector<SessionHistoryEntryDescriptor> const& a, Vector<SessionHistoryEntryDescriptor> const& b)
{
if (a.size() != b.size())
return false;
for (size_t i = 0; i < a.size(); ++i) {
if (session_history_entry_descriptors_match_ignoring_document_state_id(a[i], b[i]))
continue;
return false;
}
return true;
}
static bool session_history_nested_history_descriptors_match(Vector<SessionHistoryNestedHistoryDescriptor> const& a, Vector<SessionHistoryNestedHistoryDescriptor> const& b)
{
if (a.size() != b.size())
return false;
for (size_t i = 0; i < a.size(); ++i) {
if (a[i].id == b[i].id && session_history_entry_descriptors_match(a[i].entries, b[i].entries))
continue;
return false;
}
return true;
}
static bool session_history_nested_history_descriptors_match_ignoring_document_state_ids(Vector<SessionHistoryNestedHistoryDescriptor> const& a, Vector<SessionHistoryNestedHistoryDescriptor> const& b)
{
if (a.size() != b.size())
return false;
for (size_t i = 0; i < a.size(); ++i) {
if (a[i].id == b[i].id && session_history_entry_descriptors_match_ignoring_document_state_ids(a[i].entries, b[i].entries))
continue;
return false;
}
return true;
}
static bool session_history_nested_history_descriptors_match_document_state_nested_histories(Vector<SessionHistoryNestedHistoryDescriptor> const& descriptors, Vector<DocumentState::NestedHistory> const& nested_histories)
{
if (descriptors.size() != nested_histories.size())
return false;
for (size_t i = 0; i < descriptors.size(); ++i) {
auto const& descriptor = descriptors[i];
auto const& nested_history = nested_histories[i];
if (descriptor.id != nested_history.id || descriptor.entries.size() != nested_history.entries.size())
return false;
for (size_t j = 0; j < descriptor.entries.size(); ++j) {
if (!session_history_entry_matches_descriptor_ignoring_document_state_id(*nested_history.entries[j], descriptor.entries[j]))
return false;
}
}
return true;
}
static bool session_history_document_state_descriptor_matches_document_state_ignoring_id(SessionHistoryDocumentStateDescriptor const& descriptor, DocumentState const& document_state, MatchNestedHistories match_nested_histories)
{
if (!(history_policy_containers_match(descriptor.history_policy_container, document_state.history_policy_container())
&& descriptor.request_referrer == document_state.request_referrer()
&& descriptor.request_referrer_policy == document_state.request_referrer_policy()
&& descriptor.initiator_origin == document_state.initiator_origin()
&& descriptor.origin == document_state.origin()
&& descriptor.about_base_url == document_state.about_base_url()
&& descriptor.resource == document_state.resource()
&& descriptor.reload_pending == document_state.reload_pending()
&& descriptor.ever_populated == document_state.ever_populated()
&& descriptor.navigable_target_name == document_state.navigable_target_name()))
return false;
if (match_nested_histories == MatchNestedHistories::No)
return true;
return session_history_nested_history_descriptors_match_document_state_nested_histories(descriptor.nested_histories, document_state.nested_histories());
}
bool session_history_entry_matches_descriptor_ignoring_document_state_id(SessionHistoryEntry const& entry, SessionHistoryEntryDescriptor const& descriptor, MatchNestedHistories match_nested_histories)
{
if (!entry.step().has<int>() || entry.step().get<int>() != descriptor.step)
return false;
if (entry.url() != descriptor.url)
return false;
if (descriptor.document_state.id != 0) {
auto document_state = entry.document_state();
if (!document_state || !session_history_document_state_descriptor_matches_document_state_ignoring_id(descriptor.document_state, *document_state, match_nested_histories))
return false;
}
if (entry.classic_history_api_state() != descriptor.classic_history_api_state)
return false;
if (entry.navigation_api_state() != descriptor.navigation_api_state || entry.navigation_api_key() != descriptor.navigation_api_key || entry.navigation_api_id() != descriptor.navigation_api_id)
return false;
if (entry.scroll_restoration_mode() != descriptor.scroll_restoration_mode)
return false;
if (descriptor.document_state.id != 0 && entry.scroll_position_data() != descriptor.scroll_position_data)
return false;
return true;
}
}
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) };
}