'use strict' const chai = require('chai') const dirtyChai = require('dirty-chai') const fs = require('fs') const path = require('path') const os = require('os') const qs = require('querystring') const http = require('http') const { closeWindow } = require('./window-helpers') const { emittedOnce } = require('./events-helpers') const { createNetworkSandbox } = require('./network-helper') const { ipcRenderer, remote } = require('electron') const { app, ipcMain, BrowserWindow, BrowserView, protocol, session, screen, webContents } = remote const features = process.electronBinding('features') const { expect } = chai const isCI = remote.getGlobal('isCi') const nativeModulesEnabled = remote.getGlobal('nativeModulesEnabled') chai.use(dirtyChai) describe('BrowserWindow module', () => { const fixtures = path.resolve(__dirname, 'fixtures') let w = null let iw = null let ws = null let server let postData const defaultOptions = { show: false, width: 400, height: 400, webPreferences: { backgroundThrottling: false, nodeIntegration: true } } const openTheWindow = async (options = defaultOptions) => { // The `afterEach` hook isn't called if a test fails, // we should make sure that the window is closed ourselves. await closeTheWindow() w = new BrowserWindow(options) return w } const closeTheWindow = function () { return closeWindow(w).then(() => { w = null }) } before((done) => { const filePath = path.join(fixtures, 'pages', 'a.html') const fileStats = fs.statSync(filePath) postData = [ { type: 'rawData', bytes: Buffer.from('username=test&file=') }, { type: 'file', filePath: filePath, offset: 0, length: fileStats.size, modificationTime: fileStats.mtime.getTime() / 1000 } ] server = http.createServer((req, res) => { function respond () { if (req.method === 'POST') { let body = '' req.on('data', (data) => { if (data) body += data }) req.on('end', () => { const parsedData = qs.parse(body) fs.readFile(filePath, (err, data) => { if (err) return if (parsedData.username === 'test' && parsedData.file === data.toString()) { res.end() } }) }) } else if (req.url === '/302') { res.setHeader('Location', '/200') res.statusCode = 302 res.end() } else if (req.url === '/navigate-302') { res.end(``) } else if (req.url === '/cross-site') { res.end(`

${req.url}

`) } else { res.end() } } setTimeout(respond, req.url.includes('slow') ? 200 : 0) }) server.listen(0, '127.0.0.1', () => { server.url = `http://127.0.0.1:${server.address().port}` done() }) }) after(() => { server.close() server = null }) beforeEach(openTheWindow) afterEach(closeTheWindow) describe('"webPreferences" option', () => { afterEach(() => { ipcMain.removeAllListeners('answer') }) describe('"preload" option', () => { const doesNotLeakSpec = (name, webPrefs) => { it(name, async function () { w.destroy() w = new BrowserWindow({ webPreferences: { ...webPrefs, preload: path.resolve(fixtures, 'module', 'empty.js') }, show: false }) const leakResult = emittedOnce(ipcMain, 'leak-result') w.loadFile(path.join(fixtures, 'api', 'no-leak.html')) const [, result] = await leakResult expect(result).to.have.property('require', 'undefined') expect(result).to.have.property('exports', 'undefined') expect(result).to.have.property('windowExports', 'undefined') expect(result).to.have.property('windowPreload', 'undefined') expect(result).to.have.property('windowRequire', 'undefined') }) } doesNotLeakSpec('does not leak require', { nodeIntegration: false, sandbox: false, contextIsolation: false }) doesNotLeakSpec('does not leak require when sandbox is enabled', { nodeIntegration: false, sandbox: true, contextIsolation: false }) doesNotLeakSpec('does not leak require when context isolation is enabled', { nodeIntegration: false, sandbox: false, contextIsolation: true }) doesNotLeakSpec('does not leak require when context isolation and sandbox are enabled', { nodeIntegration: false, sandbox: true, contextIsolation: true }) it('loads the script before other scripts in window', async () => { const preload = path.join(fixtures, 'module', 'set-global.js') w.destroy() w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, preload } }) const p = emittedOnce(ipcMain, 'answer') w.loadFile(path.join(fixtures, 'api', 'preload.html')) const [, test] = await p expect(test).to.eql('preload') }) it('can successfully delete the Buffer global', async () => { const preload = path.join(fixtures, 'module', 'delete-buffer.js') w.destroy() w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, preload } }) const p = emittedOnce(ipcMain, 'answer') w.loadFile(path.join(fixtures, 'api', 'preload.html')) const [, test] = await p expect(test.toString()).to.eql('buffer') }) it('has synchronous access to all eventual window APIs', async () => { const preload = path.join(fixtures, 'module', 'access-blink-apis.js') const w = await openTheWindow({ show: false, webPreferences: { nodeIntegration: true, preload } }) const p = emittedOnce(ipcMain, 'answer') w.loadFile(path.join(fixtures, 'api', 'preload.html')) const [, test] = await p expect(test).to.be.an('object') expect(test.atPreload).to.be.an('array') expect(test.atLoad).to.be.an('array') expect(test.atPreload).to.deep.equal(test.atLoad, 'should have access to the same window APIs') }) }) describe('session preload scripts', function () { const preloads = [ path.join(fixtures, 'module', 'set-global-preload-1.js'), path.join(fixtures, 'module', 'set-global-preload-2.js'), path.relative(process.cwd(), path.join(fixtures, 'module', 'set-global-preload-3.js')) ] const defaultSession = session.defaultSession beforeEach(() => { expect(defaultSession.getPreloads()).to.deep.equal([]) defaultSession.setPreloads(preloads) }) afterEach(() => { defaultSession.setPreloads([]) }) it('can set multiple session preload script', function () { expect(defaultSession.getPreloads()).to.deep.equal(preloads) }) const generateSpecs = (description, sandbox) => { describe(description, () => { it('loads the script before other scripts in window including normal preloads', function (done) { ipcMain.once('vars', function (event, preload1, preload2, preload3) { expect(preload1).to.equal('preload-1') expect(preload2).to.equal('preload-1-2') expect(preload3).to.be.null() done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox, preload: path.join(fixtures, 'module', 'get-global-preload.js') } }) w.loadURL('about:blank') }) }) } generateSpecs('without sandbox', false) generateSpecs('with sandbox', true) }) describe('"additionalArguments" option', () => { it('adds extra args to process.argv in the renderer process', (done) => { const preload = path.join(fixtures, 'module', 'check-arguments.js') ipcMain.once('answer', (event, argv) => { expect(argv).to.include('--my-magic-arg') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, preload, additionalArguments: ['--my-magic-arg'] } }) w.loadFile(path.join(fixtures, 'api', 'blank.html')) }) it('adds extra value args to process.argv in the renderer process', (done) => { const preload = path.join(fixtures, 'module', 'check-arguments.js') ipcMain.once('answer', (event, argv) => { expect(argv).to.include('--my-magic-arg=foo') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, preload, additionalArguments: ['--my-magic-arg=foo'] } }) w.loadFile(path.join(fixtures, 'api', 'blank.html')) }) }) describe('"node-integration" option', () => { it('disables node integration by default', (done) => { const preload = path.join(fixtures, 'module', 'send-later.js') ipcMain.once('answer', (event, typeofProcess, typeofBuffer) => { expect(typeofProcess).to.equal('undefined') expect(typeofBuffer).to.equal('undefined') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload } }) w.loadFile(path.join(fixtures, 'api', 'blank.html')) }) }) describe('"enableRemoteModule" option', () => { const generateSpecs = (description, sandbox) => { describe(description, () => { const preload = path.join(fixtures, 'module', 'preload-remote.js') it('enables the remote module by default', async () => { const w = await openTheWindow({ show: false, webPreferences: { preload, sandbox } }) const p = emittedOnce(ipcMain, 'remote') w.loadFile(path.join(fixtures, 'api', 'blank.html')) const [, remote] = await p expect(remote).to.equal('object') }) it('disables the remote module when false', async () => { const w = await openTheWindow({ show: false, webPreferences: { preload, sandbox, enableRemoteModule: false } }) const p = emittedOnce(ipcMain, 'remote') w.loadFile(path.join(fixtures, 'api', 'blank.html')) const [, remote] = await p expect(remote).to.equal('undefined') }) }) } generateSpecs('without sandbox', false) generateSpecs('with sandbox', true) }) describe('"sandbox" option', () => { function waitForEvents (emitter, events, callback) { let count = events.length for (const event of events) { emitter.once(event, () => { if (!--count) callback() }) } } const preload = path.join(fixtures, 'module', 'preload-sandbox.js') it('exposes ipcRenderer to preload script', (done) => { ipcMain.once('answer', function (event, test) { expect(test).to.equal('preload') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload } }) w.loadFile(path.join(fixtures, 'api', 'preload.html')) }) it('exposes ipcRenderer to preload script (path has special chars)', function (done) { const preloadSpecialChars = path.join(fixtures, 'module', 'preload-sandboxæø åü.js') ipcMain.once('answer', function (event, test) { expect(test).to.equal('preload') done() }) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload: preloadSpecialChars } }) w.loadFile(path.join(fixtures, 'api', 'preload.html')) }) it('exposes "loaded" event to preload script', function (done) { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload } }) ipcMain.once('process-loaded', () => done()) w.loadURL('about:blank') }) it('exposes "exit" event to preload script', function (done) { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload } }) const htmlPath = path.join(fixtures, 'api', 'sandbox.html?exit-event') const pageUrl = 'file://' + htmlPath ipcMain.once('answer', function (event, url) { let expectedUrl = pageUrl if (process.platform === 'win32') { expectedUrl = 'file:///' + htmlPath.replace(/\\/g, '/') } expect(url).to.equal(expectedUrl) done() }) w.loadURL(pageUrl) }) it('should open windows in same domain with cross-scripting enabled', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload } }) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preload) const htmlPath = path.join(fixtures, 'api', 'sandbox.html?window-open') const pageUrl = 'file://' + htmlPath w.webContents.once('new-window', (e, url, frameName, disposition, options) => { let expectedUrl = pageUrl if (process.platform === 'win32') { expectedUrl = 'file:///' + htmlPath.replace(/\\/g, '/') } expect(url).to.equal(expectedUrl) expect(frameName).to.equal('popup!') expect(options.width).to.equal(500) expect(options.height).to.equal(600) ipcMain.once('answer', function (event, html) { expect(html).to.equal('

scripting from opener

') done() }) }) w.loadURL(pageUrl) }) it('should open windows in another domain with cross-scripting disabled', async () => { const w = await openTheWindow({ show: false, webPreferences: { sandbox: true, preload } }) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preload) const openerWindowOpen = emittedOnce(ipcMain, 'opener-loaded') w.loadFile( path.join(fixtures, 'api', 'sandbox.html'), { search: 'window-open-external' } ) // Wait for a message from the main window saying that it's ready. await openerWindowOpen // Ask the opener to open a popup with window.opener. const expectedPopupUrl = `${server.url}/cross-site` // Set in "sandbox.html". const browserWindowCreated = emittedOnce(app, 'browser-window-created') w.webContents.send('open-the-popup', expectedPopupUrl) // The page is going to open a popup that it won't be able to close. // We have to close it from here later. // XXX(alexeykuzmin): It will leak if the test fails too soon. const [, popupWindow] = await browserWindowCreated // Ask the popup window for details. const detailsAnswer = emittedOnce(ipcMain, 'child-loaded') popupWindow.webContents.send('provide-details') const [, openerIsNull, , locationHref] = await detailsAnswer expect(openerIsNull).to.be.false('window.opener is null') expect(locationHref).to.equal(expectedPopupUrl) // Ask the page to access the popup. const touchPopupResult = emittedOnce(ipcMain, 'answer') w.webContents.send('touch-the-popup') const [, popupAccessMessage] = await touchPopupResult // Ask the popup to access the opener. const touchOpenerResult = emittedOnce(ipcMain, 'answer') popupWindow.webContents.send('touch-the-opener') const [, openerAccessMessage] = await touchOpenerResult // We don't need the popup anymore, and its parent page can't close it, // so let's close it from here before we run any checks. await closeWindow(popupWindow, { assertSingleWindow: false }) expect(popupAccessMessage).to.be.a('string', `child's .document is accessible from its parent window`) expect(popupAccessMessage).to.match(/^Blocked a frame with origin/) expect(openerAccessMessage).to.be.a('string', `opener .document is accessible from a popup window`) expect(openerAccessMessage).to.match(/^Blocked a frame with origin/) }) it('should inherit the sandbox setting in opened windows', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }) const preloadPath = path.join(fixtures, 'api', 'new-window-preload.js') ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preloadPath) ipcMain.once('answer', (event, args) => { expect(args).to.include('--enable-sandbox') done() }) w.loadFile(path.join(fixtures, 'api', 'new-window.html')) }) it('should open windows with the options configured via new-window event listeners', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }) const preloadPath = path.join(fixtures, 'api', 'new-window-preload.js') ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preloadPath) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'foo', 'bar') ipcMain.once('answer', (event, args, webPreferences) => { expect(webPreferences.foo).to.equal('bar') done() }) w.loadFile(path.join(fixtures, 'api', 'new-window.html')) }) it('should set ipc event sender correctly', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload } }) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preload) let childWc w.webContents.once('new-window', (e, url, frameName, disposition, options) => { childWc = options.webContents expect(w.webContents).to.not.equal(childWc) }) ipcMain.once('parent-ready', function (event) { expect(w.webContents).to.equal(event.sender) event.sender.send('verified') }) ipcMain.once('child-ready', function (event) { expect(childWc).to.be.an('object') expect(childWc).to.equal(event.sender) event.sender.send('verified') }) waitForEvents(ipcMain, [ 'parent-answer', 'child-answer' ], done) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'verify-ipc-sender' }) }) describe('event handling', () => { it('works for window events', (done) => { waitForEvents(w, [ 'page-title-updated' ], done) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'window-events' }) }) it('works for stop events', (done) => { waitForEvents(w.webContents, [ 'did-navigate', 'did-fail-load', 'did-stop-loading' ], done) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'webcontents-stop' }) }) it('works for web contents events', (done) => { waitForEvents(w.webContents, [ 'did-finish-load', 'did-frame-finish-load', 'did-navigate-in-page', 'will-navigate', 'did-start-loading', 'did-stop-loading', 'did-frame-finish-load', 'dom-ready' ], done) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'webcontents-events' }) }) }) it('supports calling preventDefault on new-window events', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }) const initialWebContents = webContents.getAllWebContents().map((i) => i.id) ipcRenderer.send('prevent-next-new-window', w.webContents.id) w.webContents.once('new-window', () => { // We need to give it some time so the windows get properly disposed (at least on OSX). setTimeout(() => { const currentWebContents = webContents.getAllWebContents().map((i) => i.id) expect(currentWebContents).to.deep.equal(initialWebContents) done() }, 100) }) w.loadFile(path.join(fixtures, 'pages', 'window-open.html')) }) // see #9387 it('properly manages remote object references after page reload', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload, sandbox: true } }) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'reload-remote' }) ipcMain.on('get-remote-module-path', (event) => { event.returnValue = path.join(fixtures, 'module', 'hello.js') }) let reload = false ipcMain.on('reloaded', (event) => { event.returnValue = reload reload = !reload }) ipcMain.once('reload', (event) => { event.sender.reload() }) ipcMain.once('answer', (event, arg) => { ipcMain.removeAllListeners('reloaded') ipcMain.removeAllListeners('get-remote-module-path') expect(arg).to.equal('hi') done() }) }) it('properly manages remote object references after page reload in child window', (done) => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { preload, sandbox: true } }) ipcRenderer.send('set-web-preferences-on-next-new-window', w.webContents.id, 'preload', preload) w.loadFile(path.join(fixtures, 'api', 'sandbox.html'), { search: 'reload-remote-child' }) ipcMain.on('get-remote-module-path', (event) => { event.returnValue = path.join(fixtures, 'module', 'hello-child.js') }) let reload = false ipcMain.on('reloaded', (event) => { event.returnValue = reload reload = !reload }) ipcMain.once('reload', (event) => { event.sender.reload() }) ipcMain.once('answer', (event, arg) => { ipcMain.removeAllListeners('reloaded') ipcMain.removeAllListeners('get-remote-module-path') expect(arg).to.equal('hi child window') done() }) }) it('validates process APIs access in sandboxed renderer', (done) => { ipcMain.once('answer', function (event, test) { expect(test.hasCrash).to.be.true() expect(test.hasHang).to.be.true() expect(test.heapStatistics).to.be.an('object') expect(test.blinkMemoryInfo).to.be.an('object') expect(test.processMemoryInfo).to.be.an('object') expect(test.systemVersion).to.be.a('string') expect(test.cpuUsage).to.be.an('object') expect(test.ioCounters).to.be.an('object') expect(test.arch).to.equal(remote.process.arch) expect(test.platform).to.equal(remote.process.platform) expect(test.env).to.deep.equal(remote.process.env) expect(test.execPath).to.equal(remote.process.helperExecPath) expect(test.sandboxed).to.be.true() expect(test.type).to.equal('renderer') expect(test.version).to.equal(remote.process.version) expect(test.versions).to.deep.equal(remote.process.versions) if (process.platform === 'linux' && test.osSandbox) { expect(test.creationTime).to.be.null() expect(test.systemMemoryInfo).to.be.null() } else { expect(test.creationTime).to.be.a('number') expect(test.systemMemoryInfo).to.be.an('object') } done() }) remote.process.env.sandboxmain = 'foo' w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload } }) w.webContents.once('preload-error', (event, preloadPath, error) => { done(error) }) w.loadFile(path.join(fixtures, 'api', 'preload.html')) }) it('webview in sandbox renderer', async () => { w.destroy() w = new BrowserWindow({ show: false, webPreferences: { sandbox: true, preload, webviewTag: true } }) const didAttachWebview = emittedOnce(w.webContents, 'did-attach-webview') const webviewDomReady = emittedOnce(ipcMain, 'webview-dom-ready') w.loadFile(path.join(fixtures, 'pages', 'webview-did-attach-event.html')) const [, webContents] = await didAttachWebview const [, id] = await webviewDomReady expect(webContents.id).to.equal(id) }) }) describe('nativeWindowOpen option', () => { const networkSandbox = createNetworkSandbox(protocol) beforeEach(async () => { // used to create cross-origin navigation situations await networkSandbox.serveFileFromProtocol('foo', path.join(fixtures, 'api', 'window-open-location-change.html')) await networkSandbox.serveFileFromProtocol('bar', path.join(fixtures, 'api', 'window-open-location-final.html')) w.destroy() w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, nativeWindowOpen: true, // tests relies on preloads in opened windows nodeIntegrationInSubFrames: true } }) }) afterEach(async () => { await networkSandbox.reset() }) it('opens window of about:blank with cross-scripting enabled', (done) => { ipcMain.once('answer', (event, content) => { expect(content).to.equal('Hello') done() }) w.loadFile(path.join(fixtures, 'api', 'native-window-open-blank.html')) }) it('opens window of same domain with cross-scripting enabled', (done) => { ipcMain.once('answer', (event, content) => { expect(content).to.equal('Hello') done() }) w.loadFile(path.join(fixtures, 'api', 'native-window-open-file.html')) }) it('blocks accessing cross-origin frames', (done) => { ipcMain.once('answer', (event, content) => { expect(content).to.equal('Blocked a frame with origin "file://" from accessing a cross-origin frame.') done() }) w.loadFile(path.join(fixtures, 'api', 'native-window-open-cross-origin.html')) }) it('opens window from