feat: add exposeInIsolatedWorld(worldId, key, api) to contextBridge (#34974)
* feat: add exposeInIsolatedWorld(worldId, key, api) to contextBridge * Updates exposeInIslatedWorld worldId documentation
This commit is contained in:
parent
8c3c0f0b50
commit
dfc134de42
5 changed files with 95 additions and 21 deletions
|
@ -46,6 +46,12 @@ The `contextBridge` module has the following methods:
|
||||||
* `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`.
|
* `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`.
|
||||||
* `api` any - Your API, more information on what this API can be and how it works is available below.
|
* `api` any - Your API, more information on what this API can be and how it works is available below.
|
||||||
|
|
||||||
|
### `contextBridge.exposeInIsolatedWorld(worldId, apiKey, api)`
|
||||||
|
|
||||||
|
* `worldId` Integer - The ID of the world to inject the API into. `0` is the default world, `999` is the world used by Electron's `contextIsolation` feature. Using 999 would expose the object for preload context. We recommend using 1000+ while creating isolated world.
|
||||||
|
* `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`.
|
||||||
|
* `api` any - Your API, more information on what this API can be and how it works is available below.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### API
|
### API
|
||||||
|
@ -84,6 +90,26 @@ contextBridge.exposeInMainWorld(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
An example of `exposeInIsolatedWorld` is shown below:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { contextBridge, ipcRenderer } = require('electron')
|
||||||
|
|
||||||
|
contextBridge.exposeInIsolatedWorld(
|
||||||
|
1004,
|
||||||
|
'electron',
|
||||||
|
{
|
||||||
|
doThing: () => ipcRenderer.send('do-a-thing')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Renderer (In isolated world id1004)
|
||||||
|
|
||||||
|
window.electron.doThing()
|
||||||
|
```
|
||||||
|
|
||||||
### API Functions
|
### API Functions
|
||||||
|
|
||||||
`Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated. This
|
`Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated. This
|
||||||
|
|
|
@ -140,6 +140,7 @@ auto_filenames = {
|
||||||
"lib/common/define-properties.ts",
|
"lib/common/define-properties.ts",
|
||||||
"lib/common/ipc-messages.ts",
|
"lib/common/ipc-messages.ts",
|
||||||
"lib/common/web-view-methods.ts",
|
"lib/common/web-view-methods.ts",
|
||||||
|
"lib/common/webpack-globals-provider.ts",
|
||||||
"lib/renderer/api/context-bridge.ts",
|
"lib/renderer/api/context-bridge.ts",
|
||||||
"lib/renderer/api/crash-reporter.ts",
|
"lib/renderer/api/crash-reporter.ts",
|
||||||
"lib/renderer/api/ipc-renderer.ts",
|
"lib/renderer/api/ipc-renderer.ts",
|
||||||
|
|
|
@ -7,7 +7,11 @@ const checkContextIsolationEnabled = () => {
|
||||||
const contextBridge: Electron.ContextBridge = {
|
const contextBridge: Electron.ContextBridge = {
|
||||||
exposeInMainWorld: (key: string, api: any) => {
|
exposeInMainWorld: (key: string, api: any) => {
|
||||||
checkContextIsolationEnabled();
|
checkContextIsolationEnabled();
|
||||||
return binding.exposeAPIInMainWorld(key, api);
|
return binding.exposeAPIInWorld(0, key, api);
|
||||||
|
},
|
||||||
|
exposeInIsolatedWorld: (worldId: number, key: string, api: any) => {
|
||||||
|
checkContextIsolationEnabled();
|
||||||
|
return binding.exposeAPIInWorld(worldId, key, api);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -561,19 +561,26 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExposeAPIInMainWorld(v8::Isolate* isolate,
|
void ExposeAPIInWorld(v8::Isolate* isolate,
|
||||||
|
const int world_id,
|
||||||
const std::string& key,
|
const std::string& key,
|
||||||
v8::Local<v8::Value> api,
|
v8::Local<v8::Value> api,
|
||||||
gin_helper::Arguments* args) {
|
gin_helper::Arguments* args) {
|
||||||
TRACE_EVENT1("electron", "ContextBridge::ExposeAPIInMainWorld", "key", key);
|
TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key,
|
||||||
|
"worldId", world_id);
|
||||||
|
|
||||||
auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global());
|
auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global());
|
||||||
CHECK(render_frame);
|
CHECK(render_frame);
|
||||||
auto* frame = render_frame->GetWebFrame();
|
auto* frame = render_frame->GetWebFrame();
|
||||||
CHECK(frame);
|
CHECK(frame);
|
||||||
v8::Local<v8::Context> main_context = frame->MainWorldScriptContext();
|
|
||||||
gin_helper::Dictionary global(main_context->GetIsolate(),
|
v8::Local<v8::Context> target_context =
|
||||||
main_context->Global());
|
world_id == WorldIDs::MAIN_WORLD_ID
|
||||||
|
? frame->MainWorldScriptContext()
|
||||||
|
: frame->GetScriptContextFromWorldId(isolate, world_id);
|
||||||
|
|
||||||
|
gin_helper::Dictionary global(target_context->GetIsolate(),
|
||||||
|
target_context->Global());
|
||||||
|
|
||||||
if (global.Has(key)) {
|
if (global.Has(key)) {
|
||||||
args->ThrowError(
|
args->ThrowError(
|
||||||
|
@ -582,15 +589,17 @@ void ExposeAPIInMainWorld(v8::Isolate* isolate,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
v8::Local<v8::Context> isolated_context = frame->GetScriptContextFromWorldId(
|
v8::Local<v8::Context> electron_isolated_context =
|
||||||
args->isolate(), WorldIDs::ISOLATED_WORLD_ID);
|
frame->GetScriptContextFromWorldId(args->isolate(),
|
||||||
|
WorldIDs::ISOLATED_WORLD_ID);
|
||||||
|
|
||||||
{
|
{
|
||||||
context_bridge::ObjectCache object_cache;
|
context_bridge::ObjectCache object_cache;
|
||||||
v8::Context::Scope main_context_scope(main_context);
|
v8::Context::Scope target_context_scope(target_context);
|
||||||
|
|
||||||
v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext(
|
v8::MaybeLocal<v8::Value> maybe_proxy =
|
||||||
isolated_context, main_context, api, &object_cache, false, 0);
|
PassValueToOtherContext(electron_isolated_context, target_context, api,
|
||||||
|
&object_cache, false, 0);
|
||||||
if (maybe_proxy.IsEmpty())
|
if (maybe_proxy.IsEmpty())
|
||||||
return;
|
return;
|
||||||
auto proxy = maybe_proxy.ToLocalChecked();
|
auto proxy = maybe_proxy.ToLocalChecked();
|
||||||
|
@ -601,7 +610,7 @@ void ExposeAPIInMainWorld(v8::Isolate* isolate,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (proxy->IsObject() && !proxy->IsTypedArray() &&
|
if (proxy->IsObject() && !proxy->IsTypedArray() &&
|
||||||
!DeepFreeze(proxy.As<v8::Object>(), main_context))
|
!DeepFreeze(proxy.As<v8::Object>(), target_context))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
global.SetReadOnlyNonConfigurable(key, proxy);
|
global.SetReadOnlyNonConfigurable(key, proxy);
|
||||||
|
@ -717,7 +726,7 @@ void Initialize(v8::Local<v8::Object> exports,
|
||||||
void* priv) {
|
void* priv) {
|
||||||
v8::Isolate* isolate = context->GetIsolate();
|
v8::Isolate* isolate = context->GetIsolate();
|
||||||
gin_helper::Dictionary dict(isolate, exports);
|
gin_helper::Dictionary dict(isolate, exports);
|
||||||
dict.SetMethod("exposeAPIInMainWorld", &electron::api::ExposeAPIInMainWorld);
|
dict.SetMethod("exposeAPIInWorld", &electron::api::ExposeAPIInWorld);
|
||||||
dict.SetMethod("_overrideGlobalValueFromIsolatedWorld",
|
dict.SetMethod("_overrideGlobalValueFromIsolatedWorld",
|
||||||
&electron::api::OverrideGlobalValueFromIsolatedWorld);
|
&electron::api::OverrideGlobalValueFromIsolatedWorld);
|
||||||
dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld",
|
dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld",
|
||||||
|
|
|
@ -62,17 +62,29 @@ describe('contextBridge', () => {
|
||||||
|
|
||||||
const generateTests = (useSandbox: boolean) => {
|
const generateTests = (useSandbox: boolean) => {
|
||||||
describe(`with sandbox=${useSandbox}`, () => {
|
describe(`with sandbox=${useSandbox}`, () => {
|
||||||
const makeBindingWindow = async (bindingCreator: Function) => {
|
const makeBindingWindow = async (bindingCreator: Function, worldId: number = 0) => {
|
||||||
const preloadContent = `const renderer_1 = require('electron');
|
const preloadContentForMainWorld = `const renderer_1 = require('electron');
|
||||||
${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc');
|
${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc');
|
||||||
const gc=require('vm').runInNewContext('gc');
|
const gc=require('vm').runInNewContext('gc');
|
||||||
renderer_1.contextBridge.exposeInMainWorld('GCRunner', {
|
renderer_1.contextBridge.exposeInMainWorld('GCRunner', {
|
||||||
run: () => gc()
|
run: () => gc()
|
||||||
});`}
|
});`}
|
||||||
(${bindingCreator.toString()})();`;
|
(${bindingCreator.toString()})();`;
|
||||||
|
|
||||||
|
const preloadContentForIsolatedWorld = `const renderer_1 = require('electron');
|
||||||
|
${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc');
|
||||||
|
const gc=require('vm').runInNewContext('gc');
|
||||||
|
renderer_1.webFrame.setIsolatedWorldInfo(${worldId}, {
|
||||||
|
name: "Isolated World"
|
||||||
|
});
|
||||||
|
renderer_1.contextBridge.exposeInIsolatedWorld(${worldId}, 'GCRunner', {
|
||||||
|
run: () => gc()
|
||||||
|
});`}
|
||||||
|
(${bindingCreator.toString()})();`;
|
||||||
|
|
||||||
const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'));
|
const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'));
|
||||||
dir = tmpDir;
|
dir = tmpDir;
|
||||||
await fs.writeFile(path.resolve(tmpDir, 'preload.js'), preloadContent);
|
await fs.writeFile(path.resolve(tmpDir, 'preload.js'), worldId === 0 ? preloadContentForMainWorld : preloadContentForIsolatedWorld);
|
||||||
w = new BrowserWindow({
|
w = new BrowserWindow({
|
||||||
show: false,
|
show: false,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
|
@ -86,8 +98,8 @@ describe('contextBridge', () => {
|
||||||
await w.loadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}`);
|
await w.loadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const callWithBindings = (fn: Function) =>
|
const callWithBindings = (fn: Function, worldId: number = 0) =>
|
||||||
w.webContents.executeJavaScript(`(${fn.toString()})(window)`);
|
worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]); ;
|
||||||
|
|
||||||
const getGCInfo = async (): Promise<{
|
const getGCInfo = async (): Promise<{
|
||||||
trackedValues: number;
|
trackedValues: number;
|
||||||
|
@ -114,6 +126,16 @@ describe('contextBridge', () => {
|
||||||
expect(result).to.equal(123);
|
expect(result).to.equal(123);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should proxy numbers when exposed in isolated world', async () => {
|
||||||
|
await makeBindingWindow(() => {
|
||||||
|
contextBridge.exposeInIsolatedWorld(1004, 'example', 123);
|
||||||
|
}, 1004);
|
||||||
|
const result = await callWithBindings((root: any) => {
|
||||||
|
return root.example;
|
||||||
|
}, 1004);
|
||||||
|
expect(result).to.equal(123);
|
||||||
|
});
|
||||||
|
|
||||||
it('should make global properties read-only', async () => {
|
it('should make global properties read-only', async () => {
|
||||||
await makeBindingWindow(() => {
|
await makeBindingWindow(() => {
|
||||||
contextBridge.exposeInMainWorld('example', 123);
|
contextBridge.exposeInMainWorld('example', 123);
|
||||||
|
@ -172,6 +194,18 @@ describe('contextBridge', () => {
|
||||||
expect(result).to.equal('my-words');
|
expect(result).to.equal('my-words');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should proxy nested strings when exposed in isolated world', async () => {
|
||||||
|
await makeBindingWindow(() => {
|
||||||
|
contextBridge.exposeInIsolatedWorld(1004, 'example', {
|
||||||
|
myString: 'my-words'
|
||||||
|
});
|
||||||
|
}, 1004);
|
||||||
|
const result = await callWithBindings((root: any) => {
|
||||||
|
return root.example.myString;
|
||||||
|
}, 1004);
|
||||||
|
expect(result).to.equal('my-words');
|
||||||
|
});
|
||||||
|
|
||||||
it('should proxy arrays', async () => {
|
it('should proxy arrays', async () => {
|
||||||
await makeBindingWindow(() => {
|
await makeBindingWindow(() => {
|
||||||
contextBridge.exposeInMainWorld('example', [123, 'my-words']);
|
contextBridge.exposeInMainWorld('example', [123, 'my-words']);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue