blob: 08e658d64cdd082b73c069591c2fc9a4621f4042 [file]
// Copyright 2012 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/extension_tab_util.h"
#include <stddef.h>
#include <algorithm>
#include <cmath>
#include <memory>
#include <optional>
#include <utility>
#include "base/auto_reset.h"
#include "base/containers/fixed_flat_set.h"
#include "base/hash/hash.h"
#include "base/metrics/histogram_functions.h"
#include "base/notimplemented.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/browser_extension_window_controller.h"
#include "chrome/browser/extensions/browser_window_util.h"
#include "chrome/browser/extensions/chrome_extension_function_details.h"
#include "chrome/browser/extensions/extension_management.h"
#include "chrome/browser/extensions/window_controller_list.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/tab_contents/tab_contents_iterator.h"
#include "chrome/browser/ui/tabs/tab_muted_utils.h"
#include "chrome/common/webui_url_constants.h"
#include "components/data_sharing/public/features.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "components/sessions/content/session_tab_helper.h"
#include "components/split_tabs/split_tab_id.h"
#include "components/split_tabs/split_tab_visual_data.h"
#include "components/tab_groups/tab_group_id.h" // nogncheck
#include "components/tab_groups/tab_group_visual_data.h"
#include "components/tabs/public/tab_group.h"
#include "components/tabs/public/tab_interface.h"
#include "components/url_formatter/url_fixer.h"
#include "content/public/browser/favicon_status.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_function.h"
#include "extensions/browser/extension_util.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/constants.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/options_page_info.h"
#include "extensions/common/mojom/api_permission_id.mojom-shared.h"
#include "extensions/common/mojom/context_type.mojom.h"
#include "extensions/common/permissions/permissions_data.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/url_constants.h"
#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/android/tab_model/tab_model.h"
#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
#else
#include "chrome/browser/resource_coordinator/tab_lifecycle_unit_external.h"
#include "chrome/browser/ui/browser.h" // nogncheck
#include "chrome/browser/ui/browser_finder.h" // nogncheck
#include "chrome/browser/ui/browser_navigator_params.h" // nogncheck
#include "chrome/browser/ui/recently_audible_helper.h" // nogncheck
#include "chrome/browser/ui/tabs/tab_enums.h" // nogncheck
#include "chrome/browser/ui/tabs/tab_group_model.h" // nogncheck
#include "chrome/browser/ui/tabs/tab_strip_model.h" // nogncheck
#include "chrome/browser/ui/tabs/tab_utils.h" // nogncheck
#include "chrome/common/extensions/api/tabs.h"
#include "chrome/common/url_constants.h"
#include "content/public/browser/back_forward_cache.h"
#include "extensions/common/manifest_handlers/incognito_info.h"
#endif
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
using content::NavigationEntry;
using content::WebContents;
using extensions::mojom::APIPermissionID;
namespace extensions {
namespace {
// Whether to disable tab list editing for testing purposes.
bool g_disable_tab_list_editing_for_testing = false;
constexpr char kGroupNotFoundError[] = "No group with id: *.";
constexpr char kInvalidUrlError[] = "Invalid url: \"*\".";
// This enum is used for counting schemes used via a navigation triggered by
// extensions.
enum class NavigationScheme {
// http: or https: scheme.
kHttpOrHttps = 0,
// chrome: scheme.
kChrome = 1,
// file: scheme where extension has access to local files.
kFileWithPermission = 2,
// file: scheme where extension does NOT have access to local files.
kFileWithoutPermission = 3,
// Everything else.
kOther = 4,
kMaxValue = kOther,
};
// Guaranteed non-null for any initialized browser window when the extensions
// system is still attached to the Browser (callers shouldn't need to null
// check).
WindowController* WindowControllerFromBrowser(BrowserWindowInterface* browser) {
return BrowserExtensionWindowController::From(browser);
}
// Use this function for reporting a tab id to an extension. It will
// take care of setting the id to TAB_ID_NONE if necessary (for
// example with devtools).
int GetTabIdForExtensions(WebContents& web_contents) {
BrowserWindowInterface* browser =
browser_window_util::GetBrowserForTabContents(web_contents);
if (browser && !ExtensionTabUtil::BrowserSupportsTabs(browser)) {
return -1;
}
return sessions::SessionTabHelper::IdForTab(&web_contents).id();
}
bool IsFileUrl(const GURL& url) {
return url.SchemeIsFile() || (url.SchemeIs(content::kViewSourceScheme) &&
GURL(url.GetContentPiece()).SchemeIsFile());
}
ExtensionTabUtil::ScrubTabBehaviorType GetScrubTabBehaviorImpl(
const Extension* extension,
mojom::ContextType context,
const GURL& url,
int tab_id) {
if (context == mojom::ContextType::kWebUi) {
return ExtensionTabUtil::kDontScrubTab;
}
if (context == mojom::ContextType::kUntrustedWebUi) {
return ExtensionTabUtil::kScrubTabFully;
}
bool has_permission = false;
if (extension) {
bool api_permission = false;
if (tab_id == api::tabs::TAB_ID_NONE) {
api_permission = extension->permissions_data()->HasAPIPermission(
APIPermissionID::kTab);
} else {
api_permission = extension->permissions_data()->HasAPIPermissionForTab(
tab_id, APIPermissionID::kTab);
}
bool host_permission = extension->permissions_data()
->active_permissions()
.HasExplicitAccessToOrigin(url);
has_permission = api_permission || host_permission;
}
if (!has_permission) {
return ExtensionTabUtil::kScrubTabFully;
}
return ExtensionTabUtil::kDontScrubTab;
}
bool HasValidMainFrameProcess(content::WebContents* contents) {
content::RenderFrameHost* main_frame_host = contents->GetPrimaryMainFrame();
content::RenderProcessHost* process_host = main_frame_host->GetProcess();
return process_host->IsReady() && process_host->IsInitializedAndNotDead();
}
void RecordNavigationScheme(const GURL& url,
const Extension& extension,
content::BrowserContext* browser_context) {
NavigationScheme scheme = NavigationScheme::kOther;
if (url.SchemeIsHTTPOrHTTPS()) {
scheme = NavigationScheme::kHttpOrHttps;
} else if (url.SchemeIs(content::kChromeUIScheme)) {
scheme = NavigationScheme::kChrome;
} else if (url.SchemeIsFile()) {
scheme = (util::AllowFileAccess(extension.id(), browser_context))
? NavigationScheme::kFileWithPermission
: NavigationScheme::kFileWithoutPermission;
}
base::UmaHistogramEnumeration("Extensions.Navigation.Scheme", scheme);
}
bool ShouldOpenInTab(const Extension* extension) {
// We always open the options page in new tab on android. Embedding the page on
// chrome://extensions is done with guest_view, but it's not enabled on android.
#if BUILDFLAG(IS_ANDROID)
return true;
#else
return OptionsPageInfo::ShouldOpenInTab(extension);
#endif
}
// Returns the URL to the extension's options page, if any.
std::optional<GURL> GetOptionsPageUrlToNavigate(const Extension* extension) {
if (!OptionsPageInfo::HasOptionsPage(extension)) {
return std::nullopt;
}
if (ShouldOpenInTab(extension)) {
// Options page tab is simply e.g. chrome-extension://.../options.html.
return OptionsPageInfo::GetOptionsPage(extension);
} else {
// Options page tab is Extension settings pointed at that Extension's ID,
// e.g. chrome://extensions?options=...
GURL::Replacements replacements;
const std::string query = base::StringPrintf("options=%s", extension->id());
replacements.SetQueryStr(query);
return GURL(chrome::kChromeUIExtensionsURL).ReplaceComponents(replacements);
}
}
// Returns the browser that contains the tab group with `id` or null if none is
// found.
BrowserWindowInterface* FindBrowserWithGroup(const tab_groups::TabGroupId& id) {
for (BrowserWindowInterface* const bwi : GetAllBrowserWindowInterfaces()) {
TabListInterface* const tab_list = TabListInterface::From(bwi);
if (tab_list && tab_list->ContainsTabGroup(id)) {
return bwi;
}
}
return nullptr;
}
// Gets the window ID that the group belongs to.
int GetWindowIdOfGroup(const tab_groups::TabGroupId& id) {
if (BrowserWindowInterface* const browser = FindBrowserWithGroup(id);
browser) {
return browser->GetSessionID().id();
}
return -1;
}
// Creates a tab MutedInfo object (see chrome/common/extensions/api/tabs.json)
// with information about the mute state of a browser tab.
api::tabs::MutedInfo CreateMutedInfo(content::WebContents* contents) {
DCHECK(contents);
api::tabs::MutedInfo info;
info.muted = contents->IsAudioMuted();
switch (GetTabAudioMutedReason(contents)) {
case TabMutedReason::kNone:
break;
case TabMutedReason::kAudioIndicator:
case TabMutedReason::kContentSetting:
case TabMutedReason::kContentSettingChrome:
info.reason = api::tabs::MutedInfoReason::kUser;
break;
case TabMutedReason::kExtension:
info.reason = api::tabs::MutedInfoReason::kExtension;
info.extension_id =
LastMuteMetadata::FromWebContents(contents)->extension_id;
DCHECK(!info.extension_id->empty());
break;
}
return info;
}
} // namespace
WindowController* ExtensionTabUtil::GetControllerFromWindowID(
const ChromeExtensionFunctionDetails& details,
int window_id,
std::string* error) {
if (window_id == extension_misc::kCurrentWindowId) {
if (WindowController* window_controller =
details.GetCurrentWindowController()) {
return window_controller;
}
if (error) {
*error = kNoCurrentWindowError;
}
return nullptr;
}
return GetControllerInProfileWithId(
Profile::FromBrowserContext(details.function()->browser_context()),
window_id, details.function()->include_incognito_information(), error);
}
WindowController* ExtensionTabUtil::GetControllerInProfileWithId(
Profile* profile,
int window_id,
bool also_match_incognito_profile,
std::string* error_message) {
const Profile* incognito_profile =
also_match_incognito_profile
? profile->GetPrimaryOTRProfile(/*create_if_needed=*/false)
: nullptr;
for (WindowController* window_controller :
*WindowControllerList::GetInstance()) {
const Profile* controller_profile = window_controller->profile();
if ((controller_profile == profile ||
controller_profile == incognito_profile) &&
window_controller->GetWindowId() == window_id) {
return window_controller;
}
}
if (error_message) {
*error_message = ErrorUtils::FormatErrorMessage(
kWindowNotFoundError, base::NumberToString(window_id));
}
return nullptr;
}
int ExtensionTabUtil::GetWindowId(BrowserWindowInterface* browser) {
return WindowControllerFromBrowser(browser)->GetWindowId();
}
int ExtensionTabUtil::GetTabId(const WebContents* web_contents) {
return sessions::SessionTabHelper::IdForTab(web_contents).id();
}
int ExtensionTabUtil::GetWindowIdOfTab(const WebContents* web_contents) {
return sessions::SessionTabHelper::IdForWindowContainingTab(web_contents)
.id();
}
// static
api::tabs::Tab ExtensionTabUtil::CreateTabObject(
WebContents* contents,
ScrubTabBehavior scrub_tab_behavior,
const Extension* extension,
TabListInterface* tab_list,
int tab_index) {
if (!tab_list) {
GetTabListInterface(*contents, &tab_list, &tab_index);
}
api::tabs::Tab tab_object;
tab_object.id = GetTabIdForExtensions(*contents);
tab_object.index = tab_index;
tab_object.window_id = GetWindowIdOfTab(contents);
tab_object.status = GetLoadingStatus(contents);
tab_object.last_accessed =
contents->GetLastActiveTime().InMillisecondsFSinceUnixEpoch();
tabs::TabInterface* tab_interface =
tab_list ? tab_list->GetTab(tab_index) : nullptr;
bool is_active = tab_interface && tab_interface->IsActivated();
tab_object.active = is_active;
tab_object.selected = is_active;
tab_object.highlighted = tab_interface && tab_interface->IsSelected();
tab_object.pinned = tab_interface && tab_interface->IsPinned();
tab_object.group_id = -1;
if (tab_interface) {
std::optional<tab_groups::TabGroupId> group = tab_interface->GetGroup();
if (group.has_value()) {
tab_object.group_id = GetGroupId(group.value());
}
}
tab_object.split_view_id = -1;
if (tab_interface) {
std::optional<split_tabs::SplitTabId> split = tab_interface->GetSplit();
if (split.has_value()) {
tab_object.split_view_id = GetSplitId(split.value());
}
}
auto get_audible = [contents]() {
#if BUILDFLAG(ENABLE_EXTENSIONS)
auto* audible_helper = RecentlyAudibleHelper::FromWebContents(contents);
if (audible_helper) {
// WebContents in a tab strip have RecentlyAudible helpers. They endow
// the tab with a notion of audibility that has a timeout for quiet
// periods. Use that if available.
return audible_helper->WasRecentlyAudible();
}
#endif
// Otherwise use the instantaneous notion of audibility.
return contents->IsCurrentlyAudible();
};
tab_object.audible = get_audible();
#if BUILDFLAG(IS_ANDROID)
tab_object.discarded = contents->WasDiscarded();
// TODO(crbug.com/505306735): Determine auto-discardable and frozen states on
// desktop Android where the TabLifecycleUnit is not available.
#else
auto* tab_lifecycle_unit_external =
resource_coordinator::TabLifecycleUnitExternal::FromWebContents(contents);
// Note that while a discarded tab *must* have an unloaded status, its
// possible for an unloaded tab to not be discarded (session restored tabs
// whose loads have been deferred, for example).
tab_object.discarded = tab_lifecycle_unit_external &&
tab_lifecycle_unit_external->GetTabState() ==
::mojom::LifecycleUnitState::DISCARDED;
DCHECK(!tab_object.discarded ||
tab_object.status == api::tabs::TabStatus::kUnloaded);
tab_object.auto_discardable =
!tab_lifecycle_unit_external ||
tab_lifecycle_unit_external->IsAutoDiscardable();
tab_object.frozen = tab_lifecycle_unit_external &&
tab_lifecycle_unit_external->GetTabState() ==
::mojom::LifecycleUnitState::FROZEN;
#endif // BUILDFLAG(IS_ANDROID)
tab_object.muted_info = CreateMutedInfo(contents);
tab_object.incognito = contents->GetBrowserContext()->IsOffTheRecord();
gfx::Size contents_size = contents->GetContainerBounds().size();
tab_object.width = contents_size.width();
tab_object.height = contents_size.height();
tab_object.url = contents->GetLastCommittedURL().spec();
NavigationEntry* pending_entry = contents->GetController().GetPendingEntry();
if (pending_entry) {
tab_object.pending_url = pending_entry->GetVirtualURL().spec();
}
tab_object.title = base::UTF16ToUTF8(contents->GetTitle());
// TODO(tjudkins) This should probably use the LastCommittedEntry() for
// consistency.
NavigationEntry* visible_entry = contents->GetController().GetVisibleEntry();
if (visible_entry && visible_entry->GetFavicon().valid) {
tab_object.fav_icon_url = visible_entry->GetFavicon().url.spec();
}
if (tab_list && tab_interface) {
tabs::TabInterface* opener =
tab_list->GetOpenerForTab(tab_interface->GetHandle());
if (opener) {
content::WebContents* opener_contents = opener->GetContents();
CHECK(opener_contents);
tab_object.opener_tab_id = GetTabIdForExtensions(*opener_contents);
}
}
ScrubTabForExtension(extension, contents, &tab_object, scrub_tab_behavior);
return tab_object;
}
// static
base::DictValue ExtensionTabUtil::CreateWindowValueForExtension(
BrowserWindowInterface& browser,
const Extension* extension,
WindowController::PopulateTabBehavior populate_tab_behavior,
mojom::ContextType context) {
return WindowControllerFromBrowser(&browser)->CreateWindowValueForExtension(
extension, populate_tab_behavior, context);
}
// static
ExtensionTabUtil::ScrubTabBehavior ExtensionTabUtil::GetScrubTabBehavior(
const Extension* extension,
mojom::ContextType context,
content::WebContents* contents) {
int tab_id = GetTabId(contents);
ScrubTabBehavior behavior;
behavior.committed_info = GetScrubTabBehaviorImpl(
extension, context, contents->GetLastCommittedURL(), tab_id);
NavigationEntry* entry = contents->GetController().GetPendingEntry();
GURL pending_url;
if (entry) {
pending_url = entry->GetVirtualURL();
}
behavior.pending_info =
GetScrubTabBehaviorImpl(extension, context, pending_url, tab_id);
return behavior;
}
// static
ExtensionTabUtil::ScrubTabBehavior ExtensionTabUtil::GetScrubTabBehavior(
const Extension* extension,
mojom::ContextType context,
const GURL& url) {
ScrubTabBehaviorType type =
GetScrubTabBehaviorImpl(extension, context, url, api::tabs::TAB_ID_NONE);
return {type, type};
}
// static
void ExtensionTabUtil::ScrubTabForExtension(
const Extension* extension,
content::WebContents* contents,
api::tabs::Tab* tab,
ScrubTabBehavior scrub_tab_behavior) {
// Remove sensitive committed tab info if necessary.
switch (scrub_tab_behavior.committed_info) {
case kScrubTabFully:
tab->url.reset();
tab->title.reset();
tab->fav_icon_url.reset();
break;
case kScrubTabUrlToOrigin:
tab->url = GURL(*tab->url).DeprecatedGetOriginAsURL().spec();
break;
case kDontScrubTab:
break;
}
// Remove sensitive pending tab info if necessary.
if (tab->pending_url) {
switch (scrub_tab_behavior.pending_info) {
case kScrubTabFully:
tab->pending_url.reset();
break;
case kScrubTabUrlToOrigin:
tab->pending_url =
GURL(*tab->pending_url).DeprecatedGetOriginAsURL().spec();
break;
case kDontScrubTab:
break;
}
}
}
bool ExtensionTabUtil::GetTabListInterface(content::WebContents& web_contents,
TabListInterface** tab_list_out,
int* index_out) {
tabs::TabInterface* tab_interface =
tabs::TabInterface::MaybeGetFromContents(&web_contents);
if (!tab_interface) {
return false;
}
BrowserWindowInterface* browser =
#if BUILDFLAG(IS_ANDROID)
browser_window_util::GetBrowserForTabContents(web_contents);
#else
tab_interface->GetBrowserWindowInterface();
#endif
if (!browser) {
return false;
}
TabListInterface* tab_list = TabListInterface::From(browser);
if (!tab_list) {
return false;
}
// Find the index of the tab within the browser window.
// TODO(https://crbug.com/415961057): This is clunky. Let's add a
// GetIndexOfTab() method.
std::vector<tabs::TabInterface*> all_tabs = tab_list->GetAllTabs();
int index = -1;
for (size_t i = 0; i < all_tabs.size(); ++i) {
if (all_tabs[i] == tab_interface) {
index = i;
break;
}
}
// Even though we got here by looking at the tab strip from the browser window
// we got from the tab, it's possible the tab isn't in the tab strip. One case
// in which this happens is if the tab is in the process of being removed.
if (index == -1) {
return false;
}
*index_out = index;
*tab_list_out = tab_list;
return true;
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
// static
bool ExtensionTabUtil::GetTabStripModel(const WebContents* web_contents,
TabStripModel** tab_strip_model,
int* tab_index) {
DCHECK(web_contents);
DCHECK(tab_strip_model);
DCHECK(tab_index);
bool found = false;
ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
[web_contents, tab_strip_model, tab_index,
&found](BrowserWindowInterface* browser_window_interface) {
TabStripModel* tab_strip = browser_window_interface->GetTabStripModel();
int index = tab_strip->GetIndexOfWebContents(web_contents);
if (index != -1) {
*tab_strip_model = tab_strip;
*tab_index = index;
found = true;
return false;
}
return true;
});
return found;
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
// static
bool ExtensionTabUtil::GetTabById(int tab_id,
content::BrowserContext* browser_context,
bool include_incognito,
WindowController** out_window,
WebContents** out_contents,
int* out_tab_index) {
// Zero the output parameters so they have predictable values on failure.
if (out_window) {
*out_window = nullptr;
}
if (out_contents) {
*out_contents = nullptr;
}
if (out_tab_index) {
*out_tab_index = api::tabs::TAB_INDEX_NONE;
}
if (tab_id == api::tabs::TAB_ID_NONE)
return false;
// `browser_context` can be null during shutdown.
if (!browser_context) {
return false;
}
Profile* profile = Profile::FromBrowserContext(browser_context);
Profile* incognito_profile =
include_incognito
? (profile ? profile->GetPrimaryOTRProfile(/*create_if_needed=*/false)
: nullptr)
: nullptr;
for (WindowController* window : *WindowControllerList::GetInstance()) {
bool is_alive = window->GetBrowserWindowInterface() &&
!window->GetBrowserWindowInterface()->IsDeleteScheduled();
bool profile_matches =
window->profile() == profile || window->profile() == incognito_profile;
if (!is_alive || !profile_matches) {
continue;
}
for (int i = 0; i < window->GetTabCount(); ++i) {
WebContents* target_contents = window->GetWebContentsAt(i);
if (sessions::SessionTabHelper::IdForTab(target_contents).id() ==
tab_id) {
if (out_window) {
*out_window = window;
}
if (out_contents) {
*out_contents = target_contents;
}
if (out_tab_index) {
*out_tab_index = i;
}
return true;
}
}
}
// Prerendering tab is not visible and it cannot be in `TabStripModel`, if the
// tab id exists as a prerendering tab, and the API will returns
// `api::tabs::TAB_INDEX_NONE` for `out_tab_index` and a valid `WebContents`.
for (auto rph_iterator = content::RenderProcessHost::AllHostsIterator();
!rph_iterator.IsAtEnd(); rph_iterator.Advance()) {
content::RenderProcessHost* rph = rph_iterator.GetCurrentValue();
// Ignore renderers that aren't ready.
if (!rph->IsInitializedAndNotDead()) {
continue;
}
// Ignore renderers that aren't from a valid profile. This is either the
// same profile or the incognito profile if `include_incognito` is true.
Profile* process_profile =
Profile::FromBrowserContext(rph->GetBrowserContext());
if (process_profile != profile &&
!(include_incognito && profile->IsSameOrParent(process_profile))) {
continue;
}
content::WebContents* found_prerender_contents = nullptr;
rph->ForEachRenderFrameHost([&found_prerender_contents,
tab_id](content::RenderFrameHost* rfh) {
CHECK(rfh);
WebContents* web_contents = WebContents::FromRenderFrameHost(rfh);
CHECK(web_contents);
if (sessions::SessionTabHelper::IdForTab(web_contents).id() != tab_id) {
return;
}
// We only consider prerendered frames in this loop. Otherwise, we could
// end up returning a tab for a different web contents that shouldn't be
// exposed to extensions.
if (!web_contents->IsPrerenderedFrame(rfh->GetFrameTreeNodeId())) {
return;
}
found_prerender_contents = web_contents;
});
if (found_prerender_contents && out_contents) {
*out_contents = found_prerender_contents;
return true;
}
}
return false;
}
// static
bool ExtensionTabUtil::GetTabById(int tab_id,
content::BrowserContext* browser_context,
bool include_incognito,
WebContents** contents) {
return GetTabById(tab_id, browser_context, include_incognito, nullptr,
contents, nullptr);
}
// static
int ExtensionTabUtil::GetGroupId(const tab_groups::TabGroupId& id) {
uint32_t hash = base::PersistentHash(id.ToString());
return std::abs(static_cast<int>(hash));
}
// static
int ExtensionTabUtil::GetSplitId(const split_tabs::SplitTabId& id) {
uint32_t hash = base::PersistentHash(id.ToString());
return std::abs(static_cast<int>(hash));
}
// static
bool ExtensionTabUtil::SupportsTabGroups(BrowserWindowInterface* browser) {
CHECK(browser);
#if BUILDFLAG(IS_ANDROID)
// Android only supports tab groups for normal browser windows.
return browser->GetType() == BrowserWindowInterface::TYPE_NORMAL;
#else
// Other platforms have more complex logic (i.e. more browser types).
return browser->GetTabStripModel()->SupportsTabGroups();
#endif
}
// static
bool ExtensionTabUtil::GetGroupById(
int group_id,
content::BrowserContext* browser_context,
bool include_incognito,
WindowController** out_window,
tab_groups::TabGroupId* out_id,
tab_groups::TabGroupVisualData* out_visual_data,
std::string* error) {
// Zero output parameters for the error cases.
if (out_window) {
*out_window = nullptr;
}
if (out_visual_data) {
*out_visual_data = {};
}
if (group_id == -1) {
return false;
}
Profile* profile = Profile::FromBrowserContext(browser_context);
Profile* incognito_profile =
include_incognito && profile->HasPrimaryOTRProfile()
? profile->GetPrimaryOTRProfile(/*create_if_needed=*/true)
: nullptr;
for (WindowController* target_window : *WindowControllerList::GetInstance()) {
if (target_window->profile() != profile &&
target_window->profile() != incognito_profile) {
continue;
}
BrowserWindowInterface* target_browser =
target_window->GetBrowserWindowInterface();
if (!target_browser) {
continue;
}
if (!SupportsTabGroups(target_browser)) {
continue;
}
TabListInterface* tab_list = TabListInterface::From(target_browser);
if (!tab_list) {
continue;
}
for (tab_groups::TabGroupId target_group : tab_list->ListTabGroups()) {
if (ExtensionTabUtil::GetGroupId(target_group) == group_id) {
if (out_window) {
*out_window = target_window;
}
if (out_id) {
*out_id = target_group;
}
if (out_visual_data) {
std::optional<tab_groups::TabGroupVisualData> visual_data =
tab_list->GetTabGroupVisualData(target_group);
if (visual_data.has_value()) {
*out_visual_data = visual_data.value();
}
}
return true;
}
}
}
*error = ErrorUtils::FormatErrorMessage(kGroupNotFoundError,
base::NumberToString(group_id));
return false;
}
// static
api::tab_groups::TabGroup ExtensionTabUtil::CreateTabGroupObject(
const tab_groups::TabGroupId& id,
const tab_groups::TabGroupVisualData& visual_data) {
api::tab_groups::TabGroup tab_group_object;
tab_group_object.id = GetGroupId(id);
tab_group_object.collapsed = visual_data.is_collapsed();
tab_group_object.color = ColorIdToColor(visual_data.color());
tab_group_object.title = base::UTF16ToUTF8(visual_data.title());
tab_group_object.window_id = GetWindowIdOfGroup(id);
tab_group_object.shared = GetSharedStateOfGroup(id);
return tab_group_object;
}
// static
bool ExtensionTabUtil::GetSharedStateOfGroup(const tab_groups::TabGroupId& id) {
if (!data_sharing::features::IsDataSharingFunctionalityEnabled()) {
return false;
}
BrowserWindowInterface* browser = FindBrowserWithGroup(id);
if (!browser) {
return false;
}
tab_groups::TabGroupSyncService* tab_group_service =
tab_groups::TabGroupSyncServiceFactory::GetForProfile(
browser->GetProfile());
if (!tab_group_service) {
return false;
}
#if BUILDFLAG(IS_ANDROID)
// TabGroupService uses a different type on Android.
const base::Token local_id = id.token();
#else
const tab_groups::TabGroupId local_id = id;
#endif
std::optional<tab_groups::SavedTabGroup> saved_group =
tab_group_service->GetGroup(local_id);
if (!saved_group) {
return false;
}
return saved_group->is_shared_tab_group();
}
// static
std::optional<api::tab_groups::TabGroup> ExtensionTabUtil::CreateTabGroupObject(
const tab_groups::TabGroupId& id) {
BrowserWindowInterface* browser = FindBrowserWithGroup(id);
if (!browser) {
return std::nullopt;
}
CHECK(SupportsTabGroups(browser));
TabListInterface* tab_list = TabListInterface::From(browser);
if (!tab_list) {
return std::nullopt;
}
std::optional<tab_groups::TabGroupVisualData> visual_data =
tab_list->GetTabGroupVisualData(id);
DCHECK(visual_data);
return CreateTabGroupObject(id, *visual_data);
}
// static
api::tab_groups::Color ExtensionTabUtil::ColorIdToColor(
const tab_groups::TabGroupColorId& color_id) {
switch (color_id) {
case tab_groups::TabGroupColorId::kGrey:
return api::tab_groups::Color::kGrey;
case tab_groups::TabGroupColorId::kBlue:
return api::tab_groups::Color::kBlue;
case tab_groups::TabGroupColorId::kRed:
return api::tab_groups::Color::kRed;
case tab_groups::TabGroupColorId::kYellow:
return api::tab_groups::Color::kYellow;
case tab_groups::TabGroupColorId::kGreen:
return api::tab_groups::Color::kGreen;
case tab_groups::TabGroupColorId::kPink:
return api::tab_groups::Color::kPink;
case tab_groups::TabGroupColorId::kPurple:
return api::tab_groups::Color::kPurple;
case tab_groups::TabGroupColorId::kCyan:
return api::tab_groups::Color::kCyan;
case tab_groups::TabGroupColorId::kOrange:
return api::tab_groups::Color::kOrange;
case tab_groups::TabGroupColorId::kNumEntries:
NOTREACHED() << "kNumEntries is not a support color enum.";
}
NOTREACHED();
}
// static
tab_groups::TabGroupColorId ExtensionTabUtil::ColorToColorId(
api::tab_groups::Color color) {
switch (color) {
case api::tab_groups::Color::kGrey:
return tab_groups::TabGroupColorId::kGrey;
case api::tab_groups::Color::kBlue:
return tab_groups::TabGroupColorId::kBlue;
case api::tab_groups::Color::kRed:
return tab_groups::TabGroupColorId::kRed;
case api::tab_groups::Color::kYellow:
return tab_groups::TabGroupColorId::kYellow;
case api::tab_groups::Color::kGreen:
return tab_groups::TabGroupColorId::kGreen;
case api::tab_groups::Color::kPink:
return tab_groups::TabGroupColorId::kPink;
case api::tab_groups::Color::kPurple:
return tab_groups::TabGroupColorId::kPurple;
case api::tab_groups::Color::kCyan:
return tab_groups::TabGroupColorId::kCyan;
case api::tab_groups::Color::kOrange:
return tab_groups::TabGroupColorId::kOrange;
case api::tab_groups::Color::kNone:
NOTREACHED();
}
NOTREACHED();
}
// static
std::vector<content::WebContents*>
ExtensionTabUtil::GetAllActiveWebContentsForContext(
content::BrowserContext* browser_context,
bool include_incognito) {
std::vector<content::WebContents*> active_contents;
Profile* profile = Profile::FromBrowserContext(browser_context);
Profile* incognito_profile =
include_incognito
? profile->GetPrimaryOTRProfile(/*create_if_needed=*/false)
: nullptr;
ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
[profile, incognito_profile,
&active_contents](BrowserWindowInterface* browser) {
const Profile* browser_profile = browser->GetProfile();
if (browser_profile == profile ||
browser_profile == incognito_profile) {
TabListInterface* tab_list = TabListInterface::From(browser);
if (tab_list) {
content::WebContents* tab_contents =
tab_list->GetActiveTab()->GetContents();
CHECK(tab_contents);
active_contents.push_back(tab_contents);
}
}
return true;
});
return active_contents;
}
// static
bool ExtensionTabUtil::IsWebContentsInContext(
content::WebContents* web_contents,
content::BrowserContext* browser_context,
bool include_incognito) {
// Look at the WebContents BrowserContext and see if it is the same.
content::BrowserContext* web_contents_browser_context =
web_contents->GetBrowserContext();
if (web_contents_browser_context == browser_context)
return true;
// If not it might be to include the incognito mode, so we if the profiles
// are the same or the parent.
return include_incognito && Profile::FromBrowserContext(browser_context)
->IsSameOrParent(Profile::FromBrowserContext(
web_contents_browser_context));
}
GURL ExtensionTabUtil::ResolvePossiblyRelativeURL(const std::string& url_string,
const Extension* extension) {
GURL url = GURL(url_string);
if (!url.is_valid() && extension) {
url = extension->ResolveExtensionURL(url_string);
}
return url;
}
void ExtensionTabUtil::NavigateToURL(WindowOpenDisposition disposition,
content::WebContents* source_contents,
const GURL& url,
base::OnceClosure done_callback) {
BrowserWindowInterface* browser =
source_contents
? browser_window_util::GetBrowserForTabContents(*source_contents)
: nullptr;
auto params = std::make_unique<NavigateParams>(browser, url,
ui::PAGE_TRANSITION_FROM_API);
params->disposition = disposition;
params->window_action = NavigateParams::WindowAction::kShowWindow;
if (source_contents) {
params->source_contents = source_contents;
}
// Navigation on desktop Android can be asynchronous, in particular if it
// creates a new window. Ensure `params` stays alive by transferring
// ownership to the navigate callback below. Cache the pointer first, as the
// call to release() will set `params` to null.
NavigateParams* raw_params = params.get();
auto callback = base::BindOnce(
[](NavigateParams* params, base::OnceClosure done_callback,
base::WeakPtr<content::NavigationHandle> handle) {
std::move(done_callback).Run();
},
base::Owned(params.release()), std::move(done_callback));
Navigate(raw_params, std::move(callback));
}
bool ExtensionTabUtil::IsKillURL(const GURL& url) {
#if DCHECK_IS_ON()
// Caller should ensure that |url| is already "fixed up" by
// url_formatter::FixupURL, which (among many other things) takes care
// of rewriting about:kill into chrome://kill/.
if (url.SchemeIs(url::kAboutScheme))
DCHECK(url.IsAboutBlank() || url.IsAboutSrcdoc());
#endif
// Disallow common renderer debug URLs.
// Note: this would also disallow JavaScript URLs, but we already explicitly
// check for those before calling into here from PrepareURLForNavigation.
if (blink::IsRendererDebugURL(url)) {
return true;
}
if (!url.SchemeIs(content::kChromeUIScheme)) {
return false;
}
// Also disallow a few more hosts which are not covered by the check above.
constexpr auto kKillHosts = base::MakeFixedFlatSet<std::string_view>({
chrome::kChromeUIDelayedHangUIHost,
chrome::kChromeUIHangUIHost,
chrome::kChromeUIQuitHost,
chrome::kChromeUIRestartHost,
content::kChromeUIBrowserCrashHost,
content::kChromeUIMemoryExhaustHost,
});
return kKillHosts.contains(url.host());
}
base::expected<GURL, std::string> ExtensionTabUtil::PrepareURLForNavigation(
const std::string& url_string,
const Extension* extension,
content::BrowserContext* browser_context) {
GURL url =
ExtensionTabUtil::ResolvePossiblyRelativeURL(url_string, extension);
// TODO(crbug.com/385086924): url_formatter::FixupURL transforms a URL
// with a 'mailto' scheme into a URL with an HTTP scheme. This is a
// mitigation pending a fix for the bug.
if (url.SchemeIs(url::kMailToScheme)) {
return url;
}
// Ideally, the URL would only be "fixed" for user input (e.g. for URLs
// entered into the Omnibox), but some extensions rely on the legacy behavior
// where all navigations were subject to the "fixing". See also
// https://crbug.com/40155847.
url = url_formatter::FixupURL(url.spec());
// Reject invalid URLs.
if (!url.is_valid()) {
return base::unexpected(
ErrorUtils::FormatErrorMessage(kInvalidUrlError, url_string));
}
// Don't let the extension use JavaScript URLs in API triggered navigations.
if (url.SchemeIs(url::kJavaScriptScheme)) {
return base::unexpected(kJavaScriptUrlsNotAllowedInExtensionNavigations);
}
// Don't let the extension crash the browser or renderers.
if (ExtensionTabUtil::IsKillURL(url)) {
return base::unexpected(kNoCrashBrowserError);
}
// Don't let the extension navigate directly to devtools scheme pages.
if (url.SchemeIs(content::kChromeDevToolsScheme)) {
return base::unexpected(kCannotNavigateToDevtools);
}
// Don't let the extension navigate directly to chrome-untrusted scheme pages.
if (url.SchemeIs(content::kChromeUIUntrustedScheme)) {
return base::unexpected(kCannotNavigateToChromeUntrusted);
}
// Don't let the extension navigate directly to file scheme pages, unless
// they have file access. `extension` can be null if the call is made from
// non-extension contexts (e.g. WebUI pages). In that case, we allow the
// navigation as such contexts are trusted and do not have a concept of file
// access.
if (extension && IsFileUrl(url) &&
// PDF viewer extension can navigate to file URLs.
extension->id() != extension_misc::kPdfExtensionId &&
!util::AllowFileAccess(extension->id(), browser_context) &&
!extensions::ExtensionManagementFactory::GetForBrowserContext(
browser_context)
->IsFileUrlNavigationAllowed(extension->id())) {
return base::unexpected(kFileUrlsNotAllowedInExtensionNavigations);
}
if (extension && browser_context) {
RecordNavigationScheme(url, *extension, browser_context);
}
return url;
}
// static
void ExtensionTabUtil::ForEachTab(
base::RepeatingCallback<void(WebContents*)> callback) {
tabs::ForEachTabInterface([&callback](tabs::TabInterface* tab) {
callback.Run(tab->GetContents());
return true;
});
}
// static
bool ExtensionTabUtil::OpenOptionsPageFromWebContents(
const Extension* extension,
content::WebContents* web_contents) {
const std::optional<GURL> url = GetOptionsPageUrlToNavigate(extension);
if (!url) {
return false;
}
const bool open_in_tab = ShouldOpenInTab(extension);
BrowserWindowInterface* browser =
browser_window_util::GetBrowserForTabContents(*web_contents);
CHECK(browser);
return WindowControllerFromBrowser(browser)->OpenOptionsPage(extension, *url,
open_in_tab);
}
// static
WindowController* ExtensionTabUtil::GetWindowControllerOfTab(
WebContents* web_contents) {
BrowserWindowInterface* browser =
browser_window_util::GetBrowserForTabContents(*web_contents);
if (browser) {
return BrowserExtensionWindowController::From(browser);
}
return nullptr;
}
// static
bool ExtensionTabUtil::OpenOptionsPage(const Extension* extension,
BrowserWindowInterface* browser) {
const std::optional<GURL> url = GetOptionsPageUrlToNavigate(extension);
if (!url) {
return false;
}
const bool open_in_tab = ShouldOpenInTab(extension);
return WindowControllerFromBrowser(browser)->OpenOptionsPage(extension, *url,
open_in_tab);
}
// static
bool ExtensionTabUtil::BrowserSupportsTabs(BrowserWindowInterface* browser) {
if (!browser) {
return false;
}
// On non-android platforms, devtools windows are backed by a Browser
// instance.
#if !BUILDFLAG(IS_ANDROID)
// TODO(devlin): Should we be checking for other types, too? Like PiP?
if (browser->GetType() == BrowserWindowInterface::TYPE_DEVTOOLS) {
return false;
}
#endif // BUILDFLAG(IS_ANDROID)
return true;
}
// static
api::tabs::TabStatus ExtensionTabUtil::GetLoadingStatus(WebContents* contents) {
if (contents->IsLoading()) {
return api::tabs::TabStatus::kLoading;
}
// Anything that isn't backed by a process is considered unloaded. Discarded
// tabs should also be considered unloaded as the tab itself may be retained
// but the hosted document discarded to reclaim resources.
if (!HasValidMainFrameProcess(contents) || contents->WasDiscarded()) {
return api::tabs::TabStatus::kUnloaded;
}
// Otherwise its considered loaded.
return api::tabs::TabStatus::kComplete;
}
void ExtensionTabUtil::ClearBackForwardCache() {
ForEachTab(base::BindRepeating([](WebContents* web_contents) {
web_contents->GetController().GetBackForwardCache().Flush();
}));
}
// static
bool ExtensionTabUtil::IsTabStripEditable(Profile& profile) {
if (g_disable_tab_list_editing_for_testing) {
return false;
}
return TabListInterface::CanEditTabList(profile);
}
// static
TabListInterface* ExtensionTabUtil::GetEditableTabList(
BrowserWindowInterface& browser) {
if (!IsTabStripEditable(*browser.GetProfile())) {
return nullptr;
}
return TabListInterface::From(&browser);
}
// static
base::AutoReset<bool> ExtensionTabUtil::DisableTabListEditingForTesting() {
return base::AutoReset<bool>(&g_disable_tab_list_editing_for_testing, true);
}
} // namespace extensions