blob: 0582cf9b5141ac4b40e4dabe2ad580dc56f1a37e [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/extensions/api/side_panel/side_panel_service.h"
#include <cstddef>
#include <memory>
#include <optional>
#include "base/no_destructor.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/extensions/extension_side_panel_utils.h"
#include "chrome/common/extensions/api/side_panel.h"
#include "chrome/common/extensions/api/side_panel/side_panel_info.h"
#include "chrome/common/pref_names.h"
#include "components/sessions/core/session_id.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/pref_types.h"
#include "extensions/common/error_utils.h"
namespace extensions {
namespace {
// Key corresponding to whether the extension's side panel entry (if one exists)
// should be opened when its icon is clicked in the toolbar.
constexpr PrefMap kOpenSidePanelOnIconClickPref = {
"open_side_panel_on_icon_click", PrefType::kBool,
PrefScope::kExtensionSpecific};
api::side_panel::PanelOptions GetPanelOptionsFromManifest(
const Extension& extension) {
std::string path = SidePanelInfo::GetDefaultPath(&extension);
api::side_panel::PanelOptions options;
if (!path.empty()) {
options.path = std::move(path);
options.enabled = true;
}
return options;
}
} // namespace
SidePanelService::~SidePanelService() = default;
SidePanelService::SidePanelService(content::BrowserContext* context)
: browser_context_(context) {
extensions::ExtensionRegistry* extension_registry =
extensions::ExtensionRegistry::Get(context);
extension_registry_observation_.Observe(extension_registry);
}
bool SidePanelService::HasSidePanelActionForTab(const Extension& extension,
TabId tab_id) {
if (!OpenSidePanelOnIconClick(extension.id())) {
return false;
}
return HasSidePanelAvailableForTab(extension, tab_id);
}
bool SidePanelService::HasSidePanelContextMenuActionForTab(
const Extension& extension,
TabId tab_id) {
return HasSidePanelAvailableForTab(extension, tab_id);
}
bool SidePanelService::HasSidePanelAvailableForTab(const Extension& extension,
TabId tab_id) {
api::side_panel::PanelOptions options = GetOptions(extension, tab_id);
return options.enabled.has_value() && *options.enabled &&
options.path.has_value();
}
api::side_panel::PanelOptions SidePanelService::GetOptions(
const Extension& extension,
std::optional<TabId> id) {
auto extension_panel_options = panels_.find(extension.id());
// Get default path from manifest if nothing was stored in this service for
// the calling extension.
if (extension_panel_options == panels_.end()) {
return GetPanelOptionsFromManifest(extension);
}
TabId default_tab_id = SessionID::InvalidValue().id();
TabId tab_id = id.has_value() ? id.value() : default_tab_id;
TabPanelOptions& tab_panel_options = extension_panel_options->second;
// The specific `tab_id` may have already been saved.
if (tab_id != default_tab_id) {
auto specific_tab_options = tab_panel_options.find(tab_id);
if (specific_tab_options != tab_panel_options.end())
return specific_tab_options->second.Clone();
}
// Fall back to the default tab if no tab ID was specified or entries for the
// specific tab weren't found.
auto default_options = tab_panel_options.find(default_tab_id);
if (default_options != tab_panel_options.end()) {
return default_options->second.Clone();
}
// Fall back to the manifest-specified options as a last resort.
return GetPanelOptionsFromManifest(extension);
}
api::side_panel::PanelOptions SidePanelService::GetSpecificOptionsForTab(
const Extension& extension,
TabId tab_id) {
auto extension_panel_options = panels_.find(extension.id());
if (extension_panel_options == panels_.end()) {
return api::side_panel::PanelOptions();
}
TabPanelOptions& tab_panel_options = extension_panel_options->second;
auto specific_tab_options = tab_panel_options.find(tab_id);
return specific_tab_options == tab_panel_options.end()
? api::side_panel::PanelOptions()
: specific_tab_options->second.Clone();
}
// Upsert to merge `panels_[extension_id][tab_id]` with `set_options`.
void SidePanelService::SetOptions(const Extension& extension,
api::side_panel::PanelOptions options) {
auto update_existing_options =
[&options](api::side_panel::PanelOptions& existing_options) {
if (options.path) {
existing_options.path = std::move(options.path);
}
if (options.enabled) {
existing_options.enabled = std::move(options.enabled);
}
};
TabId tab_id = SessionID::InvalidValue().id();
if (options.tab_id)
tab_id = *options.tab_id;
TabPanelOptions& extension_panel_options = panels_[extension.id()];
auto it = extension_panel_options.find(tab_id);
// Create the options if they don't exist, otherwise update them.
if (it != extension_panel_options.end()) {
update_existing_options(it->second);
} else {
// The default value for the optional enabled option is true. This default
// is applied when the supplied option is being inserted for the first time.
if (!options.enabled.has_value()) {
options.enabled = true;
}
// If there is no entry for the default tab, merge `options` into the
// manifest-specified options.
if (tab_id == SessionID::InvalidValue().id()) {
extension_panel_options[tab_id] = GetPanelOptionsFromManifest(extension);
update_existing_options(extension_panel_options[tab_id]);
} else {
// Update an existing option.
extension_panel_options[tab_id] = std::move(options);
}
}
for (auto& observer : observers_) {
observer.OnPanelOptionsChanged(extension.id(),
extension_panel_options[tab_id]);
}
}
bool SidePanelService::HasExtensionPanelOptionsForTest(const ExtensionId& id) {
return panels_.count(id) != 0;
}
// static
BrowserContextKeyedAPIFactory<SidePanelService>*
SidePanelService::GetFactoryInstance() {
static base::NoDestructor<BrowserContextKeyedAPIFactory<SidePanelService>>
instance;
return instance.get();
}
// static
SidePanelService* SidePanelService::Get(content::BrowserContext* context) {
return BrowserContextKeyedAPIFactory<SidePanelService>::Get(context);
}
void SidePanelService::RemoveExtensionOptions(const ExtensionId& id) {
panels_.erase(id);
}
bool SidePanelService::OpenSidePanelOnIconClick(
const ExtensionId& extension_id) {
bool open_side_panel_on_icon_click = false;
// TODO(tjudkins): This should be taking in a browser context to read the pref
// on, rather than using the one the service was created with.
ExtensionPrefs::Get(browser_context_)
->ReadPrefAsBoolean(extension_id, kOpenSidePanelOnIconClickPref,
&open_side_panel_on_icon_click);
return open_side_panel_on_icon_click;
}
void SidePanelService::SetOpenSidePanelOnIconClick(
const ExtensionId& extension_id,
bool open_side_panel_on_icon_click) {
// TODO(tjudkins): This should be taking in a browser context to set the pref
// on, rather than using the one the service was created with.
ExtensionPrefs::Get(browser_context_)
->SetBooleanPref(extension_id, kOpenSidePanelOnIconClickPref,
open_side_panel_on_icon_click);
}
base::expected<bool, std::string> SidePanelService::OpenSidePanelForWindow(
const Extension& extension,
content::BrowserContext* context,
int window_id,
bool include_incognito_information) {
std::string error;
WindowController* window_controller =
ExtensionTabUtil::GetControllerInProfileWithId(
Profile::FromBrowserContext(context), window_id,
include_incognito_information, &error);
if (!window_controller) {
return base::unexpected(error);
}
auto global_options = GetOptions(extension, std::nullopt);
if (!global_options.path || !global_options.enabled.has_value() ||
!(*global_options.enabled)) {
return base::unexpected(
base::StringPrintf("No active side panel for windowId: %d", window_id));
}
BrowserWindowInterface* browser_window =
window_controller->GetBrowserWindowInterface();
if (!browser_window) {
return base::unexpected(
base::StringPrintf("No browser window for windowId: %d", window_id));
}
side_panel_util::OpenGlobalExtensionSidePanel(
*browser_window, /*web_contents=*/nullptr, extension.id());
return true;
}
base::expected<bool, std::string> SidePanelService::OpenSidePanelForTab(
const Extension& extension,
content::BrowserContext* context,
int tab_id,
std::optional<int> window_id,
bool include_incognito_information) {
// First, find the corresponding tab.
WindowController* window = nullptr;
content::WebContents* web_contents = nullptr;
if (!ExtensionTabUtil::GetTabById(tab_id, context,
include_incognito_information, &window,
&web_contents, nullptr) ||
!window) {
return base::unexpected(ErrorUtils::FormatErrorMessage(
ExtensionTabUtil::kTabNotFoundError, base::ToString(tab_id)));
}
BrowserWindowInterface* browser_window = window->GetBrowserWindowInterface();
if (!browser_window) {
return base::unexpected(
base::StringPrintf("No browser window for tabId: %d", tab_id));
}
// If both `tab_id` and `window_id` were provided, ensure the tab is in
// the specified window. The window can also be null for prerender tabs
// which can't have a side panel.
if (window_id && window_id != window->GetWindowId()) {
return base::unexpected(
"The specified tab does not belong to the specified window.");
}
// Verify that an active side panel (contextual or global) exists for the tab.
api::side_panel::PanelOptions panel_options = GetOptions(extension, tab_id);
if (!panel_options.path || !panel_options.enabled.has_value() ||
!(*panel_options.enabled)) {
return base::unexpected(
base::StringPrintf("No active side panel for tabId: %d", tab_id));
}
// If we do have an active panel, check if it's a contextual panel.
bool has_contextual_panel = false;
auto panels_iter = panels_.find(extension.id());
if (panels_iter != panels_.end()) {
auto tab_panels_iter = panels_iter->second.find(tab_id);
if (tab_panels_iter != panels_iter->second.end()) {
auto& options = tab_panels_iter->second;
CHECK(options.path);
CHECK(options.enabled.has_value());
CHECK(options.enabled.value());
has_contextual_panel = true;
}
}
// Open the appropriate panel.
if (has_contextual_panel) {
side_panel_util::OpenContextualExtensionSidePanel(
*browser_window, *web_contents, extension.id());
} else {
side_panel_util::OpenGlobalExtensionSidePanel(*browser_window, web_contents,
extension.id());
}
return true;
}
void SidePanelService::DispatchOnClosedEvent(const ExtensionId& extension_id,
int window_id,
std::optional<int> tab_id,
const std::string& path) {
auto* router = EventRouter::Get(browser_context_);
if (!router->ExtensionHasEventListener(
extension_id, api::side_panel::OnClosed::kEventName)) {
return;
}
base::Value::List args;
api::side_panel::PanelClosedInfo info;
info.window_id = window_id;
info.tab_id = std::move(tab_id);
info.path = path;
args.Append(info.ToValue());
auto event = std::make_unique<Event>(events::SIDE_PANEL_ON_CLOSED,
api::side_panel::OnClosed::kEventName,
std::move(args));
router->DispatchEventToExtension(extension_id, std::move(event));
}
api::side_panel::PanelLayout SidePanelService::GetSidePanelLayout() {
Profile* profile = Profile::FromBrowserContext(browser_context_);
api::side_panel::PanelLayout layout;
layout.side =
profile->GetPrefs()->GetBoolean(prefs::kSidePanelHorizontalAlignment)
? api::side_panel::Side::kRight
: api::side_panel::Side::kLeft;
return layout;
}
base::expected<bool, std::string> SidePanelService::CloseSidePanelForTab(
const Extension& extension,
content::BrowserContext* context,
int tab_id,
std::optional<int> window_id,
bool include_incognito_information) {
WindowController* window = nullptr;
content::WebContents* web_contents = nullptr;
if (!ExtensionTabUtil::GetTabById(tab_id, context,
include_incognito_information, &window,
&web_contents, /*tab_index=*/nullptr) ||
!web_contents) {
return base::unexpected(ErrorUtils::FormatErrorMessage(
ExtensionTabUtil::kTabNotFoundError, base::ToString(tab_id)));
}
// Retrieve the corresponding browser window, since the active side panel for
// the tab might be a global one.
BrowserWindowInterface* browser_window = window->GetBrowserWindowInterface();
if (!browser_window) {
return base::unexpected(
base::StringPrintf("No browser window for tabId: %d", tab_id));
}
// Check that the given `tab_id` belongs to the given `window_id`.
if (window_id.has_value() && *window_id != window->GetWindowId()) {
return base::unexpected(
"The specified tab does not belong to the specified window.");
}
// Verify that an active side panel (contextual or global) exists for the tab.
api::side_panel::PanelOptions panel_options = GetOptions(extension, tab_id);
if (!panel_options.path || !panel_options.enabled.value_or(false)) {
return base::unexpected(
base::StringPrintf("No active side panel for tabId: %d", tab_id));
}
side_panel_util::CloseContextualExtensionSidePanel(
browser_window, web_contents, extension.id(), window_id);
return true;
}
base::expected<bool, std::string> SidePanelService::CloseSidePanelForWindow(
const Extension& extension,
content::BrowserContext* context,
int window_id,
bool include_incognito_information) {
std::string error;
WindowController* window_controller =
ExtensionTabUtil::GetControllerInProfileWithId(
Profile::FromBrowserContext(context), window_id,
include_incognito_information, &error);
if (!window_controller) {
return base::unexpected(error);
}
// Verify that an active global side panel exists for the window.
auto global_options = GetOptions(extension, /*tab_id=*/std::nullopt);
if (!global_options.path || !global_options.enabled.value_or(false)) {
return base::unexpected(
base::StringPrintf("No active side panel for windowId: %d", window_id));
}
BrowserWindowInterface* browser_window =
window_controller->GetBrowserWindowInterface();
if (!browser_window) {
return base::unexpected(
base::StringPrintf("No browser window for windowId: %d", window_id));
}
side_panel_util::CloseGlobalExtensionSidePanel(browser_window,
extension.id());
return true;
}
void SidePanelService::DispatchOnOpenedEvent(const ExtensionId& extension_id,
int window_id,
std::optional<int> tab_id,
const std::string& path) {
auto* router = EventRouter::Get(browser_context_);
if (!router->ExtensionHasEventListener(
extension_id, api::side_panel::OnOpened::kEventName)) {
return;
}
api::side_panel::PanelOpenedInfo info;
info.window_id = window_id;
info.tab_id = std::move(tab_id);
info.path = path;
base::Value::List args;
args.Append(info.ToValue());
auto event = std::make_unique<Event>(events::SIDE_PANEL_ON_OPENED,
api::side_panel::OnOpened::kEventName,
std::move(args));
router->DispatchEventToExtension(extension_id, std::move(event));
}
void SidePanelService::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void SidePanelService::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void SidePanelService::OnExtensionUnloaded(
content::BrowserContext* browser_context,
const Extension* extension,
UnloadedExtensionReason reason) {
RemoveExtensionOptions(extension->id());
}
void SidePanelService::OnExtensionUninstalled(
content::BrowserContext* browser_context,
const Extension* extension,
UninstallReason reason) {
RemoveExtensionOptions(extension->id());
}
void SidePanelService::Shutdown() {
for (auto& observer : observers_) {
observer.OnSidePanelServiceShutdown();
}
}
} // namespace extensions