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:
parent
92b9525cfd
commit
58a6fe13d6
26 changed files with 332 additions and 49 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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[];
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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++
|
||||||
})
|
})
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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|.
|
||||||
|
|
|
@ -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.
|
||||||
|
if (process.isMainFrame) {
|
||||||
require('@electron/internal/renderer/content-scripts-injector')
|
require('@electron/internal/renderer/content-scripts-injector')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load webview tag implementation.
|
// Load webview tag implementation.
|
||||||
|
if (process.isMainFrame) {
|
||||||
require('@electron/internal/renderer/web-view/web-view-init')(contextIsolation, webviewTag, guestInstanceId)
|
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
|
||||||
|
if (process.isMainFrame) {
|
||||||
require('@electron/internal/renderer/security-warnings')(nodeIntegration)
|
require('@electron/internal/renderer/security-warnings')(nodeIntegration)
|
||||||
|
}
|
||||||
|
|
|
@ -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
88
spec/api-subframe-spec.js
Normal 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)
|
||||||
|
})
|
|
@ -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
|
||||||
|
|
13
spec/fixtures/sub-frames/frame-container.html
vendored
Normal file
13
spec/fixtures/sub-frames/frame-container.html
vendored
Normal 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>
|
13
spec/fixtures/sub-frames/frame-with-frame-container.html
vendored
Normal file
13
spec/fixtures/sub-frames/frame-with-frame-container.html
vendored
Normal 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>
|
13
spec/fixtures/sub-frames/frame-with-frame.html
vendored
Normal file
13
spec/fixtures/sub-frames/frame-with-frame.html
vendored
Normal 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
12
spec/fixtures/sub-frames/frame.html
vendored
Normal 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
7
spec/fixtures/sub-frames/preload.js
vendored
Normal 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)
|
||||||
|
})
|
Loading…
Add table
Reference in a new issue