| // 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 "components/permissions/permission_request_manager.h" |
| |
| #include <algorithm> |
| #include <string> |
| |
| #include "base/auto_reset.h" |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/containers/circular_deque.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/observer_list.h" |
| #include "base/stl_util.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "build/build_config.h" |
| #include "components/autofill_assistant/browser/public/runtime_manager.h" |
| #include "components/back_forward_cache/back_forward_cache_disable.h" |
| #include "components/permissions/features.h" |
| #include "components/permissions/permission_decision_auto_blocker.h" |
| #include "components/permissions/permission_prompt.h" |
| #include "components/permissions/permission_request.h" |
| #include "components/permissions/permission_request_id.h" |
| #include "components/permissions/permissions_client.h" |
| #include "components/permissions/request_type.h" |
| #include "components/permissions/switches.h" |
| #include "content/public/browser/back_forward_cache.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/disallow_activation_reason.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "components/permissions/android/android_permission_util.h" |
| #endif |
| |
| namespace permissions { |
| |
| const char kAbusiveNotificationRequestsEnforcementMessage[] = |
| "Chrome is blocking notification permission requests on this site because " |
| "the site tends to show permission requests that mislead, trick, or force " |
| "users into allowing notifications. You should fix the issues as soon as " |
| "possible and submit your site for another review. Learn more at " |
| "https://support.google.com/webtools/answer/9799048."; |
| |
| const char kAbusiveNotificationRequestsWarningMessage[] = |
| "Chrome might start blocking notification permission requests on this site " |
| "in the future because the site tends to show permission requests that " |
| "mislead, trick, or force users into allowing notifications. You should " |
| "fix the issues as soon as possible and submit your site for another " |
| "review. Learn more at https://support.google.com/webtools/answer/9799048."; |
| |
| constexpr char kAbusiveNotificationContentEnforcementMessage[] = |
| "Chrome is blocking notification permission requests on this site because " |
| "the site tends to show notifications with content that mislead or trick " |
| "users. You should fix the issues as soon as possible and submit your site " |
| "for another review. Learn more at " |
| "https://support.google.com/webtools/answer/9799048"; |
| |
| constexpr char kAbusiveNotificationContentWarningMessage[] = |
| "Chrome might start blocking notification permission requests on this site " |
| "in the future because the site tends to show notifications with content " |
| "that mislead or trick users. You should fix the issues as soon as " |
| "possible and submit your site for another review. Learn more at " |
| "https://support.google.com/webtools/answer/9799048"; |
| |
| namespace { |
| |
| // When there are multiple permissions requests in `queued_requests_`, we try to |
| // reorder them based on the acceptance rates. Notifications and Geolocations |
| // have one of the lowest acceptance, hence they have the lowest priority and |
| // will be shown the last. |
| bool IsLowPriorityRequest(PermissionRequest* request) { |
| return request->request_type() == RequestType::kNotifications || |
| request->request_type() == RequestType::kGeolocation; |
| } |
| |
| // In case of multiple permission requests that use chip UI, a newly added |
| // request will preempt the currently showing request, which is put back to the |
| // queue, and will be shown later. To reduce user annoyance, if a quiet chip |
| // permission prompt was displayed longer than `kQuietChipIgnoreTimeout`, we |
| // consider it as shown long enough and it will not be shown again after it is |
| // preempted. |
| // TODO(crbug.com/1221083): If a user switched tabs, do not include that time as |
| // "shown". |
| bool ShouldShowQuietRequestAgainIfPreempted( |
| absl::optional<base::Time> request_display_start_time) { |
| if (request_display_start_time->is_null()) { |
| return true; |
| } |
| |
| static constexpr base::TimeDelta kQuietChipIgnoreTimeout = base::Seconds(8.5); |
| return base::Time::Now() - request_display_start_time.value() < |
| kQuietChipIgnoreTimeout; |
| } |
| |
| bool IsMediaRequest(RequestType type) { |
| #if !BUILDFLAG(IS_ANDROID) |
| if (type == RequestType::kCameraPanTiltZoom) |
| return true; |
| #endif |
| return type == RequestType::kMicStream || type == RequestType::kCameraStream; |
| } |
| |
| bool IsArOrCameraRequest(RequestType type) { |
| return type == RequestType::kArSession || type == RequestType::kCameraStream; |
| } |
| |
| bool ShouldGroupRequests(PermissionRequest* a, PermissionRequest* b) { |
| if (a->requesting_origin() != b->requesting_origin()) |
| return false; |
| |
| // Group if both requests are media requests. |
| if (IsMediaRequest(a->request_type()) && IsMediaRequest(b->request_type())) { |
| return true; |
| } |
| |
| // Group if the requests are an AR and a Camera Access request. |
| if (IsArOrCameraRequest(a->request_type()) && |
| IsArOrCameraRequest(b->request_type())) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| } // namespace |
| |
| // PermissionRequestManager ---------------------------------------------------- |
| |
| bool PermissionRequestManager::PermissionRequestSource:: |
| IsSourceFrameInactiveAndDisallowActivation() const { |
| content::RenderFrameHost* rfh = |
| content::RenderFrameHost::FromID(render_process_id, render_frame_id); |
| return !rfh || |
| rfh->IsInactiveAndDisallowActivation( |
| content::DisallowActivationReasonId::kPermissionRequestSource); |
| } |
| |
| PermissionRequestManager::~PermissionRequestManager() { |
| DCHECK(!IsRequestInProgress()); |
| DCHECK(duplicate_requests_.empty()); |
| DCHECK(queued_requests_.empty()); |
| } |
| |
| void PermissionRequestManager::AddRequest( |
| content::RenderFrameHost* source_frame, |
| PermissionRequest* request) { |
| DCHECK(source_frame); |
| DCHECK_EQ(content::WebContents::FromRenderFrameHost(source_frame), |
| web_contents()); |
| |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kDenyPermissionPrompts)) { |
| request->PermissionDenied(); |
| request->RequestFinished(); |
| return; |
| } |
| |
| if (source_frame->IsInactiveAndDisallowActivation( |
| content::DisallowActivationReasonId::kPermissionAddRequest)) { |
| request->Cancelled(); |
| request->RequestFinished(); |
| return; |
| } |
| |
| if (source_frame->IsNestedWithinFencedFrame()) { |
| request->Cancelled(); |
| request->RequestFinished(); |
| return; |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| if (request->GetContentSettingsType() == ContentSettingsType::NOTIFICATIONS) { |
| bool app_level_settings_allow_site_notifications = |
| enabled_app_level_notification_permission_for_testing_.has_value() |
| ? enabled_app_level_notification_permission_for_testing_.value() |
| : DoesAppLevelSettingsAllowSiteNotifications(); |
| base::UmaHistogramBoolean( |
| "Permissions.Prompt.Notifications.EnabledAppLevel", |
| app_level_settings_allow_site_notifications); |
| |
| if (!app_level_settings_allow_site_notifications && |
| base::FeatureList::IsEnabled( |
| features::kBlockNotificationPromptsIfDisabledOnAppLevel)) { |
| // Automatically cancel site Notification requests when Chrome is not able |
| // to send notifications in an app level. |
| request->Cancelled(); |
| request->RequestFinished(); |
| return; |
| } |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| if (is_notification_prompt_cooldown_active_ && |
| request->GetContentSettingsType() == ContentSettingsType::NOTIFICATIONS) { |
| // Short-circuit by canceling rather than denying to avoid creating a large |
| // number of content setting exceptions on Desktop / disabled notification |
| // channels on Android. |
| request->Cancelled(); |
| request->RequestFinished(); |
| return; |
| } |
| |
| if (!web_contents_supports_permission_requests_) { |
| request->Cancelled(); |
| request->RequestFinished(); |
| return; |
| } |
| |
| // TODO(tsergeant): change the UMA to no longer mention bubbles. |
| base::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. |
| CHECK_EQ(source_frame->GetMainFrame(), web_contents()->GetMainFrame()); |
| const GURL main_frame_origin = |
| PermissionUtil::GetLastCommittedOriginAsURL(source_frame->GetMainFrame()); |
| bool is_main_frame = |
| url::IsSameOriginWith(main_frame_origin, request->requesting_origin()); |
| |
| absl::optional<url::Origin> auto_approval_origin = |
| PermissionsClient::Get()->GetAutoApprovalOrigin(); |
| if (auto_approval_origin) { |
| if (url::Origin::Create(request->requesting_origin()) == |
| auto_approval_origin.value()) { |
| request->PermissionGranted(/*is_one_time=*/false); |
| } |
| request->RequestFinished(); |
| return; |
| } |
| |
| // Cancel permission requests wile Autofill Assistant's UI is shown. |
| auto* assistant_runtime_manager = |
| autofill_assistant::RuntimeManager::GetForWebContents(web_contents()); |
| if (assistant_runtime_manager && assistant_runtime_manager->GetState() == |
| autofill_assistant::UIState::kShown) { |
| request->Cancelled(); |
| request->RequestFinished(); |
| return; |
| } |
| |
| // Don't re-add an existing request or one with a duplicate text request. |
| PermissionRequest* existing_request = GetExistingRequest(request); |
| if (existing_request) { |
| // |request| is a duplicate. Add it to |duplicate_requests_| unless it's the |
| // same object as |existing_request| or an existing duplicate. |
| if (request == existing_request) |
| return; |
| auto range = duplicate_requests_.equal_range(existing_request); |
| for (auto it = range.first; it != range.second; ++it) { |
| if (request == it->second) |
| return; |
| } |
| duplicate_requests_.insert(std::make_pair(existing_request, request)); |
| return; |
| } |
| |
| if (is_main_frame) { |
| if (IsRequestInProgress()) { |
| base::RecordAction( |
| base::UserMetricsAction("PermissionBubbleRequestQueued")); |
| } |
| } else { |
| base::RecordAction( |
| base::UserMetricsAction("PermissionBubbleIFrameRequestQueued")); |
| } |
| |
| CurrentRequestFate current_request_fate = |
| GetCurrentRequestFateInFaceOfNewRequest(request); |
| |
| if (current_request_fate == CurrentRequestFate::Preempt) { |
| PreemptAndRequeueCurrentRequest(); |
| } |
| |
| QueueRequest(source_frame, request); |
| |
| if (current_request_fate == CurrentRequestFate::Finalize) { |
| // FinalizeCurrentRequests will call ScheduleDequeueRequest on its own. |
| FinalizeCurrentRequests(PermissionAction::IGNORED); |
| } else { |
| ScheduleDequeueRequestIfNeeded(); |
| } |
| } |
| |
| PermissionRequestManager::CurrentRequestFate |
| PermissionRequestManager::GetCurrentRequestFateInFaceOfNewRequest( |
| PermissionRequest* new_request) { |
| if (base::FeatureList::IsEnabled(features::kPermissionQuietChip)) { |
| if (ShouldCurrentRequestUseQuietUI() && |
| !ShouldShowQuietRequestAgainIfPreempted( |
| current_request_first_display_time_)) { |
| return CurrentRequestFate::Finalize; |
| } |
| |
| if (base::FeatureList::IsEnabled(features::kPermissionChip)) { |
| // Preempt current request if it is a quiet UI request or it is not |
| // Notifications or Geolocation. |
| if (ShouldCurrentRequestUseQuietUI() || |
| !IsLowPriorityRequest(new_request)) { |
| return CurrentRequestFate::Preempt; |
| } |
| } else { |
| if (ShouldCurrentRequestUseQuietUI()) { |
| return CurrentRequestFate::Preempt; |
| } |
| } |
| } else { |
| if (base::FeatureList::IsEnabled(features::kPermissionChip)) { |
| return CurrentRequestFate::Preempt; |
| } else if (ShouldCurrentRequestUseQuietUI()) { |
| // If we're displaying a quiet permission request, ignore it in favor of a |
| // new permission request. |
| return CurrentRequestFate::Finalize; |
| } |
| } |
| |
| return CurrentRequestFate::KeepCurrent; |
| } |
| |
| void PermissionRequestManager::QueueRequest( |
| content::RenderFrameHost* source_frame, |
| PermissionRequest* request) { |
| PushQueuedRequest(request); |
| request_sources_map_.emplace( |
| request, PermissionRequestSource({source_frame->GetProcess()->GetID(), |
| source_frame->GetRoutingID()})); |
| } |
| |
| void PermissionRequestManager::PreemptAndRequeueCurrentRequest() { |
| ResetViewStateForCurrentRequest(); |
| for (auto* current_request : requests_) { |
| PushQueuedRequest(current_request); |
| } |
| requests_.clear(); |
| } |
| |
| void PermissionRequestManager::UpdateAnchor() { |
| if (view_) { |
| // When the prompt's anchor is being updated, the prompt view can be |
| // recreated for the new browser. Because of that, ignore prompt callbacks |
| // while doing that. |
| base::AutoReset<bool> ignore(&ignore_callbacks_from_prompt_, true); |
| view_->UpdateAnchor(); |
| } |
| } |
| |
| void PermissionRequestManager::DidStartNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->IsInPrimaryMainFrame() || |
| navigation_handle->IsSameDocument()) { |
| return; |
| } |
| |
| // Cooldown lasts until the next user-initiated navigation, which is defined |
| // as either a renderer-initiated navigation with a user gesture, or a |
| // browser-initiated navigation. |
| // |
| // TODO(crbug.com/952347): This check has to be done at DidStartNavigation |
| // time, the HasUserGesture state is lost by the time the navigation |
| // commits. |
| if (!navigation_handle->IsRendererInitiated() || |
| navigation_handle->HasUserGesture()) { |
| is_notification_prompt_cooldown_active_ = false; |
| } |
| } |
| |
| void PermissionRequestManager::DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->IsInPrimaryMainFrame() || |
| !navigation_handle->HasCommitted() || |
| navigation_handle->IsSameDocument()) { |
| return; |
| } |
| |
| if (!queued_requests_.empty() || IsRequestInProgress()) { |
| // |queued_requests_| and |requests_| will be deleted below, which |
| // might be a problem for back-forward cache — the page might be restored |
| // later, but the requests won't be. |
| // Disable bfcache here if we have any requests here to prevent this |
| // from happening. |
| content::BackForwardCache::DisableForRenderFrameHost( |
| navigation_handle->GetPreviousRenderFrameHostId(), |
| back_forward_cache::DisabledReason( |
| back_forward_cache::DisabledReasonId::kPermissionRequestManager)); |
| } |
| |
| CleanUpRequests(); |
| } |
| |
| void PermissionRequestManager::DocumentOnLoadCompletedInPrimaryMainFrame() { |
| // 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. |
| ScheduleDequeueRequestIfNeeded(); |
| } |
| |
| void PermissionRequestManager::DOMContentLoaded( |
| content::RenderFrameHost* render_frame_host) { |
| ScheduleDequeueRequestIfNeeded(); |
| } |
| |
| void PermissionRequestManager::WebContentsDestroyed() { |
| // If the web contents has been destroyed, treat the bubble as cancelled. |
| CleanUpRequests(); |
| |
| // 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 PermissionRequestManager::OnVisibilityChanged( |
| content::Visibility visibility) { |
| bool tab_was_hidden = tab_is_hidden_; |
| tab_is_hidden_ = visibility == content::Visibility::HIDDEN; |
| if (tab_was_hidden == tab_is_hidden_) |
| return; |
| |
| if (tab_is_hidden_) { |
| if (view_) { |
| switch (view_->GetTabSwitchingBehavior()) { |
| case PermissionPrompt::TabSwitchingBehavior:: |
| kDestroyPromptButKeepRequestPending: |
| DeleteBubble(); |
| break; |
| case PermissionPrompt::TabSwitchingBehavior:: |
| kDestroyPromptAndIgnoreRequest: |
| FinalizeCurrentRequests(PermissionAction::IGNORED); |
| break; |
| case PermissionPrompt::TabSwitchingBehavior::kKeepPromptAlive: |
| break; |
| } |
| } |
| |
| return; |
| } |
| |
| if (!web_contents()->IsDocumentOnLoadCompletedInPrimaryMainFrame()) |
| return; |
| |
| if (!IsRequestInProgress()) { |
| ScheduleDequeueRequestIfNeeded(); |
| return; |
| } |
| |
| if (view_) { |
| // We switched tabs away and back while a prompt was active. |
| DCHECK_EQ(view_->GetTabSwitchingBehavior(), |
| PermissionPrompt::TabSwitchingBehavior::kKeepPromptAlive); |
| } else if (current_request_ui_to_use_.has_value()) { |
| ShowBubble(); |
| } |
| } |
| |
| const std::vector<PermissionRequest*>& PermissionRequestManager::Requests() { |
| return requests_; |
| } |
| |
| GURL PermissionRequestManager::GetRequestingOrigin() const { |
| CHECK(!requests_.empty()); |
| GURL origin = requests_.front()->requesting_origin(); |
| if (DCHECK_IS_ON()) { |
| for (auto* request : requests_) |
| DCHECK_EQ(origin, request->requesting_origin()); |
| } |
| return origin; |
| } |
| |
| GURL PermissionRequestManager::GetEmbeddingOrigin() const { |
| return PermissionUtil::GetLastCommittedOriginAsURL( |
| web_contents()->GetMainFrame()); |
| } |
| |
| void PermissionRequestManager::Accept() { |
| if (ignore_callbacks_from_prompt_) |
| return; |
| DCHECK(view_); |
| std::vector<PermissionRequest*>::iterator requests_iter; |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| PermissionGrantedIncludingDuplicates(*requests_iter, |
| /*is_one_time=*/false); |
| } |
| FinalizeCurrentRequests(PermissionAction::GRANTED); |
| } |
| |
| void PermissionRequestManager::AcceptThisTime() { |
| if (ignore_callbacks_from_prompt_) |
| return; |
| DCHECK(view_); |
| std::vector<PermissionRequest*>::iterator requests_iter; |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| PermissionGrantedIncludingDuplicates(*requests_iter, |
| /*is_one_time=*/true); |
| } |
| FinalizeCurrentRequests(PermissionAction::GRANTED_ONCE); |
| } |
| |
| void PermissionRequestManager::Deny() { |
| if (ignore_callbacks_from_prompt_) |
| return; |
| DCHECK(view_); |
| |
| // Suppress any further prompts in this WebContents, from any origin, until |
| // there is a user-initiated navigation. This stops users from getting |
| // trapped in request loops where the website automatically navigates |
| // cross-origin (e.g. to another subdomain) to be able to prompt again after |
| // a rejection. |
| if (base::FeatureList::IsEnabled( |
| features::kBlockRepeatedNotificationPermissionPrompts) && |
| std::any_of(requests_.begin(), requests_.end(), [](const auto* request) { |
| return request->GetContentSettingsType() == |
| ContentSettingsType::NOTIFICATIONS; |
| })) { |
| is_notification_prompt_cooldown_active_ = true; |
| } |
| |
| std::vector<PermissionRequest*>::iterator requests_iter; |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| PermissionDeniedIncludingDuplicates(*requests_iter); |
| } |
| FinalizeCurrentRequests(PermissionAction::DENIED); |
| } |
| |
| void PermissionRequestManager::Dismiss() { |
| if (ignore_callbacks_from_prompt_) |
| return; |
| DCHECK(view_); |
| std::vector<PermissionRequest*>::iterator requests_iter; |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| CancelledIncludingDuplicates(*requests_iter); |
| } |
| FinalizeCurrentRequests(PermissionAction::DISMISSED); |
| } |
| |
| void PermissionRequestManager::Ignore() { |
| if (ignore_callbacks_from_prompt_) |
| return; |
| DCHECK(view_); |
| std::vector<PermissionRequest*>::iterator requests_iter; |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| CancelledIncludingDuplicates(*requests_iter); |
| } |
| FinalizeCurrentRequests(PermissionAction::IGNORED); |
| } |
| |
| bool PermissionRequestManager::WasCurrentRequestAlreadyDisplayed() { |
| return current_request_already_displayed_; |
| } |
| |
| void PermissionRequestManager::SetDismissOnTabClose() { |
| should_dismiss_current_request_ = true; |
| } |
| |
| void PermissionRequestManager::SetBubbleShown() { |
| did_show_bubble_ = true; |
| } |
| |
| void PermissionRequestManager::SetDecisionTime() { |
| current_request_decision_time_ = base::Time::Now(); |
| } |
| |
| void PermissionRequestManager::SetManageClicked() { |
| set_manage_clicked(); |
| } |
| |
| void PermissionRequestManager::SetLearnMoreClicked() { |
| set_learn_more_clicked(); |
| } |
| |
| PermissionRequestManager::PermissionRequestManager( |
| content::WebContents* web_contents) |
| : content::WebContentsObserver(web_contents), |
| content::WebContentsUserData<PermissionRequestManager>(*web_contents), |
| view_factory_(base::BindRepeating(&PermissionPrompt::Create)), |
| view_(nullptr), |
| tab_is_hidden_(web_contents->GetVisibility() == |
| content::Visibility::HIDDEN), |
| auto_response_for_test_(NONE), |
| permission_ui_selectors_( |
| PermissionsClient::Get()->CreatePermissionUiSelectors( |
| web_contents->GetBrowserContext())) {} |
| |
| void PermissionRequestManager::ScheduleShowBubble() { |
| base::RecordAction(base::UserMetricsAction("PermissionBubbleRequest")); |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&PermissionRequestManager::ShowBubble, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void PermissionRequestManager::DequeueRequestIfNeeded() { |
| // TODO(olesiamarukhno): Media requests block other media requests from |
| // pre-empting them. For example, when a camera request is pending and mic |
| // is requested, the camera request remains pending and mic request appears |
| // only after the camera request is resolved. This is caused by code in |
| // PermissionBubbleMediaAccessHandler and UserMediaClient. We probably don't |
| // need two permission queues, so resolve the duplication. |
| |
| if (!web_contents()->IsDocumentOnLoadCompletedInPrimaryMainFrame() || view_ || |
| IsRequestInProgress()) { |
| return; |
| } |
| |
| // Find first valid request. |
| while (!queued_requests_.empty()) { |
| PermissionRequest* next = PopNextQueuedRequest(); |
| PermissionRequestSource& source = request_sources_map_.find(next)->second; |
| if (!source.IsSourceFrameInactiveAndDisallowActivation()) { |
| requests_.push_back(next); |
| break; |
| } |
| next->Cancelled(); |
| next->RequestFinished(); |
| request_sources_map_.erase(request_sources_map_.find(next)); |
| } |
| |
| if (requests_.empty()) { |
| return; |
| } |
| |
| // Find additional requests that can be grouped with the first one. |
| for (; !queued_requests_.empty(); PopNextQueuedRequest()) { |
| PermissionRequest* front = PeekNextQueuedRequest(); |
| PermissionRequestSource& source = request_sources_map_.find(front)->second; |
| if (source.IsSourceFrameInactiveAndDisallowActivation()) { |
| front->Cancelled(); |
| front->RequestFinished(); |
| request_sources_map_.erase(request_sources_map_.find(front)); |
| } else if (ShouldGroupRequests(requests_.front(), front)) { |
| requests_.push_back(front); |
| } else { |
| break; |
| } |
| } |
| |
| if (!permission_ui_selectors_.empty()) { |
| DCHECK(!current_request_ui_to_use_.has_value()); |
| // Initialize the selector decisions vector. |
| DCHECK(selector_decisions_.empty()); |
| selector_decisions_.resize(permission_ui_selectors_.size()); |
| |
| for (size_t selector_index = 0; |
| selector_index < permission_ui_selectors_.size(); ++selector_index) { |
| if (permission_ui_selectors_[selector_index] |
| ->IsPermissionRequestSupported( |
| requests_.front()->request_type())) { |
| permission_ui_selectors_[selector_index]->SelectUiToUse( |
| requests_.front(), |
| base::BindOnce( |
| &PermissionRequestManager::OnPermissionUiSelectorDone, |
| weak_factory_.GetWeakPtr(), selector_index)); |
| } else { |
| OnPermissionUiSelectorDone( |
| selector_index, |
| PermissionUiSelector::Decision::UseNormalUiAndShowNoWarning()); |
| } |
| } |
| } else { |
| current_request_ui_to_use_ = |
| UiDecision(UiDecision::UseNormalUi(), UiDecision::ShowNoWarning()); |
| ScheduleShowBubble(); |
| } |
| } |
| |
| void PermissionRequestManager::ScheduleDequeueRequestIfNeeded() { |
| base::SequencedTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&PermissionRequestManager::DequeueRequestIfNeeded, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void PermissionRequestManager::ShowBubble() { |
| // There is a race condition where the request might have been removed |
| // already so double-checking that there is a request in progress. |
| // |
| // There is no need to show a new bubble if the previous one still exists. |
| if (!IsRequestInProgress() || view_) |
| return; |
| |
| DCHECK(web_contents()->IsDocumentOnLoadCompletedInPrimaryMainFrame()); |
| DCHECK(current_request_ui_to_use_); |
| |
| if (tab_is_hidden_) |
| return; |
| |
| view_ = view_factory_.Run(web_contents(), this); |
| if (!view_) { |
| current_request_prompt_disposition_ = |
| PermissionPromptDisposition::NONE_VISIBLE; |
| FinalizeCurrentRequests(PermissionAction::IGNORED); |
| return; |
| } |
| |
| current_request_prompt_disposition_ = view_->GetPromptDisposition(); |
| |
| if (!current_request_already_displayed_) { |
| PermissionUmaUtil::PermissionPromptShown(requests_); |
| |
| auto quiet_ui_reason = ReasonForUsingQuietUi(); |
| if (quiet_ui_reason) { |
| switch (*quiet_ui_reason) { |
| case QuietUiReason::kEnabledInPrefs: |
| case QuietUiReason::kTriggeredByCrowdDeny: |
| case QuietUiReason::kServicePredictedVeryUnlikelyGrant: |
| case QuietUiReason::kOnDevicePredictedVeryUnlikelyGrant: |
| break; |
| case QuietUiReason::kTriggeredDueToAbusiveRequests: |
| LogWarningToConsole(kAbusiveNotificationRequestsEnforcementMessage); |
| break; |
| case QuietUiReason::kTriggeredDueToAbusiveContent: |
| LogWarningToConsole(kAbusiveNotificationContentEnforcementMessage); |
| break; |
| } |
| base::RecordAction(base::UserMetricsAction( |
| "Notifications.Quiet.PermissionRequestShown")); |
| } |
| } |
| current_request_already_displayed_ = true; |
| current_request_first_display_time_ = base::Time::Now(); |
| NotifyBubbleAdded(); |
| // If in testing mode, automatically respond to the bubble that was shown. |
| if (auto_response_for_test_ != NONE) |
| DoAutoResponseForTesting(); |
| } |
| |
| void PermissionRequestManager::DeleteBubble() { |
| DCHECK(view_); |
| { |
| base::AutoReset<bool> deleting(&ignore_callbacks_from_prompt_, true); |
| view_.reset(); |
| } |
| NotifyBubbleRemoved(); |
| } |
| |
| void PermissionRequestManager::ResetViewStateForCurrentRequest() { |
| for (const auto& selector : permission_ui_selectors_) |
| selector->Cancel(); |
| |
| current_request_already_displayed_ = false; |
| current_request_first_display_time_ = base::Time(); |
| current_request_decision_time_ = base::Time(); |
| current_request_prompt_disposition_.reset(); |
| prediction_grant_likelihood_.reset(); |
| current_request_ui_to_use_.reset(); |
| was_decision_held_back_.reset(); |
| selector_decisions_.clear(); |
| should_dismiss_current_request_ = false; |
| did_show_bubble_ = false; |
| did_click_manage_ = false; |
| did_click_learn_more_ = false; |
| |
| if (view_) |
| DeleteBubble(); |
| } |
| |
| void PermissionRequestManager::FinalizeCurrentRequests( |
| PermissionAction permission_action) { |
| DCHECK(IsRequestInProgress()); |
| base::TimeDelta time_to_decision; |
| if (!current_request_first_display_time_.is_null() && |
| permission_action != PermissionAction::IGNORED) { |
| if (current_request_decision_time_.is_null()) { |
| current_request_decision_time_ = base::Time::Now(); |
| } |
| time_to_decision = |
| current_request_decision_time_ - current_request_first_display_time_; |
| } |
| |
| if (time_to_decision_for_test_.has_value()) { |
| time_to_decision = time_to_decision_for_test_.value(); |
| time_to_decision_for_test_.reset(); |
| } |
| |
| PermissionUmaUtil::PermissionPromptResolved( |
| requests_, web_contents(), permission_action, time_to_decision, |
| DetermineCurrentRequestUIDisposition(), |
| DetermineCurrentRequestUIDispositionReasonForUMA(), |
| prediction_grant_likelihood_, was_decision_held_back_, did_show_bubble_, |
| did_click_manage_, did_click_learn_more_); |
| |
| content::BrowserContext* browser_context = |
| web_contents()->GetBrowserContext(); |
| PermissionDecisionAutoBlocker* autoblocker = |
| PermissionsClient::Get()->GetPermissionDecisionAutoBlocker( |
| browser_context); |
| |
| absl::optional<QuietUiReason> quiet_ui_reason; |
| if (ShouldCurrentRequestUseQuietUI()) |
| quiet_ui_reason = ReasonForUsingQuietUi(); |
| |
| for (PermissionRequest* request : requests_) { |
| // TODO(timloh): We only support dismiss and ignore embargo for |
| // permissions which use PermissionRequestImpl as the other subclasses |
| // don't support GetContentSettingsType. |
| if (request->GetContentSettingsType() == ContentSettingsType::DEFAULT) |
| continue; |
| |
| PermissionsClient::Get()->OnPromptResolved( |
| browser_context, request->request_type(), permission_action, |
| request->requesting_origin(), DetermineCurrentRequestUIDisposition(), |
| DetermineCurrentRequestUIDispositionReasonForUMA(), |
| request->GetGestureType(), quiet_ui_reason); |
| |
| PermissionEmbargoStatus embargo_status = |
| PermissionEmbargoStatus::NOT_EMBARGOED; |
| if (permission_action == PermissionAction::DISMISSED) { |
| if (autoblocker->RecordDismissAndEmbargo( |
| request->requesting_origin(), request->GetContentSettingsType(), |
| ShouldCurrentRequestUseQuietUI())) { |
| embargo_status = PermissionEmbargoStatus::REPEATED_DISMISSALS; |
| } |
| } else if (permission_action == PermissionAction::IGNORED) { |
| if (autoblocker->RecordIgnoreAndEmbargo( |
| request->requesting_origin(), request->GetContentSettingsType(), |
| ShouldCurrentRequestUseQuietUI())) { |
| embargo_status = PermissionEmbargoStatus::REPEATED_IGNORES; |
| } |
| } |
| PermissionUmaUtil::RecordEmbargoStatus(embargo_status); |
| } |
| ResetViewStateForCurrentRequest(); |
| std::vector<PermissionRequest*>::iterator requests_iter; |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| RequestFinishedIncludingDuplicates(*requests_iter); |
| request_sources_map_.erase(request_sources_map_.find(*requests_iter)); |
| } |
| requests_.clear(); |
| |
| for (Observer& observer : observer_list_) |
| observer.OnRequestsFinalized(); |
| |
| ScheduleDequeueRequestIfNeeded(); |
| } |
| |
| void PermissionRequestManager::CleanUpRequests() { |
| for (auto* queued_request : queued_requests_) { |
| CancelledIncludingDuplicates(queued_request); |
| RequestFinishedIncludingDuplicates(queued_request); |
| request_sources_map_.erase(request_sources_map_.find(queued_request)); |
| } |
| queued_requests_.clear(); |
| |
| if (IsRequestInProgress()) { |
| std::vector<PermissionRequest*>::iterator requests_iter; |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| CancelledIncludingDuplicates(*requests_iter); |
| } |
| |
| FinalizeCurrentRequests(should_dismiss_current_request_ |
| ? PermissionAction::DISMISSED |
| : PermissionAction::IGNORED); |
| should_dismiss_current_request_ = false; |
| } |
| } |
| |
| PermissionRequest* PermissionRequestManager::GetExistingRequest( |
| PermissionRequest* request) { |
| for (PermissionRequest* existing_request : requests_) { |
| if (request->IsDuplicateOf(existing_request)) |
| return existing_request; |
| } |
| for (PermissionRequest* queued_request : queued_requests_) { |
| if (request->IsDuplicateOf(queued_request)) |
| return queued_request; |
| } |
| return nullptr; |
| } |
| |
| void PermissionRequestManager::PermissionGrantedIncludingDuplicates( |
| PermissionRequest* request, |
| bool is_one_time) { |
| DCHECK_EQ(1, base::STLCount(requests_, request) + |
| base::STLCount(queued_requests_, request)) |
| << "Only requests in [queued_[frame_]]requests_ can have duplicates"; |
| request->PermissionGranted(is_one_time); |
| auto range = duplicate_requests_.equal_range(request); |
| for (auto it = range.first; it != range.second; ++it) |
| it->second->PermissionGranted(is_one_time); |
| } |
| |
| void PermissionRequestManager::PermissionDeniedIncludingDuplicates( |
| PermissionRequest* request) { |
| DCHECK_EQ(1, base::STLCount(requests_, request) + |
| base::STLCount(queued_requests_, request)) |
| << "Only requests in [queued_]requests_ can have duplicates"; |
| request->PermissionDenied(); |
| auto range = duplicate_requests_.equal_range(request); |
| for (auto it = range.first; it != range.second; ++it) |
| it->second->PermissionDenied(); |
| } |
| |
| void PermissionRequestManager::CancelledIncludingDuplicates( |
| PermissionRequest* request) { |
| DCHECK_EQ(1, base::STLCount(requests_, request) + |
| base::STLCount(queued_requests_, request)) |
| << "Only requests in [queued_]requests_ can have duplicates"; |
| request->Cancelled(); |
| auto range = duplicate_requests_.equal_range(request); |
| for (auto it = range.first; it != range.second; ++it) |
| it->second->Cancelled(); |
| } |
| |
| void PermissionRequestManager::RequestFinishedIncludingDuplicates( |
| PermissionRequest* request) { |
| DCHECK_EQ(1, base::STLCount(requests_, request) + |
| base::STLCount(queued_requests_, request)) |
| << "Only requests in [queued_]requests_ can have duplicates"; |
| request->RequestFinished(); |
| // Beyond this point, |request| has probably been deleted. |
| auto range = duplicate_requests_.equal_range(request); |
| for (auto it = range.first; it != range.second; ++it) |
| it->second->RequestFinished(); |
| // Additionally, we can now remove the duplicates. |
| duplicate_requests_.erase(request); |
| } |
| |
| void PermissionRequestManager::AddObserver(Observer* observer) { |
| observer_list_.AddObserver(observer); |
| } |
| |
| void PermissionRequestManager::RemoveObserver(Observer* observer) { |
| observer_list_.RemoveObserver(observer); |
| } |
| |
| bool PermissionRequestManager::ShouldCurrentRequestUseQuietUI() const { |
| // ContentSettingImageModel might call into this method if the user switches |
| // between tabs while the |notification_permission_ui_selectors_| are |
| // pending. |
| return ReasonForUsingQuietUi() != absl::nullopt; |
| } |
| |
| absl::optional<PermissionRequestManager::QuietUiReason> |
| PermissionRequestManager::ReasonForUsingQuietUi() const { |
| if (!IsRequestInProgress() || !current_request_ui_to_use_ || |
| !current_request_ui_to_use_->quiet_ui_reason) |
| return absl::nullopt; |
| |
| return *(current_request_ui_to_use_->quiet_ui_reason); |
| } |
| |
| bool PermissionRequestManager::IsRequestInProgress() const { |
| return !requests_.empty(); |
| } |
| |
| bool PermissionRequestManager::ShouldDropCurrentRequestIfCannotShowQuietly() |
| const { |
| absl::optional<QuietUiReason> quiet_ui_reason = ReasonForUsingQuietUi(); |
| if (quiet_ui_reason.has_value()) { |
| switch (quiet_ui_reason.value()) { |
| case QuietUiReason::kEnabledInPrefs: |
| case QuietUiReason::kServicePredictedVeryUnlikelyGrant: |
| case QuietUiReason::kOnDevicePredictedVeryUnlikelyGrant: |
| case QuietUiReason::kTriggeredByCrowdDeny: |
| return false; |
| case QuietUiReason::kTriggeredDueToAbusiveRequests: |
| case QuietUiReason::kTriggeredDueToAbusiveContent: |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| void PermissionRequestManager::NotifyBubbleAdded() { |
| for (Observer& observer : observer_list_) |
| observer.OnBubbleAdded(); |
| } |
| |
| void PermissionRequestManager::NotifyBubbleRemoved() { |
| for (Observer& observer : observer_list_) |
| observer.OnBubbleRemoved(); |
| } |
| |
| void PermissionRequestManager::OnPermissionUiSelectorDone( |
| size_t selector_index, |
| const UiDecision& decision) { |
| if (decision.warning_reason) { |
| switch (*(decision.warning_reason)) { |
| case WarningReason::kAbusiveRequests: |
| LogWarningToConsole(kAbusiveNotificationRequestsWarningMessage); |
| break; |
| case WarningReason::kAbusiveContent: |
| LogWarningToConsole(kAbusiveNotificationContentWarningMessage); |
| break; |
| } |
| } |
| |
| // We have already made a decision because of a higher priority selector |
| // therefore this selector's decision can be discarded. |
| if (current_request_ui_to_use_.has_value()) |
| return; |
| |
| CHECK_LT(selector_index, selector_decisions_.size()); |
| selector_decisions_[selector_index] = decision; |
| |
| size_t decision_index = 0; |
| while (decision_index < selector_decisions_.size() && |
| selector_decisions_[decision_index].has_value()) { |
| const UiDecision& current_decision = |
| selector_decisions_[decision_index].value(); |
| |
| if (!prediction_grant_likelihood_.has_value()) { |
| prediction_grant_likelihood_ = permission_ui_selectors_[decision_index] |
| ->PredictedGrantLikelihoodForUKM(); |
| } |
| |
| if (!was_decision_held_back_.has_value()) { |
| was_decision_held_back_ = permission_ui_selectors_[decision_index] |
| ->WasSelectorDecisionHeldback(); |
| } |
| |
| if (current_decision.quiet_ui_reason.has_value()) { |
| current_request_ui_to_use_ = current_decision; |
| break; |
| } |
| |
| ++decision_index; |
| } |
| |
| // All decisions have been considered and none was conclusive. |
| if (decision_index == selector_decisions_.size() && |
| !current_request_ui_to_use_.has_value()) { |
| current_request_ui_to_use_ = UiDecision::UseNormalUiAndShowNoWarning(); |
| } |
| |
| if (current_request_ui_to_use_.has_value()) { |
| ScheduleShowBubble(); |
| } |
| } |
| |
| PermissionPromptDisposition |
| PermissionRequestManager::DetermineCurrentRequestUIDisposition() { |
| if (current_request_prompt_disposition_.has_value()) |
| return current_request_prompt_disposition_.value(); |
| return PermissionPromptDisposition::NONE_VISIBLE; |
| } |
| |
| PermissionPromptDispositionReason |
| PermissionRequestManager::DetermineCurrentRequestUIDispositionReasonForUMA() { |
| auto quiet_ui_reason = ReasonForUsingQuietUi(); |
| if (!quiet_ui_reason) |
| return PermissionPromptDispositionReason::DEFAULT_FALLBACK; |
| switch (*quiet_ui_reason) { |
| case QuietUiReason::kEnabledInPrefs: |
| return PermissionPromptDispositionReason::USER_PREFERENCE_IN_SETTINGS; |
| case QuietUiReason::kTriggeredByCrowdDeny: |
| case QuietUiReason::kTriggeredDueToAbusiveRequests: |
| case QuietUiReason::kTriggeredDueToAbusiveContent: |
| return PermissionPromptDispositionReason::SAFE_BROWSING_VERDICT; |
| case QuietUiReason::kServicePredictedVeryUnlikelyGrant: |
| return PermissionPromptDispositionReason::PREDICTION_SERVICE; |
| case QuietUiReason::kOnDevicePredictedVeryUnlikelyGrant: |
| return PermissionPromptDispositionReason::ON_DEVICE_PREDICTION_MODEL; |
| } |
| } |
| |
| void PermissionRequestManager::LogWarningToConsole(const char* message) { |
| web_contents()->GetMainFrame()->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kWarning, message); |
| } |
| |
| void PermissionRequestManager::DoAutoResponseForTesting() { |
| switch (auto_response_for_test_) { |
| case ACCEPT_ONCE: |
| AcceptThisTime(); |
| break; |
| case ACCEPT_ALL: |
| Accept(); |
| break; |
| case DENY_ALL: |
| Deny(); |
| break; |
| case DISMISS: |
| Dismiss(); |
| break; |
| case NONE: |
| NOTREACHED(); |
| } |
| } |
| |
| PermissionRequest* PermissionRequestManager::PeekNextQueuedRequest() { |
| return base::FeatureList::IsEnabled(features::kPermissionChip) |
| ? queued_requests_.back() |
| : queued_requests_.front(); |
| } |
| |
| PermissionRequest* PermissionRequestManager::PopNextQueuedRequest() { |
| PermissionRequest* next = PeekNextQueuedRequest(); |
| if (base::FeatureList::IsEnabled(features::kPermissionChip)) |
| queued_requests_.pop_back(); |
| else |
| queued_requests_.pop_front(); |
| return next; |
| } |
| |
| void PermissionRequestManager::PushQueuedRequest(PermissionRequest* request) { |
| if (base::FeatureList::IsEnabled(features::kPermissionQuietChip) && |
| !base::FeatureList::IsEnabled(features::kPermissionChip)) { |
| queued_requests_.push_front(request); |
| } else { |
| queued_requests_.push_back(request); |
| } |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(PermissionRequestManager); |
| |
| } // namespace permissions |