feat: preloads and nodeIntegration in iframes (#16425)

* feat: add support for node / preloads in subframes

This feature has delibrately been built / implemented in such a way
that it has minimum impact on existing apps / code-paths.
Without enabling the new "nodeSupportInSubFrames" option basically none of this
new code will be hit.

The things that I believe need extra scrutiny are:

* Introduction of `event.reply` for IPC events and usage of `event.reply` instead of `event.sender.send()`
* Usage of `node::FreeEnvironment(env)` when the new option is enabled in order to avoid memory leaks.  I have tested this quite a bit and haven't managed to cause a crash but it is still feature flagged behind the "nodeSupportInSubFrames" flag to avoid potential impact.

Closes #10569
Closes #10401
Closes #11868
Closes #12505
Closes #14035

* feat: add support preloads in subframes for sandboxed renderers

* spec: add tests for new nodeSupportInSubFrames option

* spec: fix specs for .reply and ._replyInternal for internal messages

* chore: revert change to use flag instead of environment set size

* chore: clean up subframe impl

* chore: apply suggestions from code review

Co-Authored-By: MarshallOfSound <samuel.r.attard@gmail.com>

* chore: clean up reply usage

* chore: fix TS docs generation

* chore: cleanup after rebase

* chore: rename wrap to add in event fns
This commit is contained in:
Samuel Attard 2019-01-22 11:24:46 -08:00 committed by GitHub
parent 92b9525cfd
commit 58a6fe13d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 332 additions and 49 deletions

View file

@ -57,9 +57,10 @@ v8::Local<v8::Object> CreateJSEvent(v8::Isolate* isolate,
} else { } else {
event = CreateEventObject(isolate); event = CreateEventObject(isolate);
} }
mate::Dictionary(isolate, event).Set("sender", object); mate::Dictionary dict(isolate, event);
dict.Set("sender", object);
if (sender) if (sender)
mate::Dictionary(isolate, event).Set("frameId", sender->GetRoutingID()); dict.Set("frameId", sender->GetRoutingID());
return event; return event;
} }

View file

@ -123,6 +123,7 @@ WebContentsPreferences::WebContentsPreferences(
SetDefaultBoolIfUndefined(options::kPlugins, false); SetDefaultBoolIfUndefined(options::kPlugins, false);
SetDefaultBoolIfUndefined(options::kExperimentalFeatures, false); SetDefaultBoolIfUndefined(options::kExperimentalFeatures, false);
SetDefaultBoolIfUndefined(options::kNodeIntegration, false); SetDefaultBoolIfUndefined(options::kNodeIntegration, false);
SetDefaultBoolIfUndefined(options::kNodeIntegrationInSubFrames, false);
SetDefaultBoolIfUndefined(options::kNodeIntegrationInWorker, false); SetDefaultBoolIfUndefined(options::kNodeIntegrationInWorker, false);
SetDefaultBoolIfUndefined(options::kWebviewTag, false); SetDefaultBoolIfUndefined(options::kWebviewTag, false);
SetDefaultBoolIfUndefined(options::kSandbox, false); SetDefaultBoolIfUndefined(options::kSandbox, false);
@ -369,6 +370,9 @@ void WebContentsPreferences::AppendCommandLineSwitches(
} }
} }
if (IsEnabled(options::kNodeIntegrationInSubFrames))
command_line->AppendSwitch(switches::kNodeIntegrationInSubFrames);
// We are appending args to a webContents so let's save the current state // We are appending args to a webContents so let's save the current state
// of our preferences object so that during the lifetime of the WebContents // of our preferences object so that during the lifetime of the WebContents
// we can fetch the options used to initally configure the WebContents // we can fetch the options used to initally configure the WebContents

View file

