// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef CHROME_BROWSER_DEVTOOLS_DEVTOOLS_WINDOW_H_ #define CHROME_BROWSER_DEVTOOLS_DEVTOOLS_WINDOW_H_ #include #include #include "base/macros.h" #include "chrome/browser/devtools/devtools_contents_resizing_strategy.h" #include "chrome/browser/devtools/devtools_toggle_action.h" #include "chrome/browser/devtools/devtools_ui_bindings.h" #include "content/public/browser/web_contents_delegate.h" #include "content/public/browser/web_contents_observer.h" class Browser; class BrowserWindow; class DevToolsWindowTesting; class DevToolsEventForwarder; class DevToolsEyeDropper; namespace content { class DevToolsAgentHost; struct NativeWebKeyboardEvent; class NavigationHandle; class NavigationThrottle; class RenderFrameHost; } namespace user_prefs { class PrefRegistrySyncable; } // Values that represent different actions to open DevTools window. // These values are written to logs. New enum values can be added, but existing // enums must never be renumbered or deleted and reused. enum class DevToolsOpenedByAction { kUnknown = 0, // Main menu -> More Tools -> Developer Tools // or Ctrl+Shift+I shortcut kMainMenuOrMainShortcut = 1, // Ctrl+Shift+J shortcut to jump to Console kConsoleShortcut = 2, // Context menu -> Inspect kContextMenuInspect = 3, // Ctrl+Shift+C shortcut to turn on inspect mode kInspectorModeShortcut = 4, // Toggle-open via F12 kToggleShortcut = 5, // Add values above this line with a corresponding label in // tools/metrics/histograms/enums.xml kMaxValue = kToggleShortcut, }; class DevToolsWindow : public DevToolsUIBindings::Delegate, public content::WebContentsDelegate { public: class ObserverWithAccessor : public content::WebContentsObserver { public: explicit ObserverWithAccessor(content::WebContents* web_contents); ~ObserverWithAccessor() override; private: DISALLOW_COPY_AND_ASSIGN(ObserverWithAccessor); }; static const char kDevToolsApp[]; ~DevToolsWindow() override; static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry); // Returns whether DevTools are allowed for the specified // |profile| and |web_contents|. If |web_contents| is null, // only checks for |profile| in general. static bool AllowDevToolsFor(Profile* profile, content::WebContents* web_contents); // Return the DevToolsWindow for the given WebContents if one exists, // otherwise nullptr. static DevToolsWindow* GetInstanceForInspectedWebContents( content::WebContents* inspected_web_contents); // Return the docked DevTools WebContents for the given inspected WebContents // if one exists and should be shown in browser window, otherwise nullptr. // This method will return only fully initialized window ready to be // presented in UI. // If |out_strategy| is not nullptr, it will contain resizing strategy. // For immediately-ready-to-use but maybe not yet fully initialized DevTools // use |GetInstanceForInspectedRenderViewHost| instead. static content::WebContents* GetInTabWebContents( content::WebContents* inspected_tab, DevToolsContentsResizingStrategy* out_strategy); static bool IsDevToolsWindow(content::WebContents* web_contents); static DevToolsWindow* AsDevToolsWindow(content::WebContents* web_contents); static DevToolsWindow* AsDevToolsWindow(Browser* browser); static DevToolsWindow* FindDevToolsWindow(content::DevToolsAgentHost*); // Open or reveal DevTools window, and perform the specified action. // How to get pointer to the created window see comments for // ToggleDevToolsWindow(). static void OpenDevToolsWindow(content::WebContents* inspected_web_contents, const DevToolsToggleAction& action); // Open or reveal DevTools window, with no special action. // How to get pointer to the created window see comments for // ToggleDevToolsWindow(). static void OpenDevToolsWindow(content::WebContents* inspected_web_contents); // Open or reveal DevTools window, with no special action. Use |profile| to // open client window in, default to |host|'s profile if none given. static void OpenDevToolsWindow( scoped_refptr host, Profile* profile); // Similar to previous one, but forces the bundled frontend to be used. static void OpenDevToolsWindowWithBundledFrontend( scoped_refptr host, Profile* profile); // Perform specified action for current WebContents inside a |browser|. // This may close currently open DevTools window. // If DeveloperToolsAvailability policy disallows developer tools for the // current WebContents, no DevTools window created. In case if needed pointer // to the created window one should use DevToolsAgentHost and // DevToolsWindow::FindDevToolsWindow(). E.g.: // // scoped_refptr agent( // content::DevToolsAgentHost::GetOrCreateFor(inspected_web_contents)); // DevToolsWindow::ToggleDevToolsWindow( // inspected_web_contents, DevToolsToggleAction::Show()); // DevToolsWindow* window = DevToolsWindow::FindDevToolsWindow(agent.get()); // static void ToggleDevToolsWindow( Browser* browser, const DevToolsToggleAction& action, DevToolsOpenedByAction opened_by = DevToolsOpenedByAction::kUnknown); // Node frontend is always undocked. static DevToolsWindow* OpenNodeFrontendWindow(Profile* profile); static void InspectElement(content::RenderFrameHost* inspected_frame_host, int x, int y); static void LogDevToolsOpenedByAction(DevToolsOpenedByAction opened_by); static std::unique_ptr MaybeCreateNavigationThrottle(content::NavigationHandle* handle); // Updates the WebContents inspected by the DevToolsWindow by reattaching // the binding to |new_web_contents|. Called when swapping an outer // WebContents with its inner WebContents. void UpdateInspectedWebContents(content::WebContents* new_web_contents, base::OnceCallback callback); // Sets closure to be called after load is done. If already loaded, calls // closure immediately. void SetLoadCompletedCallback(base::OnceClosure closure); // Forwards an unhandled keyboard event to the DevTools frontend. bool ForwardKeyboardEvent(const content::NativeWebKeyboardEvent& event); // Reloads inspected web contents as if it was triggered from DevTools. // Returns true if it has successfully handled reload, false if the caller // is to proceed reload without DevTools interception. bool ReloadInspectedWebContents(bool bypass_cache); content::WebContents* OpenURLFromTab( content::WebContents* source, const content::OpenURLParams& params) override; content::WebContents* OpenURLFromInspectedTab( const content::OpenURLParams& params); // BeforeUnload interception //////////////////////////////////////////////// // In order to preserve any edits the user may have made in devtools, the // beforeunload event of the inspected page is hooked - devtools gets the // first shot at handling beforeunload and presents a dialog to the user. If // the user accepts the dialog then the script is given a chance to handle // it. This way 2 dialogs may be displayed: one from the devtools asking the // user to confirm that they're ok with their devtools edits going away and // another from the webpage as the result of its beforeunload handler. // The following set of methods handle beforeunload event flow through // devtools window. When the |contents| with devtools opened on them are // getting closed, the following sequence of calls takes place: // 1. |DevToolsWindow::InterceptPageBeforeUnload| is called and indicates // whether devtools intercept the beforeunload event. // If InterceptPageBeforeUnload() returns true then the following steps // will take place; otherwise only step 4 will be reached and none of the // corresponding functions in steps 2 & 3 will get called. // 2. |DevToolsWindow::InterceptPageBeforeUnload| fires beforeunload event // for devtools frontend, which will asynchronously call // |WebContentsDelegate::BeforeUnloadFired| method. // In case of docked devtools window, devtools are set as a delegate for // its frontend, so method |DevToolsWindow::BeforeUnloadFired| will be // called directly. // If devtools window is undocked it's not set as the delegate so the call // to BeforeUnloadFired is proxied through HandleBeforeUnload() rather // than getting called directly. // 3a. If |DevToolsWindow::BeforeUnloadFired| is called with |proceed|=false // it calls throught to the content's BeforeUnloadFired(), which from the // WebContents perspective looks the same as the |content|'s own // beforeunload dialog having had it's 'stay on this page' button clicked. // 3b. If |proceed| = true, then it fires beforeunload event on |contents| // and everything proceeds as it normally would without the Devtools // interception. // 4. If the user cancels the dialog put up by either the WebContents or // devtools frontend, then |contents|'s |BeforeUnloadFired| callback is // called with the proceed argument set to false, this causes // |DevToolsWindow::OnPageCloseCancelled| to be called. // Devtools window in undocked state is not set as a delegate of // its frontend. Instead, an instance of browser is set as the delegate, and // thus beforeunload event callback from devtools frontend is not delivered // to the instance of devtools window, which is solely responsible for // managing custom beforeunload event flow. // This is a helper method to route callback from // |Browser::BeforeUnloadFired| back to |DevToolsWindow::BeforeUnloadFired|. // * |proceed| - true if the user clicked 'ok' in the beforeunload dialog, // false otherwise. // * |proceed_to_fire_unload| - output parameter, whether we should continue // to fire the unload event or stop things here. // Returns true if devtools window is in a state of intercepting beforeunload // event and if it will manage unload process on its own. static bool HandleBeforeUnload(content::WebContents* contents, bool proceed, bool* proceed_to_fire_unload); // Returns true if this contents beforeunload event was intercepted by // devtools and false otherwise. If the event was intercepted, caller should // not fire beforeunlaod event on |contents| itself as devtools window will // take care of it, otherwise caller should continue handling the event as // usual. static bool InterceptPageBeforeUnload(content::WebContents* contents); // Returns true if devtools browser has already fired its beforeunload event // as a result of beforeunload event interception. static bool HasFiredBeforeUnloadEventForDevToolsBrowser(Browser* browser); // Returns true if devtools window would like to hook beforeunload event // of this |contents|. static bool NeedsToInterceptBeforeUnload(content::WebContents* contents); // Notify devtools window that closing of |contents| was cancelled // by user. static void OnPageCloseCanceled(content::WebContents* contents); content::WebContents* GetInspectedWebContents(); private: friend class DevToolsWindowTesting; friend class DevToolsWindowCreationObserver; friend class HatsNextWebDialogBrowserTest; using CreationCallback = base::RepeatingCallback; static void AddCreationCallbackForTest(const CreationCallback& callback); static void RemoveCreationCallbackForTest(const CreationCallback& callback); static void OpenDevToolsWindowForFrame( Profile* profile, const scoped_refptr& agent_host); static void OpenDevToolsWindowForWorker( Profile* profile, const scoped_refptr& worker_agent); // DevTools lifecycle typically follows this way: // - Toggle/Open: client call; // - Create; // - ScheduleShow: setup window to be functional, but not yet show; // - DocumentOnLoadCompletedInMainFrame: frontend loaded; // - SetIsDocked: frontend decided on docking state; // - OnLoadCompleted: ready to present frontend; // - Show: actually placing frontend WebContents to a Browser or docked place; // - DoAction: perform action passed in Toggle/Open; // - ...; // - CloseWindow: initiates before unload handling; // - CloseContents: destroys frontend; // - DevToolsWindow is dead once it's main_web_contents dies. enum LifeStage { kNotLoaded, kOnLoadFired, // Implies SetIsDocked was not yet called. kIsDockedSet, // Implies DocumentOnLoadCompleted was not yet called. kLoadCompleted, kClosing }; enum FrontendType { kFrontendDefault, kFrontendWorker, kFrontendV8, kFrontendNode, kFrontendRemote, kFrontendRemoteWorker, }; DevToolsWindow(FrontendType frontend_type, Profile* profile, std::unique_ptr main_web_contents, DevToolsUIBindings* bindings, content::WebContents* inspected_web_contents, bool can_dock); // External frontend is always undocked. static void OpenExternalFrontend( Profile* profile, const std::string& frontend_uri, const scoped_refptr& agent_host, bool use_bundled_frontend); static void OpenDevToolsWindow(scoped_refptr host, Profile* profile, bool use_bundled_frontend); static 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); static GURL GetDevToolsURL(Profile* profile, FrontendType frontend_type, const std::string& frontend_url, bool can_dock, const std::string& panel, bool has_other_clients); static void ToggleDevToolsWindow( content::WebContents* web_contents, bool force_open, const DevToolsToggleAction& action, const std::string& settings, DevToolsOpenedByAction opened_by = DevToolsOpenedByAction::kUnknown); static Profile* GetProfileForDevToolsWindow( content::WebContents* web_contents); // content::WebContentsDelegate: void ActivateContents(content::WebContents* contents) override; void AddNewContents(content::WebContents* source, std::unique_ptr new_contents, const GURL& target_url, WindowOpenDisposition disposition, const gfx::Rect& initial_rect, bool user_gesture, bool* was_blocked) override; void WebContentsCreated(content::WebContents* source_contents, int opener_render_process_id, int opener_render_frame_id, const std::string& frame_name, const GURL& target_url, content::WebContents* new_contents) override; void CloseContents(content::WebContents* source) override; void ContentsZoomChange(bool zoom_in) override; void BeforeUnloadFired(content::WebContents* tab, bool proceed, bool* proceed_to_fire_unload) override; content::KeyboardEventProcessingResult PreHandleKeyboardEvent( content::WebContents* source, const content::NativeWebKeyboardEvent& event) override; bool HandleKeyboardEvent( content::WebContents* source, const content::NativeWebKeyboardEvent& event) override; content::JavaScriptDialogManager* GetJavaScriptDialogManager( content::WebContents* source) override; void RunFileChooser(content::RenderFrameHost* render_frame_host, scoped_refptr listener, const blink::mojom::FileChooserParams& params) override; bool PreHandleGestureEvent(content::WebContents* source, const blink::WebGestureEvent& event) override; // content::DevToolsUIBindings::Delegate overrides void ActivateWindow() override; void CloseWindow() override; void Inspect(scoped_refptr host) override; void SetInspectedPageBounds(const gfx::Rect& rect) override; void InspectElementCompleted() override; void SetIsDocked(bool is_docked) override; void OpenInNewTab(const std::string& url) override; void SetWhitelistedShortcuts(const std::string& message) override; void SetEyeDropperActive(bool active) override; void OpenNodeFrontend() override; void InspectedContentsClosing() override; void OnLoadCompleted() override; void ReadyForTest() override; void ConnectionReady() override; void SetOpenNewWindowForPopups(bool value) override; infobars::ContentInfoBarManager* GetInfoBarManager() override; void RenderProcessGone(bool crashed) override; void ShowCertificateViewer(const std::string& cert_viewer) override; void ColorPickedInEyeDropper(int r, int g, int b, int a); // This method creates a new Browser object (if possible), and passes // ownership of owned_main_web_contents_ to the tab strip of the Browser. void CreateDevToolsBrowser(); BrowserWindow* GetInspectedBrowserWindow(); void ScheduleShow(const DevToolsToggleAction& action); void Show(const DevToolsToggleAction& action); void DoAction(const DevToolsToggleAction& action); void LoadCompleted(); void UpdateBrowserToolbar(); void UpdateBrowserWindow(); // Registers a WebContentsModalDialogManager for our WebContents in order to // display web modal dialogs triggered by it. void RegisterModalDialogManager(Browser* browser); void OnReattachMainTargetComplete(base::Value); // Called when the accepted language changes. |navigator.language| of the // DevTools window should match the application language. When the user // changes the accepted language then this listener flips the language back // to the application language for the DevTools renderer process. // Please note that |navigator.language| will have the wrong language for // a very short period of time (until this handler has reset it again). void OnLocaleChanged(); void OverrideAndSyncDevToolsRendererPrefs(); std::unique_ptr inspected_contents_observer_; FrontendType frontend_type_; Profile* profile_; content::WebContents* main_web_contents_; // DevToolsWindow is informed of the creation of the |toolbox_web_contents_| // in WebContentsCreated right before ownership is passed to to DevToolsWindow // in AddNewContents(). The former call has information not available in the // latter, so it's easiest to record a raw pointer first in // |toolbox_web_contents_|, and then update ownership immediately afterwards. // TODO(erikchen): If we updated AddNewContents() to also pass back the // target url, then we wouldn't need to listen to WebContentsCreated at all. content::WebContents* toolbox_web_contents_; std::unique_ptr owned_toolbox_web_contents_; DevToolsUIBindings* bindings_; Browser* browser_; // When DevToolsWindow is docked, it owns main_web_contents_. When it isn't // docked, the tab strip model owns the main_web_contents_. bool is_docked_; class OwnedMainWebContents; std::unique_ptr owned_main_web_contents_; const bool can_dock_; bool close_on_detach_; LifeStage life_stage_; DevToolsToggleAction action_on_load_; DevToolsContentsResizingStrategy contents_resizing_strategy_; // True if we're in the process of handling a beforeunload event originating // from the inspected webcontents, see InterceptPageBeforeUnload for details. bool intercepted_page_beforeunload_; base::OnceClosure load_completed_callback_; base::OnceClosure close_callback_; bool ready_for_test_; base::OnceClosure ready_for_test_callback_; base::TimeTicks inspect_element_start_time_; std::unique_ptr event_forwarder_; std::unique_ptr eye_dropper_; class Throttle; Throttle* throttle_ = nullptr; bool open_new_window_for_popups_ = false; base::OnceCallback reattach_complete_callback_; PrefChangeRegistrar pref_change_registrar_; base::ScopedClosureRunner capture_handle_; friend class DevToolsEventForwarder; DISALLOW_COPY_AND_ASSIGN(DevToolsWindow); }; #endif // CHROME_BROWSER_DEVTOOLS_DEVTOOLS_WINDOW_H_