| // 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 |