[Extensions] (Mostly) implement action.openPopup()
Add the action.openPopup() API function, which allows an extension to
programmatically open its popup. This is similar to the
browserAction.openPopup() API, but with the following differences:
* action.openPopup() allows the extension to specify a window ID.
* browserAction.openPopup() will time out after 10 seconds;
action.openPopup() does not time out.
* browserAction.openPopup() returns a handle to the HTMLWindow of the
popup; action.openPopup() returns nothing.
Add the basic implementation for the API, as well as API tests
exercising the behavior. Since the API is still in progress, restrict
access to the "dev" channel in Chrome.
Bug: 1245093
Change-Id: I956da3da62baf6a78b27b273a3bae0f91f27cb81
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3352910
Reviewed-by: David Bertoni <dbertoni@chromium.org>
Reviewed-by: Kelvin Jiang <kelvinjiang@chromium.org>
Commit-Queue: Devlin Cronin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#959462}
diff --git a/chrome/browser/extensions/api/extension_action/extension_action_api.cc b/chrome/browser/extensions/api/extension_action/extension_action_api.cc
index 66d2bcd..888bb37 100644
--- a/chrome/browser/extensions/api/extension_action/extension_action_api.cc
+++ b/chrome/browser/extensions/api/extension_action/extension_action_api.cc
@@ -62,9 +62,62 @@
"error occurred.";
const char kInvalidColorError[] =
"The color specification could not be parsed.";
+constexpr char kNoActiveWindowFound[] =
+ "Could not find an active browser window.";
bool g_report_error_for_invisible_icon = false;
+// Returns the browser that was last active in the given `profile`, optionally
+// also checking the incognito profile.
+Browser* FindLastActiveBrowserWindow(Profile* profile,
+ bool check_incognito_profile) {
+ Browser* browser = chrome::FindLastActiveWithProfile(profile);
+
+ if (browser && browser->window()->IsActive())
+ return browser; // Found an active browser.
+
+ // It's possible that the last active browser actually corresponds to the
+ // associated incognito profile, and this won't be returned by
+ // FindLastActiveWithProfile(). If the extension can operate incognito, then
+ // check the last active incognito, too.
+ if (check_incognito_profile && profile->HasPrimaryOTRProfile()) {
+ Profile* incognito_profile =
+ profile->GetPrimaryOTRProfile(/*create_if_needed=*/false);
+ DCHECK(incognito_profile);
+ Browser* incognito_browser =
+ chrome::FindLastActiveWithProfile(incognito_profile);
+ if (incognito_browser->window()->IsActive())
+ return incognito_browser;
+ }
+
+ return nullptr;
+}
+
+// Attempts to open `extension`'s popup in the given `browser`. Returns true on
+// success; otherwise, populates `error` and returns false.
+bool OpenPopupInBrowser(Browser& browser,
+ const Extension& extension,
+ std::string* error) {
+ if (!browser.window()->IsToolbarVisible()) {
+ *error = "Browser window has no toolbar.";
+ return false;
+ }
+
+ // TODO(https://crbug.com/1245093): Modify
+ // ShowExtensionActionPopupForAPICall() to take a callback so that
+ // a) the API function doesn't have to wait and observe first load for all
+ // ExtensionHosts, and
+ // b) we catch cases like the associated window being closed before the
+ // popup complete opening.
+ if (!ExtensionActionAPI::Get(browser.profile())
+ ->ShowExtensionActionPopupForAPICall(&extension, &browser)) {
+ *error = "Failed to open popup.";
+ return false;
+ }
+
+ return true;
+}
+
} // namespace
//
@@ -576,33 +629,122 @@
return RespondNow(OneArgument(std::move(ui_settings)));
}
+ActionOpenPopupFunction::ActionOpenPopupFunction() = default;
+ActionOpenPopupFunction::~ActionOpenPopupFunction() = default;
+
+ExtensionFunction::ResponseAction ActionOpenPopupFunction::Run() {
+ // Unfortunately, the action API types aren't compiled. However, the bindings
+ // should still valid the form of the arguments.
+ EXTENSION_FUNCTION_VALIDATE(args().size() == 1u);
+ EXTENSION_FUNCTION_VALIDATE(extension());
+ const base::Value& options = args()[0];
+
+ // TODO(https://crbug.com/1245093): Support specifying the tab ID? This is
+ // kind of racy (because really what the extension probably cares about is
+ // the document ID; tab ID persists across pages, whereas document ID would
+ // detect things like navigations).
+ int window_id = extension_misc::kCurrentWindowId;
+ if (options.is_dict()) {
+ const base::Value* window_value = options.FindKey("windowId");
+ if (window_value) {
+ EXTENSION_FUNCTION_VALIDATE(window_value->is_int());
+ window_id = window_value->GetInt();
+ }
+ }
+
+ Browser* browser = nullptr;
+ Profile* profile = Profile::FromBrowserContext(browser_context());
+ std::string error;
+ if (window_id == extension_misc::kCurrentWindowId) {
+ browser =
+ FindLastActiveBrowserWindow(profile, include_incognito_information());
+ if (!browser)
+ error = kNoActiveWindowFound;
+ } else {
+ browser = ExtensionTabUtil::GetBrowserInProfileWithId(
+ profile, window_id, include_incognito_information(), &error);
+ }
+
+ if (!browser) {
+ DCHECK(!error.empty());
+ return RespondNow(Error(std::move(error)));
+ }
+
+ content::WebContents* web_contents =
+ browser->tab_strip_model()->GetActiveWebContents();
+ ExtensionAction* extension_action =
+ ExtensionActionManager::Get(browser_context())
+ ->GetExtensionAction(*extension());
+ DCHECK(extension_action);
+ int tab_id = ExtensionTabUtil::GetTabId(web_contents);
+ if (!extension_action->HasPopup(tab_id) ||
+ !extension_action->GetIsVisible(tab_id)) {
+ return RespondNow(
+ Error("Extension does not have a popup on the active tab."));
+ }
+
+ if (!OpenPopupInBrowser(*browser, *extension(), &error)) {
+ DCHECK(!error.empty());
+ return RespondNow(Error(std::move(error)));
+ }
+
+ // Even if this is for an incognito window, we want to use the profile
+ // associated with the extension function.
+ // If the extension is runs in spanning mode, then extension hosts are
+ // created with the original profile, and if it's split, then we know the api
+ // call came from the associated profile.
+ host_registry_observation_.Observe(ExtensionHostRegistry::Get(profile));
+
+ // Balanced in OnExtensionHostCompletedFirstLoad() or
+ // OnBrowserContextShutdown().
+ AddRef();
+
+ return RespondLater();
+}
+
+void ActionOpenPopupFunction::OnBrowserContextShutdown() {
+ // No point in responding at this point (the context is gone). However, we
+ // need to explicitly remove the ExtensionHostRegistry observation, since the
+ // ExtensionHostRegistry's lifetime is tied to the BrowserContext. Otherwise,
+ // this would cause a UAF when the observation is destructed as part of this
+ // instance's destruction.
+ host_registry_observation_.Reset();
+ Release(); // Balanced in Run().
+}
+
+void ActionOpenPopupFunction::OnExtensionHostCompletedFirstLoad(
+ content::BrowserContext* browser_context,
+ ExtensionHost* host) {
+ if (did_respond())
+ return;
+
+ if (host->extension_host_type() != mojom::ViewType::kExtensionPopup ||
+ host->extension()->id() != extension_->id())
+ return;
+
+ // TODO(https://crbug.com/1245093): Return the tab for which the extension
+ // popup was shown?
+ Respond(NoArguments());
+ host_registry_observation_.Reset();
+ Release(); // Balanced in Run().
+}
+
BrowserActionOpenPopupFunction::BrowserActionOpenPopupFunction() = default;
BrowserActionOpenPopupFunction::~BrowserActionOpenPopupFunction() = default;
ExtensionFunction::ResponseAction BrowserActionOpenPopupFunction::Run() {
// We only allow the popup in the active window.
Profile* profile = Profile::FromBrowserContext(browser_context());
- Browser* browser = chrome::FindLastActiveWithProfile(profile);
- // It's possible that the last active browser actually corresponds to the
- // associated incognito profile, and this won't be returned by
- // FindLastActiveWithProfile. If the browser we found isn't active and the
- // extension can operate incognito, then check the last active incognito, too.
- if ((!browser || !browser->window()->IsActive()) &&
- util::IsIncognitoEnabled(extension()->id(), profile) &&
- profile->HasPrimaryOTRProfile()) {
- browser = chrome::FindLastActiveWithProfile(
- profile->GetPrimaryOTRProfile(/*create_if_needed=*/true));
- }
+ Browser* browser =
+ FindLastActiveBrowserWindow(profile, include_incognito_information());
- // If there's no active browser, or the Toolbar isn't visible, abort.
- // Otherwise, try to open a popup in the active browser.
- // TODO(justinlin): Remove toolbar check when http://crbug.com/308645 is
- // fixed.
- if (!browser || !browser->window()->IsActive() ||
- !browser->window()->IsToolbarVisible() ||
- !ExtensionActionAPI::Get(profile)->ShowExtensionActionPopupForAPICall(
- extension_.get(), browser)) {
- return RespondNow(Error(kOpenPopupError));
+ if (!browser)
+ return RespondNow(Error(kNoActiveWindowFound));
+
+ std::string error;
+ if (!OpenPopupInBrowser(*browser, *extension(), &error)) {
+ DCHECK(!error.empty());
+ return RespondNow(Error(std::move(error)));
}
// Even if this is for an incognito window, we want to use the normal profile.
diff --git a/chrome/browser/extensions/api/extension_action/extension_action_api.h b/chrome/browser/extensions/api/extension_action/extension_action_api.h
index 2d88df4..7f3c2b6 100644
--- a/chrome/browser/extensions/api/extension_action/extension_action_api.h
+++ b/chrome/browser/extensions/api/extension_action/extension_action_api.h
@@ -356,6 +356,39 @@
~ActionGetUserSettingsFunction() override;
};
+// Note: action.openPopup() and browserAction.openPopup() have subtly different
+// implementations:
+// * action.openPopup() allows the extension to specify a window ID.
+// * browserAction.openPopup() will time out after 10 seconds;
+// action.openPopup() does not time out.
+// * browserAction.openPopup() returns a handle to the HTMLWindow of the
+// popup; action.openPopup() returns nothing.
+// Due to these differences, the implementations are distinct classes.
+class ActionOpenPopupFunction : public ExtensionFunction,
+ public ExtensionHostRegistry::Observer {
+ public:
+ DECLARE_EXTENSION_FUNCTION("action.openPopup", ACTION_OPENPOPUP)
+
+ ActionOpenPopupFunction();
+ ActionOpenPopupFunction(const ActionOpenPopupFunction&) = delete;
+ ActionOpenPopupFunction& operator=(const ActionOpenPopupFunction&) = delete;
+
+ protected:
+ // ExtensionFunction:
+ ~ActionOpenPopupFunction() override;
+ ResponseAction Run() override;
+ void OnBrowserContextShutdown() override;
+
+ // ExtensionHostRegistry::Observer:
+ void OnExtensionHostCompletedFirstLoad(
+ content::BrowserContext* browser_context,
+ ExtensionHost* host) override;
+
+ base::ScopedObservation<ExtensionHostRegistry,
+ ExtensionHostRegistry::Observer>
+ host_registry_observation_{this};
+};
+
//
// browserAction.* aliases for supported browserAction APIs.
//
@@ -456,6 +489,9 @@
~BrowserActionDisableFunction() override {}
};
+// Note: action.openPopup() and browserAction.openPopup() have subtly different
+// implementations. See ActionOpenPopupFunction above.
+// TODO(devlin): Remove browserAction.openPopup().
class BrowserActionOpenPopupFunction : public ExtensionFunction,
public ExtensionHostRegistry::Observer {
public:
diff --git a/chrome/browser/extensions/api/extension_action/extension_action_api_interactive_uitest.cc b/chrome/browser/extensions/api/extension_action/extension_action_api_interactive_uitest.cc
new file mode 100644
index 0000000..30ccd1a
--- /dev/null
+++ b/chrome/browser/extensions/api/extension_action/extension_action_api_interactive_uitest.cc
@@ -0,0 +1,182 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/strings/stringprintf.h"
+#include "base/test/bind.h"
+#include "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/browser/extensions/extension_tab_util.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/extensions/extension_action_test_helper.h"
+#include "chrome/test/base/interactive_test_utils.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/version_info/channel.h"
+#include "content/public/test/browser_test.h"
+#include "extensions/browser/browsertest_util.h"
+#include "extensions/browser/extension_host_registry.h"
+#include "extensions/common/features/feature_channel.h"
+#include "extensions/test/result_catcher.h"
+#include "extensions/test/test_extension_dir.h"
+
+namespace extensions {
+
+// An interactive UI test suite for the chrome.action API. This is used for
+// tests where things like focus and window activation are important.
+class ActionAPIInteractiveUITest : public ExtensionApiTest {
+ public:
+ ActionAPIInteractiveUITest() = default;
+ ~ActionAPIInteractiveUITest() override = default;
+
+ // Loads a common "stub" extension with an action specified in its manifest;
+ // tests then execute script in this extension's context.
+ // This allows us to more seamlessly integrate C++ and JS execution by
+ // inlining the JS here in this file, rather than needing to separate it out
+ // into separate test files and coordinate messaging back-and-forth.
+ const Extension* LoadStubExtension() {
+ return LoadExtension(
+ test_data_dir_.AppendASCII("extension_action/stub_action"));
+ }
+
+ // Runs the given `script` in the background service worker context for the
+ // `extension`, and waits for a corresponding test success notification.
+ void RunScriptTest(const std::string& script, const Extension& extension) {
+ ResultCatcher result_catcher;
+ base::RunLoop run_loop;
+ auto callback = [&run_loop](base::Value value) { run_loop.Quit(); };
+ browsertest_util::ExecuteScriptInServiceWorker(
+ profile(), extension.id(), script,
+ base::BindLambdaForTesting(callback));
+ run_loop.Run();
+ EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
+ }
+
+ // Same as `RunScriptTest()`, but wraps `script` in a test.runTests() call
+ // with a single function.
+ void WrapAndRunScript(const std::string& script, const Extension& extension) {
+ constexpr char kTemplate[] =
+ R"(chrome.test.runTests([
+ async function openPopupTest() {
+ %s
+ }
+ ]);)";
+ std::string wrapped_script = base::StringPrintf(kTemplate, script.c_str());
+ RunScriptTest(wrapped_script, extension);
+ }
+
+ // Returns the active popup for the given `extension`, if one exists.
+ ExtensionHost* GetPopup(const Extension& extension) {
+ ExtensionHostRegistry* registry = ExtensionHostRegistry::Get(profile());
+ std::vector<ExtensionHost*> hosts =
+ registry->GetHostsForExtension(extension.id());
+ ExtensionHost* found_host = nullptr;
+ for (auto* host : hosts) {
+ if (host->extension_host_type() == mojom::ViewType::kExtensionPopup) {
+ if (found_host) {
+ ADD_FAILURE() << "Multiple popups found!";
+ return nullptr;
+ }
+ found_host = host;
+ }
+ }
+ return found_host;
+ }
+
+ // Returns true if the given `browser` has an active popup.
+ bool BrowserHasPopup(Browser* browser) {
+ return ExtensionActionTestHelper::Create(browser)->HasPopup();
+ }
+
+ // The action.openPopup() function is currently scoped to dev channel.
+ ScopedCurrentChannel scoped_current_channel_{version_info::Channel::DEV};
+};
+
+// Tests displaying a popup in the active window when no window ID is specified.
+IN_PROC_BROWSER_TEST_F(ActionAPIInteractiveUITest, OpenPopupInActiveWindow) {
+ const Extension* extension = LoadStubExtension();
+ ASSERT_TRUE(extension);
+
+ constexpr char kScript[] =
+ R"(await chrome.action.openPopup();
+ chrome.test.succeed();)";
+ WrapAndRunScript(kScript, *extension);
+
+ EXPECT_TRUE(BrowserHasPopup(browser()));
+ ExtensionHost* host = GetPopup(*extension);
+ ASSERT_TRUE(host);
+ EXPECT_TRUE(host->has_loaded_once());
+ EXPECT_EQ(extension->GetResourceURL("popup.html"),
+ host->main_frame_host()->GetLastCommittedURL());
+}
+
+// Tests displaying a popup in a window specified in the API call.
+IN_PROC_BROWSER_TEST_F(ActionAPIInteractiveUITest, OpenPopupInSpecifiedWindow) {
+ const Extension* extension = LoadStubExtension();
+ ASSERT_TRUE(extension);
+
+ Browser* second_browser = CreateBrowser(profile());
+ ASSERT_TRUE(second_browser);
+ ui_test_utils::BrowserActivationWaiter(second_browser).WaitForActivation();
+
+ // TODO(https://crbug.com/1245093): We should allow extensions to open a
+ // popup in an inactive window. Currently, this fails, so try to open the
+ // popup in the active window (but with a specified ID).
+ EXPECT_FALSE(browser()->window()->IsActive());
+ EXPECT_TRUE(second_browser->window()->IsActive());
+
+ int window_id = ExtensionTabUtil::GetWindowId(second_browser);
+
+ constexpr char kScript[] =
+ R"(await chrome.action.openPopup({windowId: %d});
+ chrome.test.succeed();)";
+ WrapAndRunScript(base::StringPrintf(kScript, window_id), *extension);
+
+ // The popup should be shown on the second browser.
+ {
+ EXPECT_TRUE(BrowserHasPopup(second_browser));
+ ExtensionHost* host = GetPopup(*extension);
+ ASSERT_TRUE(host);
+ EXPECT_TRUE(host->has_loaded_once());
+ EXPECT_EQ(extension->GetResourceURL("popup.html"),
+ host->main_frame_host()->GetLastCommittedURL());
+ }
+
+ EXPECT_FALSE(BrowserHasPopup(browser()));
+}
+
+// Tests a series of action.openPopup() invocations that are expected to fail.
+IN_PROC_BROWSER_TEST_F(ActionAPIInteractiveUITest, OpenPopupFailures) {
+ const Extension* extension = LoadStubExtension();
+ ASSERT_TRUE(extension);
+
+ constexpr char kScript[] =
+ R"(chrome.test.runTests([
+ async function openPopupFailsWithFakeWindow() {
+ const fakeWindowId = 99999;
+ await chrome.test.assertPromiseRejects(
+ chrome.action.openPopup({windowId: fakeWindowId}),
+ `Error: No window with id: ${fakeWindowId}.`);
+ chrome.test.succeed();
+ },
+ async function openPopupFailsWhenNoPopupSpecified() {
+ // Specifying an empty string for the popup means "no popup".
+ await chrome.action.setPopup({popup: ''});
+ await chrome.test.assertPromiseRejects(
+ chrome.action.openPopup(),
+ 'Error: Extension does not have a popup on the active tab.');
+ chrome.test.succeed();
+ },
+ async function openPopupFailsWhenPopupIsDisabled() {
+ await chrome.action.setPopup({popup: 'popup.html'});
+ await chrome.action.disable();
+ await chrome.test.assertPromiseRejects(
+ chrome.action.openPopup(),
+ 'Error: Extension does not have a popup on the active tab.');
+ chrome.test.succeed();
+ },
+ ]);)";
+ RunScriptTest(kScript, *extension);
+ EXPECT_FALSE(BrowserHasPopup(browser()));
+}
+
+} // namespace extensions
diff --git a/chrome/browser/extensions/extension_tab_util.cc b/chrome/browser/extensions/extension_tab_util.cc
index d8b78c0..1f8d710 100644
--- a/chrome/browser/extensions/extension_tab_util.cc
+++ b/chrome/browser/extensions/extension_tab_util.cc
@@ -68,32 +68,6 @@
namespace {
-// |error_message| can optionally be passed in and will be set with an
-// appropriate message if the window cannot be found by id.
-Browser* GetBrowserInProfileWithId(Profile* profile,
- const int window_id,
- bool match_incognito_profile,
- std::string* error_message) {
- Profile* incognito_profile =
- match_incognito_profile
- ? profile->GetPrimaryOTRProfile(/*create_if_needed=*/false)
- : nullptr;
- for (auto* browser : *BrowserList::GetInstance()) {
- if ((browser->profile() == profile ||
- browser->profile() == incognito_profile) &&
- ExtensionTabUtil::GetWindowId(browser) == window_id &&
- browser->window()) {
- return browser;
- }
- }
-
- if (error_message)
- *error_message = ErrorUtils::FormatErrorMessage(
- tabs_constants::kWindowNotFoundError, base::NumberToString(window_id));
-
- return nullptr;
-}
-
Browser* CreateBrowser(Profile* profile, bool user_gesture) {
if (Browser::GetCreationStatusForProfile(profile) !=
Browser::CreationStatus::kOk) {
@@ -371,6 +345,32 @@
}
}
+Browser* ExtensionTabUtil::GetBrowserInProfileWithId(
+ Profile* profile,
+ int window_id,
+ bool also_match_incognito_profile,
+ std::string* error_message) {
+ Profile* incognito_profile =
+ also_match_incognito_profile
+ ? profile->GetPrimaryOTRProfile(/*create_if_needed=*/false)
+ : nullptr;
+ for (auto* browser : *BrowserList::GetInstance()) {
+ if ((browser->profile() == profile ||
+ browser->profile() == incognito_profile) &&
+ ExtensionTabUtil::GetWindowId(browser) == window_id &&
+ browser->window()) {
+ return browser;
+ }
+ }
+
+ if (error_message) {
+ *error_message = ErrorUtils::FormatErrorMessage(
+ tabs_constants::kWindowNotFoundError, base::NumberToString(window_id));
+ }
+
+ return nullptr;
+}
+
int ExtensionTabUtil::GetWindowId(const Browser* browser) {
return browser->session_id().id();
}
diff --git a/chrome/browser/extensions/extension_tab_util.h b/chrome/browser/extensions/extension_tab_util.h
index 409177b..0b196e5 100644
--- a/chrome/browser/extensions/extension_tab_util.h
+++ b/chrome/browser/extensions/extension_tab_util.h
@@ -16,9 +16,10 @@
class Browser;
class ChromeExtensionFunctionDetails;
-class GURL;
-class TabStripModel;
class ExtensionFunction;
+class GURL;
+class Profile;
+class TabStripModel;
namespace base {
class DictionaryValue;
@@ -102,6 +103,15 @@
int window_id,
std::string* error_message);
+ // Returns the Browser with the specified `window id` and the associated
+ // `profile`. Optionally, this will also look at browsers associated with the
+ // incognito version of `profile` if `also_match_incognito_profile` is true.
+ // Populates `error_message` if no matching browser is found.
+ static Browser* GetBrowserInProfileWithId(Profile* profile,
+ int window_id,
+ bool also_match_incognito_profile,
+ std::string* error_message);
+
// Returns the tabs:: API constant for the window type of the |browser|.
static std::string GetBrowserWindowTypeText(const Browser& browser);
diff --git a/chrome/common/extensions/api/_api_features.json b/chrome/common/extensions/api/_api_features.json
index 1c1f4e1..0dc8031 100644
--- a/chrome/common/extensions/api/_api_features.json
+++ b/chrome/common/extensions/api/_api_features.json
@@ -61,6 +61,10 @@
"dependencies": ["manifest:action"],
"contexts": ["blessed_extension"]
},
+ "action.openPopup": {
+ // TODO(https://crbug.com/1245093): Upgrade to stable channel.
+ "channel": "dev"
+ },
"activityLogPrivate": [{
"dependencies": ["permission:activityLogPrivate"],
"contexts": ["blessed_extension"]
diff --git a/chrome/common/extensions/api/action.json b/chrome/common/extensions/api/action.json
index 399c9cb..521f12a 100644
--- a/chrome/common/extensions/api/action.json
+++ b/chrome/common/extensions/api/action.json
@@ -27,6 +27,17 @@
}
},
"description": "The collection of user-specified settings relating to an extension's action."
+ },
+ {
+ "id": "OpenPopupOptions",
+ "type": "object",
+ "properties": {
+ "windowId": {
+ "type": "integer",
+ "description": "The id of the window to open the action popup in. Defaults to the currently-active window if unspecified.",
+ "optional": true
+ }
+ }
}
],
"functions": [{
@@ -281,6 +292,20 @@
"$ref": "UserSettings"
}]
}
+ }, {
+ "name": "openPopup",
+ "type": "function",
+ "description": "Opens the extension's popup.",
+ "parameters": [{
+ "$ref": "OpenPopupOptions",
+ "name": "options",
+ "optional": true,
+ "description": "Specifies options for opening the popup."
+ }],
+ "returns_async": {
+ "name": "callback",
+ "parameters": []
+ }
}],
"events": [{
"name": "onClicked",
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index fb30868..441c2f3 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -8779,6 +8779,8 @@
# chrome_extensions_interactive_uitests target for more.
deps += [ "//extensions:chrome_extensions_interactive_uitests" ]
+ sources += [ "../browser/extensions/api/extension_action/extension_action_api_interactive_uitest.cc" ]
+
if (include_js_tests) {
sources += [
"../browser/ui/webui/extensions/extension_settings_browsertest.cc",
diff --git a/chrome/test/data/extensions/api_test/extension_action/stub_action/manifest.json b/chrome/test/data/extensions/api_test/extension_action/stub_action/manifest.json
new file mode 100644
index 0000000..7f8dbb1
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/extension_action/stub_action/manifest.json
@@ -0,0 +1,9 @@
+{
+ "name": "Stub Action Extension",
+ "manifest_version": 3,
+ "version": "0.1",
+ "background": {"service_worker": "worker.js"},
+ "action": {
+ "default_popup": "popup.html"
+ }
+}
diff --git a/chrome/test/data/extensions/api_test/extension_action/stub_action/popup.html b/chrome/test/data/extensions/api_test/extension_action/stub_action/popup.html
new file mode 100644
index 0000000..aee8bf7
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/extension_action/stub_action/popup.html
@@ -0,0 +1,8 @@
+<!-- Copyright 2021 The Chromium Authors. All rights reserved.
+ Use of this source code is governed by a BSD-style license that can be
+ found in the LICENSE file. -->
+
+<!doctype html>
+<html>
+ Hello, World!
+</html>
diff --git a/chrome/test/data/extensions/api_test/extension_action/stub_action/worker.js b/chrome/test/data/extensions/api_test/extension_action/stub_action/worker.js
new file mode 100644
index 0000000..4b97d70
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/extension_action/stub_action/worker.js
@@ -0,0 +1,5 @@
+// Copyright 2021 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+chrome.test.sendMessage('ready');
diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h
index 2a5357c..6e276e1 100644
--- a/extensions/browser/extension_function_histogram_value.h
+++ b/extensions/browser/extension_function_histogram_value.h
@@ -1673,6 +1673,7 @@
AUTOTESTPRIVATE_COULDALLOWCROSTINI = 1610,
WEB_AUTHENTICATION_PROXY_COMPLETE_CREATE_REQUEST = 1611,
DEVELOPERPRIVATE_GETUSERSITESETTINGS = 1612,
+ ACTION_OPENPOPUP = 1613,
// Last entry: Add new entries above, then run:
// python tools/metrics/histograms/update_extension_histograms.py
ENUM_BOUNDARY
diff --git a/extensions/browser/extension_host_registry.cc b/extensions/browser/extension_host_registry.cc
index 8a3a7f9..32b1dbc8 100644
--- a/extensions/browser/extension_host_registry.cc
+++ b/extensions/browser/extension_host_registry.cc
@@ -150,6 +150,16 @@
}
}
+std::vector<ExtensionHost*> ExtensionHostRegistry::GetHostsForExtension(
+ const ExtensionId& extension_id) {
+ std::vector<ExtensionHost*> hosts;
+ for (ExtensionHost* host : extension_hosts_) {
+ if (host->extension_id() == extension_id)
+ hosts.push_back(host);
+ }
+ return hosts;
+}
+
void ExtensionHostRegistry::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
diff --git a/extensions/browser/extension_host_registry.h b/extensions/browser/extension_host_registry.h
index 2a10a6ab..9d66bbc 100644
--- a/extensions/browser/extension_host_registry.h
+++ b/extensions/browser/extension_host_registry.h
@@ -6,9 +6,11 @@
#define EXTENSIONS_BROWSER_EXTENSION_HOST_REGISTRY_H_
#include <unordered_set>
+#include <vector>
#include "base/observer_list.h"
#include "components/keyed_service/core/keyed_service.h"
+#include "extensions/common/extension_id.h"
class BrowserContextKeyedServiceFactory;
@@ -110,6 +112,13 @@
// notifies observers.
void ExtensionHostDestroyed(ExtensionHost* extension_host);
+ // Returns the collection of ExtensionHosts associated with the specified
+ // `extension_id`.
+ // If performance ever becomes a consideration here, we can update the
+ // storage in the registry to be an unordered_map split apart by extension.
+ std::vector<ExtensionHost*> GetHostsForExtension(
+ const ExtensionId& extension_id);
+
void AddObserver(Observer* observer);
void RemoveObserver(Observer* observer);
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 8ddec8f..60030fe 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -31234,6 +31234,7 @@
<int value="1610" label="AUTOTESTPRIVATE_COULDALLOWCROSTINI"/>
<int value="1611" label="WEB_AUTHENTICATION_PROXY_COMPLETE_CREATE_REQUEST"/>
<int value="1612" label="DEVELOPERPRIVATE_GETUSERSITESETTINGS"/>
+ <int value="1613" label="ACTION_OPENPOPUP"/>
</enum>
<enum name="ExtensionIconState">