/* * Copyright (c) 2023, Linus Groh * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace WebView { static HashMap& all_views() { static NeverDestroyed> views; return *views; } static u64 s_view_count = 1; // This has to start at 1 for Firefox DevTools. void ViewImplementation::for_each_view(Function callback) { for (auto& view : all_views()) { if (callback(*view.value) == IterationDecision::Break) break; } } Optional ViewImplementation::find_view_by_id(u64 id) { if (auto view = all_views().get(id); view.has_value()) return *view.value(); return {}; } ViewImplementation::ViewImplementation() : m_document_cookie_version_buffer(Core::create_shared_version_buffer()) , m_view_id(s_view_count++) { all_views().set(m_view_id, this); initialize_context_menus(); m_repeated_crash_timer = Core::Timer::create_single_shot(1000, [this] { // Reset the "crashing a lot" counter after 1 second in case we just // happen to be visiting crashy websites a lot. this->m_crash_count = 0; }); on_request_file = [this](auto const& path, auto request_id) { auto file = Core::File::open(path, Core::File::OpenMode::Read); if (file.is_error()) client().async_handle_file_return(page_id(), file.error().code(), {}, request_id); else client().async_handle_file_return(page_id(), 0, IPC::File::adopt_file(file.release_value()), request_id); }; } ViewImplementation::~ViewImplementation() { all_views().remove(m_view_id); if (m_client_state.client) m_client_state.client->unregister_view(m_client_state.page_index); } WebContentClient& ViewImplementation::client() { VERIFY(m_client_state.client); return *m_client_state.client; } WebContentClient const& ViewImplementation::client() const { VERIFY(m_client_state.client); return *m_client_state.client; } u64 ViewImplementation::page_id() const { VERIFY(m_client_state.client); return m_client_state.page_index; } void ViewImplementation::set_url(URL::URL url) { if (m_url == url) return; auto previous_host = current_host(); m_url = move(url); update_bookmark_action(); if (current_host() != previous_host) apply_zoom_for_current_host(); } void ViewImplementation::set_favicon(Badge, Gfx::Bitmap const& favicon) { m_favicon_base64_png.clear(); if (auto favicon_png = Gfx::PNGWriter::encode(favicon); !favicon_png.is_error()) { if (auto favicon_base64_png = encode_base64(favicon_png.value().bytes()); !favicon_base64_png.is_error()) m_favicon_base64_png = favicon_base64_png.release_value(); } if (m_favicon_base64_png.has_value()) { Application::bookmark_store().update_favicon(m_url, *m_favicon_base64_png); if (!m_should_suppress_history_for_current_load) Application::history_store().update_favicon(m_url, *m_favicon_base64_png); } if (on_favicon_change) on_favicon_change(favicon); } 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); m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size; } if (m_client_state.client) { m_client_state.client->unregister_view(m_client_state.page_index); } initialize_client(); VERIFY(m_client_state.client); if (on_web_content_process_change_for_cross_site_navigation) on_web_content_process_change_for_cross_site_navigation(); handle_resize(); 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) { bool did_swap_bitmap = false; if (m_client_state.back_bitmap.id == bitmap_id) { m_client_state.has_usable_bitmap = true; m_client_state.back_bitmap.last_painted_size = size.to_type(); swap(m_client_state.back_bitmap, m_client_state.front_bitmap); m_backup_shared_image_buffer = nullptr; did_swap_bitmap = true; } dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI received presented bitmap {} for page {} size={}x{} did_swap={} front={} back={}", bitmap_id, page_id(), size.width(), size.height(), did_swap_bitmap, m_client_state.front_bitmap.id, m_client_state.back_bitmap.id); client().notify_presented_bitmap_ready_to_paint(page_id(), bitmap_id); if (did_swap_bitmap && on_ready_to_paint) on_ready_to_paint(); } void ViewImplementation::set_window_position(Gfx::IntPoint position) { client().async_set_window_position(m_client_state.page_index, position.to_type()); } void ViewImplementation::set_window_size(Gfx::IntSize size) { client().async_set_window_size(m_client_state.page_index, size.to_type()); } void ViewImplementation::did_update_window_rect() { client().async_did_update_window_rect(m_client_state.page_index); } void ViewImplementation::set_system_visibility_state(Web::HTML::VisibilityState visibility_state) { if (m_system_visibility_state == visibility_state) return; m_system_visibility_state = visibility_state; client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state); } 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; 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); } void ViewImplementation::load_navigation_error_page(StringView text) { auto message = MUST(String::formatted("Failed to load \"{}\"", text)); StringBuilder builder; builder.appendff(ERROR_HTML_HEADER, ""sv, ERROR_SVG, message); builder.append("

If you were trying to enter a search query, please enable search in settings.

"sv); builder.append(ERROR_HTML_FOOTER); load_html(builder.string_view()); } 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()); } 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; 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() { if (m_zoom_level >= ZOOM_MAX_LEVEL) return; m_zoom_level = round_to((m_zoom_level + ZOOM_STEP) * 100) / 100.0; update_zoom(); Application::settings().set_zoom_for_host(current_host(), m_zoom_level); } void ViewImplementation::zoom_out() { if (m_zoom_level <= ZOOM_MIN_LEVEL) return; m_zoom_level = round_to((m_zoom_level - ZOOM_STEP) * 100) / 100.0; update_zoom(); Application::settings().set_zoom_for_host(current_host(), m_zoom_level); } void ViewImplementation::set_zoom(double zoom_level) { m_zoom_level = max(ZOOM_MIN_LEVEL, min(zoom_level, ZOOM_MAX_LEVEL)); update_zoom(); } void ViewImplementation::reset_zoom() { m_zoom_level = 1.0; update_zoom(); client().async_reset_zoom(m_client_state.page_index); Application::settings().set_zoom_for_host(current_host(), m_zoom_level); } void ViewImplementation::enqueue_input_event(Web::InputEvent event) { auto* mouse_event = event.get_pointer(); if (mouse_event && mouse_event->type == Web::MouseEvent::Type::MouseWheel) { mouse_event->wheel_delta_x /= zoom_level(); mouse_event->wheel_delta_y /= zoom_level(); } if (Application::web_content_options().enable_async_scrolling == EnableAsyncScrolling::Yes && m_client_state.has_usable_bitmap && mouse_event) { if (mouse_event->type == Web::MouseEvent::Type::MouseWheel) { auto wheel_delta_x = mouse_event->wheel_delta_x; auto wheel_delta_y = mouse_event->wheel_delta_y; if (mouse_event->modifiers & Web::UIEvents::KeyModifier::Mod_Shift) swap(wheel_delta_x, wheel_delta_y); auto device_pixels_per_css_pixel = static_cast(device_pixel_ratio() * zoom_level()); auto position = Gfx::FloatPoint { static_cast(mouse_event->position.x().value()), static_cast(mouse_event->position.y().value()), }; auto delta_in_device_pixels = Gfx::FloatPoint { wheel_delta_x, wheel_delta_y }.scaled(device_pixels_per_css_pixel); dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI attempting compositor wheel bypass for page {} at {},{} device delta {},{}", m_client_state.page_index, position.x(), position.y(), delta_in_device_pixels.x(), delta_in_device_pixels.y()); if (client().send_async_scroll_to_compositor(m_client_state.page_index, position, delta_in_device_pixels)) mouse_event->async_scroll_performed_default_action = true; dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor wheel bypass result for page {}: {}", m_client_state.page_index, mouse_event->async_scroll_performed_default_action ? "accepted"sv : "rejected"sv); } else if (client().handle_mouse_event_in_compositor(m_client_state.page_index, *mouse_event)) { dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI compositor handled mouse event for page {} at {},{}", m_client_state.page_index, mouse_event->position.x().value(), mouse_event->position.y().value()); return; } } // Send the next event over to the WebContent to be handled by JS. We'll later get a message to say whether JS // prevented the default event behavior, at which point we either discard or handle that event, and then try to // process the next one. m_pending_input_events.enqueue(move(event)); m_pending_input_events.tail().visit( [this](Web::KeyEvent const& event) { client().async_key_event(m_client_state.page_index, event.clone_without_browser_data()); }, [this](Web::MouseEvent const& event) { client().dispatch_mouse_event_to_web_content(m_client_state.page_index, event); }, [this](Web::DragEvent& event) { auto cloned_event = event.clone_without_browser_data(); cloned_event.files = move(event.files); client().async_drag_event(m_client_state.page_index, cloned_event); }, [this](Web::PinchEvent const& event) { client().async_pinch_event(m_client_state.page_index, event); }); } void ViewImplementation::did_finish_handling_input_event(Badge, Web::EventResult event_result) { auto event = m_pending_input_events.dequeue(); if (event_result == Web::EventResult::Handled) return; // Here we handle events that were not consumed or cancelled by the WebContent. Propagate the event back // to the concrete view implementation. event.visit( [this](Web::KeyEvent const& event) { if (on_finish_handling_key_event) on_finish_handling_key_event(event); }, [this](Web::DragEvent const& event) { if (on_finish_handling_drag_event) on_finish_handling_drag_event(event); }, [](auto const&) {}); } void ViewImplementation::set_preferred_color_scheme(Web::CSS::PreferredColorScheme color_scheme) { m_preferred_color_scheme = color_scheme; set_page_background_color(preferred_canvas_background_color()); client().async_set_preferred_color_scheme(page_id(), color_scheme); } void ViewImplementation::set_preferred_contrast(Web::CSS::PreferredContrast contrast) { client().async_set_preferred_contrast(page_id(), contrast); } void ViewImplementation::set_preferred_motion(Web::CSS::PreferredMotion motion) { client().async_set_preferred_motion(page_id(), motion); } void ViewImplementation::notify_cookies_changed(HashTable const& changed_domains, ReadonlySpan page_cookies, ReadonlySpan host_cookies) { for (auto const& domain : changed_domains) { if (auto document_index = m_document_cookie_version_indices.get(domain); document_index.has_value()) Core::increment_shared_version(m_document_cookie_version_buffer, *document_index); } if (!page_cookies.is_empty()) client().async_cookies_changed(page_id(), page_cookies); if (m_on_host_cookie_change) m_on_host_cookie_change(Vector { host_cookies }); } void ViewImplementation::listen_for_host_cookie_changes(DevTools::DevToolsDelegate::OnHostCookieChange on_cookie_change) { m_on_host_cookie_change = move(on_cookie_change); } void ViewImplementation::stop_listening_for_host_cookie_changes() { m_on_host_cookie_change = nullptr; } void ViewImplementation::notify_storage_changed(DevTools::DevToolsDelegate::StorageChange change) { for (auto& listener : m_storage_change_listeners) listener.value(change); } u64 ViewImplementation::add_storage_change_listener(DevTools::DevToolsDelegate::OnStorageChange on_storage_change) { auto listener_id = m_next_storage_change_listener_id++; m_storage_change_listeners.set(listener_id, move(on_storage_change)); return listener_id; } void ViewImplementation::remove_storage_change_listener(u64 listener_id) { m_storage_change_listeners.remove(listener_id); } ErrorOr ViewImplementation::ensure_document_cookie_version_index(Badge, String const& domain) { return m_document_cookie_version_indices.try_ensure(domain, [&]() -> ErrorOr { Core::SharedVersionIndex document_index = m_document_cookie_version_indices.size(); if (!Core::initialize_shared_version(m_document_cookie_version_buffer, document_index)) { dbgln("Reached maximum document cookie version count for {}, cannot create new version for {}", m_url, domain); return Error::from_string_literal("Reached maximum document cookie version count"); } return document_index; }); } Optional ViewImplementation::document_cookie_version(URL::URL const& url) const { auto domain = HTTP::Cookie::canonicalize_domain(url); if (!domain.has_value()) return {}; auto document_index = m_document_cookie_version_indices.get(*domain); if (!document_index.has_value()) return {}; return Core::get_shared_version(m_document_cookie_version_buffer, *document_index); } ByteString ViewImplementation::selected_text() { return client().get_selected_text(page_id()); } ByteString ViewImplementation::cut_selected_text() { return client().cut_selected_text(page_id()); } Optional ViewImplementation::selected_text_with_whitespace_collapsed() { auto selected_text = MUST(Web::Infra::strip_and_collapse_whitespace(this->selected_text())); if (selected_text.is_empty()) return OptionalNone {}; return selected_text; } void ViewImplementation::select_all() { client().async_select_all(page_id()); } void ViewImplementation::find_in_page(String const& query, CaseSensitivity case_sensitivity) { client().async_find_in_page(page_id(), query, case_sensitivity); } void ViewImplementation::find_in_page_next_match() { client().async_find_in_page_next_match(page_id()); } void ViewImplementation::find_in_page_previous_match() { client().async_find_in_page_previous_match(page_id()); } void ViewImplementation::get_source() { client().async_get_source(page_id()); } void ViewImplementation::inspect_dom_tree() { client().async_inspect_dom_tree(page_id()); } void ViewImplementation::inspect_storage(Web::StorageAPI::StorageEndpointType storage_endpoint, u64 request_id) { client().async_inspect_storage(page_id(), storage_endpoint, request_id); } Optional ViewImplementation::set_session_storage_item(String const& key, String const& value) { return client().set_session_storage_item(page_id(), key, value); } Optional ViewImplementation::remove_session_storage_item(String const& key) { return client().remove_session_storage_item(page_id(), key); } bool ViewImplementation::clear_session_storage() { return client().clear_session_storage(page_id()); } void ViewImplementation::inspect_accessibility_tree() { client().async_inspect_accessibility_tree(page_id()); } void ViewImplementation::get_hovered_node_id() { client().async_get_hovered_node_id(page_id()); } void ViewImplementation::start_node_picker(DevTools::DevToolsDelegate::OnNodePickerEvent on_node_picker_event) { m_node_picker_active = true; m_node_picker_hovered_node_id.clear(); m_pending_node_picker_requests.clear(); m_on_node_picker_event = move(on_node_picker_event); } void ViewImplementation::stop_node_picker() { if (!m_node_picker_active) return; clear_node_picker(); m_node_picker_active = false; m_pending_node_picker_requests.clear(); m_on_node_picker_event = nullptr; } void ViewImplementation::clear_node_picker() { m_node_picker_hovered_node_id.clear(); clear_highlighted_dom_node(); } void ViewImplementation::node_picker_hover(Web::DevicePixelPoint position) { request_node_picker_hit_test(NodePickerRequestType::Hovered, position); } void ViewImplementation::node_picker_pick(Web::DevicePixelPoint position) { request_node_picker_hit_test(NodePickerRequestType::Picked, position); } void ViewImplementation::node_picker_preview(Web::DevicePixelPoint position) { request_node_picker_hit_test(NodePickerRequestType::Previewed, position); } void ViewImplementation::node_picker_cancel() { if (!m_node_picker_active) return; if (m_on_node_picker_event) { m_on_node_picker_event({ .type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Canceled, .node_id = {}, }); } } void ViewImplementation::request_node_picker_hit_test(NodePickerRequestType type, Web::DevicePixelPoint position) { if (!m_node_picker_active) return; auto request_id = m_next_node_picker_request_id++; m_pending_node_picker_requests.set(request_id, type); client().async_get_node_id_at_position(page_id(), request_id, position); } void ViewImplementation::did_receive_node_picker_hit_test(u64 request_id, Web::UniqueNodeID node_id) { auto request_type = m_pending_node_picker_requests.take(request_id); if (!request_type.has_value() || !m_node_picker_active) return; if (*request_type == NodePickerRequestType::Hovered) { if (node_id == m_node_picker_hovered_node_id.value_or(0)) return; m_node_picker_hovered_node_id = node_id; if (node_id == 0) { clear_node_picker(); return; } } else if (node_id == 0) { return; } if (!m_on_node_picker_event) return; DevTools::DevToolsDelegate::NodePickerEvent::Type event_type; switch (*request_type) { case NodePickerRequestType::Hovered: event_type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Hovered; break; case NodePickerRequestType::Picked: event_type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Picked; break; case NodePickerRequestType::Previewed: event_type = DevTools::DevToolsDelegate::NodePickerEvent::Type::Previewed; break; } m_on_node_picker_event({ .type = event_type, .node_id = node_id, }); } void ViewImplementation::inspect_dom_node(Web::UniqueNodeID node_id, DOMNodeProperties::Type property_type, Optional pseudo_element, JsonValue options) { client().async_inspect_dom_node(page_id(), property_type, node_id, pseudo_element, move(options)); } void ViewImplementation::inspect_grid_layouts(Web::UniqueNodeID root_node_id) { client().async_inspect_grid_layouts(page_id(), root_node_id); } void ViewImplementation::inspect_current_grid(Web::UniqueNodeID node_id) { client().async_inspect_current_grid(page_id(), node_id); } void ViewImplementation::inspect_current_flexbox(Web::UniqueNodeID node_id, bool only_look_at_parents) { client().async_inspect_current_flexbox(page_id(), node_id, only_look_at_parents); } void ViewImplementation::clear_inspected_dom_node() { client().async_clear_inspected_dom_node(page_id()); } void ViewImplementation::highlight_dom_node(Web::UniqueNodeID node_id, Optional pseudo_element) { client().async_highlight_dom_node(page_id(), node_id, pseudo_element); } void ViewImplementation::clear_highlighted_dom_node() { highlight_dom_node(0, {}); } void ViewImplementation::highlight_flexbox(Web::UniqueNodeID node_id, JsonValue options) { client().async_highlight_flexbox(page_id(), node_id, move(options)); } void ViewImplementation::clear_flexbox_highlight(Web::UniqueNodeID node_id) { client().async_clear_flexbox_highlight(page_id(), node_id); } void ViewImplementation::highlight_grid(Web::UniqueNodeID node_id, JsonValue options) { client().async_highlight_grid(page_id(), node_id, move(options)); } void ViewImplementation::clear_grid_highlight(Web::UniqueNodeID node_id) { client().async_clear_grid_highlight(page_id(), node_id); } void ViewImplementation::set_listen_for_dom_mutations(bool listen_for_dom_mutations) { client().async_set_listen_for_dom_mutations(page_id(), listen_for_dom_mutations); } void ViewImplementation::did_connect_devtools_client() { m_devtools_connected = true; client().async_did_connect_devtools_client(page_id()); } void ViewImplementation::did_disconnect_devtools_client() { m_devtools_connected = false; client().async_did_disconnect_devtools_client(page_id()); } void ViewImplementation::get_dom_node_inner_html(Web::UniqueNodeID node_id) { client().async_get_dom_node_inner_html(page_id(), node_id); } void ViewImplementation::get_dom_node_outer_html(Web::UniqueNodeID node_id) { client().async_get_dom_node_outer_html(page_id(), node_id); } void ViewImplementation::set_dom_node_outer_html(Web::UniqueNodeID node_id, String const& html) { client().async_set_dom_node_outer_html(page_id(), node_id, html); } void ViewImplementation::set_dom_node_text(Web::UniqueNodeID node_id, String const& text) { client().async_set_dom_node_text(page_id(), node_id, text); } void ViewImplementation::set_dom_node_tag(Web::UniqueNodeID node_id, String const& name) { client().async_set_dom_node_tag(page_id(), node_id, name); } void ViewImplementation::add_dom_node_attributes(Web::UniqueNodeID node_id, ReadonlySpan attributes) { client().async_add_dom_node_attributes(page_id(), node_id, attributes); } void ViewImplementation::replace_dom_node_attribute(Web::UniqueNodeID node_id, String const& name, ReadonlySpan replacement_attributes) { client().async_replace_dom_node_attribute(page_id(), node_id, name, replacement_attributes); } void ViewImplementation::create_child_element(Web::UniqueNodeID node_id) { client().async_create_child_element(page_id(), node_id); } void ViewImplementation::create_child_text_node(Web::UniqueNodeID node_id) { client().async_create_child_text_node(page_id(), node_id); } void ViewImplementation::insert_dom_node_before(Web::UniqueNodeID node_id, Web::UniqueNodeID parent_node_id, Optional sibling_node_id) { client().async_insert_dom_node_before(page_id(), node_id, parent_node_id, sibling_node_id); } void ViewImplementation::clone_dom_node(Web::UniqueNodeID node_id) { client().async_clone_dom_node(page_id(), node_id); } void ViewImplementation::remove_dom_node(Web::UniqueNodeID node_id) { client().async_remove_dom_node(page_id(), node_id); } void ViewImplementation::list_style_sheets() { client().async_list_style_sheets(page_id()); } void ViewImplementation::request_style_sheet_source(Web::CSS::StyleSheetIdentifier const& identifier) { client().async_request_style_sheet_source(page_id(), identifier); } 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); } void ViewImplementation::set_content_blockers(Core::AnonymousBuffer const& patterns) { client().async_set_content_blockers(page_id(), patterns); } void ViewImplementation::run_javascript(String const& js_source) { client().async_run_javascript(page_id(), js_source); } void ViewImplementation::js_console_input(String const& js_source) { client().async_js_console_input(page_id(), js_source); } void ViewImplementation::exit_fullscreen() { client().async_exit_fullscreen(page_id()); } void ViewImplementation::set_is_fullscreen(Web::ViewportIsFullscreen is_fullscreen) { if (m_is_fullscreen == is_fullscreen) return; m_is_fullscreen = is_fullscreen; handle_resize(); did_update_window_rect(); } void ViewImplementation::alert_closed() { client().async_alert_closed(page_id()); } void ViewImplementation::confirm_closed(bool accepted) { client().async_confirm_closed(page_id(), accepted); } void ViewImplementation::prompt_closed(Optional const& response) { client().async_prompt_closed(page_id(), response); } void ViewImplementation::color_picker_update(Optional picked_color, Web::HTML::ColorPickerUpdateState state) { client().async_color_picker_update(page_id(), picked_color, state); } void ViewImplementation::file_picker_closed(Vector selected_files) { client().async_file_picker_closed(page_id(), move(selected_files)); } void ViewImplementation::select_dropdown_closed(Optional const& selected_item_id) { client().async_select_dropdown_closed(page_id(), selected_item_id); } void ViewImplementation::paste_text_from_clipboard() { client().async_paste(page_id(), Application::the().clipboard_text()); } void ViewImplementation::set_marked_text_from_input_method(Utf16String const& text) { client().async_set_marked_text_from_input_method(page_id(), text); } void ViewImplementation::commit_text_from_input_method(Utf16String const& text) { client().async_commit_text_from_input_method(page_id(), text); } void ViewImplementation::unmark_text_from_input_method() { client().async_unmark_text_from_input_method(page_id()); } Optional ViewImplementation::get_input_caret_rect() { // Returns the most-recent caret position pushed by WebContent (see set_input_caret_rect). Deliberately makes no // synchronous IPC request: This is read from inside AppKit text-input callbacks — where blocking can re-enter the // run loop and deadlock the input method. return m_input_caret_rect; } void ViewImplementation::set_input_caret_rect(Badge, Optional rect) { m_input_caret_rect = rect; } void ViewImplementation::retrieved_clipboard_entries(u64 request_id, ReadonlySpan items) { client().async_retrieved_clipboard_entries(page_id(), request_id, items); } void ViewImplementation::toggle_page_mute_state() { m_mute_state = Web::HTML::invert_mute_state(m_mute_state); client().async_toggle_page_mute_state(page_id()); } void ViewImplementation::did_change_audio_play_state(Badge, Web::HTML::AudioPlayState play_state) { bool state_changed = false; switch (play_state) { case Web::HTML::AudioPlayState::Paused: if (--m_number_of_elements_playing_audio == 0) { m_audio_play_state = play_state; state_changed = true; } break; case Web::HTML::AudioPlayState::Playing: if (m_number_of_elements_playing_audio++ == 0) { m_audio_play_state = play_state; state_changed = true; } break; } if (state_changed && on_audio_play_state_changed) on_audio_play_state_changed(m_audio_play_state); } 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) { m_needs_beforeunload_check = needs_beforeunload_check; } void ViewImplementation::did_change_background_color(Badge, Gfx::Color color) { set_page_background_color(color); } void ViewImplementation::set_page_background_color_to_system_canvas(bool dark) { auto color_scheme = dark ? Web::CSS::PreferredColorScheme::Dark : Web::CSS::PreferredColorScheme::Light; m_system_canvas_background_color = Web::CSS::SystemColor::canvas(color_scheme); set_page_background_color(preferred_canvas_background_color()); } void ViewImplementation::set_page_background_color(Gfx::Color color) { m_page_background_color = color; if (on_page_background_color_change) on_page_background_color_change(m_page_background_color); } Gfx::Color ViewImplementation::preferred_canvas_background_color() const { if (m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Dark) return Web::CSS::SystemColor::canvas(Web::CSS::PreferredColorScheme::Dark); if (m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Light) return Web::CSS::SystemColor::canvas(Web::CSS::PreferredColorScheme::Light); return m_system_canvas_background_color; } void ViewImplementation::did_allocate_backing_stores(Badge, i32 front_bitmap_id, Gfx::SharedImage front_backing_store, i32 back_bitmap_id, Gfx::SharedImage back_backing_store) { dbgln_if(COMPOSITOR_DEBUG, "[Compositor] UI installing backing stores for page {} front={} back={} had_usable_bitmap={}", page_id(), front_bitmap_id, back_bitmap_id, m_client_state.has_usable_bitmap); if (m_client_state.has_usable_bitmap) { // NOTE: We keep the outgoing front bitmap as a backup so we have something to paint until we get a new one. m_backup_shared_image_buffer = move(m_client_state.front_bitmap.shared_image_buffer); m_backup_bitmap_size = m_client_state.front_bitmap.last_painted_size; } m_client_state.has_usable_bitmap = false; m_client_state.front_bitmap.id = front_bitmap_id; m_client_state.back_bitmap.id = back_bitmap_id; m_client_state.front_bitmap.shared_image_buffer = make(Gfx::SharedImageBuffer::import_from_shared_image(move(front_backing_store))); m_client_state.back_bitmap.shared_image_buffer = make(Gfx::SharedImageBuffer::import_from_shared_image(move(back_backing_store))); } void ViewImplementation::update_zoom() { if (m_zoom_level != 1.0) { m_reset_zoom_action->set_text(MUST(String::formatted("{}%", round_to(m_zoom_level * 100)))); m_reset_zoom_action->set_visible(true); } else { m_reset_zoom_action->set_visible(false); } client().async_set_zoom_level(m_client_state.page_index, m_zoom_level); } String ViewImplementation::current_host() const { if (!m_url.host().has_value()) return {}; return m_url.serialized_host(); } void ViewImplementation::apply_zoom_for_current_host() { auto& settings = Application::settings(); auto zoom_level = settings.zoom_for_host(current_host()).value_or(settings.default_zoom_level_factor()); if (zoom_level == m_zoom_level) return; m_zoom_level = zoom_level; update_zoom(); } void ViewImplementation::handle_resize() { client().async_set_viewport(page_id(), viewport_size(), m_device_pixel_ratio, m_is_fullscreen); Application::the().update_compositor_viewport(client().compositor_context_id_for_page(page_id()), viewport_size().to_type(), Web::Compositor::WindowResizingInProgress::Yes); } 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(); } else { m_client_state.client->register_view(m_client_state.page_index, *this); } 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); client().async_set_maximum_frames_per_second(m_client_state.page_index, m_maximum_frames_per_second); client().async_set_system_visibility_state(m_client_state.page_index, m_system_visibility_state); auto compositor_context_id = client().compositor_context_id_for_page(m_client_state.page_index); Application::the().update_compositor_viewport(compositor_context_id, viewport_size().to_type()); client().async_set_document_cookie_version_buffer(m_client_state.page_index, m_document_cookie_version_buffer); if (auto webdriver_endpoint = Application::browser_options().webdriver_endpoint; webdriver_endpoint.has_value()) client().async_connect_to_webdriver(m_client_state.page_index, *webdriver_endpoint); Application::the().apply_view_options({}, *this); default_zoom_level_factor_changed(); languages_changed(); browsing_behavior_changed(); autoplay_settings_changed(); global_privacy_control_changed(); // If DevTools is connected, notify the new WebContent process. if (m_devtools_connected) 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(); if (!headless_mode) { dbgln("\033[31;1mWebContent process crashed!\033[0m Last page loaded: {}", m_url); dbgln("Consider raising an issue at https://github.com/LadybirdBrowser/ladybird/issues/new/choose"); } ++m_crash_count; constexpr size_t max_reasonable_crash_count = 5U; if (m_crash_count >= max_reasonable_crash_count) { if (!headless_mode) { dbgln("WebContent has crashed {} times in quick succession! Not restarting...", m_crash_count); m_repeated_crash_timer->stop(); return; } // In headless mode, always respawn - tests need a working WebContent for each test. // Reset the crash count so we can continue running tests. m_crash_count = 0; } m_repeated_crash_timer->restart(); // In headless mode, respawn WebContent but skip the error page. if (headless_mode) load_error_page = LoadErrorPage::No; initialize_client(); VERIFY(m_client_state.client); // 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) { auto escaped_url = escape_html_entities(m_url.serialize()); StringBuilder builder; builder.appendff(ERROR_HTML_HEADER, NO_FALLBACK_FAVICON_LINK, CRASH_ERROR_SVG, "Ladybird flew off-course!"sv); 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(); } } void ViewImplementation::default_zoom_level_factor_changed() { apply_zoom_for_current_host(); } void ViewImplementation::zoom_per_host_changed(StringView host) { if (current_host() != host) return; apply_zoom_for_current_host(); } void ViewImplementation::languages_changed() { auto const& languages = Application::settings().languages(); client().async_set_preferred_languages(page_id(), languages); } void ViewImplementation::browsing_behavior_changed() { auto const& browsing_behavior = Application::settings().browsing_behavior(); client().async_set_browsing_behavior(page_id(), browsing_behavior); } void ViewImplementation::autoplay_settings_changed() { auto const& autoplay_settings = Application::settings().autoplay_settings(); auto const& web_content_options = Application::web_content_options(); if (autoplay_settings.enabled_globally || web_content_options.enable_autoplay == EnableAutoplay::Yes) client().async_set_autoplay_allowed_on_all_websites(page_id()); else client().async_set_autoplay_allowlist(page_id(), autoplay_settings.site_filters.values()); } void ViewImplementation::global_privacy_control_changed() { auto global_privacy_control = Application::settings().global_privacy_control(); client().async_set_enable_global_privacy_control(page_id(), global_privacy_control == GlobalPrivacyControl::Yes); } void ViewImplementation::bookmarks_changed() { update_bookmark_action(); } void ViewImplementation::update_bookmark_action() { Application::the().update_bookmark_action_for_current_web_view(); auto is_bookmarked = Application::bookmark_store().is_bookmarked(url()); m_toggle_bookmark_action->set_tooltip(is_bookmarked ? "Remove bookmark"sv : "Bookmark this page"sv); m_toggle_bookmark_action->set_engaged(is_bookmarked); } static ErrorOr save_screenshot(Gfx::Bitmap const* bitmap) { if (!bitmap) return Error::from_string_literal("Failed to take a screenshot"); auto path = TRY([] -> ErrorOr { if (auto const& screenshot_path = Application::browser_options().screenshot_path; screenshot_path.has_value()) return LexicalPath { *screenshot_path }; auto file = AK::UnixDateTime::now().to_byte_string("screenshot-%Y-%m-%d-%H-%M-%S.png"sv); return Application::the().path_for_downloaded_file(file); }()); auto encoded = TRY(Gfx::PNGWriter::encode(*bitmap)); auto dump_file = TRY(Core::File::open(path.string(), Core::File::OpenMode::Write)); TRY(dump_file->write_until_depleted(encoded)); return path; } NonnullRefPtr> ViewImplementation::take_screenshot(ScreenshotType type) { auto promise = Core::Promise::construct(); if (m_pending_screenshot) { // For simplicity, only allow taking one screenshot at a time for now. Revisit if we need // to allow spamming screenshot requests for some reason. promise->reject(Error::from_string_literal("A screenshot request is already in progress")); return promise; } switch (type) { case ScreenshotType::Visible: { Gfx::Bitmap const* visible_bitmap = nullptr; if (m_client_state.has_usable_bitmap) { VERIFY(m_client_state.front_bitmap.shared_image_buffer); visible_bitmap = m_client_state.front_bitmap.shared_image_buffer->bitmap().ptr(); } else if (m_backup_shared_image_buffer) { visible_bitmap = m_backup_shared_image_buffer->bitmap().ptr(); } if (visible_bitmap) { if (auto result = save_screenshot(visible_bitmap); result.is_error()) promise->reject(result.release_error()); else promise->resolve(result.release_value()); } break; } case ScreenshotType::Full: m_pending_screenshot = promise; client().async_take_document_screenshot(page_id()); break; } return promise; } NonnullRefPtr> ViewImplementation::take_dom_node_screenshot(Web::UniqueNodeID node_id) { auto promise = Core::Promise::construct(); if (m_pending_screenshot) { // For simplicity, only allow taking one screenshot at a time for now. Revisit if we need // to allow spamming screenshot requests for some reason. promise->reject(Error::from_string_literal("A screenshot request is already in progress")); return promise; } m_pending_screenshot = promise; client().async_take_dom_node_screenshot(page_id(), node_id); return promise; } void ViewImplementation::did_receive_screenshot(Badge, Gfx::ShareableBitmap const& screenshot) { VERIFY(m_pending_screenshot); if (auto result = save_screenshot(screenshot.bitmap()); result.is_error()) m_pending_screenshot->reject(result.release_error()); else m_pending_screenshot->resolve(result.release_value()); m_pending_screenshot = nullptr; } NonnullRefPtr> ViewImplementation::request_internal_page_info(PageInfoType type) { auto promise = Core::Promise::construct(); if (m_pending_info_request) { // For simplicity, only allow one info request at a time for now. promise->reject(Error::from_string_literal("A page info request is already in progress")); return promise; } m_pending_info_request = promise; client().async_request_internal_page_info(page_id(), type); return promise; } void ViewImplementation::did_receive_internal_page_info(Badge, PageInfoType, Optional const& info) { VERIFY(m_pending_info_request); String info_string; if (!info.has_value()) { info_string = "(no page)"_string; } else { info_string = MUST(String::from_utf8(info->bytes())); } m_pending_info_request->resolve(move(info_string)); m_pending_info_request = nullptr; } ErrorOr ViewImplementation::dump_gc_graph() { auto promise = request_internal_page_info(PageInfoType::GCGraph); auto gc_graph_json = TRY(promise->await()); LexicalPath path { Core::StandardPaths::tempfile_directory() }; path = path.append(TRY(AK::UnixDateTime::now().to_string("gc-graph-%Y-%m-%d-%H-%M-%S.js"sv))); // Write as a .js file so gc-heap-explorer.html can load it via