diff --git a/atom/browser/api/event_emitter.cc b/atom/browser/api/event_emitter.cc index 4558514d9fe56..b3cdb21d13027 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 80fbac89a91f8..080ce6bd22c16 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 d1a5d0c7499d4..fac25860de8fb 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 b2bdb8339e343..03d8e5548c8ac 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 09d36b80ccec7..a3c99841a56d4 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 a8e9f3c3499eb..e8e5b8be69d22 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 96ebadfb7a6c0..9bf99db1f1c95 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 ac543fbe62017..11c5150aab477 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 90b85ecde8b87..b3b6f3fcab2b7 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 f4439fe0a6eda..ec6a712c847c9 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 9513c15cf5483..4f92c7737bca2 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 b0a206cd31b8a..a0d51f63115b1 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 8d9d9623309a7..adbe7e5710d5a 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 3f4561f8d9de3..5c4e04bcb131d 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 fa10803079d21..bf34199b218ed 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 5c0c400248300..920b02021ae65 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 ffc1bf99dabba..84ac8ef448392 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 43fa45b74ac50..cd2ffc57389b9 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 3be08d9177f27..b4801a88681de 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 0000000000000..dc31a370b3da5 --- /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 39f53c36db14a..64a4fba447eb3 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 0000000000000..f731555a5ddaf --- /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 0000000000000..823fb1aafe9e7 --- /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 0000000000000..9d99fef71b332 --- /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 0000000000000..4340b8d4efce5 --- /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 0000000000000..3b6c461759b25 --- /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) +})