From ec6230014f7bee109e775f189d2a7cf88300a106 Mon Sep 17 00:00:00 2001 From: Akshay Deo Date: Wed, 21 Sep 2022 23:47:10 +0530 Subject: [PATCH] feat: add exposeInIsolatedWorld(worldId, key, api) to contextBridge (#34974) * feat: add exposeInIsolatedWorld(worldId, key, api) to contextBridge * Updates exposeInIslatedWorld worldId documentation --- docs/api/context-bridge.md | 26 +++++++++++ filenames.auto.gni | 1 + lib/renderer/api/context-bridge.ts | 6 ++- .../api/electron_api_context_bridge.cc | 39 +++++++++------- spec/api-context-bridge-spec.ts | 44 ++++++++++++++++--- 5 files changed, 95 insertions(+), 21 deletions(-) diff --git a/docs/api/context-bridge.md b/docs/api/context-bridge.md index 5d62220bd6070..fa5ce0a54f262 100644 --- a/docs/api/context-bridge.md +++ b/docs/api/context-bridge.md @@ -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]`. * `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 ### 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 `Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated. This diff --git a/filenames.auto.gni b/filenames.auto.gni index e60a980e5dd20..36eb140d99835 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -141,6 +141,7 @@ auto_filenames = { "lib/common/define-properties.ts", "lib/common/ipc-messages.ts", "lib/common/web-view-methods.ts", + "lib/common/webpack-globals-provider.ts", "lib/renderer/api/context-bridge.ts", "lib/renderer/api/crash-reporter.ts", "lib/renderer/api/ipc-renderer.ts", diff --git a/lib/renderer/api/context-bridge.ts b/lib/renderer/api/context-bridge.ts index dd545767c93b1..99b133e6c3ac0 100644 --- a/lib/renderer/api/context-bridge.ts +++ b/lib/renderer/api/context-bridge.ts @@ -7,7 +7,11 @@ const checkContextIsolationEnabled = () => { const contextBridge: Electron.ContextBridge = { exposeInMainWorld: (key: string, api: any) => { 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); } }; diff --git a/shell/renderer/api/electron_api_context_bridge.cc b/shell/renderer/api/electron_api_context_bridge.cc index 77f4ce7c3a8d2..1754370a3d081 100644 --- a/shell/renderer/api/electron_api_context_bridge.cc +++ b/shell/renderer/api/electron_api_context_bridge.cc @@ -561,19 +561,26 @@ v8::MaybeLocal CreateProxyForAPI( } } -void ExposeAPIInMainWorld(v8::Isolate* isolate, - const std::string& key, - v8::Local api, - gin_helper::Arguments* args) { - TRACE_EVENT1("electron", "ContextBridge::ExposeAPIInMainWorld", "key", key); +void ExposeAPIInWorld(v8::Isolate* isolate, + const int world_id, + const std::string& key, + v8::Local api, + gin_helper::Arguments* args) { + TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key, + "worldId", world_id); auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global()); CHECK(render_frame); auto* frame = render_frame->GetWebFrame(); CHECK(frame); - v8::Local main_context = frame->MainWorldScriptContext(); - gin_helper::Dictionary global(main_context->GetIsolate(), - main_context->Global()); + + v8::Local target_context = + 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)) { args->ThrowError( @@ -582,15 +589,17 @@ void ExposeAPIInMainWorld(v8::Isolate* isolate, return; } - v8::Local isolated_context = frame->GetScriptContextFromWorldId( - args->isolate(), WorldIDs::ISOLATED_WORLD_ID); + v8::Local electron_isolated_context = + frame->GetScriptContextFromWorldId(args->isolate(), + WorldIDs::ISOLATED_WORLD_ID); { context_bridge::ObjectCache object_cache; - v8::Context::Scope main_context_scope(main_context); + v8::Context::Scope target_context_scope(target_context); - v8::MaybeLocal maybe_proxy = PassValueToOtherContext( - isolated_context, main_context, api, &object_cache, false, 0); + v8::MaybeLocal maybe_proxy = + PassValueToOtherContext(electron_isolated_context, target_context, api, + &object_cache, false, 0); if (maybe_proxy.IsEmpty()) return; auto proxy = maybe_proxy.ToLocalChecked(); @@ -601,7 +610,7 @@ void ExposeAPIInMainWorld(v8::Isolate* isolate, } if (proxy->IsObject() && !proxy->IsTypedArray() && - !DeepFreeze(proxy.As(), main_context)) + !DeepFreeze(proxy.As(), target_context)) return; global.SetReadOnlyNonConfigurable(key, proxy); @@ -717,7 +726,7 @@ void Initialize(v8::Local exports, void* priv) { v8::Isolate* isolate = context->GetIsolate(); gin_helper::Dictionary dict(isolate, exports); - dict.SetMethod("exposeAPIInMainWorld", &electron::api::ExposeAPIInMainWorld); + dict.SetMethod("exposeAPIInWorld", &electron::api::ExposeAPIInWorld); dict.SetMethod("_overrideGlobalValueFromIsolatedWorld", &electron::api::OverrideGlobalValueFromIsolatedWorld); dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld", diff --git a/spec/api-context-bridge-spec.ts b/spec/api-context-bridge-spec.ts index dc3601dbf8841..8a2041f264922 100644 --- a/spec/api-context-bridge-spec.ts +++ b/spec/api-context-bridge-spec.ts @@ -62,17 +62,29 @@ describe('contextBridge', () => { const generateTests = (useSandbox: boolean) => { describe(`with sandbox=${useSandbox}`, () => { - const makeBindingWindow = async (bindingCreator: Function) => { - const preloadContent = `const renderer_1 = require('electron'); + const makeBindingWindow = async (bindingCreator: Function, worldId: number = 0) => { + const preloadContentForMainWorld = `const renderer_1 = require('electron'); ${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc'); const gc=require('vm').runInNewContext('gc'); renderer_1.contextBridge.exposeInMainWorld('GCRunner', { run: () => gc() });`} (${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-')); 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({ show: false, webPreferences: { @@ -86,8 +98,8 @@ describe('contextBridge', () => { await w.loadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}`); }; - const callWithBindings = (fn: Function) => - w.webContents.executeJavaScript(`(${fn.toString()})(window)`); + const callWithBindings = (fn: Function, worldId: number = 0) => + worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]); ; const getGCInfo = async (): Promise<{ trackedValues: number; @@ -114,6 +126,16 @@ describe('contextBridge', () => { 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 () => { await makeBindingWindow(() => { contextBridge.exposeInMainWorld('example', 123); @@ -172,6 +194,18 @@ describe('contextBridge', () => { 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 () => { await makeBindingWindow(() => { contextBridge.exposeInMainWorld('example', [123, 'my-words']);