blob: 7e6c91d9377dc8aaeaafb6fd7687b629d03af74a [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/privacy_sandbox/privacy_sandbox_prompt_helper.h"
#include "base/hash/hash.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/privacy_sandbox/notice/desktop_entrypoint_handlers_helper.h"
#include "chrome/browser/privacy_sandbox/privacy_sandbox_queue_manager.h"
#include "chrome/browser/privacy_sandbox/privacy_sandbox_service.h"
#include "chrome/browser/privacy_sandbox/privacy_sandbox_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service.h"
#include "chrome/browser/search_engine_choice/search_engine_choice_dialog_service_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/privacy_sandbox/privacy_sandbox_prompt.h"
#include "chrome/browser/ui/profiles/profile_customization_bubble_sync_controller.h"
#include "chrome/browser/ui/signin/signin_view_controller.h"
#include "chrome/common/webui_url_constants.h"
#include "components/privacy_sandbox/privacy_sandbox_features.h"
#include "components/sync/service/sync_service.h"
#include "components/web_modal/web_contents_modal_dialog_host.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/manifest_handlers/chrome_url_overrides_handler.h"
namespace {
constexpr char kPrivacySandboxPromptHelperEventHistogram[] =
"Settings.PrivacySandbox.PromptHelperEvent2";
constexpr int kMinRequiredDialogHeight = 100;
// Gets the type of prompt that should be displayed for |profile|, this includes
// the possibility of no prompt being required.
PrivacySandboxService::PromptType GetRequiredPromptType(Profile* profile) {
if (!profile || !profile->IsRegularProfile()) {
return PrivacySandboxService::PromptType::kNone;
}
auto* privacy_sandbox_service =
PrivacySandboxServiceFactory::GetForProfile(profile);
if (!privacy_sandbox_service) {
return PrivacySandboxService::PromptType::kNone;
}
return privacy_sandbox_service->GetRequiredPromptType(
PrivacySandboxService::SurfaceType::kDesktop);
}
#if BUILDFLAG(IS_CHROMEOS)
bool HasExtensionNtpOverride(
extensions::ExtensionRegistry* extension_registry) {
for (const auto& extension : extension_registry->enabled_extensions()) {
const auto& overrides =
extensions::URLOverrides::GetChromeURLOverrides(extension.get());
if (overrides.find(chrome::kChromeUINewTabHost) != overrides.end()) {
return true;
}
}
return false;
}
// Returns whether |url| is an NTP controlled entirely by Chrome.
bool IsChromeControlledNtpUrl(const GURL& url) {
// Convert to origins for comparison, as any appended paths are irrelevant.
const auto ntp_origin = url::Origin::Create(url);
return ntp_origin ==
url::Origin::Create(GURL(chrome::kChromeUINewTabPageURL)) ||
ntp_origin == url::Origin::Create(
GURL(chrome::kChromeUINewTabPageThirdPartyURL));
}
#endif // BUILDFLAG(IS_CHROMEOS)
} // namespace
PrivacySandboxPromptHelper::~PrivacySandboxPromptHelper() = default;
PrivacySandboxPromptHelper::PrivacySandboxPromptHelper(
content::WebContents* web_contents)
: WebContentsObserver(web_contents),
content::WebContentsUserData<PrivacySandboxPromptHelper>(*web_contents) {
base::UmaHistogramEnumeration(
kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::kCreated);
}
void PrivacySandboxPromptHelper::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
if (!ProfileRequiresPrompt(profile())) {
base::UmaHistogramEnumeration(
kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::kPromptNotRequired);
return;
}
// Only valid top frame navigations are considered.
if (!navigation_handle || !navigation_handle->HasCommitted() ||
!navigation_handle->IsInPrimaryMainFrame() ||
navigation_handle->IsSameDocument()) {
base::UmaHistogramEnumeration(
kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::kNonTopFrameNavigation);
return;
}
#if BUILDFLAG(IS_CHROMEOS)
// TODO(crbug.com/1315580, crbug.com/1315579): When navigating to a NTP that
// isn't Chrome-controlled on ChromeOS, open an about blank tab to display the
// prompt. On other platforms, it's being handled during the startup.
if (web_contents()->GetLastCommittedURL() == chrome::kChromeUINewTabURL) {
const bool has_extention_override =
HasExtensionNtpOverride(extensions::ExtensionRegistry::Get(profile()));
const GURL new_tab_page = search::GetNewTabPageURL(profile());
const bool is_non_chrome_controlled_ntp =
navigation_handle->GetURL() == new_tab_page &&
!IsChromeControlledNtpUrl(new_tab_page);
if (has_extention_override || is_non_chrome_controlled_ntp) {
web_contents()->OpenURL(
content::OpenURLParams(GURL(url::kAboutBlankURL), content::Referrer(),
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
/*is_renderer_initiated=*/false),
/*navigation_handle_callback=*/{});
base::UmaHistogramEnumeration(
kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::kAboutBlankOpened);
return;
}
}
#endif // BUILDFLAG(IS_CHROMEOS)
// Check whether the navigation target is a suitable prompt location. The
// navigation URL, rather than the visible or committed URL, is required to
// distinguish between different types of NTPs.
if (!privacy_sandbox::IsUrlSuitableForPrompt(navigation_handle->GetURL())) {
base::UmaHistogramEnumeration(
kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::kUrlNotSuitable);
return;
}
// If a Sync setup is in progress, the prompt should not be shown.
if (auto* sync_service = SyncServiceFactory::GetForProfile(profile())) {
if (sync_service->IsSetupInProgress()) {
base::UmaHistogramEnumeration(
kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::kSyncSetupInProgress);
return;
}
}
auto* browser =
chrome::FindBrowserWithTab(navigation_handle->GetWebContents());
// If a sign-in dialog is being currently displayed or is about to be
// displayed, the prompt should not be shown to avoid conflict.
// TODO(crbug.com/370806609): When we add sign in notice to queue, put this
// behind flag / remove.
bool signin_dialog_showing =
browser->GetFeatures().signin_view_controller()->ShowsModalDialog();
#if !BUILDFLAG(IS_CHROMEOS)
signin_dialog_showing =
signin_dialog_showing ||
IsProfileCustomizationBubbleSyncControllerRunning(browser);
#endif // !BUILDFLAG(IS_CHROMEOS)
if (signin_dialog_showing) {
base::UmaHistogramEnumeration(
kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::kSigninDialogShown);
return;
}
// If a Privacy Sandbox prompt already exists for this browser, do not attempt
// to open another one.
if (auto* privacy_sandbox_service =
PrivacySandboxServiceFactory::GetForProfile(profile())) {
if (privacy_sandbox_service->IsPromptOpenForBrowser(browser)) {
base::UmaHistogramEnumeration(kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::
kPromptAlreadyExistsForBrowser);
return;
}
}
// The PrivacySandbox prompt can always fit inside a normal tabbed window due
// to its minimum width, so checking the height is enough here. Other non
// normal tabbed browsers will be exlcuded in a later check.
const bool is_window_height_too_small =
browser->window()
->GetWebContentsModalDialogHost()
->GetMaximumDialogSize()
.height() < kMinRequiredDialogHeight;
// If the windows height is too small, it is difficult to read or interact
// with the dialog. The dialog is blocking modal, that is why we want to
// prevent it from showing if there isn't enough space.
if (is_window_height_too_small) {
base::UmaHistogramEnumeration(
kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::kWindowTooSmall);
return;
}
// Avoid showing the prompt on popups, pip, anything that isn't a normal
// browser.
if (browser->type() != Browser::TYPE_NORMAL) {
base::UmaHistogramEnumeration(
kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::kNonNormalBrowser);
return;
}
// If the handle is not being held, do not attempt to show the prompt.
// We want to check this constraint at the very end for histogram emitting
// reasons.
if (auto* privacy_sandbox_service =
PrivacySandboxServiceFactory::GetForProfile(profile())) {
privacy_sandbox::PrivacySandboxQueueManager& queue_manager =
privacy_sandbox_service->GetPrivacySandboxNoticeQueueManager();
if (base::FeatureList::IsEnabled(
privacy_sandbox::kPrivacySandboxNoticeQueue) &&
!queue_manager.IsHoldingHandle()) {
queue_manager.MaybeEmitQueueStateMetrics();
return;
}
}
// Record the URL that the prompt was displayed over.
uint32_t host_hash = base::Hash(navigation_handle->GetURL().IsAboutBlank()
? "about:blank"
: navigation_handle->GetURL().GetHost());
base::UmaHistogramSparse(
"Settings.PrivacySandbox.DialogDisplayHost",
static_cast<base::HistogramBase::Sample32>(host_hash));
browser->tab_strip_model()->ActivateTabAt(
browser->tab_strip_model()->GetIndexOfWebContents(
navigation_handle->GetWebContents()));
PrivacySandboxDialog::Show(browser, GetRequiredPromptType(profile()));
base::UmaHistogramEnumeration(
kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::kPromptShown);
}
// static
bool PrivacySandboxPromptHelper::ProfileRequiresPrompt(Profile* profile) {
bool eligible = GetRequiredPromptType(profile) !=
PrivacySandboxService::PromptType::kNone;
// TODO(crbug.com/370804492): When we add DMA notice to queue, put this behind
// flag / remove.
SearchEngineChoiceDialogService* search_engine_choice_dialog_service =
SearchEngineChoiceDialogServiceFactory::GetForProfile(profile);
if (search_engine_choice_dialog_service &&
search_engine_choice_dialog_service->CanSuppressPrivacySandboxPromo()) {
base::UmaHistogramEnumeration(kPrivacySandboxPromptHelperEventHistogram,
SettingsPrivacySandboxPromptHelperEvent::
kSearchEngineChoiceDialogShown);
eligible = false;
}
if (auto* privacy_sandbox_service =
PrivacySandboxServiceFactory::GetForProfile(profile)) {
privacy_sandbox::PrivacySandboxQueueManager& queue_manager =
privacy_sandbox_service->GetPrivacySandboxNoticeQueueManager();
// When checking profile eligibility also update the queue.
// Case 1: Profile is eligible, but not in the queue. Add to queue.
// Case 2: Profile is ineligible, but we are queued, so we must unqueue. OR
// We are holding the handle, so we must release the handle and
// prevent showing.
eligible ? queue_manager.MaybeQueueNotice()
: queue_manager.MaybeUnqueueNotice();
}
return eligible;
}
Profile* PrivacySandboxPromptHelper::profile() {
return Profile::FromBrowserContext(web_contents()->GetBrowserContext());
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(PrivacySandboxPromptHelper);