Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add a new contextBridge module #20789

Merged
merged 6 commits into from Nov 4, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions default_app/index.html
Expand Up @@ -2,10 +2,9 @@

<head>
<title>Electron</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-6PH54BfkNq/EMMhUY7nhHf3c+AxloOwfy7hWyT01CM8='; style-src 'self'; img-src 'self'; connect-src 'self'" />
<link href="./styles.css" type="text/css" rel="stylesheet" />
<link href="./octicon/build.css" type="text/css" rel="stylesheet" />
<script defer src="./index.js"></script>
</head>

<body>
Expand Down Expand Up @@ -84,6 +83,9 @@ <h4>Forge</h4>
</div>
</div>
</nav>
<script>
window.electronDefaultApp.initialize()
</script>
</body>

</html>
30 changes: 0 additions & 30 deletions default_app/index.ts

This file was deleted.

37 changes: 35 additions & 2 deletions default_app/preload.ts
@@ -1,4 +1,31 @@
import { ipcRenderer } from 'electron'
import { ipcRenderer, contextBridge } from 'electron'

async function getOcticonSvg (name: string) {
try {
const response = await fetch(`octicon/${name}.svg`)
const div = document.createElement('div')
div.innerHTML = await response.text()
return div
} catch {
return null
}
}

async function loadSVG (element: HTMLSpanElement) {
for (const cssClass of element.classList) {
if (cssClass.startsWith('octicon-')) {
const icon = await getOcticonSvg(cssClass.substr(8))
if (icon) {
for (const elemClass of element.classList) {
icon.classList.add(elemClass)
}
element.before(icon)
element.remove()
break
}
}
}
}

