feat: exposeInMainWorld allow to expose non-object APIs (#26594)
This commit is contained in:
parent
b111bba387
commit
7672aa9525
4 changed files with 313 additions and 13 deletions
|
@ -44,19 +44,19 @@ The `contextBridge` module has the following methods:
|
|||
### `contextBridge.exposeInMainWorld(apiKey, api)` _Experimental_
|
||||
|
||||
* `apiKey` String - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`.
|
||||
* `api` Record<String, any> - Your API object, 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.
|
||||
|
||||
## Usage
|
||||
|
||||
### API Objects
|
||||
### API
|
||||
|
||||
The `api` object provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api-experimental) must be an object
|
||||
The `api` provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api-experimental) must be a `Function`, `String`, `Number`, `Array`, `Boolean`, or an object
|
||||
whose keys are strings and values are a `Function`, `String`, `Number`, `Array`, `Boolean`, or another nested object that meets the same conditions.
|
||||
|
||||
`Function` values are proxied to the other context and all other values are **copied** and **frozen**. Any data / primitives sent in
|
||||
the API object become immutable and updates on either side of the bridge do not result in an update on the other side.
|
||||
the API become immutable and updates on either side of the bridge do not result in an update on the other side.
|
||||
|
||||
An example of a complex API object is shown below:
|
||||
An example of a complex API is shown below:
|
||||
|
||||
```javascript
|
||||
const { contextBridge } = require('electron')
|
||||
|
|
|
@ -8,7 +8,7 @@ const checkContextIsolationEnabled = () => {
|
|||
};
|
||||
|
||||
const contextBridge: Electron.ContextBridge = {
|
||||
exposeInMainWorld: (key: string, api: Record<string, any>) => {
|
||||
exposeInMainWorld: (key: string, api: any) => {
|
||||
checkContextIsolationEnabled();
|
||||
return binding.exposeAPIInMainWorld(key, api);
|
||||
}
|
||||
|
|
|
@ -513,11 +513,13 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
|
|||
}
|
||||
}
|
||||
|
||||
void ExposeAPIInMainWorld(const std::string& key,
|
||||
v8::Local<v8::Object> api_object,
|
||||
void ExposeAPIInMainWorld(v8::Isolate* isolate,
|
||||
const std::string& key,
|
||||
v8::Local<v8::Value> api,
|
||||
gin_helper::Arguments* args) {
|
||||
TRACE_EVENT1("electron", "ContextBridge::ExposeAPIInMainWorld", "key", key);
|
||||
auto* render_frame = GetRenderFrame(api_object);
|
||||
|
||||
auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global());
|
||||
CHECK(render_frame);
|
||||
auto* frame = render_frame->GetWebFrame();
|
||||
CHECK(frame);
|
||||
|
@ -539,12 +541,13 @@ void ExposeAPIInMainWorld(const std::string& key,
|
|||
context_bridge::ObjectCache object_cache;
|
||||
v8::Context::Scope main_context_scope(main_context);
|
||||
|
||||
v8::MaybeLocal<v8::Object> maybe_proxy = CreateProxyForAPI(
|
||||
api_object, isolated_context, main_context, &object_cache, false, 0);
|
||||
v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext(
|
||||
isolated_context, main_context, api, &object_cache, false, 0);
|
||||
if (maybe_proxy.IsEmpty())
|
||||
return;
|
||||
auto proxy = maybe_proxy.ToLocalChecked();
|
||||
if (!DeepFreeze(proxy, main_context))
|
||||
if (proxy->IsObject() && !proxy->IsTypedArray() &&
|
||||
!DeepFreeze(v8::Local<v8::Object>::Cast(proxy), main_context))
|
||||
return;
|
||||
|
||||
global.SetReadOnlyNonConfigurable(key, proxy);
|
||||
|
|
|
@ -104,6 +104,27 @@ describe('contextBridge', () => {
|
|||
};
|
||||
|
||||
it('should proxy numbers', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', 123);
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
return root.example;
|
||||
});
|
||||
expect(result).to.equal(123);
|
||||
});
|
||||
|
||||
it('should make global properties read-only', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', 123);
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
root.example = 456;
|
||||
return root.example;
|
||||
});
|
||||
expect(result).to.equal(123);
|
||||
});
|
||||
|
||||
it('should proxy nested numbers', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
myNumber: 123
|
||||
|
@ -129,6 +150,16 @@ describe('contextBridge', () => {
|
|||
});
|
||||
|
||||
it('should proxy strings', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', 'my-words');
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
return root.example;
|
||||
});
|
||||
expect(result).to.equal('my-words');
|
||||
});
|
||||
|
||||
it('should proxy nested strings', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
myString: 'my-words'
|
||||
|
@ -141,6 +172,16 @@ describe('contextBridge', () => {
|
|||
});
|
||||
|
||||
it('should proxy arrays', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', [123, 'my-words']);
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
return [root.example, Array.isArray(root.example)];
|
||||
});
|
||||
expect(result).to.deep.equal([[123, 'my-words'], true]);
|
||||
});
|
||||
|
||||
it('should proxy nested arrays', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
myArr: [123, 'my-words']
|
||||
|
@ -153,6 +194,21 @@ describe('contextBridge', () => {
|
|||
});
|
||||
|
||||
it('should make arrays immutable', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', [123, 'my-words']);
|
||||
});
|
||||
const immutable = await callWithBindings((root: any) => {
|
||||
try {
|
||||
root.example.push(456);
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
expect(immutable).to.equal(true);
|
||||
});
|
||||
|
||||
it('should make nested arrays immutable', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
myArr: [123, 'my-words']
|
||||
|
@ -170,6 +226,16 @@ describe('contextBridge', () => {
|
|||
});
|
||||
|
||||
it('should proxy booleans', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', true);
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
return root.example;
|
||||
});
|
||||
expect(result).to.equal(true);
|
||||
});
|
||||
|
||||
it('should proxy nested booleans', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
myBool: true
|
||||
|
@ -182,6 +248,18 @@ describe('contextBridge', () => {
|
|||
});
|
||||
|
||||
it('should proxy promises and resolve with the correct value', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example',
|
||||
Promise.resolve('i-resolved')
|
||||
);
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
return root.example;
|
||||
});
|
||||
expect(result).to.equal('i-resolved');
|
||||
});
|
||||
|
||||
it('should proxy nested promises and resolve with the correct value', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
myPromise: Promise.resolve('i-resolved')
|
||||
|
@ -194,6 +272,21 @@ describe('contextBridge', () => {
|
|||
});
|
||||
|
||||
it('should proxy promises and reject with the correct value', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', Promise.reject(new Error('i-rejected')));
|
||||
});
|
||||
const result = await callWithBindings(async (root: any) => {
|
||||
try {
|
||||
await root.example;
|
||||
return null;
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
});
|
||||
expect(result).to.be.an.instanceOf(Error).with.property('message', 'Uncaught Error: i-rejected');
|
||||
});
|
||||
|
||||
it('should proxy nested promises and reject with the correct value', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
myPromise: Promise.reject(new Error('i-rejected'))
|
||||
|
@ -249,6 +342,16 @@ describe('contextBridge', () => {
|
|||
expect(result).to.deep.equal([123, 'help', false, 'promise']);
|
||||
});
|
||||
|
||||
it('should proxy functions', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', () => 'return-value');
|
||||
});
|
||||
const result = await callWithBindings(async (root: any) => {
|
||||
return root.example();
|
||||
});
|
||||
expect(result).equal('return-value');
|
||||
});
|
||||
|
||||
it('should proxy methods that are callable multiple times', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
|
@ -299,7 +402,31 @@ describe('contextBridge', () => {
|
|||
expect(result).to.deep.equal([123, 456, 789, false]);
|
||||
});
|
||||
|
||||
it('it should proxy null and undefined correctly', async () => {
|
||||
it('it should proxy null', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', null);
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
// Convert to strings as although the context bridge keeps the right value
|
||||
// IPC does not
|
||||
return `${root.example}`;
|
||||
});
|
||||
expect(result).to.deep.equal('null');
|
||||
});
|
||||
|
||||
it('it should proxy undefined', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', undefined);
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
// Convert to strings as although the context bridge keeps the right value
|
||||
// IPC does not
|
||||
return `${root.example}`;
|
||||
});
|
||||
expect(result).to.deep.equal('undefined');
|
||||
});
|
||||
|
||||
it('it should proxy nested null and undefined correctly', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
values: [null, undefined]
|
||||
|
@ -313,6 +440,19 @@ describe('contextBridge', () => {
|
|||
expect(result).to.deep.equal(['null', 'undefined']);
|
||||
});
|
||||
|
||||
it('should proxy symbols', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
const mySymbol = Symbol('unique');
|
||||
const isSymbol = (s: Symbol) => s === mySymbol;
|
||||
contextBridge.exposeInMainWorld('symbol', mySymbol);
|
||||
contextBridge.exposeInMainWorld('isSymbol', isSymbol);
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
return root.isSymbol(root.symbol);
|
||||
});
|
||||
expect(result).to.equal(true, 'symbols should be equal across contexts');
|
||||
});
|
||||
|
||||
it('should proxy symbols such that symbol equality works', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
const mySymbol = Symbol('unique');
|
||||
|
@ -341,6 +481,26 @@ describe('contextBridge', () => {
|
|||
expect(result).to.equal(123, 'symbols key lookup should work across contexts');
|
||||
});
|
||||
|
||||
it('should proxy typed arrays', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', new Uint8Array(100));
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
return Object.getPrototypeOf(root.example) === Uint8Array.prototype;
|
||||
});
|
||||
expect(result).equal(true);
|
||||
});
|
||||
|
||||
it('should proxy regexps', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', /a/g);
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
return Object.getPrototypeOf(root.example) === RegExp.prototype;
|
||||
});
|
||||
expect(result).equal(true);
|
||||
});
|
||||
|
||||
it('should proxy typed arrays and regexps through the serializer', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
|
@ -466,6 +626,22 @@ describe('contextBridge', () => {
|
|||
expect(result).to.equal('final value');
|
||||
});
|
||||
|
||||
it('should work with complex nested methods and promises attached directly to the global', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example',
|
||||
(second: Function) => second((fourth: Function) => {
|
||||
return fourth();
|
||||
})
|
||||
);
|
||||
});
|
||||
const result = await callWithBindings((root: any) => {
|
||||
return root.example((third: Function) => {
|
||||
return third(() => Promise.resolve('final value'));
|
||||
});
|
||||
});
|
||||
expect(result).to.equal('final value');
|
||||
});
|
||||
|
||||
it('should throw an error when recursion depth is exceeded', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
contextBridge.exposeInMainWorld('example', {
|
||||
|
@ -641,6 +817,127 @@ describe('contextBridge', () => {
|
|||
expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true));
|
||||
});
|
||||
|
||||
it('should not leak prototypes when attaching directly to the global', async () => {
|
||||
await makeBindingWindow(() => {
|
||||
const toExpose = {
|
||||
number: 123,
|
||||
string: 'string',
|
||||
boolean: true,
|
||||
arr: [123, 'string', true, ['foo']],
|
||||
symbol: Symbol('foo'),
|
||||
bigInt: 10n,
|
||||
getObject: () => ({ thing: 123 }),
|
||||
getNumber: () => 123,
|
||||
getString: () => 'string',
|
||||
getBoolean: () => true,
|
||||
getArr: () => [123, 'string', true, ['foo']],
|
||||
getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] }),
|
||||
getFunctionFromFunction: async () => () => null,
|
||||
object: {
|
||||
number: 123,
|
||||
string: 'string',
|
||||
boolean: true,
|
||||
arr: [123, 'string', true, ['foo']],
|
||||
getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']] })
|
||||
},
|
||||
receiveArguments: (fn: any) => fn({ key: 'value' }),
|
||||
symbolKeyed: {
|
||||
[Symbol('foo')]: 123
|
||||
}
|
||||
};
|
||||
const keys: string[] = [];
|
||||
Object.entries(toExpose).forEach(([key, value]) => {
|
||||
keys.push(key);
|
||||
contextBridge.exposeInMainWorld(key, value);
|
||||
});
|
||||
contextBridge.exposeInMainWorld('keys', keys);
|
||||
});
|
||||
const result = await callWithBindings(async (root: any) => {
|
||||
const { keys } = root;
|
||||
const cleanedRoot: any = {};
|
||||
for (const [key, value] of Object.entries(root)) {
|
||||
if (keys.includes(key)) {
|
||||
cleanedRoot[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
let arg: any;
|
||||
cleanedRoot.receiveArguments((o: any) => { arg = o; });
|
||||
const protoChecks = [
|
||||
...Object.keys(cleanedRoot).map(key => [key, String]),
|
||||
...Object.getOwnPropertySymbols(cleanedRoot.symbolKeyed).map(key => [key, Symbol]),
|
||||
[cleanedRoot, Object],
|
||||
[cleanedRoot.number, Number],
|
||||
[cleanedRoot.string, String],
|
||||
[cleanedRoot.boolean, Boolean],
|
||||
[cleanedRoot.arr, Array],
|
||||
[cleanedRoot.arr[0], Number],
|
||||
[cleanedRoot.arr[1], String],
|
||||
[cleanedRoot.arr[2], Boolean],
|
||||
[cleanedRoot.arr[3], Array],
|
||||
[cleanedRoot.arr[3][0], String],
|
||||
[cleanedRoot.symbol, Symbol],
|
||||
[cleanedRoot.bigInt, BigInt],
|
||||
[cleanedRoot.getNumber, Function],
|
||||
[cleanedRoot.getNumber(), Number],
|
||||
[cleanedRoot.getObject(), Object],
|
||||
[cleanedRoot.getString(), String],
|
||||
[cleanedRoot.getBoolean(), Boolean],
|
||||
[cleanedRoot.getArr(), Array],
|
||||
[cleanedRoot.getArr()[0], Number],
|
||||
[cleanedRoot.getArr()[1], String],
|
||||
[cleanedRoot.getArr()[2], Boolean],
|
||||
[cleanedRoot.getArr()[3], Array],
|
||||
[cleanedRoot.getArr()[3][0], String],
|
||||
[cleanedRoot.getFunctionFromFunction, Function],
|
||||
[cleanedRoot.getFunctionFromFunction(), Promise],
|
||||
[await cleanedRoot.getFunctionFromFunction(), Function],
|
||||
[cleanedRoot.getPromise(), Promise],
|
||||
[await cleanedRoot.getPromise(), Object],
|
||||
[(await cleanedRoot.getPromise()).number, Number],
|
||||
[(await cleanedRoot.getPromise()).string, String],
|
||||
[(await cleanedRoot.getPromise()).boolean, Boolean],
|
||||
[(await cleanedRoot.getPromise()).fn, Function],
|
||||
[(await cleanedRoot.getPromise()).fn(), String],
|
||||
[(await cleanedRoot.getPromise()).arr, Array],
|
||||
[(await cleanedRoot.getPromise()).arr[0], Number],
|
||||
[(await cleanedRoot.getPromise()).arr[1], String],
|
||||
[(await cleanedRoot.getPromise()).arr[2], Boolean],
|
||||
[(await cleanedRoot.getPromise()).arr[3], Array],
|
||||
[(await cleanedRoot.getPromise()).arr[3][0], String],
|
||||
[cleanedRoot.object, Object],
|
||||
[cleanedRoot.object.number, Number],
|
||||
[cleanedRoot.object.string, String],
|
||||
[cleanedRoot.object.boolean, Boolean],
|
||||
[cleanedRoot.object.arr, Array],
|
||||
[cleanedRoot.object.arr[0], Number],
|
||||
[cleanedRoot.object.arr[1], String],
|
||||
[cleanedRoot.object.arr[2], Boolean],
|
||||
[cleanedRoot.object.arr[3], Array],
|
||||
[cleanedRoot.object.arr[3][0], String],
|
||||
[await cleanedRoot.object.getPromise(), Object],
|
||||
[(await cleanedRoot.object.getPromise()).number, Number],
|
||||
[(await cleanedRoot.object.getPromise()).string, String],
|
||||
[(await cleanedRoot.object.getPromise()).boolean, Boolean],
|
||||
[(await cleanedRoot.object.getPromise()).fn, Function],
|
||||
[(await cleanedRoot.object.getPromise()).fn(), String],
|
||||
[(await cleanedRoot.object.getPromise()).arr, Array],
|
||||
[(await cleanedRoot.object.getPromise()).arr[0], Number],
|
||||
[(await cleanedRoot.object.getPromise()).arr[1], String],
|
||||
[(await cleanedRoot.object.getPromise()).arr[2], Boolean],
|
||||
[(await cleanedRoot.object.getPromise()).arr[3], Array],
|
||||
[(await cleanedRoot.object.getPromise()).arr[3][0], String],
|
||||
[arg, Object],
|
||||
[arg.key, String]
|
||||
];
|
||||
return {
|
||||
protoMatches: protoChecks.map(([a, Constructor]) => Object.getPrototypeOf(a) === Constructor.prototype)
|
||||
};
|
||||
});
|
||||
// Every protomatch should be true
|
||||
expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true));
|
||||
});
|
||||
|
||||
describe('internalContextBridge', () => {
|
||||
describe('overrideGlobalValueFromIsolatedWorld', () => {
|
||||
it('should override top level properties', async () => {
|
||||
|
|
Loading…
Reference in a new issue