| // Copyright 2023 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/extensions/controlled_home_bubble_delegate.h" |
| |
| #include <utility> |
| |
| #include "base/auto_reset.h" |
| #include "base/no_destructor.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/extensions/settings_api_helpers.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/common/url_constants.h" |
| #include "chrome/grit/branded_strings.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "content/public/common/referrer.h" |
| #include "extensions/browser/disable_reason.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/extension_registrar.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_registry_observer.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_id.h" |
| #include "extensions/common/manifest.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/window_open_disposition.h" |
| |
| namespace { |
| |
| // Whether we should ignore learn more clicks. |
| bool g_should_ignore_learn_more_for_testing = false; |
| |
| // The set of profiles for which a controlled home bubble has been shown (we |
| // only show once per profile per session). |
| std::set<Profile*>& GetShownProfileSet() { |
| static base::NoDestructor<std::set<Profile*>> g_profiles; |
| return *g_profiles; |
| } |
| |
| // The set of profiles for which a bubble is pending (but hasn't yet shown). |
| std::set<Profile*>& GetPendingProfileSet() { |
| static base::NoDestructor<std::set<Profile*>> g_profiles; |
| return *g_profiles; |
| } |
| |
| // Gets the extension that currently controls the home page and has not yet |
| // been acknowledged, if any. |
| const extensions::Extension* GetExtensionToWarnAbout(Profile& profile) { |
| const extensions::Extension* controlling_extension = |
| extensions::GetExtensionOverridingHomepage(&profile); |
| if (!controlling_extension) { |
| // No controlling extension; nothing to warn about. |
| return nullptr; |
| } |
| |
| extensions::ExtensionPrefs* extension_prefs = |
| extensions::ExtensionPrefs::Get(&profile); |
| bool was_acknowledged = false; |
| if (extension_prefs->ReadPrefAsBoolean( |
| controlling_extension->id(), |
| ControlledHomeBubbleDelegate::kAcknowledgedPreference, |
| &was_acknowledged) && |
| was_acknowledged) { |
| // Extension was already acknowledged. |
| return nullptr; |
| } |
| |
| return controlling_extension; |
| } |
| |
| // Acknowledges the extension with the given `extension_id` so that we don't |
| // prompt the user about it again in the future. |
| void AcknowledgeExtension(Profile& profile, |
| const extensions::ExtensionId& extension_id) { |
| extensions::ExtensionPrefs* extension_prefs = |
| extensions::ExtensionPrefs::Get(&profile); |
| extension_prefs->UpdateExtensionPref( |
| extension_id, ControlledHomeBubbleDelegate::kAcknowledgedPreference, |
| base::Value(true)); |
| } |
| |
| } // namespace |
| |
| ControlledHomeBubbleDelegate::ControlledHomeBubbleDelegate(Browser* browser) |
| : browser_(browser), |
| profile_(browser->profile()), |
| extension_(GetExtensionToWarnAbout(*profile_)) {} |
| |
| ControlledHomeBubbleDelegate::~ControlledHomeBubbleDelegate() { |
| GetPendingProfileSet().erase(profile_); |
| } |
| |
| base::AutoReset<bool> |
| ControlledHomeBubbleDelegate::IgnoreLearnMoreForTesting() { |
| return base::AutoReset<bool>(&g_should_ignore_learn_more_for_testing, true); |
| } |
| |
| void ControlledHomeBubbleDelegate::ClearProfileSetForTesting() { |
| GetShownProfileSet().clear(); |
| } |
| |
| bool ControlledHomeBubbleDelegate::ShouldShow() { |
| // Show if there's a non-acknowledged controlling extension and we haven't |
| // shown (and aren't about to show in a pending bubble) for this profile. |
| return extension_ && GetShownProfileSet().count(profile_) == 0u && |
| GetPendingProfileSet().count(profile_) == 0u; |
| } |
| |
| void ControlledHomeBubbleDelegate::PendingShow() { |
| DCHECK_EQ(0u, GetPendingProfileSet().count(profile_)); |
| // Mark the profile as having a pending bubble. This way, we won't queue up |
| // another bubble if one is waiting for animation. |
| GetPendingProfileSet().insert(profile_); |
| } |
| |
| std::u16string ControlledHomeBubbleDelegate::GetHeadingText() { |
| return l10n_util::GetStringUTF16(IDS_EXTENSIONS_CONTROLLED_HOME_DIALOG_TITLE); |
| } |
| |
| std::u16string ControlledHomeBubbleDelegate::GetBodyText() { |
| const extensions::SettingsOverrides* settings = |
| extensions::SettingsOverrides::Get(extension_.get()); |
| CHECK(settings); |
| |
| bool startup_change = !settings->startup_pages.empty(); |
| bool search_change = settings->search_engine.has_value(); |
| int second_line_id = 0; |
| if (startup_change && search_change) { |
| second_line_id = IDS_EXTENSIONS_SETTINGS_API_SECOND_LINE_START_AND_SEARCH; |
| } else if (startup_change) { |
| second_line_id = IDS_EXTENSIONS_SETTINGS_API_SECOND_LINE_START_PAGES; |
| } else if (search_change) { |
| second_line_id = IDS_EXTENSIONS_SETTINGS_API_SECOND_LINE_SEARCH_ENGINE; |
| } |
| |
| std::u16string body; |
| body = l10n_util::GetStringFUTF16( |
| IDS_EXTENSIONS_CONTROLLED_HOME_DIALOG_DESCRIPTION, |
| extensions::util::GetFixupExtensionNameForUIDisplay(extension_->name())); |
| |
| if (second_line_id) { |
| body += l10n_util::GetStringUTF16(second_line_id); |
| } |
| |
| body += l10n_util::GetStringUTF16( |
| IDS_EXTENSIONS_SETTINGS_API_THIRD_LINE_CONFIRMATION); |
| |
| return body; |
| } |
| |
| std::u16string ControlledHomeBubbleDelegate::GetActionButtonText() { |
| // An empty string is returned so that we don't display the button prompting |
| // to remove policy-installed extensions. |
| if (IsPolicyIndicationNeeded()) { |
| return std::u16string(); |
| } |
| return l10n_util::GetStringUTF16(IDS_EXTENSION_CONTROLLED_RESTORE_SETTINGS); |
| } |
| |
| std::u16string ControlledHomeBubbleDelegate::GetDismissButtonText() { |
| return l10n_util::GetStringUTF16(IDS_EXTENSION_CONTROLLED_KEEP_CHANGES); |
| } |
| |
| std::string ControlledHomeBubbleDelegate::GetAnchorActionId() { |
| return extension_->id(); |
| } |
| |
| void ControlledHomeBubbleDelegate::OnBubbleShown() { |
| DCHECK_EQ(0u, GetShownProfileSet().count(profile_)); |
| DCHECK_EQ(1u, GetPendingProfileSet().count(profile_)); |
| |
| GetShownProfileSet().insert(profile_); |
| GetPendingProfileSet().erase(profile_); |
| } |
| |
| void ControlledHomeBubbleDelegate::OnBubbleClosed(CloseAction action) { |
| // OnBubbleClosed() can be called twice when we receive multiple |
| // "OnWidgetDestroying" notifications (this can at least happen when we close |
| // a window with a notification open). Handle this gracefully. |
| if (close_action_) { |
| DCHECK(close_action_ == CLOSE_DISMISS_USER_ACTION || |
| close_action_ == CLOSE_DISMISS_DEACTIVATION); |
| return; |
| } |
| |
| close_action_ = action; |
| |
| if (action == CLOSE_DISMISS_DEACTIVATION) { |
| return; // Do nothing if the bubble was dismissed due to focus loss. |
| } |
| |
| // We clear the profile set because the user chose to either remove, disable, |
| // or acknowledge the extension. If they acknowledged it, we won't show the |
| // bubble again, and in any other cases, we should re-show the bubble if |
| // any extension goes back to overriding the home page (because it's contrary |
| // to the user's choice). |
| GetShownProfileSet().clear(); |
| |
| switch (action) { |
| case CLOSE_EXECUTE: |
| // User clicked to disable the extension. |
| extensions::ExtensionRegistrar::Get(profile_)->DisableExtension( |
| extension_->id(), {extensions::disable_reason::DISABLE_USER_ACTION}); |
| break; |
| case CLOSE_LEARN_MORE: { |
| AcknowledgeExtension(*profile_, extension_->id()); |
| if (!g_should_ignore_learn_more_for_testing) { |
| GURL learn_more_url(chrome::kExtensionControlledSettingLearnMoreURL); |
| DCHECK(learn_more_url.is_valid()); |
| browser_->OpenURL( |
| content::OpenURLParams(learn_more_url, content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, false), |
| /*navigation_handle_callback=*/{}); |
| } |
| break; |
| } |
| case CLOSE_DISMISS_USER_ACTION: |
| AcknowledgeExtension(*profile_, extension_->id()); |
| break; |
| case CLOSE_DISMISS_DEACTIVATION: |
| NOTREACHED(); // This was handled above. |
| } |
| |
| // Warning: |this| may be deleted here! |
| } |
| |
| std::unique_ptr<ToolbarActionsBarBubbleDelegate::ExtraViewInfo> |
| ControlledHomeBubbleDelegate::GetExtraViewInfo() { |
| auto extra_view_info = std::make_unique<ExtraViewInfo>(); |
| |
| if (IsPolicyIndicationNeeded()) { |
| extra_view_info->resource = &vector_icons::kBusinessIcon; |
| extra_view_info->text = |
| l10n_util::GetStringUTF16(IDS_EXTENSIONS_INSTALLED_BY_ADMIN); |
| extra_view_info->is_learn_more = false; |
| } else { |
| extra_view_info->text = l10n_util::GetStringUTF16(IDS_LEARN_MORE); |
| extra_view_info->is_learn_more = true; |
| } |
| |
| return extra_view_info; |
| } |
| |
| bool ControlledHomeBubbleDelegate::IsPolicyIndicationNeeded() const { |
| return extensions::Manifest::IsPolicyLocation(extension_->location()); |
| } |