| // 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 <stddef.h> |
| |
| #include <algorithm> |
| #include <limits> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check_op.h" |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_set.h" |
| #include "base/functional/bind.h" |
| #include "base/location.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/types/expected.h" |
| #include "base/types/expected_macros.h" |
| #include "base/types/optional_util.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/devtools/devtools_window.h" |
| #include "chrome/browser/extensions/api/tabs/tabs_api.h" |
| #include "chrome/browser/extensions/api/tabs/tabs_constants.h" |
| #include "chrome/browser/extensions/api/tabs/windows_util.h" |
| #include "chrome/browser/extensions/browser_extension_window_controller.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/extension_tab_util.h" |
| #include "chrome/browser/extensions/tab_helper.h" |
| #include "chrome/browser/extensions/window_controller.h" |
| #include "chrome/browser/extensions/window_controller_list.h" |
| #include "chrome/browser/platform_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/resource_coordinator/tab_lifecycle_unit_external.h" |
| #include "chrome/browser/resource_coordinator/tab_manager.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/browser/ui/browser_finder.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/browser_navigator.h" |
| #include "chrome/browser/ui/browser_navigator_params.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_features.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/tabs/tab_enums.h" |
| #include "chrome/browser/ui/tabs/tab_group_model.h" |
| #include "chrome/browser/ui/tabs/tab_list_interface.h" |
| #include "chrome/browser/ui/tabs/tab_model.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/tabs/tab_strip_user_gesture_details.h" |
| #include "chrome/browser/ui/tabs/tab_utils.h" |
| #include "chrome/browser/ui/web_applications/app_browser_controller.h" |
| #include "chrome/browser/ui/web_applications/web_app_launch_utils.h" |
| #include "chrome/browser/ui/window_sizer/window_sizer.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h" |
| #include "chrome/browser/web_applications/web_app_helpers.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/browser/web_applications/web_app_registrar.h" |
| #include "chrome/common/extensions/api/tabs.h" |
| #include "chrome/common/extensions/api/windows.h" |
| #include "chrome/common/extensions/extension_constants.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/pref_registry/pref_registry_syncable.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/safe_browsing/buildflags.h" |
| #include "components/sessions/content/session_tab_helper.h" |
| #include "components/tab_groups/tab_group_color.h" |
| #include "components/tab_groups/tab_group_id.h" |
| #include "components/tab_groups/tab_group_visual_data.h" |
| #include "components/tabs/public/split_tab_data.h" |
| #include "components/tabs/public/split_tab_id.h" |
| #include "components/tabs/public/tab_group.h" |
| #include "components/tabs/public/tab_interface.h" |
| #include "components/webapps/common/web_app_id.h" |
| #include "content/public/browser/navigation_controller.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/url_constants.h" |
| #include "extensions/browser/app_window/app_window.h" |
| #include "extensions/browser/extension_api_frame_id_map.h" |
| #include "extensions/browser/extension_function_dispatcher.h" |
| #include "extensions/browser/extension_host.h" |
| #include "extensions/browser/extensions_browser_client.h" |
| #include "extensions/browser/file_reader.h" |
| #include "extensions/browser/script_executor.h" |
| #include "extensions/common/api/extension_types.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/default_locale_handler.h" |
| #include "extensions/common/message_bundle.h" |
| #include "extensions/common/mojom/context_type.mojom.h" |
| #include "extensions/common/mojom/host_id.mojom.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/user_script.h" |
| #include "skia/ext/image_operations.h" |
| #include "skia/ext/platform_canvas.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/base/base_window.h" |
| #include "ui/base/models/list_selection_model.h" |
| #include "ui/base/mojom/window_show_state.mojom.h" |
| #include "ui/base/ozone_buildflags.h" |
| #include "ui/base/ui_base_types.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/geometry/rect.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "ash/wm/window_pin_util.h" |
| #include "chrome/browser/ash/boca/on_task/locked_quiz_session_manager_factory.h" |
| #include "chrome/browser/ui/browser_command_controller.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| using content::BrowserThread; |
| using content::NavigationController; |
| using content::NavigationEntry; |
| using content::OpenURLParams; |
| using content::Referrer; |
| using content::WebContents; |
| using tabs::TabModel; |
| |
| namespace extensions { |
| |
| namespace windows = api::windows; |
| namespace tabs = api::tabs; |
| |
| using api::extension_types::InjectDetails; |
| |
| namespace { |
| |
| constexpr char kTabIndexNotFoundError[] = "No tab at index: *."; |
| constexpr char kCannotFindTabToDiscard[] = "Cannot find a tab to discard."; |
| constexpr char kNoHighlightedTabError[] = "No highlighted tab"; |
| constexpr char kInvalidWindowTypeError[] = "Invalid value for type"; |
| constexpr char kCannotUpdateMuteCaptured[] = |
| "Cannot update mute state for tab *, tab has audio or video currently " |
| "being captured"; |
| constexpr char kWindowCreateSupportsOnlySingleIwaUrlError[] = |
| "When creating a window for a URL with the 'isolated-app:' scheme, only " |
| "one tab can be added to the window."; |
| constexpr char kWindowCreateCannotParseIwaUrlError[] = |
| "Unable to parse 'isolated-app:' URL: %s"; |
| constexpr char kWindowCreateCannotUseTabIdWithIwaError[] = |
| "Creating a new window for an Isolated Web App does not support adding a " |
| "tab by its ID."; |
| constexpr char kWindowCreateCannotMoveIwaTabError[] = |
| "The tab of an Isolated Web App cannot be moved to a new window."; |
| |
| bool IsValidStateForWindowsCreateFunction( |
| const windows::Create::Params::CreateData* create_data) { |
| if (!create_data) { |
| return true; |
| } |
| |
| bool has_bound = create_data->left || create_data->top || |
| create_data->width || create_data->height; |
| |
| switch (create_data->state) { |
| case windows::WindowState::kMinimized: |
| // If minimised, default focused state should be unfocused. |
| return !(create_data->focused && *create_data->focused) && !has_bound; |
| case windows::WindowState::kMaximized: |
| case windows::WindowState::kFullscreen: |
| case windows::WindowState::kLockedFullscreen: |
| // If maximised/fullscreen, default focused state should be focused. |
| return !(create_data->focused && !*create_data->focused) && !has_bound; |
| case windows::WindowState::kNormal: |
| case windows::WindowState::kNone: |
| return true; |
| } |
| NOTREACHED(); |
| } |
| |
| // Moves the given tab to the |target_browser|. On success, returns the |
| // new index of the tab in the target tabstrip. On failure, returns -1. |
| // Assumes that the caller has already checked whether the target window is |
| // different from the source. |
| int MoveTabToWindow(ExtensionFunction* function, |
| int tab_id, |
| Browser* target_browser, |
| int new_index, |
| std::string* error) { |
| WindowController* source_window = nullptr; |
| int source_index = -1; |
| if (!tabs_internal::GetTabById(tab_id, function->browser_context(), |
| function->include_incognito_information(), |
| &source_window, nullptr, &source_index, |
| error) || |
| !source_window) { |
| return -1; |
| } |
| |
| if (!ExtensionTabUtil::IsTabStripEditable()) { |
| *error = ExtensionTabUtil::kTabStripNotEditableError; |
| return -1; |
| } |
| |
| // TODO(crbug.com/40638654): Rather than calling is_type_normal(), should |
| // this call SupportsWindowFeature(Browser::FEATURE_TABSTRIP)? |
| if (!target_browser->is_type_normal()) { |
| *error = ExtensionTabUtil::kCanOnlyMoveTabsWithinNormalWindowsError; |
| return -1; |
| } |
| |
| if (target_browser->profile() != source_window->profile()) { |
| *error = ExtensionTabUtil::kCanOnlyMoveTabsWithinSameProfileError; |
| return -1; |
| } |
| |
| TabStripModel* target_tab_strip = |
| ExtensionTabUtil::GetEditableTabStripModel(target_browser); |
| DCHECK(target_tab_strip); |
| |
| // Clamp move location to the last position. |
| // This is ">" because it can append to a new index position. |
| // -1 means set the move location to the last position. |
| int target_index = new_index; |
| if (target_index > target_tab_strip->count() || target_index < 0) { |
| target_index = target_tab_strip->count(); |
| } |
| |
| if (target_tab_strip->SupportsTabGroups()) { |
| std::optional<tab_groups::TabGroupId> next_tab_dst_group = |
| target_tab_strip->GetTabGroupForTab(target_index); |
| std::optional<tab_groups::TabGroupId> prev_tab_dst_group = |
| target_tab_strip->GetTabGroupForTab(target_index - 1); |
| |
| // Group contiguity is not respected in the target tabstrip. |
| if (next_tab_dst_group.has_value() && prev_tab_dst_group.has_value() && |
| next_tab_dst_group == prev_tab_dst_group) { |
| *error = tabs_constants::kInvalidTabIndexBreaksGroupContiguity; |
| return -1; |
| } |
| } |
| |
| Browser* source_browser = source_window->GetBrowser(); |
| if (!source_browser) { |
| *error = ExtensionTabUtil::kCanOnlyMoveTabsWithinNormalWindowsError; |
| return -1; |
| } |
| |
| std::unique_ptr<TabModel> detached_tab = |
| source_browser->tab_strip_model()->DetachTabAtForInsertion(source_index); |
| if (!detached_tab) { |
| *error = ErrorUtils::FormatErrorMessage(ExtensionTabUtil::kTabNotFoundError, |
| base::NumberToString(tab_id)); |
| return -1; |
| } |
| |
| return target_tab_strip->InsertDetachedTabAt( |
| target_index, std::move(detached_tab), AddTabTypes::ADD_NONE); |
| } |
| |
| class ScopedPinBrowserAtFront { |
| public: |
| explicit ScopedPinBrowserAtFront(Browser* browser) |
| : browser_(browser->AsWeakPtr()) { |
| old_z_order_level_ = browser_->window()->GetZOrderLevel(); |
| browser_->window()->SetZOrderLevel(ui::ZOrderLevel::kFloatingWindow); |
| } |
| |
| ~ScopedPinBrowserAtFront() { |
| if (browser_) { |
| browser_->window()->SetZOrderLevel(old_z_order_level_); |
| } |
| } |
| |
| private: |
| base::WeakPtr<Browser> browser_; |
| ui::ZOrderLevel old_z_order_level_; |
| }; |
| |
| } // namespace |
| |
| // Windows --------------------------------------------------------------------- |
| |
| ExtensionFunction::ResponseAction WindowsCreateFunction::Run() { |
| std::optional<windows::Create::Params> params = |
| windows::Create::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| std::vector<GURL> urls; |
| int tab_index = -1; |
| |
| DCHECK(extension() || source_context_type() == mojom::ContextType::kWebUi || |
| source_context_type() == mojom::ContextType::kUntrustedWebUi); |
| std::optional<windows::Create::Params::CreateData>& create_data = |
| params->create_data; |
| |
| std::optional<web_app::IsolatedWebAppUrlInfo> isolated_web_app_url_info; |
| |
| // Look for optional url. |
| if (create_data && create_data->url) { |
| std::vector<std::string> url_strings; |
| // First, get all the URLs the client wants to open. |
| if (create_data->url->as_string) { |
| url_strings.push_back(std::move(*create_data->url->as_string)); |
| } else if (create_data->url->as_strings) { |
| url_strings = std::move(*create_data->url->as_strings); |
| } |
| |
| // Second, resolve, validate and convert them to GURLs. |
| for (auto& url_string : url_strings) { |
| auto url = ExtensionTabUtil::PrepareURLForNavigation( |
| url_string, extension(), browser_context()); |
| if (!url.has_value()) { |
| return RespondNow(Error(std::move(url.error()))); |
| } |
| if (url->SchemeIs(chrome::kIsolatedAppScheme)) { |
| if (url_strings.size() > 1) { |
| return RespondNow(Error(kWindowCreateSupportsOnlySingleIwaUrlError)); |
| } |
| |
| base::expected<web_app::IsolatedWebAppUrlInfo, std::string> |
| maybe_url_info = web_app::IsolatedWebAppUrlInfo::Create(*url); |
| if (!maybe_url_info.has_value()) { |
| return RespondNow( |
| Error(base::StringPrintf(kWindowCreateCannotParseIwaUrlError, |
| maybe_url_info.error().c_str()))); |
| } |
| isolated_web_app_url_info = *maybe_url_info; |
| } |
| urls.push_back(*url); |
| } |
| } |
| |
| // Decide whether we are opening a normal window or an incognito window. |
| std::string error; |
| Profile* calling_profile = Profile::FromBrowserContext(browser_context()); |
| windows_util::IncognitoResult incognito_result = |
| windows_util::ShouldOpenIncognitoWindow( |
| calling_profile, |
| create_data && create_data->incognito |
| ? std::optional<bool>(*create_data->incognito) |
| : std::nullopt, |
| &urls, &error); |
| if (incognito_result == windows_util::IncognitoResult::kError) { |
| return RespondNow(Error(std::move(error))); |
| } |
| |
| Profile* window_profile = |
| incognito_result == windows_util::IncognitoResult::kIncognito |
| ? calling_profile->GetPrimaryOTRProfile(/*create_if_needed=*/true) |
| : calling_profile; |
| |
| // Look for optional tab id. |
| WindowController* source_window = nullptr; |
| if (create_data && create_data->tab_id) { |
| if (isolated_web_app_url_info.has_value()) { |
| return RespondNow(Error(kWindowCreateCannotUseTabIdWithIwaError)); |
| } |
| |
| // Find the tab. `tab_index` will later be used to move the tab into the |
| // created window. |
| content::WebContents* web_contents = nullptr; |
| if (!tabs_internal::GetTabById(*create_data->tab_id, calling_profile, |
| include_incognito_information(), |
| &source_window, &web_contents, &tab_index, |
| &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| if (!source_window) { |
| // The source window can be null for prerender tabs. |
| return RespondNow(Error(tabs_constants::kInvalidWindowStateError)); |
| } |
| |
| Browser* source_browser = source_window->GetBrowser(); |
| if (!source_browser) { |
| return RespondNow( |
| Error(ExtensionTabUtil::kCanOnlyMoveTabsWithinNormalWindowsError)); |
| } |
| |
| if (web_app::AppBrowserController* controller = |
| source_browser->app_controller(); |
| controller && controller->IsIsolatedWebApp()) { |
| return RespondNow(Error(kWindowCreateCannotMoveIwaTabError)); |
| } |
| |
| if (!ExtensionTabUtil::IsTabStripEditable()) { |
| return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError)); |
| } |
| |
| if (source_window->profile() != window_profile) { |
| return RespondNow( |
| Error(ExtensionTabUtil::kCanOnlyMoveTabsWithinSameProfileError)); |
| } |
| |
| if (DevToolsWindow::IsDevToolsWindow(web_contents)) { |
| return RespondNow(Error(tabs_constants::kNotAllowedForDevToolsError)); |
| } |
| } |
| |
| if (!IsValidStateForWindowsCreateFunction(base::OptionalToPtr(create_data))) { |
| return RespondNow(Error(tabs_constants::kInvalidWindowStateError)); |
| } |
| |
| Browser::Type window_type = Browser::TYPE_NORMAL; |
| |
| gfx::Rect window_bounds; |
| bool focused = true; |
| std::string extension_id; |
| |
| if (create_data) { |
| // Figure out window type before figuring out bounds so that default |
| // bounds can be set according to the window type. |
| switch (create_data->type) { |
| // TODO(stevenjb): Remove 'panel' from windows.json. |
| case windows::CreateType::kPanel: |
| case windows::CreateType::kPopup: |
| window_type = Browser::TYPE_POPUP; |
| if (isolated_web_app_url_info.has_value()) { |
| return RespondNow(Error(kInvalidWindowTypeError)); |
| } |
| if (extension()) { |
| extension_id = extension()->id(); |
| } |
| break; |
| case windows::CreateType::kNone: |
| case windows::CreateType::kNormal: |
| break; |
| default: |
| return RespondNow(Error(kInvalidWindowTypeError)); |
| } |
| |
| // Initialize default window bounds according to window type. |
| ui::mojom::WindowShowState ignored_show_state = |
| ui::mojom::WindowShowState::kDefault; |
| WindowSizer::GetBrowserWindowBoundsAndShowState( |
| gfx::Rect(), nullptr, &window_bounds, &ignored_show_state); |
| |
| // Update the window bounds based on the create parameters. |
| bool set_window_position = false; |
| bool set_window_size = false; |
| if (create_data->left) { |
| window_bounds.set_x(*create_data->left); |
| set_window_position = true; |
| } |
| if (create_data->top) { |
| window_bounds.set_y(*create_data->top); |
| set_window_position = true; |
| } |
| if (create_data->width) { |
| window_bounds.set_width(*create_data->width); |
| set_window_size = true; |
| } |
| if (create_data->height) { |
| window_bounds.set_height(*create_data->height); |
| set_window_size = true; |
| } |
| |
| // If the extension specified the window size but no position, adjust the |
| // window to fit in the display. |
| if (!set_window_position && set_window_size) { |
| const display::Display& display = |
| display::Screen::Get()->GetDisplayMatching(window_bounds); |
| window_bounds.AdjustToFit(display.bounds()); |
| } |
| |
| // Immediately fail if the window bounds don't intersect the displays. |
| if ((set_window_position || set_window_size) && |
| !tabs_internal::WindowBoundsIntersectDisplays(window_bounds)) { |
| return RespondNow(Error(tabs_constants::kInvalidWindowBoundsError)); |
| } |
| |
| if (create_data->focused) { |
| focused = *create_data->focused; |
| } |
| |
| // Record the window height and width to determine if we |
| // can set a mininimum value for them (crbug.com/1369103). |
| UMA_HISTOGRAM_COUNTS_1000("Extensions.CreateWindowWidth", |
| window_bounds.width()); |
| UMA_HISTOGRAM_COUNTS_1000("Extensions.CreateWindowHeight", |
| window_bounds.height()); |
| } |
| |
| // Create a new BrowserWindow if possible. |
| if (Browser::GetCreationStatusForProfile(window_profile) != |
| Browser::CreationStatus::kOk) { |
| return RespondNow(Error(ExtensionTabUtil::kBrowserWindowNotAllowed)); |
| } |
| Browser::CreateParams create_params(window_type, window_profile, |
| user_gesture()); |
| if (isolated_web_app_url_info.has_value()) { |
| // For Isolated Web Apps, the actual navigating-to URL will be the app's |
| // start_url to prevent deep-linking attacks, while the original URL will be |
| // accessible via window.launchQueue; for this reason the browser is marked |
| // trusted. |
| create_params = Browser::CreateParams::CreateForApp( |
| web_app::GenerateApplicationNameFromAppId( |
| isolated_web_app_url_info->app_id()), |
| /*trusted_source=*/true, window_bounds, window_profile, user_gesture()); |
| } else if (extension_id.empty()) { |
| create_params.initial_bounds = window_bounds; |
| } else { |
| // extension_id is only set for CREATE_TYPE_POPUP. |
| create_params = Browser::CreateParams::CreateForAppPopup( |
| web_app::GenerateApplicationNameFromAppId(extension_id), |
| /*trusted_source=*/false, window_bounds, window_profile, |
| user_gesture()); |
| } |
| create_params.initial_show_state = ui::mojom::WindowShowState::kNormal; |
| if (create_data && create_data->state != windows::WindowState::kNone) { |
| if (create_data->state == windows::WindowState::kLockedFullscreen && |
| !tabs_internal::ExtensionHasLockedFullscreenPermission(extension())) { |
| return RespondNow( |
| Error(tabs_internal::kMissingLockWindowFullscreenPrivatePermission)); |
| } |
| create_params.initial_show_state = |
| tabs_internal::ConvertToWindowShowState(create_data->state); |
| } |
| |
| Browser* new_window = Browser::Create(create_params); |
| if (!new_window) { |
| return RespondNow(Error(ExtensionTabUtil::kBrowserWindowNotAllowed)); |
| } |
| |
| auto create_nav_params = |
| [&](const GURL& url) -> base::expected<NavigateParams, std::string> { |
| NavigateParams navigate_params(new_window, url, ui::PAGE_TRANSITION_LINK); |
| navigate_params.disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| // Ensure that these navigations will not get 'captured' into PWA windows, |
| // as this means that `new_window` could be ignored. It may be |
| // useful/desired in the future to allow this behavior, but this may require |
| // an API change, or at least a re-write of how these navigations are called |
| // to be compatible with the navigation capturing behavior. |
| navigate_params.pwa_navigation_capturing_force_off = true; |
| |
| // Depending on the |setSelfAsOpener| option, we need to put the new |
| // contents in the same BrowsingInstance as their opener. See also |
| // https://crbug.com/713888. |
| bool set_self_as_opener = create_data->set_self_as_opener && // present? |
| *create_data->set_self_as_opener; // set to true? |
| if (set_self_as_opener) { |
| if (is_from_service_worker()) { |
| // TODO(crbug.com/40636155): Add test for this. |
| return base::unexpected( |
| "Cannot specify setSelfAsOpener Service Worker extension."); |
| } |
| if (isolated_web_app_url_info) { |
| return base::unexpected( |
| "Cannot specify setSelfAsOpener for isolated-app:// URLs."); |
| } |
| // TODO(crbug.com/40636155): Add tests for checking opener SiteInstance |
| // behavior from a SW based extension's extension frame (e.g. from popup). |
| // See ExtensionApiTest.WindowsCreate* tests for details. |
| navigate_params.initiator_origin = |
| extension() ? extension()->origin() |
| : render_frame_host()->GetLastCommittedOrigin(); |
| navigate_params.opener = render_frame_host(); |
| navigate_params.source_site_instance = |
| render_frame_host()->GetSiteInstance(); |
| } |
| |
| return navigate_params; |
| }; |
| |
| if (!isolated_web_app_url_info) { |
| for (const GURL& url : urls) { |
| ASSIGN_OR_RETURN( |
| NavigateParams navigate_params, create_nav_params(url), |
| [&](const std::string& error) { return RespondNow(Error(error)); }); |
| Navigate(&navigate_params); |
| } |
| } else { |
| CHECK_EQ(urls.size(), 1U); |
| const GURL& original_url = urls[0]; |
| |
| const webapps::AppId& iwa_id = isolated_web_app_url_info->app_id(); |
| web_app::WebAppRegistrar& registrar = |
| web_app::WebAppProvider::GetForWebApps(window_profile) |
| ->registrar_unsafe(); |
| |
| // TODO(crbug.com/424128443): create an dummy tab in the browser so that the |
| // returned window's tab count is always equal to 1 -- this will limit the |
| // extension's ability to figure out which IWAs are installed without the |
| // `tabs` permission. |
| if (registrar.IsIsolated(iwa_id)) { |
| ASSIGN_OR_RETURN( |
| NavigateParams navigate_params, |
| create_nav_params(registrar.GetAppStartUrl(iwa_id)), |
| [&](const std::string& error) { return RespondNow(Error(error)); }); |
| base::WeakPtr<content::NavigationHandle> handle = |
| Navigate(&navigate_params); |
| CHECK(handle); |
| web_app::EnqueueLaunchParams( |
| handle->GetWebContents(), iwa_id, original_url, |
| /*wait_for_navigation_to_complete=*/true, handle->NavigationStart()); |
| } |
| } |
| |
| const TabModel* tab = nullptr; |
| // Move the tab into the created window only if it's an empty popup or it's |
| // a tabbed window. |
| if (window_type == Browser::TYPE_NORMAL || urls.empty()) { |
| if (source_window && source_window->GetBrowser()) { |
| TabStripModel* source_tab_strip = |
| source_window->GetBrowser()->tab_strip_model(); |
| CHECK(!isolated_web_app_url_info.has_value()); |
| std::unique_ptr<TabModel> detached_tab = |
| source_tab_strip->DetachTabAtForInsertion(tab_index); |
| tab = detached_tab.get(); |
| TabStripModel* target_tab_strip = |
| ExtensionTabUtil::GetEditableTabStripModel(new_window); |
| if (!target_tab_strip) { |
| return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError)); |
| } |
| target_tab_strip->InsertDetachedTabAt( |
| urls.size(), std::move(detached_tab), AddTabTypes::ADD_NONE); |
| } |
| } |
| // Create a new tab if the created window is still empty. Don't create a new |
| // tab when it is intended to create an empty popup. |
| if (!tab && urls.empty() && window_type == Browser::TYPE_NORMAL) { |
| chrome::NewTab(new_window); |
| } |
| chrome::SelectNumberedTab( |
| new_window, 0, |
| TabStripUserGestureDetails( |
| TabStripUserGestureDetails::GestureType::kNone)); |
| |
| if (focused) { |
| new_window->window()->Show(); |
| } else { |
| // Show an unfocused new window. |
| BrowserList* const browser_list = BrowserList::GetInstance(); |
| Browser* last_active_browser = browser_list->GetLastActive(); |
| |
| // On some OSes the new unfocused window is shown on top by default. |
| // ScopedPinBrowserAtFront prevents the new browser from being shown above |
| // the old active browser. |
| if (last_active_browser && last_active_browser->IsActive()) { |
| ScopedPinBrowserAtFront scoper(last_active_browser); |
| new_window->window()->ShowInactive(); |
| } else { |
| new_window->window()->ShowInactive(); |
| } |
| } |
| |
| // Despite creating the window with initial_show_state() == |
| // ui::mojom::WindowShowState::kMinimized above, on Linux the window is not |
| // created as minimized. |
| // TODO(crbug.com/40254339): Remove this workaround when linux is fixed. |
| // TODO(crbug.com/40254339): Find a fix for wayland as well. |
| #if BUILDFLAG(IS_LINUX) && BUILDFLAG(IS_OZONE_X11) |
| if (new_window->initial_show_state() == |
| ui::mojom::WindowShowState::kMinimized) { |
| new_window->window()->Minimize(); |
| } |
| #endif // BUILDFLAG(IS_LINUX) && BUILDFLAG(IS_OZONE_X11) |
| |
| // Lock the window fullscreen only after the new tab has been created |
| // (otherwise the tabstrip is empty), and window()->show() has been called |
| // (otherwise that resets the locked mode for devices in tablet mode). |
| if (create_data && |
| create_data->state == windows::WindowState::kLockedFullscreen) { |
| #if BUILDFLAG(IS_CHROMEOS) |
| ash::boca::LockedQuizSessionManagerFactory::GetInstance() |
| ->GetForBrowserContext(calling_profile) |
| ->SetLockedFullscreenState(new_window, /*pinned=*/true); |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| if (new_window->profile()->IsOffTheRecord() && |
| !browser_context()->IsOffTheRecord() && |
| !include_incognito_information()) { |
| // Don't expose incognito windows if extension itself works in non-incognito |
| // profile and CanCrossIncognito isn't allowed. |
| return RespondNow(WithArguments(base::Value())); |
| } |
| |
| return RespondNow( |
| WithArguments(ExtensionTabUtil::CreateWindowValueForExtension( |
| *new_window, extension(), WindowController::kPopulateTabs, |
| source_context_type()))); |
| } |
| |
| // Tabs ------------------------------------------------------------------------ |
| |
| ExtensionFunction::ResponseAction TabsCreateFunction::Run() { |
| std::optional<tabs::Create::Params> params = |
| tabs::Create::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| return RespondNow([&] { |
| if (!ExtensionTabUtil::IsTabStripEditable()) { |
| return Error(ExtensionTabUtil::kTabStripNotEditableError); |
| } |
| |
| ExtensionTabUtil::OpenTabParams options; |
| options.window_id = params->create_properties.window_id; |
| options.opener_tab_id = params->create_properties.opener_tab_id; |
| options.active = params->create_properties.selected; |
| // The 'active' property has replaced the 'selected' property. |
| options.active = params->create_properties.active; |
| options.pinned = params->create_properties.pinned; |
| options.index = params->create_properties.index; |
| options.url = params->create_properties.url; |
| |
| auto result = ExtensionTabUtil::OpenTab(this, options, user_gesture()); |
| if (!result.has_value()) { |
| return Error(result.error()); |
| } |
| |
| #if BUILDFLAG(FULL_SAFE_BROWSING) |
| tabs_internal::NotifyExtensionTelemetry( |
| Profile::FromBrowserContext(browser_context()), extension(), |
| safe_browsing::TabsApiInfo::CREATE, |
| /*current_url=*/std::string(), options.url.value_or(std::string()), |
| js_callstack()); |
| #endif |
| |
| // Return data about the newly created tab. |
| return has_callback() ? WithArguments(std::move(*result)) : NoArguments(); |
| }()); |
| } |
| |
| ExtensionFunction::ResponseAction TabsHighlightFunction::Run() { |
| std::optional<tabs::Highlight::Params> params = |
| tabs::Highlight::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| // Get the window id from the params; default to current window if omitted. |
| int window_id = params->highlight_info.window_id.value_or( |
| extension_misc::kCurrentWindowId); |
| |
| std::string error; |
| WindowController* window_controller = |
| ExtensionTabUtil::GetControllerFromWindowID( |
| ChromeExtensionFunctionDetails(this), window_id, &error); |
| if (!window_controller) { |
| return RespondNow(Error(std::move(error))); |
| } |
| |
| // Don't let the extension update the tab if the user is dragging tabs. |
| TabStripModel* tab_strip_model = ExtensionTabUtil::GetEditableTabStripModel( |
| window_controller->GetBrowser()); |
| if (!tab_strip_model) { |
| return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError)); |
| } |
| ui::ListSelectionModel selection; |
| std::optional<size_t> active_index; |
| |
| if (params->highlight_info.tabs.as_integers) { |
| std::vector<int>& tab_indices = *params->highlight_info.tabs.as_integers; |
| // Create a new selection model as we read the list of tab indices. |
| for (int tab_index : tab_indices) { |
| if (!HighlightTab(tab_strip_model, &selection, &active_index, tab_index, |
| &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| } |
| } else { |
| EXTENSION_FUNCTION_VALIDATE(params->highlight_info.tabs.as_integer); |
| if (!HighlightTab(tab_strip_model, &selection, &active_index, |
| *params->highlight_info.tabs.as_integer, &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| } |
| |
| // Make sure they actually specified tabs to select. |
| if (selection.empty()) { |
| return RespondNow(Error(kNoHighlightedTabError)); |
| } |
| |
| // Extend selection for any split tabs. |
| for (const auto& index : selection.selected_indices()) { |
| std::optional<split_tabs::SplitTabId> split_id = |
| tab_strip_model->GetSplitForTab(index); |
| if (!split_id.has_value()) { |
| continue; |
| } |
| // All the tabs in a split should be contiguous. |
| std::vector<::tabs::TabInterface*> split_tabs = |
| tab_strip_model->GetSplitData(split_id.value())->ListTabs(); |
| size_t start = tab_strip_model->GetIndexOfTab(split_tabs[0]); |
| selection.AddIndexRangeToSelection(start, start + split_tabs.size() - 1); |
| } |
| |
| selection.set_active(active_index); |
| tab_strip_model->SetSelectionFromModel(std::move(selection)); |
| return RespondNow( |
| WithArguments(window_controller->CreateWindowValueForExtension( |
| extension(), WindowController::kPopulateTabs, |
| source_context_type()))); |
| } |
| |
| bool TabsHighlightFunction::HighlightTab(TabStripModel* tabstrip, |
| ui::ListSelectionModel* selection, |
| std::optional<size_t>* active_index, |
| int index, |
| std::string* error) { |
| // Make sure the index is in range. |
| if (!tabstrip->ContainsIndex(index)) { |
| *error = ErrorUtils::FormatErrorMessage(kTabIndexNotFoundError, |
| base::NumberToString(index)); |
| return false; |
| } |
| |
| // By default, we make the first tab in the list active. |
| if (!active_index->has_value()) { |
| *active_index = static_cast<size_t>(index); |
| } |
| |
| selection->AddIndexToSelection(index); |
| return true; |
| } |
| |
| TabsUpdateFunction::TabsUpdateFunction() : web_contents_(nullptr) {} |
| |
| ExtensionFunction::ResponseAction TabsUpdateFunction::Run() { |
| std::optional<tabs::Update::Params> params = |
| tabs::Update::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| int tab_id = -1; |
| WebContents* contents = nullptr; |
| if (!params->tab_id) { |
| WindowController* window_controller = |
| ChromeExtensionFunctionDetails(this).GetCurrentWindowController(); |
| if (!window_controller) { |
| return RespondNow(Error(ExtensionTabUtil::kNoCurrentWindowError)); |
| } |
| if (!ExtensionTabUtil::IsTabStripEditable()) { |
| return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError)); |
| } |
| contents = window_controller->GetActiveTab(); |
| if (!contents) { |
| return RespondNow(Error(tabs_constants::kNoSelectedTabError)); |
| } |
| tab_id = ExtensionTabUtil::GetTabId(contents); |
| } else { |
| tab_id = *params->tab_id; |
| } |
| |
| int tab_index = -1; |
| WindowController* window = nullptr; |
| std::string error; |
| if (!tabs_internal::GetTabById(tab_id, browser_context(), |
| include_incognito_information(), &window, |
| &contents, &tab_index, &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| |
| if (DevToolsWindow::IsDevToolsWindow(contents)) { |
| return RespondNow(Error(tabs_constants::kNotAllowedForDevToolsError)); |
| } |
| |
| // tabs_internal::GetTabById may return a null window for prerender tabs. |
| if (!window || !window->SupportsTabs()) { |
| return RespondNow(Error(ExtensionTabUtil::kNoCurrentWindowError)); |
| } |
| Browser* browser = window->GetBrowser(); |
| TabStripModel* tab_strip = browser->tab_strip_model(); |
| |
| web_contents_ = contents; |
| |
| bool active = false; |
| // TODO(rafaelw): Setting |active| from js doesn't make much sense. |
| // Move tab selection management up to window. |
| if (params->update_properties.selected) { |
| active = *params->update_properties.selected; |
| } |
| |
| // The 'active' property has replaced 'selected'. |
| if (params->update_properties.active) { |
| active = *params->update_properties.active; |
| } |
| |
| if (active) { |
| // Bug fix for crbug.com/1197888. Don't let the extension update the tab |
| // if the user is dragging tabs. |
| if (!ExtensionTabUtil::IsTabStripEditable()) { |
| return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError)); |
| } |
| |
| if (tab_strip->active_index() != tab_index) { |
| tab_strip->ActivateTabAt(tab_index); |
| DCHECK_EQ(contents, tab_strip->GetActiveWebContents()); |
| } |
| } |
| |
| if (params->update_properties.highlighted) { |
| // Bug fix for crbug.com/1197888. Don't let the extension update the tab |
| // if the user is dragging tabs. |
| if (!ExtensionTabUtil::IsTabStripEditable()) { |
| return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError)); |
| } |
| |
| if (*params->update_properties.highlighted) { |
| tab_strip->SelectTabAt(tab_index); |
| } else { |
| tab_strip->DeselectTabAt(tab_index); |
| } |
| } |
| |
| if (params->update_properties.muted && |
| !SetTabAudioMuted(contents, *params->update_properties.muted, |
| TabMutedReason::EXTENSION, extension()->id())) { |
| return RespondNow(Error(ErrorUtils::FormatErrorMessage( |
| kCannotUpdateMuteCaptured, base::NumberToString(tab_id)))); |
| } |
| |
| if (params->update_properties.opener_tab_id) { |
| int opener_id = *params->update_properties.opener_tab_id; |
| WebContents* opener_contents = nullptr; |
| if (opener_id == tab_id) { |
| return RespondNow(Error("Cannot set a tab's opener to itself.")); |
| } |
| if (!ExtensionTabUtil::GetTabById(opener_id, browser_context(), |
| include_incognito_information(), |
| &opener_contents)) { |
| return RespondNow(Error( |
| ErrorUtils::FormatErrorMessage(ExtensionTabUtil::kTabNotFoundError, |
| base::NumberToString(opener_id)))); |
| } |
| |
| // Bug fix for crbug.com/1197888. Don't let the extension update the tab |
| // if the user is dragging tabs. |
| if (!ExtensionTabUtil::IsTabStripEditable()) { |
| return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError)); |
| } |
| |
| if (tab_strip->GetIndexOfWebContents(opener_contents) == |
| TabStripModel::kNoTab) { |
| return RespondNow( |
| Error("Tab opener must be in the same window as the updated tab.")); |
| } |
| tab_strip->SetOpenerOfWebContentsAt(tab_index, opener_contents); |
| } |
| |
| if (params->update_properties.auto_discardable) { |
| bool state = *params->update_properties.auto_discardable; |
| resource_coordinator::TabLifecycleUnitExternal::FromWebContents( |
| web_contents_) |
| ->SetAutoDiscardable(state); |
| } |
| |
| if (params->update_properties.pinned) { |
| // Bug fix for crbug.com/1197888. Don't let the extension update the tab if |
| // the user is dragging tabs. |
| if (!ExtensionTabUtil::IsTabStripEditable()) { |
| return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError)); |
| } |
| |
| bool pinned = *params->update_properties.pinned; |
| tab_strip->SetTabPinned(tab_index, pinned); |
| |
| // Update the tab index because it may move when being pinned. |
| tab_index = tab_strip->GetIndexOfWebContents(contents); |
| } |
| |
| // TODO(rafaelw): handle setting remaining tab properties: |
| // -title |
| // -favIconUrl |
| |
| // Navigate the tab to a new location if the url is different. |
| if (params->update_properties.url) { |
| std::string updated_url = *params->update_properties.url; |
| if (browser->profile()->IsIncognitoProfile() && |
| !IsURLAllowedInIncognito(GURL(updated_url))) { |
| return RespondNow(Error(ErrorUtils::FormatErrorMessage( |
| tabs_constants::kURLsNotAllowedInIncognitoError, updated_url))); |
| } |
| |
| // Get last committed or pending URL. |
| std::string current_url = contents->GetVisibleURL().is_valid() |
| ? contents->GetVisibleURL().spec() |
| : std::string(); |
| |
| if (!UpdateURL(updated_url, tab_id, &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| |
| #if BUILDFLAG(FULL_SAFE_BROWSING) |
| tabs_internal::NotifyExtensionTelemetry( |
| Profile::FromBrowserContext(browser_context()), extension(), |
| safe_browsing::TabsApiInfo::UPDATE, current_url, updated_url, |
| js_callstack()); |
| #endif |
| } |
| |
| return RespondNow(GetResult()); |
| } |
| |
| bool TabsUpdateFunction::UpdateURL(const std::string& url_string, |
| int tab_id, |
| std::string* error) { |
| auto url = ExtensionTabUtil::PrepareURLForNavigation(url_string, extension(), |
| browser_context()); |
| if (!url.has_value()) { |
| *error = std::move(url.error()); |
| return false; |
| } |
| |
| NavigationController::LoadURLParams load_params(*url); |
| |
| // Treat extension-initiated navigations as renderer-initiated so that the URL |
| // does not show in the omnibox until it commits. This avoids URL spoofs |
| // since URLs can be opened on behalf of untrusted content. |
| load_params.is_renderer_initiated = true; |
| // All renderer-initiated navigations need to have an initiator origin. |
| load_params.initiator_origin = extension()->origin(); |
| // |source_site_instance| needs to be set so that a renderer process |
| // compatible with |initiator_origin| is picked by Site Isolation. |
| load_params.source_site_instance = content::SiteInstance::CreateForURL( |
| web_contents_->GetBrowserContext(), |
| load_params.initiator_origin->GetURL()); |
| |
| // Marking the navigation as initiated via an API means that the focus |
| // will stay in the omnibox - see https://crbug.com/1085779. |
| load_params.transition_type = ui::PAGE_TRANSITION_FROM_API; |
| |
| base::WeakPtr<content::NavigationHandle> navigation_handle = |
| web_contents_->GetController().LoadURLWithParams(load_params); |
| // Navigation can fail for any number of reasons at the content layer. |
| // Unfortunately, we can't provide a detailed error message here, because |
| // there are too many possible triggers. At least notify the extension that |
| // the update failed. |
| if (!navigation_handle) { |
| *error = "Navigation rejected."; |
| return false; |
| } |
| |
| DCHECK_EQ(*url, |
| web_contents_->GetController().GetPendingEntry()->GetVirtualURL()); |
| |
| return true; |
| } |
| |
| ExtensionFunction::ResponseValue TabsUpdateFunction::GetResult() { |
| if (!has_callback()) { |
| return NoArguments(); |
| } |
| |
| return ArgumentList( |
| tabs::Get::Results::Create(tabs_internal::CreateTabObjectHelper( |
| web_contents_, extension(), source_context_type(), nullptr, -1))); |
| } |
| |
| ExtensionFunction::ResponseAction TabsMoveFunction::Run() { |
| std::optional<tabs::Move::Params> params = tabs::Move::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| int new_index = params->move_properties.index; |
| const auto& window_id = params->move_properties.window_id; |
| base::Value::List tab_values; |
| |
| size_t num_tabs = 0; |
| std::string error; |
| if (params->tab_ids.as_integers) { |
| std::vector<int>& tab_ids = *params->tab_ids.as_integers; |
| num_tabs = tab_ids.size(); |
| |
| for (int tab_id : tab_ids) { |
| if (!MoveTab(tab_id, &new_index, tab_values, window_id, &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| } |
| } else { |
| EXTENSION_FUNCTION_VALIDATE(params->tab_ids.as_integer); |
| num_tabs = 1; |
| if (!MoveTab(*params->tab_ids.as_integer, &new_index, tab_values, window_id, |
| &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| } |
| |
| // TODO(devlin): It's weird that whether or not the method provides a callback |
| // can determine its success (as we return errors below). |
| if (!has_callback()) { |
| return RespondNow(NoArguments()); |
| } |
| |
| if (num_tabs == 0) { |
| return RespondNow(Error("No tabs given.")); |
| } |
| if (num_tabs == 1) { |
| CHECK_EQ(1u, tab_values.size()); |
| return RespondNow(WithArguments(std::move(tab_values[0]))); |
| } |
| |
| // Return the results as an array if there are multiple tabs. |
| return RespondNow(WithArguments(std::move(tab_values))); |
| } |
| |
| bool TabsMoveFunction::MoveTab(int tab_id, |
| int* new_index, |
| base::Value::List& tab_values, |
| const std::optional<int>& window_id, |
| std::string* error) { |
| WindowController* source_window = nullptr; |
| WebContents* contents = nullptr; |
| int tab_index = -1; |
| if (!tabs_internal::GetTabById( |
| tab_id, browser_context(), include_incognito_information(), |
| &source_window, &contents, &tab_index, error) || |
| !source_window) { |
| return false; |
| } |
| |
| if (DevToolsWindow::IsDevToolsWindow(contents)) { |
| *error = tabs_constants::kNotAllowedForDevToolsError; |
| return false; |
| } |
| |
| // Don't let the extension move the tab if the user is dragging tabs. |
| if (!ExtensionTabUtil::IsTabStripEditable()) { |
| *error = ExtensionTabUtil::kTabStripNotEditableError; |
| return false; |
| } |
| |
| if (window_id && *window_id != ExtensionTabUtil::GetWindowIdOfTab(contents)) { |
| WindowController* target_controller = |
| ExtensionTabUtil::GetControllerFromWindowID( |
| ChromeExtensionFunctionDetails(this), *window_id, error); |
| if (!target_controller) { |
| return false; |
| } |
| |
| Browser* target_browser = target_controller->GetBrowser(); |
| int inserted_index = |
| MoveTabToWindow(this, tab_id, target_browser, *new_index, error); |
| if (inserted_index < 0) { |
| return false; |
| } |
| |
| *new_index = inserted_index; |
| |
| if (has_callback()) { |
| content::WebContents* web_contents = |
| target_controller->GetWebContentsAt(inserted_index); |
| |
| tab_values.Append(tabs_internal::CreateTabObjectHelper( |
| web_contents, extension(), source_context_type(), |
| target_browser, inserted_index) |
| .ToValue()); |
| } |
| |
| // Insert the tabs one after another. |
| *new_index += 1; |
| |
| return true; |
| } |
| |
| // Perform a simple within-window move. |
| // Clamp move location to the last position. |
| // This is ">=" because the move must be to an existing location. |
| // -1 means set the move location to the last position. |
| TabStripModel* source_tab_strip = |
| source_window->GetBrowser()->tab_strip_model(); |
| if (*new_index >= source_tab_strip->count() || *new_index < 0) { |
| *new_index = source_tab_strip->count() - 1; |
| } |
| |
| if (*new_index != tab_index) { |
| *new_index = |
| source_tab_strip->MoveWebContentsAt(tab_index, *new_index, false); |
| } |
| |
| if (has_callback()) { |
| tab_values.Append(tabs_internal::CreateTabObjectHelper( |
| contents, extension(), source_context_type(), |
| source_window->GetBrowserWindowInterface(), |
| *new_index) |
| .ToValue()); |
| } |
| |
| // Insert the tabs one after another. |
| *new_index += 1; |
| |
| return true; |
| } |
| |
| ExtensionFunction::ResponseAction TabsReloadFunction::Run() { |
| std::optional<tabs::Reload::Params> params = |
| tabs::Reload::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| bool bypass_cache = false; |
| if (params->reload_properties && params->reload_properties->bypass_cache) { |
| bypass_cache = *params->reload_properties->bypass_cache; |
| } |
| |
| // If |tab_id| is specified, look for it. Otherwise default to selected tab |
| // in the current window. |
| content::WebContents* web_contents = nullptr; |
| if (!params->tab_id) { |
| if (WindowController* window_controller = |
| ChromeExtensionFunctionDetails(this).GetCurrentWindowController()) { |
| web_contents = window_controller->GetActiveTab(); |
| if (!web_contents) { |
| return RespondNow(Error(tabs_constants::kNoSelectedTabError)); |
| } |
| } else { |
| return RespondNow(Error(ExtensionTabUtil::kNoCurrentWindowError)); |
| } |
| } else { |
| int tab_id = *params->tab_id; |
| |
| std::string error; |
| if (!tabs_internal::GetTabById(tab_id, browser_context(), |
| include_incognito_information(), nullptr, |
| &web_contents, nullptr, &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| } |
| |
| web_contents->GetController().Reload( |
| bypass_cache ? content::ReloadType::BYPASSING_CACHE |
| : content::ReloadType::NORMAL, |
| true); |
| |
| return RespondNow(NoArguments()); |
| } |
| |
| ExtensionFunction::ResponseAction TabsGroupFunction::Run() { |
| std::optional<tabs::Group::Params> params = |
| tabs::Group::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| std::string error; |
| |
| // Get the target browser from the parameters. |
| int group_id = -1; |
| WindowController* target_window = nullptr; |
| tab_groups::TabGroupId group = tab_groups::TabGroupId::CreateEmpty(); |
| if (params->options.group_id) { |
| if (params->options.create_properties) { |
| return RespondNow(Error(tabs_constants::kGroupParamsError)); |
| } |
| |
| group_id = *params->options.group_id; |
| if (!ExtensionTabUtil::GetGroupById( |
| group_id, browser_context(), include_incognito_information(), |
| &target_window, &group, nullptr, &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| } else { |
| int window_id = extension_misc::kCurrentWindowId; |
| if (params->options.create_properties && |
| params->options.create_properties->window_id) { |
| window_id = *params->options.create_properties->window_id; |
| } |
| target_window = ExtensionTabUtil::GetControllerFromWindowID( |
| ChromeExtensionFunctionDetails(this), window_id, &error); |
| if (!target_window) { |
| return RespondNow(Error(std::move(error))); |
| } |
| } |
| |
| DCHECK(target_window); |
| if (!target_window->HasEditableTabStrip()) { |
| return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError)); |
| } |
| |
| // Get all tab IDs from parameters. |
| std::vector<int> tab_ids; |
| if (params->options.tab_ids.as_integers) { |
| tab_ids = *params->options.tab_ids.as_integers; |
| EXTENSION_FUNCTION_VALIDATE(!tab_ids.empty()); |
| } else { |
| EXTENSION_FUNCTION_VALIDATE(params->options.tab_ids.as_integer); |
| tab_ids.push_back(*params->options.tab_ids.as_integer); |
| } |
| |
| // Get each tab's current window. All tabs will need to be moved into the |
| // target window before grouping. |
| std::vector<WindowController*> tab_windows; |
| tab_windows.reserve(tab_ids.size()); |
| for (int tab_id : tab_ids) { |
| WindowController* tab_window = nullptr; |
| content::WebContents* web_contents = nullptr; |
| if (!tabs_internal::GetTabById(tab_id, browser_context(), |
| include_incognito_information(), &tab_window, |
| &web_contents, nullptr, &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| if (tab_window) { |
| tab_windows.push_back(tab_window); |
| } |
| |
| if (DevToolsWindow::IsDevToolsWindow(web_contents)) { |
| return RespondNow(Error(tabs_constants::kNotAllowedForDevToolsError)); |
| } |
| } |
| |
| // Move all tabs to the target browser, appending to the end each time. Only |
| // tabs that are not already in the target browser are moved. |
| for (size_t i = 0; i < tab_ids.size(); ++i) { |
| if (tab_windows[i] != target_window) { |
| if (MoveTabToWindow(this, tab_ids[i], target_window->GetBrowser(), -1, |
| &error) < 0) { |
| return RespondNow(Error(std::move(error))); |
| } |
| } |
| } |
| |
| // Get the resulting tab indices in the target browser. We recalculate these |
| // after all tabs are moved so that any callbacks are resolved and the indices |
| // are final. |
| std::vector<int> tab_indices; |
| tab_indices.reserve(tab_ids.size()); |
| for (int tab_id : tab_ids) { |
| int tab_index = -1; |
| if (!tabs_internal::GetTabById( |
| tab_id, browser_context(), include_incognito_information(), |
| /*window_out=*/nullptr, /*contents_out=*/nullptr, &tab_index, |
| &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| tab_indices.push_back(tab_index); |
| } |
| // Sort and dedupe these indices for processing in the tabstrip. |
| std::sort(tab_indices.begin(), tab_indices.end()); |
| tab_indices.erase(std::unique(tab_indices.begin(), tab_indices.end()), |
| tab_indices.end()); |
| |
| // Get the remaining group metadata and add the tabs to the group. |
| // At this point, we assume this is a valid action due to the checks above. |
| if (!ExtensionTabUtil::IsTabStripEditable()) { |
| return RespondNow(Error(ExtensionTabUtil::kTabStripNotEditableError)); |
| } |
| TabStripModel* tab_strip = target_window->GetBrowser()->tab_strip_model(); |
| if (!tab_strip->SupportsTabGroups()) { |
| return RespondNow( |
| Error(ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError)); |
| } |
| if (group.is_empty()) { |
| group = tab_strip->AddToNewGroup(tab_indices); |
| group_id = ExtensionTabUtil::GetGroupId(group); |
| } else { |
| tab_strip->AddToExistingGroup(tab_indices, group); |
| } |
| |
| DCHECK_GT(group_id, 0); |
| |
| return RespondNow(WithArguments(group_id)); |
| } |
| |
| ExtensionFunction::ResponseAction TabsUngroupFunction::Run() { |
| std::optional<tabs::Ungroup::Params> params = |
| tabs::Ungroup::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| std::vector<int> tab_ids; |
| if (params->tab_ids.as_integers) { |
| tab_ids = *params->tab_ids.as_integers; |
| EXTENSION_FUNCTION_VALIDATE(!tab_ids.empty()); |
| } else { |
| EXTENSION_FUNCTION_VALIDATE(params->tab_ids.as_integer); |
| tab_ids.push_back(*params->tab_ids.as_integer); |
| } |
| |
| std::string error; |
| for (int tab_id : tab_ids) { |
| if (!UngroupTab(tab_id, &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| } |
| |
| return RespondNow(NoArguments()); |
| } |
| |
| bool TabsUngroupFunction::UngroupTab(int tab_id, std::string* error) { |
| WindowController* window = nullptr; |
| int tab_index = -1; |
| if (!tabs_internal::GetTabById(tab_id, browser_context(), |
| include_incognito_information(), &window, |
| nullptr, &tab_index, error) || |
| !window) { |
| return false; |
| } |
| |
| if (!window->HasEditableTabStrip()) { |
| *error = ExtensionTabUtil::kTabStripNotEditableError; |
| return false; |
| } |
| |
| TabStripModel* tab_strip_model = window->GetBrowser()->tab_strip_model(); |
| if (!tab_strip_model->SupportsTabGroups()) { |
| *error = ExtensionTabUtil::kTabStripDoesNotSupportTabGroupsError; |
| return false; |
| } |
| |
| tab_strip_model->RemoveFromGroup({tab_index}); |
| |
| return true; |
| } |
| |
| ExtensionFunction::ResponseAction TabsDiscardFunction::Run() { |
| std::optional<tabs::Discard::Params> params = |
| tabs::Discard::Params::Create(args()); |
| EXTENSION_FUNCTION_VALIDATE(params); |
| |
| WebContents* contents = nullptr; |
| // If |tab_id| is given, find the web_contents respective to it. |
| // Otherwise invoke discard function in TabManager with null web_contents |
| // that will discard the least important tab. |
| if (params->tab_id) { |
| int tab_id = *params->tab_id; |
| std::string error; |
| if (!tabs_internal::GetTabById(tab_id, browser_context(), |
| include_incognito_information(), nullptr, |
| &contents, nullptr, &error)) { |
| return RespondNow(Error(std::move(error))); |
| } |
| |
| if (DevToolsWindow::IsDevToolsWindow(contents)) { |
| return RespondNow(Error(tabs_constants::kNotAllowedForDevToolsError)); |
| } |
| } |
| |
| // Discard the tab. |
| contents = |
| g_browser_process->GetTabManager()->DiscardTabByExtension(contents); |
| |
| // Create the Tab object and return it in case of success. |
| if (!contents) { |
| // Return appropriate error message otherwise. |
| return RespondNow(Error(params->tab_id |
| ? ErrorUtils::FormatErrorMessage( |
| tabs_constants::kCannotDiscardTab, |
| base::NumberToString(*params->tab_id)) |
| : kCannotFindTabToDiscard)); |
| } |
| |
| return RespondNow(ArgumentList( |
| tabs::Discard::Results::Create(tabs_internal::CreateTabObjectHelper( |
| contents, extension(), source_context_type(), nullptr, -1)))); |
| } |
| |
| TabsDiscardFunction::TabsDiscardFunction() = default; |
| TabsDiscardFunction::~TabsDiscardFunction() = default; |
| |
| } // namespace extensions |