blob: fbc5833fe185c55a95a481fd77d7a30a00a6cb01 [file] [log] [blame]
// Copyright 2014 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/ui/website_settings/permission_bubble_manager.h"
#include "base/command_line.h"
#include "base/metrics/user_metrics_action.h"
#include "chrome/browser/ui/website_settings/permission_bubble_request.h"
#include "chrome/common/chrome_switches.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/user_metrics.h"
namespace {
class CancelledRequest : public PermissionBubbleRequest {
public:
explicit CancelledRequest(PermissionBubbleRequest* cancelled)
: icon_(cancelled->GetIconID()),
message_text_(cancelled->GetMessageText()),
message_fragment_(cancelled->GetMessageTextFragment()),
user_gesture_(cancelled->HasUserGesture()),
hostname_(cancelled->GetRequestingHostname()) {}
~CancelledRequest() override {}
int GetIconID() const override { return icon_; }
base::string16 GetMessageText() const override { return message_text_; }
base::string16 GetMessageTextFragment() const override {
return message_fragment_;
}
bool HasUserGesture() const override { return user_gesture_; }
GURL GetRequestingHostname() const override { return hostname_; }
// These are all no-ops since the placeholder is non-forwarding.
void PermissionGranted() override {}
void PermissionDenied() override {}
void Cancelled() override {}
void RequestFinished() override { delete this; }
private:
int icon_;
base::string16 message_text_;
base::string16 message_fragment_;
bool user_gesture_;
GURL hostname_;
};
} // namespace
DEFINE_WEB_CONTENTS_USER_DATA_KEY(PermissionBubbleManager);
// static
bool PermissionBubbleManager::Enabled() {
if (CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnablePermissionsBubbles))
return true;
if (CommandLine::ForCurrentProcess()->HasSwitch(
switches::kDisablePermissionsBubbles))
return false;
return false;
}
PermissionBubbleManager::PermissionBubbleManager(
content::WebContents* web_contents)
: content::WebContentsObserver(web_contents),
bubble_showing_(false),
view_(NULL),
request_url_has_loaded_(false),
customization_mode_(false),
weak_factory_(this) {}
PermissionBubbleManager::~PermissionBubbleManager() {
if (view_ != NULL)
view_->SetDelegate(NULL);
std::vector<PermissionBubbleRequest*>::iterator requests_iter;
for (requests_iter = requests_.begin();
requests_iter != requests_.end();
requests_iter++) {
(*requests_iter)->RequestFinished();
}
for (requests_iter = queued_requests_.begin();
requests_iter != queued_requests_.end();
requests_iter++) {
(*requests_iter)->RequestFinished();
}
}
void PermissionBubbleManager::AddRequest(PermissionBubbleRequest* request) {
content::RecordAction(base::UserMetricsAction("PermissionBubbleRequest"));
// TODO(gbillock): is there a race between an early request on a
// newly-navigated page and the to-be-cleaned-up requests on the previous
// page? We should maybe listen to DidStartNavigationToPendingEntry (and
// any other renderer-side nav initiations?). Double-check this for
// correct behavior on interstitials -- we probably want to basically queue
// any request for which GetVisibleURL != GetLastCommittedURL.
request_url_ = web_contents()->GetLastCommittedURL();
bool is_main_frame =
request->GetRequestingHostname().GetOrigin() == request_url_.GetOrigin();
// Don't re-add an existing request or one with a duplicate text request.
bool same_object = false;
if (ExistingRequest(request, requests_, &same_object) ||
ExistingRequest(request, queued_requests_, &same_object) ||
ExistingRequest(request, queued_frame_requests_, &same_object)) {
if (!same_object)
request->RequestFinished();
return;
}
if (bubble_showing_) {
if (is_main_frame) {
content::RecordAction(
base::UserMetricsAction("PermissionBubbleRequestQueued"));
queued_requests_.push_back(request);
} else {
content::RecordAction(
base::UserMetricsAction("PermissionBubbleIFrameRequestQueued"));
queued_frame_requests_.push_back(request);
}
return;
}
if (is_main_frame) {
requests_.push_back(request);
// TODO(gbillock): do we need to make default state a request property?
accept_states_.push_back(true);
} else {
content::RecordAction(
base::UserMetricsAction("PermissionBubbleIFrameRequestQueued"));
queued_frame_requests_.push_back(request);
}
if (request->HasUserGesture())
ScheduleShowBubble();
}
void PermissionBubbleManager::CancelRequest(PermissionBubbleRequest* request) {
// First look in the queued requests, where we can simply delete the request
// and go on.
std::vector<PermissionBubbleRequest*>::iterator requests_iter;
for (requests_iter = queued_requests_.begin();
requests_iter != queued_requests_.end();
requests_iter++) {
if (*requests_iter == request) {
(*requests_iter)->RequestFinished();
queued_requests_.erase(requests_iter);
return;
}
}
std::vector<bool>::iterator accepts_iter = accept_states_.begin();
for (requests_iter = requests_.begin(), accepts_iter = accept_states_.begin();
requests_iter != requests_.end();
requests_iter++, accepts_iter++) {
if (*requests_iter != request)
continue;
// We can simply erase the current entry in the request table if we aren't
// showing the dialog, or if we are showing it and it can accept the update.
bool can_erase = !bubble_showing_ ||
!view_ || view_->CanAcceptRequestUpdate();
if (can_erase) {
(*requests_iter)->RequestFinished();
requests_.erase(requests_iter);
accept_states_.erase(accepts_iter);
TriggerShowBubble(); // Will redraw the bubble if it is being shown.
return;
}
// Cancel the existing request and replace it with a dummy.
PermissionBubbleRequest* cancelled_request =
new CancelledRequest(*requests_iter);
(*requests_iter)->RequestFinished();
*requests_iter = cancelled_request;
return;
}
NOTREACHED(); // Callers should not cancel requests that are not pending.
}
void PermissionBubbleManager::SetView(PermissionBubbleView* view) {
if (view == view_)
return;
// Disengage from the existing view if there is one.
if (view_ != NULL) {
view_->SetDelegate(NULL);
view_->Hide();
bubble_showing_ = false;
}
view_ = view;
if (!view)
return;
view->SetDelegate(this);
TriggerShowBubble();
}
void PermissionBubbleManager::DocumentOnLoadCompletedInMainFrame() {
request_url_has_loaded_ = true;
// This is scheduled because while all calls to the browser have been
// issued at DOMContentLoaded, they may be bouncing around in scheduled
// callbacks finding the UI thread still. This makes sure we allow those
// scheduled calls to AddRequest to complete before we show the page-load
// permissions bubble.
ScheduleShowBubble();
}
void PermissionBubbleManager::DocumentLoadedInFrame(
content::RenderFrameHost* render_frame_host) {
if (request_url_has_loaded_)
ScheduleShowBubble();
}
void PermissionBubbleManager::NavigationEntryCommitted(
const content::LoadCommittedDetails& details) {
// No permissions requests pending.
if (request_url_.is_empty())
return;
// If we have navigated to a new url or reloaded the page...
// GetAsReferrer strips fragment and username/password, meaning
// the navigation is really to the same page.
if ((request_url_.GetAsReferrer() !=
web_contents()->GetLastCommittedURL().GetAsReferrer()) ||
details.type == content::NAVIGATION_TYPE_EXISTING_PAGE) {
// Kill off existing bubble and cancel any pending requests.
CancelPendingQueues();
FinalizeBubble();
}
}
void PermissionBubbleManager::WebContentsDestroyed() {
// If the web contents has been destroyed, treat the bubble as cancelled.
CancelPendingQueues();
FinalizeBubble();
// The WebContents is going away; be aggressively paranoid and delete
// ourselves lest other parts of the system attempt to add permission bubbles
// or use us otherwise during the destruction.
web_contents()->RemoveUserData(UserDataKey());
// That was the equivalent of "delete this". This object is now destroyed;
// returning from this function is the only safe thing to do.
}
void PermissionBubbleManager::ToggleAccept(int request_index, bool new_value) {
DCHECK(request_index < static_cast<int>(accept_states_.size()));
accept_states_[request_index] = new_value;
}
void PermissionBubbleManager::SetCustomizationMode() {
customization_mode_ = true;
if (view_)
view_->Show(requests_, accept_states_, customization_mode_);
}
void PermissionBubbleManager::Accept() {
std::vector<PermissionBubbleRequest*>::iterator requests_iter;
std::vector<bool>::iterator accepts_iter = accept_states_.begin();
for (requests_iter = requests_.begin(), accepts_iter = accept_states_.begin();
requests_iter != requests_.end();
requests_iter++, accepts_iter++) {
if (*accepts_iter)
(*requests_iter)->PermissionGranted();
else
(*requests_iter)->PermissionDenied();
}
FinalizeBubble();
}
void PermissionBubbleManager::Deny() {
std::vector<PermissionBubbleRequest*>::iterator requests_iter;
for (requests_iter = requests_.begin();
requests_iter != requests_.end();
requests_iter++) {
(*requests_iter)->PermissionDenied();
}
FinalizeBubble();
}
void PermissionBubbleManager::Closing() {
std::vector<PermissionBubbleRequest*>::iterator requests_iter;
for (requests_iter = requests_.begin();
requests_iter != requests_.end();
requests_iter++) {
(*requests_iter)->Cancelled();
}
FinalizeBubble();
}
void PermissionBubbleManager::ScheduleShowBubble() {
content::BrowserThread::PostTask(
content::BrowserThread::UI,
FROM_HERE,
base::Bind(&PermissionBubbleManager::TriggerShowBubble,
weak_factory_.GetWeakPtr()));
}
void PermissionBubbleManager::TriggerShowBubble() {
if (!view_)
return;
if (bubble_showing_)
return;
if (!request_url_has_loaded_)
return;
if (requests_.empty() && queued_requests_.empty() &&
queued_frame_requests_.empty()) {
return;
}
if (requests_.empty()) {
// Queues containing a user-gesture-generated request have priority.
if (HasUserGestureRequest(queued_requests_))
requests_.swap(queued_requests_);
else if (HasUserGestureRequest(queued_frame_requests_))
requests_.swap(queued_frame_requests_);
else if (queued_requests_.size())
requests_.swap(queued_requests_);
else
requests_.swap(queued_frame_requests_);
// Sets the default value for each request to be 'accept'.
// TODO(leng): Currently all requests default to true. If that changes:
// a) Add additional accept_state queues to store default values.
// b) Change the request API to provide the default value.
accept_states_.resize(requests_.size(), true);
}
// Note: this should appear above Show() for testing, since in that
// case we may do in-line calling of finalization.
bubble_showing_ = true;
view_->Show(requests_, accept_states_, customization_mode_);
}
void PermissionBubbleManager::FinalizeBubble() {
if (view_)
view_->Hide();
bubble_showing_ = false;
std::vector<PermissionBubbleRequest*>::iterator requests_iter;
for (requests_iter = requests_.begin();
requests_iter != requests_.end();
requests_iter++) {
(*requests_iter)->RequestFinished();
}
requests_.clear();
accept_states_.clear();
if (queued_requests_.size() || queued_frame_requests_.size())
TriggerShowBubble();
else
request_url_ = GURL();
}
void PermissionBubbleManager::CancelPendingQueues() {
std::vector<PermissionBubbleRequest*>::iterator requests_iter;
for (requests_iter = queued_requests_.begin();
requests_iter != queued_requests_.end();
requests_iter++) {
(*requests_iter)->RequestFinished();
}
for (requests_iter = queued_frame_requests_.begin();
requests_iter != queued_frame_requests_.end();
requests_iter++) {
(*requests_iter)->RequestFinished();
}
queued_requests_.clear();
queued_frame_requests_.clear();
}
bool PermissionBubbleManager::ExistingRequest(
PermissionBubbleRequest* request,
const std::vector<PermissionBubbleRequest*>& queue,
bool* same_object) {
CHECK(same_object);
*same_object = false;
std::vector<PermissionBubbleRequest*>::const_iterator iter;
for (iter = queue.begin(); iter != queue.end(); iter++) {
if (*iter == request) {
*same_object = true;
return true;
}
if ((*iter)->GetMessageTextFragment() ==
request->GetMessageTextFragment() &&
(*iter)->GetRequestingHostname() == request->GetRequestingHostname()) {
return true;
}
}
return false;
}
bool PermissionBubbleManager::HasUserGestureRequest(
const std::vector<PermissionBubbleRequest*>& queue) {
std::vector<PermissionBubbleRequest*>::const_iterator iter;
for (iter = queue.begin(); iter != queue.end(); iter++) {
if ((*iter)->HasUserGesture())
return true;
}
return false;
}