| // Copyright 2014 The Chromium Authors |
| // 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 <memory> |
| #include <optional> |
| #include <string> |
| |
| #include "base/auto_reset.h" |
| #include "base/check_op.h" |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/weak_ptr.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/rand_util.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/time/clock.h" |
| #include "base/time/time.h" |
| #include "components/back_forward_cache/back_forward_cache_disable.h" |
| #include "components/content_settings/core/browser/content_settings_registry.h" |
| #include "components/content_settings/core/common/content_settings.h" |
| #include "components/content_settings/core/common/content_settings_types.h" |
| #include "components/permissions/constants.h" |
| #include "components/permissions/features.h" |
| #include "components/permissions/origin_keyed_permission_action_service.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_uma_util.h" |
| #include "components/permissions/permission_util.h" |
| #include "components/permissions/permissions_client.h" |
| #include "components/permissions/request_type.h" |
| #include "components/permissions/switches.h" |
| #include "components/tabs/public/tab_interface.h" |
| #include "content/public/browser/back_forward_cache.h" |
| #include "content/public/browser/browser_context.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 "ui/base/window_open_disposition_utils.h" |
| #include "ui/events/event.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"; |
| |
| constexpr char kDisruptiveNotificationBehaviorEnforcementMessage[] = |
| "Chrome is blocking notification permission requests on this site because " |
| "the site exhibits behaviors that may be disruptive to users."; |
| |
| namespace { |
| |
| // 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/40186690): If a user switched tabs, do not include that time |
| // as "shown". |
| bool ShouldShowQuietRequestAgainIfPreempted( |
| std::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; |
| } |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| bool IsExclusiveAccessRequest(RequestType type) { |
| return type == RequestType::kPointerLock || |
| type == RequestType::kKeyboardLock; |
| } |
| #endif |
| |
| bool ShouldGroupRequests(PermissionRequest* a, PermissionRequest* b) { |
| if (a->requesting_origin() != b->requesting_origin()) { |
| return false; |
| } |
| // Group if both requests are of the same category. |
| if (IsMediaRequest(a->request_type()) && IsMediaRequest(b->request_type())) { |
| return true; |
| } |
| #if !BUILDFLAG(IS_ANDROID) |
| if (IsExclusiveAccessRequest(a->request_type()) && |
| IsExclusiveAccessRequest(b->request_type())) { |
| return true; |
| } |
| #endif |
| return false; |
| } |
| |
| bool RequestExistsExactlyOnce( |
| PermissionRequest* request, |
| const PermissionRequestQueue& request_queue, |
| const std::vector<std::unique_ptr<PermissionRequest>>& requests) { |
| return request_queue.Contains(request) != |
| std::ranges::any_of(requests, [request](const auto& current_request) { |
| return current_request.get() == request; |
| }); |
| } |
| |
| void EraseRequest(std::vector<base::WeakPtr<PermissionRequest>>& requests, |
| PermissionRequest* request) { |
| std::erase_if(requests, |
| [request](base::WeakPtr<PermissionRequest> weak_ptr) -> bool { |
| CHECK(weak_ptr); |
| return weak_ptr.get() == request; |
| }); |
| } |
| |
| } // namespace |
| |
| // PermissionRequestManager ---------------------------------------------------- |
| |
| bool PermissionRequestManager::PermissionRequestSource:: |
| IsSourceFrameInactiveAndDisallowActivation() const { |
| content::RenderFrameHost* rfh = |
| content::RenderFrameHost::FromID(requesting_frame_id); |
| return !rfh || |
| rfh->IsInactiveAndDisallowActivation( |
| content::DisallowActivationReasonId::kPermissionRequestSource); |
| } |
| |
| PermissionRequestManager::~PermissionRequestManager() { |
| DCHECK(!IsRequestInProgress()); |
| DCHECK(duplicate_requests_.empty()); |
| DCHECK(pending_permission_requests_.IsEmpty()); |
| |
| for (Observer& observer : observer_list_) { |
| observer.OnPermissionRequestManagerDestructed(); |
| } |
| |
| tab_subscriptions_.clear(); |
| } |
| |
| void PermissionRequestManager::AddRequest( |
| content::RenderFrameHost* source_frame, |
| std::unique_ptr<PermissionRequest> request) { |
| DCHECK(source_frame); |
| DCHECK_EQ(content::WebContents::FromRenderFrameHost(source_frame), |
| web_contents()); |
| |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kDenyPermissionPrompts)) { |
| request->PermissionDenied(); |
| return; |
| } |
| |
| if (source_frame->IsInactiveAndDisallowActivation( |
| content::DisallowActivationReasonId::kPermissionAddRequest)) { |
| request->Cancelled(); |
| return; |
| } |
| |
| if (source_frame->IsNestedWithinFencedFrame()) { |
| request->Cancelled(); |
| 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) { |
| // Automatically cancel site Notification requests when Chrome is not able |
| // to send notifications in an app level. |
| request->Cancelled(); |
| 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(); |
| return; |
| } |
| |
| if (!web_contents_supports_permission_requests_) { |
| request->Cancelled(); |
| return; |
| } |
| |
| // TODO(tsergeant): change the UMA to no longer mention bubble. |
| 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(source_frame->GetMainFrame()->IsInPrimaryMainFrame()); |
| const GURL main_frame_origin = |
| PermissionUtil::GetLastCommittedOriginAsURL(source_frame->GetMainFrame()); |
| bool is_main_frame = |
| url::IsSameOriginWith(main_frame_origin, request->requesting_origin()); |
| |
| std::optional<PermissionAction> should_auto_approve_request = |
| PermissionsClient::Get()->GetAutoApprovalStatus( |
| web_contents()->GetBrowserContext(), request->requesting_origin()); |
| |
| if (should_auto_approve_request) { |
| if (should_auto_approve_request == PermissionAction::GRANTED) { |
| request->PermissionGranted(/*is_one_time=*/true); |
| } |
| return; |
| } |
| |
| // Don't re-add an existing request or one with a duplicate text request. |
| if (auto* existing_request = GetExistingRequest(request.get())) { |
| // |request| is a duplicate. Add it to |duplicate_requests_| unless it's the |
| // same object as |existing_request| or an existing duplicate. |
| auto iter = FindDuplicateRequestList(existing_request); |
| if (iter == duplicate_requests_.end()) { |
| std::list<std::unique_ptr<PermissionRequest>> list; |
| list.push_back(std::move(request)); |
| duplicate_requests_.push_back(std::move(list)); |
| return; |
| } |
| |
| iter->push_back(std::move(request)); |
| return; |
| } |
| |
| if (is_main_frame) { |
| if (IsRequestInProgress()) { |
| base::RecordAction( |
| base::UserMetricsAction("PermissionBubbleRequestQueued")); |
| } |
| } else { |
| base::RecordAction( |
| base::UserMetricsAction("PermissionBubbleIFrameRequestQueued")); |
| } |
| |
| request->set_requesting_frame_id(source_frame->GetGlobalId()); |
| |
| QueueRequest(source_frame, std::move(request)); |
| |
| if (!IsRequestInProgress()) { |
| ScheduleDequeueRequestIfNeeded(); |
| return; |
| } |
| |
| ReprioritizeCurrentRequestIfNeeded(); |
| } |
| |
| bool PermissionRequestManager::ReprioritizeCurrentRequestIfNeeded() { |
| if (!IsRequestInProgress() || |
| IsCurrentRequestEmbeddedPermissionElementInitiated() || |
| !can_preempt_current_request_) { |
| return true; |
| } |
| |
| // Pop out all invalid requests in front of the queue. |
| while (!pending_permission_requests_.IsEmpty() && |
| !HasActiveSourceFrameOrDisallowActivationOtherwise( |
| pending_permission_requests_.Peek())) { |
| auto request = pending_permission_requests_.Pop(); |
| FinalizeAndCancelRequest(request.get()); |
| } |
| |
| if (pending_permission_requests_.IsEmpty()) { |
| return true; |
| } |
| |
| auto current_request_fate = CurrentRequestFate::kKeepCurrent; |
| |
| if (PermissionUtil::DoesPlatformSupportChip()) { |
| if (ShouldCurrentRequestUseQuietUI() && |
| !ShouldShowQuietRequestAgainIfPreempted( |
| current_request_first_display_time_)) { |
| current_request_fate = CurrentRequestFate::kFinalize; |
| } else { |
| // Preempt current request if it is a quiet UI request. |
| if (ShouldCurrentRequestUseQuietUI()) { |
| current_request_fate = CurrentRequestFate::kPreempt; |
| } else { |
| // Here we also try to prioritise the requests. If there's a valid high |
| // priority request (high acceptance rate request) in the pending queue, |
| // preempt the current request. The valid high priority request, if |
| // there's any, is always the front of the queue. |
| if (!pending_permission_requests_.IsEmpty() && |
| !PermissionUtil::IsLowPriorityPermissionRequest( |
| pending_permission_requests_.Peek())) { |
| current_request_fate = CurrentRequestFate::kPreempt; |
| } |
| } |
| } |
| } else if (ShouldCurrentRequestUseQuietUI()) { |
| // If we're displaying a quiet permission request, ignore it in favor of a |
| // new permission request. |
| current_request_fate = CurrentRequestFate::kFinalize; |
| } |
| |
| if (current_request_fate == CurrentRequestFate::kKeepCurrent && |
| !pending_permission_requests_.IsEmpty() && |
| pending_permission_requests_.Peek() |
| ->IsEmbeddedPermissionElementInitiated()) { |
| current_request_fate = CurrentRequestFate::kPreempt; |
| } |
| |
| switch (current_request_fate) { |
| case CurrentRequestFate::kKeepCurrent: |
| return true; |
| case CurrentRequestFate::kPreempt: { |
| CHECK(!pending_permission_requests_.IsEmpty()); |
| // Consider a case of infinite loop here (eg: 2 low priority requests can |
| // preempt each other, causing a loop). We only preempt the current |
| // request if the next candidate has just been added to pending queue but |
| // not validated yet. |
| if (std::ranges::any_of( |
| validated_requests_.begin(), validated_requests_.end(), |
| [&](const auto& element) -> bool { |
| CHECK(element); |
| return element.get() == pending_permission_requests_.Peek(); |
| })) { |
| return true; |
| } |
| |
| auto next = pending_permission_requests_.Pop(); |
| PreemptAndRequeueCurrentRequest(); |
| pending_permission_requests_.PushFront(std::move(next)); |
| ScheduleDequeueRequestIfNeeded(); |
| return false; |
| } |
| case CurrentRequestFate::kFinalize: |
| // FinalizeCurrentRequests() will call ScheduleDequeueRequestIfNeeded on |
| // its own. |
| CurrentRequestsDecided(PermissionAction::IGNORED); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool PermissionRequestManager:: |
| HasActiveSourceFrameOrDisallowActivationOtherwise( |
| PermissionRequest* request) const { |
| const auto iter = request_sources_map_.find(request); |
| if (iter != request_sources_map_.end()) { |
| return !iter->second.IsSourceFrameInactiveAndDisallowActivation(); |
| } |
| return false; |
| } |
| |
| void PermissionRequestManager::FinalizeAndCancelRequest( |
| PermissionRequest* request) { |
| if (request_sources_map_.erase(request) > 0) { |
| EraseRequest(validated_requests_, request); |
| } |
| request->Cancelled(); |
| } |
| |
| void PermissionRequestManager::QueueRequest( |
| content::RenderFrameHost* source_frame, |
| std::unique_ptr<PermissionRequest> request) { |
| request_sources_map_.emplace( |
| request.get(), PermissionRequestSource({source_frame->GetGlobalId()})); |
| pending_permission_requests_.Push(std::move(request)); |
| } |
| |
| void PermissionRequestManager::PreemptAndRequeueCurrentRequest() { |
| ResetViewStateForCurrentRequest(); |
| for (auto& current_request : requests_) { |
| pending_permission_requests_.PushFront(std::move(current_request)); |
| } |
| |
| // Because the order of the requests is changed, we should not preignore it. |
| preignore_timer_.Stop(); |
| |
| 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); |
| if (!view_->UpdateAnchor()) { |
| RecreateView(); |
| } |
| } |
| } |
| |
| void PermissionRequestManager::DidStartNavigation( |
| content::NavigationHandle* navigation_handle) { |
| for (Observer& observer : observer_list_) { |
| observer.OnNavigation(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/40622940): 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 (!navigation_handle->IsErrorPage()) { |
| permissions::PermissionUmaUtil:: |
| RecordTopLevelPermissionsHeaderPolicyOnNavigation( |
| navigation_handle->GetRenderFrameHost()); |
| } |
| |
| if (!base::FeatureList::IsEnabled( |
| features::kBackForwardCacheUnblockPermissionRequest)) { |
| if (!pending_permission_requests_.IsEmpty() || IsRequestInProgress()) { |
| // |pending_permission_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()` will update activity indicators. `DidFinishNavigation` |
| // means that a new document was recently created, it should not display |
| // blocked indicators from a previous document. |
| auto* pscs = content_settings::PageSpecificContentSettings::GetForFrame( |
| web_contents()->GetPrimaryMainFrame()); |
| // `pscs` can be nullptr in tests. |
| if (pscs) { |
| pscs->OnPermissionRequestCleanupStart(); |
| } |
| CleanUpRequests(); |
| if (pscs) { |
| pscs->OnPermissionRequestCleanupEnd(); |
| } |
| } |
| |
| 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 prompt. |
| ScheduleDequeueRequestIfNeeded(); |
| } |
| |
| void PermissionRequestManager::DOMContentLoaded( |
| content::RenderFrameHost* render_frame_host) { |
| ScheduleDequeueRequestIfNeeded(); |
| } |
| |
| void PermissionRequestManager::WebContentsDestroyed() { |
| // If the web contents has been destroyed, treat the prompt as cancelled. |
| CleanUpRequests(); |
| |
| // The WebContents is going away; be aggressively paranoid and delete |
| // ourselves lest other parts of the system attempt to add permission |
| // prompts 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) { |
| // If `tab_subscriptions_` isn't empty, defer to those listeners instead. |
| if (!tab_subscriptions_.empty()) { |
| return; |
| } |
| bool prior_tab_is_active_ = tab_is_active_; |
| tab_is_active_ = visibility != content::Visibility::HIDDEN; |
| if (prior_tab_is_active_ != tab_is_active_) { |
| OnTabActiveChanged(); |
| } |
| } |
| |
| const std::vector<std::unique_ptr<PermissionRequest>>& |
| PermissionRequestManager::Requests() { |
| return requests_; |
| } |
| |
| GURL PermissionRequestManager::GetRequestingOrigin() const { |
| CHECK(!requests_.empty()); |
| GURL origin = requests_.front()->requesting_origin(); |
| if (DCHECK_IS_ON()) { |
| for (const auto& request : requests_) { |
| DCHECK_EQ(origin, request->requesting_origin()); |
| } |
| } |
| return origin; |
| } |
| |
| GURL PermissionRequestManager::GetEmbeddingOrigin() const { |
| if (embedding_origin_for_testing_.has_value()) { |
| return embedding_origin_for_testing_.value(); |
| } |
| |
| return PermissionUtil::GetLastCommittedOriginAsURL( |
| web_contents()->GetPrimaryMainFrame()); |
| } |
| |
| void PermissionRequestManager::Accept() { |
| if (ignore_callbacks_from_prompt_) { |
| return; |
| } |
| DCHECK(view_); |
| base::AutoReset<bool> block_preempt(&can_preempt_current_request_, false); |
| std::vector<std::unique_ptr<PermissionRequest>>::iterator requests_iter; |
| |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| StorePermissionActionForUMA((*requests_iter)->requesting_origin(), |
| (*requests_iter)->request_type(), |
| PermissionAction::GRANTED); |
| PermissionGrantedIncludingDuplicates(requests_iter->get(), |
| /*is_one_time=*/false); |
| |
| #if !BUILDFLAG(IS_ANDROID) |
| std::optional<ContentSettingsType> content_settings_type = |
| RequestTypeToContentSettingsType((*requests_iter)->request_type()); |
| if (content_settings_type.has_value()) { |
| PermissionUmaUtil::RecordPermissionRegrantForUnusedSites( |
| (*requests_iter)->requesting_origin(), content_settings_type.value(), |
| PermissionSourceUI::PROMPT, web_contents()->GetBrowserContext(), |
| base::Time::Now()); |
| } |
| #endif |
| } |
| |
| NotifyRequestDecided(PermissionAction::GRANTED); |
| CurrentRequestsDecided(PermissionAction::GRANTED); |
| } |
| |
| void PermissionRequestManager::AcceptThisTime() { |
| if (ignore_callbacks_from_prompt_) { |
| return; |
| } |
| DCHECK(view_); |
| base::AutoReset<bool> block_preempt(&can_preempt_current_request_, false); |
| std::vector<std::unique_ptr<PermissionRequest>>::iterator requests_iter; |
| |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| StorePermissionActionForUMA((*requests_iter)->requesting_origin(), |
| (*requests_iter)->request_type(), |
| PermissionAction::GRANTED_ONCE); |
| PermissionGrantedIncludingDuplicates(requests_iter->get(), |
| /*is_one_time=*/true); |
| } |
| |
| NotifyRequestDecided(PermissionAction::GRANTED_ONCE); |
| CurrentRequestsDecided(PermissionAction::GRANTED_ONCE); |
| } |
| |
| void PermissionRequestManager::Deny() { |
| if (ignore_callbacks_from_prompt_) { |
| return; |
| } |
| DCHECK(view_); |
| base::AutoReset<bool> block_preempt(&can_preempt_current_request_, false); |
| |
| // 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::Contains(requests_, ContentSettingsType::NOTIFICATIONS, |
| &PermissionRequest::GetContentSettingsType)) { |
| is_notification_prompt_cooldown_active_ = true; |
| } |
| std::vector<std::unique_ptr<PermissionRequest>>::iterator requests_iter; |
| |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| StorePermissionActionForUMA((*requests_iter)->requesting_origin(), |
| (*requests_iter)->request_type(), |
| PermissionAction::DENIED); |
| PermissionDeniedIncludingDuplicates(requests_iter->get()); |
| } |
| |
| NotifyRequestDecided(PermissionAction::DENIED); |
| CurrentRequestsDecided(PermissionAction::DENIED); |
| } |
| |
| void PermissionRequestManager::Dismiss() { |
| if (ignore_callbacks_from_prompt_) { |
| return; |
| } |
| DCHECK(view_); |
| base::AutoReset<bool> block_preempt(&can_preempt_current_request_, false); |
| std::vector<std::unique_ptr<PermissionRequest>>::iterator requests_iter; |
| |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| StorePermissionActionForUMA((*requests_iter)->requesting_origin(), |
| (*requests_iter)->request_type(), |
| PermissionAction::DISMISSED); |
| CancelRequestIncludingDuplicates(requests_iter->get()); |
| } |
| |
| NotifyRequestDecided(PermissionAction::DISMISSED); |
| CurrentRequestsDecided(PermissionAction::DISMISSED); |
| } |
| |
| void PermissionRequestManager::Ignore() { |
| if (ignore_callbacks_from_prompt_) { |
| return; |
| } |
| base::AutoReset<bool> block_preempt(&can_preempt_current_request_, false); |
| std::vector<std::unique_ptr<PermissionRequest>>::iterator requests_iter; |
| |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| StorePermissionActionForUMA((*requests_iter)->requesting_origin(), |
| (*requests_iter)->request_type(), |
| PermissionAction::IGNORED); |
| CancelRequestIncludingDuplicates(requests_iter->get()); |
| } |
| |
| NotifyRequestDecided(PermissionAction::IGNORED); |
| CurrentRequestsDecided(PermissionAction::IGNORED); |
| } |
| |
| void PermissionRequestManager::FinalizeCurrentRequests() { |
| CHECK(IsRequestInProgress()); |
| ResetViewStateForCurrentRequest(); |
| base::AutoReset<bool> block_preempt(&can_preempt_current_request_, false); |
| std::vector<std::unique_ptr<PermissionRequest>>::iterator requests_iter; |
| |
| // Erase the request from |validated_requests_| before its destruction |
| // during requests_.clear() at the end of this function. |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| EraseRequest(validated_requests_, requests_iter->get()); |
| request_sources_map_.erase(requests_iter->get()); |
| FinishRequestIncludingDuplicates(requests_iter->get()); |
| } |
| |
| // No need to execute the preignore logic as we canceling currently active |
| // requests anyway. |
| preignore_timer_.Stop(); |
| |
| // We have no need to block preemption anymore. |
| std::ignore = std::move(block_preempt); |
| |
| for (Observer& observer : observer_list_) { |
| observer.OnRequestsFinalized(); |
| } |
| |
| requests_.clear(); |
| ScheduleDequeueRequestIfNeeded(); |
| } |
| |
| void PermissionRequestManager::OpenHelpCenterLink(const ui::Event& event) { |
| CHECK_GT(requests_.size(), 0u); |
| switch (requests_[0]->request_type()) { |
| case permissions::RequestType::kStorageAccess: |
| GetAssociatedWebContents()->OpenURL( |
| content::OpenURLParams( |
| GURL(permissions::kEmbeddedContentHelpCenterURL), |
| content::Referrer(), |
| ui::DispositionFromEventFlags( |
| event.flags(), WindowOpenDisposition::NEW_FOREGROUND_TAB), |
| ui::PAGE_TRANSITION_LINK, /*is_renderer_initiated=*/false), |
| /*navigation_handle_callback=*/{}); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void PermissionRequestManager::PreIgnoreQuietPrompt() { |
| // Random number of seconds in the range [1.0, 2.0). |
| double delay_seconds = 1.0 + 1.0 * base::RandDouble(); |
| preignore_timer_.Start( |
| FROM_HERE, base::Seconds(delay_seconds), this, |
| &PermissionRequestManager::PreIgnoreQuietPromptInternal); |
| } |
| |
| void PermissionRequestManager::PreIgnoreQuietPromptInternal() { |
| DCHECK(!requests_.empty()); |
| |
| if (requests_.empty()) { |
| // If `requests_` was cleared then there is nothing preignore. |
| return; |
| } |
| |
| std::vector<std::unique_ptr<PermissionRequest>>::iterator requests_iter; |
| |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| CancelRequestIncludingDuplicates(requests_iter->get(), |
| /*is_final_decision=*/false); |
| } |
| |
| blink::PermissionType permission; |
| bool success = PermissionUtil::GetPermissionType( |
| requests_[0]->GetContentSettingsType(), &permission); |
| DCHECK(success); |
| |
| PermissionUmaUtil::PermissionRequestPreignored(permission); |
| } |
| |
| bool PermissionRequestManager::WasCurrentRequestAlreadyDisplayed() { |
| return current_request_already_displayed_; |
| } |
| |
| void PermissionRequestManager::SetDismissOnTabClose() { |
| should_dismiss_current_request_ = true; |
| } |
| |
| void PermissionRequestManager::SetPromptShown() { |
| did_show_prompt_ = true; |
| } |
| |
| void PermissionRequestManager::SetDecisionTime() { |
| current_request_decision_time_ = base::Time::Now(); |
| } |
| |
| void PermissionRequestManager::SetManageClicked() { |
| set_manage_clicked(); |
| } |
| |
| void PermissionRequestManager::SetLearnMoreClicked() { |
| set_learn_more_clicked(); |
| } |
| |
| base::WeakPtr<PermissionPrompt::Delegate> |
| PermissionRequestManager::GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| content::WebContents* PermissionRequestManager::GetAssociatedWebContents() { |
| content::WebContents& web_contents = GetWebContents(); |
| return &web_contents; |
| } |
| |
| bool PermissionRequestManager::RecreateView() { |
| const bool should_do_auto_response_for_testing = |
| (current_request_prompt_disposition_ == |
| PermissionPromptDisposition::MAC_OS_PROMPT); |
| view_ = view_factory_.Run(web_contents(), this); |
| if (!view_) { |
| current_request_prompt_disposition_ = |
| PermissionPromptDisposition::NONE_VISIBLE; |
| if (ShouldDropCurrentRequestIfCannotShowQuietly()) { |
| CurrentRequestsDecided(PermissionAction::IGNORED); |
| } else if (IsCurrentRequestEmbeddedPermissionElementInitiated() || |
| IsCurrentRequestExclusiveAccess()) { |
| Ignore(); |
| } |
| NotifyPromptRecreateFailed(); |
| return false; |
| } |
| |
| current_request_prompt_disposition_ = view_->GetPromptDisposition(); |
| current_request_pepc_prompt_position_ = view_->GetPromptPosition(); |
| SetCurrentRequestsInitialStatuses(); |
| |
| if (auto_response_for_test_ != NONE && should_do_auto_response_for_testing) { |
| // MAC_OS_PROMPT disposition has it's own auto-response logic for testing, |
| // so if that was the original disposition we would have skipped our own |
| // auto-response logic. Since the disposition can have changed, trigger |
| // a possible auto response again here. |
| DoAutoResponseForTesting(); // IN-TEST |
| } |
| return true; |
| } |
| |
| const PermissionPrompt* PermissionRequestManager::GetCurrentPrompt() const { |
| return view_.get(); |
| } |
| |
| void PermissionRequestManager::SetPromptOptions( |
| PromptOptions prompt_options) { |
| for (auto& request : requests_) { |
| request->SetPromptOptions(prompt_options); |
| } |
| } |
| |
| bool PermissionRequestManager:: |
| IsCurrentRequestEmbeddedPermissionElementInitiated() const { |
| return IsRequestInProgress() && |
| requests_[0]->IsEmbeddedPermissionElementInitiated(); |
| } |
| |
| std::optional<gfx::Rect> |
| PermissionRequestManager::GetPromptBubbleViewBoundsInScreen() const { |
| return view_ ? view_->GetViewBoundsInScreen() : std::nullopt; |
| } |
| |
| PermissionRequestManager::PermissionRequestManager( |
| content::WebContents* web_contents, |
| tabs::TabInterface* tab_interface) |
| : content::WebContentsObserver(web_contents), |
| content::WebContentsUserData<PermissionRequestManager>(*web_contents), |
| view_factory_(base::BindRepeating(&PermissionPrompt::Create)), |
| auto_response_for_test_(NONE), |
| permission_ui_selectors_( |
| PermissionsClient::Get()->CreatePermissionUiSelectors( |
| web_contents->GetBrowserContext())) { |
| if (tab_interface) { |
| tab_is_active_ = tab_interface->IsActivated(); |
| RegisterTabSubscriptions(tab_interface); |
| } else { |
| tab_is_active_ = |
| web_contents->GetVisibility() != content::Visibility::HIDDEN; |
| } |
| } |
| |
| PermissionRequestManager::PermissionRequestManager( |
| content::WebContents* web_contents) |
| : PermissionRequestManager(web_contents, nullptr) { |
| ; |
| } |
| |
| 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()->HasUncommittedNavigationInPrimaryMainFrame() || view_ || |
| IsRequestInProgress()) { |
| return; |
| } |
| |
| // Find first valid request. |
| while (!pending_permission_requests_.IsEmpty()) { |
| auto next = pending_permission_requests_.Pop(); |
| if (HasActiveSourceFrameOrDisallowActivationOtherwise(next.get())) { |
| validated_requests_.push_back(next->GetWeakPtr()); |
| requests_.push_back(std::move(next)); |
| break; |
| } |
| FinalizeAndCancelRequest(next.get()); |
| } |
| |
| if (requests_.empty()) { |
| return; |
| } |
| |
| // Find additional requests that can be grouped with the first one. |
| for (; !pending_permission_requests_.IsEmpty();) { |
| auto* front = pending_permission_requests_.Peek(); |
| if (!HasActiveSourceFrameOrDisallowActivationOtherwise(front)) { |
| FinalizeAndCancelRequest(front); |
| continue; |
| } |
| |
| validated_requests_.push_back(front->GetWeakPtr()); |
| if (!ShouldGroupRequests(requests_.front().get(), front)) { |
| break; |
| } |
| |
| requests_.push_back(pending_permission_requests_.Pop()); |
| } |
| |
| // Mark the remaining pending requests as validated, so only the "new and has |
| // not been validated" requests added to the queue could have effect to |
| // priority order |
| for (const auto& request_list : pending_permission_requests_) { |
| for (auto& request : request_list) { |
| if (HasActiveSourceFrameOrDisallowActivationOtherwise(request.get())) { |
| validated_requests_.push_back(request->GetWeakPtr()); |
| } |
| } |
| } |
| |
| if (permission_ui_selectors_.empty()) { |
| current_request_ui_to_use_ = |
| UiDecision(UiDecision::UseNormalUi(), UiDecision::ShowNoWarning()); |
| ShowPrompt(); |
| return; |
| } |
| |
| 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) { |
| // Skip if we have already made a decision due to a higher priority |
| // selector |
| if (current_request_ui_to_use_.has_value() || !IsRequestInProgress()) { |
| break; |
| } |
| |
| if (!requests_.front()->IsEmbeddedPermissionElementInitiated() && |
| permission_ui_selectors_[selector_index]->IsPermissionRequestSupported( |
| requests_.front()->request_type())) { |
| permission_ui_selectors_[selector_index]->SelectUiToUse( |
| web_contents(), requests_.front().get(), |
| base::BindOnce(&PermissionRequestManager::OnPermissionUiSelectorDone, |
| weak_factory_.GetWeakPtr(), selector_index)); |
| continue; |
| } |
| |
| OnPermissionUiSelectorDone( |
| selector_index, |
| PermissionUiSelector::Decision::UseNormalUiAndShowNoWarning()); |
| } |
| } |
| |
| void PermissionRequestManager::ScheduleDequeueRequestIfNeeded() { |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&PermissionRequestManager::DequeueRequestIfNeeded, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void PermissionRequestManager::ShowPrompt() { |
| // 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 prompt if the previous one still exists. |
| if (!IsRequestInProgress() || view_) { |
| return; |
| } |
| |
| DCHECK(!web_contents()->HasUncommittedNavigationInPrimaryMainFrame()); |
| DCHECK(current_request_ui_to_use_); |
| |
| if (!tab_is_active_) { |
| NotifyPromptCreationFailedHiddenTab(); |
| return; |
| } |
| |
| // We check `requests_.empty()` after some following calls |
| // (`ReprioritizeCurrentRequestIfNeeded` and `RecreateView`) to prevent |
| // accidentally finalizing the requests, which could be triggered in the |
| // callback chains or error handling (e.g the factory implementation can't |
| // show a permission prompt). |
| if (!ReprioritizeCurrentRequestIfNeeded() || requests_.empty()) { |
| return; |
| } |
| |
| if (!RecreateView() || requests_.empty()) { |
| return; |
| } |
| |
| 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; |
| case QuietUiReason::kTriggeredDueToDisruptiveBehavior: |
| LogWarningToConsole( |
| kDisruptiveNotificationBehaviorEnforcementMessage); |
| break; |
| } |
| base::RecordAction(base::UserMetricsAction( |
| "Notifications.Quiet.PermissionRequestShown")); |
| } |
| |
| PermissionsClient::Get()->TriggerPromptHatsSurveyIfEnabled( |
| web_contents(), requests_[0]->request_type(), std::nullopt, |
| DetermineCurrentRequestUIDisposition(), |
| DetermineCurrentRequestUIDispositionReasonForUMA(), |
| requests_[0]->GetGestureType(), |
| /*prompt_display_duration=*/std::nullopt, /*is_post_prompt=*/false, |
| web_contents() |
| ->GetPrimaryMainFrame() |
| ->GetLastCommittedOrigin() |
| .GetURL(), |
| current_request_pepc_prompt_position_, |
| GetRequestInitialStatus(requests_[0].get()), |
| hats_shown_callback_.has_value() |
| ? std::move(hats_shown_callback_.value()) |
| : base::DoNothing(), |
| requests_[0]->prompt_options()); |
| |
| hats_shown_callback_.reset(); |
| } |
| current_request_already_displayed_ = true; |
| current_request_first_display_time_ = base::Time::Now(); |
| |
| NotifyPromptAdded(); |
| |
| // If in testing mode, automatically respond to the bubble that was shown. |
| if (auto_response_for_test_ != NONE) { |
| DoAutoResponseForTesting(); |
| } |
| } |
| |
| void PermissionRequestManager::SetHatsShownCallback( |
| base::OnceCallback<void()> callback) { |
| hats_shown_callback_ = std::move(callback); |
| } |
| |
| void PermissionRequestManager::DeletePrompt() { |
| DCHECK(view_); |
| { |
| base::AutoReset<bool> deleting(&ignore_callbacks_from_prompt_, true); |
| view_.reset(); |
| } |
| NotifyPromptRemoved(); |
| } |
| |
| 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(); |
| permission_request_relevance_.reset(); |
| current_request_ui_to_use_.reset(); |
| was_decision_held_back_.reset(); |
| selector_decisions_.clear(); |
| should_dismiss_current_request_ = false; |
| did_show_prompt_ = false; |
| did_click_manage_ = false; |
| did_click_learn_more_ = false; |
| hats_shown_callback_.reset(); |
| current_request_pepc_prompt_position_.reset(); |
| current_requests_initial_statuses_.clear(); |
| if (view_) { |
| DeletePrompt(); |
| } |
| } |
| |
| bool PermissionRequestManager::ShouldRecordUmaForCurrentPrompt() const { |
| return (!view_ || view_->IsAskPrompt()); |
| } |
| |
| void PermissionRequestManager::CurrentRequestsDecided( |
| 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(); |
| } |
| |
| std::optional<permissions::PermissionIgnoredReason> ignore_reason = |
| std::nullopt; |
| #if !BUILDFLAG(IS_ANDROID) |
| // ignore reason metric currently not supported on android |
| if (permission_action == PermissionAction::IGNORED) { |
| ignore_reason = std::make_optional( |
| PermissionsClient::Get()->DetermineIgnoreReason(web_contents())); |
| } |
| #endif |
| |
| content::BrowserContext* browser_context = |
| web_contents()->GetBrowserContext(); |
| |
| if (ShouldRecordUmaForCurrentPrompt()) { |
| PermissionUmaUtil::PermissionPromptResolved( |
| requests_, web_contents(), permission_action, time_to_decision, |
| DetermineCurrentRequestUIDisposition(), |
| DetermineCurrentRequestUIDispositionReasonForUMA(), |
| view_ ? std::optional(view_->GetPromptVariants()) : std::nullopt, |
| prediction_grant_likelihood_, permission_request_relevance_, |
| was_decision_held_back_, ignore_reason, did_show_prompt_, |
| did_click_manage_, did_click_learn_more_); |
| } |
| |
| std::optional<QuietUiReason> quiet_ui_reason; |
| if (ShouldCurrentRequestUseQuietUI()) { |
| quiet_ui_reason = ReasonForUsingQuietUi(); |
| } |
| |
| for (auto& 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; |
| } |
| |
| auto time_since_shown = |
| current_request_first_display_time_.is_null() |
| ? base::TimeDelta::Max() |
| : base::Time::Now() - current_request_first_display_time_; |
| PermissionsClient::Get()->OnPromptResolved( |
| request.get(), permission_action, |
| DetermineCurrentRequestUIDisposition(), |
| DetermineCurrentRequestUIDispositionReasonForUMA(), quiet_ui_reason, |
| time_since_shown, current_request_pepc_prompt_position_, |
| GetRequestInitialStatus(request.get()), web_contents()); |
| |
| PermissionUmaUtil::RecordEmbargoStatus(RecordActionAndGetEmbargoStatus( |
| browser_context, request.get(), permission_action)); |
| } |
| |
| if (ShouldFinalizeRequestAfterDecided(permission_action)) { |
| FinalizeCurrentRequests(); |
| } |
| } |
| |
| void PermissionRequestManager::CleanUpRequests() { |
| // No need to execute the preignore logic as we canceling currently active |
| // requests anyway. |
| preignore_timer_.Stop(); |
| |
| for (; !pending_permission_requests_.IsEmpty(); |
| pending_permission_requests_.Pop()) { |
| auto* pending_request = pending_permission_requests_.Peek(); |
| EraseRequest(validated_requests_, pending_request); |
| request_sources_map_.erase(pending_request); |
| CancelRequestIncludingDuplicates(pending_request); |
| FinishRequestIncludingDuplicates(pending_request); |
| } |
| |
| if (IsRequestInProgress()) { |
| std::vector<std::unique_ptr<PermissionRequest>>::iterator requests_iter; |
| |
| for (requests_iter = requests_.begin(); requests_iter != requests_.end(); |
| requests_iter++) { |
| CancelRequestIncludingDuplicates(requests_iter->get()); |
| } |
| |
| CurrentRequestsDecided(should_dismiss_current_request_ |
| ? PermissionAction::DISMISSED |
| : PermissionAction::IGNORED); |
| should_dismiss_current_request_ = false; |
| } |
| } |
| |
| PermissionRequest* PermissionRequestManager::GetExistingRequest( |
| PermissionRequest* request) const { |
| for (const auto& existing_request : requests_) { |
| if (request->IsDuplicateOf(existing_request.get())) { |
| return existing_request.get(); |
| } |
| } |
| return pending_permission_requests_.FindDuplicate(request); |
| } |
| |
| PermissionRequestManager::PermissionRequestList::iterator |
| PermissionRequestManager::FindDuplicateRequestList(PermissionRequest* request) { |
| for (auto request_list = duplicate_requests_.begin(); |
| request_list != duplicate_requests_.end(); ++request_list) { |
| for (auto iter = request_list->begin(); iter != request_list->end();) { |
| const auto& current_request = (*iter); |
| |
| // The first valid request in the list will indicate whether all other |
| // members are duplicate or not. |
| if (current_request->IsDuplicateOf(request)) { |
| return request_list; |
| } |
| |
| break; |
| } |
| } |
| |
| return duplicate_requests_.end(); |
| } |
| |
| PermissionRequestManager::PermissionRequestList::iterator |
| PermissionRequestManager::VisitDuplicateRequests( |
| DuplicateRequestVisitor visitor, |
| PermissionRequest* request) { |
| auto request_list = FindDuplicateRequestList(request); |
| if (request_list == duplicate_requests_.end()) { |
| return request_list; |
| } |
| |
| for (auto iter = request_list->begin(); iter != request_list->end();) { |
| if (auto& weak_request = (*iter)) { |
| visitor.Run(weak_request); |
| ++iter; |
| } else { |
| // Remove any requests that have been destroyed. |
| iter = request_list->erase(iter); |
| } |
| } |
| |
| return request_list; |
| } |
| |
| void PermissionRequestManager::PermissionGrantedIncludingDuplicates( |
| PermissionRequest* request, |
| bool is_one_time) { |
| CHECK(RequestExistsExactlyOnce(request, pending_permission_requests_, |
| requests_)) |
| << "Only requests in [pending_permission_]requests_ can have duplicates"; |
| request->PermissionGranted(is_one_time); |
| VisitDuplicateRequests( |
| base::BindRepeating( |
| [](bool is_one_time, |
| const std::unique_ptr<PermissionRequest>& request) { |
| request->PermissionGranted(is_one_time); |
| }, |
| is_one_time), |
| request); |
| } |
| |
| void PermissionRequestManager::PermissionDeniedIncludingDuplicates( |
| PermissionRequest* request) { |
| CHECK(RequestExistsExactlyOnce(request, pending_permission_requests_, |
| requests_)) |
| << "Only requests in [pending_permission_]requests_ can have duplicates"; |
| request->PermissionDenied(); |
| VisitDuplicateRequests( |
| base::BindRepeating( |
| [](const std::unique_ptr<PermissionRequest>& request) { |
| request->PermissionDenied(); |
| }), |
| request); |
| } |
| |
| void PermissionRequestManager::CancelRequestIncludingDuplicates( |
| PermissionRequest* request, |
| bool is_final_decision) { |
| CHECK(RequestExistsExactlyOnce(request, pending_permission_requests_, |
| requests_)) |
| << "Only requests in [pending_permission_]requests_ can have duplicates"; |
| request->Cancelled(is_final_decision); |
| VisitDuplicateRequests( |
| base::BindRepeating( |
| [](bool is_final, const std::unique_ptr<PermissionRequest>& request) { |
| request->Cancelled(is_final); |
| }, |
| is_final_decision), |
| request); |
| } |
| |
| void PermissionRequestManager::FinishRequestIncludingDuplicates( |
| PermissionRequest* request) { |
| CHECK(RequestExistsExactlyOnce(request, pending_permission_requests_, |
| requests_)) |
| << "Only requests in [pending_permission_]requests_ can have duplicates"; |
| auto duplicate_list = FindDuplicateRequestList(request); |
| |
| // Additionally, we can now remove the duplicates. |
| if (duplicate_list != duplicate_requests_.end()) { |
| duplicate_requests_.erase(duplicate_list); |
| } |
| } |
| |
| void PermissionRequestManager::AddObserver(Observer* observer) { |
| if (!observer_list_.HasObserver(observer)) { |
| observer_list_.AddObserver(observer); |
| } |
| } |
| |
| void PermissionRequestManager::RemoveObserver(Observer* observer) { |
| observer_list_.RemoveObserver(observer); |
| } |
| |
| bool PermissionRequestManager::ShouldCurrentRequestUseQuietUI() const { |
| if (IsCurrentRequestEmbeddedPermissionElementInitiated()) { |
| return false; |
| } |
| // ContentSettingImageModel might call into this method if the user switches |
| // between tabs while the |notification_permission_ui_selectors_| are |
| // pending. |
| return ReasonForUsingQuietUi() != std::nullopt; |
| } |
| |
| std::optional<PermissionRequestManager::QuietUiReason> |
| PermissionRequestManager::ReasonForUsingQuietUi() const { |
| if (!IsRequestInProgress() || !current_request_ui_to_use_ || |
| !current_request_ui_to_use_->quiet_ui_reason) { |
| return std::nullopt; |
| } |
| |
| return *(current_request_ui_to_use_->quiet_ui_reason); |
| } |
| |
| bool PermissionRequestManager::IsRequestInProgress() const { |
| return !requests_.empty(); |
| } |
| |
| bool PermissionRequestManager::CanRestorePrompt() { |
| #if BUILDFLAG(IS_ANDROID) |
| return false; |
| #else |
| return IsRequestInProgress() && |
| current_request_prompt_disposition_.has_value() && !view_; |
| #endif |
| } |
| |
| void PermissionRequestManager::RestorePrompt() { |
| if (CanRestorePrompt()) { |
| ShowPrompt(); |
| } |
| } |
| |
| bool PermissionRequestManager::ShouldDropCurrentRequestIfCannotShowQuietly() |
| const { |
| std::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: |
| case QuietUiReason::kTriggeredDueToDisruptiveBehavior: |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| void PermissionRequestManager::NotifyTabActiveChanged(bool is_active) { |
| for (Observer& observer : observer_list_) { |
| observer.OnTabActiveChanged(is_active); |
| } |
| } |
| |
| void PermissionRequestManager::NotifyPromptAdded() { |
| for (Observer& observer : observer_list_) { |
| observer.OnPromptAdded(); |
| } |
| } |
| |
| void PermissionRequestManager::NotifyPromptRemoved() { |
| for (Observer& observer : observer_list_) { |
| observer.OnPromptRemoved(); |
| } |
| } |
| |
| void PermissionRequestManager::NotifyPromptRecreateFailed() { |
| for (Observer& observer : observer_list_) { |
| observer.OnPromptRecreateViewFailed(); |
| } |
| } |
| |
| void PermissionRequestManager::NotifyPromptCreationFailedHiddenTab() { |
| for (Observer& observer : observer_list_) { |
| observer.OnPromptCreationFailedHiddenTab(); |
| } |
| } |
| |
| void PermissionRequestManager::NotifyRequestDecided( |
| permissions::PermissionAction permission_action) { |
| for (Observer& observer : observer_list_) { |
| observer.OnRequestDecided(permission_action); |
| } |
| } |
| |
| void PermissionRequestManager::StorePermissionActionForUMA( |
| const GURL& origin, |
| RequestType request_type, |
| PermissionAction permission_action) { |
| if (!ShouldRecordUmaForCurrentPrompt()) { |
| return; |
| } |
| |
| std::optional<ContentSettingsType> content_settings_type = |
| RequestTypeToContentSettingsType(request_type); |
| if (content_settings_type.has_value()) { |
| PermissionsClient::Get() |
| ->GetOriginKeyedPermissionActionService( |
| web_contents()->GetBrowserContext()) |
| ->RecordAction(PermissionUtil::GetLastCommittedOriginAsURL( |
| web_contents()->GetPrimaryMainFrame()), |
| content_settings_type.value(), permission_action); |
| } |
| } |
| |
| 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; |
| case WarningReason::kDisruptiveBehavior: |
| 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 (permission_ui_selectors_[decision_index]->IsPermissionRequestSupported( |
| requests_.front()->request_type())) { |
| if (!prediction_grant_likelihood_.has_value()) { |
| prediction_grant_likelihood_ = permission_ui_selectors_[decision_index] |
| ->PredictedGrantLikelihoodForUKM(); |
| } |
| |
| if (!permission_request_relevance_.has_value()) { |
| permission_request_relevance_ = |
| permission_ui_selectors_[decision_index] |
| ->PermissionRequestRelevanceForUKM(); |
| } |
| |
| 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()) { |
| ShowPrompt(); |
| } |
| } |
| |
| 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: |
| case QuietUiReason::kTriggeredDueToDisruptiveBehavior: |
| 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()->GetPrimaryMainFrame()->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kWarning, message); |
| } |
| |
| void PermissionRequestManager::DoAutoResponseForTesting() { |
| // The macOS prompt has its own mechanism of auto responding. |
| if (current_request_prompt_disposition_ == |
| PermissionPromptDisposition::MAC_OS_PROMPT) { |
| return; |
| } |
| 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(); |
| } |
| } |
| |
| bool PermissionRequestManager::IsCurrentRequestExclusiveAccess() const { |
| #if !BUILDFLAG(IS_ANDROID) |
| return IsRequestInProgress() && |
| IsExclusiveAccessRequest(requests_[0]->request_type()); |
| #else |
| return false; |
| #endif |
| } |
| |
| bool PermissionRequestManager::ShouldFinalizeRequestAfterDecided( |
| PermissionAction action) const { |
| // If the action is IGNORED, it is not coming from the prompt itself but |
| // rather from external circumstance (like tab switching) and therefore |
| // |view_->ShouldFinalizeRequestAfterDecided| is not queried. |
| |
| // If there is an autoresponse set, or there is no |view_|, finalize the |
| // request since there won't be a separate |FinalizeCurrentRequests()| call. |
| if (action == PermissionAction::IGNORED || auto_response_for_test_ != NONE || |
| !view_) { |
| return true; |
| } |
| |
| return view_->ShouldFinalizeRequestAfterDecided(); |
| } |
| |
| PermissionEmbargoStatus |
| PermissionRequestManager::RecordActionAndGetEmbargoStatus( |
| content::BrowserContext* browser_context, |
| PermissionRequest* request, |
| PermissionAction permission_action) { |
| if (!request->uses_automatic_embargo()) { |
| return PermissionEmbargoStatus::NOT_EMBARGOED; |
| } |
| |
| PermissionDecisionAutoBlocker* const autoblocker = |
| PermissionsClient::Get()->GetPermissionDecisionAutoBlocker( |
| browser_context); |
| |
| if (permission_action == PermissionAction::DISMISSED && |
| !request->IsEmbeddedPermissionElementInitiated()) { |
| if (autoblocker->RecordDismissAndEmbargo( |
| request->requesting_origin(), request->GetContentSettingsType(), |
| ShouldCurrentRequestUseQuietUI())) { |
| return PermissionEmbargoStatus::REPEATED_DISMISSALS; |
| } |
| } else if (permission_action == PermissionAction::IGNORED && |
| !request->IsEmbeddedPermissionElementInitiated()) { |
| if (autoblocker->RecordIgnoreAndEmbargo(request->requesting_origin(), |
| request->GetContentSettingsType(), |
| ShouldCurrentRequestUseQuietUI())) { |
| return PermissionEmbargoStatus::REPEATED_IGNORES; |
| } |
| } else if (permission_action == PermissionAction::GRANTED_ONCE) { |
| autoblocker->RemoveEmbargoAndResetCounts(request->requesting_origin(), |
| request->GetContentSettingsType()); |
| } |
| |
| return PermissionEmbargoStatus::NOT_EMBARGOED; |
| } |
| |
| void PermissionRequestManager::SetCurrentRequestsInitialStatuses() { |
| // This function is called whenever the view is created which can happen |
| // multiple times for the same request (e.g. by tab switching). Only actually |
| // compute this if |current_requests_initial_statuses_| has been cleared |
| // before to mark a view being closed. |
| if (!current_requests_initial_statuses_.empty()) { |
| return; |
| } |
| |
| auto* map = PermissionsClient::Get()->GetSettingsMap( |
| web_contents()->GetBrowserContext()); |
| for (const auto& request : requests_) { |
| // It's possible in tests for |map| to not be initialized yet. Also there |
| // are some permission requests (like SMART_CARD_DATA) which are not for |
| // content settings. |
| if (!map || !content_settings::ContentSettingsRegistry::GetInstance()->Get( |
| request->GetContentSettingsType())) { |
| current_requests_initial_statuses_.emplace(request.get(), |
| CONTENT_SETTING_DEFAULT); |
| } else { |
| current_requests_initial_statuses_.emplace( |
| request.get(), |
| map->GetContentSetting(GetRequestingOrigin(), GetEmbeddingOrigin(), |
| request->GetContentSettingsType())); |
| } |
| } |
| } |
| |
| ContentSetting PermissionRequestManager::GetRequestInitialStatus( |
| PermissionRequest* request) { |
| if (current_requests_initial_statuses_.contains(request)) { |
| return current_requests_initial_statuses_.at(request); |
| } |
| |
| return CONTENT_SETTING_DEFAULT; |
| } |
| |
| void PermissionRequestManager::RegisterTabSubscriptions( |
| tabs::TabInterface* tab_interface) { |
| tab_subscriptions_.clear(); |
| |
| tab_subscriptions_.push_back(tab_interface->RegisterDidActivate( |
| base::BindRepeating(&PermissionRequestManager::OnTabActiveStatusChanged, |
| weak_factory_.GetWeakPtr(), /*is_active=*/true))); |
| tab_subscriptions_.push_back(tab_interface->RegisterWillDeactivate( |
| base::BindRepeating(&PermissionRequestManager::OnTabActiveStatusChanged, |
| weak_factory_.GetWeakPtr(), /*is_active=*/false))); |
| |
| tab_subscriptions_.push_back(tab_interface->RegisterWillDetach( |
| base::BindRepeating(&PermissionRequestManager::OnTabDetached, |
| weak_factory_.GetWeakPtr()))); |
| // Store this separately because this must not be cleared when a tab is |
| // detached. |
| tab_insert_subscription_ = tab_interface->RegisterDidInsert( |
| base::BindRepeating(&PermissionRequestManager::OnTabAttached, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void PermissionRequestManager::OnTabActiveStatusChanged( |
| bool is_active, |
| tabs::TabInterface* tab_interface) { |
| const bool prior_tab_is_active_ = tab_is_active_; |
| tab_is_active_ = is_active; |
| if (prior_tab_is_active_ != tab_is_active_) { |
| OnTabActiveChanged(); |
| } |
| } |
| |
| void PermissionRequestManager::OnTabDetached( |
| tabs::TabInterface* tab_interface, |
| tabs::TabInterface::DetachReason reason) { |
| // Clear the existing tab subscriptions while the tab is detached from a tab |
| // strip. This might mean the TabInterface will become part of a PWA which |
| // should fall back to the OnVisibilityChanged listeners. |
| tab_subscriptions_.clear(); |
| } |
| |
| void PermissionRequestManager::OnTabAttached( |
| tabs::TabInterface* tab_interface) { |
| RegisterTabSubscriptions(tab_interface); |
| } |
| |
| void PermissionRequestManager::OnTabActiveChanged() { |
| NotifyTabActiveChanged(tab_is_active_); |
| if (!tab_is_active_) { |
| if (view_) { |
| switch (view_->GetTabSwitchingBehavior()) { |
| case PermissionPrompt::TabSwitchingBehavior:: |
| kDestroyPromptButKeepRequestPending: { |
| DeletePrompt(); |
| break; |
| } |
| case PermissionPrompt::TabSwitchingBehavior:: |
| kDestroyPromptAndIgnoreRequest: { |
| Ignore(); |
| break; |
| } |
| case PermissionPrompt::TabSwitchingBehavior::kKeepPromptAlive: { |
| break; |
| } |
| } |
| } |
| |
| return; |
| } |
| |
| if (web_contents()->HasUncommittedNavigationInPrimaryMainFrame()) { |
| 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()) { |
| ShowPrompt(); |
| } |
| } |
| |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(PermissionRequestManager); |
| |
| } // namespace permissions |