@ -154,6 +154,8 @@ const char kAllowRunningInsecureContent[] = "allowRunningInsecureContent";
const char kOffscreen[] = "offscreen"; const char kOffscreen[] = "offscreen";
const char kNodeIntegrationInSubFrames[] = "nodeIntegrationInSubFrames";
} // namespace options } // namespace options
namespace switches { namespace switches {
@ -205,6 +207,10 @@ const char kWebviewTag[] = "webview-tag";
// Command switch passed to renderer process to control nodeIntegration. // Command switch passed to renderer process to control nodeIntegration.
const char kNodeIntegrationInWorker[] = "node-integration-in-worker"; const char kNodeIntegrationInWorker[] = "node-integration-in-worker";
// Command switch passed to renderer process to control whether node
// environments will be created in sub-frames.
const char kNodeIntegrationInSubFrames[] = "node-integration-in-subframes";
// Widevine options // Widevine options
// Path to Widevine CDM binaries. // Path to Widevine CDM binaries.
const char kWidevineCdmPath[] = "widevine-cdm-path"; const char kWidevineCdmPath[] = "widevine-cdm-path";

View file

@ -75,6 +75,7 @@ extern const char kSandbox[];
extern const char kWebSecurity[]; extern const char kWebSecurity[];
extern const char kAllowRunningInsecureContent[]; extern const char kAllowRunningInsecureContent[];
extern const char kOffscreen[]; extern const char kOffscreen[];
extern const char kNodeIntegrationInSubFrames[];
} // namespace options } // namespace options
@ -106,6 +107,7 @@ extern const char kHiddenPage[];
extern const char kNativeWindowOpen[]; extern const char kNativeWindowOpen[];
extern const char kNodeIntegrationInWorker[]; extern const char kNodeIntegrationInWorker[];
extern const char kWebviewTag[]; extern const char kWebviewTag[];
extern const char kNodeIntegrationInSubFrames[];
extern const char kWidevineCdmPath[]; extern const char kWidevineCdmPath[];
extern const char kWidevineCdmVersion[]; extern const char kWidevineCdmVersion[];

View file

@ -187,7 +187,7 @@ void AtomRenderFrameObserver::OnBrowserMessage(bool internal,
return; return;
blink::WebLocalFrame* frame = render_frame_->GetWebFrame(); blink::WebLocalFrame* frame = render_frame_->GetWebFrame();
if (!frame || !render_frame_->IsMainFrame()) if (!frame)
return; return;
EmitIPCEvent(frame, internal, channel, args, sender_id); EmitIPCEvent(frame, internal, channel, args, sender_id);

View file

@ -79,25 +79,27 @@ void AtomRendererClient::DidCreateScriptContext(
content::RenderFrame* render_frame) { content::RenderFrame* render_frame) {
RendererClientBase::DidCreateScriptContext(context, render_frame); RendererClientBase::DidCreateScriptContext(context, render_frame);
// Only allow node integration for the main frame of the top window, unless it
// is a devtools extension page. Allowing child frames or child windows to
// have node integration would result in memory leak, since we don't destroy
// node environment when script context is destroyed.
//
// DevTools extensions do not follow this rule because our implementation
// requires node integration in iframes to work. And usually DevTools
// extensions do not dynamically add/remove iframes.
//
// TODO(zcbenz): Do not create Node environment if node integration is not // TODO(zcbenz): Do not create Node environment if node integration is not
// enabled. // enabled.
if (!(render_frame->IsMainFrame() &&
!render_frame->GetWebFrame()->Opener()) && // Do not load node if we're aren't a main frame or a devtools extension
!IsDevToolsExtension(render_frame)) // unless node support has been explicitly enabled for sub frames
bool is_main_frame =
render_frame->IsMainFrame() && !render_frame->GetWebFrame()->Opener();
bool is_devtools = IsDevToolsExtension(render_frame);
bool allow_node_in_subframes =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames);
bool should_load_node =
is_main_frame || is_devtools || allow_node_in_subframes;
if (!should_load_node) {
return; return;
}
injected_frames_.insert(render_frame); injected_frames_.insert(render_frame);
// Prepare the node bindings. // If this is the first environment we are creating, prepare the node
// bindings.
if (!node_integration_initialized_) { if (!node_integration_initialized_) {
node_integration_initialized_ = true; node_integration_initialized_ = true;
node_bindings_->Initialize(); node_bindings_->Initialize();
@ -115,6 +117,8 @@ void AtomRendererClient::DidCreateScriptContext(
// Add Electron extended APIs. // Add Electron extended APIs.
atom_bindings_->BindTo(env->isolate(), env->process_object()); atom_bindings_->BindTo(env->isolate(), env->process_object());
AddRenderBindings(env->isolate(), env->process_object()); AddRenderBindings(env->isolate(), env->process_object());
mate::Dictionary process_dict(env->isolate(), env->process_object());
process_dict.SetReadOnly("isMainFrame", render_frame->IsMainFrame());
// Load everything. // Load everything.
node_bindings_->LoadEnvironment(env); node_bindings_->LoadEnvironment(env);
@ -146,11 +150,13 @@ void AtomRendererClient::WillReleaseScriptContext(
if (env == node_bindings_->uv_env()) if (env == node_bindings_->uv_env())
node_bindings_->set_uv_env(nullptr); node_bindings_->set_uv_env(nullptr);
// Destroy the node environment. // Destroy the node environment. We only do this if node support has been
// This is disabled because pending async tasks may still use the environment // enabled for sub-frames to avoid a change-of-behavior / introduce crashes
// and would cause crashes later. Node does not seem to clear all async tasks // for existing users.
// when the environment is destroyed. // TODO(MarshallOfSOund): Free the environment regardless of this switch
// node::FreeEnvironment(env); if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames))
node::FreeEnvironment(env);
// AtomBindings is tracking node environments. // AtomBindings is tracking node environments.
atom_bindings_->EnvironmentDestroyed(env); atom_bindings_->EnvironmentDestroyed(env);

View file

@ -139,7 +139,8 @@ AtomSandboxedRendererClient::~AtomSandboxedRendererClient() {}
void AtomSandboxedRendererClient::InitializeBindings( void AtomSandboxedRendererClient::InitializeBindings(
v8::Local<v8::Object> binding, v8::Local<v8::Object> binding,
v8::Local<v8::Context> context) { v8::Local<v8::Context> context,
bool is_main_frame) {
auto* isolate = context->GetIsolate(); auto* isolate = context->GetIsolate();
mate::Dictionary b(isolate, binding); mate::Dictionary b(isolate, binding);
b.SetMethod("get", GetBinding); b.SetMethod("get", GetBinding);
@ -154,6 +155,7 @@ void AtomSandboxedRendererClient::InitializeBindings(
process.SetReadOnly("pid", base::GetCurrentProcId()); process.SetReadOnly("pid", base::GetCurrentProcId());
process.SetReadOnly("sandboxed", true); process.SetReadOnly("sandboxed", true);
process.SetReadOnly("type", "renderer"); process.SetReadOnly("type", "renderer");
process.SetReadOnly("isMainFrame", is_main_frame);
// Pass in CLI flags needed to setup the renderer // Pass in CLI flags needed to setup the renderer
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
@ -180,15 +182,23 @@ void AtomSandboxedRendererClient::DidCreateScriptContext(
// Only allow preload for the main frame or // Only allow preload for the main frame or
// For devtools we still want to run the preload_bundle script // For devtools we still want to run the preload_bundle script
if (!render_frame->IsMainFrame() && !IsDevTools(render_frame) && // Or when nodeSupport is explicitly enabled in sub frames
!IsDevToolsExtension(render_frame)) bool is_main_frame = render_frame->IsMainFrame();
bool is_devtools =
IsDevTools(render_frame) || IsDevToolsExtension(render_frame);
bool allow_node_in_sub_frames =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames);
bool should_load_preload =
is_main_frame || is_devtools || allow_node_in_sub_frames;
if (!should_load_preload)
return; return;
// Wrap the bundle into a function that receives the binding object as // Wrap the bundle into a function that receives the binding object as
// argument. // argument.
auto* isolate = context->GetIsolate(); auto* isolate = context->GetIsolate();
auto binding = v8::Object::New(isolate); auto binding = v8::Object::New(isolate);
InitializeBindings(binding, context); InitializeBindings(binding, context, render_frame->IsMainFrame());
AddRenderBindings(isolate, binding); AddRenderBindings(isolate, binding);
std::vector<v8::Local<v8::String>> preload_bundle_params = { std::vector<v8::Local<v8::String>> preload_bundle_params = {
@ -229,7 +239,10 @@ void AtomSandboxedRendererClient::WillReleaseScriptContext(
v8::Handle<v8::Context> context, v8::Handle<v8::Context> context,
content::RenderFrame* render_frame) { content::RenderFrame* render_frame) {
// Only allow preload for the main frame // Only allow preload for the main frame
if (!render_frame->IsMainFrame()) // Or for sub frames when explicitly enabled
if (!render_frame->IsMainFrame() &&
!base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames))
return; return;
auto* isolate = context->GetIsolate(); auto* isolate = context->GetIsolate();

View file

@ -19,7 +19,8 @@ class AtomSandboxedRendererClient : public RendererClientBase {
~AtomSandboxedRendererClient() override; ~AtomSandboxedRendererClient() override;
void InitializeBindings(v8::Local<v8::Object> binding, void InitializeBindings(v8::Local<v8::Object> binding,
v8::Local<v8::Context> context); v8::Local<v8::Context> context,
bool is_main_frame);
void InvokeIpcCallback(v8::Handle<v8::Context> context, void InvokeIpcCallback(v8::Handle<v8::Context> context,
const std::string& callback_name, const std::string& callback_name,
std::vector<v8::Handle<v8::Value>> args); std::vector<v8::Handle<v8::Value>> args);

View file

@ -255,6 +255,10 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
* `nodeIntegrationInWorker` Boolean (optional) - Whether node integration is * `nodeIntegrationInWorker` Boolean (optional) - Whether node integration is
enabled in web workers. Default is `false`. More about this can be found enabled in web workers. Default is `false`. More about this can be found
in [Multithreading](../tutorial/multithreading.md). in [Multithreading](../tutorial/multithreading.md).
* `nodeIntegrationInSubFrames` Boolean (optional) - Experimental option for
enabling NodeJS support in sub-frames such as iframes. All your preloads will load for
every iframe, you can use `process.isMainFrame` to determine if you are
in the main frame or not.
* `preload` String (optional) - Specifies a script that will be loaded before other * `preload` String (optional) - Specifies a script that will be loaded before other
scripts run in the page. This script will always have access to node APIs scripts run in the page. This script will always have access to node APIs
no matter whether node integration is turned on or off. The value should no matter whether node integration is turned on or off. The value should

View file

@ -18,7 +18,9 @@ process, see [webContents.send][web-contents-send] for more information.
* When sending a message, the event name is the `channel`. * When sending a message, the event name is the `channel`.
* To reply to a synchronous message, you need to set `event.returnValue`. * To reply to a synchronous message, you need to set `event.returnValue`.
* To send an asynchronous message back to the sender, you can use * To send an asynchronous message back to the sender, you can use
`event.sender.send(...)`. `event.reply(...)`. This helper method will automatically handle messages
coming from frames that aren't the main frame (e.g. iframes) whereas
`event.sender.send(...)` will always send to the main frame.
An example of sending and handling messages between the render and main An example of sending and handling messages between the render and main
processes: processes:
@ -28,7 +30,7 @@ processes:
const { ipcMain } = require('electron') const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => { ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping" console.log(arg) // prints "ping"
event.sender.send('asynchronous-reply', 'pong') event.reply('asynchronous-reply', 'pong')
}) })
ipcMain.on('synchronous-message', (event, arg) => { ipcMain.on('synchronous-message', (event, arg) => {
@ -86,6 +88,10 @@ Removes listeners of the specified `channel`.
The `event` object passed to the `callback` has the following methods: The `event` object passed to the `callback` has the following methods:
### `event.frameId`
An `Integer` representing the ID of the renderer frame that sent this message.
### `event.returnValue` ### `event.returnValue`
Set this to the value to be returned in a synchronous message. Set this to the value to be returned in a synchronous message.
@ -97,3 +103,10 @@ Returns the `webContents` that sent the message, you can call
[webContents.send][web-contents-send] for more information. [webContents.send][web-contents-send] for more information.
[web-contents-send]: web-contents.md#contentssendchannel-arg1-arg2- [web-contents-send]: web-contents.md#contentssendchannel-arg1-arg2-
### `event.reply`
A function that will send an IPC message to the renderer frane that sent
the original message that you are currently handling. You should use this
method to "reply" to the sent message in order to guaruntee the reply will go
to the correct process and frame.

View file

@ -59,6 +59,11 @@ process.once('loaded', () => {
A `Boolean`. When app is started by being passed as parameter to the default app, this A `Boolean`. When app is started by being passed as parameter to the default app, this
property is `true` in the main process, otherwise it is `undefined`. property is `true` in the main process, otherwise it is `undefined`.
### `process.isMainFrame`
A `Boolean`, `true` when the current renderer context is the "main" renderer
frame. If you want the ID of the current frame you should use `webFrame.routingId`.
### `process.mas` ### `process.mas`
A `Boolean`. For Mac App Store build, this property is `true`, for other builds it is A `Boolean`. For Mac App Store build, this property is `true`, for other builds it is

View file

@ -1420,6 +1420,36 @@ app.on('ready', () => {
</html> </html>
``` ```
#### `contents.sendToFrame(frameId, channel[, arg1][, arg2][, ...])`
* `frameId` Integer
* `channel` String
* `...args` any[]
Send an asynchronous message to a specific frame in a renderer process via
`channel`. Arguments will be serialized
as JSON internally and as such no functions or prototype chains will be included.
The renderer process can handle the message by listening to `channel` with the
[`ipcRenderer`](ipc-renderer.md) module.
If you want to get the `frameId` of a given renderer context you should use
the `webFrame.routingId` value. E.g.
```js
// In a renderer process
console.log('My frameId is:', require('electron').webFrame.routingId)
```
You can also read `frameId` from all incoming IPC messages in the main process.
```js
// In the main process
ipcMain.on('ping', (event) => {
console.info('Message came from frameId:', event.frameId)
})
```
#### `contents.enableDeviceEmulation(parameters)` #### `contents.enableDeviceEmulation(parameters)`
* `parameters` Object * `parameters` Object

View file

@ -143,6 +143,18 @@ WebContents.prototype._sendInternalToAll = function (channel, ...args) {
return this._send(internal, sendToAll, channel, args) return this._send(internal, sendToAll, channel, args)
} }
WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument')
} else if (typeof frameId !== 'number') {
throw new Error('Missing required frameId argument')
}
const internal = false
const sendToAll = false
return this._sendToFrame(internal, sendToAll, frameId, channel, args)
}
WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args) { WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args) {
if (typeof channel !== 'string') { if (typeof channel !== 'string') {
throw new Error('Missing required channel argument') throw new Error('Missing required channel argument')
@ -330,6 +342,22 @@ WebContents.prototype.loadFile = function (filePath, options = {}) {
})) }))
} }
const addReplyToEvent = (event) => {
event.reply = (...args) => {
event.sender.sendToFrame(event.frameId, ...args)
}
}
const addReplyInternalToEvent = (event) => {
Object.defineProperty(event, '_replyInternal', {
configurable: false,
enumerable: false,
value: (...args) => {
event.sender._sendToFrameInternal(event.frameId, ...args)
}
})
}
// Add JavaScript wrappers for WebContents class. // Add JavaScript wrappers for WebContents class.
WebContents.prototype._init = function () { WebContents.prototype._init = function () {
// The navigation controller. // The navigation controller.
@ -343,6 +371,7 @@ WebContents.prototype._init = function () {
// Dispatch IPC messages to the ipc module. // Dispatch IPC messages to the ipc module.
this.on('-ipc-message', function (event, [channel, ...args]) { this.on('-ipc-message', function (event, [channel, ...args]) {
addReplyToEvent(event)
this.emit('ipc-message', event, channel, ...args) this.emit('ipc-message', event, channel, ...args)
ipcMain.emit(channel, event, ...args) ipcMain.emit(channel, event, ...args)
}) })
@ -354,11 +383,13 @@ WebContents.prototype._init = function () {
}, },
get: function () {} get: function () {}
}) })
addReplyToEvent(event)
this.emit('ipc-message-sync', event, channel, ...args) this.emit('ipc-message-sync', event, channel, ...args)
ipcMain.emit(channel, event, ...args) ipcMain.emit(channel, event, ...args)
}) })
this.on('ipc-internal-message', function (event, [channel, ...args]) { this.on('ipc-internal-message', function (event, [channel, ...args]) {
addReplyInternalToEvent(event)
ipcMainInternal.emit(channel, event, ...args) ipcMainInternal.emit(channel, event, ...args)
}) })
@ -369,6 +400,7 @@ WebContents.prototype._init = function () {
}, },
get: function () {} get: function () {}
}) })
addReplyInternalToEvent(event)
ipcMainInternal.emit(channel, event, ...args) ipcMainInternal.emit(channel, event, ...args)
}) })

