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