Skip to content

Commit

Permalink
feat: add support for chrome.tabs.query (#39431)
Browse files Browse the repository at this point in the history
* feat: add support for tabs.query

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* fix: scope to webContents in current session

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

* test: add test for session behavior

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
  • Loading branch information
trop[bot] and codebytere committed Aug 14, 2023
1 parent c709e04 commit ca899eb
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 2 deletions.
10 changes: 8 additions & 2 deletions docs/api/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand All @@ -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`.

Expand All @@ -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`

Expand Down
10 changes: 10 additions & 0 deletions shell/browser/api/electron_api_web_contents.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4443,6 +4443,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};

Expand Down
1 change: 1 addition & 0 deletions shell/browser/api/electron_api_web_contents.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,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.
//
Expand Down
98 changes: 98 additions & 0 deletions shell/browser/extensions/api/tabs/tabs_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -58,13 +61,21 @@ 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;
info.muted = contents->IsAudioMuted();
info.reason = api::tabs::MUTED_INFO_REASON_USER;
return info;
}

} // namespace

ExecuteCodeInTabFunction::ExecuteCodeInTabFunction() : execute_tab_id_(-1) {}
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions shell/browser/extensions/api/tabs/tabs_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand Down
140 changes: 140 additions & 0 deletions shell/common/extensions/api/tabs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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": [
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit ca899eb

Please sign in to comment.