blob: 23510dff8e38d1db18a94228f239532134af63db [file] [log] [blame]
// Copyright 2018 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.
#include "chrome/browser/resource_coordinator/discard_before_unload_helper.h"
#include "base/bind.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
namespace resource_coordinator {
namespace {
// This is a helper class that determines whether or not there is a beforeunload
// handler associated with a given WebContents, and if so, whether or not it
// indicates that the page contains unsaved state.
//
// This is done by actually running the beforeunload handler if there is one. If
// the beforeunload handler returns a non-empty string then a javascript dialog
// request is made, in which case it is intercepted before it is displayed and
// implicitly canceled.
//
// The beforeunload is initiated via WebContents::DispatchBeforeUnload, and the
// outcome of the beforeunload is monitored via
// WebContentsObserver::BeforeUnloadFired and ::BeforeUnloadDialogCanceled.
//
// Note that the callback is guaranteed to be invoked; in the worst case
// scenario it will be invoked when the WebContents is destroyed, with a
// |proceed|=true value.
class DiscardBeforeUnloadHelper : public content::WebContentsObserver {
public:
static void HasBeforeUnloadHandler(content::WebContents* contents,
HasBeforeUnloadHandlerCallback&& callback);
~DiscardBeforeUnloadHelper() override;
private:
// This is only meant to be called via "new", as the object takes ownership
// of itself.
DiscardBeforeUnloadHelper(content::WebContents* contents,
HasBeforeUnloadHandlerCallback&& callback);
// WebContentsObserver:
void BeforeUnloadFired(bool proceed,
const base::TimeTicks& proceed_time) override;
void BeforeUnloadDialogCancelled() override;
void WebContentsDestroyed() override;
// Responds by invoking the callback, and cleaning itself up.
void Respond(bool has_beforeunload_handler);
// This object keeps itself alive while waiting for a callback, and cleans
// itself up when done.
std::unique_ptr<DiscardBeforeUnloadHelper> self_;
HasBeforeUnloadHandlerCallback callback_;
DISALLOW_COPY_AND_ASSIGN(DiscardBeforeUnloadHelper);
};
void DiscardBeforeUnloadHelper::HasBeforeUnloadHandler(
content::WebContents* contents,
HasBeforeUnloadHandlerCallback&& callback) {
// Create this object and deliberately let go of it. It deletes itself after
// it has invoked the callback.
new DiscardBeforeUnloadHelper(contents, std::move(callback));
}
DiscardBeforeUnloadHelper::DiscardBeforeUnloadHelper(
content::WebContents* contents,
HasBeforeUnloadHandlerCallback&& callback)
: WebContentsObserver(contents),
self_(this),
callback_(std::move(callback)) {
DCHECK(!callback_.is_null());
// NOTE: Ideally this would call NeedToFireBeforeUnload and entirely skip
// on the dispatch if there are no unload handlers installed. Unfortunately,
// NeedToFireBeforeUnload doesn't check the main frame, so this doesn't quite
// work. See this related bug for more information: crbug.com/869956
web_contents()->DispatchBeforeUnload(true /* auto_cancel */);
}
DiscardBeforeUnloadHelper::~DiscardBeforeUnloadHelper() = default;
void DiscardBeforeUnloadHelper::BeforeUnloadFired(
bool proceed,
const base::TimeTicks& proceed_time) {
// |proceed = true| means no beforeunload handler and vice-versa.
Respond(!proceed /* has_beforeunload_handler */);
}
void DiscardBeforeUnloadHelper::BeforeUnloadDialogCancelled() {
// Canceling a beforeunload dialog means that there was a beforeunload handler
// that was already running prior to our request, and more specifically that
// the user wants to stay on the page.
Respond(true /* has_beforeunload_handler */);
}
void DiscardBeforeUnloadHelper::WebContentsDestroyed() {
// If a WebContents is destroyed while waiting for the beforeunload response
// this can be interpreted as |proceed| being true, as the page is no longer
// going to be around to be preserved.
Respond(false /* has_beforeunload_handler */);
}
void DiscardBeforeUnloadHelper::Respond(bool has_beforeunload_handler) {
std::move(callback_).Run(has_beforeunload_handler);
self_.reset();
}
} // namespace
void HasBeforeUnloadHandler(content::WebContents* contents,
HasBeforeUnloadHandlerCallback&& callback) {
DiscardBeforeUnloadHelper::HasBeforeUnloadHandler(contents,
std::move(callback));
}
} // namespace resource_coordinator