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);