LibWeb: Flesh out apply the history step to setup the navigation API

We now populate the navigation history entries in the navigation API and
fire more navigation events as appropriate.
This commit is contained in:
Andrew Kaster 2023-09-27 22:59:57 -06:00 committed by Alexander Kalenik
parent a8091c009b
commit 6c1944ee61
Notes: sideshowbarker 2024-07-16 22:34:39 +09:00
11 changed files with 339 additions and 78 deletions

View file

@ -1,5 +1,5 @@
entries is empty: true
currentEntry is null: true
entries[length - 1] is current entry: true
currentEntry is a [object NavigationHistoryEntry]
transition is null: true
canGoBack: false
canGoForward: false
canGoBack: true
canGoForward: true

View file

@ -3,10 +3,10 @@
test(() => {
let n = window.navigation;
// FIXME: Once we set up the interaction between Navigables and Navigation,
// These two should become 1 and [object NavigationHistoryEntry], respectively
println(`entries is empty: ${n.entries().length == 0}`);
println(`currentEntry is null: ${n.currentEntry == null}`);
let len = n.entries().length;
println(`entries[length - 1] is current entry: ${n.entries()[len - 1] === n.currentEntry}`);
println(`currentEntry is a ${n.currentEntry}`);
println(`transition is null: ${n.transition == null}`);
println(`canGoBack: ${n.canGoBack}`);

View file

@ -71,6 +71,7 @@
#include <LibWeb/HTML/Location.h>
#include <LibWeb/HTML/MessageEvent.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/Navigation.h>
#include <LibWeb/HTML/NavigationParams.h>
#include <LibWeb/HTML/Numbers.h>
#include <LibWeb/HTML/Origin.h>
@ -3621,7 +3622,7 @@ void Document::restore_the_history_object_state(JS::NonnullGCPtr<HTML::SessionHi
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#update-document-for-history-step-application
void Document::update_for_history_step_application(JS::NonnullGCPtr<HTML::SessionHistoryEntry> entry, bool do_not_reactive, size_t script_history_length, size_t script_history_index)
void Document::update_for_history_step_application(JS::NonnullGCPtr<HTML::SessionHistoryEntry> entry, bool do_not_reactivate, size_t script_history_length, size_t script_history_index, Optional<Vector<JS::NonnullGCPtr<HTML::SessionHistoryEntry>>> entries_for_navigation_api, bool update_navigation_api)
{
// 1. Let documentIsNew be true if document's latest entry is null; otherwise false.
auto document_is_new = !m_latest_entry;
@ -3636,8 +3637,52 @@ void Document::update_for_history_step_application(JS::NonnullGCPtr<HTML::Sessio
history()->m_length = script_history_length;
// 5. If documentsEntryChanged is true, then:
// NOTE: documentsEntryChanged can be false for one of two reasons: either we are restoring from bfcache,
// or we are asynchronously finishing up a synchronous navigation which already synchronously set document's latest entry.
// The doNotReactivate argument distinguishes between these two cases.
if (documents_entry_changed) {
// FIXME: Implement this.
// 1. Let oldURL be document's latest entry's URL.
auto old_url = m_latest_entry ? m_latest_entry->url : AK::URL {};
// 2. Set document's latest entry to entry.
m_latest_entry = entry;
// 3. Restore the history object state given document and entry.
restore_the_history_object_state(entry);
// 4. Let navigation be history's relevant global object's navigation API.
auto navigation = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).navigation();
// 5. If documentIsNew is false, then:
if (!document_is_new) {
// AD HOC: Skip this in situations the spec steps don't account for
if (update_navigation_api) {
// 1. Update the navigation API entries for a same-document navigation given navigation, entry, and "traverse".
navigation->update_the_navigation_api_entries_for_a_same_document_navigation(entry, Bindings::NavigationType::Traverse);
}
// FIXME: 2. Fire an event named popstate at document's relevant global object, using PopStateEvent,
// with the state attribute initialized to document's history object's state and hasUAVisualTransition initialized to true
// if a visual transition, to display a cached rendered state of the latest entry, was done by the user agent.
// FIXME: 3. Restore persisted state given entry.
// FIXME: 4. If oldURL's fragment is not equal to entry's URL's fragment, then queue a global task on the DOM manipulation task source
// given document's relevant global object to fire an event named hashchange at document's relevant global object,
// using HashChangeEvent, with the oldURL attribute initialized to the serialization of oldURL and the newURL attribute
// initialized to the serialization of entry's URL.
}
// 6. Otherwise:
else {
// 1. Assert: entriesForNavigationAPI is given.
VERIFY(entries_for_navigation_api.has_value());
// FIXME: 2. Restore persisted state given entry.
// 3. Initialize the navigation API entries for a new document given navigation, entriesForNavigationAPI, and entry.
navigation->initialize_the_navigation_api_entries_for_a_new_document(*entries_for_navigation_api, entry);
}
}
// 6. If documentIsNew is true, then:
@ -3653,7 +3698,8 @@ void Document::update_for_history_step_application(JS::NonnullGCPtr<HTML::Sessio
}
// 7. Otherwise, if documentsEntryChanged is false and doNotReactivate is false, then:
if (!documents_entry_changed && !do_not_reactive) {
// NOTE: This is for bfcache restoration
if (!documents_entry_changed && !do_not_reactivate) {
// FIXME: 1. Assert: entriesForNavigationAPI is given.
// FIXME: 2. Reactivate document given entry and entriesForNavigationAPI.
}

View file

@ -535,9 +535,10 @@ public:
HTML::SourceSnapshotParams snapshot_source_snapshot_params() const;
void update_for_history_step_application(JS::NonnullGCPtr<HTML::SessionHistoryEntry>, bool do_not_reactive, size_t script_history_length, size_t script_history_index);
void update_for_history_step_application(JS::NonnullGCPtr<HTML::SessionHistoryEntry>, bool do_not_reactivate, size_t script_history_length, size_t script_history_index, Optional<Vector<JS::NonnullGCPtr<HTML::SessionHistoryEntry>>> entries_for_navigation_api = {}, bool update_navigation_api = true);
HashMap<AK::URL, JS::GCPtr<HTML::SharedImageRequest>>& shared_image_requests();
void restore_the_history_object_state(JS::NonnullGCPtr<HTML::SessionHistoryEntry> entry);
JS::NonnullGCPtr<Animations::DocumentTimeline> timeline();
@ -554,6 +555,9 @@ public:
bool ready_to_run_scripts() const { return m_ready_to_run_scripts; }
JS::GCPtr<HTML::SessionHistoryEntry> latest_entry() const { return m_latest_entry; }
void set_latest_entry(JS::GCPtr<HTML::SessionHistoryEntry> e) { m_latest_entry = e; }
protected:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;

View file

@ -160,8 +160,10 @@ bool can_have_its_url_rewritten(DOM::Document const& document, AK::URL const& ta
}
// https://html.spec.whatwg.org/multipage/history.html#shared-history-push/replace-state-steps
WebIDL::ExceptionOr<void> History::shared_history_push_replace_state(JS::Value value, Optional<String> const& url, HistoryHandlingBehavior history_handling)
WebIDL::ExceptionOr<void> History::shared_history_push_replace_state(JS::Value data, Optional<String> const& url, HistoryHandlingBehavior history_handling)
{
auto& vm = this->vm();
// 1. Let document be history's associated Document.
auto& document = m_associated_document;
@ -175,7 +177,8 @@ WebIDL::ExceptionOr<void> History::shared_history_push_replace_state(JS::Value v
// 4. Let serializedData be StructuredSerializeForStorage(data). Rethrow any exceptions.
// FIXME: Actually rethrow exceptions here once we start using the serialized data.
// Throwing here on data types we don't yet serialize will regress sites that use push/replaceState.
[[maybe_unused]] auto serialized_data_or_error = structured_serialize_for_storage(vm(), value);
auto serialized_data_or_error = structured_serialize_for_storage(vm, data);
auto serialized_data = serialized_data_or_error.is_error() ? MUST(structured_serialize_for_storage(vm, JS::js_null())) : serialized_data_or_error.release_value();
// 5. Let newURL be document's URL.
auto new_url = document->url();
@ -211,7 +214,7 @@ WebIDL::ExceptionOr<void> History::shared_history_push_replace_state(JS::Value v
// 10. Run the URL and history update steps given document and newURL, with serializedData set to
// serializedData and historyHandling set to historyHandling.
perform_url_and_history_update_steps(document, new_url, history_handling);
perform_url_and_history_update_steps(document, new_url, serialized_data, history_handling);
return {};
}

View file

@ -1261,8 +1261,8 @@ WebIDL::ExceptionOr<void> Navigable::navigate(NavigateParams params)
&& !response
&& url.equals(active_session_history_entry()->url, AK::URL::ExcludeFragment::Yes)
&& url.fragment().has_value()) {
// 1. Navigate to a fragment given navigable, url, historyHandling, and navigationId.
TRY(navigate_to_a_fragment(url, to_history_handling_behavior(history_handling), navigation_id));
// 1. Navigate to a fragment given navigable, url, historyHandling, userInvolvement, navigationAPIState, and navigationId.
TRY(navigate_to_a_fragment(url, to_history_handling_behavior(history_handling), user_involvement, navigation_api_state, navigation_id));
traversable_navigable()->process_session_history_traversal_queue();
@ -1387,14 +1387,14 @@ WebIDL::ExceptionOr<void> Navigable::navigate(NavigateParams params)
history_entry->url = url;
history_entry->document_state = document_state;
// 8. Let navigationParams be null.
// 7. Let navigationParams be null.
Variant<Empty, NavigationParams, NonFetchSchemeNavigationParams> navigation_params = Empty {};
// FIXME: 9. If response is non-null:
// FIXME: 8. If response is non-null:
if (response) {
}
// 10. Attempt to populate the history entry's document
// 9. Attempt to populate the history entry's document
// for historyEntry, given navigable, "navigate", sourceSnapshotParams,
// targetSnapshotParams, navigationId, navigationParams, cspNavigationType, with allowPOST
// set to true and completionSteps set to the following step:
@ -1417,14 +1417,26 @@ WebIDL::ExceptionOr<void> Navigable::navigate(NavigateParams params)
return {};
}
WebIDL::ExceptionOr<void> Navigable::navigate_to_a_fragment(AK::URL const& url, HistoryHandlingBehavior history_handling, String)
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate-fragid
WebIDL::ExceptionOr<void> Navigable::navigate_to_a_fragment(AK::URL const& url, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, Optional<SerializationRecord> navigation_api_state, String navigation_id)
{
// FIXME: 1. Let navigation be navigable's active window's navigation API.
// FIXME: 2. Let destinationNavigationAPIState be navigable's active session history entry's navigation API state.
// FIXME: 3. If navigationAPIState is not null, then set destinationNavigationAPIState to navigationAPIState.
// FIXME: 4. Let continue be the result of firing a push/replace/reload navigate event at navigation with navigationType set to historyHandling, isSameDocument set to true,
// userInvolvement set to userInvolvement, and destinationURL set to url, and navigationAPIState set to destinationNavigationAPIState.
// FIXME: 5. If continue is false, then return.
(void)navigation_id;
// 1. Let navigation be navigable's active window's navigation API.
auto navigation = active_window()->navigation();
// 2. Let destinationNavigationAPIState be navigable's active session history entry's navigation API state.
// 3. If navigationAPIState is not null, then set destinationNavigationAPIState to navigationAPIState.
auto destination_navigation_api_state = navigation_api_state.has_value() ? *navigation_api_state : active_session_history_entry()->navigation_api_state;
// 4. Let continue be the result of firing a push/replace/reload navigate event at navigation with navigationType set to historyHandling, isSameDocument set to true,
// userInvolvement set to userInvolvement, and destinationURL set to url, and navigationAPIState set to destinationNavigationAPIState.
auto navigation_type = history_handling == HistoryHandlingBehavior::Push ? Bindings::NavigationType::Push : Bindings::NavigationType::Replace;
bool const continue_ = navigation->fire_a_push_replace_reload_navigate_event(navigation_type, url, true, user_involvement, {}, destination_navigation_api_state);
// 5. If continue is false, then return.
if (!continue_)
return {};
// 6. Let historyEntry be a new session history entry, with
// URL: url
@ -1434,6 +1446,7 @@ WebIDL::ExceptionOr<void> Navigable::navigate_to_a_fragment(AK::URL const& url,
JS::NonnullGCPtr<SessionHistoryEntry> history_entry = heap().allocate_without_realm<SessionHistoryEntry>();
history_entry->url = url;
history_entry->document_state = active_session_history_entry()->document_state;
history_entry->navigation_api_state = destination_navigation_api_state;
history_entry->scroll_restoration_mode = active_session_history_entry()->scroll_restoration_mode;
// 7. Let entryToReplace be navigable's active session history entry if historyHandling is "replace", otherwise null.
@ -1450,7 +1463,8 @@ WebIDL::ExceptionOr<void> Navigable::navigate_to_a_fragment(AK::URL const& url,
// 11. If historyHandling is "push", then:
if (history_handling == HistoryHandlingBehavior::Push) {
// FIXME: 1. Set history's state to null.
// 1. Set history's state to null.
history->set_state(JS::js_null());
// 2. Increment scriptHistoryIndex.
script_history_index++;
@ -1463,9 +1477,11 @@ WebIDL::ExceptionOr<void> Navigable::navigate_to_a_fragment(AK::URL const& url,
m_active_session_history_entry = history_entry;
// 13. Update document for history step application given navigable's active document, historyEntry, true, scriptHistoryIndex, and scriptHistoryLength.
active_document()->update_for_history_step_application(*history_entry, true, script_history_length, script_history_index);
// AD HOC: Skip updating the navigation api entries twice here
active_document()->update_for_history_step_application(*history_entry, true, script_history_length, script_history_index, {}, false);
// FIXME: 14. Update the navigation API entries for a same-document navigation given navigation, historyEntry, and historyHandling.
// 14. Update the navigation API entries for a same-document navigation given navigation, historyEntry, and historyHandling.
navigation->update_the_navigation_api_entries_for_a_same_document_navigation(history_entry, navigation_type);
// 15. Scroll to the fragment given navigable's active document.
// FIXME: Specification doesn't say when document url needs to update during fragment navigation
@ -1827,7 +1843,7 @@ void finalize_a_cross_document_navigation(JS::NonnullGCPtr<Navigable> navigable,
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#url-and-history-update-steps
void perform_url_and_history_update_steps(DOM::Document& document, AK::URL new_url, HistoryHandlingBehavior history_handling)
void perform_url_and_history_update_steps(DOM::Document& document, AK::URL new_url, Optional<SerializationRecord> serialized_data, HistoryHandlingBehavior history_handling)
{
// 1. Let navigable be document's node navigable.
auto navigable = document.navigable();
@ -1835,14 +1851,16 @@ void perform_url_and_history_update_steps(DOM::Document& document, AK::URL new_u
// 2. Let activeEntry be navigable's active session history entry.
auto active_entry = navigable->active_session_history_entry();
// FIXME: Spec should be updated to say "classic history api state" instead of serialized state
// 3. Let newEntry be a new session history entry, with
// URL: newURL
// serialized state: if serializedData is not null, serializedData; otherwise activeEntry's classic history API state
// document state: activeEntry's document state
// scroll restoration mode: activeEntry's scroll restoration mode
// persisted user state: activeEntry's persisted user state
// FIXME: persisted user state: activeEntry's persisted user state
JS::NonnullGCPtr<SessionHistoryEntry> new_entry = document.heap().allocate_without_realm<SessionHistoryEntry>();
new_entry->url = new_url;
new_entry->classic_history_api_state = serialized_data.value_or(active_entry->classic_history_api_state);
new_entry->document_state = active_entry->document_state;
new_entry->scroll_restoration_mode = active_entry->scroll_restoration_mode;
@ -1863,17 +1881,23 @@ void perform_url_and_history_update_steps(DOM::Document& document, AK::URL new_u
document.history()->m_length = document.history()->m_index + 1;
}
// FIXME: 7. If serializedData is not null, then restore the history object state given document and newEntry.
// If serializedData is not null, then restore the history object state given document and newEntry.
if (serialized_data.has_value())
document.restore_the_history_object_state(new_entry);
// 8. Set document's URL to newURL.
document.set_url(new_url);
// FIXME: 9. Set document's latest entry to newEntry.
// 9. Set document's latest entry to newEntry.
document.set_latest_entry(new_entry);
// 10. Set navigable's active session history entry to newEntry.
navigable->set_active_session_history_entry(new_entry);
// FIXME: 11. Update the navigation API entries for a same-document navigation given document's relevant global object's navigation API, newEntry, and historyHandling.
// 11. Update the navigation API entries for a same-document navigation given document's relevant global object's navigation API, newEntry, and historyHandling.
auto& relevant_global_object = verify_cast<Window>(HTML::relevant_global_object(document));
auto navigation_type = history_handling == HistoryHandlingBehavior::Push ? Bindings::NavigationType::Push : Bindings::NavigationType::Replace;
relevant_global_object.navigation()->update_the_navigation_api_entries_for_a_same_document_navigation(new_entry, navigation_type);
// 12. Let traversable be navigable's traversable navigable.
auto traversable = navigable->traversable_navigable();

View file

@ -139,11 +139,13 @@ public:
WebIDL::ExceptionOr<void> navigate(NavigateParams);
WebIDL::ExceptionOr<void> navigate_to_a_fragment(AK::URL const&, HistoryHandlingBehavior, String navigation_id);
WebIDL::ExceptionOr<void> navigate_to_a_fragment(AK::URL const&, HistoryHandlingBehavior, UserNavigationInvolvement, Optional<SerializationRecord> navigation_api_state, String navigation_id);
WebIDL::ExceptionOr<JS::GCPtr<DOM::Document>> evaluate_javascript_url(AK::URL const&, Origin const& new_document_origin, String navigation_id);
WebIDL::ExceptionOr<void> navigate_to_a_javascript_url(AK::URL const&, HistoryHandlingBehavior, Origin const& initiator_origin, CSPNavigationType csp_navigation_type, String navigation_id);
bool allowed_by_sandboxing_to_navigate(Navigable const& target, SourceSnapshotParams const&);
void reload();
// https://github.com/whatwg/html/issues/9690
@ -189,8 +191,6 @@ protected:
TokenizedFeature::Popup m_is_popup { TokenizedFeature::Popup::No };
private:
bool allowed_by_sandboxing_to_navigate(Navigable const& target, SourceSnapshotParams const&);
void scroll_offset_did_change();
void inform_the_navigation_api_about_aborting_navigation();
@ -226,6 +226,6 @@ HashTable<Navigable*>& all_navigables();
bool navigation_must_be_a_replace(AK::URL const& url, DOM::Document const& document);
void finalize_a_cross_document_navigation(JS::NonnullGCPtr<Navigable>, HistoryHandlingBehavior, JS::NonnullGCPtr<SessionHistoryEntry>);
void perform_url_and_history_update_steps(DOM::Document& document, AK::URL new_url, HistoryHandlingBehavior history_handling = HistoryHandlingBehavior::Reload);
void perform_url_and_history_update_steps(DOM::Document& document, AK::URL new_url, Optional<SerializationRecord> = {}, HistoryHandlingBehavior history_handling = HistoryHandlingBehavior::Reload);
}

View file

@ -75,6 +75,7 @@ public:
JS::NonnullGCPtr<DOM::AbortController> abort_controller() const { return *m_abort_controller; }
InterceptionState interception_state() const { return m_interception_state; }
Vector<NavigationInterceptHandler> const& navigation_handler_list() const { return m_navigation_handler_list; }
Optional<SerializationRecord> classic_history_api_state() const { return m_classic_history_api_state; }
void set_abort_controller(JS::NonnullGCPtr<DOM::AbortController> c) { m_abort_controller = c; }
void set_interception_state(InterceptionState s) { m_interception_state = s; }

View file

@ -1096,10 +1096,9 @@ bool Navigation::inner_navigate_event_firing_algorithm(
// 7. If navigationType is "push" or "replace", then run the URL and history update steps given document and
// event's destination's URL, with serialiedData set to event's classic history API state and historyHandling
// set to navigationType.
// FIXME: Pass the serialized data to this algorithm
if (navigation_type == Bindings::NavigationType::Push || navigation_type == Bindings::NavigationType::Replace) {
auto history_handling = navigation_type == Bindings::NavigationType::Push ? HistoryHandlingBehavior::Push : HistoryHandlingBehavior::Replace;
perform_url_and_history_update_steps(document, event->destination()->raw_url(), history_handling);
perform_url_and_history_update_steps(document, event->destination()->raw_url(), event->classic_history_api_state(), history_handling);
}
// Big spec note about reload here
}

View file

@ -9,6 +9,7 @@
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/BrowsingContextGroup.h>
#include <LibWeb/HTML/DocumentState.h>
#include <LibWeb/HTML/Navigation.h>
#include <LibWeb/HTML/NavigationParams.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/HTML/TraversableNavigable.h>
@ -239,24 +240,130 @@ Vector<JS::Handle<Navigable>> TraversableNavigable::get_all_navigables_whose_cur
return results;
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-history-step
void TraversableNavigable::apply_the_history_step(int step, Optional<SourceSnapshotParams> source_snapshot_params)
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-all-navigables-that-only-need-history-object-length/index-update
Vector<JS::Handle<Navigable>> TraversableNavigable::get_all_navigables_that_only_need_history_object_length_index_update(int target_step) const
{
// NOTE: Other navigables might not be impacted by the traversal. For example, if the response is a 204, the currently active document will remain.
// Additionally, going 'back' after a 204 will change the current session history entry, but the active session history entry will already be correct.
// 1. Let results be an empty list.
Vector<JS::Handle<Navigable>> results;
// 2. Let navigablesToCheck be « traversable ».
Vector<JS::Handle<Navigable>> navigables_to_check;
navigables_to_check.append(const_cast<TraversableNavigable&>(*this));
// 3. For each navigable of navigablesToCheck:
while (!navigables_to_check.is_empty()) {
auto navigable = navigables_to_check.take_first();
// 1. Let targetEntry be the result of getting the target history entry given navigable and targetStep.
auto target_entry = navigable->get_the_target_history_entry(target_step);
// 2. If targetEntry is navigable's current session history entry and targetEntry's document state's reload pending is false, then:
if (target_entry == navigable->current_session_history_entry() && !target_entry->document_state->reload_pending()) {
// 1. Append navigable to results.
results.append(navigable);
// 2. Extend navigablesToCheck with navigable's child navigables.
navigables_to_check.extend(navigable->child_navigables());
}
}
// 4. Return results.
return results;
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-all-navigables-that-might-experience-a-cross-document-traversal
Vector<JS::Handle<Navigable>> TraversableNavigable::get_all_navigables_that_might_experience_a_cross_document_traversal(int target_step) const
{
// NOTE: From traversable's session history traversal queue's perspective, these documents are candidates for going cross-document during the
// traversal described by targetStep. They will not experience a cross-document traversal if the status code for their target document is
// HTTP 204 No Content.
// Note that if a given navigable might experience a cross-document traversal, this algorithm will return navigable but not its child navigables.
// Those would end up unloaded, not traversed.
// 1. Let results be an empty list.
Vector<JS::Handle<Navigable>> results;
// 2. Let navigablesToCheck be « traversable ».
Vector<JS::Handle<Navigable>> navigables_to_check;
navigables_to_check.append(const_cast<TraversableNavigable&>(*this));
// 3. For each navigable of navigablesToCheck:
while (!navigables_to_check.is_empty()) {
auto navigable = navigables_to_check.take_first();
// 1. Let targetEntry be the result of getting the target history entry given navigable and targetStep.
auto target_entry = navigable->get_the_target_history_entry(target_step);
// 2. If targetEntry's document is not navigable's document or targetEntry's document state's reload pending is true, then append navigable to results.
// NOTE: Although navigable's active history entry can change synchronously, the new entry will always have the same Document,
// so accessing navigable's document is reliable.
if (target_entry->document_state->document() != navigable->active_document() || target_entry->document_state->reload_pending()) {
results.append(navigable);
}
// 3. Otherwise, extend navigablesToCheck with navigable's child navigables.
// Adding child navigables to navigablesToCheck means those navigables will also be checked by this loop.
// Child navigables are only checked if the navigable's active document will not change as part of this traversal.
else {
navigables_to_check.extend(navigable->child_navigables());
}
}
// 4. Return results.
return results;
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-history-step
TraversableNavigable::HistoryStepResult TraversableNavigable::apply_the_history_step(
int step,
bool check_for_cancelation,
bool fire_navigate_event_on_commit,
Optional<SourceSnapshotParams> source_snapshot_params,
JS::GCPtr<Navigable> initiator_to_check,
Optional<UserNavigationInvolvement> user_involvement_for_navigate_events)
{
// FIXME: fireNavigateEventOnCommit is unused in this algorithm (https://github.com/whatwg/html/issues/9800)
(void)fire_navigate_event_on_commit;
auto& vm = this->vm();
// FIXME: 1. Assert: This is running within traversable's session history traversal queue.
// 2. Let targetStep be the result of getting the used step given traversable and step.
auto target_step = get_the_used_step(step);
// FIXME: 3. If initiatorToCheck is given, then:
// Note: Calling this early so we can re-use the same list in 3.2 and 6.
auto change_or_reload_navigables = get_all_navigables_whose_current_session_history_entry_will_change_or_reload(target_step);
// FIXME: 4. Let navigablesCrossingDocuments be the result of getting all navigables that might experience a cross-document traversal given traversable and targetStep.
// 3. If initiatorToCheck is not null, then:
if (initiator_to_check != nullptr) {
// 1. Assert: sourceSnapshotParams is not null.
VERIFY(source_snapshot_params.has_value());
// FIXME: 5. If checkForUserCancelation is true, and the result of checking if unloading is user-canceled given navigablesCrossingDocuments given traversable and targetStep is true, then return.
// 2. For each navigable of get all navigables whose current session history entry will change or reload:
// if initiatorToCheck is not allowed by sandboxing to navigate navigable given sourceSnapshotParams, then return "initiator-disallowed".
for (auto const& navigable : change_or_reload_navigables) {
if (!initiator_to_check->allowed_by_sandboxing_to_navigate(*navigable, *source_snapshot_params))
return HistoryStepResult::InitiatorDisallowed;
}
}
// 4. Let navigablesCrossingDocuments be the result of getting all navigables that might experience a cross-document traversal given traversable and targetStep.
[[maybe_unused]] auto navigables_crossing_documents = get_all_navigables_that_might_experience_a_cross_document_traversal(target_step);
// 5. FIXME: If checkForCancelation is true, and the result of checking if unloading is canceled given navigablesCrossingDocuments, traversable, targetStep,
// and userInvolvementForNavigateEvents is not "continue", then return that result.
(void)check_for_cancelation;
(void)user_involvement_for_navigate_events;
// 6. Let changingNavigables be the result of get all navigables whose current session history entry will change or reload given traversable and targetStep.
auto changing_navigables = get_all_navigables_whose_current_session_history_entry_will_change_or_reload(target_step);
auto changing_navigables = move(change_or_reload_navigables);
// FIXME: 7. Let nonchangingNavigablesThatStillNeedUpdates be the result of getting all navigables that only need history object length/index update given traversable and targetStep.
// 7. Let nonchangingNavigablesThatStillNeedUpdates be the result of getting all navigables that only need history object length/index update given traversable and targetStep.
auto non_changing_navigables_that_still_need_updates = get_all_navigables_that_only_need_history_object_length_index_update(target_step);
// 8. For each navigable of changingNavigables:
for (auto& navigable : changing_navigables) {
@ -280,10 +387,11 @@ void TraversableNavigable::apply_the_history_step(int step, Optional<SourceSnaps
JS::Handle<DOM::Document> displayed_document;
JS::Handle<SessionHistoryEntry> target_entry;
JS::Handle<Navigable> navigable;
bool update_only;
bool update_only = false;
};
// 11. Let changingNavigableContinuations be an empty queue of changing navigable continuation states.
// NOTE: This queue is used to split the operations on changingNavigables into two parts. Specifically, changingNavigableContinuations holds data for the second part.
Queue<ChangingNavigableContinuationState> changing_navigable_continuations;
// 12. For each navigable of changingNavigables, queue a global task on the navigation and traversal task source of navigable's active window to run the steps:
@ -320,17 +428,31 @@ void TraversableNavigable::apply_the_history_step(int step, Optional<SourceSnaps
}
// 5. Let oldOrigin be targetEntry's document state's origin.
[[maybe_unused]] auto old_origin = target_entry->document_state->origin();
auto old_origin = target_entry->document_state->origin();
auto after_document_populated = [target_entry, changing_navigable_continuation, &changing_navigable_continuations]() mutable {
auto after_document_populated = [old_origin, target_entry, changing_navigable_continuation, &changing_navigable_continuations, &vm, &navigable]() mutable {
// 1. If targetEntry's document is null, then set changingNavigableContinuation's update-only to true.
if (!target_entry->document_state->document()) {
changing_navigable_continuation.update_only = true;
}
// FIXME: 2. If targetEntry's document's origin is not oldOrigin, then set targetEntry's serialized state to StructuredSerializeForStorage(null).
else {
// 2. If targetEntry's document's origin is not oldOrigin, then set targetEntry's classic history API state to StructuredSerializeForStorage(null).
if (target_entry->document_state->document()->origin() != old_origin) {
target_entry->classic_history_api_state = MUST(structured_serialize_for_storage(vm, JS::js_null()));
}
// FIXME: 3. If all of the following are true:
// 3. If all of the following are true:
// - navigable's parent is null;
// - targetEntry's document's browsing context is not an auxiliary browsing context whose opener browsing context is non-null; and
// - targetEntry's document's origin is not oldOrigin,
// then set targetEntry's document state's navigable target name to the empty string.
if (navigable->parent() != nullptr
&& target_entry->document_state->document()->browsing_context()->opener_browsing_context() == nullptr
&& target_entry->document_state->origin() != old_origin) {
target_entry->document_state->set_navigable_target_name(String {});
}
}
// 4. Enqueue changingNavigableContinuation on changingNavigableContinuations.
changing_navigable_continuations.enqueue(move(changing_navigable_continuation));
@ -419,8 +541,11 @@ void TraversableNavigable::apply_the_history_step(int step, Optional<SourceSnaps
// FIXME: 9. Append navigable to navigablesThatMustWaitBeforeHandlingSyncNavigation.
// 10. Queue a global task on the navigation and traversal task source given navigable's active window to run the steps:
queue_global_task(Task::Source::NavigationAndTraversal, *navigable->active_window(), [&, target_entry, navigable, displayed_document, update_only = changing_navigable_continuation.update_only, script_history_length, script_history_index] {
// 10. Let entriesForNavigationAPI be the result of getting session history entries for the navigation API given navigable and targetStep.
auto entries_for_navigation_api = get_session_history_entries_for_the_navigation_api(*navigable, target_step);
// 11. Queue a global task on the navigation and traversal task source given navigable's active window to run the steps:
queue_global_task(Task::Source::NavigationAndTraversal, *navigable->active_window(), [&, target_entry, navigable, displayed_document, update_only = changing_navigable_continuation.update_only, script_history_length, script_history_index, entries_for_navigation_api = move(entries_for_navigation_api)]() mutable {
// NOTE: This check is not in the spec but we should not continue navigation if navigable has been destroyed.
if (navigable->has_been_destroyed())
return;
@ -445,15 +570,33 @@ void TraversableNavigable::apply_the_history_step(int step, Optional<SourceSnaps
navigable->activate_history_entry(*target_entry);
}
// FIXME: 2. If targetEntry's document is not equal to displayedDocument, then queue a global task on the navigation and traversal task source given targetEntry's document's
// relevant global object to perform the following step. Otherwise, continue onward to perform the following step within the currently-queued task.
// 2. If navigable is not traversable, and targetEntry is not navigable's current session history entry, and targetEntry's document state's origin is the same as
// navigable's current session history entry's document state's origin, then fire a traverse navigate event given targetEntry and userInvolvementForNavigateEvents.
auto target_origin = target_entry->document_state->origin();
auto current_origin = navigable->current_session_history_entry()->document_state->origin();
bool const is_same_origin = target_origin.has_value() && current_origin.has_value() && target_origin->is_same_origin(*current_origin);
if (!navigable->is_traversable()
&& target_entry.ptr() != navigable->current_session_history_entry()
&& is_same_origin) {
navigable->active_window()->navigation()->fire_a_traverse_navigate_event(*target_entry, user_involvement_for_navigate_events.value_or(UserNavigationInvolvement::None));
}
// 3. Update document for history step application given targetEntry's document, targetEntry, changingNavigableContinuation's update-only, scriptHistoryLength, and
// scriptHistoryIndex and entriesForNavigationAPI.
// FIXME: Pass entriesForNavigationAPI
target_entry->document_state->document()->update_for_history_step_application(*target_entry, update_only, script_history_length, script_history_index);
// 3. Let updateDocument be an algorithm step which performs update document for history step application given targetEntry's document,
// targetEntry, changingNavigableContinuation's update-only, scriptHistoryLength, scriptHistoryIndex, and entriesForNavigationAPI.
auto update_document = JS::SafeFunction<void()>([target_entry, update_only, script_history_length, script_history_index, entries_for_navigation_api = move(entries_for_navigation_api)] {
target_entry->document_state->document()->update_for_history_step_application(*target_entry, update_only, script_history_length, script_history_index, entries_for_navigation_api);
});
// 4. Increment completedChangeJobs.
// 4. If targetEntry's document is equal to displayedDocument, then perform updateDocument.
if (target_entry->document_state->document() == displayed_document.ptr()) {
update_document();
}
// 5. Otherwise, queue a global task on the navigation and traversal task source given targetEntry's document's relevant global object to perform updateDocument
else {
queue_global_task(Task::Source::NavigationAndTraversal, relevant_global_object(*target_entry->document_state->document()), move(update_document));
}
// 6. Increment completedChangeJobs.
completed_change_jobs++;
});
}
@ -470,6 +613,9 @@ void TraversableNavigable::apply_the_history_step(int step, Optional<SourceSnaps
// 20. Set traversable's current session history step to targetStep.
m_current_session_history_step = target_step;
// 21. Return "applied".
return HistoryStepResult::Applied;
}
Vector<JS::NonnullGCPtr<SessionHistoryEntry>> TraversableNavigable::get_session_history_entries_for_the_navigation_api(JS::NonnullGCPtr<Navigable> navigable, int target_step)
@ -575,14 +721,29 @@ void TraversableNavigable::clear_the_forward_session_history()
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#traverse-the-history-by-a-delta
void TraversableNavigable::traverse_the_history_by_delta(int delta)
void TraversableNavigable::traverse_the_history_by_delta(int delta, Optional<DOM::Document&> source_document)
{
// FIXME: 1. Let sourceSnapshotParams and initiatorToCheck be null.
// 1. Let sourceSnapshotParams and initiatorToCheck be null.
Optional<SourceSnapshotParams> source_snapshot_params = {};
JS::GCPtr<Navigable> initiator_to_check = nullptr;
// FIXME: 2. If sourceDocument is given, then:
// 2. Let userInvolvement be "browser UI".
UserNavigationInvolvement user_involvement = UserNavigationInvolvement::BrowserUI;
// 3. Append the following session history traversal steps to traversable:
append_session_history_traversal_steps([this, delta] {
// 1. If sourceDocument is given, then:
if (source_document.has_value()) {
// 1. Set sourceSnapshotParams to the result of snapshotting source snapshot params given sourceDocument.
source_snapshot_params = source_document->snapshot_source_snapshot_params();
// 2. Set initiatorToCheck to sourceDocument's node navigable.
initiator_to_check = source_document->navigable();
// 3. Set userInvolvement to "none".
user_involvement = UserNavigationInvolvement::None;
}
// 4. Append the following session history traversal steps to traversable:
append_session_history_traversal_steps([this, delta, source_snapshot_params = move(source_snapshot_params), initiator_to_check, user_involvement] {
// 1. Let allSteps be the result of getting all used history steps for traversable.
auto all_steps = get_all_used_history_steps();
@ -597,9 +758,9 @@ void TraversableNavigable::traverse_the_history_by_delta(int delta)
return;
}
// 5. Apply the history step allSteps[targetStepIndex] to traversable, with checkForUserCancelation set to true,
// sourceSnapshotParams set to sourceSnapshotParams, and initiatorToCheck set to initiatorToCheck.
apply_the_history_step(all_steps[target_step_index]);
// 5. Apply the traverse history step allSteps[targetStepIndex] to traversable, given sourceSnapshotParams,
// initiatorToCheck, and userInvolvement.
apply_the_traverse_history_step(all_steps[target_step_index], source_snapshot_params, initiator_to_check, user_involvement);
});
}
@ -610,8 +771,8 @@ void TraversableNavigable::update_for_navigable_creation_or_destruction()
auto step = current_session_history_step();
// 2. Return the result of applying the history step step to traversable given false, false, null, null, and null.
// FIXME: Pass false, false, null, null, and null as arguments.
apply_the_history_step(step);
// FIXME: Return result of history application.
apply_the_history_step(step, false, false, {}, {}, {});
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-reload-history-step
@ -621,16 +782,22 @@ void TraversableNavigable::apply_the_reload_history_step()
auto step = current_session_history_step();
// 2. Return the result of applying the history step step to traversable given true, false, null, null, and null.
// FIXME: Pass true, false, null, null, and null as arguments.
apply_the_history_step(step);
// FIXME: Return result of history application.
apply_the_history_step(step, true, false, {}, {}, {});
}
void TraversableNavigable::apply_the_push_or_replace_history_step(int step)
{
// 1. Return the result of applying the history step step to traversable given false, false, null, null, and null.
// FIXME: Pass false, false, null, null, and null as arguments.
// FIXME: Return result of history application.
apply_the_history_step(step);
apply_the_history_step(step, false, false, {}, {}, {});
}
void TraversableNavigable::apply_the_traverse_history_step(int step, Optional<SourceSnapshotParams> source_snapshot_params, JS::GCPtr<Navigable> initiator_to_check, UserNavigationInvolvement user_involvement)
{
// 1. Return the result of applying the history step step to traversable given true, true, sourceSnapshotParams, initiatorToCheck, and userInvolvement.
// FIXME: Return result of history application.
apply_the_history_step(step, true, true, move(source_snapshot_params), initiator_to_check, user_involvement);
}
// https://html.spec.whatwg.org/multipage/document-sequences.html#close-a-top-level-traversable

View file

@ -40,15 +40,19 @@ public:
};
HistoryObjectLengthAndIndex get_the_history_object_length_and_index(int) const;
void apply_the_traverse_history_step(int, Optional<SourceSnapshotParams>, JS::GCPtr<Navigable>, UserNavigationInvolvement);
void apply_the_reload_history_step();
void apply_the_push_or_replace_history_step(int step);
void update_for_navigable_creation_or_destruction();
int get_the_used_step(int step) const;
Vector<JS::Handle<Navigable>> get_all_navigables_whose_current_session_history_entry_will_change_or_reload(int) const;
Vector<JS::Handle<Navigable>> get_all_navigables_that_only_need_history_object_length_index_update(int) const;
Vector<JS::Handle<Navigable>> get_all_navigables_that_might_experience_a_cross_document_traversal(int) const;
Vector<int> get_all_used_history_steps() const;
void clear_the_forward_session_history();
void traverse_the_history_by_delta(int delta);
void traverse_the_history_by_delta(int delta, Optional<DOM::Document&> source_document = {});
void close_top_level_traversable();
void destroy_top_level_traversable();
@ -71,7 +75,20 @@ private:
virtual void visit_edges(Cell::Visitor&) override;
void apply_the_history_step(int step, Optional<SourceSnapshotParams> = {});
enum class HistoryStepResult {
InitiatorDisallowed,
CanceledByBeforeUnload,
CanceledByNavigate,
Applied,
};
// FIXME: Fix spec typo cancelation --> cancellation
HistoryStepResult apply_the_history_step(
int step,
bool check_for_cancelation,
bool fire_navigate_event_on_commit,
Optional<SourceSnapshotParams>,
JS::GCPtr<Navigable> initiator_to_check,
Optional<UserNavigationInvolvement> user_involvement_for_navigate_events);
Vector<JS::NonnullGCPtr<SessionHistoryEntry>> get_session_history_entries_for_the_navigation_api(JS::NonnullGCPtr<Navigable>, int);