Skip to content

Commit 55368e4

Browse files
authored
feat: just enough //extensions to load a simple devtools extension (electron#19515)
1 parent 9c1310d commit 55368e4

File tree

14 files changed

+250
-14
lines changed

14 files changed

+250
-14
lines changed

shell/browser/atom_browser_client.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,13 @@ void AtomBrowserClient::RegisterNonNetworkNavigationURLLoaderFactories(
10341034
content::WebContents::FromFrameTreeNodeId(frame_tree_node_id);
10351035
api::Protocol* protocol = api::Protocol::FromWrappedClass(
10361036
v8::Isolate::GetCurrent(), web_contents->GetBrowserContext());
1037+
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
1038+
factories->emplace(
1039+
extensions::kExtensionScheme,
1040+
extensions::CreateExtensionNavigationURLLoaderFactory(
1041+
web_contents->GetBrowserContext(),
1042+
false /* we don't support extensions::WebViewGuest */));
1043+
#endif
10371044
if (protocol)
10381045
protocol->RegisterURLLoaderFactories(factories);
10391046
}
@@ -1042,6 +1049,13 @@ void AtomBrowserClient::RegisterNonNetworkSubresourceURLLoaderFactories(
10421049
int render_process_id,
10431050
int render_frame_id,
10441051
NonNetworkURLLoaderFactoryMap* factories) {
1052+
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
1053+
auto factory = extensions::CreateExtensionURLLoaderFactory(render_process_id,
1054+
render_frame_id);
1055+
if (factory)
1056+
factories->emplace(extensions::kExtensionScheme, std::move(factory));
1057+
#endif
1058+
10451059
// Chromium may call this even when NetworkService is not enabled.
10461060
content::RenderFrameHost* frame_host =
10471061
content::RenderFrameHost::FromID(render_process_id, render_frame_id);

shell/browser/atom_browser_main_parts.cc

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,6 @@ AtomBrowserMainParts::AtomBrowserMainParts(
226226
electron_bindings_(new ElectronBindings(uv_default_loop())) {
227227
DCHECK(!self_) << "Cannot have two AtomBrowserMainParts";
228228
self_ = this;
229-
// Register extension scheme as web safe scheme.
230-
content::ChildProcessSecurityPolicy::GetInstance()->RegisterWebSafeScheme(
231-
"chrome-extension");
232229
}
233230

234231
AtomBrowserMainParts::~AtomBrowserMainParts() {

shell/browser/browser_process_impl.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
#include "components/proxy_config/pref_proxy_config_tracker_impl.h"
1818
#include "components/proxy_config/proxy_config_dictionary.h"
1919
#include "components/proxy_config/proxy_config_pref_names.h"
20+
#include "content/public/browser/child_process_security_policy.h"
2021
#include "content/public/common/content_switches.h"
22+
#include "extensions/common/constants.h"
2123
#include "net/proxy_resolution/proxy_config.h"
2224
#include "net/proxy_resolution/proxy_config_service.h"
2325
#include "net/proxy_resolution/proxy_config_with_annotation.h"
@@ -89,6 +91,10 @@ void BrowserProcessImpl::PostEarlyInitialization() {
8991
}
9092

9193
void BrowserProcessImpl::PreCreateThreads() {
94+
// chrome-extension:// URLs are safe to request anywhere, but may only
95+
// commit (including in iframes) in extension processes.
96+
content::ChildProcessSecurityPolicy::GetInstance()
97+
->RegisterWebSafeIsolatedScheme(extensions::kExtensionScheme, true);
9298
// Must be created before the IOThread.
9399
// Once IOThread class is no longer needed,
94100
// this can be created on first use.

shell/browser/extensions/atom_extensions_browser_client.cc

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@
2424
#include "extensions/browser/updater/null_extension_cache.h"
2525
#include "extensions/browser/url_request_util.h"
2626
#include "extensions/common/features/feature_channel.h"
27+
#include "extensions/common/manifest_constants.h"
28+
#include "extensions/common/manifest_url_handlers.h"
29+
#include "services/network/public/mojom/url_loader.mojom.h"
2730
#include "shell/browser/atom_browser_client.h"
2831
#include "shell/browser/atom_browser_context.h"
2932
#include "shell/browser/browser.h"
3033
#include "shell/browser/extensions/api/runtime/atom_runtime_api_delegate.h"
3134
#include "shell/browser/extensions/atom_extension_host_delegate.h"
3235
#include "shell/browser/extensions/atom_extension_system_factory.h"
3336
#include "shell/browser/extensions/atom_extension_web_contents_observer.h"
34-
// #include "shell/browser/extensions/atom_extensions_api_client.h"
35-
// #include "shell/browser/extensions/atom_extensions_browser_api_provider.h"
36-
#include "services/network/public/mojom/url_loader.mojom.h"
3737
#include "shell/browser/extensions/atom_navigation_ui_data.h"
3838
#include "shell/browser/extensions/electron_extensions_api_client.h"
3939
#include "shell/browser/extensions/electron_process_manager_delegate.h"
@@ -139,6 +139,37 @@ void AtomExtensionsBrowserClient::LoadResourceFromResourceBundle(
139139
NOTREACHED() << "Load resources from bundles not supported.";
140140
}
141141

142+
namespace {
143+
bool AllowCrossRendererResourceLoad(const GURL& url,
144+
content::ResourceType resource_type,
145+
ui::PageTransition page_transition,
146+
int child_id,
147+
bool is_incognito,
148+
const extensions::Extension* extension,
149+
const extensions::ExtensionSet& extensions,
150+
const extensions::ProcessMap& process_map,
151+
bool* allowed) {
152+
if (extensions::url_request_util::AllowCrossRendererResourceLoad(
153+
url, resource_type, page_transition, child_id, is_incognito,
154+
extension, extensions, process_map, allowed)) {
155+
return true;
156+
}
157+
158+
// If there aren't any explicitly marked web accessible resources, the
159+
// load should be allowed only if it is by DevTools. A close approximation is
160+
// checking if the extension contains a DevTools page.
161+
if (extension && !extensions::ManifestURL::Get(
162+
extension, extensions::manifest_keys::kDevToolsPage)
163+
.is_empty()) {
164+
*allowed = true;
165+
return true;
166+
}
167+
168+
// Couldn't determine if the resource is allowed or not.
169+
return false;
170+
}
171+
} // namespace
172+
142173
bool AtomExtensionsBrowserClient::AllowCrossRendererResourceLoad(
143174
const GURL& url,
144175
content::ResourceType resource_type,
@@ -149,7 +180,7 @@ bool AtomExtensionsBrowserClient::AllowCrossRendererResourceLoad(
149180
const extensions::ExtensionSet& extensions,
150181
const extensions::ProcessMap& process_map) {
151182
bool allowed = false;
152-
if (extensions::url_request_util::AllowCrossRendererResourceLoad(
183+
if (::electron::AllowCrossRendererResourceLoad(
153184
url, resource_type, page_transition, child_id, is_incognito,
154185
extension, extensions, process_map, &allowed)) {
155186
return allowed;

shell/browser/ui/inspectable_web_contents_impl.cc

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@
4848
#include "ui/display/display.h"
4949
#include "ui/display/screen.h"
5050

51+
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
52+
#include "content/public/browser/child_process_security_policy.h"
53+
#include "content/public/browser/render_process_host.h"
54+
#include "extensions/browser/extension_registry.h"
55+
#include "extensions/common/manifest_constants.h"
56+
#include "extensions/common/manifest_url_handlers.h"
57+
#include "extensions/common/permissions/permissions_data.h"
58+
#include "shell/browser/atom_browser_context.h"
59+
#endif
60+
5161
namespace electron {
5262

5363
namespace {
@@ -571,10 +581,51 @@ void InspectableWebContentsImpl::LoadCompleted() {
571581
javascript, base::NullCallback());
572582
}
573583

584+
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
585+
AddDevToolsExtensionsToClient();
586+
#endif
587+
574588
if (view_->GetDelegate())
575589
view_->GetDelegate()->DevToolsOpened();
576590
}
577591

592+
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
593+
void InspectableWebContentsImpl::AddDevToolsExtensionsToClient() {
594+
// get main browser context
595+
auto* browser_context = web_contents_->GetBrowserContext();
596+
const extensions::ExtensionRegistry* registry =
597+
extensions::ExtensionRegistry::Get(browser_context);
598+
if (!registry)
599+
return;
600+
601+
base::ListValue results;
602+
for (auto& extension : registry->enabled_extensions()) {
603+
auto devtools_page_url = extensions::ManifestURL::Get(
604+
extension.get(), extensions::manifest_keys::kDevToolsPage);
605+
if (devtools_page_url.is_empty())
606+
continue;
607+
608+
// Each devtools extension will need to be able to run in the devtools
609+
// process. Grant the devtools process the ability to request URLs from the
610+
// extension.
611+
content::ChildProcessSecurityPolicy::GetInstance()->GrantRequestOrigin(
612+
web_contents_->GetMainFrame()->GetProcess()->GetID(),
613+
url::Origin::Create(extension->url()));
614+
615+
std::unique_ptr<base::DictionaryValue> extension_info(
616+
new base::DictionaryValue());
617+
extension_info->SetString("startPage", devtools_page_url.spec());
618+
extension_info->SetString("name", extension->name());
619+
extension_info->SetBoolean("exposeExperimentalAPIs",
620+
extension->permissions_data()->HasAPIPermission(
621+
extensions::APIPermission::kExperimental));
622+
results.Append(std::move(extension_info));
623+
}
624+
625+
CallClientFunction("DevToolsAPI.addExtensions", &results, NULL, NULL);
626+
}
627+
#endif
628+
578629
void InspectableWebContentsImpl::SetInspectedPageBounds(const gfx::Rect& rect) {
579630
DevToolsContentsResizingStrategy strategy(rect);
580631
if (contents_resizing_strategy_.Equals(strategy))

shell/browser/ui/inspectable_web_contents_impl.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "content/public/browser/devtools_frontend_host.h"
2222
#include "content/public/browser/web_contents_delegate.h"
2323
#include "content/public/browser/web_contents_observer.h"
24+
#include "electron/buildflags/buildflags.h"
2425
#include "shell/browser/ui/inspectable_web_contents.h"
2526
#include "ui/gfx/geometry/rect.h"
2627

@@ -193,6 +194,10 @@ class InspectableWebContentsImpl
193194

194195
void SendMessageAck(int request_id, const base::Value* arg1);
195196

197+
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
198+
void AddDevToolsExtensionsToClient();
199+
#endif
200+
196201
bool frontend_loaded_;
197202
scoped_refptr<content::DevToolsAgentHost> agent_host_;
198203
std::unique_ptr<content::DevToolsFrontendHost> frontend_host_;

shell/common/extensions/api/_manifest_features.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
{
1010
"content_scripts": {
1111
"channel": "stable",
12-
"extension_types": ["extension", "legacy_packaged_app"]
12+
"extension_types": ["extension"]
13+
},
14+
"devtools_page": {
15+
"channel": "stable",
16+
"extension_types": ["extension"]
1317
}
1418
}

shell/common/extensions/atom_extensions_api_provider.cc

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,69 @@
44

55
#include "shell/common/extensions/atom_extensions_api_provider.h"
66

7+
#include <memory>
78
#include <string>
9+
#include <utility>
810

11+
#include "base/containers/span.h"
12+
#include "base/strings/utf_string_conversions.h"
913
#include "electron/buildflags/buildflags.h"
14+
#include "extensions/common/alias.h"
1015
#include "extensions/common/features/json_feature_provider_source.h"
11-
12-
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
16+
#include "extensions/common/manifest_constants.h"
17+
#include "extensions/common/manifest_handler.h"
18+
#include "extensions/common/manifest_handlers/permissions_parser.h"
19+
#include "extensions/common/manifest_url_handlers.h"
20+
#include "extensions/common/permissions/permissions_info.h"
1321
#include "shell/common/extensions/api/manifest_features.h"
14-
#endif
22+
23+
namespace extensions {
24+
25+
namespace keys = manifest_keys;
26+
namespace errors = manifest_errors;
27+
28+
// Parses the "devtools_page" manifest key.
29+
class DevToolsPageHandler : public ManifestHandler {
30+
public:
31+
DevToolsPageHandler() = default;
32+
~DevToolsPageHandler() override = default;
33+
34+
bool Parse(Extension* extension, base::string16* error) override {
35+
std::unique_ptr<ManifestURL> manifest_url(new ManifestURL);
36+
std::string devtools_str;
37+
if (!extension->manifest()->GetString(keys::kDevToolsPage, &devtools_str)) {
38+
*error = base::ASCIIToUTF16(errors::kInvalidDevToolsPage);
39+
return false;
40+
}
41+
manifest_url->url_ = extension->GetResourceURL(devtools_str);
42+
extension->SetManifestData(keys::kDevToolsPage, std::move(manifest_url));
43+
PermissionsParser::AddAPIPermission(extension, APIPermission::kDevtools);
44+
return true;
45+
}
46+
47+
private:
48+
base::span<const char* const> Keys() const override {
49+
static constexpr const char* kKeys[] = {keys::kDevToolsPage};
50+
return kKeys;
51+
}
52+
53+
DISALLOW_COPY_AND_ASSIGN(DevToolsPageHandler);
54+
};
55+
56+
constexpr APIPermissionInfo::InitInfo permissions_to_register[] = {
57+
{APIPermission::kDevtools, "devtools",
58+
APIPermissionInfo::kFlagImpliesFullURLAccess |
59+
APIPermissionInfo::kFlagCannotBeOptional |
60+
APIPermissionInfo::kFlagInternal},
61+
};
62+
base::span<const APIPermissionInfo::InitInfo> GetPermissionInfos() {
63+
return base::make_span(permissions_to_register);
64+
}
65+
base::span<const Alias> GetPermissionAliases() {
66+
return base::span<const Alias>();
67+
}
68+
69+
} // namespace extensions
1570

1671
namespace electron {
1772

@@ -60,8 +115,16 @@ base::StringPiece AtomExtensionsAPIProvider::GetAPISchema(
60115
}
61116

62117
void AtomExtensionsAPIProvider::RegisterPermissions(
63-
extensions::PermissionsInfo* permissions_info) {}
118+
extensions::PermissionsInfo* permissions_info) {
119+
permissions_info->RegisterPermissions(extensions::GetPermissionInfos(),
120+
extensions::GetPermissionAliases());
121+
}
64122

65-
void AtomExtensionsAPIProvider::RegisterManifestHandlers() {}
123+
void AtomExtensionsAPIProvider::RegisterManifestHandlers() {
124+
extensions::ManifestHandlerRegistry* registry =
125+
extensions::ManifestHandlerRegistry::Get();
126+
registry->RegisterHandler(
127+
std::make_unique<extensions::DevToolsPageHandler>());
128+
}
66129

67130
} // namespace electron

spec-main/extensions-spec.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect } from 'chai'
2-
import { session, BrowserWindow, ipcMain } from 'electron'
2+
import { session, BrowserWindow, ipcMain, WebContents } from 'electron'
33
import { closeAllWindows, closeWindow } from './window-helpers'
44
import * as http from 'http'
55
import { AddressInfo } from 'net'
@@ -138,6 +138,45 @@ ifdescribe(process.electronBinding('features').isExtensionsEnabled())('chrome ex
138138
}
139139
})
140140
})
141+
142+
describe('devtools extensions', () => {
143+
let showPanelTimeoutId: any = null
144+
afterEach(() => {
145+
if (showPanelTimeoutId) clearTimeout(showPanelTimeoutId)
146+
})
147+
const showLastDevToolsPanel = (w: BrowserWindow) => {
148+
w.webContents.once('devtools-opened', () => {
149+
const show = () => {
150+
if (w == null || w.isDestroyed()) return
151+
const { devToolsWebContents } = w as unknown as { devToolsWebContents: WebContents | undefined }
152+
if (devToolsWebContents == null || devToolsWebContents.isDestroyed()) {
153+
return
154+
}
155+
156+
const showLastPanel = () => {
157+
// this is executed in the devtools context, where UI is a global
158+
const { UI } = (window as any)
159+
const lastPanelId = UI.inspectorView._tabbedPane._tabs.peekLast().id
160+
UI.inspectorView.showPanel(lastPanelId)
161+
}
162+
devToolsWebContents.executeJavaScript(`(${showLastPanel})()`, false).then(() => {
163+
showPanelTimeoutId = setTimeout(show, 100)
164+
})
165+
}
166+
showPanelTimeoutId = setTimeout(show, 100)
167+
})
168+
}
169+
170+
it('loads a devtools extension', async () => {
171+
const customSession = session.fromPartition(`persist:${require('uuid').v4()}`);
172+
(customSession as any).loadExtension(path.join(fixtures, 'extensions', 'devtools-extension'))
173+
const w = new BrowserWindow({ show: true, webPreferences: { session: customSession, nodeIntegration: true } })
174+
await w.loadURL('data:text/html,hello')
175+
w.webContents.openDevTools()
176+
showLastDevToolsPanel(w)
177+
await emittedOnce(ipcMain, 'winning')
178+
})
179+
})
141180
})
142181

143182
ifdescribe(!process.electronBinding('features').isExtensionsEnabled())('chrome extensions', () => {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>foo</title>
6+
<!-- can't be inline, because of CSP -->
7+
<script src="foo.js"></script>
8+
</head>
9+
</html>

0 commit comments

Comments
 (0)