View file

@ -180,7 +180,7 @@ ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message,
page.webContents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message, resultID) page.webContents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message, resultID)
ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => { ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => {
event.sender._sendInternal(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, result) event._replyInternal(`CHROME_RUNTIME_SENDMESSAGE_RESULT_${originResultID}`, result)
}) })
resultID++ resultID++
}) })
@ -196,7 +196,7 @@ ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBa
contents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message, resultID) contents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message, resultID)
ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => { ipcMain.once(`CHROME_RUNTIME_ONMESSAGE_RESULT_${resultID}`, (event, result) => {
event.sender._sendInternal(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, result) event._replyInternal(`CHROME_TABS_SEND_MESSAGE_RESULT_${originResultID}`, result)
}) })
resultID++ resultID++
}) })

View file

@ -18,7 +18,7 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize,
event.sender.emit('desktop-capturer-get-sources', customEvent) event.sender.emit('desktop-capturer-get-sources', customEvent)
if (customEvent.defaultPrevented) { if (customEvent.defaultPrevented) {
event.sender._sendInternal(capturerResult(id), []) event._replyInternal(capturerResult(id), [])
return return
} }
@ -30,7 +30,7 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize,
thumbnailSize, thumbnailSize,
fetchWindowIcons fetchWindowIcons
}, },
webContents: event.sender event
} }
requestsQueue.push(request) requestsQueue.push(request)
if (requestsQueue.length === 1) { if (requestsQueue.length === 1) {
@ -40,14 +40,13 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize,
// If the WebContents is destroyed before receiving result, just remove the // If the WebContents is destroyed before receiving result, just remove the
// reference from requestsQueue to make the module not send the result to it. // reference from requestsQueue to make the module not send the result to it.
event.sender.once('destroyed', () => { event.sender.once('destroyed', () => {
request.webContents = null request.event = null
}) })
}) })
desktopCapturer.emit = (event, name, sources, fetchWindowIcons) => { desktopCapturer.emit = (event, name, sources, fetchWindowIcons) => {
// Receiving sources result from main process, now send them back to renderer. // Receiving sources result from main process, now send them back to renderer.
const handledRequest = requestsQueue.shift() const handledRequest = requestsQueue.shift()
const handledWebContents = handledRequest.webContents
const unhandledRequestsQueue = [] const unhandledRequestsQueue = []
const result = sources.map(source => { const result = sources.map(source => {
@ -60,16 +59,16 @@ desktopCapturer.emit = (event, name, sources, fetchWindowIcons) => {
} }
}) })
if (handledWebContents) { if (handledRequest.event) {
handledWebContents._sendInternal(capturerResult(handledRequest.id), result) handledRequest.event._replyInternal(capturerResult(handledRequest.id), result)
} }
// Check the queue to see whether there is another identical request & handle // Check the queue to see whether there is another identical request & handle
requestsQueue.forEach(request => { requestsQueue.forEach(request => {
const webContents = request.webContents const event = request.event
if (deepEqual(handledRequest.options, request.options)) { if (deepEqual(handledRequest.options, request.options)) {
if (webContents) { if (event) {
webContents._sendInternal(capturerResult(request.id), result) event._replyInternal(capturerResult(request.id), result)
} }
} else { } else {
unhandledRequestsQueue.push(request) unhandledRequestsQueue.push(request)

View file

@ -246,7 +246,8 @@ const attachGuest = function (event, embedderFrameId, elementInstanceId, guestIn
['nativeWindowOpen', true], ['nativeWindowOpen', true],
['nodeIntegration', false], ['nodeIntegration', false],
['enableRemoteModule', false], ['enableRemoteModule', false],
['sandbox', true] ['sandbox', true],
['nodeIntegrationInSubFrames', false]
]) ])
// Inherit certain option values from embedder // Inherit certain option values from embedder
@ -350,7 +351,7 @@ const handleMessage = function (channel, handler) {
} }
handleMessage('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST', function (event, params, requestId) { handleMessage('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST', function (event, params, requestId) {
event.sender._sendInternal(`ELECTRON_RESPONSE_${requestId}`, createGuest(event.sender, params)) event._replyInternal(`ELECTRON_RESPONSE_${requestId}`, createGuest(event.sender, params))
}) })
handleMessage('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST_SYNC', function (event, params) { handleMessage('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST_SYNC', function (event, params) {
@ -400,7 +401,7 @@ handleMessage('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', function (event, request
}, error => { }, error => {
return [errorUtils.serialize(error)] return [errorUtils.serialize(error)]
}).then(responseArgs => { }).then(responseArgs => {
event.sender._sendInternal(`ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL_RESPONSE_${requestId}`, ...responseArgs) event._replyInternal(`ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL_RESPONSE_${requestId}`, ...responseArgs)
}) })
}) })

View file

@ -16,7 +16,8 @@ const inheritedWebPreferences = new Map([
['nodeIntegration', false], ['nodeIntegration', false],
['enableRemoteModule', false], ['enableRemoteModule', false],
['sandbox', true], ['sandbox', true],
['webviewTag', false] ['webviewTag', false],
['nodeIntegrationInSubFrames', false]
]) ])
// Copy attribute of |parent| to |child| if it is not defined in |child|. // Copy attribute of |parent| to |child| if it is not defined in |child|.

