diff --git a/Userland/Libraries/LibWeb/HTML/NavigateEvent.h b/Userland/Libraries/LibWeb/HTML/NavigateEvent.h index 5141d4a6f74..4749b3e0ab7 100644 --- a/Userland/Libraries/LibWeb/HTML/NavigateEvent.h +++ b/Userland/Libraries/LibWeb/HTML/NavigateEvent.h @@ -42,6 +42,15 @@ class NavigateEvent : public DOM::Event { WEB_PLATFORM_OBJECT(NavigateEvent, DOM::Event); public: + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#concept-navigateevent-interception-state + enum class InterceptionState { + None, + Intercepted, + Committed, + Scrolled, + Finished + }; + [[nodiscard]] static JS::NonnullGCPtr construct_impl(JS::Realm&, FlyString const& event_name, NavigateEventInit const&); // The navigationType, destination, canIntercept, userInitiated, hashChange, signal, formData, @@ -63,6 +72,12 @@ public: virtual ~NavigateEvent() override; 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; } + + void set_abort_controller(JS::NonnullGCPtr c) { m_abort_controller = c; } + void set_interception_state(InterceptionState s) { m_interception_state = s; } + void set_classic_history_api_state(Optional r) { m_classic_history_api_state = move(r); } void finish(bool did_fulfill); @@ -78,13 +93,6 @@ private: void potentially_reset_the_focus(); // https://html.spec.whatwg.org/multipage/nav-history-apis.html#concept-navigateevent-interception-state - enum class InterceptionState { - None, - Intercepted, - Committed, - Scrolled, - Finished - }; InterceptionState m_interception_state = InterceptionState::None; // https://html.spec.whatwg.org/multipage/nav-history-apis.html#concept-navigateevent-navigation-handler-list diff --git a/Userland/Libraries/LibWeb/HTML/Navigation.cpp b/Userland/Libraries/LibWeb/HTML/Navigation.cpp index 568f8cf222c..0e1366fc668 100644 --- a/Userland/Libraries/LibWeb/HTML/Navigation.cpp +++ b/Userland/Libraries/LibWeb/HTML/Navigation.cpp @@ -13,14 +13,18 @@ #include #include #include +#include #include #include #include +#include #include #include +#include #include #include #include +#include namespace Web::HTML { @@ -861,4 +865,341 @@ void Navigation::reject_the_finished_promise(JS::NonnullGCPtr destination, + UserNavigationInvolvement user_involvement, + Optional&> form_data_entry_list, + Optional download_request_filename, + Optional classic_history_api_state) +{ + auto& realm = relevant_realm(*this); + + // 1. If navigation has entries and events disabled, then: + // NOTE: These assertions holds because traverseTo(), back(), and forward() will immediately fail when entries and events are disabled + // (since there are no entries to traverse to), and if our starting point is instead navigate() or reload(), + // then we avoided setting the upcoming non-traverse API method tracker in the first place. + if (has_entries_and_events_disabled()) { + // 1. Assert: navigation's ongoing API method tracker is null. + VERIFY(m_ongoing_api_method_tracker == nullptr); + + // 2. Assert: navigation's upcoming non-traverse API method tracker is null. + VERIFY(m_upcoming_non_traverse_api_method_tracker == nullptr); + + // 3. Assert: navigation's upcoming traverse API method trackers is empty. + VERIFY(m_upcoming_traverse_api_method_trackers.is_empty()); + + // 4. Return true. + return true; + } + + // 2. Let destinationKey be null. + Optional destination_key = {}; + + // 3. If destination's entry is non-null, then set destinationKey to destination's entry's key. + if (destination->navigation_history_entry() != nullptr) + destination_key = destination->navigation_history_entry()->key(); + + // 4. Assert: destinationKey is not the empty string. + VERIFY(destination_key != ""sv); + + // 5. Promote an upcoming API method tracker to ongoing given navigation and destinationKey. + promote_an_upcoming_api_method_tracker_to_ongoing(destination_key); + + // 6. Let apiMethodTracker be navigation's ongoing API method tracker. + auto api_method_tracker = m_ongoing_api_method_tracker; + + // 7. Let navigable be navigation's relevant global object's navigable. + auto& relevant_global_object = verify_cast(Web::HTML::relevant_global_object(*this)); + auto navigable = relevant_global_object.navigable(); + + // 8. Let document be navigation's relevant global object's associated Document. + auto& document = relevant_global_object.associated_document(); + + // Note: We create the Event in this algorithm instead of passing it in, + // and have all the following "initialize" steps set up the event init + NavigateEventInit event_init = {}; + + // 9. If document can have its URL rewritten to destination's URL, + // and either destination's is same document is true or navigationType is not "traverse", + // then initialize event's canIntercept to true. Otherwise, initialize it to false. + event_init.can_intercept = can_have_its_url_rewritten(document, destination->raw_url()) && (destination->same_document() || navigation_type != Bindings::NavigationType::Traverse); + + // 10. Let traverseCanBeCanceled be true if all of the following are true: + // - navigable is a top-level traversable; + // - destination's is same document is true; and + // - either userInvolvement is not "browser UI", or navigation's relevant global object has transient activation. + // Otherwise, let it be false. + bool const traverse_can_be_canceled = navigable->is_top_level_traversable() + && destination->same_document() + && (user_involvement != UserNavigationInvolvement::BrowserUI || relevant_global_object.has_transient_activation()); + + // FIXME: Fix spec grammaro, extra 'the -> set' + // 11. If either: + // - navigationType is not "traverse"; or + // - traverseCanBeCanceled is true + // the initialize event's cancelable to true. Otherwise, initialize it to false. + event_init.cancelable = (navigation_type != Bindings::NavigationType::Traverse) || traverse_can_be_canceled; + + // 12. Initialize event's type to "navigate". + // AD-HOC: Happens later, when calling the factory function + + // 13. Initialize event's navigationType to navigationType. + event_init.navigation_type = navigation_type; + + // 14. Initialize event's destination to destination. + event_init.destination = destination; + + // 15. Initialize event's downloadRequest to downloadRequestFilename. + event_init.download_request = move(download_request_filename); + + // 16. If apiMethodTracker is not null, then initialize event's info to apiMethodTracker's info. Otherwise, initialize it to undefined. + // NOTE: At this point apiMethodTracker's info is no longer needed and can be nulled out instead of keeping it alive for the lifetime of the navigation API method tracker. + if (api_method_tracker) { + event_init.info = api_method_tracker->info; + api_method_tracker->info = JS::js_undefined(); + } else { + event_init.info = JS::js_undefined(); + } + + // FIXME: 17: Initialize event's hasUAVisualTransition to true if a visual transition, to display a cached rendered state + // of the document's latest entry, was done by the user agent. Otherwise, initialize it to false. + event_init.has_ua_visual_transition = false; + + // 18. Set event's abort controller to a new AbortController created in navigation's relevant realm. + // AD-HOC: Set on the NavigateEvent later after construction + auto abort_controller = MUST(DOM::AbortController::construct_impl(realm)); + + // 19. Initialize event's signal to event's abort controller's signal. + event_init.signal = abort_controller->signal(); + + // 20. Let currentURL be document's URL. + auto current_url = document.url(); + + // 21. If all of the following are true: + // - destination's is same document is true; + // - destination's URL equals currentURL with exclude fragments set to true; and + // - destination's URL's fragment is not identical to currentURL's fragment, + // then initialize event's hashChange to true. Otherwise, initialize it to false. + event_init.hash_change = (destination->same_document() + && destination->raw_url().equals(current_url, AK::URL::ExcludeFragment::Yes) + && destination->raw_url().fragment() != current_url.fragment()); + + // 22. If userInvolvement is not "none", then initialize event's userInitiated to true. Otherwise, initialize it to false. + event_init.user_initiated = user_involvement != UserNavigationInvolvement::None; + + // 23. If formDataEntryList is not null, then initialize event's formData to a new FormData created in navigation's relevant realm, + // associated to formDataEntryList. Otherwise, initialize it to null. + if (form_data_entry_list.has_value()) { + event_init.form_data = MUST(XHR::FormData::construct_impl(realm, form_data_entry_list.release_value())); + } else { + event_init.form_data = nullptr; + } + + // AD-HOC: *Now* we have all the info required to create the event + auto event = NavigateEvent::construct_impl(realm, EventNames::navigate, event_init); + event->set_abort_controller(abort_controller); + + // AD-HOC: This is supposed to be set in "fire a navigate event", and is only non-null when + // we're doing a push or replace. We set it here because we create the event here + event->set_classic_history_api_state(move(classic_history_api_state)); + + // 24. Assert: navigation's ongoing navigate event is null. + VERIFY(m_ongoing_navigate_event == nullptr); + + // 25. Set navigation's ongoing navigate event to event. + m_ongoing_navigate_event = event; + + // 26. Set navigation's focus changed during ongoing navigation to false. + m_focus_changed_during_ongoing_navigation = false; + + // 27. Set navigation's suppress normal scroll restoration during ongoing navigation to false. + m_suppress_scroll_restoration_during_ongoing_navigation = false; + + // 28. Let dispatchResult be the result of dispatching event at navigation. + auto dispatch_result = dispatch_event(*event); + + // 29. If dispatchResult is false: + if (!dispatch_result) { + // FIXME: 1. If navigationType is "traverse", then consume history-action user activation. + + // 2. If event's abort controller's signal is not aborted, then abort the ongoing navigation given navigation. + if (!event->abort_controller()->signal()->aborted()) + abort_the_ongoing_navigation(); + + // 3. Return false. + return false; + } + + // 30. Let endResultIsSameDocument be true if event's interception state + // is not "none" or event's destination's is same document is true. + bool const end_result_is_same_document = (event->interception_state() != NavigateEvent::InterceptionState::None) || event->destination()->same_document(); + + // 31. Prepare to run script given navigation's relevant settings object. + // NOTE: There's a massive spec note here + relevant_settings_object(*this).prepare_to_run_script(); + + // 32. If event's interception state is not "none": + if (event->interception_state() != NavigateEvent::InterceptionState::None) { + // 1. Set event's interception state to "committed". + event->set_interception_state(NavigateEvent::InterceptionState::Committed); + + // 2. Let fromNHE be the current entry of navigation. + auto from_nhe = current_entry(); + + // 3. Assert: fromNHE is not null. + VERIFY(from_nhe != nullptr); + + // 4. Set navigation's transition to a new NavigationTransition created in navigation's relevant realm, + // whose navigation type is navigationType, from entry is fromNHE, and whose finished promise is a new promise + // created in navigation's relevant realm. + m_transition = NavigationTransition::create(realm, navigation_type, *from_nhe, JS::Promise::create(realm)); + + // 5. Mark as handled navigation's transition's finished promise. + m_transition->finished()->set_is_handled(); + + // 6. If navigationType is "traverse", then set navigation's suppress normal scroll restoration during ongoing navigation to true. + // NOTE: If event's scroll behavior was set to "after-transition", then scroll restoration will happen as part of finishing + // the relevant NavigateEvent. Otherwise, there will be no scroll restoration. That is, no navigation which is intercepted + // by intercept() goes through the normal scroll restoration process; scroll restoration for such navigations + // is either done manually, by the web developer, or is done after the transition. + if (navigation_type == Bindings::NavigationType::Traverse) + m_suppress_scroll_restoration_during_ongoing_navigation = true; + + // FIXME: Fix spec typo "serialied" + // 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); + } + // Big spec note about reload here + } + + // 33. If endResultIsSameDocument is true: + if (end_result_is_same_document) { + // 1. Let promisesList be an empty list. + JS::MarkedVector> promises_list(realm.heap()); + + // 2. For each handler of event's navigation handler list: + for (auto const& handler : event->navigation_handler_list()) { + // 1. Append the result of invoking handler with an empty arguments list to promisesList. + auto result = WebIDL::invoke_callback(handler, {}); + if (result.is_abrupt()) { + // FIXME: https://github.com/whatwg/html/issues/9774 + report_exception(result.release_error(), realm); + continue; + } + // This *should* be equivalent to converting a promise to a promise capability + promises_list.append(WebIDL::create_resolved_promise(realm, result.value().value())); + } + + // 3. If promisesList's size is 0, then set promisesList to « a promise resolved with undefined ». + // NOTE: There is a subtle timing difference between how waiting for all schedules its success and failure + // steps when given zero promises versus ≥1 promises. For most uses of waiting for all, this does not matter. + // However, with this API, there are so many events and promise handlers which could fire around the same time + // that the difference is pretty easily observable: it can cause the event/promise handler sequence to vary. + // (Some of the events and promises involved include: navigatesuccess / navigateerror, currententrychange, + // dispose, apiMethodTracker's promises, and the navigation.transition.finished promise.) + if (promises_list.size() == 0) { + promises_list.append(WebIDL::create_resolved_promise(realm, JS::js_undefined())); + } + + // 4. Wait for all of promisesList, with the following success steps: + WebIDL::wait_for_all( + realm, promises_list, [&](JS::MarkedVector const&) -> void { + // FIXME: Spec issue: Event's relevant global objects' *associated document* + // 1. If event's relevant global object is not fully active, then abort these steps. + if (!relevant_global_object.associated_document().is_fully_active()) + return; + + // 2. If event's abort controller's signal is aborted, then abort these steps. + if (event->abort_controller()->signal()->aborted()) + return; + + // 3. Assert: event equals navigation's ongoing navigate event. + VERIFY(event == m_ongoing_navigate_event); + + // 4. Set navigation's ongoing navigate event to null. + m_ongoing_navigate_event = nullptr; + + // 5. Finish event given true. + event->finish(true); + + // FIXME: Implement https://dom.spec.whatwg.org/#concept-event-fire somewhere + // 6. Fire an event named navigatesuccess at navigation. + dispatch_event(DOM::Event::create(realm, EventNames::navigatesuccess)); + + // 7. If navigation's transition is not null, then resolve navigation's transition's finished promise with undefined. + if (m_transition != nullptr) + m_transition->finished()->fulfill(JS::js_undefined()); + + // 8. Set navigation's transition to null. + m_transition = nullptr; + + // 9. If apiMethodTracker is non-null, then resolve the finished promise for apiMethodTracker. + if (api_method_tracker) + resolve_the_finished_promise(*api_method_tracker); }, + // and the following failure steps given reason rejectionReason: + [&](JS::Value rejection_reason) -> void { + // FIXME: Spec issue: Event's relevant global objects' *associated document* + // 1. If event's relevant global object is not fully active, then abort these steps. + if (!relevant_global_object.associated_document().is_fully_active()) + return; + + // 2. If event's abort controller's signal is aborted, then abort these steps. + if (event->abort_controller()->signal()->aborted()) + return; + + // 3. Assert: event equals navigation's ongoing navigate event. + VERIFY(event == m_ongoing_navigate_event); + + // 4. Set navigation's ongoing navigate event to null. + m_ongoing_navigate_event = nullptr; + + // 5. Finish event given false. + event->finish(false); + + // 6. Fire an event named navigateerror at navigation using ErrorEvent, with error initialized to rejectionReason, and message, + // filename, lineno, and colno initialized to appropriate values that can be extracted from rejectionReason in the same + // underspecified way that the report the exception algorithm does. + ErrorEventInit event_init = {}; + event_init.error = rejection_reason; + // FIXME: Extract information from the exception and the JS context in the wishy-washy way the spec says here. + event_init.filename = String {}; + event_init.colno = 0; + event_init.lineno = 0; + event_init.message = String {}; + + dispatch_event(ErrorEvent::create(realm, EventNames::navigateerror, event_init)); + + // 7. If navigation's transition is not null, then reject navigation's transition's finished promise with rejectionReason. + if (m_transition) + m_transition->finished()->reject(rejection_reason); + + // 8. Set navigation's transition to null. + m_transition = nullptr; + + // 9. If apiMethodTracker is non-null, then reject the finished promise for apiMethodTracker with rejectionReason. + if (api_method_tracker) + reject_the_finished_promise(*api_method_tracker, rejection_reason); + }); + } + + // 34. Otherwise, if apiMethodTracker is non-null, then clean up apiMethodTracker. + else if (api_method_tracker) { + clean_up(*api_method_tracker); + } + + // 35. Clean up after running script given navigation's relevant settings object. + relevant_settings_object(*this).clean_up_after_running_script(); + + // 36. If event's interception state is "none", then return true. + // 37. Return false. + return event->interception_state() == NavigateEvent::InterceptionState::None; +} + } diff --git a/Userland/Libraries/LibWeb/HTML/Navigation.h b/Userland/Libraries/LibWeb/HTML/Navigation.h index b1f144b53d9..1f403c6e014 100644 --- a/Userland/Libraries/LibWeb/HTML/Navigation.h +++ b/Userland/Libraries/LibWeb/HTML/Navigation.h @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include namespace Web::HTML { @@ -133,6 +135,14 @@ private: void reject_the_finished_promise(JS::NonnullGCPtr, JS::Value exception); void clean_up(JS::NonnullGCPtr); + bool inner_navigate_event_firing_algorithm( + Bindings::NavigationType, + JS::NonnullGCPtr, + UserNavigationInvolvement, + Optional&> form_data_entry_list, + Optional download_request_filename, + Optional classic_history_api_state); + // 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. Vector> m_entry_list;