Skip to content

Commit

Permalink
feat: enhance native window.open to match the custom implementation's…
Browse files Browse the repository at this point in the history
… behavior (electron#19703)

Co-authored-by: Andy Locascio <[email protected]>
  • Loading branch information
brenca and loc authored Mar 26, 2020
1 parent b1f4ac0 commit 74372d6
Show file tree
Hide file tree
Showing 20 changed files with 350 additions and 143 deletions.
23 changes: 23 additions & 0 deletions docs/api/structures/post-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# PostBody Object

* `data` Array<[PostData](./post-data.md)> - The post data to be sent to the
new window.
* `contentType` String - The `content-type` header used for the data. One of
`application/x-www-form-urlencoded` or `multipart/form-data`. Corresponds to
the `enctype` attribute of the submitted HTML form.
* `boundary` String (optional) - The boundary used to separate multiple parts of
the message. Only valid when `contentType` is `multipart/form-data`.

Note that keys starting with `--` are not currently supported. For example, this will errantly submit as `multipart/form-data` when `nativeWindowOpen` is set to `false` in webPreferences:

```html
<form
target="_blank"
method="POST"
enctype="application/x-www-form-urlencoded"
action="https://postman-echo.com/post"
>
<input type="text" name="--theKey">
<input type="submit">
</form>
```
21 changes: 21 additions & 0 deletions docs/api/structures/post-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# PostData Object

* `type` String - One of the following:
* `rawData` - The data is available as a `Buffer`, in the `rawData` field.
* `file` - The object represents a file. The `filePath`, `offset`, `length`
and `modificationTime` fields will be used to describe the file.
* `blob` - The object represents a `Blob`. The `blobUUID` field will be used
to describe the `Blob`.
* `bytes` String (optional) - The raw bytes of the post data in a `Buffer`.
Required for the `rawData` type.
* `filePath` String (optional) - The path of the file being uploaded. Required
for the `file` type.
* `blobUUID` String (optional) - The `UUID` of the `Blob` being uploaded.
Required for the `blob` type.
* `offset` Integer (optional) - The offset from the beginning of the file being
uploaded, in bytes. Only valid for `file` types.
* `length` Integer (optional) - The length of the file being uploaded, in bytes.
If set to `-1`, the whole file will be uploaded. Only valid for `file` types.
* `modificationTime` Double (optional) - The modification time of the file
represented by a double, which is the number of seconds since the `UNIX Epoch`
(Jan 1, 1970). Only valid for `file` types.
17 changes: 15 additions & 2 deletions docs/api/web-contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ Returns:
* `referrer` [Referrer](structures/referrer.md) - The referrer that will be
passed to the new window. May or may not result in the `Referer` header being
sent, depending on the referrer policy.
* `postBody` [PostBody](structures/post-body.md) (optional) - The post data that
will be sent to the new window, along with the appropriate headers that will
be set. If no post data is to be sent, the value will be `null`. Only defined
when the window is being created by a form that set `target=_blank`.

Emitted when the page requests to open a new window for a `url`. It could be
requested by `window.open` or an external link like `<a target='_blank'>`.
Expand All @@ -162,15 +166,24 @@ new [`BrowserWindow`](browser-window.md). If you call `event.preventDefault()` a
instance, failing to do so may result in unexpected behavior. For example:

```javascript
myBrowserWindow.webContents.on('new-window', (event, url, frameName, disposition, options) => {
myBrowserWindow.webContents.on('new-window', (event, url, frameName, disposition, options, additionalFeatures, referrer, postBody) => {
event.preventDefault()
const win = new BrowserWindow({
webContents: options.webContents, // use existing webContents if provided
show: false
})
win.once('ready-to-show', () => win.show())
if (!options.webContents) {
win.loadURL(url) // existing webContents will be navigated automatically
const loadOptions = {
httpReferrer: referrer
}
if (postBody != null) {
const { data, contentType, boundary } = postBody
loadOptions.postData = postBody.data
loadOptions.extraHeaders = `content-type: ${contentType}; boundary=${boundary}`
}

win.loadURL(url, loadOptions) // existing webContents will be navigated automatically
}
event.newGuest = win
})
Expand Down
2 changes: 2 additions & 0 deletions filenames.auto.gni
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ auto_filenames = {
"docs/api/structures/mouse-wheel-input-event.md",
"docs/api/structures/notification-action.md",
"docs/api/structures/point.md",
"docs/api/structures/post-body.md",
"docs/api/structures/post-data.md",
"docs/api/structures/printer-info.md",
"docs/api/structures/process-memory-info.md",
"docs/api/structures/process-metric.md",
Expand Down
1 change: 0 additions & 1 deletion lib/browser/api/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ class SlurpStream extends Writable {
this._data = Buffer.concat([this._data, chunk]);
callback();
}

data () { return this._data; }
}

Expand Down
35 changes: 19 additions & 16 deletions lib/browser/api/web-contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { internalWindowOpen } = require('@electron/internal/browser/guest-window-
const NavigationController = require('@electron/internal/browser/navigation-controller');
const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal');
const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils');
const { convertFeaturesString } = require('@electron/internal/common/parse-features-string');
const { MessagePortMain } = require('@electron/internal/browser/message-port-main');

// session is not used here, the purpose is to make sure session is initalized
Expand Down Expand Up @@ -506,40 +507,42 @@ WebContents.prototype._init = function () {
this.reload();
});

// Handle window.open for BrowserWindow and BrowserView.
if (['browserView', 'window'].includes(this.getType())) {
if (this.getType() !== 'remote') {
// Make new windows requested by links behave like "window.open".
this.on('-new-window', (event, url, frameName, disposition,
additionalFeatures, postData,
referrer) => {
const options = {
rawFeatures, referrer, postData) => {
const { options, additionalFeatures } = convertFeaturesString(rawFeatures, frameName);
const mergedOptions = {
show: true,
width: 800,
height: 600
height: 600,
...options
};
internalWindowOpen(event, url, referrer, frameName, disposition, options, additionalFeatures, postData);

internalWindowOpen(event, url, referrer, frameName, disposition, mergedOptions, additionalFeatures, postData);
});

// Create a new browser window for the native implementation of
// "window.open", used in sandbox and nativeWindowOpen mode.
this.on('-add-new-contents', (event, webContents, disposition,
userGesture, left, top, width, height, url, frameName) => {
userGesture, left, top, width, height, url, frameName,
referrer, rawFeatures, postData) => {
if ((disposition !== 'foreground-tab' && disposition !== 'new-window' &&
disposition !== 'background-tab')) {
event.preventDefault();
return;
}

const options = {
const { options, additionalFeatures } = convertFeaturesString(rawFeatures, frameName);
const mergedOptions = {
show: true,
x: left,
y: top,
width: width || 800,
height: height || 600,
webContents
width: 800,
height: 600,
webContents,
...options
};
const referrer = { url: '', policy: 'default' };
internalWindowOpen(event, url, referrer, frameName, disposition, options);

internalWindowOpen(event, url, referrer, frameName, disposition, mergedOptions, additionalFeatures, postData);
});
}

Expand Down
13 changes: 1 addition & 12 deletions lib/browser/guest-view-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const { webContents } = require('electron');
const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal');
const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils');
const parseFeaturesString = require('@electron/internal/common/parse-features-string');
const { parseFeaturesString } = require('@electron/internal/common/parse-features-string');
const { syncMethods, asyncMethods, properties } = require('@electron/internal/common/web-view-methods');
const { serialize } = require('@electron/internal/common/type-utils');

Expand Down Expand Up @@ -141,17 +141,6 @@ const createGuest = function (embedder, params) {
}
});

// Forward internal web contents event to embedder to handle
// native window.open setup
guest.on('-add-new-contents', (...args) => {
if (guest.getLastWebPreferences().nativeWindowOpen === true) {
const embedder = getEmbedder(guestInstanceId);
if (embedder != null) {
embedder.emit('-add-new-contents', ...args);
}
}
});

return guestInstanceId;
};

Expand Down
114 changes: 49 additions & 65 deletions lib/browser/guest-window-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { BrowserWindow } = electron;
const { isSameOrigin } = process.electronBinding('v8_util');
const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal');
const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils');
const parseFeaturesString = require('@electron/internal/common/parse-features-string');
const { convertFeaturesString } = require('@electron/internal/common/parse-features-string');

const hasProp = {}.hasOwnProperty;
const frameToGuest = new Map();
Expand Down Expand Up @@ -57,7 +57,11 @@ const mergeBrowserWindowOptions = function (embedder, options) {
// if parent's visibility is available, that overrides 'show' flag (#12125)
const win = BrowserWindow.fromWebContents(embedder.webContents);
if (win != null) {
parentOptions = { ...embedder.browserWindowOptions, show: win.isVisible() };
parentOptions = {
...win.getBounds(),
...embedder.browserWindowOptions,
show: win.isVisible()
};
}

// Inherit the original options if it is a BrowserWindow.
Expand All @@ -83,6 +87,40 @@ const mergeBrowserWindowOptions = function (embedder, options) {
return options;
};

const MULTIPART_CONTENT_TYPE = 'multipart/form-data';
const URL_ENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded';
function makeContentTypeHeader ({ contentType, boundary }) {
const header = `content-type: ${contentType};`;
if (contentType === MULTIPART_CONTENT_TYPE) {
return `${header} boundary=${boundary}`;
}
return header;
}

// Figure out appropriate headers for post data.
const parseContentTypeFormat = function (postData) {
if (postData.length) {
// For multipart forms, the first element will start with the boundary
// notice, which looks something like `------WebKitFormBoundary12345678`
// Note, this regex would fail when submitting a urlencoded form with an
// input attribute of name="--theKey", but, uhh, don't do that?
const postDataFront = postData[0].bytes.toString();
const boundary = /^--.*[^-\r\n]/.exec(postDataFront);
if (boundary) {
return {
boundary: boundary[0].substr(2),
contentType: MULTIPART_CONTENT_TYPE
};
}
}
// Either the form submission didn't contain any inputs (the postData array
// was empty), or we couldn't find the boundary and thus we can assume this is
// a key=value style form.
return {
contentType: URL_ENCODED_CONTENT_TYPE
};
};

// Setup a new guest with |embedder|
const setupGuest = function (embedder, frameName, guest, options) {
// When |embedder| is destroyed we should also destroy attached guest, and if
Expand Down Expand Up @@ -134,14 +172,7 @@ const createGuest = function (embedder, url, referrer, frameName, options, postD
};
if (postData != null) {
loadOptions.postData = postData;
loadOptions.extraHeaders = 'content-type: application/x-www-form-urlencoded';
if (postData.length > 0) {
const postDataFront = postData[0].bytes.toString();
const boundary = /^--.*[^-\r\n]/.exec(postDataFront);
if (boundary != null) {
loadOptions.extraHeaders = `content-type: multipart/form-data; boundary=${boundary[0].substr(2)}`;
}
}
loadOptions.extraHeaders = makeContentTypeHeader(parseContentTypeFormat(postData));
}
guest.loadURL(url, loadOptions);
}
Expand Down Expand Up @@ -192,68 +223,21 @@ ipcMainInternal.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', (event, url, fra
if (frameName == null) frameName = '';
if (features == null) features = '';

const options = {};

const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor'];
const webPreferences = ['zoomFactor', 'nodeIntegration', 'enableRemoteModule', 'preload', 'javascript', 'contextIsolation', 'webviewTag'];
const disposition = 'new-window';

// Used to store additional features
const additionalFeatures = [];

// Parse the features
parseFeaturesString(features, function (key, value) {
if (value === undefined) {
additionalFeatures.push(key);
} else {
// Don't allow webPreferences to be set since it must be an object
// that cannot be directly overridden
if (key === 'webPreferences') return;

if (webPreferences.includes(key)) {
if (options.webPreferences == null) {
options.webPreferences = {};
}
options.webPreferences[key] = value;
} else {
options[key] = value;
}
}
});
if (options.left) {
if (options.x == null) {
options.x = options.left;
}
}
if (options.top) {
if (options.y == null) {
options.y = options.top;
}
}
if (options.title == null) {
options.title = frameName;
}
if (options.width == null) {
options.width = 800;
}
if (options.height == null) {
options.height = 600;
}

for (const name of ints) {
if (options[name] != null) {
options[name] = parseInt(options[name], 10);
}
}

const { options, additionalFeatures } = convertFeaturesString(features, frameName);
const referrer = { url: '', policy: 'default' };
internalWindowOpen(event, url, referrer, frameName, disposition, options, additionalFeatures);
internalWindowOpen(event, url, referrer, frameName, disposition, options, additionalFeatures, null);
});

// Routed window.open messages with fully parsed options
function internalWindowOpen (event, url, referrer, frameName, disposition, options, additionalFeatures, postData) {
options = mergeBrowserWindowOptions(event.sender, options);
event.sender.emit('new-window', event, url, frameName, disposition, options, additionalFeatures, referrer);
const postBody = postData ? {
data: postData,
...parseContentTypeFormat(postData)
} : null;

event.sender.emit('new-window', event, url, frameName, disposition, options, additionalFeatures, referrer, postBody);
const { newGuest } = event;
if ((event.sender.getType() === 'webview' && event.sender.getLastWebPreferences().disablePopups) || event.defaultPrevented) {
if (newGuest != null) {
Expand Down
Loading

0 comments on commit 74372d6

Please sign in to comment.