From 24f37c6732f5e0bae86113d8921e067b30b52c6a Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sat, 13 Jun 2026 16:06:49 +0200 Subject: [PATCH] LibWebView: Keep browser history in the UI process Use the LibWebView history mirror to preserve traversable session history across WebContent process swaps. WebContent reports snapshots to the UI process, and new renderers can be seeded from the mirror. Browser back and forward now resolve through the UI-owned used history steps. WebContent still runs the spec traversal path when the current renderer has enough matching state to do so. Handle canceled and no-op UI navigations without leaving speculative history entries or pending WebDriver waits behind. Preserve traversal precheck state across synchronous IPC shutdown, and avoid overwriting a restored target entry's persisted scroll state before the document has adopted that entry. --- Libraries/LibWeb/DOM/Document.cpp | 25 +- Libraries/LibWeb/DOM/Document.h | 1 + Libraries/LibWeb/Dump.cpp | 7 +- Libraries/LibWeb/HTML/HTMLFormElement.cpp | 2 +- Libraries/LibWeb/HTML/Navigable.cpp | 145 ++- Libraries/LibWeb/HTML/Navigable.h | 4 + Libraries/LibWeb/HTML/NavigateEvent.cpp | 10 +- Libraries/LibWeb/HTML/Navigation.cpp | 13 +- Libraries/LibWeb/HTML/Navigation.h | 5 +- .../LibWeb/HTML/TraversableNavigable.cpp | 553 +++++++- Libraries/LibWeb/HTML/TraversableNavigable.h | 28 +- Libraries/LibWeb/Internals/Internals.cpp | 13 +- Libraries/LibWeb/Internals/Internals.h | 1 + Libraries/LibWeb/Internals/Internals.idl | 1 + Libraries/LibWeb/Page/Page.cpp | 24 +- Libraries/LibWeb/Page/Page.h | 28 +- Libraries/LibWeb/WebDriver/Capabilities.cpp | 8 +- Libraries/LibWeb/WebDriver/Capabilities.h | 1 + Libraries/LibWeb/WebDriver/Client.cpp | 5 + Libraries/LibWeb/WebDriver/Client.h | 7 + Libraries/LibWeb/WebDriver/UserPrompt.cpp | 30 + Libraries/LibWeb/WebDriver/UserPrompt.h | 1 + Libraries/LibWebView/Application.cpp | 2 +- Libraries/LibWebView/HelperProcess.cpp | 2 + Libraries/LibWebView/Options.h | 6 + Libraries/LibWebView/ViewImplementation.cpp | 1127 ++++++++++++++++- Libraries/LibWebView/ViewImplementation.h | 124 +- Libraries/LibWebView/WebContentClient.cpp | 206 ++- Libraries/LibWebView/WebContentClient.h | 22 +- Services/WebContent/ConnectionFromClient.cpp | 90 +- Services/WebContent/ConnectionFromClient.h | 12 +- Services/WebContent/PageClient.cpp | 121 +- Services/WebContent/PageClient.h | 34 +- Services/WebContent/WebContentClient.ipc | 23 +- Services/WebContent/WebContentServer.ipc | 14 +- Services/WebContent/WebDriverClient.ipc | 12 +- Services/WebContent/WebDriverConnection.cpp | 395 +++--- Services/WebContent/WebDriverConnection.h | 13 + Services/WebContent/WebDriverServer.ipc | 2 + Services/WebContent/main.cpp | 3 + Services/WebDriver/Client.cpp | 174 ++- Services/WebDriver/Client.h | 5 + Services/WebDriver/Session.cpp | 214 +++- Services/WebDriver/Session.h | 47 +- Services/WebDriver/WebContentConnection.cpp | 12 + Services/WebDriver/WebContentConnection.h | 11 + Services/WebDriver/main.cpp | 4 +- UI/Gtk/BrowserWindow.cpp | 15 +- UI/Gtk/BrowserWindow.h | 1 - UI/Qt/WebContentView.cpp | 4 +- 50 files changed, 3252 insertions(+), 355 deletions(-) diff --git a/Libraries/LibWeb/DOM/Document.cpp b/Libraries/LibWeb/DOM/Document.cpp index 2607e92f8b..2932287e31 100644 --- a/Libraries/LibWeb/DOM/Document.cpp +++ b/Libraries/LibWeb/DOM/Document.cpp @@ -222,6 +222,7 @@ #include #include #include +#include #include #include #include @@ -6652,7 +6653,9 @@ void Document::update_for_history_step_application(NonnullRefPtrnavigable()) + navigable->restore_persisted_state_from_session_history_entry(*entry); // 5. 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, @@ -6674,7 +6677,9 @@ void Document::update_for_history_step_application(NonnullRefPtrnavigable()) + navigable->restore_persisted_state_from_session_history_entry(*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); @@ -8805,8 +8810,8 @@ Document::StepsToFireBeforeunloadResult Document::steps_to_fire_beforeunload(boo // 5. Decrease document's relevant agent's event loop's termination nesting level by 1. event_loop.decrement_termination_nesting_level(); - // FIXME: 6. If all of the following are true: - if (false && + // 6. If all of the following are true: + if ( // - unloadPromptShown is false; !unload_prompt_shown // - document's active sandboxing flag set does not have its sandboxed modals flag set; @@ -8817,10 +8822,18 @@ Document::StepsToFireBeforeunloadResult Document::steps_to_fire_beforeunload(boo && (!event_firing_result || !beforeunload_event->return_value().is_empty()) // - FIXME: showing an unload prompt is unlikely to be annoying, deceptive, or pointless ) { - // FIXME: 1. Set unloadPromptShown to true. + // 1. Set unloadPromptShown to true. + unload_prompt_shown = true; + // FIXME: 2. Invoke WebDriver BiDi user prompt opened with document's relevant global object, "beforeunload", and "". // FIXME: 3. Ask the user to confirm that they wish to unload the document, and pause while waiting for the user's response. - // FIXME: 4. If the user did not confirm the page navigation, set unloadPromptCanceled to true. + + auto user_prompt_handler = WebDriver::get_the_prompt_handler(WebDriver::PromptType::BeforeUnload); + + // 4. If the user did not confirm the page navigation, set unloadPromptCanceled to true. + if (user_prompt_handler.handler == WebDriver::PromptHandler::Dismiss) + unload_prompt_canceled = true; + // FIXME: 5. Invoke WebDriver BiDi user prompt closed with document's relevant global object and true if unloadPromptCanceled is false or false otherwise. } diff --git a/Libraries/LibWeb/DOM/Document.h b/Libraries/LibWeb/DOM/Document.h index ed3b634c63..7e73759fa6 100644 --- a/Libraries/LibWeb/DOM/Document.h +++ b/Libraries/LibWeb/DOM/Document.h @@ -864,6 +864,7 @@ public: void set_deferred_parser_start(GC::Ref>); bool has_deferred_parser_start() const { return m_deferred_parser_start; } + RefPtr latest_entry() const { return m_latest_entry; } void set_latest_entry(RefPtr); void element_id_changed(Badge, GC::Ref element, Optional old_id); diff --git a/Libraries/LibWeb/Dump.cpp b/Libraries/LibWeb/Dump.cpp index 535109fc20..bd7295b507 100644 --- a/Libraries/LibWeb/Dump.cpp +++ b/Libraries/LibWeb/Dump.cpp @@ -59,7 +59,12 @@ namespace Web { static void dump_session_history_entry(StringBuilder& builder, HTML::SessionHistoryEntry const& session_history_entry, int indent_levels) { dump_indent(builder, indent_levels); - builder.appendff("step=({}) url=({})\n", session_history_entry.step().get(), session_history_entry.url()); + builder.appendff("step=({}) url=({})", session_history_entry.step().get(), session_history_entry.url()); + if (session_history_entry.scroll_position_data().viewport_scroll_position.has_value()) { + auto const& viewport_scroll_position = *session_history_entry.scroll_position_data().viewport_scroll_position; + builder.appendff(" viewport-scroll=({}, {})", viewport_scroll_position.x(), viewport_scroll_position.y()); + } + builder.append('\n'); for (auto const& nested_history : session_history_entry.document_state()->nested_histories()) { for (auto const& nested_she : nested_history.entries) { dump_session_history_entry(builder, *nested_she, indent_levels + 1); diff --git a/Libraries/LibWeb/HTML/HTMLFormElement.cpp b/Libraries/LibWeb/HTML/HTMLFormElement.cpp index 2160f2dc4a..684049dd32 100644 --- a/Libraries/LibWeb/HTML/HTMLFormElement.cpp +++ b/Libraries/LibWeb/HTML/HTMLFormElement.cpp @@ -851,7 +851,7 @@ ErrorOr HTMLFormElement::submit_as_entity_body(URL::URL parsed_action, GC: // 2. Let mimeType be the isomorphic encoding of the concatenation of "multipart/form-data; boundary=" and the multipart/form-data // boundary string generated by the multipart/form-data encoding algorithm. mime_type = POSTResource::RequestContentType::MultipartFormData; - mime_type_directives.empend("boundary"sv, move(body_and_mime_type.boundary)); + mime_type_directives.empend(TRY(String::from_utf8("boundary"sv)), move(body_and_mime_type.boundary)); break; } case EncodingTypeAttributeState::PlainText: { diff --git a/Libraries/LibWeb/HTML/Navigable.cpp b/Libraries/LibWeb/HTML/Navigable.cpp index 49cbebb129..e967eb2b6a 100644 --- a/Libraries/LibWeb/HTML/Navigable.cpp +++ b/Libraries/LibWeb/HTML/Navigable.cpp @@ -473,7 +473,8 @@ RefPtr Navigable::get_the_target_history_entry(int target_s // https://html.spec.whatwg.org/multipage/browsing-the-web.html#activate-history-entry void Navigable::activate_history_entry(RefPtr entry, GC::Ref document) { - // FIXME: 1. Save persisted state to the navigable's active session history entry. + // 1. Save persisted state to the navigable's active session history entry. + save_persisted_state_to_active_session_history_entry(); // 2. Let newDocument be entry's document. auto new_document = document; @@ -523,6 +524,51 @@ void Navigable::activate_history_entry(RefPtr entry, GC::Re } } +// https://html.spec.whatwg.org/multipage/browsing-the-web.html#save-persisted-state +void Navigable::save_persisted_state_to_active_session_history_entry() +{ + auto entry = active_session_history_entry(); + if (!entry) + return; + + // 1. Set the scroll position data of entry to contain the scroll positions for all of entry's document's + // restorable scrollable regions. + auto scroll_position_data = entry->scroll_position_data(); + scroll_position_data.viewport_scroll_position = viewport_scroll_offset(); + entry->set_scroll_position_data(move(scroll_position_data)); + + // FIXME: 2. Optionally, update entry's persisted user state. +} + +// https://html.spec.whatwg.org/multipage/browsing-the-web.html#restore-persisted-user-state +void Navigable::restore_persisted_state_from_session_history_entry(SessionHistoryEntry const& entry) +{ + // 1. If entry's scroll restoration mode is "auto", and entry's document's relevant global object's navigation + // API's suppress normal scroll restoration during ongoing navigation is false, then restore scroll position + // data given entry. + if (entry.scroll_restoration_mode() == ScrollRestorationMode::Auto) { + if (auto window = active_window()) { + if (!window->navigation()->suppress_normal_scroll_restoration_during_ongoing_navigation()) + restore_scroll_position_data(entry); + } + } + + // FIXME: 2. Optionally, update other aspects of entry's document and its rendering, for instance values of form + // fields, that the user agent had previously recorded in entry's persisted user state. +} + +// https://html.spec.whatwg.org/multipage/browsing-the-web.html#restore-scroll-position-data +void Navigable::restore_scroll_position_data(SessionHistoryEntry const& entry) +{ + auto const& scroll_position_data = entry.scroll_position_data(); + if (!scroll_position_data.viewport_scroll_position.has_value()) + return; + + // FIXME: If the document has been scrolled by the user, return. + perform_scroll_of_viewport_scrolling_box(*scroll_position_data.viewport_scroll_position); + clamp_viewport_scroll_offset(); +} + // https://html.spec.whatwg.org/multipage/document-sequences.html#nav-document GC::Ptr Navigable::active_document() const { @@ -1838,6 +1884,30 @@ void Navigable::populate_session_history_entry_document( } } +static Bindings::NavigationHistoryBehavior determine_history_handling_for_navigation(Bindings::NavigationHistoryBehavior history_handling, URL::URL const& url, DOM::Document const& active_document, URL::Origin const& initiator_origin_snapshot) +{ + // 12. If historyHandling is "auto", then: + if (history_handling == Bindings::NavigationHistoryBehavior::Auto) { + // NB: The spec says "targetNavigable" here, but this algorithm has a "navigable". + // 1. If url equals navigable's active document's URL, + // and initiatorOriginSnapshot is same origin with targetNavigable's active document's origin, + // then set historyHandling to "replace". + if (url == active_document.url() && initiator_origin_snapshot.is_same_origin(active_document.origin())) + history_handling = Bindings::NavigationHistoryBehavior::Replace; + + // 2. Otherwise, set historyHandling to "push". + else + history_handling = Bindings::NavigationHistoryBehavior::Push; + } + + // 13. If the navigation must be a replace given url and navigable's active document, then set historyHandling to + // "replace". + if (navigation_must_be_a_replace(url, active_document)) + history_handling = Bindings::NavigationHistoryBehavior::Replace; + + return history_handling; +} + WebIDL::ExceptionOr Navigable::navigate(NavigateParams params) { // AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window. @@ -1849,13 +1919,6 @@ WebIDL::ExceptionOr Navigable::navigate(NavigateParams params) auto& active_document = *this->active_document(); auto& realm = active_document.realm(); - auto& page_client = active_document.page().client(); - - // AD-HOC: If we are not able to continue in this process, request a new process from the UI. - if (is_top_level_traversable() && !page_client.is_url_suitable_for_same_process_navigation(active_document.url(), params.url)) { - page_client.request_new_process_for_navigation(params.url); - return {}; - } // 2. Let sourceSnapshotParams be the result of snapshotting source snapshot params given sourceDocument. auto source_snapshot_params = source_document->snapshot_source_snapshot_params(); @@ -1994,23 +2057,8 @@ void Navigable::begin_navigation(NavigateParams params) } } - // 12. If historyHandling is "auto", then: - if (history_handling == Bindings::NavigationHistoryBehavior::Auto) { - // FIXME: Fix spec typo targetNavigable --> navigable - // 1. If url equals navigable's active document's URL, - // and initiatorOriginSnapshot is same origin with targetNavigable's active document's origin, - // then set historyHandling to "replace". - if (url == active_document.url() && initiator_origin_snapshot.is_same_origin(active_document.origin())) - history_handling = Bindings::NavigationHistoryBehavior::Replace; - - // 2. Otherwise, set historyHandling to "push". - else - history_handling = Bindings::NavigationHistoryBehavior::Push; - } - - // 13. If the navigation must be a replace given url and navigable's active document, then set historyHandling to "replace". - if (navigation_must_be_a_replace(url, active_document)) - history_handling = Bindings::NavigationHistoryBehavior::Replace; + // 12-13. Determine historyHandling for this navigation. + history_handling = determine_history_handling_for_navigation(history_handling, url, active_document, initiator_origin_snapshot); // 14. If all of the following are true: // - documentResource is null; @@ -2113,11 +2161,6 @@ void Navigable::begin_navigation(NavigateParams params) return; } - // AD-HOC: Tell the UI that we started loading. - if (is_top_level_traversable()) { - active_browsing_context()->page().client().page_did_start_loading(url, false); - } - // FIXME: 22. If sourceDocument is navigable's container document, then reserve deferred fetch quota for navigable's container given url's origin. // 23. In parallel, run these steps: @@ -2132,15 +2175,34 @@ void Navigable::begin_navigation(NavigateParams params) traversable_navigable()->check_if_unloading_is_canceled(this->active_document()->inclusive_descendant_navigables(), GC::create_function(heap(), [this, source_snapshot_params, target_snapshot_params, csp_navigation_type, document_resource, url, navigation_id, referrer_policy, initiator_origin_snapshot, response, history_handling, initiator_base_url_snapshot, user_involvement](TraversableNavigable::CheckIfUnloadingIsCanceledResult unload_prompt_canceled) { // 2. If unloadPromptCanceled is not "continue", or navigable's ongoing navigation is no longer navigationId: - if (unload_prompt_canceled != TraversableNavigable::CheckIfUnloadingIsCanceledResult::Continue || ongoing_navigation() != navigation_id) { + if (unload_prompt_canceled != TraversableNavigable::CheckIfUnloadingIsCanceledResult::Continue) { // FIXME: 1. Invoke WebDriver BiDi navigation failed with navigable and a new WebDriver BiDi navigation status whose id is navigationId, status is "canceled", and url is url. + if (is_top_level_traversable()) + active_browsing_context()->page().client().page_did_cancel_loading(url); // 2. Abort these steps. set_delaying_load_events(false); return; } - // AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window. + if (ongoing_navigation() != navigation_id) { + set_delaying_load_events(false); + return; + } + + // AD-HOC: If we are not able to continue in this process, request a new process from the UI. + if (is_top_level_traversable() && !active_browsing_context()->page().client().is_url_suitable_for_same_process_navigation(this->active_document()->url(), url)) { + active_browsing_context()->page().client().request_new_process_for_navigation(url, document_resource, history_handling); + set_delaying_load_events(false); + return; + } + + // AD-HOC: Tell the UI that we started loading. + if (is_top_level_traversable()) { + active_browsing_context()->page().client().page_did_start_loading(url, document_resource, false, history_handling); + } + + // AD-HOC: Subsequent steps will fail if the navigable doesn't have an active window. if (!active_window()) { set_delaying_load_events(false); return; @@ -2327,6 +2389,9 @@ void Navigable::navigate_to_a_fragment(URL::URL const& url, HistoryHandlingBehav if (!continue_) return; + save_persisted_state_to_active_session_history_entry(); + auto active_entry = active_session_history_entry(); + // 6. Let historyEntry be a new session history entry, with // URL: url // document state: navigable's active session history entry's document state @@ -2334,12 +2399,13 @@ void Navigable::navigate_to_a_fragment(URL::URL const& url, HistoryHandlingBehav // scroll restoration mode: navigable's active session history entry's scroll restoration mode auto history_entry = SessionHistoryEntry::create(); history_entry->set_url(url); - history_entry->set_document_state(active_session_history_entry()->document_state()); + history_entry->set_document_state(active_entry->document_state()); history_entry->set_navigation_api_state(destination_navigation_api_state); - history_entry->set_scroll_restoration_mode(active_session_history_entry()->scroll_restoration_mode()); + history_entry->set_scroll_restoration_mode(active_entry->scroll_restoration_mode()); + history_entry->set_scroll_position_data(active_entry->scroll_position_data()); // 7. Let entryToReplace be navigable's active session history entry if historyHandling is "replace", otherwise null. - auto entry_to_replace = history_handling == HistoryHandlingBehavior::Replace ? active_session_history_entry() : nullptr; + auto entry_to_replace = history_handling == HistoryHandlingBehavior::Replace ? active_entry : nullptr; // 8. Let history be navigable's active document's history object. auto history = active_document()->history(); @@ -2641,6 +2707,13 @@ void Navigable::reload(Optional navigation_api_state, UserN // 3. Let traversable be navigable's traversable navigable. auto traversable = traversable_navigable(); + // AD-HOC: Report the reload-pending document state to the UI process before the reload history step finishes, + // so the UI-owned session history mirror remains synchronized during an in-flight reload. + if (traversable->page().client().should_report_session_history_updates()) { + auto session_history_snapshot = traversable->create_session_history_snapshot(); + traversable->page().client().page_did_update_session_history(session_history_snapshot.top_level_session_history_entries, session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index); + } + // 4. Append the following session history traversal steps to traversable: traversable->append_session_history_traversal_steps(GC::create_function(heap(), [traversable, user_involvement](NonnullRefPtr> signal) { // 1. Apply the reload history step to traversable given userInvolvement. @@ -2832,6 +2905,7 @@ void perform_url_and_history_update_steps(DOM::Document& document, URL::URL new_ // 2. Let activeEntry be navigable's active session history entry. auto active_entry = navigable->active_session_history_entry(); + navigable->save_persisted_state_to_active_session_history_entry(); // 3. Let newEntry be a new session history entry, with // URL: newURL @@ -2844,6 +2918,7 @@ void perform_url_and_history_update_steps(DOM::Document& document, URL::URL new_ new_entry->set_classic_history_api_state(serialized_data.value_or(active_entry->classic_history_api_state())); new_entry->set_document_state(active_entry->document_state()); new_entry->set_scroll_restoration_mode(active_entry->scroll_restoration_mode()); + new_entry->set_scroll_position_data(active_entry->scroll_position_data()); // 4. If document's is initial about:blank is true, then set historyHandling to "replace". if (document.is_initial_about_blank()) { diff --git a/Libraries/LibWeb/HTML/Navigable.h b/Libraries/LibWeb/HTML/Navigable.h index 172c6c0033..9ee5392083 100644 --- a/Libraries/LibWeb/HTML/Navigable.h +++ b/Libraries/LibWeb/HTML/Navigable.h @@ -115,6 +115,10 @@ public: RefPtr get_the_target_history_entry(int target_step) const; + void save_persisted_state_to_active_session_history_entry(); + void restore_persisted_state_from_session_history_entry(SessionHistoryEntry const&); + void restore_scroll_position_data(SessionHistoryEntry const&); + String target_name() const; GC::Ptr container() const; diff --git a/Libraries/LibWeb/HTML/NavigateEvent.cpp b/Libraries/LibWeb/HTML/NavigateEvent.cpp index 35cc6c5337..f6ac195d0d 100644 --- a/Libraries/LibWeb/HTML/NavigateEvent.cpp +++ b/Libraries/LibWeb/HTML/NavigateEvent.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -184,10 +185,13 @@ void NavigateEvent::process_scroll_behavior() // 2. Set event's interception state to "scrolled". m_interception_state = InterceptionState::Scrolled; - // FIXME: 3. If event's navigationType was initialized to "traverse" or "reload", then restore scroll position data - // given event's relevant global object's navigable's active session history entry. + // 3. If event's navigationType was initialized to "traverse" or "reload", then restore scroll position data + // given event's relevant global object's navigable's active session history entry. if (m_navigation_type == Bindings::NavigationType::Traverse || m_navigation_type == Bindings::NavigationType::Reload) { - dbgln("FIXME: restore scroll position data after traversal or reload navigation"); + auto navigable = as(HTML::relevant_global_object(*this)).navigable(); + VERIFY(navigable); + if (auto active_entry = navigable->active_session_history_entry()) + navigable->restore_scroll_position_data(*active_entry); } // 4. Otherwise: diff --git a/Libraries/LibWeb/HTML/Navigation.cpp b/Libraries/LibWeb/HTML/Navigation.cpp index d616d4c09d..6a1bee4a42 100644 --- a/Libraries/LibWeb/HTML/Navigation.cpp +++ b/Libraries/LibWeb/HTML/Navigation.cpp @@ -758,7 +758,7 @@ void Navigation::abort_the_ongoing_navigation(GC::Ptr erro m_focus_changed_during_ongoing_navigation = false; // 4. Set navigation's suppress normal scroll restoration during ongoing navigation to false. - m_suppress_scroll_restoration_during_ongoing_navigation = false; + m_suppress_normal_scroll_restoration_during_ongoing_navigation = false; // 5. If error was not given, then let error be a new "AbortError" DOMException created in navigation's relevant realm. if (!error) @@ -1193,7 +1193,7 @@ bool Navigation::inner_navigate_event_firing_algorithm( m_focus_changed_during_ongoing_navigation = false; // 28. Set navigation's suppress normal scroll restoration during ongoing navigation to false. - m_suppress_scroll_restoration_during_ongoing_navigation = false; + m_suppress_normal_scroll_restoration_during_ongoing_navigation = false; // 29. Let dispatchResult be the result of dispatching event at navigation. auto dispatch_result = dispatch_event(*event); @@ -1254,7 +1254,7 @@ bool Navigation::inner_navigate_event_firing_algorithm( // 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. - m_suppress_scroll_restoration_during_ongoing_navigation = true; + m_suppress_normal_scroll_restoration_during_ongoing_navigation = true; // 2. Let userInvolvement be "none". auto user_involvement_for_resume = UserNavigationInvolvement::None; @@ -1495,6 +1495,13 @@ void Navigation::initialize_the_navigation_api_entries_for_a_new_document(Vector m_current_entry_index = get_the_navigation_api_entry_index(*initial_she); } +void Navigation::initialize_the_navigation_api_entries_for_reconstructed_session_history(Vector> const& new_shes, NonnullRefPtr initial_she) +{ + m_entry_list.clear(); + m_current_entry_index = -1; + initialize_the_navigation_api_entries_for_a_new_document(new_shes, move(initial_she)); +} + // https://html.spec.whatwg.org/multipage/nav-history-apis.html#update-the-navigation-api-entries-for-a-same-document-navigation void Navigation::update_the_navigation_api_entries_for_a_same_document_navigation(NonnullRefPtr destination_she, Bindings::NavigationType navigation_type) { diff --git a/Libraries/LibWeb/HTML/Navigation.h b/Libraries/LibWeb/HTML/Navigation.h index eb9798fe11..152f7a7f3a 100644 --- a/Libraries/LibWeb/HTML/Navigation.h +++ b/Libraries/LibWeb/HTML/Navigation.h @@ -97,6 +97,7 @@ public: bool fire_a_download_request_navigate_event(URL::URL destination_url, UserNavigationInvolvement user_involvement, GC::Ptr source_element, String filename); void initialize_the_navigation_api_entries_for_a_new_document(Vector> const& new_shes, NonnullRefPtr initial_she); + void initialize_the_navigation_api_entries_for_reconstructed_session_history(Vector> const& new_shes, NonnullRefPtr initial_she); void update_the_navigation_api_entries_for_a_same_document_navigation(NonnullRefPtr destination_she, Bindings::NavigationType); virtual ~Navigation() override; @@ -107,6 +108,8 @@ public: bool focus_changed_during_ongoing_navigation() const { return m_focus_changed_during_ongoing_navigation; } void set_focus_changed_during_ongoing_navigation(bool b) { m_focus_changed_during_ongoing_navigation = b; } + bool suppress_normal_scroll_restoration_during_ongoing_navigation() const { return m_suppress_normal_scroll_restoration_during_ongoing_navigation; } + void set_was_initial_about_blank_opened(bool b) { m_was_initial_about_blank_opened = b; } private: @@ -156,7 +159,7 @@ private: bool m_focus_changed_during_ongoing_navigation { false }; // https://html.spec.whatwg.org/multipage/nav-history-apis.html#suppress-normal-scroll-restoration-during-ongoing-navigation - bool m_suppress_scroll_restoration_during_ongoing_navigation { false }; + bool m_suppress_normal_scroll_restoration_during_ongoing_navigation { false }; // https://html.spec.whatwg.org/multipage/nav-history-apis.html#ongoing-api-method-tracker GC::Ptr m_ongoing_api_method_tracker = nullptr; diff --git a/Libraries/LibWeb/HTML/TraversableNavigable.cpp b/Libraries/LibWeb/HTML/TraversableNavigable.cpp index 64d09357f7..fd8d846fcb 100644 --- a/Libraries/LibWeb/HTML/TraversableNavigable.cpp +++ b/Libraries/LibWeb/HTML/TraversableNavigable.cpp @@ -6,7 +6,9 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include +#include #include #include #include @@ -197,6 +199,232 @@ bool TraversableNavigable::is_top_level_traversable() const return parent() == nullptr; } +static bool session_history_entry_descriptors_are_valid(Vector const& entries) +{ + Optional previous_step; + for (auto const& entry : entries) { + if (entry.step < 0) + return false; + if (previous_step.has_value() && entry.step <= *previous_step) + return false; + for (auto const& nested_history : entry.document_state.nested_histories) { + if (!session_history_entry_descriptors_are_valid(nested_history.entries)) + return false; + } + previous_step = entry.step; + } + return true; +} + +struct SessionHistoryEntryReconstructionState { + HashMap> document_states; +}; + +static NonnullRefPtr create_session_history_entry_from_ui_process(SessionHistoryEntryDescriptor, SessionHistoryEntryReconstructionState&); + +static DocumentState::NestedHistory create_nested_history_from_ui_process(SessionHistoryNestedHistoryDescriptor nested_history_descriptor, SessionHistoryEntryReconstructionState& reconstruction_state) +{ + Vector> entries; + entries.ensure_capacity(nested_history_descriptor.entries.size()); + for (auto& entry_descriptor : nested_history_descriptor.entries) + entries.unchecked_append(create_session_history_entry_from_ui_process(move(entry_descriptor), reconstruction_state)); + + return { + .id = move(nested_history_descriptor.id), + .entries = move(entries), + }; +} + +static void populate_nested_histories_from_ui_process(DocumentState& document_state, Vector nested_history_descriptors, SessionHistoryEntryReconstructionState& reconstruction_state) +{ + auto& nested_histories = document_state.nested_histories(); + if (nested_histories.size() == nested_history_descriptors.size()) { + // NB: The UI process keeps session history across WebContent process + // swaps, but nested history ids are process-local navigable ids. When + // rebuilding history for an already-loaded document, preserve the live + // ids created by the new WebContent process. + for (size_t i = 0; i < nested_history_descriptors.size(); ++i) + nested_history_descriptors[i].id = nested_histories[i].id; + } + nested_histories.clear(); + nested_histories.ensure_capacity(nested_history_descriptors.size()); + for (auto& nested_history_descriptor : nested_history_descriptors) + nested_histories.unchecked_append(create_nested_history_from_ui_process(move(nested_history_descriptor), reconstruction_state)); +} + +static void apply_session_history_entry_descriptor_from_ui_process(SessionHistoryEntry& entry, SessionHistoryEntryDescriptor& entry_descriptor) +{ + entry.set_url(move(entry_descriptor.url)); + entry.set_step(static_cast(entry_descriptor.step)); + // NB: Older UI-process mirrors can carry an empty serialization record for + // provisional entries. Do not preserve stale state from a reused entry, but + // also do not install an invalid record that would crash when restored. + auto& vm = Bindings::main_thread_vm(); + if (entry_descriptor.classic_history_api_state.is_empty()) + entry.set_classic_history_api_state(MUST(structured_serialize_for_storage(vm, JS::js_null()))); + else + entry.set_classic_history_api_state(move(entry_descriptor.classic_history_api_state)); + if (entry_descriptor.navigation_api_state.is_empty()) + entry.set_navigation_api_state(MUST(structured_serialize_for_storage(vm, JS::js_undefined()))); + else + entry.set_navigation_api_state(move(entry_descriptor.navigation_api_state)); + entry.set_navigation_api_key(move(entry_descriptor.navigation_api_key)); + entry.set_navigation_api_id(move(entry_descriptor.navigation_api_id)); + entry.set_scroll_restoration_mode(entry_descriptor.scroll_restoration_mode); + entry.set_scroll_position_data(move(entry_descriptor.scroll_position_data)); +} + +static void apply_session_history_document_state_descriptor_from_ui_process(DocumentState& document_state, SessionHistoryDocumentStateDescriptor const& document_state_descriptor) +{ + document_state.set_history_policy_container(document_state_descriptor.history_policy_container); + document_state.set_request_referrer(document_state_descriptor.request_referrer); + document_state.set_request_referrer_policy(document_state_descriptor.request_referrer_policy); + document_state.set_initiator_origin(document_state_descriptor.initiator_origin); + document_state.set_origin(document_state_descriptor.origin); + document_state.set_about_base_url(document_state_descriptor.about_base_url); + document_state.set_resource(document_state_descriptor.resource); + document_state.set_reload_pending(document_state_descriptor.reload_pending); + // AD-HOC: Descriptor ID 0 marks a provisional UI-created document state whose entry has not yet been populated + // by WebContent. Treat it as already populated when reseeding the active entry, since a replacement + // WebContent process already has the active document for that entry. + document_state.set_ever_populated(document_state_descriptor.id == 0 ? true : document_state_descriptor.ever_populated); + document_state.set_navigable_target_name(document_state_descriptor.navigable_target_name); +} + +static RefPtr get_or_create_document_state_from_ui_process(SessionHistoryDocumentStateDescriptor const& document_state_descriptor, SessionHistoryEntryReconstructionState& reconstruction_state) +{ + RefPtr document_state; + if (document_state_descriptor.id != 0) { + if (auto existing_document_state = reconstruction_state.document_states.get(document_state_descriptor.id); existing_document_state.has_value()) + document_state = *existing_document_state; + } + + if (!document_state) { + document_state = DocumentState::create(); + if (document_state_descriptor.id != 0) + reconstruction_state.document_states.set(document_state_descriptor.id, document_state); + } + + apply_session_history_document_state_descriptor_from_ui_process(*document_state, document_state_descriptor); + return document_state; +} + +static NonnullRefPtr create_session_history_entry_from_ui_process(SessionHistoryEntryDescriptor entry_descriptor, SessionHistoryEntryReconstructionState& reconstruction_state) +{ + auto entry = SessionHistoryEntry::create(); + apply_session_history_entry_descriptor_from_ui_process(*entry, entry_descriptor); + + auto document_state = get_or_create_document_state_from_ui_process(entry_descriptor.document_state, reconstruction_state); + VERIFY(document_state); + populate_nested_histories_from_ui_process(*document_state, move(entry_descriptor.document_state.nested_histories), reconstruction_state); + entry->set_document_state(move(document_state)); + return entry; +} + +bool TraversableNavigable::replace_top_level_session_history_entries_from_ui_process(Vector entries_from_ui_process, size_t current_top_level_entry_index) +{ + if (entries_from_ui_process.is_empty() || current_top_level_entry_index >= entries_from_ui_process.size()) + return false; + + VERIFY(is_top_level_traversable()); + + if (!session_history_entry_descriptors_are_valid(entries_from_ui_process)) + return false; + + // NB: The UI process stores a traversable's top-level session history entries + // across WebContent process swaps. When seeding a fresh WebContent process, + // current_top_level_entry_index is an index into the traversable's session + // history entries list, not an index into the result of getting all used + // history steps. + // https://html.spec.whatwg.org/multipage/document-sequences.html#tn-session-history-entries + // https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-all-used-history-steps + auto active_entry = active_session_history_entry(); + VERIFY(active_entry); + + SessionHistoryEntryReconstructionState reconstruction_state; + if (entries_from_ui_process[current_top_level_entry_index].document_state.id != 0) { + auto active_document_state = active_entry->document_state(); + VERIFY(active_document_state); + reconstruction_state.document_states.set(entries_from_ui_process[current_top_level_entry_index].document_state.id, active_document_state); + } + + Vector> entries; + entries.ensure_capacity(entries_from_ui_process.size()); + for (size_t i = 0; i < entries_from_ui_process.size(); ++i) { + auto entry_descriptor = move(entries_from_ui_process[i]); + NonnullRefPtr entry = *active_entry; + if (i == current_top_level_entry_index) { + VERIFY(entry->document_state()); + apply_session_history_entry_descriptor_from_ui_process(*entry, entry_descriptor); + apply_session_history_document_state_descriptor_from_ui_process(*entry->document_state(), entry_descriptor.document_state); + populate_nested_histories_from_ui_process(*entry->document_state(), move(entry_descriptor.document_state.nested_histories), reconstruction_state); + } else { + entry = create_session_history_entry_from_ui_process(move(entry_descriptor), reconstruction_state); + } + + entries.unchecked_append(move(entry)); + } + + m_session_history_entries = move(entries); + auto current_entry = m_session_history_entries[current_top_level_entry_index]; + set_active_session_history_entry(current_entry); + set_current_session_history_entry(current_entry); + m_current_session_history_step = current_entry->step().get(); + + auto document = active_document(); + VERIFY(document); + auto history_object_length_and_index = get_the_history_object_length_and_index(m_current_session_history_step); + document->history()->m_index = history_object_length_and_index.script_history_index; + document->history()->m_length = history_object_length_and_index.script_history_length; + + // NB: The UI process can seed a replacement WebContent process before the new document has loaded. Do not + // restore the UI-owned entry's classic history API state or persisted state onto the initial about:blank + // document; the navigation algorithm will restore them onto the document that is actually created for the + // entry. + if (!document->is_initial_about_blank()) { + document->restore_the_history_object_state(current_entry); + restore_persisted_state_from_session_history_entry(*current_entry); + } + + auto entries_for_navigation_api = get_session_history_entries_for_the_navigation_api(*this, m_current_session_history_step); + active_window()->navigation()->initialize_the_navigation_api_entries_for_reconstructed_session_history(entries_for_navigation_api, current_entry); + return true; +} + +void TraversableNavigable::reset_session_history_for_testing(GC::Ref> on_complete) +{ + append_session_history_traversal_steps(GC::create_function(heap(), [this, on_complete](NonnullRefPtr> signal) { + auto maybe_active_entry = active_session_history_entry(); + VERIFY(maybe_active_entry); + auto active_entry = maybe_active_entry.release_nonnull(); + + active_entry->set_step(0); + m_session_history_entries.clear(); + m_session_history_entries.append(active_entry); + set_active_session_history_entry(active_entry); + set_current_session_history_entry(active_entry); + m_current_session_history_step = 0; + + auto document = active_document(); + VERIFY(document); + auto history_object_length_and_index = get_the_history_object_length_and_index(m_current_session_history_step); + document->history()->m_index = history_object_length_and_index.script_history_index; + document->history()->m_length = history_object_length_and_index.script_history_length; + + auto entries_for_navigation_api = get_session_history_entries_for_the_navigation_api(*this, m_current_session_history_step); + active_window()->navigation()->initialize_the_navigation_api_entries_for_reconstructed_session_history(entries_for_navigation_api, active_entry); + + if (page().client().should_report_session_history_updates()) { + auto session_history_snapshot = create_session_history_snapshot(); + page().client().page_did_update_session_history(session_history_snapshot.top_level_session_history_entries, session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index); + } + + page().client().page_did_update_navigation_buttons_state(false, false); + signal->resolve({}); + on_complete->function()(); + })); +} + // https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-all-used-history-steps Vector TraversableNavigable::get_all_used_history_steps() const { @@ -787,7 +1015,7 @@ void ApplyHistoryStepState::start() || target_entry->document_state()->reload_pending()); if (needs_population) { if (target_entry->document_state()->reload_pending() && navigable->is_top_level_traversable()) - navigable->page().client().page_did_start_loading(target_entry->url(), false); + navigable->page().client().page_did_start_loading(target_entry->url(), Empty {}, false); // FIXME: 1. Let navTimingType be "back_forward" if targetEntry's document is null; otherwise "reload". @@ -1022,6 +1250,14 @@ void ApplyHistoryStepState::process_continuations() // 2. Queue a global task on the navigation and traversal task source given navigable's active window to perform afterPotentialUnloads. queue_global_task(Task::Source::NavigationAndTraversal, *navigable->active_window(), after_potential_unload); } + // AD-HOC: During navigable creation, the initial about:blank document can be + // replaced by the container's initial navigation while applying the + // creation/destruction history step. That hook passes a null + // navigationType per spec, and there is no outgoing document to unload. + else if (!m_navigation_type.has_value() && displayed_document->is_initial_about_blank()) { + navigable->set_ongoing_navigation({}, m_navigation_api_abort_behavior); + after_potential_unload->function()(); + } // 11. Otherwise: else { // 1. Assert: navigationType is not null. @@ -1078,6 +1314,106 @@ void ApplyHistoryStepState::enter_waiting_for_non_changing_jobs() try_advance(); } +struct SessionHistoryEntryDescriptorCreationState { + HashMap document_state_ids; + u64 next_document_state_id { 1 }; +}; + +static u64 document_state_id_for_descriptor(DocumentState const& document_state, SessionHistoryEntryDescriptorCreationState& creation_state) +{ + if (auto id = creation_state.document_state_ids.get(&document_state); id.has_value()) + return *id; + + auto id = creation_state.next_document_state_id++; + VERIFY(id != 0); + creation_state.document_state_ids.set(&document_state, id); + return id; +} + +static SessionHistoryEntryDescriptor create_session_history_entry_descriptor(SessionHistoryEntry const&, SessionHistoryEntryDescriptorCreationState&); + +static SessionHistoryDocumentStateDescriptor create_session_history_document_state_descriptor(DocumentState const& document_state, SessionHistoryEntryDescriptorCreationState& creation_state) +{ + Vector nested_history_descriptors; + nested_history_descriptors.ensure_capacity(document_state.nested_histories().size()); + for (auto const& nested_history : document_state.nested_histories()) { + Vector nested_entry_descriptors; + nested_entry_descriptors.ensure_capacity(nested_history.entries.size()); + for (auto const& nested_entry : nested_history.entries) + nested_entry_descriptors.unchecked_append(create_session_history_entry_descriptor(nested_entry, creation_state)); + + nested_history_descriptors.unchecked_append({ + .id = nested_history.id, + .entries = move(nested_entry_descriptors), + }); + } + + return { + .id = document_state_id_for_descriptor(document_state, creation_state), + .history_policy_container = document_state.history_policy_container(), + .request_referrer = document_state.request_referrer(), + .request_referrer_policy = document_state.request_referrer_policy(), + .initiator_origin = document_state.initiator_origin(), + .origin = document_state.origin(), + .about_base_url = document_state.about_base_url(), + .resource = document_state.resource(), + .reload_pending = document_state.reload_pending(), + .ever_populated = document_state.ever_populated(), + .navigable_target_name = document_state.navigable_target_name(), + .nested_histories = move(nested_history_descriptors), + }; +} + +static SessionHistoryEntryDescriptor create_session_history_entry_descriptor(SessionHistoryEntry const& entry, SessionHistoryEntryDescriptorCreationState& creation_state) +{ + SessionHistoryDocumentStateDescriptor document_state_descriptor; + if (auto document_state = entry.document_state()) { + document_state_descriptor = create_session_history_document_state_descriptor(*document_state, creation_state); + } + + return { + .step = static_cast(entry.step().get()), + .url = entry.url(), + .document_state = move(document_state_descriptor), + .classic_history_api_state = entry.classic_history_api_state(), + .navigation_api_state = entry.navigation_api_state(), + .navigation_api_key = entry.navigation_api_key(), + .navigation_api_id = entry.navigation_api_id(), + .scroll_restoration_mode = entry.scroll_restoration_mode(), + .scroll_position_data = entry.scroll_position_data(), + }; +} + +TraversableNavigable::SessionHistorySnapshot TraversableNavigable::create_session_history_snapshot(SaveActiveEntryPersistedState save_active_entry_persisted_state) +{ + if (save_active_entry_persisted_state == SaveActiveEntryPersistedState::Yes) + save_persisted_state_to_active_session_history_entry(); + + Vector top_level_session_history_entries; + top_level_session_history_entries.ensure_capacity(session_history_entries().size()); + SessionHistoryEntryDescriptorCreationState creation_state; + for (auto const& entry : session_history_entries()) + top_level_session_history_entries.unchecked_append(create_session_history_entry_descriptor(entry, creation_state)); + + auto used_history_steps = get_all_used_history_steps(); + Vector used_session_history_steps; + used_session_history_steps.ensure_capacity(used_history_steps.size()); + Optional current_used_step_index; + for (size_t i = 0; i < used_history_steps.size(); ++i) { + auto step = used_history_steps[i]; + used_session_history_steps.unchecked_append(static_cast(step)); + if (step == current_session_history_step()) + current_used_step_index = i; + } + VERIFY(current_used_step_index.has_value()); + + return { + .top_level_session_history_entries = move(top_level_session_history_entries), + .used_session_history_steps = move(used_session_history_steps), + .current_used_step_index = *current_used_step_index, + }; +} + void ApplyHistoryStepState::complete() { if (m_phase == Phase::Completed) @@ -1088,9 +1424,25 @@ void ApplyHistoryStepState::complete() // 20. Set traversable's current session history step to targetStep. m_traversable->m_current_session_history_step = m_target_step; - // Not in the spec: - auto back_enabled = m_traversable->m_current_session_history_step > 0; + // AD-HOC: Report the updated session history descriptors to the UI-process mirror. + if (m_traversable->page().client().should_report_session_history_updates()) { + auto save_active_entry_persisted_state = TraversableNavigable::SaveActiveEntryPersistedState::Yes; + // NB: During history traversal, the active entry can point at the target + // entry before the active document's queued history-step update has + // restored the target entry's persisted state. Do not overwrite that + // target entry with the document's pre-restoration viewport offset. + if (m_navigation_type == Bindings::NavigationType::Traverse) { + auto document = m_traversable->active_document(); + auto active_entry = m_traversable->active_session_history_entry(); + if (document && active_entry && document->latest_entry() != active_entry) + save_active_entry_persisted_state = TraversableNavigable::SaveActiveEntryPersistedState::No; + } + auto session_history_snapshot = m_traversable->create_session_history_snapshot(save_active_entry_persisted_state); + m_traversable->page().client().page_did_update_session_history(session_history_snapshot.top_level_session_history_entries, session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index); + } + VERIFY(m_traversable->m_session_history_entries.size() > 0); + auto back_enabled = m_traversable->can_go_back(); auto forward_enabled = m_traversable->can_go_forward(); m_traversable->page().client().page_did_update_navigation_buttons_state(back_enabled, forward_enabled); m_traversable->page().client().page_did_change_url(m_traversable->current_session_history_entry()->url()); @@ -1123,6 +1475,29 @@ void TraversableNavigable::apply_the_history_step( VERIFY(!m_apply_history_step_state || m_paused_apply_history_step_state); + run_the_history_step_prechecks(step, check_for_cancelation, source_snapshot_params, initiator_to_check, user_involvement, navigation_type, navigation_api_abort_behavior, + GC::create_function(heap(), [this, step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, pending_document, on_complete](HistoryStepResult result, int target_step, Navigable::NavigationAPIAbortBehavior navigation_api_abort_behavior) { + if (result != HistoryStepResult::Applied) { + on_complete->function()(result); + return; + } + + // 6. Let changingNavigables be the result of get all navigables whose current session history entry will + // change or reload given traversable and targetStep. + apply_the_history_step_after_unload_check(step, target_step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, on_complete); + })); +} + +void TraversableNavigable::run_the_history_step_prechecks( + int step, + bool check_for_cancelation, + GC::Ptr source_snapshot_params, + GC::Ptr initiator_to_check, + UserNavigationInvolvement user_involvement, + Optional navigation_type, + Navigable::NavigationAPIAbortBehavior navigation_api_abort_behavior, + GC::Ref on_complete) +{ // 2. Let targetStep be the result of getting the used step given traversable and step. auto target_step = get_the_used_step(step); @@ -1131,11 +1506,18 @@ void TraversableNavigable::apply_the_history_step( // 1. Assert: sourceSnapshotParams is not null. VERIFY(source_snapshot_params); + auto target_top_level_entry = get_the_target_history_entry(target_step); + if (target_top_level_entry != current_session_history_entry() + && !initiator_to_check->allowed_by_sandboxing_to_navigate(*this, *source_snapshot_params)) { + on_complete->function()(HistoryStepResult::InitiatorDisallowed, target_step, navigation_api_abort_behavior); + 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 : get_all_navigables_whose_current_session_history_entry_will_change_or_reload(target_step)) { if (!initiator_to_check->allowed_by_sandboxing_to_navigate(*navigable, *source_snapshot_params)) { - on_complete->function()(HistoryStepResult::InitiatorDisallowed); + on_complete->function()(HistoryStepResult::InitiatorDisallowed, target_step, navigation_api_abort_behavior); return; } } @@ -1153,22 +1535,21 @@ void TraversableNavigable::apply_the_history_step( // and userInvolvement is not "continue", then return that result. if (check_for_cancelation) { check_if_unloading_is_canceled(navigables_crossing_documents, *this, target_step, user_involvement, - GC::create_function(heap(), [this, step, target_step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, on_complete](CheckIfUnloadingIsCanceledResult result) mutable { + GC::create_function(heap(), [target_step, navigation_api_abort_behavior, on_complete](CheckIfUnloadingIsCanceledResult result) mutable { if (result == CheckIfUnloadingIsCanceledResult::CanceledByBeforeUnload) { - on_complete->function()(HistoryStepResult::CanceledByBeforeUnload); + on_complete->function()(HistoryStepResult::CanceledByBeforeUnload, target_step, navigation_api_abort_behavior); return; } if (result == CheckIfUnloadingIsCanceledResult::CanceledByNavigate) { - on_complete->function()(HistoryStepResult::CanceledByNavigate); + on_complete->function()(HistoryStepResult::CanceledByNavigate, target_step, navigation_api_abort_behavior); return; } - apply_the_history_step_after_unload_check(step, target_step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, on_complete); + on_complete->function()(HistoryStepResult::Applied, target_step, navigation_api_abort_behavior); })); return; } - // 6. Let changingNavigables be the result of get all navigables whose current session history entry will change or reload given traversable and targetStep. - apply_the_history_step_after_unload_check(step, target_step, source_snapshot_params, user_involvement, navigation_type, synchronous_navigation, navigation_api_abort_behavior, pending_document, on_complete); + on_complete->function()(HistoryStepResult::Applied, target_step, navigation_api_abort_behavior); } void TraversableNavigable::apply_the_history_step_after_unload_check( @@ -1526,26 +1907,20 @@ void TraversableNavigable::clear_the_forward_session_history() } } +bool TraversableNavigable::can_go_back() const +{ + auto all_steps = get_all_used_history_steps(); + auto current_step_index = all_steps.find_first_index(current_session_history_step()); + VERIFY(current_step_index.has_value()); + return *current_step_index > 0; +} + bool TraversableNavigable::can_go_forward() const { - auto step = current_session_history_step(); - - Vector> const&> entry_lists; - entry_lists.append(session_history_entries()); - - while (!entry_lists.is_empty()) { - auto const& entry_list = entry_lists.take_first(); - - for (auto const& entry : entry_list) { - if (entry->step().template get() > step) - return true; - - for (auto& nested_history : entry->document_state()->nested_histories()) - entry_lists.append(nested_history.entries); - } - } - - return false; + auto all_steps = get_all_used_history_steps(); + auto current_step_index = all_steps.find_first_index(current_session_history_step()); + VERIFY(current_step_index.has_value()); + return *current_step_index + 1 < all_steps.size(); } // https://html.spec.whatwg.org/multipage/browsing-the-web.html#traverse-the-history-by-a-delta @@ -1558,7 +1933,7 @@ void TraversableNavigable::traverse_the_history_by_delta(int delta, GC::Ptrsnapshot_source_snapshot_params(); @@ -1579,23 +1954,122 @@ void TraversableNavigable::traverse_the_history_by_delta(int delta, GC::Ptr(-static_cast(delta)); + if (magnitude > current_step_index) { + if (source_snapshot_params && page().client().page_did_request_traverse_the_history_by_delta(delta, HistoryTraversalPrecheck::Needed)) { + signal->resolve({}); + return; + } + signal->resolve({}); + return; + } + target_step_index = current_step_index - magnitude; + } else { + auto magnitude = static_cast(delta); + if (magnitude >= all_steps.size() - current_step_index) { + if (source_snapshot_params && page().client().page_did_request_traverse_the_history_by_delta(delta, HistoryTraversalPrecheck::Needed)) { + signal->resolve({}); + return; + } + signal->resolve({}); + return; + } + target_step_index = current_step_index + magnitude; + } // 4. If allSteps[targetStepIndex] does not exist, then abort these steps. if (target_step_index >= all_steps.size()) { + if (source_snapshot_params && page().client().page_did_request_traverse_the_history_by_delta(delta, HistoryTraversalPrecheck::Needed)) { + signal->resolve({}); + return; + } signal->resolve({}); return; } + auto target_step = all_steps[target_step_index]; + + if (source_snapshot_params) { + RefPtr target_top_level_entry; + for (auto const& entry : session_history_entries()) { + if (entry->step().template get() > target_step) + break; + target_top_level_entry = entry; + } + + if (target_top_level_entry && current_session_history_entry() && !page().client().is_url_suitable_for_same_process_navigation(current_session_history_entry()->url(), target_top_level_entry->url())) { + run_the_history_step_prechecks(target_step, true, source_snapshot_params, initiator_to_check, user_involvement, Bindings::NavigationType::Traverse, Navigable::NavigationAPIAbortBehavior::Abort, + GC::create_function(heap(), [this, delta, signal](HistoryStepResult result, int, Navigable::NavigationAPIAbortBehavior) { + if (result == HistoryStepResult::Applied) + (void)page().client().page_did_request_traverse_the_history_by_delta(delta, HistoryTraversalPrecheck::AlreadyDone); + signal->resolve({}); + })); + return; + } + } + // 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, + apply_the_traverse_history_step(target_step, source_snapshot_params, initiator_to_check, user_involvement, GC::create_function(heap(), [signal](HistoryStepResult) { signal->resolve({}); })); })); } +void TraversableNavigable::traverse_the_history_to_step(int step, GC::Ref> on_complete) +{ + // NB: This is used when the UI process owns the top-level session + // history and has already resolved the browser UI delta to a stable step. + append_session_history_traversal_steps(GC::create_function(heap(), [this, step, on_complete](NonnullRefPtr> signal) { + auto all_steps = get_all_used_history_steps(); + if (!all_steps.contains_slow(step)) { + on_complete->function()(false, HistoryStepResult::Applied); + signal->resolve({}); + return; + } + + apply_the_traverse_history_step(step, nullptr, nullptr, UserNavigationInvolvement::BrowserUI, + GC::create_function(heap(), [signal, on_complete](HistoryStepResult result) { + on_complete->function()(true, result); + signal->resolve({}); + })); + })); +} + +void TraversableNavigable::check_if_traverse_history_step_is_canceled(int step, GC::Ref on_complete) +{ + // NB: This is used when the UI process owns the top-level session history + // and needs WebContent to run the cancelable part of the traverse algorithm + // before the UI process applies its own history mirror update. + append_session_history_traversal_steps(GC::create_function(heap(), [this, step, on_complete](NonnullRefPtr> signal) { + auto all_steps = get_all_used_history_steps(); + if (!all_steps.contains_slow(step)) { + // The UI process can ask about a step in its authoritative + // session history mirror that this WebContent process cannot + // address locally, for example after a process swap with a partial + // restored history. We cannot run the full traverse prechecks + // without the target entry, but the active document tree still must + // get a chance to cancel unloading before the UI process replaces + // this WebContent process or fallback-loads the target entry. + check_if_unloading_is_canceled(active_document()->inclusive_descendant_navigables(), + GC::create_function(heap(), [signal, on_complete](CheckIfUnloadingIsCanceledResult result) { + on_complete->function()(result == CheckIfUnloadingIsCanceledResult::Continue ? HistoryStepResult::Applied : HistoryStepResult::CanceledByBeforeUnload); + signal->resolve({}); + })); + return; + } + + run_the_history_step_prechecks(step, true, nullptr, nullptr, UserNavigationInvolvement::BrowserUI, Bindings::NavigationType::Traverse, Navigable::NavigationAPIAbortBehavior::Abort, + GC::create_function(heap(), [signal, on_complete](HistoryStepResult result, int, Navigable::NavigationAPIAbortBehavior) { + on_complete->function()(result); + signal->resolve({}); + })); + })); +} + // https://html.spec.whatwg.org/multipage/browsing-the-web.html#update-for-navigable-creation/destruction void TraversableNavigable::update_for_navigable_creation_or_destruction(GC::Ref on_complete) { @@ -1613,7 +2087,22 @@ void TraversableNavigable::apply_the_reload_history_step(UserNavigationInvolveme auto step = current_session_history_step(); // 2. Return the result of applying the history step step to traversable given true, null, null, null, and "reload". - apply_the_history_step(step, true, {}, {}, user_involvement, Bindings::NavigationType::Reload, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Abort, nullptr, on_complete); + apply_the_history_step(step, true, {}, {}, user_involvement, Bindings::NavigationType::Reload, SynchronousNavigation::No, Navigable::NavigationAPIAbortBehavior::Abort, nullptr, + GC::create_function(heap(), [this, on_complete](HistoryStepResult result) { + if (result != HistoryStepResult::Applied) { + // NB: A canceled reload must not keep treating the active + // session history entry as an in-flight reload. + if (auto current_entry = current_session_history_entry(); current_entry && current_entry->document_state()->reload_pending()) { + current_entry->document_state()->set_reload_pending(false); + + if (page().client().should_report_session_history_updates()) { + auto session_history_snapshot = create_session_history_snapshot(); + page().client().page_did_update_session_history(session_history_snapshot.top_level_session_history_entries, session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index); + } + } + } + on_complete->function()(result); + })); } // https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-push/replace-history-step diff --git a/Libraries/LibWeb/HTML/TraversableNavigable.h b/Libraries/LibWeb/HTML/TraversableNavigable.h index d199ea2f27..06596d587b 100644 --- a/Libraries/LibWeb/HTML/TraversableNavigable.h +++ b/Libraries/LibWeb/HTML/TraversableNavigable.h @@ -46,6 +46,16 @@ public: int current_session_history_step() const { return m_current_session_history_step; } Vector>& session_history_entries() { return m_session_history_entries; } Vector> const& session_history_entries() const { return m_session_history_entries; } + struct SessionHistorySnapshot { + Vector top_level_session_history_entries; + Vector used_session_history_steps; + size_t current_used_step_index { 0 }; + }; + enum class SaveActiveEntryPersistedState : bool { + No, + Yes, + }; + SessionHistorySnapshot create_session_history_snapshot(SaveActiveEntryPersistedState = SaveActiveEntryPersistedState::Yes); VisibilityState system_visibility_state() const { return m_system_visibility_state; } void set_system_visibility_state(VisibilityState); @@ -77,6 +87,10 @@ public: Vector get_all_used_history_steps() const; void clear_the_forward_session_history(); void traverse_the_history_by_delta(int delta, GC::Ptr source_document = {}); + void traverse_the_history_to_step(int step, GC::Ref> on_complete); + void check_if_traverse_history_step_is_canceled(int step, GC::Ref on_complete); + bool replace_top_level_session_history_entries_from_ui_process(Vector, size_t current_top_level_entry_index); + void reset_session_history_for_testing(GC::Ref> on_complete); void close_top_level_traversable(); void definitely_close_top_level_traversable(); @@ -128,7 +142,7 @@ private: virtual void visit_edges(Cell::Visitor&) override; - // FIXME: Fix spec typo cancelation --> cancellation + // NB: The HTML Standard spells this algorithm argument "checkForCancelation". void apply_the_history_step( int step, bool check_for_cancelation, @@ -152,10 +166,22 @@ private: GC::Ptr pending_document, GC::Ref on_complete); + using OnHistoryStepPrechecksComplete = GC::Function; + void run_the_history_step_prechecks( + int step, + bool check_for_cancelation, + GC::Ptr, + GC::Ptr initiator_to_check, + UserNavigationInvolvement user_involvement, + Optional navigation_type, + Navigable::NavigationAPIAbortBehavior, + GC::Ref); + void check_if_unloading_is_canceled(Vector> navigables_that_need_before_unload, GC::Ptr traversable, Optional target_step, Optional user_involvement_for_navigate_events, GC::Ref> callback); Vector> get_session_history_entries_for_the_navigation_api(GC::Ref, int); + [[nodiscard]] bool can_go_back() const; [[nodiscard]] bool can_go_forward() const; // https://html.spec.whatwg.org/multipage/document-sequences.html#tn-current-session-history-step diff --git a/Libraries/LibWeb/Internals/Internals.cpp b/Libraries/LibWeb/Internals/Internals.cpp index e0ed6cc665..0e5efc62f0 100644 --- a/Libraries/LibWeb/Internals/Internals.cpp +++ b/Libraries/LibWeb/Internals/Internals.cpp @@ -671,7 +671,13 @@ String Internals::dump_session_history() auto step = entry->step(); auto const& url = entry->url(); auto filename = url.basename(); - auto display = url.fragment().has_value() ? MUST(String::formatted("{}#{}", filename, *url.fragment())) : MUST(String::from_byte_string(filename)); + StringBuilder display_builder; + display_builder.append(filename); + if (url.query().has_value()) + display_builder.appendff("?{}", *url.query()); + if (url.fragment().has_value()) + display_builder.appendff("#{}", *url.fragment()); + auto display = display_builder.to_string_without_validation(); auto is_current = step.has() && step.get() == current_step; auto relative_step = step.has() && min_step.has_value() ? String::number(step.get() - *min_step) : "pending"_string; builder.appendff(" step {} {}{}\n", relative_step, display, is_current ? " (current)"sv : ""sv); @@ -679,6 +685,11 @@ String Internals::dump_session_history() return builder.to_string_without_validation(); } +String Internals::dump_ui_process_session_history() +{ + return window().associated_document().page().client().page_did_request_ui_process_session_history_for_testing(); +} + GC::Ptr Internals::get_shadow_root(GC::Ref element) { return element->shadow_root(); diff --git a/Libraries/LibWeb/Internals/Internals.h b/Libraries/LibWeb/Internals/Internals.h index dd65bafc94..6dfb7fcb7f 100644 --- a/Libraries/LibWeb/Internals/Internals.h +++ b/Libraries/LibWeb/Internals/Internals.h @@ -111,6 +111,7 @@ public: String dump_stacking_context_tree(); String dump_gc_graph(); String dump_session_history(); + String dump_ui_process_session_history(); GC::Ptr get_shadow_root(GC::Ref); diff --git a/Libraries/LibWeb/Internals/Internals.idl b/Libraries/LibWeb/Internals/Internals.idl index a8e19b0cab..73cb013b06 100644 --- a/Libraries/LibWeb/Internals/Internals.idl +++ b/Libraries/LibWeb/Internals/Internals.idl @@ -97,6 +97,7 @@ interface Internals { DOMString dumpStackingContextTree(); DOMString dumpGCGraph(); DOMString dumpSessionHistory(); + DOMString dumpUIProcessSessionHistory(); // Returns the shadow root of the element, if it has one, even if it's not normally accessible to JS. ShadowRoot? getShadowRoot(Element element); diff --git a/Libraries/LibWeb/Page/Page.cpp b/Libraries/LibWeb/Page/Page.cpp index 1f69b14731..ba3fb12945 100644 --- a/Libraries/LibWeb/Page/Page.cpp +++ b/Libraries/LibWeb/Page/Page.cpp @@ -116,9 +116,21 @@ void Page::navigable_document_destroyed(Badge, HTML::Navigable& n m_focused_navigable = nullptr; } -void Page::load(URL::URL const& url) +void Page::load(URL::URL const& url, Bindings::NavigationHistoryBehavior history_handling) { - (void)top_level_traversable()->navigate({ .url = url, .source_document = *top_level_traversable()->active_document(), .user_involvement = HTML::UserNavigationInvolvement::BrowserUI }); + (void)top_level_traversable()->navigate({ .url = url, .source_document = *top_level_traversable()->active_document(), .history_handling = history_handling, .user_involvement = HTML::UserNavigationInvolvement::BrowserUI }); +} + +void Page::load(URL::URL const& url, Variant document_resource, + Bindings::NavigationHistoryBehavior history_handling) +{ + (void)top_level_traversable()->navigate({ + .url = url, + .source_document = *top_level_traversable()->active_document(), + .document_resource = move(document_resource), + .history_handling = history_handling, + .user_involvement = HTML::UserNavigationInvolvement::BrowserUI, + }); } void Page::load_html(StringView html) @@ -163,6 +175,14 @@ void Page::reload() } void Page::traverse_the_history_by_delta(int delta) +{ + if (m_client->page_did_request_traverse_the_history_by_delta(delta, HistoryTraversalPrecheck::Needed)) + return; + + traverse_the_history_by_delta_from_ui_process(delta); +} + +void Page::traverse_the_history_by_delta_from_ui_process(int delta) { top_level_traversable()->traverse_the_history_by_delta(delta); } diff --git a/Libraries/LibWeb/Page/Page.h b/Libraries/LibWeb/Page/Page.h index 00d71d3ffa..2a1e2e07c4 100644 --- a/Libraries/LibWeb/Page/Page.h +++ b/Libraries/LibWeb/Page/Page.h @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -42,7 +43,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -97,7 +100,9 @@ public: void set_focused_navigable(Badge, HTML::Navigable&); void navigable_document_destroyed(Badge, HTML::Navigable&); - void load(URL::URL const&); + void load(URL::URL const&, Bindings::NavigationHistoryBehavior = Bindings::NavigationHistoryBehavior::Auto); + void load(URL::URL const&, Variant, + Bindings::NavigationHistoryBehavior = Bindings::NavigationHistoryBehavior::Auto); void load_html(StringView); void load_html(StringView, URL::URL const&); @@ -105,6 +110,7 @@ public: void reload(); void traverse_the_history_by_delta(int delta); + void traverse_the_history_by_delta_from_ui_process(int delta); CSSPixelPoint device_to_css_point(DevicePixelPoint) const; DevicePixelPoint css_to_device_point(CSSPixelPoint) const; @@ -406,6 +412,12 @@ enum class ContextMenuForInputEventsTarget : u8 { Yes, }; +enum class HistoryTraversalPrecheck : u8 { + Needed, + AlreadyDone, + SourceDocumentSandboxingAlreadyDone, +}; + class PageClient : public JS::Cell { GC_CELL(PageClient, JS::Cell); @@ -417,7 +429,7 @@ public: virtual bool has_focus() const { return true; } virtual bool has_active_devtools_client() const { return false; } virtual bool is_url_suitable_for_same_process_navigation([[maybe_unused]] URL::URL const& current_url, [[maybe_unused]] URL::URL const& target_url) const { return true; } - virtual void request_new_process_for_navigation(URL::URL const&) { } + virtual void request_new_process_for_navigation(URL::URL const&, Variant, Bindings::NavigationHistoryBehavior) { } virtual Gfx::Palette palette() const = 0; virtual DevicePixelRect screen_rect() const = 0; virtual double zoom_level() const = 0; @@ -446,7 +458,13 @@ public: virtual void page_did_request_minimize_window() { } virtual void page_did_request_fullscreen_window() { } virtual void page_did_request_exit_fullscreen() { } - virtual void page_did_start_loading(URL::URL const&, bool is_redirect) { (void)is_redirect; } + virtual void page_did_start_loading(URL::URL const&, Variant document_resource, bool is_redirect, Bindings::NavigationHistoryBehavior history_handling = Bindings::NavigationHistoryBehavior::Auto) + { + (void)document_resource; + (void)is_redirect; + (void)history_handling; + } + virtual void page_did_cancel_loading(URL::URL const&) { } virtual void page_did_create_new_document(Web::DOM::Document&) { } virtual void page_did_change_active_document_in_top_level_browsing_context(Web::DOM::Document&) { } virtual void page_did_finish_loading(URL::URL const&) { } @@ -499,6 +517,10 @@ public: virtual void page_did_request_activate_tab() { } virtual void page_did_close_top_level_traversable() { } virtual void page_did_update_navigation_buttons_state([[maybe_unused]] bool back_enabled, [[maybe_unused]] bool forward_enabled) { } + virtual bool should_report_session_history_updates() const { return true; } + virtual void page_did_update_session_history([[maybe_unused]] Vector const& entries, [[maybe_unused]] Vector const& used_steps, [[maybe_unused]] size_t current_used_step_index) { } + virtual String page_did_request_ui_process_session_history_for_testing() { return "{}"_string; } + virtual bool page_did_request_traverse_the_history_by_delta([[maybe_unused]] int delta, [[maybe_unused]] HistoryTraversalPrecheck history_traversal_precheck) { return false; } virtual void page_did_change_needs_beforeunload_check([[maybe_unused]] bool needs_beforeunload_check) { } virtual void request_file(FileRequest) = 0; diff --git a/Libraries/LibWeb/WebDriver/Capabilities.cpp b/Libraries/LibWeb/WebDriver/Capabilities.cpp index a458424c36..924e9644c1 100644 --- a/Libraries/LibWeb/WebDriver/Capabilities.cpp +++ b/Libraries/LibWeb/WebDriver/Capabilities.cpp @@ -41,9 +41,10 @@ void set_default_interface_mode(InterfaceMode interface_mode) static Response deserialize_as_ladybird_capability(StringView name, JsonValue value) { - if (name == "ladybird:headless"sv) { + if (name.is_one_of("ladybird:headless"sv, "ladybird:enableTestHooks"sv)) { if (!value.is_bool()) - return Error::from_code(ErrorCode::InvalidArgument, "Extension capability ladybird:headless must be a boolean"sv); + return Error::from_code(ErrorCode::InvalidArgument, + MUST(String::formatted("Extension capability {} must be a boolean", name))); } return value; @@ -52,6 +53,7 @@ static Response deserialize_as_ladybird_capability(StringView name, JsonValue va static void set_default_ladybird_capabilities(JsonObject& options) { options.set("ladybird:headless"sv, default_interface_mode == InterfaceMode::Headless); + options.set("ladybird:enableTestHooks"sv, false); } // https://w3c.github.io/webdriver/#dfn-validate-capabilities @@ -440,6 +442,8 @@ LadybirdOptions::LadybirdOptions(JsonObject const& capabilities) { if (auto headless = capabilities.get_bool("ladybird:headless"sv); headless.has_value()) this->headless = *headless; + if (auto enable_test_hooks = capabilities.get_bool("ladybird:enableTestHooks"sv); enable_test_hooks.has_value()) + this->enable_test_hooks = *enable_test_hooks; } } diff --git a/Libraries/LibWeb/WebDriver/Capabilities.h b/Libraries/LibWeb/WebDriver/Capabilities.h index cd73aaee51..ea10d9e1ce 100644 --- a/Libraries/LibWeb/WebDriver/Capabilities.h +++ b/Libraries/LibWeb/WebDriver/Capabilities.h @@ -48,6 +48,7 @@ struct WEB_API LadybirdOptions { explicit LadybirdOptions(JsonObject const& capabilities); bool headless { false }; + bool enable_test_hooks { false }; }; WEB_API Response process_capabilities(JsonValue const& parameters, SessionFlags flags); diff --git a/Libraries/LibWeb/WebDriver/Client.cpp b/Libraries/LibWeb/WebDriver/Client.cpp index 9994d15fde..9a4d69fd4a 100644 --- a/Libraries/LibWeb/WebDriver/Client.cpp +++ b/Libraries/LibWeb/WebDriver/Client.cpp @@ -71,6 +71,11 @@ static constexpr auto s_webdriver_endpoints = Array { ROUTE(POST, "/session/:session_id/window/minimize"sv, minimize_window), ROUTE(POST, "/session/:session_id/window/fullscreen"sv, fullscreen_window), ROUTE(POST, "/session/:session_id/window/consume-user-activation"sv, consume_user_activation), + ROUTE(POST, "/session/:session_id/ladybird/crash-current-page"sv, crash_current_page), + ROUTE(POST, "/session/:session_id/ladybird/load-url-from-ui"sv, load_url_from_ui), + ROUTE(POST, "/session/:session_id/ladybird/traverse-history-from-ui"sv, traverse_history_from_ui), + ROUTE(POST, "/session/:session_id/ladybird/mark-web-content-session-history-stale"sv, mark_web_content_session_history_stale), + ROUTE(GET, "/session/:session_id/ladybird/session-history"sv, get_session_history), ROUTE(POST, "/session/:session_id/element"sv, find_element), ROUTE(POST, "/session/:session_id/elements"sv, find_elements), ROUTE(POST, "/session/:session_id/element/:element_id/element"sv, find_element_from_element), diff --git a/Libraries/LibWeb/WebDriver/Client.h b/Libraries/LibWeb/WebDriver/Client.h index b34ea19d74..922d8cb7cb 100644 --- a/Libraries/LibWeb/WebDriver/Client.h +++ b/Libraries/LibWeb/WebDriver/Client.h @@ -65,6 +65,13 @@ public: // Extension: https://html.spec.whatwg.org/multipage/interaction.html#user-activation-user-agent-automation virtual Response consume_user_activation(Parameters parameters, JsonValue payload) = 0; + // Ladybird extension for browser integration tests. + virtual Response crash_current_page(Parameters parameters, JsonValue payload) = 0; + virtual Response load_url_from_ui(Parameters parameters, JsonValue payload) = 0; + virtual Response traverse_history_from_ui(Parameters parameters, JsonValue payload) = 0; + virtual Response mark_web_content_session_history_stale(Parameters parameters, JsonValue payload) = 0; + virtual Response get_session_history(Parameters parameters, JsonValue payload) = 0; + // 12. Elements, https://w3c.github.io/webdriver/#elements virtual Response find_element(Parameters parameters, JsonValue payload) = 0; virtual Response find_elements(Parameters parameters, JsonValue payload) = 0; diff --git a/Libraries/LibWeb/WebDriver/UserPrompt.cpp b/Libraries/LibWeb/WebDriver/UserPrompt.cpp index fdf21fc812..e8e6442014 100644 --- a/Libraries/LibWeb/WebDriver/UserPrompt.cpp +++ b/Libraries/LibWeb/WebDriver/UserPrompt.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -116,6 +117,35 @@ void set_user_prompt_handler(UserPromptHandler user_prompt_handler) user_prompt_handler_storage() = move(user_prompt_handler); } +// https://w3c.github.io/webdriver/#dfn-get-the-prompt-handler +PromptHandlerConfiguration get_the_prompt_handler(PromptType type) +{ + static NeverDestroyed empty_user_prompt_handler; + + // 1. If the user prompt handler is null, let handlers be an empty map. Otherwise let handlers be user prompt + // handler. + auto const& handlers = user_prompt_handler_storage().has_value() ? *user_prompt_handler_storage() : *empty_user_prompt_handler; + + // 2. If handlers contains type return handlers[type]. + if (auto handler = handlers.get(type); handler.has_value()) + return *handler; + + // 3. If handlers contains "default" return handlers["default"]. + if (auto handler = handlers.get(PromptType::Default); handler.has_value()) + return *handler; + + // 4. If type is "beforeUnload", return a prompt handler configuration with handler "accept" and notify false. + if (type == PromptType::BeforeUnload) + return { .handler = PromptHandler::Accept, .notify = PromptHandlerConfiguration::Notify::No }; + + // 5. If handlers contains "fallbackDefault" return handlers["fallbackDefault"]. + if (auto handler = handlers.get(PromptType::FallbackDefault); handler.has_value()) + return *handler; + + // 6. Return a prompt handler configuration with handler "dismiss" and notify true. + return { .handler = PromptHandler::Dismiss, .notify = PromptHandlerConfiguration::Notify::Yes }; +} + // https://w3c.github.io/webdriver/#dfn-deserialize-as-an-unhandled-prompt-behavior Response deserialize_as_an_unhandled_prompt_behavior(JsonValue value) { diff --git a/Libraries/LibWeb/WebDriver/UserPrompt.h b/Libraries/LibWeb/WebDriver/UserPrompt.h index 3b17ee376e..07069e9fe2 100644 --- a/Libraries/LibWeb/WebDriver/UserPrompt.h +++ b/Libraries/LibWeb/WebDriver/UserPrompt.h @@ -56,6 +56,7 @@ using UserPromptHandler = Optionaltraverse_the_history_by_delta(delta); + (void)view->traverse_the_history_by_delta(delta); } Vector Application::cookies(DevTools::TabDescription const& description) const diff --git a/Libraries/LibWebView/HelperProcess.cpp b/Libraries/LibWebView/HelperProcess.cpp index c25818a490..efb302768c 100644 --- a/Libraries/LibWebView/HelperProcess.cpp +++ b/Libraries/LibWebView/HelperProcess.cpp @@ -122,6 +122,8 @@ ErrorOr> launch_web_content_process(u64 arguments.append("--disable-async-scrolling"sv); if (web_content_options.file_scheme_urls_have_tuple_origins == FileSchemeUrlsHaveTupleOrigins::Yes) arguments.append("--tuple-file-origins"sv); + if (web_content_options.report_session_history_updates_in_test_mode == ReportSessionHistoryUpdatesInTestMode::Yes) + arguments.append("--report-session-history-updates-in-test-mode"sv); if (browser_options.enable_sandbox == EnableSandbox::Yes) arguments.append("--enable-sandbox"sv); diff --git a/Libraries/LibWebView/Options.h b/Libraries/LibWebView/Options.h index c1431f0b6f..28a43be82e 100644 --- a/Libraries/LibWebView/Options.h +++ b/Libraries/LibWebView/Options.h @@ -180,6 +180,11 @@ enum class FileSchemeUrlsHaveTupleOrigins { Yes, }; +enum class ReportSessionHistoryUpdatesInTestMode { + No, + Yes, +}; + struct WebContentOptions { Optional config_path {}; Optional user_agent_preset {}; @@ -198,6 +203,7 @@ struct WebContentOptions { PaintViewportScrollbars paint_viewport_scrollbars { PaintViewportScrollbars::Yes }; EnableAsyncScrolling enable_async_scrolling { EnableAsyncScrolling::Yes }; FileSchemeUrlsHaveTupleOrigins file_scheme_urls_have_tuple_origins { FileSchemeUrlsHaveTupleOrigins::No }; + ReportSessionHistoryUpdatesInTestMode report_session_history_updates_in_test_mode { ReportSessionHistoryUpdatesInTestMode::No }; Optional default_time_zone {}; Optional style_invalidation_counter_dump_interval {}; }; diff --git a/Libraries/LibWebView/ViewImplementation.cpp b/Libraries/LibWebView/ViewImplementation.cpp index f8e19887f5..f2fdb27be5 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -7,9 +7,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -18,12 +20,15 @@ #include #include #include +#include #include #include #include #include +#include #include #include +#include #include #include #include @@ -134,8 +139,11 @@ void ViewImplementation::set_favicon(Badge, Gfx::Bitmap const& on_favicon_change(favicon); } -void ViewImplementation::create_new_process_for_cross_site_navigation(URL::URL const& url) +void ViewImplementation::create_new_process_for_cross_site_navigation(URL::URL const& url, Variant document_resource, Web::Bindings::NavigationHistoryBehavior history_handling) { + dump_session_history("before-process-swap"sv); + m_webdriver_pending_navigation_url = url; + if (m_client_state.has_usable_bitmap) { // Keep showing the old page until the new WebContent process paints its first frame. m_backup_shared_image_buffer = move(m_client_state.front_bitmap.shared_image_buffer); @@ -154,7 +162,74 @@ void ViewImplementation::create_new_process_for_cross_site_navigation(URL::URL c handle_resize(); - load(url); + auto ui_session_history_already_points_to_url = false; + if (should_manage_session_history_in_ui_process()) { + if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) + ui_session_history_already_points_to_url = true; + + if (m_pending_session_history_traversal.has_value() && m_pending_session_history_traversal->will_replace_web_content_process) + m_pending_session_history_traversal->stage = PendingSessionHistoryTraversal::Stage::ReplacingWebContentProcess; + if (m_pending_session_history_navigation.has_value()) + m_pending_session_history_navigation->web_content_restore_mode = PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess; + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + m_pending_web_content_session_history_seed.waiting_for_ack = false; + m_pending_web_content_session_history_seed.should_send_entries = true; + m_pending_web_content_session_history_seed.ignore_updates_until_seed = true; + } + + auto seed_replacement_process_before_load_if_possible = [&] { + if (!should_manage_session_history_in_ui_process()) + return false; + if (!m_pending_web_content_session_history_seed.should_send_entries) + return false; + if (m_loading_session_history_entry_from_ui_process) + return false; + if (m_session_history.current_step_to_restore_after_loading_top_level_entry().has_value()) + return false; + seed_web_content_session_history_from_ui_process(); + return true; + }; + + if (ui_session_history_already_points_to_url) { + m_should_suppress_history_for_current_load = false; + m_should_suppress_history_for_next_load = false; + if (!m_loading_session_history_entry_from_ui_process) + m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry(); + set_url(url); + auto seeded_replacement_process_before_load = seed_replacement_process_before_load_if_possible(); + auto web_content_history_handling = seeded_replacement_process_before_load ? Web::Bindings::NavigationHistoryBehavior::Replace : history_handling; + dump_session_history("process-swap-load-existing-ui-entry"sv); + client().async_load_url_with_document_resource(page_id(), url, document_resource, web_content_history_handling); + } else { + m_should_suppress_history_for_current_load = false; + m_should_suppress_history_for_next_load = false; + if (should_manage_session_history_in_ui_process() && !m_loading_session_history_entry_from_ui_process) { + if (m_session_history.current_entry()) { + m_pending_session_history_navigation = PendingSessionHistoryNavigation { + url, + m_session_history, + PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess, + }; + } else { + m_pending_session_history_navigation.clear(); + } + if (history_handling == Web::Bindings::NavigationHistoryBehavior::Replace) + m_session_history.replace_current_entry(url, document_resource); + else + m_session_history.navigate(url, document_resource); + m_current_web_content_session_history_matches_mirror = false; + update_navigation_action_state(); + } + if (!m_loading_session_history_entry_from_ui_process) + m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry(); + set_url(url); + auto seeded_replacement_process_before_load = seed_replacement_process_before_load_if_possible(); + auto web_content_history_handling = seeded_replacement_process_before_load ? Web::Bindings::NavigationHistoryBehavior::Replace : history_handling; + dump_session_history("load"sv); + client().async_load_url_with_document_resource(page_id(), url, document_resource, web_content_history_handling); + } + dump_session_history("after-process-swap-load"sv); } void ViewImplementation::server_did_paint(Badge, i32 bitmap_id, Gfx::IntSize size) @@ -201,25 +276,74 @@ void ViewImplementation::set_system_visibility_state(Web::HTML::VisibilityState client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state); } -void ViewImplementation::load(URL::URL const& url) +void ViewImplementation::load(URL::URL const& url, Web::Bindings::NavigationHistoryBehavior history_handling) { + m_is_showing_crash_page = false; m_should_suppress_history_for_current_load = false; m_should_suppress_history_for_next_load = false; - set_url(url); - client().async_load_url(page_id(), url); + auto should_defer_ui_process_history_update = false; + if (!m_loading_session_history_entry_from_ui_process) + abandon_pending_web_content_session_history_seed(); + if (should_manage_session_history_in_ui_process() && !m_loading_session_history_entry_from_ui_process) { + m_pending_session_history_traversal.clear(); + auto const* current_entry = m_session_history.current_entry(); + auto is_javascript_navigation = url.scheme() == "javascript"sv; + should_defer_ui_process_history_update = is_javascript_navigation; + if (current_entry && !is_javascript_navigation) + m_pending_session_history_navigation = PendingSessionHistoryNavigation { url, m_session_history }; + else + m_pending_session_history_navigation.clear(); + + if (!is_javascript_navigation) { + auto ui_process_history_handling = history_handling; + if (ui_process_history_handling == Web::Bindings::NavigationHistoryBehavior::Auto) { + // https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate + // If url equals navigable's active document's URL, and + // initiatorOriginSnapshot is same origin with targetNavigable's + // active document's origin, then set historyHandling to "replace". + if (current_entry && current_entry->url == url) + ui_process_history_handling = Web::Bindings::NavigationHistoryBehavior::Replace; + else + ui_process_history_handling = Web::Bindings::NavigationHistoryBehavior::Push; + } + + if (ui_process_history_handling == Web::Bindings::NavigationHistoryBehavior::Replace) + m_session_history.replace_current_entry(url, Empty {}); + else + m_session_history.navigate(url); + m_current_web_content_session_history_matches_mirror = false; + update_navigation_action_state(); + } + } + if (!should_defer_ui_process_history_update) + set_url(url); + dump_session_history("load"sv); + client().async_load_url(page_id(), url, history_handling); } void ViewImplementation::load_html(StringView html) { + m_is_showing_crash_page = false; m_should_suppress_history_for_current_load = false; m_should_suppress_history_for_next_load = false; + abandon_pending_web_content_session_history_seed(); + if (should_manage_session_history_in_ui_process()) { + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + } client().async_load_html(page_id(), html); } void ViewImplementation::load_crash_page_html(StringView html, URL::URL const& crashed_url) { + m_is_showing_crash_page = true; m_should_suppress_history_for_current_load = true; m_should_suppress_history_for_next_load = true; + abandon_pending_web_content_session_history_seed(); + if (should_manage_session_history_in_ui_process()) { + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + } set_url(crashed_url); client().async_load_html_with_url(page_id(), html, crashed_url); } @@ -237,16 +361,132 @@ void ViewImplementation::load_navigation_error_page(StringView text) void ViewImplementation::reload() { + if (should_manage_session_history_in_ui_process() && m_is_showing_crash_page) { + m_is_showing_crash_page = false; + m_should_suppress_history_for_current_load = false; + m_should_suppress_history_for_next_load = false; + prepare_to_seed_web_content_session_history_from_ui_process(); + restore_current_session_history_entry_from_ui_process(); + return; + } + + m_is_showing_crash_page = false; m_should_suppress_history_for_current_load = false; m_should_suppress_history_for_next_load = false; + abandon_pending_web_content_session_history_seed(); + if (should_manage_session_history_in_ui_process()) { + m_session_history.mark_current_entry_reload_pending(); + m_current_web_content_session_history_matches_mirror = false; + update_navigation_action_state(); + dump_session_history("reload-mark-current-entry-reload-pending"sv); + } client().async_reload(page_id()); } -void ViewImplementation::traverse_the_history_by_delta(int delta) +ViewImplementation::HistoryTraversalOutcome ViewImplementation::traverse_the_history_by_delta( + int delta, + CheckForCancelation check_for_cancelation, + Function on_cancelation_check_complete) { + if (!should_manage_session_history_in_ui_process()) { + m_should_suppress_history_for_current_load = false; + m_should_suppress_history_for_next_load = false; + m_webdriver_pending_navigation_completes_with_session_history_update = false; + client().async_traverse_the_history_by_delta(page_id(), delta); + return { + .status = HistoryTraversalStatus::Started, + .will_change_top_level_entry = true, + }; + } + + // https://html.spec.whatwg.org/multipage/browsing-the-web.html#traverse-the-history-by-a-delta + // Let allSteps be the result of getting all used history steps for traversable. + // Let currentStepIndex be the index of traversable's current session history step within allSteps. + // Let targetStepIndex be currentStepIndex plus delta. + // + // AD-HOC: Browser UI calls do not pass sourceDocument. Since the UI process owns the top-level session history + // mirror, it resolves targetStepIndex before asking WebContent to apply or precheck the resulting + // traverse history step. + auto target = m_session_history.traversal_target_for_delta(delta); + if (!target.has_value()) { + dump_session_history("traverse-no-entry"sv); + return { .status = HistoryTraversalStatus::NoEntry }; + } + m_should_suppress_history_for_current_load = false; m_should_suppress_history_for_next_load = false; - client().async_traverse_the_history_by_delta(page_id(), delta); + + auto will_replace_web_content_process = !is_url_suitable_for_same_process_navigation(m_url, target->target_top_level_entry->url); + auto pending_traversal = PendingSessionHistoryTraversal { + .target_step = target->target_step, + .target_step_index = target->target_step_index, + .will_change_top_level_entry = target->changes_top_level_entry, + .will_replace_web_content_process = will_replace_web_content_process, + .on_cancelation_check_complete = nullptr, + }; + + auto web_content_can_apply_traversal = !m_pending_web_content_session_history_seed.should_send_entries + && !m_pending_web_content_session_history_seed.ignore_updates_until_seed + && !m_pending_web_content_session_history_seed.waiting_for_ack + && !m_loading_session_history_entry_from_ui_process + && !m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value() + && m_session_history.web_content_can_traverse_to(*target); + + // If WebContent already has enough state to apply the traverse history step, + // let it run the spec algorithm directly. + if (web_content_can_apply_traversal && !will_replace_web_content_process) { + m_pending_session_history_traversal = move(pending_traversal); + m_webdriver_pending_navigation_url = target->target_top_level_entry->url; + if (auto const* current_entry = m_session_history.current_entry()) { + m_webdriver_pending_navigation_completes_with_session_history_update = current_entry->document_state.id != 0 + && current_entry->document_state.id == target->target_top_level_entry->document_state.id; + } else { + m_webdriver_pending_navigation_completes_with_session_history_update = false; + } + dump_session_history("traverse-delegate-to-webcontent"sv); + client().async_traverse_the_history_to_step(page_id(), target->target_step); + return { + .status = HistoryTraversalStatus::Started, + .will_replace_web_content_process = will_replace_web_content_process, + .will_change_top_level_entry = target->changes_top_level_entry, + }; + } + + // https://html.spec.whatwg.org/multipage/browsing-the-web.html#apply-the-history-step + // If checkForCancelation is true, and the result of checking if unloading is canceled given + // navigablesCrossingDocuments, traversable, targetStep, and userInvolvement is not "continue", then return that + // result. + // + // AD-HOC: If WebContent cannot apply the step itself, the UI still asks it to run that precheck before moving + // the UI-owned history mirror and loading or reseeding WebContent from that state. A renderer-initiated + // traversal can report that this already happened, but only trust that precheck if WebContent can + // traverse to the same target step in the UI-owned history mirror. + auto needs_cancelation_check = check_for_cancelation == CheckForCancelation::Yes + || (check_for_cancelation == CheckForCancelation::IfWebContentCannotTraverseTarget && !web_content_can_apply_traversal); + if (needs_cancelation_check) { + pending_traversal.stage = PendingSessionHistoryTraversal::Stage::CheckingCancelation; + pending_traversal.cancelation_check_request_id = m_next_traverse_history_step_cancelation_check_request_id++; + pending_traversal.on_cancelation_check_complete = move(on_cancelation_check_complete); + auto request_id = pending_traversal.cancelation_check_request_id; + m_pending_session_history_traversal = move(pending_traversal); + client().async_check_if_traverse_history_step_is_canceled(page_id(), request_id, target->target_step); + dump_session_history("traverse-fallback-check-cancelation"sv); + return { + .status = HistoryTraversalStatus::Started, + .will_replace_web_content_process = will_replace_web_content_process, + .will_change_top_level_entry = target->changes_top_level_entry, + .waiting_for_cancelation_check = true, + }; + } + + pending_traversal.stage = PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess; + m_pending_session_history_traversal = move(pending_traversal); + load_session_history_traversal_target_from_ui_process(*target, "traverse-fallback-load"sv); + return { + .status = HistoryTraversalStatus::Started, + .will_replace_web_content_process = will_replace_web_content_process, + .will_change_top_level_entry = target->changes_top_level_entry, + }; } void ViewImplementation::zoom_in() @@ -773,6 +1013,9 @@ void ViewImplementation::request_style_sheet_source(Web::CSS::StyleSheetIdentifi void ViewImplementation::debug_request(ByteString const& request, ByteString const& argument) { + if (request == "dump-session-history"sv) + dump_session_history("debug-request"sv, SessionHistoryDumpMode::Always); + client().async_debug_request(page_id(), request, argument); } @@ -904,10 +1147,76 @@ void ViewImplementation::did_change_audio_play_state(Badge, We on_audio_play_state_changed(m_audio_play_state); } -void ViewImplementation::did_update_navigation_buttons_state(Badge, bool back_enabled, bool forward_enabled) const +void ViewImplementation::did_update_navigation_buttons_state(Badge, bool back_enabled, bool forward_enabled) { + VERIFY(!should_manage_session_history_in_ui_process()); + m_navigate_back_action->set_enabled(back_enabled); m_navigate_forward_action->set_enabled(forward_enabled); + dump_session_history("did-update-navigation-buttons-state-using-webcontent"sv); +} + +void ViewImplementation::did_update_session_history(Badge, Vector entries, Vector used_steps, size_t current_used_step_index) +{ + if (!should_manage_session_history_in_ui_process()) + return; + + if (history_debug_enabled()) { + dbgln("[History] UI received WebContent session history snapshot page={} pid={} current_used_step={} entries={} used_steps={}", + page_id(), + client().pid(), + current_used_step_index, + history_log_entries(entries), + history_log_steps(used_steps, current_used_step_index)); + } + if (m_pending_web_content_session_history_seed.waiting_for_ack) { + dump_session_history("ignored-session-history-before-ui-seed-ack"sv); + update_navigation_action_state(); + return; + } + auto pending_step_after_fallback_load_was_restored = false; + if (m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) { + if (current_used_step_index >= used_steps.size() || used_steps[current_used_step_index] != *m_pending_web_content_session_history_seed.step_after_loading_top_level_entry) { + dump_session_history("ignored-partial-session-history-before-fallback-seed"sv); + update_navigation_action_state(); + return; + } + pending_step_after_fallback_load_was_restored = true; + } + if (m_pending_web_content_session_history_seed.ignore_updates_until_seed) { + dump_session_history("ignored-session-history-before-ui-seed"sv); + update_navigation_action_state(); + return; + } + auto update_result = m_session_history.update_from_web_content(move(entries), move(used_steps), current_used_step_index); + m_current_web_content_session_history_matches_mirror = update_result == TraversableSessionHistory::UpdateResult::CompleteSnapshot + && m_session_history.web_content_history_matches_mirror(); + if (update_result != TraversableSessionHistory::UpdateResult::InvalidSnapshot) { + // A complete WebContent snapshot means the UI-owned navigation settled, + // including redirected navigations whose final URL differs from the + // original pending navigation URL. A partial snapshot only updates the + // UI mirror with the current WebContent-visible subset. + if (update_result == TraversableSessionHistory::UpdateResult::CompleteSnapshot) + m_pending_session_history_navigation.clear(); + if (auto* current_entry = m_session_history.current_entry()) { + auto current_url = current_entry->url; + auto const url_changed = m_url != current_url; + set_url(current_url); + if (url_changed && on_url_change) + on_url_change(m_url); + if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url != current_url) + m_webdriver_pending_navigation_url = current_url; + if (m_webdriver_pending_navigation_completes_with_session_history_update) + complete_webdriver_pending_navigation_if_url_matches(m_url); + } + if (pending_step_after_fallback_load_was_restored) + m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.clear(); + } else if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == m_url) { + prepare_to_seed_web_content_session_history_from_ui_process(); + seed_web_content_session_history_from_ui_process(); + } + update_navigation_action_state(); + dump_session_history("did-update-session-history"sv); } void ViewImplementation::did_change_needs_beforeunload_check(Badge, bool needs_beforeunload_check) @@ -999,7 +1308,9 @@ void ViewImplementation::initialize_client(CreateNewClient create_new_client) m_needs_beforeunload_check = true; if (create_new_client == CreateNewClient::Yes) { + auto client_handle = m_client_state.client_handle; m_client_state = {}; + m_client_state.client_handle = move(client_handle); // FIXME: Fail to open the tab, rather than crashing the whole application if this fails. m_client_state.client = Application::the().launch_web_content_process(*this).release_value_but_fixme_should_propagate_errors(); @@ -1007,7 +1318,8 @@ void ViewImplementation::initialize_client(CreateNewClient create_new_client) m_client_state.client->register_view(m_client_state.page_index, *this); } - m_client_state.client_handle = Web::Crypto::generate_random_uuid(); + if (m_client_state.client_handle.is_empty()) + m_client_state.client_handle = Web::Crypto::generate_random_uuid(); client().async_set_window_handle(m_client_state.page_index, m_client_state.client_handle); client().async_set_zoom_level(m_client_state.page_index, m_zoom_level); client().async_set_viewport(m_client_state.page_index, viewport_size(), m_device_pixel_ratio, m_is_fullscreen); @@ -1033,6 +1345,790 @@ void ViewImplementation::initialize_client(CreateNewClient create_new_client) client().async_did_connect_devtools_client(page_id()); } +void ViewImplementation::did_start_navigation(URL::URL const& url, Variant document_resource, bool is_redirect, Web::Bindings::NavigationHistoryBehavior history_handling) +{ + if (!should_manage_session_history_in_ui_process()) + return; + + if (m_should_suppress_history_for_next_load || m_should_suppress_history_for_current_load) + return; + + if (m_loading_session_history_entry_from_ui_process) { + auto should_keep_preseeded_web_content_history = m_pending_web_content_session_history_seed.waiting_for_ack || m_session_history.web_content_uses_ui_step_coordinates(); + m_loading_session_history_entry_from_ui_process = false; + if (!should_keep_preseeded_web_content_history) { + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + } + dump_session_history("did-start-navigation-from-ui-history-load"sv); + return; + } + + if (m_is_showing_crash_page) { + m_is_showing_crash_page = false; + if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) { + prepare_to_seed_web_content_session_history_from_ui_process(); + dump_session_history("did-start-navigation-from-crash-page"sv); + return; + } + } + + if (is_redirect) { + m_session_history.replace_current_entry_url(url); + if (m_pending_session_history_navigation.has_value()) + m_pending_session_history_navigation->url = url; + if (m_webdriver_pending_navigation_url.has_value()) + m_webdriver_pending_navigation_url = url; + m_current_web_content_session_history_matches_mirror = false; + update_navigation_action_state(); + dump_session_history("did-start-navigation-redirect"sv); + return; + } + + if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) { + if (m_pending_session_history_navigation.has_value() && m_pending_session_history_navigation->url == url) + return; + + if (history_handling == Web::Bindings::NavigationHistoryBehavior::Push && m_current_web_content_session_history_matches_mirror) + m_pending_session_history_navigation = PendingSessionHistoryNavigation { url, m_session_history }; + else + m_pending_session_history_navigation.clear(); + + if (history_handling == Web::Bindings::NavigationHistoryBehavior::Replace) { + m_session_history.replace_current_entry(url, move(document_resource)); + m_current_web_content_session_history_matches_mirror = false; + update_navigation_action_state(); + dump_session_history("did-start-navigation-replace-current-url"sv); + } else if (history_handling == Web::Bindings::NavigationHistoryBehavior::Push) { + m_session_history.navigate(url, move(document_resource)); + m_current_web_content_session_history_matches_mirror = false; + update_navigation_action_state(); + dump_session_history("did-start-navigation-push-current-url"sv); + } + return; + } + + if (m_session_history.current_entry()) + m_pending_session_history_navigation = PendingSessionHistoryNavigation { url, m_session_history }; + else + m_pending_session_history_navigation.clear(); + if (history_handling == Web::Bindings::NavigationHistoryBehavior::Replace) + m_session_history.replace_current_entry(url, move(document_resource)); + else + m_session_history.navigate(url, move(document_resource)); + m_current_web_content_session_history_matches_mirror = false; + update_navigation_action_state(); + dump_session_history("did-start-navigation"sv); +} + +void ViewImplementation::did_cancel_navigation(URL::URL const& url) +{ + if (m_pending_session_history_navigation.has_value() && m_pending_session_history_navigation->url == url) { + restore_pending_session_history_navigation("did-cancel-navigation"sv); + return; + } + + if (m_loading_session_history_entry_from_ui_process) { + m_loading_session_history_entry_from_ui_process = false; + abandon_pending_web_content_session_history_seed(); + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + update_navigation_action_state(); + dump_session_history("did-cancel-ui-history-load"sv); + return; + } + + if (m_webdriver_pending_navigation_url.has_value()) { + m_session_history.clear_current_entry_reload_pending(); + m_webdriver_pending_navigation_url = m_url; + m_webdriver_pending_navigation_completes_with_session_history_update = false; + complete_webdriver_pending_navigation_if_url_matches(m_url); + } + + dump_session_history("did-cancel-navigation-ignored"sv); +} + +void ViewImplementation::did_finish_navigation(URL::URL const& url) +{ + if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url == url && !m_webdriver_pending_navigation_completes_with_session_history_update) + complete_webdriver_pending_navigation_if_url_matches(url); + + if (m_pending_session_history_navigation.has_value() && m_pending_session_history_navigation->url == url) + m_pending_session_history_navigation.clear(); + + if (should_manage_session_history_in_ui_process() && m_pending_web_content_session_history_seed.should_send_entries) { + if (auto const* current_entry = m_session_history.current_entry(); current_entry && current_entry->url == url) { + m_session_history.clear_current_entry_reload_pending(); + m_pending_web_content_session_history_seed.should_reseed_after_current_history_load = false; + seed_web_content_session_history_from_ui_process(); + } else { + // The first finish notification from a fresh WebContent process can + // still report about:blank before the traversed-to entry is ready. + // Keep the pending seed state intact so partial snapshots remain + // ignored until we can seed the full UI-owned history. + dump_session_history("skip-seed-webcontent-session-history"sv); + } + } +} + +bool ViewImplementation::restore_pending_session_history_navigation(StringView reason) +{ + if (!m_pending_session_history_navigation.has_value()) + return false; + + auto web_content_restore_mode = m_pending_session_history_navigation->web_content_restore_mode; + m_session_history = move(m_pending_session_history_navigation->previous_session_history); + m_pending_session_history_navigation.clear(); + m_pending_session_history_traversal.clear(); + + if (auto* current_entry = m_session_history.current_entry()) { + auto current_url = current_entry->url; + auto const url_changed = m_url != current_url; + set_url(current_url); + if (url_changed && on_url_change) + on_url_change(m_url); + + if (web_content_restore_mode == PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess) { + prepare_to_seed_web_content_session_history_from_ui_process(); + m_should_suppress_history_for_current_load = false; + m_should_suppress_history_for_next_load = false; + m_webdriver_pending_navigation_url = current_url; + m_webdriver_pending_navigation_completes_with_session_history_update = true; + load_current_session_history_entry_from_ui_process(); + } else { + m_loading_session_history_entry_from_ui_process = false; + abandon_pending_web_content_session_history_seed(); + m_current_web_content_session_history_matches_mirror = m_session_history.web_content_history_matches_mirror(); + m_webdriver_pending_navigation_url.clear(); + m_webdriver_pending_navigation_completes_with_session_history_update = false; + complete_webdriver_pending_navigation_if_url_matches(current_url); + } + } else { + m_current_web_content_session_history_matches_mirror = false; + } + + update_navigation_action_state(); + dump_session_history(reason); + return true; +} + +void ViewImplementation::abandon_pending_web_content_session_history_seed() +{ + m_pending_web_content_session_history_seed.clear(); +} + +StringView ViewImplementation::pending_session_history_navigation_web_content_restore_mode_to_string(PendingSessionHistoryNavigation::WebContentRestoreMode mode) +{ + switch (mode) { + case PendingSessionHistoryNavigation::WebContentRestoreMode::PreserveCurrentProcessState: + return "preserve-current-process-state"sv; + case PendingSessionHistoryNavigation::WebContentRestoreMode::RestoreFromUIProcess: + return "restore-from-ui-process"sv; + } + VERIFY_NOT_REACHED(); +} + +StringView ViewImplementation::pending_session_history_traversal_stage_to_string(PendingSessionHistoryTraversal::Stage stage) +{ + switch (stage) { + case PendingSessionHistoryTraversal::Stage::ApplyingInWebContent: + return "applying-in-webcontent"sv; + case PendingSessionHistoryTraversal::Stage::CheckingCancelation: + return "checking-cancelation"sv; + case PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess: + return "loading-entry-from-ui-process"sv; + case PendingSessionHistoryTraversal::Stage::ReplacingWebContentProcess: + return "replacing-webcontent-process"sv; + case PendingSessionHistoryTraversal::Stage::RestoringNestedStepAfterSeed: + return "restoring-nested-step-after-seed"sv; + } + VERIFY_NOT_REACHED(); +} + +static Optional current_top_level_history_entry_index_for_step(Vector const& entries, Optional current_step) +{ + if (!current_step.has_value()) + return {}; + + Optional current_entry_index; + for (size_t i = 0; i < entries.size(); ++i) { + if (entries[i].step > *current_step) + break; + current_entry_index = i; + } + return current_entry_index; +} + +void ViewImplementation::did_start_webdriver_navigation(Badge, URL::URL const& url) +{ + m_webdriver_pending_navigation_url = url; + m_webdriver_pending_navigation_completes_with_session_history_update = false; +} + +void ViewImplementation::wait_for_webdriver_navigation_completion(Badge, Optional page_load_timeout, Function on_complete) +{ + if (!m_webdriver_pending_navigation_url.has_value()) { + on_complete(JsonValue {}); + return; + } + + auto request_id = m_next_webdriver_navigation_completion_request_id++; + auto request = make(); + request->on_complete = move(on_complete); + m_pending_webdriver_navigation_completion_requests.set(request_id, move(request)); + + auto listener_id = add_navigation_listener({ + .on_load_start = nullptr, + .on_load_finish = [this](URL::URL const& url) { + complete_webdriver_pending_navigation_if_url_matches(url); + }, + }); + m_pending_webdriver_navigation_completion_requests.get(request_id).value()->navigation_listener_id = listener_id; + + if (page_load_timeout.has_value()) { + auto timer_interval = *page_load_timeout > NumericLimits::max() ? NumericLimits::max() : static_cast(*page_load_timeout); + auto timer = Core::Timer::create_single_shot(timer_interval, [this, request_id] { + complete_webdriver_navigation_completion(request_id, Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::Timeout, "Navigation timed out"sv)); + }); + timer->start(); + m_pending_webdriver_navigation_completion_requests.get(request_id).value()->timer = move(timer); + } +} + +void ViewImplementation::complete_webdriver_navigation_completion(u64 request_id, Web::WebDriver::Response response) +{ + auto maybe_request = m_pending_webdriver_navigation_completion_requests.take(request_id); + if (!maybe_request.has_value()) + return; + + auto request = maybe_request.release_value(); + remove_navigation_listener(request->navigation_listener_id); + if (request->timer) + request->timer->stop(); + request->on_complete(move(response)); +} + +void ViewImplementation::complete_webdriver_pending_navigation_if_url_matches(URL::URL const& url) +{ + if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url != url) + return; + + m_webdriver_pending_navigation_url.clear(); + m_webdriver_pending_navigation_completes_with_session_history_update = false; + + Vector request_ids; + request_ids.ensure_capacity(m_pending_webdriver_navigation_completion_requests.size()); + for (auto const& request : m_pending_webdriver_navigation_completion_requests) + request_ids.unchecked_append(request.key); + + for (auto request_id : request_ids) { + Core::EventLoop::current().deferred_invoke([this, request_id] { + complete_webdriver_navigation_completion(request_id, JsonValue {}); + }); + } +} + +JsonValue ViewImplementation::webdriver_session_history() const +{ + JsonObject serialized; + serialized.set("currentURL"sv, m_url.serialize()); + serialized.set("webContentProcessID"sv, client().pid()); + serialized.set("backButtonEnabled"sv, m_navigate_back_action->enabled()); + serialized.set("forwardButtonEnabled"sv, m_navigate_forward_action->enabled()); + serialized.set("webContentHistoryMatchesUI"sv, m_current_web_content_session_history_matches_mirror); + serialized.set("loadingSessionHistoryEntryFromUI"sv, m_loading_session_history_entry_from_ui_process); + serialized.set("waitingToSeedWebContent"sv, m_pending_web_content_session_history_seed.should_send_entries); + serialized.set("waitingForWebContentSeedAck"sv, m_pending_web_content_session_history_seed.waiting_for_ack); + serialized.set("ignoringWebContentUpdatesUntilSeed"sv, m_pending_web_content_session_history_seed.ignore_updates_until_seed); + serialized.set("reseedAfterCurrentHistoryLoad"sv, m_pending_web_content_session_history_seed.should_reseed_after_current_history_load); + serialized.set("hasOnlyTopLevelUsedSteps"sv, m_session_history.has_only_top_level_used_steps()); + serialized.set("webContentUsesUIStepCoordinates"sv, m_session_history.web_content_uses_ui_step_coordinates()); + auto web_content_known_entries = m_session_history.web_content_known_entries(); + auto web_content_known_used_steps = m_session_history.web_content_known_used_steps(); + auto web_content_current_step = m_session_history.web_content_current_step(); + Optional web_content_current_step_index; + if (web_content_current_step.has_value()) + web_content_current_step_index = web_content_known_used_steps.find_first_index(*web_content_current_step); + serialized.set("webContentKnownEntries"sv, history_json_entries(web_content_known_entries, current_top_level_history_entry_index_for_step(web_content_known_entries, web_content_current_step))); + serialized.set("webContentKnownUsedSteps"sv, history_json_steps(web_content_known_used_steps, web_content_current_step_index)); + if (web_content_current_step.has_value()) + serialized.set("webContentCurrentStep"sv, *web_content_current_step); + else + serialized.set("webContentCurrentStep"sv, JsonValue {}); + + if (auto current_used_step_index = m_session_history.current_used_step_index(); current_used_step_index.has_value()) + serialized.set("currentUsedStepIndex"sv, *current_used_step_index); + else + serialized.set("currentUsedStepIndex"sv, JsonValue {}); + + if (auto pending_step = m_pending_web_content_session_history_seed.step_after_loading_top_level_entry; pending_step.has_value()) + serialized.set("pendingWebContentHistoryStepAfterFallbackLoad"sv, *pending_step); + else + serialized.set("pendingWebContentHistoryStepAfterFallbackLoad"sv, JsonValue {}); + + if (m_pending_session_history_navigation.has_value()) { + JsonObject pending_navigation; + pending_navigation.set("url"sv, m_pending_session_history_navigation->url.serialize()); + pending_navigation.set("webContentRestoreMode"sv, pending_session_history_navigation_web_content_restore_mode_to_string(m_pending_session_history_navigation->web_content_restore_mode)); + if (auto const* previous_current_entry = m_pending_session_history_navigation->previous_session_history.current_entry()) + pending_navigation.set("previousCurrentURL"sv, previous_current_entry->url.serialize()); + else + pending_navigation.set("previousCurrentURL"sv, JsonValue {}); + serialized.set("pendingSessionHistoryNavigation"sv, move(pending_navigation)); + } else { + serialized.set("pendingSessionHistoryNavigation"sv, JsonValue {}); + } + + if (m_pending_session_history_traversal.has_value()) { + JsonObject pending_traversal; + pending_traversal.set("targetStep"sv, m_pending_session_history_traversal->target_step); + pending_traversal.set("targetStepIndex"sv, m_pending_session_history_traversal->target_step_index); + pending_traversal.set("willChangeTopLevelEntry"sv, m_pending_session_history_traversal->will_change_top_level_entry); + pending_traversal.set("willReplaceWebContentProcess"sv, m_pending_session_history_traversal->will_replace_web_content_process); + pending_traversal.set("stage"sv, pending_session_history_traversal_stage_to_string(m_pending_session_history_traversal->stage)); + serialized.set("pendingSessionHistoryTraversal"sv, move(pending_traversal)); + } else { + serialized.set("pendingSessionHistoryTraversal"sv, JsonValue {}); + } + + serialized.set("entries"sv, history_json_entries(m_session_history)); + serialized.set("usedSteps"sv, history_json_steps(m_session_history)); + return serialized; +} + +String ViewImplementation::ui_process_session_history_for_testing(Badge) const +{ + return webdriver_session_history().serialized(); +} + +void ViewImplementation::update_navigation_action_state() +{ + m_navigate_back_action->set_enabled(m_session_history.can_go_back()); + m_navigate_forward_action->set_enabled(m_session_history.can_go_forward()); +} + +void ViewImplementation::seed_web_content_session_history_from_ui_process() +{ + auto current_top_level_entry_index = m_session_history.current_top_level_entry_index(); + if (!current_top_level_entry_index.has_value()) { + abandon_pending_web_content_session_history_seed(); + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + update_navigation_action_state(); + dump_session_history("skip-webcontent-session-history-seed-without-current-entry"sv); + return; + } + + auto entries = m_session_history.entries(); + if (entries.is_empty()) { + abandon_pending_web_content_session_history_seed(); + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + update_navigation_action_state(); + dump_session_history("skip-webcontent-session-history-seed-without-entries"sv); + return; + } + + if (history_debug_enabled()) { + dbgln("[History] UI seeds WebContent session history page={} pid={} current={} entries={}", + page_id(), + client().pid(), + *current_top_level_entry_index, + history_log_entries(entries, current_top_level_entry_index)); + } + + client().async_set_top_level_session_history(page_id(), move(entries), *current_top_level_entry_index); + m_pending_web_content_session_history_seed.waiting_for_ack = true; + m_pending_web_content_session_history_seed.should_send_entries = false; + update_navigation_action_state(); + dump_session_history("sent-webcontent-session-history-seed"sv); +} + +void ViewImplementation::prepare_to_seed_web_content_session_history_from_ui_process() +{ + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + m_pending_session_history_navigation.clear(); + m_pending_web_content_session_history_seed.clear(); + // A fresh or repaired WebContent process reaches the current top-level + // session history entry after loading or reseeding m_url. If the + // traversable's current session history step is nested, finish restoration + // by traversing to that step after seeding the top-level entries. + m_pending_web_content_session_history_seed.step_after_loading_top_level_entry = m_session_history.current_step_to_restore_after_loading_top_level_entry(); + m_pending_web_content_session_history_seed.should_send_entries = true; + m_pending_web_content_session_history_seed.ignore_updates_until_seed = true; +} + +void ViewImplementation::restore_current_session_history_entry_from_ui_process() +{ + m_webdriver_pending_navigation_url = m_url; + if (!m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) { + m_pending_web_content_session_history_seed.should_reseed_after_current_history_load = true; + seed_web_content_session_history_from_ui_process(); + } + load_current_session_history_entry_from_ui_process(); +} + +void ViewImplementation::load_current_session_history_entry_from_ui_process() +{ + m_loading_session_history_entry_from_ui_process = true; + + auto const* current_entry = m_session_history.current_entry(); + if (!current_entry) { + client().async_load_url(page_id(), m_url, Web::Bindings::NavigationHistoryBehavior::Auto); + return; + } + + auto history_handling = m_pending_web_content_session_history_seed.waiting_for_ack || m_session_history.web_content_uses_ui_step_coordinates() + ? Web::Bindings::NavigationHistoryBehavior::Replace + : Web::Bindings::NavigationHistoryBehavior::Auto; + client().async_load_url_with_document_resource( + page_id(), + current_entry->url, + current_entry->document_state.resource, + history_handling); +} + +void ViewImplementation::load_session_history_traversal_target_from_ui_process(TraversableSessionHistory::TraversalTarget const& target, StringView dump_reason) +{ + if (!m_pending_session_history_traversal.has_value() || m_pending_session_history_traversal->target_step != target.target_step) { + m_pending_session_history_traversal = PendingSessionHistoryTraversal { + .target_step = target.target_step, + .target_step_index = target.target_step_index, + .will_change_top_level_entry = target.changes_top_level_entry, + .will_replace_web_content_process = !is_url_suitable_for_same_process_navigation(m_url, target.target_top_level_entry->url), + .stage = PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess, + .on_cancelation_check_complete = nullptr, + }; + } else { + m_pending_session_history_traversal->stage = PendingSessionHistoryTraversal::Stage::LoadingEntryFromUIProcess; + } + + auto previous_session_history = m_session_history; + m_session_history.traverse_to(target.target_step_index); + update_navigation_action_state(); + + prepare_to_seed_web_content_session_history_from_ui_process(); + m_pending_session_history_navigation = PendingSessionHistoryNavigation { + target.target_top_level_entry->url, + move(previous_session_history), + }; + m_webdriver_pending_navigation_url = target.target_top_level_entry->url; + m_webdriver_pending_navigation_completes_with_session_history_update = false; + set_url(target.target_top_level_entry->url); + dump_session_history(dump_reason); + load_current_session_history_entry_from_ui_process(); +} + +NonnullRefPtr> ViewImplementation::reset_session_history_for_testing() +{ + m_pending_session_history_reset_for_testing = Core::Promise::construct(); + client().async_reset_session_history_for_testing(page_id()); + return *m_pending_session_history_reset_for_testing; +} + +void ViewImplementation::did_set_top_level_session_history(Badge, bool accepted, Vector entries, Vector used_steps, size_t current_used_step_index) +{ + if (!should_manage_session_history_in_ui_process()) + return; + + if (history_debug_enabled()) { + dbgln("[History] UI received WebContent session history seed ack page={} pid={} accepted={} current_used_step={} entries={} used_steps={}", + page_id(), + client().pid(), + accepted, + current_used_step_index, + history_log_entries(entries), + history_log_steps(used_steps, current_used_step_index)); + } + + if (!m_pending_web_content_session_history_seed.waiting_for_ack) { + dump_session_history("ignored-webcontent-session-history-seed-ack"sv); + return; + } + + if (!accepted) { + abandon_pending_web_content_session_history_seed(); + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + m_pending_session_history_traversal.clear(); + update_navigation_action_state(); + dump_session_history("webcontent-session-history-seed-rejected"sv); + return; + } + + if (!m_session_history.did_seed_web_content_from_ui_process(move(entries), move(used_steps), current_used_step_index)) { + if (m_pending_web_content_session_history_seed.should_reseed_after_current_history_load) { + m_pending_web_content_session_history_seed.waiting_for_ack = false; + m_pending_web_content_session_history_seed.should_send_entries = true; + m_pending_web_content_session_history_seed.ignore_updates_until_seed = true; + m_current_web_content_session_history_matches_mirror = false; + update_navigation_action_state(); + dump_session_history("webcontent-session-history-preload-seed-ack-mismatch"sv); + return; + } + + abandon_pending_web_content_session_history_seed(); + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + m_pending_session_history_traversal.clear(); + update_navigation_action_state(); + dump_session_history("webcontent-session-history-seed-ack-mismatch"sv); + return; + } + + m_pending_web_content_session_history_seed.waiting_for_ack = false; + if (m_pending_web_content_session_history_seed.should_reseed_after_current_history_load) { + m_pending_web_content_session_history_seed.should_send_entries = true; + m_pending_web_content_session_history_seed.ignore_updates_until_seed = true; + m_current_web_content_session_history_matches_mirror = false; + update_navigation_action_state(); + dump_session_history("webcontent-session-history-preload-seed-ack"sv); + return; + } + + m_pending_web_content_session_history_seed.ignore_updates_until_seed = false; + // A seed ack can arrive while a top-level navigation is still blocked. + // Keep the mirror provisional until that navigation settles. + m_current_web_content_session_history_matches_mirror = !m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value() + && !m_pending_session_history_navigation.has_value(); + if (m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) { + if (m_pending_session_history_traversal.has_value()) + m_pending_session_history_traversal->stage = PendingSessionHistoryTraversal::Stage::RestoringNestedStepAfterSeed; + client().async_traverse_the_history_to_step(page_id(), *m_pending_web_content_session_history_seed.step_after_loading_top_level_entry); + } else { + auto is_waiting_for_history_step_cancelation_check = m_pending_session_history_traversal.has_value() + && m_pending_session_history_traversal->stage == PendingSessionHistoryTraversal::Stage::CheckingCancelation; + if (!is_waiting_for_history_step_cancelation_check) { + m_pending_session_history_traversal.clear(); + if (!m_pending_session_history_navigation.has_value()) + complete_webdriver_pending_navigation_if_url_matches(m_url); + } + } + + update_navigation_action_state(); + dump_session_history("webcontent-session-history-seed-ack"sv); +} + +void ViewImplementation::did_traverse_the_history_to_step(Badge, i32 step, bool step_was_available, Web::HTML::HistoryStepResult result) +{ + if (!should_manage_session_history_in_ui_process()) + return; + + if (!m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.has_value()) { + if (!m_pending_session_history_traversal.has_value() || m_pending_session_history_traversal->target_step != step) { + dump_session_history("ignored-stale-webcontent-history-step-result"sv); + return; + } + + if (!step_was_available) { + auto target = m_session_history.traversal_target_for_step(step); + if (target.has_value()) { + load_session_history_traversal_target_from_ui_process(*target, "webcontent-history-step-unavailable-fallback-load"sv); + return; + } + + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + m_pending_session_history_traversal.clear(); + update_navigation_action_state(); + dump_session_history("webcontent-history-step-unavailable"sv); + return; + } + + if (result != Web::HTML::HistoryStepResult::Applied) { + if (m_webdriver_pending_navigation_url.has_value()) + m_webdriver_pending_navigation_url = m_url; + m_webdriver_pending_navigation_completes_with_session_history_update = false; + complete_webdriver_pending_navigation_if_url_matches(m_url); + m_pending_session_history_traversal.clear(); + update_navigation_action_state(); + dump_session_history("webcontent-history-step-canceled"sv); + } else { + if (!m_session_history.did_apply_web_content_traversal_to_step(step)) { + if (auto target = m_session_history.traversal_target_for_step(step); target.has_value()) { + load_session_history_traversal_target_from_ui_process(*target, "webcontent-history-step-applied-with-stale-mirror-fallback-load"sv); + return; + } + + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + m_pending_session_history_traversal.clear(); + update_navigation_action_state(); + dump_session_history("webcontent-history-step-applied-without-ui-target"sv); + return; + } + + m_current_web_content_session_history_matches_mirror = true; + if (auto const* current_entry = m_session_history.current_entry()) { + auto current_url = current_entry->url; + auto const url_changed = m_url != current_url; + set_url(current_url); + if (url_changed && on_url_change) + on_url_change(m_url); + if (m_webdriver_pending_navigation_url.has_value() && *m_webdriver_pending_navigation_url != current_url) + m_webdriver_pending_navigation_url = current_url; + } + m_pending_session_history_traversal.clear(); + update_navigation_action_state(); + dump_session_history("webcontent-history-step-applied"sv); + } + return; + } + + if (*m_pending_web_content_session_history_seed.step_after_loading_top_level_entry != step) { + dump_session_history("ignored-stale-webcontent-history-step-result"sv); + return; + } + + if (step_was_available && result == Web::HTML::HistoryStepResult::Applied) { + m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.clear(); + m_current_web_content_session_history_matches_mirror = m_session_history.did_restore_web_content_to_current_step(step); + m_pending_session_history_traversal.clear(); + complete_webdriver_pending_navigation_if_url_matches(m_url); + update_navigation_action_state(); + dump_session_history("webcontent-history-step-restored"sv); + } else { + auto reason = step_was_available ? "webcontent-pending-history-step-canceled"sv : "webcontent-history-step-unavailable"sv; + if (restore_pending_session_history_navigation(reason)) + return; + + m_pending_web_content_session_history_seed.step_after_loading_top_level_entry.clear(); + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + m_pending_session_history_traversal.clear(); + update_navigation_action_state(); + dump_session_history(reason); + } +} + +void ViewImplementation::did_check_if_traverse_history_step_is_canceled( + Badge, u64 request_id, i32 step, bool canceled) +{ + if (!should_manage_session_history_in_ui_process()) + return; + + if (!m_pending_session_history_traversal.has_value() + || m_pending_session_history_traversal->stage != PendingSessionHistoryTraversal::Stage::CheckingCancelation + || m_pending_session_history_traversal->cancelation_check_request_id != request_id + || m_pending_session_history_traversal->target_step != step) { + dump_session_history("ignored-stale-history-step-cancelation-check-result"sv); + return; + } + + if (canceled) { + auto on_cancelation_check_complete = move(m_pending_session_history_traversal->on_cancelation_check_complete); + if (m_webdriver_pending_navigation_url.has_value()) + m_webdriver_pending_navigation_url = m_url; + m_webdriver_pending_navigation_completes_with_session_history_update = false; + complete_webdriver_pending_navigation_if_url_matches(m_url); + m_pending_session_history_traversal.clear(); + update_navigation_action_state(); + dump_session_history("traverse-fallback-canceled-by-webcontent"sv); + if (on_cancelation_check_complete) + on_cancelation_check_complete({ + .status = HistoryTraversalStatus::Canceled, + }); + return; + } + + auto target = m_session_history.traversal_target_for_step(step); + if (!target.has_value()) { + auto on_cancelation_check_complete = move(m_pending_session_history_traversal->on_cancelation_check_complete); + m_current_web_content_session_history_matches_mirror = false; + m_session_history.forget_web_content_state(); + m_pending_session_history_traversal.clear(); + update_navigation_action_state(); + dump_session_history("traverse-fallback-cancelation-check-without-ui-target"sv); + if (on_cancelation_check_complete) + on_cancelation_check_complete({ + .status = HistoryTraversalStatus::NoEntry, + }); + return; + } + + auto on_cancelation_check_complete = move(m_pending_session_history_traversal->on_cancelation_check_complete); + if (on_cancelation_check_complete) + on_cancelation_check_complete({ + .status = HistoryTraversalStatus::Started, + .will_replace_web_content_process = m_pending_session_history_traversal->will_replace_web_content_process, + .will_change_top_level_entry = m_pending_session_history_traversal->will_change_top_level_entry, + }); + + load_session_history_traversal_target_from_ui_process(*target, "traverse-fallback-load-after-cancelation-check"sv); +} + +void ViewImplementation::did_reset_session_history_for_testing(Badge) +{ + auto promise = move(m_pending_session_history_reset_for_testing); + m_session_history.clear(); + m_current_web_content_session_history_matches_mirror = false; + m_pending_session_history_navigation.clear(); + m_pending_session_history_traversal.clear(); + m_loading_session_history_entry_from_ui_process = false; + abandon_pending_web_content_session_history_seed(); + m_webdriver_pending_navigation_url.clear(); + m_webdriver_pending_navigation_completes_with_session_history_update = false; + update_navigation_action_state(); + + if (promise) + promise->resolve({}); +} + +void ViewImplementation::mark_web_content_session_history_stale_for_testing(Badge) +{ + m_current_web_content_session_history_matches_mirror = false; + update_navigation_action_state(); + dump_session_history("marked-webcontent-session-history-stale-for-testing"sv); +} + +void ViewImplementation::dump_session_history(StringView reason, SessionHistoryDumpMode mode) const +{ + if (mode == SessionHistoryDumpMode::IfDebuggingEnabled && !history_debug_enabled()) + return; + + auto web_content_known_used_steps = m_session_history.web_content_known_used_steps(); + auto web_content_current_step = m_session_history.web_content_current_step(); + Optional web_content_current_step_index; + if (web_content_current_step.has_value()) + web_content_current_step_index = web_content_known_used_steps.find_first_index(*web_content_current_step); + auto web_content_known_entries = m_session_history.web_content_known_entries(); + auto web_content_current_top_level_entry_index = current_top_level_history_entry_index_for_step(web_content_known_entries, web_content_current_step); + + auto pending_navigation_url = "none"sv; + auto pending_navigation_restore_mode = "none"sv; + String pending_navigation_url_storage; + if (m_pending_session_history_navigation.has_value()) { + pending_navigation_url_storage = m_pending_session_history_navigation->url.serialize(); + pending_navigation_url = pending_navigation_url_storage.bytes_as_string_view(); + pending_navigation_restore_mode = pending_session_history_navigation_web_content_restore_mode_to_string(m_pending_session_history_navigation->web_content_restore_mode); + } + + dbgln("[History] UI session history page={} pid={} reason={} url='{}' webcontent_matches={} webcontent_uses_ui_steps={} loading_from_ui={} waiting_to_seed={} waiting_for_seed_ack={} ignore_until_seed={} reseed_after_current_load={} pending_webcontent_step={} pending_navigation_url={} pending_navigation_restore={} pending_traversal_target={} pending_traversal_stage={} webcontent_current_step={} back={} forward={} entries={} webcontent_known_entries={} webcontent_known_used_steps={}", + page_id(), + client().pid(), + reason, + m_url, + m_current_web_content_session_history_matches_mirror, + m_session_history.web_content_uses_ui_step_coordinates(), + m_loading_session_history_entry_from_ui_process, + m_pending_web_content_session_history_seed.should_send_entries, + m_pending_web_content_session_history_seed.waiting_for_ack, + m_pending_web_content_session_history_seed.ignore_updates_until_seed, + m_pending_web_content_session_history_seed.should_reseed_after_current_history_load, + m_pending_web_content_session_history_seed.step_after_loading_top_level_entry, + pending_navigation_url, + pending_navigation_restore_mode, + m_pending_session_history_traversal.has_value() ? Optional { m_pending_session_history_traversal->target_step } : Optional {}, + m_pending_session_history_traversal.has_value() ? pending_session_history_traversal_stage_to_string(m_pending_session_history_traversal->stage) : "none"sv, + web_content_current_step, + m_navigate_back_action->enabled(), + m_navigate_forward_action->enabled(), + history_log_entries(m_session_history), + history_log_entries(web_content_known_entries, web_content_current_top_level_entry_index), + history_log_steps(web_content_known_used_steps, web_content_current_step_index)); +} + void ViewImplementation::handle_web_content_process_crash(LoadErrorPage load_error_page) { auto const headless_mode = Application::browser_options().headless_mode.has_value(); @@ -1066,6 +2162,11 @@ void ViewImplementation::handle_web_content_process_crash(LoadErrorPage load_err // Don't keep a stale backup bitmap around. m_backup_shared_image_buffer = nullptr; + if (should_manage_session_history_in_ui_process()) { + m_loading_session_history_entry_from_ui_process = false; + prepare_to_seed_web_content_session_history_from_ui_process(); + } + handle_resize(); if (load_error_page == LoadErrorPage::Yes) { @@ -1076,6 +2177,10 @@ void ViewImplementation::handle_web_content_process_crash(LoadErrorPage load_err builder.appendff("

