feat: enable nativeWindowOpen by default (#28552)

* feat: enable nativeWindowOpen by default

* set nativeWindowOpen: false on spec/ main window

* update snapshots

* fix tests

* fix test

* fix webview test missing allowpopups

* fix other test

* update default
This commit is contained in:
Jeremy Rose 2021-04-13 12:36:38 -07:00 committed by GitHub
parent dba4df9326
commit f8bdef5349
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 90 additions and 95 deletions

View file

@ -349,9 +349,8 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
context in the dev tools by selecting the 'Electron Isolated Context' context in the dev tools by selecting the 'Electron Isolated Context'
entry in the combo box at the top of the Console tab. entry in the combo box at the top of the Console tab.
* `nativeWindowOpen` Boolean (optional) - Whether to use native * `nativeWindowOpen` Boolean (optional) - Whether to use native
`window.open()`. Defaults to `false`. Child windows will always have node `window.open()`. Defaults to `true`. Child windows will always have node
integration disabled unless `nodeIntegrationInSubFrames` is true. **Note:** This option is currently integration disabled unless `nodeIntegrationInSubFrames` is true.
experimental.
* `webviewTag` Boolean (optional) - Whether to enable the [`<webview>` tag](webview-tag.md). * `webviewTag` Boolean (optional) - Whether to enable the [`<webview>` tag](webview-tag.md).
Defaults to `false`. **Note:** The Defaults to `false`. **Note:** The
`preload` script configured for the `<webview>` will have node integration `preload` script configured for the `<webview>` will have node integration

View file

@ -6,16 +6,15 @@ untrusted content within a renderer. Windows can be created from the renderer in
* clicking on links or submitting forms adorned with `target=_blank` * clicking on links or submitting forms adorned with `target=_blank`
* JavaScript calling `window.open()` * JavaScript calling `window.open()`
In non-sandboxed renderers, or when `nativeWindowOpen` is false (the default), this results in the creation of a For same-origin content, the new window is created within the same process,
[`BrowserWindowProxy`](browser-window-proxy.md), a light wrapper around enabling the parent to access the child window directly. This can be very
`BrowserWindow`. useful for app sub-windows that act as preference panels, or similar, as the
parent can render to the sub-window directly, as if it were a `div` in the
parent. This is the same behavior as in the browser.
However, when the `sandbox` (or directly, `nativeWindowOpen`) option is set, a When `nativeWindowOpen` is set to false, `window.open` instead results in the
`Window` instance is created, as you'd expect in the browser. For same-origin creation of a [`BrowserWindowProxy`](browser-window-proxy.md), a light wrapper
content, the new window is created within the same process, enabling the parent around `BrowserWindow`.
to access the child window directly. This can be very useful for app sub-windows that act
as preference panels, or similar, as the parent can render to the sub-window
directly, as if it were a `div` in the parent.
Electron pairs this native Chrome `Window` with a BrowserWindow under the hood. Electron pairs this native Chrome `Window` with a BrowserWindow under the hood.
You can take advantage of all the customization available when creating a You can take advantage of all the customization available when creating a
@ -69,49 +68,18 @@ window.open('https://github.com', '_blank', 'top=500,left=200,frame=false,nodeIn
To customize or cancel the creation of the window, you can optionally set an To customize or cancel the creation of the window, you can optionally set an
override handler with `webContents.setWindowOpenHandler()` from the main override handler with `webContents.setWindowOpenHandler()` from the main
process. Returning `false` cancels the window, while returning an object sets process. Returning `{ action: 'deny' }` cancels the window. Returning `{
the `BrowserWindowConstructorOptions` used when creating the window. Note that action: 'allow', overrideBrowserWindowOptions: { ... } }` will allow opening
this is more powerful than passing options through the feature string, as the the window and setting the `BrowserWindowConstructorOptions` to be used when
renderer has more limited privileges in deciding security preferences than the creating the window. Note that this is more powerful than passing options
main process. through the feature string, as the renderer has more limited privileges in
deciding security preferences than the main process.
### `BrowserWindowProxy` example
```javascript
// main.js
const mainWindow = new BrowserWindow()
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https://github.com/')) {
return { action: 'allow' }
}
return { action: 'deny' }
})
mainWindow.webContents.on('did-create-window', (childWindow) => {
// For example...
childWindow.webContents.on('will-navigate', (e) => {
e.preventDefault()
})
})
```
```javascript
// renderer.js
const windowProxy = window.open('https://github.com/', null, 'minimizable=false')
windowProxy.postMessage('hi', '*')
```
### Native `Window` example ### Native `Window` example
```javascript ```javascript
// main.js // main.js
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow()
webPreferences: {
nativeWindowOpen: true
}
})
// In this example, only windows with the `about:blank` url will be created. // In this example, only windows with the `about:blank` url will be created.
// All other urls will be blocked. // All other urls will be blocked.
@ -135,3 +103,33 @@ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
const childWindow = window.open('', 'modal') const childWindow = window.open('', 'modal')
childWindow.document.write('<h1>Hello</h1>') childWindow.document.write('<h1>Hello</h1>')
``` ```
### `BrowserWindowProxy` example
```javascript
// main.js
const mainWindow = new BrowserWindow({
webPreferences: { nativeWindowOpen: false }
})
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https://github.com/')) {
return { action: 'allow' }
}
return { action: 'deny' }
})
mainWindow.webContents.on('did-create-window', (childWindow) => {
// For example...
childWindow.webContents.on('will-navigate', (e) => {
e.preventDefault()
})
})
```
```javascript
// renderer.js
const windowProxy = window.open('https://github.com/', null, 'minimizable=false')
windowProxy.postMessage('hi', '*')
```

