mirror of
https://github.com/electron/electron.git
synced 2026-04-10 03:01:51 -04:00
Compare commits
24 Commits
v25.0.0-ni
...
v25.0.0-ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee58e18fd | ||
|
|
f33bf2a271 | ||
|
|
77bd80dfb2 | ||
|
|
0d3aee26b9 | ||
|
|
efde7a140b | ||
|
|
4e85bb921b | ||
|
|
512e56baf7 | ||
|
|
c8f715f9a1 | ||
|
|
829fb4f586 | ||
|
|
17ccb3c6ec | ||
|
|
6bd9ee6988 | ||
|
|
76c825d619 | ||
|
|
692876c737 | ||
|
|
c3f06ef037 | ||
|
|
9b20b3a722 | ||
|
|
8f2917db01 | ||
|
|
49df19214e | ||
|
|
5e25d23794 | ||
|
|
8fb0f43030 | ||
|
|
1f390119fe | ||
|
|
87f2a1d572 | ||
|
|
2e03bdb9b6 | ||
|
|
3a5ae28c95 | ||
|
|
ed7b5c44a2 |
@@ -23,8 +23,7 @@ The `app` object emits the following events:
|
||||
Emitted when the application has finished basic startup. On Windows and Linux,
|
||||
the `will-finish-launching` event is the same as the `ready` event; on macOS,
|
||||
this event represents the `applicationWillFinishLaunching` notification of
|
||||
`NSApplication`. You would usually set up listeners for the `open-file` and
|
||||
`open-url` events here, and start the crash reporter and auto updater.
|
||||
`NSApplication`.
|
||||
|
||||
In most cases, you should do everything in the `ready` event handler.
|
||||
|
||||
@@ -1357,7 +1356,7 @@ This API must be called after the `ready` event is emitted.
|
||||
|
||||
### `app.showAboutPanel()`
|
||||
|
||||
Show the app's about panel options. These options can be overridden with `app.setAboutPanelOptions(options)`.
|
||||
Show the app's about panel options. These options can be overridden with `app.setAboutPanelOptions(options)`. This function runs asynchronously.
|
||||
|
||||
### `app.setAboutPanelOptions(options)`
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ clipboard.writeBuffer('public/utf8-plain-text', buffer)
|
||||
|
||||
const ret = clipboard.readBuffer('public/utf8-plain-text')
|
||||
|
||||
console.log(buffer.equals(out))
|
||||
console.log(buffer.equals(ret))
|
||||
// true
|
||||
```
|
||||
|
||||
|
||||
@@ -119,13 +119,15 @@ Returns `NativeImage`
|
||||
|
||||
Creates an empty `NativeImage` instance.
|
||||
|
||||
### `nativeImage.createThumbnailFromPath(path, maxSize)` _macOS_ _Windows_
|
||||
### `nativeImage.createThumbnailFromPath(path, size)` _macOS_ _Windows_
|
||||
|
||||
* `path` string - path to a file that we intend to construct a thumbnail out of.
|
||||
* `maxSize` [Size](structures/size.md) - the maximum width and height (positive numbers) the thumbnail returned can be. The Windows implementation will ignore `maxSize.height` and scale the height according to `maxSize.width`.
|
||||
* `size` [Size](structures/size.md) - the desired width and height (positive numbers) of the thumbnail.
|
||||
|
||||
Returns `Promise<NativeImage>` - fulfilled with the file's thumbnail preview image, which is a [NativeImage](native-image.md).
|
||||
|
||||
Note: The Windows implementation will ignore `size.height` and scale the height according to `size.width`.
|
||||
|
||||
### `nativeImage.createFromPath(path)`
|
||||
|
||||
* `path` string
|
||||
|
||||
@@ -101,6 +101,10 @@ Limitations:
|
||||
* The `.type` and `.url` values of the returned `Response` object are
|
||||
incorrect.
|
||||
|
||||
Requests made with `net.fetch` can be made to [custom protocols](protocol.md)
|
||||
as well as `file:`, and will trigger [webRequest](web-request.md) handlers if
|
||||
present.
|
||||
|
||||
### `net.isOnline()`
|
||||
|
||||
Returns `boolean` - Whether there is currently internet connection.
|
||||
|
||||
@@ -768,6 +768,10 @@ Limitations:
|
||||
* The `.type` and `.url` values of the returned `Response` object are
|
||||
incorrect.
|
||||
|
||||
Requests made with `ses.fetch` can be made to [custom protocols](protocol.md)
|
||||
as well as `file:`, and will trigger [webRequest](web-request.md) handlers if
|
||||
present.
|
||||
|
||||
#### `ses.disableNetworkEmulation()`
|
||||
|
||||
Disables any network emulation already active for the `session`. Resets to
|
||||
@@ -916,6 +920,10 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
|
||||
Specifying a loopback device will capture system audio, and is
|
||||
currently only supported on Windows. If a WebFrameMain is specified,
|
||||
will capture audio from that frame.
|
||||
* `enableLocalEcho` Boolean (optional) - If `audio` is a [WebFrameMain](web-frame-main.md)
|
||||
and this is set to `true`, then local playback of audio will not be muted (e.g. using `MediaRecorder`
|
||||
to record `WebFrameMain` with this flag set to `true` will allow audio to pass through to the speakers
|
||||
while recording). Default is `false`.
|
||||
|
||||
This handler will be called when web content requests access to display media
|
||||
via the `navigator.mediaDevices.getDisplayMedia` API. Use the
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# WebRequestFilter Object
|
||||
|
||||
* `urls` string[] - Array of [URL patterns](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns) that will be used to filter out the requests that do not match the URL patterns.
|
||||
* `types` String[] (optional) - Array of types that will be used to filter out the requests that do not match the types. When not specified, all types will be matched. Can be `mainFrame`, `subFrame`, `stylesheet`, `script`, `image`, `font`, `object`, `xhr`, `ping`, `cspReport`, `media` or `webSocket`.
|
||||
|
||||
@@ -207,8 +207,23 @@ See [`window.open()`](window-open.md) for more details and how to use this in co
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `url` string
|
||||
* `details` Event<>
|
||||
* `url` string - The URL the frame is navigating to.
|
||||
* `isSameDocument` boolean - Whether the navigation happened without changing
|
||||
document. Examples of same document navigations are reference fragment
|
||||
navigations, pushState/replaceState, and same page history navigation.
|
||||
* `isMainFrame` boolean - True if the navigation is taking place in a main frame.
|
||||
* `frame` WebFrameMain - The frame to be navigated.
|
||||
* `initiator` WebFrameMain (optional) - The frame which initiated the
|
||||
navigation, which can be a parent frame (e.g. via `window.open` with a
|
||||
frame's name), or null if the navigation was not initiated by a frame. This
|
||||
can also be null if the initiating frame was deleted before the event was
|
||||
emitted.
|
||||
* `url` string _Deprecated_
|
||||
* `isInPlace` boolean _Deprecated_
|
||||
* `isMainFrame` boolean _Deprecated_
|
||||
* `frameProcessId` Integer _Deprecated_
|
||||
* `frameRoutingId` Integer _Deprecated_
|
||||
|
||||
Emitted when a user or the page wants to start navigation. It can happen when
|
||||
the `window.location` object is changed or a user clicks a link in the page.
|
||||
@@ -226,26 +241,47 @@ Calling `event.preventDefault()` will prevent the navigation.
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `url` string
|
||||
* `isInPlace` boolean
|
||||
* `isMainFrame` boolean
|
||||
* `frameProcessId` Integer
|
||||
* `frameRoutingId` Integer
|
||||
* `details` Event<>
|
||||
* `url` string - The URL the frame is navigating to.
|
||||
* `isSameDocument` boolean - Whether the navigation happened without changing
|
||||
document. Examples of same document navigations are reference fragment
|
||||
navigations, pushState/replaceState, and same page history navigation.
|
||||
* `isMainFrame` boolean - True if the navigation is taking place in a main frame.
|
||||
* `frame` WebFrameMain - The frame to be navigated.
|
||||
* `initiator` WebFrameMain (optional) - The frame which initiated the
|
||||
navigation, which can be a parent frame (e.g. via `window.open` with a
|
||||
frame's name), or null if the navigation was not initiated by a frame. This
|
||||
can also be null if the initiating frame was deleted before the event was
|
||||
emitted.
|
||||
* `url` string _Deprecated_
|
||||
* `isInPlace` boolean _Deprecated_
|
||||
* `isMainFrame` boolean _Deprecated_
|
||||
* `frameProcessId` Integer _Deprecated_
|
||||
* `frameRoutingId` Integer _Deprecated_
|
||||
|
||||
Emitted when any frame (including main) starts navigating. `isInPlace` will be
|
||||
`true` for in-page navigations.
|
||||
Emitted when any frame (including main) starts navigating.
|
||||
|
||||
#### Event: 'will-redirect'
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `url` string
|
||||
* `isInPlace` boolean
|
||||
* `isMainFrame` boolean
|
||||
* `frameProcessId` Integer
|
||||
* `frameRoutingId` Integer
|
||||
* `details` Event<>
|
||||
* `url` string - The URL the frame is navigating to.
|
||||
* `isSameDocument` boolean - Whether the navigation happened without changing
|
||||
document. Examples of same document navigations are reference fragment
|
||||
navigations, pushState/replaceState, and same page history navigation.
|
||||
* `isMainFrame` boolean - True if the navigation is taking place in a main frame.
|
||||
* `frame` WebFrameMain - The frame to be navigated.
|
||||
* `initiator` WebFrameMain (optional) - The frame which initiated the
|
||||
navigation, which can be a parent frame (e.g. via `window.open` with a
|
||||
frame's name), or null if the navigation was not initiated by a frame. This
|
||||
can also be null if the initiating frame was deleted before the event was
|
||||
emitted.
|
||||
* `url` string _Deprecated_
|
||||
* `isInPlace` boolean _Deprecated_
|
||||
* `isMainFrame` boolean _Deprecated_
|
||||
* `frameProcessId` Integer _Deprecated_
|
||||
* `frameRoutingId` Integer _Deprecated_
|
||||
|
||||
Emitted when a server side redirect occurs during navigation. For example a 302
|
||||
redirect.
|
||||
@@ -260,12 +296,23 @@ redirect).
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `url` string
|
||||
* `isInPlace` boolean
|
||||
* `isMainFrame` boolean
|
||||
* `frameProcessId` Integer
|
||||
* `frameRoutingId` Integer
|
||||
* `details` Event<>
|
||||
* `url` string - The URL the frame is navigating to.
|
||||
* `isSameDocument` boolean - Whether the navigation happened without changing
|
||||
document. Examples of same document navigations are reference fragment
|
||||
navigations, pushState/replaceState, and same page history navigation.
|
||||
* `isMainFrame` boolean - True if the navigation is taking place in a main frame.
|
||||
* `frame` WebFrameMain - The frame to be navigated.
|
||||
* `initiator` WebFrameMain (optional) - The frame which initiated the
|
||||
navigation, which can be a parent frame (e.g. via `window.open` with a
|
||||
frame's name), or null if the navigation was not initiated by a frame. This
|
||||
can also be null if the initiating frame was deleted before the event was
|
||||
emitted.
|
||||
* `url` string _Deprecated_
|
||||
* `isInPlace` boolean _Deprecated_
|
||||
* `isMainFrame` boolean _Deprecated_
|
||||
* `frameProcessId` Integer _Deprecated_
|
||||
* `frameRoutingId` Integer _Deprecated_
|
||||
|
||||
Emitted after a server side redirect occurs during navigation. For example a 302
|
||||
redirect.
|
||||
@@ -588,6 +635,15 @@ Emitted when media starts playing.
|
||||
|
||||
Emitted when media is paused or done playing.
|
||||
|
||||
#### Event: 'audio-state-changed'
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event<>
|
||||
* `audible` boolean - True if one or more frames or child `webContents` are emitting audio.
|
||||
|
||||
Emitted when media becomes audible or inaudible.
|
||||
|
||||
#### Event: 'did-change-theme-color'
|
||||
|
||||
Returns:
|
||||
@@ -730,7 +786,7 @@ cancel the request.
|
||||
|
||||
If no event listener is added for this event, all bluetooth requests will be cancelled.
|
||||
|
||||
```javascript
|
||||
```javascript title='main.js'
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
|
||||
let win = null
|
||||
@@ -1575,7 +1631,7 @@ ipcMain.on('open-devtools', (event, targetContentsId, devtoolsContentsId) => {
|
||||
|
||||
An example of showing devtools in a `BrowserWindow`:
|
||||
|
||||
```js
|
||||
```js title='main.js'
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
|
||||
let win = null
|
||||
@@ -1658,40 +1714,14 @@ Algorithm][SCA], just like [`postMessage`][], so prototype chains will not be
|
||||
included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will
|
||||
throw an exception.
|
||||
|
||||
> **NOTE**: Sending non-standard JavaScript types such as DOM objects or
|
||||
> special Electron objects will throw an exception.
|
||||
:::warning
|
||||
|
||||
The renderer process can handle the message by listening to `channel` with the
|
||||
[`ipcRenderer`](ipc-renderer.md) module.
|
||||
Sending non-standard JavaScript types such as DOM objects or
|
||||
special Electron objects will throw an exception.
|
||||
|
||||
An example of sending messages from the main process to the renderer process:
|
||||
:::
|
||||
|
||||
```javascript
|
||||
// In the main process.
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
let win = null
|
||||
|
||||
app.whenReady().then(() => {
|
||||
win = new BrowserWindow({ width: 800, height: 600 })
|
||||
win.loadURL(`file://${__dirname}/index.html`)
|
||||
win.webContents.on('did-finish-load', () => {
|
||||
win.webContents.send('ping', 'whoooooooh!')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- index.html -->
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
require('electron').ipcRenderer.on('ping', (event, message) => {
|
||||
console.log(message) // Prints 'whoooooooh!'
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
For additional reading, refer to [Electron's IPC guide](../tutorial/ipc.md).
|
||||
|
||||
#### `contents.sendToFrame(frameId, channel, ...args)`
|
||||
|
||||
|
||||
@@ -14,6 +14,41 @@ This document uses the following convention to categorize breaking changes:
|
||||
|
||||
## Planned Breaking API Changes (24.0)
|
||||
|
||||
### API Changed: `nativeImage.createThumbnailFromPath(path, size)`
|
||||
|
||||
The `maxSize` parameter has been changed to `size` to reflect that the size passed in will be the size the thumbnail created. Previously, Windows would not scale the image up if it were smaller than `maxSize`, and
|
||||
macOS would always set the size to `maxSize`. Behavior is now the same across platforms.
|
||||
|
||||
Updated Behavior:
|
||||
|
||||
```js
|
||||
// a 128x128 image.
|
||||
const imagePath = path.join('path', 'to', 'capybara.png')
|
||||
|
||||
// Scaling up a smaller image.
|
||||
const upSize = { width: 256, height: 256 }
|
||||
nativeImage.createThumbnailFromPath(imagePath, upSize).then(result => {
|
||||
console.log(result.getSize()) // { width: 256, height: 256 }
|
||||
})
|
||||
|
||||
// Scaling down a larger image.
|
||||
const downSize = { width: 64, height: 64 }
|
||||
nativeImage.createThumbnailFromPath(imagePath, downSize).then(result => {
|
||||
console.log(result.getSize()) // { width: 64, height: 64 }
|
||||
})
|
||||
```
|
||||
|
||||
Previous Behavior (on Windows):
|
||||
|
||||
```js
|
||||
// a 128x128 image
|
||||
const imagePath = path.join('path', 'to', 'capybara.png')
|
||||
const size = { width: 256, height: 256 }
|
||||
nativeImage.createThumbnailFromPath(imagePath, size).then(result => {
|
||||
console.log(result.getSize()) // { width: 128, height: 128 }
|
||||
})
|
||||
```
|
||||
|
||||
### Deprecated: `BrowserWindow.setTrafficLightPosition(position)`
|
||||
|
||||
`BrowserWindow.setTrafficLightPosition(position)` has been deprecated, the
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const {app, BrowserWindow} = require('electron')
|
||||
const e = require('express')
|
||||
const path = require('path')
|
||||
|
||||
function createWindow () {
|
||||
@@ -44,7 +43,6 @@ function createWindow () {
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
mainWindow.webContents.session.setDevicePermissionHandler((details) => {
|
||||
if (details.deviceType === 'usb' && details.origin === 'file://') {
|
||||
if (!grantedDeviceThroughPermHandler) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
function getDeviceDetails(device) {
|
||||
return grantedDevice.productName || `Unknown device ${grantedDevice.deviceId}`
|
||||
return device.productName || `Unknown device ${device.deviceId}`
|
||||
}
|
||||
|
||||
async function testIt() {
|
||||
|
||||
@@ -175,8 +175,6 @@ filenames = {
|
||||
"shell/browser/ui/cocoa/electron_touch_bar.mm",
|
||||
"shell/browser/ui/cocoa/event_dispatching_window.h",
|
||||
"shell/browser/ui/cocoa/event_dispatching_window.mm",
|
||||
"shell/browser/ui/cocoa/NSColor+Hex.h",
|
||||
"shell/browser/ui/cocoa/NSColor+Hex.mm",
|
||||
"shell/browser/ui/cocoa/NSString+ANSI.h",
|
||||
"shell/browser/ui/cocoa/NSString+ANSI.mm",
|
||||
"shell/browser/ui/cocoa/root_view_mac.h",
|
||||
|
||||
@@ -10,7 +10,7 @@ const {
|
||||
} = process._linkedBinding('electron_browser_net');
|
||||
const { Session } = process._linkedBinding('electron_browser_session');
|
||||
|
||||
const kSupportedProtocols = new Set(['http:', 'https:']);
|
||||
const kHttpProtocols = new Set(['http:', 'https:']);
|
||||
|
||||
// set of headers that Node.js discards duplicates for
|
||||
// see https://nodejs.org/api/http.html#http_message_headers
|
||||
@@ -195,7 +195,20 @@ class ChunkedBodyStream extends Writable {
|
||||
|
||||
type RedirectPolicy = 'manual' | 'follow' | 'error';
|
||||
|
||||
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } {
|
||||
const kAllowNonHttpProtocols = Symbol('kAllowNonHttpProtocols');
|
||||
export function allowAnyProtocol (opts: ClientRequestConstructorOptions): ClientRequestConstructorOptions {
|
||||
return {
|
||||
...opts,
|
||||
[kAllowNonHttpProtocols]: true
|
||||
} as any;
|
||||
}
|
||||
|
||||
type ExtraURLLoaderOptions = {
|
||||
redirectPolicy: RedirectPolicy;
|
||||
headers: Record<string, { name: string, value: string | string[] }>;
|
||||
allowNonHttpProtocols: boolean;
|
||||
}
|
||||
function parseOptions (optionsIn: ClientRequestConstructorOptions | string): NodeJS.CreateURLLoaderOptions & ExtraURLLoaderOptions {
|
||||
const options: any = typeof optionsIn === 'string' ? url.parse(optionsIn) : { ...optionsIn };
|
||||
|
||||
let urlStr: string = options.url;
|
||||
@@ -203,9 +216,6 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
|
||||
if (!urlStr) {
|
||||
const urlObj: url.UrlObject = {};
|
||||
const protocol = options.protocol || 'http:';
|
||||
if (!kSupportedProtocols.has(protocol)) {
|
||||
throw new Error('Protocol "' + protocol + '" not supported');
|
||||
}
|
||||
urlObj.protocol = protocol;
|
||||
|
||||
if (options.host) {
|
||||
@@ -247,7 +257,7 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
|
||||
throw new TypeError('headers must be an object');
|
||||
}
|
||||
|
||||
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }> } = {
|
||||
const urlLoaderOptions: NodeJS.CreateURLLoaderOptions & { redirectPolicy: RedirectPolicy, headers: Record<string, { name: string, value: string | string[] }>, allowNonHttpProtocols: boolean } = {
|
||||
method: (options.method || 'GET').toUpperCase(),
|
||||
url: urlStr,
|
||||
redirectPolicy,
|
||||
@@ -257,7 +267,8 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
|
||||
credentials: options.credentials,
|
||||
origin: options.origin,
|
||||
referrerPolicy: options.referrerPolicy,
|
||||
cache: options.cache
|
||||
cache: options.cache,
|
||||
allowNonHttpProtocols: Object.prototype.hasOwnProperty.call(options, kAllowNonHttpProtocols)
|
||||
};
|
||||
const headers: Record<string, string | string[]> = options.headers || {};
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
@@ -308,6 +319,10 @@ export class ClientRequest extends Writable implements Electron.ClientRequest {
|
||||
}
|
||||
|
||||
const { redirectPolicy, ...urlLoaderOptions } = parseOptions(options);
|
||||
const urlObj = new URL(urlLoaderOptions.url);
|
||||
if (!urlLoaderOptions.allowNonHttpProtocols && !kHttpProtocols.has(urlObj.protocol)) {
|
||||
throw new Error('ClientRequest only supports http: and https: protocols');
|
||||
}
|
||||
if (urlLoaderOptions.credentials === 'same-origin' && !urlLoaderOptions.origin) { throw new Error('credentials: same-origin requires origin to be set'); }
|
||||
this._urlLoaderOptions = urlLoaderOptions;
|
||||
this._redirectPolicy = redirectPolicy;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { net, IncomingMessage, Session as SessionT } from 'electron/main';
|
||||
import { Readable, Writable, isReadable } from 'stream';
|
||||
import { allowAnyProtocol } from '@electron/internal/browser/api/net-client-request';
|
||||
|
||||
function createDeferredPromise<T, E extends Error = Error> (): { promise: Promise<T>; resolve: (x: T) => void; reject: (e: E) => void; } {
|
||||
let res: (x: T) => void;
|
||||
@@ -72,7 +73,7 @@ export function fetchWithSession (input: RequestInfo, init: RequestInit | undefi
|
||||
// We can't set credentials to same-origin unless there's an origin set.
|
||||
const credentials = req.credentials === 'same-origin' && !origin ? 'include' : req.credentials;
|
||||
|
||||
const r = net.request({
|
||||
const r = net.request(allowAnyProtocol({
|
||||
session,
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
@@ -81,7 +82,7 @@ export function fetchWithSession (input: RequestInfo, init: RequestInit | undefi
|
||||
cache: req.cache,
|
||||
referrerPolicy: req.referrerPolicy,
|
||||
redirect: req.redirect
|
||||
});
|
||||
}));
|
||||
|
||||
// cors is the default mode, but we can't set mode=cors without an origin.
|
||||
if (req.mode && (req.mode !== 'cors' || origin)) {
|
||||
|
||||
@@ -126,3 +126,4 @@ chore_patch_out_partition_attribute_dcheck_for_webviews.patch
|
||||
expose_v8initializer_codegenerationcheckcallbackinmainthread.patch
|
||||
chore_patch_out_profile_methods_in_profile_selections_cc.patch
|
||||
fix_x11_window_restore_minimized_maximized_window.patch
|
||||
chore_defer_usb_service_getdevices_request_until_usb_service_is.patch
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: John Kleinschmidt <jkleinsc@electronjs.org>
|
||||
Date: Thu, 2 Mar 2023 15:26:46 -0500
|
||||
Subject: chore: defer USB service GetDevices request until USB service is
|
||||
ready.
|
||||
|
||||
On macOS we need to wait until the USB service is ready before the list of
|
||||
devices is available. This should no longer be necessary if/when
|
||||
https://crbug.com/1096743 is completed.
|
||||
|
||||
diff --git a/services/device/usb/usb_service_impl.cc b/services/device/usb/usb_service_impl.cc
|
||||
index 99456d1baa098e8a48b4a31cc8f5661586f7fc82..11d98208c85093598813897a144812c76bc67bea 100644
|
||||
--- a/services/device/usb/usb_service_impl.cc
|
||||
+++ b/services/device/usb/usb_service_impl.cc
|
||||
@@ -198,7 +198,7 @@ void UsbServiceImpl::GetDevices(GetDevicesCallback callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
- if (enumeration_in_progress_) {
|
||||
+ if (enumeration_in_progress_ || !enumeration_ready_) {
|
||||
pending_enumeration_callbacks_.push_back(std::move(callback));
|
||||
return;
|
||||
}
|
||||
@@ -21,5 +21,7 @@
|
||||
|
||||
"src/electron/patches/Mantle": "src/third_party/squirrel.mac/vendor/Mantle",
|
||||
|
||||
"src/electron/patches/ReactiveObjC": "src/third_party/squirrel.mac/vendor/ReactiveObjC"
|
||||
"src/electron/patches/ReactiveObjC": "src/third_party/squirrel.mac/vendor/ReactiveObjC",
|
||||
|
||||
"src/electron/patches/webrtc": "src/third_party/webrtc"
|
||||
}
|
||||
|
||||
@@ -35,3 +35,4 @@ enable_crashpad_linux_node_processes.patch
|
||||
allow_embedder_to_control_codegenerationfromstringscallback.patch
|
||||
src_allow_optional_isolation_termination_in_node.patch
|
||||
test_mark_cpu_prof_tests_as_flaky_in_electron.patch
|
||||
lib_fix_broadcastchannel_initialization_location.patch
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Shelley Vohr <shelley.vohr@gmail.com>
|
||||
Date: Mon, 27 Feb 2023 12:56:15 +0100
|
||||
Subject: lib: fix BroadcastChannel initialization location
|
||||
|
||||
Refs https://github.com/nodejs/node/pull/40532.
|
||||
|
||||
Fixes a bug in the above, wherein BroadcastChannel should have been
|
||||
initialized in bootstrap/browser instead of bootstrap/node. That
|
||||
inadvertently made it such that there was incorrect handling of the
|
||||
DOM vs Node.js implementations of BroadcastChannel.
|
||||
|
||||
This will be upstreamed.
|
||||
|
||||
diff --git a/lib/internal/bootstrap/browser.js b/lib/internal/bootstrap/browser.js
|
||||
index d0c01ca2a512be549b0fea8a829c05eabbec799a..210a1bb7e929021725b04786bc11d9b3ce09ad04 100644
|
||||
--- a/lib/internal/bootstrap/browser.js
|
||||
+++ b/lib/internal/bootstrap/browser.js
|
||||
@@ -12,6 +12,10 @@ const {
|
||||
} = require('internal/util');
|
||||
const config = internalBinding('config');
|
||||
|
||||
+// Non-standard extensions:
|
||||
+const { BroadcastChannel } = require('internal/worker/io');
|
||||
+exposeInterface(globalThis, 'BroadcastChannel', BroadcastChannel);
|
||||
+
|
||||
// https://console.spec.whatwg.org/#console-namespace
|
||||
exposeNamespace(globalThis, 'console',
|
||||
createGlobalConsole());
|
||||
diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js
|
||||
index 7dd89d5f134b09da2678dd54fa9139466fea179c..f235545b6433c0f1dd335e0bb97cd3dc77616c0f 100644
|
||||
--- a/lib/internal/bootstrap/node.js
|
||||
+++ b/lib/internal/bootstrap/node.js
|
||||
@@ -237,10 +237,6 @@ const {
|
||||
queueMicrotask
|
||||
} = require('internal/process/task_queues');
|
||||
|
||||
-// Non-standard extensions:
|
||||
-const { BroadcastChannel } = require('internal/worker/io');
|
||||
-exposeInterface(globalThis, 'BroadcastChannel', BroadcastChannel);
|
||||
-
|
||||
defineOperation(globalThis, 'queueMicrotask', queueMicrotask);
|
||||
|
||||
const timers = require('timers');
|
||||
@@ -0,0 +1 @@
|
||||
fix_fallback_to_x11_capturer_on_wayland.patch
|
||||
|
||||
58
patches/webrtc/fix_fallback_to_x11_capturer_on_wayland.patch
Normal file
58
patches/webrtc/fix_fallback_to_x11_capturer_on_wayland.patch
Normal file
@@ -0,0 +1,58 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: VerteDinde <keeleymhammond@gmail.com>
|
||||
Date: Sun, 5 Mar 2023 21:04:37 -0800
|
||||
Subject: fix: fallback to X11 capturer on Wayland
|
||||
|
||||
CL: https://webrtc-review.googlesource.com/c/src/+/279163
|
||||
|
||||
Desktop Capturer behaves inconsistently on Wayland. PipeWire does not
|
||||
always successfully start; if it does not, we return a nullptr rather
|
||||
than falling back on the X11 capturer, crashing the application.
|
||||
|
||||
If the X11 capturer is enabled, we should at minimum try to fallback
|
||||
to X11 for desktop capturer. This patch re-enables that fallback,
|
||||
which was previously default behavior.
|
||||
|
||||
This patch can be removed when 1) this fix is upstreamed, or 2) the
|
||||
stability of PipeWire initialization is improved upstream.
|
||||
|
||||
Patch_Filename: fix_fallback_to_x11_desktop_capturer_wayland.patch
|
||||
|
||||
diff --git a/modules/desktop_capture/screen_capturer_linux.cc b/modules/desktop_capture/screen_capturer_linux.cc
|
||||
index 44993837e8bbd84a11ec9d187349a95f83258910..cd9f8b0be6a19d057fe9150382fb72bc4032b343 100644
|
||||
--- a/modules/desktop_capture/screen_capturer_linux.cc
|
||||
+++ b/modules/desktop_capture/screen_capturer_linux.cc
|
||||
@@ -34,11 +34,10 @@ std::unique_ptr<DesktopCapturer> DesktopCapturer::CreateRawScreenCapturer(
|
||||
#endif // defined(WEBRTC_USE_PIPEWIRE)
|
||||
|
||||
#if defined(WEBRTC_USE_X11)
|
||||
- if (!DesktopCapturer::IsRunningUnderWayland())
|
||||
- return ScreenCapturerX11::CreateRawScreenCapturer(options);
|
||||
-#endif // defined(WEBRTC_USE_X11)
|
||||
-
|
||||
+ return ScreenCapturerX11::CreateRawScreenCapturer(options);
|
||||
+#else
|
||||
return nullptr;
|
||||
+#endif // defined(WEBRTC_USE_X11)
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
diff --git a/modules/desktop_capture/window_capturer_linux.cc b/modules/desktop_capture/window_capturer_linux.cc
|
||||
index 4205bf9bc0eede48cdc39353c77ceb6e7529fd51..785dc01a1911fd027401b1461223668333e05558 100644
|
||||
--- a/modules/desktop_capture/window_capturer_linux.cc
|
||||
+++ b/modules/desktop_capture/window_capturer_linux.cc
|
||||
@@ -34,11 +34,10 @@ std::unique_ptr<DesktopCapturer> DesktopCapturer::CreateRawWindowCapturer(
|
||||
#endif // defined(WEBRTC_USE_PIPEWIRE)
|
||||
|
||||
#if defined(WEBRTC_USE_X11)
|
||||
- if (!DesktopCapturer::IsRunningUnderWayland())
|
||||
- return WindowCapturerX11::CreateRawWindowCapturer(options);
|
||||
-#endif // defined(WEBRTC_USE_X11)
|
||||
-
|
||||
+ return WindowCapturerX11::CreateRawWindowCapturer(options);
|
||||
+#else
|
||||
return nullptr;
|
||||
+#endif // defined(WEBRTC_USE_X11)
|
||||
}
|
||||
|
||||
} // namespace webrtc
|
||||
@@ -126,6 +126,9 @@ BrowserView::~BrowserView() {
|
||||
}
|
||||
|
||||
void BrowserView::WebContentsDestroyed() {
|
||||
if (owner_window())
|
||||
owner_window()->window()->RemoveDraggableRegionProvider(this);
|
||||
|
||||
api_web_contents_ = nullptr;
|
||||
web_contents_.Reset();
|
||||
Unpin();
|
||||
|
||||
@@ -112,6 +112,7 @@ BrowserWindow::~BrowserWindow() {
|
||||
api_web_contents_->RemoveObserver(this);
|
||||
// Destroy the WebContents.
|
||||
OnCloseContents();
|
||||
api_web_contents_->Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +140,6 @@ void BrowserWindow::WebContentsDestroyed() {
|
||||
|
||||
void BrowserWindow::OnCloseContents() {
|
||||
BaseWindow::ResetBrowserViews();
|
||||
api_web_contents_->Destroy();
|
||||
}
|
||||
|
||||
void BrowserWindow::OnRendererResponsive(content::RenderProcessHost*) {
|
||||
@@ -198,7 +198,11 @@ void BrowserWindow::OnCloseButtonClicked(bool* prevent_default) {
|
||||
|
||||
// Trigger beforeunload events for associated BrowserViews.
|
||||
for (NativeBrowserView* view : window_->browser_views()) {
|
||||
auto* vwc = view->GetInspectableWebContents()->GetWebContents();
|
||||
auto* iwc = view->GetInspectableWebContents();
|
||||
if (!iwc)
|
||||
continue;
|
||||
|
||||
auto* vwc = iwc->GetWebContents();
|
||||
auto* api_web_contents = api::WebContents::From(vwc);
|
||||
|
||||
// Required to make beforeunload handler work.
|
||||
|
||||
@@ -207,27 +207,31 @@ void DesktopCapturer::StartHandling(bool capture_window,
|
||||
// Initialize the source list.
|
||||
// Apply the new thumbnail size and restart capture.
|
||||
if (capture_window) {
|
||||
window_capturer_ = std::make_unique<NativeDesktopMediaList>(
|
||||
DesktopMediaList::Type::kWindow,
|
||||
content::desktop_capture::CreateWindowCapturer());
|
||||
window_capturer_->SetThumbnailSize(thumbnail_size);
|
||||
window_capturer_->Update(
|
||||
base::BindOnce(&DesktopCapturer::UpdateSourcesList,
|
||||
weak_ptr_factory_.GetWeakPtr(),
|
||||
window_capturer_.get()),
|
||||
/* refresh_thumbnails = */ true);
|
||||
if (auto capturer = content::desktop_capture::CreateWindowCapturer();
|
||||
capturer) {
|
||||
window_capturer_ = std::make_unique<NativeDesktopMediaList>(
|
||||
DesktopMediaList::Type::kWindow, std::move(capturer));
|
||||
window_capturer_->SetThumbnailSize(thumbnail_size);
|
||||
window_capturer_->Update(
|
||||
base::BindOnce(&DesktopCapturer::UpdateSourcesList,
|
||||
weak_ptr_factory_.GetWeakPtr(),
|
||||
window_capturer_.get()),
|
||||
/* refresh_thumbnails = */ true);
|
||||
}
|
||||
}
|
||||
|
||||
if (capture_screen) {
|
||||
screen_capturer_ = std::make_unique<NativeDesktopMediaList>(
|
||||
DesktopMediaList::Type::kScreen,
|
||||
content::desktop_capture::CreateScreenCapturer());
|
||||
screen_capturer_->SetThumbnailSize(thumbnail_size);
|
||||
screen_capturer_->Update(
|
||||
base::BindOnce(&DesktopCapturer::UpdateSourcesList,
|
||||
weak_ptr_factory_.GetWeakPtr(),
|
||||
screen_capturer_.get()),
|
||||
/* refresh_thumbnails = */ true);
|
||||
if (auto capturer = content::desktop_capture::CreateScreenCapturer();
|
||||
capturer) {
|
||||
screen_capturer_ = std::make_unique<NativeDesktopMediaList>(
|
||||
DesktopMediaList::Type::kScreen, std::move(capturer));
|
||||
screen_capturer_->SetThumbnailSize(thumbnail_size);
|
||||
screen_capturer_->Update(
|
||||
base::BindOnce(&DesktopCapturer::UpdateSourcesList,
|
||||
weak_ptr_factory_.GetWeakPtr(),
|
||||
screen_capturer_.get()),
|
||||
/* refresh_thumbnails = */ true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "net/base/mac/url_conversions.h"
|
||||
#include "shell/browser/mac/dict_util.h"
|
||||
#include "shell/browser/mac/electron_application.h"
|
||||
#include "shell/browser/ui/cocoa/NSColor+Hex.h"
|
||||
#include "shell/common/color_util.h"
|
||||
#include "shell/common/gin_converters/gurl_converter.h"
|
||||
#include "shell/common/gin_converters/value_converter.h"
|
||||
|
||||
@@ -18,14 +18,19 @@
|
||||
#include "mojo/public/cpp/system/data_pipe_producer.h"
|
||||
#include "net/base/load_flags.h"
|
||||
#include "net/http/http_util.h"
|
||||
#include "net/url_request/redirect_util.h"
|
||||
#include "services/network/public/cpp/resource_request.h"
|
||||
#include "services/network/public/cpp/simple_url_loader.h"
|
||||
#include "services/network/public/cpp/url_util.h"
|
||||
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"
|
||||
#include "services/network/public/mojom/chunked_data_pipe_getter.mojom.h"
|
||||
#include "services/network/public/mojom/http_raw_headers.mojom.h"
|
||||
#include "services/network/public/mojom/url_loader_factory.mojom.h"
|
||||
#include "shell/browser/api/electron_api_session.h"
|
||||
#include "shell/browser/electron_browser_context.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
#include "shell/browser/net/asar/asar_url_loader_factory.h"
|
||||
#include "shell/browser/protocol_registry.h"
|
||||
#include "shell/common/gin_converters/callback_converter.h"
|
||||
#include "shell/common/gin_converters/gurl_converter.h"
|
||||
#include "shell/common/gin_converters/net_converter.h"
|
||||
@@ -336,34 +341,49 @@ gin::WrapperInfo SimpleURLLoaderWrapper::kWrapperInfo = {
|
||||
gin::kEmbedderNativeGin};
|
||||
|
||||
SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
|
||||
ElectronBrowserContext* browser_context,
|
||||
std::unique_ptr<network::ResourceRequest> request,
|
||||
network::mojom::URLLoaderFactory* url_loader_factory,
|
||||
int options) {
|
||||
if (!request->trusted_params)
|
||||
request->trusted_params = network::ResourceRequest::TrustedParams();
|
||||
int options)
|
||||
: browser_context_(browser_context),
|
||||
request_options_(options),
|
||||
request_(std::move(request)) {
|
||||
if (!request_->trusted_params)
|
||||
request_->trusted_params = network::ResourceRequest::TrustedParams();
|
||||
mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
|
||||
url_loader_network_observer_remote;
|
||||
url_loader_network_observer_receivers_.Add(
|
||||
this,
|
||||
url_loader_network_observer_remote.InitWithNewPipeAndPassReceiver());
|
||||
request->trusted_params->url_loader_network_observer =
|
||||
request_->trusted_params->url_loader_network_observer =
|
||||
std::move(url_loader_network_observer_remote);
|
||||
// Chromium filters headers using browser rules, while for net module we have
|
||||
// every header passed. The following setting will allow us to capture the
|
||||
// raw headers in the URLLoader.
|
||||
request->trusted_params->report_raw_headers = true;
|
||||
// SimpleURLLoader wants to control the request body itself. We have other
|
||||
// ideas.
|
||||
auto request_body = std::move(request->request_body);
|
||||
auto* request_ref = request.get();
|
||||
request_->trusted_params->report_raw_headers = true;
|
||||
Start();
|
||||
}
|
||||
|
||||
void SimpleURLLoaderWrapper::Start() {
|
||||
// Make a copy of the request; we'll need to re-send it if we get redirected.
|
||||
auto request = std::make_unique<network::ResourceRequest>();
|
||||
*request = *request_;
|
||||
|
||||
// SimpleURLLoader has no way to set a data pipe as the request body, which
|
||||
// we need to do for streaming upload, so instead we "cheat" and pretend to
|
||||
// SimpleURLLoader like there is no request_body when we construct it. Later,
|
||||
// we will sneakily put the request_body back while it isn't looking.
|
||||
scoped_refptr<network::ResourceRequestBody> request_body =
|
||||
std::move(request->request_body);
|
||||
|
||||
network::ResourceRequest* request_ref = request.get();
|
||||
loader_ =
|
||||
network::SimpleURLLoader::Create(std::move(request), kTrafficAnnotation);
|
||||
if (request_body) {
|
||||
|
||||
if (request_body)
|
||||
request_ref->request_body = std::move(request_body);
|
||||
}
|
||||
|
||||
loader_->SetAllowHttpErrorResults(true);
|
||||
loader_->SetURLLoaderFactoryOptions(options);
|
||||
loader_->SetURLLoaderFactoryOptions(request_options_);
|
||||
loader_->SetOnResponseStartedCallback(base::BindOnce(
|
||||
&SimpleURLLoaderWrapper::OnResponseStarted, base::Unretained(this)));
|
||||
loader_->SetOnRedirectCallback(base::BindRepeating(
|
||||
@@ -373,7 +393,8 @@ SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
|
||||
loader_->SetOnDownloadProgressCallback(base::BindRepeating(
|
||||
&SimpleURLLoaderWrapper::OnDownloadProgress, base::Unretained(this)));
|
||||
|
||||
loader_->DownloadAsStream(url_loader_factory, this);
|
||||
url_loader_factory_ = GetURLLoaderFactoryForURL(request_ref->url);
|
||||
loader_->DownloadAsStream(url_loader_factory_.get(), this);
|
||||
}
|
||||
|
||||
void SimpleURLLoaderWrapper::Pin() {
|
||||
@@ -458,6 +479,42 @@ void SimpleURLLoaderWrapper::Cancel() {
|
||||
// This ensures that no further callbacks will be called, so there's no need
|
||||
// for additional guards.
|
||||
}
|
||||
scoped_refptr<network::SharedURLLoaderFactory>
|
||||
SimpleURLLoaderWrapper::GetURLLoaderFactoryForURL(const GURL& url) {
|
||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory;
|
||||
auto* protocol_registry =
|
||||
ProtocolRegistry::FromBrowserContext(browser_context_);
|
||||
// Explicitly handle intercepted protocols here, even though
|
||||
// ProxyingURLLoaderFactory would handle them later on, so that we can
|
||||
// correctly intercept file:// scheme URLs.
|
||||
if (protocol_registry->IsProtocolIntercepted(url.scheme())) {
|
||||
auto& protocol_handler =
|
||||
protocol_registry->intercept_handlers().at(url.scheme());
|
||||
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
|
||||
ElectronURLLoaderFactory::Create(protocol_handler.first,
|
||||
protocol_handler.second);
|
||||
url_loader_factory = network::SharedURLLoaderFactory::Create(
|
||||
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
|
||||
std::move(pending_remote)));
|
||||
} else if (protocol_registry->IsProtocolRegistered(url.scheme())) {
|
||||
auto& protocol_handler = protocol_registry->handlers().at(url.scheme());
|
||||
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
|
||||
ElectronURLLoaderFactory::Create(protocol_handler.first,
|
||||
protocol_handler.second);
|
||||
url_loader_factory = network::SharedURLLoaderFactory::Create(
|
||||
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
|
||||
std::move(pending_remote)));
|
||||
} else if (url.SchemeIsFile()) {
|
||||
mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote =
|
||||
AsarURLLoaderFactory::Create();
|
||||
url_loader_factory = network::SharedURLLoaderFactory::Create(
|
||||
std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
|
||||
std::move(pending_remote)));
|
||||
} else {
|
||||
url_loader_factory = browser_context_->GetURLLoaderFactory();
|
||||
}
|
||||
return url_loader_factory;
|
||||
}
|
||||
|
||||
// static
|
||||
gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
||||
@@ -634,12 +691,9 @@ gin::Handle<SimpleURLLoaderWrapper> SimpleURLLoaderWrapper::Create(
|
||||
session = Session::FromPartition(args->isolate(), "");
|
||||
}
|
||||
|
||||
auto url_loader_factory = session->browser_context()->GetURLLoaderFactory();
|
||||
|
||||
auto ret = gin::CreateHandle(
|
||||
args->isolate(),
|
||||
new SimpleURLLoaderWrapper(std::move(request), url_loader_factory.get(),
|
||||
options));
|
||||
args->isolate(), new SimpleURLLoaderWrapper(session->browser_context(),
|
||||
std::move(request), options));
|
||||
ret->Pin();
|
||||
if (!chunk_pipe_getter.IsEmpty()) {
|
||||
ret->PinBodyGetter(chunk_pipe_getter);
|
||||
@@ -691,6 +745,45 @@ void SimpleURLLoaderWrapper::OnRedirect(
|
||||
const network::mojom::URLResponseHead& response_head,
|
||||
std::vector<std::string>* removed_headers) {
|
||||
Emit("redirect", redirect_info, response_head.headers.get());
|
||||
|
||||
if (!loader_)
|
||||
// The redirect was aborted by JS.
|
||||
return;
|
||||
|
||||
// Optimization: if both the old and new URLs are handled by the network
|
||||
// service, just FollowRedirect.
|
||||
if (network::IsURLHandledByNetworkService(redirect_info.new_url) &&
|
||||
network::IsURLHandledByNetworkService(request_->url))
|
||||
return;
|
||||
|
||||
// Otherwise, restart the request (potentially picking a new
|
||||
// URLLoaderFactory). See
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/loader/navigation_url_loader_impl.cc;l=534-550;drc=fbaec92ad5982f83aa4544d5c88d66d08034a9f4
|
||||
|
||||
bool should_clear_upload = false;
|
||||
net::RedirectUtil::UpdateHttpRequest(
|
||||
request_->url, request_->method, redirect_info, *removed_headers,
|
||||
/* modified_headers = */ absl::nullopt, &request_->headers,
|
||||
&should_clear_upload);
|
||||
if (should_clear_upload) {
|
||||
// The request body is no longer applicable.
|
||||
request_->request_body.reset();
|
||||
}
|
||||
|
||||
request_->url = redirect_info.new_url;
|
||||
request_->method = redirect_info.new_method;
|
||||
request_->site_for_cookies = redirect_info.new_site_for_cookies;
|
||||
|
||||
// See if navigation network isolation key needs to be updated.
|
||||
request_->trusted_params->isolation_info =
|
||||
request_->trusted_params->isolation_info.CreateForRedirect(
|
||||
url::Origin::Create(request_->url));
|
||||
|
||||
request_->referrer = GURL(redirect_info.new_referrer);
|
||||
request_->referrer_policy = redirect_info.new_referrer_policy;
|
||||
request_->navigation_redirect_chain.push_back(redirect_info.new_url);
|
||||
|
||||
Start();
|
||||
}
|
||||
|
||||
void SimpleURLLoaderWrapper::OnUploadProgress(uint64_t position,
|
||||
|
||||
@@ -31,8 +31,13 @@ class Handle;
|
||||
namespace network {
|
||||
class SimpleURLLoader;
|
||||
struct ResourceRequest;
|
||||
class SharedURLLoaderFactory;
|
||||
} // namespace network
|
||||
|
||||
namespace electron {
|
||||
class ElectronBrowserContext;
|
||||
}
|
||||
|
||||
namespace electron::api {
|
||||
|
||||
/** Wraps a SimpleURLLoader to make it usable from JavaScript */
|
||||
@@ -54,8 +59,8 @@ class SimpleURLLoaderWrapper
|
||||
const char* GetTypeName() override;
|
||||
|
||||
private:
|
||||
SimpleURLLoaderWrapper(std::unique_ptr<network::ResourceRequest> request,
|
||||
network::mojom::URLLoaderFactory* url_loader_factory,
|
||||
SimpleURLLoaderWrapper(ElectronBrowserContext* browser_context,
|
||||
std::unique_ptr<network::ResourceRequest> request,
|
||||
int options);
|
||||
|
||||
// SimpleURLLoaderStreamConsumer:
|
||||
@@ -99,6 +104,9 @@ class SimpleURLLoaderWrapper
|
||||
mojo::PendingReceiver<network::mojom::URLLoaderNetworkServiceObserver>
|
||||
observer) override;
|
||||
|
||||
scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactoryForURL(
|
||||
const GURL& url);
|
||||
|
||||
// SimpleURLLoader callbacks
|
||||
void OnResponseStarted(const GURL& final_url,
|
||||
const network::mojom::URLResponseHead& response_head);
|
||||
@@ -112,6 +120,10 @@ class SimpleURLLoaderWrapper
|
||||
void Pin();
|
||||
void PinBodyGetter(v8::Local<v8::Value>);
|
||||
|
||||
ElectronBrowserContext* browser_context_;
|
||||
int request_options_;
|
||||
std::unique_ptr<network::ResourceRequest> request_;
|
||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
|
||||
std::unique_ptr<network::SimpleURLLoader> loader_;
|
||||
v8::Global<v8::Value> pinned_wrapper_;
|
||||
v8::Global<v8::Value> pinned_chunk_pipe_getter_;
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
#include "shell/common/gin_converters/gurl_converter.h"
|
||||
#include "shell/common/gin_converters/image_converter.h"
|
||||
#include "shell/common/gin_converters/net_converter.h"
|
||||
#include "shell/common/gin_converters/optional_converter.h"
|
||||
#include "shell/common/gin_converters/value_converter.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/gin_helper/object_template_builder.h"
|
||||
@@ -1222,9 +1223,7 @@ void WebContents::CloseContents(content::WebContents* source) {
|
||||
for (ExtendedWebContentsObserver& observer : observers_)
|
||||
observer.OnCloseContents();
|
||||
|
||||
// If there are observers, OnCloseContents will call Destroy()
|
||||
if (observers_.empty())
|
||||
Destroy();
|
||||
Destroy();
|
||||
}
|
||||
|
||||
void WebContents::ActivateContents(content::WebContents* source) {
|
||||
@@ -1508,7 +1507,14 @@ content::JavaScriptDialogManager* WebContents::GetJavaScriptDialogManager(
|
||||
}
|
||||
|
||||
void WebContents::OnAudioStateChanged(bool audible) {
|
||||
Emit("-audio-state-changed", audible);
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope handle_scope(isolate);
|
||||
gin::Handle<gin_helper::internal::Event> event =
|
||||
gin_helper::internal::Event::New(isolate);
|
||||
v8::Local<v8::Object> event_object = event.ToV8().As<v8::Object>();
|
||||
gin::Dictionary dict(isolate, event_object);
|
||||
dict.Set("audible", audible);
|
||||
EmitWithoutEvent("audio-state-changed", event);
|
||||
}
|
||||
|
||||
void WebContents::BeforeUnloadFired(bool proceed,
|
||||
@@ -1621,8 +1627,7 @@ void WebContents::RenderFrameHostChanged(content::RenderFrameHost* old_host,
|
||||
//
|
||||
// |old_host| can be a nullptr so we use |new_host| for looking up the
|
||||
// WebFrameMain instance.
|
||||
auto* web_frame =
|
||||
WebFrameMain::FromFrameTreeNodeId(new_host->GetFrameTreeNodeId());
|
||||
auto* web_frame = WebFrameMain::FromRenderFrameHost(new_host);
|
||||
if (web_frame) {
|
||||
web_frame->UpdateRenderFrameHost(new_host);
|
||||
}
|
||||
@@ -1761,7 +1766,7 @@ void WebContents::DidStopLoading() {
|
||||
}
|
||||
|
||||
bool WebContents::EmitNavigationEvent(
|
||||
const std::string& event,
|
||||
const std::string& event_name,
|
||||
content::NavigationHandle* navigation_handle) {
|
||||
bool is_main_frame = navigation_handle->IsInMainFrame();
|
||||
int frame_tree_node_id = navigation_handle->GetFrameTreeNodeId();
|
||||
@@ -1782,8 +1787,30 @@ bool WebContents::EmitNavigationEvent(
|
||||
}
|
||||
bool is_same_document = navigation_handle->IsSameDocument();
|
||||
auto url = navigation_handle->GetURL();
|
||||
return Emit(event, url, is_same_document, is_main_frame, frame_process_id,
|
||||
frame_routing_id);
|
||||
content::RenderFrameHost* initiator_frame_host =
|
||||
navigation_handle->GetInitiatorFrameToken().has_value()
|
||||
? content::RenderFrameHost::FromFrameToken(
|
||||
navigation_handle->GetInitiatorProcessID(),
|
||||
navigation_handle->GetInitiatorFrameToken().value())
|
||||
: nullptr;
|
||||
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope handle_scope(isolate);
|
||||
|
||||
gin::Handle<gin_helper::internal::Event> event =
|
||||
gin_helper::internal::Event::New(isolate);
|
||||
v8::Local<v8::Object> event_object = event.ToV8().As<v8::Object>();
|
||||
|
||||
gin_helper::Dictionary dict(isolate, event_object);
|
||||
dict.Set("url", url);
|
||||
dict.Set("isSameDocument", is_same_document);
|
||||
dict.Set("isMainFrame", is_main_frame);
|
||||
dict.Set("frame", frame_host);
|
||||
dict.SetGetter("initiator", initiator_frame_host);
|
||||
|
||||
EmitWithoutEvent(event_name, event, url, is_same_document, is_main_frame,
|
||||
frame_process_id, frame_routing_id);
|
||||
return event->GetDefaultPrevented();
|
||||
}
|
||||
|
||||
void WebContents::Message(bool internal,
|
||||
@@ -1837,22 +1864,23 @@ class ReplyChannel : public gin::Wrappable<ReplyChannel> {
|
||||
}
|
||||
const char* GetTypeName() override { return "ReplyChannel"; }
|
||||
|
||||
void SendError(const std::string& msg) {
|
||||
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
// If there's no current context, it means we're shutting down, so we
|
||||
// don't need to send an event.
|
||||
if (!isolate->GetCurrentContext().IsEmpty()) {
|
||||
v8::HandleScope scope(isolate);
|
||||
auto message = gin::DataObjectBuilder(isolate).Set("error", msg).Build();
|
||||
SendReply(isolate, message);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
explicit ReplyChannel(InvokeCallback callback)
|
||||
: callback_(std::move(callback)) {}
|
||||
~ReplyChannel() override {
|
||||
if (callback_) {
|
||||
v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
|
||||
// If there's no current context, it means we're shutting down, so we
|
||||
// don't need to send an event.
|
||||
if (!isolate->GetCurrentContext().IsEmpty()) {
|
||||
v8::HandleScope scope(isolate);
|
||||
auto message = gin::DataObjectBuilder(isolate)
|
||||
.Set("error", "reply was never sent")
|
||||
.Build();
|
||||
SendReply(isolate, message);
|
||||
}
|
||||
}
|
||||
if (callback_)
|
||||
SendError("reply was never sent");
|
||||
}
|
||||
|
||||
bool SendReply(v8::Isolate* isolate, v8::Local<v8::Value> arg) {
|
||||
@@ -1877,8 +1905,14 @@ gin::Handle<gin_helper::internal::Event> WebContents::MakeEventWithSender(
|
||||
content::RenderFrameHost* frame,
|
||||
electron::mojom::ElectronApiIPC::InvokeCallback callback) {
|
||||
v8::Local<v8::Object> wrapper;
|
||||
if (!GetWrapper(isolate).ToLocal(&wrapper))
|
||||
if (!GetWrapper(isolate).ToLocal(&wrapper)) {
|
||||
if (callback) {
|
||||
// We must always invoke the callback if present.
|
||||
ReplyChannel::Create(isolate, std::move(callback))
|
||||
->SendError("WebContents was destroyed");
|
||||
}
|
||||
return gin::Handle<gin_helper::internal::Event>();
|
||||
}
|
||||
gin::Handle<gin_helper::internal::Event> event =
|
||||
gin_helper::internal::Event::New(isolate);
|
||||
gin_helper::Dictionary dict(isolate, event.ToV8().As<v8::Object>());
|
||||
@@ -3589,18 +3623,26 @@ v8::Local<v8::Promise> WebContents::TakeHeapSnapshot(
|
||||
flags = base::File::AddFlagsForPassingToUntrustedProcess(flags);
|
||||
base::File file(file_path, flags);
|
||||
if (!file.IsValid()) {
|
||||
promise.RejectWithErrorMessage("takeHeapSnapshot failed");
|
||||
promise.RejectWithErrorMessage(
|
||||
"Failed to take heap snapshot with invalid file path " +
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
base::WideToUTF8(file_path.value()));
|
||||
#else
|
||||
file_path.value());
|
||||
#endif
|
||||
return handle;
|
||||
}
|
||||
|
||||
auto* frame_host = web_contents()->GetPrimaryMainFrame();
|
||||
if (!frame_host) {
|
||||
promise.RejectWithErrorMessage("takeHeapSnapshot failed");
|
||||
promise.RejectWithErrorMessage(
|
||||
"Failed to take heap snapshot with invalid webContents main frame");
|
||||
return handle;
|
||||
}
|
||||
|
||||
if (!frame_host->IsRenderFrameLive()) {
|
||||
promise.RejectWithErrorMessage("takeHeapSnapshot failed");
|
||||
promise.RejectWithErrorMessage(
|
||||
"Failed to take heap snapshot with nonexistent render frame");
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -3620,7 +3662,7 @@ v8::Local<v8::Promise> WebContents::TakeHeapSnapshot(
|
||||
if (success) {
|
||||
promise.Resolve();
|
||||
} else {
|
||||
promise.RejectWithErrorMessage("takeHeapSnapshot failed");
|
||||
promise.RejectWithErrorMessage("Failed to take heap snapshot");
|
||||
}
|
||||
},
|
||||
base::Owned(std::move(electron_renderer)), std::move(promise)));
|
||||
|
||||
@@ -94,17 +94,34 @@ struct UserData : public base::SupportsUserData::Data {
|
||||
WebRequest* data;
|
||||
};
|
||||
|
||||
// Test whether the URL of |request| matches |patterns|.
|
||||
bool MatchesFilterCondition(extensions::WebRequestInfo* info,
|
||||
const std::set<URLPattern>& patterns) {
|
||||
if (patterns.empty())
|
||||
return true;
|
||||
|
||||
for (const auto& pattern : patterns) {
|
||||
if (pattern.MatchesURL(info->url))
|
||||
return true;
|
||||
extensions::WebRequestResourceType ParseResourceType(const std::string& value) {
|
||||
if (value == "mainFrame") {
|
||||
return extensions::WebRequestResourceType::MAIN_FRAME;
|
||||
} else if (value == "subFrame") {
|
||||
return extensions::WebRequestResourceType::SUB_FRAME;
|
||||
} else if (value == "stylesheet") {
|
||||
return extensions::WebRequestResourceType::STYLESHEET;
|
||||
} else if (value == "script") {
|
||||
return extensions::WebRequestResourceType::SCRIPT;
|
||||
} else if (value == "image") {
|
||||
return extensions::WebRequestResourceType::IMAGE;
|
||||
} else if (value == "font") {
|
||||
return extensions::WebRequestResourceType::FONT;
|
||||
} else if (value == "object") {
|
||||
return extensions::WebRequestResourceType::OBJECT;
|
||||
} else if (value == "xhr") {
|
||||
return extensions::WebRequestResourceType::XHR;
|
||||
} else if (value == "ping") {
|
||||
return extensions::WebRequestResourceType::PING;
|
||||
} else if (value == "cspReport") {
|
||||
return extensions::WebRequestResourceType::CSP_REPORT;
|
||||
} else if (value == "media") {
|
||||
return extensions::WebRequestResourceType::MEDIA;
|
||||
} else if (value == "webSocket") {
|
||||
return extensions::WebRequestResourceType::WEB_SOCKET;
|
||||
} else {
|
||||
return extensions::WebRequestResourceType::OTHER;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert HttpResponseHeaders to V8.
|
||||
@@ -247,17 +264,54 @@ void ReadFromResponse(v8::Isolate* isolate,
|
||||
|
||||
gin::WrapperInfo WebRequest::kWrapperInfo = {gin::kEmbedderNativeGin};
|
||||
|
||||
WebRequest::SimpleListenerInfo::SimpleListenerInfo(
|
||||
std::set<URLPattern> patterns_,
|
||||
SimpleListener listener_)
|
||||
: url_patterns(std::move(patterns_)), listener(listener_) {}
|
||||
WebRequest::RequestFilter::RequestFilter(
|
||||
std::set<URLPattern> url_patterns,
|
||||
std::set<extensions::WebRequestResourceType> types)
|
||||
: url_patterns_(std::move(url_patterns)), types_(std::move(types)) {}
|
||||
WebRequest::RequestFilter::RequestFilter(const RequestFilter&) = default;
|
||||
WebRequest::RequestFilter::RequestFilter() = default;
|
||||
WebRequest::RequestFilter::~RequestFilter() = default;
|
||||
|
||||
void WebRequest::RequestFilter::AddUrlPattern(URLPattern pattern) {
|
||||
url_patterns_.emplace(std::move(pattern));
|
||||
}
|
||||
|
||||
void WebRequest::RequestFilter::AddType(
|
||||
extensions::WebRequestResourceType type) {
|
||||
types_.insert(type);
|
||||
}
|
||||
|
||||
bool WebRequest::RequestFilter::MatchesURL(const GURL& url) const {
|
||||
if (url_patterns_.empty())
|
||||
return true;
|
||||
|
||||
for (const auto& pattern : url_patterns_) {
|
||||
if (pattern.MatchesURL(url))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WebRequest::RequestFilter::MatchesType(
|
||||
extensions::WebRequestResourceType type) const {
|
||||
return types_.empty() || types_.find(type) != types_.end();
|
||||
}
|
||||
|
||||
bool WebRequest::RequestFilter::MatchesRequest(
|
||||
extensions::WebRequestInfo* info) const {
|
||||
return MatchesURL(info->url) && MatchesType(info->web_request_type);
|
||||
}
|
||||
|
||||
WebRequest::SimpleListenerInfo::SimpleListenerInfo(RequestFilter filter_,
|
||||
SimpleListener listener_)
|
||||
: filter(std::move(filter_)), listener(listener_) {}
|
||||
WebRequest::SimpleListenerInfo::SimpleListenerInfo() = default;
|
||||
WebRequest::SimpleListenerInfo::~SimpleListenerInfo() = default;
|
||||
|
||||
WebRequest::ResponseListenerInfo::ResponseListenerInfo(
|
||||
std::set<URLPattern> patterns_,
|
||||
RequestFilter filter_,
|
||||
ResponseListener listener_)
|
||||
: url_patterns(std::move(patterns_)), listener(listener_) {}
|
||||
: filter(std::move(filter_)), listener(listener_) {}
|
||||
WebRequest::ResponseListenerInfo::ResponseListenerInfo() = default;
|
||||
WebRequest::ResponseListenerInfo::~ResponseListenerInfo() = default;
|
||||
|
||||
@@ -392,8 +446,8 @@ void WebRequest::SetListener(Event event,
|
||||
gin::Arguments* args) {
|
||||
v8::Local<v8::Value> arg;
|
||||
|
||||
// { urls }.
|
||||
std::set<std::string> filter_patterns;
|
||||
// { urls, types }.
|
||||
std::set<std::string> filter_patterns, filter_types;
|
||||
gin::Dictionary dict(args->isolate());
|
||||
if (args->GetNext(&arg) && !arg->IsFunction()) {
|
||||
// Note that gin treats Function as Dictionary when doing conversions, so we
|
||||
@@ -404,16 +458,18 @@ void WebRequest::SetListener(Event event,
|
||||
args->ThrowTypeError("Parameter 'filter' must have property 'urls'.");
|
||||
return;
|
||||
}
|
||||
dict.Get("types", &filter_types);
|
||||
args->GetNext(&arg);
|
||||
}
|
||||
}
|
||||
|
||||
std::set<URLPattern> patterns;
|
||||
RequestFilter filter;
|
||||
|
||||
for (const std::string& filter_pattern : filter_patterns) {
|
||||
URLPattern pattern(URLPattern::SCHEME_ALL);
|
||||
const URLPattern::ParseResult result = pattern.Parse(filter_pattern);
|
||||
if (result == URLPattern::ParseResult::kSuccess) {
|
||||
patterns.insert(pattern);
|
||||
filter.AddUrlPattern(std::move(pattern));
|
||||
} else {
|
||||
const char* error_type = URLPattern::GetParseResultString(result);
|
||||
args->ThrowTypeError("Invalid url pattern " + filter_pattern + ": " +
|
||||
@@ -422,6 +478,16 @@ void WebRequest::SetListener(Event event,
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string& filter_type : filter_types) {
|
||||
auto type = ParseResourceType(filter_type);
|
||||
if (type != extensions::WebRequestResourceType::OTHER) {
|
||||
filter.AddType(type);
|
||||
} else {
|
||||
args->ThrowTypeError("Invalid type " + filter_type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Function or null.
|
||||
Listener listener;
|
||||
if (arg.IsEmpty() ||
|
||||
@@ -433,7 +499,7 @@ void WebRequest::SetListener(Event event,
|
||||
if (listener.is_null())
|
||||
listeners->erase(event);
|
||||
else
|
||||
(*listeners)[event] = {std::move(patterns), std::move(listener)};
|
||||
(*listeners)[event] = {std::move(filter), std::move(listener)};
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
@@ -445,7 +511,7 @@ void WebRequest::HandleSimpleEvent(SimpleEvent event,
|
||||
return;
|
||||
|
||||
const auto& info = iter->second;
|
||||
if (!MatchesFilterCondition(request_info, info.url_patterns))
|
||||
if (!info.filter.MatchesRequest(request_info))
|
||||
return;
|
||||
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
@@ -466,7 +532,7 @@ int WebRequest::HandleResponseEvent(ResponseEvent event,
|
||||
return net::OK;
|
||||
|
||||
const auto& info = iter->second;
|
||||
if (!MatchesFilterCondition(request_info, info.url_patterns))
|
||||
if (!info.filter.MatchesRequest(request_info))
|
||||
return net::OK;
|
||||
|
||||
callbacks_[request_info->id] = std::move(callback);
|
||||
|
||||
@@ -123,20 +123,41 @@ class WebRequest : public gin::Wrappable<WebRequest>, public WebRequestAPI {
|
||||
template <typename T>
|
||||
void OnListenerResult(uint64_t id, T out, v8::Local<v8::Value> response);
|
||||
|
||||
class RequestFilter {
|
||||
public:
|
||||
RequestFilter(std::set<URLPattern>,
|
||||
std::set<extensions::WebRequestResourceType>);
|
||||
RequestFilter(const RequestFilter&);
|
||||
RequestFilter();
|
||||
~RequestFilter();
|
||||
|
||||
void AddUrlPattern(URLPattern pattern);
|
||||
void AddType(extensions::WebRequestResourceType type);
|
||||
|
||||
bool MatchesRequest(extensions::WebRequestInfo* info) const;
|
||||
|
||||
private:
|
||||
bool MatchesURL(const GURL& url) const;
|
||||
bool MatchesType(extensions::WebRequestResourceType type) const;
|
||||
|
||||
std::set<URLPattern> url_patterns_;
|
||||
std::set<extensions::WebRequestResourceType> types_;
|
||||
};
|
||||
|
||||
struct SimpleListenerInfo {
|
||||
std::set<URLPattern> url_patterns;
|
||||
RequestFilter filter;
|
||||
SimpleListener listener;
|
||||
|
||||
SimpleListenerInfo(std::set<URLPattern>, SimpleListener);
|
||||
SimpleListenerInfo(RequestFilter, SimpleListener);
|
||||
SimpleListenerInfo();
|
||||
~SimpleListenerInfo();
|
||||
};
|
||||
|
||||
struct ResponseListenerInfo {
|
||||
std::set<URLPattern> url_patterns;
|
||||
RequestFilter filter;
|
||||
ResponseListener listener;
|
||||
|
||||
ResponseListenerInfo(std::set<URLPattern>, ResponseListener);
|
||||
ResponseListenerInfo(RequestFilter, ResponseListener);
|
||||
ResponseListenerInfo();
|
||||
~ResponseListenerInfo();
|
||||
};
|
||||
|
||||
@@ -363,7 +363,7 @@ class Browser : public WindowListObserver {
|
||||
base::Time last_dock_show_;
|
||||
#endif
|
||||
|
||||
base::Value about_panel_options_;
|
||||
base::Value::Dict about_panel_options_;
|
||||
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
void UpdateBadgeContents(HWND hwnd,
|
||||
|
||||
@@ -162,31 +162,25 @@ bool Browser::IsEmojiPanelSupported() {
|
||||
void Browser::ShowAboutPanel() {
|
||||
const auto& opts = about_panel_options_;
|
||||
|
||||
if (!opts.is_dict()) {
|
||||
LOG(WARNING) << "Called showAboutPanel(), but didn't use "
|
||||
"setAboutPanelSettings() first";
|
||||
return;
|
||||
}
|
||||
|
||||
GtkWidget* dialogWidget = gtk_about_dialog_new();
|
||||
GtkAboutDialog* dialog = GTK_ABOUT_DIALOG(dialogWidget);
|
||||
|
||||
const std::string* str;
|
||||
const base::Value* val;
|
||||
const base::Value::List* list;
|
||||
|
||||
if ((str = opts.FindStringKey("applicationName"))) {
|
||||
if ((str = opts.FindString("applicationName"))) {
|
||||
gtk_about_dialog_set_program_name(dialog, str->c_str());
|
||||
}
|
||||
if ((str = opts.FindStringKey("applicationVersion"))) {
|
||||
if ((str = opts.FindString("applicationVersion"))) {
|
||||
gtk_about_dialog_set_version(dialog, str->c_str());
|
||||
}
|
||||
if ((str = opts.FindStringKey("copyright"))) {
|
||||
if ((str = opts.FindString("copyright"))) {
|
||||
gtk_about_dialog_set_copyright(dialog, str->c_str());
|
||||
}
|
||||
if ((str = opts.FindStringKey("website"))) {
|
||||
if ((str = opts.FindString("website"))) {
|
||||
gtk_about_dialog_set_website(dialog, str->c_str());
|
||||
}
|
||||
if ((str = opts.FindStringKey("iconPath"))) {
|
||||
if ((str = opts.FindString("iconPath"))) {
|
||||
GError* error = nullptr;
|
||||
constexpr int width = 64; // width of about panel icon in pixels
|
||||
constexpr int height = 64; // height of about panel icon in pixels
|
||||
@@ -203,9 +197,9 @@ void Browser::ShowAboutPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
if ((val = opts.FindListKey("authors"))) {
|
||||
if ((list = opts.FindList("authors"))) {
|
||||
std::vector<const char*> cstrs;
|
||||
for (const auto& authorVal : val->GetList()) {
|
||||
for (const auto& authorVal : *list) {
|
||||
if (authorVal.is_string()) {
|
||||
cstrs.push_back(authorVal.GetString().c_str());
|
||||
}
|
||||
@@ -218,12 +212,15 @@ void Browser::ShowAboutPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
gtk_dialog_run(GTK_DIALOG(dialog));
|
||||
gtk_widget_destroy(dialogWidget);
|
||||
// destroy the widget when it closes
|
||||
g_signal_connect_swapped(dialogWidget, "response",
|
||||
G_CALLBACK(gtk_widget_destroy), dialogWidget);
|
||||
|
||||
gtk_widget_show_all(dialogWidget);
|
||||
}
|
||||
|
||||
void Browser::SetAboutPanelOptions(base::Value::Dict options) {
|
||||
about_panel_options_ = base::Value(std::move(options));
|
||||
about_panel_options_ = std::move(options);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -513,8 +513,7 @@ void Browser::DockSetIcon(v8::Isolate* isolate, v8::Local<v8::Value> icon) {
|
||||
}
|
||||
|
||||
void Browser::ShowAboutPanel() {
|
||||
NSDictionary* options =
|
||||
DictionaryValueToNSDictionary(about_panel_options_.GetDict());
|
||||
NSDictionary* options = DictionaryValueToNSDictionary(about_panel_options_);
|
||||
|
||||
// Credits must be a NSAttributedString instead of NSString
|
||||
NSString* credits = (NSString*)options[@"Credits"];
|
||||
@@ -537,13 +536,13 @@ void Browser::ShowAboutPanel() {
|
||||
}
|
||||
|
||||
void Browser::SetAboutPanelOptions(base::Value::Dict options) {
|
||||
about_panel_options_.GetDict().clear();
|
||||
about_panel_options_.clear();
|
||||
|
||||
for (const auto pair : options) {
|
||||
std::string key = pair.first;
|
||||
if (!key.empty() && pair.second.is_string()) {
|
||||
key[0] = base::ToUpperASCII(key[0]);
|
||||
about_panel_options_.GetDict().Set(key, pair.second.Clone());
|
||||
about_panel_options_.Set(key, pair.second.Clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "base/functional/bind.h"
|
||||
#include "shell/browser/browser.h"
|
||||
|
||||
// must come before other includes. fixes bad #defines from <shlwapi.h>.
|
||||
@@ -734,30 +735,29 @@ void Browser::ShowEmojiPanel() {
|
||||
}
|
||||
|
||||
void Browser::ShowAboutPanel() {
|
||||
base::Value dict(base::Value::Type::DICTIONARY);
|
||||
base::Value::Dict dict;
|
||||
std::string aboutMessage = "";
|
||||
gfx::ImageSkia image;
|
||||
|
||||
// grab defaults from Windows .EXE file
|
||||
std::unique_ptr<FileVersionInfo> exe_info = FetchFileVersionInfo();
|
||||
dict.SetStringKey("applicationName", exe_info->file_description());
|
||||
dict.SetStringKey("applicationVersion", exe_info->product_version());
|
||||
dict.Set("applicationName", exe_info->file_description());
|
||||
dict.Set("applicationVersion", exe_info->product_version());
|
||||
|
||||
if (about_panel_options_.is_dict()) {
|
||||
dict.MergeDictionary(&about_panel_options_);
|
||||
}
|
||||
// Merge user-provided options, overwriting any of the above
|
||||
dict.Merge(about_panel_options_.Clone());
|
||||
|
||||
std::vector<std::string> stringOptions = {
|
||||
"applicationName", "applicationVersion", "copyright", "credits"};
|
||||
|
||||
const std::string* str;
|
||||
for (std::string opt : stringOptions) {
|
||||
if ((str = dict.FindStringKey(opt))) {
|
||||
if ((str = dict.FindString(opt))) {
|
||||
aboutMessage.append(*str).append("\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
if ((str = dict.FindStringKey("iconPath"))) {
|
||||
if ((str = dict.FindString("iconPath"))) {
|
||||
base::FilePath path = base::FilePath::FromUTF8Unsafe(*str);
|
||||
electron::util::PopulateImageSkiaRepsFromPath(&image, path);
|
||||
}
|
||||
@@ -766,11 +766,12 @@ void Browser::ShowAboutPanel() {
|
||||
settings.message = aboutMessage;
|
||||
settings.icon = image;
|
||||
settings.type = electron::MessageBoxType::kInformation;
|
||||
electron::ShowMessageBoxSync(settings);
|
||||
electron::ShowMessageBox(settings,
|
||||
base::BindOnce([](int, bool) { /* do nothing. */ }));
|
||||
}
|
||||
|
||||
void Browser::SetAboutPanelOptions(base::Value::Dict options) {
|
||||
about_panel_options_ = base::Value(std::move(options));
|
||||
about_panel_options_ = std::move(options);
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
||||
@@ -501,13 +501,16 @@ void ElectronBrowserContext::DisplayMediaDeviceChosen(
|
||||
devices.audio_device =
|
||||
blink::MediaStreamDevice(request.audio_type, id, name);
|
||||
} else if (result_dict.Get("audio", &rfh)) {
|
||||
devices.audio_device = blink::MediaStreamDevice(
|
||||
request.audio_type,
|
||||
content::WebContentsMediaCaptureId(rfh->GetProcess()->GetID(),
|
||||
rfh->GetRoutingID(),
|
||||
/* disable_local_echo= */ true)
|
||||
.ToString(),
|
||||
"Tab audio");
|
||||
bool enable_local_echo = false;
|
||||
result_dict.Get("enableLocalEcho", &enable_local_echo);
|
||||
bool disable_local_echo = !enable_local_echo;
|
||||
devices.audio_device =
|
||||
blink::MediaStreamDevice(request.audio_type,
|
||||
content::WebContentsMediaCaptureId(
|
||||
rfh->GetProcess()->GetID(),
|
||||
rfh->GetRoutingID(), disable_local_echo)
|
||||
.ToString(),
|
||||
"Tab audio");
|
||||
} else if (result_dict.Get("audio", &id)) {
|
||||
devices.audio_device =
|
||||
blink::MediaStreamDevice(request.audio_type, id, "System audio");
|
||||
|
||||
@@ -279,7 +279,7 @@ void ElectronBrowserMainParts::PostEarlyInitialization() {
|
||||
env->set_trace_sync_io(env->options()->trace_sync_io);
|
||||
|
||||
// We do not want to crash the main process on unhandled rejections.
|
||||
env->options()->unhandled_rejections = "warn";
|
||||
env->options()->unhandled_rejections = "warn-with-error-code";
|
||||
|
||||
// Add Electron extended APIs.
|
||||
electron_bindings_->BindTo(js_env_->isolate(), env->process_object());
|
||||
|
||||
@@ -36,11 +36,6 @@ void InitializeFeatureList() {
|
||||
disable_features +=
|
||||
std::string(",") + features::kSpareRendererForSitePerProcess.name;
|
||||
|
||||
#if BUILDFLAG(IS_MAC)
|
||||
// Needed for WebUSB implementation
|
||||
enable_features += std::string(",") + device::kNewUsbBackend.name;
|
||||
#endif
|
||||
|
||||
#if !BUILDFLAG(ENABLE_PICTURE_IN_PICTURE)
|
||||
disable_features += std::string(",") + media::kPictureInPicture.name;
|
||||
#endif
|
||||
|
||||
@@ -58,7 +58,11 @@ void CocoaNotification::Show(const NotificationOptions& options) {
|
||||
[notification_ setSoundName:base::SysUTF16ToNSString(options.sound)];
|
||||
}
|
||||
|
||||
[notification_ setHasActionButton:false];
|
||||
if (options.has_reply) {
|
||||
[notification_ setHasReplyButton:true];
|
||||
[notification_ setResponsePlaceholder:base::SysUTF16ToNSString(
|
||||
options.reply_placeholder)];
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
action_index_ = UINT_MAX;
|
||||
@@ -66,7 +70,10 @@ void CocoaNotification::Show(const NotificationOptions& options) {
|
||||
[[[NSMutableArray alloc] init] autorelease];
|
||||
for (const auto& action : options.actions) {
|
||||
if (action.type == u"button") {
|
||||
if (action_index_ == UINT_MAX) {
|
||||
// If the notification has both a reply and actions,
|
||||
// the reply takes precedence and the actions all
|
||||
// become additional actions.
|
||||
if (!options.has_reply && action_index_ == UINT_MAX) {
|
||||
// First button observed is the displayed action
|
||||
[notification_ setHasActionButton:true];
|
||||
[notification_
|
||||
@@ -86,16 +93,11 @@ void CocoaNotification::Show(const NotificationOptions& options) {
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if ([additionalActions count] > 0) {
|
||||
[notification_ setAdditionalActions:additionalActions];
|
||||
}
|
||||
|
||||
if (options.has_reply) {
|
||||
[notification_ setResponsePlaceholder:base::SysUTF16ToNSString(
|
||||
options.reply_placeholder)];
|
||||
[notification_ setHasReplyButton:true];
|
||||
}
|
||||
|
||||
if (!options.close_button_text.empty()) {
|
||||
[notification_ setOtherButtonTitle:base::SysUTF16ToNSString(
|
||||
options.close_button_text)];
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Created by Mathias Leppich on 03/02/14.
|
||||
// Copyright (c) 2014 Bit Bar. All rights reserved.
|
||||
// Copyright (c) 2017 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef ELECTRON_SHELL_BROWSER_UI_COCOA_NSCOLOR_HEX_H_
|
||||
#define ELECTRON_SHELL_BROWSER_UI_COCOA_NSCOLOR_HEX_H_
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface NSColor (Hex)
|
||||
- (NSString*)hexadecimalValue;
|
||||
- (NSString*)RGBAValue;
|
||||
+ (NSColor*)colorWithHexColorString:(NSString*)hex;
|
||||
@end
|
||||
|
||||
#endif // ELECTRON_SHELL_BROWSER_UI_COCOA_NSCOLOR_HEX_H_
|
||||
@@ -1,77 +0,0 @@
|
||||
// Created by Mathias Leppich on 03/02/14.
|
||||
// Copyright (c) 2014 Bit Bar. All rights reserved.
|
||||
// Copyright (c) 2017 GitHub, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/ui/cocoa/NSColor+Hex.h"
|
||||
|
||||
@implementation NSColor (Hex)
|
||||
|
||||
- (NSString*)RGBAValue {
|
||||
double redFloatValue, greenFloatValue, blueFloatValue, alphaFloatValue;
|
||||
|
||||
NSColor* convertedColor =
|
||||
[self colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
|
||||
|
||||
if (convertedColor) {
|
||||
[convertedColor getRed:&redFloatValue
|
||||
green:&greenFloatValue
|
||||
blue:&blueFloatValue
|
||||
alpha:&alphaFloatValue];
|
||||
|
||||
int redIntValue = redFloatValue * 255.99999f;
|
||||
int greenIntValue = greenFloatValue * 255.99999f;
|
||||
int blueIntValue = blueFloatValue * 255.99999f;
|
||||
int alphaIntValue = alphaFloatValue * 255.99999f;
|
||||
|
||||
return
|
||||
[NSString stringWithFormat:@"%02x%02x%02x%02x", redIntValue,
|
||||
greenIntValue, blueIntValue, alphaIntValue];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString*)hexadecimalValue {
|
||||
double redFloatValue, greenFloatValue, blueFloatValue;
|
||||
|
||||
NSColor* convertedColor =
|
||||
[self colorUsingColorSpaceName:NSCalibratedRGBColorSpace];
|
||||
|
||||
if (convertedColor) {
|
||||
[convertedColor getRed:&redFloatValue
|
||||
green:&greenFloatValue
|
||||
blue:&blueFloatValue
|
||||
alpha:NULL];
|
||||
|
||||
int redIntValue = redFloatValue * 255.99999f;
|
||||
int greenIntValue = greenFloatValue * 255.99999f;
|
||||
int blueIntValue = blueFloatValue * 255.99999f;
|
||||
|
||||
return [NSString stringWithFormat:@"#%02x%02x%02x", redIntValue,
|
||||
greenIntValue, blueIntValue];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (NSColor*)colorWithHexColorString:(NSString*)inColorString {
|
||||
unsigned colorCode = 0;
|
||||
|
||||
if (inColorString) {
|
||||
NSScanner* scanner = [NSScanner scannerWithString:inColorString];
|
||||
(void)[scanner scanHexInt:&colorCode]; // ignore error
|
||||
}
|
||||
|
||||
unsigned char redByte = (unsigned char)(colorCode >> 16);
|
||||
unsigned char greenByte = (unsigned char)(colorCode >> 8);
|
||||
unsigned char blueByte = (unsigned char)(colorCode); // masks off high bits
|
||||
|
||||
return [NSColor colorWithCalibratedRed:(CGFloat)redByte / 0xff
|
||||
green:(CGFloat)greenByte / 0xff
|
||||
blue:(CGFloat)blueByte / 0xff
|
||||
alpha:1.0];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -4,9 +4,12 @@
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#include "base/mac/scoped_nsobject.h"
|
||||
#include "shell/browser/ui/cocoa/NSColor+Hex.h"
|
||||
#include "content/public/common/color_parser.h"
|
||||
#include "shell/browser/ui/cocoa/NSString+ANSI.h"
|
||||
#include "skia/ext/skia_utils_mac.h"
|
||||
|
||||
@implementation NSMutableDictionary (ANSI)
|
||||
|
||||
@@ -18,6 +21,7 @@
|
||||
|
||||
for (NSString* codeString in codeArray) {
|
||||
int code = codeString.intValue;
|
||||
SkColor color;
|
||||
switch (code) {
|
||||
case 0:
|
||||
[self removeAllObjects];
|
||||
@@ -37,36 +41,44 @@
|
||||
// case 24: underlined off
|
||||
|
||||
case 30:
|
||||
content::ParseHexColorString(bold ? "#7f7f7f" : "#000000", &color);
|
||||
self[NSForegroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:bold ? @"7f7f7f" : @"000000"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 31:
|
||||
content::ParseHexColorString(bold ? "#cd0000" : "#ff0000", &color);
|
||||
self[NSForegroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:bold ? @"cd0000" : @"ff0000"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 32:
|
||||
content::ParseHexColorString(bold ? "#00cd00" : "#00ff00", &color);
|
||||
self[NSForegroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:bold ? @"00cd00" : @"00ff00"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 33:
|
||||
content::ParseHexColorString(bold ? "#cdcd00" : "#ffff00", &color);
|
||||
self[NSForegroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:bold ? @"cdcd00" : @"ffff00"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 34:
|
||||
content::ParseHexColorString(bold ? "#0000ee" : "#5c5cff", &color);
|
||||
self[NSForegroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:bold ? @"0000ee" : @"5c5cff"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 35:
|
||||
content::ParseHexColorString(bold ? "#cd00cd" : "#ff00ff", &color);
|
||||
self[NSForegroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:bold ? @"cd00cd" : @"ff00ff"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 36:
|
||||
content::ParseHexColorString(bold ? "#00cdcd" : "#00ffff", &color);
|
||||
self[NSForegroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:bold ? @"00cdcd" : @"00ffff"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 37:
|
||||
content::ParseHexColorString(bold ? "#e5e5e5" : "#ffffff", &color);
|
||||
self[NSForegroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:bold ? @"e5e5e5" : @"ffffff"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
|
||||
case 39:
|
||||
@@ -74,36 +86,44 @@
|
||||
break;
|
||||
|
||||
case 40:
|
||||
content::ParseHexColorString("#7f7f7f", &color);
|
||||
self[NSBackgroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:@"7f7f7f"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 41:
|
||||
content::ParseHexColorString("#cd0000", &color);
|
||||
self[NSBackgroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:@"cd0000"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 42:
|
||||
content::ParseHexColorString("#00cd00", &color);
|
||||
self[NSBackgroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:@"00cd00"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 43:
|
||||
content::ParseHexColorString("#cdcd00", &color);
|
||||
self[NSBackgroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:@"cdcd00"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 44:
|
||||
content::ParseHexColorString("#0000ee", &color);
|
||||
self[NSBackgroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:@"0000ee"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 45:
|
||||
content::ParseHexColorString("cd00cd", &color);
|
||||
self[NSBackgroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:@"cd00cd"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 46:
|
||||
content::ParseHexColorString("#00cdcd", &color);
|
||||
self[NSBackgroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:@"00cdcd"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
case 47:
|
||||
content::ParseHexColorString("#e5e5e5", &color);
|
||||
self[NSBackgroundColorAttributeName] =
|
||||
[NSColor colorWithHexColorString:@"e5e5e5"];
|
||||
skia::SkColorToCalibratedNSColor(color);
|
||||
break;
|
||||
|
||||
case 49:
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include "shell/browser/ui/cocoa/root_view_mac.h"
|
||||
#include "ui/base/cocoa/window_size_constants.h"
|
||||
|
||||
#import <objc/message.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
namespace electron {
|
||||
|
||||
int ScopedDisableResize::disable_resize_ = 0;
|
||||
@@ -19,8 +22,65 @@ int ScopedDisableResize::disable_resize_ = 0;
|
||||
|
||||
@interface NSWindow (PrivateAPI)
|
||||
- (NSImage*)_cornerMask;
|
||||
- (int64_t)_resizeDirectionForMouseLocation:(CGPoint)location;
|
||||
@end
|
||||
|
||||
#if IS_MAS_BUILD()
|
||||
// See components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm
|
||||
@interface NSView (CRFrameViewAdditions)
|
||||
- (void)cr_mouseDownOnFrameView:(NSEvent*)event;
|
||||
@end
|
||||
|
||||
typedef void (*MouseDownImpl)(id, SEL, NSEvent*);
|
||||
|
||||
namespace {
|
||||
MouseDownImpl g_nsthemeframe_mousedown;
|
||||
MouseDownImpl g_nsnextstepframe_mousedown;
|
||||
} // namespace
|
||||
|
||||
// This class is never instantiated, it's just a container for our swizzled
|
||||
// mouseDown method.
|
||||
@interface SwizzledMouseDownHolderClass : NSView
|
||||
@end
|
||||
|
||||
@implementation SwizzledMouseDownHolderClass
|
||||
- (void)swiz_nsthemeframe_mouseDown:(NSEvent*)event {
|
||||
if ([self.window respondsToSelector:@selector(shell)]) {
|
||||
electron::NativeWindowMac* shell =
|
||||
(electron::NativeWindowMac*)[(id)self.window shell];
|
||||
if (shell && !shell->has_frame())
|
||||
[self cr_mouseDownOnFrameView:event];
|
||||
g_nsthemeframe_mousedown(self, @selector(mouseDown:), event);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)swiz_nsnextstepframe_mouseDown:(NSEvent*)event {
|
||||
if ([self.window respondsToSelector:@selector(shell)]) {
|
||||
electron::NativeWindowMac* shell =
|
||||
(electron::NativeWindowMac*)[(id)self.window shell];
|
||||
if (shell && !shell->has_frame()) {
|
||||
[self cr_mouseDownOnFrameView:event];
|
||||
}
|
||||
g_nsnextstepframe_mousedown(self, @selector(mouseDown:), event);
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
namespace {
|
||||
void SwizzleMouseDown(NSView* frame_view,
|
||||
SEL swiz_selector,
|
||||
MouseDownImpl* orig_impl) {
|
||||
Method original_mousedown =
|
||||
class_getInstanceMethod([frame_view class], @selector(mouseDown:));
|
||||
*orig_impl = (MouseDownImpl)method_getImplementation(original_mousedown);
|
||||
Method new_mousedown = class_getInstanceMethod(
|
||||
[SwizzledMouseDownHolderClass class], swiz_selector);
|
||||
method_setImplementation(original_mousedown,
|
||||
method_getImplementation(new_mousedown));
|
||||
}
|
||||
} // namespace
|
||||
#endif // IS_MAS_BUILD
|
||||
|
||||
@implementation ElectronNSWindow
|
||||
|
||||
@synthesize acceptsFirstMouse;
|
||||
@@ -36,6 +96,37 @@ int ScopedDisableResize::disable_resize_ = 0;
|
||||
styleMask:styleMask
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO])) {
|
||||
#if IS_MAS_BUILD()
|
||||
// The first time we create a frameless window, we swizzle the
|
||||
// implementation of -[NSNextStepFrame mouseDown:], replacing it with our
|
||||
// own.
|
||||
// This is only necessary on MAS where we can't directly refer to
|
||||
// NSNextStepFrame or NSThemeFrame, as they are private APIs.
|
||||
// See components/remote_cocoa/app_shim/native_widget_mac_nswindow.mm for
|
||||
// the non-MAS-compatible way of doing this.
|
||||
if (styleMask & NSWindowStyleMaskTitled) {
|
||||
if (!g_nsthemeframe_mousedown) {
|
||||
NSView* theme_frame = [[self contentView] superview];
|
||||
DCHECK(strcmp(class_getName([theme_frame class]), "NSThemeFrame") == 0)
|
||||
<< "Expected NSThemeFrame but was "
|
||||
<< class_getName([theme_frame class]);
|
||||
SwizzleMouseDown(theme_frame, @selector(swiz_nsthemeframe_mouseDown:),
|
||||
&g_nsthemeframe_mousedown);
|
||||
}
|
||||
} else {
|
||||
if (!g_nsnextstepframe_mousedown) {
|
||||
NSView* nextstep_frame = [[self contentView] superview];
|
||||
DCHECK(strcmp(class_getName([nextstep_frame class]),
|
||||
"NSNextStepFrame") == 0)
|
||||
<< "Expected NSNextStepFrame but was "
|
||||
<< class_getName([nextstep_frame class]);
|
||||
SwizzleMouseDown(nextstep_frame,
|
||||
@selector(swiz_nsnextstepframe_mouseDown:),
|
||||
&g_nsnextstepframe_mousedown);
|
||||
}
|
||||
}
|
||||
#endif // IS_MAS_BUILD
|
||||
|
||||
shell_ = shell;
|
||||
}
|
||||
return self;
|
||||
|
||||
@@ -128,16 +128,17 @@ using FullScreenTransitionState =
|
||||
- (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)frameSize {
|
||||
NSSize newSize = frameSize;
|
||||
double aspectRatio = shell_->GetAspectRatio();
|
||||
NSWindow* window = shell_->GetNativeWindow().GetNativeNSWindow();
|
||||
|
||||
if (aspectRatio > 0.0) {
|
||||
gfx::Size windowSize = shell_->GetSize();
|
||||
gfx::Size contentSize = shell_->GetContentSize();
|
||||
gfx::Size extraSize = shell_->GetAspectRatioExtraSize();
|
||||
|
||||
double titleBarHeight = windowSize.height() - contentSize.height();
|
||||
double extraWidthPlusFrame =
|
||||
windowSize.width() - contentSize.width() + extraSize.width();
|
||||
double extraHeightPlusFrame =
|
||||
windowSize.height() - contentSize.height() + extraSize.height();
|
||||
double extraHeightPlusFrame = titleBarHeight + extraSize.height();
|
||||
|
||||
newSize.width =
|
||||
roundf((frameSize.height - extraHeightPlusFrame) * aspectRatio +
|
||||
@@ -145,10 +146,44 @@ using FullScreenTransitionState =
|
||||
newSize.height =
|
||||
roundf((newSize.width - extraWidthPlusFrame) / aspectRatio +
|
||||
extraHeightPlusFrame);
|
||||
|
||||
// Clamp to minimum width/height while ensuring aspect ratio remains.
|
||||
NSSize minSize = [window minSize];
|
||||
NSSize zeroSize =
|
||||
shell_->has_frame() ? NSMakeSize(0, titleBarHeight) : NSZeroSize;
|
||||
if (!NSEqualSizes(minSize, zeroSize)) {
|
||||
double minWidthForAspectRatio =
|
||||
(minSize.height - titleBarHeight) * aspectRatio;
|
||||
bool atMinHeight =
|
||||
minSize.height > zeroSize.height && newSize.height <= minSize.height;
|
||||
newSize.width = atMinHeight ? minWidthForAspectRatio
|
||||
: std::max(newSize.width, minSize.width);
|
||||
|
||||
double minHeightForAspectRatio = minSize.width / aspectRatio;
|
||||
bool atMinWidth =
|
||||
minSize.width > zeroSize.width && newSize.width <= minSize.width;
|
||||
newSize.height = atMinWidth ? minHeightForAspectRatio
|
||||
: std::max(newSize.height, minSize.height);
|
||||
}
|
||||
|
||||
// Clamp to maximum width/height while ensuring aspect ratio remains.
|
||||
NSSize maxSize = [window maxSize];
|
||||
if (!NSEqualSizes(maxSize, NSMakeSize(FLT_MAX, FLT_MAX))) {
|
||||
double maxWidthForAspectRatio = maxSize.height * aspectRatio;
|
||||
bool atMaxHeight =
|
||||
maxSize.height < FLT_MAX && newSize.height >= maxSize.height;
|
||||
newSize.width = atMaxHeight ? maxWidthForAspectRatio
|
||||
: std::min(newSize.width, maxSize.width);
|
||||
|
||||
double maxHeightForAspectRatio = maxSize.width / aspectRatio;
|
||||
bool atMaxWidth =
|
||||
maxSize.width < FLT_MAX && newSize.width >= maxSize.width;
|
||||
newSize.height = atMaxWidth ? maxHeightForAspectRatio
|
||||
: std::min(newSize.height, maxSize.height);
|
||||
}
|
||||
}
|
||||
|
||||
if (!resizingHorizontally_) {
|
||||
NSWindow* window = shell_->GetNativeWindow().GetNativeNSWindow();
|
||||
const auto widthDelta = frameSize.width - [window frame].size.width;
|
||||
const auto heightDelta = frameSize.height - [window frame].size.height;
|
||||
resizingHorizontally_ = std::abs(widthDelta) > std::abs(heightDelta);
|
||||
|
||||
@@ -43,7 +43,7 @@ v8::Local<v8::Promise> NativeImage::CreateThumbnailFromPath(
|
||||
|
||||
if (FAILED(hr)) {
|
||||
promise.RejectWithErrorMessage(
|
||||
"failed to create IShellItem from the given path");
|
||||
"Failed to create IShellItem from the given path");
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -53,21 +53,20 @@ v8::Local<v8::Promise> NativeImage::CreateThumbnailFromPath(
|
||||
IID_PPV_ARGS(&pThumbnailCache));
|
||||
if (FAILED(hr)) {
|
||||
promise.RejectWithErrorMessage(
|
||||
"failed to acquire local thumbnail cache reference");
|
||||
"Failed to acquire local thumbnail cache reference");
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Populate the IShellBitmap
|
||||
Microsoft::WRL::ComPtr<ISharedBitmap> pThumbnail;
|
||||
WTS_CACHEFLAGS flags;
|
||||
WTS_THUMBNAILID thumbId;
|
||||
hr = pThumbnailCache->GetThumbnail(pItem.Get(), size.width(),
|
||||
WTS_FLAGS::WTS_NONE, &pThumbnail, &flags,
|
||||
&thumbId);
|
||||
hr = pThumbnailCache->GetThumbnail(
|
||||
pItem.Get(), size.width(),
|
||||
WTS_FLAGS::WTS_SCALETOREQUESTEDSIZE | WTS_FLAGS::WTS_SCALEUP, &pThumbnail,
|
||||
NULL, NULL);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
promise.RejectWithErrorMessage(
|
||||
"failed to get thumbnail from local thumbnail cache reference");
|
||||
"Failed to get thumbnail from local thumbnail cache reference");
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -75,14 +74,14 @@ v8::Local<v8::Promise> NativeImage::CreateThumbnailFromPath(
|
||||
HBITMAP hBitmap = NULL;
|
||||
hr = pThumbnail->GetSharedBitmap(&hBitmap);
|
||||
if (FAILED(hr)) {
|
||||
promise.RejectWithErrorMessage("failed to extract bitmap from thumbnail");
|
||||
promise.RejectWithErrorMessage("Failed to extract bitmap from thumbnail");
|
||||
return handle;
|
||||
}
|
||||
|
||||
// convert HBITMAP to gfx::Image
|
||||
BITMAP bitmap;
|
||||
if (!GetObject(hBitmap, sizeof(bitmap), &bitmap)) {
|
||||
promise.RejectWithErrorMessage("could not convert HBITMAP to BITMAP");
|
||||
promise.RejectWithErrorMessage("Could not convert HBITMAP to BITMAP");
|
||||
return handle;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ void ElectronRendererClient::DidCreateScriptContext(
|
||||
env->options()->force_context_aware = true;
|
||||
|
||||
// We do not want to crash the renderer process on unhandled rejections.
|
||||
env->options()->unhandled_rejections = "warn";
|
||||
env->options()->unhandled_rejections = "warn-with-error-code";
|
||||
|
||||
environments_.insert(env);
|
||||
|
||||
|
||||
@@ -1859,6 +1859,19 @@ describe('app module', () => {
|
||||
})).to.eventually.be.rejectedWith(/ERR_NAME_NOT_RESOLVED/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('about panel', () => {
|
||||
it('app.setAboutPanelOptions() does not crash', () => {
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: 'electron!!',
|
||||
version: '1.2.3'
|
||||
});
|
||||
});
|
||||
|
||||
it('app.showAboutPanel() does not crash & runs asynchronously', () => {
|
||||
app.showAboutPanel();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('default behavior', () => {
|
||||
|
||||
@@ -345,6 +345,24 @@ describe('BrowserView module', () => {
|
||||
const [code] = await once(rc.process, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
});
|
||||
|
||||
it('emits the destroyed event when webContents.close() is called', async () => {
|
||||
view = new BrowserView();
|
||||
w.setBrowserView(view);
|
||||
await view.webContents.loadFile(path.join(fixtures, 'pages', 'a.html'));
|
||||
|
||||
view.webContents.close();
|
||||
await once(view.webContents, 'destroyed');
|
||||
});
|
||||
|
||||
it('emits the destroyed event when window.close() is called', async () => {
|
||||
view = new BrowserView();
|
||||
w.setBrowserView(view);
|
||||
await view.webContents.loadFile(path.join(fixtures, 'pages', 'a.html'));
|
||||
|
||||
view.webContents.executeJavaScript('window.close()');
|
||||
await once(view.webContents, 'destroyed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('window.open()', () => {
|
||||
|
||||
@@ -5,7 +5,8 @@ import * as fs from 'fs';
|
||||
import * as qs from 'querystring';
|
||||
import * as http from 'http';
|
||||
import * as os from 'os';
|
||||
import { app, BrowserWindow, BrowserView, dialog, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents } from 'electron/main';
|
||||
import { AddressInfo } from 'net';
|
||||
import { app, BrowserWindow, BrowserView, dialog, ipcMain, OnBeforeSendHeadersListenerDetails, protocol, screen, webContents, session, WebContents, WebFrameMain } from 'electron/main';
|
||||
|
||||
import { emittedUntil, emittedNTimes } from './lib/events-helpers';
|
||||
import { ifit, ifdescribe, defer, listen } from './lib/spec-helpers';
|
||||
@@ -553,35 +554,17 @@ describe('BrowserWindow module', () => {
|
||||
});
|
||||
|
||||
it('is triggered when a cross-origin iframe navigates _top', async () => {
|
||||
await w.loadURL(`data:text/html,<iframe src="${url}/navigate-top"></iframe>`);
|
||||
await setTimeout(1000);
|
||||
w.webContents.debugger.attach('1.1');
|
||||
const targets = await w.webContents.debugger.sendCommand('Target.getTargets');
|
||||
const iframeTarget = targets.targetInfos.find((t: any) => t.type === 'iframe');
|
||||
const { sessionId } = await w.webContents.debugger.sendCommand('Target.attachToTarget', {
|
||||
targetId: iframeTarget.targetId,
|
||||
flatten: true
|
||||
w.loadURL(`data:text/html,<iframe src="http://127.0.0.1:${(server.address() as AddressInfo).port}/navigate-top"></iframe>`);
|
||||
await emittedUntil(w.webContents, 'did-frame-finish-load', (e: any, isMainFrame: boolean) => !isMainFrame);
|
||||
let initiator: WebFrameMain | undefined;
|
||||
w.webContents.on('will-navigate', (e) => {
|
||||
initiator = e.initiator;
|
||||
});
|
||||
let willNavigateEmitted = false;
|
||||
w.webContents.on('will-navigate', () => {
|
||||
willNavigateEmitted = true;
|
||||
});
|
||||
await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
|
||||
type: 'mousePressed',
|
||||
x: 10,
|
||||
y: 10,
|
||||
clickCount: 1,
|
||||
button: 'left'
|
||||
}, sessionId);
|
||||
await w.webContents.debugger.sendCommand('Input.dispatchMouseEvent', {
|
||||
type: 'mouseReleased',
|
||||
x: 10,
|
||||
y: 10,
|
||||
clickCount: 1,
|
||||
button: 'left'
|
||||
}, sessionId);
|
||||
const subframe = w.webContents.mainFrame.frames[0];
|
||||
subframe.executeJavaScript('document.getElementsByTagName("a")[0].click()', true);
|
||||
await once(w.webContents, 'did-navigate');
|
||||
expect(willNavigateEmitted).to.be.true();
|
||||
expect(initiator).not.to.be.undefined();
|
||||
expect(initiator).to.equal(subframe);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -447,6 +447,30 @@ describe('nativeImage module', () => {
|
||||
const result = await nativeImage.createThumbnailFromPath(goodPath, goodSize);
|
||||
expect(result.isEmpty()).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns the correct size if larger than the initial image', async () => {
|
||||
// capybara.png is a 128x128 image.
|
||||
const imgPath = path.join(fixturesPath, 'assets', 'capybara.png');
|
||||
const size = { width: 256, height: 256 };
|
||||
const result = await nativeImage.createThumbnailFromPath(imgPath, size);
|
||||
expect(result.getSize()).to.deep.equal(size);
|
||||
});
|
||||
|
||||
it('returns the correct size if is the same as the initial image', async () => {
|
||||
// capybara.png is a 128x128 image.
|
||||
const imgPath = path.join(fixturesPath, 'assets', 'capybara.png');
|
||||
const size = { width: 128, height: 128 };
|
||||
const result = await nativeImage.createThumbnailFromPath(imgPath, size);
|
||||
expect(result.getSize()).to.deep.equal(size);
|
||||
});
|
||||
|
||||
it('returns the correct size if smaller than the initial image', async () => {
|
||||
// capybara.png is a 128x128 image.
|
||||
const imgPath = path.join(fixturesPath, 'assets', 'capybara.png');
|
||||
const maxSize = { width: 64, height: 64 };
|
||||
const result = await nativeImage.createThumbnailFromPath(imgPath, maxSize);
|
||||
expect(result.getSize()).to.deep.equal(maxSize);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addRepresentation()', () => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { expect } from 'chai';
|
||||
import * as dns from 'dns';
|
||||
import { net, session, ClientRequest, BrowserWindow, ClientRequestConstructorOptions } from 'electron/main';
|
||||
import { net, session, ClientRequest, BrowserWindow, ClientRequestConstructorOptions, protocol } from 'electron/main';
|
||||
import * as http from 'http';
|
||||
import * as url from 'url';
|
||||
import * as path from 'path';
|
||||
import { Socket } from 'net';
|
||||
import { defer, listen } from './lib/spec-helpers';
|
||||
import { once } from 'events';
|
||||
@@ -163,9 +164,9 @@ describe('net module', () => {
|
||||
|
||||
it('should post the correct data in a POST request', async () => {
|
||||
const bodyData = 'Hello World!';
|
||||
let postedBodyData: string = '';
|
||||
const serverUrl = await respondOnce.toSingleURL(async (request, response) => {
|
||||
const postedBodyData = await collectStreamBody(request);
|
||||
expect(postedBodyData).to.equal(bodyData);
|
||||
postedBodyData = await collectStreamBody(request);
|
||||
response.end();
|
||||
});
|
||||
const urlRequest = net.request({
|
||||
@@ -175,16 +176,72 @@ describe('net module', () => {
|
||||
urlRequest.write(bodyData);
|
||||
const response = await getResponse(urlRequest);
|
||||
expect(response.statusCode).to.equal(200);
|
||||
expect(postedBodyData).to.equal(bodyData);
|
||||
});
|
||||
|
||||
it('a 307 redirected POST request preserves the body', async () => {
|
||||
const bodyData = 'Hello World!';
|
||||
let postedBodyData: string = '';
|
||||
let methodAfterRedirect: string | undefined;
|
||||
const serverUrl = await respondNTimes.toRoutes({
|
||||
'/redirect': (req, res) => {
|
||||
res.statusCode = 307;
|
||||
res.setHeader('location', serverUrl);
|
||||
return res.end();
|
||||
},
|
||||
'/': async (req, res) => {
|
||||
methodAfterRedirect = req.method;
|
||||
postedBodyData = await collectStreamBody(req);
|
||||
res.end();
|
||||
}
|
||||
}, 2);
|
||||
const urlRequest = net.request({
|
||||
method: 'POST',
|
||||
url: serverUrl + '/redirect'
|
||||
});
|
||||
urlRequest.write(bodyData);
|
||||
const response = await getResponse(urlRequest);
|
||||
expect(response.statusCode).to.equal(200);
|
||||
await collectStreamBody(response);
|
||||
expect(methodAfterRedirect).to.equal('POST');
|
||||
expect(postedBodyData).to.equal(bodyData);
|
||||
});
|
||||
|
||||
it('a 302 redirected POST request DOES NOT preserve the body', async () => {
|
||||
const bodyData = 'Hello World!';
|
||||
let postedBodyData: string = '';
|
||||
let methodAfterRedirect: string | undefined;
|
||||
const serverUrl = await respondNTimes.toRoutes({
|
||||
'/redirect': (req, res) => {
|
||||
res.statusCode = 302;
|
||||
res.setHeader('location', serverUrl);
|
||||
return res.end();
|
||||
},
|
||||
'/': async (req, res) => {
|
||||
methodAfterRedirect = req.method;
|
||||
postedBodyData = await collectStreamBody(req);
|
||||
res.end();
|
||||
}
|
||||
}, 2);
|
||||
const urlRequest = net.request({
|
||||
method: 'POST',
|
||||
url: serverUrl + '/redirect'
|
||||
});
|
||||
urlRequest.write(bodyData);
|
||||
const response = await getResponse(urlRequest);
|
||||
expect(response.statusCode).to.equal(200);
|
||||
await collectStreamBody(response);
|
||||
expect(methodAfterRedirect).to.equal('GET');
|
||||
expect(postedBodyData).to.equal('');
|
||||
});
|
||||
|
||||
it('should support chunked encoding', async () => {
|
||||
let receivedRequest: http.IncomingMessage = null as any;
|
||||
const serverUrl = await respondOnce.toSingleURL((request, response) => {
|
||||
response.statusCode = 200;
|
||||
response.statusMessage = 'OK';
|
||||
response.chunkedEncoding = true;
|
||||
expect(request.method).to.equal('POST');
|
||||
expect(request.headers['transfer-encoding']).to.equal('chunked');
|
||||
expect(request.headers['content-length']).to.equal(undefined);
|
||||
receivedRequest = request;
|
||||
request.on('data', (chunk: Buffer) => {
|
||||
response.write(chunk);
|
||||
});
|
||||
@@ -210,6 +267,9 @@ describe('net module', () => {
|
||||
}
|
||||
|
||||
const response = await getResponse(urlRequest);
|
||||
expect(receivedRequest.method).to.equal('POST');
|
||||
expect(receivedRequest.headers['transfer-encoding']).to.equal('chunked');
|
||||
expect(receivedRequest.headers['content-length']).to.equal(undefined);
|
||||
expect(response.statusCode).to.equal(200);
|
||||
const received = await collectStreamBodyBuffer(response);
|
||||
expect(sent.equals(received)).to.be.true();
|
||||
@@ -1446,6 +1506,9 @@ describe('net module', () => {
|
||||
urlRequest.end();
|
||||
urlRequest.on('redirect', () => { urlRequest.abort(); });
|
||||
urlRequest.on('error', () => {});
|
||||
urlRequest.on('response', () => {
|
||||
expect.fail('Unexpected response');
|
||||
});
|
||||
await once(urlRequest, 'abort');
|
||||
});
|
||||
|
||||
@@ -2078,6 +2141,20 @@ describe('net module', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-http schemes', () => {
|
||||
it('should be rejected by net.request', async () => {
|
||||
expect(() => {
|
||||
net.request('file://bar');
|
||||
}).to.throw('ClientRequest only supports http: and https: protocols');
|
||||
});
|
||||
|
||||
it('should be rejected by net.request when passed in url:', async () => {
|
||||
expect(() => {
|
||||
net.request({ url: 'file://bar' });
|
||||
}).to.throw('ClientRequest only supports http: and https: protocols');
|
||||
});
|
||||
});
|
||||
|
||||
describe('net.fetch', () => {
|
||||
// NB. there exist much more comprehensive tests for fetch() in the form of
|
||||
// the WPT: https://github.com/web-platform-tests/wpt/tree/master/fetch
|
||||
@@ -2167,5 +2244,83 @@ describe('net module', () => {
|
||||
await expect(r.text()).to.be.rejectedWith(/ERR_INCOMPLETE_CHUNKED_ENCODING/);
|
||||
});
|
||||
});
|
||||
|
||||
it('can request file:// URLs', async () => {
|
||||
const resp = await net.fetch(url.pathToFileURL(path.join(__dirname, 'fixtures', 'hello.txt')).toString());
|
||||
expect(resp.ok).to.be.true();
|
||||
// trimRight instead of asserting the whole string to avoid line ending shenanigans on WOA
|
||||
expect((await resp.text()).trimRight()).to.equal('hello world');
|
||||
});
|
||||
|
||||
it('can make requests to custom protocols', async () => {
|
||||
protocol.registerStringProtocol('electron-test', (req, cb) => { cb('hello ' + req.url); });
|
||||
defer(() => {
|
||||
protocol.unregisterProtocol('electron-test');
|
||||
});
|
||||
const body = await net.fetch('electron-test://foo').then(r => r.text());
|
||||
expect(body).to.equal('hello electron-test://foo');
|
||||
});
|
||||
|
||||
it('runs through intercept handlers', async () => {
|
||||
protocol.interceptStringProtocol('http', (req, cb) => { cb('hello ' + req.url); });
|
||||
defer(() => {
|
||||
protocol.uninterceptProtocol('http');
|
||||
});
|
||||
const body = await net.fetch('http://foo').then(r => r.text());
|
||||
expect(body).to.equal('hello http://foo/');
|
||||
});
|
||||
|
||||
it('file: runs through intercept handlers', async () => {
|
||||
protocol.interceptStringProtocol('file', (req, cb) => { cb('hello ' + req.url); });
|
||||
defer(() => {
|
||||
protocol.uninterceptProtocol('file');
|
||||
});
|
||||
const body = await net.fetch('file://foo').then(r => r.text());
|
||||
expect(body).to.equal('hello file://foo/');
|
||||
});
|
||||
|
||||
it('can be redirected', async () => {
|
||||
protocol.interceptStringProtocol('file', (req, cb) => { cb({ statusCode: 302, headers: { location: 'electron-test://bar' } }); });
|
||||
defer(() => {
|
||||
protocol.uninterceptProtocol('file');
|
||||
});
|
||||
protocol.registerStringProtocol('electron-test', (req, cb) => { cb('hello ' + req.url); });
|
||||
defer(() => {
|
||||
protocol.unregisterProtocol('electron-test');
|
||||
});
|
||||
const body = await net.fetch('file://foo').then(r => r.text());
|
||||
expect(body).to.equal('hello electron-test://bar');
|
||||
});
|
||||
|
||||
it('should not follow redirect when redirect: error', async () => {
|
||||
protocol.registerStringProtocol('electron-test', (req, cb) => {
|
||||
if (/redirect/.test(req.url)) return cb({ statusCode: 302, headers: { location: 'electron-test://bar' } });
|
||||
cb('hello ' + req.url);
|
||||
});
|
||||
defer(() => {
|
||||
protocol.unregisterProtocol('electron-test');
|
||||
});
|
||||
await expect(net.fetch('electron-test://redirect', { redirect: 'error' })).to.eventually.be.rejectedWith('Attempted to redirect, but redirect policy was \'error\'');
|
||||
});
|
||||
|
||||
it('a 307 redirected POST request preserves the body', async () => {
|
||||
const bodyData = 'Hello World!';
|
||||
let postedBodyData: any;
|
||||
protocol.registerStringProtocol('electron-test', async (req, cb) => {
|
||||
if (/redirect/.test(req.url)) return cb({ statusCode: 307, headers: { location: 'electron-test://bar' } });
|
||||
postedBodyData = req.uploadData![0].bytes.toString();
|
||||
cb('hello ' + req.url);
|
||||
});
|
||||
defer(() => {
|
||||
protocol.unregisterProtocol('electron-test');
|
||||
});
|
||||
const response = await net.fetch('electron-test://redirect', {
|
||||
method: 'POST',
|
||||
body: bodyData
|
||||
});
|
||||
expect(response.status).to.equal(200);
|
||||
await response.text();
|
||||
expect(postedBodyData).to.equal(bodyData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -565,11 +565,11 @@ describe('webContents module', () => {
|
||||
oscillator.connect(context.destination)
|
||||
oscillator.start()
|
||||
`);
|
||||
let p = once(w.webContents, '-audio-state-changed');
|
||||
let p = once(w.webContents, 'audio-state-changed');
|
||||
w.webContents.executeJavaScript('context.resume()');
|
||||
await p;
|
||||
expect(w.webContents.isCurrentlyAudible()).to.be.true();
|
||||
p = once(w.webContents, '-audio-state-changed');
|
||||
p = once(w.webContents, 'audio-state-changed');
|
||||
w.webContents.executeJavaScript('oscillator.stop()');
|
||||
await p;
|
||||
expect(w.webContents.isCurrentlyAudible()).to.be.false();
|
||||
@@ -1803,8 +1803,24 @@ describe('webContents module', () => {
|
||||
|
||||
await w.loadURL('about:blank');
|
||||
|
||||
const promise = w.webContents.takeHeapSnapshot('');
|
||||
return expect(promise).to.be.eventually.rejectedWith(Error, 'takeHeapSnapshot failed');
|
||||
const badPath = path.join('i', 'am', 'a', 'super', 'bad', 'path');
|
||||
const promise = w.webContents.takeHeapSnapshot(badPath);
|
||||
return expect(promise).to.be.eventually.rejectedWith(Error, `Failed to take heap snapshot with invalid file path ${badPath}`);
|
||||
});
|
||||
|
||||
it('fails with invalid render process', async () => {
|
||||
const w = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
sandbox: true
|
||||
}
|
||||
});
|
||||
|
||||
const filePath = path.join(app.getPath('temp'), 'test.heapsnapshot');
|
||||
|
||||
w.webContents.destroy();
|
||||
const promise = w.webContents.takeHeapSnapshot(filePath);
|
||||
return expect(promise).to.be.eventually.rejectedWith(Error, 'Failed to take heap snapshot with nonexistent render frame');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -63,25 +63,36 @@ describe('webRequest module', () => {
|
||||
ses.webRequest.onBeforeRequest(null);
|
||||
});
|
||||
|
||||
const cancel = (details: Electron.OnBeforeRequestListenerDetails, callback: (response: Electron.CallbackResponse) => void) => {
|
||||
callback({ cancel: true });
|
||||
};
|
||||
|
||||
it('can cancel the request', async () => {
|
||||
ses.webRequest.onBeforeRequest((details, callback) => {
|
||||
callback({
|
||||
cancel: true
|
||||
});
|
||||
});
|
||||
ses.webRequest.onBeforeRequest(cancel);
|
||||
await expect(ajax(defaultURL)).to.eventually.be.rejected();
|
||||
});
|
||||
|
||||
it('can filter URLs', async () => {
|
||||
const filter = { urls: [defaultURL + 'filter/*'] };
|
||||
ses.webRequest.onBeforeRequest(filter, (details, callback) => {
|
||||
callback({ cancel: true });
|
||||
});
|
||||
ses.webRequest.onBeforeRequest(filter, cancel);
|
||||
const { data } = await ajax(`${defaultURL}nofilter/test`);
|
||||
expect(data).to.equal('/nofilter/test');
|
||||
await expect(ajax(`${defaultURL}filter/test`)).to.eventually.be.rejected();
|
||||
});
|
||||
|
||||
it('can filter URLs and types', async () => {
|
||||
const filter1: Electron.WebRequestFilter = { urls: [defaultURL + 'filter/*'], types: ['xhr'] };
|
||||
ses.webRequest.onBeforeRequest(filter1, cancel);
|
||||
const { data } = await ajax(`${defaultURL}nofilter/test`);
|
||||
expect(data).to.equal('/nofilter/test');
|
||||
await expect(ajax(`${defaultURL}filter/test`)).to.eventually.be.rejected();
|
||||
|
||||
const filter2: Electron.WebRequestFilter = { urls: [defaultURL + 'filter/*'], types: ['stylesheet'] };
|
||||
ses.webRequest.onBeforeRequest(filter2, cancel);
|
||||
expect((await ajax(`${defaultURL}nofilter/test`)).data).to.equal('/nofilter/test');
|
||||
expect((await ajax(`${defaultURL}filter/test`)).data).to.equal('/filter/test');
|
||||
});
|
||||
|
||||
it('receives details object', async () => {
|
||||
ses.webRequest.onBeforeRequest((details, callback) => {
|
||||
expect(details.id).to.be.a('number');
|
||||
|
||||
3
spec/disabled-tests.json
Normal file
3
spec/disabled-tests.json
Normal file
@@ -0,0 +1,3 @@
|
||||
[
|
||||
"// NOTE: this file is used to disable tests in our test suite by their full title."
|
||||
]
|
||||
13
spec/fixtures/api/unhandled-rejection-handled.js
vendored
Normal file
13
spec/fixtures/api/unhandled-rejection-handled.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
const { app } = require('electron');
|
||||
|
||||
const handleUnhandledRejection = (reason) => {
|
||||
console.error(`Unhandled Rejection: ${reason.stack}`);
|
||||
app.quit();
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
process.on('unhandledRejection', handleUnhandledRejection);
|
||||
throw new Error('oops');
|
||||
};
|
||||
|
||||
main();
|
||||
5
spec/fixtures/api/unhandled-rejection.js
vendored
Normal file
5
spec/fixtures/api/unhandled-rejection.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
const main = async () => {
|
||||
throw new Error('oops');
|
||||
};
|
||||
|
||||
main();
|
||||
BIN
spec/fixtures/assets/capybara.png
vendored
Normal file
BIN
spec/fixtures/assets/capybara.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
1
spec/fixtures/hello.txt
vendored
Normal file
1
spec/fixtures/hello.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
hello world
|
||||
@@ -1,3 +1,4 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const v8 = require('v8');
|
||||
|
||||
@@ -65,6 +66,18 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
const mocha = new Mocha(mochaOptions);
|
||||
|
||||
// Add a root hook on mocha to skip any tests that are disabled
|
||||
const disabledTests = new Set(
|
||||
JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, 'disabled-tests.json'), 'utf8')
|
||||
)
|
||||
);
|
||||
mocha.suite.beforeEach(function () {
|
||||
if (disabledTests.has(this.currentTest?.fullTitle())) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
// The cleanup method is registered this way rather than through an
|
||||
// `afterEach` at the top level so that it can run before other `afterEach`
|
||||
// methods.
|
||||
|
||||
@@ -226,6 +226,42 @@ describe('node feature', () => {
|
||||
}));
|
||||
expect(result).to.equal('hello');
|
||||
});
|
||||
|
||||
it('does not log the warning more than once when the rejection is unhandled', async () => {
|
||||
const appPath = path.join(mainFixturesPath, 'api', 'unhandled-rejection.js');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
|
||||
let output = '';
|
||||
const out = (data: string) => {
|
||||
output += data;
|
||||
if (/UnhandledPromiseRejectionWarning/.test(data)) {
|
||||
appProcess.kill();
|
||||
}
|
||||
};
|
||||
appProcess.stdout!.on('data', out);
|
||||
appProcess.stderr!.on('data', out);
|
||||
|
||||
await once(appProcess, 'exit');
|
||||
expect(/UnhandledPromiseRejectionWarning/.test(output)).to.equal(true);
|
||||
const matches = output.match(/Error: oops/gm);
|
||||
expect(matches).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('does not log the warning more than once when the rejection is handled', async () => {
|
||||
const appPath = path.join(mainFixturesPath, 'api', 'unhandled-rejection-handled.js');
|
||||
const appProcess = childProcess.spawn(process.execPath, [appPath]);
|
||||
|
||||
let output = '';
|
||||
const out = (data: string) => { output += data; };
|
||||
appProcess.stdout!.on('data', out);
|
||||
appProcess.stderr!.on('data', out);
|
||||
|
||||
const [code] = await once(appProcess, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
expect(/UnhandledPromiseRejectionWarning/.test(output)).to.equal(false);
|
||||
const matches = output.match(/Error: oops/gm);
|
||||
expect(matches).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user