From b1f06e42cef9f90093f9a9316faa1d56ece4bb76 Mon Sep 17 00:00:00 2001 From: Andrew Kaster Date: Fri, 25 Aug 2023 20:35:22 -0600 Subject: [PATCH] LibWeb: Implement navigation.{traverseTo, back, forward} The proper abstract operations on Navigable and TraversableNavigable are not quite ready to call from Navigation. With this commit all of the user-facing APIs of Navigation are in place, and the stage should be set to implement the parts of the navigation and traversal AOs that need to interact with the Navigation object. --- Userland/Libraries/LibWeb/HTML/Navigation.cpp | 202 ++++++++++++++++++ Userland/Libraries/LibWeb/HTML/Navigation.h | 6 + Userland/Libraries/LibWeb/HTML/Navigation.idl | 6 +- 3 files changed, 211 insertions(+), 3 deletions(-) diff --git a/Userland/Libraries/LibWeb/HTML/Navigation.cpp b/Userland/Libraries/LibWeb/HTML/Navigation.cpp index a4aaea60c3..4c07411e99 100644 --- a/Userland/Libraries/LibWeb/HTML/Navigation.cpp +++ b/Userland/Libraries/LibWeb/HTML/Navigation.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include namespace Web::HTML { @@ -328,6 +329,63 @@ WebIDL::ExceptionOr Navigation::reload(NavigationReloadOptions return navigation_api_method_tracker_derived_result(api_method_tracker); } +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-traverseto +WebIDL::ExceptionOr Navigation::traverse_to(String key, NavigationOptions const& options) +{ + auto& realm = this->realm(); + // The traverseTo(key, options) method steps are: + + // 1. If this's current entry index is −1, then return an early error result for an "InvalidStateError" DOMException. + if (m_current_entry_index == -1) + return early_error_result(WebIDL::InvalidStateError::create(realm, "Cannot traverseTo: no current session history entry")); + + // 2. If this's entry list does not contain a NavigationHistoryEntry whose session history entry's navigation API key equals key, + // then return an early error result for an "InvalidStateError" DOMException. + auto it = m_entry_list.find_if([&key](auto const& entry) { + return entry->session_history_entry().navigation_api_key == key; + }); + if (it == m_entry_list.end()) + return early_error_result(WebIDL::InvalidStateError::create(realm, "Cannot traverseTo: key not found in session history list")); + + // 3. Return the result of performing a navigation API traversal given this, key, and options. + return perform_a_navigation_api_traversal(key, options); +} + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#performing-a-navigation-api-traversal +WebIDL::ExceptionOr Navigation::back(NavigationOptions const& options) +{ + auto& realm = this->realm(); + // The back(options) method steps are: + + // 1. If this's current entry index is −1 or 0, then return an early error result for an "InvalidStateError" DOMException. + if (m_current_entry_index == -1 || m_current_entry_index == 0) + return early_error_result(WebIDL::InvalidStateError::create(realm, "Cannot navigate back: no previous session history entry")); + + // 2. Let key be this's entry list[this's current entry index − 1]'s session history entry's navigation API key. + auto key = m_entry_list[m_current_entry_index - 1]->session_history_entry().navigation_api_key; + + // 3. Return the result of performing a navigation API traversal given this, key, and options. + return perform_a_navigation_api_traversal(key, options); +} + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#dom-navigation-forward +WebIDL::ExceptionOr Navigation::forward(NavigationOptions const& options) +{ + auto& realm = this->realm(); + // The forward(options) method steps are: + + // 1. If this's current entry index is −1 or is equal to this's entry list's size − 1, + // then return an early error result for an "InvalidStateError" DOMException. + if (m_current_entry_index == -1 || m_current_entry_index == static_cast(m_entry_list.size() - 1)) + return early_error_result(WebIDL::InvalidStateError::create(realm, "Cannot navigate forward: no next session history entry")); + + // 2. Let key be this's entry list[this's current entry index + 1]'s session history entry's navigation API key. + auto key = m_entry_list[m_current_entry_index + 1]->session_history_entry().navigation_api_key; + + // 3. Return the result of performing a navigation API traversal given this, key, and options. + return perform_a_navigation_api_traversal(key, options); +} + void Navigation::set_onnavigate(WebIDL::CallbackType* event_handler) { set_event_handler_attribute(HTML::EventNames::navigate, event_handler); @@ -496,4 +554,148 @@ JS::NonnullGCPtr Navigation::maybe_set_the_upcoming_ return api_method_tracker; } +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#add-an-upcoming-traverse-api-method-tracker +JS::NonnullGCPtr Navigation::add_an_upcoming_traverse_api_method_tracker(String destination_key, JS::Value info) +{ + auto& vm = this->vm(); + auto& realm = relevant_realm(*this); + // To add an upcoming traverse API method tracker given a Navigation navigation, a string destinationKey, and a JavaScript value info: + + // 1. Let committedPromise and finishedPromise be new promises created in navigation's relevant realm. + auto committed_promise = WebIDL::create_promise(realm); + auto finished_promise = WebIDL::create_promise(realm); + + // 2. Mark as handled finishedPromise. + // NOTE: See the previous discussion about why this is done + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#note-mark-as-handled-navigation-api-finished + WebIDL::mark_promise_as_handled(*finished_promise); + + // 3. Let apiMethodTracker be a new navigation API method tracker with: + // navigation object: navigation + // key: destinationKey + // info: info + // serialized state: null + // comitted-to entry: null + // comitted promise: committedPromise + // finished promise: finishedPromise + auto api_method_tracker = vm.heap().allocate_without_realm( + /* .navigation = */ *this, + /* .key = */ destination_key, + /* .info = */ info, + /* .serialized_state = */ OptionalNone {}, + /* .commited_to_entry = */ nullptr, + /* .committed_promise = */ committed_promise, + /* .finished_promise = */ finished_promise); + + // 4. Set navigation's upcoming traverse API method trackers[key] to apiMethodTracker. + // FIXME: Fix spec typo key --> destinationKey + m_upcoming_traverse_api_method_trackers.set(destination_key, api_method_tracker); + + // 5. Return apiMethodTracker. + return api_method_tracker; +} + +// https://html.spec.whatwg.org/multipage/nav-history-apis.html#performing-a-navigation-api-traversal +WebIDL::ExceptionOr Navigation::perform_a_navigation_api_traversal(String key, NavigationOptions const& options) +{ + auto& realm = this->realm(); + // To perform a navigation API traversal given a Navigation navigation, a string key, and a NavigationOptions options: + + // 1. Let document be this's relevant global object's associated Document. + auto& document = verify_cast(relevant_global_object(*this)).associated_document(); + + // 2. If document is not fully active, then return an early error result for an "InvalidStateError" DOMException. + if (!document.is_fully_active()) + return early_error_result(WebIDL::InvalidStateError::create(realm, "Document is not fully active")); + + // 3. If document's unload counter is greater than 0, then return an early error result for an "InvalidStateError" DOMException. + if (document.unload_counter() > 0) + return early_error_result(WebIDL::InvalidStateError::create(realm, "Document already unloaded")); + + // 4. Let current be the current entry of navigation. + auto current = current_entry(); + + // 5. If key equals current's session history entry's navigation API key, then return + // «[ "committed" → a promise resolved with current, "finished" → a promise resolved with current ]». + if (key == current->session_history_entry().navigation_api_key) { + return NavigationResult { + .committed = WebIDL::create_resolved_promise(realm, current)->promise(), + .finished = WebIDL::create_resolved_promise(realm, current)->promise() + }; + } + + // 6. If navigation's upcoming traverse API method trackers[key] exists, + // then return a navigation API method tracker-derived result for navigation's upcoming traverse API method trackers[key]. + if (auto maybe_tracker = m_upcoming_traverse_api_method_trackers.get(key); maybe_tracker.has_value()) + return navigation_api_method_tracker_derived_result(maybe_tracker.value()); + + // 7. Let info be options["info"], if it exists; otherwise, undefined. + auto info = options.info.value_or(JS::js_undefined()); + + // 8. Let apiMethodTracker be the result of adding an upcoming traverse API method tracker for navigation given key and info. + auto api_method_tracker = add_an_upcoming_traverse_api_method_tracker(key, info); + + // 9. Let navigable be document's node navigable. + auto navigable = document.navigable(); + + // 10. Let traversable be navigable's traversable navigable. + auto traversable = navigable->traversable_navigable(); + + // 11. Let sourceSnapshotParams be the result of snapshotting source snapshot params given document. + auto source_snapshot_params = document.snapshot_source_snapshot_params(); + + // 12. Append the following session history traversal steps to traversable: + traversable->append_session_history_traversal_steps([key, api_method_tracker, navigable, source_snapshot_params, this] { + // 1. Let navigableSHEs be the result of getting session history entries given navigable. + auto navigable_shes = navigable->get_session_history_entries(); + + // 2. Let targetSHE be the session history entry in navigableSHEs whose navigation API key is key. If no such entry exists, then: + auto it = navigable_shes.find_if([&key](auto const& entry) { + return entry->navigation_api_key == key; + }); + if (it == navigable_shes.end()) { + // NOTE: This path is taken if navigation's entry list was outdated compared to navigableSHEs, + // which can occur for brief periods while all the relevant threads and processes are being synchronized in reaction to a history change. + + // 1. Queue a global task on the navigation and traversal task source given navigation's relevant global object + // to reject the finished promise for apiMethodTracker with an "InvalidStateError" DOMException. + queue_global_task(HTML::Task::Source::NavigationAndTraversal, relevant_global_object(*this), [this, api_method_tracker] { + auto& reject_realm = relevant_realm(*this); + WebIDL::reject_promise(reject_realm, api_method_tracker->finished_promise, + WebIDL::InvalidStateError::create(reject_realm, "Cannot traverse with stale session history entry")); + }); + + // 2. Abort these steps. + return; + } + auto target_she = *it; + + // 3. If targetSHE is navigable's active session history entry, then abort these steps. + // NOTE: This can occur if a previously queued traversal already took us to this session history entry. + // In that case the previous traversal will have dealt with apiMethodTracker already. + if (target_she == navigable->active_session_history_entry()) + return; + + // FIXME: 4. Let result be the result of applying the traverse history step given by targetSHE's step to traversable, + // given sourceSnapshotParams, navigable, and "none". + (void)source_snapshot_params; + + // NOTE: When result is "canceled-by-beforeunload" or "initiator-disallowed", the navigate event was never fired, + // aborting the ongoing navigation would not be correct; it would result in a navigateerror event without a + // preceding navigate event. In the "canceled-by-navigate" case, navigate is fired, but the inner navigate event + // firing algorithm will take care of aborting the ongoing navigation. + + // FIXME: 5. If result is "canceled-by-beforeunload", then queue a global task on the navigation and traversal task source + // given navigation's relevant global object to reject the finished promise for apiMethodTracker with a + // new "AbortError"DOMException created in navigation's relevant realm. + + // FIXME: 6. If result is "initiator-disallowed", then queue a global task on the navigation and traversal task source + // given navigation's relevant global object to reject the finished promise for apiMethodTracker with a + // new "SecurityError" DOMException created in navigation's relevant realm. + }); + + // 13. Return a navigation API method tracker-derived result for apiMethodTracker. + return navigation_api_method_tracker_derived_result(api_method_tracker); +} + } diff --git a/Userland/Libraries/LibWeb/HTML/Navigation.h b/Userland/Libraries/LibWeb/HTML/Navigation.h index 508ff44c42..57a90f49d0 100644 --- a/Userland/Libraries/LibWeb/HTML/Navigation.h +++ b/Userland/Libraries/LibWeb/HTML/Navigation.h @@ -85,6 +85,10 @@ public: WebIDL::ExceptionOr navigate(String url, NavigationNavigateOptions const&); WebIDL::ExceptionOr reload(NavigationReloadOptions const&); + WebIDL::ExceptionOr traverse_to(String key, NavigationOptions const&); + WebIDL::ExceptionOr back(NavigationOptions const&); + WebIDL::ExceptionOr forward(NavigationOptions const&); + // Event Handlers void set_onnavigate(WebIDL::CallbackType*); WebIDL::CallbackType* onnavigate(); @@ -114,6 +118,8 @@ private: NavigationResult early_error_result(AnyException); JS::NonnullGCPtr maybe_set_the_upcoming_non_traverse_api_method_tracker(JS::Value info, Optional); + JS::NonnullGCPtr add_an_upcoming_traverse_api_method_tracker(String destination_key, JS::Value info); + WebIDL::ExceptionOr perform_a_navigation_api_traversal(String key, NavigationOptions const&); // https://html.spec.whatwg.org/multipage/nav-history-apis.html#navigation-entry-list // Each Navigation has an associated entry list, a list of NavigationHistoryEntry objects, initially empty. diff --git a/Userland/Libraries/LibWeb/HTML/Navigation.idl b/Userland/Libraries/LibWeb/HTML/Navigation.idl index 3b3b7e1076..0b7fba4767 100644 --- a/Userland/Libraries/LibWeb/HTML/Navigation.idl +++ b/Userland/Libraries/LibWeb/HTML/Navigation.idl @@ -17,9 +17,9 @@ interface Navigation : EventTarget { NavigationResult navigate(USVString url, optional NavigationNavigateOptions options = {}); NavigationResult reload(optional NavigationReloadOptions options = {}); - // NavigationResult traverseTo(DOMString key, optional NavigationOptions options = {}); - // NavigationResult back(optional NavigationOptions options = {}); - // NavigationResult forward(optional NavigationOptions options = {}); + NavigationResult traverseTo(DOMString key, optional NavigationOptions options = {}); + NavigationResult back(optional NavigationOptions options = {}); + NavigationResult forward(optional NavigationOptions options = {}); attribute EventHandler onnavigate; attribute EventHandler onnavigatesuccess;