1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #ifndef CHROME_BROWSER_DEVTOOLS_DEVTOOLS_WINDOW_H_
6 #define CHROME_BROWSER_DEVTOOLS_DEVTOOLS_WINDOW_H_
7 
8 #include <memory>
9 #include <string>
10 
11 #include "base/macros.h"
12 #include "chrome/browser/devtools/devtools_contents_resizing_strategy.h"
13 #include "chrome/browser/devtools/devtools_toggle_action.h"
14 #include "chrome/browser/devtools/devtools_ui_bindings.h"
15 #include "content/public/browser/web_contents_delegate.h"
16 #include "content/public/browser/web_contents_observer.h"
17 
18 class Browser;
19 class BrowserWindow;
20 class DevToolsWindowTesting;
21 class DevToolsEventForwarder;
22 class DevToolsEyeDropper;
23 
24 namespace content {
25 class DevToolsAgentHost;
26 struct NativeWebKeyboardEvent;
27 class NavigationHandle;
28 class NavigationThrottle;
29 class RenderFrameHost;
30 }
31 
32 namespace user_prefs {
33 class PrefRegistrySyncable;
34 }
35 
36 // Values that represent different actions to open DevTools window.
37 // These values are written to logs. New enum values can be added, but existing
38 // enums must never be renumbered or deleted and reused.
39 enum class DevToolsOpenedByAction {
40   kUnknown = 0,
41   // Main menu -> More Tools -> Developer Tools
42   // or Ctrl+Shift+I shortcut
43   kMainMenuOrMainShortcut = 1,
44   // Ctrl+Shift+J shortcut to jump to Console
45   kConsoleShortcut = 2,
46   // Context menu -> Inspect
47   kContextMenuInspect = 3,
48   // Ctrl+Shift+C shortcut to turn on inspect mode
49   kInspectorModeShortcut = 4,
50   // Toggle-open via F12
51   kToggleShortcut = 5,
52   // Add values above this line with a corresponding label in
53   // tools/metrics/histograms/enums.xml
54   kMaxValue = kToggleShortcut,
55 };
56 
57 class DevToolsWindow : public DevToolsUIBindings::Delegate,
58                        public content::WebContentsDelegate {
59  public:
60   class ObserverWithAccessor : public content::WebContentsObserver {
61    public:
62     explicit ObserverWithAccessor(content::WebContents* web_contents);
63     ~ObserverWithAccessor() override;
64 
65    private:
66     DISALLOW_COPY_AND_ASSIGN(ObserverWithAccessor);
67   };
68 
69   static const char kDevToolsApp[];
70 
71   ~DevToolsWindow() override;
72 
73   static void RegisterProfilePrefs(user_prefs::PrefRegistrySyncable* registry);
74 
75   // Returns whether DevTools are allowed for the specified
76   // |profile| and |web_contents|. If |web_contents| is null,
77   // only checks for |profile| in general.
78   static bool AllowDevToolsFor(Profile* profile,
79                                content::WebContents* web_contents);
80 
81   // Return the DevToolsWindow for the given WebContents if one exists,
82   // otherwise nullptr.
83   static DevToolsWindow* GetInstanceForInspectedWebContents(
84       content::WebContents* inspected_web_contents);
85 
86   // Return the docked DevTools WebContents for the given inspected WebContents
87   // if one exists and should be shown in browser window, otherwise nullptr.
88   // This method will return only fully initialized window ready to be
89   // presented in UI.
90   // If |out_strategy| is not nullptr, it will contain resizing strategy.
91   // For immediately-ready-to-use but maybe not yet fully initialized DevTools
92   // use |GetInstanceForInspectedRenderViewHost| instead.
93   static content::WebContents* GetInTabWebContents(
94       content::WebContents* inspected_tab,
95       DevToolsContentsResizingStrategy* out_strategy);
96 
97   static bool IsDevToolsWindow(content::WebContents* web_contents);
98   static DevToolsWindow* AsDevToolsWindow(content::WebContents* web_contents);
99   static DevToolsWindow* AsDevToolsWindow(Browser* browser);
100   static DevToolsWindow* FindDevToolsWindow(content::DevToolsAgentHost*);
101 
102   // Open or reveal DevTools window, and perform the specified action.
103   // How to get pointer to the created window see comments for
104   // ToggleDevToolsWindow().
105   static void OpenDevToolsWindow(content::WebContents* inspected_web_contents,
106                                  const DevToolsToggleAction& action);
107 
108   // Open or reveal DevTools window, with no special action.
109   // How to get pointer to the created window see comments for
110   // ToggleDevToolsWindow().
111   static void OpenDevToolsWindow(content::WebContents* inspected_web_contents);
112 
113   // Open or reveal DevTools window, with no special action. Use |profile| to
114   // open client window in, default to |host|'s profile if none given.
115   static void OpenDevToolsWindow(
116       scoped_refptr<content::DevToolsAgentHost> host,
117       Profile* profile);
118   // Similar to previous one, but forces the bundled frontend to be used.
119   static void OpenDevToolsWindowWithBundledFrontend(
120       scoped_refptr<content::DevToolsAgentHost> host,
121       Profile* profile);
122 
123   // Perform specified action for current WebContents inside a |browser|.
124   // This may close currently open DevTools window.
125   // If DeveloperToolsAvailability policy disallows developer tools for the
126   // current WebContents, no DevTools window created. In case if needed pointer
127   // to the created window one should use DevToolsAgentHost and
128   // DevToolsWindow::FindDevToolsWindow(). E.g.:
129   //
130   // scoped_refptr<content::DevToolsAgentHost> agent(
131   //   content::DevToolsAgentHost::GetOrCreateFor(inspected_web_contents));
132   // DevToolsWindow::ToggleDevToolsWindow(
133   //   inspected_web_contents, DevToolsToggleAction::Show());
134   // DevToolsWindow* window = DevToolsWindow::FindDevToolsWindow(agent.get());
135   //
136   static void ToggleDevToolsWindow(
137       Browser* browser,
138       const DevToolsToggleAction& action,
139       DevToolsOpenedByAction opened_by = DevToolsOpenedByAction::kUnknown);
140 
141   // Node frontend is always undocked.
142   static DevToolsWindow* OpenNodeFrontendWindow(Profile* profile);
143 
144   static void InspectElement(content::RenderFrameHost* inspected_frame_host,
145                              int x,
146                              int y);
147 
148   static void LogDevToolsOpenedByAction(DevToolsOpenedByAction opened_by);
149 
150   static std::unique_ptr<content::NavigationThrottle>
151   MaybeCreateNavigationThrottle(content::NavigationHandle* handle);
152 
153   // Updates the WebContents inspected by the DevToolsWindow by reattaching
154   // the binding to |new_web_contents|. Called when swapping an outer
155   // WebContents with its inner WebContents.
156   void UpdateInspectedWebContents(content::WebContents* new_web_contents,
157                                   base::OnceCallback<void()> callback);
158 
159   // Sets closure to be called after load is done. If already loaded, calls
160   // closure immediately.
161   void SetLoadCompletedCallback(base::OnceClosure closure);
162 
163   // Forwards an unhandled keyboard event to the DevTools frontend.
164   bool ForwardKeyboardEvent(const content::NativeWebKeyboardEvent& event);
165 
166   // Reloads inspected web contents as if it was triggered from DevTools.
167   // Returns true if it has successfully handled reload, false if the caller
168   // is to proceed reload without DevTools interception.
169   bool ReloadInspectedWebContents(bool bypass_cache);
170 
171   content::WebContents* OpenURLFromTab(
172       content::WebContents* source,
173       const content::OpenURLParams& params) override;
174 
175   content::WebContents* OpenURLFromInspectedTab(
176       const content::OpenURLParams& params);
177 
178   // BeforeUnload interception ////////////////////////////////////////////////
179 
180   // In order to preserve any edits the user may have made in devtools, the
181   // beforeunload event of the inspected page is hooked - devtools gets the
182   // first shot at handling beforeunload and presents a dialog to the user. If
183   // the user accepts the dialog then the script is given a chance to handle
184   // it. This way 2 dialogs may be displayed: one from the devtools asking the
185   // user to confirm that they're ok with their devtools edits going away and
186   // another from the webpage as the result of its beforeunload handler.
187   // The following set of methods handle beforeunload event flow through
188   // devtools window. When the |contents| with devtools opened on them are
189   // getting closed, the following sequence of calls takes place:
190   // 1. |DevToolsWindow::InterceptPageBeforeUnload| is called and indicates
191   //    whether devtools intercept the beforeunload event.
192   //    If InterceptPageBeforeUnload() returns true then the following steps
193   //    will take place; otherwise only step 4 will be reached and none of the
194   //    corresponding functions in steps 2 & 3 will get called.
195   // 2. |DevToolsWindow::InterceptPageBeforeUnload| fires beforeunload event
196   //    for devtools frontend, which will asynchronously call
197   //    |WebContentsDelegate::BeforeUnloadFired| method.
198   //    In case of docked devtools window, devtools are set as a delegate for
199   //    its frontend, so method |DevToolsWindow::BeforeUnloadFired| will be
200   //    called directly.
201   //    If devtools window is undocked it's not set as the delegate so the call
202   //    to BeforeUnloadFired is proxied through HandleBeforeUnload() rather
203   //    than getting called directly.
204   // 3a. If |DevToolsWindow::BeforeUnloadFired| is called with |proceed|=false
205   //     it calls throught to the content's BeforeUnloadFired(), which from the
206   //     WebContents perspective looks the same as the |content|'s own
207   //     beforeunload dialog having had it's 'stay on this page' button clicked.
208   // 3b. If |proceed| = true, then it fires beforeunload event on |contents|
209   //     and everything proceeds as it normally would without the Devtools
210   //     interception.
211   // 4. If the user cancels the dialog put up by either the WebContents or
212   //    devtools frontend, then |contents|'s |BeforeUnloadFired| callback is
213   //    called with the proceed argument set to false, this causes
214   //    |DevToolsWindow::OnPageCloseCancelled| to be called.
215 
216   // Devtools window in undocked state is not set as a delegate of
217   // its frontend. Instead, an instance of browser is set as the delegate, and
218   // thus beforeunload event callback from devtools frontend is not delivered
219   // to the instance of devtools window, which is solely responsible for
220   // managing custom beforeunload event flow.
221   // This is a helper method to route callback from
222   // |Browser::BeforeUnloadFired| back to |DevToolsWindow::BeforeUnloadFired|.
223   // * |proceed| - true if the user clicked 'ok' in the beforeunload dialog,
224   //   false otherwise.
225   // * |proceed_to_fire_unload| - output parameter, whether we should continue
226   //   to fire the unload event or stop things here.
227   // Returns true if devtools window is in a state of intercepting beforeunload
228   // event and if it will manage unload process on its own.
229   static bool HandleBeforeUnload(content::WebContents* contents,
230                                  bool proceed,
231                                  bool* proceed_to_fire_unload);
232 
233   // Returns true if this contents beforeunload event was intercepted by
234   // devtools and false otherwise. If the event was intercepted, caller should
235   // not fire beforeunlaod event on |contents| itself as devtools window will
236   // take care of it, otherwise caller should continue handling the event as
237   // usual.
238   static bool InterceptPageBeforeUnload(content::WebContents* contents);
239 
240   // Returns true if devtools browser has already fired its beforeunload event
241   // as a result of beforeunload event interception.
242   static bool HasFiredBeforeUnloadEventForDevToolsBrowser(Browser* browser);
243 
244   // Returns true if devtools window would like to hook beforeunload event
245   // of this |contents|.
246   static bool NeedsToInterceptBeforeUnload(content::WebContents* contents);
247 
248   // Notify devtools window that closing of |contents| was cancelled
249   // by user.
250   static void OnPageCloseCanceled(content::WebContents* contents);
251 
252   content::WebContents* GetInspectedWebContents();
253 
254  private:
255   friend class DevToolsWindowTesting;
256   friend class DevToolsWindowCreationObserver;
257   friend class HatsNextWebDialogBrowserTest;
258 
259   using CreationCallback = base::Callback<void(DevToolsWindow*)>;
260   static void AddCreationCallbackForTest(const CreationCallback& callback);
261   static void RemoveCreationCallbackForTest(const CreationCallback& callback);
262 
263   static void OpenDevToolsWindowForFrame(
264       Profile* profile,
265       const scoped_refptr<content::DevToolsAgentHost>& agent_host);
266   static void OpenDevToolsWindowForWorker(
267       Profile* profile,
268       const scoped_refptr<content::DevToolsAgentHost>& worker_agent);
269 
270   // DevTools lifecycle typically follows this way:
271   // - Toggle/Open: client call;
272   // - Create;
273   // - ScheduleShow: setup window to be functional, but not yet show;
274   // - DocumentOnLoadCompletedInMainFrame: frontend loaded;
275   // - SetIsDocked: frontend decided on docking state;
276   // - OnLoadCompleted: ready to present frontend;
277   // - Show: actually placing frontend WebContents to a Browser or docked place;
278   // - DoAction: perform action passed in Toggle/Open;
279   // - ...;
280   // - CloseWindow: initiates before unload handling;
281   // - CloseContents: destroys frontend;
282   // - DevToolsWindow is dead once it's main_web_contents dies.
283   enum LifeStage {
284     kNotLoaded,
285     kOnLoadFired, // Implies SetIsDocked was not yet called.
286     kIsDockedSet, // Implies DocumentOnLoadCompleted was not yet called.
287     kLoadCompleted,
288     kClosing
289   };
290 
291   enum FrontendType {
292     kFrontendDefault,
293     kFrontendWorker,
294     kFrontendV8,
295     kFrontendNode,
296     kFrontendRemote,
297     kFrontendRemoteWorker,
298   };
299 
300   DevToolsWindow(FrontendType frontend_type,
301                  Profile* profile,
302                  std::unique_ptr<content::WebContents> main_web_contents,
303                  DevToolsUIBindings* bindings,
304                  content::WebContents* inspected_web_contents,
305                  bool can_dock);
306 
307   // External frontend is always undocked.
308   static void OpenExternalFrontend(
309       Profile* profile,
310       const std::string& frontend_uri,
311       const scoped_refptr<content::DevToolsAgentHost>& agent_host,
312       bool use_bundled_frontend);
313   static void OpenDevToolsWindow(scoped_refptr<content::DevToolsAgentHost> host,
314                                  Profile* profile,
315                                  bool use_bundled_frontend);
316 
317   static DevToolsWindow* Create(Profile* profile,
318                                 content::WebContents* inspected_web_contents,
319                                 FrontendType frontend_type,
320                                 const std::string& frontend_url,
321                                 bool can_dock,
322                                 const std::string& settings,
323                                 const std::string& panel,
324                                 bool has_other_clients);
325   static GURL GetDevToolsURL(Profile* profile,
326                              FrontendType frontend_type,
327                              const std::string& frontend_url,
328                              bool can_dock,
329                              const std::string& panel,
330                              bool has_other_clients);
331 
332   static void ToggleDevToolsWindow(
333       content::WebContents* web_contents,
334       bool force_open,
335       const DevToolsToggleAction& action,
336       const std::string& settings,
337       DevToolsOpenedByAction opened_by = DevToolsOpenedByAction::kUnknown);
338 
339   // content::WebContentsDelegate:
340   void ActivateContents(content::WebContents* contents) override;
341   void AddNewContents(content::WebContents* source,
342                       std::unique_ptr<content::WebContents> new_contents,
343                       const GURL& target_url,
344                       WindowOpenDisposition disposition,
345                       const gfx::Rect& initial_rect,
346                       bool user_gesture,
347                       bool* was_blocked) override;
348   void WebContentsCreated(content::WebContents* source_contents,
349                           int opener_render_process_id,
350                           int opener_render_frame_id,
351                           const std::string& frame_name,
352                           const GURL& target_url,
353                           content::WebContents* new_contents) override;
354   void CloseContents(content::WebContents* source) override;
355   void ContentsZoomChange(bool zoom_in) override;
356   void BeforeUnloadFired(content::WebContents* tab,
357                          bool proceed,
358                          bool* proceed_to_fire_unload) override;
359   content::KeyboardEventProcessingResult PreHandleKeyboardEvent(
360       content::WebContents* source,
361       const content::NativeWebKeyboardEvent& event) override;
362   bool HandleKeyboardEvent(
363       content::WebContents* source,
364       const content::NativeWebKeyboardEvent& event) override;
365   content::JavaScriptDialogManager* GetJavaScriptDialogManager(
366       content::WebContents* source) override;
367   content::ColorChooser* OpenColorChooser(
368       content::WebContents* web_contents,
369       SkColor color,
370       const std::vector<blink::mojom::ColorSuggestionPtr>& suggestions)
371       override;
372   void RunFileChooser(content::RenderFrameHost* render_frame_host,
373                       scoped_refptr<content::FileSelectListener> listener,
374                       const blink::mojom::FileChooserParams& params) override;
375   bool PreHandleGestureEvent(content::WebContents* source,
376                              const blink::WebGestureEvent& event) override;
377 
378   // content::DevToolsUIBindings::Delegate overrides
379   void ActivateWindow() override;
380   void CloseWindow() override;
381   void Inspect(scoped_refptr<content::DevToolsAgentHost> host) override;
382   void SetInspectedPageBounds(const gfx::Rect& rect) override;
383   void InspectElementCompleted() override;
384   void SetIsDocked(bool is_docked) override;
385   void OpenInNewTab(const std::string& url) override;
386   void SetWhitelistedShortcuts(const std::string& message) override;
387   void SetEyeDropperActive(bool active) override;
388   void OpenNodeFrontend() override;
389   void InspectedContentsClosing() override;
390   void OnLoadCompleted() override;
391   void ReadyForTest() override;
392   void ConnectionReady() override;
393   void SetOpenNewWindowForPopups(bool value) override;
394   InfoBarService* GetInfoBarService() override;
395   void RenderProcessGone(bool crashed) override;
396   void ShowCertificateViewer(const std::string& cert_viewer) override;
397 
398   void ColorPickedInEyeDropper(int r, int g, int b, int a);
399 
400   // This method create a new Browser object, and passes ownership of
401   // owned_main_web_contents_ to the tab strip of the Browser.
402   void CreateDevToolsBrowser();
403   BrowserWindow* GetInspectedBrowserWindow();
404   void ScheduleShow(const DevToolsToggleAction& action);
405   void Show(const DevToolsToggleAction& action);
406   void DoAction(const DevToolsToggleAction& action);
407   void LoadCompleted();
408   void UpdateBrowserToolbar();
409   void UpdateBrowserWindow();
410 
411   // Registers a WebContentsModalDialogManager for our WebContents in order to
412   // display web modal dialogs triggered by it.
413   void RegisterModalDialogManager(Browser* browser);
414 
415   void OnReattachMainTargetComplete(base::Value);
416 
417   // Called when the accepted language changes. |navigator.language| of the
418   // DevTools window should match the application language. When the user
419   // changes the accepted language then this listener flips the language back
420   // to the application language for the DevTools renderer process.
421   // Please note that |navigator.language| will have the wrong language for
422   // a very short period of time (until this handler has reset it again).
423   void OnLocaleChanged();
424   void OverrideAndSyncDevToolsRendererPrefs();
425 
426   std::unique_ptr<ObserverWithAccessor> inspected_contents_observer_;
427 
428   FrontendType frontend_type_;
429   Profile* profile_;
430   content::WebContents* main_web_contents_;
431 
432   // DevToolsWindow is informed of the creation of the |toolbox_web_contents_|
433   // in WebContentsCreated right before ownership is passed to to DevToolsWindow
434   // in AddNewContents(). The former call has information not available in the
435   // latter, so it's easiest to record a raw pointer first in
436   // |toolbox_web_contents_|, and then update ownership immediately afterwards.
437   // TODO(erikchen): If we updated AddNewContents() to also pass back the
438   // target url, then we wouldn't need to listen to WebContentsCreated at all.
439   content::WebContents* toolbox_web_contents_;
440   std::unique_ptr<content::WebContents> owned_toolbox_web_contents_;
441 
442   DevToolsUIBindings* bindings_;
443   Browser* browser_;
444 
445   // When DevToolsWindow is docked, it owns main_web_contents_. When it isn't
446   // docked, the tab strip model owns the main_web_contents_.
447   bool is_docked_;
448   class OwnedMainWebContents;
449   std::unique_ptr<OwnedMainWebContents> owned_main_web_contents_;
450 
451   const bool can_dock_;
452   bool close_on_detach_;
453   LifeStage life_stage_;
454   DevToolsToggleAction action_on_load_;
455   DevToolsContentsResizingStrategy contents_resizing_strategy_;
456   // True if we're in the process of handling a beforeunload event originating
457   // from the inspected webcontents, see InterceptPageBeforeUnload for details.
458   bool intercepted_page_beforeunload_;
459   base::OnceClosure load_completed_callback_;
460   base::OnceClosure close_callback_;
461   bool ready_for_test_;
462   base::OnceClosure ready_for_test_callback_;
463 
464   base::TimeTicks inspect_element_start_time_;
465   std::unique_ptr<DevToolsEventForwarder> event_forwarder_;
466   std::unique_ptr<DevToolsEyeDropper> eye_dropper_;
467 
468   class Throttle;
469   Throttle* throttle_ = nullptr;
470   bool open_new_window_for_popups_ = false;
471 
472   base::OnceCallback<void()> reattach_complete_callback_;
473 
474   PrefChangeRegistrar pref_change_registrar_;
475 
476   friend class DevToolsEventForwarder;
477   DISALLOW_COPY_AND_ASSIGN(DevToolsWindow);
478 };
479 
480 #endif  // CHROME_BROWSER_DEVTOOLS_DEVTOOLS_WINDOW_H_
481