The web page {} has crashed.

You can reload the page to try again.

", escaped_url, escaped_url); builder.append(ERROR_HTML_FOOTER); load_crash_page_html(builder.string_view(), m_url); + } else if (should_manage_session_history_in_ui_process()) { + m_should_suppress_history_for_current_load = false; + m_should_suppress_history_for_next_load = false; + restore_current_session_history_entry_from_ui_process(); } } @@ -1280,10 +2385,10 @@ void ViewImplementation::initialize_context_menus() auto& application = Application::the(); m_navigate_back_action = Action::create("Go Back"sv, ActionID::NavigateBack, [this]() { - traverse_the_history_by_delta(-1); + (void)traverse_the_history_by_delta(-1); }); m_navigate_forward_action = Action::create("Go Forward"sv, ActionID::NavigateForward, [this]() { - traverse_the_history_by_delta(+1); + (void)traverse_the_history_by_delta(+1); }); m_navigate_back_action->set_enabled(false); m_navigate_forward_action->set_enabled(false); diff --git a/Libraries/LibWebView/ViewImplementation.h b/Libraries/LibWebView/ViewImplementation.h index 818301ee2a..75f4683fe2 100644 --- a/Libraries/LibWebView/ViewImplementation.h +++ b/Libraries/LibWebView/ViewImplementation.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -39,10 +41,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -73,7 +77,7 @@ public: String const& handle() const { return m_client_state.client_handle; } - void create_new_process_for_cross_site_navigation(URL::URL const&); + void create_new_process_for_cross_site_navigation(URL::URL const&, Variant, Web::Bindings::NavigationHistoryBehavior); void server_did_paint(Badge, i32 bitmap_id, Gfx::IntSize size); @@ -83,12 +87,32 @@ public: void set_system_visibility_state(Web::HTML::VisibilityState); - void load(URL::URL const&); + void load(URL::URL const&, Web::Bindings::NavigationHistoryBehavior = Web::Bindings::NavigationHistoryBehavior::Auto); void load_html(StringView); void load_navigation_error_page(StringView); void reload(); - void traverse_the_history_by_delta(int delta); + enum class HistoryTraversalStatus : u8 { + Started, + NoEntry, + Canceled, + }; + // NB: The HTML Standard spells this algorithm argument "checkForCancelation". + enum class CheckForCancelation : u8 { + Yes, + No, + IfWebContentCannotTraverseTarget, + }; + struct HistoryTraversalOutcome { + HistoryTraversalStatus status { HistoryTraversalStatus::NoEntry }; + bool will_replace_web_content_process { false }; + bool will_change_top_level_entry { false }; + bool waiting_for_cancelation_check { false }; + }; + [[nodiscard]] HistoryTraversalOutcome traverse_the_history_by_delta( + int delta, + CheckForCancelation = CheckForCancelation::Yes, + Function = nullptr); void zoom_in(); void zoom_out(); @@ -208,7 +232,18 @@ public: void did_change_audio_play_state(Badge, Web::HTML::AudioPlayState); Web::HTML::AudioPlayState audio_play_state() const { return m_audio_play_state; } - void did_update_navigation_buttons_state(Badge, bool back_enabled, bool forward_enabled) const; + void did_update_navigation_buttons_state(Badge, bool back_enabled, bool forward_enabled); + void did_update_session_history(Badge, Vector, Vector, size_t current_used_step_index); + void did_set_top_level_session_history(Badge, bool accepted, Vector, Vector used_steps, size_t current_used_step_index); + void did_traverse_the_history_to_step(Badge, i32 step, bool step_was_available, Web::HTML::HistoryStepResult); + void did_check_if_traverse_history_step_is_canceled( + Badge, u64 request_id, i32 step, bool canceled); + void did_reset_session_history_for_testing(Badge); + void mark_web_content_session_history_stale_for_testing(Badge); + void did_start_webdriver_navigation(Badge, URL::URL const&); + String ui_process_session_history_for_testing(Badge) const; + JsonValue webdriver_session_history() const; + void wait_for_webdriver_navigation_completion(Badge, Optional page_load_timeout, Function); void did_change_needs_beforeunload_check(Badge, bool needs_beforeunload_check); void did_change_background_color(Badge, Gfx::Color); Gfx::Color page_background_color() const { return m_page_background_color; } @@ -339,8 +374,28 @@ protected: u64 page_id() const; void set_url(URL::URL); + void did_start_navigation(URL::URL const&, Variant, bool is_redirect, Web::Bindings::NavigationHistoryBehavior); + void did_cancel_navigation(URL::URL const&); + void did_finish_navigation(URL::URL const&); + void complete_webdriver_navigation_completion(u64 request_id, Web::WebDriver::Response); + void complete_webdriver_pending_navigation_if_url_matches(URL::URL const&); + void update_navigation_action_state(); + enum class SessionHistoryDumpMode { + IfDebuggingEnabled, + Always, + }; + void dump_session_history(StringView reason, SessionHistoryDumpMode = SessionHistoryDumpMode::IfDebuggingEnabled) const; + bool restore_pending_session_history_navigation(StringView reason); + void abandon_pending_web_content_session_history_seed(); + void seed_web_content_session_history_from_ui_process(); + void prepare_to_seed_web_content_session_history_from_ui_process(); + void restore_current_session_history_entry_from_ui_process(); + void load_current_session_history_entry_from_ui_process(); + void load_session_history_traversal_target_from_ui_process(TraversableSessionHistory::TraversalTarget const&, StringView dump_reason); + NonnullRefPtr> reset_session_history_for_testing(); virtual void update_zoom(); + virtual bool should_manage_session_history_in_ui_process() const { return true; } String current_host() const; void apply_zoom_for_current_host(); @@ -392,6 +447,7 @@ protected: URL::URL m_url; Utf16String m_title; Optional m_favicon_base64_png; + bool m_is_showing_crash_page { false }; double m_zoom_level { 1.0 }; double m_device_pixel_ratio { 1.0 }; @@ -463,6 +519,66 @@ protected: Web::HTML::MuteState m_mute_state { Web::HTML::MuteState::Unmuted }; + struct PendingSessionHistoryNavigation { + enum class WebContentRestoreMode : u8 { + PreserveCurrentProcessState, + RestoreFromUIProcess, + }; + + URL::URL url; + TraversableSessionHistory previous_session_history; + WebContentRestoreMode web_content_restore_mode { WebContentRestoreMode::PreserveCurrentProcessState }; + }; + static StringView pending_session_history_navigation_web_content_restore_mode_to_string(PendingSessionHistoryNavigation::WebContentRestoreMode); + + struct PendingWebContentSessionHistorySeed { + bool should_send_entries { false }; + bool ignore_updates_until_seed { false }; + bool waiting_for_ack { false }; + bool should_reseed_after_current_history_load { false }; + Optional step_after_loading_top_level_entry; + + void clear() { *this = {}; } + }; + + struct PendingSessionHistoryTraversal { + enum class Stage : u8 { + ApplyingInWebContent, + CheckingCancelation, + LoadingEntryFromUIProcess, + ReplacingWebContentProcess, + RestoringNestedStepAfterSeed, + }; + + i32 target_step { 0 }; + size_t target_step_index { 0 }; + u64 cancelation_check_request_id { 0 }; + bool will_change_top_level_entry { false }; + bool will_replace_web_content_process { false }; + Stage stage { Stage::ApplyingInWebContent }; + Function on_cancelation_check_complete; + }; + static StringView pending_session_history_traversal_stage_to_string(PendingSessionHistoryTraversal::Stage); + + TraversableSessionHistory m_session_history; + bool m_current_web_content_session_history_matches_mirror { false }; + Optional m_pending_session_history_navigation; + Optional m_pending_session_history_traversal; + u64 m_next_traverse_history_step_cancelation_check_request_id { 0 }; + bool m_loading_session_history_entry_from_ui_process { false }; + PendingWebContentSessionHistorySeed m_pending_web_content_session_history_seed; + Optional m_webdriver_pending_navigation_url; + bool m_webdriver_pending_navigation_completes_with_session_history_update { false }; + RefPtr> m_pending_session_history_reset_for_testing; + + struct WebDriverNavigationCompletionRequest { + Function on_complete; + RefPtr timer; + u64 navigation_listener_id { 0 }; + }; + u64 m_next_webdriver_navigation_completion_request_id { 0 }; + HashMap> m_pending_webdriver_navigation_completion_requests; + // Most recent caret position pushed by WebContent, Used for placing platform IME overlays without a sync IPC. Optional m_input_caret_rect; diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index 95e7c6235f..7153ba8ea1 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -10,12 +10,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -341,10 +343,16 @@ void WebContentClient::did_present_bitmap(u64 page_id, Gfx::IntRect rect, i32 bi } } -void WebContentClient::did_request_new_process_for_navigation(u64 page_id, URL::URL url) +void WebContentClient::did_request_new_process_for_navigation(u64 page_id, URL::URL url, Variant document_resource, Web::Bindings::NavigationHistoryBehavior history_handling) { if (auto view = view_for_page_id(page_id); view.has_value()) - view->create_new_process_for_cross_site_navigation(url); + view->create_new_process_for_cross_site_navigation(url, move(document_resource), history_handling); +} + +void WebContentClient::did_start_webdriver_navigation(u64 page_id, URL::URL url) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) + view->did_start_webdriver_navigation({}, url); } void WebContentClient::maybe_record_history_visit_for_current_load(u64 page_id, URL::URL const& url, Optional title, StringView reason) @@ -366,7 +374,7 @@ void WebContentClient::maybe_record_history_visit_for_current_load(u64 page_id, m_history_recorded_urls_for_current_load.set(page_id, normalized_url.release_value()); } -void WebContentClient::did_start_loading(u64 page_id, URL::URL url, bool is_redirect) +void WebContentClient::did_start_loading(u64 page_id, URL::URL url, Variant document_resource, bool is_redirect, Web::Bindings::NavigationHistoryBehavior history_handling) { if (auto process = WebView::Application::the().find_process(m_process_handle.pid); process.has_value()) process->set_title(OptionalNone {}); @@ -376,6 +384,7 @@ void WebContentClient::did_start_loading(u64 page_id, URL::URL url, bool is_redi if (auto view = view_for_page_id(page_id); view.has_value()) { view->m_should_suppress_history_for_current_load = view->m_should_suppress_history_for_next_load; view->m_should_suppress_history_for_next_load = false; + view->did_start_navigation(url, move(document_resource), is_redirect, history_handling); view->set_url({}, url); @@ -389,6 +398,14 @@ void WebContentClient::did_start_loading(u64 page_id, URL::URL url, bool is_redi } } +void WebContentClient::did_cancel_loading(u64 page_id, URL::URL url) +{ + m_history_recorded_urls_for_current_load.remove(page_id); + + if (auto view = view_for_page_id(page_id); view.has_value()) + view->did_cancel_navigation(url); +} + void WebContentClient::did_finish_loading(u64 page_id, URL::URL url) { if (url.scheme() == "about"sv && url.paths().size() == 1) { @@ -421,6 +438,9 @@ void WebContentClient::did_finish_loading(u64 page_id, URL::URL url) if (view->favicon_base64_png().has_value()) Application::history_store().update_favicon(url, *view->favicon_base64_png()); } + + view->did_finish_navigation(client_url); + if (view->on_load_finish) view->on_load_finish(client_url); @@ -1098,6 +1118,149 @@ void WebContentClient::did_change_needs_beforeunload_check(u64 page_id, bool nee view->did_change_needs_beforeunload_check({}, needs_beforeunload_check); } +void WebContentClient::did_check_if_traverse_history_step_is_canceled( + u64 page_id, u64 request_id, i32 step, bool canceled) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) + view->did_check_if_traverse_history_step_is_canceled({}, request_id, step, canceled); +} + +Messages::WebContentClient::DidRequestTraverseTheHistoryByDeltaResponse WebContentClient::did_request_traverse_the_history_by_delta(u64 page_id, i32 delta, Web::HistoryTraversalPrecheck history_traversal_precheck) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) { + auto view_id = view->view_id(); + // This request is already a synchronous IPC from WebContent, so defer + // the UI traversal before it possibly calls back into WebContent for + // cancelation checks. + Core::deferred_invoke([view_id, delta, history_traversal_precheck] { + auto view = ViewImplementation::find_view_by_id(view_id); + if (!view.has_value()) + return; + auto check_for_cancelation = ViewImplementation::CheckForCancelation::IfWebContentCannotTraverseTarget; + if (history_traversal_precheck == Web::HistoryTraversalPrecheck::Needed) + check_for_cancelation = ViewImplementation::CheckForCancelation::Yes; + // NB: SourceDocumentSandboxingAlreadyDone only covers the source-document sandboxing + // check. If the UI process has to apply the traversal itself, WebContent still needs + // to run the cancelable part of the traverse history step prechecks. + else if (history_traversal_precheck == Web::HistoryTraversalPrecheck::SourceDocumentSandboxingAlreadyDone) + check_for_cancelation = ViewImplementation::CheckForCancelation::Yes; + (void)view->traverse_the_history_by_delta(delta, check_for_cancelation); + }); + return true; + } + + return false; +} + +void WebContentClient::did_request_webdriver_history_traversal(u64 page_id, u64 request_id, i32 delta) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) { + auto view_id = view->view_id(); + // This request originates from WebDriver in WebContent. Defer the UI + // traversal so it can safely call back into WebContent for the + // cancelation checks from the traverse history step algorithm. + Core::deferred_invoke([this, page_id, request_id, view_id, delta] { + auto view = ViewImplementation::find_view_by_id(view_id); + if (!view.has_value()) { + async_complete_webdriver_history_traversal(page_id, request_id, false, false, false); + return; + } + + auto complete = [this, page_id, request_id](ViewImplementation::HistoryTraversalOutcome outcome) { + auto traversal_started = outcome.status == ViewImplementation::HistoryTraversalStatus::Started; + async_complete_webdriver_history_traversal( + page_id, + request_id, + true, + traversal_started && outcome.will_replace_web_content_process, + traversal_started && outcome.will_change_top_level_entry); + }; + + auto outcome = view->traverse_the_history_by_delta(delta, ViewImplementation::CheckForCancelation::Yes, + [this, page_id, request_id](ViewImplementation::HistoryTraversalOutcome outcome) { + auto traversal_started = outcome.status == ViewImplementation::HistoryTraversalStatus::Started; + async_complete_webdriver_history_traversal( + page_id, + request_id, + true, + traversal_started && outcome.will_replace_web_content_process, + traversal_started && outcome.will_change_top_level_entry); + }); + if (!outcome.waiting_for_cancelation_check) + complete(outcome); + }); + return; + } + + async_complete_webdriver_history_traversal(page_id, request_id, false, false, false); +} + +Messages::WebContentClient::DidRequestWebdriverLoadUrlFromUiResponse WebContentClient::did_request_webdriver_load_url_from_ui(u64 page_id, URL::URL url) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) { + auto view_id = view->view_id(); + if (url.scheme() != "javascript"sv) + view->did_start_webdriver_navigation({}, url); + Core::deferred_invoke([view_id, url = move(url)] { + auto view = ViewImplementation::find_view_by_id(view_id); + if (!view.has_value()) + return; + view->load(url); + }); + return { JsonValue {} }; + } + + return { Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv) }; +} + +Messages::WebContentClient::DidRequestWebdriverTraverseHistoryFromUiResponse WebContentClient::did_request_webdriver_traverse_history_from_ui(u64 page_id, i32 delta) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) { + auto view_id = view->view_id(); + // This request is already a synchronous IPC from WebContent, so defer the + // UI traversal before running cancelation checks against WebContent. + Core::deferred_invoke([view_id, delta] { + auto view = ViewImplementation::find_view_by_id(view_id); + if (!view.has_value()) + return; + (void)view->traverse_the_history_by_delta(delta, ViewImplementation::CheckForCancelation::Yes); + }); + return { JsonValue {} }; + } + + return { Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv) }; +} + +Messages::WebContentClient::DidRequestWebdriverMarkWebContentSessionHistoryStaleResponse WebContentClient::did_request_webdriver_mark_web_content_session_history_stale(u64 page_id) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) { + view->mark_web_content_session_history_stale_for_testing({}); + return { JsonValue {} }; + } + + return { Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv) }; +} + +Messages::WebContentClient::DidRequestWebdriverSessionHistoryResponse WebContentClient::did_request_webdriver_session_history(u64 page_id) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) + return { view->webdriver_session_history() }; + + return { Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv) }; +} + +void WebContentClient::did_request_webdriver_navigation_completion(u64 page_id, u64 request_id, Optional page_load_timeout) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) { + view->wait_for_webdriver_navigation_completion({}, page_load_timeout, [this, page_id, request_id](Web::WebDriver::Response response) { + async_complete_webdriver_navigation_completion(page_id, request_id, move(response)); + }); + return; + } + + async_complete_webdriver_navigation_completion(page_id, request_id, Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv)); +} + void WebContentClient::did_update_resource_count(u64 page_id, i32 count_waiting) { if (auto view = view_for_page_id(page_id); view.has_value()) { @@ -1258,8 +1421,43 @@ void WebContentClient::did_change_audio_play_state(u64 page_id, Web::HTML::Audio void WebContentClient::did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) { - if (auto view = view_for_page_id(page_id); view.has_value()) + if (auto view = view_for_page_id(page_id); view.has_value()) { + if (view->should_manage_session_history_in_ui_process()) + return; view->did_update_navigation_buttons_state({}, back_enabled, forward_enabled); + } +} + +void WebContentClient::did_update_session_history(u64 page_id, Vector entries, Vector used_steps, size_t current_used_step_index) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) + view->did_update_session_history({}, move(entries), move(used_steps), current_used_step_index); +} + +Messages::WebContentClient::DidRequestUiProcessSessionHistoryForTestingResponse WebContentClient::did_request_ui_process_session_history_for_testing(u64 page_id) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) + return { view->ui_process_session_history_for_testing({}) }; + + return { "{}"_string }; +} + +void WebContentClient::did_set_top_level_session_history(u64 page_id, bool accepted, Vector entries, Vector used_steps, size_t current_used_step_index) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) + view->did_set_top_level_session_history({}, accepted, move(entries), move(used_steps), current_used_step_index); +} + +void WebContentClient::did_traverse_the_history_to_step(u64 page_id, i32 step, bool step_was_available, Web::HTML::HistoryStepResult result) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) + view->did_traverse_the_history_to_step({}, step, step_was_available, result); +} + +void WebContentClient::did_reset_session_history_for_testing(u64 page_id) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) + view->did_reset_session_history_for_testing({}); } void WebContentClient::did_present_backing_stores(u64 page_id, i32 front_bitmap_id, Gfx::SharedImage front_backing_store, i32 back_bitmap_id, Gfx::SharedImage back_backing_store) diff --git a/Libraries/LibWebView/WebContentClient.h b/Libraries/LibWebView/WebContentClient.h index 5bb116cbf7..1a1b5a8585 100644 --- a/Libraries/LibWebView/WebContentClient.h +++ b/Libraries/LibWebView/WebContentClient.h @@ -22,12 +22,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -96,7 +98,8 @@ private: virtual Messages::WebContentClient::AllocateCompositorContextIdResponse allocate_compositor_context_id(u64 page_id, Web::Compositor::PagePresentationRegistration) override; virtual void did_destroy_compositor_context(Web::Compositor::CompositorContextId) override; - virtual void did_request_new_process_for_navigation(u64 page_id, URL::URL url) override; + virtual void did_request_new_process_for_navigation(u64 page_id, URL::URL url, Variant document_resource, Web::Bindings::NavigationHistoryBehavior history_handling) override; + virtual void did_start_webdriver_navigation(u64 page_id, URL::URL url) override; virtual void did_finish_loading(u64 page_id, URL::URL) override; virtual void did_request_refresh(u64 page_id) override; virtual void did_request_cursor_change(u64 page_id, Gfx::Cursor) override; @@ -110,7 +113,8 @@ private: virtual void did_unhover_link(u64 page_id) override; virtual void did_click_link(u64 page_id, URL::URL, ByteString, unsigned) override; virtual void did_middle_click_link(u64 page_id, URL::URL, ByteString, unsigned) override; - virtual void did_start_loading(u64 page_id, URL::URL, bool) override; + virtual void did_start_loading(u64 page_id, URL::URL, Variant, bool, Web::Bindings::NavigationHistoryBehavior) override; + virtual void did_cancel_loading(u64 page_id, URL::URL) override; virtual void did_request_context_menu(u64 page_id, Gfx::IntPoint, Web::ContextMenuForInputEventsTarget) override; virtual void did_request_link_context_menu(u64 page_id, Gfx::IntPoint, URL::URL, ByteString, unsigned) override; virtual void did_request_image_context_menu(u64 page_id, Gfx::IntPoint, URL::URL, ByteString, unsigned, Optional) override; @@ -167,6 +171,13 @@ private: virtual void did_request_activate_tab(u64 page_id) override; virtual void did_close_browsing_context(u64 page_id) override; virtual void did_change_needs_beforeunload_check(u64 page_id, bool needs_beforeunload_check) override; + virtual Messages::WebContentClient::DidRequestTraverseTheHistoryByDeltaResponse did_request_traverse_the_history_by_delta(u64 page_id, i32 delta, Web::HistoryTraversalPrecheck) override; + virtual void did_request_webdriver_history_traversal(u64 page_id, u64 request_id, i32 delta) override; + virtual Messages::WebContentClient::DidRequestWebdriverLoadUrlFromUiResponse did_request_webdriver_load_url_from_ui(u64 page_id, URL::URL url) override; + virtual Messages::WebContentClient::DidRequestWebdriverTraverseHistoryFromUiResponse did_request_webdriver_traverse_history_from_ui(u64 page_id, i32 delta) override; + virtual Messages::WebContentClient::DidRequestWebdriverMarkWebContentSessionHistoryStaleResponse did_request_webdriver_mark_web_content_session_history_stale(u64 page_id) override; + virtual Messages::WebContentClient::DidRequestWebdriverSessionHistoryResponse did_request_webdriver_session_history(u64 page_id) override; + virtual void did_request_webdriver_navigation_completion(u64 page_id, u64 request_id, Optional page_load_timeout) override; virtual void did_update_resource_count(u64 page_id, i32 count_waiting) override; virtual void did_request_restore_window(u64 page_id) override; virtual void did_request_reposition_window(u64 page_id, Gfx::IntPoint) override; @@ -194,6 +205,13 @@ private: virtual void did_update_primary_selection(u64 page_id, String) override; virtual void did_change_audio_play_state(u64 page_id, Web::HTML::AudioPlayState) override; virtual void did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) override; + virtual void did_update_session_history(u64 page_id, Vector, Vector, size_t current_used_step_index) override; + virtual Messages::WebContentClient::DidRequestUiProcessSessionHistoryForTestingResponse did_request_ui_process_session_history_for_testing(u64 page_id) override; + virtual void did_set_top_level_session_history(u64 page_id, bool accepted, Vector, Vector used_steps, size_t current_used_step_index) override; + virtual void did_traverse_the_history_to_step(u64 page_id, i32 step, bool step_was_available, Web::HTML::HistoryStepResult) override; + virtual void did_check_if_traverse_history_step_is_canceled( + u64 page_id, u64 request_id, i32 step, bool canceled) override; + virtual void did_reset_session_history_for_testing(u64 page_id) override; virtual Messages::WebContentClient::StartWorkerAgentResponse start_worker_agent(u64 page_id, Web::HTML::WorkerAgentStartRequest request) override; virtual void close_worker_agent(u64 page_id, Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) override; diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index f7d0ed139f..a6043606a2 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -152,7 +153,7 @@ Messages::WebContentServer::GetWindowHandleResponse ConnectionFromClient::get_wi void ConnectionFromClient::set_window_handle(u64 page_id, String handle) { if (auto page = this->page(page_id); page.has_value()) { - page->page().top_level_traversable()->set_window_handle(move(handle)); + page->set_window_handle(move(handle)); page->send_current_needs_beforeunload_check(); } } @@ -166,6 +167,18 @@ void ConnectionFromClient::connect_to_webdriver(u64 page_id, ByteString webdrive } } +void ConnectionFromClient::complete_webdriver_navigation_completion(u64 page_id, u64 request_id, Web::WebDriver::Response response) +{ + if (auto page = this->page(page_id); page.has_value()) + page->did_complete_webdriver_navigation_completion(request_id, move(response)); +} + +void ConnectionFromClient::complete_webdriver_history_traversal(u64 page_id, u64 request_id, bool accepted, bool will_replace_web_content_process, bool will_change_top_level_entry) +{ + if (auto page = this->page(page_id); page.has_value()) + page->did_complete_webdriver_history_traversal(request_id, accepted, will_replace_web_content_process, will_change_top_level_entry); +} + void ConnectionFromClient::connect_to_web_ui(u64 page_id, IPC::TransportHandle handle) { if (auto page = this->page(page_id); page.has_value()) { @@ -218,13 +231,24 @@ void ConnectionFromClient::update_screen_rects(u64 page_id, Vectorset_screen_rects(rects, main_screen); } -void ConnectionFromClient::load_url(u64 page_id, URL::URL url) +void ConnectionFromClient::load_url(u64 page_id, URL::URL url, Web::Bindings::NavigationHistoryBehavior history_handling) { auto page = this->page(page_id); if (!page.has_value()) return; - page->page().load(url); + page->page().load(url, history_handling); +} + +void ConnectionFromClient::load_url_with_document_resource(u64 page_id, URL::URL url, + Variant document_resource, + Web::Bindings::NavigationHistoryBehavior history_handling) +{ + auto page = this->page(page_id); + if (!page.has_value()) + return; + + page->page().load(url, move(document_resource), history_handling); } void ConnectionFromClient::load_html(u64 page_id, ByteString html) @@ -248,7 +272,65 @@ void ConnectionFromClient::reload(u64 page_id) void ConnectionFromClient::traverse_the_history_by_delta(u64 page_id, i32 delta) { if (auto page = this->page(page_id); page.has_value()) - page->page().traverse_the_history_by_delta(delta); + page->page().traverse_the_history_by_delta_from_ui_process(delta); +} + +void ConnectionFromClient::traverse_the_history_to_step(u64 page_id, i32 step) +{ + auto page = this->page(page_id); + if (!page.has_value()) { + async_did_traverse_the_history_to_step(page_id, step, false, Web::HTML::HistoryStepResult::Applied); + return; + } + + page->page().top_level_traversable()->traverse_the_history_to_step(step, + GC::create_function(Web::HTML::main_thread_event_loop().heap(), [this, page_id, step](bool step_was_available, Web::HTML::HistoryStepResult result) { + async_did_traverse_the_history_to_step(page_id, step, step_was_available, result); + })); +} + +void ConnectionFromClient::check_if_traverse_history_step_is_canceled(u64 page_id, u64 request_id, i32 step) +{ + auto page = this->page(page_id); + if (!page.has_value()) { + async_did_check_if_traverse_history_step_is_canceled(page_id, request_id, step, true); + return; + } + + auto& heap = Web::HTML::main_thread_event_loop().heap(); + page->page().top_level_traversable()->check_if_traverse_history_step_is_canceled(step, + GC::create_function(heap, [this, page_id, request_id, step](Web::HTML::HistoryStepResult result) { + async_did_check_if_traverse_history_step_is_canceled( + page_id, request_id, step, result != Web::HTML::HistoryStepResult::Applied); + })); +} + +void ConnectionFromClient::set_top_level_session_history(u64 page_id, Vector entries, size_t current_top_level_entry_index) +{ + if (auto page = this->page(page_id); page.has_value()) { + auto accepted = page->page().top_level_traversable()->replace_top_level_session_history_entries_from_ui_process(move(entries), current_top_level_entry_index); + if (accepted) { + auto session_history_snapshot = page->page().top_level_traversable()->create_session_history_snapshot(); + async_did_set_top_level_session_history(page_id, accepted, move(session_history_snapshot.top_level_session_history_entries), move(session_history_snapshot.used_session_history_steps), session_history_snapshot.current_used_step_index); + } else { + async_did_set_top_level_session_history(page_id, accepted, {}, {}, 0); + } + } else { + async_did_set_top_level_session_history(page_id, false, {}, {}, 0); + } +} + +void ConnectionFromClient::reset_session_history_for_testing(u64 page_id) +{ + if (auto page = this->page(page_id); page.has_value()) { + auto& event_loop = Web::HTML::main_thread_event_loop(); + page->page().top_level_traversable()->reset_session_history_for_testing( + GC::create_function(event_loop.heap(), [this, page_id] { + async_did_reset_session_history_for_testing(page_id); + })); + } else { + async_did_reset_session_history_for_testing(page_id); + } } void ConnectionFromClient::set_viewport(u64 page_id, Web::DevicePixelSize size, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen) diff --git a/Services/WebContent/ConnectionFromClient.h b/Services/WebContent/ConnectionFromClient.h index a63a255381..cd0054d974 100644 --- a/Services/WebContent/ConnectionFromClient.h +++ b/Services/WebContent/ConnectionFromClient.h @@ -18,11 +18,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -72,6 +74,8 @@ private: virtual Messages::WebContentServer::GetWindowHandleResponse get_window_handle(u64 page_id) override; virtual void set_window_handle(u64 page_id, String handle) override; virtual void connect_to_webdriver(u64 page_id, ByteString webdriver_endpoint) override; + virtual void complete_webdriver_history_traversal(u64 page_id, u64 request_id, bool accepted, bool will_replace_web_content_process, bool will_change_top_level_entry) override; + virtual void complete_webdriver_navigation_completion(u64 page_id, u64 request_id, Web::WebDriver::Response response) override; virtual void connect_to_web_ui(u64 page_id, IPC::TransportHandle handle) override; virtual void connect_to_request_server(IPC::TransportHandle handle) override; virtual void connect_to_image_decoder(IPC::TransportHandle handle) override; @@ -79,11 +83,17 @@ private: virtual void compositor_process_reconnected() override; virtual void update_system_theme(u64 page_id, Core::AnonymousBuffer) override; virtual void update_screen_rects(u64 page_id, Vector, u32) override; - virtual void load_url(u64 page_id, URL::URL) override; + virtual void load_url(u64 page_id, URL::URL, Web::Bindings::NavigationHistoryBehavior) override; + virtual void load_url_with_document_resource(u64 page_id, URL::URL, + Variant, Web::Bindings::NavigationHistoryBehavior) override; virtual void load_html(u64 page_id, ByteString) override; virtual void load_html_with_url(u64 page_id, ByteString, URL::URL) override; virtual void reload(u64 page_id) override; virtual void traverse_the_history_by_delta(u64 page_id, i32 delta) override; + virtual void traverse_the_history_to_step(u64 page_id, i32 step) override; + virtual void check_if_traverse_history_step_is_canceled(u64 page_id, u64 request_id, i32 step) override; + virtual void set_top_level_session_history(u64 page_id, Vector, size_t current_top_level_entry_index) override; + virtual void reset_session_history_for_testing(u64 page_id) override; virtual void set_viewport(u64 page_id, Web::DevicePixelSize, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen) override; virtual void key_event(u64 page_id, Web::KeyEvent) override; virtual void mouse_event(u64 page_id, Web::MouseEvent) override; diff --git a/Services/WebContent/PageClient.cpp b/Services/WebContent/PageClient.cpp index f4e393a966..a3915ed91f 100644 --- a/Services/WebContent/PageClient.cpp +++ b/Services/WebContent/PageClient.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,7 @@ namespace WebContent { static PageClient::UseSkiaPainter s_use_skia_painter = PageClient::UseSkiaPainter::GPUBackendIfAvailable; static bool s_is_headless { false }; static bool s_async_scrolling_enabled { false }; +static bool s_should_report_session_history_updates_in_test_mode { false }; GC_DEFINE_ALLOCATOR(PageClient); @@ -86,6 +88,11 @@ void PageClient::set_async_scrolling_enabled(bool enabled) s_async_scrolling_enabled = enabled; } +void PageClient::set_should_report_session_history_updates_in_test_mode(bool should_report) +{ + s_should_report_session_history_updates_in_test_mode = should_report; +} + GC::Ref PageClient::create(JS::VM& vm, PageHost& page_host, u64 id) { return vm.heap().allocate(page_host, id); @@ -143,6 +150,19 @@ void PageClient::set_has_focus(bool has_focus) } } +void PageClient::set_window_handle(String window_handle) +{ + page().top_level_traversable()->set_window_handle(move(window_handle)); + + if (m_webdriver) + m_webdriver->page_did_set_window_handle({}, page().top_level_traversable()->window_handle()); +} + +void PageClient::did_start_webdriver_navigation(URL::URL const& url) +{ + client().async_did_start_webdriver_navigation(m_id, url); +} + void PageClient::setup_palette() { // FIXME: Get the proper palette from our peer somehow @@ -165,9 +185,12 @@ bool PageClient::is_url_suitable_for_same_process_navigation(URL::URL const& cur return WebView::is_url_suitable_for_same_process_navigation(current_url, target_url); } -void PageClient::request_new_process_for_navigation(URL::URL const& url) +void PageClient::request_new_process_for_navigation(URL::URL const& url, Variant document_resource, Web::Bindings::NavigationHistoryBehavior history_handling) { - client().async_did_request_new_process_for_navigation(m_id, url); + if (m_webdriver) + m_webdriver->page_did_start_window_replacement({}, page().top_level_traversable()->window_handle()); + + client().async_did_request_new_process_for_navigation(m_id, url, move(document_resource), history_handling); } Gfx::Palette PageClient::palette() const @@ -384,9 +407,20 @@ void PageClient::page_did_middle_click_link(URL::URL const& url, ByteString cons client().async_did_middle_click_link(m_id, url, target, modifiers); } -void PageClient::page_did_start_loading(URL::URL const& url, bool is_redirect) +void PageClient::page_did_start_loading(URL::URL const& url, Variant document_resource, bool is_redirect, Web::Bindings::NavigationHistoryBehavior history_handling) { - client().async_did_start_loading(m_id, url, is_redirect); + if (m_webdriver) + m_webdriver->page_did_start_loading({}, url); + + client().async_did_start_loading(m_id, url, move(document_resource), is_redirect, history_handling); +} + +void PageClient::page_did_cancel_loading(URL::URL const& url) +{ + if (m_webdriver) + m_webdriver->page_did_cancel_loading({}, url); + + client().async_did_cancel_loading(m_id, url); } void PageClient::page_did_create_new_document(Web::DOM::Document& document) @@ -415,6 +449,22 @@ void PageClient::page_did_finish_loading(URL::URL const& url) client().async_did_finish_loading(m_id, url); } +void PageClient::wait_for_webdriver_navigation_completion(Optional page_load_timeout, Function on_complete) +{ + auto request_id = m_next_webdriver_navigation_completion_request_id++; + m_pending_webdriver_navigation_completion_requests.set(request_id, move(on_complete)); + client().async_did_request_webdriver_navigation_completion(m_id, request_id, page_load_timeout); +} + +void PageClient::did_complete_webdriver_navigation_completion(u64 request_id, Web::WebDriver::Response response) +{ + auto maybe_callback = m_pending_webdriver_navigation_completion_requests.take(request_id); + if (!maybe_callback.has_value()) + return; + + maybe_callback.value()(move(response)); +} + void PageClient::page_did_finish_test(String const& text) { client().async_did_finish_test(m_id, text); @@ -764,6 +814,9 @@ void PageClient::page_did_close_top_level_traversable() { page().top_level_traversable()->compositor_context().set_presentation_mode(Empty {}); + if (m_webdriver) + m_webdriver->page_did_close_window({}, page().top_level_traversable()->window_handle()); + // FIXME: Rename this IPC call client().async_did_close_browsing_context(m_id); @@ -787,6 +840,66 @@ void PageClient::page_did_update_navigation_buttons_state(bool back_enabled, boo client().async_did_update_navigation_buttons_state(m_id, back_enabled, forward_enabled); } +bool PageClient::should_report_session_history_updates() const +{ + return !Web::HTML::Window::in_test_mode() || s_should_report_session_history_updates_in_test_mode; +} + +void PageClient::page_did_update_session_history(Vector const& entries, Vector const& used_steps, size_t current_used_step_index) +{ + client().async_did_update_session_history(m_id, entries, used_steps, current_used_step_index); +} + +String PageClient::page_did_request_ui_process_session_history_for_testing() +{ + return client().did_request_ui_process_session_history_for_testing(m_id); +} + +bool PageClient::page_did_request_traverse_the_history_by_delta(int delta, Web::HistoryTraversalPrecheck history_traversal_precheck) +{ + return client().did_request_traverse_the_history_by_delta(m_id, delta, history_traversal_precheck); +} + +void PageClient::request_webdriver_history_traversal(int delta, Function on_complete) +{ + auto request_id = m_next_webdriver_history_traversal_request_id++; + m_pending_webdriver_history_traversal_requests.set(request_id, move(on_complete)); + client().async_did_request_webdriver_history_traversal(m_id, request_id, delta); +} + +void PageClient::did_complete_webdriver_history_traversal(u64 request_id, bool accepted, bool will_replace_web_content_process, bool will_change_top_level_entry) +{ + auto maybe_callback = m_pending_webdriver_history_traversal_requests.take(request_id); + if (!maybe_callback.has_value()) + return; + + maybe_callback.value()(WebDriverHistoryTraversalResult { + .accepted = accepted, + .will_replace_web_content_process = will_replace_web_content_process, + .will_change_top_level_entry = will_change_top_level_entry, + }); +} + +Web::WebDriver::Response PageClient::request_webdriver_load_url_from_ui(URL::URL const& url) +{ + return client().did_request_webdriver_load_url_from_ui(m_id, url); +} + +Web::WebDriver::Response PageClient::request_webdriver_traverse_history_from_ui(int delta) +{ + return client().did_request_webdriver_traverse_history_from_ui(m_id, delta); +} + +Web::WebDriver::Response PageClient::request_webdriver_mark_web_content_session_history_stale() +{ + return client().did_request_webdriver_mark_web_content_session_history_stale(m_id); +} + +Web::WebDriver::Response PageClient::request_webdriver_session_history() +{ + return client().did_request_webdriver_session_history(m_id); +} + void PageClient::request_file(Web::FileRequest file_request) { client().request_file(m_id, move(file_request)); diff --git a/Services/WebContent/PageClient.h b/Services/WebContent/PageClient.h index 4eb01d7551..866141cc40 100644 --- a/Services/WebContent/PageClient.h +++ b/Services/WebContent/PageClient.h @@ -8,13 +8,17 @@ #pragma once +#include +#include #include #include #include #include +#include #include #include #include +#include #include #include #include @@ -43,6 +47,7 @@ public: static void set_is_headless(bool); static void set_async_scrolling_enabled(bool); + static void set_should_report_session_history_updates_in_test_mode(bool); virtual Web::Page& page() override { return *m_page; } virtual Web::Page const& page() const override { return *m_page; } @@ -68,6 +73,19 @@ public: void set_preferred_contrast(Web::CSS::PreferredContrast); void set_preferred_motion(Web::CSS::PreferredMotion); void set_has_focus(bool); + void set_window_handle(String); + void did_start_webdriver_navigation(URL::URL const&); + struct WebDriverHistoryTraversalResult { + bool accepted { false }; + bool will_replace_web_content_process { false }; + bool will_change_top_level_entry { false }; + }; + void request_webdriver_history_traversal(int delta, Function); + void did_complete_webdriver_history_traversal(u64 request_id, bool accepted, bool will_replace_web_content_process, bool will_change_top_level_entry); + Web::WebDriver::Response request_webdriver_load_url_from_ui(URL::URL const&); + Web::WebDriver::Response request_webdriver_traverse_history_from_ui(int delta); + Web::WebDriver::Response request_webdriver_mark_web_content_session_history_stale(); + Web::WebDriver::Response request_webdriver_session_history(); void set_is_scripting_enabled(bool); void set_window_position(Web::DevicePixelPoint); void set_window_size(Web::DevicePixelSize); @@ -111,6 +129,8 @@ public: void queue_screenshot_task(Optional node_id); void send_current_needs_beforeunload_check(); + void wait_for_webdriver_navigation_completion(Optional page_load_timeout, Function); + void did_complete_webdriver_navigation_completion(u64 request_id, Web::WebDriver::Response); void clear_pending_dom_mutations(); void did_delete_all_cookies(u64 request_id); @@ -127,7 +147,7 @@ private: // ^PageClient virtual bool is_connection_open() const override; virtual bool is_url_suitable_for_same_process_navigation(URL::URL const& current_url, URL::URL const& target_url) const override; - virtual void request_new_process_for_navigation(URL::URL const&) override; + virtual void request_new_process_for_navigation(URL::URL const&, Variant, Web::Bindings::NavigationHistoryBehavior) override; virtual Gfx::Palette palette() const override; virtual Web::DevicePixelRect screen_rect() const override { return m_all_screen_rects[m_main_screen_index]; } virtual size_t screen_count() const override { return m_all_screen_rects.size(); } @@ -158,7 +178,8 @@ private: virtual void page_did_request_link_context_menu(Web::CSSPixelPoint, URL::URL const&, ByteString const& target, unsigned modifiers) override; virtual void page_did_request_image_context_menu(Web::CSSPixelPoint, URL::URL const&, ByteString const& target, unsigned modifiers, Optional) override; virtual void page_did_request_media_context_menu(Web::CSSPixelPoint, ByteString const& target, unsigned modifiers, Web::Page::MediaContextMenu const&) override; - virtual void page_did_start_loading(URL::URL const&, bool) override; + virtual void page_did_start_loading(URL::URL const&, Variant, bool, Web::Bindings::NavigationHistoryBehavior) override; + virtual void page_did_cancel_loading(URL::URL const&) override; virtual void page_did_create_new_document(Web::DOM::Document&) override; virtual void page_did_change_active_document_in_top_level_browsing_context(Web::DOM::Document&) override; virtual void page_did_finish_loading(URL::URL const&) override; @@ -195,6 +216,10 @@ private: virtual void page_did_close_top_level_traversable() override; virtual void page_did_change_needs_beforeunload_check(bool needs_beforeunload_check) override; virtual void page_did_update_navigation_buttons_state(bool back_enabled, bool forward_enabled) override; + virtual bool should_report_session_history_updates() const override; + virtual void page_did_update_session_history(Vector const&, Vector const& used_steps, size_t current_used_step_index) override; + virtual String page_did_request_ui_process_session_history_for_testing() override; + virtual bool page_did_request_traverse_the_history_by_delta(int delta, Web::HistoryTraversalPrecheck) override; virtual void request_file(Web::FileRequest) override; virtual void page_did_request_color_picker(Color current_color) override; virtual void page_did_request_file_picker(Web::HTML::FileFilter const& accepted_file_types, Web::HTML::AllowMultipleFiles) override; @@ -247,6 +272,11 @@ private: Core::AnonymousBuffer m_document_cookie_version_buffer; + u64 m_next_webdriver_navigation_completion_request_id { 0 }; + HashMap> m_pending_webdriver_navigation_completion_requests; + u64 m_next_webdriver_history_traversal_request_id { 0 }; + HashMap> m_pending_webdriver_history_traversal_requests; + RefPtr m_webdriver; RefPtr m_web_ui; diff --git a/Services/WebContent/WebContentClient.ipc b/Services/WebContent/WebContentClient.ipc index 59419fe280..d51de31155 100644 --- a/Services/WebContent/WebContentClient.ipc +++ b/Services/WebContent/WebContentClient.ipc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -19,12 +20,15 @@ #include #include #include +#include #include #include +#include #include #include #include #include +#include #include #include #include @@ -39,8 +43,10 @@ endpoint WebContentClient allocate_compositor_context_id(u64 page_id, Web::Compositor::PagePresentationRegistration page_presentation_registration) => (Web::Compositor::CompositorContextId context_id) did_destroy_compositor_context(Web::Compositor::CompositorContextId context_id) =| - did_request_new_process_for_navigation(u64 page_id, URL::URL url) =| - did_start_loading(u64 page_id, URL::URL url, bool is_redirect) =| + did_request_new_process_for_navigation(u64 page_id, URL::URL url, Variant document_resource, Web::Bindings::NavigationHistoryBehavior history_handling) =| + did_start_webdriver_navigation(u64 page_id, URL::URL url) =| + did_start_loading(u64 page_id, URL::URL url, Variant document_resource, bool is_redirect, Web::Bindings::NavigationHistoryBehavior history_handling) =| + did_cancel_loading(u64 page_id, URL::URL url) =| did_finish_loading(u64 page_id, URL::URL url) =| did_request_refresh(u64 page_id) =| did_request_cursor_change(u64 page_id, Gfx::Cursor cursor) =| @@ -114,6 +120,13 @@ endpoint WebContentClient did_request_activate_tab(u64 page_id) =| did_close_browsing_context(u64 page_id) =| did_change_needs_beforeunload_check(u64 page_id, bool needs_beforeunload_check) =| + did_request_traverse_the_history_by_delta(u64 page_id, i32 delta, Web::HistoryTraversalPrecheck history_traversal_precheck) => (bool accepted) + did_request_webdriver_history_traversal(u64 page_id, u64 request_id, i32 delta) =| + did_request_webdriver_load_url_from_ui(u64 page_id, URL::URL url) => (Web::WebDriver::Response response) + did_request_webdriver_traverse_history_from_ui(u64 page_id, i32 delta) => (Web::WebDriver::Response response) + did_request_webdriver_mark_web_content_session_history_stale(u64 page_id) => (Web::WebDriver::Response response) + did_request_webdriver_session_history(u64 page_id) => (Web::WebDriver::Response response) + did_request_webdriver_navigation_completion(u64 page_id, u64 request_id, Optional page_load_timeout) =| did_request_restore_window(u64 page_id) =| did_request_reposition_window(u64 page_id, Gfx::IntPoint position) =| did_request_resize_window(u64 page_id, Gfx::IntSize size) =| @@ -136,6 +149,12 @@ endpoint WebContentClient did_update_primary_selection(u64 page_id, String text) =| did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) =| + did_update_session_history(u64 page_id, Vector entries, Vector used_steps, size_t current_used_step_index) =| + did_request_ui_process_session_history_for_testing(u64 page_id) => (String session_history) + did_set_top_level_session_history(u64 page_id, bool accepted, Vector entries, Vector used_steps, size_t current_used_step_index) =| + did_traverse_the_history_to_step(u64 page_id, i32 step, bool step_was_available, Web::HTML::HistoryStepResult result) =| + did_check_if_traverse_history_step_is_canceled(u64 page_id, u64 request_id, i32 step, bool canceled) =| + did_reset_session_history_for_testing(u64 page_id) =| did_change_audio_play_state(u64 page_id, Web::HTML::AudioPlayState play_state) =| diff --git a/Services/WebContent/WebContentServer.ipc b/Services/WebContent/WebContentServer.ipc index 3c43ef68d5..5afdced5ce 100644 --- a/Services/WebContent/WebContentServer.ipc +++ b/Services/WebContent/WebContentServer.ipc @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,12 +14,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -35,6 +38,8 @@ endpoint WebContentServer set_window_handle(u64 page_id, String handle) =| connect_to_webdriver(u64 page_id, ByteString webdriver_endpoint) =| + complete_webdriver_history_traversal(u64 page_id, u64 request_id, bool accepted, bool will_replace_web_content_process, bool will_change_top_level_entry) =| + complete_webdriver_navigation_completion(u64 page_id, u64 request_id, Web::WebDriver::Response response) =| connect_to_web_ui(u64 page_id, IPC::TransportHandle handle) =| connect_to_request_server(IPC::TransportHandle handle) =| connect_to_image_decoder(IPC::TransportHandle handle) =| @@ -44,11 +49,18 @@ endpoint WebContentServer update_system_theme(u64 page_id, Core::AnonymousBuffer theme_buffer) =| update_screen_rects(u64 page_id, Vector rects, u32 main_screen_index) =| - load_url(u64 page_id, URL::URL url) =| + load_url(u64 page_id, URL::URL url, Web::Bindings::NavigationHistoryBehavior history_handling) =| + load_url_with_document_resource(u64 page_id, URL::URL url, + Variant document_resource, + Web::Bindings::NavigationHistoryBehavior history_handling) =| load_html(u64 page_id, ByteString html) =| load_html_with_url(u64 page_id, ByteString html, URL::URL url) =| reload(u64 page_id) =| traverse_the_history_by_delta(u64 page_id, i32 delta) =| + traverse_the_history_to_step(u64 page_id, i32 step) =| + check_if_traverse_history_step_is_canceled(u64 page_id, u64 request_id, i32 step) =| + set_top_level_session_history(u64 page_id, Vector entries, size_t current_top_level_entry_index) =| + reset_session_history_for_testing(u64 page_id) =| set_viewport(u64 page_id, Web::DevicePixelSize size, double device_pixel_ratio, Web::ViewportIsFullscreen is_fullscreen) =| diff --git a/Services/WebContent/WebDriverClient.ipc b/Services/WebContent/WebDriverClient.ipc index be9a4534bf..2994da927f 100644 --- a/Services/WebContent/WebDriverClient.ipc +++ b/Services/WebContent/WebDriverClient.ipc @@ -10,11 +10,12 @@ endpoint WebDriverClient { set_is_webdriver_active(bool active) =| get_timeouts() => (Web::WebDriver::Response response) set_timeouts(JsonValue payload) => (Web::WebDriver::Response response) - navigate_to(JsonValue payload) => (Web::WebDriver::Response response) + navigate_to(JsonValue payload) => (Web::WebDriver::Response response, bool will_replace_web_content_process) get_current_url() => (Web::WebDriver::Response response) - back() => (Web::WebDriver::Response response) - forward() => (Web::WebDriver::Response response) + back() => (Web::WebDriver::Response response, bool will_replace_web_content_process, bool wait_for_driver_execution_complete, bool wait_for_navigation_completion) + forward() => (Web::WebDriver::Response response, bool will_replace_web_content_process, bool wait_for_driver_execution_complete, bool wait_for_navigation_completion) refresh() => (Web::WebDriver::Response response) + wait_for_navigation_completion() => (Web::WebDriver::Response response) get_title() => (Web::WebDriver::Response response) close_window() => (Web::WebDriver::Response response) switch_to_window(String handle) => (Web::WebDriver::Response response) @@ -27,6 +28,11 @@ endpoint WebDriverClient { minimize_window() => (Web::WebDriver::Response response) fullscreen_window() => (Web::WebDriver::Response response) consume_user_activation() => (Web::WebDriver::Response response) + crash_current_page() =| + load_url_from_ui(JsonValue payload) => (Web::WebDriver::Response response) + traverse_history_from_ui(JsonValue payload) => (Web::WebDriver::Response response) + mark_web_content_session_history_stale() => (Web::WebDriver::Response response) + get_session_history() => (Web::WebDriver::Response response) find_element(JsonValue payload) => (Web::WebDriver::Response response) find_elements(JsonValue payload) => (Web::WebDriver::Response response) find_element_from_element(JsonValue payload, String element_id) => (Web::WebDriver::Response response) diff --git a/Services/WebContent/WebDriverConnection.cpp b/Services/WebContent/WebDriverConnection.cpp index 331665456e..5782f0f21c 100644 --- a/Services/WebContent/WebDriverConnection.cpp +++ b/Services/WebContent/WebDriverConnection.cpp @@ -8,13 +8,15 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include -#include +#include #include #include #include +#include #if !defined(AK_OS_MACOS) # include #else @@ -70,10 +72,21 @@ #include #include #include +#include +#include #include namespace WebContent { +struct WebDriverHistoryTraversalMetadata + : public RefCounted { + Web::WebDriver::Response response { JsonValue {} }; + bool will_replace_web_content_process { false }; + bool wait_for_driver_execution_complete { true }; + bool wait_for_navigation_completion { true }; + bool sync_response_returned { false }; +}; + #define WEBDRIVER_TRY(expression) \ ({ \ /* Ignore -Wshadow to allow nesting the macro. */ \ @@ -126,6 +139,30 @@ static Gfx::IntRect compute_window_rect(Web::Page const& page) }; } +static Optional current_top_level_entry_index(Web::HTML::TraversableNavigable::SessionHistorySnapshot const& session_history_snapshot) +{ + VERIFY(session_history_snapshot.current_used_step_index < session_history_snapshot.used_session_history_steps.size()); + auto current_step = session_history_snapshot.used_session_history_steps[session_history_snapshot.current_used_step_index]; + + Optional result; + for (size_t i = 0; i < session_history_snapshot.top_level_session_history_entries.size(); ++i) { + if (session_history_snapshot.top_level_session_history_entries[i].step > current_step) + break; + result = i; + } + return result; +} + +static JsonObject serialize_session_history_snapshot_for_webdriver(Web::HTML::TraversableNavigable::SessionHistorySnapshot const& session_history_snapshot) +{ + JsonObject serialized; + serialized.set("currentUsedStepIndex"sv, session_history_snapshot.current_used_step_index); + serialized.set("currentStep"sv, session_history_snapshot.used_session_history_steps[session_history_snapshot.current_used_step_index]); + serialized.set("entries"sv, WebView::history_json_entries(session_history_snapshot.top_level_session_history_entries, current_top_level_entry_index(session_history_snapshot))); + serialized.set("usedSteps"sv, WebView::history_json_steps(session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index)); + return serialized; +} + // https://w3c.github.io/webdriver/#dfn-scrolls-into-view static void scroll_element_into_view(Web::DOM::Element& element) { @@ -228,6 +265,41 @@ WebDriverConnection::WebDriverConnection(NonnullOwnPtr transport set_current_top_level_browsing_context(page_client.page().top_level_browsing_context()); } +void WebDriverConnection::page_did_set_window_handle(Badge, String const& window_handle) +{ + async_did_set_window_handle(window_handle); +} + +void WebDriverConnection::page_did_start_window_replacement(Badge, String const& window_handle) +{ + async_did_start_window_replacement(window_handle); + if (m_should_complete_driver_execution_when_navigation_starts_or_is_canceled) { + m_should_complete_driver_execution_when_navigation_starts_or_is_canceled = false; + async_driver_execution_complete(JsonValue {}); + } +} + +void WebDriverConnection::page_did_start_loading(Badge, URL::URL const&) +{ + if (m_should_complete_driver_execution_when_navigation_starts_or_is_canceled) { + m_should_complete_driver_execution_when_navigation_starts_or_is_canceled = false; + async_driver_execution_complete(JsonValue {}); + } +} + +void WebDriverConnection::page_did_cancel_loading(Badge, URL::URL const&) +{ + if (m_should_complete_driver_execution_when_navigation_starts_or_is_canceled) { + m_should_complete_driver_execution_when_navigation_starts_or_is_canceled = false; + async_driver_execution_complete(JsonValue {}); + } +} + +void WebDriverConnection::page_did_close_window(Badge, String const& window_handle) +{ + async_did_close_window(window_handle); +} + void WebDriverConnection::visit_edges(JS::Cell::Visitor& visitor) { visitor.visit(m_current_browsing_context); @@ -305,17 +377,21 @@ Messages::WebDriverClient::SetTimeoutsResponse WebDriverConnection::set_timeouts Messages::WebDriverClient::NavigateToResponse WebDriverConnection::navigate_to(JsonValue payload) { // 1. If the current top-level browsing context is no longer open, return error with error code no such window. - TRY(ensure_current_top_level_browsing_context_is_open()); + if (auto result = ensure_current_top_level_browsing_context_is_open(); result.is_error()) + return { Web::WebDriver::Response { result.release_error() }, false }; // 2. Let url be the result of getting the property url from the parameters argument. if (!payload.is_object() || !payload.as_object().has_string("url"sv)) - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload doesn't have a string `url`"sv); + return { Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload doesn't have a string `url`"sv), false }; auto url = URL::Parser::basic_parse(payload.as_object().get_string("url"sv).value()); // FIXME: 3. If url is not an absolute URL or is not an absolute URL with fragment or not a local scheme, return error with error code invalid argument. + auto const& current_url = current_top_level_browsing_context()->active_document()->url(); + auto will_replace_web_content_process = !current_top_level_browsing_context()->page().client().is_url_suitable_for_same_process_navigation(current_url, url.value()); + // 4. Handle any user prompts and return its value if it is an error. - handle_any_user_prompts([this, url = move(url)]() { + handle_any_user_prompts([this, url = move(url), will_replace_web_content_process]() { // 5. Let current URL be the current top-level browsing context’s active document’s URL. auto const& current_url = current_top_level_browsing_context()->active_document()->url(); @@ -323,33 +399,32 @@ Messages::WebDriverClient::NavigateToResponse WebDriverConnection::navigate_to(J // FIXME: a. If timer has not been started, start a timer. If this algorithm has not completed before timer reaches the session’s session page load timeout in milliseconds, return an error with error code timeout. // 7. Navigate the current top-level browsing context to url. + // NB: "Navigate to a javascript: URL" can evaluate without producing a new Document, + // in which case "we will not perform a navigation". + // https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate-to-a-javascript:-url + auto is_same_document_fragment_navigation = url->fragment().has_value() + && url->equals(current_url, URL::ExcludeFragment::Yes); + if (url->scheme() != "javascript"sv && !is_same_document_fragment_navigation) + static_cast(current_top_level_browsing_context()->page().client()).did_start_webdriver_navigation(url.value()); current_top_level_browsing_context()->page().load(url.value()); - auto navigation_complete = GC::create_function(current_top_level_browsing_context()->heap(), [this](Web::WebDriver::Response result) { + auto navigation_complete = GC::create_function(current_top_level_browsing_context()->heap(), [this, will_replace_web_content_process](Web::WebDriver::Response result) { // 9. Set the current browsing context with the current top-level browsing context. set_current_browsing_context(*current_top_level_browsing_context()); // FIXME: 10. If the current top-level browsing context contains a refresh state pragma directive of time 1 second or less, wait until the refresh timeout has elapsed, a new navigate has begun, and return to the first step of this algorithm. - async_driver_execution_complete(move(result)); + if (will_replace_web_content_process) + m_should_complete_driver_execution_when_navigation_starts_or_is_canceled = true; + else + async_driver_execution_complete(move(result)); }); - // 8. If url is special except for file and current URL and URL do not have the same absolute URL: - // AD-HOC: We wait for the navigation to complete regardless of whether the current URL differs from the provided - // URL. Even if they're the same, the navigation queues a tasks that we must await, otherwise subsequent - // endpoint invocations will attempt to operate on the wrong page. - if (url->is_special() && url->scheme() != "file"sv) { - // a. Try to wait for navigation to complete. - wait_for_navigation_to_complete(navigation_complete); - - // FIXME: b. Try to run the post-navigation checks. - } else { - navigation_complete->function()(JsonValue {}); - } + navigation_complete->function()(JsonValue {}); }); // 11. Return success with data null. - return JsonValue {}; + return { JsonValue {}, will_replace_web_content_process }; } // 10.2 Get Current URL, https://w3c.github.io/webdriver/#get-current-url @@ -374,148 +449,64 @@ Messages::WebDriverClient::GetCurrentUrlResponse WebDriverConnection::get_curren Messages::WebDriverClient::BackResponse WebDriverConnection::back() { // 1. If session's current top-level browsing context is no longer open, return error with error code no such window. - TRY(ensure_current_top_level_browsing_context_is_open()); + if (auto result = ensure_current_top_level_browsing_context_is_open(); result.is_error()) + return { Web::WebDriver::Response { result.release_error() }, false, false, false }; // 2. Try to handle any user prompts with session. - handle_any_user_prompts([this]() { - auto& realm = current_top_level_browsing_context()->active_document()->realm(); - - // 3. Let timeout be session' session timeouts page load timeout. - auto timeout = m_timeouts_configuration.page_load_timeout; - - // 4. Let timer be a new timer. - auto timer = realm.heap().allocate(); - - auto on_complete = GC::create_function(realm.heap(), [this, timer]() { - timer->stop(); - - if (m_document_observer) { - m_document_observer->set_document_page_showing_observer({}); - m_document_observer = nullptr; - } - - // 8. If timer' timeout fired flag is set: - if (timer->is_timed_out()) { - // 1. Handle any user prompts. - handle_any_user_prompts([this]() { - // 2. Return error with error code timeout. - async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::Timeout, "Navigation timed out"sv)); - }); - + auto metadata = adopt_ref(*new WebDriverHistoryTraversalMetadata); + handle_any_user_prompts([this, metadata]() { + auto& page_client = static_cast(current_top_level_browsing_context()->page().client()); + page_client.request_webdriver_history_traversal(-1, [this, metadata](auto traversal_result) { + if (!traversal_result.accepted) { + async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv)); return; } - // 9. Return success with data null. - async_driver_execution_complete(JsonValue {}); + metadata->will_replace_web_content_process = traversal_result.will_replace_web_content_process; + metadata->wait_for_navigation_completion = traversal_result.will_change_top_level_entry; + if (metadata->will_replace_web_content_process) + async_did_start_window_replacement(current_top_level_browsing_context()->page().top_level_traversable()->window_handle()); + if (metadata->sync_response_returned) + async_driver_execution_complete(JsonValue {}); + else + metadata->wait_for_driver_execution_complete = false; }); - - // 5. If timeout is not null: - if (timeout.has_value()) { - // 1. Start the timer with timer and timeout. - timer->start(*timeout, on_complete); - } - - // 6. Traverse the history by a delta –1 for session's current browsing context. - current_top_level_browsing_context()->top_level_traversable()->traverse_the_history_by_delta(-1); - - // 7. If the previous step completed results in a pageHide event firing, wait until pageShow event fires or - // timer' timeout fired flag to be set, whichever occurs first. - current_top_level_browsing_context()->top_level_traversable()->append_session_history_traversal_steps(GC::create_function(realm.heap(), [this, timer, on_complete](NonnullRefPtr> signal) { - if (timer->is_timed_out()) { - signal->resolve({}); - return; - } - - if (auto* document = current_top_level_browsing_context()->active_document(); document->page_showing()) { - on_complete->function()(); - } else { - auto& realm = document->realm(); - - m_document_observer = realm.create(realm, *document); - m_document_observer->set_document_page_showing_observer([on_complete](auto) { - on_complete->function()(); - }); - } - - signal->resolve({}); - })); }); - return JsonValue {}; + metadata->sync_response_returned = true; + return { move(metadata->response), metadata->will_replace_web_content_process, metadata->wait_for_driver_execution_complete, metadata->wait_for_navigation_completion }; } // 10.4 Forward, https://w3c.github.io/webdriver/#dfn-forward Messages::WebDriverClient::ForwardResponse WebDriverConnection::forward() { // 1. If session's current top-level browsing context is no longer open, return error with error code no such window. - TRY(ensure_current_top_level_browsing_context_is_open()); + if (auto result = ensure_current_top_level_browsing_context_is_open(); result.is_error()) + return { Web::WebDriver::Response { result.release_error() }, false, false, false }; // 2. Try to handle any user prompts with session. - handle_any_user_prompts([this]() { - auto& realm = current_top_level_browsing_context()->active_document()->realm(); - - // 3. Let timeout be session' session timeouts page load timeout. - auto timeout = m_timeouts_configuration.page_load_timeout; - - // 4. Let timer be a new timer. - auto timer = realm.heap().allocate(); - - auto on_complete = GC::create_function(realm.heap(), [this, timer]() { - timer->stop(); - - if (m_document_observer) { - m_document_observer->set_document_page_showing_observer({}); - m_document_observer = nullptr; - } - - // 8. If timer' timeout fired flag is set: - if (timer->is_timed_out()) { - // 1. Handle any user prompts. - handle_any_user_prompts([this]() { - // 2. Return error with error code timeout. - async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::Timeout, "Navigation timed out"sv)); - }); - + auto metadata = adopt_ref(*new WebDriverHistoryTraversalMetadata); + handle_any_user_prompts([this, metadata]() { + auto& page_client = static_cast(current_top_level_browsing_context()->page().client()); + page_client.request_webdriver_history_traversal(1, [this, metadata](auto traversal_result) { + if (!traversal_result.accepted) { + async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv)); return; } - // 9. Return success with data null. - async_driver_execution_complete(JsonValue {}); + metadata->will_replace_web_content_process = traversal_result.will_replace_web_content_process; + metadata->wait_for_navigation_completion = traversal_result.will_change_top_level_entry; + if (metadata->will_replace_web_content_process) + async_did_start_window_replacement(current_top_level_browsing_context()->page().top_level_traversable()->window_handle()); + if (metadata->sync_response_returned) + async_driver_execution_complete(JsonValue {}); + else + metadata->wait_for_driver_execution_complete = false; }); - - // 5. If timeout is not null: - if (timeout.has_value()) { - // 1. Start the timer with timer and timeout. - timer->start(*timeout, on_complete); - } - - // 6. Traverse the history by a delta 1 for session's current browsing context. - current_top_level_browsing_context()->top_level_traversable()->traverse_the_history_by_delta(1); - - // 7. If the previous step completed results in a pageHide event firing, wait until pageShow event fires or - // timer' timeout fired flag to be set, whichever occurs first. - current_top_level_browsing_context()->top_level_traversable()->append_session_history_traversal_steps(GC::create_function(realm.heap(), [this, timer, on_complete](NonnullRefPtr> signal) { - if (timer->is_timed_out()) { - signal->resolve({}); - return; - } - - if (auto* document = current_top_level_browsing_context()->active_document(); document->page_showing()) { - on_complete->function()(); - } else { - auto& realm = document->realm(); - - m_document_observer = realm.create(realm, *document); - m_document_observer->set_document_page_showing_observer([on_complete](auto) { - on_complete->function()(); - }); - } - - signal->resolve({}); - })); }); - return JsonValue {}; + metadata->sync_response_returned = true; + return { move(metadata->response), metadata->will_replace_web_content_process, metadata->wait_for_driver_execution_complete, metadata->wait_for_navigation_completion }; } // 10.5 Refresh, https://w3c.github.io/webdriver/#dfn-refresh @@ -543,6 +534,107 @@ Messages::WebDriverClient::RefreshResponse WebDriverConnection::refresh() return JsonValue {}; } +Messages::WebDriverClient::WaitForNavigationCompletionResponse WebDriverConnection::wait_for_navigation_completion() +{ + if (m_page_load_strategy == Web::WebDriver::PageLoadStrategy::None) { + async_driver_execution_complete(JsonValue {}); + return JsonValue {}; + } + + auto& page_client = static_cast(current_top_level_browsing_context()->page().client()); + page_client.wait_for_webdriver_navigation_completion(m_timeouts_configuration.page_load_timeout, [this](Web::WebDriver::Response response) { + async_driver_execution_complete(move(response)); + }); + return JsonValue {}; +} + +void WebDriverConnection::crash_current_page() +{ + Core::deferred_invoke([] { + Core::Process::terminate_immediately(1); + }); +} + +Messages::WebDriverClient::LoadUrlFromUiResponse WebDriverConnection::load_url_from_ui(JsonValue payload) +{ + TRY(ensure_current_top_level_browsing_context_is_open()); + + if (!payload.is_object() || !payload.as_object().has_string("url"sv)) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload doesn't have a string `url`"sv); + auto url = URL::Parser::basic_parse(payload.as_object().get_string("url"sv).value()); + if (!url.has_value()) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload has an invalid `url`"sv); + + auto const& current_url = current_top_level_browsing_context()->active_document()->url(); + auto will_replace_web_content_process = !current_top_level_browsing_context()->page().client().is_url_suitable_for_same_process_navigation(current_url, *url); + + auto& page_client = static_cast(current_top_level_browsing_context()->page().client()); + auto response = page_client.request_webdriver_load_url_from_ui(*url); + if (response.is_error()) + return response.release_error(); + + JsonObject result; + result.set("willReplaceWebContentProcess"sv, will_replace_web_content_process); + async_driver_execution_complete(JsonValue { move(result) }); + return JsonValue {}; +} + +Messages::WebDriverClient::TraverseHistoryFromUiResponse WebDriverConnection::traverse_history_from_ui(JsonValue payload) +{ + TRY(ensure_current_top_level_browsing_context_is_open()); + + if (!payload.is_object() || !payload.as_object().has_i32("delta"sv)) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload doesn't have an integer `delta`"sv); + + auto& page_client = static_cast(current_top_level_browsing_context()->page().client()); + page_client.request_webdriver_history_traversal(payload.as_object().get_i32("delta"sv).value(), [this](auto traversal_result) { + if (!traversal_result.accepted) { + async_driver_execution_complete(Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv)); + return; + } + + if (traversal_result.will_replace_web_content_process) + async_did_start_window_replacement(current_top_level_browsing_context()->page().top_level_traversable()->window_handle()); + + JsonObject result; + result.set("willReplaceWebContentProcess"sv, traversal_result.will_replace_web_content_process); + result.set("willChangeTopLevelEntry"sv, traversal_result.will_change_top_level_entry); + async_driver_execution_complete(JsonValue { move(result) }); + }); + return JsonValue {}; +} + +Messages::WebDriverClient::MarkWebContentSessionHistoryStaleResponse WebDriverConnection::mark_web_content_session_history_stale() +{ + TRY(ensure_current_top_level_browsing_context_is_open()); + + auto& page_client = static_cast(current_top_level_browsing_context()->page().client()); + auto response = page_client.request_webdriver_mark_web_content_session_history_stale(); + if (response.is_error()) + return response.release_error(); + + async_driver_execution_complete(JsonValue {}); + return JsonValue {}; +} + +Messages::WebDriverClient::GetSessionHistoryResponse WebDriverConnection::get_session_history() +{ + TRY(ensure_current_top_level_browsing_context_is_open()); + + auto& page_client = static_cast(current_top_level_browsing_context()->page().client()); + auto ui_session_history = page_client.request_webdriver_session_history(); + if (ui_session_history.is_error()) + return ui_session_history.release_error(); + + auto web_content_session_history_snapshot = current_top_level_browsing_context()->top_level_traversable()->create_session_history_snapshot(); + + JsonObject result; + result.set("ui"sv, ui_session_history.release_value()); + result.set("webContent"sv, serialize_session_history_snapshot_for_webdriver(web_content_session_history_snapshot)); + async_driver_execution_complete(JsonValue { move(result) }); + return JsonValue {}; +} + // 10.6 Get Title, https://w3c.github.io/webdriver/#dfn-get-title Messages::WebDriverClient::GetTitleResponse WebDriverConnection::get_title() { @@ -2668,30 +2760,7 @@ ErrorOr WebDriverConnection::ensure_current_top_lev // https://w3c.github.io/webdriver/#dfn-get-the-prompt-handler Web::WebDriver::PromptHandlerConfiguration WebDriverConnection::get_the_prompt_handler(Web::WebDriver::PromptType type) const { - static NeverDestroyed empty_user_prompt_handler; - auto const& user_prompt_handler = Web::WebDriver::user_prompt_handler(); - - // 1. If the user prompt handler is null, let handlers be an empty map. Otherwise let handlers be user prompt handler. - auto const& handlers = user_prompt_handler.has_value() ? *user_prompt_handler : *empty_user_prompt_handler; - - // 2. If handlers contains type return handlers[type]. - if (auto handler = handlers.get(type); handler.has_value()) - return *handler; - - // 3. If handlers contains "default" return handlers["default"]. - if (auto handler = handlers.get(Web::WebDriver::PromptType::Default); handler.has_value()) - return *handler; - - // 4. If type is "beforeUnload", return a prompt handler configuration with handler "accept" and notify false. - if (type == Web::WebDriver::PromptType::BeforeUnload) - return { .handler = Web::WebDriver::PromptHandler::Accept, .notify = Web::WebDriver::PromptHandlerConfiguration::Notify::No }; - - // 5. If handlers contains "fallbackDefault" return handlers["fallbackDefault"]. - if (auto handler = handlers.get(Web::WebDriver::PromptType::FallbackDefault); handler.has_value()) - return *handler; - - // 6. Return a prompt handler configuration with handler "dismiss" and notify true. - return { .handler = Web::WebDriver::PromptHandler::Dismiss, .notify = Web::WebDriver::PromptHandlerConfiguration::Notify::Yes }; + return Web::WebDriver::get_the_prompt_handler(type); } // https://w3c.github.io/webdriver/#dfn-annotated-unexpected-alert-open-error diff --git a/Services/WebContent/WebDriverConnection.h b/Services/WebContent/WebDriverConnection.h index 402b290552..b768b9ad69 100644 --- a/Services/WebContent/WebDriverConnection.h +++ b/Services/WebContent/WebDriverConnection.h @@ -40,6 +40,11 @@ public: void visit_edges(JS::Cell::Visitor&); void page_did_open_dialog(Badge); + void page_did_set_window_handle(Badge, String const& window_handle); + void page_did_start_window_replacement(Badge, String const& window_handle); + void page_did_start_loading(Badge, URL::URL const& url); + void page_did_cancel_loading(Badge, URL::URL const& url); + void page_did_close_window(Badge, String const& window_handle); private: WebDriverConnection(NonnullOwnPtr transport, Web::PageClient& page_client); @@ -58,6 +63,7 @@ private: virtual Messages::WebDriverClient::BackResponse back() override; virtual Messages::WebDriverClient::ForwardResponse forward() override; virtual Messages::WebDriverClient::RefreshResponse refresh() override; + virtual Messages::WebDriverClient::WaitForNavigationCompletionResponse wait_for_navigation_completion() override; virtual Messages::WebDriverClient::GetTitleResponse get_title() override; virtual Messages::WebDriverClient::CloseWindowResponse close_window() override; virtual Messages::WebDriverClient::SwitchToWindowResponse switch_to_window(String handle) override; @@ -70,6 +76,11 @@ private: virtual Messages::WebDriverClient::MinimizeWindowResponse minimize_window() override; virtual Messages::WebDriverClient::FullscreenWindowResponse fullscreen_window() override; virtual Messages::WebDriverClient::ConsumeUserActivationResponse consume_user_activation() override; + virtual void crash_current_page() override; + virtual Messages::WebDriverClient::LoadUrlFromUiResponse load_url_from_ui(JsonValue payload) override; + virtual Messages::WebDriverClient::TraverseHistoryFromUiResponse traverse_history_from_ui(JsonValue payload) override; + virtual Messages::WebDriverClient::MarkWebContentSessionHistoryStaleResponse mark_web_content_session_history_stale() override; + virtual Messages::WebDriverClient::GetSessionHistoryResponse get_session_history() override; virtual Messages::WebDriverClient::FindElementResponse find_element(JsonValue payload) override; virtual Messages::WebDriverClient::FindElementsResponse find_elements(JsonValue payload) override; virtual Messages::WebDriverClient::FindElementFromElementResponse find_element_from_element(JsonValue payload, String element_id) override; @@ -183,6 +194,8 @@ private: GC::Ptr m_document_observer; GC::Ptr m_navigation_observer; GC::Ptr m_navigation_timer; + + bool m_should_complete_driver_execution_when_navigation_starts_or_is_canceled { false }; }; } diff --git a/Services/WebContent/WebDriverServer.ipc b/Services/WebContent/WebDriverServer.ipc index a3cbbb73ba..098872db73 100644 --- a/Services/WebContent/WebDriverServer.ipc +++ b/Services/WebContent/WebDriverServer.ipc @@ -3,4 +3,6 @@ endpoint WebDriverServer { driver_execution_complete(Web::WebDriver::Response response) =| did_set_window_handle(String handle) =| + did_start_window_replacement(String handle) =| + did_close_window(String handle) =| } diff --git a/Services/WebContent/main.cpp b/Services/WebContent/main.cpp index 2762f8faa7..c855e1b05b 100644 --- a/Services/WebContent/main.cpp +++ b/Services/WebContent/main.cpp @@ -152,6 +152,7 @@ ErrorOr ladybird_main(Main::Arguments arguments) bool disable_scrollbar_painting = false; bool disable_async_scrolling = false; bool enable_sandbox = false; + bool report_session_history_updates_in_test_mode = false; StringView echo_server_port_string_view {}; StringView default_time_zone {}; StringView style_invalidation_counter_dump_interval {}; @@ -175,6 +176,7 @@ ErrorOr ladybird_main(Main::Arguments arguments) args_parser.add_option(disable_scrollbar_painting, "Don't paint horizontal or vertical viewport scrollbars", "disable-scrollbar-painting"); args_parser.add_option(disable_async_scrolling, "Disable async scrolling", "disable-async-scrolling"); args_parser.add_option(enable_sandbox, "Enable process sandboxing", "enable-sandbox"); + args_parser.add_option(report_session_history_updates_in_test_mode, "Report session history updates in test mode", "report-session-history-updates-in-test-mode"); args_parser.add_option(echo_server_port_string_view, "Echo server port used in test internals", "echo-server-port", 0, "echo_server_port"); args_parser.add_option(is_headless, "Report that the browser is running in headless mode", "headless"); args_parser.add_option(default_time_zone, "Default time zone", "default-time-zone", 0, "time-zone-id"); @@ -226,6 +228,7 @@ ErrorOr ladybird_main(Main::Arguments arguments) Web::Painting::set_paint_viewport_scrollbars(!disable_scrollbar_painting); WebContent::PageClient::set_async_scrolling_enabled(!disable_async_scrolling); + WebContent::PageClient::set_should_report_session_history_updates_in_test_mode(report_session_history_updates_in_test_mode); if (!echo_server_port_string_view.is_empty()) { if (auto maybe_echo_server_port = echo_server_port_string_view.to_number(); maybe_echo_server_port.has_value()) diff --git a/Services/WebDriver/Client.cpp b/Services/WebDriver/Client.cpp index 758750bdca..af407cf663 100644 --- a/Services/WebDriver/Client.cpp +++ b/Services/WebDriver/Client.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,41 @@ namespace WebDriver { +template +static Web::WebDriver::Response perform_history_traversal(Session& session, StartTraversal start_traversal, bool& wait_for_navigation_completion) +{ + Optional response; + RefPtr connection { &session.web_content_connection() }; + + ScopeGuard guard { [&]() { connection->on_driver_execution_complete = nullptr; } }; + connection->on_driver_execution_complete = [&](auto result) { response = move(result); }; + + auto traversal_response = start_traversal(*connection); + auto immediate_response = TRY(traversal_response.take_response()); + wait_for_navigation_completion = traversal_response.wait_for_navigation_completion(); + if (traversal_response.will_replace_web_content_process()) + session.mark_current_window_as_awaiting_replacement(*connection); + + if (!traversal_response.wait_for_driver_execution_complete()) + return immediate_response; + + Core::EventLoop::current().spin_until([&]() { + return response.has_value(); + }); + + return TRY(response.release_value()); +} + +static ErrorOr, Web::WebDriver::Error> +find_session_with_ladybird_test_hooks(Web::WebDriver::Parameters const& parameters) +{ + auto session = TRY(Session::find_session(parameters[0])); + if (!session->test_hooks_enabled()) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnknownCommand, + "Ladybird test hooks are not enabled for this session"sv); + return session; +} + ErrorOr> Client::try_create(NonnullOwnPtr socket, LaunchBrowserCallback launch_browser_callback) { if (!launch_browser_callback) @@ -162,9 +198,17 @@ Web::WebDriver::Response Client::navigate_to(Web::WebDriver::Parameters paramete dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session//url"); auto session = TRY(Session::find_session(parameters[0])); - return session->perform_async_action([&](auto& connection) { - return connection.navigate_to(move(payload)); - }); + auto response = TRY(session->perform_async_action([&](auto& connection) { + auto navigate_response = connection.navigate_to(move(payload)); + return navigate_response.response(); + }, + Session::WebContentReplacement::Allow)); + TRY(session->wait_for_current_window_to_have_web_content_connection()); + response = TRY(session->perform_async_action([&](auto& connection) { + return connection.wait_for_navigation_completion(); + })); + TRY(session->wait_for_current_window_to_have_web_content_connection()); + return response; } // 10.2 Get Current URL, https://w3c.github.io/webdriver/#dfn-get-current-url @@ -186,9 +230,16 @@ Web::WebDriver::Response Client::back(Web::WebDriver::Parameters parameters, Jso dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session//back"); auto session = TRY(Session::find_session(parameters[0])); - return session->perform_async_action([&](auto& connection) { - return connection.back(); - }); + bool wait_for_navigation_completion = true; + auto response = TRY(perform_history_traversal(*session, [&](auto& connection) { return connection.back(); }, wait_for_navigation_completion)); + TRY(session->wait_for_current_window_to_have_web_content_connection()); + if (wait_for_navigation_completion) { + response = TRY(session->perform_async_action([&](auto& connection) { + return connection.wait_for_navigation_completion(); + })); + TRY(session->wait_for_current_window_to_have_web_content_connection()); + } + return response; } // 10.4 Forward, https://w3c.github.io/webdriver/#dfn-forward @@ -198,9 +249,16 @@ Web::WebDriver::Response Client::forward(Web::WebDriver::Parameters parameters, dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session//forward"); auto session = TRY(Session::find_session(parameters[0])); - return session->perform_async_action([&](auto& connection) { - return connection.forward(); - }); + bool wait_for_navigation_completion = true; + auto response = TRY(perform_history_traversal(*session, [&](auto& connection) { return connection.forward(); }, wait_for_navigation_completion)); + TRY(session->wait_for_current_window_to_have_web_content_connection()); + if (wait_for_navigation_completion) { + response = TRY(session->perform_async_action([&](auto& connection) { + return connection.wait_for_navigation_completion(); + })); + TRY(session->wait_for_current_window_to_have_web_content_connection()); + } + return response; } // 10.5 Refresh, https://w3c.github.io/webdriver/#dfn-refresh @@ -210,9 +268,14 @@ Web::WebDriver::Response Client::refresh(Web::WebDriver::Parameters parameters, dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session//refresh"); auto session = TRY(Session::find_session(parameters[0])); - return session->perform_async_action([&](auto& connection) { + auto response = TRY(session->perform_async_action([&](auto& connection) { return connection.refresh(); - }); + })); + if (TRY(session->wait_for_current_window_to_have_web_content_connection())) + response = TRY(session->perform_async_action([&](auto& connection) { + return connection.wait_for_navigation_completion(); + })); + return response; } // 10.6 Get Title, https://w3c.github.io/webdriver/#dfn-get-title @@ -227,6 +290,92 @@ Web::WebDriver::Response Client::get_title(Web::WebDriver::Parameters parameters }); } +// Extension: POST /session/{session id}/ladybird/crash-current-page +Web::WebDriver::Response Client::crash_current_page(Web::WebDriver::Parameters parameters, JsonValue) +{ + dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session//ladybird/crash-current-page"); + auto session = TRY(find_session_with_ladybird_test_hooks(parameters)); + + RefPtr connection { &session->web_content_connection() }; + session->mark_current_window_as_awaiting_replacement(*connection); + connection->async_crash_current_page(); + TRY(session->wait_for_current_window_to_have_web_content_connection()); + return session->perform_async_action([&](auto& connection) { + return connection.wait_for_navigation_completion(); + }); +} + +// Extension: POST /session/{session id}/ladybird/load-url-from-ui +Web::WebDriver::Response Client::load_url_from_ui(Web::WebDriver::Parameters parameters, JsonValue payload) +{ + dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session//ladybird/load-url-from-ui"); + auto session = TRY(find_session_with_ladybird_test_hooks(parameters)); + + RefPtr previous_connection { &session->web_content_connection() }; + auto response = TRY(session->perform_async_action([&](auto& connection) { + return connection.load_url_from_ui(move(payload)); + })); + if (response.is_object() && response.as_object().get_bool("willReplaceWebContentProcess"sv).value_or(false)) + session->mark_current_window_as_awaiting_replacement(*previous_connection); + + TRY(session->wait_for_current_window_to_have_web_content_connection()); + response = TRY(session->perform_async_action([&](auto& connection) { + return connection.wait_for_navigation_completion(); + })); + TRY(session->wait_for_current_window_to_have_web_content_connection()); + return response; +} + +// Extension: POST /session/{session id}/ladybird/traverse-history-from-ui +Web::WebDriver::Response Client::traverse_history_from_ui(Web::WebDriver::Parameters parameters, JsonValue payload) +{ + dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session//ladybird/traverse-history-from-ui"); + auto session = TRY(find_session_with_ladybird_test_hooks(parameters)); + + auto wait_for_navigation_completion = true; + if (payload.is_object()) + wait_for_navigation_completion = payload.as_object().get_bool("waitForNavigationCompletion"sv).value_or(true); + + RefPtr previous_connection { &session->web_content_connection() }; + auto response = TRY(session->perform_async_action([&](auto& connection) { + return connection.traverse_history_from_ui(move(payload)); + })); + if (response.is_object() && response.as_object().get_bool("willReplaceWebContentProcess"sv).value_or(false)) + session->mark_current_window_as_awaiting_replacement(*previous_connection); + + TRY(session->wait_for_current_window_to_have_web_content_connection()); + if (!wait_for_navigation_completion) + return JsonValue {}; + + response = TRY(session->perform_async_action([&](auto& connection) { + return connection.wait_for_navigation_completion(); + })); + TRY(session->wait_for_current_window_to_have_web_content_connection()); + return response; +} + +// Extension: POST /session/{session id}/ladybird/mark-web-content-session-history-stale +Web::WebDriver::Response Client::mark_web_content_session_history_stale(Web::WebDriver::Parameters parameters, JsonValue) +{ + dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session//ladybird/mark-web-content-session-history-stale"); + auto session = TRY(find_session_with_ladybird_test_hooks(parameters)); + + return session->perform_async_action([&](auto& connection) { + return connection.mark_web_content_session_history_stale(); + }); +} + +// Extension: GET /session/{session id}/ladybird/session-history +Web::WebDriver::Response Client::get_session_history(Web::WebDriver::Parameters parameters, JsonValue) +{ + dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session//ladybird/session-history"); + auto session = TRY(find_session_with_ladybird_test_hooks(parameters)); + + return session->perform_async_action([&](auto& connection) { + return connection.get_session_history(); + }); +} + // 11.1 Get Window Handle, https://w3c.github.io/webdriver/#get-window-handle // GET /session/{session id}/window Web::WebDriver::Response Client::get_window_handle(Web::WebDriver::Parameters parameters, JsonValue) @@ -755,7 +904,8 @@ Web::WebDriver::Response Client::perform_actions(Web::WebDriver::Parameters para return session->perform_async_action([&](auto& connection) { return connection.perform_actions(move(payload)); - }); + }, + Session::WebContentReplacement::Allow); } // 15.8 Release Actions, https://w3c.github.io/webdriver/#release-actions diff --git a/Services/WebDriver/Client.h b/Services/WebDriver/Client.h index 7a73b8f042..098dd5adcf 100644 --- a/Services/WebDriver/Client.h +++ b/Services/WebDriver/Client.h @@ -55,6 +55,11 @@ private: virtual Web::WebDriver::Response minimize_window(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response fullscreen_window(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response consume_user_activation(Web::WebDriver::Parameters parameters, JsonValue payload) override; + virtual Web::WebDriver::Response crash_current_page(Web::WebDriver::Parameters parameters, JsonValue payload) override; + virtual Web::WebDriver::Response load_url_from_ui(Web::WebDriver::Parameters parameters, JsonValue payload) override; + virtual Web::WebDriver::Response traverse_history_from_ui(Web::WebDriver::Parameters parameters, JsonValue payload) override; + virtual Web::WebDriver::Response mark_web_content_session_history_stale(Web::WebDriver::Parameters parameters, JsonValue payload) override; + virtual Web::WebDriver::Response get_session_history(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response find_element(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response find_elements(Web::WebDriver::Parameters parameters, JsonValue payload) override; virtual Web::WebDriver::Response find_element_from_element(Web::WebDriver::Parameters parameters, JsonValue payload) override; diff --git a/Services/WebDriver/Session.cpp b/Services/WebDriver/Session.cpp index 22b1cb6a45..32f6e691a3 100644 --- a/Services/WebDriver/Session.cpp +++ b/Services/WebDriver/Session.cpp @@ -10,6 +10,7 @@ #include #include +#include #if !defined(AK_OS_MACOS) # include # include @@ -19,6 +20,7 @@ # include #endif #include +#include #include #include #include @@ -140,7 +142,7 @@ ErrorOr, Web::WebDriver::Error> Session::find_session(Str if (auto session = sessions.get(session_id); session.has_value()) { if (allow_invalid_window_handle == AllowInvalidWindowHandle::No) - TRY(session.value()->ensure_current_window_handle_is_valid()); + TRY(session.value()->wait_for_current_window_to_have_web_content_connection()); return *session.release_value(); } @@ -181,8 +183,17 @@ void Session::close() Web::WebDriver::reset_has_proxy_configuration(); // 5. Optionally, close all top-level browsing contexts, without prompting to unload. - for (auto& it : m_windows) + for (auto& it : m_windows) { + if (!it.value.web_content_connection) + continue; + + it.value.web_content_connection->on_close = nullptr; + it.value.web_content_connection->on_driver_execution_complete = nullptr; + it.value.web_content_connection->on_did_set_window_handle = nullptr; + it.value.web_content_connection->on_did_start_window_replacement = nullptr; + it.value.web_content_connection->on_did_close_window = nullptr; it.value.web_content_connection->close_session(); + } } // -> Remote end is an intermediary node // 1. Close the associated session. If this causes an error to occur, complete the remainder of this algorithm @@ -191,7 +202,10 @@ void Session::close() // 4. Perform any implementation-specific cleanup steps. for (auto& [_, connection] : m_pending_connections) { connection->on_close = nullptr; + connection->on_driver_execution_complete = nullptr; connection->on_did_set_window_handle = nullptr; + connection->on_did_start_window_replacement = nullptr; + connection->on_did_close_window = nullptr; } m_pending_connections.clear(); @@ -225,21 +239,26 @@ ErrorOr Session::accept_web_content_transport(NonnullOwnPtron_did_set_window_handle = [this, promise, connection_id](String window_handle) { + web_content_connection->on_did_set_window_handle = [this, promise, connection_id, connection = web_content_connection.ptr()](String window_handle) { auto maybe_pending_connection = m_pending_connections.take(connection_id); - if (!maybe_pending_connection.has_value()) + if (!maybe_pending_connection.has_value()) { + did_update_window_handle(move(window_handle), *connection); return; + } auto pending_connection = maybe_pending_connection.value(); - pending_connection->on_did_set_window_handle = nullptr; dbgln_if(WEBDRIVER_DEBUG, "Window {} registered with WebDriver.", window_handle); - pending_connection->on_close = [this, window_handle]() { - dbgln_if(WEBDRIVER_DEBUG, "Window {} was closed remotely.", window_handle); - m_windows.remove(window_handle); - if (m_windows.is_empty()) - close(); + pending_connection->on_close = [this, connection]() { + dbgln_if(WEBDRIVER_DEBUG, "WebContent connection closed remotely."); + web_content_connection_closed(*connection); + }; + pending_connection->on_did_start_window_replacement = [this, connection](String replaced_window_handle) { + did_start_window_replacement(replaced_window_handle, *connection); + }; + pending_connection->on_did_close_window = [this, connection](String closed_window_handle) { + did_close_window(closed_window_handle, *connection); }; pending_connection->async_set_page_load_strategy(m_page_load_strategy); @@ -248,7 +267,12 @@ ErrorOr Session::accept_web_content_transport(NonnullOwnPtrasync_set_timeouts(*m_timeouts_configuration); - m_windows.set(window_handle, Session::Window { window_handle, move(pending_connection) }); + if (auto window = m_windows.find(window_handle); window != m_windows.end()) { + window->value.web_content_connection = move(pending_connection); + window->value.is_awaiting_replacement = false; + } else { + m_windows.set(window_handle, Session::Window { window_handle, move(pending_connection) }); + } if (m_current_window_handle.is_empty()) m_current_window_handle = window_handle; @@ -258,6 +282,98 @@ ErrorOr Session::accept_web_content_transport(NonnullOwnPtr closed_window_handle; + for (auto& window : m_windows) { + if (window.value.web_content_connection.ptr() != &connection) + continue; + + if (window.value.is_awaiting_replacement) { + window.value.web_content_connection = nullptr; + return; + } + + closed_window_handle = window.key; + break; + } + + if (closed_window_handle.has_value()) + remove_window(*closed_window_handle); +} + +void Session::did_update_window_handle(String window_handle, WebContentConnection const& connection) +{ + Optional previous_window_handle; + for (auto const& window : m_windows) { + if (window.value.web_content_connection.ptr() == &connection) { + previous_window_handle = window.key; + break; + } + } + + if (!previous_window_handle.has_value() || *previous_window_handle == window_handle) + return; + + auto maybe_window = m_windows.take(*previous_window_handle); + if (!maybe_window.has_value()) + return; + + auto window = maybe_window.release_value(); + window.handle = window_handle; + window.is_awaiting_replacement = false; + + if (auto existing_window = m_windows.find(window_handle); existing_window != m_windows.end()) { + existing_window->value.web_content_connection = move(window.web_content_connection); + existing_window->value.is_awaiting_replacement = false; + } else { + m_windows.set(window_handle, move(window)); + } + + if (m_current_window_handle == *previous_window_handle) + m_current_window_handle = move(window_handle); +} + +void Session::did_start_window_replacement(String const& window_handle, WebContentConnection const& connection) +{ + auto window = m_windows.find(window_handle); + if (window == m_windows.end() || window->value.web_content_connection.ptr() != &connection) + return; + + window->value.is_awaiting_replacement = true; + window->value.web_content_connection = nullptr; +} + +void Session::mark_current_window_as_awaiting_replacement(WebContentConnection const& connection) +{ + auto window = m_windows.find(m_current_window_handle); + if (window == m_windows.end() || window->value.web_content_connection.ptr() != &connection) + return; + + window->value.is_awaiting_replacement = true; + window->value.web_content_connection = nullptr; +} + +void Session::did_close_window(String const& window_handle, WebContentConnection const& connection) +{ + auto window = m_windows.find(window_handle); + if (window == m_windows.end() || window->value.web_content_connection.ptr() != &connection) + return; + + remove_window(window_handle); +} + +void Session::remove_window(StringView window_handle) +{ + m_windows.remove(window_handle); + + if (m_current_window_handle == window_handle) + m_current_window_handle = "NoSuchWindowPleaseSelectANewOne"_string; + + if (m_windows.is_empty()) + close(); +} + ErrorOr Session::create_server(NonnullRefPtr promise) { #if defined(AK_OS_WINDOWS) @@ -356,14 +472,9 @@ Web::WebDriver::Response Session::close_window() return connection.close_window(); })); - { - // Defer removing the window handle from this session until after we know we are done with its connection. - ScopeGuard guard { [this] { m_windows.remove(m_current_window_handle); m_current_window_handle = "NoSuchWindowPleaseSelectANewOne"_string; } }; - - // 4. If there are no more open top-level browsing contexts, then close the session. - if (m_windows.size() == 1) - close(); - } + // 4. If there are no more open top-level browsing contexts, then close the session. + auto closed_window_handle = m_current_window_handle; + remove_window(closed_window_handle); // 5. Return the result of running the remote end steps for the Get Window Handles command. return get_window_handles(); @@ -375,10 +486,14 @@ Web::WebDriver::Response Session::switch_to_window(StringView handle) // 4. If handle is equal to the associated window handle for some top-level browsing context, let context be the that // browsing context, and set the current top-level browsing context with session and context. // Otherwise, return error with error code no such window. - if (auto it = m_windows.find(handle); it != m_windows.end()) + if (auto it = m_windows.find(handle); it != m_windows.end()) { + if (!it->value.web_content_connection) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnknownError, "Window is waiting for a replacement WebContent process"sv); + m_current_window_handle = it->key; - else + } else { return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv); + } // 5. Update any implementation-specific state that would result from the user selecting the current // browsing context for interaction, without altering OS-level focus. @@ -405,9 +520,60 @@ Web::WebDriver::Response Session::get_window_handles() const ErrorOr Session::ensure_current_window_handle_is_valid() const { - if (auto current_window = m_windows.get(m_current_window_handle); current_window.has_value()) - return {}; - return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv); + auto current_window = m_windows.get(m_current_window_handle); + if (!current_window.has_value()) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv); + + if (!current_window->web_content_connection) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::UnknownError, "Window is waiting for a replacement WebContent process"sv); + + return {}; +} + +ErrorOr Session::wait_for_current_window_to_have_web_content_connection() +{ + m_event_loop.pump(Core::EventLoop::WaitMode::PollForEvents); + + auto current_window = m_windows.get(m_current_window_handle); + if (!current_window.has_value()) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv); + + if (current_window->web_content_connection) + return false; + + Optional page_load_timeout = Web::WebDriver::TimeoutsConfiguration {}.page_load_timeout; + if (m_timeouts_configuration.has_value() && m_timeouts_configuration->is_object()) { + if (auto value = m_timeouts_configuration->as_object().get("pageLoad"sv); value.has_value()) { + if (value->is_null()) + page_load_timeout = {}; + else + page_load_timeout = value->get_integer().value_or(*page_load_timeout); + } + } + + bool timed_out = false; + RefPtr timer; + if (page_load_timeout.has_value()) { + auto timer_interval = *page_load_timeout > NumericLimits::max() ? NumericLimits::max() : static_cast(*page_load_timeout); + timer = Core::Timer::create_single_shot(timer_interval, [&timed_out] { + timed_out = true; + }); + timer->start(); + } + + Core::EventLoop::current().spin_until([this, &timed_out] { + auto current_window = m_windows.get(m_current_window_handle); + return !current_window.has_value() || current_window->web_content_connection || timed_out; + }); + + if (timer) + timer->stop(); + + if (timed_out) + return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::Timeout, "Timed out waiting for replacement WebContent process"sv); + + TRY(ensure_current_window_handle_is_valid()); + return true; } } diff --git a/Services/WebDriver/Session.h b/Services/WebDriver/Session.h index 72615d31a9..6cfd68a214 100644 --- a/Services/WebDriver/Session.h +++ b/Services/WebDriver/Session.h @@ -46,15 +46,17 @@ public: struct Window { String handle; - NonnullRefPtr web_content_connection; + RefPtr web_content_connection; + bool is_awaiting_replacement { false }; }; WebContentConnection& web_content_connection() const { auto current_window = m_windows.get(m_current_window_handle); VERIFY(current_window.has_value()); + VERIFY(current_window->web_content_connection); - return current_window->web_content_connection; + return *current_window->web_content_connection; } void close(); @@ -62,6 +64,7 @@ public: String session_id() const { return m_session_id; } Web::WebDriver::SessionFlags session_flags() const { return m_session_flags; } String const& current_window_handle() const { return m_current_window_handle; } + bool test_hooks_enabled() const { return m_options.enable_test_hooks; } bool has_window_handle(StringView handle) const { return m_windows.contains(handle); } @@ -70,23 +73,44 @@ public: Web::WebDriver::Response switch_to_window(StringView); Web::WebDriver::Response get_window_handles() const; ErrorOr ensure_current_window_handle_is_valid() const; + ErrorOr wait_for_current_window_to_have_web_content_connection(); + void mark_current_window_as_awaiting_replacement(WebContentConnection const&); + + enum class WebContentReplacement { + Disallow, + Allow, + }; template - Web::WebDriver::Response perform_async_action(Action&& action) + Web::WebDriver::Response perform_async_action(Action&& action, WebContentReplacement web_content_replacement = WebContentReplacement::Disallow) { Optional response; - auto& connection = web_content_connection(); + RefPtr connection { &web_content_connection() }; - ScopeGuard guard { [&]() { connection.on_driver_execution_complete = nullptr; } }; - connection.on_driver_execution_complete = [&](auto result) { response = move(result); }; + ScopeGuard guard { [&]() { connection->on_driver_execution_complete = nullptr; } }; + connection->on_driver_execution_complete = [&](auto result) { response = move(result); }; - TRY(action(connection)); + TRY(action(*connection)); Core::EventLoop::current().spin_until([&]() { - return response.has_value(); + if (response.has_value()) + return true; + + if (web_content_replacement == WebContentReplacement::Disallow) + return false; + + auto current_window = m_windows.get(m_current_window_handle); + return !current_window.has_value() || (current_window->is_awaiting_replacement && !current_window->web_content_connection); }); - return response.release_value(); + if (response.has_value()) + return response.release_value(); + + TRY(wait_for_current_window_to_have_web_content_connection()); + if (response.has_value()) + return response.release_value(); + + return JsonValue {}; } private: @@ -97,6 +121,11 @@ private: ErrorOr start(LaunchBrowserCallback const&); ErrorOr accept_web_content_transport(NonnullOwnPtr, NonnullRefPtr promise); ErrorOr create_server(NonnullRefPtr promise); + void web_content_connection_closed(WebContentConnection const&); + void did_update_window_handle(String window_handle, WebContentConnection const&); + void did_start_window_replacement(String const& window_handle, WebContentConnection const&); + void did_close_window(String const& window_handle, WebContentConnection const&); + void remove_window(StringView window_handle); NonnullRefPtr m_client; Web::WebDriver::LadybirdOptions m_options; diff --git a/Services/WebDriver/WebContentConnection.cpp b/Services/WebDriver/WebContentConnection.cpp index be5dbc2039..cf746a8ddd 100644 --- a/Services/WebDriver/WebContentConnection.cpp +++ b/Services/WebDriver/WebContentConnection.cpp @@ -32,4 +32,16 @@ void WebContentConnection::did_set_window_handle(String handle) on_did_set_window_handle(move(handle)); } +void WebContentConnection::did_start_window_replacement(String handle) +{ + if (on_did_start_window_replacement) + on_did_start_window_replacement(move(handle)); +} + +void WebContentConnection::did_close_window(String handle) +{ + if (on_did_close_window) + on_did_close_window(move(handle)); +} + } diff --git a/Services/WebDriver/WebContentConnection.h b/Services/WebDriver/WebContentConnection.h index 233eb50ca3..32f006d21b 100644 --- a/Services/WebDriver/WebContentConnection.h +++ b/Services/WebDriver/WebContentConnection.h @@ -21,15 +21,26 @@ class WebContentConnection public: explicit WebContentConnection(NonnullOwnPtr transport); + Web::WebDriver::Response wait_for_navigation_completion() + { + auto response = send_sync_but_allow_failure(); + VERIFY(response); + return response->response(); + } + Function on_close; Function on_driver_execution_complete; Function on_did_set_window_handle; + Function on_did_start_window_replacement; + Function on_did_close_window; private: virtual void die() override; virtual void driver_execution_complete(Web::WebDriver::Response) override; virtual void did_set_window_handle(String) override; + virtual void did_start_window_replacement(String) override; + virtual void did_close_window(String) override; }; } diff --git a/Services/WebDriver/main.cpp b/Services/WebDriver/main.cpp index eb66bbf09c..9c7fbe6055 100644 --- a/Services/WebDriver/main.cpp +++ b/Services/WebDriver/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -68,7 +69,8 @@ static Vector create_arguments(ByteString const& webdriver_endpoint, arguments.append(ByteString::formatted("--default-time-zone={}", default_time_zone.value())); // FIXME: WebDriver does not yet handle the WebContent process switch brought by site isolation. - arguments.append("--disable-site-isolation"sv); + if (!Core::Environment::has("LADYBIRD_WEBDRIVER_ENABLE_SITE_ISOLATION"sv)) + arguments.append("--disable-site-isolation"sv); arguments.append("about:blank"sv); return arguments; diff --git a/UI/Gtk/BrowserWindow.cpp b/UI/Gtk/BrowserWindow.cpp index 4d9a910a4f..0ed100bffa 100644 --- a/UI/Gtk/BrowserWindow.cpp +++ b/UI/Gtk/BrowserWindow.cpp @@ -89,11 +89,11 @@ void BrowserWindow::register_actions() add_action("go-back", [](BrowserWindow& self) { if (auto* tab = self.current_tab()) - tab->view().traverse_the_history_by_delta(-1); }, false); + (void)tab->view().traverse_the_history_by_delta(-1); }, false); add_action("go-forward", [](BrowserWindow& self) { if (auto* tab = self.current_tab()) - tab->view().traverse_the_history_by_delta(1); }, false); + (void)tab->view().traverse_the_history_by_delta(1); }, false); add_action("zoom-in", [](BrowserWindow& self) { if (auto* tab = self.current_tab()) @@ -400,17 +400,6 @@ int BrowserWindow::tab_count() const return adw_tab_view_get_n_pages(m_tab_view); } -void BrowserWindow::update_navigation_buttons(bool back_enabled, bool forward_enabled) -{ - auto* back_action = G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(m_window), "go-back")); - if (back_action) - g_simple_action_set_enabled(back_action, back_enabled); - - auto* forward_action = G_SIMPLE_ACTION(g_action_map_lookup_action(G_ACTION_MAP(m_window), "go-forward")); - if (forward_action) - g_simple_action_set_enabled(forward_action, forward_enabled); -} - void BrowserWindow::bind_navigation_actions(WebContentView& view) { m_back_binding.detach(); diff --git a/UI/Gtk/BrowserWindow.h b/UI/Gtk/BrowserWindow.h index e62fa1361e..6435fea85c 100644 --- a/UI/Gtk/BrowserWindow.h +++ b/UI/Gtk/BrowserWindow.h @@ -37,7 +37,6 @@ public: void present(); int tab_count() const; - void update_navigation_buttons(bool back_enabled, bool forward_enabled); void update_location_entry(StringView url); void update_location_favicon(GdkPaintable* favicon); void update_location_loading(bool is_loading); diff --git a/UI/Qt/WebContentView.cpp b/UI/Qt/WebContentView.cpp index 395568638e..cfc9528f50 100644 --- a/UI/Qt/WebContentView.cpp +++ b/UI/Qt/WebContentView.cpp @@ -575,9 +575,9 @@ void WebContentView::mouseReleaseEvent(QMouseEvent* event) enqueue_native_event(Web::MouseEvent::Type::MouseUp, *event); if (event->button() == Qt::MouseButton::BackButton) - traverse_the_history_by_delta(-1); + (void)traverse_the_history_by_delta(-1); else if (event->button() == Qt::MouseButton::ForwardButton) - traverse_the_history_by_delta(1); + (void)traverse_the_history_by_delta(1); } void WebContentView::wheelEvent(QWheelEvent* event)