| // Copyright 2025 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/extensions_toolbar_view_model.h" |
| |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/extensions/extension_ui_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/tab_list/tab_list_interface.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/permissions/site_permissions_helper.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/gfx/vector_icon_types.h" |
| #include "url/origin.h" |
| |
| static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE)); |
| |
| using content::WebContentsObserver; |
| |
| ExtensionsToolbarViewModel::ExtensionsToolbarViewModel( |
| Delegate* delegate, |
| BrowserWindowInterface* browser, |
| ToolbarActionsModel* actions_model) |
| : browser_(browser), delegate_(delegate), actions_model_(actions_model) { |
| WebContentsObserver::Observe(GetCurrentWebContents()); |
| actions_model_observation_.Observe(actions_model_); |
| auto* tab_list = TabListInterface::From(browser_); |
| if (tab_list) { |
| tab_list_observation_.Observe(tab_list); |
| } |
| |
| permissions_manager_observation_.Observe( |
| extensions::PermissionsManager::Get(browser_->GetProfile())); |
| |
| if (actions_model_->actions_initialized()) { |
| OnToolbarModelInitialized(); |
| } |
| } |
| |
| ExtensionsToolbarViewModel::~ExtensionsToolbarViewModel() { |
| WebContentsObserver::Observe(nullptr); |
| } |
| |
| ExtensionsToolbarViewModel::RequestAccessButtonParams:: |
| RequestAccessButtonParams() = default; |
| ExtensionsToolbarViewModel::RequestAccessButtonParams:: |
| RequestAccessButtonParams(RequestAccessButtonParams&&) = default; |
| ExtensionsToolbarViewModel::RequestAccessButtonParams& |
| ExtensionsToolbarViewModel::RequestAccessButtonParams::operator=( |
| RequestAccessButtonParams&&) = default; |
| ExtensionsToolbarViewModel::RequestAccessButtonParams:: |
| ~RequestAccessButtonParams() = default; |
| |
| void ExtensionsToolbarViewModel::AddObserver(Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void ExtensionsToolbarViewModel::RemoveObserver(Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| ToolbarActionViewModel* ExtensionsToolbarViewModel::GetActionModelForId( |
| const ToolbarActionsModel::ActionId& action_id) const { |
| auto it = actions_.find(action_id); |
| if (it == actions_.end()) { |
| return nullptr; |
| } |
| return it->second.get(); |
| } |
| |
| bool ExtensionsToolbarViewModel::IsActionDraggable( |
| const ToolbarActionsModel::ActionId& action_id) const { |
| Profile* profile = browser_->GetProfile(); |
| |
| // We don't allow dragging if the container isn't in the toolbar, or if |
| // the profile is incognito (to avoid changing state from an incognito |
| // window). |
| if (!ToolbarActionsModel::CanShowActionsInToolbar(*browser_) || |
| profile->IsOffTheRecord()) { |
| return false; |
| } |
| |
| // Only pinned extensions should be draggable. |
| auto it = std::ranges::find(GetPinnedActionIds(), action_id); |
| if (it == GetPinnedActionIds().cend()) { |
| return false; |
| } |
| |
| // TODO(crbug.com/40808374): Force-pinned extensions are not draggable. |
| return !actions_model_->IsActionForcePinned(*it); |
| } |
| |
| void ExtensionsToolbarViewModel::MovePinnedAction( |
| const ToolbarActionsModel::ActionId& action_id, |
| size_t target_index) { |
| actions_model_->MovePinnedAction(action_id, target_index); |
| } |
| |
| void ExtensionsToolbarViewModel::MovePinnedActionBy( |
| const std::string& action_id, |
| int move_by) { |
| // Find the action's current index and verify that it's currently pinned. |
| auto iter = std::ranges::find(actions_model_->pinned_action_ids(), action_id); |
| CHECK(iter != actions_model_->pinned_action_ids().cend()); |
| |
| // Calculate the target index, clamping it between 0 and `size - 1` to prevent |
| // out-of-bounds errors. |
| int current_index = iter - actions_model_->pinned_action_ids().cbegin(); |
| int new_index = std::clamp( |
| current_index + move_by, 0, |
| static_cast<int>(actions_model_->pinned_action_ids().size()) - 1); |
| if (new_index == current_index) { |
| return; |
| } |
| MovePinnedAction(action_id, new_index); |
| } |
| |
| const base::flat_set<ToolbarActionsModel::ActionId>& |
| ExtensionsToolbarViewModel::GetAllActionIds() const { |
| return actions_model_->action_ids(); |
| } |
| |
| const std::vector<ToolbarActionsModel::ActionId>& |
| ExtensionsToolbarViewModel::GetPinnedActionIds() const { |
| return actions_model_->pinned_action_ids(); |
| } |
| |
| bool ExtensionsToolbarViewModel::AreActionsInitialized() { |
| return actions_model_->actions_initialized(); |
| } |
| |
| // static |
| const gfx::VectorIcon& ExtensionsToolbarViewModel::GetToolbarButtonIcon( |
| ExtensionsToolbarButtonState state) { |
| switch (state) { |
| case ExtensionsToolbarButtonState::kDefault: |
| return vector_icons::kExtensionChromeRefreshIcon; |
| case ExtensionsToolbarButtonState::kAllExtensionsBlocked: |
| return vector_icons::kExtensionOffIcon; |
| case ExtensionsToolbarButtonState::kAnyExtensionHasAccess: |
| return vector_icons::kExtensionOnIcon; |
| } |
| } |
| |
| // static |
| std::u16string ExtensionsToolbarViewModel::GetToolbarButtonAccessibleText( |
| ExtensionsToolbarButtonState state) { |
| int message_id; |
| switch (state) { |
| case ExtensionsToolbarButtonState::kDefault: |
| message_id = IDS_ACC_NAME_EXTENSIONS_BUTTON; |
| break; |
| case ExtensionsToolbarButtonState::kAllExtensionsBlocked: |
| message_id = IDS_ACC_NAME_EXTENSIONS_BUTTON_ALL_EXTENSIONS_BLOCKED; |
| break; |
| case ExtensionsToolbarButtonState::kAnyExtensionHasAccess: |
| message_id = IDS_ACC_NAME_EXTENSIONS_BUTTON_ANY_EXTENSION_HAS_ACCESS; |
| break; |
| } |
| return l10n_util::GetStringUTF16(message_id); |
| } |
| |
| // static |
| std::u16string ExtensionsToolbarViewModel::GetToolbarButtonTooltipText( |
| ExtensionsToolbarButtonState state) { |
| int message_id; |
| switch (state) { |
| case ExtensionsToolbarButtonState::kDefault: |
| message_id = IDS_TOOLTIP_EXTENSIONS_BUTTON; |
| break; |
| case ExtensionsToolbarButtonState::kAllExtensionsBlocked: |
| message_id = IDS_TOOLTIP_EXTENSIONS_BUTTON_ALL_EXTENSIONS_BLOCKED; |
| break; |
| case ExtensionsToolbarButtonState::kAnyExtensionHasAccess: |
| message_id = IDS_TOOLTIP_EXTENSIONS_BUTTON_ANY_EXTENSION_HAS_ACCESS; |
| break; |
| } |
| return l10n_util::GetStringUTF16(message_id); |
| } |
| |
| ExtensionsToolbarViewModel::ExtensionsToolbarButtonState |
| ExtensionsToolbarViewModel::GetButtonState( |
| content::WebContents& web_contents) const { |
| Profile* profile = browser_->GetProfile(); |
| const GURL& url = web_contents.GetLastCommittedURL(); |
| |
| if (actions_model_->IsRestrictedUrl(url)) { |
| return ExtensionsToolbarButtonState::kAllExtensionsBlocked; |
| } |
| |
| extensions::PermissionsManager* manager = |
| extensions::PermissionsManager::Get(profile); |
| extensions::PermissionsManager::UserSiteSetting site_setting = |
| manager->GetUserSiteSetting(url::Origin::Create(url)); |
| |
| if (site_setting == |
| extensions::PermissionsManager::UserSiteSetting::kBlockAllExtensions) { |
| return ExtensionsToolbarButtonState::kAllExtensionsBlocked; |
| } |
| |
| if (AnyActionHasCurrentSiteAccess(web_contents)) { |
| return ExtensionsToolbarButtonState::kAnyExtensionHasAccess; |
| } |
| |
| return ExtensionsToolbarButtonState::kDefault; |
| } |
| |
| void ExtensionsToolbarViewModel::ExecuteUserAction( |
| const ToolbarActionsModel::ActionId& action_id, |
| ToolbarActionViewModel::InvocationSource source) { |
| GetActionModelForId(action_id)->ExecuteUserAction(source); |
| } |
| |
| void ExtensionsToolbarViewModel::GrantSiteAccess( |
| content::WebContents* web_contents, |
| const std::vector<extensions::ExtensionId>& extension_ids) { |
| Profile* profile = browser_->GetProfile(); |
| auto* registry = extensions::ExtensionRegistry::Get(profile); |
| std::vector<const extensions::Extension*> extensions_to_run; |
| for (const auto& id : extension_ids) { |
| const extensions::Extension* extension = |
| registry->enabled_extensions().GetByID(id); |
| if (extension) { |
| extensions_to_run.push_back(extension); |
| } |
| } |
| |
| extensions::SitePermissionsHelper(profile).UpdateSiteAccess( |
| extensions_to_run, web_contents, |
| extensions::PermissionsManager::UserSiteAccess::kOnSite); |
| } |
| |
| // Extensions are included in the request access button only when: |
| // - site allows customizing site access by extension |
| // - extension added a request that has not been dismissed |
| // - requests can be shown in the toolbar |
| ExtensionsToolbarViewModel::RequestAccessButtonParams |
| ExtensionsToolbarViewModel::GetRequestAccessButtonParams( |
| content::WebContents* web_contents) const { |
| RequestAccessButtonParams params; |
| if (!web_contents) { |
| return params; |
| } |
| |
| Profile* profile = browser_->GetProfile(); |
| extensions::PermissionsManager* permissions_manager = |
| extensions::PermissionsManager::Get(profile); |
| auto origin = web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin(); |
| extensions::PermissionsManager::UserSiteSetting site_setting = |
| permissions_manager->GetUserSiteSetting(origin); |
| |
| if (site_setting != |
| extensions::PermissionsManager::UserSiteSetting::kCustomizeByExtension) { |
| return params; |
| } |
| |
| int tab_id = extensions::ExtensionTabUtil::GetTabId(web_contents); |
| extensions::SitePermissionsHelper site_permissions_helper(profile); |
| |
| std::vector<std::u16string> extension_names; |
| for (const auto& action_id : actions_model_->action_ids()) { |
| bool has_active_request = |
| permissions_manager->HasActiveHostAccessRequest(tab_id, action_id); |
| bool can_show_access_requests_in_toolbar = |
| site_permissions_helper.ShowAccessRequestsInToolbar(action_id); |
| |
| if (has_active_request && can_show_access_requests_in_toolbar) { |
| params.extension_ids.push_back(action_id); |
| ToolbarActionViewModel* action_model = GetActionModelForId(action_id); |
| // If an extension has an active request, it should have an action model. |
| CHECK(action_model); |
| extension_names.push_back(action_model->GetActionName()); |
| } |
| } |
| |
| if (params.extension_ids.empty()) { |
| return params; |
| } |
| |
| std::vector<std::u16string> tooltip_parts; |
| tooltip_parts.push_back(l10n_util::GetStringFUTF16( |
| IDS_EXTENSIONS_REQUEST_ACCESS_BUTTON_TOOLTIP_MULTIPLE_EXTENSIONS, |
| extensions::ui_util::GetFormattedHostForDisplay(*web_contents))); |
| tooltip_parts.insert(tooltip_parts.end(), extension_names.begin(), |
| extension_names.end()); |
| params.tooltip_text = base::JoinString(tooltip_parts, u"\n"); |
| |
| return params; |
| } |
| |
| ToolbarActionViewModel* ExtensionsToolbarViewModel::GetActionForId( |
| const std::string& action_id) { |
| return GetActionModelForId(action_id); |
| } |
| |
| void ExtensionsToolbarViewModel::HideActivePopup() { |
| delegate_->HideActivePopup(); |
| } |
| |
| void ExtensionsToolbarViewModel::CloseExtensionsMenuIfOpen() { |
| delegate_->CloseExtensionsMenuIfOpen(); |
| } |
| |
| bool ExtensionsToolbarViewModel::ShowToolbarActionPopupForAPICall( |
| const std::string& action_id, |
| ShowPopupCallback callback) { |
| if (!delegate_->CanShowToolbarActionPopupForAPICall(action_id)) { |
| return false; |
| } |
| |
| ToolbarActionViewModel* action = GetActionModelForId(action_id); |
| DCHECK(action); |
| action->TriggerPopupForAPI(std::move(callback)); |
| return true; |
| } |
| |
| void ExtensionsToolbarViewModel::ToggleExtensionsMenu() { |
| delegate_->ToggleExtensionsMenu(); |
| } |
| |
| void ExtensionsToolbarViewModel::ShowManageExtensionsIPH() { |
| delegate_->ShowManageExtensionsIPH(); |
| } |
| |
| bool ExtensionsToolbarViewModel::HasAnyExtensions() const { |
| return !GetAllActionIds().empty(); |
| } |
| |
| void ExtensionsToolbarViewModel::OnToolbarModelInitialized() { |
| CHECK(actions_.empty()); |
| CHECK(actions_model_->actions_initialized()); |
| |
| // Create a vector first to initialize flat_map more efficiently. |
| std::vector<std::pair<ToolbarActionsModel::ActionId, |
| std::unique_ptr<ToolbarActionViewModel>>> |
| initial_actions; |
| initial_actions.reserve(actions_model_->action_ids().size()); |
| for (const auto& action_id : actions_model_->action_ids()) { |
| initial_actions.emplace_back( |
| action_id, delegate_->CreateActionViewModel(action_id, this)); |
| } |
| actions_ = base::flat_map<ToolbarActionsModel::ActionId, |
| std::unique_ptr<ToolbarActionViewModel>>( |
| std::move(initial_actions)); |
| |
| for (Observer& obs : observers_) { |
| obs.OnActionsInitialized(); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnToolbarActionAdded( |
| const ToolbarActionsModel::ActionId& action_id) { |
| AppendActionModel(action_id); |
| |
| for (Observer& obs : observers_) { |
| obs.OnActionAdded(action_id); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnToolbarActionRemoved( |
| const ToolbarActionsModel::ActionId& action_id) { |
| auto iter = actions_.find(action_id); |
| CHECK(iter != actions_.end()); |
| |
| // Transfer ownership to a local variable to ensure the model remains alive |
| // during the subsequent UI cleanup notifications. |
| std::unique_ptr<ToolbarActionViewModel> model = std::move(iter->second); |
| |
| actions_.erase(iter); |
| |
| for (Observer& obs : observers_) { |
| obs.OnActionRemoved(action_id); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnToolbarActionUpdated( |
| const ToolbarActionsModel::ActionId& action_id) { |
| for (Observer& obs : observers_) { |
| obs.OnActionUpdated(action_id); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnToolbarPinnedActionsChanged() { |
| for (Observer& obs : observers_) { |
| obs.OnPinnedActionsChanged(); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::DidFinishNavigation( |
| content::NavigationHandle* handle) { |
| if (!handle->IsInPrimaryMainFrame() || !handle->HasCommitted()) { |
| return; |
| } |
| for (Observer& obs : observers_) { |
| obs.OnActiveWebContentsChanged(handle->IsSameDocument(), |
| handle->GetWebContents()); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnActiveTabChanged(TabListInterface& tab_list, |
| tabs::TabInterface* tab) { |
| content::WebContents* contents = tab->GetContents(); |
| WebContentsObserver::Observe(contents); |
| for (Observer& obs : observers_) { |
| obs.OnActiveWebContentsChanged(/*is_same_document=*/false, contents); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnTabListDestroyed( |
| TabListInterface& tab_list) { |
| tab_list_observation_.Reset(); |
| } |
| |
| bool ExtensionsToolbarViewModel::AnyActionHasCurrentSiteAccess( |
| content::WebContents& web_contents) const { |
| for (const auto& [action_id, model] : actions_) { |
| if (model->GetSiteInteraction(&web_contents) == |
| extensions::SitePermissionsHelper::SiteInteraction::kGranted) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void ExtensionsToolbarViewModel::AppendActionModel( |
| const ToolbarActionsModel::ActionId& action_id) { |
| actions_.emplace(action_id, |
| delegate_->CreateActionViewModel(action_id, this)); |
| } |
| |
| content::WebContents* ExtensionsToolbarViewModel::GetCurrentWebContents() |
| const { |
| tabs::TabInterface* tab = TabListInterface::From(browser_)->GetActiveTab(); |
| if (!tab) { |
| return nullptr; |
| } |
| return tab->GetContents(); |
| } |
| |
| void ExtensionsToolbarViewModel::OnHostAccessRequestAdded( |
| const extensions::ExtensionId& extension_id, |
| int tab_id) { |
| content::WebContents* web_contents = GetCurrentWebContents(); |
| int current_tab_id = extensions::ExtensionTabUtil::GetTabId(web_contents); |
| if (tab_id != current_tab_id) { |
| return; |
| } |
| for (Observer& obs : observers_) { |
| obs.OnRequestAccessButtonParamsChanged(web_contents); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnHostAccessRequestUpdated( |
| const extensions::ExtensionId& extension_id, |
| int tab_id) { |
| content::WebContents* web_contents = GetCurrentWebContents(); |
| int current_tab_id = extensions::ExtensionTabUtil::GetTabId(web_contents); |
| if (tab_id != current_tab_id) { |
| return; |
| } |
| for (Observer& obs : observers_) { |
| obs.OnRequestAccessButtonParamsChanged(web_contents); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnHostAccessRequestRemoved( |
| const extensions::ExtensionId& extension_id, |
| int tab_id) { |
| content::WebContents* web_contents = GetCurrentWebContents(); |
| int current_tab_id = extensions::ExtensionTabUtil::GetTabId(web_contents); |
| if (tab_id != current_tab_id) { |
| return; |
| } |
| for (Observer& obs : observers_) { |
| obs.OnRequestAccessButtonParamsChanged(web_contents); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnHostAccessRequestsCleared(int tab_id) { |
| content::WebContents* web_contents = GetCurrentWebContents(); |
| int current_tab_id = extensions::ExtensionTabUtil::GetTabId(web_contents); |
| if (tab_id != current_tab_id) { |
| return; |
| } |
| for (Observer& obs : observers_) { |
| obs.OnRequestAccessButtonParamsChanged(web_contents); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnHostAccessRequestDismissedByUser( |
| const extensions::ExtensionId& extension_id, |
| const url::Origin& origin) { |
| content::WebContents* web_contents = GetCurrentWebContents(); |
| for (Observer& obs : observers_) { |
| obs.OnRequestAccessButtonParamsChanged(web_contents); |
| } |
| } |
| |
| void ExtensionsToolbarViewModel::OnUserPermissionsSettingsChanged( |
| const extensions::PermissionsManager::UserPermissionsSettings& settings) { |
| for (Observer& obs : observers_) { |
| obs.OnToolbarControlStateUpdated(); |
| } |
| // TODO(crbug.com/40857356): Update request access button hover card. This |
| // will be slightly different than 'OnToolbarActionUpdated' since site |
| // settings update are not tied to a specific action. |
| } |
| |
| void ExtensionsToolbarViewModel::OnShowAccessRequestsInToolbarChanged( |
| const extensions::ExtensionId& extension_id, |
| bool can_show_requests) { |
| content::WebContents* web_contents = GetCurrentWebContents(); |
| for (Observer& obs : observers_) { |
| obs.OnRequestAccessButtonParamsChanged(web_contents); |
| } |
| } |