// 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/field_trial.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 { // String constants to control whether bubbles are enabled by default. const char kTrialName[] = "PermissionBubbleRollout"; const char kEnabled[] = "Enabled"; const char kDisabled[] = "Disabled"; 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 // PermissionBubbleManager::Observer ------------------------------------------- PermissionBubbleManager::Observer::~Observer() { } void PermissionBubbleManager::Observer::OnBubbleAdded() { } // PermissionBubbleManager ----------------------------------------------------- DEFINE_WEB_CONTENTS_USER_DATA_KEY(PermissionBubbleManager); // static bool PermissionBubbleManager::Enabled() { // Command line flags take precedence. if (base::CommandLine::ForCurrentProcess()->HasSwitch( switches::kEnablePermissionsBubbles)) return true; if (base::CommandLine::ForCurrentProcess()->HasSwitch( switches::kDisablePermissionsBubbles)) return false; std::string group(base::FieldTrialList::FindFullName(kTrialName)); if (group == kEnabled) return true; if (group == kDisabled) return false; return false; } PermissionBubbleManager::PermissionBubbleManager( content::WebContents* web_contents) : content::WebContentsObserver(web_contents), require_user_gesture_(false), bubble_showing_(false), view_(NULL), request_url_has_loaded_(false), auto_response_for_test_(NONE), weak_factory_(this) { } PermissionBubbleManager::~PermissionBubbleManager() { if (view_ != NULL) view_->SetDelegate(NULL); std::vector::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 (!require_user_gesture_ || 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::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::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::RequireUserGesture(bool required) { require_user_gesture_ = required; } 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(accept_states_.size())); accept_states_[request_index] = new_value; } void PermissionBubbleManager::Accept() { std::vector::iterator requests_iter; std::vector::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::iterator requests_iter; for (requests_iter = requests_.begin(); requests_iter != requests_.end(); requests_iter++) { (*requests_iter)->PermissionDenied(); } FinalizeBubble(); } void PermissionBubbleManager::Closing() { std::vector::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_); NotifyBubbleAdded(); // If in testing mode, automatically respond to the bubble that was shown. if (auto_response_for_test_ != NONE) DoAutoResponseForTesting(); } void PermissionBubbleManager::FinalizeBubble() { if (view_) view_->Hide(); bubble_showing_ = false; std::vector::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::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& queue, bool* same_object) { CHECK(same_object); *same_object = false; std::vector::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& queue) { std::vector::const_iterator iter; for (iter = queue.begin(); iter != queue.end(); iter++) { if ((*iter)->HasUserGesture()) return true; } return false; } void PermissionBubbleManager::AddObserver(Observer* observer) { observer_list_.AddObserver(observer); } void PermissionBubbleManager::RemoveObserver(Observer* observer) { observer_list_.RemoveObserver(observer); } void PermissionBubbleManager::NotifyBubbleAdded() { FOR_EACH_OBSERVER(Observer, observer_list_, OnBubbleAdded()); } void PermissionBubbleManager::DoAutoResponseForTesting() { switch (auto_response_for_test_) { case ACCEPT_ALL: Accept(); break; case DENY_ALL: Deny(); break; case DISMISS: Closing(); break; case NONE: NOTREACHED(); } }