diff --git a/Tests/LibWeb/Text/expected/HTML/Navigation-object-properties.txt b/Tests/LibWeb/Text/expected/HTML/Navigation-object-properties.txt
index 364ff8ecae..e27cf3a074 100644
--- a/Tests/LibWeb/Text/expected/HTML/Navigation-object-properties.txt
+++ b/Tests/LibWeb/Text/expected/HTML/Navigation-object-properties.txt
@@ -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
diff --git a/Tests/LibWeb/Text/input/HTML/Navigation-object-properties.html b/Tests/LibWeb/Text/input/HTML/Navigation-object-properties.html
index e037fa2ac6..9945bab042 100644
--- a/Tests/LibWeb/Text/input/HTML/Navigation-object-properties.html
+++ b/Tests/LibWeb/Text/input/HTML/Navigation-object-properties.html
@@ -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}`);
diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp
index d29353979c..39cc5158ee 100644
--- a/Userland/Libraries/LibWeb/DOM/Document.cpp
+++ b/Userland/Libraries/LibWeb/DOM/Document.cpp
@@ -71,6 +71,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -3621,7 +3622,7 @@ void Document::restore_the_history_object_state(JS::NonnullGCPtr entry, bool do_not_reactive, size_t script_history_length, size_t script_history_index)
+void Document::update_for_history_step_application(JS::NonnullGCPtr entry, bool do_not_reactivate, size_t script_history_length, size_t script_history_index, Optional>> 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::NonnullGCPtrm_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::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, bool do_not_reactive, size_t script_history_length, size_t script_history_index);
+ void update_for_history_step_application(JS::NonnullGCPtr, bool do_not_reactivate, size_t script_history_length, size_t script_history_index, Optional>> entries_for_navigation_api = {}, bool update_navigation_api = true);
HashMap>& shared_image_requests();
+
void restore_the_history_object_state(JS::NonnullGCPtr entry);
JS::NonnullGCPtr timeline();
@@ -554,6 +555,9 @@ public:
bool ready_to_run_scripts() const { return m_ready_to_run_scripts; }
+ JS::GCPtr latest_entry() const { return m_latest_entry; }
+ void set_latest_entry(JS::GCPtr e) { m_latest_entry = e; }
+
protected:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
diff --git a/Userland/Libraries/LibWeb/HTML/History.cpp b/Userland/Libraries/LibWeb/HTML/History.cpp
index d45e60bc1b..d2bea6b0a5 100644
--- a/Userland/Libraries/LibWeb/HTML/History.cpp
+++ b/Userland/Libraries/LibWeb/HTML/History.cpp
@@ -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 History::shared_history_push_replace_state(JS::Value value, Optional const& url, HistoryHandlingBehavior history_handling)
+WebIDL::ExceptionOr History::shared_history_push_replace_state(JS::Value data, Optional 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 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 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 {};
}
diff --git a/Userland/Libraries/LibWeb/HTML/Navigable.cpp b/Userland/Libraries/LibWeb/HTML/Navigable.cpp
index 7581855b83..51a25832da 100644
--- a/Userland/Libraries/LibWeb/HTML/Navigable.cpp
+++ b/Userland/Libraries/LibWeb/HTML/Navigable.cpp
@@ -1261,8 +1261,8 @@ WebIDL::ExceptionOr 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 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 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 Navigable::navigate(NavigateParams params)
return {};
}
-WebIDL::ExceptionOr 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 Navigable::navigate_to_a_fragment(AK::URL const& url, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, Optional 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 Navigable::navigate_to_a_fragment(AK::URL const& url,
JS::NonnullGCPtr history_entry = heap().allocate_without_realm();
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 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 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,
}
// 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 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 new_entry = document.heap().allocate_without_realm();
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(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();
diff --git a/Userland/Libraries/LibWeb/HTML/Navigable.h b/Userland/Libraries/LibWeb/HTML/Navigable.h
index 7de25ca6c9..9ea1e097f4 100644
--- a/Userland/Libraries/LibWeb/HTML/Navigable.h
+++ b/Userland/Libraries/LibWeb/HTML/Navigable.h
@@ -139,11 +139,13 @@ public:
WebIDL::ExceptionOr navigate(NavigateParams);
- WebIDL::ExceptionOr navigate_to_a_fragment(AK::URL const&, HistoryHandlingBehavior, String navigation_id);
+ WebIDL::ExceptionOr navigate_to_a_fragment(AK::URL const&, HistoryHandlingBehavior, UserNavigationInvolvement, Optional navigation_api_state, String navigation_id);
WebIDL::ExceptionOr> evaluate_javascript_url(AK::URL const&, Origin const& new_document_origin, String navigation_id);
WebIDL::ExceptionOr 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& all_navigables();
bool navigation_must_be_a_replace(AK::URL const& url, DOM::Document const& document);
void finalize_a_cross_document_navigation(JS::NonnullGCPtr, HistoryHandlingBehavior, JS::NonnullGCPtr);
-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 = {}, HistoryHandlingBehavior history_handling = HistoryHandlingBehavior::Reload);
}
diff --git a/Userland/Libraries/LibWeb/HTML/NavigateEvent.h b/Userland/Libraries/LibWeb/HTML/NavigateEvent.h
index 0f8fcee078..a948be935f 100644
--- a/Userland/Libraries/LibWeb/HTML/NavigateEvent.h
+++ b/Userland/Libraries/LibWeb/HTML/NavigateEvent.h
@@ -75,6 +75,7 @@ public:
JS::NonnullGCPtr abort_controller() const { return *m_abort_controller; }
InterceptionState interception_state() const { return m_interception_state; }
Vector const& navigation_handler_list() const { return m_navigation_handler_list; }
+ Optional classic_history_api_state() const { return m_classic_history_api_state; }
void set_abort_controller(JS::NonnullGCPtr c) { m_abort_controller = c; }
void set_interception_state(InterceptionState s) { m_interception_state = s; }
diff --git a/Userland/Libraries/LibWeb/HTML/Navigation.cpp b/Userland/Libraries/LibWeb/HTML/Navigation.cpp
index 8dcab2d2f4..d1479c2c43 100644
--- a/Userland/Libraries/LibWeb/HTML/Navigation.cpp
+++ b/Userland/Libraries/LibWeb/HTML/Navigation.cpp
@@ -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
}
diff --git a/Userland/Libraries/LibWeb/HTML/TraversableNavigable.cpp b/Userland/Libraries/LibWeb/HTML/TraversableNavigable.cpp
index 77484af555..3e7b0f26d4 100644
--- a/Userland/Libraries/LibWeb/HTML/TraversableNavigable.cpp
+++ b/Userland/Libraries/LibWeb/HTML/TraversableNavigable.cpp
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -239,24 +240,130 @@ Vector> 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 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> 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> results;
+
+ // 2. Let navigablesToCheck be « traversable ».
+ Vector> navigables_to_check;
+ navigables_to_check.append(const_cast(*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> 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> results;
+
+ // 2. Let navigablesToCheck be « traversable ».
+ Vector> navigables_to_check;
+ navigables_to_check.append(const_cast(*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 source_snapshot_params,
+ JS::GCPtr initiator_to_check,
+ Optional 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 displayed_document;
JS::Handle target_entry;
JS::Handle 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 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, Optionaldocument_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, Optionalactive_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, Optionalactivate_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([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> TraversableNavigable::get_session_history_entries_for_the_navigation_api(JS::NonnullGCPtr 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 source_document)
{
- // FIXME: 1. Let sourceSnapshotParams and initiatorToCheck be null.
+ // 1. Let sourceSnapshotParams and initiatorToCheck be null.
+ Optional source_snapshot_params = {};
+ JS::GCPtr 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 source_snapshot_params, JS::GCPtr 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
diff --git a/Userland/Libraries/LibWeb/HTML/TraversableNavigable.h b/Userland/Libraries/LibWeb/HTML/TraversableNavigable.h
index d1c02cb2fc..b51598e689 100644
--- a/Userland/Libraries/LibWeb/HTML/TraversableNavigable.h
+++ b/Userland/Libraries/LibWeb/HTML/TraversableNavigable.h
@@ -40,15 +40,19 @@ public:
};
HistoryObjectLengthAndIndex get_the_history_object_length_and_index(int) const;
+ void apply_the_traverse_history_step(int, Optional, JS::GCPtr, 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> get_all_navigables_whose_current_session_history_entry_will_change_or_reload(int) const;
+ Vector> get_all_navigables_that_only_need_history_object_length_index_update(int) const;
+ Vector> get_all_navigables_that_might_experience_a_cross_document_traversal(int) const;
+
Vector 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 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 = {});
+ 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,
+ JS::GCPtr initiator_to_check,
+ Optional user_involvement_for_navigate_events);
Vector> get_session_history_entries_for_the_navigation_api(JS::NonnullGCPtr, int);