diff --git a/appasar/canary/app_bootstrap/splashScreen.js b/appasar/canary/app_bootstrap/splashScreen.js index e068dac..fc4b15a 100644 --- a/appasar/canary/app_bootstrap/splashScreen.js +++ b/appasar/canary/app_bootstrap/splashScreen.js @@ -212,7 +212,10 @@ function launchSplashWindow(startMinimized) { frame: false, resizable: false, center: true, - show: false + show: false, + webPreferences: { + nodeIntegration: true + } }; splashWindow = new _electron.BrowserWindow(windowConfig); diff --git a/electronasar/canary/browser/api/app.js b/electronasar/canary/browser/api/app.js index 4cef536..8c51a02 100644 --- a/electronasar/canary/browser/api/app.js +++ b/electronasar/canary/browser/api/app.js @@ -1,6 +1,7 @@ 'use strict' const bindings = process.atomBinding('app') +const commandLine = process.atomBinding('command_line') const path = require('path') const { app, App } = bindings @@ -25,24 +26,21 @@ Object.assign(app, { return Menu.getApplicationMenu() }, commandLine: { - appendSwitch (...args) { - const castedArgs = args.map((arg) => { - return typeof arg !== 'string' ? `${arg}` : arg - }) - return bindings.appendSwitch(...castedArgs) - }, - appendArgument (...args) { - const castedArgs = args.map((arg) => { - return typeof arg !== 'string' ? `${arg}` : arg - }) - return bindings.appendArgument(...castedArgs) - } + hasSwitch: (...args) => commandLine.hasSwitch(...args.map(String)), + getSwitchValue: (...args) => commandLine.getSwitchValue(...args.map(String)), + appendSwitch: (...args) => commandLine.appendSwitch(...args.map(String)), + appendArgument: (...args) => commandLine.appendArgument(...args.map(String)) + }, + enableMixedSandbox () { + deprecate.log(`'enableMixedSandbox' is deprecated. Mixed-sandbox mode is now enabled by default. You can safely remove the call to enableMixedSandbox().`) } }) -const nativeFn = app.getAppMetrics +app.getFileIcon = deprecate.promisify(app.getFileIcon) + +const nativeAppMetrics = app.getAppMetrics app.getAppMetrics = () => { - const metrics = nativeFn.call(app) + const metrics = nativeAppMetrics.call(app) for (const metric of metrics) { if ('memory' in metric) { deprecate.removeProperty(metric, 'memory') @@ -93,9 +91,7 @@ if (process.platform === 'linux') { } app.allowNTLMCredentialsForAllDomains = function (allow) { - if (!process.noDeprecations) { - deprecate.warn('app.allowNTLMCredentialsForAllDomains', 'session.allowNTLMCredentialsForDomains') - } + deprecate.warn('app.allowNTLMCredentialsForAllDomains', 'session.allowNTLMCredentialsForDomains') const domains = allow ? '*' : '' if (!this.isReady()) { this.commandLine.appendSwitch('auth-server-whitelist', domains) diff --git a/electronasar/canary/browser/api/browser-window.js b/electronasar/canary/browser/api/browser-window.js index 7fdbf8e..3694acd 100644 --- a/electronasar/canary/browser/api/browser-window.js +++ b/electronasar/canary/browser/api/browser-window.js @@ -3,8 +3,6 @@ const electron = require('electron') const { WebContentsView, TopLevelWindow } = electron const { BrowserWindow } = process.atomBinding('window') -const v8Util = process.atomBinding('v8_util') -const ipcMain = require('@electron/internal/browser/ipc-main-internal') Object.setPrototypeOf(BrowserWindow.prototype, TopLevelWindow.prototype) @@ -27,69 +25,6 @@ BrowserWindow.prototype._init = function () { nativeSetBounds.call(this, bounds, ...opts) } - // Make new windows requested by links behave like "window.open" - this.webContents.on('-new-window', (event, url, frameName, disposition, - additionalFeatures, postData, - referrer) => { - const options = { - show: true, - width: 800, - height: 600 - } - ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', - event, url, referrer, frameName, disposition, - options, additionalFeatures, postData) - }) - - this.webContents.on('-web-contents-created', (event, webContents, url, - frameName) => { - v8Util.setHiddenValue(webContents, 'url-framename', { url, frameName }) - }) - - // Create a new browser window for the native implementation of - // "window.open", used in sandbox and nativeWindowOpen mode - this.webContents.on('-add-new-contents', (event, webContents, disposition, - userGesture, left, top, width, - height) => { - const urlFrameName = v8Util.getHiddenValue(webContents, 'url-framename') - if ((disposition !== 'foreground-tab' && disposition !== 'new-window' && - disposition !== 'background-tab') || !urlFrameName) { - event.preventDefault() - return - } - - if (webContents.getLastWebPreferences().nodeIntegration === true) { - const message = - 'Enabling Node.js integration in child windows opened with the ' + - '"nativeWindowOpen" option will cause memory leaks, please turn off ' + - 'the "nodeIntegration" option.\\n' + - 'From 5.x child windows opened with the "nativeWindowOpen" option ' + - 'will always have Node.js integration disabled.\\n' + - 'See https://github.com/electron/electron/pull/15076 for more.' - // console is only available after DOM is created. - const printWarning = () => this.webContents.executeJavaScript(`console.warn('${message}')`) - if (this.webContents.isDomReady()) { - printWarning() - } else { - this.webContents.once('dom-ready', printWarning) - } - } - - const { url, frameName } = urlFrameName - v8Util.deleteHiddenValue(webContents, 'url-framename') - const options = { - show: true, - x: left, - y: top, - width: width || 800, - height: height || 600, - webContents: webContents - } - const referrer = { url: '', policy: 'default' } - ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', - event, url, referrer, frameName, disposition, options) - }) - // window.resizeTo(...) // window.moveTo(...) this.webContents.on('move', (event, size) => { diff --git a/electronasar/canary/browser/api/content-tracing.js b/electronasar/canary/browser/api/content-tracing.js index 81bd70f..9a4f480 100644 --- a/electronasar/canary/browser/api/content-tracing.js +++ b/electronasar/canary/browser/api/content-tracing.js @@ -1,3 +1,9 @@ 'use strict' +const { deprecate } = require('electron') +const contentTracing = process.atomBinding('content_tracing') -module.exports = process.atomBinding('content_tracing') +contentTracing.startRecording = deprecate.promisify(contentTracing.startRecording) +contentTracing.stopRecording = deprecate.promisify(contentTracing.stopRecording) +contentTracing.getCategories = deprecate.promisify(contentTracing.getCategories) + +module.exports = contentTracing diff --git a/electronasar/canary/browser/api/menu-item-roles.js b/electronasar/canary/browser/api/menu-item-roles.js index 61c6226..22949b3 100644 --- a/electronasar/canary/browser/api/menu-item-roles.js +++ b/electronasar/canary/browser/api/menu-item-roles.js @@ -2,14 +2,18 @@ const { app } = require('electron') +const isMac = process.platform === 'darwin' +const isWindows = process.platform === 'win32' +const isLinux = process.platform === 'linux' + const roles = { about: { get label () { - return process.platform === 'linux' ? 'About' : `About ${app.getName()}` + return isLinux ? 'About' : `About ${app.getName()}` } }, close: { - label: process.platform === 'darwin' ? 'Close Window' : 'Close', + label: isMac ? 'Close Window' : 'Close', accelerator: 'CommandOrControl+W', windowMethod: 'close' }, @@ -78,12 +82,12 @@ const roles = { default: return 'Quit' } }, - accelerator: process.platform === 'win32' ? null : 'CommandOrControl+Q', + accelerator: isWindows ? null : 'CommandOrControl+Q', appMethod: 'quit' }, redo: { label: 'Redo', - accelerator: process.platform === 'win32' ? 'Control+Y' : 'Shift+CommandOrControl+Z', + accelerator: isWindows ? 'Control+Y' : 'Shift+CommandOrControl+Z', webContentsMethod: 'redo' }, reload: { @@ -122,13 +126,13 @@ const roles = { }, toggledevtools: { label: 'Toggle Developer Tools', - accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', + accelerator: isMac ? 'Alt+Command+I' : 'Ctrl+Shift+I', nonNativeMacOSRole: true, windowMethod: 'toggleDevTools' }, togglefullscreen: { label: 'Toggle Full Screen', - accelerator: process.platform === 'darwin' ? 'Control+Command+F' : 'F11', + accelerator: isMac ? 'Control+Command+F' : 'F11', windowMethod: (window) => { window.setFullScreen(!window.isFullScreen()) } @@ -152,9 +156,8 @@ const roles = { accelerator: 'CommandOrControl+Plus', nonNativeMacOSRole: true, webContentsMethod: (webContents) => { - webContents.getZoomLevel((zoomLevel) => { - webContents.setZoomLevel(zoomLevel + 0.5) - }) + const zoomLevel = webContents.getZoomLevel() + webContents.setZoomLevel(zoomLevel + 0.5) } }, zoomout: { @@ -162,78 +165,97 @@ const roles = { accelerator: 'CommandOrControl+-', nonNativeMacOSRole: true, webContentsMethod: (webContents) => { - webContents.getZoomLevel((zoomLevel) => { - webContents.setZoomLevel(zoomLevel - 0.5) - }) + const zoomLevel = webContents.getZoomLevel() + webContents.setZoomLevel(zoomLevel - 0.5) } }, - // Edit submenu (should fit both Mac & Windows) + // App submenu should be used for Mac only + appmenu: { + get label () { + return app.getName() + }, + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' } + ] + }, + // File submenu + filemenu: { + label: 'File', + submenu: [ + isMac ? { role: 'close' } : { role: 'quit' } + ] + }, + // Edit submenu editmenu: { label: 'Edit', submenu: [ - { - role: 'undo' - }, - { - role: 'redo' - }, - { - type: 'separator' - }, - { - role: 'cut' - }, - { - role: 'copy' - }, - { - role: 'paste' - }, - - process.platform === 'darwin' ? { - role: 'pasteAndMatchStyle' - } : null, - - { - role: 'delete' - }, - - process.platform === 'win32' ? { - type: 'separator' - } : null, - - { - role: 'selectAll' - } + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, + ...(isMac ? [ + { role: 'pasteAndMatchStyle' }, + { role: 'delete' }, + { role: 'selectAll' }, + { type: 'separator' }, + { + label: 'Speech', + submenu: [ + { role: 'startspeaking' }, + { role: 'stopspeaking' } + ] + } + ] : [ + { role: 'delete' }, + { type: 'separator' }, + { role: 'selectAll' } + ]) ] }, - - // Window submenu should be used for Mac only + // View submenu + viewmenu: { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forcereload' }, + { role: 'toggledevtools' }, + { type: 'separator' }, + { role: 'resetzoom' }, + { role: 'zoomin' }, + { role: 'zoomout' }, + { type: 'separator' }, + { role: 'togglefullscreen' } + ] + }, + // Window submenu windowmenu: { label: 'Window', submenu: [ - { - role: 'minimize' - }, - { - role: 'close' - }, - - process.platform === 'darwin' ? { - type: 'separator' - } : null, - - process.platform === 'darwin' ? { - role: 'front' - } : null - + { role: 'minimize' }, + { role: 'zoom' }, + ...(isMac ? [ + { type: 'separator' }, + { role: 'front' } + ] : [ + { role: 'close' } + ]) ] } } const canExecuteRole = (role) => { if (!roles.hasOwnProperty(role)) return false - if (process.platform !== 'darwin') return true + if (!isMac) return true // macOS handles all roles natively except for a few return roles[role].nonNativeMacOSRole @@ -248,7 +270,8 @@ exports.getDefaultAccelerator = (role) => { } exports.shouldRegisterAccelerator = (role) => { - return roles.hasOwnProperty(role) ? roles[role].registerAccelerator : true + const hasRoleRegister = roles.hasOwnProperty(role) && roles[role].registerAccelerator !== undefined + return hasRoleRegister ? roles[role].registerAccelerator : true } exports.getDefaultSubmenu = (role) => { diff --git a/electronasar/canary/browser/api/menu.js b/electronasar/canary/browser/api/menu.js index 14fcaba..2a97753 100644 --- a/electronasar/canary/browser/api/menu.js +++ b/electronasar/canary/browser/api/menu.js @@ -145,6 +145,8 @@ Menu.setApplicationMenu = function (menu) { } applicationMenu = menu + v8Util.setHiddenValue(global, 'applicationMenuSet', true) + if (process.platform === 'darwin') { if (!menu) return menu._callMenuWillShow() @@ -159,13 +161,13 @@ Menu.buildFromTemplate = function (template) { if (!Array.isArray(template)) { throw new TypeError('Invalid template for Menu: Menu template must be an array') } - const menu = new Menu() if (!areValidTemplateItems(template)) { throw new TypeError('Invalid template for MenuItem: must have at least one of label, role or type') } const filtered = removeExtraSeparators(template) const sorted = sortTemplate(filtered) + const menu = new Menu() sorted.forEach((item) => menu.append(new MenuItem(item))) return menu diff --git a/electronasar/canary/browser/api/module-list.js b/electronasar/canary/browser/api/module-list.js index 8ec7bda..8547166 100644 --- a/electronasar/canary/browser/api/module-list.js +++ b/electronasar/canary/browser/api/module-list.js @@ -30,9 +30,7 @@ module.exports = [ { name: 'Tray', file: 'tray' }, { name: 'View', file: 'view' }, { name: 'webContents', file: 'web-contents' }, - { name: 'WebContentsView', file: 'web-contents-view' }, - // The internal modules, invisible unless you know their names. - { name: 'NavigationController', file: 'navigation-controller', private: true } + { name: 'WebContentsView', file: 'web-contents-view' } ] if (features.isViewApiEnabled()) { @@ -41,6 +39,8 @@ if (features.isViewApiEnabled()) { { name: 'Button', file: 'views/button' }, { name: 'LabelButton', file: 'views/label-button' }, { name: 'LayoutManager', file: 'views/layout-manager' }, + { name: 'MdTextButton', file: 'views/md-text-button' }, + { name: 'ResizeArea', file: 'views/resize-area' }, { name: 'TextField', file: 'views/text-field' } ) } diff --git a/electronasar/canary/browser/api/session.js b/electronasar/canary/browser/api/session.js index 08c10ee..96362cc 100644 --- a/electronasar/canary/browser/api/session.js +++ b/electronasar/canary/browser/api/session.js @@ -1,8 +1,23 @@ 'use strict' const { EventEmitter } = require('events') -const { app } = require('electron') -const { fromPartition, Session, Cookies } = process.atomBinding('session') +const { app, deprecate } = require('electron') +const { Session, Cookies } = process.atomBinding('session') +const realFromPartition = process.atomBinding('session').fromPartition + +const wrappedSymbol = Symbol('wrapped-deprecate') +const fromPartition = (partition) => { + const session = realFromPartition(partition) + if (!session[wrappedSymbol]) { + session[wrappedSymbol] = true + const { cookies } = session + cookies.flushStore = deprecate.promisify(cookies.flushStore) + cookies.get = deprecate.promisify(cookies.get) + cookies.remove = deprecate.promisify(cookies.remove) + cookies.set = deprecate.promisify(cookies.set) + } + return session +} // Public API. Object.defineProperties(exports, { @@ -20,5 +35,6 @@ Object.setPrototypeOf(Session.prototype, EventEmitter.prototype) Object.setPrototypeOf(Cookies.prototype, EventEmitter.prototype) Session.prototype._init = function () { + this.protocol.isProtocolHandled = deprecate.promisify(this.protocol.isProtocolHandled) app.emit('session-created', this) } diff --git a/electronasar/canary/browser/api/touch-bar.js b/electronasar/canary/browser/api/touch-bar.js index ddacc73..183a1e0 100644 --- a/electronasar/canary/browser/api/touch-bar.js +++ b/electronasar/canary/browser/api/touch-bar.js @@ -31,12 +31,6 @@ class TouchBar extends EventEmitter { let { items, escapeItem } = options - // FIXME Support array as first argument, remove in 2.0 - if (Array.isArray(options)) { - items = options - escapeItem = null - } - if (!Array.isArray(items)) { items = [] } diff --git a/electronasar/canary/browser/api/web-contents.js b/electronasar/canary/browser/api/web-contents.js index c5d923e..82b1629 100644 --- a/electronasar/canary/browser/api/web-contents.js +++ b/electronasar/canary/browser/api/web-contents.js @@ -5,8 +5,9 @@ const { EventEmitter } = require('events') const electron = require('electron') const path = require('path') const url = require('url') -const { app, ipcMain, session, NavigationController, deprecate } = electron +const { app, ipcMain, session, deprecate } = electron +const NavigationController = require('@electron/internal/browser/navigation-controller') const ipcMainInternal = require('@electron/internal/browser/ipc-main-internal') const errorUtils = require('@electron/internal/common/error-utils') @@ -110,6 +111,7 @@ WebContents.prototype.send = function (channel, ...args) { return this._send(internal, sendToAll, channel, args) } + WebContents.prototype.sendToAll = function (channel, ...args) { if (typeof channel !== 'string') { throw new Error('Missing required channel argument') @@ -141,6 +143,30 @@ 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') + } else if (typeof frameId !== 'number') { + throw new Error('Missing required frameId argument') + } + + const internal = true + const sendToAll = false + + return this._sendToFrame(internal, sendToAll, frameId, channel, args) +} // Following methods are mapped to webFrame. const webFrameMethods = [ @@ -196,10 +222,34 @@ WebContents.prototype.executeJavaScript = function (code, hasUserGesture, callba } } +// TODO(codebytere): remove when promisifications is complete +const nativeZoomLevel = WebContents.prototype.getZoomLevel +WebContents.prototype.getZoomLevel = function (callback) { + if (callback == null) { + return nativeZoomLevel.call(this) + } else { + process.nextTick(() => { + callback(nativeZoomLevel.call(this)) + }) + } +} + +// TODO(codebytere): remove when promisifications is complete +const nativeZoomFactor = WebContents.prototype.getZoomFactor +WebContents.prototype.getZoomFactor = function (callback) { + if (callback == null) { + return nativeZoomFactor.call(this) + } else { + process.nextTick(() => { + callback(nativeZoomFactor.call(this)) + }) + } +} + WebContents.prototype.takeHeapSnapshot = function (filePath) { return new Promise((resolve, reject) => { const channel = `ELECTRON_TAKE_HEAP_SNAPSHOT_RESULT_${getNextId()}` - ipcMain.once(channel, (event, success) => { + ipcMainInternal.once(channel, (event, success) => { if (success) { resolve() } else { @@ -207,7 +257,7 @@ WebContents.prototype.takeHeapSnapshot = function (filePath) { } }) if (!this._takeHeapSnapshot(filePath, channel)) { - ipcMain.emit(channel, false) + ipcMainInternal.emit(channel, false) } }) } @@ -276,16 +326,6 @@ WebContents.prototype.getPrinters = function () { } } -WebContents.prototype.getZoomLevel = function (callback) { - if (typeof callback !== 'function') { - throw new Error('Must pass function as an argument') - } - process.nextTick(() => { - const zoomLevel = this._getZoomLevel() - callback(zoomLevel) - }) -} - WebContents.prototype.loadFile = function (filePath, options = {}) { if (typeof filePath !== 'string') { throw new Error('Must pass filePath as a string') @@ -302,22 +342,31 @@ WebContents.prototype.loadFile = function (filePath, options = {}) { })) } -WebContents.prototype.getZoomFactor = function (callback) { - if (typeof callback !== 'function') { - throw new Error('Must pass function as an argument') +const addReplyToEvent = (event) => { + event.reply = (...args) => { + event.sender.sendToFrame(event.frameId, ...args) } - process.nextTick(() => { - const zoomFactor = this._getZoomFactor() - callback(zoomFactor) +} + +const addReplyInternalToEvent = (event) => { + Object.defineProperty(event, '_replyInternal', { + configurable: false, + enumerable: false, + value: (...args) => { + event.sender._sendToFrameInternal(event.frameId, ...args) + } }) } -WebContents.prototype.findInPage = function (text, options = {}) { - // TODO (nitsakh): Remove in 5.0 - if (options.wordStart != null || options.medialCapitalAtWordStart != null) { - deprecate.log('wordStart and medialCapitalAtWordStart options are deprecated') - } - return this._findInPage(text, options) +const safeProtocols = new Set([ + 'chrome-devtools:', + 'chrome-extension:' +]) + +const isWebContentsTrusted = function (contents) { + const pageURL = contents._getURL() + const { protocol } = url.parse(pageURL) + return safeProtocols.has(protocol) } // Add JavaScript wrappers for WebContents class. @@ -325,27 +374,38 @@ WebContents.prototype._init = function () { // The navigation controller. NavigationController.call(this, this) - // Every remote callback from renderer process would add a listenter to the - // render-view-deleted event, so ignore the listenters warning. + // Every remote callback from renderer process would add a listener to the + // render-view-deleted event, so ignore the listeners warning. this.setMaxListeners(0) + this.capturePage = deprecate.promisify(this.capturePage) + this.hasServiceWorker = deprecate.function(this.hasServiceWorker) + this.unregisterServiceWorker = deprecate.function(this.unregisterServiceWorker) + // 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) ipcMain.emit(channel, event, ...args) }) - this.on('ipc-message-sync', function (event, [channel, ...args]) { + + this.on('-ipc-message-sync', function (event, [channel, ...args]) { Object.defineProperty(event, 'returnValue', { set: function (value) { return event.sendReply([value]) }, 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) }) + this.on('ipc-internal-message-sync', function (event, [channel, ...args]) { Object.defineProperty(event, 'returnValue', { set: function (value) { @@ -353,6 +413,7 @@ WebContents.prototype._init = function () { }, get: function () {} }) + addReplyInternalToEvent(event) ipcMainInternal.emit(channel, event, ...args) }) @@ -368,13 +429,23 @@ WebContents.prototype._init = function () { }) }) - this.on('remote-require', (event, ...args) => { - app.emit('remote-require', event, this, ...args) - }) + const forwardedEvents = [ + 'desktop-capturer-get-sources', + 'remote-require', + 'remote-get-global', + 'remote-get-builtin', + 'remote-get-current-window', + 'remote-get-current-web-contents', + 'remote-get-guest-web-contents' + ] - this.on('remote-get-global', (event, ...args) => { - app.emit('remote-get-global', event, this, ...args) - }) + for (const eventName of forwardedEvents) { + this.on(eventName, (event, ...args) => { + if (!isWebContentsTrusted(event.sender)) { + app.emit(eventName, event, this, ...args) + } + }) + } deprecate.event(this, 'did-get-response-details', '-did-get-response-details') deprecate.event(this, 'did-get-redirect-request', '-did-get-redirect-request') @@ -384,6 +455,46 @@ WebContents.prototype._init = function () { this.reload() }) + // Handle window.open for BrowserWindow and BrowserView. + if (['browserView', 'window'].includes(this.getType())) { + // Make new windows requested by links behave like "window.open". + this.on('-new-window', (event, url, frameName, disposition, + additionalFeatures, postData, + referrer) => { + const options = { + show: true, + width: 800, + height: 600 + } + ipcMainInternal.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', + event, url, referrer, frameName, disposition, + options, additionalFeatures, postData) + }) + + // Create a new browser window for the native implementation of + // "window.open", used in sandbox and nativeWindowOpen mode. + this.on('-add-new-contents', (event, webContents, disposition, + userGesture, left, top, width, height, url, frameName) => { + if ((disposition !== 'foreground-tab' && disposition !== 'new-window' && + disposition !== 'background-tab')) { + event.preventDefault() + return + } + + const options = { + show: true, + x: left, + y: top, + width: width || 800, + height: height || 600, + webContents + } + const referrer = { url: '', policy: 'default' } + ipcMainInternal.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', + event, url, referrer, frameName, disposition, options) + }) + } + app.emit('web-contents-created', {}, this) } diff --git a/electronasar/canary/browser/chrome-extension.js b/electronasar/canary/browser/chrome-extension.js index 627487a..5c4e04b 100644 --- a/electronasar/canary/browser/chrome-extension.js +++ b/electronasar/canary/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,12 +196,23 @@ 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++ }) +const isChromeExtension = function (pageURL) { + const { protocol } = url.parse(pageURL) + return protocol === 'chrome-extension:' +} + ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, tabId, extensionId, details) { + const pageURL = event.sender._getURL() + if (!isChromeExtension(pageURL)) { + console.error(`Blocked ${pageURL} from calling chrome.tabs.executeScript()`) + return + } + const contents = webContents.fromId(tabId) if (!contents) { console.error(`Sending message to unknown tab ${tabId}`) diff --git a/electronasar/canary/browser/default-menu.js b/electronasar/canary/browser/default-menu.js new file mode 100644 index 0000000..09ed05b --- /dev/null +++ b/electronasar/canary/browser/default-menu.js @@ -0,0 +1,58 @@ +'use strict' + +const { shell, Menu } = require('electron') +const v8Util = process.atomBinding('v8_util') + +const isMac = process.platform === 'darwin' + +const setDefaultApplicationMenu = () => { + if (v8Util.getHiddenValue(global, 'applicationMenuSet')) return + + const helpMenu = { + role: 'help', + submenu: [ + { + label: 'Learn More', + click () { + shell.openExternalSync('https://electronjs.org') + } + }, + { + label: 'Documentation', + click () { + shell.openExternalSync( + `https://github.com/electron/electron/tree/v${process.versions.electron}/docs#readme` + ) + } + }, + { + label: 'Community Discussions', + click () { + shell.openExternalSync('https://discuss.atom.io/c/electron') + } + }, + { + label: 'Search Issues', + click () { + shell.openExternalSync('https://github.com/electron/electron/issues') + } + } + ] + } + + const template = [ + ...(isMac ? [{ role: 'appMenu' }] : []), + { role: 'fileMenu' }, + { role: 'editMenu' }, + { role: 'viewMenu' }, + { role: 'windowMenu' }, + helpMenu + ] + + const menu = Menu.buildFromTemplate(template) + Menu.setApplicationMenu(menu) +} + +module.exports = { + setDefaultApplicationMenu +} diff --git a/electronasar/canary/browser/desktop-capturer.js b/electronasar/canary/browser/desktop-capturer.js index 62e68bb..bf34199 100644 --- a/electronasar/canary/browser/desktop-capturer.js +++ b/electronasar/canary/browser/desktop-capturer.js @@ -1,7 +1,9 @@ 'use strict' const ipcMain = require('@electron/internal/browser/ipc-main-internal') + const { desktopCapturer } = process.atomBinding('desktop_capturer') +const eventBinding = process.atomBinding('event') const deepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b) @@ -11,32 +13,40 @@ let requestsQueue = [] const electronSources = 'ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES' const capturerResult = (id) => `ELECTRON_RENDERER_DESKTOP_CAPTURER_RESULT_${id}` -ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize, id) => { +ipcMain.on(electronSources, (event, captureWindow, captureScreen, thumbnailSize, fetchWindowIcons, id) => { + const customEvent = eventBinding.createWithSender(event.sender) + event.sender.emit('desktop-capturer-get-sources', customEvent) + + if (customEvent.defaultPrevented) { + event._replyInternal(capturerResult(id), []) + return + } + const request = { id, options: { captureWindow, captureScreen, - thumbnailSize + thumbnailSize, + fetchWindowIcons }, - webContents: event.sender + event } requestsQueue.push(request) if (requestsQueue.length === 1) { - desktopCapturer.startHandling(captureWindow, captureScreen, thumbnailSize) + desktopCapturer.startHandling(captureWindow, captureScreen, thumbnailSize, fetchWindowIcons) } // 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) => { +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 => { @@ -44,20 +54,21 @@ desktopCapturer.emit = (event, name, sources) => { id: source.id, name: source.name, thumbnail: source.thumbnail.toDataURL(), - display_id: source.display_id + display_id: source.display_id, + appIcon: (fetchWindowIcons && source.appIcon) ? source.appIcon.toDataURL() : null } }) - 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) @@ -67,7 +78,7 @@ desktopCapturer.emit = (event, name, sources) => { // If the requestsQueue is not empty, start a new request handling. if (requestsQueue.length > 0) { - const { captureWindow, captureScreen, thumbnailSize } = requestsQueue[0].options - return desktopCapturer.startHandling(captureWindow, captureScreen, thumbnailSize) + const { captureWindow, captureScreen, thumbnailSize, fetchWindowIcons } = requestsQueue[0].options + return desktopCapturer.startHandling(captureWindow, captureScreen, thumbnailSize, fetchWindowIcons) } } diff --git a/electronasar/canary/browser/guest-view-manager.js b/electronasar/canary/browser/guest-view-manager.js index 8693235..920b020 100644 --- a/electronasar/canary/browser/guest-view-manager.js +++ b/electronasar/canary/browser/guest-view-manager.js @@ -4,6 +4,11 @@ const { webContents } = require('electron') const ipcMain = require('@electron/internal/browser/ipc-main-internal') const parseFeaturesString = require('@electron/internal/common/parse-features-string') const errorUtils = require('@electron/internal/common/error-utils') +const { + syncMethods, + asyncCallbackMethods, + asyncPromiseMethods +} = require('@electron/internal/common/web-view-methods') // Doesn't exist in early initialization. let webViewManager = null @@ -158,14 +163,6 @@ const createGuest = function (embedder, params) { } } }) - guest.on('-web-contents-created', (...args) => { - if (guest.getLastWebPreferences().nativeWindowOpen === true) { - const embedder = getEmbedder(guestInstanceId) - if (embedder != null) { - embedder.emit('-web-contents-created', ...args) - } - } - }) return guestInstanceId } @@ -184,16 +181,19 @@ const attachGuest = function (event, embedderFrameId, elementInstanceId, guestIn const oldGuestInstance = guestInstances[oldGuestInstanceId] if (oldGuestInstance) { - oldGuestInstance.guest.destroy() + oldGuestInstance.guest.detachFromOuterFrame() } } const guestInstance = guestInstances[guestInstanceId] // If this isn't a valid guest instance then do nothing. if (!guestInstance) { - return + throw new Error(`Invalid guestInstanceId: ${guestInstanceId}`) } const { guest } = guestInstance + if (guest.hostWebContents !== event.sender) { + throw new Error(`Access denied to guestInstanceId: ${guestInstanceId}`) + } // If this guest is already attached to an element then remove it if (guestInstance.elementInstanceId) { @@ -212,7 +212,7 @@ const attachGuest = function (event, embedderFrameId, elementInstanceId, guestIn nodeIntegration: params.nodeintegration != null ? params.nodeintegration : false, enableRemoteModule: params.enableremotemodule, plugins: params.plugins, - zoomFactor: embedder._getZoomFactor(), + zoomFactor: embedder.getZoomFactor(), webSecurity: !params.disablewebsecurity, enableBlinkFeatures: params.blinkfeatures, disableBlinkFeatures: params.disableblinkfeatures @@ -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 @@ -332,8 +333,8 @@ const isWebViewTagEnabledCache = new WeakMap() const isWebViewTagEnabled = function (contents) { if (!isWebViewTagEnabledCache.has(contents)) { - const value = contents.getLastWebPreferences().webviewTag - isWebViewTagEnabledCache.set(contents, value) + const webPreferences = contents.getLastWebPreferences() || {} + isWebViewTagEnabledCache.set(contents, !!webPreferences.webviewTag) } return isWebViewTagEnabledCache.get(contents) @@ -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) { @@ -358,25 +359,37 @@ handleMessage('ELECTRON_GUEST_VIEW_MANAGER_CREATE_GUEST_SYNC', function (event, }) handleMessage('ELECTRON_GUEST_VIEW_MANAGER_DESTROY_GUEST', function (event, guestInstanceId) { - const guest = getGuest(guestInstanceId) - if (guest) { - guest.destroy() + try { + const guest = getGuestForWebContents(guestInstanceId, event.sender) + guest.detachFromOuterFrame() + } catch (error) { + console.error(`Guest destroy failed: ${error}`) } }) handleMessage('ELECTRON_GUEST_VIEW_MANAGER_ATTACH_GUEST', function (event, embedderFrameId, elementInstanceId, guestInstanceId, params) { - attachGuest(event, embedderFrameId, elementInstanceId, guestInstanceId, params) + try { + attachGuest(event, embedderFrameId, elementInstanceId, guestInstanceId, params) + } catch (error) { + console.error(`Guest attach failed: ${error}`) + } }) -handleMessage('ELECTRON_GUEST_VIEW_MANAGER_FOCUS_CHANGE', function (event, focus, guestInstanceId) { - event.sender.emit('focus-change', {}, focus, guestInstanceId) +// this message is sent by the actual +ipcMain.on('ELECTRON_GUEST_VIEW_MANAGER_FOCUS_CHANGE', function (event, focus, guestInstanceId) { + const guest = getGuest(guestInstanceId) + if (guest === event.sender) { + event.sender.emit('focus-change', {}, focus, guestInstanceId) + } else { + console.error(`focus-change for guestInstanceId: ${guestInstanceId}`) + } }) handleMessage('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', function (event, requestId, guestInstanceId, method, args, hasCallback) { new Promise(resolve => { - const guest = getGuest(guestInstanceId) - if (guest.hostWebContents !== event.sender) { - throw new Error('Access denied') + const guest = getGuestForWebContents(guestInstanceId, event.sender) + if (!asyncCallbackMethods.has(method) && !asyncPromiseMethods.has(method)) { + throw new Error(`Invalid method: ${method}`) } if (hasCallback) { guest[method](...args, resolve) @@ -388,22 +401,34 @@ 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) }) }) handleMessage('ELECTRON_GUEST_VIEW_MANAGER_SYNC_CALL', function (event, guestInstanceId, method, args) { try { - const guest = getGuest(guestInstanceId) - if (guest.hostWebContents !== event.sender) { - throw new Error('Access denied') + const guest = getGuestForWebContents(guestInstanceId, event.sender) + if (!syncMethods.has(method)) { + throw new Error(`Invalid method: ${method}`) } - event.returnValue = [null, guest[method].apply(guest, args)] + event.returnValue = [null, guest[method](...args)] } catch (error) { event.returnValue = [errorUtils.serialize(error)] } }) +// Returns WebContents from its guest id hosted in given webContents. +const getGuestForWebContents = function (guestInstanceId, contents) { + const guest = getGuest(guestInstanceId) + if (!guest) { + throw new Error(`Invalid guestInstanceId: ${guestInstanceId}`) + } + if (guest.hostWebContents !== contents) { + throw new Error(`Access denied to guestInstanceId: ${guestInstanceId}`) + } + return guest +} + // Returns WebContents from its guest id. const getGuest = function (guestInstanceId) { const guestInstance = guestInstances[guestInstanceId] @@ -416,5 +441,5 @@ const getEmbedder = function (guestInstanceId) { if (guestInstance != null) return guestInstance.embedder } -exports.getGuest = getGuest -exports.getEmbedder = getEmbedder +exports.getGuestForWebContents = getGuestForWebContents +exports.isWebViewTagEnabled = isWebViewTagEnabled diff --git a/electronasar/canary/browser/guest-window-manager.js b/electronasar/canary/browser/guest-window-manager.js index 5bc5d26..84ac8ef 100644 --- a/electronasar/canary/browser/guest-window-manager.js +++ b/electronasar/canary/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|. @@ -90,9 +91,9 @@ const setupGuest = function (embedder, frameName, guest, options) { } const closedByUser = function () { embedder._sendInternal('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_' + guestId) - embedder.removeListener('render-view-deleted', closedByEmbedder) + embedder.removeListener('current-render-view-deleted', closedByEmbedder) } - embedder.once('render-view-deleted', closedByEmbedder) + embedder.once('current-render-view-deleted', closedByEmbedder) guest.once('closed', closedByUser) if (frameName) { frameToGuest.set(frameName, guest) @@ -118,28 +119,12 @@ const createGuest = function (embedder, url, referrer, frameName, options, postD } guest = new BrowserWindow(options) - if (!options.webContents || url !== 'about:blank') { + if (!options.webContents) { // We should not call `loadURL` if the window was constructed from an - // existing webContents(window.open in a sandboxed renderer) and if the url - // is not 'about:blank'. + // existing webContents (window.open in a sandboxed renderer). // // Navigating to the url when creating the window from an existing - // webContents would not be necessary(it will navigate there anyway), but - // apparently there's a bug that allows the child window to be scripted by - // the opener, even when the child window is from another origin. - // - // That's why the second condition(url !== "about:blank") is required: to - // force `OverrideSiteInstanceForNavigation` to be called and consequently - // spawn a new renderer if the new window is targeting a different origin. - // - // If the URL is "about:blank", then it is very likely that the opener just - // wants to synchronously script the popup, for example: - // - // let popup = window.open() - // popup.document.body.write('

hello

') - // - // The above code would not work if a navigation to "about:blank" is done - // here, since the window would be cleared of all changes in the next tick. + // webContents is not necessary (it will navigate there anyway). const loadOptions = { httpReferrer: referrer } @@ -288,6 +273,11 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', function (event, guestI if (guestWindow != null) guestWindow.destroy() }) +const windowMethods = new Set([ + 'focus', + 'blur' +]) + ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function (event, guestId, method, ...args) { const guestContents = webContents.fromId(guestId) if (guestContents == null) { @@ -295,7 +285,7 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function (event, guest return } - if (!canAccessWindow(event.sender, guestContents)) { + if (!canAccessWindow(event.sender, guestContents) || !windowMethods.has(method)) { console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`) event.returnValue = null return @@ -326,17 +316,27 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', function (event, } }) +const webContentsMethods = new Set([ + 'print', + 'executeJavaScript' +]) + ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', function (event, guestId, method, ...args) { const guestContents = webContents.fromId(guestId) if (guestContents == null) return - if (canAccessWindow(event.sender, guestContents)) { + if (canAccessWindow(event.sender, guestContents) && webContentsMethods.has(method)) { guestContents[method](...args) } else { console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`) } }) +const webContentsSyncMethods = new Set([ + 'getURL', + 'loadURL' +]) + ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', function (event, guestId, method, ...args) { const guestContents = webContents.fromId(guestId) if (guestContents == null) { @@ -344,7 +344,7 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', function (e return } - if (canAccessWindow(event.sender, guestContents)) { + if (canAccessWindow(event.sender, guestContents) && webContentsSyncMethods.has(method)) { event.returnValue = guestContents[method](...args) } else { console.error(`Blocked ${event.sender.getURL()} from calling ${method} on its opener.`) diff --git a/electronasar/canary/browser/init.js b/electronasar/canary/browser/init.js index fc526e1..c50c5f0 100644 --- a/electronasar/canary/browser/init.js +++ b/electronasar/canary/browser/init.js @@ -184,5 +184,17 @@ if (currentPlatformSupportsAppIndicator()) { process.env.XDG_CURRENT_DESKTOP = 'Unity' } +// Quit when all windows are closed and no other one is listening to this. +app.on('window-all-closed', () => { + if (app.listenerCount('window-all-closed') === 1) { + app.quit() + } +}) + +const { setDefaultApplicationMenu } = require('@electron/internal/browser/default-menu') + +// Create default menu. +app.once('ready', setDefaultApplicationMenu) + // Finally load app's main.js and transfer control to C++. Module._load(path.join(packagePath, mainStartupScript), Module, true) diff --git a/electronasar/canary/browser/api/navigation-controller.js b/electronasar/canary/browser/navigation-controller.js similarity index 61% rename from electronasar/canary/browser/api/navigation-controller.js rename to electronasar/canary/browser/navigation-controller.js index 1863961..33dc256 100644 --- a/electronasar/canary/browser/api/navigation-controller.js +++ b/electronasar/canary/browser/navigation-controller.js @@ -3,12 +3,20 @@ const ipcMain = require('@electron/internal/browser/ipc-main-internal') // The history operation in renderer is redirected to browser. -ipcMain.on('ELECTRON_NAVIGATION_CONTROLLER', function (event, method, ...args) { - event.sender[method](...args) +ipcMain.on('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK', function (event) { + event.sender.goBack() }) -ipcMain.on('ELECTRON_SYNC_NAVIGATION_CONTROLLER', function (event, method, ...args) { - event.returnValue = event.sender[method](...args) +ipcMain.on('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD', function (event) { + event.sender.goForward() +}) + +ipcMain.on('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', function (event, offset) { + event.sender.goToOffset(offset) +}) + +ipcMain.on('ELECTRON_NAVIGATION_CONTROLLER_LENGTH', function (event) { + event.returnValue = event.sender.length() }) // JavaScript implementation of Chromium's NavigationController. @@ -55,9 +63,66 @@ const NavigationController = (function () { if (options == null) { options = {} } + const p = new Promise((resolve, reject) => { + const resolveAndCleanup = () => { + removeListeners() + resolve() + } + const rejectAndCleanup = (errorCode, errorDescription, url) => { + const err = new Error(`${errorDescription} (${errorCode}) loading '${url}'`) + Object.assign(err, { errno: errorCode, code: errorDescription, url }) + removeListeners() + reject(err) + } + const finishListener = () => { + resolveAndCleanup() + } + const failListener = (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => { + if (isMainFrame) { + rejectAndCleanup(errorCode, errorDescription, validatedURL) + } + } + + let navigationStarted = false + const navigationListener = (event, url, isSameDocument, isMainFrame, frameProcessId, frameRoutingId, navigationId) => { + if (isMainFrame) { + if (navigationStarted) { + // the webcontents has started another unrelated navigation in the + // main frame (probably from the app calling `loadURL` again); reject + // the promise + return rejectAndCleanup(-3, 'ERR_ABORTED', url) + } + navigationStarted = true + } + } + const stopLoadingListener = () => { + // By the time we get here, either 'finish' or 'fail' should have fired + // if the navigation occurred. However, in some situations (e.g. when + // attempting to load a page with a bad scheme), loading will stop + // without emitting finish or fail. In this case, we reject the promise + // with a generic failure. + // TODO(jeremy): enumerate all the cases in which this can happen. If + // the only one is with a bad scheme, perhaps ERR_INVALID_ARGUMENT + // would be more appropriate. + rejectAndCleanup(-2, 'ERR_FAILED', url) + } + const removeListeners = () => { + this.webContents.removeListener('did-finish-load', finishListener) + this.webContents.removeListener('did-fail-load', failListener) + this.webContents.removeListener('did-start-navigation', navigationListener) + this.webContents.removeListener('did-stop-loading', stopLoadingListener) + } + this.webContents.on('did-finish-load', finishListener) + this.webContents.on('did-fail-load', failListener) + this.webContents.on('did-start-navigation', navigationListener) + this.webContents.on('did-stop-loading', stopLoadingListener) + }) + // Add a no-op rejection handler to silence the unhandled rejection error. + p.catch(() => {}) this.pendingIndex = -1 this.webContents._loadURL(url, options) - return this.webContents.emit('load-url', url, options) + this.webContents.emit('load-url', url, options) + return p } NavigationController.prototype.getURL = function () { diff --git a/electronasar/canary/browser/rpc-server.js b/electronasar/canary/browser/rpc-server.js index bf3d0a3..962a4da 100644 --- a/electronasar/canary/browser/rpc-server.js +++ b/electronasar/canary/browser/rpc-server.js @@ -13,6 +13,7 @@ const { isPromise } = electron const ipcMain = require('@electron/internal/browser/ipc-main-internal') const objectsRegistry = require('@electron/internal/browser/objects-registry') +const guestViewManager = require('@electron/internal/browser/guest-view-manager') const bufferUtils = require('@electron/internal/common/buffer-utils') const errorUtils = require('@electron/internal/common/error-utils') @@ -138,7 +139,7 @@ const plainObjectToMeta = function (obj) { } // Convert Error into meta data. -const exceptionToMeta = function (sender, contextId, error) { +const exceptionToMeta = function (error) { return { type: 'exception', value: errorUtils.serialize(error) @@ -174,7 +175,7 @@ const removeRemoteListenersAndLogWarning = (sender, callIntoRenderer) => { } // Convert array of meta data from renderer into array of real values. -const unwrapArgs = function (sender, contextId, args) { +const unwrapArgs = function (sender, frameId, contextId, args) { const metaToValue = function (meta) { switch (meta.type) { case 'value': @@ -182,7 +183,7 @@ const unwrapArgs = function (sender, contextId, args) { case 'remote-object': return objectsRegistry.get(meta.id) case 'array': - return unwrapArgs(sender, contextId, meta.value) + return unwrapArgs(sender, frameId, contextId, meta.value) case 'buffer': return bufferUtils.metaToBuffer(meta.value) case 'date': @@ -216,9 +217,11 @@ const unwrapArgs = function (sender, contextId, args) { } const callIntoRenderer = function (...args) { + let succeed = false if (!sender.isDestroyed()) { - sender._sendInternal('ELECTRON_RENDERER_CALLBACK', contextId, meta.id, valueToMeta(sender, contextId, args)) - } else { + succeed = sender._sendToFrameInternal(frameId, 'ELECTRON_RENDERER_CALLBACK', contextId, meta.id, valueToMeta(sender, contextId, args)) + } + if (!succeed) { removeRemoteListenersAndLogWarning(this, callIntoRenderer) } } @@ -273,7 +276,7 @@ const handleRemoteCommand = function (channel, handler) { try { returnValue = handler(event, contextId, ...args) } catch (error) { - returnValue = exceptionToMeta(event.sender, contextId, error) + returnValue = exceptionToMeta(error) } if (returnValue !== undefined) { @@ -295,46 +298,79 @@ handleRemoteCommand('ELECTRON_BROWSER_REQUIRE', function (event, contextId, modu const customEvent = eventBinding.createWithSender(event.sender) event.sender.emit('remote-require', customEvent, moduleName) - if (customEvent.defaultPrevented) { - if (typeof customEvent.returnValue === 'undefined') { - throw new Error(`Invalid module: ${moduleName}`) + if (customEvent.returnValue === undefined) { + if (customEvent.defaultPrevented) { + throw new Error(`Blocked remote.require('${moduleName}')`) + } else { + customEvent.returnValue = process.mainModule.require(moduleName) } - } else { - customEvent.returnValue = process.mainModule.require(moduleName) } return valueToMeta(event.sender, contextId, customEvent.returnValue) }) -handleRemoteCommand('ELECTRON_BROWSER_GET_BUILTIN', function (event, contextId, module) { - return valueToMeta(event.sender, contextId, electron[module]) +handleRemoteCommand('ELECTRON_BROWSER_GET_BUILTIN', function (event, contextId, moduleName) { + const customEvent = eventBinding.createWithSender(event.sender) + event.sender.emit('remote-get-builtin', customEvent, moduleName) + + if (customEvent.returnValue === undefined) { + if (customEvent.defaultPrevented) { + throw new Error(`Blocked remote.getBuiltin('${moduleName}')`) + } else { + customEvent.returnValue = electron[moduleName] + } + } + + return valueToMeta(event.sender, contextId, customEvent.returnValue) }) handleRemoteCommand('ELECTRON_BROWSER_GLOBAL', function (event, contextId, globalName) { const customEvent = eventBinding.createWithSender(event.sender) event.sender.emit('remote-get-global', customEvent, globalName) - if (customEvent.defaultPrevented) { - if (typeof customEvent.returnValue === 'undefined') { - throw new Error(`Invalid global: ${globalName}`) + if (customEvent.returnValue === undefined) { + if (customEvent.defaultPrevented) { + throw new Error(`Blocked remote.getGlobal('${globalName}')`) + } else { + customEvent.returnValue = global[globalName] } - } else { - customEvent.returnValue = global[globalName] } return valueToMeta(event.sender, contextId, customEvent.returnValue) }) handleRemoteCommand('ELECTRON_BROWSER_CURRENT_WINDOW', function (event, contextId) { - return valueToMeta(event.sender, contextId, event.sender.getOwnerBrowserWindow()) + const customEvent = eventBinding.createWithSender(event.sender) + event.sender.emit('remote-get-current-window', customEvent) + + if (customEvent.returnValue === undefined) { + if (customEvent.defaultPrevented) { + throw new Error('Blocked remote.getCurrentWindow()') + } else { + customEvent.returnValue = event.sender.getOwnerBrowserWindow() + } + } + + return valueToMeta(event.sender, contextId, customEvent.returnValue) }) handleRemoteCommand('ELECTRON_BROWSER_CURRENT_WEB_CONTENTS', function (event, contextId) { - return valueToMeta(event.sender, contextId, event.sender) + const customEvent = eventBinding.createWithSender(event.sender) + event.sender.emit('remote-get-current-web-contents', customEvent) + + if (customEvent.returnValue === undefined) { + if (customEvent.defaultPrevented) { + throw new Error('Blocked remote.getCurrentWebContents()') + } else { + customEvent.returnValue = event.sender + } + } + + return valueToMeta(event.sender, contextId, customEvent.returnValue) }) handleRemoteCommand('ELECTRON_BROWSER_CONSTRUCTOR', function (event, contextId, id, args) { - args = unwrapArgs(event.sender, contextId, args) + args = unwrapArgs(event.sender, event.frameId, contextId, args) const constructor = objectsRegistry.get(id) if (constructor == null) { @@ -345,7 +381,7 @@ handleRemoteCommand('ELECTRON_BROWSER_CONSTRUCTOR', function (event, contextId, }) handleRemoteCommand('ELECTRON_BROWSER_FUNCTION_CALL', function (event, contextId, id, args) { - args = unwrapArgs(event.sender, contextId, args) + args = unwrapArgs(event.sender, event.frameId, contextId, args) const func = objectsRegistry.get(id) if (func == null) { @@ -356,7 +392,7 @@ handleRemoteCommand('ELECTRON_BROWSER_FUNCTION_CALL', function (event, contextId }) handleRemoteCommand('ELECTRON_BROWSER_MEMBER_CONSTRUCTOR', function (event, contextId, id, method, args) { - args = unwrapArgs(event.sender, contextId, args) + args = unwrapArgs(event.sender, event.frameId, contextId, args) const object = objectsRegistry.get(id) if (object == null) { @@ -367,7 +403,7 @@ handleRemoteCommand('ELECTRON_BROWSER_MEMBER_CONSTRUCTOR', function (event, cont }) handleRemoteCommand('ELECTRON_BROWSER_MEMBER_CALL', function (event, contextId, id, method, args) { - args = unwrapArgs(event.sender, contextId, args) + args = unwrapArgs(event.sender, event.frameId, contextId, args) const obj = objectsRegistry.get(id) if (obj == null) { @@ -378,7 +414,7 @@ handleRemoteCommand('ELECTRON_BROWSER_MEMBER_CALL', function (event, contextId, }) handleRemoteCommand('ELECTRON_BROWSER_MEMBER_SET', function (event, contextId, id, name, args) { - args = unwrapArgs(event.sender, contextId, args) + args = unwrapArgs(event.sender, event.frameId, contextId, args) const obj = objectsRegistry.get(id) if (obj == null) { @@ -409,8 +445,20 @@ handleRemoteCommand('ELECTRON_BROWSER_CONTEXT_RELEASE', (event, contextId) => { }) handleRemoteCommand('ELECTRON_BROWSER_GUEST_WEB_CONTENTS', function (event, contextId, guestInstanceId) { - const guestViewManager = require('@electron/internal/browser/guest-view-manager') - return valueToMeta(event.sender, contextId, guestViewManager.getGuest(guestInstanceId)) + const guest = guestViewManager.getGuestForWebContents(guestInstanceId, event.sender) + + const customEvent = eventBinding.createWithSender(event.sender) + event.sender.emit('remote-get-guest-web-contents', customEvent, guest) + + if (customEvent.returnValue === undefined) { + if (customEvent.defaultPrevented) { + throw new Error(`Blocked remote.getGuestForWebContents()`) + } else { + customEvent.returnValue = guest + } + } + + return valueToMeta(event.sender, contextId, customEvent.returnValue) }) // Implements window.close() @@ -486,27 +534,40 @@ ipcMain.on('ELECTRON_BROWSER_CLIPBOARD_WRITE_FIND_TEXT', function (event, text) setReturnValue(event, () => electron.clipboard.writeFindText(text)) }) -ipcMain.on('ELECTRON_BROWSER_SANDBOX_LOAD', function (event) { - const preloadPath = event.sender._getPreloadPath() +const getPreloadScript = function (preloadPath) { let preloadSrc = null let preloadError = null if (preloadPath) { try { preloadSrc = fs.readFileSync(preloadPath).toString() } catch (err) { - preloadError = { stack: err ? err.stack : (new Error(`Failed to load "${preloadPath}"`)).stack } + preloadError = errorUtils.serialize(err) } } + return { preloadPath, preloadSrc, preloadError } +} + +ipcMain.on('ELECTRON_BROWSER_SANDBOX_LOAD', function (event) { + const preloadPaths = [ + ...(event.sender.session ? event.sender.session.getPreloads() : []), + event.sender._getPreloadPath() + ] + event.returnValue = { - preloadSrc, - preloadError, + preloadScripts: preloadPaths.map(path => getPreloadScript(path)), isRemoteModuleEnabled: event.sender._isRemoteModuleEnabled(), + isWebViewTagEnabled: guestViewManager.isWebViewTagEnabled(event.sender), process: { arch: process.arch, platform: process.platform, env: process.env, version: process.version, - versions: process.versions + versions: process.versions, + execPath: process.helperExecPath } } }) + +ipcMain.on('ELECTRON_BROWSER_PRELOAD_ERROR', function (event, preloadPath, error) { + event.sender.emit('preload-error', event, preloadPath, errorUtils.deserialize(error)) +}) diff --git a/electronasar/canary/common/api/clipboard.js b/electronasar/canary/common/api/clipboard.js index e0c9921..530b08f 100644 --- a/electronasar/canary/common/api/clipboard.js +++ b/electronasar/canary/common/api/clipboard.js @@ -2,8 +2,8 @@ if (process.platform === 'linux' && process.type === 'renderer') { // On Linux we could not access clipboard in renderer process. - const { getRemoteForUsage } = require('@electron/internal/renderer/remote') - module.exports = getRemoteForUsage('clipboard').clipboard + const { getRemote } = require('@electron/internal/renderer/remote') + module.exports = getRemote('clipboard') } else { const clipboard = process.atomBinding('clipboard') diff --git a/electronasar/canary/common/api/deprecate.js b/electronasar/canary/common/api/deprecate.js index f72939b..ef92a63 100644 --- a/electronasar/canary/common/api/deprecate.js +++ b/electronasar/canary/common/api/deprecate.js @@ -19,7 +19,9 @@ const deprecate = { setHandler: (handler) => { deprecationHandler = handler }, getHandler: () => deprecationHandler, warn: (oldName, newName) => { - return deprecate.log(`'${oldName}' is deprecated. Use '${newName}' instead.`) + if (!process.noDeprecation) { + deprecate.log(`'${oldName}' is deprecated. Use '${newName}' instead.`) + } }, log: (message) => { if (typeof deprecationHandler === 'function') { @@ -67,6 +69,41 @@ const deprecate = { }) }, + function: (fn, newName) => { + // if newName is left blank, a removal warning will be displayed + const warn = warnOnce(fn.name, newName) + + return function () { + if (!process.noDeprecation) warn() + return fn.apply(this, arguments) + } + }, + + promisify: (fn) => { + const fnName = fn.name || 'function' + const oldName = `${fnName} with callbacks` + const newName = `${fnName} with Promises` + const warn = warnOnce(oldName, newName) + + return function (...params) { + let cb + if (params.length > 0 && typeof params[params.length - 1] === 'function') { + cb = params.pop() + } + const promise = fn.apply(this, params) + if (!cb) return promise + if (process.enablePromiseAPIs) warn() + return promise + .then(res => { + process.nextTick(() => { + cb.length === 2 ? cb(null, res) : cb(res) + }) + }, err => { + process.nextTick(() => cb(err)) + }) + } + }, + renameProperty: (o, oldName, newName) => { const warn = warnOnce(oldName, newName) diff --git a/electronasar/canary/common/parse-features-string.js b/electronasar/canary/common/parse-features-string.js index 955ab8f..1ef5109 100644 --- a/electronasar/canary/common/parse-features-string.js +++ b/electronasar/canary/common/parse-features-string.js @@ -4,12 +4,12 @@ // - `features` input string // - `emit` function(key, value) - called for each parsed KV module.exports = function parseFeaturesString (features, emit) { - features = `${features}` + features = `${features}`.trim() // split the string by ',' - features.split(/,\s*/).forEach((feature) => { + features.split(/\s*,\s*/).forEach((feature) => { // expected form is either a key by itself or a key/value pair in the form of // 'key=value' - let [key, value] = feature.split(/\s*=/) + let [key, value] = feature.split(/\s*=\s*/) if (!key) return // interpret the value as a boolean, if possible diff --git a/electronasar/canary/common/web-view-methods.js b/electronasar/canary/common/web-view-methods.js new file mode 100644 index 0000000..a71c47c --- /dev/null +++ b/electronasar/canary/common/web-view-methods.js @@ -0,0 +1,70 @@ +'use strict' + +// Public-facing API methods. +exports.syncMethods = new Set([ + 'getURL', + 'loadURL', + 'getTitle', + 'isLoading', + 'isLoadingMainFrame', + 'isWaitingForResponse', + 'stop', + 'reload', + 'reloadIgnoringCache', + 'canGoBack', + 'canGoForward', + 'canGoToOffset', + 'clearHistory', + 'goBack', + 'goForward', + 'goToIndex', + 'goToOffset', + 'isCrashed', + 'setUserAgent', + 'getUserAgent', + 'openDevTools', + 'closeDevTools', + 'isDevToolsOpened', + 'isDevToolsFocused', + 'inspectElement', + 'setAudioMuted', + 'isAudioMuted', + 'isCurrentlyAudible', + 'undo', + 'redo', + 'cut', + 'copy', + 'paste', + 'pasteAndMatchStyle', + 'delete', + 'selectAll', + 'unselect', + 'replace', + 'replaceMisspelling', + 'findInPage', + 'stopFindInPage', + 'downloadURL', + 'inspectServiceWorker', + 'showDefinitionForSelection', + 'getZoomFactor', + 'getZoomLevel', + 'setZoomFactor', + 'setZoomLevel', + 'sendImeEvent' +]) + +exports.asyncCallbackMethods = new Set([ + 'insertCSS', + 'insertText', + 'send', + 'sendInputEvent', + 'setLayoutZoomLevelLimits', + 'setVisualZoomLevelLimits', + 'print', + 'printToPDF' +]) + +exports.asyncPromiseMethods = new Set([ + 'capturePage', + 'executeJavaScript' +]) diff --git a/electronasar/canary/renderer/api/desktop-capturer.js b/electronasar/canary/renderer/api/desktop-capturer.js index e4ece82..249a998 100644 --- a/electronasar/canary/renderer/api/desktop-capturer.js +++ b/electronasar/canary/renderer/api/desktop-capturer.js @@ -1,6 +1,6 @@ 'use strict' -const { nativeImage } = require('electron') +const { nativeImage, deprecate } = require('electron') const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal') const includes = [].includes @@ -17,32 +17,43 @@ function isValid (options) { return Array.isArray(types) } -exports.getSources = function (options, callback) { - if (!isValid(options)) return callback(new Error('Invalid options')) - const captureWindow = includes.call(options.types, 'window') - const captureScreen = includes.call(options.types, 'screen') +function mapSources (sources) { + return sources.map(source => ({ + id: source.id, + name: source.name, + thumbnail: nativeImage.createFromDataURL(source.thumbnail), + display_id: source.display_id, + appIcon: source.appIcon ? nativeImage.createFromDataURL(source.appIcon) : null + })) +} - if (options.thumbnailSize == null) { - options.thumbnailSize = { - width: 150, - height: 150 +const getSources = (options) => { + return new Promise((resolve, reject) => { + if (!isValid(options)) throw new Error('Invalid options') + + const captureWindow = includes.call(options.types, 'window') + const captureScreen = includes.call(options.types, 'screen') + + if (options.thumbnailSize == null) { + options.thumbnailSize = { + width: 150, + height: 150 + } + } + if (options.fetchWindowIcons == null) { + options.fetchWindowIcons = false } - } - const id = incrementId() - ipcRenderer.send('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', captureWindow, captureScreen, options.thumbnailSize, id) - return ipcRenderer.once(`ELECTRON_RENDERER_DESKTOP_CAPTURER_RESULT_${id}`, (event, sources) => { - callback(null, (() => { - const results = [] - sources.forEach(source => { - results.push({ - id: source.id, - name: source.name, - thumbnail: nativeImage.createFromDataURL(source.thumbnail), - display_id: source.display_id - }) - }) - return results - })()) + const id = incrementId() + ipcRenderer.send('ELECTRON_BROWSER_DESKTOP_CAPTURER_GET_SOURCES', captureWindow, captureScreen, options.thumbnailSize, options.fetchWindowIcons, id) + return ipcRenderer.once(`ELECTRON_RENDERER_DESKTOP_CAPTURER_RESULT_${id}`, (event, sources) => { + try { + resolve(mapSources(sources)) + } catch (error) { + reject(error) + } + }) }) } + +exports.getSources = deprecate.promisify(getSources) diff --git a/electronasar/canary/renderer/api/ipc-renderer.js b/electronasar/canary/renderer/api/ipc-renderer.js index 7705f9f..1911543 100644 --- a/electronasar/canary/renderer/api/ipc-renderer.js +++ b/electronasar/canary/renderer/api/ipc-renderer.js @@ -8,11 +8,11 @@ const ipcRenderer = v8Util.getHiddenValue(global, 'ipc') const internal = false ipcRenderer.send = function (...args) { - return binding.send('ipc-message', args) + return binding.send('-ipc-message', args) } ipcRenderer.sendSync = function (...args) { - return binding.sendSync('ipc-message-sync', args)[0] + return binding.sendSync('-ipc-message-sync', args)[0] } ipcRenderer.sendToHost = function (...args) { diff --git a/electronasar/canary/renderer/api/remote.js b/electronasar/canary/renderer/api/remote.js index 036ebd8..98fc55c 100644 --- a/electronasar/canary/renderer/api/remote.js +++ b/electronasar/canary/renderer/api/remote.js @@ -311,7 +311,9 @@ exports.getCurrentWindow = () => { // Get current WebContents object. exports.getCurrentWebContents = () => { - return metaToValue(ipcRenderer.sendSync('ELECTRON_BROWSER_CURRENT_WEB_CONTENTS', contextId)) + const command = 'ELECTRON_BROWSER_CURRENT_WEB_CONTENTS' + const meta = ipcRenderer.sendSync(command, contextId) + return metaToValue(meta) } // Get a global object in browser. diff --git a/electronasar/canary/renderer/api/screen.js b/electronasar/canary/renderer/api/screen.js index 6cae10d..a19604c 100644 --- a/electronasar/canary/renderer/api/screen.js +++ b/electronasar/canary/renderer/api/screen.js @@ -1,4 +1,8 @@ 'use strict' -const { getRemoteForUsage } = require('@electron/internal/renderer/remote') -module.exports = getRemoteForUsage('screen').screen +const { deprecate } = require('electron') + +deprecate.warn(`electron.screen`, `electron.remote.screen`) + +const { getRemote } = require('@electron/internal/renderer/remote') +module.exports = getRemote('screen') diff --git a/electronasar/canary/renderer/extensions/i18n.js b/electronasar/canary/renderer/extensions/i18n.js index 661b264..9554ff9 100644 --- a/electronasar/canary/renderer/extensions/i18n.js +++ b/electronasar/canary/renderer/extensions/i18n.js @@ -6,9 +6,10 @@ // Does not implement predefined messages: // https://developer.chrome.com/extensions/i18n#overview-predefined +const { potentiallyRemoteRequire } = require('@electron/internal/renderer/remote') const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal') -const fs = require('fs') -const path = require('path') +const fs = potentiallyRemoteRequire('fs') +const path = potentiallyRemoteRequire('path') let metadata diff --git a/electronasar/canary/renderer/extensions/storage.js b/electronasar/canary/renderer/extensions/storage.js index 8afaac7..d308881 100644 --- a/electronasar/canary/renderer/extensions/storage.js +++ b/electronasar/canary/renderer/extensions/storage.js @@ -1,9 +1,9 @@ 'use strict' -const fs = require('fs') -const path = require('path') -const { remote } = require('electron') -const { app } = remote +const { getRemote, potentiallyRemoteRequire } = require('@electron/internal/renderer/remote') +const fs = potentiallyRemoteRequire('fs') +const path = potentiallyRemoteRequire('path') +const app = getRemote('app') const getChromeStoragePath = (storageType, extensionId) => { return path.join( diff --git a/electronasar/canary/renderer/init.js b/electronasar/canary/renderer/init.js index cf1af5b..cd2ffc5 100644 --- a/electronasar/canary/renderer/init.js +++ b/electronasar/canary/renderer/init.js @@ -31,68 +31,67 @@ const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal') require('@electron/internal/renderer/web-frame-init')() // Process command line arguments. -let nodeIntegration = false -let webviewTag = false -let preloadScript = null -let preloadScripts = [] -let isBackgroundPage = false -let appPath = null -for (const arg of process.argv) { - if (arg.indexOf('--guest-instance-id=') === 0) { - // This is a guest web view. - process.guestInstanceId = parseInt(arg.substr(arg.indexOf('=') + 1)) - } else if (arg.indexOf('--opener-id=') === 0) { - // This is a guest BrowserWindow. - process.openerId = parseInt(arg.substr(arg.indexOf('=') + 1)) - } else if (arg.indexOf('--node-integration=') === 0) { - nodeIntegration = arg.substr(arg.indexOf('=') + 1) === 'true' - } else if (arg.indexOf('--preload=') === 0) { - preloadScript = arg.substr(arg.indexOf('=') + 1) - } else if (arg === '--background-page') { - isBackgroundPage = true - } else if (arg.indexOf('--app-path=') === 0) { - appPath = arg.substr(arg.indexOf('=') + 1) - } else if (arg.indexOf('--webview-tag=') === 0) { - webviewTag = arg.substr(arg.indexOf('=') + 1) === 'true' - } else if (arg.indexOf('--preload-scripts') === 0) { - preloadScripts = arg.substr(arg.indexOf('=') + 1).split(path.delimiter) - } +const { hasSwitch, getSwitchValue } = process.atomBinding('command_line') + +const parseOption = function (name, defaultValue, converter = value => value) { + return hasSwitch(name) ? converter(getSwitchValue(name)) : defaultValue } +const contextIsolation = hasSwitch('context-isolation') +const nodeIntegration = hasSwitch('node-integration') +const webviewTag = hasSwitch('webview-tag') +const isHiddenPage = hasSwitch('hidden-page') +const isBackgroundPage = hasSwitch('background-page') +const usesNativeWindowOpen = hasSwitch('native-window-open') + +const preloadScript = parseOption('preload', null) +const preloadScripts = parseOption('preload-scripts', [], value => value.split(path.delimiter)) +const appPath = parseOption('app-path', null) +const guestInstanceId = parseOption('guest-instance-id', null, value => parseInt(value)) +const openerId = parseOption('opener-id', null, value => parseInt(value)) + +// The arguments to be passed to isolated world. +const isolatedWorldArgs = { ipcRenderer, guestInstanceId, isHiddenPage, openerId, usesNativeWindowOpen } + // The webContents preload script is loaded after the session preload scripts. if (preloadScript) { preloadScripts.push(preloadScript) } -if (window.location.protocol === 'chrome-devtools:') { - // Override some inspector APIs. - require('@electron/internal/renderer/inspector') - nodeIntegration = false -} else if (window.location.protocol === 'chrome-extension:') { - // Add implementations of chrome API. - require('@electron/internal/renderer/chrome-api').injectTo(window.location.hostname, isBackgroundPage, window) - nodeIntegration = false -} else if (window.location.protocol === 'chrome:') { - // Disable node integration for chrome UI scheme. - nodeIntegration = false -} else { - // Override default web functions. - require('@electron/internal/renderer/override') +switch (window.location.protocol) { + case 'chrome-devtools:': { + // Override some inspector APIs. + require('@electron/internal/renderer/inspector') + break + } + case 'chrome-extension:': { + // Inject the chrome.* APIs that chrome extensions require + require('@electron/internal/renderer/chrome-api').injectTo(window.location.hostname, isBackgroundPage, window) + break + } + case 'chrome:': + break + default: { + // Override default web functions. + require('@electron/internal/renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen) - // Inject content scripts. - require('@electron/internal/renderer/content-scripts-injector') - - // Load webview tag implementation. - if (webviewTag && process.guestInstanceId == null) { - const { setupWebView } = require('@electron/internal/renderer/web-view/web-view') - if (process.argv.includes('--context-isolation')) { - v8Util.setHiddenValue(window, 'setup-webview', setupWebView) - } else { - setupWebView(window) + // Inject content scripts. + if (process.isMainFrame) { + require('@electron/internal/renderer/content-scripts-injector') } } } +// Load webview tag implementation. +if (process.isMainFrame) { + require('@electron/internal/renderer/web-view/web-view-init')(contextIsolation, webviewTag, guestInstanceId) +} + +// Pass the arguments to isolatedWorld. +if (contextIsolation) { + v8Util.setHiddenValue(global, 'isolated-world-args', isolatedWorldArgs) +} + if (nodeIntegration) { // Export node bindings to global. global.require = require @@ -150,27 +149,21 @@ if (nodeIntegration) { }) } +const errorUtils = require('@electron/internal/common/error-utils') + // Load the preload scripts. for (const preloadScript of preloadScripts) { try { require(preloadScript) } catch (error) { - console.error('Unable to load preload script: ' + preloadScript) - console.error(error.stack || error.message) + console.error(`Unable to load preload script: ${preloadScript}`) + console.error(`${error}`) + + ipcRenderer.send('ELECTRON_BROWSER_PRELOAD_ERROR', preloadScript, errorUtils.serialize(error)) } } // Warn about security issues -require('@electron/internal/renderer/security-warnings')(nodeIntegration) - -// Report focus/blur events of webview to browser. -// Note that while Chromium content APIs have observer for focus/blur, they -// unfortunately do not work for webview. -if (process.guestInstanceId) { - window.addEventListener('focus', () => { - ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_FOCUS_CHANGE', true, process.guestInstanceId) - }) - window.addEventListener('blur', () => { - ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_FOCUS_CHANGE', false, process.guestInstanceId) - }) +if (process.isMainFrame) { + require('@electron/internal/renderer/security-warnings')(nodeIntegration) } diff --git a/electronasar/canary/renderer/inspector.js b/electronasar/canary/renderer/inspector.js index b358fa6..5ed4e9d 100644 --- a/electronasar/canary/renderer/inspector.js +++ b/electronasar/canary/renderer/inspector.js @@ -1,5 +1,7 @@ 'use strict' +const { getRemote, potentiallyRemoteRequire } = require('@electron/internal/renderer/remote') + window.onload = function () { // Use menu API to show context menu. window.InspectorFrontendHost.showContextMenuAtPoint = createMenu @@ -18,7 +20,7 @@ function completeURL (project, path) { } window.confirm = function (message, title) { - const { dialog } = require('electron').remote + const dialog = getRemote('dialog') if (title == null) { title = '' } @@ -62,8 +64,7 @@ const convertToMenuTemplate = function (items) { } const createMenu = function (x, y, items) { - const { remote } = require('electron') - const { Menu } = remote + const Menu = getRemote('Menu') let template = convertToMenuTemplate(items) if (useEditMenuItems(x, y, template)) { @@ -73,7 +74,8 @@ const createMenu = function (x, y, items) { // The menu is expected to show asynchronously. setTimeout(function () { - menu.popup({ window: remote.getCurrentWindow() }) + const getCurrentWindow = getRemote('getCurrentWindow') + menu.popup({ window: getCurrentWindow() }) }) } @@ -116,7 +118,7 @@ const getEditMenuItems = function () { } const showFileChooserDialog = function (callback) { - const { dialog } = require('electron').remote + const dialog = getRemote('dialog') const files = dialog.showOpenDialog({}) if (files != null) { callback(pathToHtml5FileObject(files[0])) @@ -124,7 +126,7 @@ const showFileChooserDialog = function (callback) { } const pathToHtml5FileObject = function (path) { - const fs = require('fs') + const fs = potentiallyRemoteRequire('fs') const blob = new Blob([fs.readFileSync(path)]) blob.name = path return blob diff --git a/electronasar/canary/renderer/override.js b/electronasar/canary/renderer/override.js deleted file mode 100644 index 72a4a5a..0000000 --- a/electronasar/canary/renderer/override.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' - -const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal') - -const v8Util = process.atomBinding('v8_util') - -const { guestInstanceId, openerId } = process -const hiddenPage = process.argv.includes('--hidden-page') -const usesNativeWindowOpen = process.argv.includes('--native-window-open') -const contextIsolation = process.argv.includes('--context-isolation') - -// Pass the arguments to isolatedWorld. -if (contextIsolation) { - const isolatedWorldArgs = { ipcRenderer, guestInstanceId, hiddenPage, openerId, usesNativeWindowOpen } - v8Util.setHiddenValue(global, 'isolated-world-args', isolatedWorldArgs) -} - -require('@electron/internal/renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNativeWindowOpen) diff --git a/electronasar/canary/renderer/remote.js b/electronasar/canary/renderer/remote.js index b807723..0ef98ca 100644 --- a/electronasar/canary/renderer/remote.js +++ b/electronasar/canary/renderer/remote.js @@ -2,9 +2,24 @@ const { remote } = require('electron') -exports.getRemoteForUsage = function (usage) { +exports.getRemote = function (name) { if (!remote) { - throw new Error(`${usage} requires remote, which is not enabled`) + throw new Error(`${name} requires remote, which is not enabled`) + } + return remote[name] +} + +exports.remoteRequire = function (name) { + if (!remote) { + throw new Error(`${name} requires remote, which is not enabled`) + } + return remote.require(name) +} + +exports.potentiallyRemoteRequire = function (name) { + if (process.sandboxed) { + return exports.remoteRequire(name) + } else { + return require(name) } - return remote } diff --git a/electronasar/canary/renderer/security-warnings.js b/electronasar/canary/renderer/security-warnings.js index 9d41190..9d68ee0 100644 --- a/electronasar/canary/renderer/security-warnings.js +++ b/electronasar/canary/renderer/security-warnings.js @@ -249,51 +249,6 @@ const warnAboutAllowedPopups = function () { } } -const warnAboutNodeIntegrationDefault = function (webPreferences) { - if (webPreferences.nodeIntegration && !webPreferences.nodeIntegrationWasExplicitlyEnabled) { - const warning = `This window has node integration enabled by default. In ` + - `Electron 5.0.0, node integration will be disabled by default. To prepare ` + - `for this change, set {nodeIntegration: true} in the webPreferences for ` + - `this window, or ensure that this window does not rely on node integration ` + - `and set {nodeIntegration: false}.` - console.warn('%cElectron Deprecation Warning (nodeIntegration default change)', 'font-weight: bold;', warning) - } -} - -const warnAboutContextIsolationDefault = function (webPreferences) { - if (webPreferences.preload && !webPreferences.contextIsolation && !webPreferences.contextIsolationWasExplicitlyDisabled) { - const url = 'https://electronjs.org/docs/tutorial/security#3-enable-context-isolation-for-remote-content' - const warning = `This window has context isolation disabled by default. In ` + - `Electron 5.0.0, context isolation will be enabled by default. To prepare ` + - `for this change, set {contextIsolation: false} in the webPreferences for ` + - `this window, or ensure that this window does not rely on context ` + - `isolation being disabled, and set {contextIsolation: true}.\n\n` + - `For more information, see ${url}` - console.warn('%cElectron Deprecation Warning (contextIsolation default change)', 'font-weight: bold;', warning) - } -} - -const warnAboutDeprecatedWebviewTagDefault = function (webPreferences) { - if (webPreferences.webviewTagWasExplicitlyEnabled) { - return - } - if (!document || !document.getElementsByTagName) { - return - } - const webviews = document.getElementsByTagName('webview') - if (webviews && webviews.length > 0) { - const url = 'https://github.com/electron/electron/blob/master/docs/api/breaking-changes.md#new-browserwindow-webpreferences-' - const warning = `This window has the tag enabled by default. In ` + - `Electron 5.0.0, tags will be disabled by default. To prepare ` + - `for this change, set {webviewTag: true} in the webPreferences for ` + - `this window.\n\n` + - `For more information, see ${url}` - - console.warn('%cElectron Deprecation Warning (webviewTag default change)', - 'font-weight: bold;', warning) - } -} - // Currently missing since we can't easily programmatically check for it: // #12WebViews: Verify the options and params of all `` tags @@ -306,9 +261,6 @@ const logSecurityWarnings = function (webPreferences, nodeIntegration) { warnAboutEnableBlinkFeatures(webPreferences) warnAboutInsecureCSP() warnAboutAllowedPopups() - warnAboutNodeIntegrationDefault(webPreferences) - warnAboutContextIsolationDefault(webPreferences) - warnAboutDeprecatedWebviewTagDefault(webPreferences) } const getWebPreferences = function () { diff --git a/electronasar/canary/renderer/web-view/web-view-attributes.js b/electronasar/canary/renderer/web-view/web-view-attributes.js index 39d986e..2db7736 100644 --- a/electronasar/canary/renderer/web-view/web-view-attributes.js +++ b/electronasar/canary/renderer/web-view/web-view-attributes.js @@ -1,7 +1,7 @@ 'use strict' const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal') -const { WebViewImpl } = require('@electron/internal/renderer/web-view/web-view') +const { WebViewImpl } = require('@electron/internal/renderer/web-view/web-view-impl') const webViewConstants = require('@electron/internal/renderer/web-view/web-view-constants') const errorUtils = require('@electron/internal/common/error-utils') diff --git a/electronasar/canary/renderer/web-view/web-view-element.js b/electronasar/canary/renderer/web-view/web-view-element.js new file mode 100644 index 0000000..beb0a34 --- /dev/null +++ b/electronasar/canary/renderer/web-view/web-view-element.js @@ -0,0 +1,110 @@ +'use strict' + +// When using context isolation, the WebViewElement and the custom element +// methods have to be defined in the main world to be able to be registered. +// +// Note: The hidden values can only be read/set inside the same context, all +// methods that access the "internal" hidden value must be put in this file. +// +// Note: This file could be loaded in the main world of contextIsolation page, +// which runs in browserify environment instead of Node environment, all native +// modules must be passed from outside, all included files must be plain JS. + +const webViewConstants = require('@electron/internal/renderer/web-view/web-view-constants') + +// Return a WebViewElement class that is defined in this context. +const defineWebViewElement = (v8Util, webViewImpl) => { + const { guestViewInternal, WebViewImpl } = webViewImpl + return class WebViewElement extends HTMLElement { + static get observedAttributes () { + return [ + webViewConstants.ATTRIBUTE_PARTITION, + webViewConstants.ATTRIBUTE_SRC, + webViewConstants.ATTRIBUTE_HTTPREFERRER, + webViewConstants.ATTRIBUTE_USERAGENT, + webViewConstants.ATTRIBUTE_NODEINTEGRATION, + webViewConstants.ATTRIBUTE_PLUGINS, + webViewConstants.ATTRIBUTE_DISABLEWEBSECURITY, + webViewConstants.ATTRIBUTE_ALLOWPOPUPS, + webViewConstants.ATTRIBUTE_ENABLEREMOTEMODULE, + webViewConstants.ATTRIBUTE_PRELOAD, + webViewConstants.ATTRIBUTE_BLINKFEATURES, + webViewConstants.ATTRIBUTE_DISABLEBLINKFEATURES, + webViewConstants.ATTRIBUTE_WEBPREFERENCES + ] + } + + constructor () { + super() + v8Util.setHiddenValue(this, 'internal', new WebViewImpl(this)) + } + + connectedCallback () { + const internal = v8Util.getHiddenValue(this, 'internal') + if (!internal) { + return + } + if (!internal.elementAttached) { + guestViewInternal.registerEvents(internal, internal.viewInstanceId) + internal.elementAttached = true + internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse() + } + } + + attributeChangedCallback (name, oldValue, newValue) { + const internal = v8Util.getHiddenValue(this, 'internal') + if (internal) { + internal.handleWebviewAttributeMutation(name, oldValue, newValue) + } + } + + disconnectedCallback () { + const internal = v8Util.getHiddenValue(this, 'internal') + if (!internal) { + return + } + guestViewInternal.deregisterEvents(internal.viewInstanceId) + internal.elementAttached = false + this.internalInstanceId = 0 + internal.reset() + } + } +} + +// Register custom element. +const registerWebViewElement = (v8Util, webViewImpl) => { + const WebViewElement = defineWebViewElement(v8Util, webViewImpl) + + webViewImpl.setupMethods(WebViewElement) + + // The customElements.define has to be called in a special scope. + webViewImpl.webFrame.allowGuestViewElementDefinition(window, () => { + window.customElements.define('webview', WebViewElement) + window.WebView = WebViewElement + + // Delete the callbacks so developers cannot call them and produce unexpected + // behavior. + delete WebViewElement.prototype.connectedCallback + delete WebViewElement.prototype.disconnectedCallback + delete WebViewElement.prototype.attributeChangedCallback + + // Now that |observedAttributes| has been retrieved, we can hide it from + // user code as well. + delete WebViewElement.observedAttributes + }) +} + +// Prepare to register the element. +const setupWebView = (v8Util, webViewImpl) => { + const useCapture = true + window.addEventListener('readystatechange', function listener (event) { + if (document.readyState === 'loading') { + return + } + webViewImpl.setupAttributes() + registerWebViewElement(v8Util, webViewImpl) + window.removeEventListener(event.type, listener, useCapture) + }, useCapture) +} + +module.exports = { setupWebView } diff --git a/electronasar/canary/renderer/web-view/web-view.js b/electronasar/canary/renderer/web-view/web-view-impl.js similarity index 70% rename from electronasar/canary/renderer/web-view/web-view.js rename to electronasar/canary/renderer/web-view/web-view-impl.js index 1413f4c..6579c45 100644 --- a/electronasar/canary/renderer/web-view/web-view.js +++ b/electronasar/canary/renderer/web-view/web-view-impl.js @@ -7,6 +7,11 @@ const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal') const guestViewInternal = require('@electron/internal/renderer/web-view/guest-view-internal') const webViewConstants = require('@electron/internal/renderer/web-view/web-view-constants') const errorUtils = require('@electron/internal/common/error-utils') +const { + syncMethods, + asyncCallbackMethods, + asyncPromiseMethods +} = require('@electron/internal/common/web-view-methods') // ID generator. let nextId = 0 @@ -19,7 +24,6 @@ const getNextId = function () { class WebViewImpl { constructor (webviewNode) { this.webviewNode = webviewNode - v8Util.setHiddenValue(this.webviewNode, 'internal', this) this.elementAttached = false this.beforeFirstNavigation = true this.hasFocus = false @@ -196,105 +200,26 @@ class WebViewImpl { } } -// Registers custom element. -const registerWebViewElement = (window) => { - const proto = Object.create(window.HTMLObjectElement.prototype) - proto.createdCallback = function () { - return new WebViewImpl(this) - } - proto.attributeChangedCallback = function (name, oldValue, newValue) { +const setupAttributes = () => { + require('@electron/internal/renderer/web-view/web-view-attributes') +} + +const setupMethods = (WebViewElement) => { + // WebContents associated with this webview. + WebViewElement.prototype.getWebContents = function () { + const { getRemote } = require('@electron/internal/renderer/remote') + const getGuestWebContents = getRemote('getGuestWebContents') const internal = v8Util.getHiddenValue(this, 'internal') - if (internal) { - internal.handleWebviewAttributeMutation(name, oldValue, newValue) - } - } - proto.detachedCallback = function () { - const internal = v8Util.getHiddenValue(this, 'internal') - if (!internal) { - return - } - guestViewInternal.deregisterEvents(internal.viewInstanceId) - internal.elementAttached = false - this.internalInstanceId = 0 - internal.reset() - } - proto.attachedCallback = function () { - const internal = v8Util.getHiddenValue(this, 'internal') - if (!internal) { - return - } - if (!internal.elementAttached) { - guestViewInternal.registerEvents(internal, internal.viewInstanceId) - internal.elementAttached = true - internal.attributes[webViewConstants.ATTRIBUTE_SRC].parse() + if (!internal.guestInstanceId) { + internal.createGuestSync() } + return getGuestWebContents(internal.guestInstanceId) } - // Public-facing API methods. - const methods = [ - 'getURL', - 'loadURL', - 'getTitle', - 'isLoading', - 'isLoadingMainFrame', - 'isWaitingForResponse', - 'stop', - 'reload', - 'reloadIgnoringCache', - 'canGoBack', - 'canGoForward', - 'canGoToOffset', - 'clearHistory', - 'goBack', - 'goForward', - 'goToIndex', - 'goToOffset', - 'isCrashed', - 'setUserAgent', - 'getUserAgent', - 'openDevTools', - 'closeDevTools', - 'isDevToolsOpened', - 'isDevToolsFocused', - 'inspectElement', - 'setAudioMuted', - 'isAudioMuted', - 'isCurrentlyAudible', - 'undo', - 'redo', - 'cut', - 'copy', - 'paste', - 'pasteAndMatchStyle', - 'delete', - 'selectAll', - 'unselect', - 'replace', - 'replaceMisspelling', - 'findInPage', - 'stopFindInPage', - 'downloadURL', - 'inspectServiceWorker', - 'showDefinitionForSelection', - 'setZoomFactor', - 'setZoomLevel', - 'sendImeEvent' - ] - const nonblockMethods = [ - 'insertCSS', - 'insertText', - 'send', - 'sendInputEvent', - 'setLayoutZoomLevelLimits', - 'setVisualZoomLevelLimits', - // with callback - 'capturePage', - 'executeJavaScript', - 'getZoomFactor', - 'getZoomLevel', - 'print', - 'printToPDF' - ] + // Focusing the webview should move page focus to the underlying iframe. + WebViewElement.prototype.focus = function () { + this.contentWindow.focus() + } const getGuestInstanceId = function (self) { const internal = v8Util.getHiddenValue(self, 'internal') @@ -315,8 +240,8 @@ const registerWebViewElement = (window) => { } } } - for (const method of methods) { - proto[method] = createBlockHandler(method) + for (const method of syncMethods) { + WebViewElement.prototype[method] = createBlockHandler(method) } const createNonBlockHandler = function (method) { @@ -333,49 +258,35 @@ const registerWebViewElement = (window) => { ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', requestId, getGuestInstanceId(this), method, args, callback != null) } } - for (const method of nonblockMethods) { - proto[method] = createNonBlockHandler(method) + + for (const method of asyncCallbackMethods) { + WebViewElement.prototype[method] = createNonBlockHandler(method) } - // WebContents associated with this webview. - proto.getWebContents = function () { - const { getRemoteForUsage } = require('@electron/internal/renderer/remote') - const remote = getRemoteForUsage('getWebContents()') - const internal = v8Util.getHiddenValue(this, 'internal') - if (!internal.guestInstanceId) { - internal.createGuestSync() + const createPromiseHandler = function (method) { + return function (...args) { + return new Promise((resolve, reject) => { + const callback = (typeof args[args.length - 1] === 'function') ? args.pop() : null + const requestId = getNextId() + + ipcRenderer.once(`ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL_RESPONSE_${requestId}`, function (event, error, result) { + if (error == null) { + if (callback) { + callback(result) + } + resolve(result) + } else { + reject(errorUtils.deserialize(error)) + } + }) + ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_ASYNC_CALL', requestId, getGuestInstanceId(this), method, args, callback != null) + }) } - return remote.getGuestWebContents(internal.guestInstanceId) } - // Focusing the webview should move page focus to the underlying iframe. - proto.focus = function () { - this.contentWindow.focus() + for (const method of asyncPromiseMethods) { + WebViewElement.prototype[method] = createPromiseHandler(method) } - - window.WebView = webFrame.registerEmbedderCustomElement(window, 'webview', { - prototype: proto - }) - - // Delete the callbacks so developers cannot call them and produce unexpected - // behavior. - delete proto.createdCallback - delete proto.attachedCallback - delete proto.detachedCallback - delete proto.attributeChangedCallback } -const setupWebView = (window) => { - require('@electron/internal/renderer/web-view/web-view-attributes') - - const useCapture = true - window.addEventListener('readystatechange', function listener (event) { - if (document.readyState === 'loading') { - return - } - registerWebViewElement(window) - window.removeEventListener(event.type, listener, useCapture) - }, useCapture) -} - -module.exports = { setupWebView, WebViewImpl } +module.exports = { setupAttributes, setupMethods, guestViewInternal, webFrame, WebViewImpl } diff --git a/electronasar/canary/renderer/web-view/web-view-init.js b/electronasar/canary/renderer/web-view/web-view-init.js new file mode 100644 index 0000000..da302c3 --- /dev/null +++ b/electronasar/canary/renderer/web-view/web-view-init.js @@ -0,0 +1,35 @@ +'use strict' + +const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal') +const v8Util = process.atomBinding('v8_util') + +function handleFocusBlur (guestInstanceId) { + // Note that while Chromium content APIs have observer for focus/blur, they + // unfortunately do not work for webview. + + window.addEventListener('focus', () => { + ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_FOCUS_CHANGE', true, guestInstanceId) + }) + + window.addEventListener('blur', () => { + ipcRenderer.send('ELECTRON_GUEST_VIEW_MANAGER_FOCUS_CHANGE', false, guestInstanceId) + }) +} + +module.exports = function (contextIsolation, webviewTag, guestInstanceId) { + // Don't allow recursive ``. + if (webviewTag && guestInstanceId == null) { + const webViewImpl = require('@electron/internal/renderer/web-view/web-view-impl') + if (contextIsolation) { + v8Util.setHiddenValue(window, 'web-view-impl', webViewImpl) + } else { + const { setupWebView } = require('@electron/internal/renderer/web-view/web-view-element') + setupWebView(v8Util, webViewImpl) + } + } + + if (guestInstanceId) { + // Report focus/blur events of webview to browser. + handleFocusBlur(guestInstanceId) + } +} diff --git a/electronasar/canary/renderer/window-setup.js b/electronasar/canary/renderer/window-setup.js index f4ab44a..b4801a8 100644 --- a/electronasar/canary/renderer/window-setup.js +++ b/electronasar/canary/renderer/window-setup.js @@ -23,10 +23,10 @@ // - document.hidden // - document.visibilityState -const { defineProperty } = Object +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 @@ -54,12 +54,61 @@ const removeProxy = (guestId) => { delete windowProxies[guestId] } +function LocationProxy (ipcRenderer, guestId) { + const getGuestURL = function () { + const urlString = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', guestId, 'getURL') + try { + return new URL(urlString) + } catch (e) { + console.error('LocationProxy: failed to parse string', urlString, e) + } + + return null + } + + const propertyProxyFor = function (property) { + return { + get: function () { + const guestURL = getGuestURL() + const value = guestURL ? guestURL[property] : '' + return value === undefined ? '' : value + }, + set: function (newVal) { + const guestURL = getGuestURL() + if (guestURL) { + guestURL[property] = newVal + return ipcRenderer.sendSync( + 'ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', + guestId, 'loadURL', guestURL.toString()) + } + } + } + } + + defineProperties(this, { + hash: propertyProxyFor('hash'), + href: propertyProxyFor('href'), + host: propertyProxyFor('host'), + hostname: propertyProxyFor('hostname'), + origin: propertyProxyFor('origin'), + pathname: propertyProxyFor('pathname'), + port: propertyProxyFor('port'), + protocol: propertyProxyFor('protocol'), + search: propertyProxyFor('search') + }) + + this.toString = function () { + return this.href + } +} + function BrowserWindowProxy (ipcRenderer, guestId) { this.closed = false + const location = new LocationProxy(ipcRenderer, guestId) defineProperty(this, 'location', { get: function () { - return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', guestId, 'getURL') + return location }, set: function (url) { url = resolveURL(url) @@ -97,15 +146,6 @@ function BrowserWindowProxy (ipcRenderer, guestId) { } } -// Forward history operations to browser. -const sendHistoryOperation = function (ipcRenderer, ...args) { - ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER', ...args) -} - -const getHistoryOperation = function (ipcRenderer, ...args) { - return ipcRenderer.sendSync('ELECTRON_SYNC_NAVIGATION_CONTROLLER', ...args) -} - module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNativeWindowOpen) => { if (guestInstanceId == null) { // Override default window.close. @@ -150,20 +190,20 @@ module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNative }) window.history.back = function () { - sendHistoryOperation(ipcRenderer, 'goBack') + ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK') } window.history.forward = function () { - sendHistoryOperation(ipcRenderer, 'goForward') + ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD') } window.history.go = function (offset) { - sendHistoryOperation(ipcRenderer, 'goToOffset', +offset) + ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', +offset) } defineProperty(window.history, 'length', { get: function () { - return getHistoryOperation(ipcRenderer, 'length') + return ipcRenderer.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH') } })