feat: add support for chrome.tabs.query
(#39330)
* feat: add support for tabs.query * fix: scope to webContents in current session * test: add test for session behavior
This commit is contained in:
parent
0425454687
commit
d9329042e2
9 changed files with 336 additions and 2 deletions
|
@ -91,8 +91,9 @@ The following events of `chrome.runtime` are supported:
|
|||
|
||||
### `chrome.storage`
|
||||
|
||||
Only `chrome.storage.local` is supported; `chrome.storage.sync` and
|
||||
`chrome.storage.managed` are not.
|
||||
The following methods of `chrome.storage` are supported:
|
||||
|
||||
- `chrome.storage.local`
|
||||
|
||||
### `chrome.tabs`
|
||||
|
||||
|
@ -101,6 +102,8 @@ The following methods of `chrome.tabs` are supported:
|
|||
- `chrome.tabs.sendMessage`
|
||||
- `chrome.tabs.reload`
|
||||
- `chrome.tabs.executeScript`
|
||||
- `chrome.tabs.query` (partial support)
|
||||
- supported properties: `url`, `title`, `audible`, `active`, `muted`.
|
||||
- `chrome.tabs.update` (partial support)
|
||||
- supported properties: `url`, `muted`.
|
||||
|
||||
|
@ -117,6 +120,9 @@ The following methods of `chrome.management` are supported:
|
|||
- `chrome.management.getSelf`
|
||||
- `chrome.management.getPermissionWarningsById`
|
||||
- `chrome.management.getPermissionWarningsByManifest`
|
||||
|
||||
The following events of `chrome.management` are supported:
|
||||
|
||||
- `chrome.management.onEnabled`
|
||||
- `chrome.management.onDisabled`
|
||||
|
||||
|
|
|
@ -4405,6 +4405,16 @@ WebContents* WebContents::FromID(int32_t id) {
|
|||
return GetAllWebContents().Lookup(id);
|
||||
}
|
||||
|
||||
// static
|
||||
std::list<WebContents*> WebContents::GetWebContentsList() {
|
||||
std::list<WebContents*> list;
|
||||
for (auto iter = base::IDMap<WebContents*>::iterator(&GetAllWebContents());
|
||||
!iter.IsAtEnd(); iter.Advance()) {
|
||||
list.push_back(iter.GetCurrentValue());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
// static
|
||||
gin::WrapperInfo WebContents::kWrapperInfo = {gin::kEmbedderNativeGin};
|
||||
|
||||
|
|
|
@ -134,6 +134,7 @@ class WebContents : public ExclusiveAccessContext,
|
|||
// if there is no associated wrapper.
|
||||
static WebContents* From(content::WebContents* web_contents);
|
||||
static WebContents* FromID(int32_t id);
|
||||
static std::list<WebContents*> GetWebContentsList();
|
||||
|
||||
// Get the V8 wrapper of the |web_contents|, or create one if not existed.
|
||||
//
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "base/strings/pattern.h"
|
||||
#include "chrome/common/url_constants.h"
|
||||
#include "components/url_formatter/url_fixer.h"
|
||||
#include "content/public/browser/navigation_entry.h"
|
||||
|
@ -16,7 +17,9 @@
|
|||
#include "extensions/common/mojom/host_id.mojom.h"
|
||||
#include "extensions/common/permissions/permissions_data.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
#include "shell/browser/native_window.h"
|
||||
#include "shell/browser/web_contents_zoom_controller.h"
|
||||
#include "shell/browser/window_list.h"
|
||||
#include "shell/common/extensions/api/tabs.h"
|
||||
#include "third_party/blink/public/common/page/page_zoom.h"
|
||||
#include "url/gurl.h"
|
||||
|
@ -58,6 +61,13 @@ void ZoomModeToZoomSettings(WebContentsZoomController::ZoomMode zoom_mode,
|
|||
}
|
||||
}
|
||||
|
||||
// Returns true if either |boolean| is disengaged, or if |boolean| and
|
||||
// |value| are equal. This function is used to check if a tab's parameters match
|
||||
// those of the browser.
|
||||
bool MatchesBool(const absl::optional<bool>& boolean, bool value) {
|
||||
return !boolean || *boolean == value;
|
||||
}
|
||||
|
||||
api::tabs::MutedInfo CreateMutedInfo(content::WebContents* contents) {
|
||||
DCHECK(contents);
|
||||
api::tabs::MutedInfo info;
|
||||
|
@ -65,6 +75,7 @@ api::tabs::MutedInfo CreateMutedInfo(content::WebContents* contents) {
|
|||
info.reason = api::tabs::MUTED_INFO_REASON_USER;
|
||||
return info;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ExecuteCodeInTabFunction::ExecuteCodeInTabFunction() : execute_tab_id_(-1) {}
|
||||
|
@ -214,6 +225,93 @@ ExtensionFunction::ResponseAction TabsReloadFunction::Run() {
|
|||
return RespondNow(NoArguments());
|
||||
}
|
||||
|
||||
ExtensionFunction::ResponseAction TabsQueryFunction::Run() {
|
||||
absl::optional<tabs::Query::Params> params =
|
||||
tabs::Query::Params::Create(args());
|
||||
EXTENSION_FUNCTION_VALIDATE(params);
|
||||
|
||||
URLPatternSet url_patterns;
|
||||
if (params->query_info.url) {
|
||||
std::vector<std::string> url_pattern_strings;
|
||||
if (params->query_info.url->as_string)
|
||||
url_pattern_strings.push_back(*params->query_info.url->as_string);
|
||||
else if (params->query_info.url->as_strings)
|
||||
url_pattern_strings.swap(*params->query_info.url->as_strings);
|
||||
// It is o.k. to use URLPattern::SCHEME_ALL here because this function does
|
||||
// not grant access to the content of the tabs, only to seeing their URLs
|
||||
// and meta data.
|
||||
std::string error;
|
||||
if (!url_patterns.Populate(url_pattern_strings, URLPattern::SCHEME_ALL,
|
||||
true, &error)) {
|
||||
return RespondNow(Error(std::move(error)));
|
||||
}
|
||||
}
|
||||
|
||||
std::string title = params->query_info.title.value_or(std::string());
|
||||
absl::optional<bool> audible = params->query_info.audible;
|
||||
absl::optional<bool> muted = params->query_info.muted;
|
||||
|
||||
base::Value::List result;
|
||||
|
||||
// Filter out webContents that don't belong to the current browser context.
|
||||
auto* bc = browser_context();
|
||||
auto all_contents = electron::api::WebContents::GetWebContentsList();
|
||||
all_contents.remove_if([&bc](electron::api::WebContents* wc) {
|
||||
return (bc != wc->web_contents()->GetBrowserContext());
|
||||
});
|
||||
|
||||
for (auto* contents : all_contents) {
|
||||
if (!contents || !contents->web_contents())
|
||||
continue;
|
||||
|
||||
auto* wc = contents->web_contents();
|
||||
|
||||
// Match webContents audible value.
|
||||
if (!MatchesBool(audible, wc->IsCurrentlyAudible()))
|
||||
continue;
|
||||
|
||||
// Match webContents muted value.
|
||||
if (!MatchesBool(muted, wc->IsAudioMuted()))
|
||||
continue;
|
||||
|
||||
// Match webContents active status.
|
||||
if (!MatchesBool(params->query_info.active, contents->IsFocused()))
|
||||
continue;
|
||||
|
||||
if (!title.empty() || !url_patterns.is_empty()) {
|
||||
// "title" and "url" properties are considered privileged data and can
|
||||
// only be checked if the extension has the "tabs" permission or it has
|
||||
// access to the WebContents's origin. Otherwise, this tab is considered
|
||||
// not matched.
|
||||
if (!extension()->permissions_data()->HasAPIPermissionForTab(
|
||||
contents->ID(), mojom::APIPermissionID::kTab) &&
|
||||
!extension()->permissions_data()->HasHostPermission(wc->GetURL())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Match webContents title.
|
||||
if (!title.empty() &&
|
||||
!base::MatchPattern(wc->GetTitle(), base::UTF8ToUTF16(title)))
|
||||
continue;
|
||||
|
||||
// Match webContents url.
|
||||
if (!url_patterns.is_empty() && !url_patterns.MatchesURL(wc->GetURL()))
|
||||
continue;
|
||||
}
|
||||
|
||||
tabs::Tab tab;
|
||||
tab.id = contents->ID();
|
||||
tab.url = wc->GetLastCommittedURL().spec();
|
||||
tab.active = contents->IsFocused();
|
||||
tab.audible = contents->IsCurrentlyAudible();
|
||||
tab.muted_info = CreateMutedInfo(wc);
|
||||
|
||||
result.Append(tab.ToValue());
|
||||
}
|
||||
|
||||
return RespondNow(WithArguments(std::move(result)));
|
||||
}
|
||||
|
||||
ExtensionFunction::ResponseAction TabsGetFunction::Run() {
|
||||
absl::optional<tabs::Get::Params> params = tabs::Get::Params::Create(args());
|
||||
EXTENSION_FUNCTION_VALIDATE(params);
|
||||
|
|
|
@ -55,6 +55,14 @@ class TabsReloadFunction : public ExtensionFunction {
|
|||
DECLARE_EXTENSION_FUNCTION("tabs.reload", TABS_RELOAD)
|
||||
};
|
||||
|
||||
class TabsQueryFunction : public ExtensionFunction {
|
||||
~TabsQueryFunction() override {}
|
||||
|
||||
ResponseAction Run() override;
|
||||
|
||||
DECLARE_EXTENSION_FUNCTION("tabs.query", TABS_QUERY)
|
||||
};
|
||||
|
||||
class TabsGetFunction : public ExtensionFunction {
|
||||
~TabsGetFunction() override {}
|
||||
|
||||
|
|
|
@ -3,6 +3,16 @@
|
|||
"namespace": "tabs",
|
||||
"description": "Use the <code>chrome.tabs</code> API to interact with the browser's tab system. You can use this API to create, modify, and rearrange tabs in the browser.",
|
||||
"types": [
|
||||
{
|
||||
"id": "TabStatus",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"unloaded",
|
||||
"loading",
|
||||
"complete"
|
||||
],
|
||||
"description": "The tab's loading status."
|
||||
},
|
||||
{
|
||||
"id": "MutedInfoReason",
|
||||
"type": "string",
|
||||
|
@ -210,6 +220,18 @@
|
|||
"description": "Used to return the default zoom level for the current tab in calls to tabs.getZoomSettings."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "WindowType",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"normal",
|
||||
"popup",
|
||||
"panel",
|
||||
"app",
|
||||
"devtools"
|
||||
],
|
||||
"description": "The type of window."
|
||||
}
|
||||
],
|
||||
"functions": [
|
||||
|
@ -489,6 +511,124 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "query",
|
||||
"type": "function",
|
||||
"description": "Gets all tabs that have the specified properties, or all tabs if no properties are specified.",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "object",
|
||||
"name": "queryInfo",
|
||||
"properties": {
|
||||
"active": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "Whether the tabs are active in their windows."
|
||||
},
|
||||
"pinned": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "Whether the tabs are pinned."
|
||||
},
|
||||
"audible": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "Whether the tabs are audible."
|
||||
},
|
||||
"muted": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "Whether the tabs are muted."
|
||||
},
|
||||
"highlighted": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "Whether the tabs are highlighted."
|
||||
},
|
||||
"discarded": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "Whether the tabs are discarded. A discarded tab is one whose content has been unloaded from memory, but is still visible in the tab strip. Its content is reloaded the next time it is activated."
|
||||
},
|
||||
"autoDiscardable": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "Whether the tabs can be discarded automatically by the browser when resources are low."
|
||||
},
|
||||
"currentWindow": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "Whether the tabs are in the <a href='windows#current-window'>current window</a>."
|
||||
},
|
||||
"lastFocusedWindow": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"description": "Whether the tabs are in the last focused window."
|
||||
},
|
||||
"status": {
|
||||
"$ref": "TabStatus",
|
||||
"optional": true,
|
||||
"description": "The tab loading status."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"optional": true,
|
||||
"description": "Match page titles against a pattern. This property is ignored if the extension does not have the <code>\"tabs\"</code> permission."
|
||||
},
|
||||
"url": {
|
||||
"choices": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"optional": true,
|
||||
"description": "Match tabs against one or more <a href='match_patterns'>URL patterns</a>. Fragment identifiers are not matched. This property is ignored if the extension does not have the <code>\"tabs\"</code> permission."
|
||||
},
|
||||
"groupId": {
|
||||
"type": "integer",
|
||||
"optional": true,
|
||||
"minimum": -1,
|
||||
"description": "The ID of the group that the tabs are in, or $(ref:tabGroups.TAB_GROUP_ID_NONE) for ungrouped tabs."
|
||||
},
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"optional": true,
|
||||
"minimum": -2,
|
||||
"description": "The ID of the parent window, or $(ref:windows.WINDOW_ID_CURRENT) for the <a href='windows#current-window'>current window</a>."
|
||||
},
|
||||
"windowType": {
|
||||
"$ref": "WindowType",
|
||||
"optional": true,
|
||||
"description": "The type of window the tabs are in."
|
||||
},
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"optional": true,
|
||||
"minimum": 0,
|
||||
"description": "The position of the tabs within their windows."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"returns_async": {
|
||||
"name": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "result",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Tab"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "update",
|
||||
"type": "function",
|
||||
|
|
|
@ -967,6 +967,66 @@ describe('chrome extensions', () => {
|
|||
reason: 'user'
|
||||
});
|
||||
});
|
||||
|
||||
describe('query', () => {
|
||||
it('can query for a tab with specific properties', async () => {
|
||||
await w.loadURL(url);
|
||||
|
||||
expect(w.webContents.isAudioMuted()).to.be.false('muted');
|
||||
w.webContents.setAudioMuted(true);
|
||||
expect(w.webContents.isAudioMuted()).to.be.true('not muted');
|
||||
|
||||
const message = { method: 'query', args: [{ muted: true }] };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [, , responseString] = await once(w.webContents, 'console-message');
|
||||
const response = JSON.parse(responseString);
|
||||
expect(response).to.have.lengthOf(1);
|
||||
|
||||
const tab = response[0];
|
||||
expect(tab.mutedInfo).to.deep.equal({
|
||||
muted: true,
|
||||
reason: 'user'
|
||||
});
|
||||
});
|
||||
|
||||
it('only returns tabs in the same session', async () => {
|
||||
await w.loadURL(url);
|
||||
w.webContents.setAudioMuted(true);
|
||||
|
||||
const sameSessionWin = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
session: customSession
|
||||
}
|
||||
});
|
||||
|
||||
sameSessionWin.webContents.setAudioMuted(true);
|
||||
|
||||
const newSession = session.fromPartition(`persist:${uuid.v4()}`);
|
||||
const differentSessionWin = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
session: newSession
|
||||
}
|
||||
});
|
||||
|
||||
differentSessionWin.webContents.setAudioMuted(true);
|
||||
|
||||
const message = { method: 'query', args: [{ muted: true }] };
|
||||
w.webContents.executeJavaScript(`window.postMessage('${JSON.stringify(message)}', '*')`);
|
||||
|
||||
const [, , responseString] = await once(w.webContents, 'console-message');
|
||||
const response = JSON.parse(responseString);
|
||||
expect(response).to.have.lengthOf(2);
|
||||
for (const tab of response) {
|
||||
expect(tab.mutedInfo).to.deep.equal({
|
||||
muted: true,
|
||||
reason: 'user'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -38,6 +38,12 @@ const handleRequest = (request, sender, sendResponse) => {
|
|||
break;
|
||||
}
|
||||
|
||||
case 'query': {
|
||||
const [params] = args;
|
||||
chrome.tabs.query(params).then(sendResponse);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'reload': {
|
||||
chrome.tabs.reload(tabId).then(() => {
|
||||
sendResponse({ status: 'reloaded' });
|
||||
|
|
|
@ -15,6 +15,11 @@ const testMap = {
|
|||
console.log(JSON.stringify(response));
|
||||
});
|
||||
},
|
||||
query (params) {
|
||||
chrome.runtime.sendMessage({ method: 'query', args: [params] }, response => {
|
||||
console.log(JSON.stringify(response));
|
||||
});
|
||||
},
|
||||
getZoom () {
|
||||
chrome.runtime.sendMessage({ method: 'getZoom', args: [] }, response => {
|
||||
console.log(JSON.stringify(response));
|
||||
|
|
Loading…
Reference in a new issue