Compare commits

..

35 Commits

Author SHA1 Message Date
Electron Bot
6d0a0319e1 Bump v13.0.0-nightly.20201209 2020-12-09 06:32:34 -08:00
Mimi
19ff18ac40 build: remove boto from git submodule (#26877) 2020-12-08 20:39:20 -08:00
Milan Burda
c41b8d536b refactor: move IPC handlers from navigation-controller to rpc-server (#26846) 2020-12-08 10:46:08 -08:00
Michaela Laurencin
7677576da8 chore: add @mlaurencin to manual backport config (#26872) 2020-12-08 10:04:02 -08:00
Electron Bot
1abd36f6c8 Bump v13.0.0-nightly.20201208 2020-12-08 06:33:18 -08:00
David Sanders
788e51127f chore: blank lines before lists in markdown (#26793) 2020-12-08 16:08:19 +09:00
Milan Burda
ee0550efca fix: systemPreferences.effectiveAppearance returning systemPreferences.getAppLevelAppearance() (#26852) 2020-12-08 16:07:04 +09:00
Shelley Vohr
b788ceb7bd fix: screen EventEmitter methods with remote (#26809)
* fix: screen EventEmitter methods with remote

* Review feedback
2020-12-08 13:47:48 +09:00
Erick Zhao
e87061398b docs: update OSR max FPS number (#26805) 2020-12-08 13:41:09 +09:00
Milan Burda
c9b813a1f9 refactor: convert more C++ enums to C++11 enum classes (#26850) 2020-12-08 13:39:33 +09:00
Mark Lee
3bc220db29 docs: clean up the native modules documentation (#26813) 2020-12-08 13:28:59 +09:00
windwalkr
6001f03e46 docs: add description to read-me.md (#26823)
Unhandled exception error is received if description is not defined while running "npm run make."  Defining a description solves this issue.
2020-12-08 13:26:29 +09:00
Samuel Attard
771e34a53a feat: route frame based permission checks through our permission check handler (#19903)
* feat: route frame based permission checks through our permission check handler

* docs: add change to setPermissionCheckHandler to breaking changes doc
2020-12-07 15:44:56 -08:00
Samuel Attard
3db4e612f4 fix: handle security warnings promise when JS is disabled (#26837) 2020-12-07 10:58:00 -08:00
Mimi
03b43e4d8c build: use python3 for electron hooks (#26839) 2020-12-07 10:57:41 -08:00
Alexey Kuzmin
e89b3ca1d1 fix: add a "set" trap to the "screen" module proxy (#26818) 2020-12-07 09:20:50 -08:00
Jeremy Rose
d3b1566181 chore: remove unused _replyInternal method (#26825) 2020-12-07 09:19:26 -08:00
Electron Bot
5a5d964720 Bump v13.0.0-nightly.20201207 2020-12-07 06:31:40 -08:00
Shelley Vohr
228a184b48 chore: remove unused FindByID helper (#26826) 2020-12-06 09:33:02 -08:00
Nikita Kot
7672aa9525 feat: exposeInMainWorld allow to expose non-object APIs (#26594) 2020-12-04 09:43:20 -08:00
Milan Burda
b111bba387 fix: send IPC_MESSAGES.RENDERER_RELEASE_CALLBACK as internal message (#26808) 2020-12-04 19:09:08 +03:00
Electron Bot
b133b6fd45 Bump v13.0.0-nightly.20201204 2020-12-04 06:32:29 -08:00
Electron Bot
45eee46864 Bump v13.0.0-nightly.20201203 2020-12-03 06:32:05 -08:00
Eli Skeggs
5521f8acca feat: allow path override with --ignore-scripts (#25377)
If you --ignore-scripts when installing electron currently, it'll fail
to write the path.txt file and thus fail to use the override dist path.
Open to other solutions - just hoping to be able to use a prebuilt
electron binary with the default package without having to muck around
with it installing an unused version.
2020-12-03 16:23:44 +09:00
Milan Burda
b37982987a chore: remove unused sendToAll + related APIs (#26771)
* chore: remove unused sendToAll + related APIs

* refactor: no need to args.ShallowClone() anymore
2020-12-03 15:55:50 +09:00
Electron Bot
8eee9d1290 Bump v13.0.0-nightly.20201202 2020-12-02 06:32:14 -08:00
Antonio
6fc5ff77c1 docs: app distribution page (#26239)
* docs: first draft of the app distribution page

* docs: second iteration of the app distribution page. Fixed mentions

* docs: third iteration of the app distribution page. Fixed mentions

* docs: reworked app distribution page according to mentions

* docs: minor fixes to the app distribution page according to mentions
2020-12-02 15:52:12 +09:00
Cheng Zhao
cffb51e141 chore: remove TODO on SetHidden calls (#26746) 2020-12-01 21:36:23 -08:00
Shelley Vohr
e96fa95b94 fix: properly emit after hooks after exception (#26752) 2020-12-01 21:34:08 -08:00
Vadim
efca7007b6 fix: internalModuleReadJSON for unpacked JSON (#26749) 2020-12-01 21:33:39 -08:00
Milan Burda
c2909a3b8d docs: BrowserWindow extension APIs are deprecated in Electron 9 (#26722) 2020-12-01 18:27:19 -06:00
Jim Fisher
430189fa84 docs: fix contentTracing code sample (#26737) 2020-12-01 15:45:45 -08:00
Shelley Vohr
94381cda49 docs: add debugging vars to env var doc (#26743) 2020-12-01 15:04:07 -08:00
Shelley Vohr
528b0f0e74 fix: draggable views on BrowserViews on Windows (#26738) 2020-12-01 15:03:00 -08:00
PalmerAL
cdcee04bbe fix: Add default Bluetooth permission strings (#26730) 2020-12-01 11:34:39 -08:00
57 changed files with 739 additions and 554 deletions

1
.github/config.yml vendored
View File

@@ -36,5 +36,6 @@ authorizedUsers:
- loc
- MarshallOfSound
- miniak
- mlaurencin
- nornagon
- zcbenz

3
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "vendor/requests"]
path = vendor/requests
url = https://github.com/kennethreitz/requests
[submodule "vendor/boto"]
path = vendor/boto
url = https://github.com/boto/boto.git

4
DEPS
View File

@@ -159,7 +159,7 @@ hooks = [
'action': [
'python3',
'-c',
'import os, subprocess; os.chdir(os.path.join("src", "electron")); subprocess.check_call(["python", "script/lib/npx.py", "yarn@' + (Var("yarn_version")) + '", "install", "--frozen-lockfile"]);',
'import os, subprocess; os.chdir(os.path.join("src", "electron")); subprocess.check_call(["python3", "script/lib/npx.py", "yarn@' + (Var("yarn_version")) + '", "install", "--frozen-lockfile"]);',
],
},
{
@@ -169,7 +169,7 @@ hooks = [
'action': [
'python3',
'-c',
'import os, subprocess; os.chdir(os.path.join("src", "electron", "vendor", "requests")); subprocess.check_call(["python", "setup.py", "build"]);',
'import os, subprocess; os.chdir(os.path.join("src", "electron", "vendor", "requests")); subprocess.check_call(["python3", "setup.py", "build"]);',
],
},
]

View File

@@ -1 +1 @@
13.0.0-nightly.20201201
13.0.0-nightly.20201209

View File

@@ -91,11 +91,6 @@ These individual tutorials expand on topics discussed in the guide above.
* Electron Releases & Developer Feedback
* [Versioning Policy](tutorial/electron-versioning.md)
* [Release Timelines](tutorial/electron-timelines.md)
* [Packaging App Source Code with asar](tutorial/application-packaging.md)
* [Generating asar Archives](tutorial/application-packaging.md#generating-asar-archives)
* [Using asar Archives](tutorial/application-packaging.md#using-asar-archives)
* [Limitations](tutorial/application-packaging.md#limitations-of-the-node-api)
* [Adding Unpacked Files to asar Archives](tutorial/application-packaging.md#adding-unpacked-files-to-asar-archives)
* [Testing Widevine CDM](tutorial/testing-widevine-cdm.md)
---

View File

@@ -16,7 +16,7 @@ const { app, contentTracing } = require('electron')
app.whenReady().then(() => {
(async () => {
await contentTracing.startRecording({
include_categories: ['*']
included_categories: ['*']
})
console.log('Tracing started')
await new Promise(resolve => setTimeout(resolve, 5000))

View File

@@ -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')

View File

@@ -120,6 +120,24 @@ debugging purposes.
Prints Chrome's internal logging to the console.
### `ELECTRON_DEBUG_DRAG_REGIONS`
Adds coloration to draggable regions on [`BrowserView`](./browser-view.md)s on macOS - draggable regions will be colored
green and non-draggable regions will be colored red to aid debugging.
### `ELECTRON_DEBUG_NOTIFICATIONS`
Adds extra logs to [`Notification`](./notification.md) lifecycles on macOS to aid in debugging. Extra logging will be displayed when new Notifications are created or activated. They will also be displayed when common actions are taken: a notification is shown, dismissed, its button is clicked, or it is replied to.
Sample output:
```sh
Notification created (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76-F88EC9D73D44)
Notification displayed (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76-F88EC9D73D44)
Notification activated (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76-F88EC9D73D44)
Notification replied to (com.github.Electron:notification:EAF7B87C-A113-43D7-8E76-F88EC9D73D44)
```
### `ELECTRON_LOG_ASAR_READS`
When Electron reads from an ASAR file, log the read offset and file path to

View File

@@ -500,6 +500,7 @@ win.webContents.session.setCertificateVerifyProc((request, callback) => {
* `pointerLock` - Request to directly interpret mouse movements as an input method. Click [here](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_Lock_API) to know more.
* `fullscreen` - Request for the app to enter fullscreen mode.
* `openExternal` - Request to open links in external applications.
* `unknown` - An unrecognized permission request
* `callback` Function
* `permissionGranted` Boolean - Allow or deny the permission.
* `details` Object - Some properties are only available on certain permission types.
@@ -511,7 +512,9 @@ win.webContents.session.setCertificateVerifyProc((request, callback) => {
Sets the handler which can be used to respond to permission requests for the `session`.
Calling `callback(true)` will allow the permission and `callback(false)` will reject it.
To clear the handler, call `setPermissionRequestHandler(null)`.
To clear the handler, call `setPermissionRequestHandler(null)`. Please note that
you must also implement `setPermissionCheckHandler` to get complete permission handling.
Most web APIs do a permission check and then make a permission request if the check is denied.
```javascript
const { session } = require('electron')
@@ -527,28 +530,32 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents
#### `ses.setPermissionCheckHandler(handler)`
* `handler` Function\<Boolean> | null
* `webContents` [WebContents](web-contents.md) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin.
* `webContents` ([WebContents](web-contents.md) | null) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. Cross origin sub frames making permission checks will pass a `null` webContents to this handler. You should use `embeddingOrigin` and `requestingOrigin` to determine what origin the owning frame and the requesting frame are on respectively.
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, or `serial`.
* `requestingOrigin` String - The origin URL of the permission check
* `details` Object - Some properties are only available on certain permission types.
* `securityOrigin` String - The security origin of the `media` check.
* `mediaType` String - The type of media access being requested, can be `video`,
* `embeddingOrigin` String (optional) - The origin of the frame embedding the frame that made the permission check. Only set for cross-origin sub frames making permission checks.
* `securityOrigin` String (optional) - The security origin of the `media` check.
* `mediaType` String (optional) - The type of media access being requested, can be `video`,
`audio` or `unknown`
* `requestingUrl` String - The last URL the requesting frame loaded
* `requestingUrl` String (optional) - The last URL the requesting frame loaded. This is not provided for cross-origin sub frames making permission checks.
* `isMainFrame` Boolean - Whether the frame making the request is the main frame
Sets the handler which can be used to respond to permission checks for the `session`.
Returning `true` will allow the permission and `false` will reject it.
Returning `true` will allow the permission and `false` will reject it. Please note that
you must also implement `setPermissionRequestHandler` to get complete permission handling.
Most web APIs do a permission check and then make a permission request if the check is denied.
To clear the handler, call `setPermissionCheckHandler(null)`.
```javascript
const { session } = require('electron')
session.fromPartition('some-partition').setPermissionCheckHandler((webContents, permission) => {
if (webContents.getURL() === 'some-host' && permission === 'notifications') {
return false // denied
const url = require('url')
session.fromPartition('some-partition').setPermissionCheckHandler((webContents, permission, requestingOrigin) => {
if (new URL(requestingOrigin).hostname === 'some-host' && permission === 'notifications') {
return true // granted
}
return true
return false // denied
})
```

View File

@@ -1871,7 +1871,7 @@ Returns `Boolean` - If *offscreen rendering* is enabled returns whether it is cu
* `fps` Integer
If *offscreen rendering* is enabled sets the frame rate to the specified number.
Only values between 1 and 60 are accepted.
Only values between 1 and 240 are accepted.
#### `contents.getFrameRate()`
@@ -1969,7 +1969,7 @@ The zoom factor is the zoom percent divided by 100, so 300% = 3.0.
#### `contents.frameRate`
An `Integer` property that sets the frame rate of the web contents to the specified number.
Only values between 1 and 60 are accepted.
Only values between 1 and 240 are accepted.
Only applicable if *offscreen rendering* is enabled.

View File

@@ -14,6 +14,28 @@ This document uses the following convention to categorize breaking changes:
## Planned Breaking API Changes (13.0)
### API Changed: `session.setPermissionCheckHandler(handler)`
The `handler` methods first parameter was previously always a `webContents`, it can now sometimes be `null`. You should use the `requestingOrigin`, `embeddingOrigin` and `securityOrigin` properties to respond to the permission check correctly. As the `webContents` can be `null` it can no longer be relied on.
```js
// Old code
session.setPermissionCheckHandler((webContents, permission) => {
if (webContents.getURL().startsWith('https://google.com/') && permission === 'notification') {
return true
}
return false
})
// Replace with
session.setPermissionCheckHandler((webContents, permission, requestingOrigin) => {
if (new URL(requestingOrigin).hostname === 'google.com' && permission === 'notification') {
return true
}
return false
})
```
### Removed: `shell.moveItemToTrash()`
The deprecated synchronous `shell.moveItemToTrash()` API has been removed. Use
@@ -29,6 +51,7 @@ shell.trashItem(path).then(/* ... */)
### Removed: `BrowserWindow` extension APIs
The deprecated extension APIs have been removed:
* `BrowserWindow.addExtension(path)`
* `BrowserWindow.addDevToolsExtension(path)`
* `BrowserWindow.removeExtension(name)`
@@ -37,6 +60,7 @@ The deprecated extension APIs have been removed:
* `BrowserWindow.getDevToolsExtensions()`
Use the session APIs instead:
* `ses.loadExtension(path)`
* `ses.removeExtension(extension_id)`
* `ses.getAllExtensions()`
@@ -313,6 +337,47 @@ you should plan to update your native modules to be context aware.
For more detailed information see [#18397](https://github.com/electron/electron/issues/18397).
### Deprecated: `BrowserWindow` extension APIs
The following extension APIs have been deprecated:
* `BrowserWindow.addExtension(path)`
* `BrowserWindow.addDevToolsExtension(path)`
* `BrowserWindow.removeExtension(name)`
* `BrowserWindow.removeDevToolsExtension(name)`
* `BrowserWindow.getExtensions()`
* `BrowserWindow.getDevToolsExtensions()`
Use the session APIs instead:
* `ses.loadExtension(path)`
* `ses.removeExtension(extension_id)`
* `ses.getAllExtensions()`
```js
// Deprecated in Electron 9
BrowserWindow.addExtension(path)
BrowserWindow.addDevToolsExtension(path)
// Replace with
session.defaultSession.loadExtension(path)
```
```js
// Deprecated in Electron 9
BrowserWindow.removeExtension(name)
BrowserWindow.removeDevToolsExtension(name)
// Replace with
session.defaultSession.removeExtension(extension_id)
```
```js
// Deprecated in Electron 9
BrowserWindow.getExtensions()
BrowserWindow.getDevToolsExtensions()
// Replace with
session.defaultSession.getAllExtensions()
```
### Removed: `<webview>.getWebContents()`
This API, which was deprecated in Electron 8.0, is now removed.

View File

@@ -1,25 +1,38 @@
# Application Distribution
To distribute your app with Electron, you need to package and rebrand it. The easiest way to do this is to use one of the following third party packaging tools:
## Overview
To distribute your app with Electron, you need to package and rebrand it.
To do this, you can either use specialized tooling or manual approaches.
## With tooling
You can use the following tools to distribute your application:
* [electron-forge](https://github.com/electron-userland/electron-forge)
* [electron-builder](https://github.com/electron-userland/electron-builder)
* [electron-packager](https://github.com/electron/electron-packager)
These tools will take care of all the steps you need to take to end up with a distributable Electron applications, such as packaging your application, rebranding the executable, setting the right icons and optionally creating installers.
These tools will take care of all the steps you need to take to end up with a
distributable Electron application, such as bundling your application,
rebranding the executable, and setting the right icons.
You can check the example of how to package your app with `electron-forge` in
our [Quick Start Guide](quick-start.md#package-and-distribute-the-application).
## Manual distribution
You can also choose to manually get your app ready for distribution. The steps needed to do this are outlined below.
### With prebuilt binaries
To distribute your app with Electron, you need to download Electron's [prebuilt
To distribute your app manually, you need to download Electron's [prebuilt
binaries](https://github.com/electron/electron/releases). Next, the folder
containing your app should be named `app` and placed in Electron's resources
directory as shown in the following examples. Note that the location of
Electron's prebuilt binaries is indicated with `electron/` in the examples
below.
directory as shown in the following examples.
On macOS:
> *NOTE:* the location of Electron's prebuilt binaries is indicated
with `electron/` in the examples below.
*On macOS:*
```plaintext
electron/Electron.app/Contents/Resources/app/
@@ -28,7 +41,7 @@ electron/Electron.app/Contents/Resources/app/
└── index.html
```
On Windows and Linux:
*On Windows and Linux:*
```plaintext
electron/resources/app
@@ -37,47 +50,44 @@ electron/resources/app
└── index.html
```
Then execute `Electron.app` (or `electron` on Linux, `electron.exe` on Windows),
and Electron will start as your app. The `electron` directory will then be
your distribution to deliver to final users.
Then execute `Electron.app` on macOS, `electron` on Linux, or `electron.exe`
on Windows, and Electron will start as your app. The `electron` directory
will then be your distribution to deliver to users.
## Packaging Your App into a File
### With an app source code archive
Apart from shipping your app by copying all of its source files, you can also
package your app into an [asar](https://github.com/electron/asar) archive to avoid
exposing your app's source code to users.
Instead of from shipping your app by copying all of its source files, you can
package your app into an [asar] archive to improve the performance of reading
files on platforms like Windows, if you are not already using a bundler such
as Parcel or Webpack.
To use an `asar` archive to replace the `app` folder, you need to rename the
archive to `app.asar`, and put it under Electron's resources directory like
below, and Electron will then try to read the archive and start from it.
On macOS:
*On macOS:*
```plaintext
electron/Electron.app/Contents/Resources/
└── app.asar
```
On Windows and Linux:
*On Windows and Linux:*
```plaintext
electron/resources/
└── app.asar
```
More details can be found in [Application packaging](application-packaging.md).
You can find more details on how to use `asar` in the
[`electron/asar` repository][asar].
## Rebranding with Downloaded Binaries
### Rebranding with downloaded binaries
After bundling your app into Electron, you will want to rebrand Electron
before distributing it to users.
### Windows
You can rename `electron.exe` to any name you like, and edit its icon and other
information with tools like [rcedit](https://github.com/electron/rcedit).
### macOS
#### macOS
You can rename `Electron.app` to any name you want, and you also have to rename
the `CFBundleDisplayName`, `CFBundleIdentifier` and `CFBundleName` fields in the
@@ -104,60 +114,20 @@ MyApp.app/Contents
   └── MyApp Helper
```
### Linux
#### Windows
You can rename `electron.exe` to any name you like, and edit its icon and other
information with tools like [rcedit](https://github.com/electron/rcedit).
#### Linux
You can rename the `electron` executable to any name you like.
## Rebranding by Rebuilding Electron from Source
### Rebranding by rebuilding Electron from source
It is also possible to rebrand Electron by changing the product name and
building it from source. To do this you need to set the build argument
corresponding to the product name (`electron_product_name = "YourProductName"`)
in the `args.gn` file and rebuild.
### Creating a Custom Electron Fork
Creating a custom fork of Electron is almost certainly not something you will
need to do in order to build your app, even for "Production Level" applications.
Using a tool such as `electron-packager` or `electron-forge` will allow you to
"Rebrand" Electron without having to do these steps.
You need to fork Electron when you have custom C++ code that you have patched
directly into Electron, that either cannot be upstreamed, or has been rejected
from the official version. As maintainers of Electron, we very much would like
to make your scenario work, so please try as hard as you can to get your changes
into the official version of Electron, it will be much much easier on you, and
we appreciate your help.
#### Creating a Custom Release with surf-build
1. Install [Surf](https://github.com/surf-build/surf), via npm:
`npm install -g surf-build@latest`
2. Create a new S3 bucket and create the following empty directory structure:
```sh
- electron/
- symbols/
- dist/
```
3. Set the following Environment Variables:
* `ELECTRON_GITHUB_TOKEN` - a token that can create releases on GitHub
* `ELECTRON_S3_ACCESS_KEY`, `ELECTRON_S3_BUCKET`, `ELECTRON_S3_SECRET_KEY` -
the place where you'll upload Node.js headers as well as symbols
* `ELECTRON_RELEASE` - Set to `true` and the upload part will run, leave unset
and `surf-build` will do CI-type checks, appropriate to run for every
pull request.
* `CI` - Set to `true` or else it will fail
* `GITHUB_TOKEN` - set it to the same as `ELECTRON_GITHUB_TOKEN`
* `SURF_TEMP` - set to `C:\Temp` on Windows to prevent path too long issues
* `TARGET_ARCH` - set to `ia32` or `x64`
4. In `script/upload.py`, you _must_ set `ELECTRON_REPO` to your fork (`MYORG/electron`),
especially if you are a contributor to Electron proper.
5. `surf-build -r https://github.com/MYORG/electron -s YOUR_COMMIT -n 'surf-PLATFORM-ARCH'`
6. Wait a very, very long time for the build to complete.
[asar]: https://github.com/electron/asar

View File

@@ -1,194 +0,0 @@
# Application Packaging
To mitigate [issues](https://github.com/joyent/node/issues/6960) around long
path names on Windows, slightly speed up `require` and conceal your source code
from cursory inspection, you can choose to package your app into an [asar][asar]
archive with little changes to your source code.
Most users will get this feature for free, since it's supported out of the box
by [`electron-packager`][electron-packager], [`electron-forge`][electron-forge],
and [`electron-builder`][electron-builder]. If you are not using any of these
tools, read on.
## Generating `asar` Archives
An [asar][asar] archive is a simple tar-like format that concatenates files
into a single file. Electron can read arbitrary files from it without unpacking
the whole file.
Steps to package your app into an `asar` archive:
### 1. Install the asar Utility
```sh
$ npm install -g asar
```
### 2. Package with `asar pack`
```sh
$ asar pack your-app app.asar
```
## Using `asar` Archives
In Electron there are two sets of APIs: Node APIs provided by Node.js and Web
APIs provided by Chromium. Both APIs support reading files from `asar` archives.
### Node API
With special patches in Electron, Node APIs like `fs.readFile` and `require`
treat `asar` archives as virtual directories, and the files in it as normal
files in the filesystem.
For example, suppose we have an `example.asar` archive under `/path/to`:
```sh
$ asar list /path/to/example.asar
/app.js
/file.txt
/dir/module.js
/static/index.html
/static/main.css
/static/jquery.min.js
```
Read a file in the `asar` archive:
```javascript
const fs = require('fs')
fs.readFileSync('/path/to/example.asar/file.txt')
```
List all files under the root of the archive:
```javascript
const fs = require('fs')
fs.readdirSync('/path/to/example.asar')
```
Use a module from the archive:
```javascript
require('./path/to/example.asar/dir/module.js')
```
You can also display a web page in an `asar` archive with `BrowserWindow`:
```javascript
const { BrowserWindow } = require('electron')
const win = new BrowserWindow()
win.loadURL('file:///path/to/example.asar/static/index.html')
```
### Web API
In a web page, files in an archive can be requested with the `file:` protocol.
Like the Node API, `asar` archives are treated as directories.
For example, to get a file with `$.get`:
```html
<script>
let $ = require('./jquery.min.js')
$.get('file:///path/to/example.asar/file.txt', (data) => {
console.log(data)
})
</script>
```
### Treating an `asar` Archive as a Normal File
For some cases like verifying the `asar` archive's checksum, we need to read the
content of an `asar` archive as a file. For this purpose you can use the built-in
`original-fs` module which provides original `fs` APIs without `asar` support:
```javascript
const originalFs = require('original-fs')
originalFs.readFileSync('/path/to/example.asar')
```
You can also set `process.noAsar` to `true` to disable the support for `asar` in
the `fs` module:
```javascript
const fs = require('fs')
process.noAsar = true
fs.readFileSync('/path/to/example.asar')
```
## Limitations of the Node API
Even though we tried hard to make `asar` archives in the Node API work like
directories as much as possible, there are still limitations due to the
low-level nature of the Node API.
### Archives Are Read-only
The archives can not be modified so all Node APIs that can modify files will not
work with `asar` archives.
### Working Directory Can Not Be Set to Directories in Archive
Though `asar` archives are treated as directories, there are no actual
directories in the filesystem, so you can never set the working directory to
directories in `asar` archives. Passing them as the `cwd` option of some APIs
will also cause errors.
### Extra Unpacking on Some APIs
Most `fs` APIs can read a file or get a file's information from `asar` archives
without unpacking, but for some APIs that rely on passing the real file path to
underlying system calls, Electron will extract the needed file into a
temporary file and pass the path of the temporary file to the APIs to make them
work. This adds a little overhead for those APIs.
APIs that requires extra unpacking are:
* `child_process.execFile`
* `child_process.execFileSync`
* `fs.open`
* `fs.openSync`
* `process.dlopen` - Used by `require` on native modules
### Fake Stat Information of `fs.stat`
The `Stats` object returned by `fs.stat` and its friends on files in `asar`
archives is generated by guessing, because those files do not exist on the
filesystem. So you should not trust the `Stats` object except for getting file
size and checking file type.
### Executing Binaries Inside `asar` Archive
There are Node APIs that can execute binaries like `child_process.exec`,
`child_process.spawn` and `child_process.execFile`, but only `execFile` is
supported to execute binaries inside `asar` archive.
This is because `exec` and `spawn` accept `command` instead of `file` as input,
and `command`s are executed under shell. There is no reliable way to determine
whether a command uses a file in asar archive, and even if we do, we can not be
sure whether we can replace the path in command without side effects.
## Adding Unpacked Files to `asar` Archives
As stated above, some Node APIs will unpack the file to the filesystem when
called. Apart from the performance issues, various anti-virus scanners might
be triggered by this behavior.
As a workaround, you can leave various files unpacked using the `--unpack` option.
In the following example, shared libraries of native Node.js modules will not be
packed:
```sh
$ asar pack app app.asar --unpack *.node
```
After running the command, you will notice that a folder named `app.asar.unpacked`
was created together with the `app.asar` file. It contains the unpacked files
and should be shipped together with the `app.asar` archive.
[asar]: https://github.com/electron/asar
[electron-packager]: https://github.com/electron/electron-packager
[electron-forge]: https://github.com/electron-userland/electron-forge
[electron-builder]: https://github.com/electron-userland/electron-builder

View File

@@ -13,7 +13,7 @@ project.
* There are two rendering modes that can be used (see the section below) and only
the dirty area is passed to the `paint` event to be more efficient.
* You can stop/continue the rendering as well as set the frame rate.
* The maximum frame rate is 60 because greater values bring only performance
* The maximum frame rate is 240 because greater values bring only performance
losses with no benefits.
* When nothing is happening on a webpage, no frames are generated.
* An offscreen window is always created as a

View File

@@ -124,6 +124,7 @@ Your Electron application uses the `package.json` file as the main entry point (
{
"name": "my-electron-app",
"version": "0.1.0",
"description": "My Electron app",
"main": "main.js"
}
```

View File

@@ -1,8 +1,9 @@
# Using Native Node Modules
Native Node modules are supported by Electron, but since Electron is very
likely to use a different V8 version from the Node binary installed on your
system, the modules you use will need to be recompiled for Electron. Otherwise,
Native Node.js modules are supported by Electron, but since Electron has a different
[application binary interface (ABI)][abi] from a given Node.js binary (due to
differences such as using Chromium's BoringSSL instead of OpenSSL), the native
modules you use will need to be recompiled for Electron. Otherwise,
you will get the following class of error when you try to run your app:
```sh
@@ -23,9 +24,11 @@ You can install modules like other Node projects, and then rebuild the modules
for Electron with the [`electron-rebuild`][electron-rebuild] package. This
module can automatically determine the version of Electron and handle the
manual steps of downloading headers and rebuilding native modules for your app.
If you are using [Electron Forge][electron-forge], this tool is used automatically
in both development mode and when making distributables.
For example, to install `electron-rebuild` and then rebuild modules with it
via the command line:
For example, to install the standalone `electron-rebuild` tool and then rebuild
modules with it via the command line:
```sh
npm install --save-dev electron-rebuild
@@ -33,12 +36,12 @@ npm install --save-dev electron-rebuild
# Every time you run "npm install", run this:
./node_modules/.bin/electron-rebuild
# On Windows if you have trouble, try:
# If you have trouble on Windows, try:
.\node_modules\.bin\electron-rebuild.cmd
```
For more information on usage and integration with other tools, consult the
project's README.
For more information on usage and integration with other tools such as [Electron
Packager][electron-packager], consult the project's README.
### Using `npm`
@@ -147,23 +150,25 @@ for an example delay-load hook if you're implementing your own.
native Node modules with prebuilt binaries for multiple versions of Node
and Electron.
If modules provide binaries for the usage in Electron, make sure to omit
`--build-from-source` and the `npm_config_build_from_source` environment
variable in order to take full advantage of the prebuilt binaries.
If the `prebuild`-powered module provide binaries for the usage in Electron,
make sure to omit `--build-from-source` and the `npm_config_build_from_source`
environment variable in order to take full advantage of the prebuilt binaries.
## Modules that rely on `node-pre-gyp`
The [`node-pre-gyp` tool][node-pre-gyp] provides a way to deploy native Node
modules with prebuilt binaries, and many popular modules are using it.
Usually those modules work fine under Electron, but sometimes when Electron uses
a newer version of V8 than Node and/or there are ABI changes, bad things may
happen. So in general, it is recommended to always build native modules from
source code. `electron-rebuild` handles this for you automatically.
Sometimes those modules work fine under Electron, but when there are no
Electron-specific binaries available, you'll need to build from source.
Because of this, it is recommended to use `electron-rebuild` for these modules.
If you are following the `npm` way of installing modules, then this is done
by default, if not, you have to pass `--build-from-source` to `npm`, or set the
`npm_config_build_from_source` environment variable.
If you are following the `npm` way of installing modules, you'll need to pass
`--build-from-source` to `npm`, or set the `npm_config_build_from_source`
environment variable.
[abi]: https://en.wikipedia.org/wiki/Application_binary_interface
[electron-rebuild]: https://github.com/electron/electron-rebuild
[electron-forge]: https://electronforge.io/
[electron-packager]: https://github.com/electron/electron-packager
[node-pre-gyp]: https://github.com/mapbox/node-pre-gyp

View File

@@ -689,7 +689,8 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
if (info.size === 0) return ['', false];
if (info.unpacked) {
const realPath = archive.copyFileOut(filePath);
return fs.readFileSync(realPath, { encoding: 'utf8' });
const str = fs.readFileSync(realPath, { encoding: 'utf8' });
return [str, str.length > 0];
}
logASARAccess(asarPath, filePath, info.offset);

View File

@@ -1,38 +1,48 @@
import { EventEmitter } from 'events';
const { createScreen } = process._linkedBinding('electron_common_screen');
let _screen: Electron.Screen;
const createScreenIfNeeded = () => {
if (_screen === undefined) {
_screen = createScreen();
}
};
// We can't call createScreen until after app.on('ready'), but this module
// exposes an instance created by createScreen. In order to avoid
// side-effecting and calling createScreen upon import of this module, instead
// we export a proxy which lazily calls createScreen on first access.
export default new Proxy({}, {
get: (target, prop: keyof Electron.Screen) => {
if (_screen === undefined) {
_screen = createScreen();
get: (target, property: keyof Electron.Screen) => {
createScreenIfNeeded();
const value = _screen[property];
if (typeof value === 'function') {
return value.bind(_screen);
}
const v = _screen[prop];
if (typeof v === 'function') {
return v.bind(_screen);
}
return v;
return value;
},
set: (target, property: string, value: unknown) => {
createScreenIfNeeded();
return Reflect.set(_screen, property, value);
},
ownKeys: () => {
if (_screen === undefined) {
_screen = createScreen();
}
createScreenIfNeeded();
return Reflect.ownKeys(_screen);
},
has: (target, prop: string) => {
if (_screen === undefined) {
_screen = createScreen();
}
return prop in _screen;
has: (target, property: string) => {
createScreenIfNeeded();
return property in _screen;
},
getOwnPropertyDescriptor: (target, prop: string) => {
if (_screen === undefined) {
_screen = createScreen();
}
return Reflect.getOwnPropertyDescriptor(_screen, prop);
getOwnPropertyDescriptor: (target, property: string) => {
createScreenIfNeeded();
return Reflect.getOwnPropertyDescriptor(_screen, property);
},
getPrototypeOf: () => {
// This is necessary as a result of weirdness with EventEmitterMixin
// and FunctionTemplate - we need to explicitly ensure it's returned
// in the prototype.
return EventEmitter.prototype;
}
});

View File

@@ -11,7 +11,7 @@ if ('getAppLevelAppearance' in systemPreferences) {
}
if ('getEffectiveAppearance' in systemPreferences) {
const nativeEAGetter = systemPreferences.getAppLevelAppearance;
const nativeEAGetter = systemPreferences.getEffectiveAppearance;
Object.defineProperty(systemPreferences, 'effectiveAppearance', {
get: () => nativeEAGetter.call(systemPreferences)
});

View File

@@ -131,10 +131,7 @@ WebContents.prototype.send = function (channel, ...args) {
throw new Error('Missing required channel argument');
}
const internal = false;
const sendToAll = false;
return this._send(internal, sendToAll, channel, args);
return this._send(false /* internal */, channel, args);
};
WebContents.prototype.postMessage = function (...args) {
@@ -149,20 +146,7 @@ WebContents.prototype._sendInternal = function (channel, ...args) {
throw new Error('Missing required channel argument');
}
const internal = true;
const sendToAll = false;
return this._send(internal, sendToAll, channel, args);
};
WebContents.prototype._sendInternalToAll = function (channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument');
}
const internal = true;
const sendToAll = true;
return this._send(internal, sendToAll, channel, args);
return this._send(true /* internal */, channel, args);
};
WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
if (typeof channel !== 'string') {
@@ -171,10 +155,7 @@ WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
throw new Error('Missing required frameId argument');
}
const internal = false;
const sendToAll = false;
return this._sendToFrame(internal, sendToAll, frameId, channel, args);
return this._sendToFrame(false /* internal */, frameId, channel, args);
};
WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args) {
if (typeof channel !== 'string') {
@@ -183,10 +164,7 @@ WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args
throw new Error('Missing required frameId argument');
}
const internal = true;
const sendToAll = false;
return this._sendToFrame(internal, sendToAll, frameId, channel, args);
return this._sendToFrame(true /* internal */, frameId, channel, args);
};
// Following methods are mapped to webFrame.
@@ -199,7 +177,7 @@ const webFrameMethods = [
for (const method of webFrameMethods) {
WebContents.prototype[method] = function (...args: any[]): Promise<any> {
return ipcMainUtils.invokeInWebContents(this, false, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, method, ...args);
return ipcMainUtils.invokeInWebContents(this, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, method, ...args);
};
}
@@ -217,11 +195,11 @@ const waitTillCanExecuteJavaScript = async (webContents: Electron.WebContents) =
// WebContents has been loaded.
WebContents.prototype.executeJavaScript = async function (code, hasUserGesture) {
await waitTillCanExecuteJavaScript(this);
return ipcMainUtils.invokeInWebContents(this, false, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, 'executeJavaScript', String(code), !!hasUserGesture);
return ipcMainUtils.invokeInWebContents(this, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, 'executeJavaScript', String(code), !!hasUserGesture);
};
WebContents.prototype.executeJavaScriptInIsolatedWorld = async function (worldId, code, hasUserGesture) {
await waitTillCanExecuteJavaScript(this);
return ipcMainUtils.invokeInWebContents(this, false, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, 'executeJavaScriptInIsolatedWorld', worldId, code, !!hasUserGesture);
return ipcMainUtils.invokeInWebContents(this, IPC_MESSAGES.RENDERER_WEB_FRAME_METHOD, 'executeJavaScriptInIsolatedWorld', worldId, code, !!hasUserGesture);
};
// Translate the options of printToPDF.
@@ -483,16 +461,6 @@ const addReplyToEvent = (event: any) => {
};
};
const addReplyInternalToEvent = (event: any) => {
Object.defineProperty(event, '_replyInternal', {
configurable: false,
enumerable: false,
value: (...args: any[]) => {
event.sender._sendToFrameInternal(event.frameId, ...args);
}
});
};
const addReturnValueToEvent = (event: any) => {
Object.defineProperty(event, 'returnValue', {
set: (value) => event.sendReply(value),
@@ -537,7 +505,6 @@ WebContents.prototype._init = function () {
// Dispatch IPC messages to the ipc module.
this.on('-ipc-message' as any, function (this: Electron.WebContents, event: any, internal: boolean, channel: string, args: any[]) {
if (internal) {
addReplyInternalToEvent(event);
ipcMainInternal.emit(channel, event, ...args);
} else {
addReplyToEvent(event);
@@ -563,7 +530,6 @@ WebContents.prototype._init = function () {
this.on('-ipc-message-sync' as any, function (this: Electron.WebContents, event: any, internal: boolean, channel: string, args: any[]) {
addReturnValueToEvent(event);
if (internal) {
addReplyInternalToEvent(event);
ipcMainInternal.emit(channel, event, ...args);
} else {
addReplyToEvent(event);

View File

@@ -14,7 +14,7 @@ export const handleSync = function <T extends IPCHandler> (channel: string, hand
let nextId = 0;
export function invokeInWebContents<T> (sender: Electron.WebContents, sendToAll: boolean, command: string, ...args: any[]) {
export function invokeInWebContents<T> (sender: Electron.WebContents, command: string, ...args: any[]) {
return new Promise<T>((resolve, reject) => {
const requestId = ++nextId;
const channel = `${command}_RESPONSE_${requestId}`;
@@ -33,10 +33,6 @@ export function invokeInWebContents<T> (sender: Electron.WebContents, sendToAll:
}
});
if (sendToAll) {
sender._sendInternalToAll(command, requestId, ...args);
} else {
sender._sendInternal(command, requestId, ...args);
}
sender._sendInternal(command, requestId, ...args);
});
}

View File

@@ -1,24 +1,5 @@
import { ipcMainInternal } from '@electron/internal/browser/ipc-main-internal';
import type { WebContents, LoadURLOptions } from 'electron/main';
import { EventEmitter } from 'events';
import { IPC_MESSAGES } from '@electron/internal/common/ipc-messages';
// The history operation in renderer is redirected to browser.
ipcMainInternal.on(IPC_MESSAGES.NAVIGATION_CONTROLLER_GO_BACK, function (event) {
event.sender.goBack();
});
ipcMainInternal.on(IPC_MESSAGES.NAVIGATION_CONTROLLER_GO_FORWARD, function (event) {
event.sender.goForward();
});
ipcMainInternal.on(IPC_MESSAGES.NAVIGATION_CONTROLLER_GO_TO_OFFSET, function (event, offset) {
event.sender.goToOffset(offset);
});
ipcMainInternal.on(IPC_MESSAGES.NAVIGATION_CONTROLLER_LENGTH, function (event) {
event.returnValue = event.sender.length();
});
// JavaScript implementation of Chromium's NavigationController.
// Instead of relying on Chromium for history control, we completely do history

View File

@@ -31,7 +31,7 @@ const finalizationRegistry = new FinalizationRegistry((fi: FinalizerInfo) => {
const ref = rendererFunctionCache.get(mapKey);
if (ref !== undefined && ref.deref() === undefined) {
rendererFunctionCache.delete(mapKey);
if (!fi.webContents.isDestroyed()) { fi.webContents.sendToFrame(fi.frameId, IPC_MESSAGES.RENDERER_RELEASE_CALLBACK, fi.id[0], fi.id[1]); }
if (!fi.webContents.isDestroyed()) { fi.webContents._sendToFrameInternal(fi.frameId, IPC_MESSAGES.RENDERER_RELEASE_CALLBACK, fi.id[0], fi.id[1]); }
}
});

View File

@@ -100,6 +100,22 @@ ipcMainUtils.handleSync(IPC_MESSAGES.BROWSER_SANDBOX_LOAD, async function (event
};
});
ipcMainInternal.on(IPC_MESSAGES.NAVIGATION_CONTROLLER_GO_BACK, function (event) {
event.sender.goBack();
});
ipcMainInternal.on(IPC_MESSAGES.NAVIGATION_CONTROLLER_GO_FORWARD, function (event) {
event.sender.goForward();
});
ipcMainInternal.on(IPC_MESSAGES.NAVIGATION_CONTROLLER_GO_TO_OFFSET, function (event, offset) {
event.sender.goToOffset(offset);
});
ipcMainInternal.on(IPC_MESSAGES.NAVIGATION_CONTROLLER_LENGTH, function (event) {
event.returnValue = event.sender.length();
});
ipcMainInternal.on(IPC_MESSAGES.BROWSER_PRELOAD_ERROR, function (event, preloadPath: string, error: Error) {
event.sender.emit('preload-error', event, preloadPath, error);
});

View File

@@ -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);
}

View File

@@ -18,7 +18,7 @@ ipcRenderer.sendToHost = function (channel, ...args) {
};
ipcRenderer.sendTo = function (webContentsId, channel, ...args) {
return ipc.sendTo(internal, false, webContentsId, channel, args);
return ipc.sendTo(internal, webContentsId, channel, args);
};
ipcRenderer.invoke = async function (channel, ...args) {

View File

@@ -14,11 +14,7 @@ ipcRendererInternal.sendSync = function (channel, ...args) {
};
ipcRendererInternal.sendTo = function (webContentsId, channel, ...args) {
return ipc.sendTo(internal, false, webContentsId, channel, args);
};
ipcRendererInternal.sendToAll = function (webContentsId, channel, ...args) {
return ipc.sendTo(internal, true, webContentsId, channel, args);
return ipc.sendTo(internal, webContentsId, channel, args);
};
ipcRendererInternal.invoke = async function<T> (channel: string, ...args: any[]) {

View File

@@ -180,7 +180,7 @@ const warnAboutInsecureCSP = function () {
console.warn('%cElectron Security Warning (Insecure Content-Security-Policy)',
'font-weight: bold;', warning);
});
}).catch(() => {});
};
/**

View File

@@ -4,11 +4,14 @@ const path = require('path');
const pathFile = path.join(__dirname, 'path.txt');
function getElectronPath () {
let executablePath;
if (fs.existsSync(pathFile)) {
const executablePath = fs.readFileSync(pathFile, 'utf-8');
if (process.env.ELECTRON_OVERRIDE_DIST_PATH) {
return path.join(process.env.ELECTRON_OVERRIDE_DIST_PATH, executablePath);
}
executablePath = fs.readFileSync(pathFile, 'utf-8');
}
if (process.env.ELECTRON_OVERRIDE_DIST_PATH) {
return path.join(process.env.ELECTRON_OVERRIDE_DIST_PATH, executablePath || 'electron');
}
if (executablePath) {
return path.join(__dirname, 'dist', executablePath);
} else {
throw new Error('Electron failed to install correctly, please delete node_modules/electron and try installing again');

View File

@@ -1,6 +1,6 @@
{
"name": "electron",
"version": "13.0.0-nightly.20201201",
"version": "13.0.0-nightly.20201209",
"repository": "https://github.com/electron/electron",
"description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
"devDependencies": {

View File

@@ -1321,12 +1321,8 @@ std::vector<gin_helper::Dictionary> App::GetAppMetrics(v8::Isolate* isolate) {
gin_helper::Dictionary pid_dict = gin::Dictionary::CreateEmpty(isolate);
gin_helper::Dictionary cpu_dict = gin::Dictionary::CreateEmpty(isolate);
// TODO(zcbenz): Just call SetHidden when this file is converted to gin.
gin_helper::Dictionary(isolate, pid_dict.GetHandle())
.SetHidden("simple", true);
gin_helper::Dictionary(isolate, cpu_dict.GetHandle())
.SetHidden("simple", true);
pid_dict.SetHidden("simple", true);
cpu_dict.SetHidden("simple", true);
cpu_dict.Set(
"percentCPUUsage",
process_metric.second->metrics->GetPlatformIndependentCPUUsage() /
@@ -1361,9 +1357,7 @@ std::vector<gin_helper::Dictionary> App::GetAppMetrics(v8::Isolate* isolate) {
auto memory_info = process_metric.second->GetMemoryInfo();
gin_helper::Dictionary memory_dict = gin::Dictionary::CreateEmpty(isolate);
// TODO(zcbenz): Just call SetHidden when this file is converted to gin.
gin_helper::Dictionary(isolate, memory_dict.GetHandle())
.SetHidden("simple", true);
memory_dict.SetHidden("simple", true);
memory_dict.Set("workingSetSize",
static_cast<double>(memory_info.working_set_size >> 10));
memory_dict.Set(

View File

@@ -33,15 +33,6 @@ gin::WrapperInfo Screen::kWrapperInfo = {gin::kEmbedderNativeGin};
namespace {
// Find an item in container according to its ID.
template <class T>
typename T::iterator FindById(T* container, int id) {
auto predicate = [id](const typename T::value_type& item) -> bool {
return item.id() == id;
};
return std::find_if(container->begin(), container->end(), predicate);
}
// Convert the changed_metrics bitmask to string array.
std::vector<std::string> MetricsToArray(uint32_t metrics) {
std::vector<std::string> array;

View File

@@ -26,7 +26,7 @@ namespace electron {
namespace api {
#if defined(OS_MAC)
enum NotificationCenterKind {
enum class NotificationCenterKind {
kNSDistributedNotificationCenter = 0,
kNSNotificationCenter,
kNSWorkspaceNotificationCenter,

View File

@@ -140,12 +140,13 @@ void SystemPreferences::PostNotification(const std::string& name,
int SystemPreferences::SubscribeNotification(
const std::string& name,
const NotificationCallback& callback) {
return DoSubscribeNotification(name, callback,
kNSDistributedNotificationCenter);
return DoSubscribeNotification(
name, callback, NotificationCenterKind::kNSDistributedNotificationCenter);
}
void SystemPreferences::UnsubscribeNotification(int request_id) {
DoUnsubscribeNotification(request_id, kNSDistributedNotificationCenter);
DoUnsubscribeNotification(
request_id, NotificationCenterKind::kNSDistributedNotificationCenter);
}
void SystemPreferences::PostLocalNotification(const std::string& name,
@@ -159,11 +160,13 @@ void SystemPreferences::PostLocalNotification(const std::string& name,
int SystemPreferences::SubscribeLocalNotification(
const std::string& name,
const NotificationCallback& callback) {
return DoSubscribeNotification(name, callback, kNSNotificationCenter);
return DoSubscribeNotification(name, callback,
NotificationCenterKind::kNSNotificationCenter);
}
void SystemPreferences::UnsubscribeLocalNotification(int request_id) {
DoUnsubscribeNotification(request_id, kNSNotificationCenter);
DoUnsubscribeNotification(request_id,
NotificationCenterKind::kNSNotificationCenter);
}
void SystemPreferences::PostWorkspaceNotification(
@@ -179,12 +182,13 @@ void SystemPreferences::PostWorkspaceNotification(
int SystemPreferences::SubscribeWorkspaceNotification(
const std::string& name,
const NotificationCallback& callback) {
return DoSubscribeNotification(name, callback,
kNSWorkspaceNotificationCenter);
return DoSubscribeNotification(
name, callback, NotificationCenterKind::kNSWorkspaceNotificationCenter);
}
void SystemPreferences::UnsubscribeWorkspaceNotification(int request_id) {
DoUnsubscribeNotification(request_id, kNSWorkspaceNotificationCenter);
DoUnsubscribeNotification(
request_id, NotificationCenterKind::kNSWorkspaceNotificationCenter);
}
int SystemPreferences::DoSubscribeNotification(
@@ -195,13 +199,13 @@ int SystemPreferences::DoSubscribeNotification(
__block NotificationCallback copied_callback = callback;
NSNotificationCenter* center;
switch (kind) {
case kNSDistributedNotificationCenter:
case NotificationCenterKind::kNSDistributedNotificationCenter:
center = [NSDistributedNotificationCenter defaultCenter];
break;
case kNSNotificationCenter:
case NotificationCenterKind::kNSNotificationCenter:
center = [NSNotificationCenter defaultCenter];
break;
case kNSWorkspaceNotificationCenter:
case NotificationCenterKind::kNSWorkspaceNotificationCenter:
center = [[NSWorkspace sharedWorkspace] notificationCenter];
break;
default:
@@ -239,13 +243,13 @@ void SystemPreferences::DoUnsubscribeNotification(int request_id,
id observer = iter->second;
NSNotificationCenter* center;
switch (kind) {
case kNSDistributedNotificationCenter:
case NotificationCenterKind::kNSDistributedNotificationCenter:
center = [NSDistributedNotificationCenter defaultCenter];
break;
case kNSNotificationCenter:
case NotificationCenterKind::kNSNotificationCenter:
center = [NSNotificationCenter defaultCenter];
break;
case kNSWorkspaceNotificationCenter:
case NotificationCenterKind::kNSWorkspaceNotificationCenter:
center = [[NSWorkspace sharedWorkspace] notificationCenter];
break;
default:

View File

@@ -1577,7 +1577,6 @@ void WebContents::MessageSync(bool internal,
}
void WebContents::MessageTo(bool internal,
bool send_to_all,
int32_t web_contents_id,
const std::string& channel,
blink::CloneableMessage arguments) {
@@ -1585,7 +1584,7 @@ void WebContents::MessageTo(bool internal,
auto* web_contents = FromID(web_contents_id);
if (web_contents) {
web_contents->SendIPCMessageWithSender(internal, send_to_all, channel,
web_contents->SendIPCMessageWithSender(internal, channel,
std::move(arguments), ID());
}
}
@@ -2685,7 +2684,6 @@ bool WebContents::IsFocused() const {
#endif
bool WebContents::SendIPCMessage(bool internal,
bool send_to_all,
const std::string& channel,
v8::Local<v8::Value> args) {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
@@ -2695,37 +2693,21 @@ bool WebContents::SendIPCMessage(bool internal,
gin::StringToV8(isolate, "Failed to serialize arguments")));
return false;
}
return SendIPCMessageWithSender(internal, send_to_all, channel,
std::move(message));
return SendIPCMessageWithSender(internal, channel, std::move(message));
}
bool WebContents::SendIPCMessageWithSender(bool internal,
bool send_to_all,
const std::string& channel,
blink::CloneableMessage args,
int32_t sender_id) {
std::vector<content::RenderFrameHost*> target_hosts;
if (!send_to_all) {
auto* frame_host = web_contents()->GetMainFrame();
if (frame_host) {
target_hosts.push_back(frame_host);
}
} else {
target_hosts = web_contents()->GetAllFrames();
}
for (auto* frame_host : target_hosts) {
mojo::AssociatedRemote<mojom::ElectronRenderer> electron_renderer;
frame_host->GetRemoteAssociatedInterfaces()->GetInterface(
&electron_renderer);
electron_renderer->Message(internal, false, channel, args.ShallowClone(),
sender_id);
}
auto* frame_host = web_contents()->GetMainFrame();
mojo::AssociatedRemote<mojom::ElectronRenderer> electron_renderer;
frame_host->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer);
electron_renderer->Message(internal, channel, std::move(args), sender_id);
return true;
}
bool WebContents::SendIPCMessageToFrame(bool internal,
bool send_to_all,
int32_t frame_id,
const std::string& channel,
v8::Local<v8::Value> args) {
@@ -2747,7 +2729,7 @@ bool WebContents::SendIPCMessageToFrame(bool internal,
mojo::AssociatedRemote<mojom::ElectronRenderer> electron_renderer;
(*iter)->GetRemoteAssociatedInterfaces()->GetInterface(&electron_renderer);
electron_renderer->Message(internal, send_to_all, channel, std::move(message),
electron_renderer->Message(internal, channel, std::move(message),
0 /* sender_id */);
return true;
}

View File

@@ -249,18 +249,15 @@ class WebContents : public gin::Wrappable<WebContents>,
// Send messages to browser.
bool SendIPCMessage(bool internal,
bool send_to_all,
const std::string& channel,
v8::Local<v8::Value> args);
bool SendIPCMessageWithSender(bool internal,
bool send_to_all,
const std::string& channel,
blink::CloneableMessage args,
int32_t sender_id = 0);
bool SendIPCMessageToFrame(bool internal,
bool send_to_all,
int32_t frame_id,
const std::string& channel,
v8::Local<v8::Value> args);
@@ -619,7 +616,6 @@ class WebContents : public gin::Wrappable<WebContents>,
blink::CloneableMessage arguments,
MessageSyncCallback callback) override;
void MessageTo(bool internal,
bool send_to_all,
int32_t web_contents_id,
const std::string& channel,
blink::CloneableMessage arguments) override;

View File

@@ -257,21 +257,26 @@ WebRequest::~WebRequest() {
gin::ObjectTemplateBuilder WebRequest::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin::Wrappable<WebRequest>::GetObjectTemplateBuilder(isolate)
.SetMethod("onBeforeRequest",
&WebRequest::SetResponseListener<kOnBeforeRequest>)
.SetMethod("onBeforeSendHeaders",
&WebRequest::SetResponseListener<kOnBeforeSendHeaders>)
.SetMethod("onHeadersReceived",
&WebRequest::SetResponseListener<kOnHeadersReceived>)
.SetMethod(
"onBeforeRequest",
&WebRequest::SetResponseListener<ResponseEvent::kOnBeforeRequest>)
.SetMethod(
"onBeforeSendHeaders",
&WebRequest::SetResponseListener<ResponseEvent::kOnBeforeSendHeaders>)
.SetMethod(
"onHeadersReceived",
&WebRequest::SetResponseListener<ResponseEvent::kOnHeadersReceived>)
.SetMethod("onSendHeaders",
&WebRequest::SetSimpleListener<kOnSendHeaders>)
&WebRequest::SetSimpleListener<SimpleEvent::kOnSendHeaders>)
.SetMethod("onBeforeRedirect",
&WebRequest::SetSimpleListener<kOnBeforeRedirect>)
.SetMethod("onResponseStarted",
&WebRequest::SetSimpleListener<kOnResponseStarted>)
&WebRequest::SetSimpleListener<SimpleEvent::kOnBeforeRedirect>)
.SetMethod(
"onResponseStarted",
&WebRequest::SetSimpleListener<SimpleEvent::kOnResponseStarted>)
.SetMethod("onErrorOccurred",
&WebRequest::SetSimpleListener<kOnErrorOccurred>)
.SetMethod("onCompleted", &WebRequest::SetSimpleListener<kOnCompleted>);
&WebRequest::SetSimpleListener<SimpleEvent::kOnErrorOccurred>)
.SetMethod("onCompleted",
&WebRequest::SetSimpleListener<SimpleEvent::kOnCompleted>);
}
const char* WebRequest::GetTypeName() {
@@ -286,8 +291,8 @@ int WebRequest::OnBeforeRequest(extensions::WebRequestInfo* info,
const network::ResourceRequest& request,
net::CompletionOnceCallback callback,
GURL* new_url) {
return HandleResponseEvent(kOnBeforeRequest, info, std::move(callback),
new_url, request);
return HandleResponseEvent(ResponseEvent::kOnBeforeRequest, info,
std::move(callback), new_url, request);
}
int WebRequest::OnBeforeSendHeaders(extensions::WebRequestInfo* info,
@@ -295,7 +300,7 @@ int WebRequest::OnBeforeSendHeaders(extensions::WebRequestInfo* info,
BeforeSendHeadersCallback callback,
net::HttpRequestHeaders* headers) {
return HandleResponseEvent(
kOnBeforeSendHeaders, info,
ResponseEvent::kOnBeforeSendHeaders, info,
base::BindOnce(std::move(callback), std::set<std::string>(),
std::set<std::string>()),
headers, request, *headers);
@@ -312,25 +317,26 @@ int WebRequest::OnHeadersReceived(
original_response_headers ? original_response_headers->GetStatusLine()
: std::string();
return HandleResponseEvent(
kOnHeadersReceived, info, std::move(callback),
ResponseEvent::kOnHeadersReceived, info, std::move(callback),
std::make_pair(override_response_headers, status_line), request);
}
void WebRequest::OnSendHeaders(extensions::WebRequestInfo* info,
const network::ResourceRequest& request,
const net::HttpRequestHeaders& headers) {
HandleSimpleEvent(kOnSendHeaders, info, request, headers);
HandleSimpleEvent(SimpleEvent::kOnSendHeaders, info, request, headers);
}
void WebRequest::OnBeforeRedirect(extensions::WebRequestInfo* info,
const network::ResourceRequest& request,
const GURL& new_location) {
HandleSimpleEvent(kOnBeforeRedirect, info, request, new_location);
HandleSimpleEvent(SimpleEvent::kOnBeforeRedirect, info, request,
new_location);
}
void WebRequest::OnResponseStarted(extensions::WebRequestInfo* info,
const network::ResourceRequest& request) {
HandleSimpleEvent(kOnResponseStarted, info, request);
HandleSimpleEvent(SimpleEvent::kOnResponseStarted, info, request);
}
void WebRequest::OnErrorOccurred(extensions::WebRequestInfo* info,
@@ -338,7 +344,7 @@ void WebRequest::OnErrorOccurred(extensions::WebRequestInfo* info,
int net_error) {
callbacks_.erase(info->id);
HandleSimpleEvent(kOnErrorOccurred, info, request, net_error);
HandleSimpleEvent(SimpleEvent::kOnErrorOccurred, info, request, net_error);
}
void WebRequest::OnCompleted(extensions::WebRequestInfo* info,
@@ -346,7 +352,7 @@ void WebRequest::OnCompleted(extensions::WebRequestInfo* info,
int net_error) {
callbacks_.erase(info->id);
HandleSimpleEvent(kOnCompleted, info, request, net_error);
HandleSimpleEvent(SimpleEvent::kOnCompleted, info, request, net_error);
}
void WebRequest::OnRequestWillBeDestroyed(extensions::WebRequestInfo* info) {

View File

@@ -86,14 +86,14 @@ class WebRequest : public gin::Wrappable<WebRequest>, public WebRequestAPI {
WebRequest(v8::Isolate* isolate, content::BrowserContext* browser_context);
~WebRequest() override;
enum SimpleEvent {
enum class SimpleEvent {
kOnSendHeaders,
kOnBeforeRedirect,
kOnResponseStarted,
kOnCompleted,
kOnErrorOccurred,
};
enum ResponseEvent {
enum class ResponseEvent {
kOnBeforeRequest,
kOnBeforeSendHeaders,
kOnHeadersReceived,

View File

@@ -224,7 +224,12 @@ blink::mojom::PermissionStatus ElectronPermissionManager::GetPermissionStatus(
content::PermissionType permission,
const GURL& requesting_origin,
const GURL& embedding_origin) {
return blink::mojom::PermissionStatus::GRANTED;
base::DictionaryValue details;
details.SetString("embeddingOrigin", embedding_origin.spec());
bool granted = CheckPermissionWithDetails(permission, nullptr,
requesting_origin, &details);
return granted ? blink::mojom::PermissionStatus::GRANTED
: blink::mojom::PermissionStatus::DENIED;
}
int ElectronPermissionManager::SubscribePermissionStatusChange(
@@ -247,13 +252,28 @@ bool ElectronPermissionManager::CheckPermissionWithDetails(
return true;
}
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
render_frame_host
? content::WebContents::FromRenderFrameHost(render_frame_host)
: nullptr;
auto mutable_details =
details == nullptr ? base::DictionaryValue() : details->Clone();
mutable_details.SetStringKey("requestingUrl",
render_frame_host->GetLastCommittedURL().spec());
mutable_details.SetBoolKey("isMainFrame",
render_frame_host->GetParent() == nullptr);
if (render_frame_host) {
mutable_details.SetStringKey(
"requestingUrl", render_frame_host->GetLastCommittedURL().spec());
}
mutable_details.SetBoolKey(
"isMainFrame",
render_frame_host && render_frame_host->GetParent() == nullptr);
switch (permission) {
case content::PermissionType::AUDIO_CAPTURE:
mutable_details.SetStringKey("mediaType", "audio");
break;
case content::PermissionType::VIDEO_CAPTURE:
mutable_details.SetStringKey("mediaType", "video");
break;
default:
break;
}
return check_handler_.Run(web_contents, permission, requesting_origin,
mutable_details);
}
@@ -263,7 +283,11 @@ ElectronPermissionManager::GetPermissionStatusForFrame(
content::PermissionType permission,
content::RenderFrameHost* render_frame_host,
const GURL& requesting_origin) {
return blink::mojom::PermissionStatus::GRANTED;
base::DictionaryValue details;
bool granted = CheckPermissionWithDetails(permission, render_frame_host,
requesting_origin, &details);
return granted ? blink::mojom::PermissionStatus::GRANTED
: blink::mojom::PermissionStatus::DENIED;
}
} // namespace electron

View File

@@ -4,6 +4,11 @@
#include "shell/browser/native_browser_view_views.h"
#include <memory>
#include <utility>
#include <vector>
#include "shell/browser/ui/drag_util.h"
#include "shell/browser/ui/inspectable_web_contents_view.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/background.h"
@@ -22,6 +27,25 @@ void NativeBrowserViewViews::SetAutoResizeFlags(uint8_t flags) {
ResetAutoResizeProportions();
}
void NativeBrowserViewViews::UpdateDraggableRegions(
const std::vector<mojom::DraggableRegionPtr>& regions) {
// We need to snap the regions to the bounds of the current BrowserView.
// For example, if an attached BrowserView is draggable but its bounds are
// { x: 200, y: 100, width: 300, height: 300 }
// then we need to add 200 to the x-value and 100 to the
// y-value of each of the passed regions or it will be incorrectly
// assumed that the regions begin in the top left corner as they
// would for the main client window.
auto const offset = GetBounds().OffsetFromOrigin();
auto snapped_regions = mojo::Clone(regions);
for (auto& snapped_region : snapped_regions) {
snapped_region->bounds.Offset(offset);
snapped_region->draggable = true;
}
draggable_region_ = DraggableRegionsToSkRegion(snapped_regions);
}
void NativeBrowserViewViews::SetAutoResizeProportions(
const gfx::Size& window_size) {
if ((auto_resize_flags_ & AutoResizeFlags::kAutoResizeHorizontal) &&

View File

@@ -5,7 +5,11 @@
#ifndef SHELL_BROWSER_NATIVE_BROWSER_VIEW_VIEWS_H_
#define SHELL_BROWSER_NATIVE_BROWSER_VIEW_VIEWS_H_
#include <memory>
#include <vector>
#include "shell/browser/native_browser_view.h"
#include "third_party/skia/include/core/SkRegion.h"
namespace electron {
@@ -26,6 +30,10 @@ class NativeBrowserViewViews : public NativeBrowserView {
void SetBounds(const gfx::Rect& bounds) override;
gfx::Rect GetBounds() override;
void SetBackgroundColor(SkColor color) override;
void UpdateDraggableRegions(
const std::vector<mojom::DraggableRegionPtr>& regions) override;
SkRegion* draggable_region() const { return draggable_region_.get(); }
private:
void ResetAutoResizeProportions();
@@ -40,6 +48,8 @@ class NativeBrowserViewViews : public NativeBrowserView {
float auto_vertical_proportion_height_ = 0.;
float auto_vertical_proportion_top_ = 0.;
std::unique_ptr<SkRegion> draggable_region_;
DISALLOW_COPY_AND_ASSIGN(NativeBrowserViewViews);
};

View File

@@ -1460,6 +1460,16 @@ views::View* NativeWindowViews::GetContentsView() {
bool NativeWindowViews::ShouldDescendIntoChildForEventHandling(
gfx::NativeView child,
const gfx::Point& location) {
// App window should claim mouse events that fall within any BrowserViews'
// draggable region.
for (auto* view : browser_views()) {
auto* native_view = static_cast<NativeBrowserViewViews*>(view);
auto* view_draggable_region = native_view->draggable_region();
if (view_draggable_region &&
view_draggable_region->contains(location.x(), location.y()))
return false;
}
// App window should claim mouse events that fall within the draggable region.
if (draggable_region() &&
draggable_region()->contains(location.x(), location.y()))

View File

@@ -40,5 +40,9 @@
<string>This app needs access to the microphone</string>
<key>NSCameraUsageDescription</key>
<string>This app needs access to the camera</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app needs access to Bluetooth</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app needs access to Bluetooth</string>
</dict>
</plist>

View File

@@ -50,8 +50,8 @@ END
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 13,0,0,20201201
PRODUCTVERSION 13,0,0,20201201
FILEVERSION 13,0,0,20201209
PRODUCTVERSION 13,0,0,20201209
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L

View File

@@ -4,6 +4,7 @@
#include "shell/browser/ui/views/frameless_view.h"
#include "shell/browser/native_browser_view_views.h"
#include "shell/browser/native_window_views.h"
#include "ui/aura/window.h"
#include "ui/base/hit_test.h"
@@ -68,6 +69,15 @@ int FramelessView::NonClientHitTest(const gfx::Point& cursor) {
if (frame_->IsFullscreen())
return HTCLIENT;
// Check attached BrowserViews for potential draggable areas.
for (auto* view : window_->browser_views()) {
auto* native_view = static_cast<NativeBrowserViewViews*>(view);
auto* view_draggable_region = native_view->draggable_region();
if (view_draggable_region &&
view_draggable_region->contains(cursor.x(), cursor.y()))
return HTCAPTION;
}
// Check for possible draggable region in the client area for the frameless
// window.
SkRegion* draggable_region = window_->draggable_region();

View File

@@ -8,7 +8,6 @@ import "third_party/blink/public/mojom/messaging/transferable_message.mojom";
interface ElectronRenderer {
Message(
bool internal,
bool send_to_all,
string channel,
blink.mojom.CloneableMessage arguments,
int32 sender_id);
@@ -67,7 +66,6 @@ interface ElectronBrowser {
// WebContents's main frame, specified by |web_contents_id|.
MessageTo(
bool internal,
bool send_to_all,
int32 web_contents_id,
string channel,
blink.mojom.CloneableMessage arguments);

View File

@@ -176,14 +176,18 @@ void ErrorMessageListener(v8::Local<v8::Message> message,
v8::Isolate* isolate = v8::Isolate::GetCurrent();
node::Environment* env = node::Environment::GetCurrent(isolate);
// TODO(codebytere): properly emit the after() hooks now
// that the exception has been handled.
// See node/lib/internal/process/execution.js#L176-L180
// Ensure that the async id stack is properly cleared so the async
// hook stack does not become corrupted.
if (env) {
// Emit the after() hooks now that the exception has been handled.
// Analogous to node/lib/internal/process/execution.js#L176-L180
if (env->async_hooks()->fields()[node::AsyncHooks::kAfter]) {
while (env->async_hooks()->fields()[node::AsyncHooks::kStackLength]) {
node::AsyncWrap::EmitAfter(env, env->execution_async_id());
env->async_hooks()->pop_async_context(env->execution_async_id());
}
}
// Ensure that the async id stack is properly cleared so the async
// hook stack does not become corrupted.
env->async_hooks()->clear_async_id_stack();
}
}

View File

@@ -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);

View File

@@ -172,7 +172,6 @@ class IPCRenderer : public gin::Wrappable<IPCRenderer>,
void SendTo(v8::Isolate* isolate,
gin_helper::ErrorThrower thrower,
bool internal,
bool send_to_all,
int32_t web_contents_id,
const std::string& channel,
v8::Local<v8::Value> arguments) {
@@ -184,8 +183,8 @@ class IPCRenderer : public gin::Wrappable<IPCRenderer>,
if (!electron::SerializeV8Value(isolate, arguments, &message)) {
return;
}
electron_browser_remote_->MessageTo(internal, send_to_all, web_contents_id,
channel, std::move(message));
electron_browser_remote_->MessageTo(internal, web_contents_id, channel,
std::move(message));
}
void SendToHost(v8::Isolate* isolate,

View File

@@ -132,7 +132,6 @@ void ElectronApiServiceImpl::OnConnectionError() {
}
void ElectronApiServiceImpl::Message(bool internal,
bool send_to_all,
const std::string& channel,
blink::CloneableMessage arguments,
int32_t sender_id) {
@@ -168,18 +167,6 @@ void ElectronApiServiceImpl::Message(bool internal,
v8::Local<v8::Value> args = gin::ConvertToV8(isolate, arguments);
EmitIPCEvent(context, internal, channel, {}, args, sender_id);
// Also send the message to all sub-frames.
// TODO(MarshallOfSound): Completely move this logic to the main process
if (send_to_all) {
for (blink::WebFrame* child = frame->FirstChild(); child;
child = child->NextSibling())
if (child->IsWebLocalFrame()) {
v8::Local<v8::Context> child_context =
renderer_client_->GetContext(child->ToWebLocalFrame(), isolate);
EmitIPCEvent(child_context, internal, channel, {}, args, sender_id);
}
}
}
void ElectronApiServiceImpl::ReceivePostMessage(

View File

@@ -29,7 +29,6 @@ class ElectronApiServiceImpl : public mojom::ElectronRenderer,
mojo::PendingAssociatedReceiver<mojom::ElectronRenderer> receiver);
void Message(bool internal,
bool send_to_all,
const std::string& channel,
blink::CloneableMessage arguments,
int32_t sender_id) override;

View File

@@ -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 () => {

View File

@@ -2,6 +2,18 @@ import { expect } from 'chai';
import { screen } from 'electron/main';
describe('screen module', () => {
describe('methods reassignment', () => {
it('works for a selected method', () => {
const originalFunction = screen.getPrimaryDisplay;
try {
(screen as any).getPrimaryDisplay = () => null;
expect(screen.getPrimaryDisplay()).to.be.null();
} finally {
screen.getPrimaryDisplay = originalFunction;
}
});
});
describe('screen.getCursorScreenPoint()', () => {
it('returns a point object', () => {
const point = screen.getCursorScreenPoint();

View File

@@ -1444,7 +1444,8 @@ describe('asar package', function () {
it('reads a normal file with unpacked files', function () {
const p = path.join(asarDir, 'unpack.asar', 'a.txt');
expect(internalModuleReadJSON(p).toString().trim()).to.equal('a');
const [s, c] = internalModuleReadJSON(p);
expect([s.toString().trim(), c]).to.eql(['a', true]);
});
});

View File

@@ -32,7 +32,7 @@ declare namespace NodeJS {
send(internal: boolean, channel: string, args: any[]): void;
sendSync(internal: boolean, channel: string, args: any[]): any;
sendToHost(channel: string, args: any[]): void;
sendTo(internal: boolean, sendToAll: boolean, webContentsId: number, channel: string, args: any[]): void;
sendTo(internal: boolean, webContentsId: number, channel: string, args: any[]): void;
invoke<T>(internal: boolean, channel: string, args: any[]): Promise<{ error: string, result: T }>;
postMessage(channel: string, message: any, transferables: MessagePort[]): void;
}

View File

@@ -67,12 +67,11 @@ declare namespace Electron {
_windowOpenHandler: ((opts: {url: string, frameName: string, features: string}) => any) | null;
_callWindowOpenHandler(event: any, url: string, frameName: string, rawFeatures: string): Electron.BrowserWindowConstructorOptions | null;
_setNextChildWebPreferences(prefs: Partial<Electron.BrowserWindowConstructorOptions['webPreferences']> & Pick<Electron.BrowserWindowConstructorOptions, 'backgroundColor'>): void;
_send(internal: boolean, sendToAll: boolean, channel: string, args: any): boolean;
_sendToFrame(internal: boolean, sendToAll: boolean, frameId: number, channel: string, args: any): boolean;
_send(internal: boolean, channel: string, args: any): boolean;
_sendToFrame(internal: boolean, frameId: number, channel: string, args: any): boolean;
_sendToFrameInternal(frameId: number, channel: string, ...args: any[]): boolean;
_postMessage(channel: string, message: any, transfer?: any[]): void;
_sendInternal(channel: string, ...args: any[]): void;
_sendInternalToAll(channel: string, ...args: any[]): void;
_printToPDF(options: any): Promise<Buffer>;
_print(options: any, callback?: (success: boolean, failureReason: string) => void): void;
_getPrinters(): Electron.PrinterInfo[];
@@ -232,14 +231,12 @@ declare namespace ElectronInternal {
interface IpcRendererInternal extends Electron.IpcRenderer {
invoke<T>(channel: string, ...args: any[]): Promise<T>;
sendToAll(webContentsId: number, channel: string, ...args: any[]): void;
onMessageFromMain(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void): this;
onceMessageFromMain(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void): this;
}
// Internal IPC has _replyInternal and NO reply method
interface IpcMainInternalEvent extends Omit<Electron.IpcMainEvent, 'reply'> {
_replyInternal(...args: any[]): void;
}
interface IpcMainInternal extends NodeJS.EventEmitter {

1
vendor/boto vendored

Submodule vendor/boto deleted from f7574aa6cc