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)