async function initialize () {
const electronPath = await ipcRenderer.invoke('bootstrap')
Expand All @@ -15,6 +42,12 @@ async function initialize () {
replaceText('.node-version', `Node v${process.versions.node}`)
replaceText('.v8-version', `v8 v${process.versions.v8}`)
replaceText('.command-example', `${electronPath} path-to-app`)

for (const element of document.querySelectorAll<HTMLSpanElement>('.octicon')) {
loadSVG(element)
}
}

document.addEventListener('DOMContentLoaded', initialize)
contextBridge.exposeInMainWorld('electronDefaultApp', {
initialize
})
111 changes: 111 additions & 0 deletions docs/api/context-bridge.md
@@ -0,0 +1,111 @@
# contextBridge

> Create a safe, bi-directional, synchronous bridge across isolated contexts

Process: [Renderer](../glossary.md#renderer-process)

An example of exposing an API to a renderer from an isolated preload script is given below:

```javascript
// Preload (Isolated World)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.send('do-a-thing')
}
)
```

```javascript
// Renderer (Main World)

window.electron.doThing()
```

## Glossary

### Main World

The "Main World" is the javascript context that your main renderer code runs in. By default the page you load in your renderer
executes code in this world.

### Isolated World

When `contextIsolation` is enabled in your `webPreferences` your `preload` scripts run in an "Isolated World". You can read more about
context isolation and what it affects in the [BrowserWindow](browser-window.md) docs.

## Methods

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.

## Usage

### API Objects

The `api` object provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api-experimental) must be 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**. I.e. 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.

An example of a complex API object is shown below.

```javascript
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.send('do-a-thing'),
myPromises: [Promise.resolve(), Promise.reject(new Error('whoops'))],
anAsyncFunction: async () => 123,
data: {
myFlags: ['a', 'b', 'c'],
bootTime: 1234
},
nestedAPI: {
evenDeeper: {
youCanDoThisAsMuchAsYouWant: {
fn: () => ({
returnData: 123
})
}
}
}
}
)
```

### API Functions

`Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated. This
results in some key limitations that we've outlined below.

#### Parameter / Error / Return Type support

Because parameters, errors and return values are **copied** when they are sent over the bridge there are only certain types that can be used.
At a high level if the type you want to use can be serialized and un-serialized into the same object it will work. A table of type support
has been included below for completeness.

| Type | Complexity | Parameter Support | Return Value Support | Limitations |
| ---- | ---------- | ----------------- | -------------------- | ----------- |
| `String` | Simple | ✅ | ✅ | N/A |
| `Number` | Simple | ✅ | ✅ | N/A |
| `Boolean` | Simple | ✅ | ✅ | N/A |
| `Object` | Complex | ✅ | ✅ | Keys must be supported "Simple" types in this table. Values must be supported in this table. Prototype modifications are dropped. Sending custom classes will copy values but not the prototype. |
| `Array` | Complex | ✅ | ✅ | Same limitations as the `Object` type |
| `Error` | Complex | ✅ | ✅ | Errors that are thrown are also copied, this can result in the message and stack trace of the error changing slightly due to being thrown in a different context |
| `Promise` | Complex | ✅ | ✅ | Promises are only proxied if they are a the return value or exact parameter. Promises nested in arrays or obejcts will be dropped. |
| `Function` | Complex | ✅ | ✅ | Prototype modifications are dropped. Sending classes or constructors will not work. |
| [Cloneable Types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) | Simple | ✅ | ✅ | See the linked document on cloneable types |
| `Symbol` | N/A | ❌ | ❌ | Symbols cannot be copied across contexts so they are dropped |


If the type you care about is not in the above table it is probably not supported.
4 changes: 4 additions & 0 deletions filenames.auto.gni
Expand Up @@ -14,6 +14,7 @@ auto_filenames = {
"docs/api/clipboard.md",
"docs/api/command-line.md",
"docs/api/content-tracing.md",
"docs/api/context-bridge.md",
"docs/api/cookies.md",
"docs/api/crash-reporter.md",
"docs/api/debugger.md",
Expand Down Expand Up @@ -140,6 +141,7 @@ auto_filenames = {
"lib/common/error-utils.ts",
"lib/common/is-promise.ts",
"lib/common/web-view-methods.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.js",
"lib/renderer/api/desktop-capturer.ts",
"lib/renderer/api/ipc-renderer.js",
Expand Down Expand Up @@ -299,6 +301,7 @@ auto_filenames = {
"lib/common/is-promise.ts",
"lib/common/reset-search-paths.ts",
"lib/common/web-view-methods.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.js",
"lib/renderer/api/desktop-capturer.ts",
"lib/renderer/api/exports/electron.js",
Expand Down Expand Up @@ -348,6 +351,7 @@ auto_filenames = {
"lib/common/init.ts",
"lib/common/is-promise.ts",
"lib/common/reset-search-paths.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.js",
"lib/renderer/api/desktop-capturer.ts",
"lib/renderer/api/exports/electron.js",
Expand Down
5 changes: 4 additions & 1 deletion filenames.gni
@@ -1,7 +1,6 @@
filenames = {
default_app_ts_sources = [
"default_app/default_app.ts",
"default_app/index.ts",
"default_app/main.ts",
"default_app/preload.ts",
]
Expand Down Expand Up @@ -537,6 +536,10 @@ filenames = {
"shell/common/promise_util.cc",
"shell/common/skia_util.h",
"shell/common/skia_util.cc",
"shell/renderer/api/context_bridge/render_frame_context_bridge_store.cc",
"shell/renderer/api/context_bridge/render_frame_context_bridge_store.h",
"shell/renderer/api/atom_api_context_bridge.cc",
"shell/renderer/api/atom_api_context_bridge.h",
"shell/renderer/api/atom_api_renderer_ipc.cc",
"shell/renderer/api/atom_api_spell_check_client.cc",
"shell/renderer/api/atom_api_spell_check_client.h",
Expand Down
20 changes: 20 additions & 0 deletions lib/renderer/api/context-bridge.ts
@@ -0,0 +1,20 @@
const { hasSwitch } = process.electronBinding('command_line')
const binding = process.electronBinding('context_bridge')

const contextIsolationEnabled = hasSwitch('context-isolation')

const checkContextIsolationEnabled = () => {
if (!contextIsolationEnabled) throw new Error('contextBridge API can only be used when contextIsolation is enabled')
}

const contextBridge = {
exposeInMainWorld: (key: string, api: Record<string, any>) => {
checkContextIsolationEnabled()
return binding.exposeAPIInMainWorld(key, api)
},
debugGC: () => binding._debugGCMaps({})
}

if (!binding._debugGCMaps) delete contextBridge.debugGC

export default contextBridge
1 change: 1 addition & 0 deletions lib/renderer/api/module-list.js
Expand Up @@ -8,6 +8,7 @@ const enableRemoteModule = v8Util.getHiddenValue(global, 'enableRemoteModule')
// Renderer side modules, please sort alphabetically.
// A module is `enabled` if there is no explicit condition defined.
module.exports = [
{ name: 'contextBridge', loader: () => require('./context-bridge') },
{ name: 'crashReporter', loader: () => require('./crash-reporter') },
{ name: 'ipcRenderer', loader: () => require('./ipc-renderer') },
{ name: 'webFrame', loader: () => require('./web-frame') }
Expand Down
4 changes: 4 additions & 0 deletions lib/sandboxed_renderer/api/module-list.js
Expand Up @@ -3,6 +3,10 @@
const features = process.electronBinding('features')

module.exports = [
{
name: 'contextBridge',
loader: () => require('@electron/internal/renderer/api/context-bridge')
},
{
name: 'crashReporter',
load: () => require('@electron/internal/renderer/api/crash-reporter')
Expand Down
4 changes: 2 additions & 2 deletions native_mate/native_mate/arguments.h
Expand Up @@ -27,7 +27,7 @@ class Arguments {

template <typename T>
bool GetHolder(T* out) {
return ConvertFromV8(isolate_, info_->Holder(), out);
return mate::ConvertFromV8(isolate_, info_->Holder(), out);
}

template <typename T>
Expand All @@ -53,7 +53,7 @@ class Arguments {
return false;
}
v8::Local<v8::Value> val = (*info_)[next_];
bool success = ConvertFromV8(isolate_, val, out);
bool success = mate::ConvertFromV8(isolate_, val, out);
if (success)
next_++;
return success;
Expand Down
17 changes: 17 additions & 0 deletions native_mate/native_mate/dictionary.h
Expand Up @@ -40,6 +40,12 @@ class Dictionary {

static Dictionary CreateEmpty(v8::Isolate* isolate);

bool Has(base::StringPiece key) const {
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::String> v8_key = StringToV8(isolate_, key);
return internal::IsTrue(GetHandle()->Has(context, v8_key));
}

template <typename T>
bool Get(base::StringPiece key, T* out) const {
// Check for existence before getting, otherwise this method will always
Expand Down Expand Up @@ -102,6 +108,17 @@ class Dictionary {
return !result.IsNothing() && result.FromJust();
}

template <typename T>
bool SetReadOnlyNonConfigurable(base::StringPiece key, T val) {
v8::Local<v8::Value> v8_value;
if (!TryConvertToV8(isolate_, val, &v8_value))
return false;
v8::Maybe<bool> result = GetHandle()->DefineOwnProperty(
isolate_->GetCurrentContext(), StringToV8(isolate_, key), v8_value,
static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete));
return !result.IsNothing() && result.FromJust();
}

template <typename T>
bool SetMethod(base::StringPiece key, const T& callback) {
return GetHandle()
Expand Down
1 change: 1 addition & 0 deletions shell/common/node_bindings.cc
Expand Up @@ -66,6 +66,7 @@
V(atom_common_screen) \
V(atom_common_shell) \
V(atom_common_v8_util) \
V(atom_renderer_context_bridge) \
V(atom_renderer_ipc) \
V(atom_renderer_web_frame)

Expand Down
10 changes: 10 additions & 0 deletions shell/common/promise_util.h
Expand Up @@ -119,6 +119,16 @@ class Promise {
return GetInner()->Reject(GetContext(), v8::Undefined(isolate()));
}

v8::Maybe<bool> Reject(v8::Local<v8::Value> exception) {
v8::HandleScope handle_scope(isolate());
v8::MicrotasksScope script_scope(isolate(),
v8::MicrotasksScope::kRunMicrotasks);
v8::Context::Scope context_scope(
v8::Local<v8::Context>::New(isolate(), GetContext()));

return GetInner()->Reject(GetContext(), exception);
}

// Please note that using Then is effectively the same as calling .then
// in javascript. This means (a) it is not type safe and (b) please note
// it is NOT type safe.
Expand Down