1// Copyright 2013 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#include "chrome/browser/ui/fast_unload_controller.h"
6
7#include "base/logging.h"
8#include "base/message_loop/message_loop.h"
9#include "chrome/browser/chrome_notification_types.h"
10#include "chrome/browser/devtools/devtools_window.h"
11#include "chrome/browser/ui/browser.h"
12#include "chrome/browser/ui/browser_tabstrip.h"
13#include "chrome/browser/ui/tab_contents/core_tab_helper.h"
14#include "chrome/browser/ui/tabs/tab_strip_model.h"
15#include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
16#include "content/public/browser/notification_service.h"
17#include "content/public/browser/notification_source.h"
18#include "content/public/browser/notification_types.h"
19#include "content/public/browser/render_view_host.h"
20#include "content/public/browser/web_contents.h"
21#include "content/public/browser/web_contents_delegate.h"
22
23namespace chrome {
24
25
26////////////////////////////////////////////////////////////////////////////////
27// DetachedWebContentsDelegate will delete web contents when they close.
28class FastUnloadController::DetachedWebContentsDelegate
29    : public content::WebContentsDelegate {
30 public:
31  DetachedWebContentsDelegate() { }
32  virtual ~DetachedWebContentsDelegate() { }
33
34 private:
35  // WebContentsDelegate implementation.
36  virtual bool ShouldSuppressDialogs() OVERRIDE {
37    return true;  // Return true so dialogs are suppressed.
38  }
39
40  virtual void CloseContents(content::WebContents* source) OVERRIDE {
41    // Finished detached close.
42    // FastUnloadController will observe
43    // |NOTIFICATION_WEB_CONTENTS_DISCONNECTED|.
44    delete source;
45  }
46
47  DISALLOW_COPY_AND_ASSIGN(DetachedWebContentsDelegate);
48};
49
50////////////////////////////////////////////////////////////////////////////////
51// FastUnloadController, public:
52
53FastUnloadController::FastUnloadController(Browser* browser)
54    : browser_(browser),
55      tab_needing_before_unload_ack_(NULL),
56      is_attempting_to_close_browser_(false),
57      detached_delegate_(new DetachedWebContentsDelegate()),
58      weak_factory_(this) {
59  browser_->tab_strip_model()->AddObserver(this);
60}
61
62FastUnloadController::~FastUnloadController() {
63  browser_->tab_strip_model()->RemoveObserver(this);
64}
65
66bool FastUnloadController::CanCloseContents(content::WebContents* contents) {
67  // Don't try to close the tab when the whole browser is being closed, since
68  // that avoids the fast shutdown path where we just kill all the renderers.
69  return !is_attempting_to_close_browser_ ||
70      is_calling_before_unload_handlers();
71}
72
73// static
74bool FastUnloadController::ShouldRunUnloadEventsHelper(
75    content::WebContents* contents) {
76  // If |contents| is being inspected, devtools needs to intercept beforeunload
77  // events.
78  return DevToolsWindow::GetInstanceForInspectedRenderViewHost(
79      contents->GetRenderViewHost()) != NULL;
80}
81
82// static
83bool FastUnloadController::RunUnloadEventsHelper(
84    content::WebContents* contents) {
85  // If there's a devtools window attached to |contents|,
86  // we would like devtools to call its own beforeunload handlers first,
87  // and then call beforeunload handlers for |contents|.
88  // See DevToolsWindow::InterceptPageBeforeUnload for details.
89  if (DevToolsWindow::InterceptPageBeforeUnload(contents)) {
90    return true;
91  }
92  // If the WebContents is not connected yet, then there's no unload
93  // handler we can fire even if the WebContents has an unload listener.
94  // One case where we hit this is in a tab that has an infinite loop
95  // before load.
96  if (contents->NeedToFireBeforeUnload()) {
97    // If the page has unload listeners, then we tell the renderer to fire
98    // them. Once they have fired, we'll get a message back saying whether
99    // to proceed closing the page or not, which sends us back to this method
100    // with the NeedToFireBeforeUnload bit cleared.
101    contents->GetRenderViewHost()->FirePageBeforeUnload(false);
102    return true;
103  }
104  return false;
105}
106
107bool FastUnloadController::BeforeUnloadFired(content::WebContents* contents,
108                                             bool proceed) {
109  if (!proceed)
110    DevToolsWindow::OnPageCloseCanceled(contents);
111
112  if (!is_attempting_to_close_browser_) {
113    if (!proceed) {
114      contents->SetClosedByUserGesture(false);
115    } else {
116      // No more dialogs are possible, so remove the tab and finish
117      // running unload listeners asynchrounously.
118      browser_->tab_strip_model()->delegate()->CreateHistoricalTab(contents);
119      DetachWebContents(contents);
120    }
121    return proceed;
122  }
123
124  if (!proceed) {
125    CancelWindowClose();
126    contents->SetClosedByUserGesture(false);
127    return false;
128  }
129
130  if (tab_needing_before_unload_ack_ == contents) {
131    // Now that beforeunload has fired, queue the tab to fire unload.
132    tab_needing_before_unload_ack_ = NULL;
133    tabs_needing_unload_.insert(contents);
134    ProcessPendingTabs();
135    // We want to handle firing the unload event ourselves since we want to
136    // fire all the beforeunload events before attempting to fire the unload
137    // events should the user cancel closing the browser.
138    return false;
139  }
140
141  return true;
142}
143
144bool FastUnloadController::ShouldCloseWindow() {
145  if (HasCompletedUnloadProcessing())
146    return true;
147
148  // Special case for when we quit an application. The Devtools window can
149  // close if it's beforeunload event has already fired which will happen due
150  // to the interception of it's content's beforeunload.
151  if (browser_->is_devtools() &&
152      DevToolsWindow::HasFiredBeforeUnloadEventForDevToolsBrowser(browser_)) {
153    return true;
154  }
155
156  // The behavior followed here varies based on the current phase of the
157  // operation and whether a batched shutdown is in progress.
158  //
159  // If there are tabs with outstanding beforeunload handlers:
160  // 1. If a batched shutdown is in progress: return false.
161  //    This is to prevent interference with batched shutdown already in
162  //    progress.
163  // 2. Otherwise: start sending beforeunload events and return false.
164  //
165  // Otherwise, If there are no tabs with outstanding beforeunload handlers:
166  // 3. If a batched shutdown is in progress: start sending unload events and
167  //    return false.
168  // 4. Otherwise: return true.
169  is_attempting_to_close_browser_ = true;
170  // Cases 1 and 4.
171  bool need_beforeunload_fired = TabsNeedBeforeUnloadFired();
172  if (need_beforeunload_fired == is_calling_before_unload_handlers())
173    return !need_beforeunload_fired;
174
175  // Cases 2 and 3.
176  on_close_confirmed_.Reset();
177  ProcessPendingTabs();
178  return false;
179}
180
181bool FastUnloadController::CallBeforeUnloadHandlers(
182    const base::Callback<void(bool)>& on_close_confirmed) {
183// The devtools browser gets its beforeunload events as the results of
184// intercepting events from the inspected tab, so don't send them here as well.
185  if (browser_->is_devtools() || !TabsNeedBeforeUnloadFired())
186    return false;
187
188  on_close_confirmed_ = on_close_confirmed;
189  is_attempting_to_close_browser_ = true;
190  ProcessPendingTabs();
191  return true;
192}
193
194void FastUnloadController::ResetBeforeUnloadHandlers() {
195  if (!is_calling_before_unload_handlers())
196    return;
197  CancelWindowClose();
198}
199
200bool FastUnloadController::TabsNeedBeforeUnloadFired() {
201  if (!tabs_needing_before_unload_.empty() ||
202      tab_needing_before_unload_ack_ != NULL)
203    return true;
204
205  if (!is_calling_before_unload_handlers() && !tabs_needing_unload_.empty())
206    return false;
207
208  for (int i = 0; i < browser_->tab_strip_model()->count(); ++i) {
209    content::WebContents* contents =
210        browser_->tab_strip_model()->GetWebContentsAt(i);
211    bool should_fire_beforeunload = contents->NeedToFireBeforeUnload() ||
212        DevToolsWindow::NeedsToInterceptBeforeUnload(contents);
213    if (!ContainsKey(tabs_needing_unload_, contents) &&
214        !ContainsKey(tabs_needing_unload_ack_, contents) &&
215        tab_needing_before_unload_ack_ != contents &&
216        should_fire_beforeunload)
217      tabs_needing_before_unload_.insert(contents);
218  }
219  return !tabs_needing_before_unload_.empty();
220}
221
222////////////////////////////////////////////////////////////////////////////////
223// FastUnloadController, content::NotificationObserver implementation:
224
225void FastUnloadController::Observe(
226      int type,
227      const content::NotificationSource& source,
228      const content::NotificationDetails& details) {
229  switch (type) {
230    case content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED: {
231      registrar_.Remove(this,
232                        content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
233                        source);
234      content::WebContents* contents =
235          content::Source<content::WebContents>(source).ptr();
236      ClearUnloadState(contents);
237      break;
238    }
239    default:
240      NOTREACHED() << "Got a notification we didn't register for.";
241  }
242}
243
244////////////////////////////////////////////////////////////////////////////////
245// FastUnloadController, TabStripModelObserver implementation:
246
247void FastUnloadController::TabInsertedAt(content::WebContents* contents,
248                                         int index,
249                                         bool foreground) {
250  TabAttachedImpl(contents);
251}
252
253void FastUnloadController::TabDetachedAt(content::WebContents* contents,
254                                         int index) {
255  TabDetachedImpl(contents);
256}
257
258void FastUnloadController::TabReplacedAt(TabStripModel* tab_strip_model,
259                                         content::WebContents* old_contents,
260                                         content::WebContents* new_contents,
261                                         int index) {
262  TabDetachedImpl(old_contents);
263  TabAttachedImpl(new_contents);
264}
265
266void FastUnloadController::TabStripEmpty() {
267  // Set is_attempting_to_close_browser_ here, so that extensions, etc, do not
268  // attempt to add tabs to the browser before it closes.
269  is_attempting_to_close_browser_ = true;
270}
271
272////////////////////////////////////////////////////////////////////////////////
273// FastUnloadController, private:
274
275void FastUnloadController::TabAttachedImpl(content::WebContents* contents) {
276  // If the tab crashes in the beforeunload or unload handler, it won't be
277  // able to ack. But we know we can close it.
278  registrar_.Add(
279      this,
280      content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
281      content::Source<content::WebContents>(contents));
282}
283
284void FastUnloadController::TabDetachedImpl(content::WebContents* contents) {
285  if (tabs_needing_unload_ack_.find(contents) !=
286      tabs_needing_unload_ack_.end()) {
287    // Tab needs unload to complete.
288    // It will send |NOTIFICATION_WEB_CONTENTS_DISCONNECTED| when done.
289    return;
290  }
291
292  // If WEB_CONTENTS_DISCONNECTED was received then the notification may have
293  // already been unregistered.
294  const content::NotificationSource& source =
295      content::Source<content::WebContents>(contents);
296  if (registrar_.IsRegistered(this,
297                              content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
298                              source)) {
299    registrar_.Remove(this,
300                      content::NOTIFICATION_WEB_CONTENTS_DISCONNECTED,
301                      source);
302  }
303
304  if (is_attempting_to_close_browser_)
305    ClearUnloadState(contents);
306}
307
308bool FastUnloadController::DetachWebContents(content::WebContents* contents) {
309  int index = browser_->tab_strip_model()->GetIndexOfWebContents(contents);
310  if (index != TabStripModel::kNoTab &&
311      contents->NeedToFireBeforeUnload()) {
312    tabs_needing_unload_ack_.insert(contents);
313    browser_->tab_strip_model()->DetachWebContentsAt(index);
314    contents->SetDelegate(detached_delegate_.get());
315    CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
316    core_tab_helper->OnUnloadDetachedStarted();
317    return true;
318  }
319  return false;
320}
321
322void FastUnloadController::ProcessPendingTabs() {
323  if (!is_attempting_to_close_browser_) {
324    // Because we might invoke this after a delay it's possible for the value of
325    // is_attempting_to_close_browser_ to have changed since we scheduled the
326    // task.
327    return;
328  }
329
330  if (tab_needing_before_unload_ack_ != NULL) {
331    // Wait for |BeforeUnloadFired| before proceeding.
332    return;
333  }
334
335  // Process a beforeunload handler.
336  if (!tabs_needing_before_unload_.empty()) {
337    WebContentsSet::iterator it = tabs_needing_before_unload_.begin();
338    content::WebContents* contents = *it;
339    tabs_needing_before_unload_.erase(it);
340    // Null check render_view_host here as this gets called on a PostTask and
341    // the tab's render_view_host may have been nulled out.
342    if (contents->GetRenderViewHost()) {
343      tab_needing_before_unload_ack_ = contents;
344
345      CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
346      core_tab_helper->OnCloseStarted();
347
348      // If there's a devtools window attached to |contents|,
349      // we would like devtools to call its own beforeunload handlers first,
350      // and then call beforeunload handlers for |contents|.
351      // See DevToolsWindow::InterceptPageBeforeUnload for details.
352      if (!DevToolsWindow::InterceptPageBeforeUnload(contents))
353        contents->GetRenderViewHost()->FirePageBeforeUnload(false);
354    } else {
355      ProcessPendingTabs();
356    }
357    return;
358  }
359
360  if (is_calling_before_unload_handlers()) {
361    on_close_confirmed_.Run(true);
362    return;
363  }
364  // Process all the unload handlers. (The beforeunload handlers have finished.)
365  if (!tabs_needing_unload_.empty()) {
366    browser_->OnWindowClosing();
367
368    // Run unload handlers detached since no more interaction is possible.
369    WebContentsSet::iterator it = tabs_needing_unload_.begin();
370    while (it != tabs_needing_unload_.end()) {
371      WebContentsSet::iterator current = it++;
372      content::WebContents* contents = *current;
373      tabs_needing_unload_.erase(current);
374      // Null check render_view_host here as this gets called on a PostTask
375      // and the tab's render_view_host may have been nulled out.
376      if (contents->GetRenderViewHost()) {
377        CoreTabHelper* core_tab_helper =
378            CoreTabHelper::FromWebContents(contents);
379        core_tab_helper->OnUnloadStarted();
380        DetachWebContents(contents);
381        contents->GetRenderViewHost()->ClosePage();
382      }
383    }
384
385    // Get the browser hidden.
386    if (browser_->tab_strip_model()->empty()) {
387      browser_->TabStripEmpty();
388    } else {
389      browser_->tab_strip_model()->CloseAllTabs();  // tabs not needing unload
390    }
391    return;
392  }
393
394  if (HasCompletedUnloadProcessing()) {
395    browser_->OnWindowClosing();
396
397    // Get the browser closed.
398    if (browser_->tab_strip_model()->empty()) {
399      browser_->TabStripEmpty();
400    } else {
401      // There may be tabs if the last tab needing beforeunload crashed.
402      browser_->tab_strip_model()->CloseAllTabs();
403    }
404    return;
405  }
406}
407
408bool FastUnloadController::HasCompletedUnloadProcessing() const {
409  return is_attempting_to_close_browser_ &&
410      tabs_needing_before_unload_.empty() &&
411      tab_needing_before_unload_ack_ == NULL &&
412      tabs_needing_unload_.empty() &&
413      tabs_needing_unload_ack_.empty();
414}
415
416void FastUnloadController::CancelWindowClose() {
417  // Closing of window can be canceled from a beforeunload handler.
418  DCHECK(is_attempting_to_close_browser_);
419  tabs_needing_before_unload_.clear();
420  if (tab_needing_before_unload_ack_ != NULL) {
421    CoreTabHelper* core_tab_helper =
422        CoreTabHelper::FromWebContents(tab_needing_before_unload_ack_);
423    core_tab_helper->OnCloseCanceled();
424    DevToolsWindow::OnPageCloseCanceled(tab_needing_before_unload_ack_);
425    tab_needing_before_unload_ack_ = NULL;
426  }
427  for (WebContentsSet::iterator it = tabs_needing_unload_.begin();
428       it != tabs_needing_unload_.end(); it++) {
429    content::WebContents* contents = *it;
430
431    CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(contents);
432    core_tab_helper->OnCloseCanceled();
433    DevToolsWindow::OnPageCloseCanceled(contents);
434  }
435  tabs_needing_unload_.clear();
436
437  // No need to clear tabs_needing_unload_ack_. Those tabs are already detached.
438
439  if (is_calling_before_unload_handlers()) {
440    base::Callback<void(bool)> on_close_confirmed = on_close_confirmed_;
441    on_close_confirmed_.Reset();
442    on_close_confirmed.Run(false);
443  }
444
445  is_attempting_to_close_browser_ = false;
446
447  content::NotificationService::current()->Notify(
448      chrome::NOTIFICATION_BROWSER_CLOSE_CANCELLED,
449      content::Source<Browser>(browser_),
450      content::NotificationService::NoDetails());
451}
452
453void FastUnloadController::ClearUnloadState(content::WebContents* contents) {
454  if (tabs_needing_unload_ack_.erase(contents) > 0) {
455    if (HasCompletedUnloadProcessing())
456      PostTaskForProcessPendingTabs();
457    return;
458  }
459
460  if (!is_attempting_to_close_browser_)
461    return;
462
463  if (tab_needing_before_unload_ack_ == contents) {
464    tab_needing_before_unload_ack_ = NULL;
465    PostTaskForProcessPendingTabs();
466    return;
467  }
468
469  if (tabs_needing_before_unload_.erase(contents) > 0 ||
470      tabs_needing_unload_.erase(contents) > 0) {
471    if (tab_needing_before_unload_ack_ == NULL)
472      PostTaskForProcessPendingTabs();
473  }
474}
475
476void FastUnloadController::PostTaskForProcessPendingTabs() {
477  base::MessageLoop::current()->PostTask(
478      FROM_HERE,
479      base::Bind(&FastUnloadController::ProcessPendingTabs,
480                 weak_factory_.GetWeakPtr()));
481}
482
483}  // namespace chrome
484