View file

@ -28,6 +28,16 @@ ensure your code works with this property enabled. It has been enabled by defau
You will be affected by this change if you use either `webFrame.executeJavaScript` or `webFrame.executeJavaScriptInIsolatedWorld`. You will need to ensure that values returned by either of those methods are supported by the [Context Bridge API](api/context-bridge.md#parameter--error--return-type-support) as these methods use the same value passing semantics. You will be affected by this change if you use either `webFrame.executeJavaScript` or `webFrame.executeJavaScriptInIsolatedWorld`. You will need to ensure that values returned by either of those methods are supported by the [Context Bridge API](api/context-bridge.md#parameter--error--return-type-support) as these methods use the same value passing semantics.
### Default Changed: `nativeWindowOpen` defaults to `true`
Prior to Electron 14, `window.open` was by default shimmed to use
`BrowserWindowProxy`. This meant that `window.open('about:blank')` did not work
to open synchronously scriptable child windows, among other incompatibilities.
`nativeWindowOpen` is no longer experimental, and is now the default.
See the documentation for [window.open in Electron](api/window-open.md)
for more details.
## Planned Breaking API Changes (13.0) ## Planned Breaking API Changes (13.0)
### API Changed: `session.setPermissionCheckHandler(handler)` ### API Changed: `session.setPermissionCheckHandler(handler)`

View file

@ -141,7 +141,7 @@ WebContentsPreferences::WebContentsPreferences(
SetDefaultBoolIfUndefined(options::kDisableHtmlFullscreenWindowResize, false); SetDefaultBoolIfUndefined(options::kDisableHtmlFullscreenWindowResize, false);
SetDefaultBoolIfUndefined(options::kWebviewTag, false); SetDefaultBoolIfUndefined(options::kWebviewTag, false);
SetDefaultBoolIfUndefined(options::kSandbox, false); SetDefaultBoolIfUndefined(options::kSandbox, false);
SetDefaultBoolIfUndefined(options::kNativeWindowOpen, false); SetDefaultBoolIfUndefined(options::kNativeWindowOpen, true);
SetDefaultBoolIfUndefined(options::kContextIsolation, true); SetDefaultBoolIfUndefined(options::kContextIsolation, true);
SetDefaultBoolIfUndefined(options::kJavaScript, true); SetDefaultBoolIfUndefined(options::kJavaScript, true);
SetDefaultBoolIfUndefined(options::kImages, true); SetDefaultBoolIfUndefined(options::kImages, true);
@ -474,7 +474,7 @@ void WebContentsPreferences::OverrideWebkitPrefs(
GetPreloadPath(&prefs->preload); GetPreloadPath(&prefs->preload);
// Check if nativeWindowOpen is enabled. // Check if nativeWindowOpen is enabled.
prefs->native_window_open = IsEnabled(options::kNativeWindowOpen); prefs->native_window_open = IsEnabled(options::kNativeWindowOpen, true);
// Check if we have node integration specified. // Check if we have node integration specified.
prefs->node_integration = IsEnabled(options::kNodeIntegration); prefs->node_integration = IsEnabled(options::kNodeIntegration);

View file

@ -3075,19 +3075,7 @@ describe('BrowserWindow module', () => {
expect(url).to.equal('http://example.com/test'); expect(url).to.equal('http://example.com/test');
expect(frameName).to.equal(''); expect(frameName).to.equal('');
expect(disposition).to.equal('foreground-tab'); expect(disposition).to.equal('foreground-tab');
expect(options).to.deep.equal({ expect(options).to.be.an('object').not.null();
show: true,
width: 800,
height: 600,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
webviewTag: false,
nodeIntegrationInSubFrames: false,
openerId: options.webPreferences!.openerId
},
webContents: undefined
});
expect(referrer.policy).to.equal('strict-origin-when-cross-origin'); expect(referrer.policy).to.equal('strict-origin-when-cross-origin');
expect(referrer.url).to.equal(''); expect(referrer.url).to.equal('');
expect(additionalFeatures).to.deep.equal([]); expect(additionalFeatures).to.deep.equal([]);

View file

@ -744,7 +744,9 @@ describe('chromium features', () => {
} }
it('disables node integration when it is disabled on the parent window for chrome devtools URLs', async () => { it('disables node integration when it is disabled on the parent window for chrome devtools URLs', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); // NB. webSecurity is disabled because native window.open() is not
// allowed to load devtools:// URLs.
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webSecurity: false } });
w.loadURL('about:blank'); w.loadURL('about:blank');
w.webContents.executeJavaScript(` w.webContents.executeJavaScript(`
{ b = window.open('devtools://devtools/bundled/inspector.html', '', 'nodeIntegration=no,show=no'); null } { b = window.open('devtools://devtools/bundled/inspector.html', '', 'nodeIntegration=no,show=no'); null }
@ -755,8 +757,8 @@ describe('chromium features', () => {
}); });
it('disables JavaScript when it is disabled on the parent window', async () => { it('disables JavaScript when it is disabled on the parent window', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true } }); const w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true } });
w.webContents.loadURL('about:blank'); w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
const windowUrl = require('url').format({ const windowUrl = require('url').format({
pathname: `${fixturesPath}/pages/window-no-javascript.html`, pathname: `${fixturesPath}/pages/window-no-javascript.html`,
protocol: 'file', protocol: 'file',
@ -783,7 +785,7 @@ describe('chromium features', () => {
targetURL = `file://${fixturesPath}/pages/base-page.html`; targetURL = `file://${fixturesPath}/pages/base-page.html`;
} }
const w = new BrowserWindow({ show: false }); const w = new BrowserWindow({ show: false });
w.loadURL('about:blank'); w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
w.webContents.executeJavaScript(`{ b = window.open(${JSON.stringify(targetURL)}); null }`); w.webContents.executeJavaScript(`{ b = window.open(${JSON.stringify(targetURL)}); null }`);
const [, window] = await emittedOnce(app, 'browser-window-created'); const [, window] = await emittedOnce(app, 'browser-window-created');
await emittedOnce(window.webContents, 'did-finish-load'); await emittedOnce(window.webContents, 'did-finish-load');
@ -792,7 +794,7 @@ describe('chromium features', () => {
it('defines a window.location setter', async () => { it('defines a window.location setter', async () => {
const w = new BrowserWindow({ show: false }); const w = new BrowserWindow({ show: false });
w.loadURL('about:blank'); w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
w.webContents.executeJavaScript('{ b = window.open("about:blank"); null }'); w.webContents.executeJavaScript('{ b = window.open("about:blank"); null }');
const [, { webContents }] = await emittedOnce(app, 'browser-window-created'); const [, { webContents }] = await emittedOnce(app, 'browser-window-created');
await emittedOnce(webContents, 'did-finish-load'); await emittedOnce(webContents, 'did-finish-load');
@ -803,7 +805,7 @@ describe('chromium features', () => {
it('defines a window.location.href setter', async () => { it('defines a window.location.href setter', async () => {
const w = new BrowserWindow({ show: false }); const w = new BrowserWindow({ show: false });
w.loadURL('about:blank'); w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));
w.webContents.executeJavaScript('{ b = window.open("about:blank"); null }'); w.webContents.executeJavaScript('{ b = window.open("about:blank"); null }');
const [, { webContents }] = await emittedOnce(app, 'browser-window-created'); const [, { webContents }] = await emittedOnce(app, 'browser-window-created');
await emittedOnce(webContents, 'did-finish-load'); await emittedOnce(webContents, 'did-finish-load');
@ -1035,7 +1037,7 @@ describe('chromium features', () => {
// We are testing whether context (3) can access context (2) under various conditions. // We are testing whether context (3) can access context (2) under various conditions.
// This is context (1), the base window for the test. // This is context (1), the base window for the test.
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false } }); const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, webviewTag: true, contextIsolation: false, nativeWindowOpen: false } });
await w.loadURL('about:blank'); await w.loadURL('about:blank');
const parentCode = `new Promise((resolve) => { const parentCode = `new Promise((resolve) => {

View file

@ -2,9 +2,7 @@
[ [
"top=5,left=10,resizable=no", "top=5,left=10,resizable=no",
{ {
"sender": "[WebContents]", "sender": "[WebContents]"
"frameId": 1,
"processId": "placeholder-process-id"
}, },
"about:blank", "about:blank",
"frame-name", "frame-name",
@ -20,10 +18,11 @@
"y": 5, "y": 5,
"webPreferences": { "webPreferences": {
"contextIsolation": true, "contextIsolation": true,
"nativeWindowOpen": true,
"nodeIntegration": false, "nodeIntegration": false,
"webviewTag": false, "webviewTag": false,
"nodeIntegrationInSubFrames": false, "nodeIntegrationInSubFrames": false,
"openerId": "placeholder-opener-id" "openerId": null
}, },
"webContents": "[WebContents]" "webContents": "[WebContents]"
}, },
@ -37,9 +36,7 @@
[ [
"zoomFactor=2,resizable=0,x=0,y=10", "zoomFactor=2,resizable=0,x=0,y=10",
{ {
"sender": "[WebContents]", "sender": "[WebContents]"
"frameId": 1,
"processId": "placeholder-process-id"
}, },
"about:blank", "about:blank",
"frame-name", "frame-name",
@ -54,10 +51,11 @@
"webPreferences": { "webPreferences": {
"zoomFactor": "2", "zoomFactor": "2",
"contextIsolation": true, "contextIsolation": true,
"nativeWindowOpen": true,
"nodeIntegration": false, "nodeIntegration": false,
"webviewTag": false, "webviewTag": false,
"nodeIntegrationInSubFrames": false, "nodeIntegrationInSubFrames": false,
"openerId": "placeholder-opener-id" "openerId": null
}, },
"webContents": "[WebContents]" "webContents": "[WebContents]"
}, },
@ -71,9 +69,7 @@
[ [
"backgroundColor=gray,webPreferences=0,x=100,y=100", "backgroundColor=gray,webPreferences=0,x=100,y=100",
{ {
"sender": "[WebContents]", "sender": "[WebContents]"
"frameId": 1,
"processId": "placeholder-process-id"
}, },
"about:blank", "about:blank",
"frame-name", "frame-name",
@ -85,10 +81,11 @@
"backgroundColor": "gray", "backgroundColor": "gray",
"webPreferences": { "webPreferences": {
"contextIsolation": true, "contextIsolation": true,
"nativeWindowOpen": true,
"nodeIntegration": false, "nodeIntegration": false,
"webviewTag": false, "webviewTag": false,
"nodeIntegrationInSubFrames": false, "nodeIntegrationInSubFrames": false,
"openerId": "placeholder-opener-id", "openerId": null,
"backgroundColor": "gray" "backgroundColor": "gray"
}, },
"x": 100, "x": 100,
@ -105,9 +102,7 @@
[ [
"x=50,y=20,title=sup", "x=50,y=20,title=sup",
{ {
"sender": "[WebContents]", "sender": "[WebContents]"
"frameId": 1,
"processId": "placeholder-process-id"
}, },
"about:blank", "about:blank",
"frame-name", "frame-name",
@ -121,10 +116,11 @@
"title": "sup", "title": "sup",
"webPreferences": { "webPreferences": {
"contextIsolation": true, "contextIsolation": true,
"nativeWindowOpen": true,
"nodeIntegration": false, "nodeIntegration": false,
"webviewTag": false, "webviewTag": false,
"nodeIntegrationInSubFrames": false, "nodeIntegrationInSubFrames": false,
"openerId": "placeholder-opener-id" "openerId": null
}, },
"webContents": "[WebContents]" "webContents": "[WebContents]"
}, },
@ -138,9 +134,7 @@
[ [
"show=false,top=1,left=1", "show=false,top=1,left=1",
{ {
"sender": "[WebContents]", "sender": "[WebContents]"
"frameId": 1,
"processId": "placeholder-process-id"
}, },
"about:blank", "about:blank",
"frame-name", "frame-name",
@ -155,10 +149,11 @@
"y": 1, "y": 1,
"webPreferences": { "webPreferences": {
"contextIsolation": true, "contextIsolation": true,
"nativeWindowOpen": true,
"nodeIntegration": false, "nodeIntegration": false,
"webviewTag": false, "webviewTag": false,
"nodeIntegrationInSubFrames": false, "nodeIntegrationInSubFrames": false,
"openerId": "placeholder-opener-id" "openerId": null
}, },
"webContents": "[WebContents]" "webContents": "[WebContents]"
}, },

View file

@ -109,7 +109,8 @@ app.whenReady().then(async function () {
backgroundThrottling: false, backgroundThrottling: false,
nodeIntegration: true, nodeIntegration: true,
webviewTag: true, webviewTag: true,
contextIsolation: false contextIsolation: false,
nativeWindowOpen: false
} }
}); });
window.loadFile('static/index.html', { window.loadFile('static/index.html', {

View file

@ -501,7 +501,8 @@ describe('<webview> tag', function () {
describe('new-window event', () => { describe('new-window event', () => {
it('emits when window.open is called', async () => { it('emits when window.open is called', async () => {
loadWebView(webview, { loadWebView(webview, {
src: `file://${fixtures}/pages/window-open.html` src: `file://${fixtures}/pages/window-open.html`,
allowpopups: true
}); });
const { url, frameName } = await waitForEvent(webview, 'new-window'); const { url, frameName } = await waitForEvent(webview, 'new-window');
@ -511,7 +512,8 @@ describe('<webview> tag', function () {
it('emits when link with target is called', async () => { it('emits when link with target is called', async () => {
loadWebView(webview, { loadWebView(webview, {
src: `file://${fixtures}/pages/target-name.html` src: `file://${fixtures}/pages/target-name.html`,
allowpopups: true
}); });
const { url, frameName } = await waitForEvent(webview, 'new-window'); const { url, frameName } = await waitForEvent(webview, 'new-window');