View file

@ -76,12 +76,16 @@ switch (window.location.protocol) {
require('@electron/internal/renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen) require('@electron/internal/renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen)
// Inject content scripts. // Inject content scripts.
require('@electron/internal/renderer/content-scripts-injector') if (process.isMainFrame) {
require('@electron/internal/renderer/content-scripts-injector')
}
} }
} }
// Load webview tag implementation. // Load webview tag implementation.
require('@electron/internal/renderer/web-view/web-view-init')(contextIsolation, webviewTag, guestInstanceId) if (process.isMainFrame) {
require('@electron/internal/renderer/web-view/web-view-init')(contextIsolation, webviewTag, guestInstanceId)
}
// Pass the arguments to isolatedWorld. // Pass the arguments to isolatedWorld.
if (contextIsolation) { if (contextIsolation) {
@ -160,4 +164,6 @@ for (const preloadScript of preloadScripts) {
} }
// Warn about security issues // Warn about security issues
require('@electron/internal/renderer/security-warnings')(nodeIntegration) if (process.isMainFrame) {
require('@electron/internal/renderer/security-warnings')(nodeIntegration)
}

View file

@ -26,7 +26,7 @@
const { defineProperty, defineProperties } = Object const { defineProperty, defineProperties } = Object
// Helper function to resolve relative url. // Helper function to resolve relative url.
const a = window.top.document.createElement('a') const a = window.document.createElement('a')
const resolveURL = function (url) { const resolveURL = function (url) {
a.href = url a.href = url
return a.href return a.href

88
spec/api-subframe-spec.js Normal file
View file

@ -0,0 +1,88 @@
const { expect } = require('chai')
const { remote } = require('electron')
const path = require('path')
const { emittedNTimes, emittedOnce } = require('./events-helpers')
const { closeWindow } = require('./window-helpers')
const { BrowserWindow } = remote
describe('renderer nodeIntegrationInSubFrames', () => {
const generateTests = (sandboxEnabled) => {
describe(`with sandbox ${sandboxEnabled ? 'enabled' : 'disabled'}`, () => {
let w
beforeEach(async () => {
await closeWindow(w)
w = new BrowserWindow({
show: false,
width: 400,
height: 400,
webPreferences: {
sandbox: sandboxEnabled,
preload: path.resolve(__dirname, 'fixtures/sub-frames/preload.js'),
nodeIntegrationInSubFrames: true
}
})
})
afterEach(() => {
return closeWindow(w).then(() => { w = null })
})
it('should load preload scripts in top level iframes', async () => {
const detailsPromise = emittedNTimes(remote.ipcMain, 'preload-ran', 2)
w.loadFile(path.resolve(__dirname, 'fixtures/sub-frames/frame-container.html'))
const [event1, event2] = await detailsPromise
expect(event1[0].frameId).to.not.equal(event2[0].frameId)
expect(event1[0].frameId).to.equal(event1[2])
expect(event2[0].frameId).to.equal(event2[2])
})
it('should load preload scripts in nested iframes', async () => {
const detailsPromise = emittedNTimes(remote.ipcMain, 'preload-ran', 3)
w.loadFile(path.resolve(__dirname, 'fixtures/sub-frames/frame-with-frame-container.html'))
const [event1, event2, event3] = await detailsPromise
expect(event1[0].frameId).to.not.equal(event2[0].frameId)
expect(event1[0].frameId).to.not.equal(event3[0].frameId)
expect(event2[0].frameId).to.not.equal(event3[0].frameId)
expect(event1[0].frameId).to.equal(event1[2])
expect(event2[0].frameId).to.equal(event2[2])
expect(event3[0].frameId).to.equal(event3[2])
})
it('should correctly reply to the main frame with using event.reply', async () => {
const detailsPromise = emittedNTimes(remote.ipcMain, 'preload-ran', 2)
w.loadFile(path.resolve(__dirname, 'fixtures/sub-frames/frame-container.html'))
const [event1] = await detailsPromise
const pongPromise = emittedOnce(remote.ipcMain, 'preload-pong')
event1[0].reply('preload-ping')
const details = await pongPromise
expect(details[1]).to.equal(event1[0].frameId)
})
it('should correctly reply to the sub-frames with using event.reply', async () => {
const detailsPromise = emittedNTimes(remote.ipcMain, 'preload-ran', 2)
w.loadFile(path.resolve(__dirname, 'fixtures/sub-frames/frame-container.html'))
const [, event2] = await detailsPromise
const pongPromise = emittedOnce(remote.ipcMain, 'preload-pong')
event2[0].reply('preload-ping')
const details = await pongPromise
expect(details[1]).to.equal(event2[0].frameId)
})
it('should correctly reply to the nested sub-frames with using event.reply', async () => {
const detailsPromise = emittedNTimes(remote.ipcMain, 'preload-ran', 3)
w.loadFile(path.resolve(__dirname, 'fixtures/sub-frames/frame-with-frame-container.html'))
const [,, event3] = await detailsPromise
const pongPromise = emittedOnce(remote.ipcMain, 'preload-pong')
event3[0].reply('preload-ping')
const details = await pongPromise
expect(details[1]).to.equal(event3[0].frameId)
})
})
}
generateTests(false)
generateTests(true)
})

View file

@ -20,10 +20,23 @@ const waitForEvent = (target, eventName) => {
* @return {!Promise<!Array>} With Event as the first item. * @return {!Promise<!Array>} With Event as the first item.
*/ */
const emittedOnce = (emitter, eventName) => { const emittedOnce = (emitter, eventName) => {
return emittedNTimes(emitter, eventName, 1).then(([result]) => result)
}
const emittedNTimes = (emitter, eventName, times) => {
const events = []
return new Promise(resolve => { return new Promise(resolve => {
emitter.once(eventName, (...args) => resolve(args)) const handler = (...args) => {
events.push(args)
if (events.length === times) {
emitter.removeListener(eventName, handler)
resolve(events)
}
}
emitter.on(eventName, handler)
}) })
} }
exports.emittedOnce = emittedOnce exports.emittedOnce = emittedOnce
exports.emittedNTimes = emittedNTimes
exports.waitForEvent = waitForEvent exports.waitForEvent = waitForEvent

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
This is the root page
<iframe src="./frame.html"></iframe>
</body>
</html>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
This is the root page
<iframe src="./frame-with-frame.html"></iframe>
</body>
</html>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
This is a frame, is has one child
<iframe src="./frame.html"></iframe>
</body>
</html>

12
spec/fixtures/sub-frames/frame.html vendored Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
This is a frame, it has no children
</body>
</html>

7
spec/fixtures/sub-frames/preload.js vendored Normal file
View file

@ -0,0 +1,7 @@
const { ipcRenderer, webFrame } = require('electron')
ipcRenderer.send('preload-ran', window.location.href, webFrame.routingId)
ipcRenderer.on('preload-ping', () => {
ipcRenderer.send('preload-pong', webFrame.routingId)
})