| // Copyright 2012 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "chrome/browser/devtools/devtools_window.h" | |
| #include <algorithm> | |
| #include <memory> | |
| #include <set> | |
| #include <string_view> | |
| #include <utility> | |
| #include "base/base64.h" | |
| #include "base/check.h" | |
| #include "base/command_line.h" | |
| #include "base/functional/bind.h" | |
| #include "base/json/json_reader.h" | |
| #include "base/memory/raw_ptr.h" | |
| #include "base/metrics/histogram_functions.h" | |
| #include "base/metrics/histogram_macros.h" | |
| #include "base/metrics/user_metrics.h" | |
| #include "base/no_destructor.h" | |
| #include "base/notimplemented.h" | |
| #include "base/strings/escape.h" | |
| #include "base/strings/utf_string_conversions.h" | |
| #include "base/task/sequenced_task_runner.h" | |
| #include "base/time/time.h" | |
| #include "base/unguessable_token.h" | |
| #include "base/values.h" | |
| #include "chrome/browser/browser_process.h" | |
| #include "chrome/browser/certificate_viewer.h" | |
| #include "chrome/browser/devtools/aida_client.h" | |
| #include "chrome/browser/devtools/devtools_availability_checker.h" | |
| #include "chrome/browser/devtools/devtools_eye_dropper.h" | |
| #include "chrome/browser/devtools/features.h" | |
| #include "chrome/browser/devtools/process_sharing_infobar.h" | |
| #include "chrome/browser/devtools/process_sharing_infobar_delegate.h" | |
| #include "chrome/browser/file_select_helper.h" | |
| #include "chrome/browser/infobars/browser_infobar_manager.h" | |
| #include "chrome/browser/infobars/confirm_infobar_creator.h" | |
| #include "chrome/browser/infobars/infobar_features.h" | |
| #include "chrome/browser/infobars/infobar_spec.h" | |
| #include "chrome/browser/policy/developer_tools_policy_checker.h" | |
| #include "chrome/browser/policy/developer_tools_policy_checker_factory.h" | |
| #include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h" | |
| #include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h" | |
| #include "chrome/browser/profiles/profile.h" | |
| #include "chrome/browser/ui/tabs/tab_enums.h" | |
| #include "components/sessions/core/session_id.h" | |
| #include "extensions/buildflags/buildflags.h" | |
| #include "ui/base/mojom/window_open_disposition.mojom.h" | |
| #if BUILDFLAG(IS_MAC) | |
| #include "chrome/browser/renderer_host/chrome_render_widget_host_view_mac_history_swiping_control.h" | |
| #endif | |
| #include "chrome/browser/search_engines/template_url_service_factory.h" | |
| #include "chrome/browser/tab_list/tab_list_interface.h" | |
| #include "chrome/browser/task_manager/web_contents_tags.h" | |
| #include "chrome/browser/ui/browser_window/public/create_browser_window.h" | |
| #include "chrome/browser/ui/dialogs/browser_dialogs.h" | |
| #include "chrome/browser/ui/prefs/prefs_tab_helper.h" | |
| #include "chrome/browser/ui/scoped_tabbed_browser_displayer.h" | |
| #include "chrome/browser/ui/tabs/tab_group_deletion_dialog_controller.h" | |
| #include "chrome/browser/ui/tabs/tab_strip_user_gesture_details.h" | |
| #include "chrome/browser/ui/webui/devtools/devtools_ui.h" | |
| #include "chrome/common/chrome_switches.h" | |
| #include "chrome/common/pref_names.h" | |
| #include "chrome/common/url_constants.h" | |
| #include "components/infobars/content/content_infobar_manager.h" | |
| #include "components/infobars/core/infobar.h" | |
| #include "components/input/native_web_keyboard_event.h" | |
| #include "components/javascript_dialogs/app_modal_dialog_manager.h" | |
| #include "components/language/core/browser/pref_names.h" | |
| #include "components/pref_registry/pref_registry_syncable.h" | |
| #include "components/prefs/scoped_user_pref_update.h" | |
| #include "components/search_engines/util.h" | |
| #include "components/sessions/content/session_tab_helper.h" | |
| #include "components/strings/grit/components_strings.h" | |
| #include "components/sync_preferences/pref_service_syncable.h" | |
| #include "components/web_modal/web_contents_modal_dialog_manager.h" | |
| #include "components/zoom/page_zoom.h" | |
| #include "components/zoom/zoom_controller.h" | |
| #include "content/public/browser/browser_context.h" | |
| #include "content/public/browser/browser_thread.h" | |
| #include "content/public/browser/child_process_security_policy.h" | |
| #include "content/public/browser/color_chooser.h" | |
| #include "content/public/browser/devtools_agent_host.h" | |
| #include "content/public/browser/devtools_manager_delegate.h" | |
| #include "content/public/browser/file_select_listener.h" | |
| #include "content/public/browser/keyboard_event_processing_result.h" | |
| #include "content/public/browser/navigation_controller.h" | |
| #include "content/public/browser/navigation_entry.h" | |
| #include "content/public/browser/navigation_handle.h" | |
| #include "content/public/browser/navigation_throttle.h" | |
| #include "content/public/browser/render_frame_host.h" | |
| #include "content/public/browser/render_process_host.h" | |
| #include "content/public/browser/render_widget_host_view.h" | |
| #include "content/public/browser/security_principal.h" | |
| #include "content/public/browser/site_instance.h" | |
| #include "content/public/browser/web_contents.h" | |
| #include "content/public/common/content_client.h" | |
| #include "content/public/common/content_features.h" | |
| #include "content/public/common/url_constants.h" | |
| #include "extensions/common/constants.h" | |
| #include "net/cert/x509_certificate.h" | |
| #include "services/metrics/public/cpp/ukm_builders.h" | |
| #include "services/metrics/public/cpp/ukm_recorder.h" | |
| #include "services/metrics/public/cpp/ukm_source_id.h" | |
| #include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h" | |
| #include "third_party/blink/public/public_buildflags.h" | |
| #include "ui/base/l10n/l10n_util.h" | |
| #include "ui/base/page_transition_types.h" | |
| #include "ui/events/keycodes/dom/keycode_converter.h" | |
| #include "ui/events/keycodes/keyboard_code_conversion.h" | |
| #include "ui/events/keycodes/keyboard_codes.h" | |
| #include "url/origin.h" | |
| #include "base/win/windows_h_disallowed.h" | |
| #if BUILDFLAG(ENABLE_EXTENSIONS_CORE) | |
| #include "extensions/browser/view_type_utils.h" | |
| #endif | |
| #if BUILDFLAG(IS_ANDROID) | |
| #include "chrome/android/chrome_jni_headers/DevToolsActivity_jni.h" | |
| #else | |
| #include "chrome/browser/devtools/devtools_policy_dialog.h" | |
| #include "chrome/browser/devtools/devtools_ui_controller.h" | |
| #include "chrome/browser/ui/browser_window.h" | |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" // nogncheck crbug.com/40147906 | |
| #include "chrome/browser/ui/browser_window/public/global_browser_collection.h" // nogncheck crbug.com/40147906 | |
| #include "chrome/browser/ui/navigator/browser_navigator.h" | |
| #include "chrome/browser/ui/navigator/browser_navigator_params.h" | |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" | |
| #include "chrome/browser/ui/web_modal/browser_window_modal_dialog_delegate.h" // nogncheck crbug.com/40147906 | |
| #include "components/keep_alive_registry/keep_alive_types.h" | |
| #include "components/keep_alive_registry/scoped_keep_alive.h" | |
| #endif | |
| using blink::WebInputEvent; | |
| using content::BrowserThread; | |
| using content::DevToolsAgentHost; | |
| using content::GlobalRenderFrameHostId; | |
| using content::WebContents; | |
| namespace { | |
| typedef std::vector<DevToolsWindow*> DevToolsWindows; | |
| DevToolsWindows& GetDevToolsWindowInstances() { | |
| static base::NoDestructor<DevToolsWindows> instances; | |
| return *instances; | |
| } | |
| std::vector<base::RepeatingCallback<void(DevToolsWindow*)>>& | |
| GetCreationCallbacks() { | |
| static base::NoDestructor< | |
| std::vector<base::RepeatingCallback<void(DevToolsWindow*)>>> | |
| creation_callbacks; | |
| return *creation_callbacks; | |
| } | |
| static const char kKeyUpEventName[] = "keyup"; | |
| static const char kKeyDownEventName[] = "keydown"; | |
| static const char kDefaultFrontendURL[] = | |
| "devtools://devtools/bundled/devtools_app.html"; | |
| static const char kNodeFrontendURL[] = | |
| "devtools://devtools/bundled/node_app.html"; | |
| static const char kWorkerFrontendURL[] = | |
| "devtools://devtools/bundled/worker_app.html"; | |
| static const char kJSFrontendURL[] = "devtools://devtools/bundled/js_app.html"; | |
| static const char kFallbackFrontendURL[] = | |
| "devtools://devtools/bundled/inspector.html"; | |
| void SetPreferencesFromJson(Profile* profile, const std::string& json) { | |
| std::optional<base::DictValue> parsed = | |
| base::JSONReader::ReadDict(json, base::JSON_PARSE_CHROMIUM_EXTENSIONS); | |
| if (!parsed) { | |
| return; | |
| } | |
| ScopedDictPrefUpdate update(profile->GetPrefs(), prefs::kDevToolsPreferences); | |
| for (auto dict_value : *parsed) { | |
| if (!dict_value.second.is_string()) { | |
| continue; | |
| } | |
| update->Set(dict_value.first, std::move(dict_value.second)); | |
| } | |
| } | |
| #if !BUILDFLAG(IS_ANDROID) | |
| // Returns true if `reason` comes from an enterprise policy check rather than a | |
| // non-policy UI restriction (like kiosk mode or a captive portal sign-in | |
| // window). | |
| bool IsPolicyBlockReason(DevToolsBlockReason reason) { | |
| switch (reason) { | |
| case DevToolsBlockReason::kUrlAllowlistOrBlocklist: | |
| case DevToolsBlockReason::kPolicyDisallowed: | |
| case DevToolsBlockReason::kForceInstalledExtension: | |
| case DevToolsBlockReason::kComponentExtensionInManagedProfile: | |
| case DevToolsBlockReason::kKioskWebApp: | |
| case DevToolsBlockReason::kPolicyInstalledIsolatedWebApp: | |
| case DevToolsBlockReason::kDevToolsUrlBlocklisted: | |
| return true; | |
| case DevToolsBlockReason::kKioskMode: | |
| case DevToolsBlockReason::kCaptivePortalSignin: | |
| return false; | |
| case DevToolsBlockReason::kNotBlocked: | |
| NOTREACHED(); | |
| } | |
| NOTREACHED(); | |
| } | |
| // Records whether an attempt to open a new DevTools window for a tab was | |
| // allowed by enterprise policy checks. | |
| // "DevTools.OpenAttempt.AllowedByPolicy" counts every policy-evaluated attempt | |
| // across all profiles. The ".OnProfileWithPolicyCheck" variant is emitted when | |
| // at least one policy check can affect DevTools on `profile` (see | |
| // `HasDevToolsPolicyCheck()`), providing a non-diluted rate denominator to | |
| // detect spikes or drops in blocks. | |
| void RecordDevToolsOpenAttemptAllowedByPolicy(Profile* profile, bool allowed) { | |
| CHECK(profile); | |
| base::UmaHistogramBoolean("DevTools.OpenAttempt.AllowedByPolicy", allowed); | |
| if (HasDevToolsPolicyCheck(profile)) { | |
| base::UmaHistogramBoolean( | |
| "DevTools.OpenAttempt.AllowedByPolicy.OnProfileWithPolicyCheck", | |
| allowed); | |
| } | |
| } | |
| #endif // !BUILDFLAG(IS_ANDROID) | |
| // DevToolsToolboxDelegate ---------------------------------------------------- | |
| class DevToolsToolboxDelegate : public content::WebContentsObserver, | |
| public content::WebContentsDelegate { | |
| public: | |
| DevToolsToolboxDelegate(WebContents* toolbox_contents, | |
| WebContents* inspected_web_contents); | |
| DevToolsToolboxDelegate(const DevToolsToolboxDelegate&) = delete; | |
| DevToolsToolboxDelegate& operator=(const DevToolsToolboxDelegate&) = delete; | |
| ~DevToolsToolboxDelegate() override; | |
| content::WebContents* OpenURLFromTab( | |
| content::WebContents* source, | |
| const content::OpenURLParams& params, | |
| base::OnceCallback<void(content::NavigationHandle&)> | |
| navigation_handle_callback) override; | |
| content::KeyboardEventProcessingResult PreHandleKeyboardEvent( | |
| content::WebContents* source, | |
| const input::NativeWebKeyboardEvent& event) override; | |
| bool HandleKeyboardEvent(content::WebContents* source, | |
| const input::NativeWebKeyboardEvent& event) override; | |
| void WebContentsDestroyed() override; | |
| private: | |
| #if !BUILDFLAG(IS_ANDROID) | |
| BrowserWindow* GetInspectedBrowserWindow(); | |
| #endif | |
| base::WeakPtr<content::WebContents> inspected_web_contents_; | |
| }; | |
| DevToolsToolboxDelegate::DevToolsToolboxDelegate(WebContents* toolbox_contents, | |
| WebContents* web_contents) | |
| : WebContentsObserver(toolbox_contents), | |
| inspected_web_contents_(web_contents ? web_contents->GetWeakPtr() | |
| : nullptr) {} | |
| DevToolsToolboxDelegate::~DevToolsToolboxDelegate() = default; | |
| content::WebContents* DevToolsToolboxDelegate::OpenURLFromTab( | |
| content::WebContents* source, | |
| const content::OpenURLParams& params, | |
| base::OnceCallback<void(content::NavigationHandle&)> | |
| navigation_handle_callback) { | |
| CHECK(source == web_contents(), base::NotFatalUntil::M161); | |
| if (!params.url.SchemeIs(content::kChromeDevToolsScheme)) { | |
| return nullptr; | |
| } | |
| base::WeakPtr<content::NavigationHandle> navigation_handle = | |
| source->GetController().LoadURLWithParams( | |
| content::NavigationController::LoadURLParams(params)); | |
| if (navigation_handle_callback && navigation_handle) { | |
| std::move(navigation_handle_callback).Run(*navigation_handle); | |
| } | |
| return source; | |
| } | |
| content::KeyboardEventProcessingResult | |
| DevToolsToolboxDelegate::PreHandleKeyboardEvent( | |
| content::WebContents* source, | |
| const input::NativeWebKeyboardEvent& event) { | |
| #if !BUILDFLAG(IS_ANDROID) | |
| BrowserWindow* window = GetInspectedBrowserWindow(); | |
| if (window) { | |
| return window->PreHandleKeyboardEvent(event); | |
| } | |
| #endif | |
| return content::KeyboardEventProcessingResult::NOT_HANDLED; | |
| } | |
| bool DevToolsToolboxDelegate::HandleKeyboardEvent( | |
| content::WebContents* source, | |
| const input::NativeWebKeyboardEvent& event) { | |
| #if BUILDFLAG(IS_ANDROID) | |
| return false; | |
| #else | |
| if (event.windows_key_code == 0x08) { | |
| // Do not navigate back in history on Windows (http://crbug.com/40529649). | |
| return false; | |
| } | |
| BrowserWindow* window = GetInspectedBrowserWindow(); | |
| return window && window->HandleKeyboardEvent(event); | |
| #endif | |
| } | |
| void DevToolsToolboxDelegate::WebContentsDestroyed() { | |
| delete this; | |
| } | |
| #if !BUILDFLAG(IS_ANDROID) | |
| BrowserWindow* DevToolsToolboxDelegate::GetInspectedBrowserWindow() { | |
| if (!inspected_web_contents_) { | |
| return nullptr; | |
| } | |
| BrowserWindowInterface* browser = | |
| GlobalBrowserCollection::GetInstance()->FindBrowserWithTab( | |
| inspected_web_contents_.get()); | |
| return browser ? BrowserWindow::FromBrowser(browser) : nullptr; | |
| } | |
| #endif | |
| // static | |
| GURL DecorateFrontendURL(const GURL& base_url) { | |
| std::string frontend_url = base_url.spec(); | |
| base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); | |
| if (command_line->HasSwitch(switches::kDevToolsFlags)) { | |
| frontend_url = frontend_url + | |
| ((frontend_url.find("?") == std::string::npos) ? "?" : "&") + | |
| command_line->GetSwitchValueASCII(switches::kDevToolsFlags); | |
| } | |
| if (command_line->HasSwitch(switches::kCustomDevtoolsFrontend)) { | |
| frontend_url = frontend_url + | |
| ((frontend_url.find("?") == std::string::npos) ? "?" : "&") + | |
| "debugFrontend=true"; | |
| } | |
| return GURL(frontend_url); | |
| } | |
| } // namespace | |
| // DevToolsEventForwarder ----------------------------------------------------- | |
| class DevToolsEventForwarder { | |
| public: | |
| explicit DevToolsEventForwarder(DevToolsWindow* window) | |
| : devtools_window_(window) {} | |
| DevToolsEventForwarder(const DevToolsEventForwarder&) = delete; | |
| DevToolsEventForwarder& operator=(const DevToolsEventForwarder&) = delete; | |
| // Registers whitelisted shortcuts with the forwarder. | |
| // Only registered keys will be forwarded to the DevTools frontend. | |
| void SetWhitelistedShortcuts(const std::string& message); | |
| // Forwards a keyboard event to the DevTools frontend if it is whitelisted. | |
| // Returns |true| if the event has been forwarded, |false| otherwise. | |
| bool ForwardEvent(const input::NativeWebKeyboardEvent& event); | |
| private: | |
| static bool KeyWhitelistingAllowed(int key_code, int modifiers); | |
| static int CombineKeyCodeAndModifiers(int key_code, int modifiers); | |
| raw_ptr<DevToolsWindow> devtools_window_; | |
| std::set<int> whitelisted_keys_; | |
| }; | |
| void DevToolsEventForwarder::SetWhitelistedShortcuts( | |
| const std::string& message) { | |
| std::optional<base::Value> parsed_message = | |
| base::JSONReader::Read(message, base::JSON_PARSE_CHROMIUM_EXTENSIONS); | |
| if (!parsed_message || !parsed_message->is_list()) { | |
| return; | |
| } | |
| for (const auto& list_item : parsed_message->GetList()) { | |
| if (!list_item.is_dict()) { | |
| continue; | |
| } | |
| int key_code = list_item.GetDict().FindInt("keyCode").value_or(0); | |
| if (key_code == 0) { | |
| continue; | |
| } | |
| int modifiers = list_item.GetDict().FindInt("modifiers").value_or(0); | |
| if (!KeyWhitelistingAllowed(key_code, modifiers)) { | |
| LOG(WARNING) << "Key whitelisting forbidden: " | |
| << "(" << key_code << "," << modifiers << ")"; | |
| continue; | |
| } | |
| whitelisted_keys_.insert(CombineKeyCodeAndModifiers(key_code, modifiers)); | |
| } | |
| } | |
| bool DevToolsEventForwarder::ForwardEvent( | |
| const input::NativeWebKeyboardEvent& event) { | |
| std::string event_type; | |
| switch (event.GetType()) { | |
| case WebInputEvent::Type::kKeyDown: | |
| case WebInputEvent::Type::kRawKeyDown: | |
| event_type = kKeyDownEventName; | |
| break; | |
| case WebInputEvent::Type::kKeyUp: | |
| event_type = kKeyUpEventName; | |
| break; | |
| default: | |
| return false; | |
| } | |
| int key_code = ui::LocatedToNonLocatedKeyboardCode( | |
| static_cast<ui::KeyboardCode>(event.windows_key_code)); | |
| int modifiers = event.GetModifiers() & | |
| (WebInputEvent::kShiftKey | WebInputEvent::kControlKey | | |
| WebInputEvent::kAltKey | WebInputEvent::kMetaKey); | |
| int key = CombineKeyCodeAndModifiers(key_code, modifiers); | |
| if (whitelisted_keys_.find(key) == whitelisted_keys_.end()) { | |
| return false; | |
| } | |
| base::DictValue event_data; | |
| event_data.Set("type", event_type); | |
| event_data.Set("key", ui::KeycodeConverter::DomKeyToKeyString( | |
| ui::DomKey(event.dom_key))); | |
| event_data.Set("code", ui::KeycodeConverter::DomCodeToCodeString( | |
| static_cast<ui::DomCode>(event.dom_code))); | |
| event_data.Set("keyCode", key_code); | |
| event_data.Set("modifiers", modifiers); | |
| devtools_window_->bindings_->CallClientMethod( | |
| "DevToolsAPI", "keyEventUnhandled", base::Value(std::move(event_data))); | |
| return true; | |
| } | |
| int DevToolsEventForwarder::CombineKeyCodeAndModifiers(int key_code, | |
| int modifiers) { | |
| return key_code | (modifiers << 16); | |
| } | |
| bool DevToolsEventForwarder::KeyWhitelistingAllowed(int key_code, | |
| int modifiers) { | |
| return (ui::VKEY_F1 <= key_code && key_code <= ui::VKEY_F12) || | |
| modifiers != 0; | |
| } | |
| void DevToolsWindow::OpenNodeFrontend() { | |
| DevToolsWindow::OpenNodeFrontendWindow( | |
| profile_, DevToolsOpenedByAction::kOpenForNodeFromAnotherTarget); | |
| } | |
| // DevToolsWindow::Throttle ------------------------------------------ | |
| class DevToolsWindow::Throttle : public content::NavigationThrottle { | |
| public: | |
| Throttle(content::NavigationThrottleRegistry& registry, | |
| DevToolsWindow* devtools_window) | |
| : content::NavigationThrottle(registry), | |
| devtools_window_(devtools_window) { | |
| devtools_window_->throttle_ = this; | |
| } | |
| Throttle(const Throttle&) = delete; | |
| Throttle& operator=(const Throttle&) = delete; | |
| ~Throttle() override { | |
| if (devtools_window_) { | |
| devtools_window_->throttle_ = nullptr; | |
| } | |
| } | |
| // content::NavigationThrottle implementation: | |
| NavigationThrottle::ThrottleCheckResult WillStartRequest() override { | |
| return DEFER; | |
| } | |
| const char* GetNameForLogging() override { return "DevToolsWindowThrottle"; } | |
| void ResumeThrottle() { | |
| if (devtools_window_) { | |
| devtools_window_->throttle_ = nullptr; | |
| devtools_window_ = nullptr; | |
| } | |
| Resume(); | |
| } | |
| private: | |
| raw_ptr<DevToolsWindow> devtools_window_; | |
| }; | |
| void DevToolsWindow::MainWebContentsObserver::RenderFrameHostChanged( | |
| content::RenderFrameHost* old_frame, | |
| content::RenderFrameHost* new_frame) { | |
| window_->MainWebContentRenderFrameHostChanged(old_frame, new_frame); | |
| } | |
| DevToolsWindow::MainWebContentsObserver::~MainWebContentsObserver() = default; | |
| // Helper class that holds the owned main WebContents for the docked | |
| // devtools window and maintains a keepalive object that keeps the browser | |
| // main loop alive long enough for the WebContents to clean up properly. | |
| class DevToolsWindow::OwnedMainWebContents { | |
| public: | |
| explicit OwnedMainWebContents( | |
| std::unique_ptr<content::WebContents> web_contents) | |
| : | |
| #if !BUILDFLAG(IS_ANDROID) | |
| keep_alive_(KeepAliveOrigin::DEVTOOLS_WINDOW, | |
| KeepAliveRestartOption::DISABLED), | |
| #endif | |
| web_contents_(std::move(web_contents)) { | |
| Profile* profile = GetProfileForDevToolsWindow(web_contents_.get()); | |
| CHECK(profile, base::NotFatalUntil::M161); | |
| if (!profile->IsOffTheRecord()) { | |
| // ScopedProfileKeepAlive does not support OTR profiles. | |
| profile_keep_alive_ = std::make_unique<ScopedProfileKeepAlive>( | |
| profile, ProfileKeepAliveOrigin::kDevToolsWindow); | |
| } | |
| } | |
| static std::unique_ptr<content::WebContents> TakeWebContents( | |
| std::unique_ptr<OwnedMainWebContents> instance) { | |
| return std::move(instance->web_contents_); | |
| } | |
| private: | |
| #if !BUILDFLAG(IS_ANDROID) | |
| ScopedKeepAlive keep_alive_; | |
| #endif | |
| std::unique_ptr<ScopedProfileKeepAlive> profile_keep_alive_; | |
| std::unique_ptr<content::WebContents> web_contents_; | |
| }; | |
| // DevToolsWindow ------------------------------------------------------------- | |
| const char DevToolsWindow::kDevToolsApp[] = "DevToolsApp"; | |
| // static | |
| void DevToolsWindow::AddCreationCallbackForTest( | |
| const CreationCallback& callback) { | |
| GetCreationCallbacks().push_back(callback); | |
| } | |
| // static | |
| void DevToolsWindow::RemoveCreationCallbackForTest( | |
| const CreationCallback& callback) { | |
| if (auto it = std::ranges::find(GetCreationCallbacks(), callback); | |
| it != GetCreationCallbacks().end()) { | |
| GetCreationCallbacks().erase(it); | |
| } | |
| } | |
| DevToolsWindow::~DevToolsWindow() { | |
| if (throttle_) { | |
| throttle_->ResumeThrottle(); | |
| } | |
| life_stage_ = kClosing; | |
| base::RecordAction(base::UserMetricsAction("DevTools_Close")); | |
| UpdateBrowserWindow(); | |
| UpdateBrowserToolbar(); | |
| RemoveSharingInfoBar(); | |
| capture_handle_.RunAndReset(); | |
| owned_toolbox_web_contents_.reset(); | |
| #if !BUILDFLAG(IS_ANDROID) | |
| browser_collection_observation_.Reset(); | |
| #endif | |
| #if BUILDFLAG(IS_MAC) | |
| // Activate the inspected browser when undocked DevTools closes, so focus | |
| // returns to the window that opened DevTools (e.g., a PWA window). | |
| if (!is_docked_ && inspected_browser_session_id_.is_valid()) { | |
| BrowserWindowInterface* const browser = | |
| GlobalBrowserCollection::GetInstance()->FindBrowserWithID( | |
| inspected_browser_session_id_); | |
| if (browser && browser->GetWindow()) { | |
| browser->GetWindow()->Activate(); | |
| } | |
| } | |
| #endif // BUILDFLAG(IS_MAC) | |
| DevToolsWindows& instances = GetDevToolsWindowInstances(); | |
| auto it = std::ranges::find(instances, this); | |
| CHECK(it != instances.end()); | |
| instances.erase(it); | |
| if (!close_callback_.is_null()) { | |
| std::move(close_callback_).Run(); | |
| } | |
| // Defer deletion of the main web contents, since we could get here | |
| // via RenderFrameHostImpl method that expects WebContents to live | |
| // for some time. See http://crbug.com/41477969 for details. | |
| if (owned_main_web_contents_) { | |
| base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon( | |
| FROM_HERE, std::move(owned_main_web_contents_)); | |
| } | |
| // If window gets destroyed during a test run, need to stop the test. | |
| if (!ready_for_test_callback_.is_null()) { | |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( | |
| FROM_HERE, std::move(ready_for_test_callback_)); | |
| } | |
| } | |
| // static | |
| void DevToolsWindow::RegisterProfilePrefs( | |
| user_prefs::PrefRegistrySyncable* registry) { | |
| registry->RegisterDictionaryPref(prefs::kDevToolsEditedFiles); | |
| registry->RegisterDictionaryPref(prefs::kDevToolsFileSystemPaths); | |
| registry->RegisterStringPref(prefs::kDevToolsAdbKey, std::string()); | |
| registry->RegisterInt64Pref(prefs::kDevToolsLastOpenTimestamp, 0L); | |
| registry->RegisterBooleanPref(prefs::kDevToolsDiscoverUsbDevicesEnabled, | |
| true); | |
| registry->RegisterBooleanPref(prefs::kDevToolsPortForwardingEnabled, false); | |
| registry->RegisterBooleanPref(prefs::kDevToolsPortForwardingDefaultSet, | |
| false); | |
| registry->RegisterDictionaryPref(prefs::kDevToolsPortForwardingConfig); | |
| registry->RegisterBooleanPref(prefs::kDevToolsDiscoverTCPTargetsEnabled, | |
| true); | |
| registry->RegisterListPref(prefs::kDevToolsTCPDiscoveryConfig); | |
| registry->RegisterDictionaryPref(prefs::kDevToolsPreferences); | |
| registry->RegisterBooleanPref( | |
| prefs::kDevToolsSyncPreferences, | |
| DevToolsSettings::kSyncDevToolsPreferencesDefault, | |
| user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); | |
| registry->RegisterDictionaryPref( | |
| prefs::kDevToolsSyncedPreferencesSyncEnabled, | |
| user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); | |
| registry->RegisterDictionaryPref( | |
| prefs::kDevToolsSyncedPreferencesSyncDisabled); | |
| registry->RegisterIntegerPref( | |
| prefs::kDevToolsGenAiSettings, | |
| static_cast<int>(DevToolsGenAiEnterprisePolicyValue::kAllow)); | |
| registry->RegisterIntegerPref( | |
| prefs::kDevToolsGoogleDeveloperProgramProfileAvailability, | |
| /* enabled */ 0); | |
| } | |
| // static | |
| content::WebContents* DevToolsWindow::GetInTabWebContents( | |
| WebContents* inspected_web_contents, | |
| DevToolsContentsResizingStrategy* out_strategy) { | |
| DevToolsWindow* window = | |
| GetInstanceForInspectedWebContents(inspected_web_contents); | |
| if (!window || window->life_stage_ == kClosing) { | |
| return nullptr; | |
| } | |
| // Not yet loaded window is treated as docked, but we should not present it | |
| // until we decided on docking. | |
| bool is_docked_set = window->life_stage_ == kLoadCompleted || | |
| window->life_stage_ == kIsDockedSet; | |
| if (!is_docked_set) { | |
| return nullptr; | |
| } | |
| // Undocked window should have toolbox web contents. | |
| if (!window->is_docked_ && !window->toolbox_web_contents_) { | |
| return nullptr; | |
| } | |
| if (out_strategy) { | |
| out_strategy->CopyFrom(window->contents_resizing_strategy_); | |
| } | |
| return window->is_docked_ ? window->main_web_contents_.get() | |
| : window->toolbox_web_contents_.get(); | |
| } | |
| // static | |
| DevToolsWindow* DevToolsWindow::GetInstanceForInspectedWebContents( | |
| WebContents* inspected_web_contents) { | |
| if (!inspected_web_contents) { | |
| return nullptr; | |
| } | |
| for (auto& instance : GetDevToolsWindowInstances()) { | |
| if (instance->GetInspectedWebContents() == inspected_web_contents && | |
| instance->life_stage_ != kClosing) { | |
| return instance; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| // static | |
| bool DevToolsWindow::IsDevToolsWindow(content::WebContents* web_contents) { | |
| if (!web_contents) { | |
| return false; | |
| } | |
| for (auto& instance : GetDevToolsWindowInstances()) { | |
| if (instance->main_web_contents_ == web_contents || | |
| instance->toolbox_web_contents_ == web_contents) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // static | |
| void DevToolsWindow::OpenDevToolsWindowForWorker( | |
| Profile* profile, | |
| const scoped_refptr<DevToolsAgentHost>& worker_agent, | |
| DevToolsOpenedByAction opened_by) { | |
| DevToolsWindow* window = FindDevToolsWindow(worker_agent.get()); | |
| if (!window) { | |
| base::RecordAction(base::UserMetricsAction("DevTools_InspectWorker")); | |
| window = Create(profile, nullptr, kFrontendWorker, std::string(), false, "", | |
| "", worker_agent->IsAttached(), | |
| /* browser_connection */ true, opened_by); | |
| if (!window) { | |
| return; | |
| } | |
| window->bindings_->AttachViaBrowserTarget(worker_agent); | |
| } | |
| window->ScheduleShow(DevToolsToggleAction::Show()); | |
| } | |
| // static | |
| void DevToolsWindow::OpenDevToolsWindow( | |
| content::WebContents* inspected_web_contents, | |
| DevToolsOpenedByAction opened_by) { | |
| ToggleDevToolsWindow(inspected_web_contents, nullptr, true, | |
| DevToolsToggleAction::Show(), "", opened_by); | |
| } | |
| // static | |
| void DevToolsWindow::OpenDevToolsWindow( | |
| content::WebContents* inspected_web_contents, | |
| Profile* profile, | |
| DevToolsOpenedByAction opened_by, | |
| const content::DevToolsManagerDelegate::DevToolsOptions& devtools_options) { | |
| ToggleDevToolsWindow(inspected_web_contents, profile, true, | |
| DevToolsToggleAction::Show(), "", opened_by, | |
| devtools_options); | |
| } | |
| // static | |
| void DevToolsWindow::OpenDevToolsWindow( | |
| scoped_refptr<content::DevToolsAgentHost> agent_host, | |
| Profile* profile, | |
| DevToolsOpenedByAction opened_by, | |
| const content::DevToolsManagerDelegate::DevToolsOptions& devtools_options) { | |
| OpenDevToolsWindow(agent_host, profile, false /* use_bundled_frontend */, | |
| opened_by, devtools_options); | |
| } | |
| // static | |
| void DevToolsWindow::OpenDevToolsWindowWithBundledFrontend( | |
| scoped_refptr<content::DevToolsAgentHost> agent_host, | |
| Profile* profile, | |
| DevToolsOpenedByAction opened_by) { | |
| OpenDevToolsWindow(agent_host, profile, true /* use_bundled_frontend */, | |
| opened_by); | |
| } | |
| // static | |
| void DevToolsWindow::OpenDevToolsWindow( | |
| scoped_refptr<content::DevToolsAgentHost> agent_host, | |
| Profile* profile, | |
| bool use_bundled_frontend, | |
| DevToolsOpenedByAction opened_by, | |
| const content::DevToolsManagerDelegate::DevToolsOptions& devtools_options) { | |
| if (!profile) { | |
| profile = Profile::FromBrowserContext(agent_host->GetBrowserContext()); | |
| } | |
| if (!profile) { | |
| return; | |
| } | |
| std::string type = agent_host->GetType(); | |
| bool is_worker = type == DevToolsAgentHost::kTypeServiceWorker || | |
| type == DevToolsAgentHost::kTypeSharedWorker; | |
| if (!agent_host->GetFrontendURL().empty()) { | |
| DevToolsWindow::OpenExternalFrontend(profile, agent_host->GetFrontendURL(), | |
| agent_host, use_bundled_frontend, | |
| opened_by); | |
| return; | |
| } | |
| if (is_worker) { | |
| DevToolsWindow::OpenDevToolsWindowForWorker(profile, agent_host, opened_by); | |
| return; | |
| } | |
| if (type == content::DevToolsAgentHost::kTypeFrame) { | |
| DevToolsWindow::OpenDevToolsWindowForFrame(profile, agent_host, opened_by); | |
| return; | |
| } | |
| content::WebContents* web_contents = agent_host->GetWebContents(); | |
| if (web_contents) { | |
| DevToolsWindow::OpenDevToolsWindow(web_contents, profile, opened_by, | |
| devtools_options); | |
| } | |
| } | |
| // static | |
| void DevToolsWindow::OpenDevToolsWindow( | |
| content::WebContents* inspected_web_contents, | |
| const DevToolsToggleAction& action, | |
| DevToolsOpenedByAction opened_by) { | |
| ToggleDevToolsWindow(inspected_web_contents, nullptr, true, action, "", | |
| opened_by); | |
| } | |
| // static | |
| void DevToolsWindow::OpenDevToolsWindowForFrame( | |
| Profile* profile, | |
| const scoped_refptr<content::DevToolsAgentHost>& agent_host, | |
| DevToolsOpenedByAction opened_by) { | |
| DevToolsWindow* window = FindDevToolsWindow(agent_host.get()); | |
| if (!window) { | |
| window = DevToolsWindow::Create( | |
| profile, nullptr, kFrontendDefault, std::string(), false, std::string(), | |
| std::string(), agent_host->IsAttached(), false, opened_by); | |
| if (!window) { | |
| return; | |
| } | |
| window->bindings_->AttachTo(agent_host); | |
| } | |
| window->ScheduleShow(DevToolsToggleAction::Show()); | |
| } | |
| // static | |
| void DevToolsWindow::OpenExternalFrontend( | |
| Profile* profile, | |
| const std::string& frontend_url, | |
| const scoped_refptr<content::DevToolsAgentHost>& agent_host, | |
| bool use_bundled_frontend, | |
| DevToolsOpenedByAction opened_by) { | |
| DevToolsWindow* window = FindDevToolsWindow(agent_host.get()); | |
| if (window) { | |
| window->ScheduleShow(DevToolsToggleAction::Show()); | |
| return; | |
| } | |
| std::string type = agent_host->GetType(); | |
| if (type == "node") { | |
| // Direct node targets will always open using ToT front-end. | |
| window = Create(profile, nullptr, kFrontendV8, std::string(), false, | |
| std::string(), std::string(), agent_host->IsAttached(), | |
| /* browser_connection */ false, opened_by); | |
| } else { | |
| bool is_worker = type == DevToolsAgentHost::kTypeServiceWorker || | |
| type == DevToolsAgentHost::kTypeSharedWorker; | |
| FrontendType frontend_type = | |
| is_worker ? kFrontendRemoteWorker | |
| : (type == "tab" ? kFrontendRemoteTab : kFrontendRemote); | |
| std::string effective_frontend_url = | |
| use_bundled_frontend ? kFallbackFrontendURL | |
| : DevToolsUI::GetProxyURL(frontend_url).spec(); | |
| window = | |
| Create(profile, nullptr, frontend_type, effective_frontend_url, false, | |
| std::string(), std::string(), agent_host->IsAttached(), | |
| /* browser_connection */ false, opened_by); | |
| } | |
| if (!window) { | |
| return; | |
| } | |
| window->bindings_->AttachTo(agent_host); | |
| window->close_on_detach_ = false; | |
| window->ScheduleShow(DevToolsToggleAction::Show()); | |
| } | |
| // static | |
| DevToolsWindow* DevToolsWindow::OpenNodeFrontendWindow( | |
| Profile* profile, | |
| DevToolsOpenedByAction opened_by) { | |
| for (DevToolsWindow* window : GetDevToolsWindowInstances()) { | |
| if (window->frontend_type_ == kFrontendNode) { | |
| window->ActivateWindow(); | |
| return window; | |
| } | |
| } | |
| DevToolsWindow* window = Create( | |
| profile, nullptr, kFrontendNode, std::string(), false, std::string(), | |
| std::string(), false, /* browser_connection */ false, opened_by); | |
| if (!window) { | |
| return nullptr; | |
| } | |
| window->bindings_->AttachTo(DevToolsAgentHost::CreateForDiscovery()); | |
| window->ScheduleShow(DevToolsToggleAction::Show()); | |
| return window; | |
| } | |
| // static | |
| Profile* DevToolsWindow::GetProfileForDevToolsWindow( | |
| content::WebContents* web_contents) { | |
| Profile* profile = | |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()); | |
| if (profile->IsPrimaryOTRProfile() || profile->IsDevToolsOTRProfile()) { | |
| return profile; | |
| } | |
| return profile->GetOriginalProfile(); | |
| } | |
| // static | |
| void DevToolsWindow::ToggleDevToolsWindow( | |
| content::WebContents* inspected_web_contents, | |
| Profile* profile, | |
| bool force_open, | |
| const DevToolsToggleAction& action, | |
| const std::string& settings, | |
| DevToolsOpenedByAction toggled_by, | |
| const content::DevToolsManagerDelegate::DevToolsOptions& devtools_options) { | |
| scoped_refptr<DevToolsAgentHost> agent( | |
| DevToolsAgentHost::GetOrCreateForTab(inspected_web_contents)); | |
| DevToolsWindow* window = FindDevToolsWindow(agent.get()); | |
| bool do_open = force_open; | |
| if (!window) { | |
| if (!profile) { | |
| profile = GetProfileForDevToolsWindow(inspected_web_contents); | |
| } | |
| base::RecordAction(base::UserMetricsAction("DevTools_InspectRenderer")); | |
| std::string panel; | |
| switch (action.type()) { | |
| case DevToolsToggleAction::kInspect: | |
| case DevToolsToggleAction::kShowElementsPanel: | |
| panel = "elements"; | |
| break; | |
| case DevToolsToggleAction::kShowConsolePanel: | |
| panel = "console"; | |
| break; | |
| case DevToolsToggleAction::kPauseInDebugger: | |
| panel = "sources"; | |
| break; | |
| case DevToolsToggleAction::kShow: | |
| if (devtools_options.panel_id.has_value()) { | |
| panel = devtools_options.panel_id.value(); | |
| } | |
| break; | |
| case DevToolsToggleAction::kToggle: | |
| case DevToolsToggleAction::kReveal: | |
| case DevToolsToggleAction::kNoOp: | |
| break; | |
| } | |
| #if !BUILDFLAG(IS_ANDROID) | |
| const DevToolsBlockReason block_reason = | |
| GetBlockReasonFor(profile, inspected_web_contents); | |
| if (block_reason != DevToolsBlockReason::kNotBlocked) { | |
| // `Show()` is a no-op (and returns false) while a dialog is already open | |
| // for this tab, so repeated attempts are not counted twice. Non-policy UI | |
| // restrictions (e.g. kiosk mode) still show the dialog, but are excluded | |
| // from `AllowedByPolicy` so they are not misattributed to policy. | |
| if (DevToolsPolicyDialog::Show(inspected_web_contents) && | |
| IsPolicyBlockReason(block_reason)) { | |
| RecordDevToolsOpenAttemptAllowedByPolicy(profile, /*allowed=*/false); | |
| } | |
| return; | |
| } | |
| #endif | |
| window = Create(profile, inspected_web_contents, kFrontendDefault, | |
| std::string(), true, settings, panel, agent->IsAttached(), | |
| /* browser_connection */ false, toggled_by); | |
| if (!window) { | |
| // Not policy related: no `DevToolsUIBindings` for the frontend. | |
| return; | |
| } | |
| #if !BUILDFLAG(IS_ANDROID) | |
| RecordDevToolsOpenAttemptAllowedByPolicy(profile, /*allowed=*/true); | |
| #endif | |
| window->bindings_->AttachTo(agent.get()); | |
| do_open = true; | |
| if (toggled_by != DevToolsOpenedByAction::kUnknown) { | |
| LogDevToolsOpenedByAction(toggled_by); | |
| LogDevToolsOpenedUKM(inspected_web_contents); | |
| } | |
| } | |
| // Update toolbar to reflect DevTools changes. | |
| window->UpdateBrowserToolbar(); | |
| // If window is docked and visible, we hide it on toggle. If window is | |
| // undocked, we show (activate) it. | |
| if (!window->is_docked_ || do_open) { | |
| window->ScheduleShow(action); | |
| } else { | |
| DevToolsClosedByAction closed_by; | |
| switch (toggled_by) { | |
| case DevToolsOpenedByAction::kMainMenuOrMainShortcut: | |
| closed_by = DevToolsClosedByAction::kMainMenuOrMainShortcut; | |
| break; | |
| case DevToolsOpenedByAction::kToggleShortcut: | |
| closed_by = DevToolsClosedByAction::kToggleShortcut; | |
| break; | |
| case DevToolsOpenedByAction::kPinnedToolbarButton: | |
| closed_by = DevToolsClosedByAction::kPinnedToolbarButton; | |
| break; | |
| default: | |
| closed_by = DevToolsClosedByAction::kUnknown; | |
| break; | |
| } | |
| window->Close(closed_by); | |
| } | |
| } | |
| // static | |
| void DevToolsWindow::InspectElement( | |
| content::RenderFrameHost* inspected_frame_host, | |
| int x, | |
| int y) { | |
| WebContents* web_contents = | |
| WebContents::FromRenderFrameHost(inspected_frame_host); | |
| scoped_refptr<DevToolsAgentHost> agent( | |
| DevToolsAgentHost::GetOrCreateForTab(web_contents)); | |
| agent->InspectElement(inspected_frame_host, x, y); | |
| bool should_measure_time = !FindDevToolsWindow(agent.get()); | |
| base::TimeTicks start_time = base::TimeTicks::Now(); | |
| // TODO(loislo): we should initiate DevTools window opening from within | |
| // renderer. Otherwise, we still can hit a race condition here. | |
| OpenDevToolsWindow(web_contents, DevToolsToggleAction::ShowElementsPanel(), | |
| DevToolsOpenedByAction::kContextMenuInspect); | |
| DevToolsWindow* window = FindDevToolsWindow(agent.get()); | |
| if (window && should_measure_time) { | |
| window->inspect_element_start_time_ = start_time; | |
| } | |
| } | |
| // static | |
| void DevToolsWindow::LogDevToolsOpenedByAction( | |
| DevToolsOpenedByAction opened_by) { | |
| base::UmaHistogramEnumeration("DevTools.OpenedByAction", opened_by); | |
| } | |
| // static | |
| void DevToolsWindow::LogDevToolsOpenedUKM(content::WebContents* web_contents) { | |
| ukm::SourceId source_id = | |
| web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId(); | |
| ukm::builders::DevTools_Opened(source_id).SetHasOccurred(true).Record( | |
| ukm::UkmRecorder::Get()); | |
| } | |
| // static | |
| void DevToolsWindow::MaybeCreateAndAddNavigationThrottle( | |
| content::NavigationThrottleRegistry& registry) { | |
| WebContents* web_contents = registry.GetNavigationHandle().GetWebContents(); | |
| if (!web_contents || !web_contents->HasLiveOriginalOpenerChain() || | |
| !web_contents->GetController() | |
| .GetLastCommittedEntry() | |
| ->IsInitialEntry()) { | |
| return; | |
| } | |
| WebContents* opener = registry.GetNavigationHandle() | |
| .GetWebContents() | |
| ->GetFirstWebContentsInLiveOriginalOpenerChain(); | |
| DevToolsWindow* window = GetInstanceForInspectedWebContents(opener); | |
| if (!window || !window->open_new_window_for_popups_ || | |
| GetInstanceForInspectedWebContents(web_contents)) { | |
| return; | |
| } | |
| DevToolsWindow::OpenDevToolsWindow( | |
| web_contents, DevToolsOpenedByAction::kAutomaticForNewTarget); | |
| window = GetInstanceForInspectedWebContents(web_contents); | |
| if (!window) { | |
| return; | |
| } | |
| registry.AddThrottle(std::make_unique<Throttle>(registry, window)); | |
| } | |
| void DevToolsWindow::ScheduleShow(const DevToolsToggleAction& action) { | |
| if (life_stage_ == kLoadCompleted) { | |
| Show(action); | |
| return; | |
| } | |
| // Action will be done only after load completed. | |
| action_on_load_ = action; | |
| if (!can_dock_) { | |
| // No harm to show always-undocked window right away. | |
| is_docked_ = false; | |
| Show(DevToolsToggleAction::Show()); | |
| } | |
| } | |
| void DevToolsWindow::Show(const DevToolsToggleAction& action) { | |
| if (life_stage_ == kClosing) { | |
| return; | |
| } | |
| if (action.type() == DevToolsToggleAction::kNoOp) { | |
| return; | |
| } | |
| #if BUILDFLAG(IS_ANDROID) | |
| if (!owned_main_web_contents_ || launched_activity_) { | |
| return; | |
| } | |
| JNIEnv* env = base::android::AttachCurrentThread(); | |
| Java_DevToolsActivity_launchDevToolsActivity( | |
| env, main_web_contents_->GetJavaWebContents()); | |
| launched_activity_ = true; | |
| OverrideAndSyncDevToolsRendererPrefs(); | |
| #else | |
| if (is_docked_) { | |
| CHECK(can_dock_, base::NotFatalUntil::M161); | |
| content::WebContents* inspected_web_contents = GetInspectedWebContents(); | |
| CHECK(inspected_web_contents, base::NotFatalUntil::M161); | |
| BrowserWindowInterface* inspected_browser = | |
| GlobalBrowserCollection::GetInstance()->FindBrowserWithTab( | |
| inspected_web_contents); | |
| CHECK(inspected_browser, base::NotFatalUntil::M161); | |
| RegisterModalDialogManager(inspected_browser); | |
| // Tell inspected browser to update splitter and switch to inspected panel. | |
| ui::BaseWindow* inspected_window = inspected_browser->GetWindow(); | |
| main_web_contents_->SetDelegate(this); | |
| main_web_contents_->SetIgnoreZoomGestures(true); | |
| // Inspected tab needs to be activated, however, this cannot be done from | |
| // here because TabStripModel does not allow reentrancy for its public | |
| // methods, see http://crbug.com/452538043. | |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( | |
| FROM_HERE, base::BindOnce(&DevToolsWindow::ActivateInspectedTab, | |
| weak_factory_.GetWeakPtr())); | |
| if (auto* devtools_ui_controller = | |
| DevtoolsUIController::From(inspected_browser)) { | |
| devtools_ui_controller->UpdateDevtools(inspected_web_contents); | |
| } | |
| main_web_contents_->SetInitialFocus(); | |
| inspected_window->Show(); | |
| // On Aura, focusing once is not enough. Do it again. | |
| // Note that focusing only here but not before isn't enough either. We just | |
| // need to focus twice. | |
| main_web_contents_->SetInitialFocus(); | |
| PrefsTabHelper::CreateForWebContents(main_web_contents_); | |
| OverrideAndSyncDevToolsRendererPrefs(); | |
| MaybeShowSharedProcessInfobar(); | |
| DoAction(action); | |
| return; | |
| } | |
| // Avoid consecutive window switching if the devtools window has been opened | |
| // and the Inspect Element shortcut is pressed in the inspected tab. | |
| bool should_show_window = | |
| !browser_ || (action.type() != DevToolsToggleAction::kInspect); | |
| if (!browser_) { | |
| CreateDevToolsBrowser(); | |
| } | |
| // Ignore action if browser does not exist and could not be created. | |
| if (!browser_) { | |
| return; | |
| } | |
| RegisterModalDialogManager(browser_); | |
| MaybeShowSharedProcessInfobar(); | |
| if (should_show_window) { | |
| browser_->GetWindow()->Show(); | |
| main_web_contents_->SetInitialFocus(); | |
| } | |
| if (toolbox_web_contents_) { | |
| UpdateBrowserWindow(); | |
| } | |
| #endif | |
| DoAction(action); | |
| } | |
| #if !BUILDFLAG(IS_ANDROID) | |
| void DevToolsWindow::ActivateInspectedTab() { | |
| content::WebContents* inspected_web_contents = GetInspectedWebContents(); | |
| if (!inspected_web_contents) { | |
| return; | |
| } | |
| BrowserWindowInterface* inspected_browser = | |
| GlobalBrowserCollection::GetInstance()->FindBrowserWithTab( | |
| inspected_web_contents); | |
| if (!inspected_browser) { | |
| return; | |
| } | |
| TabStripModel* tab_strip_model = inspected_browser->GetTabStripModel(); | |
| if (tab_strip_model->GetActiveWebContents() != inspected_web_contents) { | |
| int inspected_tab_index = | |
| tab_strip_model->GetIndexOfWebContents(inspected_web_contents); | |
| CHECK_NE(TabStripModel::kNoTab, inspected_tab_index); | |
| tab_strip_model->ActivateTabAt( | |
| inspected_tab_index, | |
| TabStripUserGestureDetails( | |
| TabStripUserGestureDetails::GestureType::kOther)); | |
| } | |
| } | |
| #endif // !BUILDFLAG(IS_ANDROID) | |
| // static | |
| bool DevToolsWindow::HandleBeforeUnload(WebContents* frontend_contents, | |
| bool proceed, | |
| bool* proceed_to_fire_unload) { | |
| DevToolsWindow* window = AsDevToolsWindow(frontend_contents); | |
| if (!window) { | |
| return false; | |
| } | |
| if (!window->intercepted_page_beforeunload_) { | |
| return false; | |
| } | |
| window->BeforeUnloadFired(frontend_contents, proceed, proceed_to_fire_unload); | |
| return true; | |
| } | |
| // static | |
| bool DevToolsWindow::InterceptPageBeforeUnload(WebContents* contents) { | |
| DevToolsWindow* window = | |
| DevToolsWindow::GetInstanceForInspectedWebContents(contents); | |
| if (!window || window->intercepted_page_beforeunload_) { | |
| return false; | |
| } | |
| // Not yet loaded frontend will not handle beforeunload. | |
| if (window->life_stage_ != kLoadCompleted) { | |
| return false; | |
| } | |
| window->intercepted_page_beforeunload_ = true; | |
| // Handle case of devtools inspecting another devtools instance by passing | |
| // the call up to the inspecting devtools instance. | |
| // TODO(chrisha): Make devtools handle |auto_cancel=false| unload handler | |
| // dispatches; otherwise, discarding queries can cause unload dialogs to | |
| // pop-up for tabs with an attached devtools. | |
| if (!DevToolsWindow::InterceptPageBeforeUnload(window->main_web_contents_)) { | |
| window->main_web_contents_->DispatchBeforeUnload(false /* auto_cancel */); | |
| } | |
| return true; | |
| } | |
| // static | |
| bool DevToolsWindow::NeedsToInterceptBeforeUnload(WebContents* contents) { | |
| DevToolsWindow* window = | |
| DevToolsWindow::GetInstanceForInspectedWebContents(contents); | |
| return window && !window->intercepted_page_beforeunload_ && | |
| window->life_stage_ == kLoadCompleted; | |
| } | |
| // static | |
| void DevToolsWindow::OnPageCloseCanceled(WebContents* contents) { | |
| DevToolsWindow* window = | |
| DevToolsWindow::GetInstanceForInspectedWebContents(contents); | |
| if (!window) { | |
| return; | |
| } | |
| window->intercepted_page_beforeunload_ = false; | |
| // Propagate to devtools opened on devtools if any. | |
| DevToolsWindow::OnPageCloseCanceled(window->main_web_contents_); | |
| } | |
| DevToolsWindow::DevToolsWindow( | |
| FrontendType frontend_type, | |
| Profile* profile, | |
| std::unique_ptr<WebContents> main_web_contents, | |
| DevToolsUIBindings* bindings, | |
| WebContents* inspected_web_contents, | |
| bool can_dock, | |
| DevToolsOpenedByAction opened_by, | |
| [[maybe_unused]] SessionID inspected_browser_session_id) | |
| : frontend_type_(frontend_type), | |
| profile_(profile), | |
| main_web_contents_(main_web_contents.get()), | |
| main_web_contents_observer_(*main_web_contents_, *this), | |
| toolbox_web_contents_(nullptr), | |
| bindings_(bindings), | |
| browser_(nullptr), | |
| is_docked_(true), | |
| owned_main_web_contents_( | |
| std::make_unique<OwnedMainWebContents>(std::move(main_web_contents))), | |
| can_dock_(can_dock), | |
| close_on_detach_(true), | |
| // This initialization allows external front-end to work without changes. | |
| // We don't wait for docking call, but instead immediately show undocked. | |
| life_stage_(can_dock ? kNotLoaded : kIsDockedSet), | |
| action_on_load_(DevToolsToggleAction::NoOp()), | |
| intercepted_page_beforeunload_(false), | |
| ready_for_test_(false), | |
| opened_by_(opened_by), | |
| closed_by_(DevToolsClosedByAction::kUnknown) { | |
| #if BUILDFLAG(IS_MAC) | |
| inspected_browser_session_id_ = inspected_browser_session_id; | |
| #endif // BUILDFLAG(IS_MAC) | |
| // Set up delegate, so we get fully-functional window immediately. | |
| // It will not appear in UI though until |life_stage_ == kLoadCompleted|. | |
| main_web_contents_->SetDelegate(this); | |
| // Bindings take ownership over devtools as its delegate. | |
| bindings_->SetDelegate(this); | |
| // DevTools uses PageZoom::Zoom(), so main_web_contents_ requires a | |
| // ZoomController. | |
| zoom::ZoomController::CreateForWebContents(main_web_contents_); | |
| zoom::ZoomController::FromWebContents(main_web_contents_) | |
| ->SetShowsNotificationBubble(false); | |
| GetDevToolsWindowInstances().push_back(this); | |
| // There is no inspected_web_contents in case of various workers. | |
| if (inspected_web_contents) { | |
| Observe(inspected_web_contents); | |
| } | |
| #if BUILDFLAG(ENABLE_EXTENSIONS_CORE) | |
| extensions::SetViewType(main_web_contents_, | |
| extensions::mojom::ViewType::kDeveloperTools); | |
| #endif | |
| // Initialize docked page to be of the right size. | |
| if (can_dock_ && inspected_web_contents) { | |
| content::RenderWidgetHostView* inspected_view = | |
| inspected_web_contents->GetRenderWidgetHostView(); | |
| if (inspected_view && main_web_contents_->GetRenderWidgetHostView()) { | |
| gfx::Size size = inspected_view->GetViewBounds().size(); | |
| main_web_contents_->GetRenderWidgetHostView()->SetSize(size); | |
| } | |
| } | |
| event_forwarder_ = std::make_unique<DevToolsEventForwarder>(this); | |
| // Tag the DevTools main WebContents with its TaskManager specific UserData | |
| // so that it shows up in the task manager. | |
| task_manager::WebContentsTags::CreateForDevToolsContents(main_web_contents_); | |
| #if BUILDFLAG(IS_MAC) | |
| history_swiper::HistorySwipingControl::CreateForWebContents( | |
| main_web_contents_, base::BindRepeating([] { return false; })); | |
| #endif | |
| std::vector<base::RepeatingCallback<void(DevToolsWindow*)>> copy( | |
| GetCreationCallbacks()); | |
| for (const auto& callback : copy) { | |
| callback.Run(this); | |
| } | |
| pref_change_registrar_.Init(profile_->GetPrefs()); | |
| pref_change_registrar_.Add( | |
| language::prefs::kAcceptLanguages, | |
| base::BindRepeating(&DevToolsWindow::OnLocaleChanged, | |
| base::Unretained(this))); | |
| pref_change_registrar_.Add( | |
| prefs::kDeveloperToolsAvailabilityAllowlist, | |
| base::BindRepeating(&DevToolsWindow::OnDevToolsPolicyChanged, | |
| base::Unretained(this))); | |
| pref_change_registrar_.Add( | |
| prefs::kDeveloperToolsAvailabilityBlocklist, | |
| base::BindRepeating(&DevToolsWindow::OnDevToolsPolicyChanged, | |
| base::Unretained(this))); | |
| int64_t now_timestamp = | |
| base::Time::Now().ToDeltaSinceWindowsEpoch().InMilliseconds(); | |
| profile_->GetPrefs()->SetInt64(prefs::kDevToolsLastOpenTimestamp, | |
| now_timestamp); | |
| policy::DeveloperToolsPolicyChecker* checker = | |
| policy::DeveloperToolsPolicyCheckerFactory::GetForBrowserContext( | |
| profile_); | |
| if (checker) { | |
| policy_checker_callback_subscription_ = | |
| checker->AddObserver(base::BindRepeating( | |
| &DevToolsWindow::OnDevToolsPolicyChanged, base::Unretained(this))); | |
| } | |
| } | |
| void DevToolsWindow::OnDevToolsPolicyChanged() { | |
| if (!AllowDevToolsFor(profile_, GetInspectedWebContents())) { | |
| CloseWindow(); | |
| } | |
| } | |
| void DevToolsWindow::OnPolicyUpdated(const policy::PolicyNamespace& ns, | |
| const policy::PolicyMap& previous, | |
| const policy::PolicyMap& current) { | |
| OnDevToolsPolicyChanged(); | |
| } | |
| namespace { | |
| // Returns why the DevTools UI is unavailable for |profile| regardless of what | |
| // is being inspected, or DevToolsBlockReason::kNotBlocked. | |
| DevToolsBlockReason GetProfileBlockReason(Profile* profile) { | |
| // Don't allow DevTools UI in kiosk mode, because the DevTools UI would be | |
| // broken there. See https://crbug.com/41191065 for context. | |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kKioskMode)) { | |
| return DevToolsBlockReason::kKioskMode; | |
| } | |
| if (IsDevToolsUrlBlockedByPolicy(profile)) { | |
| return DevToolsBlockReason::kDevToolsUrlBlocklisted; | |
| } | |
| return DevToolsBlockReason::kNotBlocked; | |
| } | |
| } // namespace | |
| // static | |
| bool DevToolsWindow::AllowDevToolsFor(Profile* profile, | |
| content::WebContents* web_contents) { | |
| return GetBlockReasonFor(profile, web_contents) == | |
| DevToolsBlockReason::kNotBlocked; | |
| } | |
| // static | |
| bool DevToolsWindow::AllowDevToolsFor(Profile* profile, | |
| content::DevToolsAgentHost* agent_host) { | |
| return GetProfileBlockReason(profile) == DevToolsBlockReason::kNotBlocked && | |
| IsInspectionAllowed(profile, agent_host); | |
| } | |
| // static | |
| DevToolsBlockReason DevToolsWindow::GetBlockReasonFor( | |
| Profile* profile, | |
| content::WebContents* web_contents) { | |
| DevToolsBlockReason reason = GetProfileBlockReason(profile); | |
| if (reason != DevToolsBlockReason::kNotBlocked) { | |
| return reason; | |
| } | |
| return GetDevToolsBlockReason(profile, web_contents); | |
| } | |
| // static | |
| DevToolsWindow* DevToolsWindow::Create( | |
| Profile* profile, | |
| content::WebContents* inspected_web_contents, | |
| FrontendType frontend_type, | |
| const std::string& frontend_url, | |
| bool can_dock, | |
| const std::string& settings, | |
| const std::string& panel, | |
| bool has_other_clients, | |
| bool browser_connection, | |
| DevToolsOpenedByAction opened_by) { | |
| if (!AllowDevToolsFor(profile, inspected_web_contents)) { | |
| return nullptr; | |
| } | |
| SessionID inspected_browser_session_id = SessionID::InvalidValue(); | |
| #if BUILDFLAG(IS_ANDROID) | |
| // Docking is not supported yet. | |
| can_dock = false; | |
| #else | |
| if (inspected_web_contents) { | |
| // Check for a place to dock. | |
| BrowserWindowInterface* browser = | |
| GlobalBrowserCollection::GetInstance()->FindBrowserWithTab( | |
| inspected_web_contents); | |
| #if BUILDFLAG(IS_MAC) | |
| if (browser) { | |
| inspected_browser_session_id = browser->GetSessionID(); | |
| } | |
| #endif // BUILDFLAG(IS_MAC) | |
| DevtoolsUIController* devtools_ui_controller = | |
| browser ? DevtoolsUIController::From(browser) : nullptr; | |
| if (!devtools_ui_controller || !devtools_ui_controller->CanDockDevtools()) { | |
| can_dock = false; | |
| } | |
| } | |
| #endif // !BUILDFLAG(IS_ANDROID) | |
| // Create WebContents with devtools. | |
| GURL url(GetDevToolsURL(profile, frontend_type, frontend_url, can_dock, panel, | |
| has_other_clients, browser_connection)); | |
| std::unique_ptr<WebContents> main_web_contents = | |
| WebContents::Create(WebContents::CreateParams(profile)); | |
| main_web_contents->GetController().LoadURL( | |
| DecorateFrontendURL(url), content::Referrer(), | |
| ui::PAGE_TRANSITION_AUTO_TOPLEVEL, std::string()); | |
| DevToolsUIBindings* bindings = | |
| DevToolsUIBindings::ForWebContents(main_web_contents.get()); | |
| if (!bindings) { | |
| return nullptr; | |
| } | |
| if (!settings.empty()) { | |
| SetPreferencesFromJson(profile, settings); | |
| } | |
| return new DevToolsWindow(frontend_type, profile, | |
| std::move(main_web_contents), bindings, | |
| inspected_web_contents, can_dock, opened_by, | |
| inspected_browser_session_id); | |
| } | |
| // static | |
| GURL DevToolsWindow::GetDevToolsURL(Profile* profile, | |
| FrontendType frontend_type, | |
| const std::string& frontend_url, | |
| bool can_dock, | |
| const std::string& panel, | |
| bool has_other_clients, | |
| bool browser_connection) { | |
| std::string url; | |
| std::string remote_base = | |
| "?remoteBase=" + DevToolsUI::GetRemoteBaseURL().spec(); | |
| const std::string valid_frontend = | |
| frontend_url.empty() ? chrome::kChromeUIDevToolsURL : frontend_url; | |
| // remoteFrontend is here for backwards compatibility only. | |
| std::string remote_frontend = | |
| valid_frontend + ((valid_frontend.find("?") == std::string::npos) | |
| ? "?remoteFrontend=true" | |
| : "&remoteFrontend=true"); | |
| std::string tab_target = "&targetType=tab"; | |
| switch (frontend_type) { | |
| case kFrontendDefault: | |
| url = kDefaultFrontendURL + remote_base + tab_target; | |
| if (can_dock) { | |
| url += "&can_dock=true"; | |
| } | |
| if (!panel.empty()) { | |
| url += "&panel=" + panel; | |
| } | |
| break; | |
| case kFrontendWorker: | |
| url = kWorkerFrontendURL + remote_base; | |
| break; | |
| case kFrontendV8: | |
| url = kJSFrontendURL + remote_base; | |
| break; | |
| case kFrontendNode: | |
| url = kNodeFrontendURL + remote_base; | |
| break; | |
| case kFrontendRemote: | |
| url = remote_frontend; | |
| break; | |
| case kFrontendRemoteWorker: | |
| // isSharedWorker is here for backwards compatibility only. | |
| url = remote_frontend + "&isSharedWorker=true"; | |
| break; | |
| case kFrontendRemoteTab: | |
| url = remote_frontend + tab_target; | |
| break; | |
| } | |
| if (has_other_clients) { | |
| url += "&hasOtherClients=true"; | |
| } | |
| if (browser_connection) { | |
| url += "&browserConnection=true"; | |
| } | |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( | |
| switches::kUnsafelyDisableDevToolsSelfXssWarnings)) { | |
| url += "&disableSelfXssWarnings=true"; | |
| } | |
| #if BUILDFLAG(CHROME_FOR_TESTING) | |
| url += "&isChromeForTesting=true"; | |
| #endif | |
| return DevToolsUIBindings::SanitizeFrontendURL(GURL(url)); | |
| } | |
| // static | |
| DevToolsWindow* DevToolsWindow::FindDevToolsWindow( | |
| DevToolsAgentHost* agent_host) { | |
| if (!agent_host) { | |
| return nullptr; | |
| } | |
| for (auto& instance : GetDevToolsWindowInstances()) { | |
| if (instance->bindings_->IsAttachedTo(agent_host)) { | |
| return instance; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| // static | |
| DevToolsWindow* DevToolsWindow::AsDevToolsWindow( | |
| content::WebContents* web_contents) { | |
| if (!web_contents) { | |
| return nullptr; | |
| } | |
| for (auto& instance : GetDevToolsWindowInstances()) { | |
| if (instance->main_web_contents_ == web_contents) { | |
| return instance; | |
| } | |
| } | |
| return nullptr; | |
| } | |
| WebContents* DevToolsWindow::OpenURLFromTab( | |
| WebContents* source, | |
| const content::OpenURLParams& params, | |
| base::OnceCallback<void(content::NavigationHandle&)> | |
| navigation_handle_callback) { | |
| CHECK(source == main_web_contents_, base::NotFatalUntil::M161); | |
| if (!params.url.SchemeIs(content::kChromeDevToolsScheme)) { | |
| // TODO(https://crbug.com/40275094): Plumb the `navigation_handle_callback`. | |
| return OpenURLFromInspectedTab(params); | |
| } | |
| // TODO(https://crbug.com/40275094): Plumb the `navigation_handle_callback`. | |
| main_web_contents_->GetController().Reload(content::ReloadType::NORMAL, | |
| false); | |
| return main_web_contents_; | |
| } | |
| WebContents* DevToolsWindow::OpenURLFromInspectedTab( | |
| const content::OpenURLParams& params) { | |
| WebContents* inspected_web_contents = GetInspectedWebContents(); | |
| if (!inspected_web_contents) { | |
| return nullptr; | |
| } | |
| content::OpenURLParams modified = params; | |
| modified.referrer = content::Referrer(); | |
| return inspected_web_contents->OpenURL(modified, | |
| /*navigation_handle_callback=*/{}); | |
| } | |
| void DevToolsWindow::ActivateContents(WebContents* contents) { | |
| if (is_docked_) { | |
| WebContents* inspected_tab = GetInspectedWebContents(); | |
| if (inspected_tab) { | |
| inspected_tab->GetDelegate()->ActivateContents(inspected_tab); | |
| } | |
| } else { | |
| #if BUILDFLAG(IS_ANDROID) | |
| NOTIMPLEMENTED(); | |
| #else | |
| if (browser_) { | |
| browser_->GetWindow()->Activate(); | |
| } | |
| #endif | |
| } | |
| } | |
| WebContents* DevToolsWindow::AddNewContents( | |
| WebContents* source, | |
| std::unique_ptr<WebContents> new_contents, | |
| const GURL& target_url, | |
| ui::mojom::WindowOpenDisposition disposition, | |
| const blink::mojom::WindowFeatures& window_features, | |
| bool user_gesture, | |
| bool* was_blocked) { | |
| if (new_contents.get() == toolbox_web_contents_) { | |
| owned_toolbox_web_contents_ = std::move(new_contents); | |
| owned_toolbox_web_contents_->SetOwnerLocationForDebug(FROM_HERE); | |
| toolbox_web_contents_->SetDelegate( | |
| new DevToolsToolboxDelegate(toolbox_web_contents_, web_contents())); | |
| if (main_web_contents_->GetRenderWidgetHostView() && | |
| toolbox_web_contents_->GetRenderWidgetHostView()) { | |
| gfx::Size size = | |
| main_web_contents_->GetRenderWidgetHostView()->GetViewBounds().size(); | |
| toolbox_web_contents_->GetRenderWidgetHostView()->SetSize(size); | |
| } | |
| UpdateBrowserWindow(); | |
| return nullptr; | |
| } | |
| WebContents* inspected_web_contents = GetInspectedWebContents(); | |
| if (inspected_web_contents) { | |
| inspected_web_contents->GetDelegate()->AddNewContents( | |
| source, std::move(new_contents), target_url, disposition, | |
| window_features, user_gesture, was_blocked); | |
| } | |
| return nullptr; | |
| } | |
| void DevToolsWindow::WebContentsCreated( | |
| WebContents* source_contents, | |
| const GlobalRenderFrameHostId& opener_id, | |
| const std::string& frame_name, | |
| const GURL& target_url, | |
| WebContents* new_contents) { | |
| if (target_url.SchemeIs(content::kChromeDevToolsScheme) && | |
| target_url.GetPath().rfind("device_mode_emulation_frame.html") != | |
| std::string::npos) { | |
| CHECK(can_dock_); | |
| // Ownership will be passed in DevToolsWindow::AddNewContents. | |
| capture_handle_.RunAndReset(); | |
| if (owned_toolbox_web_contents_) { | |
| owned_toolbox_web_contents_.reset(); | |
| } | |
| toolbox_web_contents_ = new_contents; | |
| // Tag the DevTools toolbox WebContents with its TaskManager specific | |
| // UserData so that it shows up in the task manager. | |
| task_manager::WebContentsTags::CreateForDevToolsContents( | |
| toolbox_web_contents_); | |
| #if BUILDFLAG(IS_MAC) | |
| history_swiper::HistorySwipingControl::CreateForWebContents( | |
| toolbox_web_contents_, base::BindRepeating([] { return false; })); | |
| #endif | |
| // The toolbox holds a placeholder for the inspected WebContents. When the | |
| // placeholder is resized, a frame is requested. The inspected WebContents | |
| // is resized when the frame is rendered. Force rendering of the toolbox at | |
| // all times, to make sure that a frame can be rendered even when the | |
| // inspected WebContents fully covers the toolbox. | |
| // https://crbug.com/41380417 | |
| capture_handle_ = toolbox_web_contents_->IncrementCapturerCount( | |
| gfx::Size(), | |
| /*stay_hidden=*/false, | |
| /*stay_awake=*/false, /*is_activity=*/true); | |
| } | |
| } | |
| void DevToolsWindow::CloseContents(WebContents* source) { | |
| // We shouldn't get here as long as we're owned by the browser. | |
| CHECK(!browser_); | |
| life_stage_ = kClosing; | |
| UpdateBrowserWindow(); | |
| // In case of docked main_web_contents_, we own it so delete here. | |
| // Embedding DevTools window will be deleted as a result of | |
| // DevToolsUIBindings destruction. | |
| CHECK(owned_main_web_contents_); | |
| owned_main_web_contents_.reset(); | |
| } | |
| void DevToolsWindow::ContentsZoomChange(bool zoom_in) { | |
| CHECK(is_docked_, base::NotFatalUntil::M161); | |
| zoom::PageZoom::Zoom(main_web_contents_, zoom_in ? content::PAGE_ZOOM_IN | |
| : content::PAGE_ZOOM_OUT); | |
| } | |
| void DevToolsWindow::BeforeUnloadFired(WebContents* tab, | |
| bool proceed, | |
| bool* proceed_to_fire_unload) { | |
| if (!intercepted_page_beforeunload_) { | |
| // Docked devtools window closed directly. | |
| if (proceed) { | |
| bindings_->Detach(); | |
| } | |
| *proceed_to_fire_unload = proceed; | |
| } else { | |
| // Inspected page is attempting to close. | |
| WebContents* inspected_web_contents = GetInspectedWebContents(); | |
| if (proceed) { | |
| inspected_web_contents->DispatchBeforeUnload(false /* auto_cancel */); | |
| } else { | |
| bool should_proceed; | |
| inspected_web_contents->GetDelegate()->BeforeUnloadFired( | |
| inspected_web_contents, false, &should_proceed); | |
| CHECK(!should_proceed, base::NotFatalUntil::M161); | |
| } | |
| *proceed_to_fire_unload = false; | |
| } | |
| } | |
| content::JavaScriptDialogManager* DevToolsWindow::GetJavaScriptDialogManager( | |
| WebContents* source) { | |
| return javascript_dialogs::AppModalDialogManager::GetInstance(); | |
| } | |
| void DevToolsWindow::RunFileChooser( | |
| content::RenderFrameHost* render_frame_host, | |
| scoped_refptr<content::FileSelectListener> listener, | |
| const blink::mojom::FileChooserParams& params) { | |
| FileSelectHelper::RunFileChooser(render_frame_host, std::move(listener), | |
| params); | |
| } | |
| void DevToolsWindow::ActivateWindow() { | |
| if (life_stage_ != kLoadCompleted) { | |
| return; | |
| } | |
| #if BUILDFLAG(IS_ANDROID) | |
| NOTIMPLEMENTED(); | |
| #else | |
| if (is_docked_ && GetInspectedBrowserWindow()) { | |
| main_web_contents_->Focus(); | |
| } else if (!is_docked_ && browser_ && !browser_->GetWindow()->IsActive()) { | |
| browser_->GetWindow()->Activate(); | |
| } | |
| #endif | |
| } | |
| void DevToolsWindow::CloseWindow() { | |
| if (is_docked_) { | |
| Close(DevToolsClosedByAction::kCloseButton); | |
| } else { | |
| #if BUILDFLAG(IS_ANDROID) | |
| main_web_contents_->Close(); | |
| #else | |
| if (browser_) { | |
| browser_->GetWindow()->Close(); | |
| } | |
| #endif // BUILDFLAG(IS_ANDROID) | |
| } | |
| } | |
| void DevToolsWindow::Close(DevToolsClosedByAction closed_by) { | |
| CHECK(is_docked_, base::NotFatalUntil::M161); | |
| life_stage_ = kClosing; | |
| main_web_contents_->DispatchBeforeUnload(false /* auto_cancel */); | |
| closed_by_ = closed_by; | |
| if (sharing_infobar_) { | |
| RemoveSharingInfoBar(); | |
| checked_sharing_process_id_ = content::ChildProcessHost::kInvalidUniqueID; | |
| } | |
| } | |
| void DevToolsWindow::Inspect(scoped_refptr<content::DevToolsAgentHost> host) { | |
| DevToolsWindow::OpenDevToolsWindow(host, profile_, | |
| DevToolsOpenedByAction::kUnknown); | |
| } | |
| void DevToolsWindow::SetInspectedPageBounds(const gfx::Rect& rect) { | |
| devtools::DockSide dock_side = devtools::DockSide::kNone; | |
| if (is_docked_) { | |
| dock_side = DevToolsSettings::GetDockSide(profile_); | |
| } | |
| DevToolsContentsResizingStrategy strategy(dock_side, rect); | |
| if (contents_resizing_strategy_.Equals(strategy)) { | |
| return; | |
| } | |
| contents_resizing_strategy_.CopyFrom(strategy); | |
| UpdateBrowserWindow(); | |
| } | |
| void DevToolsWindow::InspectElementCompleted() { | |
| if (!inspect_element_start_time_.is_null()) { | |
| UMA_HISTOGRAM_TIMES("DevTools.InspectElement", | |
| base::TimeTicks::Now() - inspect_element_start_time_); | |
| inspect_element_start_time_ = base::TimeTicks(); | |
| } | |
| } | |
| void DevToolsWindow::SetIsDocked(bool dock_requested) { | |
| if (life_stage_ == kClosing) { | |
| return; | |
| } | |
| // TODO(crbug.com/579584319): CHECK-exclusion: Convert to a CHECK once we are | |
| // confident it won't be triggered. | |
| DCHECK(can_dock_ || !dock_requested); | |
| if (!can_dock_) { | |
| dock_requested = false; | |
| } | |
| bool was_docked = is_docked_; | |
| is_docked_ = dock_requested; | |
| if (life_stage_ != kLoadCompleted) { | |
| // This is a first time call we waited for to initialize. | |
| life_stage_ = life_stage_ == kOnLoadFired ? kLoadCompleted : kIsDockedSet; | |
| if (life_stage_ == kLoadCompleted) { | |
| LoadCompleted(); | |
| } | |
| return; | |
| } | |
| if (dock_requested == was_docked) { | |
| return; | |
| } | |
| #if BUILDFLAG(IS_ANDROID) | |
| LOG_IF(WARNING, dock_requested) << "Docking is not supported yet."; | |
| #else | |
| if (dock_requested && !was_docked && browser_) { | |
| // Detach window from the external devtools browser. It will lead to | |
| // the browser object's close and delete. Remove observer first. | |
| TabStripModel* const tab_strip_model = browser_->GetTabStripModel(); | |
| CHECK(!owned_main_web_contents_, base::NotFatalUntil::M161); | |
| // Removing the only WebContents from the tab strip of browser_ will | |
| // eventually lead to the destruction of browser_ as well, which is why it's | |
| // okay to just null the raw pointer here. | |
| browser_ = nullptr; | |
| // TODO(crbug.com/40773744): WebContents should be removed with a reason | |
| // other than kInsertedIntoOtherTabStrip, it's not getting reinserted into | |
| // another tab strip. | |
| std::unique_ptr<WebContents> web_contents = | |
| tab_strip_model->DetachWebContentsAtForInsertion( | |
| tab_strip_model->GetIndexOfWebContents(main_web_contents_)); | |
| owned_main_web_contents_ = | |
| std::make_unique<OwnedMainWebContents>(std::move(web_contents)); | |
| } else if (!dock_requested && was_docked) { | |
| UpdateBrowserWindow(); | |
| } | |
| #endif | |
| Show(DevToolsToggleAction::Show()); | |
| } | |
| int DevToolsWindow::GetDockStateForLogging() { | |
| const int kUndocked = 0; | |
| const int kLeft = 1; | |
| const int kBottom = 2; | |
| const int kRight = 3; | |
| if (!is_docked_) { | |
| return kUndocked; | |
| } | |
| switch (contents_resizing_strategy_.dock_side()) { | |
| case devtools::DockSide::kLeft: | |
| return kLeft; | |
| case devtools::DockSide::kBottom: | |
| return kBottom; | |
| case devtools::DockSide::kRight: | |
| return kRight; | |
| case devtools::DockSide::kNone: | |
| return kUndocked; | |
| } | |
| } | |
| int DevToolsWindow::GetOpenedByForLogging() { | |
| return static_cast<int>(opened_by_); | |
| } | |
| int DevToolsWindow::GetClosedByForLogging() { | |
| return static_cast<int>(closed_by_); | |
| } | |
| namespace { | |
| // Returns the frame of `web_contents`' primary page (including any inner | |
| // frame trees) whose DevTools frame token is `token`, if any. The token is | |
| // what the DevTools frontend knows a frame by: it is the frame's id in the | |
| // DevTools protocol, and also the id of the frame's DevTools target if the | |
| // frame has one of its own (main frames and out-of-process iframes do). | |
| content::RenderFrameHost* FindFrameByDevToolsFrameToken( | |
| WebContents* web_contents, | |
| const std::string& token) { | |
| std::optional<base::UnguessableToken> parsed_token = | |
| base::UnguessableToken::DeserializeFromString(token); | |
| if (!parsed_token) { | |
| return nullptr; | |
| } | |
| content::RenderFrameHost* result = nullptr; | |
| web_contents->GetPrimaryMainFrame()->ForEachRenderFrameHostWithAction( | |
| [&](content::RenderFrameHost* frame) { | |
| if (frame->GetDevToolsFrameToken() != *parsed_token) { | |
| return content::RenderFrameHost::FrameIterationAction::kContinue; | |
| } | |
| result = frame; | |
| return content::RenderFrameHost::FrameIterationAction::kStop; | |
| }); | |
| return result; | |
| } | |
| // Returns the worker (or worklet) with the DevTools target id `target_id` | |
| // that lives in `browser_context`, if any. | |
| scoped_refptr<DevToolsAgentHost> FindWorkerByDevToolsTargetId( | |
| const std::string& target_id, | |
| content::BrowserContext* browser_context) { | |
| scoped_refptr<DevToolsAgentHost> agent_host = | |
| DevToolsAgentHost::GetForId(target_id); | |
| if (!agent_host || agent_host->GetBrowserContext() != browser_context) { | |
| return nullptr; | |
| } | |
| const std::string type = agent_host->GetType(); | |
| if (type != DevToolsAgentHost::kTypeDedicatedWorker && | |
| type != DevToolsAgentHost::kTypeSharedWorker && | |
| type != DevToolsAgentHost::kTypeServiceWorker && | |
| type != DevToolsAgentHost::kTypeWorklet) { | |
| return nullptr; | |
| } | |
| return agent_host; | |
| } | |
| } // namespace | |
| void DevToolsWindow::OpenInNewTab( | |
| const GURL& url, | |
| const std::optional<std::string>& initiator_id) { | |
| GURL fixed_url = url; | |
| WebContents* inspected_web_contents = GetInspectedWebContents(); | |
| // The URL typically originates from content that is controlled by the | |
| // inspected target (e.g. a linkified URL in the Console, or a request in the | |
| // Network panel), so attribute the navigation to the frame or worker that | |
| // produced it, like the context menu does for "Open link in new tab" (see | |
| // RenderViewContextMenuBase::GetOpenURLParamsWithExtraHeaders()). Without an | |
| // initiator, the navigation would be indistinguishable from one typed into | |
| // the omnibox: the request would carry SameSite=Strict cookies and | |
| // `Sec-Fetch-Site: none`, which would allow the inspected target to bypass | |
| // the CSRF defenses of the destination. Attributing it to the inspected | |
| // page's main frame regardless of where the URL came from would be wrong as | |
| // well: an embedded cross-site frame could then have the user issue a | |
| // same-site request to its embedder. | |
| // | |
| // `initiator_id` is the DevTools target id or frame id of the frame or | |
| // worker that the frontend got the URL from. The frame must belong to the | |
| // inspected page (not to some other tab) and the worker to this profile. | |
| // Whatever cannot be resolved that way -- the frontend did not say, e.g. | |
| // because DevTools itself chose the URL; the frame or worker is gone; the id | |
| // belongs to something else -- is attributed to an opaque origin, which is | |
| // never same-site with the destination, so the navigation is treated as | |
| // cross-site. See https://crbug.com/554558674 and | |
| // https://crbug.com/576204004. | |
| content::RenderFrameHost* initiator_frame = nullptr; | |
| scoped_refptr<DevToolsAgentHost> initiator_worker; | |
| if (initiator_id && !initiator_id->empty()) { | |
| if (inspected_web_contents) { | |
| initiator_frame = | |
| FindFrameByDevToolsFrameToken(inspected_web_contents, *initiator_id); | |
| } | |
| if (!initiator_frame) { | |
| initiator_worker = FindWorkerByDevToolsTargetId(*initiator_id, profile_); | |
| } | |
| } | |
| int child_id = content::ChildProcessHost::kInvalidUniqueID; | |
| if (initiator_frame) { | |
| child_id = initiator_frame->GetProcess()->GetDeprecatedID(); | |
| } | |
| // Use about:blank instead of an empty GURL. The browser treats an empty GURL | |
| // as navigating to the home page, which may be privileged (chrome://newtab/). | |
| if (!content::ChildProcessSecurityPolicy::GetInstance()->CanRequestURL( | |
| child_id, fixed_url)) { | |
| fixed_url = GURL(url::kAboutBlankURL); | |
| } | |
| content::OpenURLParams params = | |
| content::OpenURLParams::CreateBrowserInitiated( | |
| fixed_url, ui::mojom::WindowOpenDisposition::kNewForegroundTab, | |
| ui::PAGE_TRANSITION_LINK); | |
| if (initiator_frame) { | |
| params.initiator_origin = initiator_frame->GetLastCommittedOrigin(); | |
| params.initiator_frame_token = initiator_frame->GetFrameToken(); | |
| params.initiator_process_id = child_id; | |
| params.source_site_instance = initiator_frame->GetSiteInstance(); | |
| } else if (initiator_worker) { | |
| // A worker has an origin but no frame, like a service worker that calls | |
| // `clients.openWindow()` (see service_worker_client_utils.cc). | |
| params.initiator_origin = url::Origin::Create(initiator_worker->GetURL()); | |
| } else { | |
| params.initiator_origin = url::Origin(); | |
| } | |
| if (!inspected_web_contents || | |
| !inspected_web_contents->OpenURL(params, | |
| /*navigation_handle_callback=*/{})) { | |
| #if BUILDFLAG(IS_ANDROID) | |
| NOTIMPLEMENTED(); | |
| #else | |
| // Either there is no inspected WebContents, or its delegate did not handle | |
| // the request. Navigate through NavigateParams, which carries the | |
| // attribution from `params`, rather than through AddSelectedTabWithURL(), | |
| // which only takes a URL. | |
| chrome::ScopedTabbedBrowserDisplayer displayer(profile_); | |
| NavigateParams nav_params(displayer.browser_window_interface(), fixed_url, | |
| ui::PAGE_TRANSITION_LINK); | |
| nav_params.FillNavigateParamsFromOpenURLParams(params); | |
| // Like AddSelectedTabWithURL(), don't let web apps capture the new tab. | |
| nav_params.web_app_navigation_data.emplace(); | |
| nav_params.web_app_navigation_data->SetNavigationCapturingForceOff(true); | |
| Navigate(&nav_params); | |
| #endif | |
| } | |
| } | |
| void DevToolsWindow::OpenInNewTab( | |
| const std::string& url, | |
| const std::optional<std::string>& initiator_id) { | |
| OpenInNewTab(GURL(url), initiator_id); | |
| } | |
| void DevToolsWindow::OpenSearchResultsInNewTab(const std::string& query) { | |
| TemplateURLService* url_service = | |
| TemplateURLServiceFactory::GetForProfile(profile_); | |
| CHECK(url_service, base::NotFatalUntil::M161); | |
| GURL url = | |
| GetDefaultSearchURLForSearchTerms(url_service, base::UTF8ToUTF16(query)); | |
| // DevTools chose the URL, so there is nothing to attribute the navigation to. | |
| OpenInNewTab(url, /*initiator_id=*/std::nullopt); | |
| } | |
| void DevToolsWindow::SetWhitelistedShortcuts(const std::string& message) { | |
| event_forwarder_->SetWhitelistedShortcuts(message); | |
| } | |
| void DevToolsWindow::SetEyeDropperActive(bool active) { | |
| WebContents* web_contents = GetInspectedWebContents(); | |
| if (!web_contents) { | |
| return; | |
| } | |
| if (active) { | |
| eye_dropper_ = std::make_unique<DevToolsEyeDropper>( | |
| web_contents, | |
| base::BindRepeating(&DevToolsWindow::ColorPickedInEyeDropper, | |
| base::Unretained(this))); | |
| } else { | |
| eye_dropper_.reset(); | |
| } | |
| } | |
| void DevToolsWindow::ColorPickedInEyeDropper(int r, int g, int b, int a) { | |
| base::DictValue color; | |
| color.Set("r", r); | |
| color.Set("g", g); | |
| color.Set("b", b); | |
| color.Set("a", a); | |
| bindings_->CallClientMethod("DevToolsAPI", "eyeDropperPickedColor", | |
| base::Value(std::move(color))); | |
| } | |
| void DevToolsWindow::InspectedContentsClosing() { | |
| if (!close_on_detach_) { | |
| return; | |
| } | |
| closed_by_ = DevToolsClosedByAction::kTargetDetach; | |
| intercepted_page_beforeunload_ = false; | |
| life_stage_ = kClosing; | |
| main_web_contents_->ClosePage(); | |
| } | |
| infobars::ContentInfoBarManager* DevToolsWindow::GetInfoBarManager() { | |
| return is_docked_ ? infobars::ContentInfoBarManager::FromWebContents( | |
| GetInspectedWebContents()) | |
| : infobars::ContentInfoBarManager::FromWebContents( | |
| main_web_contents_); | |
| } | |
| void DevToolsWindow::RenderProcessGone(bool crashed) { | |
| // Docked DevToolsWindow owns its main_web_contents_ and must delete it. | |
| // Undocked main_web_contents_ are owned and handled by browser. | |
| // see crbug.com/40363970 | |
| if (is_docked_) { | |
| CloseContents(main_web_contents_); | |
| } else { | |
| #if !BUILDFLAG(IS_ANDROID) | |
| if (browser_ && crashed) { | |
| browser_->GetWindow()->Close(); | |
| } | |
| #endif | |
| } | |
| } | |
| void DevToolsWindow::ShowCertificateViewer(const std::string& cert_chain) { | |
| std::optional<base::Value> value = | |
| base::JSONReader::Read(cert_chain, base::JSON_PARSE_CHROMIUM_EXTENSIONS); | |
| CHECK(value && value->is_list()); | |
| std::vector<std::string> decoded; | |
| for (const auto& item : value->GetList()) { | |
| CHECK(item.is_string()); | |
| std::string temp; | |
| CHECK(base::Base64Decode(item.GetString(), &temp)); | |
| decoded.push_back(std::move(temp)); | |
| } | |
| std::vector<std::string_view> cert_string_piece; | |
| for (const auto& str : decoded) { | |
| cert_string_piece.push_back(str); | |
| } | |
| scoped_refptr<net::X509Certificate> cert = | |
| net::X509Certificate::CreateFromDERCertChain(cert_string_piece); | |
| CHECK(cert); | |
| WebContents* inspected_contents = | |
| is_docked_ ? GetInspectedWebContents() : main_web_contents_.get(); | |
| if (!inspected_contents) { | |
| return; | |
| } | |
| #if BUILDFLAG(IS_ANDROID) | |
| NOTIMPLEMENTED(); | |
| #else | |
| BrowserWindowInterface* browser = | |
| GlobalBrowserCollection::GetInstance()->FindBrowserWithTab( | |
| inspected_contents); | |
| if (!browser) { | |
| return; | |
| } | |
| gfx::NativeWindow parent = browser->GetWindow()->GetNativeWindow(); | |
| ::ShowCertificateViewer(inspected_contents, parent, cert.get(), | |
| CertificateViewerEntrypoint::kDevTools); | |
| #endif | |
| } | |
| void DevToolsWindow::OnLoadCompleted() { | |
| // First seed inspected tab id for extension APIs. | |
| WebContents* inspected_web_contents = GetInspectedWebContents(); | |
| if (inspected_web_contents) { | |
| sessions::SessionTabHelper* session_tab_helper = | |
| sessions::SessionTabHelper::FromWebContents(inspected_web_contents); | |
| if (session_tab_helper) { | |
| bindings_->CallClientMethod( | |
| "DevToolsAPI", "setInspectedTabId", | |
| base::Value(session_tab_helper->session_id().id())); | |
| } | |
| } | |
| if (life_stage_ == kClosing) { | |
| return; | |
| } | |
| // We could be in kLoadCompleted state already if frontend reloads itself. | |
| if (life_stage_ != kLoadCompleted) { | |
| // Load is completed when both kIsDockedSet and kOnLoadFired happened. | |
| // Here we set kOnLoadFired. | |
| life_stage_ = life_stage_ == kIsDockedSet ? kLoadCompleted : kOnLoadFired; | |
| } | |
| if (life_stage_ == kLoadCompleted) { | |
| LoadCompleted(); | |
| } | |
| } | |
| void DevToolsWindow::ReadyForTest() { | |
| ready_for_test_ = true; | |
| if (!ready_for_test_callback_.is_null()) { | |
| std::move(ready_for_test_callback_).Run(); | |
| } | |
| } | |
| void DevToolsWindow::ConnectionReady() { | |
| if (throttle_) { | |
| throttle_->ResumeThrottle(); | |
| } | |
| } | |
| void DevToolsWindow::SetOpenNewWindowForPopups(bool value) { | |
| open_new_window_for_popups_ = value; | |
| } | |
| void DevToolsWindow::CreateDevToolsBrowser() { | |
| PrefService* prefs = profile_->GetPrefs(); | |
| bool resetPrefs = false; | |
| if (!prefs->GetDict(prefs::kAppWindowPlacement).Find(kDevToolsApp)) { | |
| // Ensure there is always a default size so that | |
| // BrowserWidget::InitBrowserFrame can retrieve it later. | |
| resetPrefs = true; | |
| } else { | |
| // Reset to default if stored window size is too small. | |
| const base::DictValue& devtoolsPlacement = | |
| prefs->GetDict(prefs::kAppWindowPlacement) | |
| .Find(kDevToolsApp) | |
| ->GetDict(); | |
| const int right = devtoolsPlacement.FindInt("right").value(); | |
| const int left = devtoolsPlacement.FindInt("left").value(); | |
| const int top = devtoolsPlacement.FindInt("top").value(); | |
| const int bottom = devtoolsPlacement.FindInt("bottom").value(); | |
| const int THRESHOLD = 400; // go/smoldevtools | |
| resetPrefs = right - left < THRESHOLD || bottom - top < THRESHOLD; | |
| } | |
| if (resetPrefs) { | |
| ScopedDictPrefUpdate update(prefs, prefs::kAppWindowPlacement); | |
| base::DictValue& wp_prefs = update.Get(); | |
| base::DictValue dev_tools_defaults; | |
| dev_tools_defaults.Set("left", 100); | |
| dev_tools_defaults.Set("top", 100); | |
| dev_tools_defaults.Set("right", 740); | |
| dev_tools_defaults.Set("bottom", 740); | |
| dev_tools_defaults.Set("maximized", false); | |
| dev_tools_defaults.Set("always_on_top", false); | |
| wp_prefs.Set(kDevToolsApp, std::move(dev_tools_defaults)); | |
| } | |
| #if BUILDFLAG(IS_ANDROID) | |
| NOTIMPLEMENTED(); | |
| #else | |
| if (GetBrowserWindowCreationStatusForProfile(*profile_) != | |
| BrowserWindowInterface::CreationStatus::kOk) { | |
| return; | |
| } | |
| browser_ = CreateBrowserWindow( | |
| BrowserWindowCreateParams::CreateForDevTools(profile_)); | |
| browser_->GetTabStripModel()->AddWebContents( | |
| OwnedMainWebContents::TakeWebContents( | |
| std::move(owned_main_web_contents_)), | |
| -1, ui::PAGE_TRANSITION_AUTO_TOPLEVEL, AddTabTypes::ADD_ACTIVE); | |
| #endif | |
| OverrideAndSyncDevToolsRendererPrefs(); | |
| } | |
| void DevToolsWindow::DoAction(const DevToolsToggleAction& action) { | |
| switch (action.type()) { | |
| case DevToolsToggleAction::kInspect: | |
| bindings_->CallClientMethod("DevToolsAPI", "enterInspectElementMode"); | |
| break; | |
| case DevToolsToggleAction::kShowElementsPanel: | |
| case DevToolsToggleAction::kPauseInDebugger: | |
| case DevToolsToggleAction::kShowConsolePanel: | |
| case DevToolsToggleAction::kShow: | |
| case DevToolsToggleAction::kToggle: | |
| // Do nothing. | |
| break; | |
| case DevToolsToggleAction::kReveal: { | |
| const DevToolsToggleAction::RevealParams* params = action.params(); | |
| CHECK(params); | |
| bindings_->CallClientMethod( | |
| "DevToolsAPI", "revealSourceLine", base::Value(params->url), | |
| base::Value(static_cast<int>(params->line_number)), | |
| base::Value(static_cast<int>(params->column_number))); | |
| break; | |
| } | |
| default: | |
| NOTREACHED(); | |
| } | |
| } | |
| WebContents* DevToolsWindow::GetInspectedWebContents() { | |
| return web_contents(); | |
| } | |
| void DevToolsWindow::LoadCompleted() { | |
| Show(action_on_load_); | |
| action_on_load_ = DevToolsToggleAction::NoOp(); | |
| if (!load_completed_callback_.is_null()) { | |
| std::move(load_completed_callback_).Run(); | |
| } | |
| } | |
| void DevToolsWindow::SetLoadCompletedCallback(base::OnceClosure closure) { | |
| if (life_stage_ == kLoadCompleted || life_stage_ == kClosing) { | |
| if (!closure.is_null()) { | |
| std::move(closure).Run(); | |
| } | |
| return; | |
| } | |
| load_completed_callback_ = std::move(closure); | |
| } | |
| bool DevToolsWindow::ForwardKeyboardEvent( | |
| const input::NativeWebKeyboardEvent& event) { | |
| return event_forwarder_->ForwardEvent(event); | |
| } | |
| bool DevToolsWindow::ReloadInspectedWebContents(bool bypass_cache) { | |
| // Only route reload via front-end if the agent is attached. | |
| WebContents* wc = GetInspectedWebContents(); | |
| if (!wc || wc->GetCrashedStatus() != base::TERMINATION_STATUS_STILL_RUNNING) { | |
| return false; | |
| } | |
| bindings_->CallClientMethod("DevToolsAPI", "reloadInspectedPage", | |
| base::Value(bypass_cache)); | |
| return true; | |
| } | |
| void DevToolsWindow::OnLocaleChanged() { | |
| OverrideAndSyncDevToolsRendererPrefs(); | |
| } | |
| void DevToolsWindow::OverrideAndSyncDevToolsRendererPrefs() { | |
| main_web_contents_->GetMutableRendererPrefs()->can_accept_load_drops = false; | |
| main_web_contents_->GetMutableRendererPrefs()->accept_languages = | |
| g_browser_process->GetApplicationLocale(); | |
| main_web_contents_->SyncRendererPrefs(); | |
| } | |
| void DevToolsWindow::MaybeShowSharedProcessInfobar() { | |
| WebContents* inspected_web_contents = GetInspectedWebContents(); | |
| if (!inspected_web_contents) { | |
| return; | |
| } | |
| // Only show the infobar only if the RenderProcessHost id changes. | |
| int rph_id = inspected_web_contents->GetPrimaryMainFrame() | |
| ->GetProcess() | |
| ->GetDeprecatedID(); | |
| if (checked_sharing_process_id_ == rph_id) { | |
| return; | |
| } | |
| checked_sharing_process_id_ = rph_id; | |
| if (!base::FeatureList::IsEnabled( | |
| ::features::kDevToolsSharedProcessInfobar) || | |
| !base::FeatureList::IsEnabled( | |
| ::features::kProcessPerSiteUpToMainFrameThreshold)) { | |
| return; | |
| } | |
| const content::SecurityPrincipal& security_principal = | |
| inspected_web_contents->GetPrimaryMainFrame() | |
| ->GetSiteInstance() | |
| ->GetSecurityPrincipal(); | |
| if (security_principal.SchemeIs(extensions::kExtensionScheme) || | |
| security_principal.SchemeIs(content::kChromeDevToolsScheme) || | |
| security_principal.SchemeIs(content::kChromeUIScheme)) { | |
| return; | |
| } | |
| size_t primary_main_frame_count = 0; | |
| inspected_web_contents->GetPrimaryMainFrame() | |
| ->GetProcess() | |
| ->ForEachRenderFrameHost( | |
| [&primary_main_frame_count]( | |
| content::RenderFrameHost* render_frame_host) { | |
| if (render_frame_host->IsInPrimaryMainFrame()) { | |
| ++primary_main_frame_count; | |
| } | |
| }); | |
| // Dismiss old infobar. | |
| RemoveSharingInfoBar(); | |
| if (primary_main_frame_count > 1) { | |
| #if !BUILDFLAG(IS_ANDROID) | |
| tabs::TabInterface* tab = nullptr; | |
| auto* browser_infobar_manager = | |
| infobars::BrowserInfoBarManager::From(g_browser_process); | |
| if (browser_infobar_manager && | |
| infobars::IsInfoBarMigrated( | |
| infobars::InfoBarDelegate::DEV_TOOLS_SHARED_PROCESS_DELEGATE)) { | |
| tab = tabs::TabInterface::MaybeGetFromContents( | |
| is_docked_ ? inspected_web_contents : main_web_contents_.get()); | |
| } | |
| is_sharing_infobar_migrated_ = !!tab; | |
| if (is_sharing_infobar_migrated_) { | |
| RegisterProcessSharingInfoBarSpec(*browser_infobar_manager); | |
| infobars::InfoBarShowParams params; | |
| params.ok_button_callback = base::BindRepeating( | |
| [](base::WeakPtr<content::WebContents> inspected_web_contents, | |
| content::WebContents*) { | |
| if (inspected_web_contents) { | |
| ShowProcessSharingRestartDialog(inspected_web_contents.get()); | |
| } | |
| }, | |
| inspected_web_contents->GetWeakPtr()); | |
| sharing_infobar_ = browser_infobar_manager->Show( | |
| tab, infobars::InfoBarDelegate::DEV_TOOLS_SHARED_PROCESS_DELEGATE, | |
| std::move(params)); | |
| } else if (auto* infobar_manager = GetInfoBarManager()) { | |
| sharing_infobar_ = infobar_manager->AddInfoBar( | |
| CreateConfirmInfoBar(ConfirmInfoBarCreationAllowlist::GetPassKey(), | |
| std::make_unique<ProcessSharingInfobarDelegate>( | |
| inspected_web_contents))); | |
| } | |
| if (sharing_infobar_) { | |
| sharing_infobar_->owner()->AddObserver(this); | |
| } | |
| #else | |
| NOTIMPLEMENTED(); | |
| #endif | |
| } | |
| } | |
| void DevToolsWindow::RemoveSharingInfoBar() { | |
| #if !BUILDFLAG(IS_ANDROID) | |
| if (!sharing_infobar_) { | |
| return; | |
| } | |
| // Dispatch on how the infobar was created, not on the feature state: a | |
| // migrated prompt for a tabless WebContents falls back to the legacy | |
| // delegate, which Hide() must not be handed. | |
| if (is_sharing_infobar_migrated_) { | |
| auto* browser_infobar_manager = | |
| infobars::BrowserInfoBarManager::From(g_browser_process); | |
| CHECK(browser_infobar_manager); | |
| browser_infobar_manager->Hide(sharing_infobar_.get()); | |
| } else { | |
| sharing_infobar_->RemoveSelf(); | |
| } | |
| #endif | |
| } | |
| #if !BUILDFLAG(IS_ANDROID) | |
| void DevToolsWindow::OnBrowserClosed(BrowserWindowInterface* browser) { | |
| // If the modal dialog manager has this browser as its delegate, clear the | |
| // reference before it becomes dangling. | |
| web_modal::WebContentsModalDialogManager* dialog_manager = | |
| web_modal::WebContentsModalDialogManager::FromWebContents( | |
| main_web_contents_); | |
| if (dialog_manager && dialog_manager->delegate() == | |
| BrowserWindowModalDialogDelegate::From(browser)) { | |
| dialog_manager->SetDelegate(nullptr); | |
| } | |
| } | |
| #endif | |
| void DevToolsWindow::OnInfoBarRemoved(infobars::InfoBar* infobar, | |
| bool animate) { | |
| if (sharing_infobar_ == infobar) { | |
| infobar->owner()->RemoveObserver(this); | |
| sharing_infobar_ = nullptr; | |
| is_sharing_infobar_migrated_ = false; | |
| } | |
| } | |
| void DevToolsWindow::DidFinishNavigation( | |
| content::NavigationHandle* navigation_handle) { | |
| if (!navigation_handle->HasCommitted() || | |
| navigation_handle->IsSameDocument()) { | |
| return; | |
| } | |
| if (!AllowDevToolsFor(profile_, web_contents())) { | |
| CloseWindow(); | |
| } | |
| } | |
| void DevToolsWindow::PrimaryPageChanged(content::Page& page) { | |
| MaybeShowSharedProcessInfobar(); | |
| } | |
| void DevToolsWindow::MainWebContentRenderFrameHostChanged( | |
| content::RenderFrameHost* old_frame, | |
| content::RenderFrameHost* new_frame) { | |
| DevToolsUIBindings* new_bindings = | |
| DevToolsUIBindings::ForWebContents(main_web_contents_); | |
| if (!new_bindings || new_bindings == bindings_) { | |
| return; | |
| } | |
| bindings_->TransferDelegate(*new_bindings); | |
| bindings_ = new_bindings; | |
| } | |
| raw_ptr<content::WebContents> DevToolsWindow::GetDevToolsWebContents() { | |
| return main_web_contents_; | |
| } | |
| void DevToolsWindow::AttachToBrowser(BrowserWindowInterface* browser) { | |
| if (!owned_main_web_contents_) { | |
| return; | |
| } | |
| std::unique_ptr<content::WebContents> owned_web_contents = | |
| OwnedMainWebContents::TakeWebContents( | |
| std::move(owned_main_web_contents_)); | |
| if (!owned_web_contents) { | |
| return; | |
| } | |
| auto* tab_list = TabListInterface::From(browser); | |
| if (!tab_list) { | |
| return; | |
| } | |
| tab_list->InsertWebContentsAt(0, std::move(owned_web_contents), false, | |
| std::nullopt); | |
| } |