diff --git a/atom/browser/api/event_emitter.cc b/atom/browser/api/event_emitter.cc index 4558514d9f..b3cdb21d13 100644 --- a/atom/browser/api/event_emitter.cc +++ b/atom/browser/api/event_emitter.cc @@ -57,9 +57,10 @@ v8::Local CreateJSEvent(v8::Isolate* isolate, } else { event = CreateEventObject(isolate); } - mate::Dictionary(isolate, event).Set("sender", object); + mate::Dictionary dict(isolate, event); + dict.Set("sender", object); if (sender) - mate::Dictionary(isolate, event).Set("frameId", sender->GetRoutingID()); + dict.Set("frameId", sender->GetRoutingID()); return event; } diff --git a/atom/browser/web_contents_preferences.cc b/atom/browser/web_contents_preferences.cc index 80fbac89a9..080ce6bd22 100644 --- a/atom/browser/web_contents_preferences.cc +++ b/atom/browser/web_contents_preferences.cc @@ -123,6 +123,7 @@ WebContentsPreferences::WebContentsPreferences( SetDefaultBoolIfUndefined(options::kPlugins, false); SetDefaultBoolIfUndefined(options::kExperimentalFeatures, false); SetDefaultBoolIfUndefined(options::kNodeIntegration, false); + SetDefaultBoolIfUndefined(options::kNodeIntegrationInSubFrames, false); SetDefaultBoolIfUndefined(options::kNodeIntegrationInWorker, false); SetDefaultBoolIfUndefined(options::kWebviewTag, 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 // of our preferences object so that during the lifetime of the WebContents // we can fetch the options used to initally configure the WebContents diff --git a/atom/common/options_switches.cc b/atom/common/options_switches.cc index d1a5d0c749..fac25860de 100644 --- a/atom/common/options_switches.cc +++ b/atom/common/options_switches.cc @@ -154,6 +154,8 @@ const char kAllowRunningInsecureContent[] = "allowRunningInsecureContent"; const char kOffscreen[] = "offscreen"; +const char kNodeIntegrationInSubFrames[] = "nodeIntegrationInSubFrames"; + } // namespace options namespace switches { @@ -205,6 +207,10 @@ const char kWebviewTag[] = "webview-tag"; // Command switch passed to renderer process to control nodeIntegration. 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 // Path to Widevine CDM binaries. const char kWidevineCdmPath[] = "widevine-cdm-path"; diff --git a/atom/common/options_switches.h b/atom/common/options_switches.h index b2bdb8339e..03d8e5548c 100644 --- a/atom/common/options_switches.h +++ b/atom/common/options_switches.h @@ -75,6 +75,7 @@ extern const char kSandbox[]; extern const char kWebSecurity[]; extern const char kAllowRunningInsecureContent[]; extern const char kOffscreen[]; +extern const char kNodeIntegrationInSubFrames[]; } // namespace options @@ -106,6 +107,7 @@ extern const char kHiddenPage[]; extern const char kNativeWindowOpen[]; extern const char kNodeIntegrationInWorker[]; extern const char kWebviewTag[]; +extern const char kNodeIntegrationInSubFrames[]; extern const char kWidevineCdmPath[]; extern const char kWidevineCdmVersion[]; diff --git a/atom/renderer/atom_render_frame_observer.cc b/atom/renderer/atom_render_frame_observer.cc index 09d36b80cc..a3c99841a5 100644 --- a/atom/renderer/atom_render_frame_observer.cc +++ b/atom/renderer/atom_render_frame_observer.cc @@ -187,7 +187,7 @@ void AtomRenderFrameObserver::OnBrowserMessage(bool internal, return; blink::WebLocalFrame* frame = render_frame_->GetWebFrame(); - if (!frame || !render_frame_->IsMainFrame()) + if (!frame) return; EmitIPCEvent(frame, internal, channel, args, sender_id); diff --git a/atom/renderer/atom_renderer_client.cc b/atom/renderer/atom_renderer_client.cc index a8e9f3c349..e8e5b8be69 100644 --- a/atom/renderer/atom_renderer_client.cc +++ b/atom/renderer/atom_renderer_client.cc @@ -79,25 +79,27 @@ void AtomRendererClient::DidCreateScriptContext( content::RenderFrame* 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 // enabled. - if (!(render_frame->IsMainFrame() && - !render_frame->GetWebFrame()->Opener()) && - !IsDevToolsExtension(render_frame)) + + // Do not load node if we're aren't a main frame or a devtools extension + // 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; + } 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_) { node_integration_initialized_ = true; node_bindings_->Initialize(); @@ -115,6 +117,8 @@ void AtomRendererClient::DidCreateScriptContext( // Add Electron extended APIs. atom_bindings_->BindTo(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. node_bindings_->LoadEnvironment(env); @@ -146,11 +150,13 @@ void AtomRendererClient::WillReleaseScriptContext( if (env == node_bindings_->uv_env()) node_bindings_->set_uv_env(nullptr); - // Destroy the node environment. - // This is disabled because pending async tasks may still use the environment - // and would cause crashes later. Node does not seem to clear all async tasks - // when the environment is destroyed. - // node::FreeEnvironment(env); + // Destroy the node environment. We only do this if node support has been + // enabled for sub-frames to avoid a change-of-behavior / introduce crashes + // for existing users. + // TODO(MarshallOfSOund): Free the environment regardless of this switch + if (base::CommandLine::ForCurrentProcess()->HasSwitch( + switches::kNodeIntegrationInSubFrames)) + node::FreeEnvironment(env); // AtomBindings is tracking node environments. atom_bindings_->EnvironmentDestroyed(env); diff --git a/atom/renderer/atom_sandboxed_renderer_client.cc b/atom/renderer/atom_sandboxed_renderer_client.cc index 96ebadfb7a..9bf99db1f1 100644 --- a/atom/renderer/atom_sandboxed_renderer_client.cc +++ b/atom/renderer/atom_sandboxed_renderer_client.cc @@ -139,7 +139,8 @@ AtomSandboxedRendererClient::~AtomSandboxedRendererClient() {} void AtomSandboxedRendererClient::InitializeBindings( v8::Local binding, - v8::Local context) { + v8::Local context, + bool is_main_frame) { auto* isolate = context->GetIsolate(); mate::Dictionary b(isolate, binding); b.SetMethod("get", GetBinding); @@ -154,6 +155,7 @@ void AtomSandboxedRendererClient::InitializeBindings( process.SetReadOnly("pid", base::GetCurrentProcId()); process.SetReadOnly("sandboxed", true); process.SetReadOnly("type", "renderer"); + process.SetReadOnly("isMainFrame", is_main_frame); // Pass in CLI flags needed to setup the renderer base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); @@ -180,15 +182,23 @@ void AtomSandboxedRendererClient::DidCreateScriptContext( // Only allow preload for the main frame or // For devtools we still want to run the preload_bundle script - if (!render_frame->IsMainFrame() && !IsDevTools(render_frame) && - !IsDevToolsExtension(render_frame)) + // Or when nodeSupport is explicitly enabled in sub frames + 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; // Wrap the bundle into a function that receives the binding object as // argument. auto* isolate = context->GetIsolate(); auto binding = v8::Object::New(isolate); - InitializeBindings(binding, context); + InitializeBindings(binding, context, render_frame->IsMainFrame()); AddRenderBindings(isolate, binding); std::vector> preload_bundle_params = { @@ -229,7 +239,10 @@ void AtomSandboxedRendererClient::WillReleaseScriptContext( v8::Handle context, content::RenderFrame* render_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; auto* isolate = context->GetIsolate(); diff --git a/atom/renderer/atom_sandboxed_renderer_client.h b/atom/renderer/atom_sandboxed_renderer_client.h index ac543fbe62..11c5150aab 100644 --- a/atom/renderer/atom_sandboxed_renderer_client.h +++ b/atom/renderer/atom_sandboxed_renderer_client.h @@ -19,7 +19,8 @@ class AtomSandboxedRendererClient : public RendererClientBase { ~AtomSandboxedRendererClient() override; void InitializeBindings(v8::Local binding, - v8::Local context); + v8::Local context, + bool is_main_frame); void InvokeIpcCallback(v8::Handle context, const std::string& callback_name, std::vector> args); diff --git a/docs/api/browser-window.md b/docs/api/browser-window.md index 90b85ecde8..b3b6f3fcab 100644 --- a/docs/api/browser-window.md +++ b/docs/api/browser-window.md @@ -255,6 +255,10 @@ It creates a new `BrowserWindow` with native properties as set by the `options`. * `nodeIntegrationInWorker` Boolean (optional) - Whether node integration is enabled in web workers. Default is `false`. More about this can be found 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 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 diff --git a/docs/api/ipc-main.md b/docs/api/ipc-main.md index f4439fe0a6..ec6a712c84 100644 --- a/docs/api/ipc-main.md +++ b/docs/api/ipc-main.md @@ -18,7 +18,9 @@ process, see [webContents.send][web-contents-send] for more information. * When sending a message, the event name is the `channel`. * To reply to a synchronous message, you need to set `event.returnValue`. * 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 processes: @@ -28,7 +30,7 @@ processes: const { ipcMain } = require('electron') ipcMain.on('asynchronous-message', (event, arg) => { console.log(arg) // prints "ping" - event.sender.send('asynchronous-reply', 'pong') + event.reply('asynchronous-reply', 'pong') }) 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: +### `event.frameId` + +An `Integer` representing the ID of the renderer frame that sent this message. + ### `event.returnValue` 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. [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. diff --git a/docs/api/process.md b/docs/api/process.md index 9513c15cf5..4f92c7737b 100644 --- a/docs/api/process.md +++ b/docs/api/process.md @@ -59,6 +59,11 @@ process.once('loaded', () => { 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`. +### `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` A `Boolean`. For Mac App Store build, this property is `true`, for other builds it is diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index b0a206cd31..a0d51f6311 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1420,6 +1420,36 @@ app.on('ready', () => { ``` +#### `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)` * `parameters` Object diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index 8d9d962330..adbe7e5710 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -143,6 +143,18 @@ WebContents.prototype._sendInternalToAll = function (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) { if (typeof channel !== 'string') { 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. WebContents.prototype._init = function () { // The navigation controller. @@ -343,6 +371,7 @@ WebContents.prototype._init = function () { // Dispatch IPC messages to the ipc module. this.on('-ipc-message', function (event, [channel, ...args]) { + addReplyToEvent(event) this.emit('ipc-message', event, channel, ...args) ipcMain.emit(channel, event, ...args) }) @@ -354,11 +383,13 @@ WebContents.prototype._init = function () { }, get: function () {} }) + addReplyToEvent(event) this.emit('ipc-message-sync', event, channel, ...args) ipcMain.emit(channel, event, ...args) }) this.on('ipc-internal-message', function (event, [channel, ...args]) { + addReplyInternalToEvent(event) ipcMainInternal.emit(channel, event, ...args) }) @@ -369,6 +400,7 @@ WebContents.prototype._init = function () { }, get: function () {} }) + addReplyInternalToEvent(event) ipcMainInternal.emit(channel, event, ...args) }) diff --git a/lib/browser/chrome-extension.js b/lib/browser/chrome-extension.js index 3f4561f8d9..5c4e04bcb1 100644 --- a/lib/browser/chrome-extension.js +++ b/lib/browser/chrome-extension.js @@ -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) 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++ }) @@ -196,7 +196,7 @@ ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBa contents._sendInternalToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message, resultID) 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++ }) diff --git a/lib/browser/desktop-capturer.js b/lib/browser/desktop-capturer.js index fa10803079..bf34199b21 100644 --- a/lib/browser/desktop-capturer.js +++ b/lib/browser/desktop-capturer.js @@ -18,7 +18,7 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize, event.sender.emit('desktop-capturer-get-sources', customEvent) if (customEvent.defaultPrevented) { - event.sender._sendInternal(capturerResult(id), []) + event._replyInternal(capturerResult(id), []) return } @@ -30,7 +30,7 @@ ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize, thumbnailSize, fetchWindowIcons }, - webContents: event.sender + event } requestsQueue.push(request) 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 // reference from requestsQueue to make the module not send the result to it. event.sender.once('destroyed', () => { - request.webContents = null + request.event = null }) }) desktopCapturer.emit = (event, name, sources, fetchWindowIcons) => { // Receiving sources result from main process, now send them back to renderer. const handledRequest = requestsQueue.shift() - const handledWebContents = handledRequest.webContents const unhandledRequestsQueue = [] const result = sources.map(source => { @@ -60,16 +59,16 @@ desktopCapturer.emit = (event, name, sources, fetchWindowIcons) => { } }) - if (handledWebContents) { - handledWebContents._sendInternal(capturerResult(handledRequest.id), result) + if (handledRequest.event) { + handledRequest.event._replyInternal(capturerResult(handledRequest.id), result) } // Check the queue to see whether there is another identical request & handle requestsQueue.forEach(request => { - const webContents = request.webContents + const event = request.event if (deepEqual(handledRequest.options, request.options)) { - if (webContents) { - webContents._sendInternal(capturerResult(request.id), result) + if (event) { + event._replyInternal(capturerResult(request.id), result) } } else { unhandledRequestsQueue.push(request) diff --git a/lib/browser/guest-view-manager.js b/lib/browser/guest-view-manager.js index 5c0c400248..920b02021a 100644 --- a/lib/browser/guest-view-manager.js +++ b/lib/browser/guest-view-manager.js @@ -246,7 +246,8 @@ const attachGuest = function (event, embedderFrameId, elementInstanceId, guestIn ['nativeWindowOpen', true], ['nodeIntegration', false], ['enableRemoteModule', false], - ['sandbox', true] + ['sandbox', true], + ['nodeIntegrationInSubFrames', false] ]) // 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) { - 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) { @@ -400,7 +401,7 @@ handleMessage('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', function (event, request }, error => { return [errorUtils.serialize(error)] }).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) }) }) diff --git a/lib/browser/guest-window-manager.js b/lib/browser/guest-window-manager.js index ffc1bf99da..84ac8ef448 100644 --- a/lib/browser/guest-window-manager.js +++ b/lib/browser/guest-window-manager.js @@ -16,7 +16,8 @@ const inheritedWebPreferences = new Map([ ['nodeIntegration', false], ['enableRemoteModule', false], ['sandbox', true], - ['webviewTag', false] + ['webviewTag', false], + ['nodeIntegrationInSubFrames', false] ]) // Copy attribute of |parent| to |child| if it is not defined in |child|. diff --git a/lib/renderer/init.js b/lib/renderer/init.js index 43fa45b74a..cd2ffc5738 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -76,12 +76,16 @@ switch (window.location.protocol) { require('@electron/internal/renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen) // Inject content scripts. - require('@electron/internal/renderer/content-scripts-injector') + if (process.isMainFrame) { + require('@electron/internal/renderer/content-scripts-injector') + } } } // 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. if (contextIsolation) { @@ -160,4 +164,6 @@ for (const preloadScript of preloadScripts) { } // Warn about security issues -require('@electron/internal/renderer/security-warnings')(nodeIntegration) +if (process.isMainFrame) { + require('@electron/internal/renderer/security-warnings')(nodeIntegration) +} diff --git a/lib/renderer/window-setup.js b/lib/renderer/window-setup.js index 3be08d9177..b4801a8868 100644 --- a/lib/renderer/window-setup.js +++ b/lib/renderer/window-setup.js @@ -26,7 +26,7 @@ const { defineProperty, defineProperties } = Object // Helper function to resolve relative url. -const a = window.top.document.createElement('a') +const a = window.document.createElement('a') const resolveURL = function (url) { a.href = url return a.href diff --git a/spec/api-subframe-spec.js b/spec/api-subframe-spec.js new file mode 100644 index 0000000000..dc31a370b3 --- /dev/null +++ b/spec/api-subframe-spec.js @@ -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) +}) diff --git a/spec/events-helpers.js b/spec/events-helpers.js index 39f53c36db..64a4fba447 100644 --- a/spec/events-helpers.js +++ b/spec/events-helpers.js @@ -20,10 +20,23 @@ const waitForEvent = (target, eventName) => { * @return {!Promise} With Event as the first item. */ const emittedOnce = (emitter, eventName) => { + return emittedNTimes(emitter, eventName, 1).then(([result]) => result) +} + +const emittedNTimes = (emitter, eventName, times) => { + const events = [] 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.emittedNTimes = emittedNTimes exports.waitForEvent = waitForEvent diff --git a/spec/fixtures/sub-frames/frame-container.html b/spec/fixtures/sub-frames/frame-container.html new file mode 100644 index 0000000000..f731555a5d --- /dev/null +++ b/spec/fixtures/sub-frames/frame-container.html @@ -0,0 +1,13 @@ + + + + + + + Document + + + This is the root page + + + \ No newline at end of file diff --git a/spec/fixtures/sub-frames/frame-with-frame-container.html b/spec/fixtures/sub-frames/frame-with-frame-container.html new file mode 100644 index 0000000000..823fb1aafe --- /dev/null +++ b/spec/fixtures/sub-frames/frame-with-frame-container.html @@ -0,0 +1,13 @@ + + + + + + + Document + + + This is the root page + + + \ No newline at end of file diff --git a/spec/fixtures/sub-frames/frame-with-frame.html b/spec/fixtures/sub-frames/frame-with-frame.html new file mode 100644 index 0000000000..9d99fef71b --- /dev/null +++ b/spec/fixtures/sub-frames/frame-with-frame.html @@ -0,0 +1,13 @@ + + + + + + + Document + + + This is a frame, is has one child + + + \ No newline at end of file diff --git a/spec/fixtures/sub-frames/frame.html b/spec/fixtures/sub-frames/frame.html new file mode 100644 index 0000000000..4340b8d4ef --- /dev/null +++ b/spec/fixtures/sub-frames/frame.html @@ -0,0 +1,12 @@ + + + + + + + Document + + + This is a frame, it has no children + + \ No newline at end of file diff --git a/spec/fixtures/sub-frames/preload.js b/spec/fixtures/sub-frames/preload.js new file mode 100644 index 0000000000..3b6c461759 --- /dev/null +++ b/spec/fixtures/sub-frames/preload.js @@ -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) +})